Mercurial > repos > shellac > guppy_basecaller
diff env/lib/python3.7/site-packages/humanfriendly/deprecation.py @ 2:6af9afd405e9 draft
"planemo upload commit 0a63dd5f4d38a1f6944587f52a8cd79874177fc1"
author | shellac |
---|---|
date | Thu, 14 May 2020 14:56:58 -0400 |
parents | 26e78fe6e8c4 |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/env/lib/python3.7/site-packages/humanfriendly/deprecation.py Thu May 14 14:56:58 2020 -0400 @@ -0,0 +1,251 @@ +# Human friendly input/output in Python. +# +# Author: Peter Odding <peter@peterodding.com> +# Last Change: March 2, 2020 +# URL: https://humanfriendly.readthedocs.io + +""" +Support for deprecation warnings when importing names from old locations. + +When software evolves, things tend to move around. This is usually detrimental +to backwards compatibility (in Python this primarily manifests itself as +:exc:`~exceptions.ImportError` exceptions). + +While backwards compatibility is very important, it should not get in the way +of progress. It would be great to have the agility to move things around +without breaking backwards compatibility. + +This is where the :mod:`humanfriendly.deprecation` module comes in: It enables +the definition of backwards compatible aliases that emit a deprecation warning +when they are accessed. + +The way it works is that it wraps the original module in an :class:`DeprecationProxy` +object that defines a :func:`~DeprecationProxy.__getattr__()` special method to +override attribute access of the module. +""" + +# Standard library modules. +import collections +import functools +import importlib +import inspect +import sys +import types +import warnings + +# Modules included in our package. +from humanfriendly.text import format + +# Registry of known aliases (used by humanfriendly.sphinx). +REGISTRY = collections.defaultdict(dict) + +# Public identifiers that require documentation. +__all__ = ("DeprecationProxy", "define_aliases", "deprecated_args", "get_aliases", "is_method") + + +def define_aliases(module_name, **aliases): + """ + Update a module with backwards compatible aliases. + + :param module_name: The ``__name__`` of the module (a string). + :param aliases: Each keyword argument defines an alias. The values + are expected to be "dotted paths" (strings). + + The behavior of this function depends on whether the Sphinx documentation + generator is active, because the use of :class:`DeprecationProxy` to shadow the + real module in :data:`sys.modules` has the unintended side effect of + breaking autodoc support for ``:data:`` members (module variables). + + To avoid breaking Sphinx the proxy object is omitted and instead the + aliased names are injected into the original module namespace, to make sure + that imports can be satisfied when the documentation is being rendered. + + If you run into cyclic dependencies caused by :func:`define_aliases()` when + running Sphinx, you can try moving the call to :func:`define_aliases()` to + the bottom of the Python module you're working on. + """ + module = sys.modules[module_name] + proxy = DeprecationProxy(module, aliases) + # Populate the registry of aliases. + for name, target in aliases.items(): + REGISTRY[module.__name__][name] = target + # Avoid confusing Sphinx. + if "sphinx" in sys.modules: + for name, target in aliases.items(): + setattr(module, name, proxy.resolve(target)) + else: + # Install a proxy object to raise DeprecationWarning. + sys.modules[module_name] = proxy + + +def get_aliases(module_name): + """ + Get the aliases defined by a module. + + :param module_name: The ``__name__`` of the module (a string). + :returns: A dictionary with string keys and values: + + 1. Each key gives the name of an alias + created for backwards compatibility. + + 2. Each value gives the dotted path of + the proper location of the identifier. + + An empty dictionary is returned for modules that + don't define any backwards compatible aliases. + """ + return REGISTRY.get(module_name, {}) + + +def deprecated_args(*names): + """ + Deprecate positional arguments without dropping backwards compatibility. + + :param names: + + The positional arguments to :func:`deprecated_args()` give the names of + the positional arguments that the to-be-decorated function should warn + about being deprecated and translate to keyword arguments. + + :returns: A decorator function specialized to `names`. + + The :func:`deprecated_args()` decorator function was created to make it + easy to switch from positional arguments to keyword arguments [#]_ while + preserving backwards compatibility [#]_ and informing call sites + about the change. + + .. [#] Increased flexibility is the main reason why I find myself switching + from positional arguments to (optional) keyword arguments as my code + evolves to support more use cases. + + .. [#] In my experience positional argument order implicitly becomes part + of API compatibility whether intended or not. While this makes sense + for functions that over time adopt more and more optional arguments, + at a certain point it becomes an inconvenience to code maintenance. + + Here's an example of how to use the decorator:: + + @deprecated_args('text') + def report_choice(**options): + print(options['text']) + + When the decorated function is called with positional arguments + a deprecation warning is given:: + + >>> report_choice('this will give a deprecation warning') + DeprecationWarning: report_choice has deprecated positional arguments, please switch to keyword arguments + this will give a deprecation warning + + But when the function is called with keyword arguments no deprecation + warning is emitted:: + + >>> report_choice(text='this will not give a deprecation warning') + this will not give a deprecation warning + """ + def decorator(function): + def translate(args, kw): + # Raise TypeError when too many positional arguments are passed to the decorated function. + if len(args) > len(names): + raise TypeError( + format( + "{name} expected at most {limit} arguments, got {count}", + name=function.__name__, + limit=len(names), + count=len(args), + ) + ) + # Emit a deprecation warning when positional arguments are used. + if args: + warnings.warn( + format( + "{name} has deprecated positional arguments, please switch to keyword arguments", + name=function.__name__, + ), + category=DeprecationWarning, + stacklevel=3, + ) + # Translate positional arguments to keyword arguments. + for name, value in zip(names, args): + kw[name] = value + if is_method(function): + @functools.wraps(function) + def wrapper(*args, **kw): + """Wrapper for instance methods.""" + args = list(args) + self = args.pop(0) + translate(args, kw) + return function(self, **kw) + else: + @functools.wraps(function) + def wrapper(*args, **kw): + """Wrapper for module level functions.""" + translate(args, kw) + return function(**kw) + return wrapper + return decorator + + +def is_method(function): + """Check if the expected usage of the given function is as an instance method.""" + try: + # Python 3.3 and newer. + signature = inspect.signature(function) + return "self" in signature.parameters + except AttributeError: + # Python 3.2 and older. + metadata = inspect.getargspec(function) + return "self" in metadata.args + + +class DeprecationProxy(types.ModuleType): + + """Emit deprecation warnings for imports that should be updated.""" + + def __init__(self, module, aliases): + """ + Initialize an :class:`DeprecationProxy` object. + + :param module: The original module object. + :param aliases: A dictionary of aliases. + """ + # Initialize our superclass. + super(DeprecationProxy, self).__init__(name=module.__name__) + # Store initializer arguments. + self.module = module + self.aliases = aliases + + def __getattr__(self, name): + """ + Override module attribute lookup. + + :param name: The name to look up (a string). + :returns: The attribute value. + """ + # Check if the given name is an alias. + target = self.aliases.get(name) + if target is not None: + # Emit the deprecation warning. + warnings.warn( + format("%s.%s was moved to %s, please update your imports", self.module.__name__, name, target), + category=DeprecationWarning, + stacklevel=2, + ) + # Resolve the dotted path. + return self.resolve(target) + # Look up the name in the original module namespace. + value = getattr(self.module, name, None) + if value is not None: + return value + # Fall back to the default behavior. + raise AttributeError(format("module '%s' has no attribute '%s'", self.module.__name__, name)) + + def resolve(self, target): + """ + Look up the target of an alias. + + :param target: The fully qualified dotted path (a string). + :returns: The value of the given target. + """ + module_name, _, member = target.rpartition(".") + module = importlib.import_module(module_name) + return getattr(module, member)