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) |