Mercurial > repos > shellac > guppy_basecaller
comparison env/lib/python3.7/site-packages/humanfriendly/deprecation.py @ 0:26e78fe6e8c4 draft
"planemo upload commit c699937486c35866861690329de38ec1a5d9f783"
| author | shellac |
|---|---|
| date | Sat, 02 May 2020 07:14:21 -0400 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| -1:000000000000 | 0:26e78fe6e8c4 |
|---|---|
| 1 # Human friendly input/output in Python. | |
| 2 # | |
| 3 # Author: Peter Odding <peter@peterodding.com> | |
| 4 # Last Change: March 2, 2020 | |
| 5 # URL: https://humanfriendly.readthedocs.io | |
| 6 | |
| 7 """ | |
| 8 Support for deprecation warnings when importing names from old locations. | |
| 9 | |
| 10 When software evolves, things tend to move around. This is usually detrimental | |
| 11 to backwards compatibility (in Python this primarily manifests itself as | |
| 12 :exc:`~exceptions.ImportError` exceptions). | |
| 13 | |
| 14 While backwards compatibility is very important, it should not get in the way | |
| 15 of progress. It would be great to have the agility to move things around | |
| 16 without breaking backwards compatibility. | |
| 17 | |
| 18 This is where the :mod:`humanfriendly.deprecation` module comes in: It enables | |
| 19 the definition of backwards compatible aliases that emit a deprecation warning | |
| 20 when they are accessed. | |
| 21 | |
| 22 The way it works is that it wraps the original module in an :class:`DeprecationProxy` | |
| 23 object that defines a :func:`~DeprecationProxy.__getattr__()` special method to | |
| 24 override attribute access of the module. | |
| 25 """ | |
| 26 | |
| 27 # Standard library modules. | |
| 28 import collections | |
| 29 import functools | |
| 30 import importlib | |
| 31 import inspect | |
| 32 import sys | |
| 33 import types | |
| 34 import warnings | |
| 35 | |
| 36 # Modules included in our package. | |
| 37 from humanfriendly.text import format | |
| 38 | |
| 39 # Registry of known aliases (used by humanfriendly.sphinx). | |
| 40 REGISTRY = collections.defaultdict(dict) | |
| 41 | |
| 42 # Public identifiers that require documentation. | |
| 43 __all__ = ("DeprecationProxy", "define_aliases", "deprecated_args", "get_aliases", "is_method") | |
| 44 | |
| 45 | |
| 46 def define_aliases(module_name, **aliases): | |
| 47 """ | |
| 48 Update a module with backwards compatible aliases. | |
| 49 | |
| 50 :param module_name: The ``__name__`` of the module (a string). | |
| 51 :param aliases: Each keyword argument defines an alias. The values | |
| 52 are expected to be "dotted paths" (strings). | |
| 53 | |
| 54 The behavior of this function depends on whether the Sphinx documentation | |
| 55 generator is active, because the use of :class:`DeprecationProxy` to shadow the | |
| 56 real module in :data:`sys.modules` has the unintended side effect of | |
| 57 breaking autodoc support for ``:data:`` members (module variables). | |
| 58 | |
| 59 To avoid breaking Sphinx the proxy object is omitted and instead the | |
| 60 aliased names are injected into the original module namespace, to make sure | |
| 61 that imports can be satisfied when the documentation is being rendered. | |
| 62 | |
| 63 If you run into cyclic dependencies caused by :func:`define_aliases()` when | |
| 64 running Sphinx, you can try moving the call to :func:`define_aliases()` to | |
| 65 the bottom of the Python module you're working on. | |
| 66 """ | |
| 67 module = sys.modules[module_name] | |
| 68 proxy = DeprecationProxy(module, aliases) | |
| 69 # Populate the registry of aliases. | |
| 70 for name, target in aliases.items(): | |
| 71 REGISTRY[module.__name__][name] = target | |
| 72 # Avoid confusing Sphinx. | |
| 73 if "sphinx" in sys.modules: | |
| 74 for name, target in aliases.items(): | |
| 75 setattr(module, name, proxy.resolve(target)) | |
| 76 else: | |
| 77 # Install a proxy object to raise DeprecationWarning. | |
| 78 sys.modules[module_name] = proxy | |
| 79 | |
| 80 | |
| 81 def get_aliases(module_name): | |
| 82 """ | |
| 83 Get the aliases defined by a module. | |
| 84 | |
| 85 :param module_name: The ``__name__`` of the module (a string). | |
| 86 :returns: A dictionary with string keys and values: | |
| 87 | |
| 88 1. Each key gives the name of an alias | |
| 89 created for backwards compatibility. | |
| 90 | |
| 91 2. Each value gives the dotted path of | |
| 92 the proper location of the identifier. | |
| 93 | |
| 94 An empty dictionary is returned for modules that | |
| 95 don't define any backwards compatible aliases. | |
| 96 """ | |
| 97 return REGISTRY.get(module_name, {}) | |
| 98 | |
| 99 | |
| 100 def deprecated_args(*names): | |
| 101 """ | |
| 102 Deprecate positional arguments without dropping backwards compatibility. | |
| 103 | |
| 104 :param names: | |
| 105 | |
| 106 The positional arguments to :func:`deprecated_args()` give the names of | |
| 107 the positional arguments that the to-be-decorated function should warn | |
| 108 about being deprecated and translate to keyword arguments. | |
| 109 | |
| 110 :returns: A decorator function specialized to `names`. | |
| 111 | |
| 112 The :func:`deprecated_args()` decorator function was created to make it | |
| 113 easy to switch from positional arguments to keyword arguments [#]_ while | |
| 114 preserving backwards compatibility [#]_ and informing call sites | |
| 115 about the change. | |
| 116 | |
| 117 .. [#] Increased flexibility is the main reason why I find myself switching | |
| 118 from positional arguments to (optional) keyword arguments as my code | |
| 119 evolves to support more use cases. | |
| 120 | |
| 121 .. [#] In my experience positional argument order implicitly becomes part | |
| 122 of API compatibility whether intended or not. While this makes sense | |
| 123 for functions that over time adopt more and more optional arguments, | |
| 124 at a certain point it becomes an inconvenience to code maintenance. | |
| 125 | |
| 126 Here's an example of how to use the decorator:: | |
| 127 | |
| 128 @deprecated_args('text') | |
| 129 def report_choice(**options): | |
| 130 print(options['text']) | |
| 131 | |
| 132 When the decorated function is called with positional arguments | |
| 133 a deprecation warning is given:: | |
| 134 | |
| 135 >>> report_choice('this will give a deprecation warning') | |
| 136 DeprecationWarning: report_choice has deprecated positional arguments, please switch to keyword arguments | |
| 137 this will give a deprecation warning | |
| 138 | |
| 139 But when the function is called with keyword arguments no deprecation | |
| 140 warning is emitted:: | |
| 141 | |
| 142 >>> report_choice(text='this will not give a deprecation warning') | |
| 143 this will not give a deprecation warning | |
| 144 """ | |
| 145 def decorator(function): | |
| 146 def translate(args, kw): | |
| 147 # Raise TypeError when too many positional arguments are passed to the decorated function. | |
| 148 if len(args) > len(names): | |
| 149 raise TypeError( | |
| 150 format( | |
| 151 "{name} expected at most {limit} arguments, got {count}", | |
| 152 name=function.__name__, | |
| 153 limit=len(names), | |
| 154 count=len(args), | |
| 155 ) | |
| 156 ) | |
| 157 # Emit a deprecation warning when positional arguments are used. | |
| 158 if args: | |
| 159 warnings.warn( | |
| 160 format( | |
| 161 "{name} has deprecated positional arguments, please switch to keyword arguments", | |
| 162 name=function.__name__, | |
| 163 ), | |
| 164 category=DeprecationWarning, | |
| 165 stacklevel=3, | |
| 166 ) | |
| 167 # Translate positional arguments to keyword arguments. | |
| 168 for name, value in zip(names, args): | |
| 169 kw[name] = value | |
| 170 if is_method(function): | |
| 171 @functools.wraps(function) | |
| 172 def wrapper(*args, **kw): | |
| 173 """Wrapper for instance methods.""" | |
| 174 args = list(args) | |
| 175 self = args.pop(0) | |
| 176 translate(args, kw) | |
| 177 return function(self, **kw) | |
| 178 else: | |
| 179 @functools.wraps(function) | |
| 180 def wrapper(*args, **kw): | |
| 181 """Wrapper for module level functions.""" | |
| 182 translate(args, kw) | |
| 183 return function(**kw) | |
| 184 return wrapper | |
| 185 return decorator | |
| 186 | |
| 187 | |
| 188 def is_method(function): | |
| 189 """Check if the expected usage of the given function is as an instance method.""" | |
| 190 try: | |
| 191 # Python 3.3 and newer. | |
| 192 signature = inspect.signature(function) | |
| 193 return "self" in signature.parameters | |
| 194 except AttributeError: | |
| 195 # Python 3.2 and older. | |
| 196 metadata = inspect.getargspec(function) | |
| 197 return "self" in metadata.args | |
| 198 | |
| 199 | |
| 200 class DeprecationProxy(types.ModuleType): | |
| 201 | |
| 202 """Emit deprecation warnings for imports that should be updated.""" | |
| 203 | |
| 204 def __init__(self, module, aliases): | |
| 205 """ | |
| 206 Initialize an :class:`DeprecationProxy` object. | |
| 207 | |
| 208 :param module: The original module object. | |
| 209 :param aliases: A dictionary of aliases. | |
| 210 """ | |
| 211 # Initialize our superclass. | |
| 212 super(DeprecationProxy, self).__init__(name=module.__name__) | |
| 213 # Store initializer arguments. | |
| 214 self.module = module | |
| 215 self.aliases = aliases | |
| 216 | |
| 217 def __getattr__(self, name): | |
| 218 """ | |
| 219 Override module attribute lookup. | |
| 220 | |
| 221 :param name: The name to look up (a string). | |
| 222 :returns: The attribute value. | |
| 223 """ | |
| 224 # Check if the given name is an alias. | |
| 225 target = self.aliases.get(name) | |
| 226 if target is not None: | |
| 227 # Emit the deprecation warning. | |
| 228 warnings.warn( | |
| 229 format("%s.%s was moved to %s, please update your imports", self.module.__name__, name, target), | |
| 230 category=DeprecationWarning, | |
| 231 stacklevel=2, | |
| 232 ) | |
| 233 # Resolve the dotted path. | |
| 234 return self.resolve(target) | |
| 235 # Look up the name in the original module namespace. | |
| 236 value = getattr(self.module, name, None) | |
| 237 if value is not None: | |
| 238 return value | |
| 239 # Fall back to the default behavior. | |
| 240 raise AttributeError(format("module '%s' has no attribute '%s'", self.module.__name__, name)) | |
| 241 | |
| 242 def resolve(self, target): | |
| 243 """ | |
| 244 Look up the target of an alias. | |
| 245 | |
| 246 :param target: The fully qualified dotted path (a string). | |
| 247 :returns: The value of the given target. | |
| 248 """ | |
| 249 module_name, _, member = target.rpartition(".") | |
| 250 module = importlib.import_module(module_name) | |
| 251 return getattr(module, member) |
