Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/cardinal_pythonlib/dogpile_cache.py : 12%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#!/usr/bin/env python
2# cardinal_pythonlib/dogpile_cache.py
4"""
5===============================================================================
7 Original code copyright (C) 2009-2021 Rudolf Cardinal (rudolf@pobox.com).
9 This file is part of cardinal_pythonlib.
11 Licensed under the Apache License, Version 2.0 (the "License");
12 you may not use this file except in compliance with the License.
13 You may obtain a copy of the License at
15 https://www.apache.org/licenses/LICENSE-2.0
17 Unless required by applicable law or agreed to in writing, software
18 distributed under the License is distributed on an "AS IS" BASIS,
19 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20 See the License for the specific language governing permissions and
21 limitations under the License.
23===============================================================================
25**Extensions to dogpile.cache.**
271. The basic cache objects.
292. FIX FOR DOGPILE.CACHE FOR DECORATED FUNCTIONS, 2017-07-28 (PLUS SOME OTHER
30 IMPROVEMENTS). SEE
32 https://bitbucket.org/zzzeek/dogpile.cache/issues/96/error-in-python-35-with-use-of-deprecated
34 This fixes a crash using type-hinted functions under Python 3.5 with
35 ``dogpile.cache==0.6.4``:
37 .. code-block:: none
39 Traceback (most recent call last):
40 File "/usr/lib/python3.5/runpy.py", line 184, in _run_module_as_main
41 "__main__", mod_spec)
42 File "/usr/lib/python3.5/runpy.py", line 85, in _run_code
43 exec(code, run_globals)
44 File "/home/rudolf/Documents/code/camcops/server/camcops_server/cc_modules/cc_cache.py", line 64, in <module>
45 unit_test_cache()
46 File "/home/rudolf/Documents/code/camcops/server/camcops_server/cc_modules/cc_cache.py", line 50, in unit_test_cache
47 def testfunc() -> str:
48 File "/home/rudolf/dev/venvs/camcops/lib/python3.5/site-packages/dogpile/cache/region.py", line 1215, in decorator
49 key_generator = function_key_generator(namespace, fn)
50 File "/home/rudolf/dev/venvs/camcops/lib/python3.5/site-packages/dogpile/cache/util.py", line 31, in function_key_generator
51 args = inspect.getargspec(fn)
52 File "/usr/lib/python3.5/inspect.py", line 1045, in getargspec
53 raise ValueError("Function has keyword-only arguments or annotations"
54 ValueError: Function has keyword-only arguments or annotations, use getfullargspec() API which can support them
563. Other improvements include:
58 - the cache decorators operate as:
59 - PER-INSTANCE caches for class instances, provided the first parameter
60 is named "self";
61 - PER-CLASS caches for classmethods, provided the first parameter is
62 named "cls";
63 - PER-FUNCTION caches for staticmethods and plain functions
65 - keyword arguments are supported
67 - properties are supported (the @property decorator must be ABOVE the
68 cache decorator)
70 - Note that this sort of cache relies on the generation of a STRING KEY
71 from the function arguments. It uses the ``hex(id())`` function for
72 ``self``/``cls`` arguments, and the ``to_str()`` function, passed as a
73 parameter, for others (for which the default is ``"repr"``; see
74 discussion below as to why ``"repr"`` is suitable while ``"str"`` is
75 not).
77""" # noqa
80# =============================================================================
81# Imports; logging
82# =============================================================================
84import inspect
85import logging
86from typing import Any, Callable, Dict, List, Optional
88# noinspection PyUnresolvedReferences,PyPackageRequirements
89from dogpile.cache import make_region
90# from dogpile.util import compat # repr used as the default instead of compat.to_str # noqa
92from cardinal_pythonlib.logs import (
93 get_brace_style_log_with_null_handler,
94 main_only_quicksetup_rootlogger,
95)
97TESTING_VERBOSE = True
98TESTING_USE_PRETTY_LOGS = True # False to make this standalone
100DEBUG_INTERNALS = False
102log = get_brace_style_log_with_null_handler(__name__)
105# =============================================================================
106# Helper functions
107# =============================================================================
109def repr_parameter(param: inspect.Parameter) -> str:
110 """
111 Provides a ``repr``-style representation of a function parameter.
112 """
113 return (
114 f"Parameter(name={param.name}, annotation={param.annotation}, "
115 f"kind={param.kind}, default={param.default}"
116 )
119def get_namespace(fn: Callable, namespace: Optional[str]) -> str:
120 """
121 Returns a representation of a function's name (perhaps within a namespace),
122 like
124 .. code-block:: none
126 mymodule:MyClass.myclassfunc # with no namespace
127 mymodule:MyClass.myclassfunc|somenamespace # with a namespace
129 Args:
130 fn: a function
131 namespace: an optional namespace, which can be of any type but is
132 normally a ``str``; if not ``None``, ``str(namespace)`` will be
133 added to the result. See
134 https://dogpilecache.readthedocs.io/en/latest/api.html#dogpile.cache.region.CacheRegion.cache_on_arguments
135 """ # noqa
136 # See hidden attributes with dir(fn)
137 # noinspection PyUnresolvedReferences
138 return "{module}:{name}{extra}".format(
139 module=fn.__module__,
140 name=fn.__qualname__, # __qualname__ includes class name, if present
141 extra=f"|{namespace}" if namespace is not None else "",
142 )
145# =============================================================================
146# New function key generators
147# =============================================================================
149def fkg_allowing_type_hints(
150 namespace: Optional[str],
151 fn: Callable,
152 to_str: Callable[[Any], str] = repr) -> Callable[[Any], str]:
153 """
154 Replacement for :func:`dogpile.cache.util.function_key_generator` that
155 handles type-hinted functions like
157 .. code-block:: python
159 def testfunc(param: str) -> str:
160 return param + "hello"
162 ... at which :func:`inspect.getargspec` balks; plus
163 :func:`inspect.getargspec` is deprecated in Python 3.
165 Used as an argument to e.g. ``@cache_region_static.cache_on_arguments()``.
167 Also modified to make the cached function unique per INSTANCE for normal
168 methods of a class.
170 Args:
171 namespace: optional namespace, as per :func:`get_namespace`
172 fn: function to generate a key for (usually the function being
173 decorated)
174 to_str: function to apply to map arguments to a string (to make a
175 unique key for a particular call to the function); by default it
176 is :func:`repr`
178 Returns:
179 a function that generates a string key, based on a given function as
180 well as arguments to the returned function itself.
181 """
183 namespace = get_namespace(fn, namespace)
185 sig = inspect.signature(fn)
186 argnames = [p.name for p in sig.parameters.values()
187 if p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD]
188 has_self = bool(argnames and argnames[0] in ('self', 'cls'))
190 def generate_key(*args: Any, **kw: Any) -> str:
191 """
192 Makes the actual key for a specific call to the decorated function,
193 with particular ``args``/``kwargs``.
194 """
195 if kw:
196 raise ValueError("This dogpile.cache key function generator, "
197 "fkg_allowing_type_hints, "
198 "does not accept keyword arguments.")
199 if has_self:
200 # Unlike dogpile's default, make it instance- (or class-) specific
201 # by including a representation of the "self" or "cls" argument:
202 args = [hex(id(args[0]))] + list(args[1:])
203 key = namespace + "|" + " ".join(map(to_str, args))
204 if DEBUG_INTERNALS:
205 log.debug("fkg_allowing_type_hints.generate_key() -> {!r}", key)
206 return key
208 return generate_key
211def multikey_fkg_allowing_type_hints(
212 namespace: Optional[str],
213 fn: Callable,
214 to_str: Callable[[Any], str] = repr) -> Callable[[Any], List[str]]:
215 """
216 Equivalent of :func:`dogpile.cache.util.function_multi_key_generator`, but
217 using :func:`inspect.signature` instead.
219 Also modified to make the cached function unique per INSTANCE for normal
220 methods of a class.
221 """
223 namespace = get_namespace(fn, namespace)
225 sig = inspect.signature(fn)
226 argnames = [p.name for p in sig.parameters.values()
227 if p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD]
228 has_self = bool(argnames and argnames[0] in ('self', 'cls'))
230 def generate_keys(*args: Any, **kw: Any) -> List[str]:
231 if kw:
232 raise ValueError("This dogpile.cache key function generator, "
233 "multikey_fkg_allowing_type_hints, "
234 "does not accept keyword arguments.")
235 if has_self:
236 # Unlike dogpile's default, make it instance- (or class-) specific
237 # by including a representation of the "self" or "cls" argument:
238 args = [hex(id(args[0]))] + list(args[1:])
239 keys = [namespace + "|" + key for key in map(to_str, args)]
240 if DEBUG_INTERNALS:
241 log.debug(
242 "multikey_fkg_allowing_type_hints.generate_keys() -> {!r}",
243 keys)
244 return keys
246 return generate_keys
249def kw_fkg_allowing_type_hints(
250 namespace: Optional[str],
251 fn: Callable,
252 to_str: Callable[[Any], str] = repr) -> Callable[[Any], str]:
253 """
254 As for :func:`fkg_allowing_type_hints`, but allowing keyword arguments.
256 For ``kwargs`` passed in, we will build a ``dict`` of all argname (key) to
257 argvalue (values) pairs, including default args from the argspec, and then
258 alphabetize the list before generating the key.
260 NOTE ALSO that once we have keyword arguments, we should be using
261 :func:`repr`, because we need to distinguish
263 .. code-block:: python
265 kwargs = {'p': 'another', 'q': 'thing'}
266 # ... which compat.string_type will make into
267 # p=another q=thing
268 # ... from
269 kwargs = {'p': 'another q=thing'}
271 Also modified to make the cached function unique per INSTANCE for normal
272 methods of a class.
273 """
275 namespace = get_namespace(fn, namespace)
277 sig = inspect.signature(fn)
278 parameters = list(sig.parameters.values()) # convert from odict_values
279 argnames = [p.name for p in parameters
280 if p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD]
281 has_self = bool(argnames and argnames[0] in ('self', 'cls'))
283 if DEBUG_INTERNALS:
284 log.debug(
285 "At start of kw_fkg_allowing_type_hints: namespace={namespace},"
286 "parameters=[{parameters}], argnames={argnames}, "
287 "has_self={has_self}, fn={fn}",
288 namespace=namespace,
289 parameters=", ".join(repr_parameter(p) for p in parameters),
290 argnames=repr(argnames),
291 has_self=has_self,
292 fn=repr(fn),
293 )
295 def generate_key(*args: Any, **kwargs: Any) -> str:
296 as_kwargs = {} # type: Dict[str, Any]
297 loose_args = [] # type: List[Any] # those captured by *args
298 # 1. args: get the name as well.
299 for idx, arg in enumerate(args):
300 if idx >= len(argnames):
301 # positional argument to be scooped up with *args
302 loose_args.append(arg)
303 else:
304 # normal plain positional argument
305 if has_self and idx == 0: # "self" or "cls" initial argument
306 argvalue = hex(id(arg))
307 else:
308 argvalue = arg
309 as_kwargs[argnames[idx]] = argvalue
310 # 1b. args with no name
311 if loose_args:
312 as_kwargs['*args'] = loose_args
313 # '*args' is guaranteed not to be a parameter name in its own right
314 # 2. kwargs
315 as_kwargs.update(kwargs)
316 # 3. default values
317 for param in parameters:
318 if param.default != inspect.Parameter.empty:
319 if param.name not in as_kwargs:
320 as_kwargs[param.name] = param.default
321 # 4. sorted by name
322 # ... but also incorporating the name of the argument, because once
323 # we allow the arbitrary **kwargs format, order is no longer
324 # sufficient to discriminate
325 # fn(p="another", q="thing")
326 # from
327 # fn(r="another", s="thing")
328 argument_values = ["{k}={v}".format(k=key, v=to_str(as_kwargs[key]))
329 for key in sorted(as_kwargs.keys())]
330 key = namespace + '|' + " ".join(argument_values)
331 if DEBUG_INTERNALS:
332 log.debug("kw_fkg_allowing_type_hints.generate_key() -> {!r}", key)
333 return key
335 return generate_key
338# =============================================================================
339# Default function key generator with a short name
340# =============================================================================
342fkg = kw_fkg_allowing_type_hints
344# Can now do:
345#
346# @mycache.cache_on_arguments(function_key_generator=fkg)
347# def myfunc():
348# pass
351# =============================================================================
352# Unit tests
353# =============================================================================
355def unit_test_cache() -> None:
356 mycache = make_region()
357 mycache.configure(backend='dogpile.cache.memory')
359 plain_fkg = fkg_allowing_type_hints
360 kw_fkg = kw_fkg_allowing_type_hints
361 # I'm not sure what dogpile.cache.utils.function_multi_key_generator is
362 # used for, so haven't fully tested multikey_fkg_allowing_type_hints, but
363 # it works internally in the same way as fkg_allowing_type_hints.
365 fn_was_called = False
367 def test(result: str, should_call_fn: bool, reset: bool = True) -> None:
368 nonlocal fn_was_called
369 log.info("{}", result)
370 assert fn_was_called == should_call_fn, (
371 f"fn_was_called={fn_was_called}, should_call_fn={should_call_fn}")
372 if reset:
373 fn_was_called = False
375 def fn_called(text: str) -> None:
376 log.info("{}", text)
377 nonlocal fn_was_called
378 fn_was_called = True
380 @mycache.cache_on_arguments(function_key_generator=None)
381 def no_params_dogpile_default_fkg(): # no type hints!
382 fn_called("CACHED FUNCTION no_params_dogpile_default_fkg() CALLED")
383 return "no_params_dogpile_default_fkg: hello"
385 @mycache.cache_on_arguments(function_key_generator=plain_fkg)
386 def noparams() -> str:
387 fn_called("CACHED FUNCTION noparams() CALLED")
388 return "noparams: hello"
390 @mycache.cache_on_arguments(function_key_generator=plain_fkg)
391 def oneparam(a: str) -> str:
392 fn_called("CACHED FUNCTION oneparam() CALLED")
393 return "oneparam: hello, " + a
395 @mycache.cache_on_arguments(function_key_generator=plain_fkg)
396 def twoparam_with_default_wrong_dec(a: str, b: str = "Zelda") -> str:
397 fn_called("CACHED FUNCTION twoparam_with_default_wrong_dec() CALLED")
398 return ("twoparam_with_default_wrong_dec: hello, " + a +
399 "; this is " + b)
401 @mycache.cache_on_arguments(function_key_generator=kw_fkg)
402 def twoparam_with_default_right_dec(a: str, b: str = "Zelda") -> str:
403 fn_called("CACHED FUNCTION twoparam_with_default_right_dec() CALLED")
404 return ("twoparam_with_default_right_dec: hello, " + a +
405 "; this is " + b)
407 @mycache.cache_on_arguments(function_key_generator=kw_fkg)
408 def twoparam_all_defaults_no_typehints(a="David", b="Zelda"):
409 fn_called("CACHED FUNCTION twoparam_all_defaults_no_typehints() "
410 "CALLED")
411 return ("twoparam_all_defaults_no_typehints: hello, " + a +
412 "; this is " + b)
414 @mycache.cache_on_arguments(function_key_generator=kw_fkg)
415 def fn_args_kwargs(*args, **kwargs):
416 fn_called("CACHED FUNCTION fn_args_kwargs() CALLED")
417 return f"fn_args_kwargs: args={args!r}, kwargs={kwargs!r}"
419 @mycache.cache_on_arguments(function_key_generator=kw_fkg)
420 def fn_all_possible(a, b, *args, d="David", **kwargs):
421 fn_called("CACHED FUNCTION fn_all_possible() CALLED")
422 return (
423 f"fn_all_possible: a={a!r}, b={b!r}, args={args!r}, d={d!r}, "
424 f"kwargs={kwargs!r}"
425 )
427 class TestClass(object):
428 c = 999
430 def __init__(self, a: int = 200) -> None:
431 self.a = a
433 @mycache.cache_on_arguments(function_key_generator=None)
434 def no_params_dogpile_default_fkg(self): # no type hints!
435 fn_called("CACHED FUNCTION TestClass."
436 "no_params_dogpile_default_fkg() CALLED")
437 return "TestClass.no_params_dogpile_default_fkg: hello"
439 @mycache.cache_on_arguments(function_key_generator=None)
440 def dogpile_default_test_2(self): # no type hints!
441 fn_called("CACHED FUNCTION TestClass."
442 "dogpile_default_test_2() CALLED")
443 return "TestClass.dogpile_default_test_2: hello"
445 @mycache.cache_on_arguments(function_key_generator=plain_fkg)
446 def noparams(self) -> str:
447 fn_called("CACHED FUNCTION TestClass.noparams() CALLED")
448 return f"Testclass.noparams: hello; a={self.a}"
450 @mycache.cache_on_arguments(function_key_generator=kw_fkg)
451 def no_params_instance_cache(self) -> str:
452 fn_called("PER-INSTANCE-CACHED FUNCTION "
453 "TestClass.no_params_instance_cache() CALLED")
454 return f"TestClass.no_params_instance_cache: a={self.a}"
456 # Decorator order is critical here:
457 # https://stackoverflow.com/questions/1987919/why-can-decorator-not-decorate-a-staticmethod-or-a-classmethod # noqa
458 @classmethod
459 @mycache.cache_on_arguments(function_key_generator=plain_fkg)
460 def classy(cls) -> str:
461 fn_called("CACHED FUNCTION TestClass.classy() CALLED")
462 return f"TestClass.classy: hello; c={cls.c}"
464 @staticmethod
465 @mycache.cache_on_arguments(function_key_generator=plain_fkg)
466 def static() -> str:
467 fn_called("CACHED FUNCTION TestClass.static() CALLED")
468 return "TestClass.static: hello"
470 @mycache.cache_on_arguments(function_key_generator=plain_fkg)
471 def oneparam(self, q: str) -> str:
472 fn_called("CACHED FUNCTION TestClass.oneparam() CALLED")
473 return "TestClass.oneparam: hello, " + q
475 @mycache.cache_on_arguments(function_key_generator=kw_fkg)
476 def fn_all_possible(self, a, b, *args, d="David", **kwargs):
477 fn_called("CACHED FUNCTION TestClass.fn_all_possible() CALLED")
478 return (
479 f"TestClass.fn_all_possible: a={a!r}, b={b!r}, args={args!r}, "
480 f"d={d!r}, kwargs={kwargs!r}")
482 @property
483 @mycache.cache_on_arguments(function_key_generator=kw_fkg)
484 def prop(self) -> str:
485 fn_called("CACHED PROPERTY TestClass.prop() CALLED")
486 return f"TestClass.prop: a={self.a!r}"
488 class SecondTestClass:
489 def __init__(self) -> None:
490 self.d = 5
492 @mycache.cache_on_arguments(function_key_generator=None)
493 def dogpile_default_test_2(self): # no type hints!
494 fn_called("CACHED FUNCTION SecondTestClass."
495 "dogpile_default_test_2() CALLED")
496 return "SecondTestClass.dogpile_default_test_2: hello"
498 class Inherited(TestClass):
499 def __init__(self, a=101010):
500 super().__init__(a=a)
502 @mycache.cache_on_arguments(function_key_generator=plain_fkg)
503 def noparams(self) -> str:
504 fn_called("CACHED FUNCTION Inherited.noparams() CALLED")
505 return f"Inherited.noparams: hello; a={self.a}"
507 @mycache.cache_on_arguments(function_key_generator=kw_fkg)
508 def no_params_instance_cache(self) -> str:
509 fn_called("PER-INSTANCE-CACHED FUNCTION "
510 "Inherited.no_params_instance_cache() CALLED")
511 return f"Inherited.no_params_instance_cache: a={self.a}"
513 # Decorator order is critical here:
514 # https://stackoverflow.com/questions/1987919/why-can-decorator-not-decorate-a-staticmethod-or-a-classmethod # noqa
515 @classmethod
516 @mycache.cache_on_arguments(function_key_generator=plain_fkg)
517 def classy(cls) -> str:
518 fn_called("CACHED FUNCTION Inherited.classy() CALLED")
519 return f"Inherited.classy: hello; c={cls.c}"
521 @staticmethod
522 @mycache.cache_on_arguments(function_key_generator=plain_fkg)
523 def static() -> str:
524 fn_called("CACHED FUNCTION Inherited.static() CALLED")
525 return "Inherited.static: hello"
527 @mycache.cache_on_arguments(function_key_generator=plain_fkg)
528 def oneparam(self, q: str) -> str:
529 fn_called("CACHED FUNCTION Inherited.oneparam() CALLED")
530 return "Inherited.oneparam: hello, " + q
532 # BUT fn_all_possible IS NOT OVERRIDDEN
534 @property
535 @mycache.cache_on_arguments(function_key_generator=kw_fkg)
536 def prop(self) -> str:
537 fn_called("CACHED PROPERTY Inherited.prop() CALLED")
538 return f"Inherited.prop: a={self.a!r}"
540 log.warning("Fetching cached information #1 (should call noparams())...")
541 test(noparams(), True)
542 log.warning("Fetching cached information #2 (should not call noparams())...") # noqa
543 test(noparams(), False)
545 log.warning("Testing functions with other signatures...")
546 test(oneparam("Arthur"), True)
547 test(oneparam("Arthur"), False)
548 test(oneparam("Bob"), True)
549 test(oneparam("Bob"), False)
550 test(twoparam_with_default_wrong_dec("Celia"), True)
551 test(twoparam_with_default_wrong_dec("Celia"), False)
552 test(twoparam_with_default_wrong_dec("Celia", "Yorick"), True)
553 test(twoparam_with_default_wrong_dec("Celia", "Yorick"), False)
555 log.warning("Trying with keyword arguments and wrong key generator")
556 try:
557 log.info("{}", twoparam_with_default_wrong_dec(a="Celia", b="Yorick"))
558 raise AssertionError("Inappropriate success with keyword arguments!")
559 except ValueError:
560 log.info("Correct rejection of keyword arguments")
562 log.warning("Trying with keyword arguments and right key generator")
563 test(twoparam_with_default_right_dec(a="Celia"), True)
564 test(twoparam_with_default_right_dec(a="Celia", b="Yorick"), True)
565 test(twoparam_with_default_right_dec(b="Yorick", a="Celia"), False)
566 test(twoparam_with_default_right_dec("Celia", b="Yorick"), False)
568 test(twoparam_all_defaults_no_typehints(), True)
569 test(twoparam_all_defaults_no_typehints(a="Edith"), True)
570 test(twoparam_all_defaults_no_typehints(a="Edith"), False)
571 test(twoparam_all_defaults_no_typehints(b="Romeo"), True)
572 test(twoparam_all_defaults_no_typehints(b="Romeo"), False)
573 test(twoparam_all_defaults_no_typehints("Greta", b="Romeo"), True)
574 test(twoparam_all_defaults_no_typehints("Greta", b="Romeo"), False)
575 test(twoparam_all_defaults_no_typehints(a="Felicity", b="Sigurd"), True)
576 test(twoparam_all_defaults_no_typehints(a="Felicity", b="Sigurd"), False)
577 test(twoparam_all_defaults_no_typehints("Felicity", "Sigurd"), False)
578 test(twoparam_all_defaults_no_typehints("Felicity", "Sigurd"), False)
579 test(twoparam_all_defaults_no_typehints(b="Sigurd", a="Felicity"), False)
580 test(twoparam_all_defaults_no_typehints(b="Sigurd", a="Felicity"), False)
582 test(fn_args_kwargs(1, 2, 3, d="David", f="Edgar"), True)
583 test(fn_args_kwargs(1, 2, 3, d="David", f="Edgar"), False)
585 test(fn_args_kwargs(p="another", q="thing"), True)
586 test(fn_args_kwargs(p="another", q="thing"), False)
587 log.warning("The next call MUST NOT go via the cache, i.e. func should be CALLED") # noqa
588 test(fn_args_kwargs(p="another q=thing"), True)
589 test(fn_args_kwargs(p="another q=thing"), False)
591 test(fn_all_possible(10, 11, 12, "Horace", "Iris"), True)
592 test(fn_all_possible(10, 11, 12, "Horace", "Iris"), False)
593 test(fn_all_possible(10, 11, 12, d="Horace"), True)
594 test(fn_all_possible(10, 11, 12, d="Horace"), False)
595 test(fn_all_possible(98, 99, d="Horace"), True)
596 test(fn_all_possible(98, 99, d="Horace"), False)
597 test(fn_all_possible(98, 99, d="Horace", p="another", q="thing"), True)
598 test(fn_all_possible(98, 99, d="Horace", p="another", q="thing"), False)
599 test(fn_all_possible(98, 99, d="Horace", r="another", s="thing"), True)
600 test(fn_all_possible(98, 99, d="Horace", r="another", s="thing"), False)
602 log.warning("Testing class member functions")
603 t = TestClass()
604 test(t.noparams(), True)
605 test(t.noparams(), False)
606 test(t.classy(), True)
607 test(t.classy(), False)
608 test(t.static(), True)
609 test(t.static(), False)
610 test(t.oneparam("Arthur"), True)
611 test(t.oneparam("Arthur"), False)
612 test(t.oneparam("Bob"), True)
613 test(t.oneparam("Bob"), False)
614 test(t.fn_all_possible(10, 11, 12, "Horace", "Iris"), True)
615 test(t.fn_all_possible(10, 11, 12, "Horace", "Iris"), False)
616 test(t.fn_all_possible(10, 11, 12, d="Horace"), True)
617 test(t.fn_all_possible(10, 11, 12, d="Horace"), False)
618 test(t.fn_all_possible(98, 99, d="Horace"), True)
619 test(t.fn_all_possible(98, 99, d="Horace"), False)
620 test(t.fn_all_possible(98, 99, d="Horace", p="another", q="thing"), True)
621 test(t.fn_all_possible(98, 99, d="Horace", p="another", q="thing"), False)
622 test(t.fn_all_possible(98, 99, d="Horace", r="another", s="thing"), True)
623 test(t.fn_all_possible(98, 99, d="Horace", r="another", s="thing"), False)
624 test(t.prop, True)
625 test(t.prop, False)
627 log.warning("Testing functions for another INSTANCE of the same class")
628 t_other = TestClass(a=999)
629 test(t_other.noparams(), True)
630 test(t_other.noparams(), False)
631 test(t_other.classy(), False) # SAME CLASS as t; shouldn't be re-called
632 test(t_other.classy(), False)
633 test(t_other.static(), False) # SAME CLASS as t; shouldn't be re-called
634 test(t_other.static(), False)
635 test(t_other.oneparam("Arthur"), True)
636 test(t_other.oneparam("Arthur"), False)
637 test(t_other.oneparam("Bob"), True)
638 test(t_other.oneparam("Bob"), False)
639 test(t_other.fn_all_possible(10, 11, 12, "Horace", "Iris"), True)
640 test(t_other.fn_all_possible(10, 11, 12, "Horace", "Iris"), False)
641 test(t_other.fn_all_possible(10, 11, 12, d="Horace"), True)
642 test(t_other.fn_all_possible(10, 11, 12, d="Horace"), False)
643 test(t_other.fn_all_possible(98, 99, d="Horace"), True)
644 test(t_other.fn_all_possible(98, 99, d="Horace"), False)
645 test(t_other.fn_all_possible(98, 99, d="Horace", p="another", q="thing"), True) # noqa
646 test(t_other.fn_all_possible(98, 99, d="Horace", p="another", q="thing"), False) # noqa
647 test(t_other.fn_all_possible(98, 99, d="Horace", r="another", s="thing"), True) # noqa
648 test(t_other.fn_all_possible(98, 99, d="Horace", r="another", s="thing"), False) # noqa
649 test(t_other.prop, True)
650 test(t_other.prop, False)
652 test(t.no_params_instance_cache(), True)
653 test(t.no_params_instance_cache(), False)
654 test(t_other.no_params_instance_cache(), True)
655 test(t_other.no_params_instance_cache(), False)
657 log.warning("Testing functions for instance of a derived class")
658 t_inh = Inherited(a=777)
659 test(t_inh.noparams(), True)
660 test(t_inh.noparams(), False)
661 test(t_inh.classy(), True)
662 test(t_inh.classy(), False)
663 test(t_inh.static(), True)
664 test(t_inh.static(), False)
665 test(t_inh.oneparam("Arthur"), True)
666 test(t_inh.oneparam("Arthur"), False)
667 test(t_inh.oneparam("Bob"), True)
668 test(t_inh.oneparam("Bob"), False)
669 test(t_inh.fn_all_possible(10, 11, 12, "Horace", "Iris"), True)
670 test(t_inh.fn_all_possible(10, 11, 12, "Horace", "Iris"), False)
671 test(t_inh.fn_all_possible(10, 11, 12, d="Horace"), True)
672 test(t_inh.fn_all_possible(10, 11, 12, d="Horace"), False)
673 test(t_inh.fn_all_possible(98, 99, d="Horace"), True)
674 test(t_inh.fn_all_possible(98, 99, d="Horace"), False)
675 test(t_inh.fn_all_possible(98, 99, d="Horace", p="another", q="thing"), True) # noqa
676 test(t_inh.fn_all_possible(98, 99, d="Horace", p="another", q="thing"), False) # noqa
677 test(t_inh.fn_all_possible(98, 99, d="Horace", r="another", s="thing"), True) # noqa
678 test(t_inh.fn_all_possible(98, 99, d="Horace", r="another", s="thing"), False) # noqa
679 test(t_inh.prop, True)
680 test(t_inh.prop, False)
682 test(no_params_dogpile_default_fkg(), True)
683 test(no_params_dogpile_default_fkg(), False)
684 try:
685 test(t.no_params_dogpile_default_fkg(), True)
686 log.info("dogpile.cache default FKG correctly distinguishing between "
687 "plain and class-member function in same module")
688 except AssertionError:
689 log.warning("Known dogpile.cache default FKG problem - conflates "
690 "plain/class member function of same name (unless "
691 "namespace is manually given)")
692 test(t.no_params_dogpile_default_fkg(), False)
694 t2 = SecondTestClass()
695 test(t.dogpile_default_test_2(), True)
696 test(t.dogpile_default_test_2(), False)
697 try:
698 test(t2.dogpile_default_test_2(), True)
699 log.info("dogpile.cache default FKG correctly distinguishing between "
700 "member functions of two different classes with same name")
701 except AssertionError:
702 log.warning("Known dogpile.cache default FKG problem - conflates "
703 "member functions of two different classes where "
704 "functions have same name (unless namespace is manually "
705 "given)")
706 test(t2.dogpile_default_test_2(), False)
708 log.info("Success!")
711# TEST THIS WITH:
712# python -m cardinal_pythonlib.dogpile_cache
713if __name__ == '__main__':
714 level = logging.DEBUG if TESTING_VERBOSE else logging.INFO
715 if TESTING_USE_PRETTY_LOGS:
716 main_only_quicksetup_rootlogger(level=level)
717 else:
718 logging.basicConfig(level=level)
719 unit_test_cache()