1# Copyright (c) 2013, Mahmoud Hashemi
2#
3# Redistribution and use in source and binary forms, with or without
4# modification, are permitted provided that the following conditions are
5# met:
6#
7# * Redistributions of source code must retain the above copyright
8# notice, this list of conditions and the following disclaimer.
9#
10# * Redistributions in binary form must reproduce the above
11# copyright notice, this list of conditions and the following
12# disclaimer in the documentation and/or other materials provided
13# with the distribution.
14#
15# * The names of the contributors may not be used to endorse or
16# promote products derived from this software without specific
17# prior written permission.
18#
19# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
31"""Python's built-in :mod:`functools` module builds several useful
32utilities on top of Python's first-class function
33support. ``funcutils`` generally stays in the same vein, adding to and
34correcting Python's standard metaprogramming facilities.
35"""
36
37import sys
38import re
39import inspect
40import functools
41import itertools
42from inspect import formatannotation
43from types import MethodType, FunctionType
44
45make_method = lambda desc, obj, obj_type: MethodType(desc, obj)
46
47
48try:
49 from .typeutils import make_sentinel
50 NO_DEFAULT = make_sentinel(var_name='NO_DEFAULT')
51except ImportError:
52 NO_DEFAULT = object()
53
54
55def inspect_formatargspec(
56 args, varargs=None, varkw=None, defaults=None,
57 kwonlyargs=(), kwonlydefaults={}, annotations={},
58 formatarg=str,
59 formatvarargs=lambda name: '*' + name,
60 formatvarkw=lambda name: '**' + name,
61 formatvalue=lambda value: '=' + repr(value),
62 formatreturns=lambda text: ' -> ' + text,
63 formatannotation=formatannotation):
64 """Copy formatargspec from python 3.7 standard library.
65 Python 3 has deprecated formatargspec and requested that Signature
66 be used instead, however this requires a full reimplementation
67 of formatargspec() in terms of creating Parameter objects and such.
68 Instead of introducing all the object-creation overhead and having
69 to reinvent from scratch, just copy their compatibility routine.
70 """
71
72 def formatargandannotation(arg):
73 result = formatarg(arg)
74 if arg in annotations:
75 result += ': ' + formatannotation(annotations[arg])
76 return result
77 specs = []
78 if defaults:
79 firstdefault = len(args) - len(defaults)
80 for i, arg in enumerate(args):
81 spec = formatargandannotation(arg)
82 if defaults and i >= firstdefault:
83 spec = spec + formatvalue(defaults[i - firstdefault])
84 specs.append(spec)
85 if varargs is not None:
86 specs.append(formatvarargs(formatargandannotation(varargs)))
87 else:
88 if kwonlyargs:
89 specs.append('*')
90 if kwonlyargs:
91 for kwonlyarg in kwonlyargs:
92 spec = formatargandannotation(kwonlyarg)
93 if kwonlydefaults and kwonlyarg in kwonlydefaults:
94 spec += formatvalue(kwonlydefaults[kwonlyarg])
95 specs.append(spec)
96 if varkw is not None:
97 specs.append(formatvarkw(formatargandannotation(varkw)))
98 result = '(' + ', '.join(specs) + ')'
99 if 'return' in annotations:
100 result += formatreturns(formatannotation(annotations['return']))
101 return result
102
103
104def get_module_callables(mod, ignore=None):
105 """Returns two maps of (*types*, *funcs*) from *mod*, optionally
106 ignoring based on the :class:`bool` return value of the *ignore*
107 callable. *mod* can be a string name of a module in
108 :data:`sys.modules` or the module instance itself.
109 """
110 if isinstance(mod, str):
111 mod = sys.modules[mod]
112 types, funcs = {}, {}
113 for attr_name in dir(mod):
114 if ignore and ignore(attr_name):
115 continue
116 try:
117 attr = getattr(mod, attr_name)
118 except Exception:
119 continue
120 try:
121 attr_mod_name = attr.__module__
122 except AttributeError:
123 continue
124 if attr_mod_name != mod.__name__:
125 continue
126 if isinstance(attr, type):
127 types[attr_name] = attr
128 elif callable(attr):
129 funcs[attr_name] = attr
130 return types, funcs
131
132
133def mro_items(type_obj):
134 """Takes a type and returns an iterator over all class variables
135 throughout the type hierarchy (respecting the MRO).
136
137 >>> sorted(set([k for k, v in mro_items(int) if not k.startswith('__') and 'bytes' not in k and not callable(v)]))
138 ['denominator', 'imag', 'numerator', 'real']
139 """
140 # TODO: handle slots?
141 return itertools.chain.from_iterable(ct.__dict__.items()
142 for ct in type_obj.__mro__)
143
144
145def dir_dict(obj, raise_exc=False):
146 """Return a dictionary of attribute names to values for a given
147 object. Unlike ``obj.__dict__``, this function returns all
148 attributes on the object, including ones on parent classes.
149 """
150 # TODO: separate function for handling descriptors on types?
151 ret = {}
152 for k in dir(obj):
153 try:
154 ret[k] = getattr(obj, k)
155 except Exception:
156 if raise_exc:
157 raise
158 return ret
159
160
161def copy_function(orig, copy_dict=True):
162 """Returns a shallow copy of the function, including code object,
163 globals, closure, etc.
164
165 >>> func = lambda: func
166 >>> func() is func
167 True
168 >>> func_copy = copy_function(func)
169 >>> func_copy() is func
170 True
171 >>> func_copy is not func
172 True
173
174 Args:
175 orig (function): The function to be copied. Must be a
176 function, not just any method or callable.
177 copy_dict (bool): Also copy any attributes set on the function
178 instance. Defaults to ``True``.
179 """
180 ret = FunctionType(orig.__code__,
181 orig.__globals__,
182 name=orig.__name__,
183 argdefs=getattr(orig, "__defaults__", None),
184 closure=getattr(orig, "__closure__", None))
185 if hasattr(orig, "__kwdefaults__"):
186 ret.__kwdefaults__ = orig.__kwdefaults__
187 if copy_dict:
188 ret.__dict__.update(orig.__dict__)
189 return ret
190
191
192def partial_ordering(cls):
193 """Class decorator, similar to :func:`functools.total_ordering`,
194 except it is used to define `partial orderings`_ (i.e., it is
195 possible that *x* is neither greater than, equal to, or less than
196 *y*). It assumes the presence of the ``__le__()`` and ``__ge__()``
197 method, but nothing else. It will not override any existing
198 additional comparison methods.
199
200 .. _partial orderings: https://en.wikipedia.org/wiki/Partially_ordered_set
201
202 >>> @partial_ordering
203 ... class MySet(set):
204 ... def __le__(self, other):
205 ... return self.issubset(other)
206 ... def __ge__(self, other):
207 ... return self.issuperset(other)
208 ...
209 >>> a = MySet([1,2,3])
210 >>> b = MySet([1,2])
211 >>> c = MySet([1,2,4])
212 >>> b < a
213 True
214 >>> b > a
215 False
216 >>> b < c
217 True
218 >>> a < c
219 False
220 >>> c > a
221 False
222 """
223 def __lt__(self, other): return self <= other and not self >= other
224 def __gt__(self, other): return self >= other and not self <= other
225 def __eq__(self, other): return self >= other and self <= other
226
227 if not hasattr(cls, '__lt__'): cls.__lt__ = __lt__
228 if not hasattr(cls, '__gt__'): cls.__gt__ = __gt__
229 if not hasattr(cls, '__eq__'): cls.__eq__ = __eq__
230
231 return cls
232
233
234class InstancePartial(functools.partial):
235 """:class:`functools.partial` is a huge convenience for anyone
236 working with Python's great first-class functions. It allows
237 developers to curry arguments and incrementally create simpler
238 callables for a variety of use cases.
239
240 Unfortunately there's one big gap in its usefulness:
241 methods. Partials just don't get bound as methods and
242 automatically handed a reference to ``self``. The
243 ``InstancePartial`` type remedies this by inheriting from
244 :class:`functools.partial` and implementing the necessary
245 descriptor protocol. There are no other differences in
246 implementation or usage. :class:`CachedInstancePartial`, below,
247 has the same ability, but is slightly more efficient.
248
249 """
250 @property
251 def _partialmethod(self):
252 return functools.partialmethod(self.func, *self.args, **self.keywords)
253
254 def __get__(self, obj, obj_type):
255 return make_method(self, obj, obj_type)
256
257
258
259class CachedInstancePartial(functools.partial):
260 """The ``CachedInstancePartial`` is virtually the same as
261 :class:`InstancePartial`, adding support for method-usage to
262 :class:`functools.partial`, except that upon first access, it
263 caches the bound method on the associated object, speeding it up
264 for future accesses, and bringing the method call overhead to
265 about the same as non-``partial`` methods.
266
267 See the :class:`InstancePartial` docstring for more details.
268 """
269 @property
270 def _partialmethod(self):
271 return functools.partialmethod(self.func, *self.args, **self.keywords)
272
273 def __set_name__(self, obj_type, name):
274 self.__name__ = name
275
276 def __get__(self, obj, obj_type):
277 # These assignments could've been in __init__, but there was
278 # no simple way to do it without breaking one of PyPy or Py3.
279 self.__name__ = getattr(self, "__name__", None)
280 self.__doc__ = self.func.__doc__
281 self.__module__ = self.func.__module__
282
283 name = self.__name__
284
285 if obj is None:
286 return make_method(self, obj, obj_type)
287 try:
288 # since this is a data descriptor, this block
289 # is probably only hit once (per object)
290 return obj.__dict__[name]
291 except KeyError:
292 obj.__dict__[name] = ret = make_method(self, obj, obj_type)
293 return ret
294
295
296partial = CachedInstancePartial
297
298
299def format_invocation(name='', args=(), kwargs=None, **kw):
300 """Given a name, positional arguments, and keyword arguments, format
301 a basic Python-style function call.
302
303 >>> print(format_invocation('func', args=(1, 2), kwargs={'c': 3}))
304 func(1, 2, c=3)
305 >>> print(format_invocation('a_func', args=(1,)))
306 a_func(1)
307 >>> print(format_invocation('kw_func', kwargs=[('a', 1), ('b', 2)]))
308 kw_func(a=1, b=2)
309
310 """
311 _repr = kw.pop('repr', repr)
312 if kw:
313 raise TypeError('unexpected keyword args: %r' % ', '.join(kw.keys()))
314 kwargs = kwargs or {}
315 a_text = ', '.join([_repr(a) for a in args])
316 if isinstance(kwargs, dict):
317 kwarg_items = [(k, kwargs[k]) for k in sorted(kwargs)]
318 else:
319 kwarg_items = kwargs
320 kw_text = ', '.join([f'{k}={_repr(v)}' for k, v in kwarg_items])
321
322 all_args_text = a_text
323 if all_args_text and kw_text:
324 all_args_text += ', '
325 all_args_text += kw_text
326
327 return f'{name}({all_args_text})'
328
329
330def format_exp_repr(obj, pos_names, req_names=None, opt_names=None, opt_key=None):
331 """Render an expression-style repr of an object, based on attribute
332 names, which are assumed to line up with arguments to an initializer.
333
334 >>> class Flag(object):
335 ... def __init__(self, length, width, depth=None):
336 ... self.length = length
337 ... self.width = width
338 ... self.depth = depth
339 ...
340
341 That's our Flag object, here are some example reprs for it:
342
343 >>> flag = Flag(5, 10)
344 >>> print(format_exp_repr(flag, ['length', 'width'], [], ['depth']))
345 Flag(5, 10)
346 >>> flag2 = Flag(5, 15, 2)
347 >>> print(format_exp_repr(flag2, ['length'], ['width', 'depth']))
348 Flag(5, width=15, depth=2)
349
350 By picking the pos_names, req_names, opt_names, and opt_key, you
351 can fine-tune how you want the repr to look.
352
353 Args:
354 obj (object): The object whose type name will be used and
355 attributes will be checked
356 pos_names (list): Required list of attribute names which will be
357 rendered as positional arguments in the output repr.
358 req_names (list): List of attribute names which will always
359 appear in the keyword arguments in the output repr. Defaults to None.
360 opt_names (list): List of attribute names which may appear in
361 the keyword arguments in the output repr, provided they pass
362 the *opt_key* check. Defaults to None.
363 opt_key (callable): A function or callable which checks whether
364 an opt_name should be in the repr. Defaults to a
365 ``None``-check.
366
367 """
368 cn = type(obj).__name__
369 req_names = req_names or []
370 opt_names = opt_names or []
371 uniq_names, all_names = set(), []
372 for name in req_names + opt_names:
373 if name in uniq_names:
374 continue
375 uniq_names.add(name)
376 all_names.append(name)
377
378 if opt_key is None:
379 opt_key = lambda v: v is None
380 assert callable(opt_key)
381
382 args = [getattr(obj, name, None) for name in pos_names]
383
384 kw_items = [(name, getattr(obj, name, None)) for name in all_names]
385 kw_items = [(name, val) for name, val in kw_items
386 if not (name in opt_names and opt_key(val))]
387
388 return format_invocation(cn, args, kw_items)
389
390
391def format_nonexp_repr(obj, req_names=None, opt_names=None, opt_key=None):
392 """Format a non-expression-style repr
393
394 Some object reprs look like object instantiation, e.g., App(r=[], mw=[]).
395
396 This makes sense for smaller, lower-level objects whose state
397 roundtrips. But a lot of objects contain values that don't
398 roundtrip, like types and functions.
399
400 For those objects, there is the non-expression style repr, which
401 mimic's Python's default style to make a repr like so:
402
403 >>> class Flag(object):
404 ... def __init__(self, length, width, depth=None):
405 ... self.length = length
406 ... self.width = width
407 ... self.depth = depth
408 ...
409 >>> flag = Flag(5, 10)
410 >>> print(format_nonexp_repr(flag, ['length', 'width'], ['depth']))
411 <Flag length=5 width=10>
412
413 If no attributes are specified or set, utilizes the id, not unlike Python's
414 built-in behavior.
415
416 >>> print(format_nonexp_repr(flag))
417 <Flag id=...>
418 """
419 cn = obj.__class__.__name__
420 req_names = req_names or []
421 opt_names = opt_names or []
422 uniq_names, all_names = set(), []
423 for name in req_names + opt_names:
424 if name in uniq_names:
425 continue
426 uniq_names.add(name)
427 all_names.append(name)
428
429 if opt_key is None:
430 opt_key = lambda v: v is None
431 assert callable(opt_key)
432
433 items = [(name, getattr(obj, name, None)) for name in all_names]
434 labels = [f'{name}={val!r}' for name, val in items
435 if not (name in opt_names and opt_key(val))]
436 if not labels:
437 labels = ['id=%s' % id(obj)]
438 ret = '<{} {}>'.format(cn, ' '.join(labels))
439 return ret
440
441
442
443# # #
444# # # Function builder
445# # #
446
447
448def wraps(func, injected=None, expected=None, **kw):
449 """Decorator factory to apply update_wrapper() to a wrapper function.
450
451 Modeled after built-in :func:`functools.wraps`. Returns a decorator
452 that invokes update_wrapper() with the decorated function as the wrapper
453 argument and the arguments to wraps() as the remaining arguments.
454 Default arguments are as for update_wrapper(). This is a convenience
455 function to simplify applying partial() to update_wrapper().
456
457 Same example as in update_wrapper's doc but with wraps:
458
459 >>> from boltons.funcutils import wraps
460 >>>
461 >>> def print_return(func):
462 ... @wraps(func)
463 ... def wrapper(*args, **kwargs):
464 ... ret = func(*args, **kwargs)
465 ... print(ret)
466 ... return ret
467 ... return wrapper
468 ...
469 >>> @print_return
470 ... def example():
471 ... '''docstring'''
472 ... return 'example return value'
473 >>>
474 >>> val = example()
475 example return value
476 >>> example.__name__
477 'example'
478 >>> example.__doc__
479 'docstring'
480 """
481 return partial(update_wrapper, func=func, build_from=None,
482 injected=injected, expected=expected, **kw)
483
484
485def update_wrapper(wrapper, func, injected=None, expected=None, build_from=None, **kw):
486 """Modeled after the built-in :func:`functools.update_wrapper`,
487 this function is used to make your wrapper function reflect the
488 wrapped function's:
489
490 * Name
491 * Documentation
492 * Module
493 * Signature
494
495 The built-in :func:`functools.update_wrapper` copies the first three, but
496 does not copy the signature. This version of ``update_wrapper`` can copy
497 the inner function's signature exactly, allowing seamless usage
498 and :mod:`introspection <inspect>`. Usage is identical to the
499 built-in version::
500
501 >>> from boltons.funcutils import update_wrapper
502 >>>
503 >>> def print_return(func):
504 ... def wrapper(*args, **kwargs):
505 ... ret = func(*args, **kwargs)
506 ... print(ret)
507 ... return ret
508 ... return update_wrapper(wrapper, func)
509 ...
510 >>> @print_return
511 ... def example():
512 ... '''docstring'''
513 ... return 'example return value'
514 >>>
515 >>> val = example()
516 example return value
517 >>> example.__name__
518 'example'
519 >>> example.__doc__
520 'docstring'
521
522 In addition, the boltons version of update_wrapper supports
523 modifying the outer signature. By passing a list of
524 *injected* argument names, those arguments will be removed from
525 the outer wrapper's signature, allowing your decorator to provide
526 arguments that aren't passed in.
527
528 Args:
529
530 wrapper (function) : The callable to which the attributes of
531 *func* are to be copied.
532 func (function): The callable whose attributes are to be copied.
533 injected (list): An optional list of argument names which
534 should not appear in the new wrapper's signature.
535 expected (list): An optional list of argument names (or (name,
536 default) pairs) representing new arguments introduced by
537 the wrapper (the opposite of *injected*). See
538 :meth:`FunctionBuilder.add_arg()` for more details.
539 build_from (function): The callable from which the new wrapper
540 is built. Defaults to *func*, unless *wrapper* is partial object
541 built from *func*, in which case it defaults to *wrapper*.
542 Useful in some specific cases where *wrapper* and *func* have the
543 same arguments but differ on which are keyword-only and positional-only.
544 update_dict (bool): Whether to copy other, non-standard
545 attributes of *func* over to the wrapper. Defaults to True.
546 inject_to_varkw (bool): Ignore missing arguments when a
547 ``**kwargs``-type catch-all is present. Defaults to True.
548 hide_wrapped (bool): Remove reference to the wrapped function(s)
549 in the updated function.
550
551 In opposition to the built-in :func:`functools.update_wrapper` bolton's
552 version returns a copy of the function and does not modify anything in place.
553 For more in-depth wrapping of functions, see the
554 :class:`FunctionBuilder` type, on which update_wrapper was built.
555 """
556 if injected is None:
557 injected = []
558 elif isinstance(injected, str):
559 injected = [injected]
560 else:
561 injected = list(injected)
562
563 expected_items = _parse_wraps_expected(expected)
564
565 if isinstance(func, (classmethod, staticmethod)):
566 raise TypeError('wraps does not support wrapping classmethods and'
567 ' staticmethods, change the order of wrapping to'
568 ' wrap the underlying function: %r'
569 % (getattr(func, '__func__', None),))
570
571 update_dict = kw.pop('update_dict', True)
572 inject_to_varkw = kw.pop('inject_to_varkw', True)
573 hide_wrapped = kw.pop('hide_wrapped', False)
574 if kw:
575 raise TypeError('unexpected kwargs: %r' % kw.keys())
576
577 if isinstance(wrapper, functools.partial) and func is wrapper.func:
578 build_from = build_from or wrapper
579
580 fb = FunctionBuilder.from_func(build_from or func)
581
582 for arg in injected:
583 try:
584 fb.remove_arg(arg)
585 except MissingArgument:
586 if inject_to_varkw and fb.varkw is not None:
587 continue # keyword arg will be caught by the varkw
588 raise
589
590 for arg, default in expected_items:
591 fb.add_arg(arg, default) # may raise ExistingArgument
592
593 if fb.is_async:
594 fb.body = 'return await _call(%s)' % fb.get_invocation_str()
595 else:
596 fb.body = 'return _call(%s)' % fb.get_invocation_str()
597
598 execdict = dict(_call=wrapper, _func=func)
599 fully_wrapped = fb.get_func(execdict, with_dict=update_dict)
600
601 if hide_wrapped and hasattr(fully_wrapped, '__wrapped__'):
602 del fully_wrapped.__dict__['__wrapped__']
603 elif not hide_wrapped:
604 fully_wrapped.__wrapped__ = func # ref to the original function (#115)
605
606 return fully_wrapped
607
608
609def _parse_wraps_expected(expected):
610 # expected takes a pretty powerful argument, it's processed
611 # here. admittedly this would be less trouble if I relied on
612 # OrderedDict (there's an impl of that in the commit history if
613 # you look
614 if expected is None:
615 expected = []
616 elif isinstance(expected, str):
617 expected = [(expected, NO_DEFAULT)]
618
619 expected_items = []
620 try:
621 expected_iter = iter(expected)
622 except TypeError as e:
623 raise ValueError('"expected" takes string name, sequence of string names,'
624 ' iterable of (name, default) pairs, or a mapping of '
625 ' {name: default}, not %r (got: %r)' % (expected, e))
626 for argname in expected_iter:
627 if isinstance(argname, str):
628 # dict keys and bare strings
629 try:
630 default = expected[argname]
631 except TypeError:
632 default = NO_DEFAULT
633 else:
634 # pairs
635 try:
636 argname, default = argname
637 except (TypeError, ValueError):
638 raise ValueError('"expected" takes string name, sequence of string names,'
639 ' iterable of (name, default) pairs, or a mapping of '
640 ' {name: default}, not %r')
641 if not isinstance(argname, str):
642 raise ValueError(f'all "expected" argnames must be strings, not {argname!r}')
643
644 expected_items.append((argname, default))
645
646 return expected_items
647
648
649class FunctionBuilder:
650 """The FunctionBuilder type provides an interface for programmatically
651 creating new functions, either based on existing functions or from
652 scratch.
653
654 Values are passed in at construction or set as attributes on the
655 instance. For creating a new function based of an existing one,
656 see the :meth:`~FunctionBuilder.from_func` classmethod. At any
657 point, :meth:`~FunctionBuilder.get_func` can be called to get a
658 newly compiled function, based on the values configured.
659
660 >>> fb = FunctionBuilder('return_five', doc='returns the integer 5',
661 ... body='return 5')
662 >>> f = fb.get_func()
663 >>> f()
664 5
665 >>> fb.varkw = 'kw'
666 >>> f_kw = fb.get_func()
667 >>> f_kw(ignored_arg='ignored_val')
668 5
669
670 Note that function signatures themselves changed quite a bit in
671 Python 3, so several arguments are only applicable to
672 FunctionBuilder in Python 3. Except for *name*, all arguments to
673 the constructor are keyword arguments.
674
675 Args:
676 name (str): Name of the function.
677 doc (str): `Docstring`_ for the function, defaults to empty.
678 module (str): Name of the module from which this function was
679 imported. Defaults to None.
680 body (str): String version of the code representing the body
681 of the function. Defaults to ``'pass'``, which will result
682 in a function which does nothing and returns ``None``.
683 args (list): List of argument names, defaults to empty list,
684 denoting no arguments.
685 varargs (str): Name of the catch-all variable for positional
686 arguments. E.g., "args" if the resultant function is to have
687 ``*args`` in the signature. Defaults to None.
688 varkw (str): Name of the catch-all variable for keyword
689 arguments. E.g., "kwargs" if the resultant function is to have
690 ``**kwargs`` in the signature. Defaults to None.
691 defaults (tuple): A tuple containing default argument values for
692 those arguments that have defaults.
693 kwonlyargs (list): Argument names which are only valid as
694 keyword arguments. **Python 3 only.**
695 kwonlydefaults (dict): A mapping, same as normal *defaults*,
696 but only for the *kwonlyargs*. **Python 3 only.**
697 annotations (dict): Mapping of type hints and so
698 forth. **Python 3 only.**
699 filename (str): The filename that will appear in
700 tracebacks. Defaults to "boltons.funcutils.FunctionBuilder".
701 indent (int): Number of spaces with which to indent the
702 function *body*. Values less than 1 will result in an error.
703 dict (dict): Any other attributes which should be added to the
704 functions compiled with this FunctionBuilder.
705
706 All of these arguments are also made available as attributes which
707 can be mutated as necessary.
708
709 .. _Docstring: https://en.wikipedia.org/wiki/Docstring#Python
710
711 """
712
713 _argspec_defaults = {'args': list,
714 'varargs': lambda: None,
715 'varkw': lambda: None,
716 'defaults': lambda: None,
717 'kwonlyargs': list,
718 'kwonlydefaults': dict,
719 'annotations': dict}
720
721 @classmethod
722 def _argspec_to_dict(cls, f):
723 argspec = inspect.getfullargspec(f)
724 return {attr: getattr(argspec, attr)
725 for attr in cls._argspec_defaults}
726
727 _defaults = {'doc': str,
728 'dict': dict,
729 'is_async': lambda: False,
730 'module': lambda: None,
731 'body': lambda: 'pass',
732 'indent': lambda: 4,
733 "annotations": dict,
734 'filename': lambda: 'boltons.funcutils.FunctionBuilder'}
735
736 _defaults.update(_argspec_defaults)
737
738 _compile_count = itertools.count()
739
740 def __init__(self, name, **kw):
741 self.name = name
742 for a, default_factory in self._defaults.items():
743 val = kw.pop(a, None)
744 if val is None:
745 val = default_factory()
746 setattr(self, a, val)
747
748 if kw:
749 raise TypeError('unexpected kwargs: %r' % kw.keys())
750 return
751
752 # def get_argspec(self): # TODO
753
754 def get_sig_str(self, with_annotations=True):
755 """Return function signature as a string.
756
757 with_annotations is ignored on Python 2. On Python 3 signature
758 will omit annotations if it is set to False.
759 """
760 if with_annotations:
761 annotations = self.annotations
762 else:
763 annotations = {}
764
765 return inspect_formatargspec(self.args,
766 self.varargs,
767 self.varkw,
768 [],
769 self.kwonlyargs,
770 {},
771 annotations)
772
773 _KWONLY_MARKER = re.compile(r"""
774 \* # a star
775 \s* # followed by any amount of whitespace
776 , # followed by a comma
777 \s* # followed by any amount of whitespace
778 """, re.VERBOSE)
779
780 def get_invocation_str(self):
781 kwonly_pairs = None
782 formatters = {}
783 if self.kwonlyargs:
784 kwonly_pairs = {arg: arg
785 for arg in self.kwonlyargs}
786 formatters['formatvalue'] = lambda value: '=' + value
787
788 sig = inspect_formatargspec(self.args,
789 self.varargs,
790 self.varkw,
791 [],
792 kwonly_pairs,
793 kwonly_pairs,
794 {},
795 **formatters)
796 sig = self._KWONLY_MARKER.sub('', sig)
797 return sig[1:-1]
798
799 @classmethod
800 def from_func(cls, func):
801 """Create a new FunctionBuilder instance based on an existing
802 function. The original function will not be stored or
803 modified.
804 """
805 # TODO: copy_body? gonna need a good signature regex.
806 # TODO: might worry about __closure__?
807 if not callable(func):
808 raise TypeError(f'expected callable object, not {func!r}')
809
810 if isinstance(func, functools.partial):
811 kwargs = {'name': func.func.__name__,
812 'doc': func.func.__doc__,
813 'module': getattr(func.func, '__module__', None), # e.g., method_descriptor
814 'annotations': getattr(func.func, "__annotations__", {}),
815 'dict': getattr(func.func, '__dict__', {})}
816 else:
817 kwargs = {'name': func.__name__,
818 'doc': func.__doc__,
819 'module': getattr(func, '__module__', None), # e.g., method_descriptor
820 'annotations': getattr(func, "__annotations__", {}),
821 'dict': getattr(func, '__dict__', {})}
822
823 kwargs.update(cls._argspec_to_dict(func))
824
825 if inspect.iscoroutinefunction(func):
826 kwargs['is_async'] = True
827
828 return cls(**kwargs)
829
830 def get_func(self, execdict=None, add_source=True, with_dict=True):
831 """Compile and return a new function based on the current values of
832 the FunctionBuilder.
833
834 Args:
835 execdict (dict): The dictionary representing the scope in
836 which the compilation should take place. Defaults to an empty
837 dict.
838 add_source (bool): Whether to add the source used to a
839 special ``__source__`` attribute on the resulting
840 function. Defaults to True.
841 with_dict (bool): Add any custom attributes, if
842 applicable. Defaults to True.
843
844 To see an example of usage, see the implementation of
845 :func:`~boltons.funcutils.wraps`.
846 """
847 execdict = execdict or {}
848 body = self.body or self._default_body
849
850 tmpl = 'def {name}{sig_str}:'
851 tmpl += '\n{body}'
852
853 if self.is_async:
854 tmpl = 'async ' + tmpl
855
856 body = _indent(self.body, ' ' * self.indent)
857
858 name = self.name.replace('<', '_').replace('>', '_') # lambdas
859 src = tmpl.format(name=name, sig_str=self.get_sig_str(with_annotations=False),
860 doc=self.doc, body=body)
861 self._compile(src, execdict)
862 func = execdict[name]
863
864 func.__name__ = self.name
865 func.__doc__ = self.doc
866 func.__defaults__ = self.defaults
867 func.__kwdefaults__ = self.kwonlydefaults
868 func.__annotations__ = self.annotations
869
870 if with_dict:
871 func.__dict__.update(self.dict)
872 func.__module__ = self.module
873 # TODO: caller module fallback?
874
875 if add_source:
876 func.__source__ = src
877
878 return func
879
880 def get_defaults_dict(self):
881 """Get a dictionary of function arguments with defaults and the
882 respective values.
883 """
884 ret = dict(reversed(list(zip(reversed(self.args),
885 reversed(self.defaults or [])))))
886 kwonlydefaults = getattr(self, 'kwonlydefaults', None)
887 if kwonlydefaults:
888 ret.update(kwonlydefaults)
889 return ret
890
891 def get_arg_names(self, only_required=False):
892 arg_names = tuple(self.args) + tuple(getattr(self, 'kwonlyargs', ()))
893 if only_required:
894 defaults_dict = self.get_defaults_dict()
895 arg_names = tuple([an for an in arg_names if an not in defaults_dict])
896 return arg_names
897
898 def add_arg(self, arg_name, default=NO_DEFAULT, kwonly=False):
899 """Add an argument with optional *default* (defaults to
900 ``funcutils.NO_DEFAULT``). Pass *kwonly=True* to add a
901 keyword-only argument
902 """
903 if arg_name in self.args:
904 raise ExistingArgument(f'arg {arg_name!r} already in func {self.name} arg list')
905 if arg_name in self.kwonlyargs:
906 raise ExistingArgument(f'arg {arg_name!r} already in func {self.name} kwonly arg list')
907 if not kwonly:
908 self.args.append(arg_name)
909 if default is not NO_DEFAULT:
910 self.defaults = (self.defaults or ()) + (default,)
911 else:
912 self.kwonlyargs.append(arg_name)
913 if default is not NO_DEFAULT:
914 self.kwonlydefaults[arg_name] = default
915
916 def remove_arg(self, arg_name):
917 """Remove an argument from this FunctionBuilder's argument list. The
918 resulting function will have one less argument per call to
919 this function.
920
921 Args:
922 arg_name (str): The name of the argument to remove.
923
924 Raises a :exc:`ValueError` if the argument is not present.
925
926 """
927 args = self.args
928 d_dict = self.get_defaults_dict()
929 try:
930 args.remove(arg_name)
931 except ValueError:
932 try:
933 self.kwonlyargs.remove(arg_name)
934 except (AttributeError, ValueError):
935 # missing from both
936 exc = MissingArgument('arg %r not found in %s argument list:'
937 ' %r' % (arg_name, self.name, args))
938 exc.arg_name = arg_name
939 raise exc
940 else:
941 self.kwonlydefaults.pop(arg_name, None)
942 else:
943 d_dict.pop(arg_name, None)
944 self.defaults = tuple([d_dict[a] for a in args if a in d_dict])
945 return
946
947 def _compile(self, src, execdict):
948
949 filename = ('<%s-%d>'
950 % (self.filename, next(self._compile_count),))
951 try:
952 code = compile(src, filename, 'single')
953 exec(code, execdict)
954 except Exception:
955 raise
956 return execdict
957
958
959class MissingArgument(ValueError):
960 pass
961
962
963class ExistingArgument(ValueError):
964 pass
965
966
967def _indent(text, margin, newline='\n', key=bool):
968 "based on boltons.strutils.indent"
969 indented_lines = [(margin + line if key(line) else line)
970 for line in text.splitlines()]
971 return newline.join(indented_lines)
972
973
974from functools import total_ordering
975
976
977def noop(*args, **kwargs):
978 """
979 Simple function that should be used when no effect is desired.
980 An alternative to checking for an optional function type parameter.
981
982 e.g.
983 def decorate(func, pre_func=None, post_func=None):
984 if pre_func:
985 pre_func()
986 func()
987 if post_func:
988 post_func()
989
990 vs
991
992 def decorate(func, pre_func=noop, post_func=noop):
993 pre_func()
994 func()
995 post_func()
996 """
997 return None
998
999# end funcutils.py