cs.deco 20250306__py2.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.
- cs/deco.py +1107 -0
- cs_deco-20250306.dist-info/METADATA +810 -0
- cs_deco-20250306.dist-info/RECORD +4 -0
- cs_deco-20250306.dist-info/WHEEL +5 -0
cs/deco.py
ADDED
|
@@ -0,0 +1,1107 @@
|
|
|
1
|
+
#!/usr/bin/python
|
|
2
|
+
#
|
|
3
|
+
# Decorators.
|
|
4
|
+
# - Cameron Simpson <cs@cskk.id.au> 02jul2017
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
r'''
|
|
8
|
+
Assorted function decorators.
|
|
9
|
+
'''
|
|
10
|
+
|
|
11
|
+
from collections import defaultdict
|
|
12
|
+
from contextlib import contextmanager
|
|
13
|
+
from inspect import isgeneratorfunction, ismethod, signature, Parameter
|
|
14
|
+
import sys
|
|
15
|
+
import traceback
|
|
16
|
+
import typing
|
|
17
|
+
|
|
18
|
+
from cs.gimmicks import warning
|
|
19
|
+
|
|
20
|
+
__version__ = '20250306'
|
|
21
|
+
|
|
22
|
+
DISTINFO = {
|
|
23
|
+
'keywords': ["python2", "python3"],
|
|
24
|
+
'classifiers': [
|
|
25
|
+
"Programming Language :: Python",
|
|
26
|
+
"Programming Language :: Python :: 3",
|
|
27
|
+
],
|
|
28
|
+
'install_requires': ['cs.gimmicks'],
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
def ALL(func):
|
|
32
|
+
''' Include this function's name in its module's `__all__` list.
|
|
33
|
+
|
|
34
|
+
Example:
|
|
35
|
+
|
|
36
|
+
from cs.deco import ALL
|
|
37
|
+
|
|
38
|
+
__all__ = []
|
|
39
|
+
|
|
40
|
+
def obscure_function(...):
|
|
41
|
+
...
|
|
42
|
+
|
|
43
|
+
@ALL
|
|
44
|
+
def well_known_function(...):
|
|
45
|
+
...
|
|
46
|
+
'''
|
|
47
|
+
sys.modules[func.__module__].__all__.append(func.__name__)
|
|
48
|
+
return func
|
|
49
|
+
|
|
50
|
+
def fmtdoc(func):
|
|
51
|
+
''' Decorator to replace a function's docstring with that string
|
|
52
|
+
formatted against the function's module `__dict__`.
|
|
53
|
+
|
|
54
|
+
This supports simple formatted docstrings:
|
|
55
|
+
|
|
56
|
+
ENVVAR_NAME = 'FUNC_DEFAULT'
|
|
57
|
+
|
|
58
|
+
@fmtdoc
|
|
59
|
+
def func():
|
|
60
|
+
"""Do something with os.environ[{ENVVAR_NAME}]."""
|
|
61
|
+
print(os.environ[ENVVAR_NAME])
|
|
62
|
+
|
|
63
|
+
This gives `func` this docstring:
|
|
64
|
+
|
|
65
|
+
Do something with os.environ[FUNC_DEFAULT].
|
|
66
|
+
|
|
67
|
+
*Warning*: this decorator is intended for wiring "constants"
|
|
68
|
+
into docstrings, not for dynamic values. Use for other types
|
|
69
|
+
of values should be considered with trepidation.
|
|
70
|
+
'''
|
|
71
|
+
func.__doc__ = func.__doc__.format(**sys.modules[func.__module__].__dict__)
|
|
72
|
+
return func
|
|
73
|
+
|
|
74
|
+
def decorator(deco):
|
|
75
|
+
''' Wrapper for decorator functions to support optional arguments.
|
|
76
|
+
|
|
77
|
+
The actual decorator function ends up being called as:
|
|
78
|
+
|
|
79
|
+
mydeco(func, *da, **dkw)
|
|
80
|
+
|
|
81
|
+
allowing `da` and `dkw` to affect the behaviour of the decorator `mydeco`.
|
|
82
|
+
|
|
83
|
+
Examples:
|
|
84
|
+
|
|
85
|
+
# define your decorator as if always called with func and args
|
|
86
|
+
@decorator
|
|
87
|
+
def mydeco(func, *da, arg2=None):
|
|
88
|
+
... decorate func subject to the values of da and arg2
|
|
89
|
+
|
|
90
|
+
# mydeco called with defaults
|
|
91
|
+
@mydeco
|
|
92
|
+
def func1(...):
|
|
93
|
+
...
|
|
94
|
+
|
|
95
|
+
@ mydeco called with nondefault arguments
|
|
96
|
+
@mydeco('foo', arg2='bah')
|
|
97
|
+
def func2(...):
|
|
98
|
+
...
|
|
99
|
+
'''
|
|
100
|
+
|
|
101
|
+
def decorate(func, *dargs, **dkwargs):
|
|
102
|
+
''' Final decoration when we have the function and the decorator arguments.
|
|
103
|
+
'''
|
|
104
|
+
# decorate func
|
|
105
|
+
decorated = deco(func, *dargs, **dkwargs)
|
|
106
|
+
# catch mucked decorators which forget to return the new function
|
|
107
|
+
assert decorated is not None, (
|
|
108
|
+
"deco:%r(func:%r,...) -> None" % (deco, func)
|
|
109
|
+
)
|
|
110
|
+
if decorated is not func:
|
|
111
|
+
# We got a wrapper function back, pretty up the returned wrapper.
|
|
112
|
+
# Try functools.update_wrapper, otherwise do stuff by hand.
|
|
113
|
+
try:
|
|
114
|
+
from functools import update_wrapper # pylint: disable=import-outside-toplevel
|
|
115
|
+
update_wrapper(decorated, func)
|
|
116
|
+
except (AttributeError, ImportError):
|
|
117
|
+
try:
|
|
118
|
+
decorated.__name__ = getattr(func, '__name__', str(func))
|
|
119
|
+
except AttributeError:
|
|
120
|
+
pass
|
|
121
|
+
doc = getattr(func, '__doc__', None) or ''
|
|
122
|
+
try:
|
|
123
|
+
decorated.__doc__ = doc
|
|
124
|
+
except AttributeError:
|
|
125
|
+
warning("cannot set __doc__ on %r", decorated)
|
|
126
|
+
func_module = getattr(func, '__module__', None)
|
|
127
|
+
try:
|
|
128
|
+
decorated.__module__ = func_module
|
|
129
|
+
except AttributeError:
|
|
130
|
+
pass
|
|
131
|
+
return decorated
|
|
132
|
+
|
|
133
|
+
def metadeco(*da, **dkw):
|
|
134
|
+
''' Compute either the wrapper function for `func`
|
|
135
|
+
or a decorator expecting to get `func` when used.
|
|
136
|
+
|
|
137
|
+
If there is at least one positional parameter
|
|
138
|
+
and it is callable the it is presumed to be the function to decorate;
|
|
139
|
+
decorate it directly.
|
|
140
|
+
|
|
141
|
+
Otherwise return a decorator using the provided arguments,
|
|
142
|
+
ready for the subsequent function.
|
|
143
|
+
'''
|
|
144
|
+
if len(da) > 0 and callable(da[0]):
|
|
145
|
+
# `func` is already supplied, pop it off and decorate it now.
|
|
146
|
+
func = da[0]
|
|
147
|
+
da = tuple(da[1:])
|
|
148
|
+
return decorate(func, *da, **dkw)
|
|
149
|
+
|
|
150
|
+
# `func` is not supplied, collect the arguments supplied and return a
|
|
151
|
+
# decorator which takes the subsequent callable and returns
|
|
152
|
+
# `deco(func, *da, **kw)`.
|
|
153
|
+
return lambda func: decorate(func, *da, **dkw)
|
|
154
|
+
|
|
155
|
+
metadeco.__name__ = getattr(deco, '__name__', repr(deco))
|
|
156
|
+
metadeco.__doc__ = getattr(deco, '__doc__', '')
|
|
157
|
+
metadeco.__module__ = getattr(deco, '__module__', None)
|
|
158
|
+
return metadeco
|
|
159
|
+
|
|
160
|
+
@decorator
|
|
161
|
+
def attr(func, **attrs):
|
|
162
|
+
''' A decorator to set attributes on a function.
|
|
163
|
+
Example:
|
|
164
|
+
|
|
165
|
+
@attr(hook_names=('hook1', 'hook2'))
|
|
166
|
+
def func():
|
|
167
|
+
.....
|
|
168
|
+
|
|
169
|
+
This is just a more overt and clear form of:
|
|
170
|
+
|
|
171
|
+
def func():
|
|
172
|
+
.....
|
|
173
|
+
|
|
174
|
+
func.hook_names=('hook1', 'hook2')
|
|
175
|
+
'''
|
|
176
|
+
if not attrs:
|
|
177
|
+
raise ValueError('@attr: no attributes upplied')
|
|
178
|
+
for attr, value in attrs.items():
|
|
179
|
+
setattr(func, attr, value)
|
|
180
|
+
return func
|
|
181
|
+
|
|
182
|
+
@decorator
|
|
183
|
+
def contextdecorator(cmgrfunc):
|
|
184
|
+
''' A decorator for a context manager function `cmgrfunc`
|
|
185
|
+
which turns it into a decorator for other functions.
|
|
186
|
+
|
|
187
|
+
This supports easy implementation of "setup" and "teardown"
|
|
188
|
+
code around other functions without the tedium of defining
|
|
189
|
+
the wrapper function itself. See the examples below.
|
|
190
|
+
|
|
191
|
+
The resulting context manager accepts an optional keyword
|
|
192
|
+
parameter `provide_context`, default `False`. If true, the
|
|
193
|
+
context returned from the context manager is provided as the
|
|
194
|
+
first argument to the call to the wrapped function.
|
|
195
|
+
|
|
196
|
+
Note that the context manager function `cmgrfunc`
|
|
197
|
+
has _not_ yet been wrapped with `@contextmanager`,
|
|
198
|
+
that is done by `@contextdecorator`.
|
|
199
|
+
|
|
200
|
+
This decorator supports both normal functions and generator functions.
|
|
201
|
+
|
|
202
|
+
With a normal function the process is:
|
|
203
|
+
* call the context manager with `(func,a,kw,*da,**dkw)`,
|
|
204
|
+
returning `ctxt`,
|
|
205
|
+
where `da` and `dkw` are the positional and keyword parameters
|
|
206
|
+
supplied when the decorator was defined.
|
|
207
|
+
* within the context
|
|
208
|
+
return the value of `func(ctxt,*a,**kw)` if `provide_context` is true
|
|
209
|
+
or the value of `func(*a,**kw)` if not (the default)
|
|
210
|
+
|
|
211
|
+
With a generator function the process is:
|
|
212
|
+
* obtain an iterator by calling `func(*a,**kw)`
|
|
213
|
+
* for iterate over the iterator, yielding its results,
|
|
214
|
+
by calling the context manager with `(func,a,kw,**da,**dkw)`,
|
|
215
|
+
around each `next()`
|
|
216
|
+
Note that it is an error to provide a true value for `provide_context`
|
|
217
|
+
if the decorated function is a generator function.
|
|
218
|
+
|
|
219
|
+
Some examples follow.
|
|
220
|
+
|
|
221
|
+
Trace the call and return of a specific function:
|
|
222
|
+
|
|
223
|
+
@contextdecorator
|
|
224
|
+
def tracecall(func, a, kw):
|
|
225
|
+
""" Trace the call and return from some function.
|
|
226
|
+
This can easily be adapted to purposes such as timing a
|
|
227
|
+
function call or logging use.
|
|
228
|
+
"""
|
|
229
|
+
print("call %s(*%r,**%r)" % (func, a, kw))
|
|
230
|
+
try:
|
|
231
|
+
yield
|
|
232
|
+
except Exception as e:
|
|
233
|
+
print("exception from %s(*%r,**%r): %s" % (func, a, kw, e))
|
|
234
|
+
raise
|
|
235
|
+
else:
|
|
236
|
+
print("return from %s(*%r,**%r)" % (func, a, kw))
|
|
237
|
+
|
|
238
|
+
@tracecall
|
|
239
|
+
def f():
|
|
240
|
+
""" Some function to trace.
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
@tracecall(provide_context=True):
|
|
244
|
+
def f(ctxt, *a, **kw):
|
|
245
|
+
""" A function expecting the context object as its first argument,
|
|
246
|
+
ahead of whatever other arguments it would normally require.
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
See who is making use of a generator's values,
|
|
250
|
+
when a generator might be invoked in one place and consumed elsewhere:
|
|
251
|
+
|
|
252
|
+
from cs.py.stack import caller
|
|
253
|
+
|
|
254
|
+
@contextdecorator
|
|
255
|
+
def genuser(genfunc, *a, **kw):
|
|
256
|
+
user = caller(-4)
|
|
257
|
+
print(f"iterate over {genfunc}(*{a!r},**{kw!r}) from {user}")
|
|
258
|
+
yield
|
|
259
|
+
|
|
260
|
+
@genuser
|
|
261
|
+
def linesof(filename):
|
|
262
|
+
with open(filename) as f:
|
|
263
|
+
yield from f
|
|
264
|
+
|
|
265
|
+
# obtain a generator of lines here
|
|
266
|
+
lines = linesof(__file__)
|
|
267
|
+
|
|
268
|
+
# perhaps much later, or in another function
|
|
269
|
+
for lineno, line in enumerate(lines, 1):
|
|
270
|
+
print("line %d: %d words" % (lineno, len(line.split())))
|
|
271
|
+
|
|
272
|
+
Turn on "verbose mode" around a particular function:
|
|
273
|
+
|
|
274
|
+
import sys
|
|
275
|
+
import threading
|
|
276
|
+
from cs.context import stackattrs
|
|
277
|
+
|
|
278
|
+
class State(threading.local):
|
|
279
|
+
def __init__(self):
|
|
280
|
+
# verbose if stderr is on a terminal
|
|
281
|
+
self.verbose = sys.stderr.isatty()
|
|
282
|
+
|
|
283
|
+
# per thread global state
|
|
284
|
+
state = State()
|
|
285
|
+
|
|
286
|
+
@contextdecorator
|
|
287
|
+
def verbose(func):
|
|
288
|
+
with stackattrs(state, verbose=True) as old_attrs:
|
|
289
|
+
if not old_attrs['verbose']:
|
|
290
|
+
print(f"enabled verbose={state.verbose} for function {func}")
|
|
291
|
+
# yield the previous verbosity as the context
|
|
292
|
+
yield old_attrs['verbose']
|
|
293
|
+
|
|
294
|
+
# turn on verbose mode
|
|
295
|
+
@verbose
|
|
296
|
+
def func(x, y):
|
|
297
|
+
if state.verbose:
|
|
298
|
+
# print if verbose
|
|
299
|
+
print("x =", x, "y =", y)
|
|
300
|
+
|
|
301
|
+
# turn on verbose mode and also pass in the previous state
|
|
302
|
+
# as the first argument
|
|
303
|
+
@verbose(provide_context=True):
|
|
304
|
+
def func2(old_verbose, x, y):
|
|
305
|
+
if state.verbose:
|
|
306
|
+
# print if verbose
|
|
307
|
+
print("old_verbosity =", old_verbose, "x =", x, "y =", y)
|
|
308
|
+
'''
|
|
309
|
+
# turn the function into a context manager
|
|
310
|
+
cmgr = contextmanager(cmgrfunc)
|
|
311
|
+
|
|
312
|
+
# prepare a new decorator which wraps functions in a context
|
|
313
|
+
# manager using `cmgrfunc`
|
|
314
|
+
@decorator
|
|
315
|
+
def cmgrdeco(func, *da, provide_context=False, **dkw):
|
|
316
|
+
''' Decorator for functions which wraps calls to the function
|
|
317
|
+
in a context manager, optionally supplying the context
|
|
318
|
+
as the first argument to the called function.
|
|
319
|
+
'''
|
|
320
|
+
if isgeneratorfunction(func):
|
|
321
|
+
if provide_context:
|
|
322
|
+
raise ValueError(
|
|
323
|
+
"provide_context may not be true when func:%s is a generator" %
|
|
324
|
+
(func,)
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
def wrapped(*a, **kw):
|
|
328
|
+
''' Wrapper function:
|
|
329
|
+
* obtain an iterator by calling `func(*a,**kw)`
|
|
330
|
+
* iterate over the iterator, yielding its results,
|
|
331
|
+
by calling the context manager with `(func,a,kw,**da,**dkw)`,
|
|
332
|
+
around each `next()`
|
|
333
|
+
'''
|
|
334
|
+
it = func(*a, **kw)
|
|
335
|
+
while True:
|
|
336
|
+
with cmgr(func, a, kw, *da, **dkw):
|
|
337
|
+
try:
|
|
338
|
+
value = next(it)
|
|
339
|
+
except StopIteration:
|
|
340
|
+
break
|
|
341
|
+
yield value
|
|
342
|
+
|
|
343
|
+
else:
|
|
344
|
+
|
|
345
|
+
def wrapped(*a, **kw):
|
|
346
|
+
''' Wrapper function:
|
|
347
|
+
* call the context manager with `(func,a,kw,**da,**dkw)`,
|
|
348
|
+
returning `ctxt`
|
|
349
|
+
* within the context
|
|
350
|
+
return the value of `func(ctxt,*a,**kw)`
|
|
351
|
+
if `provide_context` is true
|
|
352
|
+
or the value of `func(*a,**kw)` if not (the default)
|
|
353
|
+
'''
|
|
354
|
+
with cmgr(func, a, kw, *da, **dkw) as ctxt:
|
|
355
|
+
if provide_context:
|
|
356
|
+
a = [ctxt] + list(a)
|
|
357
|
+
return func(*a, **kw)
|
|
358
|
+
|
|
359
|
+
return wrapped
|
|
360
|
+
|
|
361
|
+
return cmgrdeco
|
|
362
|
+
|
|
363
|
+
@decorator
|
|
364
|
+
def logging_wrapper(log_call, stacklevel_increment=1):
|
|
365
|
+
''' Decorator for logging call shims
|
|
366
|
+
which bumps the `stacklevel` keyword argument so that the logging system
|
|
367
|
+
chooses the correct frame to cite in messages.
|
|
368
|
+
|
|
369
|
+
Note: has no effect on Python < 3.8 because `stacklevel` only
|
|
370
|
+
appeared in that version.
|
|
371
|
+
'''
|
|
372
|
+
if (sys.version_info.major, sys.version_info.minor) < (3, 8):
|
|
373
|
+
# do not wrap older Python log calls, no stacklevel keyword argument
|
|
374
|
+
return log_call
|
|
375
|
+
|
|
376
|
+
def log_func_wrapper(*a, **kw):
|
|
377
|
+
stacklevel = kw.pop('stacklevel', 1)
|
|
378
|
+
return log_call(*a, stacklevel=stacklevel + stacklevel_increment + 1, **kw)
|
|
379
|
+
|
|
380
|
+
log_func_wrapper.__name__ = log_call.__name__
|
|
381
|
+
log_func_wrapper.__doc__ = log_call.__doc__
|
|
382
|
+
return log_func_wrapper
|
|
383
|
+
|
|
384
|
+
@decorator
|
|
385
|
+
def OBSOLETE(func, suggestion=None):
|
|
386
|
+
''' A decorator for obsolete functions or classes.
|
|
387
|
+
|
|
388
|
+
Use:
|
|
389
|
+
|
|
390
|
+
@OBSOLETE
|
|
391
|
+
def func(...):
|
|
392
|
+
|
|
393
|
+
or
|
|
394
|
+
|
|
395
|
+
@OBSOLETE("new_func_name")
|
|
396
|
+
def func(...):
|
|
397
|
+
|
|
398
|
+
This emits a warning log message before calling the decorated function.
|
|
399
|
+
Only one warning is emitted per calling location.
|
|
400
|
+
'''
|
|
401
|
+
|
|
402
|
+
callers = set()
|
|
403
|
+
|
|
404
|
+
def OBSOLETE_func_wrapper(*args, **kwargs):
|
|
405
|
+
''' Wrap `func` to emit an "OBSOLETE" warning before calling `func`.
|
|
406
|
+
'''
|
|
407
|
+
frame = traceback.extract_stack(None, 2)[0]
|
|
408
|
+
caller = frame[0], frame[1]
|
|
409
|
+
if caller not in callers:
|
|
410
|
+
callers.add(caller)
|
|
411
|
+
prefix = (
|
|
412
|
+
"OBSOLETE call" if suggestion is None else
|
|
413
|
+
("OBSOLETE (suggest %r) call" % suggestion)
|
|
414
|
+
)
|
|
415
|
+
fmt = "%s to %s:%d:%s(), called from %s:%d:%s"
|
|
416
|
+
fmtargs = [
|
|
417
|
+
prefix, func.__code__.co_filename, func.__code__.co_firstlineno,
|
|
418
|
+
func.__name__, frame[0], frame[1], frame[2]
|
|
419
|
+
]
|
|
420
|
+
warning(fmt, *fmtargs)
|
|
421
|
+
return func(*args, **kwargs)
|
|
422
|
+
|
|
423
|
+
funcname = getattr(func, '__name__', str(func))
|
|
424
|
+
funcdoc = getattr(func, '__doc__', None) or ''
|
|
425
|
+
doc = "OBSOLETE " + funcname
|
|
426
|
+
func.__doc__ = doc + '\n\n' + funcdoc
|
|
427
|
+
if suggestion:
|
|
428
|
+
doc += ' suggestion: ' + suggestion
|
|
429
|
+
OBSOLETE_func_wrapper.__name__ = '@OBSOLETE(%s)' % (funcname,)
|
|
430
|
+
return OBSOLETE_func_wrapper
|
|
431
|
+
|
|
432
|
+
@OBSOLETE(suggestion='cs.cache.cachedmethod')
|
|
433
|
+
def cached(*a, **kw):
|
|
434
|
+
''' Former name for @cachedmethod.
|
|
435
|
+
'''
|
|
436
|
+
from cs.cache import cachedmethod
|
|
437
|
+
return cachedmethod(*a, **kw)
|
|
438
|
+
|
|
439
|
+
@OBSOLETE(suggestion='cs.cache.cachedmethod')
|
|
440
|
+
def cachedmethod(*a, **kw):
|
|
441
|
+
''' @cachedmethod is now in cs.cache.
|
|
442
|
+
'''
|
|
443
|
+
from cs.cache import cachedmethod
|
|
444
|
+
return cachedmethod(*a, **kw)
|
|
445
|
+
|
|
446
|
+
def contextual(func):
|
|
447
|
+
''' Wrap a simple function as a context manager.
|
|
448
|
+
|
|
449
|
+
This was written to support users of `@strable`,
|
|
450
|
+
which requires its `open_func` to return a context manager;
|
|
451
|
+
this turns an arbitrary function into a context manager.
|
|
452
|
+
|
|
453
|
+
Example promoting a trivial function:
|
|
454
|
+
|
|
455
|
+
>>> f = lambda: 3
|
|
456
|
+
>>> cf = contextual(f)
|
|
457
|
+
>>> with cf() as x: print(x)
|
|
458
|
+
3
|
|
459
|
+
'''
|
|
460
|
+
|
|
461
|
+
@contextmanager
|
|
462
|
+
def cmgr(*a, **kw):
|
|
463
|
+
''' Wrapper for `func` as a context manager.
|
|
464
|
+
'''
|
|
465
|
+
yield func(*a, **kw)
|
|
466
|
+
|
|
467
|
+
func_name = getattr(func, '__name__', str(func))
|
|
468
|
+
cmgr.__name__ = '@contextual(%s)' % func_name
|
|
469
|
+
cmgr.__doc__ = func.__doc__
|
|
470
|
+
return cmgr
|
|
471
|
+
|
|
472
|
+
@decorator
|
|
473
|
+
def strable(func, open_func=None):
|
|
474
|
+
''' Decorator for functions which may accept a `str`
|
|
475
|
+
instead of their core type.
|
|
476
|
+
|
|
477
|
+
Parameters:
|
|
478
|
+
* `func`: the function to decorate
|
|
479
|
+
* `open_func`: the "open" factory to produce the core type
|
|
480
|
+
if a string is provided;
|
|
481
|
+
the default is the builtin "open" function.
|
|
482
|
+
The returned value should be a context manager.
|
|
483
|
+
Simpler functions can be decorated with `@contextual`
|
|
484
|
+
to turn them into context managers if need be.
|
|
485
|
+
|
|
486
|
+
The usual (and default) example is a function to process an
|
|
487
|
+
open file, designed to be handed a file object but which may
|
|
488
|
+
be called with a filename. If the first argument is a `str`
|
|
489
|
+
then that file is opened and the function called with the
|
|
490
|
+
open file.
|
|
491
|
+
|
|
492
|
+
Examples:
|
|
493
|
+
|
|
494
|
+
@strable
|
|
495
|
+
def count_lines(f):
|
|
496
|
+
return len(line for line in f)
|
|
497
|
+
|
|
498
|
+
class Recording:
|
|
499
|
+
"Class representing a video recording."
|
|
500
|
+
...
|
|
501
|
+
@strable(open_func=Recording)
|
|
502
|
+
def process_video(r):
|
|
503
|
+
... do stuff with `r` as a Recording instance ...
|
|
504
|
+
|
|
505
|
+
*Note*: use of this decorator requires the `cs.pfx` module.
|
|
506
|
+
'''
|
|
507
|
+
from cs.pfx import Pfx # pylint: disable=import-outside-toplevel
|
|
508
|
+
if open_func is None:
|
|
509
|
+
open_func = open
|
|
510
|
+
|
|
511
|
+
if isgeneratorfunction(func):
|
|
512
|
+
|
|
513
|
+
def accepts_str(arg, *a, **kw):
|
|
514
|
+
if isinstance(arg, str):
|
|
515
|
+
with Pfx(arg):
|
|
516
|
+
with open_func(arg) as opened:
|
|
517
|
+
for item in func(opened, *a, **kw):
|
|
518
|
+
yield item
|
|
519
|
+
else:
|
|
520
|
+
for item in func(arg, *a, **kw):
|
|
521
|
+
yield item
|
|
522
|
+
else:
|
|
523
|
+
|
|
524
|
+
def accepts_str(arg, *a, **kw):
|
|
525
|
+
if isinstance(arg, str):
|
|
526
|
+
with Pfx(arg):
|
|
527
|
+
with open_func(arg) as opened:
|
|
528
|
+
return func(opened, *a, **kw)
|
|
529
|
+
return func(arg, *a, **kw)
|
|
530
|
+
|
|
531
|
+
return accepts_str
|
|
532
|
+
|
|
533
|
+
def observable_class(property_names, only_unequal=False):
|
|
534
|
+
''' Class decorator to make various instance attributes observable.
|
|
535
|
+
|
|
536
|
+
Parameters:
|
|
537
|
+
* `property_names`:
|
|
538
|
+
an interable of instance property names to set up as
|
|
539
|
+
observable properties. As a special case a single `str` can
|
|
540
|
+
be supplied if only one attribute is to be observed.
|
|
541
|
+
* `only_unequal`:
|
|
542
|
+
only call the observers if the new property value is not
|
|
543
|
+
equal to the previous proerty value. This requires property
|
|
544
|
+
values to be comparable for inequality.
|
|
545
|
+
Default: `False`, meaning that all updates will be reported.
|
|
546
|
+
'''
|
|
547
|
+
|
|
548
|
+
if isinstance(property_names, str):
|
|
549
|
+
property_names = (property_names,)
|
|
550
|
+
|
|
551
|
+
# pylint: disable=protected-access
|
|
552
|
+
def make_observable_class(cls):
|
|
553
|
+
''' Annotate the class `cls` with observable properties.
|
|
554
|
+
'''
|
|
555
|
+
|
|
556
|
+
# push the per instance initialisation
|
|
557
|
+
old_init = cls.__init__
|
|
558
|
+
|
|
559
|
+
def new_init(self, *a, **kw):
|
|
560
|
+
''' New init, much like the old init...
|
|
561
|
+
'''
|
|
562
|
+
self._observable_class__observers = defaultdict(set)
|
|
563
|
+
old_init(self, *a, **kw)
|
|
564
|
+
|
|
565
|
+
cls.__init__ = new_init
|
|
566
|
+
|
|
567
|
+
def add_observer(self, attr, observer):
|
|
568
|
+
''' Add an observer on `.attr` to this instance.
|
|
569
|
+
'''
|
|
570
|
+
self._observable_class__observers[attr].add(observer)
|
|
571
|
+
|
|
572
|
+
cls.add_observer = add_observer
|
|
573
|
+
|
|
574
|
+
def remove_observer(self, attr, observer):
|
|
575
|
+
''' Remove an observer on `.attr` from this instance.
|
|
576
|
+
'''
|
|
577
|
+
self._observable_class__observers[attr].remove(observer)
|
|
578
|
+
|
|
579
|
+
cls.remove_observer = remove_observer
|
|
580
|
+
|
|
581
|
+
def report_observation(self, attr):
|
|
582
|
+
''' Notify all the observers of the current value of `attr`.
|
|
583
|
+
'''
|
|
584
|
+
val_attr = '_' + attr
|
|
585
|
+
value = getattr(self, val_attr, None)
|
|
586
|
+
for observer in self._observable_class__observers[attr]:
|
|
587
|
+
try:
|
|
588
|
+
observer(self, attr, value)
|
|
589
|
+
except Exception as e: # pylint: disable=broad-except
|
|
590
|
+
warning(
|
|
591
|
+
"%s.%s=%r: observer %s(...) raises: %s",
|
|
592
|
+
self,
|
|
593
|
+
val_attr,
|
|
594
|
+
value,
|
|
595
|
+
observer,
|
|
596
|
+
e,
|
|
597
|
+
exc_info=True
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
cls.report_observation = report_observation
|
|
601
|
+
|
|
602
|
+
def make_property(cls, attr):
|
|
603
|
+
''' Make `cls.attr` into a property which reports setattr events.
|
|
604
|
+
'''
|
|
605
|
+
val_attr = '_' + attr
|
|
606
|
+
|
|
607
|
+
def getter(self):
|
|
608
|
+
return getattr(self, val_attr)
|
|
609
|
+
|
|
610
|
+
getter.__name__ = attr
|
|
611
|
+
get_prop = property(getter)
|
|
612
|
+
setattr(cls, attr, get_prop)
|
|
613
|
+
|
|
614
|
+
def setter(self, new_value):
|
|
615
|
+
''' Set the attribute value and tell all the observers.
|
|
616
|
+
'''
|
|
617
|
+
old_value = getattr(self, val_attr, None)
|
|
618
|
+
setattr(self, val_attr, new_value)
|
|
619
|
+
if not only_unequal or old_value != new_value:
|
|
620
|
+
self.report_observation(attr)
|
|
621
|
+
|
|
622
|
+
setter.__name__ = attr
|
|
623
|
+
set_prop = get_prop.setter(setter)
|
|
624
|
+
setattr(cls, attr, set_prop)
|
|
625
|
+
|
|
626
|
+
for property_name in property_names:
|
|
627
|
+
if hasattr(cls, property_name):
|
|
628
|
+
raise ValueError("%s.%s already exists" % (cls, property_name))
|
|
629
|
+
make_property(cls, property_name)
|
|
630
|
+
|
|
631
|
+
return cls
|
|
632
|
+
|
|
633
|
+
return make_observable_class
|
|
634
|
+
|
|
635
|
+
@decorator
|
|
636
|
+
def default_params(func, _strict=False, **param_defaults):
|
|
637
|
+
''' Decorator to provide factory functions for default parameters.
|
|
638
|
+
|
|
639
|
+
This decorator accepts the following keyword parameters:
|
|
640
|
+
* `_strict`: default `False`; if true only replace genuinely
|
|
641
|
+
missing parameters; if false also replace the traditional
|
|
642
|
+
`None` placeholder value
|
|
643
|
+
The remaining keyword parameters are factory functions
|
|
644
|
+
providing the respective default values.
|
|
645
|
+
|
|
646
|
+
Atypical one off direct use:
|
|
647
|
+
|
|
648
|
+
@default_params(dbconn=open_default_dbconn,debug=lambda:settings.DB_DEBUG_MODE)
|
|
649
|
+
def dbquery(query, *, dbconn):
|
|
650
|
+
dbconn.query(query)
|
|
651
|
+
|
|
652
|
+
Typical use as a decorator factory:
|
|
653
|
+
|
|
654
|
+
# in your support module
|
|
655
|
+
uses_ds3 = default_params(ds3client=get_ds3client)
|
|
656
|
+
|
|
657
|
+
# calling code which needs a ds3client
|
|
658
|
+
@uses_ds3
|
|
659
|
+
def do_something(.., *, ds3client, ...):
|
|
660
|
+
... make queries using ds3client ...
|
|
661
|
+
|
|
662
|
+
This replaces the standard boilerplate and avoids replicating
|
|
663
|
+
knowledge of the default factory as exhibited in this legacy code:
|
|
664
|
+
|
|
665
|
+
def do_something(.., *, ds3client=None, ...):
|
|
666
|
+
if ds3client is None:
|
|
667
|
+
ds3client = get_ds3client()
|
|
668
|
+
... make queries using ds3client ...
|
|
669
|
+
'''
|
|
670
|
+
if not param_defaults:
|
|
671
|
+
raise ValueError("@default_params(%s): no defaults?" % (func,))
|
|
672
|
+
|
|
673
|
+
def update_kw(kw):
|
|
674
|
+
''' Update keyword parameters `kw` from the `param_defaults`.
|
|
675
|
+
'''
|
|
676
|
+
for param_name, param_default in param_defaults.items():
|
|
677
|
+
try:
|
|
678
|
+
v = kw[param_name]
|
|
679
|
+
except KeyError:
|
|
680
|
+
kw[param_name] = param_default()
|
|
681
|
+
else:
|
|
682
|
+
if v is None and not _strict:
|
|
683
|
+
kw[param_name] = param_default()
|
|
684
|
+
|
|
685
|
+
if isgeneratorfunction(func):
|
|
686
|
+
|
|
687
|
+
def defaulted_func(*a, **kw):
|
|
688
|
+
update_kw(kw)
|
|
689
|
+
yield from func(*a, **kw)
|
|
690
|
+
else:
|
|
691
|
+
|
|
692
|
+
def defaulted_func(*a, **kw):
|
|
693
|
+
update_kw(kw)
|
|
694
|
+
return func(*a, **kw)
|
|
695
|
+
|
|
696
|
+
defaulted_func.__name__ = func.__name__
|
|
697
|
+
# TODO: get the indent from some aspect of stripped_dedent
|
|
698
|
+
defaulted_func.__doc__ = '\n '.join(
|
|
699
|
+
[
|
|
700
|
+
getattr(func, '__doc__', '') or '',
|
|
701
|
+
'',
|
|
702
|
+
'This function also accepts the following optional keyword parameters:',
|
|
703
|
+
*[
|
|
704
|
+
'* `%s`: default from `%s()`' % (param_name, param_default)
|
|
705
|
+
for param_name, param_default in sorted(param_defaults.items())
|
|
706
|
+
],
|
|
707
|
+
]
|
|
708
|
+
)
|
|
709
|
+
sig0 = signature(func)
|
|
710
|
+
sig = sig0
|
|
711
|
+
modified_params = []
|
|
712
|
+
for param in sig0.parameters.values():
|
|
713
|
+
modified_param = None
|
|
714
|
+
try:
|
|
715
|
+
param_default = param_defaults[param.name]
|
|
716
|
+
except KeyError:
|
|
717
|
+
pass
|
|
718
|
+
else:
|
|
719
|
+
modified_param = param.replace(
|
|
720
|
+
annotation=typing.Optional[param.annotation],
|
|
721
|
+
default=None if param_default is param.empty else param_default,
|
|
722
|
+
)
|
|
723
|
+
if modified_param is None:
|
|
724
|
+
modified_param = param.replace()
|
|
725
|
+
modified_params.append(modified_param)
|
|
726
|
+
sig = sig.replace(parameters=modified_params)
|
|
727
|
+
defaulted_func.__signature__ = sig
|
|
728
|
+
return defaulted_func
|
|
729
|
+
|
|
730
|
+
@decorator
|
|
731
|
+
def uses_cmd_options(
|
|
732
|
+
func, _strict=False, _options_param_name='options', **option_defaults
|
|
733
|
+
):
|
|
734
|
+
''' A decorator to provide default keyword arguments
|
|
735
|
+
from the prevailing `cs.cmdutils.BaseCommandOptions`
|
|
736
|
+
if available, otherwise from `option_defaults`.
|
|
737
|
+
|
|
738
|
+
This exists to provide plumbing free access to options set
|
|
739
|
+
up by a command line invocation using `cs.cmdutils.BaseCommand`.
|
|
740
|
+
|
|
741
|
+
If no `option_defaults` are provided, a single `options`
|
|
742
|
+
keyword argument is provided which is the prevailing
|
|
743
|
+
`BaseCommand.Options` instance.
|
|
744
|
+
|
|
745
|
+
The decorator accepts two optional "private" keyword arguments
|
|
746
|
+
commencing with underscores:
|
|
747
|
+
* `_strict`: default `False`; if true then an `option_defaults`
|
|
748
|
+
will only be applied if the argument is _missing_ from the
|
|
749
|
+
function arguments, otherwise it will be applied if the
|
|
750
|
+
argument is missing or `None`
|
|
751
|
+
* `_options_param_name`: default `'options'`; this is the
|
|
752
|
+
name of the single `options` keyword argument which will be
|
|
753
|
+
supplied if there are no `option_defaults`
|
|
754
|
+
|
|
755
|
+
Examples:
|
|
756
|
+
|
|
757
|
+
@uses_cmd_options(doit=True, quiet=False)
|
|
758
|
+
def func(x, *, doit, quiet, **kw):
|
|
759
|
+
if not quiet:
|
|
760
|
+
print("something", x, kw)
|
|
761
|
+
if doit:
|
|
762
|
+
... do the thing ...
|
|
763
|
+
... etc ...
|
|
764
|
+
|
|
765
|
+
@uses_cmd_options()
|
|
766
|
+
def func(x, *, options, **kw):
|
|
767
|
+
if not options.quiet:
|
|
768
|
+
print("something", x, kw)
|
|
769
|
+
if options.doit:
|
|
770
|
+
... do the thing ...
|
|
771
|
+
... etc ...
|
|
772
|
+
'''
|
|
773
|
+
|
|
774
|
+
def uses_cmd_wrapper(*func_a, **func_kw):
|
|
775
|
+
# fill in the func_kw from the defaults
|
|
776
|
+
# and keep a record of the chosen values
|
|
777
|
+
# run with the prevailing BaseCommand suitably updated
|
|
778
|
+
try:
|
|
779
|
+
from cs.cmdutils import BaseCommand
|
|
780
|
+
from cs.context import stackattrs
|
|
781
|
+
except ImportError:
|
|
782
|
+
# missing cs.cmdutils or cs.context,
|
|
783
|
+
# make an options with no attributes
|
|
784
|
+
class Options:
|
|
785
|
+
'''Dummy options object for accruing attributes.'''
|
|
786
|
+
|
|
787
|
+
options = Options()
|
|
788
|
+
else:
|
|
789
|
+
options_class = BaseCommand.Options
|
|
790
|
+
options = options_class.default() or options_class()
|
|
791
|
+
option_updates = {}
|
|
792
|
+
if not option_defaults:
|
|
793
|
+
option_defaults[_options_param_name] = options
|
|
794
|
+
for option_name, option_default in option_defaults.items():
|
|
795
|
+
if _strict:
|
|
796
|
+
# skip if the option is not provided by the caller
|
|
797
|
+
if option_name in func_kw:
|
|
798
|
+
continue
|
|
799
|
+
elif func_kw.get(option_name) is not None:
|
|
800
|
+
# skip if the option is not provided by the caller
|
|
801
|
+
# or is provided as None
|
|
802
|
+
continue
|
|
803
|
+
option_value = getattr(options, option_name, None)
|
|
804
|
+
if option_value is None:
|
|
805
|
+
option_value = option_default
|
|
806
|
+
option_updates[option_name] = option_value
|
|
807
|
+
func_kw.update(option_updates)
|
|
808
|
+
with stackattrs(options, **option_updates):
|
|
809
|
+
with options:
|
|
810
|
+
return func(*func_a, **func_kw)
|
|
811
|
+
|
|
812
|
+
return uses_cmd_wrapper
|
|
813
|
+
|
|
814
|
+
uses_doit = uses_cmd_options(doit=True)
|
|
815
|
+
uses_force = uses_cmd_options(force=False)
|
|
816
|
+
uses_quiet = uses_cmd_options(quiet=False)
|
|
817
|
+
uses_verbose = uses_cmd_options(verbose=False)
|
|
818
|
+
|
|
819
|
+
# pylint: disable=too-many-statements
|
|
820
|
+
@decorator
|
|
821
|
+
def promote(func, params=None, types=None):
|
|
822
|
+
''' A decorator to promote argument values automatically in annotated functions.
|
|
823
|
+
|
|
824
|
+
If the annotation is `Optional[some_type]` or `Union[some_type,None]`
|
|
825
|
+
then the promotion will be to `some_type` but a value of `None`
|
|
826
|
+
will be passed through unchanged.
|
|
827
|
+
|
|
828
|
+
The decorator accepts optional parameters:
|
|
829
|
+
* `params`: if supplied, only parameters in this list will
|
|
830
|
+
be promoted
|
|
831
|
+
* `types`: if supplied, only types in this list will be
|
|
832
|
+
considered for promotion
|
|
833
|
+
|
|
834
|
+
For any parameter with a type annotation, if that type has a
|
|
835
|
+
`.promote(value)` class method and the function is called with a
|
|
836
|
+
value not of the type of the annotation, the `.promote` method
|
|
837
|
+
will be called to promote the value to the expected type.
|
|
838
|
+
|
|
839
|
+
Note that the `Promotable` mixin provides a `.promote()`
|
|
840
|
+
method which promotes `obj` to the class if the class has a
|
|
841
|
+
factory class method `from_`*typename*`(obj)` where *typename*
|
|
842
|
+
is `obj.__class__.__name__`.
|
|
843
|
+
A common case for me is lexical objects which have a `from_str(str)`
|
|
844
|
+
factory to produce an instance from its textual form.
|
|
845
|
+
|
|
846
|
+
Additionally, if the `.promote(value)` class method raises a `TypeError`
|
|
847
|
+
and `value` has a `.as_`*typename* attribute
|
|
848
|
+
(where *typename* is the name of the type annotation),
|
|
849
|
+
if that attribute is an instance method of `value`
|
|
850
|
+
then promotion will be attempted by calling `value.as_`*typename*`()`
|
|
851
|
+
otherwise the attribute will be used directly
|
|
852
|
+
on the presumption that it is a property.
|
|
853
|
+
|
|
854
|
+
A typical `promote(cls, obj)` method looks like this:
|
|
855
|
+
|
|
856
|
+
@classmethod
|
|
857
|
+
def promote(cls, obj):
|
|
858
|
+
if isinstance(obj, cls):
|
|
859
|
+
return obj
|
|
860
|
+
... recognise various types ...
|
|
861
|
+
... and return a suitable instance of cls ...
|
|
862
|
+
raise TypeError(
|
|
863
|
+
"%s.promote: cannot promote %s:%r",
|
|
864
|
+
cls.__name__, obj.__class__.__name__, obj)
|
|
865
|
+
|
|
866
|
+
Example:
|
|
867
|
+
|
|
868
|
+
>>> from cs.timeseries import Epoch
|
|
869
|
+
>>> from typeguard import typechecked
|
|
870
|
+
>>>
|
|
871
|
+
>>> @promote
|
|
872
|
+
... @typechecked
|
|
873
|
+
... def f(data, epoch:Epoch=None):
|
|
874
|
+
... print("epoch =", type(epoch), epoch)
|
|
875
|
+
...
|
|
876
|
+
>>> f([1,2,3], epoch=12.0)
|
|
877
|
+
epoch = <class 'cs.timeseries.Epoch'> Epoch(start=0, step=12)
|
|
878
|
+
|
|
879
|
+
Example using a class with an `as_P` instance method:
|
|
880
|
+
|
|
881
|
+
>>> class P:
|
|
882
|
+
... def __init__(self, x):
|
|
883
|
+
... self.x = x
|
|
884
|
+
... @classmethod
|
|
885
|
+
... def promote(cls, obj):
|
|
886
|
+
... raise TypeError("dummy promote method")
|
|
887
|
+
...
|
|
888
|
+
>>> class C:
|
|
889
|
+
... def __init__(self, n):
|
|
890
|
+
... self.n = n
|
|
891
|
+
... def as_P(self):
|
|
892
|
+
... return P(self.n + 1)
|
|
893
|
+
...
|
|
894
|
+
>>> @promote
|
|
895
|
+
... def p(p: P):
|
|
896
|
+
... print("P =", type(p), p.x)
|
|
897
|
+
...
|
|
898
|
+
>>> c = C(1)
|
|
899
|
+
>>> p(c)
|
|
900
|
+
P = <class 'cs.deco.P'> 2
|
|
901
|
+
|
|
902
|
+
*Note*: one issue with this is due to the conflict in name
|
|
903
|
+
between this decorator and the method it looks for in a class.
|
|
904
|
+
The `promote` _method_ must appear after any methods in the
|
|
905
|
+
class which are decorated with `@promote`, otherwise the
|
|
906
|
+
`promote` method supplants the name `promote` making it
|
|
907
|
+
unavailable as the decorator.
|
|
908
|
+
I usually just make `.promote` the last method.
|
|
909
|
+
|
|
910
|
+
Failing example:
|
|
911
|
+
|
|
912
|
+
class Foo:
|
|
913
|
+
@classmethod
|
|
914
|
+
def promote(cls, obj):
|
|
915
|
+
... return promoted obj ...
|
|
916
|
+
@promote
|
|
917
|
+
def method(self, param:Type, ...):
|
|
918
|
+
...
|
|
919
|
+
|
|
920
|
+
Working example:
|
|
921
|
+
|
|
922
|
+
class Foo:
|
|
923
|
+
@promote
|
|
924
|
+
def method(self, param:Type, ...):
|
|
925
|
+
...
|
|
926
|
+
# promote method as the final method of the class
|
|
927
|
+
@classmethod
|
|
928
|
+
def promote(cls, obj):
|
|
929
|
+
... return promoted obj ...
|
|
930
|
+
'''
|
|
931
|
+
sig = signature(func)
|
|
932
|
+
if params is not None:
|
|
933
|
+
for param_name in params:
|
|
934
|
+
if param_name not in sig.parameters:
|
|
935
|
+
raise ValueError(
|
|
936
|
+
"@promote(%r,params=%r): no %r parameter in signature (sig.parameters=%r)"
|
|
937
|
+
% (func, params, param_name, dict(sig.parameters))
|
|
938
|
+
)
|
|
939
|
+
promotions = {} # mapping of arg->(type,promote)
|
|
940
|
+
for param_name, param in sig.parameters.items():
|
|
941
|
+
if params is not None and param_name not in params:
|
|
942
|
+
continue
|
|
943
|
+
annotation = param.annotation
|
|
944
|
+
if annotation is Parameter.empty:
|
|
945
|
+
continue
|
|
946
|
+
# recognise optional parameters and use their primary type
|
|
947
|
+
optional = False
|
|
948
|
+
if param.default is not Parameter.empty:
|
|
949
|
+
anno_origin = typing.get_origin(annotation)
|
|
950
|
+
anno_args = typing.get_args(annotation)
|
|
951
|
+
# recognise Optional[T], which becomes Union[T,None]
|
|
952
|
+
if (anno_origin is typing.Union and len(anno_args) == 2
|
|
953
|
+
and anno_args[-1] is type(None)):
|
|
954
|
+
optional = True
|
|
955
|
+
annotation, _ = anno_args
|
|
956
|
+
optional = True
|
|
957
|
+
if types is not None and annotation not in types:
|
|
958
|
+
continue
|
|
959
|
+
try:
|
|
960
|
+
promote_method = annotation.promote
|
|
961
|
+
except AttributeError:
|
|
962
|
+
continue
|
|
963
|
+
if not callable(promote_method):
|
|
964
|
+
continue
|
|
965
|
+
promotions[param_name] = (param, annotation, promote_method, optional)
|
|
966
|
+
if not promotions:
|
|
967
|
+
warning("@promote(%s): no promotable parameters", func)
|
|
968
|
+
return func
|
|
969
|
+
|
|
970
|
+
def promote_args(a, kw):
|
|
971
|
+
''' Promote the position and keyword arguments.
|
|
972
|
+
'''
|
|
973
|
+
bound_args = sig.bind(*a, **kw)
|
|
974
|
+
arg_mapping = bound_args.arguments
|
|
975
|
+
# we don't import cs.pfx (many dependencies!)
|
|
976
|
+
# pylint: disable=unnecessary-lambda-assignment
|
|
977
|
+
get_context = lambda: (
|
|
978
|
+
"@promote(%s.%s)(%s=%s:%r)" % (
|
|
979
|
+
func.__module__, func.__name__, param_name, arg_value.__class__.
|
|
980
|
+
__name__, arg_value
|
|
981
|
+
)
|
|
982
|
+
)
|
|
983
|
+
for param_name, (param, annotation, promote_method,
|
|
984
|
+
optional) in promotions.items():
|
|
985
|
+
try:
|
|
986
|
+
arg_value = arg_mapping[param_name]
|
|
987
|
+
except KeyError:
|
|
988
|
+
# parameter not supplied
|
|
989
|
+
if param.default is Parameter.empty:
|
|
990
|
+
continue
|
|
991
|
+
# fill in the default values
|
|
992
|
+
arg_value = param.default
|
|
993
|
+
if isinstance(arg_value, annotation):
|
|
994
|
+
# already of the desired type
|
|
995
|
+
continue
|
|
996
|
+
try:
|
|
997
|
+
try:
|
|
998
|
+
promoted_value = promote_method(arg_value)
|
|
999
|
+
except TypeError as te:
|
|
1000
|
+
# see if the value has an as_TypeName() method
|
|
1001
|
+
as_method_name = "as_" + annotation.__name__
|
|
1002
|
+
try:
|
|
1003
|
+
as_annotation = getattr(arg_value, as_method_name)
|
|
1004
|
+
except AttributeError:
|
|
1005
|
+
# no .as_TypeName, reraise the original TypeError
|
|
1006
|
+
raise te # pylint: disable=raise-missing-from
|
|
1007
|
+
else:
|
|
1008
|
+
if ismethod(as_annotation) and as_annotation.__self__ is arg_value:
|
|
1009
|
+
# bound instance method of arg_value
|
|
1010
|
+
try:
|
|
1011
|
+
as_value = as_annotation()
|
|
1012
|
+
except (TypeError, ValueError) as e:
|
|
1013
|
+
raise TypeError(
|
|
1014
|
+
"%s: %s.%s(): %s" %
|
|
1015
|
+
(get_context(), param_name, as_method_name, e)
|
|
1016
|
+
) from e
|
|
1017
|
+
else:
|
|
1018
|
+
# assuming a property or even a plain attribute
|
|
1019
|
+
as_value = as_annotation
|
|
1020
|
+
arg_value = as_value
|
|
1021
|
+
except TypeError:
|
|
1022
|
+
# promotion fails
|
|
1023
|
+
if (optional and arg_value is param.default
|
|
1024
|
+
and param.default is not Parameter.empty):
|
|
1025
|
+
# allow omitted/unconverted optional value with default None
|
|
1026
|
+
continue
|
|
1027
|
+
raise
|
|
1028
|
+
else:
|
|
1029
|
+
arg_value = promoted_value
|
|
1030
|
+
arg_mapping[param_name] = arg_value
|
|
1031
|
+
return bound_args.args, bound_args.kwargs
|
|
1032
|
+
|
|
1033
|
+
if isgeneratorfunction(func):
|
|
1034
|
+
|
|
1035
|
+
def promoting_func(*a, **kw):
|
|
1036
|
+
a, kw = promote_args(a, kw)
|
|
1037
|
+
yield from func(*a, **kw)
|
|
1038
|
+
|
|
1039
|
+
else:
|
|
1040
|
+
|
|
1041
|
+
def promoting_func(*a, **kw):
|
|
1042
|
+
a, kw = promote_args(a, kw)
|
|
1043
|
+
return func(*a, **kw)
|
|
1044
|
+
|
|
1045
|
+
return promoting_func
|
|
1046
|
+
|
|
1047
|
+
# pylint: disable=too-few-public-methods
|
|
1048
|
+
class Promotable:
|
|
1049
|
+
''' A mixin class which supports the `@promote` decorator.
|
|
1050
|
+
'''
|
|
1051
|
+
|
|
1052
|
+
@classmethod
|
|
1053
|
+
def promote(cls, obj, **from_t_kw):
|
|
1054
|
+
''' Promote `obj` to an instance of `cls` or raise `TypeError`.
|
|
1055
|
+
This method supports the `@promote` decorator.
|
|
1056
|
+
|
|
1057
|
+
This base method will call the `from_`*typename*`(obj,**from_t_kw)`
|
|
1058
|
+
class factory method if present, where *typename* is
|
|
1059
|
+
`obj.__class__.__name__`.
|
|
1060
|
+
|
|
1061
|
+
Subclasses may override this method to promote other types,
|
|
1062
|
+
typically:
|
|
1063
|
+
|
|
1064
|
+
@classmethod
|
|
1065
|
+
def promote(cls, obj):
|
|
1066
|
+
if isinstance(obj, cls):
|
|
1067
|
+
return obj
|
|
1068
|
+
... various specific type promotions
|
|
1069
|
+
... not done via a from_typename factory method
|
|
1070
|
+
# fall back to Promotable.promote
|
|
1071
|
+
return super().promote(obj)
|
|
1072
|
+
|
|
1073
|
+
An typical `from_`*typename*` factory method:
|
|
1074
|
+
|
|
1075
|
+
class Foo(Promotable):
|
|
1076
|
+
|
|
1077
|
+
def __init__(self, dbkey, dbrow):
|
|
1078
|
+
self.key = dbkey
|
|
1079
|
+
self.row_data = row
|
|
1080
|
+
|
|
1081
|
+
@classmethod
|
|
1082
|
+
def from_str(cls, s : str):
|
|
1083
|
+
"""Accept a database key string, return a `Foo` instance."""
|
|
1084
|
+
row = db_lookup(s)
|
|
1085
|
+
return cls(s, row)
|
|
1086
|
+
|
|
1087
|
+
This supports using `@promote` on functions with `Foo` instances:
|
|
1088
|
+
|
|
1089
|
+
@promote
|
|
1090
|
+
def do_it(foo : Foo):
|
|
1091
|
+
... work with foo ...
|
|
1092
|
+
|
|
1093
|
+
but calling it as:
|
|
1094
|
+
|
|
1095
|
+
do_it("foo_key_value")
|
|
1096
|
+
'''
|
|
1097
|
+
if isinstance(obj, cls):
|
|
1098
|
+
return obj
|
|
1099
|
+
try:
|
|
1100
|
+
from_type = getattr(cls, f'from_{obj.__class__.__name__}')
|
|
1101
|
+
except AttributeError:
|
|
1102
|
+
pass
|
|
1103
|
+
else:
|
|
1104
|
+
return from_type(obj, **from_t_kw)
|
|
1105
|
+
raise TypeError(
|
|
1106
|
+
f'{cls.__name__}.promote: cannot promote {obj.__class__.__name__}:{obj!r}'
|
|
1107
|
+
)
|