cdxcore 0.1.6__py3-none-any.whl → 0.1.10__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 +1 -9
- cdxcore/config.py +1188 -521
- cdxcore/crman.py +95 -25
- cdxcore/err.py +371 -0
- cdxcore/pretty.py +468 -0
- cdxcore/pretty.py_bak.py +750 -0
- cdxcore/subdir.py +2238 -1339
- cdxcore/uniquehash.py +515 -363
- cdxcore/util.py +358 -417
- cdxcore/verbose.py +683 -248
- cdxcore/version.py +399 -140
- cdxcore-0.1.10.dist-info/METADATA +27 -0
- cdxcore-0.1.10.dist-info/RECORD +35 -0
- {cdxcore-0.1.6.dist-info → cdxcore-0.1.10.dist-info}/top_level.txt +2 -1
- docs/source/conf.py +123 -0
- tests/test_config.py +500 -0
- tests/test_crman.py +54 -0
- tests/test_err.py +86 -0
- tests/test_pretty.py +404 -0
- tests/test_subdir.py +289 -0
- tests/test_uniquehash.py +159 -144
- tests/test_util.py +122 -83
- tests/test_verbose.py +119 -0
- tests/test_version.py +153 -0
- up/git_message.py +2 -2
- cdxcore/logger.py +0 -319
- cdxcore/prettydict.py +0 -388
- cdxcore/prettyobject.py +0 -64
- cdxcore-0.1.6.dist-info/METADATA +0 -1418
- cdxcore-0.1.6.dist-info/RECORD +0 -30
- conda/conda_exists.py +0 -10
- conda/conda_modify_yaml.py +0 -42
- tests/_cdxbasics.py +0 -1086
- {cdxcore-0.1.6.dist-info → cdxcore-0.1.10.dist-info}/WHEEL +0 -0
- {cdxcore-0.1.6.dist-info → cdxcore-0.1.10.dist-info}/licenses/LICENSE +0 -0
- {cdxcore → tmp}/deferred.py +0 -0
- {cdxcore → tmp}/dynaplot.py +0 -0
- {cdxcore → tmp}/filelock.py +0 -0
- {cdxcore → tmp}/np.py +0 -0
- {cdxcore → tmp}/npio.py +0 -0
- {cdxcore → tmp}/sharedarray.py +0 -0
cdxcore/version.py
CHANGED
|
@@ -1,33 +1,211 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
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
|
-
|
|
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 =
|
|
9
|
-
uniqueLabel60 =
|
|
10
|
-
uniqueLabel48 =
|
|
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
|
|
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
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
"""
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
91
|
-
|
|
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 =
|
|
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
|
|
102
|
-
in human readable form.
|
|
103
|
-
|
|
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
|
|
125
|
-
|
|
126
|
-
For a given function the format is
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
138
|
-
|
|
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
|
-
|
|
141
|
-
|
|
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 =
|
|
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
|
|
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
|
|
389
|
+
Function to be called to compute dependencies for `original`.
|
|
164
390
|
|
|
165
391
|
Parameters
|
|
166
392
|
----------
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
238
|
-
if
|
|
239
|
-
|
|
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
|
|
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,118 +476,151 @@ 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,
|
|
497
|
+
def __unique_hash__( self, unique_hash, debug_trace ) -> str:
|
|
256
498
|
"""
|
|
257
|
-
Compute
|
|
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
|
-
return self.unique_id(max_len=
|
|
501
|
+
return self.unique_id(max_len=unique_hash.length)
|
|
261
502
|
|
|
262
503
|
# =======================================================
|
|
263
504
|
# @version
|
|
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
|
|
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
|
-
|
|
297
|
-
class C(A):
|
|
298
|
-
pass
|
|
516
|
+
You can :dec:`cdxcore.version.version` functions, classes, and their member functions.
|
|
299
517
|
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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)
|
|
362
|
-
print("full version", g.version.full )
|
|
363
|
-
print("full version ID",g.version.unique_id48 )
|
|
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
|
|
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
|
|
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
|
|
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 )
|