Source code for coalib.settings.FunctionMetadata

from collections import OrderedDict
from copy import copy
from inspect import getfullargspec, ismethod

from coala_utils.decorators import enforce_signature
from coalib.settings.DocstringMetadata import DocstringMetadata


[docs]class FunctionMetadata: str_nodesc = 'No description given.' str_optional = "Optional, defaults to '{}'." @enforce_signature def __init__(self, name: str, desc: str = '', retval_desc: str = '', non_optional_params: (dict, None) = None, optional_params: (dict, None) = None, omit: (set, tuple, list, frozenset) = frozenset(), deprecated_params: (set, tuple, list, frozenset) = frozenset() ): """ Creates the FunctionMetadata object. :param name: The name of the function. :param desc: The description of the function. :param retval_desc: The retval description of the function. :param non_optional_params: A dict containing the name of non optional parameters as the key and a tuple of a description and the python annotation. To preserve the order, use OrderedDict. :param optional_params: A dict containing the name of optional parameters as the key and a tuple of a description, the python annotation and the default value. To preserve the order, use OrderedDict. :param omit: A set of parameters to omit. :param deprecated_params: A list of params that are deprecated. """ if non_optional_params is None: non_optional_params = OrderedDict() if optional_params is None: optional_params = OrderedDict() self.name = name self._desc = desc self.retval_desc = retval_desc self._non_optional_params = non_optional_params self._optional_params = optional_params self.omit = set(omit) self.deprecated_params = set(deprecated_params) @property def desc(self): """ Returns description of the function. """ return self._desc @desc.setter @enforce_signature def desc(self, new_desc: str): """ Set's the description to the new_desc. """ self._desc = new_desc def _filter_out_omitted(self, params): """ Filters out parameters that are to omit. This is a helper method for the param related properties. :param params: The parameter dictionary to filter. :return: The filtered dictionary. """ return OrderedDict(filter(lambda p: p[0] not in self.omit, tuple(params.items()))) @property def non_optional_params(self): """ Retrieves a dict containing the name of non optional parameters as the key and a tuple of a description and the python annotation. Values that are present in self.omit will be omitted. """ return self._filter_out_omitted(self._non_optional_params) @property def optional_params(self): """ Retrieves a dict containing the name of optional parameters as the key and a tuple of a description, the python annotation and the default value. Values that are present in self.omit will be omitted. """ return self._filter_out_omitted(self._optional_params)
[docs] def add_deprecated_param(self, original, alias): """ Adds an alias for the original setting. The alias setting will have the same metadata as the original one. If the original setting is not optional, the alias will default to ``None``. :param original: The name of the original setting. :param alias: The name of the alias for the original. :raises KeyError: If the new setting doesn't exist in the metadata. """ self.deprecated_params.add(alias) self._optional_params[alias] = ( self._optional_params[original] if original in self._optional_params else self._non_optional_params[original] + (None, ))
[docs] def create_params_from_section(self, section): """ Create a params dictionary for this function that holds all values the function needs plus optional ones that are available. :param section: The section to retrieve the values from. :return: The params dictionary. """ params = {} for param in self.non_optional_params: _, annotation = self.non_optional_params[param] params[param] = self._get_param(param, section, annotation) for param in self.optional_params: if param in section: _, annotation, _ = self.optional_params[param] params[param] = self._get_param(param, section, annotation) return params
@staticmethod def _get_param(param, section, annotation): def return_arg(x): return x if annotation is None: annotation = return_arg try: return annotation(section[param]) except (TypeError, ValueError): raise ValueError('Unable to convert parameter {!r} into type ' '{}.'.format(param, annotation))
[docs] @classmethod def from_function(cls, func, omit=frozenset()): """ Creates a FunctionMetadata object from a function. Please note that any variable argument lists are not supported. If you do not want the first (usual named 'self') argument to appear please pass the method of an actual INSTANCE of a class; passing the method of the class isn't enough. Alternatively you can add "self" to the omit set. :param func: The function. If __metadata__ of the unbound function is present it will be copied and used, otherwise it will be generated. :param omit: A set of parameter names that are to be ignored. :return: The FunctionMetadata object corresponding to the given function. """ if hasattr(func, '__metadata__'): metadata = copy(func.__metadata__) metadata.omit = omit return metadata doc = func.__doc__ or '' doc_comment = DocstringMetadata.from_docstring(doc) non_optional_params = OrderedDict() optional_params = OrderedDict() argspec = getfullargspec(func) args = () if argspec.args is None else argspec.args defaults = () if argspec.defaults is None else argspec.defaults num_non_defaults = len(args) - len(defaults) for i, arg in enumerate(args): # Implicit self argument or omitted explicitly if i < 1 and ismethod(func): continue if i < num_non_defaults: non_optional_params[arg] = ( doc_comment.param_dict.get(arg, cls.str_nodesc), argspec.annotations.get(arg, None)) else: optional_params[arg] = ( doc_comment.param_dict.get(arg, cls.str_nodesc) + ' (' + cls.str_optional.format( defaults[i-num_non_defaults]) + ')', argspec.annotations.get(arg, None), defaults[i-num_non_defaults]) return cls(name=func.__name__, desc=doc_comment.desc, retval_desc=doc_comment.retval_desc, non_optional_params=non_optional_params, optional_params=optional_params, omit=omit)
[docs] def filter_parameters(self, dct): """ Filters the given dict for keys that are declared as parameters inside this metadata (either optional or non-optional). You can use this function to safely pass parameters from a given dictionary: >>> def multiply(a, b=2, c=0): ... return a * b + c >>> metadata = FunctionMetadata.from_function(multiply) >>> args = metadata.filter_parameters({'a': 10, 'b': 20, 'd': 30}) You can safely pass the arguments to the function now: >>> multiply(**args) # 10 * 20 200 :param dct: The dict to filter. :return: A new dict containing the filtered items. """ return {key: dct[key] for key in (self.non_optional_params.keys() | self.optional_params.keys()) if key in dct}
[docs] @classmethod def merge(cls, *metadatas): """ Merges signatures of ``FunctionMetadata`` objects. Parameter (either optional or non-optional) and non-parameter descriptions are merged from left to right, meaning the right hand metadata overrides the left hand one. >>> def a(x, y): ... ''' ... desc of *a* ... :param x: x of a ... :param y: y of a ... :return: 5*x*y ... ''' ... return 5 * x * y >>> def b(x): ... ''' ... desc of *b* ... :param x: x of b ... :return: 100*x ... ''' ... return 100 * x >>> metadata1 = FunctionMetadata.from_function(a) >>> metadata2 = FunctionMetadata.from_function(b) >>> merged = FunctionMetadata.merge(metadata1, metadata2) >>> merged.name "<Merged signature of 'a', 'b'>" >>> merged.desc 'desc of *b*' >>> merged.retval_desc '100*x' >>> merged.non_optional_params['x'][0] 'x of b' >>> merged.non_optional_params['y'][0] 'y of a' :param metadatas: The sequence of metadatas to merge. :return: A ``FunctionMetadata`` object containing the merged signature of all given metadatas. """ # Collect the metadatas, as we operate on them more often and we want # to support arbitrary sequences. metadatas = tuple(metadatas) merged_name = ('<Merged signature of ' + ', '.join(repr(metadata.name) for metadata in metadatas) + '>') merged_desc = next((m.desc for m in reversed(metadatas) if m.desc), '') merged_retval_desc = next( (m.retval_desc for m in reversed(metadatas) if m.retval_desc), '') merged_non_optional_params = {} merged_optional_params = {} for metadata in metadatas: # Use the fields and not the properties to get also omitted # parameters. merged_non_optional_params.update(metadata._non_optional_params) merged_optional_params.update(metadata._optional_params) merged_omit = set.union(*(metadata.omit for metadata in metadatas)) merged_deprecated_params = set.union(*( metadata.deprecated_params for metadata in metadatas)) return cls(merged_name, merged_desc, merged_retval_desc, merged_non_optional_params, merged_optional_params, merged_omit, merged_deprecated_params)