diff env/lib/python3.9/site-packages/humanfriendly/deprecation.py @ 0:4f3585e2f14b draft default tip

"planemo upload commit 60cee0fc7c0cda8592644e1aad72851dec82c959"
author shellac
date Mon, 22 Mar 2021 18:12:50 +0000
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/env/lib/python3.9/site-packages/humanfriendly/deprecation.py	Mon Mar 22 18:12:50 2021 +0000
@@ -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)