Source code for doctor.utils

import functools
import inspect
import logging
import os
import re
import sys
import types
from copy import copy
from inspect import Parameter, Signature
from typing import Callable, List

try:
    from sphinx.util.docstrings import prepare_docstring
except ImportError:
    prepare_docstring = None

from doctor.types import SuperType

#: Used to identify the end of the description block, and the beginning of the
#: parameters. This assumes that the parameters and such will always occur at
#: the end of the docstring.
DESCRIPTION_END_RE = re.compile(':(arg|param|returns|throws)', re.I)


[docs]def copy_func(func: Callable) -> Callable: """Returns a copy of a function. :param func: The function to copy. :returns: The copied function. """ copied = types.FunctionType( func.__code__, func.__globals__, name=func.__name__, argdefs=func.__defaults__, closure=func.__closure__) copied = functools.update_wrapper(copied, func) copied.__kwdefaults__ = func.__kwdefaults__ return copied
[docs]class RequestParamAnnotation(object): """Represents a new request parameter annotation. :param name: The name of the parameter. :param annotation: The annotation type of the parameter. :type annotation: A doctor type that should subclass :class:`~doctor.types.SuperType`. :param required: Indicates if the parameter is required or not. """ def __init__(self, name: str, annotation, required: bool=False): self.annotation = annotation self.name = name self.required = required
[docs]class Params(object): """Represents parameters for a request. :param all: A list of all paramter names for a request. :param required: A list of all required parameter names for a request. :param optional: A list of all optional parameter names for a request. :param logic: A list of all parameter names that are part ofthe logic function signature. """ def __init__(self, all: List[str], required: List[str], optional: List[str], logic: List[str]): self.all = all self.optional = optional self.required = required self.logic = logic def __repr__(self): return str({ 'all': self.all, 'logic': self.logic, 'optional': self.optional, 'required': self.required }) def __eq__(self, other): for attr in ('all', 'logic', 'optional', 'required'): if getattr(self, attr) != getattr(other, attr): return False return True def __ne__(self, other): return not self.__eq__(other)
[docs]def get_params_from_func(func: Callable, signature: Signature=None) -> Params: """Gets all parameters from a function signature. :param func: The function to inspect. :param signature: An inspect.Signature instance. :returns: A named tuple containing information about all, optional, required and logic function parameters. """ if signature is None: # Check if the function already parsed the signature signature = getattr(func, '_doctor_signature', None) # Otherwise parse the signature if signature is None: signature = inspect.signature(func) # Check if a `req_obj_type` was provided for the function. If so we should # derrive the parameters from that defined type instead of the signature. if getattr(func, '_doctor_req_obj_type', None): annotation = func._doctor_req_obj_type all_params = list(annotation.properties.keys()) required = annotation.required optional = list(set(all_params) - set(required)) else: # Required is a positional argument with no defualt value and it's # annotation must sub class SuperType. This is so we don't try to # require parameters passed to a logic function by a decorator that are # not part of a request. required = [key for key, p in signature.parameters.items() if p.default == p.empty and issubclass(p.annotation, SuperType)] optional = [key for key, p in signature.parameters.items() if p.default != p.empty] all_params = [key for key in signature.parameters.keys()] # Logic params are all parameters that are part of the logic signature. logic_params = copy(all_params) return Params(all_params, required, optional, logic_params)
[docs]def add_param_annotations( logic: Callable, params: List[RequestParamAnnotation]) -> Callable: """Adds parameter annotations to a logic function. This adds additional required and/or optional parameters to the logic function that are not part of it's signature. It's intended to be used by decorators decorating logic functions or middleware. :param logic: The logic function to add the parameter annotations to. :param params: The list of RequestParamAnnotations to add to the logic func. :returns: The logic func with updated parameter annotations. """ # If we've already added param annotations to this function get the # values from the logic, otherwise we need to inspect it. if hasattr(logic, '_doctor_signature'): sig = logic._doctor_signature doctor_params = logic._doctor_params else: sig = inspect.signature(logic) doctor_params = get_params_from_func(logic, sig) prev_parameters = {name: param for name, param in sig.parameters.items()} new_params = [] for param in params: # If the parameter already exists in the function signature, log # a warning and skip it. if param.name in prev_parameters: logging.warning('Not adding %s to signature of %s, function ' 'already has that parameter in its signature.', param.name, logic.__name__) continue doctor_params.all.append(param.name) default = None if param.required: default = Parameter.empty doctor_params.required.append(param.name) else: doctor_params.optional.append(param.name) new_params.append( Parameter(param.name, Parameter.KEYWORD_ONLY, default=default, annotation=param.annotation)) new_sig = sig.replace( parameters=list(prev_parameters.values()) + new_params) logic._doctor_signature = new_sig logic._doctor_params = doctor_params return logic
[docs]def get_module_attr(module_filename, module_attr, namespace=None): """Get an attribute from a module. This uses exec to load the module with a private namespace, and then plucks and returns the given attribute from that module's namespace. Note that, while this method doesn't have any explicit unit tests, it is tested implicitly by the doctor's own documentation. The Sphinx build process will fail to generate docs if this does not work. :param str module_filename: Path to the module to execute (e.g. "../src/app.py"). :param str module_attr: Attribute to pluck from the module's namespace. (e.g. "app"). :param dict namespace: Optional namespace. If one is not passed, an empty dict will be used instead. Note that this function mutates the passed namespace, so you can inspect a passed dict after calling this method to see how the module changed it. :returns: The attribute from the module. :raises KeyError: if the module doesn't have the given attribute. """ if namespace is None: namespace = {} module_filename = os.path.abspath(module_filename) namespace['__file__'] = module_filename module_dir = os.path.dirname(module_filename) old_cwd = os.getcwd() old_sys_path = sys.path[:] try: os.chdir(module_dir) sys.path.append(module_dir) with open(module_filename, 'r') as mf: exec(compile(mf.read(), module_filename, 'exec'), namespace) return namespace[module_attr] finally: os.chdir(old_cwd) sys.path = old_sys_path
[docs]def get_description_lines(docstring): """Extract the description from the given docstring. This grabs everything up to the first occurrence of something that looks like a parameter description. The docstring will be dedented and cleaned up using the standard Sphinx methods. :param str docstring: The source docstring. :returns: list """ if prepare_docstring is None: raise ImportError('sphinx must be installed to use this function.') if not isinstance(docstring, str): return [] lines = [] for line in prepare_docstring(docstring): if DESCRIPTION_END_RE.match(line): break lines.append(line) if lines and lines[-1] != '': lines.append('') return lines
[docs]def get_valid_class_name(s: str) -> str: """Return the given string converted so that it can be used for a class name Remove leading and trailing spaces; removes spaces and capitalizes each word; and remove anything that is not alphanumeric. Returns a pep8 compatible class name. :param s: The string to convert. :returns: The updated string. """ s = str(s).strip() s = ''.join([w.title() for w in re.split(r'\W+|_', s)]) return re.sub(r'[^\w|_]', '', s)