Hide keyboard shortcuts

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 

3 

4""" 

5=============================================================================== 

6 

7 Original code copyright (C) 2009-2021 Rudolf Cardinal (rudolf@pobox.com). 

8 

9 This file is part of cardinal_pythonlib. 

10 

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 

14 

15 https://www.apache.org/licenses/LICENSE-2.0 

16 

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. 

22 

23=============================================================================== 

24 

25**Extensions to dogpile.cache.** 

26 

271. The basic cache objects. 

28 

292. FIX FOR DOGPILE.CACHE FOR DECORATED FUNCTIONS, 2017-07-28 (PLUS SOME OTHER 

30 IMPROVEMENTS). SEE  

31  

32 https://bitbucket.org/zzzeek/dogpile.cache/issues/96/error-in-python-35-with-use-of-deprecated 

33 

34 This fixes a crash using type-hinted functions under Python 3.5 with  

35 ``dogpile.cache==0.6.4``: 

36  

37 .. code-block:: none 

38 

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 

55 

563. Other improvements include: 

57 

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 

64  

65 - keyword arguments are supported 

66  

67 - properties are supported (the @property decorator must be ABOVE the 

68 cache decorator) 

69  

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). 

76 

77""" # noqa 

78 

79 

80# ============================================================================= 

81# Imports; logging 

82# ============================================================================= 

83 

84import inspect 

85import logging 

86from typing import Any, Callable, Dict, List, Optional 

87 

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 

91 

92from cardinal_pythonlib.logs import ( 

93 get_brace_style_log_with_null_handler, 

94 main_only_quicksetup_rootlogger, 

95) 

96 

97TESTING_VERBOSE = True 

98TESTING_USE_PRETTY_LOGS = True # False to make this standalone 

99 

100DEBUG_INTERNALS = False 

101 

102log = get_brace_style_log_with_null_handler(__name__) 

103 

104 

105# ============================================================================= 

106# Helper functions 

107# ============================================================================= 

108 

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 ) 

117 

118 

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 

123 

124 .. code-block:: none 

125 

126 mymodule:MyClass.myclassfunc # with no namespace 

127 mymodule:MyClass.myclassfunc|somenamespace # with a namespace 

128 

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 ) 

143 

144 

145# ============================================================================= 

146# New function key generators 

147# ============================================================================= 

148 

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 

156 

157 .. code-block:: python 

158 

159 def testfunc(param: str) -> str: 

160 return param + "hello" 

161 

162 ... at which :func:`inspect.getargspec` balks; plus 

163 :func:`inspect.getargspec` is deprecated in Python 3. 

164 

165 Used as an argument to e.g. ``@cache_region_static.cache_on_arguments()``. 

166 

167 Also modified to make the cached function unique per INSTANCE for normal 

168 methods of a class. 

169 

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` 

177 

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 """ 

182 

183 namespace = get_namespace(fn, namespace) 

184 

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')) 

189 

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 

207 

208 return generate_key 

209 

210 

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. 

218 

219 Also modified to make the cached function unique per INSTANCE for normal 

220 methods of a class. 

221 """ 

222 

223 namespace = get_namespace(fn, namespace) 

224 

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')) 

229 

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 

245 

246 return generate_keys 

247 

248 

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. 

255 

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. 

259 

260 NOTE ALSO that once we have keyword arguments, we should be using 

261 :func:`repr`, because we need to distinguish 

262 

263 .. code-block:: python 

264 

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'} 

270 

271 Also modified to make the cached function unique per INSTANCE for normal 

272 methods of a class. 

273 """ 

274 

275 namespace = get_namespace(fn, namespace) 

276 

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')) 

282 

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 ) 

294 

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 

334 

335 return generate_key 

336 

337 

338# ============================================================================= 

339# Default function key generator with a short name 

340# ============================================================================= 

341 

342fkg = kw_fkg_allowing_type_hints 

343 

344# Can now do: 

345# 

346# @mycache.cache_on_arguments(function_key_generator=fkg) 

347# def myfunc(): 

348# pass 

349 

350 

351# ============================================================================= 

352# Unit tests 

353# ============================================================================= 

354 

355def unit_test_cache() -> None: 

356 mycache = make_region() 

357 mycache.configure(backend='dogpile.cache.memory') 

358 

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. 

364 

365 fn_was_called = False 

366 

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 

374 

375 def fn_called(text: str) -> None: 

376 log.info("{}", text) 

377 nonlocal fn_was_called 

378 fn_was_called = True 

379 

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" 

384 

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" 

389 

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 

394 

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) 

400 

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) 

406 

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) 

413 

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}" 

418 

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 ) 

426 

427 class TestClass(object): 

428 c = 999 

429 

430 def __init__(self, a: int = 200) -> None: 

431 self.a = a 

432 

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" 

438 

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" 

444 

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}" 

449 

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}" 

455 

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}" 

463 

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" 

469 

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 

474 

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}") 

481 

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}" 

487 

488 class SecondTestClass: 

489 def __init__(self) -> None: 

490 self.d = 5 

491 

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" 

497 

498 class Inherited(TestClass): 

499 def __init__(self, a=101010): 

500 super().__init__(a=a) 

501 

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}" 

506 

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}" 

512 

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}" 

520 

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" 

526 

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 

531 

532 # BUT fn_all_possible IS NOT OVERRIDDEN 

533 

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}" 

539 

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) 

544 

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) 

554 

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") 

561 

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) 

567 

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) 

581 

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) 

584 

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) 

590 

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) 

601 

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) 

626 

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) 

651 

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) 

656 

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) 

681 

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) 

693 

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) 

707 

708 log.info("Success!") 

709 

710 

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()