123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131 |
- # -*- coding: utf-8 -*-
- # Copyright (c) 2013, Mahmoud Hashemi
- #
- # Redistribution and use in source and binary forms, with or without
- # modification, are permitted provided that the following conditions are
- # met:
- #
- # * Redistributions of source code must retain the above copyright
- # notice, this list of conditions and the following disclaimer.
- #
- # * Redistributions in binary form must reproduce the above
- # copyright notice, this list of conditions and the following
- # disclaimer in the documentation and/or other materials provided
- # with the distribution.
- #
- # * The names of the contributors may not be used to endorse or
- # promote products derived from this software without specific
- # prior written permission.
- #
- # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- """Python's built-in :mod:`functools` module builds several useful
- utilities on top of Python's first-class function
- support. ``funcutils`` generally stays in the same vein, adding to and
- correcting Python's standard metaprogramming facilities.
- """
- from __future__ import print_function
- import sys
- import re
- import inspect
- import functools
- import itertools
- from types import MethodType, FunctionType
- try:
- xrange
- make_method = MethodType
- except NameError:
- # Python 3
- make_method = lambda desc, obj, obj_type: MethodType(desc, obj)
- basestring = (str, bytes) # Python 3 compat
- _IS_PY2 = False
- else:
- _IS_PY2 = True
- try:
- _inspect_iscoroutinefunction = inspect.iscoroutinefunction
- except AttributeError:
- # Python 3.4
- _inspect_iscoroutinefunction = lambda func: False
- try:
- from .typeutils import make_sentinel
- NO_DEFAULT = make_sentinel(var_name='NO_DEFAULT')
- except ImportError:
- NO_DEFAULT = object()
- try:
- from functools import partialmethod
- except ImportError:
- partialmethod = None
- _IS_PY35 = sys.version_info >= (3, 5)
- if not _IS_PY35:
- # py35+ wants you to use signature instead, but
- # inspect_formatargspec is way simpler for what it is. Copied the
- # vendoring approach from alembic:
- # https://github.com/sqlalchemy/alembic/blob/4cdad6aec32b4b5573a2009cc356cb4b144bd359/alembic/util/compat.py#L92
- from inspect import formatargspec as inspect_formatargspec
- else:
- from inspect import formatannotation
- def inspect_formatargspec(
- args, varargs=None, varkw=None, defaults=None,
- kwonlyargs=(), kwonlydefaults={}, annotations={},
- formatarg=str,
- formatvarargs=lambda name: '*' + name,
- formatvarkw=lambda name: '**' + name,
- formatvalue=lambda value: '=' + repr(value),
- formatreturns=lambda text: ' -> ' + text,
- formatannotation=formatannotation):
- """Copy formatargspec from python 3.7 standard library.
- Python 3 has deprecated formatargspec and requested that Signature
- be used instead, however this requires a full reimplementation
- of formatargspec() in terms of creating Parameter objects and such.
- Instead of introducing all the object-creation overhead and having
- to reinvent from scratch, just copy their compatibility routine.
- """
- def formatargandannotation(arg):
- result = formatarg(arg)
- if arg in annotations:
- result += ': ' + formatannotation(annotations[arg])
- return result
- specs = []
- if defaults:
- firstdefault = len(args) - len(defaults)
- for i, arg in enumerate(args):
- spec = formatargandannotation(arg)
- if defaults and i >= firstdefault:
- spec = spec + formatvalue(defaults[i - firstdefault])
- specs.append(spec)
- if varargs is not None:
- specs.append(formatvarargs(formatargandannotation(varargs)))
- else:
- if kwonlyargs:
- specs.append('*')
- if kwonlyargs:
- for kwonlyarg in kwonlyargs:
- spec = formatargandannotation(kwonlyarg)
- if kwonlydefaults and kwonlyarg in kwonlydefaults:
- spec += formatvalue(kwonlydefaults[kwonlyarg])
- specs.append(spec)
- if varkw is not None:
- specs.append(formatvarkw(formatargandannotation(varkw)))
- result = '(' + ', '.join(specs) + ')'
- if 'return' in annotations:
- result += formatreturns(formatannotation(annotations['return']))
- return result
- def get_module_callables(mod, ignore=None):
- """Returns two maps of (*types*, *funcs*) from *mod*, optionally
- ignoring based on the :class:`bool` return value of the *ignore*
- callable. *mod* can be a string name of a module in
- :data:`sys.modules` or the module instance itself.
- """
- if isinstance(mod, basestring):
- mod = sys.modules[mod]
- types, funcs = {}, {}
- for attr_name in dir(mod):
- if ignore and ignore(attr_name):
- continue
- try:
- attr = getattr(mod, attr_name)
- except Exception:
- continue
- try:
- attr_mod_name = attr.__module__
- except AttributeError:
- continue
- if attr_mod_name != mod.__name__:
- continue
- if isinstance(attr, type):
- types[attr_name] = attr
- elif callable(attr):
- funcs[attr_name] = attr
- return types, funcs
- def mro_items(type_obj):
- """Takes a type and returns an iterator over all class variables
- throughout the type hierarchy (respecting the MRO).
- >>> sorted(set([k for k, v in mro_items(int) if not k.startswith('__') and 'bytes' not in k and not callable(v)]))
- ['denominator', 'imag', 'numerator', 'real']
- """
- # TODO: handle slots?
- return itertools.chain.from_iterable(ct.__dict__.items()
- for ct in type_obj.__mro__)
- def dir_dict(obj, raise_exc=False):
- """Return a dictionary of attribute names to values for a given
- object. Unlike ``obj.__dict__``, this function returns all
- attributes on the object, including ones on parent classes.
- """
- # TODO: separate function for handling descriptors on types?
- ret = {}
- for k in dir(obj):
- try:
- ret[k] = getattr(obj, k)
- except Exception:
- if raise_exc:
- raise
- return ret
- def copy_function(orig, copy_dict=True):
- """Returns a shallow copy of the function, including code object,
- globals, closure, etc.
- >>> func = lambda: func
- >>> func() is func
- True
- >>> func_copy = copy_function(func)
- >>> func_copy() is func
- True
- >>> func_copy is not func
- True
- Args:
- orig (function): The function to be copied. Must be a
- function, not just any method or callable.
- copy_dict (bool): Also copy any attributes set on the function
- instance. Defaults to ``True``.
- """
- ret = FunctionType(orig.__code__,
- orig.__globals__,
- name=orig.__name__,
- argdefs=getattr(orig, "__defaults__", None),
- closure=getattr(orig, "__closure__", None))
- if copy_dict:
- ret.__dict__.update(orig.__dict__)
- return ret
- def partial_ordering(cls):
- """Class decorator, similar to :func:`functools.total_ordering`,
- except it is used to define `partial orderings`_ (i.e., it is
- possible that *x* is neither greater than, equal to, or less than
- *y*). It assumes the presence of the ``__le__()`` and ``__ge__()``
- method, but nothing else. It will not override any existing
- additional comparison methods.
- .. _partial orderings: https://en.wikipedia.org/wiki/Partially_ordered_set
- >>> @partial_ordering
- ... class MySet(set):
- ... def __le__(self, other):
- ... return self.issubset(other)
- ... def __ge__(self, other):
- ... return self.issuperset(other)
- ...
- >>> a = MySet([1,2,3])
- >>> b = MySet([1,2])
- >>> c = MySet([1,2,4])
- >>> b < a
- True
- >>> b > a
- False
- >>> b < c
- True
- >>> a < c
- False
- >>> c > a
- False
- """
- def __lt__(self, other): return self <= other and not self >= other
- def __gt__(self, other): return self >= other and not self <= other
- def __eq__(self, other): return self >= other and self <= other
- if not hasattr(cls, '__lt__'): cls.__lt__ = __lt__
- if not hasattr(cls, '__gt__'): cls.__gt__ = __gt__
- if not hasattr(cls, '__eq__'): cls.__eq__ = __eq__
- return cls
- class InstancePartial(functools.partial):
- """:class:`functools.partial` is a huge convenience for anyone
- working with Python's great first-class functions. It allows
- developers to curry arguments and incrementally create simpler
- callables for a variety of use cases.
- Unfortunately there's one big gap in its usefulness:
- methods. Partials just don't get bound as methods and
- automatically handed a reference to ``self``. The
- ``InstancePartial`` type remedies this by inheriting from
- :class:`functools.partial` and implementing the necessary
- descriptor protocol. There are no other differences in
- implementation or usage. :class:`CachedInstancePartial`, below,
- has the same ability, but is slightly more efficient.
- """
- if partialmethod is not None: # NB: See https://github.com/mahmoud/boltons/pull/244
- @property
- def _partialmethod(self):
- return partialmethod(self.func, *self.args, **self.keywords)
- def __get__(self, obj, obj_type):
- return make_method(self, obj, obj_type)
- class CachedInstancePartial(functools.partial):
- """The ``CachedInstancePartial`` is virtually the same as
- :class:`InstancePartial`, adding support for method-usage to
- :class:`functools.partial`, except that upon first access, it
- caches the bound method on the associated object, speeding it up
- for future accesses, and bringing the method call overhead to
- about the same as non-``partial`` methods.
- See the :class:`InstancePartial` docstring for more details.
- """
- if partialmethod is not None: # NB: See https://github.com/mahmoud/boltons/pull/244
- @property
- def _partialmethod(self):
- return partialmethod(self.func, *self.args, **self.keywords)
- if sys.version_info >= (3, 6):
- def __set_name__(self, obj_type, name):
- self.__name__ = name
- def __get__(self, obj, obj_type):
- # These assignments could've been in __init__, but there was
- # no simple way to do it without breaking one of PyPy or Py3.
- self.__name__ = getattr(self, "__name__", None)
- self.__doc__ = self.func.__doc__
- self.__module__ = self.func.__module__
- name = self.__name__
- # if you're on python 3.6+, name will never be `None` bc `__set_name__` sets it when descriptor getting assigned
- if name is None:
- for k, v in mro_items(obj_type):
- if v is self:
- self.__name__ = name = k
- if obj is None:
- return make_method(self, obj, obj_type)
- try:
- # since this is a data descriptor, this block
- # is probably only hit once (per object)
- return obj.__dict__[name]
- except KeyError:
- obj.__dict__[name] = ret = make_method(self, obj, obj_type)
- return ret
- partial = CachedInstancePartial
- def format_invocation(name='', args=(), kwargs=None, **kw):
- """Given a name, positional arguments, and keyword arguments, format
- a basic Python-style function call.
- >>> print(format_invocation('func', args=(1, 2), kwargs={'c': 3}))
- func(1, 2, c=3)
- >>> print(format_invocation('a_func', args=(1,)))
- a_func(1)
- >>> print(format_invocation('kw_func', kwargs=[('a', 1), ('b', 2)]))
- kw_func(a=1, b=2)
- """
- _repr = kw.pop('repr', repr)
- if kw:
- raise TypeError('unexpected keyword args: %r' % ', '.join(kw.keys()))
- kwargs = kwargs or {}
- a_text = ', '.join([_repr(a) for a in args])
- if isinstance(kwargs, dict):
- kwarg_items = [(k, kwargs[k]) for k in sorted(kwargs)]
- else:
- kwarg_items = kwargs
- kw_text = ', '.join(['%s=%s' % (k, _repr(v)) for k, v in kwarg_items])
- all_args_text = a_text
- if all_args_text and kw_text:
- all_args_text += ', '
- all_args_text += kw_text
- return '%s(%s)' % (name, all_args_text)
- def format_exp_repr(obj, pos_names, req_names=None, opt_names=None, opt_key=None):
- """Render an expression-style repr of an object, based on attribute
- names, which are assumed to line up with arguments to an initializer.
- >>> class Flag(object):
- ... def __init__(self, length, width, depth=None):
- ... self.length = length
- ... self.width = width
- ... self.depth = depth
- ...
- That's our Flag object, here are some example reprs for it:
- >>> flag = Flag(5, 10)
- >>> print(format_exp_repr(flag, ['length', 'width'], [], ['depth']))
- Flag(5, 10)
- >>> flag2 = Flag(5, 15, 2)
- >>> print(format_exp_repr(flag2, ['length'], ['width', 'depth']))
- Flag(5, width=15, depth=2)
- By picking the pos_names, req_names, opt_names, and opt_key, you
- can fine-tune how you want the repr to look.
- Args:
- obj (object): The object whose type name will be used and
- attributes will be checked
- pos_names (list): Required list of attribute names which will be
- rendered as positional arguments in the output repr.
- req_names (list): List of attribute names which will always
- appear in the keyword arguments in the output repr. Defaults to None.
- opt_names (list): List of attribute names which may appear in
- the keyword arguments in the output repr, provided they pass
- the *opt_key* check. Defaults to None.
- opt_key (callable): A function or callable which checks whether
- an opt_name should be in the repr. Defaults to a
- ``None``-check.
- """
- cn = type(obj).__name__
- req_names = req_names or []
- opt_names = opt_names or []
- uniq_names, all_names = set(), []
- for name in req_names + opt_names:
- if name in uniq_names:
- continue
- uniq_names.add(name)
- all_names.append(name)
- if opt_key is None:
- opt_key = lambda v: v is None
- assert callable(opt_key)
- args = [getattr(obj, name, None) for name in pos_names]
- kw_items = [(name, getattr(obj, name, None)) for name in all_names]
- kw_items = [(name, val) for name, val in kw_items
- if not (name in opt_names and opt_key(val))]
- return format_invocation(cn, args, kw_items)
- def format_nonexp_repr(obj, req_names=None, opt_names=None, opt_key=None):
- """Format a non-expression-style repr
- Some object reprs look like object instantiation, e.g., App(r=[], mw=[]).
- This makes sense for smaller, lower-level objects whose state
- roundtrips. But a lot of objects contain values that don't
- roundtrip, like types and functions.
- For those objects, there is the non-expression style repr, which
- mimic's Python's default style to make a repr like so:
- >>> class Flag(object):
- ... def __init__(self, length, width, depth=None):
- ... self.length = length
- ... self.width = width
- ... self.depth = depth
- ...
- >>> flag = Flag(5, 10)
- >>> print(format_nonexp_repr(flag, ['length', 'width'], ['depth']))
- <Flag length=5 width=10>
- If no attributes are specified or set, utilizes the id, not unlike Python's
- built-in behavior.
- >>> print(format_nonexp_repr(flag))
- <Flag id=...>
- """
- cn = obj.__class__.__name__
- req_names = req_names or []
- opt_names = opt_names or []
- uniq_names, all_names = set(), []
- for name in req_names + opt_names:
- if name in uniq_names:
- continue
- uniq_names.add(name)
- all_names.append(name)
- if opt_key is None:
- opt_key = lambda v: v is None
- assert callable(opt_key)
- items = [(name, getattr(obj, name, None)) for name in all_names]
- labels = ['%s=%r' % (name, val) for name, val in items
- if not (name in opt_names and opt_key(val))]
- if not labels:
- labels = ['id=%s' % id(obj)]
- ret = '<%s %s>' % (cn, ' '.join(labels))
- return ret
- # # #
- # # # Function builder
- # # #
- def wraps(func, injected=None, expected=None, **kw):
- """Decorator factory to apply update_wrapper() to a wrapper function.
- Modeled after built-in :func:`functools.wraps`. Returns a decorator
- that invokes update_wrapper() with the decorated function as the wrapper
- argument and the arguments to wraps() as the remaining arguments.
- Default arguments are as for update_wrapper(). This is a convenience
- function to simplify applying partial() to update_wrapper().
- Same example as in update_wrapper's doc but with wraps:
- >>> from boltons.funcutils import wraps
- >>>
- >>> def print_return(func):
- ... @wraps(func)
- ... def wrapper(*args, **kwargs):
- ... ret = func(*args, **kwargs)
- ... print(ret)
- ... return ret
- ... return wrapper
- ...
- >>> @print_return
- ... def example():
- ... '''docstring'''
- ... return 'example return value'
- >>>
- >>> val = example()
- example return value
- >>> example.__name__
- 'example'
- >>> example.__doc__
- 'docstring'
- """
- return partial(update_wrapper, func=func, build_from=None,
- injected=injected, expected=expected, **kw)
- def update_wrapper(wrapper, func, injected=None, expected=None, build_from=None, **kw):
- """Modeled after the built-in :func:`functools.update_wrapper`,
- this function is used to make your wrapper function reflect the
- wrapped function's:
- * Name
- * Documentation
- * Module
- * Signature
- The built-in :func:`functools.update_wrapper` copies the first three, but
- does not copy the signature. This version of ``update_wrapper`` can copy
- the inner function's signature exactly, allowing seamless usage
- and :mod:`introspection <inspect>`. Usage is identical to the
- built-in version::
- >>> from boltons.funcutils import update_wrapper
- >>>
- >>> def print_return(func):
- ... def wrapper(*args, **kwargs):
- ... ret = func(*args, **kwargs)
- ... print(ret)
- ... return ret
- ... return update_wrapper(wrapper, func)
- ...
- >>> @print_return
- ... def example():
- ... '''docstring'''
- ... return 'example return value'
- >>>
- >>> val = example()
- example return value
- >>> example.__name__
- 'example'
- >>> example.__doc__
- 'docstring'
- In addition, the boltons version of update_wrapper supports
- modifying the outer signature. By passing a list of
- *injected* argument names, those arguments will be removed from
- the outer wrapper's signature, allowing your decorator to provide
- arguments that aren't passed in.
- Args:
- wrapper (function) : The callable to which the attributes of
- *func* are to be copied.
- func (function): The callable whose attributes are to be copied.
- injected (list): An optional list of argument names which
- should not appear in the new wrapper's signature.
- expected (list): An optional list of argument names (or (name,
- default) pairs) representing new arguments introduced by
- the wrapper (the opposite of *injected*). See
- :meth:`FunctionBuilder.add_arg()` for more details.
- build_from (function): The callable from which the new wrapper
- is built. Defaults to *func*, unless *wrapper* is partial object
- built from *func*, in which case it defaults to *wrapper*.
- Useful in some specific cases where *wrapper* and *func* have the
- same arguments but differ on which are keyword-only and positional-only.
- update_dict (bool): Whether to copy other, non-standard
- attributes of *func* over to the wrapper. Defaults to True.
- inject_to_varkw (bool): Ignore missing arguments when a
- ``**kwargs``-type catch-all is present. Defaults to True.
- hide_wrapped (bool): Remove reference to the wrapped function(s)
- in the updated function.
- In opposition to the built-in :func:`functools.update_wrapper` bolton's
- version returns a copy of the function and does not modify anything in place.
- For more in-depth wrapping of functions, see the
- :class:`FunctionBuilder` type, on which update_wrapper was built.
- """
- if injected is None:
- injected = []
- elif isinstance(injected, basestring):
- injected = [injected]
- else:
- injected = list(injected)
- expected_items = _parse_wraps_expected(expected)
- if isinstance(func, (classmethod, staticmethod)):
- raise TypeError('wraps does not support wrapping classmethods and'
- ' staticmethods, change the order of wrapping to'
- ' wrap the underlying function: %r'
- % (getattr(func, '__func__', None),))
- update_dict = kw.pop('update_dict', True)
- inject_to_varkw = kw.pop('inject_to_varkw', True)
- hide_wrapped = kw.pop('hide_wrapped', False)
- if kw:
- raise TypeError('unexpected kwargs: %r' % kw.keys())
- if isinstance(wrapper, functools.partial) and func is wrapper.func:
- build_from = build_from or wrapper
- fb = FunctionBuilder.from_func(build_from or func)
- for arg in injected:
- try:
- fb.remove_arg(arg)
- except MissingArgument:
- if inject_to_varkw and fb.varkw is not None:
- continue # keyword arg will be caught by the varkw
- raise
- for arg, default in expected_items:
- fb.add_arg(arg, default) # may raise ExistingArgument
- if fb.is_async:
- fb.body = 'return await _call(%s)' % fb.get_invocation_str()
- else:
- fb.body = 'return _call(%s)' % fb.get_invocation_str()
- execdict = dict(_call=wrapper, _func=func)
- fully_wrapped = fb.get_func(execdict, with_dict=update_dict)
- if hide_wrapped and hasattr(fully_wrapped, '__wrapped__'):
- del fully_wrapped.__dict__['__wrapped__']
- elif not hide_wrapped:
- fully_wrapped.__wrapped__ = func # ref to the original function (#115)
- return fully_wrapped
- def _parse_wraps_expected(expected):
- # expected takes a pretty powerful argument, it's processed
- # here. admittedly this would be less trouble if I relied on
- # OrderedDict (there's an impl of that in the commit history if
- # you look
- if expected is None:
- expected = []
- elif isinstance(expected, basestring):
- expected = [(expected, NO_DEFAULT)]
- expected_items = []
- try:
- expected_iter = iter(expected)
- except TypeError as e:
- raise ValueError('"expected" takes string name, sequence of string names,'
- ' iterable of (name, default) pairs, or a mapping of '
- ' {name: default}, not %r (got: %r)' % (expected, e))
- for argname in expected_iter:
- if isinstance(argname, basestring):
- # dict keys and bare strings
- try:
- default = expected[argname]
- except TypeError:
- default = NO_DEFAULT
- else:
- # pairs
- try:
- argname, default = argname
- except (TypeError, ValueError):
- raise ValueError('"expected" takes string name, sequence of string names,'
- ' iterable of (name, default) pairs, or a mapping of '
- ' {name: default}, not %r')
- if not isinstance(argname, basestring):
- raise ValueError('all "expected" argnames must be strings, not %r' % (argname,))
- expected_items.append((argname, default))
- return expected_items
- class FunctionBuilder(object):
- """The FunctionBuilder type provides an interface for programmatically
- creating new functions, either based on existing functions or from
- scratch.
- Values are passed in at construction or set as attributes on the
- instance. For creating a new function based of an existing one,
- see the :meth:`~FunctionBuilder.from_func` classmethod. At any
- point, :meth:`~FunctionBuilder.get_func` can be called to get a
- newly compiled function, based on the values configured.
- >>> fb = FunctionBuilder('return_five', doc='returns the integer 5',
- ... body='return 5')
- >>> f = fb.get_func()
- >>> f()
- 5
- >>> fb.varkw = 'kw'
- >>> f_kw = fb.get_func()
- >>> f_kw(ignored_arg='ignored_val')
- 5
- Note that function signatures themselves changed quite a bit in
- Python 3, so several arguments are only applicable to
- FunctionBuilder in Python 3. Except for *name*, all arguments to
- the constructor are keyword arguments.
- Args:
- name (str): Name of the function.
- doc (str): `Docstring`_ for the function, defaults to empty.
- module (str): Name of the module from which this function was
- imported. Defaults to None.
- body (str): String version of the code representing the body
- of the function. Defaults to ``'pass'``, which will result
- in a function which does nothing and returns ``None``.
- args (list): List of argument names, defaults to empty list,
- denoting no arguments.
- varargs (str): Name of the catch-all variable for positional
- arguments. E.g., "args" if the resultant function is to have
- ``*args`` in the signature. Defaults to None.
- varkw (str): Name of the catch-all variable for keyword
- arguments. E.g., "kwargs" if the resultant function is to have
- ``**kwargs`` in the signature. Defaults to None.
- defaults (tuple): A tuple containing default argument values for
- those arguments that have defaults.
- kwonlyargs (list): Argument names which are only valid as
- keyword arguments. **Python 3 only.**
- kwonlydefaults (dict): A mapping, same as normal *defaults*,
- but only for the *kwonlyargs*. **Python 3 only.**
- annotations (dict): Mapping of type hints and so
- forth. **Python 3 only.**
- filename (str): The filename that will appear in
- tracebacks. Defaults to "boltons.funcutils.FunctionBuilder".
- indent (int): Number of spaces with which to indent the
- function *body*. Values less than 1 will result in an error.
- dict (dict): Any other attributes which should be added to the
- functions compiled with this FunctionBuilder.
- All of these arguments are also made available as attributes which
- can be mutated as necessary.
- .. _Docstring: https://en.wikipedia.org/wiki/Docstring#Python
- """
- if _IS_PY2:
- _argspec_defaults = {'args': list,
- 'varargs': lambda: None,
- 'varkw': lambda: None,
- 'defaults': lambda: None}
- @classmethod
- def _argspec_to_dict(cls, f):
- args, varargs, varkw, defaults = inspect.getargspec(f)
- return {'args': args,
- 'varargs': varargs,
- 'varkw': varkw,
- 'defaults': defaults}
- else:
- _argspec_defaults = {'args': list,
- 'varargs': lambda: None,
- 'varkw': lambda: None,
- 'defaults': lambda: None,
- 'kwonlyargs': list,
- 'kwonlydefaults': dict,
- 'annotations': dict}
- @classmethod
- def _argspec_to_dict(cls, f):
- argspec = inspect.getfullargspec(f)
- return dict((attr, getattr(argspec, attr))
- for attr in cls._argspec_defaults)
- _defaults = {'doc': str,
- 'dict': dict,
- 'is_async': lambda: False,
- 'module': lambda: None,
- 'body': lambda: 'pass',
- 'indent': lambda: 4,
- "annotations": dict,
- 'filename': lambda: 'boltons.funcutils.FunctionBuilder'}
- _defaults.update(_argspec_defaults)
- _compile_count = itertools.count()
- def __init__(self, name, **kw):
- self.name = name
- for a, default_factory in self._defaults.items():
- val = kw.pop(a, None)
- if val is None:
- val = default_factory()
- setattr(self, a, val)
- if kw:
- raise TypeError('unexpected kwargs: %r' % kw.keys())
- return
- # def get_argspec(self): # TODO
- if _IS_PY2:
- def get_sig_str(self, with_annotations=True):
- """Return function signature as a string.
- with_annotations is ignored on Python 2. On Python 3 signature
- will omit annotations if it is set to False.
- """
- return inspect_formatargspec(self.args, self.varargs,
- self.varkw, [])
- def get_invocation_str(self):
- return inspect_formatargspec(self.args, self.varargs,
- self.varkw, [])[1:-1]
- else:
- def get_sig_str(self, with_annotations=True):
- """Return function signature as a string.
- with_annotations is ignored on Python 2. On Python 3 signature
- will omit annotations if it is set to False.
- """
- if with_annotations:
- annotations = self.annotations
- else:
- annotations = {}
- return inspect_formatargspec(self.args,
- self.varargs,
- self.varkw,
- [],
- self.kwonlyargs,
- {},
- annotations)
- _KWONLY_MARKER = re.compile(r"""
- \* # a star
- \s* # followed by any amount of whitespace
- , # followed by a comma
- \s* # followed by any amount of whitespace
- """, re.VERBOSE)
- def get_invocation_str(self):
- kwonly_pairs = None
- formatters = {}
- if self.kwonlyargs:
- kwonly_pairs = dict((arg, arg)
- for arg in self.kwonlyargs)
- formatters['formatvalue'] = lambda value: '=' + value
- sig = inspect_formatargspec(self.args,
- self.varargs,
- self.varkw,
- [],
- kwonly_pairs,
- kwonly_pairs,
- {},
- **formatters)
- sig = self._KWONLY_MARKER.sub('', sig)
- return sig[1:-1]
- @classmethod
- def from_func(cls, func):
- """Create a new FunctionBuilder instance based on an existing
- function. The original function will not be stored or
- modified.
- """
- # TODO: copy_body? gonna need a good signature regex.
- # TODO: might worry about __closure__?
- if not callable(func):
- raise TypeError('expected callable object, not %r' % (func,))
- if isinstance(func, functools.partial):
- if _IS_PY2:
- raise ValueError('Cannot build FunctionBuilder instances from partials in python 2.')
- kwargs = {'name': func.func.__name__,
- 'doc': func.func.__doc__,
- 'module': getattr(func.func, '__module__', None), # e.g., method_descriptor
- 'annotations': getattr(func.func, "__annotations__", {}),
- 'dict': getattr(func.func, '__dict__', {})}
- else:
- kwargs = {'name': func.__name__,
- 'doc': func.__doc__,
- 'module': getattr(func, '__module__', None), # e.g., method_descriptor
- 'annotations': getattr(func, "__annotations__", {}),
- 'dict': getattr(func, '__dict__', {})}
- kwargs.update(cls._argspec_to_dict(func))
- if _inspect_iscoroutinefunction(func):
- kwargs['is_async'] = True
- return cls(**kwargs)
- def get_func(self, execdict=None, add_source=True, with_dict=True):
- """Compile and return a new function based on the current values of
- the FunctionBuilder.
- Args:
- execdict (dict): The dictionary representing the scope in
- which the compilation should take place. Defaults to an empty
- dict.
- add_source (bool): Whether to add the source used to a
- special ``__source__`` attribute on the resulting
- function. Defaults to True.
- with_dict (bool): Add any custom attributes, if
- applicable. Defaults to True.
- To see an example of usage, see the implementation of
- :func:`~boltons.funcutils.wraps`.
- """
- execdict = execdict or {}
- body = self.body or self._default_body
- tmpl = 'def {name}{sig_str}:'
- tmpl += '\n{body}'
- if self.is_async:
- tmpl = 'async ' + tmpl
- body = _indent(self.body, ' ' * self.indent)
- name = self.name.replace('<', '_').replace('>', '_') # lambdas
- src = tmpl.format(name=name, sig_str=self.get_sig_str(with_annotations=False),
- doc=self.doc, body=body)
- self._compile(src, execdict)
- func = execdict[name]
- func.__name__ = self.name
- func.__doc__ = self.doc
- func.__defaults__ = self.defaults
- if not _IS_PY2:
- func.__kwdefaults__ = self.kwonlydefaults
- func.__annotations__ = self.annotations
- if with_dict:
- func.__dict__.update(self.dict)
- func.__module__ = self.module
- # TODO: caller module fallback?
- if add_source:
- func.__source__ = src
- return func
- def get_defaults_dict(self):
- """Get a dictionary of function arguments with defaults and the
- respective values.
- """
- ret = dict(reversed(list(zip(reversed(self.args),
- reversed(self.defaults or [])))))
- kwonlydefaults = getattr(self, 'kwonlydefaults', None)
- if kwonlydefaults:
- ret.update(kwonlydefaults)
- return ret
- def get_arg_names(self, only_required=False):
- arg_names = tuple(self.args) + tuple(getattr(self, 'kwonlyargs', ()))
- if only_required:
- defaults_dict = self.get_defaults_dict()
- arg_names = tuple([an for an in arg_names if an not in defaults_dict])
- return arg_names
- if _IS_PY2:
- def add_arg(self, arg_name, default=NO_DEFAULT):
- "Add an argument with optional *default* (defaults to ``funcutils.NO_DEFAULT``)."
- if arg_name in self.args:
- raise ExistingArgument('arg %r already in func %s arg list' % (arg_name, self.name))
- self.args.append(arg_name)
- if default is not NO_DEFAULT:
- self.defaults = (self.defaults or ()) + (default,)
- return
- else:
- def add_arg(self, arg_name, default=NO_DEFAULT, kwonly=False):
- """Add an argument with optional *default* (defaults to
- ``funcutils.NO_DEFAULT``). Pass *kwonly=True* to add a
- keyword-only argument
- """
- if arg_name in self.args:
- raise ExistingArgument('arg %r already in func %s arg list' % (arg_name, self.name))
- if arg_name in self.kwonlyargs:
- raise ExistingArgument('arg %r already in func %s kwonly arg list' % (arg_name, self.name))
- if not kwonly:
- self.args.append(arg_name)
- if default is not NO_DEFAULT:
- self.defaults = (self.defaults or ()) + (default,)
- else:
- self.kwonlyargs.append(arg_name)
- if default is not NO_DEFAULT:
- self.kwonlydefaults[arg_name] = default
- return
- def remove_arg(self, arg_name):
- """Remove an argument from this FunctionBuilder's argument list. The
- resulting function will have one less argument per call to
- this function.
- Args:
- arg_name (str): The name of the argument to remove.
- Raises a :exc:`ValueError` if the argument is not present.
- """
- args = self.args
- d_dict = self.get_defaults_dict()
- try:
- args.remove(arg_name)
- except ValueError:
- try:
- self.kwonlyargs.remove(arg_name)
- except (AttributeError, ValueError):
- # py2, or py3 and missing from both
- exc = MissingArgument('arg %r not found in %s argument list:'
- ' %r' % (arg_name, self.name, args))
- exc.arg_name = arg_name
- raise exc
- else:
- self.kwonlydefaults.pop(arg_name, None)
- else:
- d_dict.pop(arg_name, None)
- self.defaults = tuple([d_dict[a] for a in args if a in d_dict])
- return
- def _compile(self, src, execdict):
- filename = ('<%s-%d>'
- % (self.filename, next(self._compile_count),))
- try:
- code = compile(src, filename, 'single')
- exec(code, execdict)
- except Exception:
- raise
- return execdict
- class MissingArgument(ValueError):
- pass
- class ExistingArgument(ValueError):
- pass
- def _indent(text, margin, newline='\n', key=bool):
- "based on boltons.strutils.indent"
- indented_lines = [(margin + line if key(line) else line)
- for line in text.splitlines()]
- return newline.join(indented_lines)
- try:
- from functools import total_ordering # 2.7+
- except ImportError:
- # python 2.6
- def total_ordering(cls):
- """Class decorator that fills in missing comparators/ordering
- methods. Backport of :func:`functools.total_ordering` to work
- with Python 2.6.
- Code from http://code.activestate.com/recipes/576685/
- """
- convert = {
- '__lt__': [
- ('__gt__',
- lambda self, other: not (self < other or self == other)),
- ('__le__',
- lambda self, other: self < other or self == other),
- ('__ge__',
- lambda self, other: not self < other)],
- '__le__': [
- ('__ge__',
- lambda self, other: not self <= other or self == other),
- ('__lt__',
- lambda self, other: self <= other and not self == other),
- ('__gt__',
- lambda self, other: not self <= other)],
- '__gt__': [
- ('__lt__',
- lambda self, other: not (self > other or self == other)),
- ('__ge__',
- lambda self, other: self > other or self == other),
- ('__le__',
- lambda self, other: not self > other)],
- '__ge__': [
- ('__le__',
- lambda self, other: (not self >= other) or self == other),
- ('__gt__',
- lambda self, other: self >= other and not self == other),
- ('__lt__',
- lambda self, other: not self >= other)]
- }
- roots = set(dir(cls)) & set(convert)
- if not roots:
- raise ValueError('must define at least one ordering operation:'
- ' < > <= >=')
- root = max(roots) # prefer __lt__ to __le__ to __gt__ to __ge__
- for opname, opfunc in convert[root]:
- if opname not in roots:
- opfunc.__name__ = opname
- opfunc.__doc__ = getattr(int, opname).__doc__
- setattr(cls, opname, opfunc)
- return cls
- def noop(*args, **kwargs):
- """
- Simple function that should be used when no effect is desired.
- An alternative to checking for an optional function type parameter.
- e.g.
- def decorate(func, pre_func=None, post_func=None):
- if pre_func:
- pre_func()
- func()
- if post_func:
- post_func()
- vs
- def decorate(func, pre_func=noop, post_func=noop):
- pre_func()
- func()
- post_func()
- """
- return None
- # end funcutils.py
|