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