cdxcore 0.1.5__py3-none-any.whl → 0.1.9__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/version.py CHANGED
@@ -1,33 +1,211 @@
1
1
  """
2
- Version handling for functions and classes including their dependencies via decorators.
3
- """
2
+ Overview
3
+ --------
4
+
5
+ This module provides a framework to track versions of functions, classes, and their members via a simple decorating mechanism
6
+ implemented with :dec:`cdxcore.verson.version`.
7
+
8
+ A main application is the use in caching results of computational intensive tasks such as data pipelines in machine learning. The version
9
+ framework allows updating dynamically only those parts of the data dependency graph whose code generation logic has changed.
10
+ Correspondingly, :class:`cdxcore.subdir.SubDir` supports fast light-weight file based read/write support for versioned files.
11
+
12
+ A full caching logic is implemented using :dec:`cdxcore.subdir.SubDir.cache`.
13
+
14
+ Versioned Functions
15
+ ^^^^^^^^^^^^^^^^^^^
16
+
17
+ For versioning, basic use is straight forward and self-explanatory::
18
+
19
+ from cdxbasics.version import version
20
+
21
+ @version("0.0.1")
22
+ def f(x):
23
+ return x
24
+
25
+ print( f.version.full ) # -> 0.0.1
26
+
27
+ Dependencies are declared with the ``dependencies`` keyword::
28
+
29
+ @version("0.0.2", dependencies=[f])
30
+ def g(x):
31
+ return f(x)
32
+
33
+ print( g.version.input ) # -> 0.0.2
34
+ print( g.version.full ) # -> 0.0.2 { f: 0.0.01 }
35
+
36
+ You have access to ``version`` from within the function::
37
+
38
+ @version("0.0.2", dependencies=[f])
39
+ def g(x):
40
+ print(g.version.full) # -> 0.0.2 { f: 0.0.01 }
41
+ return f(x)
42
+ g(1)
43
+
44
+ You can also use strings to refer to dependencies.
45
+ This functionality depends on visibility of the referred dependencies by the function in the
46
+ function's ``__global__`` scope::
47
+
48
+ @version("0.0.4", dependencies=['f'])
49
+ def r(x):
50
+ return x
51
+
52
+ print( r.version.full ) # -> 0.0.4 { f: 0.0.01 }
53
+
54
+ Versioned Classes
55
+ ^^^^^^^^^^^^^^^^^
56
+
57
+ This works with classes, too::
58
+
59
+ @version("0.0.3", dependencies=[f] )
60
+ class A(object):
61
+ def h(self, x):
62
+ return f(x)
63
+
64
+ print( A.version.input ) # -> 0.0.3
65
+ print( A.version.full ) # -> 0.0.3 { f: 0.0.01 }
66
+
67
+ a = A()
68
+ print( a.version.input ) # -> 0.0.3
69
+ print( a.version.full ) # -> 0.0.3 { f: 0.0.01 }
70
+
71
+ Dependencies on base classes are automatic::
72
+
73
+ @version("0.0.1")
74
+ class A(object):
75
+ pass
76
+
77
+ @version("0.0.2")
78
+ class B(A):
79
+ pass
4
80
 
5
- from .util import fmt_list, uniqueLabelExt
81
+ print( B.version.full ) # -> 0.0.2 { A: 0.0.1 }
82
+
83
+ Member functions are automatically dependent on their defining class::
84
+
85
+ from cdxcore.version import version
86
+
87
+ class A(object):
88
+ def __init__(self, x=2):
89
+ self.x = x
90
+ @version(version="0.4.1")
91
+ def h(self, y):
92
+ return self.x*y
93
+
94
+ @version(version="0.3.0")
95
+ def h(x,y):
96
+ return x+y
97
+
98
+ @version(version="0.0.2", dependencies=[h])
99
+ def f(x,y):
100
+ return h(y,x)
101
+
102
+ @version(version="0.0.1", dependencies=["f", A.h])
103
+ def g(x,z):
104
+ a = A()
105
+ return f(x*2,z)+a.h(z)
106
+
107
+ g(1,2)
108
+ print("version", g.version.input) # -> version 0.0.1
109
+ print("full version", g.version.full ) # -> full version 0.0.1 { f: 0.0.2 { h: 0.3.0 }, A.h: 0.4.1 }
110
+ print("full version ID",g.version.unique_id48 ) # -> full version ID 0.0.1 { f: 0.0.2 { h: 0.3.0 }, A.h: 0.4.1 }
111
+ print("full version ID",g.version.unique_id32 ) # -> full version ID 0.0.1 { f: 0.0.2 { h: 0.3.0 }, A.h: 0.4.1 }
112
+ print("depedencies",g.version.dependencies ) # -> depedencies ('0.0.1', {'f': ('0.0.2', {'h': '0.3.0'}), 'A.h': '0.4.1'})
113
+
114
+ Decorated Function Information
115
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
116
+
117
+ A decorated function or class has a member ``version`` of type :class:`cdxcore.version.Version` which has the following
118
+ key properties:
119
+
120
+ * :attr:`cdxcore.version.Version.input`: the input version as defined with :dec:`cdxcore.version.version.
121
+ * :attr:`cdxcore.version.Version.full`: a fully qualified version with all dependent functions and classes in human readable form.
122
+ * :attr:`cdxcore.version.Version.unique_id48`,
123
+ :attr:`cdxcore.version.Version.unique_id64`:
124
+ unique hashes of :attr:`cdxcore.version.Version.full` of 48 or 64 characters,
125
+ respectively. You can use the function :meth:`cdxcore.version.Version.unique_id
126
+ to compute hash IDs of any length.
127
+ * :attr:`cdxcore.version.Version.dependencies`: a hierarchical list of dependencies for systematic inspection.
128
+
129
+ Import
130
+ ------
131
+ .. code-block:: python
132
+
133
+ from cdxcore.version import version
134
+
135
+ Documentation
136
+ -------------
137
+ """
6
138
  import inspect as inspect
139
+ from .util import fmt_list
140
+ from .uniquehash import UniqueLabel
141
+ from collections.abc import Callable
7
142
 
8
- uniqueLabel64 = uniqueLabelExt(max_length=64,id_length=8)
9
- uniqueLabel60 = uniqueLabelExt(max_length=60,id_length=8)
10
- uniqueLabel48 = uniqueLabelExt(max_length=48,id_length=8)
143
+ uniqueLabel64 = UniqueLabel(max_length=64,id_length=8)
144
+ uniqueLabel60 = UniqueLabel(max_length=60,id_length=8)
145
+ uniqueLabel48 = UniqueLabel(max_length=48,id_length=8)
11
146
 
12
- class VersionError(RuntimeError):
147
+ class VersionDefinitionError(RuntimeError):
148
+ """
149
+ Error rasied if an error occured during version definition.
150
+ """
13
151
  def __init__(self, context, message):
14
152
  RuntimeError.__init__(self, context, message)
15
153
 
154
+ class VersionError(RuntimeError):
155
+ """
156
+ Standardized error type to be raised by applications if a version found did not match an expected
157
+ version.
158
+ """
159
+ def __init__(self, *args, version_found : str, version_expected : str ):
160
+ self.version_found = version_found #: The version found.
161
+ self.version_expected = version_expected #: The version expected.
162
+ RuntimeError.__init__(self, *args)
163
+
16
164
  class Version(object):
17
165
  """
18
- Class to track version dependencies for a given function or class 'f'
19
- Use @version decorator instead.
166
+ Class to track version dependencies for a given function or class.
167
+
168
+ This class is used by :dec:`cdxcore.subdir.version`. Developers will typically access
169
+ it via a decorated function's ``version`` property.
20
170
 
21
- Decorared functions and class have a 'version' member of this type, which has the following properties:
171
+ **Key Properties**
172
+
173
+ * :attr:`cdxcore.version.Version.input`: input version string as provided by the user.
174
+
175
+ * :attr:`cdxcore.version.Version.full`: qualified full version including versions of dependent
176
+ functions or classes, as a string.
22
177
 
23
- input : input version
24
- full : qualified full version including versions of dependent functions or classes
25
- unique_id64 : 64 character unique ID
26
- dependencies: hierarchy of versions
178
+ * :attr:`cdxcore.version.Version.unique_id48`:
179
+ 48 character unique ID. Versions for 60 and 64 characters are also
180
+ pre-defined.
181
+
182
+ * :attr:`cdxcore.version.Version.dependencies`: hierarchy of version dependencies as a list.
183
+
184
+ **Dependency Resolution**
185
+
186
+ Dependency resolution is lazy to allow creating dependencies on Python elements which are defined later / elsewhere.
187
+ If an error occurs during dependency resoution an exception of type :class:`cdxcore.version.VersionDefinitionError` is raised.
188
+
189
+ Parameters
190
+ ----------
191
+ original : Callable
192
+ Orignal Python element: a function, class, or member function.
193
+
194
+ version : str, optional
195
+ User version string for ``original``.
196
+
197
+ dependencies : list[type], optional
198
+ List of dependencies as types (preferably) or string names.
199
+
200
+ auto_class : bool, optional
201
+ Whether to automatically make classes dependent on their (versioned) base classes,
202
+ and member functions on their (versioned) containing classes.
27
203
  """
28
204
 
29
- def __init__(self, original, version : str, dependencies : list[type], auto_class : bool ):
30
- """ Wrapper around a versioned function 'f' """
205
+ def __init__(self, original : Callable, version : str, dependencies : list[type], auto_class : bool ):
206
+ """
207
+ :meta private:
208
+ """
31
209
  if version is None:
32
210
  raise ValueError("'version' cannot be None")
33
211
  self._original = original
@@ -38,32 +216,34 @@ class Version(object):
38
216
  self._auto_class = auto_class
39
217
 
40
218
  def __str__(self) -> str:
41
- """ Returns qualified version """
219
+ """ Returns qualified version string. """
42
220
  return self.full
43
221
 
44
222
  def __repr__(self) -> str:
45
- """ Returns qualified version """
223
+ """ Returns qualified version string. """
46
224
  return self.full
47
225
 
48
226
  def __eq__(self, other) -> bool:
49
- """ Tests equality of two versions, or a string """
227
+ """ Tests equality of two versions """
50
228
  other = other.full if isinstance(other, Version) else str(other)
51
229
  return self.full == other
52
230
 
53
231
  def __neq__(self, other) -> bool:
54
- """ Tests inequality of two versions, or a string """
232
+ """ Tests inequality of two versions """
55
233
  other = other.full if isinstance(other, Version) else str(other)
56
234
  return self.full != other
57
235
 
58
236
  @property
59
237
  def input(self) -> str:
60
- """ Returns the version of this function """
238
+ """ Returns the input version of this function. """
61
239
  return self._input_version
62
240
 
63
241
  @property
64
242
  def unique_id64(self) -> str:
65
243
  """
66
- Returns a unique version string for this version, either the simple readable version or the current version plus a unique hash if the
244
+ Returns a unique version string for this version of at most 64 characters.
245
+
246
+ Returns either the simple readable version or the current version plus a unique hash if the
67
247
  simple version exceeds 64 characters.
68
248
  """
69
249
  return uniqueLabel64(self.full)
@@ -71,36 +251,44 @@ class Version(object):
71
251
  @property
72
252
  def unique_id60(self) -> str:
73
253
  """
74
- Returns a unique version string for this version, either the simple readable version or the current version plus a unique hash if the
254
+ Returns a unique version string for this version of at most 60 characters.
255
+
256
+ Returns either the simple readable version or the current version plus a unique hash if the
75
257
  simple version exceeds 60 characters.
76
- The 60 character version is to support filenames with a three letter extension, so total file name size is at most 64.
77
258
  """
78
259
  return uniqueLabel60(self.full)
79
260
 
80
261
  @property
81
262
  def unique_id48(self) -> str:
82
263
  """
83
- Returns a unique version string for this version, either the simple readable version or the current version plus a unique hash if the
264
+ Returns a unique version string for this version of at most 48 characters.
265
+
266
+ Returns either the simple readable version or the current version plus a unique hash if the
84
267
  simple version exceeds 48 characters.
85
268
  """
86
269
  return uniqueLabel48(self.full)
87
270
 
88
271
  def unique_id(self, max_len : int = 64) -> str:
89
272
  """
90
- Returns a unique version string for this version, either the simple readable version or the current version plus a unique hash if the
91
- simple version exceeds 'max_len' characters.
273
+ Returns a unique version string for this version of at most the specified number of characters.
274
+
275
+ Returns either the simple readable version or the current version plus a unique hash if the
276
+ simple version exceeds ``max_len`` characters.
92
277
  """
93
278
  assert max_len >= 4,("'max_len' must be at least 4", max_len)
94
279
  id_len = 8 if max_len > 16 else 4
95
- uniqueHashVersion = uniqueLabelExt(max_length=max_len, id_length=id_len)
280
+ uniqueHashVersion = UniqueLabel(max_length=max_len, id_length=id_len)
96
281
  return uniqueHashVersion(self.full)
97
282
 
98
283
  @property
99
284
  def full(self) -> str:
100
285
  """
101
- Returns information on the version of 'self' and all dependent functions
102
- in human readable form. Elements are sorted by name, hence this representation
103
- can be used to test equality between two versions (see __eq__ and __neq__)
286
+ Returns information on the version of ``self`` and all dependent elements
287
+ in human readable form.
288
+
289
+ Elements are sorted by name, hence this representation
290
+ can be used to test equality between two versions (:class:`cdxcore.version.Version`
291
+ implements ``==`` and ``!=`` based on ``full``).
104
292
  """
105
293
  self._resolve_dependencies()
106
294
  def respond( deps ):
@@ -121,26 +309,64 @@ class Version(object):
121
309
  @property
122
310
  def dependencies(self):
123
311
  """
124
- Returns information on the version of 'self' and all dependent functions.
125
-
126
- For a given function the format is
127
- If the function has no dependents:
128
- function_version
129
- If the function has dependencies 'g'
130
- ( function_version, { g: g.dependencies } ]
312
+ Returns information on the version of ``self`` and all dependent elements.
313
+
314
+ For a given function the format is as follows:
315
+
316
+ * If the element has no dependents::
317
+
318
+ "version"
319
+
320
+ * If the function has dependencies, return recursively::
321
+
322
+ ( "version", { dependency: dependency.version_full() } )
323
+
324
+ **Example**::
325
+
326
+ from cdxcore.version import version
327
+
328
+ class A(object):
329
+ def __init__(self, x=2):
330
+ self.x = x
331
+ @version(version="0.4.1")
332
+ def h(self, y):
333
+ return self.x*y
334
+
335
+ @version(version="0.3.0")
336
+ def h(x,y):
337
+ return x+y
338
+
339
+ @version(version="0.0.2", dependencies=[h])
340
+ def f(x,y):
341
+ return h(y,x)
342
+
343
+ @version(version="0.0.1", dependencies=["f", A.h])
344
+ def g(x,z):
345
+ a = A()
346
+ return f(x*2,z)+a.h(z)
347
+
348
+ g(1,2)
349
+ print("version", g.version.input) # -> version 0.0.1
350
+ print("full version", g.version.full ) # -> full version 0.0.1 { f: 0.0.2 { h: 0.3.0 }, A.h: 0.4.1 }
351
+ print("depedencies",g.version.dependencies ) # -> depedencies ('0.0.1', {'f': ('0.0.2', {'h': '0.3.0'}), 'A.h': '0.4.1'})
131
352
  """
132
353
  self._resolve_dependencies()
133
354
  return self._dependencies
134
355
 
135
- def is_dependent( self, other):
356
+ def is_dependent( self, other) -> str:
136
357
  """
137
- Determines whether the current function is dependent on 'other'.
138
- The parameter 'function' can be qualified name, a function, or a class.
358
+ Determines whether the current element is dependent on another element.
359
+
360
+ The parameter ``other`` can be qualified name, a function, or a class.
139
361
 
140
- This function returns None if there is no dependency on 'other',
141
- or the version of the 'other' it is dependent on.
362
+ Returns
363
+ -------
364
+ Version : str
365
+ This function returns ``None`` if there is no dependency on ``other``,
366
+ or the direct user-specified version of the ``other``
367
+ it is dependent on.
142
368
  """
143
- other = other.__qualname__ if not isinstance(other, str) else other
369
+ other = self._qual_name( other ) if not isinstance(other, str) else other
144
370
  dependencies = self.dependencies
145
371
 
146
372
  def is_dependent( ddict ):
@@ -153,28 +379,28 @@ class Version(object):
153
379
  if not ver is None:
154
380
  return ver
155
381
  return None
156
- return is_dependent( { self._original.__qualname__: dependencies } )
382
+ return is_dependent( { self._qual_name( self._original ): dependencies } )
157
383
 
158
384
  def _resolve_dependencies( self,
159
385
  top_context : str = None, # top level context for error messages
160
386
  recursive : set = None # set of visited functions
161
387
  ):
162
388
  """
163
- Function to be called to compute dependencies for 'original'
389
+ Function to be called to compute dependencies for `original`.
164
390
 
165
391
  Parameters
166
392
  ----------
167
- top_context:
168
- Name of the top level recursive context for error messages
169
- recursive:
170
- A set to catch recursive dependencies.
393
+ top_context:
394
+ Name of the top level recursive context for error messages
395
+ recursive:
396
+ A set to catch recursive dependencies.
171
397
  """
172
398
  # quick check whether 'wrapper' has already been resolved
173
399
  if not self._dependencies is None:
174
400
  return
175
401
 
176
402
  # setup
177
- local_context = self._original.__qualname__
403
+ local_context = self._qual_name( self._original )
178
404
  top_context = top_context if not top_context is None else local_context
179
405
 
180
406
  def err_context():
@@ -194,7 +420,7 @@ class Version(object):
194
420
  version_dependencies = dict()
195
421
 
196
422
  if self._auto_class and not self._class is None:
197
- version_dependencies[self._class.__qualname__] = self._class.version.dependencies
423
+ version_dependencies[ self._qual_name( self._class )] = self._class.version.dependencies
198
424
 
199
425
  for dep in self._input_dependencies:
200
426
  # 'dep' can be a string or simply another decorated function
@@ -208,7 +434,7 @@ class Version(object):
208
434
  # expand global lookup with 'self' if present
209
435
  source = getattr(self._original,"__globals__", None)
210
436
  if source is None:
211
- raise VersionError( err_context(), f"Cannot resolve dependency for string reference '{dep}': object of type '{type(self._original).__name__}' has no __globals__ to look up in" )
437
+ raise VersionDefinitionError( err_context(), f"Cannot resolve dependency for string reference '{dep}': object of type '{type(self._original).__name__}' has no __globals__ to look up in" )
212
438
  src_name = "global name space"
213
439
  self_ = getattr(self._original,"__self__" if not isinstance(self._original,type) else "__dict__", None)
214
440
  if not self_ is None:
@@ -220,9 +446,9 @@ class Version(object):
220
446
  for part in hierarchy[:-1]:
221
447
  source = source.get(part, None)
222
448
  if source is None:
223
- raise VersionError( err_context(), f"Cannot find '{part}' in '{src_name}' as part of resolving dependency on '{str_dep}'; known names: {fmt_list(sorted(list(source.keys())))}" )
449
+ raise VersionDefinitionError( err_context(), f"Cannot find '{part}' in '{src_name}' as part of resolving dependency on '{str_dep}'; known names: {fmt_list(sorted(list(source.keys())))}" )
224
450
  if not isinstance(source, type):
225
- raise VersionError( err_context(), f"'{part}' in '{src_name}' is not a class/type, but '{type(source).__name__}'. This was part of resolving dependency on '{str_dep}'" )
451
+ raise VersionDefinitionError( err_context(), f"'{part}' in '{src_name}' is not a class/type, but '{type(source).__name__}'. This was part of resolving dependency on '{str_dep}'" )
226
452
  source = source.__dict__
227
453
  src_name = part
228
454
 
@@ -230,16 +456,17 @@ class Version(object):
230
456
  dep = source.get(hierarchy[-1], None)
231
457
  ext = "" if hierarchy[-1]==str_dep else ". (This is part of resoling dependency on '%s')" % str_dep
232
458
  if dep is None:
233
- raise VersionError( err_context(), f"Cannot find '{hierarchy[-1]}' in '{src_name}'; known names: {fmt_list((source.keys()))}{ext}" )
459
+ raise VersionDefinitionError( err_context(), f"Cannot find '{hierarchy[-1]}' in '{src_name}'; known names: {fmt_list((source.keys()))}{ext}" )
234
460
 
235
461
  if not isinstance( dep, Version ):
236
462
  dep_v = getattr(dep, "version", None)
237
- if dep_v is None: raise VersionError( err_context(), f"Cannot determine version of '{dep.__qualname__}': this is not a versioned function or class as it does not have a 'version' member", )
238
- if type(dep_v).__name__ != "Version": raise VersionError( err_context(), f"Cannot determine version of '{dep.__qualname__}': 'version' member is of type '{type(dep_v).__name__}' not of type 'Version'" )
239
- qualname = dep.__qualname__
463
+ dep_qn = self._qual_name( dep )
464
+ if dep_v is None: raise VersionDefinitionError( err_context(), f"Cannot determine version of '{dep_qn}': this is not a versioned function or class as it does not have a 'version' member", )
465
+ if type(dep_v).__name__ != "Version": raise VersionDefinitionError( err_context(), f"Cannot determine version of '{dep_qn}': 'version' member is of type '{type(dep_v).__name__}' not of type 'Version'" )
466
+ qualname = dep_qn
240
467
  else:
241
468
  dep_v = dep
242
- qualname = dep._original.__qualname__
469
+ qualname = self._qual_name( dep._original )
243
470
 
244
471
  # dynamically retrieve dependencies
245
472
  dep_v._resolve_dependencies( top_context=top_context, recursive=recursive )
@@ -249,13 +476,27 @@ class Version(object):
249
476
  # add our own to 'resolved dependencies'
250
477
  self._dependencies = ( self._input_version, version_dependencies ) if len(version_dependencies) > 0 else self._input_version
251
478
 
479
+ @staticmethod
480
+ def _qual_name( x ) -> str:
481
+ if isinstance(x, str):
482
+ return x
483
+ try:
484
+ return x.__qualname__
485
+ except:
486
+ pass
487
+ try:
488
+ return type(x).__qualname__
489
+ except:
490
+ pass
491
+ raise TypeError(f"Cannot determine qualified name for type {type(x)}, '{str(x)[:100]}'")
492
+
493
+
252
494
  # uniqueHash
253
495
  # ----------
254
496
 
255
- def __unique_hash__( self, uniqueHash, debug_trace ) -> str:
497
+ def __unique_hash__( self, unique_hash, debug_trace ) -> str:
256
498
  """
257
- Compute non-hash for use with cdxbasics.util.uniqueHash()
258
- This function always returns an empty string, which means that the object is never hashed.
499
+ Compute hash for use with :class:`cdxcore.uniquehash.UniqueHash`.
259
500
  """
260
501
  return self.unique_id(max_len=uniqueHash.length)
261
502
 
@@ -264,103 +505,122 @@ class Version(object):
264
505
  # =======================================================
265
506
 
266
507
  def version( version : str = "0.0.1" ,
267
- dependencies : list = [], *,
508
+ dependencies : list[type] = [], *,
268
509
  auto_class : bool = True,
269
510
  raise_if_has_version : bool = True ):
270
511
  """
271
- Decorator for a versioned function or class, which may depend on other versioned functions or classes.
512
+ Decorator to "version" a function or class, which may depend on other versioned functions or classes.
272
513
  The point of this decorator is being able to find out the code version of a sequence of function calls,
273
514
  and be able to update cached or otherwise stored results accordingly.
274
- Decoration also works for class members.
275
-
276
- You can 'version' fuunctions and classes.
277
- When a class is 'versioned' it will automatically be dependent on the versions of any 'versioned' base classes.
278
- The same is true for 'versioned' member functions: they will be dependent on the version of the defining class (but not
279
- of derived classes). Sometimes this behaviour is not helpful. In this case set 'auto_class' to False
280
- when setting the 'version' for a member fiunction
281
-
282
-
283
- @version("0.1")
284
- class A(object):
285
- @version("0.2") # automatically depends on A
286
- def f(self, x):
287
- return x
288
- @version("0.3", auto_class=False ) # does not depend on A
289
- def g(self, x):
290
- return x
291
-
292
- @version("0.4") # automatically depends on A
293
- class B(A):
294
- pass
295
515
 
296
- @version("0.4", auto_class=False ) # does not depend on A
297
- class C(A):
298
- pass
516
+ You can :dec:`cdxcore.version.version` functions, classes, and their member functions.
299
517
 
300
- See cdxbasics.vcache as a high level caching mechanism based on version.
301
- This wraps the more basic cdxbasics.subdir.SubDir.cache_callable.
518
+ When a class is versioned it will automatically be dependent on the versions of any versioned base classes.
519
+ The same is true for versioned member functions: by default they will be dependent on the version of the defining class.
520
+ Sometimes this behaviour is not helpful. In this case set ``auto_class`` to ``False``
521
+ when defining the :dec:`cdxcore.version.version` for a member function or derived class.
302
522
 
303
- Parameters
304
- ----------
305
- version : str, optional
306
- Version of this function
307
- dependencies : list, optional
308
- Names of member functions of self which this function depends on.
309
- The list can contain explicit function references, or strings.
310
- If strings are used, then the function's global context and, if appliable, it 'self' will be searched
311
- for the respective function.
312
- auto_class : bool
313
- If True, the default, then the version of member function or an inherited class is automatically dependent
314
- on the version of the defining/base class. Set to False to turn off.
315
- raise_if_has_version : bool
316
- Whether to throw an exception of version are already present.
317
- This is usually the desired behaviour except if used in another wrapper, see for example vcache.
318
-
319
- Returns
320
- -------
321
- Function or class.
322
- The returned function or class will the following properties:
323
- version.input
324
- returns the input 'version' above
325
- version.full
326
- A human readable version string with all dependencies.
327
- version.unique_id64
328
- A unique ID of version_full which can be used to identify changes in the total versioning
329
- accross the dependency structure, of at most 64 characters. Use unique_id() for other lengths.
330
- version.dependencies
331
- Returns a hierarchical description of the version of this function and all its dependcies.
332
- The recursive definition is:
333
- If the function has no dependencies, return
334
- version
335
- If the function has dependencies, return
336
- ( version, { dependency: dependency.version_full() } )
337
-
338
- Example
339
- -------
523
+ Simple function example::
524
+
525
+ from cdxcore.version import version
526
+
340
527
  class A(object):
341
528
  def __init__(self, x=2):
342
529
  self.x = x
343
530
  @version(version="0.4.1")
344
531
  def h(self, y):
345
532
  return self.x*y
346
-
533
+
347
534
  @version(version="0.3.0")
348
535
  def h(x,y):
349
536
  return x+y
350
-
537
+
351
538
  @version(version="0.0.2", dependencies=[h])
352
539
  def f(x,y):
353
540
  return h(y,x)
354
-
541
+
355
542
  @version(version="0.0.1", dependencies=["f", A.h])
356
543
  def g(x,z):
357
544
  a = A()
358
545
  return f(x*2,z)+a.h(z)
359
-
546
+
360
547
  g(1,2)
361
- print("version", g.version.input) -- version 0.0.1
362
- print("full version", g.version.full ) -- full version 0.0.1 { f: 0.0.2 { h: 0.3.0 }, A.h: 0.4.1 }
363
- print("full version ID",g.version.unique_id48 ) -- full version ID 0.0.1 { f: 0.0.2 { h: 0.3.0 }, A.h: 0.4.1 }
548
+ print("version", g.version.input) # -> version 0.0.1
549
+ print("full version", g.version.full ) # -> full version 0.0.1 { f: 0.0.2 { h: 0.3.0 }, A.h: 0.4.1 }
550
+ print("full version ID",g.version.unique_id48 ) # -> full version ID 0.0.1 { f: 0.0.2 { h: 0.3.0 }, A.h: 0.4.1 }
551
+ print("depedencies",g.version.dependencies ) # -> depedencies ('0.0.1', {'f': ('0.0.2', {'h': '0.3.0'}), 'A.h': '0.4.1'})
552
+
553
+ Example for classes::
554
+
555
+ @version("0.1")
556
+ class A(object):
557
+ @version("0.2") # automatically depends on A
558
+ def f(self, x):
559
+ return x
560
+ @version("0.3", auto_class=False ) # does not depend on A
561
+ def g(self, x):
562
+ return x
563
+
564
+ @version("0.4") # automatically depends on A
565
+ class B(A):
566
+ pass
567
+
568
+ @version("0.4", auto_class=False ) # does not depend on A
569
+ class C(A):
570
+ pass
571
+
572
+
573
+ b = B()
574
+ c = C()
575
+ print( "B", b.version.full ) # -> B 0.4 { A: 0.1 }
576
+ print( "C", c.version.full ) # -> C 0.4
577
+
578
+ **See Also**
579
+
580
+ :dec:`cdxcore.subdir.SubDir.cache` implements a caching mechanism which uses versions to decide
581
+ whether a cached result can still be used.
582
+
583
+ Parameters
584
+ ----------
585
+ version : str
586
+ Version string for this function or class.
587
+
588
+ dependencies : list[type], optional
589
+ List of elements this function depends on. Usually the list contains the actual other element by Python reference.
590
+ If this is not suitable (for example if the name cannot be resolved in order), a string can be used to identify the
591
+ dependency.
592
+ If strings are used, then the function's global context and, if appliable, the associated
593
+ ``self`` will be searched for the respective element.
594
+
595
+ auto_class : bool, optional
596
+ If ``True``, the default, then the version of member function or an inherited class is automatically dependent
597
+ on the version of the defining/base class. Set to ``False`` to turn off. The default is ``True``.
598
+
599
+ raise_if_has_version : bool, optional
600
+ Whether to throw an exception of version are already present.
601
+ This is usually the desired behaviour except if used in another wrapper, see for example
602
+ :dec:`cdxcore.subdir.SubDir.cache`. The default is ``True``.
603
+
604
+ Returns
605
+ -------
606
+ Wrapper : Callable
607
+ The returned decorated function or class will have a `version` property of type :class:`cdxcore.version.Version` with
608
+ the following key properties:
609
+
610
+ * :attr:`cdxcore.version.Version.input`: input version string as provided by the user.
611
+
612
+ * :attr:`cdxcore.version.Version.full`: qualified full version including versions of dependent functions or classes, as a string
613
+
614
+ * :attr:`cdxcore.version.Version.unique_id48`: a 48 character unique ID. Versions for 60 and 64 characters are also pre-defined.
615
+
616
+ * :attr:`cdxcore.version.Version.dependencies`: hierarchy of version dependencies as a list.
617
+ The recursive definition is as follows: if the function has no dependencies, return::
618
+
619
+ "version"
620
+
621
+ If the function has dependencies, return recursively::
622
+
623
+ ( "version", { dependency: dependency.version_full() } )
364
624
  """
365
625
  def wrap(f):
366
626
  dep = dependencies
@@ -369,16 +629,16 @@ def version( version : str = "0.0.1" ,
369
629
  # is 'version' a Version
370
630
  if type(existing).__name__ != Version.__name__:
371
631
  tmsg = "type" if isinstance(f,type) else "function"
372
- raise ValueError(f"@version: {tmsg} '{f.__qualname__}' already has a member 'version' but it has type {type(existing).__name__} not {Version}")
632
+ raise ValueError(f"@version: {tmsg} '{Version._qual_name( f )}' already has a member 'version' but it has type {type(existing).__name__} not {Version}")
373
633
  # make sure we were not called twice
374
634
  if existing._original == f:
375
635
  if not raise_if_has_version:
376
636
  return f
377
637
  tmsg = "type" if isinstance(f,type) else "function"
378
- raise ValueError(f"@version: {tmsg} '{f.__qualname__}' already has a member 'version'. It has initial value {existing._input_version}.")
638
+ raise ValueError(f"@version: {tmsg} '{Version._qual_name( f )}' already has a member 'version'. It has initial value {existing._input_version}.")
379
639
  # auto-create dependencies to base classes:
380
640
  # in this case 'existing' is a member of the base class.
381
- if not existing._original in dependencies and not existing._original.__qualname__ in dependencies and auto_class:
641
+ if not existing._original in dependencies and not Version._qual_name( existing._original ) in dependencies and auto_class:
382
642
  dep = list(dep)
383
643
  dep.append( existing._original )
384
644
  if isinstance( f, type ):
@@ -392,7 +652,6 @@ def version( version : str = "0.0.1" ,
392
652
  #print(f"{gname} is not versioned")
393
653
  continue
394
654
  if not gversion._class is None:
395
- #print(f"{gname} already has a class {gversion._class.__qualname__}, skipping {f.__qualname__}")
396
655
  continue
397
656
  gversion._class = f
398
657
  f.version = Version(f, version, dep, auto_class=auto_class )