cdxcore 0.1.28__py3-none-any.whl → 0.1.30__py3-none-any.whl

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.

cdxcore/__init__.py CHANGED
@@ -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.30" # auto-updated by setup.py
cdxcore/subdir.py CHANGED
@@ -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
 
@@ -2971,12 +2980,9 @@ class SubDir(object):
2971
2980
  return x*y
2972
2981
 
2973
2982
  We can also use a function to generate a ``label``. In that case all parameters
2974
- to the function including its ``name`` are passed to the function. In below example
2975
- we eat any parameters we are not interested in with ``** _``:
2976
-
2977
- .. code-block:: python
2983
+ to the function including its ``func_name`` are passed to the function.::
2978
2984
 
2979
- @cache.cache("0.1", label=lambda x,y,**_: f"h({x},{y})", exclude_args='debug')
2985
+ @cache.cache("0.1", label=lambda x,y: f"h({x},{y})", exclude_args='debug')
2980
2986
  def h(x,y,debug=False):
2981
2987
  if debug:
2982
2988
  print(f"h(x={x},y={y})")
@@ -3010,7 +3016,7 @@ class SubDir(object):
3010
3016
 
3011
3017
  .. code-block:: python
3012
3018
 
3013
- @cache.cache("0.1", uid=lambda x,y,**_: f"h2({x},{y})", exclude_args='debug')
3019
+ @cache.cache("0.1", uid=lambda x,y: f"h2({x},{y})", exclude_args='debug')
3014
3020
  def h2(x,y,debug=False):
3015
3021
  if debug:
3016
3022
  print(f"h(x={x},y={y})")
@@ -3158,7 +3164,7 @@ class SubDir(object):
3158
3164
  return self.x*y
3159
3165
 
3160
3166
  a = A(x=1)
3161
- f = cache.cache("0.1", id=lambda self, y : f"a.f({y})")(a.f) # <- decorate bound 'f'.
3167
+ f = cache.cache("0.1", uid=lambda self, y : f"a.f({y})")(a.f) # <- decorate bound 'f'.
3162
3168
  r = c(y=2)
3163
3169
 
3164
3170
  In this case the function ``f`` is bound to ``a``. The object is added as ``self`` to the function
@@ -3223,7 +3229,7 @@ class SubDir(object):
3223
3229
  @cache.cache_class("0.1")
3224
3230
  class A(object):
3225
3231
 
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 **_
3232
+ @cache.cache_init(uid=lambda x, debug: f"A.__init__(x={x})") # <-- 'self' is not passed to the lambda function
3227
3233
  def __init__(self, x, debug):
3228
3234
  if debug:
3229
3235
  print("__init__",x)
@@ -3296,7 +3302,8 @@ class SubDir(object):
3296
3302
  See :dec:`cdxcore.version.version` for details on name lookup if strings are used.
3297
3303
 
3298
3304
  label : str | Callable | None, default ``None``
3299
- Specify a human-readabl label for the function call given its parameters.
3305
+ Specify a human-readable label for the function call given its parameters.
3306
+
3300
3307
  This label is used to generate the cache file name, and is also printed in when tracing
3301
3308
  hashing operations. Labels are not assumed to be unique, hence a unique hash of
3302
3309
  the label and the parameters to this function will be appended to generate
@@ -3307,14 +3314,22 @@ class SubDir(object):
3307
3314
 
3308
3315
  **Usage:**
3309
3316
 
3317
+ * If ``label`` is a ``Callable`` then ``label( func_name=name, **parameters )`` will be called
3318
+ to generate the actual label.
3319
+
3320
+ The parameter ``func_name`` refers to the qualified
3321
+ name of the function. Its value can be overwitten by ``name``, while the parameter name itself
3322
+ can be overwritten using ``name_of_func_name_arg``, see below.
3323
+
3310
3324
  * If ``label`` is a plain string without ``{}`` formatting: use this string as-is.
3311
3325
 
3312
- * If ``label`` is a string with ``{}`` formatting, then ``label.format( name=name, **parameters )``
3313
- will be used to generate the actual label.
3326
+ * If ``label`` is a string with ``{}`` formatting, then ``label.format( func_name=name, **parameters )``
3327
+ will be used to generate the actual label.
3328
+
3329
+ The parameter ``func_name`` refers to the qualified
3330
+ name of the function. Its value can be overwitten by ``name``, while the parameter name itself
3331
+ can be overwritten using ``name_of_func_name_arg``, see below.
3314
3332
 
3315
- * If ``label`` is a ``Callable`` then ``label( name=name, **parameters )`` will be called
3316
- to generate the actual label.
3317
-
3318
3333
  See above for examples.
3319
3334
 
3320
3335
  ``label`` cannot be used alongside ``uid``.
@@ -3329,7 +3344,7 @@ class SubDir(object):
3329
3344
 
3330
3345
  name : str | None, default ``None``
3331
3346
  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
3347
+ or which passed as a parameter ``func_name`` to either the callable or the
3333
3348
  formatting operator. See above for more details.
3334
3349
 
3335
3350
  If ``name`` is not specified it defaults to ``__qualname__`` expanded
@@ -3350,6 +3365,34 @@ class SubDir(object):
3350
3365
  Whether to automaticallty add version dependencies on base classes or, for member functions, on containing
3351
3366
  classes. This is the ``auto_class`` parameter for :dec:`cdxcore.version.version`.
3352
3367
 
3368
+ name_of_func_name_arg : str, default ``"func_name"``
3369
+ When formatting ``label`` or ``uid``, by default ``"func_name"`` is used to refer to the current
3370
+ function name. If there is already a parameter ``func_name`` for the function, an error will be raised.
3371
+ Use this flag to change the parameter name. Example::
3372
+
3373
+ .. code-block:: python
3374
+
3375
+ from cdxcore.subdir import SubDir
3376
+ cache = SubDir("?/temp")
3377
+
3378
+ @cache.cache("0.1")
3379
+ def f( func_name, x ):
3380
+ pass
3381
+
3382
+ f("test", 1)
3383
+
3384
+ Generates a :class:`RuntimeError` ``f@__main__: 'func_name' is a reserved keyword
3385
+ and used as formatting parameter
3386
+ 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.``.
3387
+
3388
+ Instead, use:
3389
+
3390
+ .. code-block:: python
3391
+
3392
+ @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")
3393
+ def f( func_name, x ):
3394
+ pass
3395
+
3353
3396
  Returns
3354
3397
  -------
3355
3398
  Decorated F: Callable
@@ -3421,7 +3464,8 @@ class SubDir(object):
3421
3464
  exclude_args = exclude_args,
3422
3465
  include_args = include_args,
3423
3466
  exclude_arg_types = exclude_arg_types,
3424
- version_auto_class = version_auto_class )
3467
+ version_auto_class = version_auto_class,
3468
+ name_of_func_name_arg = name_of_func_name_arg)
3425
3469
 
3426
3470
  def cache_class( self,
3427
3471
  version : str = None , *,
@@ -3633,7 +3677,7 @@ def _ensure_has_version( F,
3633
3677
  dependencies=dependencies,
3634
3678
  auto_class=auto_class)(F)
3635
3679
 
3636
- def _qualified_name( F, name ):
3680
+ def _qualified_name( F, name = None ):
3637
3681
  """
3638
3682
  Return qualified name including module name, robustly
3639
3683
  """
@@ -3652,6 +3696,36 @@ def _qualified_name( F, name ):
3652
3696
  warn( f"Cannot determine module name for '{name}' of {type(F)}" )
3653
3697
  return name
3654
3698
 
3699
+ def _expected_str_fmt_args(fmt: str):
3700
+ """
3701
+ Inspect a format string and report what arguments it expects.
3702
+ Returns:
3703
+ - auto_positional: count of automatic '{}' fields
3704
+ - positional_indices: explicit numeric field indices used (e.g., {0}, {2})
3705
+ - keywords: named fields used (e.g., {user}, {price})
3706
+ """
3707
+ f = Formatter()
3708
+ pos = set()
3709
+ auto = 0
3710
+ kws = set()
3711
+
3712
+ for literal, field, spec, conv in f.parse(fmt):
3713
+ if field is None:
3714
+ continue
3715
+ # Keep only the first identifier before attribute/index access
3716
+ head = field.split('.')[0].split('[')[0]
3717
+ if head == "": # '{}' → automatic positional
3718
+ auto += 1
3719
+ elif head.isdigit(): # '{0}', '{2}' → explicit positional
3720
+ pos.add(int(head))
3721
+ else: # '{name}' → keyword
3722
+ kws.add(head)
3723
+
3724
+ return PrettyObject( positional=auto,
3725
+ posindices=pos,
3726
+ keywords=kws
3727
+ )
3728
+
3655
3729
  class CacheCallable(object):
3656
3730
  """
3657
3731
  Wrapper for a cached function.
@@ -3660,17 +3734,17 @@ class CacheCallable(object):
3660
3734
  """
3661
3735
 
3662
3736
  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"):
3737
+ subdir : SubDir, *,
3738
+ version : str = None,
3739
+ dependencies : list,
3740
+ label : Callable = None,
3741
+ uid : Callable = None,
3742
+ name : str = None,
3743
+ exclude_args : set[str] = None,
3744
+ include_args : set[str] = None,
3745
+ exclude_arg_types : set[type] = None,
3746
+ version_auto_class : bool = True,
3747
+ name_of_func_name_arg: str = "name"):
3674
3748
  """
3675
3749
  Utility class for :dec:`cdxcore.subdir.SubDir.cache`.
3676
3750
 
@@ -3679,18 +3753,38 @@ class CacheCallable(object):
3679
3753
  if not label is None and not uid is None:
3680
3754
  error("Cannot specify both 'label' and 'uid'.")
3681
3755
 
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)
3756
+ self._subdir = SubDir(subdir)
3757
+ self._input_version = str(version) if not version is None else None
3758
+ self._dependencies = list(dependencies) if not dependencies is None else None
3759
+ self._label = label
3760
+ self._uid = uid
3761
+ self._name = str(name) if not name is None else None
3762
+ self._exclude_args = set(exclude_args) if not exclude_args is None and len(exclude_args) > 0 else None
3763
+ self._include_args = set(include_args) if not include_args is None and len(include_args) > 0 else None
3764
+ self._exclude_arg_types = set(exclude_arg_types) if not exclude_arg_types is None and len(exclude_arg_types) > 0 else None
3765
+ self._version_auto_class = bool(version_auto_class)
3766
+ self._name_of_func_name_arg = str(name_of_func_name_arg)
3767
+ self._uid_label_params = None
3693
3768
 
3769
+ if not self.uid_or_label is None:
3770
+ F = self.uid_or_label
3771
+ which = "'uid'" if not uid is None else "'label'"
3772
+ if isinstance( F, str ):
3773
+ r = _expected_str_fmt_args( F )
3774
+ if r.positional + len(r.posindices) > 0:
3775
+ raise ValueError("f{which} '{F}' cannot have positional arguments (empty brackets {} or brackets with integer position {1}). Use only named arguments.")
3776
+ self._uid_label_params = list(r.keywords)
3777
+ del r
3778
+ else:
3779
+ if not inspect.isfunction(F):
3780
+ if not callable(F):
3781
+ raise ValueError(f"{which} '{_qualified_name(F)}' is not callable")
3782
+ F = F.__call__
3783
+ assert inspect.isfunction(F), ("Internal error - function expected")
3784
+ self._uid_label_params = list( inspect.signature(F).parameters )
3785
+ del F, which
3786
+ self._uid_label_params = self._uid_label_params if len(self._uid_label_params) > 0 else None
3787
+
3694
3788
  @property
3695
3789
  def uid_or_label(self) -> Callable:
3696
3790
  """ ID or label """
@@ -3723,6 +3817,10 @@ class CacheCallable(object):
3723
3817
  def global_exclude_arg_types(self) -> list[type]:
3724
3818
  """ Returns ``exclude_arg_types`` of the underlying :class:`cdxcore.subdir.CacheController` """
3725
3819
  return self.cache_controller.exclude_arg_types
3820
+ @property
3821
+ def uid_label_params(self) -> list:
3822
+ """ Returns the ``set`` of parameters the ``uid`` or ``label`` function expects """
3823
+ return self._uid_label_params
3726
3824
 
3727
3825
  def __call__(self, F : Callable):
3728
3826
  """
@@ -3774,7 +3872,7 @@ class CacheCallable(object):
3774
3872
  # apply version
3775
3873
  # this also ensures that __init__ picks up a version dependency on the class itse
3776
3874
  # (as we forceed 'auto_class' to be true)
3777
- C = _ensure_has_version( C, version=self._version,
3875
+ C = _ensure_has_version( C, version=self._input_version,
3778
3876
  dependencies=self._dependencies,
3779
3877
  auto_class=self._version_auto_class)
3780
3878
 
@@ -3795,7 +3893,7 @@ class CacheCallable(object):
3795
3893
  # Cannot currently decorate classes.
3796
3894
 
3797
3895
 
3798
- is_method = inspect.ismethod(F)
3896
+ is_method = inspect.ismethod(F) # for *bound* methods
3799
3897
  if is_method:
3800
3898
  assert not getattr(F, "__self__", None) is None, ("Method type must have __self__...?", F.__qualname__ )
3801
3899
  elif not inspect.isfunction(F):
@@ -3858,11 +3956,12 @@ class CacheCallable(object):
3858
3956
  # -------
3859
3957
  # Decorate now or pick up existing @version
3860
3958
 
3861
- F = _ensure_has_version( F, version=self._version,
3959
+ F = _ensure_has_version( F, version=self._input_version,
3862
3960
  dependencies=self._dependencies,
3863
3961
  auto_class=self._version_auto_class,
3864
3962
  allow_default=is_new )
3865
-
3963
+ version = F.version.unique_id64
3964
+
3866
3965
  # name
3867
3966
  # ----
3868
3967
 
@@ -3892,38 +3991,46 @@ class CacheCallable(object):
3892
3991
 
3893
3992
  uid_or_label = self.uid_or_label
3894
3993
  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
3994
+ if self.unique and self._uid_label_params is None:
3995
+ # the string or function do not require any parameters, and is unique
3996
+ assert not uid_or_label is None
3997
+ filename = uid_or_label if isinstance( uid_or_label, str ) else uid_or_label()
3903
3998
 
3904
- if not filename is None:
3905
- # generate name with the unique string provided by the user
3906
3999
  if not is_filename(filename):
3907
4000
  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)")
4001
+ "the returned ID is a valid filename (and is unique)")
3909
4002
  label = filename
3910
- filename = self.uniqueFileName( filename )
3911
4003
  arguments = None
3912
-
4004
+
3913
4005
  else:
4006
+ # need the list of parameters to compute a hash and/or a label
4007
+ which = 'uid' if not self._uid is None else 'label'
4008
+
3914
4009
  # get dictionary of named arguments
3915
4010
  arguments = execute.cache_info.signature.bind(*args,**kwargs)
3916
4011
  arguments.apply_defaults()
3917
4012
  arguments = arguments.arguments # ordered dict
3918
4013
 
4014
+ # delete 'cls' from argument list for class functions
3919
4015
  if is_new:
3920
- # delete 'cls' from argument list
3921
4016
  assert len(arguments) >= 1, ("*** Internal error", F.__qualname__, is_new, arguments)
3922
4017
  del arguments[list(arguments)[0]]
3923
- argus = set(arguments)
4018
+
4019
+ # add 'self' for methods
4020
+ if is_method:
4021
+ # add __self__ to the beginning of all arguments
4022
+ full_arguments = OrderedDict()
4023
+ if is_method:
4024
+ if 'self' in set(arguments):
4025
+ raise RuntimeError(f"'self' found in bound method '{name}' argument list {fmt_dict(execute.cache_info.signature.bind(*args,**kwargs).arguments)}.")
4026
+ full_arguments['self'] = F.__self__
4027
+ full_arguments |= arguments
4028
+ arguments = full_arguments
4029
+ del full_arguments
3924
4030
 
3925
4031
  # filter dictionary
3926
4032
  if not self._exclude_args is None or not self._include_args is None:
4033
+ argus = set(arguments)
3927
4034
  excl = set(self._exclude_args) if not self._exclude_args is None else set()
3928
4035
  if not self._exclude_args is None:
3929
4036
  if self._exclude_args > argus:
@@ -3937,7 +4044,7 @@ class CacheCallable(object):
3937
4044
  for arg in excl:
3938
4045
  if arg in arguments:
3939
4046
  del arguments[arg]
3940
- del excl
4047
+ del excl, argus
3941
4048
 
3942
4049
  if len(exclude_types) > 0:
3943
4050
  excl = []
@@ -3948,41 +4055,45 @@ class CacheCallable(object):
3948
4055
  if arg in arguments:
3949
4056
  del arguments[arg]
3950
4057
 
3951
- # did the user provide a label or unique ID?
3952
4058
  if uid_or_label is None:
4059
+ # no label or unique ID
4060
+ assert not self.unique
3953
4061
  uid_or_label = name
3954
4062
 
4063
+ elif self._uid_label_params is None:
4064
+ # label function or string does not need any parameters
4065
+ assert not self.unique
4066
+ uid_or_label = uid_or_label if isinstance( uid_or_label, str ) else uid_or_label()
4067
+
3955
4068
  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
4069
+ # function or format string required parameters
4070
+ # add parameters in order of label/uid parameters
4071
+ assert not self._uid_label_params is None
4072
+
4073
+ fmt_arguments = {}
4074
+ for k in self._uid_label_params:
4075
+ if k == self._name_of_func_name_arg:
4076
+ if self._name_of_func_name_arg in arguments:
4077
+ error(f"{name}: '{self._name_of_func_name_arg}' is a reserved keyword for '{which}' which refers to the current function name. "
4078
+ "Found it also in the function parameter list. Use 'name_of_func_name_arg' to change the internal parameter name used.")
4079
+ fmt_arguments[k] = name
4080
+ else:
4081
+ if not k in arguments:
4082
+ args_ = [ f"'{_}'" for _ in arguments ]
4083
+ raise ValueError(f"Error while generating '{which}' for '{name}': formatting function expected a parameter '{k}' which is not present "+\
4084
+ f"in the list of parameters passed to '{name}': {fmt_list(args_)}.")
4085
+ fmt_arguments[k] = arguments[k]
3969
4086
 
3970
4087
  # call format or function
3971
4088
  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
-
4089
+ uid_or_label = str.format( uid_or_label, **fmt_arguments )
3977
4090
  else:
3978
- which = 'uid' if not self._uid is None else 'label'
3979
4091
  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)}")
4092
+ uid_or_label = uid_or_label(**fmt_arguments)
3983
4093
  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))
4094
+ raise type(e)(f"Error while generating '{which}' for '{name}': attempt to call '{which}' of callable type {type(uid_or_label)} failed: {e}")
4095
+ if not isinstance(uid_or_label, str):
4096
+ raise ValueError("Error calling callable '{which}' for '{name}': callable must return a string. Found {type(uid_or_label))}")
3986
4097
 
3987
4098
  if self.unique:
3988
4099
  if not is_filename(uid_or_label):
@@ -3998,7 +4109,6 @@ class CacheCallable(object):
3998
4109
  # determine version, cache mode
3999
4110
  # ------------------
4000
4111
 
4001
- version_ = self._version if not self._version is None else F.version.unique_id64
4002
4112
  cache_mode = CacheMode(override_cache_mode) if not override_cache_mode is None else self.cache_mode
4003
4113
  del override_cache_mode
4004
4114
 
@@ -4007,7 +4117,7 @@ class CacheCallable(object):
4007
4117
 
4008
4118
  execute.cache_info.label = str(label) if not label is None else None
4009
4119
  execute.cache_info.filename = filename # that is the unique ID for this call
4010
- execute.cache_info.version = version_
4120
+ execute.cache_info.version = version
4011
4121
 
4012
4122
  if self.cache_controller.keep_last_arguments:
4013
4123
  info_arguments = OrderedDict()
@@ -4026,11 +4136,11 @@ class CacheCallable(object):
4026
4136
  pass
4027
4137
  tag = Tag()
4028
4138
  if not is_new:
4029
- r = self._subdir.read( filename, tag, version=version_ )
4139
+ r = self._subdir.read( filename, tag, version=version )
4030
4140
  else:
4031
4141
  try:
4032
4142
  execute.__new_during_read = True
4033
- r = self._subdir.read( filename, tag, version=version_ )
4143
+ r = self._subdir.read( filename, tag, version=version )
4034
4144
  finally:
4035
4145
  execute.__new_during_read = False
4036
4146
 
@@ -4039,7 +4149,7 @@ class CacheCallable(object):
4039
4149
  track_cached_files += self._fullFileName(filename)
4040
4150
  execute.cache_info.last_cached = True
4041
4151
  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)}'.")
4152
+ debug_verbose.write(f"cache({name}): read '{label}' version 'version {version}' from cache '{self._subdir.full_file_name(filename)}'.")
4043
4153
  if is_new:
4044
4154
  assert r.__magic_cache_call_init__ is None, ("**** Internal error. __init__ should reset __magic_cache_call_init__", F.__qualname__, label)
4045
4155
  r.__magic_cache_call_init__ = False # since we called __new__, __init__ will be called next
@@ -4060,7 +4170,7 @@ class CacheCallable(object):
4060
4170
  assert r.__magic_cache_call_init__ is None, ("**** Internal error. __init__ should reset __magic_cache_call_init__")
4061
4171
 
4062
4172
  if cache_mode.write:
4063
- self._subdir.write(filename,r,version=version_)
4173
+ self._subdir.write(filename,r,version=version)
4064
4174
  if not track_cached_files is None:
4065
4175
  track_cached_files += self._subdir.full_file_name(filename)
4066
4176
  execute.cache_info.last_cached = False
@@ -4072,9 +4182,9 @@ class CacheCallable(object):
4072
4182
 
4073
4183
  if not debug_verbose is None:
4074
4184
  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)}'.")
4185
+ debug_verbose.write(f"cache({name}): called '{label}' version 'version {version}' and wrote result into '{self._subdir.full_file_name(filename)}'.")
4076
4186
  else:
4077
- debug_verbose.write(f"cache({name}): called '{label}' version 'version {version_}' but did *not* write into '{self._subdir.full_file_name(filename)}'.")
4187
+ debug_verbose.write(f"cache({name}): called '{label}' version 'version {version}' but did *not* write into '{self._subdir.full_file_name(filename)}'.")
4078
4188
 
4079
4189
  if return_cache_uid:
4080
4190
  return filename, r
cdxcore/uniquehash.py CHANGED
@@ -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
cdxcore/version.py CHANGED
@@ -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.30
4
4
  Summary: Basic Python Tools; upgraded cdxbasics
5
5
  Author-email: Hans Buehler <github@buehler.london>
6
6
  License-Expression: MIT
@@ -1,4 +1,4 @@
1
- cdxcore/__init__.py,sha256=eAlbvVVYLS8fmoJ4F2yMdJWFO8woTjXMND4ABmbz1sw,127
1
+ cdxcore/__init__.py,sha256=jkKhp8G5M366IE47KjCnP6QalcxvJvXQxnLJjQCym8I,127
2
2
  cdxcore/config.py,sha256=RaxpAnTEyGXLiq-1O2prXFBb2sdyqB6hHDHPAFGe2k0,98472
3
3
  cdxcore/deferred.py,sha256=41buUit1SOAnhVSrzlzM5hd2OOhnAS2QRDSWEpjXfdE,43138
4
4
  cdxcore/dynalimits.py,sha256=TXEwa4wCdeM5epG1ceezaHwvmhnbU-oXlXpHNbOMQR8,11293
@@ -9,12 +9,12 @@ cdxcore/jcpool.py,sha256=Vw8o8S4_qTVQNIr2sRaBR_kHz_wO0zpYs0QjlKSYFz8,27280
9
9
  cdxcore/npio.py,sha256=SVpKkFt6lyRogkns-oky0wEiWgVTalgB0OdaSvyWtlU,24212
10
10
  cdxcore/npshm.py,sha256=9buYPNJoNlw69NZQ-nLF13PEWBWNx51a0gjQw5Gc24U,18368
11
11
  cdxcore/pretty.py,sha256=FsI62rlaqRX1E-uPCSnu0M4UoCQ5Z55TDvYPnzTNO70,17220
12
- cdxcore/subdir.py,sha256=S-ir_1pUQxQI8B3JkflLzvA3P0_XkBU-Qqkm3S9Fi_M,184881
13
- cdxcore/uniquehash.py,sha256=WCtieiRBavfpSVT-hW4QNYGHFrfgXG3kJuTUtAi9_b0,50729
12
+ cdxcore/subdir.py,sha256=c4QX1szgt-ZORU1DZyvVXPzNr9sphmi08gglsGDmN3Y,190192
13
+ cdxcore/uniquehash.py,sha256=j2pDmDPdDX4mZ8YIpZLbg-qWXQJPuR7OvZSTGnvxwL0,50827
14
14
  cdxcore/util.py,sha256=dqCEhrDvUM4qjeUwb6n8jPfS8WC4Navcj83gDjXfRFE,39349
15
15
  cdxcore/verbose.py,sha256=vsjGTVnAHMPg2L2RfsowWKKPjUSnQJ3F653vDTydBkI,30223
16
- cdxcore/version.py,sha256=S3H0ktEZsM0zAj3EQ5AdrZ2KxWhQtemd8clzEFE9hOQ,27160
17
- cdxcore-0.1.28.dist-info/licenses/LICENSE,sha256=M-cisgK9kb1bqVRJ7vrCxHcMQQfDxdY3c2YFJJWfNQg,1090
16
+ cdxcore/version.py,sha256=pmbFIZ6Egif_ppZNFJRqEZO0HBffIzkn-hkY6HpkMpU,27325
17
+ cdxcore-0.1.30.dist-info/licenses/LICENSE,sha256=M-cisgK9kb1bqVRJ7vrCxHcMQQfDxdY3c2YFJJWfNQg,1090
18
18
  docs/source/conf.py,sha256=yn3LYgw3sT45mUyll-B2emVp6jg7H6KfAHOcBg_MNv4,4182
19
19
  tests/test_config.py,sha256=N86mH3y7k3LXEmU8uPLfrmRMZ-80VhlD35nBbpLmebg,15617
20
20
  tests/test_deferred.py,sha256=4Xsb76r-XqHKiBuHa4jbErjMWbrgHXfPwewzzY4lf9Y,7922
@@ -23,8 +23,8 @@ tests/test_jcpool.py,sha256=bcGC3UcJ7SOHVgzZ-cooEJgncLWmhutBfqH7P5qB-iw,7901
23
23
  tests/test_npio.py,sha256=v-_oO7bj9obSJD4TBk4gBM8ACL62DMNjyQ-iZmvOOdw,3039
24
24
  tests/test_npshm.py,sha256=uVivQ3zI4_v3a4qDIY-1gjf5_2pEtKM00VlC3hubUl8,3270
25
25
  tests/test_pretty.py,sha256=pVwTBjm3XqwEf2jq5GdZvT4cDSTGqiQFBMLqmGJYuB0,11644
26
- tests/test_subdir.py,sha256=LGofLSthZMmZBOAq_XwYOOS2nmhdGgrNJuHN3Sl8ZFM,13026
27
- tests/test_uniquehash.py,sha256=n6ZCkdBw-iRsyzeAEmrnLK0hJLGH6l_Dtt_KIkSa6KA,24630
26
+ tests/test_subdir.py,sha256=vNRkPTiQZUubyu-vDfaERi1dLewxRMiAxgTEYS6TJZY,15825
27
+ tests/test_uniquehash.py,sha256=sRYhdVezNViEnpf8f0GjWfJ-fgghjEFnJCM0sUBVshM,24985
28
28
  tests/test_util.py,sha256=mwtz3o_RiLNn808928xP4jawHmGNxzUXiatMh0zBc3o,24342
29
29
  tests/test_verbose.py,sha256=zXheIqAVOnwML2zsCjLugjYzB_KNzU_S4Xu2CSb4o10,4723
30
30
  tests/test_version.py,sha256=m_RODPDFlXTC1jAIczm3t1v6tXzqczDiUFFJtGvRG98,4381
@@ -34,7 +34,7 @@ tmp/npsh1.py,sha256=mNucUl2-jNmE84GlMlliB4aJ0UQ9FqdymgcY_9mLeZY,15432
34
34
  tmp/sharedarray.py,sha256=dNOT1ObCc3nM3qA3OA508NcENIBnkmWMxRPCqvMVa8A,12862
35
35
  up/git_message.py,sha256=EfSH7Pit3ZoCiRqSMwRCUN_QyuwreU4LTIyGSutBlm4,123
36
36
  up/pip_modify_setup.py,sha256=Esaml4yA9tFsqxLhk5bWSwvKCURONjQqfyChgFV2TSY,1584
37
- cdxcore-0.1.28.dist-info/METADATA,sha256=pQce48t_g2AM8yerQ7Ltd7ddv4DbPszOXa25upO2AOs,5939
38
- cdxcore-0.1.28.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
39
- cdxcore-0.1.28.dist-info/top_level.txt,sha256=phNSwCyJFe7UP2YMoi8o6ykhotatlIbJHjTp9EHM51k,26
40
- cdxcore-0.1.28.dist-info/RECORD,,
37
+ cdxcore-0.1.30.dist-info/METADATA,sha256=D3Rm5o1JhCnDZQmoSXOxNbA08mtO0Jf-Vh9nfvroNZM,5939
38
+ cdxcore-0.1.30.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
39
+ cdxcore-0.1.30.dist-info/top_level.txt,sha256=phNSwCyJFe7UP2YMoi8o6ykhotatlIbJHjTp9EHM51k,26
40
+ cdxcore-0.1.30.dist-info/RECORD,,
tests/test_subdir.py CHANGED
@@ -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()
tests/test_uniquehash.py CHANGED
@@ -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