cdxcore 0.1.28__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.28 → cdxcore-0.1.29}/PKG-INFO +1 -1
  2. {cdxcore-0.1.28 → cdxcore-0.1.29}/cdxcore/__init__.py +1 -1
  3. {cdxcore-0.1.28 → cdxcore-0.1.29}/cdxcore/subdir.py +213 -100
  4. {cdxcore-0.1.28 → cdxcore-0.1.29}/cdxcore/uniquehash.py +8 -9
  5. {cdxcore-0.1.28 → cdxcore-0.1.29}/cdxcore/version.py +7 -1
  6. {cdxcore-0.1.28 → cdxcore-0.1.29}/cdxcore.egg-info/PKG-INFO +1 -1
  7. {cdxcore-0.1.28 → cdxcore-0.1.29}/pyproject.toml +1 -1
  8. {cdxcore-0.1.28 → cdxcore-0.1.29}/tests/test_subdir.py +83 -2
  9. {cdxcore-0.1.28 → cdxcore-0.1.29}/tests/test_uniquehash.py +9 -0
  10. {cdxcore-0.1.28 → cdxcore-0.1.29}/LICENSE +0 -0
  11. {cdxcore-0.1.28 → cdxcore-0.1.29}/README.md +0 -0
  12. {cdxcore-0.1.28 → cdxcore-0.1.29}/cdxcore/config.py +0 -0
  13. {cdxcore-0.1.28 → cdxcore-0.1.29}/cdxcore/deferred.py +0 -0
  14. {cdxcore-0.1.28 → cdxcore-0.1.29}/cdxcore/dynalimits.py +0 -0
  15. {cdxcore-0.1.28 → cdxcore-0.1.29}/cdxcore/dynaplot.py +0 -0
  16. {cdxcore-0.1.28 → cdxcore-0.1.29}/cdxcore/err.py +0 -0
  17. {cdxcore-0.1.28 → cdxcore-0.1.29}/cdxcore/filelock.py +0 -0
  18. {cdxcore-0.1.28 → cdxcore-0.1.29}/cdxcore/jcpool.py +0 -0
  19. {cdxcore-0.1.28 → cdxcore-0.1.29}/cdxcore/npio.py +0 -0
  20. {cdxcore-0.1.28 → cdxcore-0.1.29}/cdxcore/npshm.py +0 -0
  21. {cdxcore-0.1.28 → cdxcore-0.1.29}/cdxcore/pretty.py +0 -0
  22. {cdxcore-0.1.28 → cdxcore-0.1.29}/cdxcore/util.py +0 -0
  23. {cdxcore-0.1.28 → cdxcore-0.1.29}/cdxcore/verbose.py +0 -0
  24. {cdxcore-0.1.28 → cdxcore-0.1.29}/cdxcore.egg-info/SOURCES.txt +0 -0
  25. {cdxcore-0.1.28 → cdxcore-0.1.29}/cdxcore.egg-info/dependency_links.txt +0 -0
  26. {cdxcore-0.1.28 → cdxcore-0.1.29}/cdxcore.egg-info/requires.txt +0 -0
  27. {cdxcore-0.1.28 → cdxcore-0.1.29}/cdxcore.egg-info/top_level.txt +0 -0
  28. {cdxcore-0.1.28 → cdxcore-0.1.29}/docs/source/conf.py +0 -0
  29. {cdxcore-0.1.28 → cdxcore-0.1.29}/setup.cfg +0 -0
  30. {cdxcore-0.1.28 → cdxcore-0.1.29}/tests/test_config.py +0 -0
  31. {cdxcore-0.1.28 → cdxcore-0.1.29}/tests/test_deferred.py +0 -0
  32. {cdxcore-0.1.28 → cdxcore-0.1.29}/tests/test_err.py +0 -0
  33. {cdxcore-0.1.28 → cdxcore-0.1.29}/tests/test_jcpool.py +0 -0
  34. {cdxcore-0.1.28 → cdxcore-0.1.29}/tests/test_npio.py +0 -0
  35. {cdxcore-0.1.28 → cdxcore-0.1.29}/tests/test_npshm.py +0 -0
  36. {cdxcore-0.1.28 → cdxcore-0.1.29}/tests/test_pretty.py +0 -0
  37. {cdxcore-0.1.28 → cdxcore-0.1.29}/tests/test_util.py +0 -0
  38. {cdxcore-0.1.28 → cdxcore-0.1.29}/tests/test_verbose.py +0 -0
  39. {cdxcore-0.1.28 → cdxcore-0.1.29}/tests/test_version.py +0 -0
  40. {cdxcore-0.1.28 → cdxcore-0.1.29}/tmp/filelock.py +0 -0
  41. {cdxcore-0.1.28 → cdxcore-0.1.29}/tmp/np.py +0 -0
  42. {cdxcore-0.1.28 → cdxcore-0.1.29}/tmp/npsh1.py +0 -0
  43. {cdxcore-0.1.28 → cdxcore-0.1.29}/tmp/sharedarray.py +0 -0
  44. {cdxcore-0.1.28 → cdxcore-0.1.29}/up/git_message.py +0 -0
  45. {cdxcore-0.1.28 → 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.28
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.28" # 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 = None , *,
2805
- dependencies : list|None = None,
2806
- label : Callable|None = None,
2807
- uid : Callable|None = None,
2808
- name : str|None = None,
2809
- exclude_args : list[str]|None = None,
2810
- include_args : list[str]|None = None,
2811
- exclude_arg_types : list[type]|None = 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
@@ -3223,7 +3232,7 @@ class SubDir(object):
3223
3232
  @cache.cache_class("0.1")
3224
3233
  class A(object):
3225
3234
 
3226
- @cache.cache_init(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 **_
3227
3236
  def __init__(self, x, debug):
3228
3237
  if debug:
3229
3238
  print("__init__",x)
@@ -3296,7 +3305,8 @@ class SubDir(object):
3296
3305
  See :dec:`cdxcore.version.version` for details on name lookup if strings are used.
3297
3306
 
3298
3307
  label : str | Callable | None, default ``None``
3299
- Specify a human-readabl label for the function call given its parameters.
3308
+ Specify a human-readable label for the function call given its parameters.
3309
+
3300
3310
  This label is used to generate the cache file name, and is also printed in when tracing
3301
3311
  hashing operations. Labels are not assumed to be unique, hence a unique hash of
3302
3312
  the label and the parameters to this function will be appended to generate
@@ -3307,14 +3317,22 @@ class SubDir(object):
3307
3317
 
3308
3318
  **Usage:**
3309
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
+
3310
3327
  * If ``label`` is a plain string without ``{}`` formatting: use this string as-is.
3311
3328
 
3312
- * If ``label`` is a string with ``{}`` formatting, then ``label.format( name=name, **parameters )``
3313
- 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.
3314
3335
 
3315
- * If ``label`` is a ``Callable`` then ``label( name=name, **parameters )`` will be called
3316
- to generate the actual label.
3317
-
3318
3336
  See above for examples.
3319
3337
 
3320
3338
  ``label`` cannot be used alongside ``uid``.
@@ -3329,7 +3347,7 @@ class SubDir(object):
3329
3347
 
3330
3348
  name : str | None, default ``None``
3331
3349
  Name of this function which is used either on its own if neither ``label`` not ``uid`` are used,
3332
- or which passed as a parameter ``name`` to either the callable or the
3350
+ or which passed as a parameter ``func_name`` to either the callable or the
3333
3351
  formatting operator. See above for more details.
3334
3352
 
3335
3353
  If ``name`` is not specified it defaults to ``__qualname__`` expanded
@@ -3350,6 +3368,34 @@ class SubDir(object):
3350
3368
  Whether to automaticallty add version dependencies on base classes or, for member functions, on containing
3351
3369
  classes. This is the ``auto_class`` parameter for :dec:`cdxcore.version.version`.
3352
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
+
3353
3399
  Returns
3354
3400
  -------
3355
3401
  Decorated F: Callable
@@ -3421,7 +3467,8 @@ class SubDir(object):
3421
3467
  exclude_args = exclude_args,
3422
3468
  include_args = include_args,
3423
3469
  exclude_arg_types = exclude_arg_types,
3424
- version_auto_class = version_auto_class )
3470
+ version_auto_class = version_auto_class,
3471
+ name_of_func_name_arg = name_of_func_name_arg)
3425
3472
 
3426
3473
  def cache_class( self,
3427
3474
  version : str = None , *,
@@ -3633,7 +3680,7 @@ def _ensure_has_version( F,
3633
3680
  dependencies=dependencies,
3634
3681
  auto_class=auto_class)(F)
3635
3682
 
3636
- def _qualified_name( F, name ):
3683
+ def _qualified_name( F, name = None ):
3637
3684
  """
3638
3685
  Return qualified name including module name, robustly
3639
3686
  """
@@ -3652,6 +3699,36 @@ def _qualified_name( F, name ):
3652
3699
  warn( f"Cannot determine module name for '{name}' of {type(F)}" )
3653
3700
  return name
3654
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
+
3655
3732
  class CacheCallable(object):
3656
3733
  """
3657
3734
  Wrapper for a cached function.
@@ -3660,17 +3737,17 @@ class CacheCallable(object):
3660
3737
  """
3661
3738
 
3662
3739
  def __init__(self,
3663
- subdir : SubDir, *,
3664
- version : str = None,
3665
- dependencies : list,
3666
- label : Callable = None,
3667
- uid : Callable = None,
3668
- name : str = None,
3669
- exclude_args : set[str] = None,
3670
- include_args : set[str] = None,
3671
- exclude_arg_types : set[type] = None,
3672
- version_auto_class : bool = True,
3673
- 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"):
3674
3751
  """
3675
3752
  Utility class for :dec:`cdxcore.subdir.SubDir.cache`.
3676
3753
 
@@ -3679,18 +3756,38 @@ class CacheCallable(object):
3679
3756
  if not label is None and not uid is None:
3680
3757
  error("Cannot specify both 'label' and 'uid'.")
3681
3758
 
3682
- self._subdir = SubDir(subdir)
3683
- self._version = str(version) if not version is None else None
3684
- self._dependencies = list(dependencies) if not dependencies is None else None
3685
- self._label = label
3686
- self._uid = uid
3687
- self._name = str(name) if not name is None else None
3688
- self._exclude_args = set(exclude_args) if not exclude_args is None and len(exclude_args) > 0 else None
3689
- self._include_args = set(include_args) if not include_args is None and len(include_args) > 0 else None
3690
- self._exclude_arg_types = set(exclude_arg_types) if not exclude_arg_types is None and len(exclude_arg_types) > 0 else None
3691
- self._version_auto_class = bool(version_auto_class)
3692
- 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
3693
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
+
3694
3791
  @property
3695
3792
  def uid_or_label(self) -> Callable:
3696
3793
  """ ID or label """
@@ -3723,6 +3820,10 @@ class CacheCallable(object):
3723
3820
  def global_exclude_arg_types(self) -> list[type]:
3724
3821
  """ Returns ``exclude_arg_types`` of the underlying :class:`cdxcore.subdir.CacheController` """
3725
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
3726
3827
 
3727
3828
  def __call__(self, F : Callable):
3728
3829
  """
@@ -3774,7 +3875,7 @@ class CacheCallable(object):
3774
3875
  # apply version
3775
3876
  # this also ensures that __init__ picks up a version dependency on the class itse
3776
3877
  # (as we forceed 'auto_class' to be true)
3777
- C = _ensure_has_version( C, version=self._version,
3878
+ C = _ensure_has_version( C, version=self._input_version,
3778
3879
  dependencies=self._dependencies,
3779
3880
  auto_class=self._version_auto_class)
3780
3881
 
@@ -3795,7 +3896,7 @@ class CacheCallable(object):
3795
3896
  # Cannot currently decorate classes.
3796
3897
 
3797
3898
 
3798
- is_method = inspect.ismethod(F)
3899
+ is_method = inspect.ismethod(F) # for *bound* methods
3799
3900
  if is_method:
3800
3901
  assert not getattr(F, "__self__", None) is None, ("Method type must have __self__...?", F.__qualname__ )
3801
3902
  elif not inspect.isfunction(F):
@@ -3858,11 +3959,12 @@ class CacheCallable(object):
3858
3959
  # -------
3859
3960
  # Decorate now or pick up existing @version
3860
3961
 
3861
- F = _ensure_has_version( F, version=self._version,
3962
+ F = _ensure_has_version( F, version=self._input_version,
3862
3963
  dependencies=self._dependencies,
3863
3964
  auto_class=self._version_auto_class,
3864
3965
  allow_default=is_new )
3865
-
3966
+ version = F.version.unique_id64
3967
+
3866
3968
  # name
3867
3969
  # ----
3868
3970
 
@@ -3892,38 +3994,46 @@ class CacheCallable(object):
3892
3994
 
3893
3995
  uid_or_label = self.uid_or_label
3894
3996
  filename = None
3895
- if isinstance(uid_or_label, str) and self.unique:
3896
- # if 'id' does not contain formatting codes,
3897
- # and the result is 'unique' then do not bother collecting
3898
- # function arguments
3899
- try:
3900
- filename = uid_or_label.format() # throws a KeyError if 'id' contains formatting information
3901
- except KeyError:
3902
- 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()
3903
4001
 
3904
- if not filename is None:
3905
- # generate name with the unique string provided by the user
3906
4002
  if not is_filename(filename):
3907
4003
  raise ValueError(f"The unique filename '{filename}' computed for '{name}' contains invalid characters for filename. When using `uid` make sure that "+\
3908
- "the returned ID is a valid filename (and unique)")
4004
+ "the returned ID is a valid filename (and is unique)")
3909
4005
  label = filename
3910
- filename = self.uniqueFileName( filename )
3911
4006
  arguments = None
3912
-
4007
+
3913
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
+
3914
4012
  # get dictionary of named arguments
3915
4013
  arguments = execute.cache_info.signature.bind(*args,**kwargs)
3916
4014
  arguments.apply_defaults()
3917
4015
  arguments = arguments.arguments # ordered dict
3918
4016
 
4017
+ # delete 'cls' from argument list for class functions
3919
4018
  if is_new:
3920
- # delete 'cls' from argument list
3921
4019
  assert len(arguments) >= 1, ("*** Internal error", F.__qualname__, is_new, arguments)
3922
4020
  del arguments[list(arguments)[0]]
3923
- 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
3924
4033
 
3925
4034
  # filter dictionary
3926
4035
  if not self._exclude_args is None or not self._include_args is None:
4036
+ argus = set(arguments)
3927
4037
  excl = set(self._exclude_args) if not self._exclude_args is None else set()
3928
4038
  if not self._exclude_args is None:
3929
4039
  if self._exclude_args > argus:
@@ -3937,7 +4047,7 @@ class CacheCallable(object):
3937
4047
  for arg in excl:
3938
4048
  if arg in arguments:
3939
4049
  del arguments[arg]
3940
- del excl
4050
+ del excl, argus
3941
4051
 
3942
4052
  if len(exclude_types) > 0:
3943
4053
  excl = []
@@ -3948,41 +4058,45 @@ class CacheCallable(object):
3948
4058
  if arg in arguments:
3949
4059
  del arguments[arg]
3950
4060
 
3951
- # did the user provide a label or unique ID?
3952
4061
  if uid_or_label is None:
4062
+ # no label or unique ID
4063
+ assert not self.unique
3953
4064
  uid_or_label = name
3954
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
+
3955
4071
  else:
3956
- if self._name_of_name_arg in arguments:
3957
- 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.")
3958
-
3959
- # add standard arguments
3960
- full_arguments = OrderedDict()
3961
- if is_method:
3962
- assert not 'self' in set(arguments), ("__self__ found in bound method argument list...?", F.__qualname__, execute.cache_info.signature.bind(*args,**kwargs).arguments )
3963
- full_arguments['self'] = F.__self__
3964
- full_arguments[self._name_of_name_arg] = name
3965
- for k,v in arguments.items():
3966
- full_arguments[k] = v
3967
- arguments = full_arguments
3968
- 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]
3969
4089
 
3970
4090
  # call format or function
3971
4091
  if isinstance( uid_or_label, str ):
3972
- try:
3973
- uid_or_label = str.format( uid_or_label, **arguments )
3974
- except KeyError as e:
3975
- raise KeyError(e, f"Error while generating id for '{name}' using format string '{uid_or_label}': {e}. Available arguments: {list(arguments)}")
3976
-
4092
+ uid_or_label = str.format( uid_or_label, **fmt_arguments )
3977
4093
  else:
3978
- which = 'uid' if not self._uid is None else 'label'
3979
4094
  try:
3980
- uid_or_label = uid_or_label(**arguments)
3981
- except TypeError as e:
3982
- 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)
3983
4096
  except Exception as e:
3984
- raise type(e)(f"Error while generating '{which}' for '{name}': attempt to call '{which}' of type {type(uid_or_label)} failed: {e}")
3985
- 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))}")
3986
4100
 
3987
4101
  if self.unique:
3988
4102
  if not is_filename(uid_or_label):
@@ -3998,7 +4112,6 @@ class CacheCallable(object):
3998
4112
  # determine version, cache mode
3999
4113
  # ------------------
4000
4114
 
4001
- version_ = self._version if not self._version is None else F.version.unique_id64
4002
4115
  cache_mode = CacheMode(override_cache_mode) if not override_cache_mode is None else self.cache_mode
4003
4116
  del override_cache_mode
4004
4117
 
@@ -4007,7 +4120,7 @@ class CacheCallable(object):
4007
4120
 
4008
4121
  execute.cache_info.label = str(label) if not label is None else None
4009
4122
  execute.cache_info.filename = filename # that is the unique ID for this call
4010
- execute.cache_info.version = version_
4123
+ execute.cache_info.version = version
4011
4124
 
4012
4125
  if self.cache_controller.keep_last_arguments:
4013
4126
  info_arguments = OrderedDict()
@@ -4026,11 +4139,11 @@ class CacheCallable(object):
4026
4139
  pass
4027
4140
  tag = Tag()
4028
4141
  if not is_new:
4029
- r = self._subdir.read( filename, tag, version=version_ )
4142
+ r = self._subdir.read( filename, tag, version=version )
4030
4143
  else:
4031
4144
  try:
4032
4145
  execute.__new_during_read = True
4033
- r = self._subdir.read( filename, tag, version=version_ )
4146
+ r = self._subdir.read( filename, tag, version=version )
4034
4147
  finally:
4035
4148
  execute.__new_during_read = False
4036
4149
 
@@ -4039,7 +4152,7 @@ class CacheCallable(object):
4039
4152
  track_cached_files += self._fullFileName(filename)
4040
4153
  execute.cache_info.last_cached = True
4041
4154
  if not debug_verbose is None:
4042
- 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)}'.")
4043
4156
  if is_new:
4044
4157
  assert r.__magic_cache_call_init__ is None, ("**** Internal error. __init__ should reset __magic_cache_call_init__", F.__qualname__, label)
4045
4158
  r.__magic_cache_call_init__ = False # since we called __new__, __init__ will be called next
@@ -4060,7 +4173,7 @@ class CacheCallable(object):
4060
4173
  assert r.__magic_cache_call_init__ is None, ("**** Internal error. __init__ should reset __magic_cache_call_init__")
4061
4174
 
4062
4175
  if cache_mode.write:
4063
- self._subdir.write(filename,r,version=version_)
4176
+ self._subdir.write(filename,r,version=version)
4064
4177
  if not track_cached_files is None:
4065
4178
  track_cached_files += self._subdir.full_file_name(filename)
4066
4179
  execute.cache_info.last_cached = False
@@ -4072,9 +4185,9 @@ class CacheCallable(object):
4072
4185
 
4073
4186
  if not debug_verbose is None:
4074
4187
  if cache_mode.write:
4075
- 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)}'.")
4076
4189
  else:
4077
- 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)}'.")
4078
4191
 
4079
4192
  if return_cache_uid:
4080
4193
  return filename, r
@@ -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.28
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.28"
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"
@@ -26,6 +26,7 @@ import_local()
26
26
  Imports
27
27
  """
28
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):
@@ -277,6 +278,15 @@ class A(object):
277
278
  class B(object):
278
279
  def __init__(self, x):
279
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
280
290
 
281
291
  class Test(unittest.TestCase):
282
292
 
@@ -311,7 +321,7 @@ class Test(unittest.TestCase):
311
321
  _ = f(A(2))
312
322
  self.assertTrue( f.cache_info.last_cached )
313
323
 
314
- @sub.cache("1.0", label=lambda x, **_: f"f({x})")
324
+ @sub.cache("1.0", label=lambda x: f"f({x})")
315
325
  def f(x):
316
326
  return x
317
327
 
@@ -320,7 +330,7 @@ class Test(unittest.TestCase):
320
330
  uid, _ = f(1, return_cache_uid=True)
321
331
  self.assertEqual( uid[:5], "f(1) " )
322
332
 
323
- @sub.cache("1.0", uid=lambda x, **_: f"f({x})")
333
+ @sub.cache("1.0", uid=lambda x: f"f({x})")
324
334
  def f(x):
325
335
  return x
326
336
 
@@ -329,6 +339,77 @@ class Test(unittest.TestCase):
329
339
  uid, _ = f(1, return_cache_uid=True)
330
340
  self.assertEqual( uid, "f(1)" )
331
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
+
332
413
 
333
414
  if __name__ == '__main__':
334
415
  unittest.main()
@@ -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