deprecated-parameters 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- deprecated_parameters/__init__.py +14 -0
- deprecated_parameters/_decorator.py +538 -0
- deprecated_parameters/_mypy.py +280 -0
- deprecated_parameters/_sphinx.py +63 -0
- deprecated_parameters/_stubgen.py +217 -0
- deprecated_parameters/py.typed +0 -0
- deprecated_parameters-0.1.0.dist-info/METADATA +536 -0
- deprecated_parameters-0.1.0.dist-info/RECORD +18 -0
- deprecated_parameters-0.1.0.dist-info/WHEEL +5 -0
- deprecated_parameters-0.1.0.dist-info/entry_points.txt +2 -0
- deprecated_parameters-0.1.0.dist-info/licenses/LICENSE +21 -0
- deprecated_parameters-0.1.0.dist-info/top_level.txt +2 -0
- deprecated_parameters_tests/__init__.py +1 -0
- deprecated_parameters_tests/__main__.py +22 -0
- deprecated_parameters_tests/test_decorator.py +943 -0
- deprecated_parameters_tests/test_mypy.py +612 -0
- deprecated_parameters_tests/test_sphinx.py +188 -0
- deprecated_parameters_tests/test_stubgen.py +244 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from ._decorator import * # noqa: F403
|
|
2
|
+
from ._mypy import * # noqa: F403
|
|
3
|
+
from ._sphinx import * # noqa: F403
|
|
4
|
+
from ._stubgen import * # noqa: F403
|
|
5
|
+
|
|
6
|
+
__version__ = "0.1.0"
|
|
7
|
+
__all__ = ["__version__"]
|
|
8
|
+
|
|
9
|
+
from . import _decorator, _mypy, _sphinx, _stubgen
|
|
10
|
+
|
|
11
|
+
__all__ += _decorator.__all__
|
|
12
|
+
__all__ += _mypy.__all__
|
|
13
|
+
__all__ += _sphinx.__all__
|
|
14
|
+
__all__ += _stubgen.__all__
|
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import warnings
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from typing import Any, Callable, List, Literal, Optional, Tuple, Type, TypeVar, Union
|
|
6
|
+
|
|
7
|
+
from ._sphinx import document_deprecations, documenting
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"ParameterRemove",
|
|
11
|
+
"ParameterRename",
|
|
12
|
+
"ParameterPositional",
|
|
13
|
+
"ParameterValueRemove",
|
|
14
|
+
"DeprecatedParameters",
|
|
15
|
+
"deprecated_parameters",
|
|
16
|
+
"get_deprecated_parameters",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
default_when = "the future"
|
|
20
|
+
#: The messages say what the call is going to run into, not what happened to the signature, which for a
|
|
21
|
+
#: deprecation declared with a transform has already changed.
|
|
22
|
+
default_remove_message = (
|
|
23
|
+
'Argument "%(old_name)s" for "%(func)s" is deprecated%(since)s and will no longer be accepted in %(when)s'
|
|
24
|
+
)
|
|
25
|
+
#: Used instead of the above when the transform drops the argument, since then the caller also needs to
|
|
26
|
+
#: know that the value it gives no longer has any effect.
|
|
27
|
+
default_remove_ignored_message = (
|
|
28
|
+
'Argument "%(old_name)s" for "%(func)s" is deprecated%(since)s, its value is ignored and it will no '
|
|
29
|
+
"longer be accepted in %(when)s"
|
|
30
|
+
)
|
|
31
|
+
default_rename_message = (
|
|
32
|
+
'Argument "%(old_name)s" for "%(func)s" is deprecated%(since)s, it has been renamed to "%(new_name)s" '
|
|
33
|
+
'and "%(old_name)s" will no longer be accepted in %(when)s'
|
|
34
|
+
)
|
|
35
|
+
default_positional_message = (
|
|
36
|
+
'Giving argument "%(name)s" for "%(func)s" positionally is deprecated%(since)s, it must be given as a '
|
|
37
|
+
"keyword argument in %(when)s"
|
|
38
|
+
)
|
|
39
|
+
default_value_message = (
|
|
40
|
+
'Value %(old_value)r for argument "%(name)s" of "%(func)s" is deprecated%(since)s and will not be '
|
|
41
|
+
"supported in %(when)s%(instead)s"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
#: Attribute in which the deprecations are stored, in the callable returned by the decorator.
|
|
45
|
+
deprecations_attribute = "__deprecated_parameters__"
|
|
46
|
+
|
|
47
|
+
F = TypeVar("F", bound=Callable)
|
|
48
|
+
|
|
49
|
+
positional_kinds = (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class _Unset:
|
|
53
|
+
"""Sentinel for values that were not given, distinguishing them from None."""
|
|
54
|
+
|
|
55
|
+
def __repr__(self) -> str:
|
|
56
|
+
return "<unset>"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
unset: Any = _Unset()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _validate_version(version: Optional[str]) -> None:
|
|
63
|
+
if version is None:
|
|
64
|
+
return
|
|
65
|
+
if not isinstance(version, str):
|
|
66
|
+
raise TypeError("version must be a string.")
|
|
67
|
+
from packaging.version import InvalidVersion, Version
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
Version(version)
|
|
71
|
+
except InvalidVersion:
|
|
72
|
+
raise ValueError(f"version {version!r} is not a valid PEP 440 version.") from None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _validate_old_index(old_index: Optional[int], required: bool = False) -> None:
|
|
76
|
+
if old_index is None and not required:
|
|
77
|
+
return
|
|
78
|
+
if not isinstance(old_index, int) or isinstance(old_index, bool) or old_index < 0:
|
|
79
|
+
raise ValueError("old_index must be a non-negative integer.")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _validate_category(category: Type[Warning]) -> None:
|
|
83
|
+
if not (isinstance(category, type) and issubclass(category, Warning)):
|
|
84
|
+
raise TypeError("category must be a Warning subclass.")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class ParameterDeprecation:
|
|
88
|
+
"""Base class for the deprecation of a parameter."""
|
|
89
|
+
|
|
90
|
+
#: Name of the parameter that the deprecation refers to.
|
|
91
|
+
name: str
|
|
92
|
+
|
|
93
|
+
#: Position the parameter had in the previous signature, when it can still be given positionally.
|
|
94
|
+
old_index: Optional[int] = None
|
|
95
|
+
|
|
96
|
+
def __init__(
|
|
97
|
+
self,
|
|
98
|
+
*,
|
|
99
|
+
when: str,
|
|
100
|
+
message: str,
|
|
101
|
+
transform: Optional[str],
|
|
102
|
+
version: Optional[str],
|
|
103
|
+
category: Type[Warning],
|
|
104
|
+
) -> None:
|
|
105
|
+
_validate_version(version)
|
|
106
|
+
_validate_category(category)
|
|
107
|
+
self.when = when
|
|
108
|
+
self.message = message
|
|
109
|
+
self.transform = transform
|
|
110
|
+
self.version = version
|
|
111
|
+
self.category = category
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def since(self) -> str:
|
|
115
|
+
return f" since {self.version}" if self.version else ""
|
|
116
|
+
|
|
117
|
+
def message_values(self, func_name: str) -> dict:
|
|
118
|
+
return {
|
|
119
|
+
"func": func_name,
|
|
120
|
+
"when": self.when,
|
|
121
|
+
"version": self.version,
|
|
122
|
+
"since": self.since,
|
|
123
|
+
"name": self.name,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
def format(self, func_name: str) -> str:
|
|
127
|
+
return self.message % self.message_values(func_name)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class ParameterRemove(ParameterDeprecation):
|
|
131
|
+
"""A parameter that is being removed.
|
|
132
|
+
|
|
133
|
+
``old_index`` is only needed for a parameter that callers could also give positionally. It is the
|
|
134
|
+
position it had in the previous signature, zero based and counting every positional parameter, which
|
|
135
|
+
includes ``self`` for methods. Without it only the keyword form is recognized, and a call still
|
|
136
|
+
giving the parameter positionally fails as it would without the decorator.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
def __init__(
|
|
140
|
+
self,
|
|
141
|
+
*,
|
|
142
|
+
old_name: str,
|
|
143
|
+
old_index: Optional[int] = None,
|
|
144
|
+
when: str = default_when,
|
|
145
|
+
message: Optional[str] = None,
|
|
146
|
+
transform: Literal["remove", None] = "remove",
|
|
147
|
+
version: Optional[str] = None,
|
|
148
|
+
category: Type[Warning] = DeprecationWarning,
|
|
149
|
+
) -> None:
|
|
150
|
+
if transform not in ["remove", None]:
|
|
151
|
+
raise ValueError("transform must be 'remove' or None.")
|
|
152
|
+
_validate_old_index(old_index)
|
|
153
|
+
if message is None:
|
|
154
|
+
message = default_remove_ignored_message if transform == "remove" else default_remove_message
|
|
155
|
+
self.old_name = old_name
|
|
156
|
+
self.name = old_name
|
|
157
|
+
self.old_index = old_index
|
|
158
|
+
super().__init__(when=when, message=message, transform=transform, version=version, category=category)
|
|
159
|
+
|
|
160
|
+
def message_values(self, func_name: str) -> dict:
|
|
161
|
+
return {**super().message_values(func_name), "old_name": self.old_name}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class ParameterRename(ParameterDeprecation):
|
|
165
|
+
"""A parameter that is being renamed."""
|
|
166
|
+
|
|
167
|
+
def __init__(
|
|
168
|
+
self,
|
|
169
|
+
*,
|
|
170
|
+
new_name: str,
|
|
171
|
+
old_name: str,
|
|
172
|
+
when: str = default_when,
|
|
173
|
+
message: str = default_rename_message,
|
|
174
|
+
transform: Literal["reassign", None] = "reassign",
|
|
175
|
+
version: Optional[str] = None,
|
|
176
|
+
category: Type[Warning] = DeprecationWarning,
|
|
177
|
+
) -> None:
|
|
178
|
+
if transform not in ["reassign", None]:
|
|
179
|
+
raise ValueError("transform must be 'reassign' or None.")
|
|
180
|
+
self.new_name = new_name
|
|
181
|
+
self.old_name = old_name
|
|
182
|
+
self.name = old_name
|
|
183
|
+
super().__init__(when=when, message=message, transform=transform, version=version, category=category)
|
|
184
|
+
|
|
185
|
+
def message_values(self, func_name: str) -> dict:
|
|
186
|
+
return {**super().message_values(func_name), "old_name": self.old_name, "new_name": self.new_name}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class ParameterPositional(ParameterDeprecation):
|
|
190
|
+
"""A parameter that is becoming keyword-only.
|
|
191
|
+
|
|
192
|
+
``old_index`` is the position the parameter had in the previous signature, zero based and counting
|
|
193
|
+
every positional parameter, which includes ``self`` for methods. It is given explicitly instead of
|
|
194
|
+
being taken from the order of the deprecations, so that reordering them can not silently change
|
|
195
|
+
which argument goes where. Applying the decorator fails when it does not match the signature, and
|
|
196
|
+
the error states which indexes are expected.
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
old_index: int
|
|
200
|
+
|
|
201
|
+
def __init__(
|
|
202
|
+
self,
|
|
203
|
+
*,
|
|
204
|
+
name: str,
|
|
205
|
+
old_index: int,
|
|
206
|
+
when: str = default_when,
|
|
207
|
+
message: str = default_positional_message,
|
|
208
|
+
transform: Literal["keyword", None] = "keyword",
|
|
209
|
+
version: Optional[str] = None,
|
|
210
|
+
category: Type[Warning] = DeprecationWarning,
|
|
211
|
+
) -> None:
|
|
212
|
+
if transform not in ["keyword", None]:
|
|
213
|
+
raise ValueError("transform must be 'keyword' or None.")
|
|
214
|
+
_validate_old_index(old_index, required=True)
|
|
215
|
+
self.name = name
|
|
216
|
+
self.old_index = old_index
|
|
217
|
+
super().__init__(when=when, message=message, transform=transform, version=version, category=category)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class ParameterValueRemove(ParameterDeprecation):
|
|
221
|
+
"""A single value of a parameter that is being deprecated, rather than the parameter itself.
|
|
222
|
+
|
|
223
|
+
The transform replaces the deprecated value with ``new_value``. When there is no replacement,
|
|
224
|
+
``new_value`` is simply not given and the transform has nothing to do, so the caller keeps
|
|
225
|
+
receiving the deprecated value. ``transform=None`` is only needed to warn about a value that does
|
|
226
|
+
have a replacement without applying it.
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
def __init__(
|
|
230
|
+
self,
|
|
231
|
+
*,
|
|
232
|
+
name: str,
|
|
233
|
+
old_value: Any,
|
|
234
|
+
new_value: Any = unset,
|
|
235
|
+
when: str = default_when,
|
|
236
|
+
message: str = default_value_message,
|
|
237
|
+
transform: Literal["replace", None] = "replace",
|
|
238
|
+
version: Optional[str] = None,
|
|
239
|
+
category: Type[Warning] = DeprecationWarning,
|
|
240
|
+
) -> None:
|
|
241
|
+
if transform not in ["replace", None]:
|
|
242
|
+
raise ValueError("transform must be 'replace' or None.")
|
|
243
|
+
self.name = name
|
|
244
|
+
self.old_value = old_value
|
|
245
|
+
self.new_value = new_value
|
|
246
|
+
super().__init__(when=when, message=message, transform=transform, version=version, category=category)
|
|
247
|
+
|
|
248
|
+
def message_values(self, func_name: str) -> dict:
|
|
249
|
+
instead = "" if self.new_value is unset else f", use {self.new_value!r} instead"
|
|
250
|
+
return {
|
|
251
|
+
**super().message_values(func_name),
|
|
252
|
+
"old_value": self.old_value,
|
|
253
|
+
"new_value": self.new_value,
|
|
254
|
+
"instead": instead,
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@dataclass
|
|
259
|
+
class DeprecatedParameters:
|
|
260
|
+
removed: List[ParameterRemove] = field(default_factory=list)
|
|
261
|
+
renamed: List[ParameterRename] = field(default_factory=list)
|
|
262
|
+
positional: List[ParameterPositional] = field(default_factory=list)
|
|
263
|
+
values: List[ParameterValueRemove] = field(default_factory=list)
|
|
264
|
+
signature: Optional[inspect.Signature] = None
|
|
265
|
+
|
|
266
|
+
@property
|
|
267
|
+
def all(self) -> List[ParameterDeprecation]:
|
|
268
|
+
return [*self.removed, *self.renamed, *self.positional, *self.values]
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def get_deprecated_parameters(func: Callable, /) -> Optional[DeprecatedParameters]:
|
|
272
|
+
"""Return the parameter deprecations of a callable, or None if it has none."""
|
|
273
|
+
return getattr(func, deprecations_attribute, None)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _mark_coroutine_function(wrapper: Callable) -> None:
|
|
277
|
+
"""Mark a sync wrapper that returns a coroutine as being a coroutine function.
|
|
278
|
+
|
|
279
|
+
From python 3.12 this makes both inspect.iscoroutinefunction and asyncio.iscoroutinefunction true.
|
|
280
|
+
Before that only asyncio.iscoroutinefunction can be influenced, which is what async frameworks used.
|
|
281
|
+
"""
|
|
282
|
+
mark = getattr(inspect, "markcoroutinefunction", None)
|
|
283
|
+
if mark is not None: # python>=3.12
|
|
284
|
+
mark(wrapper)
|
|
285
|
+
return
|
|
286
|
+
import asyncio
|
|
287
|
+
|
|
288
|
+
marker = getattr(asyncio.coroutines, "_is_coroutine", None)
|
|
289
|
+
if marker is not None: # python<3.12
|
|
290
|
+
wrapper._is_coroutine = marker # type: ignore[attr-defined]
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _positional_index(signature: inspect.Signature, name: str) -> Optional[int]:
|
|
294
|
+
"""Index at which a parameter can be given positionally, or None if it can not."""
|
|
295
|
+
for index, (param_name, param) in enumerate(signature.parameters.items()):
|
|
296
|
+
if param_name == name:
|
|
297
|
+
return index if param.kind in positional_kinds else None
|
|
298
|
+
return None
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _num_positional(signature: inspect.Signature) -> int:
|
|
302
|
+
return sum(1 for param in signature.parameters.values() if param.kind in positional_kinds)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _has_var_keyword(signature: inspect.Signature) -> bool:
|
|
306
|
+
return any(param.kind is inspect.Parameter.VAR_KEYWORD for param in signature.parameters.values())
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _rescued_positional(deprecations: List[Any]) -> List[Any]:
|
|
310
|
+
"""Deprecations of arguments that callers may still give beyond the positional parameters.
|
|
311
|
+
|
|
312
|
+
These are the parameters that left the positional part of the signature, either because they were
|
|
313
|
+
removed or because they became keyword-only. Together they occupy one contiguous range of old
|
|
314
|
+
positions, right after the positional parameters that remain, which is what makes an argument given
|
|
315
|
+
for them distinguishable from one given for a parameter that is still there. They are therefore
|
|
316
|
+
validated and applied as a single block, sorted by the position each one had.
|
|
317
|
+
"""
|
|
318
|
+
extra = [x for x in deprecations if x.old_index is not None and x.transform is not None]
|
|
319
|
+
return sorted(extra, key=lambda x: x.old_index)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _given_value(signature: inspect.Signature, name: str, args: list, kwargs: dict) -> Any:
|
|
323
|
+
"""Value given for a parameter, either positionally or as a keyword, or unset if not given."""
|
|
324
|
+
if name in kwargs:
|
|
325
|
+
return kwargs[name]
|
|
326
|
+
index = _positional_index(signature, name)
|
|
327
|
+
if index is not None and index < len(args):
|
|
328
|
+
return args[index]
|
|
329
|
+
return unset
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _values_equal(given: Any, deprecated: Any) -> bool:
|
|
333
|
+
"""Whether a given value is the deprecated one, tolerating types with an unusual __eq__."""
|
|
334
|
+
try:
|
|
335
|
+
return bool(given == deprecated)
|
|
336
|
+
except Exception:
|
|
337
|
+
return False
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _validate_current_index(func: Callable, signature: inspect.Signature, name: str, old_index: int) -> None:
|
|
341
|
+
"""Check where a parameter that is only warned about, not transformed, sits in the signature."""
|
|
342
|
+
index = _positional_index(signature, name)
|
|
343
|
+
if index is None:
|
|
344
|
+
raise ValueError(
|
|
345
|
+
f"Parameter '{name}' must be positional in the signature of {func.__name__} "
|
|
346
|
+
f"for transform=None, otherwise it can not be given positionally at all."
|
|
347
|
+
)
|
|
348
|
+
if old_index != index:
|
|
349
|
+
raise ValueError(
|
|
350
|
+
f"old_index for '{name}' must be {index}, the position it has in the "
|
|
351
|
+
f"signature of {func.__name__}, got {old_index}."
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _validate(func: Callable, deprecation: DeprecatedParameters) -> None:
|
|
356
|
+
signature = deprecation.signature
|
|
357
|
+
assert signature is not None
|
|
358
|
+
params = signature.parameters
|
|
359
|
+
|
|
360
|
+
for rename in deprecation.renamed:
|
|
361
|
+
if rename.new_name not in params:
|
|
362
|
+
raise ValueError(f"Parameter '{rename.new_name}' not found in signature of {func.__name__}.")
|
|
363
|
+
|
|
364
|
+
changes: List[Union[ParameterRemove, ParameterRename]] = [*deprecation.removed, *deprecation.renamed]
|
|
365
|
+
for change in changes:
|
|
366
|
+
if change.transform and change.old_name in params:
|
|
367
|
+
raise ValueError(
|
|
368
|
+
f"Parameter '{change.old_name}' is still in the signature of {func.__name__}, so the "
|
|
369
|
+
f"'{change.transform}' transform would silently discard the value given by the caller. "
|
|
370
|
+
f"Remove it from the signature, or use transform=None to keep receiving it."
|
|
371
|
+
)
|
|
372
|
+
if change.transform is None and change.old_name not in params and not _has_var_keyword(signature):
|
|
373
|
+
raise ValueError(
|
|
374
|
+
f"Parameter '{change.old_name}' is not in the signature of {func.__name__} and there is no "
|
|
375
|
+
f"**kwargs to receive it, so with transform=None every call giving it would fail. Put it "
|
|
376
|
+
f"back in the signature, or drop transform=None so that the call is adapted to it."
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
for removal in deprecation.removed:
|
|
380
|
+
if removal.transform is None and removal.old_index is not None:
|
|
381
|
+
_validate_current_index(func, signature, removal.old_name, removal.old_index)
|
|
382
|
+
|
|
383
|
+
for positional in deprecation.positional:
|
|
384
|
+
if positional.name not in params:
|
|
385
|
+
raise ValueError(f"Parameter '{positional.name}' not found in signature of {func.__name__}.")
|
|
386
|
+
kind = params[positional.name].kind
|
|
387
|
+
if positional.transform == "keyword" and kind != inspect.Parameter.KEYWORD_ONLY:
|
|
388
|
+
raise ValueError(
|
|
389
|
+
f"Parameter '{positional.name}' must be keyword-only in the signature of {func.__name__} for "
|
|
390
|
+
f"the 'keyword' transform, since the transform exists to rescue callers that can no longer "
|
|
391
|
+
f"give it positionally. Use transform=None to only warn about it."
|
|
392
|
+
)
|
|
393
|
+
if positional.transform is None:
|
|
394
|
+
_validate_current_index(func, signature, positional.name, positional.old_index)
|
|
395
|
+
|
|
396
|
+
extra = _rescued_positional(deprecation.all)
|
|
397
|
+
if extra:
|
|
398
|
+
first = _num_positional(signature)
|
|
399
|
+
expected = list(range(first, first + len(extra)))
|
|
400
|
+
given = sorted(x.old_index for x in extra)
|
|
401
|
+
if given != expected:
|
|
402
|
+
raise ValueError(
|
|
403
|
+
f"The old_index values of the parameters of {func.__name__} that are no longer positional "
|
|
404
|
+
f"must be {expected}, got {given}. They are the positions the parameters had in the "
|
|
405
|
+
f"previous signature, counting every positional parameter, self included for methods."
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
for value in deprecation.values:
|
|
409
|
+
if value.name not in params:
|
|
410
|
+
raise ValueError(f"Parameter '{value.name}' not found in signature of {func.__name__}.")
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _warn(deprecation: ParameterDeprecation, func_name: str) -> None:
|
|
414
|
+
warnings.warn(deprecation.format(func_name), category=deprecation.category, stacklevel=4)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _apply_deprecations(
|
|
418
|
+
func: Callable, deprecation: DeprecatedParameters, args: tuple, kwargs: dict
|
|
419
|
+
) -> Tuple[tuple, dict]:
|
|
420
|
+
"""Warn about deprecated arguments and transform the call, returning the new args and kwargs."""
|
|
421
|
+
signature = deprecation.signature
|
|
422
|
+
assert signature is not None
|
|
423
|
+
func_name = func.__name__
|
|
424
|
+
arguments = list(args)
|
|
425
|
+
num_positional = _num_positional(signature)
|
|
426
|
+
|
|
427
|
+
# Arguments given at positions the signature no longer has, that is for parameters which were
|
|
428
|
+
# removed or became keyword-only. Worked out before anything is changed, so that the checks below
|
|
429
|
+
# see the call as the caller wrote it.
|
|
430
|
+
extra = _rescued_positional(deprecation.all)
|
|
431
|
+
rescued = [x for x in extra if len(arguments) > num_positional and x.old_index < len(arguments)]
|
|
432
|
+
rescued_names = {x.name for x in rescued}
|
|
433
|
+
|
|
434
|
+
for removal in deprecation.removed:
|
|
435
|
+
given_keyword = removal.old_name in kwargs
|
|
436
|
+
index = _positional_index(signature, removal.old_name)
|
|
437
|
+
given_positional = removal.old_name in rescued_names or (index is not None and index < len(arguments))
|
|
438
|
+
if given_keyword or given_positional:
|
|
439
|
+
_warn(removal, func_name)
|
|
440
|
+
if removal.transform == "remove" and given_keyword:
|
|
441
|
+
del kwargs[removal.old_name]
|
|
442
|
+
|
|
443
|
+
for rename in deprecation.renamed:
|
|
444
|
+
if rename.old_name in kwargs:
|
|
445
|
+
_warn(rename, func_name)
|
|
446
|
+
if rename.transform == "reassign":
|
|
447
|
+
positionals = list(signature.parameters)[: len(arguments)]
|
|
448
|
+
if rename.new_name in kwargs or rename.new_name in positionals:
|
|
449
|
+
raise ValueError(f"Unable to reassign '{rename.old_name}' because '{rename.new_name}' is also set.")
|
|
450
|
+
kwargs[rename.new_name] = kwargs.pop(rename.old_name)
|
|
451
|
+
|
|
452
|
+
if rescued:
|
|
453
|
+
for deprecated_positional in rescued:
|
|
454
|
+
# Removals are already warned about above, and their value is simply not carried over.
|
|
455
|
+
if isinstance(deprecated_positional, ParameterPositional):
|
|
456
|
+
_warn(deprecated_positional, func_name)
|
|
457
|
+
kwargs[deprecated_positional.name] = arguments[deprecated_positional.old_index]
|
|
458
|
+
# Anything beyond the declared deprecations is left alone, to fail as it normally would.
|
|
459
|
+
last = max(x.old_index for x in extra)
|
|
460
|
+
arguments = arguments[:num_positional] + arguments[last + 1 :]
|
|
461
|
+
|
|
462
|
+
for positional in deprecation.positional:
|
|
463
|
+
if positional.transform is None and positional.old_index < len(arguments):
|
|
464
|
+
_warn(positional, func_name)
|
|
465
|
+
|
|
466
|
+
for value in deprecation.values:
|
|
467
|
+
given = _given_value(signature, value.name, arguments, kwargs)
|
|
468
|
+
if given is not unset and _values_equal(given, value.old_value):
|
|
469
|
+
_warn(value, func_name)
|
|
470
|
+
# Without a new_value there is nothing to replace with, so the transform has nothing to do.
|
|
471
|
+
if value.transform == "replace" and value.new_value is not unset:
|
|
472
|
+
if value.name in kwargs:
|
|
473
|
+
kwargs[value.name] = value.new_value
|
|
474
|
+
else:
|
|
475
|
+
index = _positional_index(signature, value.name)
|
|
476
|
+
assert index is not None
|
|
477
|
+
arguments[index] = value.new_value
|
|
478
|
+
|
|
479
|
+
return tuple(arguments), kwargs
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
Deprecation = Union[ParameterRemove, ParameterRename, ParameterPositional, ParameterValueRemove]
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def deprecated_parameters(*deprecations: Deprecation) -> Callable[[F], F]:
|
|
486
|
+
"""
|
|
487
|
+
A decorator to mark parameters of a function or method as deprecated.
|
|
488
|
+
|
|
489
|
+
While sphinx is building, and only then, the deprecations are also appended to the docstring of the
|
|
490
|
+
decorated callable as ``.. deprecated::`` directives, one per version.
|
|
491
|
+
|
|
492
|
+
Args:
|
|
493
|
+
deprecations: parameter deprecation instances.
|
|
494
|
+
|
|
495
|
+
Returns:
|
|
496
|
+
The decorated function with registered parameter deprecations.
|
|
497
|
+
"""
|
|
498
|
+
if len(deprecations) == 0:
|
|
499
|
+
raise ValueError("At least one deprecation must be provided.")
|
|
500
|
+
|
|
501
|
+
def decorator(func):
|
|
502
|
+
if inspect.isclass(func):
|
|
503
|
+
raise TypeError(
|
|
504
|
+
"The @deprecated_parameters decorator can not be applied to classes. Apply it to the "
|
|
505
|
+
"__init__ method instead."
|
|
506
|
+
)
|
|
507
|
+
if get_deprecated_parameters(func) is not None:
|
|
508
|
+
raise ValueError("The @deprecated_parameters decorator can only be applied once per callable.")
|
|
509
|
+
|
|
510
|
+
deprecation = DeprecatedParameters(
|
|
511
|
+
removed=[x for x in deprecations if isinstance(x, ParameterRemove)],
|
|
512
|
+
renamed=[x for x in deprecations if isinstance(x, ParameterRename)],
|
|
513
|
+
positional=[x for x in deprecations if isinstance(x, ParameterPositional)],
|
|
514
|
+
values=[x for x in deprecations if isinstance(x, ParameterValueRemove)],
|
|
515
|
+
signature=inspect.signature(func),
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
_validate(func, deprecation)
|
|
519
|
+
|
|
520
|
+
# For coroutine functions the wrapper is intentionally sync, such that the warning is issued when
|
|
521
|
+
# the coroutine is created, i.e. at the call site, instead of when it is awaited, i.e. deep inside
|
|
522
|
+
# the event loop where the stack no longer relates to the caller.
|
|
523
|
+
@wraps(func)
|
|
524
|
+
def wrapper(*args, **kwargs):
|
|
525
|
+
args, kwargs = _apply_deprecations(func, deprecation, args, kwargs)
|
|
526
|
+
return func(*args, **kwargs)
|
|
527
|
+
|
|
528
|
+
if inspect.iscoroutinefunction(func):
|
|
529
|
+
_mark_coroutine_function(wrapper)
|
|
530
|
+
|
|
531
|
+
setattr(wrapper, deprecations_attribute, deprecation)
|
|
532
|
+
|
|
533
|
+
if documenting():
|
|
534
|
+
wrapper.__doc__ = document_deprecations(func.__doc__, func.__name__, deprecation)
|
|
535
|
+
|
|
536
|
+
return wrapper
|
|
537
|
+
|
|
538
|
+
return decorator
|