Source code for moldesign.utils.callsigs

# Copyright 2016 Autodesk Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import functools
import inspect
import os
from functools import wraps

import collections
import funcsigs

from .utils import if_not_none
from .docparsers import GoogleDocArgumentInjector


[docs]def args_from(original_function, only=None, allexcept=None, inject_kwargs=None, inject_docs=None, wraps=None, update_docstring_args=False): """ Decorator to transfer call signatures - helps to hide ugly *args and **kwargs in delegated calls Args: original_function (callable): the function to take the call signature from only (List[str]): only transfer these arguments (incompatible with `allexcept`) wraps (bool): Transfer documentation and attributes from original_function to decorated_function, using functools.wraps (default: True if call signature is unchanged, False otherwise) allexcept (List[str]): transfer all except these arguments (incompatible with `only`) inject_kwargs (dict): Inject new kwargs into the call signature (of the form ``{argname: defaultvalue}``) inject_docs (dict): Add or modifies argument documentation (requires google-style docstrings) with a dict of the form `{argname: "(type): description"}` update_docstring_args (bool): Update "arguments" section of the docstring using the original function's documentation (requires google-style docstrings and wraps=False) Note: To use arguments from a classes' __init__ method, pass the class itself as ``original_function`` - this will also allow us to inject the documentation Returns: Decorator function """ # NEWFEATURE - verify arguments? if only and allexcept: raise ValueError('Error in keyword arguments - ' 'pass *either* "only" or "allexcept", not both') origname = get_qualified_name(original_function) if hasattr(original_function, '__signature__'): sig = original_function.__signature__.replace() else: sig = funcsigs.signature(original_function) # Modify the call signature if necessary if only or allexcept or inject_kwargs: wraps = if_not_none(wraps, False) newparams = [] if only: for param in only: newparams.append(sig.parameters[param]) elif allexcept: for name, param in sig.parameters.iteritems(): if name not in allexcept: newparams.append(param) else: newparams = sig.parameters.values() if inject_kwargs: for name, default in inject_kwargs.iteritems(): newp = funcsigs.Parameter(name, funcsigs.Parameter.POSITIONAL_OR_KEYWORD, default=default) newparams.append(newp) sig = sig.replace(parameters=newparams) else: wraps = if_not_none(wraps, True) # Get the docstring arguments if update_docstring_args: original_docs = GoogleDocArgumentInjector(original_function.__doc__) argument_docstrings = collections.OrderedDict((p.name, original_docs.args[p.name]) for p in newparams) def decorator(f): """Modify f's call signature (using the `__signature__` attribute)""" if wraps: fname = original_function.__name__ f = functools.wraps(original_function)(f) f.__name__ = fname # revert name change else: fname = f.__name__ f.__signature__ = sig if update_docstring_args or inject_kwargs: if not update_docstring_args: argument_docstrings = GoogleDocArgumentInjector(f.__doc__).args docs = GoogleDocArgumentInjector(f.__doc__) docs.args = argument_docstrings if not hasattr(f, '__orig_docs'): f.__orig_docs = [] f.__orig_docs.append(f.__doc__) f.__doc__ = docs.new_docstring() # Only for building sphinx documentation: if os.environ.get('SPHINX_IS_BUILDING_DOCS', ""): sigstring = '%s%s\n' % (fname, sig) if hasattr(f, '__doc__') and f.__doc__ is not None: f.__doc__ = sigstring + f.__doc__ else: f.__doc__ = sigstring return f return decorator
[docs]def kwargs_from(reference_function, mod_docs=True): """ Replaces ``**kwargs`` in a call signature with keyword arguments from another function. Args: reference_function (function): function to get kwargs from mod_docs (bool): whether to modify the decorated function's docstring Note: ``mod_docs`` works ONLY for google-style docstrings """ refsig = funcsigs.signature(reference_function) origname = get_qualified_name(reference_function) kwparams = [] for name, param in refsig.parameters.iteritems(): if param.default != param.empty or param.kind in (param.VAR_KEYWORD, param.KEYWORD_ONLY): if param.name[0] != '_': kwparams.append(param) if mod_docs: refdocs = GoogleDocArgumentInjector(reference_function.__doc__) def decorator(f): sig = funcsigs.signature(f) fparams = [] found_varkeyword = None for name, param in sig.parameters.iteritems(): if param.kind == param.VAR_KEYWORD: fparams.extend(kwparams) found_varkeyword = name else: fparams.append(param) if not found_varkeyword: raise TypeError("Function has no **kwargs wildcard.") f.__signature__ = sig.replace(parameters=fparams) if mod_docs: docs = GoogleDocArgumentInjector(f.__doc__) new_args = collections.OrderedDict() for argname, doc in docs.args.iteritems(): if argname == found_varkeyword: for param in kwparams: default_argdoc = '%s: argument for %s' % (param.name, origname) new_args[param.name] = refdocs.args.get(param.name, default_argdoc) else: new_args[argname] = doc docs.args = new_args if not hasattr(f, '__orig_docs'): f.__orig_docs = [] f.__orig_docs.append(f.__doc__) f.__doc__ = docs.new_docstring() return f return decorator
[docs]def get_qualified_name(original_function): if inspect.ismethod(original_function): origname = '.'.join([original_function.__module__, original_function.im_class.__name__, original_function.__name__]) return ':meth:`%s`' % origname else: origname = original_function.__module__+'.'+original_function.__name__ return ':meth:`%s`' % origname
[docs]class DocInherit(object): """ Allows methods to inherit docstrings from their superclasses FROM http://code.activestate.com/recipes/576862/ """ def __init__(self, mthd): self.mthd = mthd self.name = mthd.__name__ def __get__(self, obj, cls): if obj: return self.get_with_inst(obj, cls) else: return self.get_no_inst(cls)
[docs] def get_with_inst(self, obj, cls): overridden = getattr(super(cls, obj), self.name, None) @wraps(self.mthd, assigned=('__name__','__module__')) def f(*args, **kwargs): return self.mthd(obj, *args, **kwargs) return self.use_parent_doc(f, overridden)
[docs] def get_no_inst(self, cls): for parent in cls.__mro__[1:]: overridden = getattr(parent, self.name, None) if overridden: break @wraps(self.mthd, assigned=('__name__','__module__')) def f(*args, **kwargs): return self.mthd(*args, **kwargs) return self.use_parent_doc(f, overridden)
[docs] def use_parent_doc(self, func, source): if source is None: raise NameError, ("Can't find '%s' in parents"%self.name) func.__doc__ = source.__doc__ return func
#idiomatic decorator name doc_inherit = DocInherit