cdxcore 0.1.13__py3-none-any.whl → 0.1.15__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 -1
- cdxcore/crman.py +1 -1
- cdxcore/deferred.py +329 -143
- cdxcore/dynaplot.py +14 -264
- cdxcore/dynaplotlimits.py +245 -0
- cdxcore/util.py +5 -5
- cdxcore/version.py +3 -3
- {cdxcore-0.1.13.dist-info → cdxcore-0.1.15.dist-info}/METADATA +1 -1
- {cdxcore-0.1.13.dist-info → cdxcore-0.1.15.dist-info}/RECORD +15 -13
- docs/source/conf.py +1 -6
- tests/test_deferred.py +23 -3
- tmp/deferred-hierarchical.py +682 -0
- {cdxcore-0.1.13.dist-info → cdxcore-0.1.15.dist-info}/WHEEL +0 -0
- {cdxcore-0.1.13.dist-info → cdxcore-0.1.15.dist-info}/licenses/LICENSE +0 -0
- {cdxcore-0.1.13.dist-info → cdxcore-0.1.15.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Overview
|
|
3
|
+
--------
|
|
4
|
+
|
|
5
|
+
Framework for delayed execution of a Python code tree.
|
|
6
|
+
|
|
7
|
+
Used by :mod:`cdxcore.dnaplot`.
|
|
8
|
+
|
|
9
|
+
Import
|
|
10
|
+
------
|
|
11
|
+
|
|
12
|
+
.. code-block:: python
|
|
13
|
+
|
|
14
|
+
from cdxcore.deferrred import Deferred
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from .err import verify
|
|
19
|
+
from. util import qualified_name, fmt_list
|
|
20
|
+
from .verbose import Context
|
|
21
|
+
from collections.abc import Collection, Sequence, Mapping
|
|
22
|
+
import gc as gc
|
|
23
|
+
|
|
24
|
+
class ResolutionDependencyError(RuntimeError):
|
|
25
|
+
"""
|
|
26
|
+
Exeception if the resolution of a deferred action failed because one
|
|
27
|
+
of its source items has not been resolved.
|
|
28
|
+
"""
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
class DeferredAction(object):
|
|
32
|
+
"""
|
|
33
|
+
A deferred action keeps track of an action dependency tree
|
|
34
|
+
for an object which has not been created, yet.
|
|
35
|
+
|
|
36
|
+
This class is returned by :func:`cdxcore.deferred.Deferred`.
|
|
37
|
+
|
|
38
|
+
The aim is to be able to record a sequence of actions on a placeholder
|
|
39
|
+
for an object which does not yet exist
|
|
40
|
+
such as function calls, item access, or attribute access,
|
|
41
|
+
and store the resulting logical dependency tree. Once the target
|
|
42
|
+
element is available, execute the dependency tree iteratively.
|
|
43
|
+
|
|
44
|
+
A basic example is as follows: assume we have a class ``A`` of which
|
|
45
|
+
we will create an object ``a``, but only at a later stage. We
|
|
46
|
+
wish to record a list of deferred actions on ``a`` ahead of its creation.
|
|
47
|
+
|
|
48
|
+
Define the class::
|
|
49
|
+
|
|
50
|
+
class A(object):
|
|
51
|
+
def __init__(self, x):
|
|
52
|
+
self.x = x
|
|
53
|
+
def a_func(self, y):
|
|
54
|
+
return self.x * y
|
|
55
|
+
@staticmethod
|
|
56
|
+
def a_static(y):
|
|
57
|
+
return y
|
|
58
|
+
@property
|
|
59
|
+
def a_propery(self):
|
|
60
|
+
return self.x
|
|
61
|
+
def __getitem__(self, y):
|
|
62
|
+
return self.x * y
|
|
63
|
+
def __call__(self, y):
|
|
64
|
+
return self.x * y
|
|
65
|
+
def another(self):#
|
|
66
|
+
return A(x=self.x*2)
|
|
67
|
+
|
|
68
|
+
Use :func:`cdxcore.deferred.Deferred` to create a root ``DeferredAction`` object::
|
|
69
|
+
|
|
70
|
+
from cdxcore.deferred import Deferred
|
|
71
|
+
da = Deferred("A")
|
|
72
|
+
|
|
73
|
+
Record a few actions::
|
|
74
|
+
|
|
75
|
+
af = da.a_func(1) # defer function call to a_func(1)
|
|
76
|
+
ai = da[2] # defer item access
|
|
77
|
+
aa = da.x # defer attribute access
|
|
78
|
+
|
|
79
|
+
:class:`cdxcore.deferred.DeferredAction` works iteratively, e.g. all values returned from a function call, ``__call__``, or ``__getitem__``
|
|
80
|
+
are themselves deferred automatically.`:class:`cdxcore.deferred.DeferredAction` is also able to defer item and attribute assignments.
|
|
81
|
+
|
|
82
|
+
As an example::
|
|
83
|
+
|
|
84
|
+
an = da.another()
|
|
85
|
+
an = an.another() # call a.another().another() --> an.x = 1*2*2 = 4
|
|
86
|
+
|
|
87
|
+
Finally, resolve the execution by instantiating ``A`` and calling :meth:`cdxcore.deferred.DeferredAction.deferred_resolve`::
|
|
88
|
+
|
|
89
|
+
a = A()
|
|
90
|
+
da.deferred_resolve( a )
|
|
91
|
+
|
|
92
|
+
print( an.x ) # -> 4
|
|
93
|
+
|
|
94
|
+
This functionality is used in :mod:`cdxcore.dynaplot`::
|
|
95
|
+
|
|
96
|
+
from cdxcore.dynaplot import figure
|
|
97
|
+
|
|
98
|
+
fig = figure() # the DynanicFig returned by figure() is derived from DeferredAction
|
|
99
|
+
ax = fig.add_subplot() # Deferred call for add_subplot()
|
|
100
|
+
lns = ax.plot( x, y )[0] # This is a deferred call to plot() and then [0].
|
|
101
|
+
fig.render() # renders the figure and executes plot() then [0]
|
|
102
|
+
lns.set_ydata( y2 ) # we can now access the resulting Line2D object via the DeferredAction wrapper
|
|
103
|
+
fig.close() # update graph
|
|
104
|
+
|
|
105
|
+
**Constructor**
|
|
106
|
+
|
|
107
|
+
External applications do not usually need to directly create objects
|
|
108
|
+
of type :class:`cdxcore.deferred.DeferredAction`. Use :func:`cdxcore.deferred.Deferred`
|
|
109
|
+
as top level entry point.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
def __init__(self, *, action : str, #: :meta private:
|
|
113
|
+
info : str, #: :meta private:
|
|
114
|
+
parent_info : str, #: :meta private:
|
|
115
|
+
sources : dict, #: :meta private:
|
|
116
|
+
args : Collection,#: :meta private:
|
|
117
|
+
kwargs : Mapping, #: :meta private:
|
|
118
|
+
max_err_len : int, #: :meta private:
|
|
119
|
+
):
|
|
120
|
+
"""
|
|
121
|
+
Initialize a deferred action.
|
|
122
|
+
:meta private:
|
|
123
|
+
"""
|
|
124
|
+
self.__max_err_len = max_err_len
|
|
125
|
+
|
|
126
|
+
verify( isinstance(info, str), lambda : f"'info' must be a string; found {self.__to_str(info)}.", exception=ValueError)
|
|
127
|
+
verify( isinstance(parent_info, str), lambda : f"'parent_info' must be a string; found {self.__to_str(parent_info)}.", exception=ValueError)
|
|
128
|
+
verify( isinstance(action, str), lambda : f"'action' must be a string; found {self.__to_str(action)}.", exception=ValueError)
|
|
129
|
+
verify( isinstance(args, (Sequence,Collection)), lambda : f"'args' must be a Collection; found {type(args)}.", exception=ValueError)
|
|
130
|
+
verify( isinstance(kwargs, Mapping), lambda : f"'kwargs' must be a Mapping; found {type(kwargs)}.", exception=ValueError)
|
|
131
|
+
|
|
132
|
+
if action == "":
|
|
133
|
+
verify( len(info)>0 and
|
|
134
|
+
len(parent_info) == 0 and
|
|
135
|
+
len(args) == 0 and
|
|
136
|
+
len(sources) == 0 and
|
|
137
|
+
len(kwargs) == 0, "Must specify 'info' but cannot specify 'parent_info', args', or 'kwargs' for the root action \"\"", exception=ValueError)
|
|
138
|
+
|
|
139
|
+
self.__action = action # action code
|
|
140
|
+
self.__parent_info = parent_info
|
|
141
|
+
self.__args = args
|
|
142
|
+
self.__kwargs = kwargs
|
|
143
|
+
self.__live = None # once the object exists, it goes here.
|
|
144
|
+
self.__was_resolved = False # whether the action was executed.
|
|
145
|
+
self.__dependants = [] # list of all dependent actions.
|
|
146
|
+
|
|
147
|
+
if action == "":
|
|
148
|
+
info = "$"+info
|
|
149
|
+
self.__sources = {id(self):info}
|
|
150
|
+
self.__info = info
|
|
151
|
+
else:
|
|
152
|
+
self.__sources = sources
|
|
153
|
+
self.__info = info
|
|
154
|
+
|
|
155
|
+
self.__dict__["_ready_for_the_deferred_magic_"] = 1
|
|
156
|
+
|
|
157
|
+
def __to_str( self, x ):
|
|
158
|
+
""" Limit string 'x' to ``max_err_len`` by adding ``...`` if necessary. """
|
|
159
|
+
x = str(x)
|
|
160
|
+
if len(x) > self.__max_err_len:
|
|
161
|
+
if x[-1] in [')', ']', '}']:
|
|
162
|
+
x = x[:self.__max_err_len-4] + "..." + x[-1]
|
|
163
|
+
else:
|
|
164
|
+
x = x[:self.__max_err_len-3] + "..."
|
|
165
|
+
return x
|
|
166
|
+
|
|
167
|
+
def __to_argstr( self, x ):
|
|
168
|
+
if isinstance(x, DeferredAction):
|
|
169
|
+
if not x.deferred_was_resolved:
|
|
170
|
+
return f"{{{x.__info}}}"
|
|
171
|
+
x = x._live
|
|
172
|
+
return str(x)
|
|
173
|
+
|
|
174
|
+
def __fmt_args(self, args, kwargs ):
|
|
175
|
+
args_ = [ self.__to_argstr(x) for x in args ]
|
|
176
|
+
kwargs_ = { k: self.__to_argstr(x) for k,x in kwargs.items() }
|
|
177
|
+
fmt_args = ""
|
|
178
|
+
for _ in args_:
|
|
179
|
+
fmt_args += str(_) + ","
|
|
180
|
+
for _ in kwargs_.items():
|
|
181
|
+
fmt_args += f"{_[0]}={_[1]},"
|
|
182
|
+
return fmt_args[:-1]
|
|
183
|
+
|
|
184
|
+
def __qualname( self, x ):
|
|
185
|
+
""" Attempt to obtain a human readable name for ``x``/ """
|
|
186
|
+
if x is None:
|
|
187
|
+
return "None"
|
|
188
|
+
name = getattr(x,"__qualname__", getattr(x, "__name__", None) )
|
|
189
|
+
if not name is None:
|
|
190
|
+
return name
|
|
191
|
+
try:
|
|
192
|
+
return qualified_name( type(x) )
|
|
193
|
+
except:
|
|
194
|
+
pass
|
|
195
|
+
return self.__to_str(x)
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def deferred_info(self) -> str:
|
|
199
|
+
"""
|
|
200
|
+
Text description of the current action.
|
|
201
|
+
|
|
202
|
+
Top level sources are indicated by a ``$``. Curly brackets ``{}`` indicate
|
|
203
|
+
deferred actions themselves. For example:
|
|
204
|
+
|
|
205
|
+
.. code-block:: python
|
|
206
|
+
|
|
207
|
+
$a.f({$b.g(1)})
|
|
208
|
+
|
|
209
|
+
Is generated by:
|
|
210
|
+
|
|
211
|
+
.. code-block:: python
|
|
212
|
+
|
|
213
|
+
from cdxcore.deferred import Deferred
|
|
214
|
+
|
|
215
|
+
a = Deferred("a")
|
|
216
|
+
b = Deferred("b")
|
|
217
|
+
_ = a.f( b.g(1) )
|
|
218
|
+
print( _.deferred_info ) # -> '$a.f({$b.g(1)})'
|
|
219
|
+
"""
|
|
220
|
+
return self.__info
|
|
221
|
+
|
|
222
|
+
@property
|
|
223
|
+
def deferred_was_resolved(self) -> bool:
|
|
224
|
+
""" Whether the underlying operation has already been resolved. """
|
|
225
|
+
return self.__was_resolved
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def deferred_dependants(self) -> list:
|
|
229
|
+
""" Retrieve list of dependant :class:`cdxcore.deferred.DeferredAction` objects. """
|
|
230
|
+
return self.__dependants
|
|
231
|
+
|
|
232
|
+
@property
|
|
233
|
+
def deferred_sources(self) -> dict:
|
|
234
|
+
"""
|
|
235
|
+
Retrieve a dictionary with information on all top-level sources
|
|
236
|
+
this deferred action depends on.
|
|
237
|
+
|
|
238
|
+
A top level source is an element which must be created explicitly by the user.
|
|
239
|
+
These are the elements generated by :func:'cdxcore.deferred.Deferred'.
|
|
240
|
+
|
|
241
|
+
The list contains a unique ``id`` and the name of the
|
|
242
|
+
source. The ``id`` is used to allow the same name for
|
|
243
|
+
several :func:`Deferred` elements.
|
|
244
|
+
|
|
245
|
+
Most users will prefer a simple list of names of sources.
|
|
246
|
+
In that case, use :meth:`cdxcore.deferred.Deferred.deferred_sources_names`.
|
|
247
|
+
|
|
248
|
+
"""
|
|
249
|
+
return self.__sources
|
|
250
|
+
|
|
251
|
+
@property
|
|
252
|
+
def deferred_sources_names(self) -> list:
|
|
253
|
+
"""
|
|
254
|
+
Retrieve a list of names of all top-level sources
|
|
255
|
+
this deferred action depends on.
|
|
256
|
+
|
|
257
|
+
A top level source is an element which must be created explicitly by the user.
|
|
258
|
+
These are the elements generated by :func:'cdxcore.deferred.Deferred'.
|
|
259
|
+
|
|
260
|
+
The list returned by this function contains the ``info`` names
|
|
261
|
+
for each of the sources.
|
|
262
|
+
|
|
263
|
+
Example:
|
|
264
|
+
|
|
265
|
+
.. code-block:: python
|
|
266
|
+
|
|
267
|
+
from cdxcore.deferred import Deferred
|
|
268
|
+
|
|
269
|
+
a = Deferred("a")
|
|
270
|
+
b = Deferred("b")
|
|
271
|
+
_ = a.f( b.g(1) )
|
|
272
|
+
print( _.deferred_sources_names ) # -> ['$a', '$b']
|
|
273
|
+
|
|
274
|
+
The purpose of this function is to allow users to detect dependencies on
|
|
275
|
+
source objects and organize resolution code accordingly.
|
|
276
|
+
"""
|
|
277
|
+
return list( self.deferred_sources.values() )
|
|
278
|
+
|
|
279
|
+
@property
|
|
280
|
+
def deferred_action_result(self):
|
|
281
|
+
"""
|
|
282
|
+
Returns the result of the deferred action, if available.
|
|
283
|
+
|
|
284
|
+
Raises a :class:`RuntimeError` if the action has not been
|
|
285
|
+
executed with :meth:`cdxcore.deferred.Deferred.deferred_resolve` yet.
|
|
286
|
+
|
|
287
|
+
Note that this function might return ``None`` if the resolved
|
|
288
|
+
action had returned ``None``.
|
|
289
|
+
"""
|
|
290
|
+
verify( self.__was_resolved, lambda : f"Deferred action '{self.__info}' has not been executed yet" )
|
|
291
|
+
return self.__live
|
|
292
|
+
|
|
293
|
+
def deferred_print_dependency_tree( self, verbose : Context = Context.all, *, with_sources : bool = False ):
|
|
294
|
+
"""
|
|
295
|
+
Prints the dependency tree recorded by this object and its descendants.
|
|
296
|
+
|
|
297
|
+
This function must be called *before* :meth:`cdxcore.deferred.Deferred.deferred_resolve` is called
|
|
298
|
+
(``deferred_resolve`` clears the dependency tree to free memory).
|
|
299
|
+
|
|
300
|
+
You can collect this information manually as follows if required:
|
|
301
|
+
|
|
302
|
+
.. code-block:: python
|
|
303
|
+
|
|
304
|
+
from cdxcore.deferred import Deferred
|
|
305
|
+
|
|
306
|
+
a = Deferred("a")
|
|
307
|
+
b = Deferred("b")
|
|
308
|
+
_ = a.f( b.g(1) )
|
|
309
|
+
|
|
310
|
+
def collect(x,i=0,data=None):
|
|
311
|
+
data = data if not data is None else list()
|
|
312
|
+
data.append((i,x.deferred_info,x.deferred_sources_names))
|
|
313
|
+
for d in x.deferred_dependants:
|
|
314
|
+
collect(d,i+1,data)
|
|
315
|
+
return data
|
|
316
|
+
for i, info, sources in collect(a):
|
|
317
|
+
print( f"{' '*i} {info} <- {sources}" )
|
|
318
|
+
|
|
319
|
+
prints:
|
|
320
|
+
|
|
321
|
+
.. code-block:: python
|
|
322
|
+
|
|
323
|
+
$a <- ['$a']
|
|
324
|
+
$a.f <- ['$a']
|
|
325
|
+
$a.f({$b.g(1)}) <- ['$a', '$b']
|
|
326
|
+
"""
|
|
327
|
+
if with_sources:
|
|
328
|
+
sources = sorted( self.__sources.values() )
|
|
329
|
+
s = ""
|
|
330
|
+
for _ in sources:
|
|
331
|
+
s += _+","
|
|
332
|
+
s = s[:-1]
|
|
333
|
+
verbose.write( f"{self.__info} <= {s}" )
|
|
334
|
+
else:
|
|
335
|
+
verbose.write( self.__info )
|
|
336
|
+
|
|
337
|
+
for d in self.__dependants:
|
|
338
|
+
d.deferred_print_dependency_tree( with_sources=with_sources, verbose=verbose(1) )
|
|
339
|
+
|
|
340
|
+
def deferred_resolve(self, owner, verbose : Context = None ):
|
|
341
|
+
"""
|
|
342
|
+
Executes the deferred action with ``owner`` as the subject of the action to be performed.
|
|
343
|
+
|
|
344
|
+
For example, if the action is ``__getitem__`` parameter ``key``, then this function
|
|
345
|
+
will execute ``owner[key]``, resolve all dependent functions, and then return the
|
|
346
|
+
value of ``owner[key]``.
|
|
347
|
+
"""
|
|
348
|
+
verbose = Context.quiet if verbose is None else verbose
|
|
349
|
+
|
|
350
|
+
# deferred_resolve the deferred action
|
|
351
|
+
verify( not self.__was_resolved, lambda : f"Deferred action '{self.__info}' has already been executed '{qualified_name(owner)}'" )
|
|
352
|
+
verify( not owner is None, lambda : f"Cannot resolve '{self.__info}': the parent action '{self.__parent_info}' returned 'None'" )
|
|
353
|
+
verify( not isinstance(owner, DeferredAction), lambda : f"Cannot resolve '{self.__info}' using a 'DeferredAction'" )
|
|
354
|
+
|
|
355
|
+
if self.__action == "":
|
|
356
|
+
# no action means `self` references the object itself
|
|
357
|
+
live = owner
|
|
358
|
+
|
|
359
|
+
else:
|
|
360
|
+
def morph(x):
|
|
361
|
+
if not isinstance(x, DeferredAction):
|
|
362
|
+
return x
|
|
363
|
+
if x.deferred_was_resolved:
|
|
364
|
+
return x.__live
|
|
365
|
+
raise ResolutionDependencyError(
|
|
366
|
+
f"Cannot resolve '{self.__info}' with the concrete element of type '{qualified_name(owner)}': "+\
|
|
367
|
+
f"execution is dependent on yet-unresolved element '{x.__info}'. "+\
|
|
368
|
+
"Resolve that element first. "+\
|
|
369
|
+
f"Note: '{self.__info}' is dependent on the following sources: {fmt_list(self.__sources.values(),sort=True)}."
|
|
370
|
+
)
|
|
371
|
+
args = [ morph(x) for x in self.__args ]
|
|
372
|
+
kwargs = { k: morph(x) for k,x in self.__kwargs.items() }
|
|
373
|
+
|
|
374
|
+
if self.__action == "__getattr__":
|
|
375
|
+
# __getattr__ is not a standard member and cannot be obtained with getattr()
|
|
376
|
+
try:
|
|
377
|
+
live = getattr(owner,*args,**kwargs)
|
|
378
|
+
except AttributeError as e:
|
|
379
|
+
arguments = self.__fmt_args(args,kwargs)
|
|
380
|
+
parent = f"provided by '{self.__parent_info}' " if self.__parent_info!="" else ''
|
|
381
|
+
emsg = f"Cannot resolve '{self.__info}': the concrete element of type '{qualified_name(owner)}' {parent}"+\
|
|
382
|
+
f"does not have the requested attribute '{arguments}'."
|
|
383
|
+
e.args = (emsg,) + e.args
|
|
384
|
+
raise e
|
|
385
|
+
else:
|
|
386
|
+
# all other properties -> standard handling
|
|
387
|
+
try:
|
|
388
|
+
action = getattr(owner, self.__action)
|
|
389
|
+
except AttributeError as e:
|
|
390
|
+
parent = f"provided by '{self.__parent_info}' " if self.__parent_info!="" else ''
|
|
391
|
+
emsg = f"Cannot resolve '{self.__info}': the concrete element of type '{qualified_name(owner)}' {parent}"+\
|
|
392
|
+
f"does not contain the action '{self.__action}'"
|
|
393
|
+
e.args = (emsg,) + e.args
|
|
394
|
+
raise e
|
|
395
|
+
|
|
396
|
+
try:
|
|
397
|
+
|
|
398
|
+
live = action( *args, **kwargs )
|
|
399
|
+
except Exception as e:
|
|
400
|
+
arguments = self.__fmt_args(args,kwargs)
|
|
401
|
+
parent = f"provided by '{self.__parent_info}' " if self.__parent_info!="" else ''
|
|
402
|
+
emsg = f"Cannot resolve '{self.__info}': when attempting to execute the action '{self.__action}' "+\
|
|
403
|
+
f"using the concrete element of type '{qualified_name(owner)}' {parent}"+\
|
|
404
|
+
f"with parameters '{arguments}' "+\
|
|
405
|
+
f"an exception was raised: {e}"
|
|
406
|
+
e.args = (emsg,) + e.args
|
|
407
|
+
raise e
|
|
408
|
+
del action
|
|
409
|
+
|
|
410
|
+
verbose.write(f"{self.__info} -> '{qualified_name(live)}' : {self.__to_str(live)}")
|
|
411
|
+
# clear object
|
|
412
|
+
# note that as soon as we write to self.__live we can no longer
|
|
413
|
+
# access any information via getattr()/setattr()/delattr()
|
|
414
|
+
|
|
415
|
+
# resolve all deferred calls for this object
|
|
416
|
+
# note that self.__live might be None.
|
|
417
|
+
while len(self.__dependants) > 0:
|
|
418
|
+
self.__dependants.pop(0).deferred_resolve( live, verbose=verbose(1) )
|
|
419
|
+
|
|
420
|
+
# make sure the parameter scope can be released
|
|
421
|
+
# 'args' and 'kwargs' may hold substantial amounts of memory,
|
|
422
|
+
# for example when this framework is used for delayed
|
|
423
|
+
# plotting with cdxcore.dynaplot
|
|
424
|
+
del self.__kwargs
|
|
425
|
+
del self.__args
|
|
426
|
+
gc.collect()
|
|
427
|
+
|
|
428
|
+
# action
|
|
429
|
+
self.__was_resolved = True
|
|
430
|
+
self.__live = live
|
|
431
|
+
|
|
432
|
+
# Iteration
|
|
433
|
+
# =========
|
|
434
|
+
# The following are deferred function calls on the object subject of the action ``self``.
|
|
435
|
+
|
|
436
|
+
def _act( self, action : str, *,
|
|
437
|
+
args : Collection = [],
|
|
438
|
+
kwargs : Mapping = {},
|
|
439
|
+
num_args : int = None,
|
|
440
|
+
fmt : str = None
|
|
441
|
+
):
|
|
442
|
+
""" Standard action handling """
|
|
443
|
+
|
|
444
|
+
# we already have a live object --> action directly
|
|
445
|
+
if self.__was_resolved:
|
|
446
|
+
if action == "":
|
|
447
|
+
return self._live
|
|
448
|
+
if action == "__getattr__":
|
|
449
|
+
return getattr(self._live, *args, **kwargs)
|
|
450
|
+
try:
|
|
451
|
+
element = getattr(self.__live, action)
|
|
452
|
+
except AttributeError as e:
|
|
453
|
+
emsg = f"Cannot route '{self.__info}' to the object '{qualified_name(self.__live)}' "+\
|
|
454
|
+
f"provided by '{self.__parent_info}': the object "+\
|
|
455
|
+
f"does not contain the action '{action}'"
|
|
456
|
+
e.args = (emsg,) + e.args
|
|
457
|
+
raise e
|
|
458
|
+
|
|
459
|
+
return element(*args, **kwargs)
|
|
460
|
+
|
|
461
|
+
# format info string
|
|
462
|
+
fmt_args = lambda : self.__fmt_args(args,kwargs)
|
|
463
|
+
if fmt is None:
|
|
464
|
+
verify( num_args is None, f"Error defining action '{action}' for '{self.__info}': cannot specify 'num_args' if 'fmt' is None")
|
|
465
|
+
info = f"{self.__info}.{action}({fmt_args()})"
|
|
466
|
+
else:
|
|
467
|
+
if not num_args is None:
|
|
468
|
+
# specific list of arguments
|
|
469
|
+
verify( num_args is None or len(args) == num_args, lambda : f"Error defining action '{action}' for '{self.__info}': expected {num_args} but found {len(args)}" )
|
|
470
|
+
verify( len(kwargs) == 0, lambda : f"Error defining action '{action}' for '{self.__info}': expected {num_args} ... in this case no kwargs are expected, but {len(kwargs)} where found" )
|
|
471
|
+
verify( not fmt is None, lambda : f"Error defining action '{action}' for '{self.__info}': 'fmt' not specified" )
|
|
472
|
+
|
|
473
|
+
def label(x):
|
|
474
|
+
return x if not isinstance(x, DeferredAction) else x.__info
|
|
475
|
+
arguments = { f"arg{i}" : label(arg) for i, arg in enumerate(args) }
|
|
476
|
+
arguments['parent'] = self.__info
|
|
477
|
+
try:
|
|
478
|
+
info = fmt.format(**arguments)
|
|
479
|
+
except ValueError as e:
|
|
480
|
+
raise ValueError( fmt, e, arguments ) from e
|
|
481
|
+
|
|
482
|
+
else:
|
|
483
|
+
# __call__
|
|
484
|
+
info = fmt.format(parent=self.__info, args=fmt_args())
|
|
485
|
+
|
|
486
|
+
# detected dependencies in arguments on other
|
|
487
|
+
sources = dict(self.__sources)
|
|
488
|
+
for x in args:
|
|
489
|
+
if isinstance(x, DeferredAction):
|
|
490
|
+
sources |= x.__sources
|
|
491
|
+
for x in kwargs.values():
|
|
492
|
+
if isinstance(x, DeferredAction):
|
|
493
|
+
sources |= x.__sources
|
|
494
|
+
|
|
495
|
+
# create new action
|
|
496
|
+
deferred = DeferredAction(
|
|
497
|
+
action=action,
|
|
498
|
+
parent_info=self.__info,
|
|
499
|
+
info=self.__to_str(info),
|
|
500
|
+
args=list(args),
|
|
501
|
+
kwargs=dict(kwargs),
|
|
502
|
+
max_err_len=self.__max_err_len,
|
|
503
|
+
sources=sources )
|
|
504
|
+
self.__dependants.append( deferred )
|
|
505
|
+
return deferred
|
|
506
|
+
|
|
507
|
+
# Routed actions
|
|
508
|
+
# --------------
|
|
509
|
+
# We handle __getattr__ and __setattr__ explicitly
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def __getattr__(self, attr ):
|
|
513
|
+
""" Deferred attribute access """
|
|
514
|
+
#print("__getattr__", attr, self.__dict__.get(private_str+"info", "?"))
|
|
515
|
+
private_str = "_DeferredAction__"
|
|
516
|
+
if attr in self.__dict__ or\
|
|
517
|
+
attr[:2] == "__" or\
|
|
518
|
+
attr[:len(private_str)] == private_str or\
|
|
519
|
+
not "_ready_for_the_deferred_magic_" in self.__dict__:
|
|
520
|
+
#print("__getattr__: direct", attr)
|
|
521
|
+
try:
|
|
522
|
+
return self.__dict__[attr]
|
|
523
|
+
except KeyError as e:
|
|
524
|
+
raise AttributeError(*e.args, self.__dict__[private_str+"info"]) from e
|
|
525
|
+
if not self.__live is None:
|
|
526
|
+
#print("__getattr__: live", attr)
|
|
527
|
+
return getattr(self.__live, attr)
|
|
528
|
+
#print("__getattr__: act", attr)
|
|
529
|
+
return self._act("__getattr__", args=[attr], num_args=1, fmt="{parent}.{arg0}")
|
|
530
|
+
|
|
531
|
+
def __setattr__(self, attr, value):
|
|
532
|
+
""" Deferred attribute access """
|
|
533
|
+
#print("__getattr__", attr, self.__dict__.get(private_str+"info", "?"))
|
|
534
|
+
private_str = "_DeferredAction__"
|
|
535
|
+
if attr in self.__dict__ or\
|
|
536
|
+
attr[:2] == "__" or\
|
|
537
|
+
attr[:len(private_str)] == private_str or\
|
|
538
|
+
not "_ready_for_the_deferred_magic_" in self.__dict__:
|
|
539
|
+
try:
|
|
540
|
+
self.__dict__[attr] = value
|
|
541
|
+
except KeyError as e:
|
|
542
|
+
raise AttributeError(*e.args, self.__dict__[private_str+"info"]) from e
|
|
543
|
+
#print("__setattr__: direct", attr)
|
|
544
|
+
return
|
|
545
|
+
if not self.__live is None:
|
|
546
|
+
#print("__setattr__: live", attr)
|
|
547
|
+
return setattr(self.__live, attr, value)
|
|
548
|
+
#print("__setattr__: act", attr)
|
|
549
|
+
return self._act("__setattr__", args=[attr, value], num_args=2, fmt="{parent}.{arg0}={arg1}")
|
|
550
|
+
|
|
551
|
+
def __delattr__(self, attr):
|
|
552
|
+
""" Deferred attribute access """
|
|
553
|
+
#print("__delattr__", attr)
|
|
554
|
+
private_str = "_DeferredAction__"
|
|
555
|
+
if attr in self.__dict__ or\
|
|
556
|
+
attr[:2] == "__" or\
|
|
557
|
+
attr[:len(private_str)] == private_str or\
|
|
558
|
+
not "_ready_for_the_deferred_magic_" in self.__dict__:
|
|
559
|
+
#print("__delattr__: direct", attr)
|
|
560
|
+
try:
|
|
561
|
+
del self.__dict__[attr]
|
|
562
|
+
except KeyError as e:
|
|
563
|
+
raise AttributeError(*e.args, self.__dict__[private_str+"info"]) from e
|
|
564
|
+
return
|
|
565
|
+
if not self.__live is None:
|
|
566
|
+
#print("__delattr__: live", attr)
|
|
567
|
+
return delattr(self.__live, attr)
|
|
568
|
+
#print("__delattr__: act", attr)
|
|
569
|
+
return self._act("__delattr__", args=[attr], num_args=1, fmt="del {parent}.{arg0}")
|
|
570
|
+
|
|
571
|
+
@staticmethod
|
|
572
|
+
def _generate_handle( action : str,
|
|
573
|
+
return_deferred : bool = True, *,
|
|
574
|
+
num_args : int = None,
|
|
575
|
+
fmt : str = None ):
|
|
576
|
+
def act(self, *args, **kwargs):
|
|
577
|
+
r = self._act(action, args=args, kwargs=kwargs, num_args=num_args, fmt=fmt )
|
|
578
|
+
return r if return_deferred else self
|
|
579
|
+
act.__name__ = action
|
|
580
|
+
act.__doc__ = f"Deferred ``{action}`` action"
|
|
581
|
+
return act
|
|
582
|
+
|
|
583
|
+
# core functionality
|
|
584
|
+
__call__ = _generate_handle("__call__", num_args=None, fmt="{parent}({args})")
|
|
585
|
+
__setitem__ = _generate_handle("__setitem__", num_args=2, fmt="{parent}[{arg0}]={arg1}")
|
|
586
|
+
__getitem__ = _generate_handle("__getitem__", num_args=1, fmt="{parent}[{arg0}]")
|
|
587
|
+
|
|
588
|
+
# collections
|
|
589
|
+
__contains__ = _generate_handle("__contains__", num_args=1, fmt="({arg0} in {parent})")
|
|
590
|
+
__iter__ = _generate_handle("__iter__")
|
|
591
|
+
__len__ = _generate_handle("__len__", num_args=0, fmt="len({parent})")
|
|
592
|
+
__hash__ = _generate_handle("__hash__", num_args=0, fmt="hash({parent})")
|
|
593
|
+
|
|
594
|
+
# cannot implement __bool__, __str__, or, __repr__ as these
|
|
595
|
+
# have defined return types
|
|
596
|
+
|
|
597
|
+
# comparison operators
|
|
598
|
+
__neq__ = _generate_handle("__neq__", num_args=1, fmt="({parent}!={arg0})")
|
|
599
|
+
__eq__ = _generate_handle("__eq__", num_args=1, fmt="({parent}=={arg0})")
|
|
600
|
+
__ge__ = _generate_handle("__ge__", num_args=1, fmt="({parent}>={arg0})")
|
|
601
|
+
__le__ = _generate_handle("__le__", num_args=1, fmt="({parent}<={arg0})")
|
|
602
|
+
__gt__ = _generate_handle("__gt__", num_args=1, fmt="({parent}>{arg0})")
|
|
603
|
+
__lt__ = _generate_handle("__lt__", num_args=1, fmt="({parent}<{arg0})")
|
|
604
|
+
|
|
605
|
+
# i*
|
|
606
|
+
__ior__ = _generate_handle("__ior__", False, num_args=1, fmt="{parent}|={arg0}")
|
|
607
|
+
__iand__ = _generate_handle("__iand__", False, num_args=1, fmt="{parent}&={arg0}")
|
|
608
|
+
__ixor__ = _generate_handle("__iand__", False, num_args=1, fmt="{parent}^={arg0}")
|
|
609
|
+
__imod__ = _generate_handle("__imod__", False, num_args=1, fmt="{parent}%={arg0}")
|
|
610
|
+
__iadd__ = _generate_handle("__iadd__", False, num_args=1, fmt="{parent}+={arg0}")
|
|
611
|
+
__iconcat__ = _generate_handle("__iconcat__", False, num_args=1, fmt="{parent}+={arg0}")
|
|
612
|
+
__isub__ = _generate_handle("__isub__", False, num_args=1, fmt="{parent}-={arg0}")
|
|
613
|
+
__imul__ = _generate_handle("__imul__", False, num_args=1, fmt="{parent}*={arg0}")
|
|
614
|
+
__imatmul__ = _generate_handle("__imatmul__", False, num_args=1, fmt="{parent}@={arg0}")
|
|
615
|
+
__ipow__ = _generate_handle("__ipow__", False, num_args=1, fmt="{parent}**={arg0}")
|
|
616
|
+
__itruediv__ = _generate_handle("__itruediv__", False, num_args=1, fmt="{parent}/={arg0}")
|
|
617
|
+
__ifloordiv__ = _generate_handle("__ifloordiv__", False, num_args=1, fmt="{parent}//={arg0}")
|
|
618
|
+
# Py2 __idiv__ = _generate_handle("__idiv__", False, num_args=1, fmt="{parent}/={arg0}")
|
|
619
|
+
|
|
620
|
+
# binary
|
|
621
|
+
__or__ = _generate_handle("__or__", num_args=1, fmt="({parent}|{arg0})")
|
|
622
|
+
__and__ = _generate_handle("__and__", num_args=1, fmt="({parent}&{arg0})")
|
|
623
|
+
__xor__ = _generate_handle("__xor__", num_args=1, fmt="({parent}^{arg0})")
|
|
624
|
+
__mod__ = _generate_handle("__mod__", num_args=1, fmt="({parent}%{arg0})")
|
|
625
|
+
__add__ = _generate_handle("__add__", num_args=1, fmt="({parent}+{arg0})")
|
|
626
|
+
__concat__ = _generate_handle("__concat__", num_args=1, fmt="({parent}+{arg0})")
|
|
627
|
+
__sub__ = _generate_handle("__sub__", num_args=1, fmt="({parent}-{arg0})")
|
|
628
|
+
__mul__ = _generate_handle("__mul__", num_args=1, fmt="({parent}*{arg0})")
|
|
629
|
+
__pow__ = _generate_handle("__pow__", num_args=1, fmt="({parent}**{arg0})")
|
|
630
|
+
__matmul__ = _generate_handle("__matmul__", num_args=1, fmt="({parent}@{arg0})")
|
|
631
|
+
__truediv__ = _generate_handle("__truediv__", num_args=1, fmt="({parent}/{arg0})")
|
|
632
|
+
__floordiv__ = _generate_handle("__floordiv__", num_args=1, fmt="({parent}//{arg0})")
|
|
633
|
+
# Py2__div__ = _generate_handle("__div__", num_args=1, fmt="({parent}/{arg0})")
|
|
634
|
+
|
|
635
|
+
# rbinary
|
|
636
|
+
__ror__ = _generate_handle("__ror__", num_args=1, fmt="({arg0}|{parent})")
|
|
637
|
+
__rand__ = _generate_handle("__rand__", num_args=1, fmt="({arg0}&{parent})")
|
|
638
|
+
__rxor__ = _generate_handle("__rxor__", num_args=1, fmt="({arg0}^{parent})")
|
|
639
|
+
__rmod__ = _generate_handle("__rmod__", num_args=1, fmt="({arg0}%{parent})")
|
|
640
|
+
__radd__ = _generate_handle("__radd__", num_args=1, fmt="({arg0}+{parent})")
|
|
641
|
+
__rconcat__ = _generate_handle("__rconcat__", num_args=1, fmt="({arg0}+{parent})")
|
|
642
|
+
__rsub__ = _generate_handle("__rsub__", num_args=1, fmt="({arg0}-{parent})")
|
|
643
|
+
__rmul__ = _generate_handle("__rmul__", num_args=1, fmt="({arg0}*{parent})")
|
|
644
|
+
__rpow__ = _generate_handle("__rpow__", num_args=1, fmt="({arg0}**{parent})")
|
|
645
|
+
__rmatmul__ = _generate_handle("__rmatmul__", num_args=1, fmt="({arg0}@{parent})")
|
|
646
|
+
__rtruediv__ = _generate_handle("__rtruediv__", num_args=1, fmt="({arg0}/{parent})")
|
|
647
|
+
__rfloordiv__ = _generate_handle("__rfloordiv__", num_args=1,fmt="({arg0}//{parent})")
|
|
648
|
+
# Py2__rdiv__ = _generate_handle("__rdiv__", num_args=1, fmt="({arg0}/{parent})")
|
|
649
|
+
|
|
650
|
+
def Deferred( info : str,
|
|
651
|
+
max_err_len : int = 100
|
|
652
|
+
):
|
|
653
|
+
"""
|
|
654
|
+
Create a :class:`cdxcore.deferred.DeferredAction` object to keep track
|
|
655
|
+
of a dependency tree of a sequence of actions performed an object
|
|
656
|
+
that does not yet exist.
|
|
657
|
+
|
|
658
|
+
See :class:`cdxcore.deferred.DeferredAction` for a comprehensive discussion.
|
|
659
|
+
|
|
660
|
+
Parameters
|
|
661
|
+
----------
|
|
662
|
+
info : str
|
|
663
|
+
Descriptive name of the usually not-yet-created object deferred actions
|
|
664
|
+
will act upon.
|
|
665
|
+
|
|
666
|
+
max_err_len : int, optional
|
|
667
|
+
Maximum length of output information. The default is 100.
|
|
668
|
+
|
|
669
|
+
Returns
|
|
670
|
+
-------
|
|
671
|
+
:class:`cdxcore.deferred.DeferredAction`
|
|
672
|
+
A deferred action.
|
|
673
|
+
"""
|
|
674
|
+
return DeferredAction( info=info,
|
|
675
|
+
action="",
|
|
676
|
+
parent_info="",
|
|
677
|
+
sources={},
|
|
678
|
+
args=[],
|
|
679
|
+
kwargs={},
|
|
680
|
+
max_err_len=max_err_len )
|
|
681
|
+
|
|
682
|
+
|
|
File without changes
|
|
File without changes
|
|
File without changes
|