cdxcore 0.1.27__tar.gz → 0.1.29__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of cdxcore might be problematic. Click here for more details.

Files changed (45) hide show
  1. {cdxcore-0.1.27 → cdxcore-0.1.29}/PKG-INFO +1 -1
  2. {cdxcore-0.1.27 → cdxcore-0.1.29}/cdxcore/__init__.py +1 -1
  3. {cdxcore-0.1.27 → cdxcore-0.1.29}/cdxcore/subdir.py +360 -135
  4. {cdxcore-0.1.27 → cdxcore-0.1.29}/cdxcore/uniquehash.py +8 -9
  5. {cdxcore-0.1.27 → cdxcore-0.1.29}/cdxcore/version.py +7 -1
  6. {cdxcore-0.1.27 → cdxcore-0.1.29}/cdxcore.egg-info/PKG-INFO +1 -1
  7. {cdxcore-0.1.27 → cdxcore-0.1.29}/pyproject.toml +1 -1
  8. {cdxcore-0.1.27 → cdxcore-0.1.29}/tests/test_subdir.py +143 -7
  9. {cdxcore-0.1.27 → cdxcore-0.1.29}/tests/test_uniquehash.py +9 -0
  10. {cdxcore-0.1.27 → cdxcore-0.1.29}/LICENSE +0 -0
  11. {cdxcore-0.1.27 → cdxcore-0.1.29}/README.md +0 -0
  12. {cdxcore-0.1.27 → cdxcore-0.1.29}/cdxcore/config.py +0 -0
  13. {cdxcore-0.1.27 → cdxcore-0.1.29}/cdxcore/deferred.py +0 -0
  14. {cdxcore-0.1.27 → cdxcore-0.1.29}/cdxcore/dynalimits.py +0 -0
  15. {cdxcore-0.1.27 → cdxcore-0.1.29}/cdxcore/dynaplot.py +0 -0
  16. {cdxcore-0.1.27 → cdxcore-0.1.29}/cdxcore/err.py +0 -0
  17. {cdxcore-0.1.27 → cdxcore-0.1.29}/cdxcore/filelock.py +0 -0
  18. {cdxcore-0.1.27 → cdxcore-0.1.29}/cdxcore/jcpool.py +0 -0
  19. {cdxcore-0.1.27 → cdxcore-0.1.29}/cdxcore/npio.py +0 -0
  20. {cdxcore-0.1.27 → cdxcore-0.1.29}/cdxcore/npshm.py +0 -0
  21. {cdxcore-0.1.27 → cdxcore-0.1.29}/cdxcore/pretty.py +0 -0
  22. {cdxcore-0.1.27 → cdxcore-0.1.29}/cdxcore/util.py +0 -0
  23. {cdxcore-0.1.27 → cdxcore-0.1.29}/cdxcore/verbose.py +0 -0
  24. {cdxcore-0.1.27 → cdxcore-0.1.29}/cdxcore.egg-info/SOURCES.txt +0 -0
  25. {cdxcore-0.1.27 → cdxcore-0.1.29}/cdxcore.egg-info/dependency_links.txt +0 -0
  26. {cdxcore-0.1.27 → cdxcore-0.1.29}/cdxcore.egg-info/requires.txt +0 -0
  27. {cdxcore-0.1.27 → cdxcore-0.1.29}/cdxcore.egg-info/top_level.txt +0 -0
  28. {cdxcore-0.1.27 → cdxcore-0.1.29}/docs/source/conf.py +0 -0
  29. {cdxcore-0.1.27 → cdxcore-0.1.29}/setup.cfg +0 -0
  30. {cdxcore-0.1.27 → cdxcore-0.1.29}/tests/test_config.py +0 -0
  31. {cdxcore-0.1.27 → cdxcore-0.1.29}/tests/test_deferred.py +0 -0
  32. {cdxcore-0.1.27 → cdxcore-0.1.29}/tests/test_err.py +0 -0
  33. {cdxcore-0.1.27 → cdxcore-0.1.29}/tests/test_jcpool.py +0 -0
  34. {cdxcore-0.1.27 → cdxcore-0.1.29}/tests/test_npio.py +0 -0
  35. {cdxcore-0.1.27 → cdxcore-0.1.29}/tests/test_npshm.py +0 -0
  36. {cdxcore-0.1.27 → cdxcore-0.1.29}/tests/test_pretty.py +0 -0
  37. {cdxcore-0.1.27 → cdxcore-0.1.29}/tests/test_util.py +0 -0
  38. {cdxcore-0.1.27 → cdxcore-0.1.29}/tests/test_verbose.py +0 -0
  39. {cdxcore-0.1.27 → cdxcore-0.1.29}/tests/test_version.py +0 -0
  40. {cdxcore-0.1.27 → cdxcore-0.1.29}/tmp/filelock.py +0 -0
  41. {cdxcore-0.1.27 → cdxcore-0.1.29}/tmp/np.py +0 -0
  42. {cdxcore-0.1.27 → cdxcore-0.1.29}/tmp/npsh1.py +0 -0
  43. {cdxcore-0.1.27 → cdxcore-0.1.29}/tmp/sharedarray.py +0 -0
  44. {cdxcore-0.1.27 → cdxcore-0.1.29}/up/git_message.py +0 -0
  45. {cdxcore-0.1.27 → cdxcore-0.1.29}/up/pip_modify_setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cdxcore
3
- Version: 0.1.27
3
+ Version: 0.1.29
4
4
  Summary: Basic Python Tools; upgraded cdxbasics
5
5
  Author-email: Hans Buehler <github@buehler.london>
6
6
  License-Expression: MIT
@@ -4,4 +4,4 @@ Created on June 2022
4
4
  @author: hansb
5
5
  """
6
6
 
7
- __version__ = "0.1.27" # auto-updated by setup.py
7
+ __version__ = "0.1.29" # auto-updated by setup.py
@@ -366,6 +366,7 @@ from collections import OrderedDict
366
366
  from collections.abc import Collection, Mapping, Callable, Iterable
367
367
  from enum import Enum
368
368
  from functools import update_wrapper
369
+ from string import Formatter
369
370
 
370
371
  import json as json
371
372
  import gzip as gzip
@@ -375,7 +376,7 @@ from .err import verify, error, warn, fmt as txtfmt
375
376
  from .pretty import PrettyObject
376
377
  from .verbose import Context
377
378
  from .version import Version, version as version_decorator, VersionError
378
- from .util import fmt_list, fmt_filename, DEF_FILE_NAME_MAP, plain, is_filename
379
+ from .util import fmt_list, fmt_filename, DEF_FILE_NAME_MAP, plain, is_filename, fmt_dict
379
380
  from .uniquehash import unique_hash48, UniqueLabel, NamedUniqueHash, named_unique_filename48_8
380
381
 
381
382
  """
@@ -1031,8 +1032,9 @@ class SubDir(object):
1031
1032
  """
1032
1033
  Delete all of the current directory if ``tclean`` is ``True``
1033
1034
  """
1034
- if self._tclean:
1035
+ if getattr(self, "_tclean", False):
1035
1036
  self.delete_everything(keep_directory=False)
1037
+ self._path = None
1036
1038
 
1037
1039
  @staticmethod
1038
1040
  def expand_std_root( name ):
@@ -1078,7 +1080,9 @@ class SubDir(object):
1078
1080
 
1079
1081
  def path_exists(self) -> bool:
1080
1082
  """ Whether the current directory exists """
1081
- return os.path.exists( self._path[:-1] ) if not self._path is None else False
1083
+ if self._path is None:
1084
+ return False
1085
+ return os.path.exists( self._path[:-1] )
1082
1086
 
1083
1087
  # -- a few basic properties --
1084
1088
 
@@ -2193,8 +2197,11 @@ class SubDir(object):
2193
2197
  ----------
2194
2198
  delete_self: bool
2195
2199
  Whether to delete the directory itself as well, or only its contents.
2200
+ If ``True``, the current object will be left in ``None`` state.
2201
+
2196
2202
  raise_on_error: bool
2197
2203
  ``False`` for silent failure
2204
+
2198
2205
  ext : str | None, default ``None``
2199
2206
  Extension for keys, or ``None`` for the directory's default.
2200
2207
  Use ``""`` to match all files regardless of extension.
@@ -2228,8 +2235,9 @@ class SubDir(object):
2228
2235
  Deletes the entire sub directory will all contents.
2229
2236
 
2230
2237
  *WARNING:* deletes *all* files and sub-directories, not just those with the present extension.
2231
- If ``keep_directory`` is ``False``, the directory referred to by this object will also be deleted.
2232
- In this case, ``self`` will be set to ``None``.
2238
+ If ``keep_directory`` is ``False``, then the directory referred to by this object will also be deleted.
2239
+
2240
+ In this case, ``self`` will be set to ``None`` state.
2233
2241
  """
2234
2242
  if self._path is None:
2235
2243
  return
@@ -2801,15 +2809,16 @@ class SubDir(object):
2801
2809
  # caching
2802
2810
  # -------
2803
2811
 
2804
- def cache( self, version : str = None , *,
2805
- dependencies : list = None,
2806
- label : Callable = None,
2807
- uid : Callable = None,
2808
- name : str = None,
2809
- exclude_args : list[str] = None,
2810
- include_args : list[str] = None,
2811
- exclude_arg_types : list[type] = None,
2812
- version_auto_class : bool = True):
2812
+ def cache( self, version : str|None = None , *,
2813
+ dependencies : list|None = None,
2814
+ label : Callable|None = None,
2815
+ uid : Callable|None = None,
2816
+ name : str|None = None,
2817
+ exclude_args : list[str]|None = None,
2818
+ include_args : list[str]|None = None,
2819
+ exclude_arg_types : list[type]|None = None,
2820
+ version_auto_class : bool = True,
2821
+ name_of_func_name_arg: str = "func_name"):
2813
2822
  """
2814
2823
  Advanced versioned caching for callables.
2815
2824
 
@@ -3158,7 +3167,7 @@ class SubDir(object):
3158
3167
  return self.x*y
3159
3168
 
3160
3169
  a = A(x=1)
3161
- f = cache.cache("0.1", id=lambda self, y : f"a.f({y})")(a.f) # <- decorate bound 'f'.
3170
+ f = cache.cache("0.1", uid=lambda self, y : f"a.f({y})")(a.f) # <- decorate bound 'f'.
3162
3171
  r = c(y=2)
3163
3172
 
3164
3173
  In this case the function ``f`` is bound to ``a``. The object is added as ``self`` to the function
@@ -3178,9 +3187,13 @@ class SubDir(object):
3178
3187
  :dec:`cdxcore.subdir.SubDir.cache`
3179
3188
  to provide version information at class level. Only version information are provided here.
3180
3189
 
3190
+ You can use :dec:`cdxcore.subdir.SubDir.cache_class` as an alias.
3191
+
3181
3192
  2) Secondly, decorate ``__init__``. You do not need to specify a version
3182
3193
  for ``__init__`` as its version usually coincides with the version of the class. At ``__init__``
3183
3194
  you define how unique IDs are generated from the parameters passed to object construction.
3195
+
3196
+ You can use :dec:`cdxcore.subdir.SubDir.cache_init` as an alias.
3184
3197
 
3185
3198
  Simple example:
3186
3199
 
@@ -3190,10 +3203,10 @@ class SubDir(object):
3190
3203
  cache = SubDir("!/.cache")
3191
3204
  cache.delete_all_content() # for illustration
3192
3205
 
3193
- @cache.cache("0.1")
3206
+ @cache.cache_class("0.1")
3194
3207
  class A(object):
3195
3208
 
3196
- @cache.cache(exclude_args=['debug'])
3209
+ @cache.cache_init(exclude_args=['debug'])
3197
3210
  def __init__(self, x, debug):
3198
3211
  if debug:
3199
3212
  print("__init__",x)
@@ -3216,10 +3229,10 @@ class SubDir(object):
3216
3229
 
3217
3230
  .. code-block:: python
3218
3231
 
3219
- @cache.cache("0.1")
3232
+ @cache.cache_class("0.1")
3220
3233
  class A(object):
3221
3234
 
3222
- @cache.cache("0.1", id=lambda x, debug: f"A.__init__(x={x})") # <-- 'self' is not passed to the lambda function; no need to add **_
3235
+ @cache.cache_init(uid=lambda x, debug: f"A.__init__(x={x})") # <-- 'self' is not passed to the lambda function; no need to add **_
3223
3236
  def __init__(self, x, debug):
3224
3237
  if debug:
3225
3238
  print("__init__",x)
@@ -3227,77 +3240,162 @@ class SubDir(object):
3227
3240
 
3228
3241
  Decorating classes with ``__slots__`` does not yet work.
3229
3242
 
3230
- See also
3231
- ^^^^^^^^
3243
+ Managing Caching Accross a Project
3244
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
3232
3245
 
3233
- For project-wide use it is usually inconvenient to control caching at the level of a
3246
+ For project-wide use it is usually convenient to control caching at the level of a
3234
3247
  project-wide cache root directory.
3235
- See :class:`cdxcore.subdir.VersionedCacheRoot` for a thin convenience wrapper around a :class:`cdxcore.subdir.SubDir`
3248
+ The classs :class:`cdxcore.subdir.VersionedCacheRoot` is a thin convenience wrapper around a :class:`cdxcore.subdir.SubDir`
3236
3249
  with a :class:`cdxcore.subdir.CacheController`.
3237
3250
 
3251
+ The idea is to have a central file, ``cache.py`` which contains the central root for caching.
3252
+ We recommend using an environment variable to be able to control the location of this directory
3253
+ out side the code. Here is an example with an environment variable ``PROJECT_CACHE_DIR``::
3254
+
3255
+ # file cache.py
3256
+
3257
+ from cdxcore.subdir import VersionedCacheRoot
3258
+ import os as os
3259
+
3260
+ cache_root = VersionedCacheRoot(
3261
+ os.getenv("PROJECT_CACHE_DIR", "!/.cache")
3262
+ )
3263
+
3264
+ In a particular project file, say ``pipeline.py`` create a file-local cache directory
3265
+ and use it::
3266
+
3267
+ # file pipeline.py
3268
+
3269
+ from cache import cache_root
3270
+
3271
+ cache_dir = cache_root("pipeline")
3272
+
3273
+ @cache_dir.cache("0.1")
3274
+ def f(x):
3275
+ return x+2
3276
+
3277
+ @cache_dir.cache("0.1", dependencies=[f])
3278
+ def g(x)
3279
+ return f(x)**2
3280
+
3281
+ # ...
3282
+
3283
+ In case you have issues with caching you can use the central root directory to turn on tracing
3284
+ accross your project:
3285
+
3286
+ .. code-block:: python
3287
+ :emphasize-lines: 4
3288
+
3289
+ from cdxcore.verbose import Context
3290
+ cache_root = VersionedCacheRoot(
3291
+ os.getenv("PROJECT_CACHE_DIR", "!/.cache"),
3292
+ debug_verbose=Context.all # turn full traing on
3293
+ )
3294
+
3238
3295
  Parameters
3239
3296
  ----------
3240
- version : str, optional
3297
+ version : str | None, default ``None``
3241
3298
  Version of the function.
3242
3299
 
3243
- * If ``None`` then ``F`` must be decorated with :dec:`cdxcore.version.version`.
3244
- * If set, the function ``F`` is first decorated with :dec:`cdxcore.version.version`.
3300
+ * If ``None`` then a common `F`` must be decorated manually with :dec:`cdxcore.version.version`.
3301
+ * If set, the function ``F`` is automatically first decorated with :dec:`cdxcore.version.version` for you.
3245
3302
 
3246
- dependencies : list[type], optional
3303
+ dependencies : list[type] | None, default ``None``
3247
3304
  A list of version dependencies, either by reference or by name.
3248
3305
  See :dec:`cdxcore.version.version` for details on name lookup if strings are used.
3249
3306
 
3250
- label : str | Callable
3307
+ label : str | Callable | None, default ``None``
3251
3308
  Specify a human-readable label for the function call given its parameters.
3309
+
3252
3310
  This label is used to generate the cache file name, and is also printed in when tracing
3253
3311
  hashing operations. Labels are not assumed to be unique, hence a unique hash of
3254
3312
  the label and the parameters to this function will be appended to generate
3255
3313
  the actual cache file name.
3256
3314
 
3257
- Use ``uid`` instead if ``label`` represents valid unique filenames.
3258
-
3315
+ Use ``uid`` instead if ``label`` represents valid unique filenames. You cannot specify both ``uid`` and ``label``.
3316
+ If neither ``uid`` and ``label`` are present, ``name`` will be used.
3259
3317
 
3260
3318
  **Usage:**
3261
3319
 
3320
+ * If ``label`` is a ``Callable`` then ``label( func_name=name, **parameters )`` will be called
3321
+ to generate the actual label.
3322
+
3323
+ The parameter ``func_name`` refers to the qualified
3324
+ name of the function. Its value can be overwitten by ``name``, while the parameter name itself
3325
+ can be overwritten using ``name_of_func_name_arg``, see below.
3326
+
3262
3327
  * If ``label`` is a plain string without ``{}`` formatting: use this string as-is.
3263
3328
 
3264
- * If ``label`` is a string with ``{}`` formatting, then ``label.format( name=name, **parameters )``
3265
- will be used to generate the actual label.
3329
+ * If ``label`` is a string with ``{}`` formatting, then ``label.format( func_name=name, **parameters )``
3330
+ will be used to generate the actual label.
3331
+
3332
+ The parameter ``func_name`` refers to the qualified
3333
+ name of the function. Its value can be overwitten by ``name``, while the parameter name itself
3334
+ can be overwritten using ``name_of_func_name_arg``, see below.
3266
3335
 
3267
- * If ``label`` is a ``Callable`` then ``label( name=name, **parameters )`` will be called
3268
- to generate the actual label.
3269
-
3270
3336
  See above for examples.
3271
3337
 
3272
3338
  ``label`` cannot be used alongside ``uid``.
3273
3339
 
3274
- uid : str | Callable
3340
+ uid : str | Callable | None, default ``None``
3275
3341
  Alternative to ``label`` which is assumed to generate a unique cache file name. It has the same
3276
- semantics as ``label``. When used, parameters to the decorated function are not hashed.
3342
+ semantics as ``label``. When used, parameters to the decorated function are not hashed
3343
+ as the ``uid`` is assumed to be already unique. The string must be a valid file name
3277
3344
 
3278
- ``uid`` be used alongside ``label``.
3345
+ Use ``label`` if the id is not unique. You cannot specify both ``uid`` and ``label``.
3346
+ If neither ``uid`` and ``label`` are present, ``name`` will be used (as non-unique ``label``).
3279
3347
 
3280
- name : str, optional
3281
- Name of this function which is used either on its own if neither ``label`` not ``uid`` are used.
3282
- If either of them is used, ``name`` is passed as a parameter to either the callable or the
3283
- formatting operator.
3348
+ name : str | None, default ``None``
3349
+ Name of this function which is used either on its own if neither ``label`` not ``uid`` are used,
3350
+ or which passed as a parameter ``func_name`` to either the callable or the
3351
+ formatting operator. See above for more details.
3284
3352
 
3285
3353
  If ``name`` is not specified it defaults to ``__qualname__`` expanded
3286
3354
  by the module name the function is defined in.
3287
3355
 
3288
- include_args : list[str]
3356
+ include_args : list[str] | None, default ``None``
3289
3357
  List of arguments to include in generating an unqiue ID, or ``None`` for all.
3290
3358
 
3291
- exclude_args : list[str]:
3292
- List of arguments to exclude from generating an unique ID.
3359
+ exclude_args : list[str] | None, default ``None``
3360
+ List of arguments to exclude from generating an unique ID. Examples of such non-functional arguments
3361
+ are workflow controls (debugging) and i/o elements.
3293
3362
 
3294
- exclude_arg_types : list[type]
3295
- List of parameter types to exclude from generating an unique ID.
3363
+ exclude_arg_types : list[type] | None, default ``None``
3364
+ List of parameter types to exclude from generating an unique ID. Examples of such non-functional arguments
3365
+ are workflow controls (debugging) and i/o elements.
3296
3366
 
3297
- version_auto_class : bool
3367
+ version_auto_class : bool, default ``True``
3298
3368
  Whether to automaticallty add version dependencies on base classes or, for member functions, on containing
3299
3369
  classes. This is the ``auto_class`` parameter for :dec:`cdxcore.version.version`.
3300
3370
 
3371
+ name_of_func_name_arg : str, default ``"func_name"``
3372
+ When formatting ``label`` or ``uid``, by default ``"func_name"`` is used to refer to the current
3373
+ function name. If there is already a parameter ``func_name`` for the function, an error will be raised.
3374
+ Use this flag to change the parameter name. Example::
3375
+
3376
+ .. code-block:: python
3377
+
3378
+ from cdxcore.subdir import SubDir
3379
+ cache = SubDir("?/temp")
3380
+
3381
+ @cache.cache("0.1")
3382
+ def f( func_name, x ):
3383
+ pass
3384
+
3385
+ f("test", 1)
3386
+
3387
+ Generates a :class:`RuntimeError` ``f@__main__: 'func_name' is a reserved keyword
3388
+ and used as formatting parameter
3389
+ name for the function name. Found it also in the function parameter list. Use 'name_of_name_arg' to change the internal parameter name used.``.
3390
+
3391
+ Instead, use:
3392
+
3393
+ .. code-block:: python
3394
+
3395
+ @cache.cache("0.1", label=lambda new_func_name, func_name, x : f"{new_func_name}(): {func_name} {x}", name_of_func_name_arg="new_func_name")
3396
+ def f( func_name, x ):
3397
+ pass
3398
+
3301
3399
  Returns
3302
3400
  -------
3303
3401
  Decorated F: Callable
@@ -3326,12 +3424,39 @@ class SubDir(object):
3326
3424
 
3327
3425
  The decorated ``F()`` has additional function parameters, namely:
3328
3426
 
3329
- * ``override_cache_mode`` : allows to override caching mode temporarily, in particular you can set it to ``"off"``.
3330
- * ``track_cached_files`` : allows passing a :class:`cdxcore.subdir.CacheTracker`
3427
+ * ``override_cache_mode`` : ``CacheMode`` | None, default ``None``
3428
+
3429
+ Allows overriding the ``CacheMode`` temporarily, in particular you can set it to ``"off"``.
3430
+
3431
+ * ``track_cached_files`` : :class:`cdxcore.subdir.CacheTracker` | None, default ``None``
3432
+
3433
+ Allows passing a :class:`cdxcore.subdir.CacheTracker`
3331
3434
  object to keep track of all
3332
3435
  files used (loaded from or saved to).
3333
3436
  The function :meth:`cdxcore.subdir.CacheTracker.delete_cache_files` can be used
3334
3437
  to delete all files involved in caching.
3438
+
3439
+ * ``return_cache_uid`` : bool, default ``False``
3440
+
3441
+ If ``True``, then the decorated function will return a tuple ``uid, result``
3442
+ where ``uid`` is the unique filename generated for this function call,
3443
+ and where ``result`` is the actual result from the function, cached or not.
3444
+
3445
+ Usage::
3446
+
3447
+ from cdxcore.subdir import SubDir
3448
+ cache_dir = SubDir("!/.cache")
3449
+
3450
+ @cache_dir.cache()
3451
+ def f(x, y):
3452
+ return x*y
3453
+
3454
+ uid, xy = f( x=1, y=2, return_cache_uid=True )
3455
+
3456
+ This pattern is thread-safe when compared to using::
3457
+
3458
+ xy = f( x=1, y=2 )
3459
+ uid = f.cache_info.filename
3335
3460
  """
3336
3461
  return CacheCallable(subdir = self,
3337
3462
  version = version,
@@ -3342,7 +3467,8 @@ class SubDir(object):
3342
3467
  exclude_args = exclude_args,
3343
3468
  include_args = include_args,
3344
3469
  exclude_arg_types = exclude_arg_types,
3345
- version_auto_class = version_auto_class )
3470
+ version_auto_class = version_auto_class,
3471
+ name_of_func_name_arg = name_of_func_name_arg)
3346
3472
 
3347
3473
  def cache_class( self,
3348
3474
  version : str = None , *,
@@ -3361,17 +3487,41 @@ class SubDir(object):
3361
3487
  @cache.cache_class("0.1")
3362
3488
  class A(object):
3363
3489
 
3364
- @cache.cache(exclude_args=['debug'])
3490
+ @cache.cache_init(exclude_args=['debug'])
3365
3491
  def __init__(self, x, debug):
3366
3492
  if debug:
3367
3493
  print("__init__",x)
3368
3494
  self.x = x
3369
3495
 
3370
3496
  """
3371
- return self.cache( name=name,
3372
- version=version,
3373
- dependencies=dependencies,
3374
- version_auto_class=version_auto_class)
3497
+ return self.cache( name=name, version=version, dependencies=dependencies, version_auto_class=version_auto_class)
3498
+
3499
+ def cache_init( self,
3500
+ label : Callable = None,
3501
+ uid : Callable = None,
3502
+ exclude_args : list[str] = None,
3503
+ include_args : list[str] = None,
3504
+ exclude_arg_types : list[type] = None,
3505
+ ):
3506
+ """
3507
+ Short-cut for :dec:`cdxcore.subdir.SubDir.cache` applied to decorating ``__init__``
3508
+ with a reduced number of available parameters.
3509
+
3510
+ Example::
3511
+
3512
+ cache = SubDir("!/.cache")
3513
+
3514
+ @cache.cache_class("0.1")
3515
+ class A(object):
3516
+
3517
+ @cache.cache_init(exclude_args=['debug'])
3518
+ def __init__(self, x, debug):
3519
+ if debug:
3520
+ print("__init__",x)
3521
+ self.x = x
3522
+
3523
+ """
3524
+ return self.cache( label=label, uid=uid, exclude_args=exclude_args, include_args=include_args, exclude_arg_types=exclude_arg_types )
3375
3525
 
3376
3526
  # ========================================================================
3377
3527
  # Caching, convenience
@@ -3444,6 +3594,9 @@ def VersionedCacheRoot( directory : str, *,
3444
3594
 
3445
3595
  * ``hash_length``: length used for hashes, see :class:`cdxcore.uniquehash.UniqueHash`.
3446
3596
 
3597
+ * ``debug_verbose`` set to ``Context.all`` after importing ``from cdxcore.verbose import Context``
3598
+ will turn on tracing all caching operations.
3599
+
3447
3600
  Returns
3448
3601
  -------
3449
3602
  Root : :class:`cdxcore.subdir.SubDir`
@@ -3527,7 +3680,7 @@ def _ensure_has_version( F,
3527
3680
  dependencies=dependencies,
3528
3681
  auto_class=auto_class)(F)
3529
3682
 
3530
- def _qualified_name( F, name ):
3683
+ def _qualified_name( F, name = None ):
3531
3684
  """
3532
3685
  Return qualified name including module name, robustly
3533
3686
  """
@@ -3546,6 +3699,36 @@ def _qualified_name( F, name ):
3546
3699
  warn( f"Cannot determine module name for '{name}' of {type(F)}" )
3547
3700
  return name
3548
3701
 
3702
+ def _expected_str_fmt_args(fmt: str):
3703
+ """
3704
+ Inspect a format string and report what arguments it expects.
3705
+ Returns:
3706
+ - auto_positional: count of automatic '{}' fields
3707
+ - positional_indices: explicit numeric field indices used (e.g., {0}, {2})
3708
+ - keywords: named fields used (e.g., {user}, {price})
3709
+ """
3710
+ f = Formatter()
3711
+ pos = set()
3712
+ auto = 0
3713
+ kws = set()
3714
+
3715
+ for literal, field, spec, conv in f.parse(fmt):
3716
+ if field is None:
3717
+ continue
3718
+ # Keep only the first identifier before attribute/index access
3719
+ head = field.split('.')[0].split('[')[0]
3720
+ if head == "": # '{}' → automatic positional
3721
+ auto += 1
3722
+ elif head.isdigit(): # '{0}', '{2}' → explicit positional
3723
+ pos.add(int(head))
3724
+ else: # '{name}' → keyword
3725
+ kws.add(head)
3726
+
3727
+ return PrettyObject( positional=auto,
3728
+ posindices=pos,
3729
+ keywords=kws
3730
+ )
3731
+
3549
3732
  class CacheCallable(object):
3550
3733
  """
3551
3734
  Wrapper for a cached function.
@@ -3554,17 +3737,17 @@ class CacheCallable(object):
3554
3737
  """
3555
3738
 
3556
3739
  def __init__(self,
3557
- subdir : SubDir, *,
3558
- version : str = None,
3559
- dependencies : list,
3560
- label : Callable = None,
3561
- uid : Callable = None,
3562
- name : str = None,
3563
- exclude_args : set[str] = None,
3564
- include_args : set[str] = None,
3565
- exclude_arg_types : set[type] = None,
3566
- version_auto_class : bool = True,
3567
- name_of_name_arg : str = "name"):
3740
+ subdir : SubDir, *,
3741
+ version : str = None,
3742
+ dependencies : list,
3743
+ label : Callable = None,
3744
+ uid : Callable = None,
3745
+ name : str = None,
3746
+ exclude_args : set[str] = None,
3747
+ include_args : set[str] = None,
3748
+ exclude_arg_types : set[type] = None,
3749
+ version_auto_class : bool = True,
3750
+ name_of_func_name_arg: str = "name"):
3568
3751
  """
3569
3752
  Utility class for :dec:`cdxcore.subdir.SubDir.cache`.
3570
3753
 
@@ -3573,18 +3756,38 @@ class CacheCallable(object):
3573
3756
  if not label is None and not uid is None:
3574
3757
  error("Cannot specify both 'label' and 'uid'.")
3575
3758
 
3576
- self._subdir = SubDir(subdir)
3577
- self._version = str(version) if not version is None else None
3578
- self._dependencies = list(dependencies) if not dependencies is None else None
3579
- self._label = label
3580
- self._uid = uid
3581
- self._name = str(name) if not name is None else None
3582
- self._exclude_args = set(exclude_args) if not exclude_args is None and len(exclude_args) > 0 else None
3583
- self._include_args = set(include_args) if not include_args is None and len(include_args) > 0 else None
3584
- self._exclude_arg_types = set(exclude_arg_types) if not exclude_arg_types is None and len(exclude_arg_types) > 0 else None
3585
- self._version_auto_class = bool(version_auto_class)
3586
- self._name_of_name_arg = str(name_of_name_arg)
3759
+ self._subdir = SubDir(subdir)
3760
+ self._input_version = str(version) if not version is None else None
3761
+ self._dependencies = list(dependencies) if not dependencies is None else None
3762
+ self._label = label
3763
+ self._uid = uid
3764
+ self._name = str(name) if not name is None else None
3765
+ self._exclude_args = set(exclude_args) if not exclude_args is None and len(exclude_args) > 0 else None
3766
+ self._include_args = set(include_args) if not include_args is None and len(include_args) > 0 else None
3767
+ self._exclude_arg_types = set(exclude_arg_types) if not exclude_arg_types is None and len(exclude_arg_types) > 0 else None
3768
+ self._version_auto_class = bool(version_auto_class)
3769
+ self._name_of_func_name_arg = str(name_of_func_name_arg)
3770
+ self._uid_label_params = None
3587
3771
 
3772
+ if not self.uid_or_label is None:
3773
+ F = self.uid_or_label
3774
+ which = "'uid'" if not uid is None else "'label'"
3775
+ if isinstance( F, str ):
3776
+ r = _expected_str_fmt_args( F )
3777
+ if len(r.positional) + len(r.posindices) > 0:
3778
+ raise ValueError("f{which} '{F}' cannot have positional arguments (empty brackets {} or brackets with integer position {1}). Use only named arguments.")
3779
+ self._uid_label_params = list(r.keywords)
3780
+ del r
3781
+ else:
3782
+ if not inspect.isfunction(F):
3783
+ if not callable(F):
3784
+ raise ValueError(f"{which} '{_qualified_name(F)}' is not callable")
3785
+ F = F.__call__
3786
+ assert inspect.isfunction(F), ("Internal error - function expected")
3787
+ self._uid_label_params = list( inspect.signature(F).parameters )
3788
+ del F, which
3789
+ self._uid_label_params = self._uid_label_params if len(self._uid_label_params) > 0 else None
3790
+
3588
3791
  @property
3589
3792
  def uid_or_label(self) -> Callable:
3590
3793
  """ ID or label """
@@ -3617,6 +3820,10 @@ class CacheCallable(object):
3617
3820
  def global_exclude_arg_types(self) -> list[type]:
3618
3821
  """ Returns ``exclude_arg_types`` of the underlying :class:`cdxcore.subdir.CacheController` """
3619
3822
  return self.cache_controller.exclude_arg_types
3823
+ @property
3824
+ def uid_label_params(self) -> list:
3825
+ """ Returns the ``set`` of parameters the ``uid`` or ``label`` function expects """
3826
+ return self._uid_label_params
3620
3827
 
3621
3828
  def __call__(self, F : Callable):
3622
3829
  """
@@ -3668,7 +3875,7 @@ class CacheCallable(object):
3668
3875
  # apply version
3669
3876
  # this also ensures that __init__ picks up a version dependency on the class itse
3670
3877
  # (as we forceed 'auto_class' to be true)
3671
- C = _ensure_has_version( C, version=self._version,
3878
+ C = _ensure_has_version( C, version=self._input_version,
3672
3879
  dependencies=self._dependencies,
3673
3880
  auto_class=self._version_auto_class)
3674
3881
 
@@ -3689,7 +3896,7 @@ class CacheCallable(object):
3689
3896
  # Cannot currently decorate classes.
3690
3897
 
3691
3898
 
3692
- is_method = inspect.ismethod(F)
3899
+ is_method = inspect.ismethod(F) # for *bound* methods
3693
3900
  if is_method:
3694
3901
  assert not getattr(F, "__self__", None) is None, ("Method type must have __self__...?", F.__qualname__ )
3695
3902
  elif not inspect.isfunction(F):
@@ -3752,11 +3959,12 @@ class CacheCallable(object):
3752
3959
  # -------
3753
3960
  # Decorate now or pick up existing @version
3754
3961
 
3755
- F = _ensure_has_version( F, version=self._version,
3962
+ F = _ensure_has_version( F, version=self._input_version,
3756
3963
  dependencies=self._dependencies,
3757
3964
  auto_class=self._version_auto_class,
3758
3965
  allow_default=is_new )
3759
-
3966
+ version = F.version.unique_id64
3967
+
3760
3968
  # name
3761
3969
  # ----
3762
3970
 
@@ -3768,8 +3976,9 @@ class CacheCallable(object):
3768
3976
  exclude_types = ( self._exclude_arg_types if not self._exclude_arg_types is None else set() )\
3769
3977
  | ( self.global_exclude_arg_types if not self.global_exclude_arg_types is None else set())
3770
3978
 
3771
- def execute( *args, override_cache_mode : CacheMode = None,
3772
- track_cached_files : CacheTracker = None,
3979
+ def execute( *args, override_cache_mode : CacheMode|None = None,
3980
+ track_cached_files : CacheTracker|None = None,
3981
+ return_cache_uid : bool = False,
3773
3982
  **kwargs ):
3774
3983
  """
3775
3984
  Cached execution of the wrapped function
@@ -3785,38 +3994,46 @@ class CacheCallable(object):
3785
3994
 
3786
3995
  uid_or_label = self.uid_or_label
3787
3996
  filename = None
3788
- if isinstance(uid_or_label, str) and self.unique:
3789
- # if 'id' does not contain formatting codes,
3790
- # and the result is 'unique' then do not bother collecting
3791
- # function arguments
3792
- try:
3793
- filename = uid_or_label.format() # throws a KeyError if 'id' contains formatting information
3794
- except KeyError:
3795
- pass
3997
+ if self.unique and self._uid_label_params is None:
3998
+ # the string or function do not require any parameters, and is unique
3999
+ assert not uid_or_label is None
4000
+ filename = uid_or_label if isinstance( uid_or_label, str ) else uid_or_label()
3796
4001
 
3797
- if not filename is None:
3798
- # generate name with the unique string provided by the user
3799
4002
  if not is_filename(filename):
3800
4003
  raise ValueError(f"The unique filename '{filename}' computed for '{name}' contains invalid characters for filename. When using `uid` make sure that "+\
3801
- "the returned ID is a valid filename (and unique)")
4004
+ "the returned ID is a valid filename (and is unique)")
3802
4005
  label = filename
3803
- filename = self.uniqueFileName( filename )
3804
4006
  arguments = None
3805
-
4007
+
3806
4008
  else:
4009
+ # need the list of parameters to compute a hash and/or a label
4010
+ which = 'uid' if not self._uid is None else 'label'
4011
+
3807
4012
  # get dictionary of named arguments
3808
4013
  arguments = execute.cache_info.signature.bind(*args,**kwargs)
3809
4014
  arguments.apply_defaults()
3810
4015
  arguments = arguments.arguments # ordered dict
3811
4016
 
4017
+ # delete 'cls' from argument list for class functions
3812
4018
  if is_new:
3813
- # delete 'cls' from argument list
3814
4019
  assert len(arguments) >= 1, ("*** Internal error", F.__qualname__, is_new, arguments)
3815
4020
  del arguments[list(arguments)[0]]
3816
- argus = set(arguments)
4021
+
4022
+ # add 'self' for methods
4023
+ if is_method:
4024
+ # add __self__ to the beginning of all arguments
4025
+ full_arguments = OrderedDict()
4026
+ if is_method:
4027
+ if 'self' in set(arguments):
4028
+ raise RuntimeError(f"'self' found in bound method '{name}' argument list {fmt_dict(execute.cache_info.signature.bind(*args,**kwargs).arguments)}.")
4029
+ full_arguments['self'] = F.__self__
4030
+ full_arguments |= arguments
4031
+ arguments = full_arguments
4032
+ del full_arguments
3817
4033
 
3818
4034
  # filter dictionary
3819
4035
  if not self._exclude_args is None or not self._include_args is None:
4036
+ argus = set(arguments)
3820
4037
  excl = set(self._exclude_args) if not self._exclude_args is None else set()
3821
4038
  if not self._exclude_args is None:
3822
4039
  if self._exclude_args > argus:
@@ -3830,7 +4047,7 @@ class CacheCallable(object):
3830
4047
  for arg in excl:
3831
4048
  if arg in arguments:
3832
4049
  del arguments[arg]
3833
- del excl
4050
+ del excl, argus
3834
4051
 
3835
4052
  if len(exclude_types) > 0:
3836
4053
  excl = []
@@ -3841,41 +4058,45 @@ class CacheCallable(object):
3841
4058
  if arg in arguments:
3842
4059
  del arguments[arg]
3843
4060
 
3844
- # did the user provide a label or unique ID?
3845
4061
  if uid_or_label is None:
4062
+ # no label or unique ID
4063
+ assert not self.unique
3846
4064
  uid_or_label = name
3847
4065
 
4066
+ elif self._uid_label_params is None:
4067
+ # label function or string does not need any parameters
4068
+ assert not self.unique
4069
+ uid_or_label = uid_or_label if isinstance( uid_or_label, str ) else uid_or_label()
4070
+
3848
4071
  else:
3849
- if self._name_of_name_arg in arguments:
3850
- error(f"{name}: '{self._name_of_name_arg}' is a reserved keyword and used as parameter name for the function name. Found it also in the function parameter list. Use 'name_of_name_arg' to change the internal parameter name used.")
3851
-
3852
- # add standard arguments
3853
- full_arguments = OrderedDict()
3854
- if is_method:
3855
- assert not 'self' in set(arguments), ("__self__ found in bound method argument list...?", F.__qualname__, execute.cache_info.signature.bind(*args,**kwargs).arguments )
3856
- full_arguments['self'] = F.__self__
3857
- full_arguments[self._name_of_name_arg] = name
3858
- for k,v in arguments.items():
3859
- full_arguments[k] = v
3860
- arguments = full_arguments
3861
- del full_arguments, k, v
4072
+ # function or format string required parameters
4073
+ # add parameters in order of label/uid parameters
4074
+ assert not self._uid_label_params is None
4075
+
4076
+ fmt_arguments = {}
4077
+ for k in self._uid_label_params:
4078
+ if k == self._name_of_func_name_arg:
4079
+ if self._name_of_func_name_arg in arguments:
4080
+ error(f"{name}: '{self._name_of_func_name_arg}' is a reserved keyword for '{which}' which refers to the current function name. "
4081
+ "Found it also in the function parameter list. Use 'name_of_func_name_arg' to change the internal parameter name used.")
4082
+ fmt_arguments[k] = name
4083
+ else:
4084
+ if not k in arguments:
4085
+ args_ = [ f"'{_}'" for _ in arguments ]
4086
+ raise ValueError(f"Error while generating '{which}' for '{name}': formatting function expected a parameter '{k}' which is not present "+\
4087
+ f"in the list of parameters passed to '{name}': {fmt_list(args_)}.")
4088
+ fmt_arguments[k] = arguments[k]
3862
4089
 
3863
4090
  # call format or function
3864
4091
  if isinstance( uid_or_label, str ):
3865
- try:
3866
- uid_or_label = str.format( uid_or_label, **arguments )
3867
- except KeyError as e:
3868
- raise KeyError(e, f"Error while generating id for '{name}' using format string '{uid_or_label}': {e}. Available arguments: {list(arguments)}")
3869
-
4092
+ uid_or_label = str.format( uid_or_label, **fmt_arguments )
3870
4093
  else:
3871
- which = 'uid' if not self._uid is None else 'label'
3872
4094
  try:
3873
- uid_or_label = uid_or_label(**arguments)
3874
- except TypeError as e:
3875
- raise TypeError(e, f"Error while generating '{which}' for '{name}' using a function: {e}. Available arguments: {list(arguments)}")
4095
+ uid_or_label = uid_or_label(**fmt_arguments)
3876
4096
  except Exception as e:
3877
- raise type(e)(f"Error while generating '{which}' for '{name}': attempt to call '{which}' of type {type(uid_or_label)} failed: {e}")
3878
- assert isinstance(uid_or_label, str), ("Error:", which, "callable must return a string. Found",type(uid_or_label))
4097
+ raise type(e)(f"Error while generating '{which}' for '{name}': attempt to call '{which}' of callable type {type(uid_or_label)} failed: {e}")
4098
+ if not isinstance(uid_or_label, str):
4099
+ raise ValueError("Error calling callable '{which}' for '{name}': callable must return a string. Found {type(uid_or_label))}")
3879
4100
 
3880
4101
  if self.unique:
3881
4102
  if not is_filename(uid_or_label):
@@ -3891,7 +4112,6 @@ class CacheCallable(object):
3891
4112
  # determine version, cache mode
3892
4113
  # ------------------
3893
4114
 
3894
- version_ = self._version if not self._version is None else F.version.unique_id64
3895
4115
  cache_mode = CacheMode(override_cache_mode) if not override_cache_mode is None else self.cache_mode
3896
4116
  del override_cache_mode
3897
4117
 
@@ -3899,8 +4119,8 @@ class CacheCallable(object):
3899
4119
  # -------------------------
3900
4120
 
3901
4121
  execute.cache_info.label = str(label) if not label is None else None
3902
- execute.cache_info.filename = filename
3903
- execute.cache_info.version = version_
4122
+ execute.cache_info.filename = filename # that is the unique ID for this call
4123
+ execute.cache_info.version = version
3904
4124
 
3905
4125
  if self.cache_controller.keep_last_arguments:
3906
4126
  info_arguments = OrderedDict()
@@ -3919,11 +4139,11 @@ class CacheCallable(object):
3919
4139
  pass
3920
4140
  tag = Tag()
3921
4141
  if not is_new:
3922
- r = self._subdir.read( filename, tag, version=version_ )
4142
+ r = self._subdir.read( filename, tag, version=version )
3923
4143
  else:
3924
4144
  try:
3925
4145
  execute.__new_during_read = True
3926
- r = self._subdir.read( filename, tag, version=version_ )
4146
+ r = self._subdir.read( filename, tag, version=version )
3927
4147
  finally:
3928
4148
  execute.__new_during_read = False
3929
4149
 
@@ -3932,11 +4152,13 @@ class CacheCallable(object):
3932
4152
  track_cached_files += self._fullFileName(filename)
3933
4153
  execute.cache_info.last_cached = True
3934
4154
  if not debug_verbose is None:
3935
- debug_verbose.write(f"cache({name}): read '{label}' version 'version {version_}' from cache '{self._subdir.full_file_name(filename)}'.")
4155
+ debug_verbose.write(f"cache({name}): read '{label}' version 'version {version}' from cache '{self._subdir.full_file_name(filename)}'.")
3936
4156
  if is_new:
3937
4157
  assert r.__magic_cache_call_init__ is None, ("**** Internal error. __init__ should reset __magic_cache_call_init__", F.__qualname__, label)
3938
4158
  r.__magic_cache_call_init__ = False # since we called __new__, __init__ will be called next
3939
4159
 
4160
+ if return_cache_uid:
4161
+ return filename, r
3940
4162
  return r
3941
4163
 
3942
4164
  r = F(*args, **kwargs)
@@ -3951,7 +4173,7 @@ class CacheCallable(object):
3951
4173
  assert r.__magic_cache_call_init__ is None, ("**** Internal error. __init__ should reset __magic_cache_call_init__")
3952
4174
 
3953
4175
  if cache_mode.write:
3954
- self._subdir.write(filename,r,version=version_)
4176
+ self._subdir.write(filename,r,version=version)
3955
4177
  if not track_cached_files is None:
3956
4178
  track_cached_files += self._subdir.full_file_name(filename)
3957
4179
  execute.cache_info.last_cached = False
@@ -3963,9 +4185,12 @@ class CacheCallable(object):
3963
4185
 
3964
4186
  if not debug_verbose is None:
3965
4187
  if cache_mode.write:
3966
- debug_verbose.write(f"cache({name}): called '{label}' version 'version {version_}' and wrote result into '{self._subdir.full_file_name(filename)}'.")
4188
+ debug_verbose.write(f"cache({name}): called '{label}' version 'version {version}' and wrote result into '{self._subdir.full_file_name(filename)}'.")
3967
4189
  else:
3968
- debug_verbose.write(f"cache({name}): called '{label}' version 'version {version_}' but did *not* write into '{self._subdir.full_file_name(filename)}'.")
4190
+ debug_verbose.write(f"cache({name}): called '{label}' version 'version {version}' but did *not* write into '{self._subdir.full_file_name(filename)}'.")
4191
+
4192
+ if return_cache_uid:
4193
+ return filename, r
3969
4194
  return r
3970
4195
 
3971
4196
  update_wrapper( wrapper=execute, wrapped=F )
@@ -266,11 +266,10 @@ class UniqueHash( object ):
266
266
  """ Return copy of `self`. """
267
267
  return UniqueHash( **{ k:v for k,v in self.__dict__.items() if not k[:1] == "_"} )
268
268
 
269
- def __call__(self, *args, debug_trace : DebugTrace = None, **kwargs) -> str:
269
+ def __call__(__self__, # LEAVE THIS NAME. **kwargs might contain 'self' arguments.
270
+ *args, debug_trace : DebugTrace = None, **kwargs) -> str:
270
271
  """
271
- :meta public:
272
-
273
- Returns a unique hash for the `arg` and `kwargs` parameters passed to this function.
272
+ Returns a unique hash for the ``arg`` and ``kwargs`` parameters passed to this function.
274
273
 
275
274
  Example::
276
275
 
@@ -289,7 +288,7 @@ class UniqueHash( object ):
289
288
  args, kwargs:
290
289
  Parameters to hash.
291
290
 
292
- debug_trace : :class:`cdxcore.uniquehash.DebugTrace`
291
+ debug_trace : :class:`cdxcore.uniquehash.DebugTrace` | None, default ``None``
293
292
  Allows tracing of hashing activity for debugging purposes.
294
293
  Two implementations of ``DebugTrace`` are available:
295
294
 
@@ -302,13 +301,13 @@ class UniqueHash( object ):
302
301
  Returns
303
302
  -------
304
303
  Hash : str
305
- String of at most `length`
304
+ String of at most ``self.length``.
306
305
  """
307
- h, _ = self._mk_blake( h=self.length//2 )
306
+ h, _ = __self__._mk_blake( h=__self__.length//2 )
308
307
  if len(args) > 0:
309
- self._hash_any( h, args, debug_trace = debug_trace )
308
+ __self__._hash_any( h, args, debug_trace = debug_trace )
310
309
  if len(kwargs) > 0:
311
- self._hash_any( h, kwargs, debug_trace = debug_trace )
310
+ __self__._hash_any( h, kwargs, debug_trace = debug_trace )
312
311
  return h.hexdigest()
313
312
 
314
313
  # Utility functions
@@ -655,7 +655,13 @@ def version( version : str = "0.0.1" ,
655
655
  if not gversion._class is None:
656
656
  continue
657
657
  gversion._class = f
658
- f.version = Version(f, version, dep, auto_class=auto_class )
658
+
659
+ version_ = Version(f, version, dep, auto_class=auto_class )
660
+ try:
661
+ f.version = version_
662
+ except AttributeError:
663
+ f.__dict__['version'] = version_
664
+ del version_
659
665
  assert type(f.version).__name__ == Version.__name__
660
666
  return f
661
667
  return wrap
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cdxcore
3
- Version: 0.1.27
3
+ Version: 0.1.29
4
4
  Summary: Basic Python Tools; upgraded cdxbasics
5
5
  Author-email: Hans Buehler <github@buehler.london>
6
6
  License-Expression: MIT
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "cdxcore"
9
- version = "0.1.27"
9
+ version = "0.1.29"
10
10
  description = "Basic Python Tools; upgraded cdxbasics"
11
11
  authors = [{ name = "Hans Buehler", email = "github@buehler.london" }]
12
12
  readme = "README.md"
@@ -25,14 +25,15 @@ import_local()
25
25
  """
26
26
  Imports
27
27
  """
28
- from cdxcore.subdir import SubDir, CacheMode, VersionError, VersionPresentError
28
+ from cdxcore.subdir import SubDir, CacheMode, VersionError, VersionPresentError, VersionedCacheRoot
29
+ from cdxcore.version import version
29
30
  import numpy as np
30
31
 
31
32
  class Test(unittest.TestCase):
32
33
 
33
34
  def test_subdir(self):
34
35
 
35
- sub = SubDir("!/.tmp_test_for_cdxbasics.subdir", delete_everything=True )
36
+ sub = SubDir("?/.tmp_test_for_cdxbasics.subdir", delete_everything=True )
36
37
  sub['y'] = 2
37
38
  sub.write('z',3)
38
39
  sub.write_string('l',"hallo")
@@ -193,11 +194,8 @@ class Test(unittest.TestCase):
193
194
  with self.assertRaises(VersionError):
194
195
  r = sub.read("test", None, version="2", raise_on_error=True)
195
196
  # wrong version
196
- sub.delete_everything()
197
197
 
198
198
  def test_new(self):
199
-
200
-
201
199
  subdir = SubDir("my_directory") # relative to current working directory
202
200
  subdir = SubDir("./my_directory") # relative to current working directory
203
201
  subdir = SubDir("~/my_directory") # relative to home directory
@@ -248,8 +246,6 @@ class Test(unittest.TestCase):
248
246
  test_format( SubDir.GZIP, True )
249
247
  test_format( SubDir.JSON_PICKLE )
250
248
 
251
-
252
-
253
249
  def test_cache_mode(self):
254
250
 
255
251
  on = CacheMode("on")
@@ -275,6 +271,146 @@ class Test(unittest.TestCase):
275
271
  self.assertEqual( [ x.write for x in allc ], [True, True, False, False, True, False] )
276
272
  self.assertEqual( [ x.delete for x in allc ], [False, False, False, True, True, False ] )
277
273
  self.assertEqual( [ x.del_incomp for x in allc ], [True, False, False, True, True, False ] )
274
+
275
+ class A(object):
276
+ def __init__(self, x):
277
+ self.x = x
278
+ class B(object):
279
+ def __init__(self, x):
280
+ self.x = x
281
+
282
+ raw_sub = VersionedCacheRoot("?/subdir_cache_test2", exclude_arg_types=[A] )
283
+
284
+ class C(object):
285
+ def __init__(self, x):
286
+ self.x = x
287
+ @raw_sub.cache("0.1")
288
+ def f(self, y):
289
+ return self.x*y
290
+
291
+ class Test(unittest.TestCase):
292
+
293
+ def test_cache( self ):
294
+
295
+ sub = VersionedCacheRoot("?/subdir_cache_test", exclude_arg_types=[A] )
296
+
297
+ @sub.cache("1.0")
298
+ def f(x):
299
+ return x
300
+
301
+ _ = f(1)
302
+ _ = f(1)
303
+ self.assertTrue( f.cache_info.last_cached )
304
+
305
+ @sub.cache("1.0")
306
+ def g(x):
307
+ return x
308
+
309
+ # a new B object each time -> no caching
310
+ _ = f(B(1))
311
+ _ = f(B(2))
312
+ self.assertFalse( f.cache_info.last_cached )
313
+
314
+ # same B object each time -> no caching
315
+ _ = f(B(1))
316
+ _ = f(B(1))
317
+ self.assertTrue( f.cache_info.last_cached )
318
+
319
+ # a new A object each time -> caching, as we ignore 'A' types
320
+ _ = f(A(1))
321
+ _ = f(A(2))
322
+ self.assertTrue( f.cache_info.last_cached )
323
+
324
+ @sub.cache("1.0", label=lambda x: f"f({x})")
325
+ def f(x):
326
+ return x
327
+
328
+ _ = f(1)
329
+ self.assertEqual( f.cache_info.filename[:5], "f(1) " )
330
+ uid, _ = f(1, return_cache_uid=True)
331
+ self.assertEqual( uid[:5], "f(1) " )
332
+
333
+ @sub.cache("1.0", uid=lambda x: f"f({x})")
334
+ def f(x):
335
+ return x
336
+
337
+ _ = f(1)
338
+ self.assertEqual( f.cache_info.filename, "f(1)" )
339
+ uid, _ = f(1, return_cache_uid=True)
340
+ self.assertEqual( uid, "f(1)" )
341
+
342
+ # test member caching
343
+
344
+ c = C(2.)
345
+ _ = c.f(3)
346
+ self.assertFalse( c.f.cache_info.last_cached )
347
+ _ = c.f(3)
348
+ self.assertTrue( c.f.cache_info.last_cached )
349
+ _ = c.f(2)
350
+ self.assertFalse( c.f.cache_info.last_cached )
351
+
352
+ # test versioning
353
+
354
+ @version("F")
355
+ def F(x):
356
+ return x
357
+
358
+ @sub.cache("G")
359
+ def G(x):
360
+ return x
361
+
362
+ @sub.cache("H", dependencies=[F,G])
363
+ def H(x):
364
+ return G(x)*F(x)
365
+
366
+ _ = H(2.)
367
+ self.assertEqual( H.cache_info.version, "H { Test.test_cache.<locals>.F: F, Test.test_cache.<loc 3fabc694" )
368
+ self.assertEqual( H.cache_info.version, H.version.unique_id64 )
369
+
370
+ # decorate live member functions
371
+
372
+ class AA(object):
373
+ def __init__(self,x):
374
+ self.x = x
375
+ def f(self,y):
376
+ return self.x*y
377
+
378
+ a = AA(x=1)
379
+ f = sub.cache("0.1", label=lambda y : f"a.f({y})")(a.f) # <- decorate bound 'f'.
380
+ _ = f(y=2)
381
+ self.assertFalse( f.cache_info.last_cached )
382
+ self.assertEqual( f.cache_info.version, "0.1" )
383
+ _ = f(y=2)
384
+ self.assertTrue( f.cache_info.last_cached )
385
+
386
+ # funcname
387
+
388
+ sub = VersionedCacheRoot("?/subdir_cache_test", exclude_arg_types=[A], keep_last_arguments=True)
389
+
390
+ @sub.cache("0.1", label=lambda new_func_name, func_name, x, y : f"{new_func_name}(): {func_name} {x} {y}", name_of_func_name_arg="new_func_name")
391
+ def f( func_name, x, y ):
392
+ pass
393
+ f("test",1,y=2)
394
+ self.assertEqual( repr(f.cache_info.arguments), "OrderedDict({'func_name': 'test', 'x': '1', 'y': '2'})" )
395
+
396
+ # this should just work
397
+ @sub.cache("0.1", label=lambda func_name, x : f"{func_name} {x}")
398
+ def f( func_name, x ):
399
+ pass
400
+ with self.assertRaises(RuntimeError):
401
+ f("test",1)
402
+
403
+ # cuttinf off
404
+ @sub.cache("0.1", uid=lambda x,y: f"h2({x},{y})_______________________________________________________________________", exclude_args='debug')
405
+ def h2(x,y,debug=False):
406
+ if debug:
407
+ print(f"h(x={x},y={y})")
408
+ return x*y
409
+ h2(1,1)
410
+ # %%
411
+ self.assertEqual( h2.cache_info.filename, "h2(1,1)________________________________ 46a70d67" )
412
+
413
+
278
414
  if __name__ == '__main__':
279
415
  unittest.main()
280
416
 
@@ -457,16 +457,25 @@ class Test(unittest.TestCase):
457
457
  def __unique_hash__( self, unique_hash, debug_trace ):
458
458
  return ( self._seed, self._size )
459
459
 
460
+ class E(A):
461
+ """ Fixed string """
462
+ def __init__(self):
463
+ self.__unique_hash__ = "some_string"
464
+
460
465
  empty = unique_hash( ('Test.test_uniqueHash.<locals>.A') )
461
466
  hash1 = unique_hash( A() )
462
467
  hash2 = unique_hash( B() )
463
468
  hash3 = unique_hash( C() )
464
469
  hash4 = unique_hash( D() )
470
+ hash5 = unique_hash( E() )
471
+ h_5 = unique_hash( E().__unique_hash__ )
465
472
  self.assertEqual( empty, "653a05ac14649dbceebbd7f3d4a0b89f" )
466
473
  self.assertEqual( hash1, empty )
467
474
  self.assertEqual( hash2, "ae7cc6d56596eaa20dcd6aedc6e89d85" )
468
475
  self.assertEqual( hash3, "5e387f9e86426319577d2121a4e1437b" )
469
476
  self.assertEqual( hash4, "c9b449e95339458df155752acadbebb1" )
477
+ self.assertEqual( hash5, "79ee12b070c036e874263c6f4d70df98" )
478
+ self.assertEqual( hash5, h_5 )
470
479
 
471
480
 
472
481
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes