cdxcore 0.1.5__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 +15 -0
- cdxcore/config.py +1633 -0
- cdxcore/crman.py +105 -0
- cdxcore/deferred.py +220 -0
- cdxcore/dynaplot.py +1155 -0
- cdxcore/filelock.py +430 -0
- cdxcore/jcpool.py +411 -0
- cdxcore/logger.py +319 -0
- cdxcore/np.py +1098 -0
- cdxcore/npio.py +270 -0
- cdxcore/prettydict.py +388 -0
- cdxcore/prettyobject.py +64 -0
- cdxcore/sharedarray.py +285 -0
- cdxcore/subdir.py +2963 -0
- cdxcore/uniquehash.py +970 -0
- cdxcore/util.py +1041 -0
- cdxcore/verbose.py +403 -0
- cdxcore/version.py +402 -0
- cdxcore-0.1.5.dist-info/METADATA +1418 -0
- cdxcore-0.1.5.dist-info/RECORD +30 -0
- cdxcore-0.1.5.dist-info/WHEEL +5 -0
- cdxcore-0.1.5.dist-info/licenses/LICENSE +21 -0
- cdxcore-0.1.5.dist-info/top_level.txt +4 -0
- conda/conda_exists.py +10 -0
- conda/conda_modify_yaml.py +42 -0
- tests/_cdxbasics.py +1086 -0
- tests/test_uniquehash.py +469 -0
- tests/test_util.py +329 -0
- up/git_message.py +7 -0
- up/pip_modify_setup.py +55 -0
cdxcore/version.py
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Version handling for functions and classes including their dependencies via decorators.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .util import fmt_list, uniqueLabelExt
|
|
6
|
+
import inspect as inspect
|
|
7
|
+
|
|
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)
|
|
11
|
+
|
|
12
|
+
class VersionError(RuntimeError):
|
|
13
|
+
def __init__(self, context, message):
|
|
14
|
+
RuntimeError.__init__(self, context, message)
|
|
15
|
+
|
|
16
|
+
class Version(object):
|
|
17
|
+
"""
|
|
18
|
+
Class to track version dependencies for a given function or class 'f'
|
|
19
|
+
Use @version decorator instead.
|
|
20
|
+
|
|
21
|
+
Decorared functions and class have a 'version' member of this type, which has the following properties:
|
|
22
|
+
|
|
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
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, original, version : str, dependencies : list[type], auto_class : bool ):
|
|
30
|
+
""" Wrapper around a versioned function 'f' """
|
|
31
|
+
if version is None:
|
|
32
|
+
raise ValueError("'version' cannot be None")
|
|
33
|
+
self._original = original
|
|
34
|
+
self._input_version = str(version)
|
|
35
|
+
self._input_dependencies = list(dependencies) if not dependencies is None else list()
|
|
36
|
+
self._dependencies = None
|
|
37
|
+
self._class = None # class defining this function
|
|
38
|
+
self._auto_class = auto_class
|
|
39
|
+
|
|
40
|
+
def __str__(self) -> str:
|
|
41
|
+
""" Returns qualified version """
|
|
42
|
+
return self.full
|
|
43
|
+
|
|
44
|
+
def __repr__(self) -> str:
|
|
45
|
+
""" Returns qualified version """
|
|
46
|
+
return self.full
|
|
47
|
+
|
|
48
|
+
def __eq__(self, other) -> bool:
|
|
49
|
+
""" Tests equality of two versions, or a string """
|
|
50
|
+
other = other.full if isinstance(other, Version) else str(other)
|
|
51
|
+
return self.full == other
|
|
52
|
+
|
|
53
|
+
def __neq__(self, other) -> bool:
|
|
54
|
+
""" Tests inequality of two versions, or a string """
|
|
55
|
+
other = other.full if isinstance(other, Version) else str(other)
|
|
56
|
+
return self.full != other
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def input(self) -> str:
|
|
60
|
+
""" Returns the version of this function """
|
|
61
|
+
return self._input_version
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def unique_id64(self) -> str:
|
|
65
|
+
"""
|
|
66
|
+
Returns a unique version string for this version, either the simple readable version or the current version plus a unique hash if the
|
|
67
|
+
simple version exceeds 64 characters.
|
|
68
|
+
"""
|
|
69
|
+
return uniqueLabel64(self.full)
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def unique_id60(self) -> str:
|
|
73
|
+
"""
|
|
74
|
+
Returns a unique version string for this version, either the simple readable version or the current version plus a unique hash if the
|
|
75
|
+
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
|
+
"""
|
|
78
|
+
return uniqueLabel60(self.full)
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def unique_id48(self) -> str:
|
|
82
|
+
"""
|
|
83
|
+
Returns a unique version string for this version, either the simple readable version or the current version plus a unique hash if the
|
|
84
|
+
simple version exceeds 48 characters.
|
|
85
|
+
"""
|
|
86
|
+
return uniqueLabel48(self.full)
|
|
87
|
+
|
|
88
|
+
def unique_id(self, max_len : int = 64) -> str:
|
|
89
|
+
"""
|
|
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.
|
|
92
|
+
"""
|
|
93
|
+
assert max_len >= 4,("'max_len' must be at least 4", max_len)
|
|
94
|
+
id_len = 8 if max_len > 16 else 4
|
|
95
|
+
uniqueHashVersion = uniqueLabelExt(max_length=max_len, id_length=id_len)
|
|
96
|
+
return uniqueHashVersion(self.full)
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def full(self) -> str:
|
|
100
|
+
"""
|
|
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__)
|
|
104
|
+
"""
|
|
105
|
+
self._resolve_dependencies()
|
|
106
|
+
def respond( deps ):
|
|
107
|
+
if isinstance(deps,str):
|
|
108
|
+
return deps
|
|
109
|
+
s = ""
|
|
110
|
+
d = deps[1]
|
|
111
|
+
keys = sorted(list(d.keys()))
|
|
112
|
+
for k in keys:
|
|
113
|
+
v = d[k]
|
|
114
|
+
r = k + ": " + respond(v)
|
|
115
|
+
s = r if s=="" else s + ", " + r
|
|
116
|
+
s += " }"
|
|
117
|
+
s = deps[0] + " { " + s
|
|
118
|
+
return s
|
|
119
|
+
return respond(self._dependencies)
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def dependencies(self):
|
|
123
|
+
"""
|
|
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 } ]
|
|
131
|
+
"""
|
|
132
|
+
self._resolve_dependencies()
|
|
133
|
+
return self._dependencies
|
|
134
|
+
|
|
135
|
+
def is_dependent( self, other):
|
|
136
|
+
"""
|
|
137
|
+
Determines whether the current function is dependent on 'other'.
|
|
138
|
+
The parameter 'function' can be qualified name, a function, or a class.
|
|
139
|
+
|
|
140
|
+
This function returns None if there is no dependency on 'other',
|
|
141
|
+
or the version of the 'other' it is dependent on.
|
|
142
|
+
"""
|
|
143
|
+
other = other.__qualname__ if not isinstance(other, str) else other
|
|
144
|
+
dependencies = self.dependencies
|
|
145
|
+
|
|
146
|
+
def is_dependent( ddict ):
|
|
147
|
+
for k, d in ddict.items():
|
|
148
|
+
if k == other:
|
|
149
|
+
return d if isinstance(d, str) else d[0]
|
|
150
|
+
if isinstance(d, str):
|
|
151
|
+
continue
|
|
152
|
+
ver = is_dependent( d[1] )
|
|
153
|
+
if not ver is None:
|
|
154
|
+
return ver
|
|
155
|
+
return None
|
|
156
|
+
return is_dependent( { self._original.__qualname__: dependencies } )
|
|
157
|
+
|
|
158
|
+
def _resolve_dependencies( self,
|
|
159
|
+
top_context : str = None, # top level context for error messages
|
|
160
|
+
recursive : set = None # set of visited functions
|
|
161
|
+
):
|
|
162
|
+
"""
|
|
163
|
+
Function to be called to compute dependencies for 'original'
|
|
164
|
+
|
|
165
|
+
Parameters
|
|
166
|
+
----------
|
|
167
|
+
top_context:
|
|
168
|
+
Name of the top level recursive context for error messages
|
|
169
|
+
recursive:
|
|
170
|
+
A set to catch recursive dependencies.
|
|
171
|
+
"""
|
|
172
|
+
# quick check whether 'wrapper' has already been resolved
|
|
173
|
+
if not self._dependencies is None:
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
# setup
|
|
177
|
+
local_context = self._original.__qualname__
|
|
178
|
+
top_context = top_context if not top_context is None else local_context
|
|
179
|
+
|
|
180
|
+
def err_context():
|
|
181
|
+
if local_context != top_context:
|
|
182
|
+
return "Error while resolving dependencies for '%s' (as part of resolving dependencies for '%s')" % ( local_context, top_context )
|
|
183
|
+
else:
|
|
184
|
+
return "Error while resolving dependencies for '%s'" % top_context
|
|
185
|
+
|
|
186
|
+
# ensure we do not have a recursive loop
|
|
187
|
+
if not recursive is None:
|
|
188
|
+
if local_context in recursive: raise RecursionError( err_context() + f": recursive dependency on function '{local_context}'" )
|
|
189
|
+
else:
|
|
190
|
+
recursive = set()
|
|
191
|
+
recursive.add(local_context)
|
|
192
|
+
|
|
193
|
+
# collect full qualified dependencies resursively
|
|
194
|
+
version_dependencies = dict()
|
|
195
|
+
|
|
196
|
+
if self._auto_class and not self._class is None:
|
|
197
|
+
version_dependencies[self._class.__qualname__] = self._class.version.dependencies
|
|
198
|
+
|
|
199
|
+
for dep in self._input_dependencies:
|
|
200
|
+
# 'dep' can be a string or simply another decorated function
|
|
201
|
+
# if it is a string, it is of the form A.B.C.f where A,B,C are types and f is a method.
|
|
202
|
+
|
|
203
|
+
if isinstance(dep, str):
|
|
204
|
+
# handle A.B.C.f
|
|
205
|
+
hierarchy = dep.split(".")
|
|
206
|
+
str_dep = dep
|
|
207
|
+
|
|
208
|
+
# expand global lookup with 'self' if present
|
|
209
|
+
source = getattr(self._original,"__globals__", None)
|
|
210
|
+
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" )
|
|
212
|
+
src_name = "global name space"
|
|
213
|
+
self_ = getattr(self._original,"__self__" if not isinstance(self._original,type) else "__dict__", None)
|
|
214
|
+
if not self_ is None:
|
|
215
|
+
source = dict(source)
|
|
216
|
+
source.update(self_.__dict__)
|
|
217
|
+
src_name = "global name space or members of " + type(self_).__name__
|
|
218
|
+
|
|
219
|
+
# resolve types iteratively
|
|
220
|
+
for part in hierarchy[:-1]:
|
|
221
|
+
source = source.get(part, None)
|
|
222
|
+
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())))}" )
|
|
224
|
+
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}'" )
|
|
226
|
+
source = source.__dict__
|
|
227
|
+
src_name = part
|
|
228
|
+
|
|
229
|
+
# get function
|
|
230
|
+
dep = source.get(hierarchy[-1], None)
|
|
231
|
+
ext = "" if hierarchy[-1]==str_dep else ". (This is part of resoling dependency on '%s')" % str_dep
|
|
232
|
+
if dep is None:
|
|
233
|
+
raise VersionError( err_context(), f"Cannot find '{hierarchy[-1]}' in '{src_name}'; known names: {fmt_list((source.keys()))}{ext}" )
|
|
234
|
+
|
|
235
|
+
if not isinstance( dep, Version ):
|
|
236
|
+
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__
|
|
240
|
+
else:
|
|
241
|
+
dep_v = dep
|
|
242
|
+
qualname = dep._original.__qualname__
|
|
243
|
+
|
|
244
|
+
# dynamically retrieve dependencies
|
|
245
|
+
dep_v._resolve_dependencies( top_context=top_context, recursive=recursive )
|
|
246
|
+
assert not dep_v._dependencies is None, ("Internal error", qualname, ":", dep, "//", dep_v)
|
|
247
|
+
version_dependencies[qualname] = dep_v._dependencies
|
|
248
|
+
|
|
249
|
+
# add our own to 'resolved dependencies'
|
|
250
|
+
self._dependencies = ( self._input_version, version_dependencies ) if len(version_dependencies) > 0 else self._input_version
|
|
251
|
+
|
|
252
|
+
# uniqueHash
|
|
253
|
+
# ----------
|
|
254
|
+
|
|
255
|
+
def __unique_hash__( self, uniqueHash, debug_trace ) -> str:
|
|
256
|
+
"""
|
|
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.
|
|
259
|
+
"""
|
|
260
|
+
return self.unique_id(max_len=uniqueHash.length)
|
|
261
|
+
|
|
262
|
+
# =======================================================
|
|
263
|
+
# @version
|
|
264
|
+
# =======================================================
|
|
265
|
+
|
|
266
|
+
def version( version : str = "0.0.1" ,
|
|
267
|
+
dependencies : list = [], *,
|
|
268
|
+
auto_class : bool = True,
|
|
269
|
+
raise_if_has_version : bool = True ):
|
|
270
|
+
"""
|
|
271
|
+
Decorator for a versioned function or class, which may depend on other versioned functions or classes.
|
|
272
|
+
The point of this decorator is being able to find out the code version of a sequence of function calls,
|
|
273
|
+
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
|
+
|
|
296
|
+
@version("0.4", auto_class=False ) # does not depend on A
|
|
297
|
+
class C(A):
|
|
298
|
+
pass
|
|
299
|
+
|
|
300
|
+
See cdxbasics.vcache as a high level caching mechanism based on version.
|
|
301
|
+
This wraps the more basic cdxbasics.subdir.SubDir.cache_callable.
|
|
302
|
+
|
|
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
|
+
-------
|
|
340
|
+
class A(object):
|
|
341
|
+
def __init__(self, x=2):
|
|
342
|
+
self.x = x
|
|
343
|
+
@version(version="0.4.1")
|
|
344
|
+
def h(self, y):
|
|
345
|
+
return self.x*y
|
|
346
|
+
|
|
347
|
+
@version(version="0.3.0")
|
|
348
|
+
def h(x,y):
|
|
349
|
+
return x+y
|
|
350
|
+
|
|
351
|
+
@version(version="0.0.2", dependencies=[h])
|
|
352
|
+
def f(x,y):
|
|
353
|
+
return h(y,x)
|
|
354
|
+
|
|
355
|
+
@version(version="0.0.1", dependencies=["f", A.h])
|
|
356
|
+
def g(x,z):
|
|
357
|
+
a = A()
|
|
358
|
+
return f(x*2,z)+a.h(z)
|
|
359
|
+
|
|
360
|
+
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 }
|
|
364
|
+
"""
|
|
365
|
+
def wrap(f):
|
|
366
|
+
dep = dependencies
|
|
367
|
+
existing = getattr(f, "version", None)
|
|
368
|
+
if not existing is None:
|
|
369
|
+
# is 'version' a Version
|
|
370
|
+
if type(existing).__name__ != Version.__name__:
|
|
371
|
+
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}")
|
|
373
|
+
# make sure we were not called twice
|
|
374
|
+
if existing._original == f:
|
|
375
|
+
if not raise_if_has_version:
|
|
376
|
+
return f
|
|
377
|
+
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}.")
|
|
379
|
+
# auto-create dependencies to base classes:
|
|
380
|
+
# 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:
|
|
382
|
+
dep = list(dep)
|
|
383
|
+
dep.append( existing._original )
|
|
384
|
+
if isinstance( f, type ):
|
|
385
|
+
# set '_class' for all Version objects
|
|
386
|
+
# of all members of a type
|
|
387
|
+
funcs = list( inspect.getmembers(f, predicate=inspect.isfunction) )\
|
|
388
|
+
+ [ c for c in inspect.getmembers(f, predicate=inspect.isclass) if c[0] != "__class__" ]
|
|
389
|
+
for gname, gf in funcs:
|
|
390
|
+
gversion = getattr(gf, "version", None)
|
|
391
|
+
if gversion is None:
|
|
392
|
+
#print(f"{gname} is not versioned")
|
|
393
|
+
continue
|
|
394
|
+
if not gversion._class is None:
|
|
395
|
+
#print(f"{gname} already has a class {gversion._class.__qualname__}, skipping {f.__qualname__}")
|
|
396
|
+
continue
|
|
397
|
+
gversion._class = f
|
|
398
|
+
f.version = Version(f, version, dep, auto_class=auto_class )
|
|
399
|
+
assert type(f.version).__name__ == Version.__name__
|
|
400
|
+
return f
|
|
401
|
+
return wrap
|
|
402
|
+
|