mirror of
https://github.com/django/django.git
synced 2025-08-21 01:09:13 +00:00
Fixed #36477, Refs #36163 -- Added @deprecate_posargs decorator to simplify deprecation of positional arguments.
This helper allows marking positional-or-keyword parameters as keyword-only with a deprecation period, in a consistent and correct manner.
This commit is contained in:
parent
10386fac00
commit
f42b89f1bf
@ -1,8 +1,13 @@
|
|||||||
|
import functools
|
||||||
import inspect
|
import inspect
|
||||||
|
import os
|
||||||
import warnings
|
import warnings
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
from asgiref.sync import iscoroutinefunction, markcoroutinefunction, sync_to_async
|
from asgiref.sync import iscoroutinefunction, markcoroutinefunction, sync_to_async
|
||||||
|
|
||||||
|
import django
|
||||||
|
|
||||||
|
|
||||||
class RemovedInDjango61Warning(DeprecationWarning):
|
class RemovedInDjango61Warning(DeprecationWarning):
|
||||||
pass
|
pass
|
||||||
@ -83,6 +88,181 @@ class RenameMethodsBase(type):
|
|||||||
return new_class
|
return new_class
|
||||||
|
|
||||||
|
|
||||||
|
def deprecate_posargs(deprecation_warning, remappable_names, /):
|
||||||
|
"""
|
||||||
|
Function/method decorator to deprecate some or all positional arguments.
|
||||||
|
|
||||||
|
The decorated function will map any positional arguments after the ``*`` to
|
||||||
|
the corresponding keyword arguments and issue a deprecation warning.
|
||||||
|
|
||||||
|
The decorator takes two arguments: a RemovedInDjangoXXWarning warning
|
||||||
|
category and a list of parameter names that have been changed from
|
||||||
|
positional-or-keyword to keyword-only, in their original positional order.
|
||||||
|
|
||||||
|
Works on both functions and methods. To apply to a class constructor,
|
||||||
|
decorate its __init__() method. To apply to a staticmethod or classmethod,
|
||||||
|
use @deprecate_posargs after @staticmethod or @classmethod.
|
||||||
|
|
||||||
|
Example: to deprecate passing option1 or option2 as posargs, change::
|
||||||
|
|
||||||
|
def some_func(request, option1, option2=True):
|
||||||
|
...
|
||||||
|
|
||||||
|
to::
|
||||||
|
|
||||||
|
@deprecate_posargs(RemovedInDjangoXXWarning, ["option1", "option2"])
|
||||||
|
def some_func(request, *, option1, option2=True):
|
||||||
|
...
|
||||||
|
|
||||||
|
After the deprecation period, remove the decorator (but keep the ``*``)::
|
||||||
|
|
||||||
|
def some_func(request, *, option1, option2=True):
|
||||||
|
...
|
||||||
|
|
||||||
|
Caution: during the deprecation period, do not add any new *positional*
|
||||||
|
parameters or change the remaining ones. For example, this attempt to add a
|
||||||
|
new param would break code using the deprecated posargs::
|
||||||
|
|
||||||
|
@deprecate_posargs(RemovedInDjangoXXWarning, ["option1", "option2"])
|
||||||
|
def some_func(request, wrong_new_param=None, *, option1, option2=True):
|
||||||
|
# Broken: existing code may pass a value intended as option1 in the
|
||||||
|
# wrong_new_param position.
|
||||||
|
...
|
||||||
|
|
||||||
|
However, it's acceptable to add new *keyword-only* parameters and to
|
||||||
|
re-order the existing ones, so long as the list passed to
|
||||||
|
@deprecate_posargs is kept in the original posargs order. This change will
|
||||||
|
work without breaking existing code::
|
||||||
|
|
||||||
|
@deprecate_posargs(RemovedInDjangoXXWarning, ["option1", "option2"])
|
||||||
|
def some_func(request, *, new_param=None, option2=True, option1):
|
||||||
|
...
|
||||||
|
|
||||||
|
The @deprecate_posargs decorator adds a small amount of overhead. In most
|
||||||
|
cases it won't be significant, but use with care in performance-critical
|
||||||
|
code paths.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(func):
|
||||||
|
if isinstance(func, type):
|
||||||
|
raise TypeError(
|
||||||
|
"@deprecate_posargs cannot be applied to a class. (Apply it "
|
||||||
|
"to the __init__ method.)"
|
||||||
|
)
|
||||||
|
if isinstance(func, classmethod):
|
||||||
|
raise TypeError("Apply @classmethod before @deprecate_posargs.")
|
||||||
|
if isinstance(func, staticmethod):
|
||||||
|
raise TypeError("Apply @staticmethod before @deprecate_posargs.")
|
||||||
|
|
||||||
|
params = inspect.signature(func).parameters
|
||||||
|
num_by_kind = Counter(param.kind for param in params.values())
|
||||||
|
|
||||||
|
if num_by_kind[inspect.Parameter.VAR_POSITIONAL] > 0:
|
||||||
|
raise TypeError(
|
||||||
|
"@deprecate_posargs() cannot be used with variable positional `*args`."
|
||||||
|
)
|
||||||
|
|
||||||
|
num_positional_params = (
|
||||||
|
num_by_kind[inspect.Parameter.POSITIONAL_ONLY]
|
||||||
|
+ num_by_kind[inspect.Parameter.POSITIONAL_OR_KEYWORD]
|
||||||
|
)
|
||||||
|
num_keyword_only_params = num_by_kind[inspect.Parameter.KEYWORD_ONLY]
|
||||||
|
if num_keyword_only_params < 1:
|
||||||
|
raise TypeError(
|
||||||
|
"@deprecate_posargs() requires at least one keyword-only parameter "
|
||||||
|
"(after a `*` entry in the parameters list)."
|
||||||
|
)
|
||||||
|
if any(
|
||||||
|
name not in params or params[name].kind != inspect.Parameter.KEYWORD_ONLY
|
||||||
|
for name in remappable_names
|
||||||
|
):
|
||||||
|
raise TypeError(
|
||||||
|
"@deprecate_posargs() requires all remappable_names to be "
|
||||||
|
"keyword-only parameters."
|
||||||
|
)
|
||||||
|
|
||||||
|
num_remappable_args = len(remappable_names)
|
||||||
|
max_positional_args = num_positional_params + num_remappable_args
|
||||||
|
|
||||||
|
func_name = func.__name__
|
||||||
|
if func_name == "__init__":
|
||||||
|
# In the warning, show "ClassName()" instead of "__init__()".
|
||||||
|
# The class isn't defined yet, but its name is in __qualname__.
|
||||||
|
# Some examples of __qualname__:
|
||||||
|
# - ClassName.__init__
|
||||||
|
# - Nested.ClassName.__init__
|
||||||
|
# - MyTests.test_case.<locals>.ClassName.__init__
|
||||||
|
local_name = func.__qualname__.rsplit("<locals>.", 1)[-1]
|
||||||
|
class_name = local_name.replace(".__init__", "")
|
||||||
|
func_name = class_name
|
||||||
|
|
||||||
|
def remap_deprecated_args(args, kwargs):
|
||||||
|
"""
|
||||||
|
Move deprecated positional args to kwargs and issue a warning.
|
||||||
|
Return updated (args, kwargs).
|
||||||
|
"""
|
||||||
|
if (num_positional_args := len(args)) > max_positional_args:
|
||||||
|
raise TypeError(
|
||||||
|
f"{func_name}() takes at most {max_positional_args} positional "
|
||||||
|
f"argument(s) (including {num_remappable_args} deprecated) but "
|
||||||
|
f"{num_positional_args} were given."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Identify which of the _potentially remappable_ params are
|
||||||
|
# actually _being remapped_ in this particular call.
|
||||||
|
remapped_names = remappable_names[
|
||||||
|
: num_positional_args - num_positional_params
|
||||||
|
]
|
||||||
|
conflicts = set(remapped_names) & set(kwargs)
|
||||||
|
if conflicts:
|
||||||
|
# Report duplicate names in the original parameter order.
|
||||||
|
conflicts_str = ", ".join(
|
||||||
|
f"'{name}'" for name in remapped_names if name in conflicts
|
||||||
|
)
|
||||||
|
raise TypeError(
|
||||||
|
f"{func_name}() got both deprecated positional and keyword "
|
||||||
|
f"argument values for {conflicts_str}."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Do the remapping.
|
||||||
|
remapped_kwargs = dict(
|
||||||
|
zip(remapped_names, args[num_positional_params:], strict=True)
|
||||||
|
)
|
||||||
|
remaining_args = args[:num_positional_params]
|
||||||
|
updated_kwargs = kwargs | remapped_kwargs
|
||||||
|
|
||||||
|
# Issue the deprecation warning.
|
||||||
|
remapped_names_str = ", ".join(f"'{name}'" for name in remapped_names)
|
||||||
|
warnings.warn(
|
||||||
|
f"Passing positional argument(s) {remapped_names_str} to {func_name}() "
|
||||||
|
"is deprecated. Use keyword arguments instead.",
|
||||||
|
deprecation_warning,
|
||||||
|
skip_file_prefixes=(os.path.dirname(django.__file__),),
|
||||||
|
)
|
||||||
|
|
||||||
|
return remaining_args, updated_kwargs
|
||||||
|
|
||||||
|
if iscoroutinefunction(func):
|
||||||
|
|
||||||
|
@functools.wraps(func)
|
||||||
|
async def wrapper(*args, **kwargs):
|
||||||
|
if len(args) > num_positional_params:
|
||||||
|
args, kwargs = remap_deprecated_args(args, kwargs)
|
||||||
|
return await func(*args, **kwargs)
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
if len(args) > num_positional_params:
|
||||||
|
args, kwargs = remap_deprecated_args(args, kwargs)
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
class MiddlewareMixin:
|
class MiddlewareMixin:
|
||||||
sync_capable = True
|
sync_capable = True
|
||||||
async_capable = True
|
async_capable = True
|
||||||
|
@ -312,6 +312,11 @@ Once you have completed these steps, you are finished with the deprecation.
|
|||||||
In each :term:`feature release <Feature release>`, all
|
In each :term:`feature release <Feature release>`, all
|
||||||
``RemovedInDjangoXXWarning``\s matching the new version are removed.
|
``RemovedInDjangoXXWarning``\s matching the new version are removed.
|
||||||
|
|
||||||
|
The ``django.utils.deprecation`` module provides some helpful deprecation
|
||||||
|
utilities, such as a ``@deprecate_posargs`` decorator to assist with converting
|
||||||
|
positional-or-keyword arguments to keyword-only. See the inline documentation
|
||||||
|
in the module source.
|
||||||
|
|
||||||
JavaScript contributions
|
JavaScript contributions
|
||||||
========================
|
========================
|
||||||
|
|
||||||
|
404
tests/deprecation/test_deprecate_posargs.py
Normal file
404
tests/deprecation/test_deprecate_posargs.py
Normal file
@ -0,0 +1,404 @@
|
|||||||
|
import inspect
|
||||||
|
|
||||||
|
from django.test import SimpleTestCase
|
||||||
|
from django.utils.deprecation import RemovedAfterNextVersionWarning, deprecate_posargs
|
||||||
|
|
||||||
|
|
||||||
|
class DeprecatePosargsTests(SimpleTestCase):
|
||||||
|
# Note: these tests use the generic RemovedAfterNextVersionWarning so they
|
||||||
|
# don't need to be updated each release. In actual use, you must substitute
|
||||||
|
# a specific RemovedInDjangoXXWarning.
|
||||||
|
|
||||||
|
def assertDeprecated(self, params, name):
|
||||||
|
msg = (
|
||||||
|
"Passing positional argument(s) {0} to {1}() is deprecated. Use keyword "
|
||||||
|
"arguments instead."
|
||||||
|
)
|
||||||
|
return self.assertWarnsMessage(
|
||||||
|
RemovedAfterNextVersionWarning, msg.format(params, name)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_all_keyword_only_params(self):
|
||||||
|
"""All positional arguments are remapped to keyword-only arguments."""
|
||||||
|
|
||||||
|
@deprecate_posargs(RemovedAfterNextVersionWarning, ["a", "b"])
|
||||||
|
def some_func(*, a=1, b=2):
|
||||||
|
return a, b
|
||||||
|
|
||||||
|
with (
|
||||||
|
self.subTest("Multiple affected args"),
|
||||||
|
self.assertDeprecated("'a', 'b'", "some_func"),
|
||||||
|
):
|
||||||
|
result = some_func(10, 20)
|
||||||
|
self.assertEqual(result, (10, 20))
|
||||||
|
|
||||||
|
with (
|
||||||
|
self.subTest("One affected arg"),
|
||||||
|
self.assertDeprecated("'a'", "some_func"),
|
||||||
|
):
|
||||||
|
result = some_func(10)
|
||||||
|
self.assertEqual(result, (10, 2))
|
||||||
|
|
||||||
|
def test_some_keyword_only_params(self):
|
||||||
|
"""Works when keeping some params as positional-or-keyword."""
|
||||||
|
|
||||||
|
@deprecate_posargs(RemovedAfterNextVersionWarning, ["b"])
|
||||||
|
def some_func(a, *, b=1):
|
||||||
|
return a, b
|
||||||
|
|
||||||
|
with self.assertDeprecated("'b'", "some_func"):
|
||||||
|
result = some_func(10, 20)
|
||||||
|
self.assertEqual(result, (10, 20))
|
||||||
|
|
||||||
|
def test_no_warning_when_not_needed(self):
|
||||||
|
@deprecate_posargs(RemovedAfterNextVersionWarning, ["b"])
|
||||||
|
def some_func(a=0, *, b=1):
|
||||||
|
return a, b
|
||||||
|
|
||||||
|
with self.subTest("All arguments supplied"), self.assertNoLogs(level="WARNING"):
|
||||||
|
result = some_func(10, b=20)
|
||||||
|
self.assertEqual(result, (10, 20))
|
||||||
|
|
||||||
|
with self.subTest("All default arguments"), self.assertNoLogs(level="WARNING"):
|
||||||
|
result = some_func()
|
||||||
|
self.assertEqual(result, (0, 1))
|
||||||
|
|
||||||
|
with (
|
||||||
|
self.subTest("Partial arguments supplied"),
|
||||||
|
self.assertNoLogs(level="WARNING"),
|
||||||
|
):
|
||||||
|
result = some_func(10)
|
||||||
|
self.assertEqual(result, (10, 1))
|
||||||
|
|
||||||
|
def test_allows_reordering_keyword_only_params(self):
|
||||||
|
"""Keyword-only params can be freely added and rearranged."""
|
||||||
|
|
||||||
|
# Original signature: some_func(b=2, a=1), and remappable_names
|
||||||
|
# reflects the original positional argument order.
|
||||||
|
@deprecate_posargs(RemovedAfterNextVersionWarning, ["b", "a"])
|
||||||
|
def some_func(*, aa_new=0, a=1, b=2):
|
||||||
|
return aa_new, a, b
|
||||||
|
|
||||||
|
with self.assertDeprecated("'b', 'a'", "some_func"):
|
||||||
|
result = some_func(20, 10)
|
||||||
|
self.assertEqual(result, (0, 10, 20))
|
||||||
|
|
||||||
|
def test_detects_duplicate_arguments(self):
|
||||||
|
@deprecate_posargs(RemovedAfterNextVersionWarning, ["b", "c"])
|
||||||
|
def func(a, *, b=1, c=2):
|
||||||
|
return a, b, c
|
||||||
|
|
||||||
|
msg = (
|
||||||
|
"func() got both deprecated positional and keyword argument values for {0}"
|
||||||
|
)
|
||||||
|
with (
|
||||||
|
self.subTest("One duplicate"),
|
||||||
|
self.assertRaisesMessage(TypeError, msg.format("'b'")),
|
||||||
|
):
|
||||||
|
func(0, 10, b=12)
|
||||||
|
|
||||||
|
with (
|
||||||
|
self.subTest("Multiple duplicates"),
|
||||||
|
self.assertRaisesMessage(TypeError, msg.format("'b', 'c'")),
|
||||||
|
):
|
||||||
|
func(0, 10, 20, b=12, c=22)
|
||||||
|
|
||||||
|
with (
|
||||||
|
self.subTest("No false positives for valid kwargs"),
|
||||||
|
# Deprecation warning for 'b', not TypeError for duplicate 'c'.
|
||||||
|
self.assertDeprecated("'b'", "func"),
|
||||||
|
):
|
||||||
|
result = func(0, 11, c=22)
|
||||||
|
self.assertEqual(result, (0, 11, 22))
|
||||||
|
|
||||||
|
def test_detects_extra_positional_arguments(self):
|
||||||
|
@deprecate_posargs(RemovedAfterNextVersionWarning, ["b"])
|
||||||
|
def func(a, *, b=1):
|
||||||
|
return a, b
|
||||||
|
|
||||||
|
with self.assertRaisesMessage(
|
||||||
|
TypeError,
|
||||||
|
"func() takes at most 2 positional argument(s) (including 1 deprecated) "
|
||||||
|
"but 3 were given.",
|
||||||
|
):
|
||||||
|
func(10, 20, 30)
|
||||||
|
|
||||||
|
def test_avoids_remapping_to_new_keyword_arguments(self):
|
||||||
|
# Only 'b' is moving; 'c' was added later.
|
||||||
|
@deprecate_posargs(RemovedAfterNextVersionWarning, ["b"])
|
||||||
|
def func(a, *, b=1, c=2):
|
||||||
|
return a, b, c
|
||||||
|
|
||||||
|
with self.assertRaisesMessage(
|
||||||
|
TypeError,
|
||||||
|
"func() takes at most 2 positional argument(s) (including 1 deprecated) "
|
||||||
|
"but 3 were given.",
|
||||||
|
):
|
||||||
|
func(10, 20, 30)
|
||||||
|
|
||||||
|
def test_variable_kwargs(self):
|
||||||
|
"""Works with **kwargs."""
|
||||||
|
|
||||||
|
@deprecate_posargs(RemovedAfterNextVersionWarning, ["b"])
|
||||||
|
def some_func(a, *, b=1, **kwargs):
|
||||||
|
return a, b, kwargs
|
||||||
|
|
||||||
|
with (
|
||||||
|
self.subTest("Called with additional kwargs"),
|
||||||
|
self.assertDeprecated("'b'", "some_func"),
|
||||||
|
):
|
||||||
|
result = some_func(10, 20, c=30)
|
||||||
|
self.assertEqual(result, (10, 20, {"c": 30}))
|
||||||
|
|
||||||
|
with (
|
||||||
|
self.subTest("Called without additional kwargs"),
|
||||||
|
self.assertDeprecated("'b'", "some_func"),
|
||||||
|
):
|
||||||
|
result = some_func(10, 20)
|
||||||
|
self.assertEqual(result, (10, 20, {}))
|
||||||
|
|
||||||
|
with (
|
||||||
|
self.subTest("Called with too many positional arguments"),
|
||||||
|
# Similar to test_detects_extra_positional_arguments() above,
|
||||||
|
# but verifying logic is not confused by variable **kwargs.
|
||||||
|
self.assertRaisesMessage(
|
||||||
|
TypeError,
|
||||||
|
"some_func() takes at most 2 positional argument(s) (including 1 "
|
||||||
|
"deprecated) but 3 were given.",
|
||||||
|
),
|
||||||
|
):
|
||||||
|
some_func(10, 20, 30)
|
||||||
|
|
||||||
|
with self.subTest("No warning needed"):
|
||||||
|
result = some_func(10, b=20, c=30)
|
||||||
|
self.assertEqual(result, (10, 20, {"c": 30}))
|
||||||
|
|
||||||
|
def test_positional_only_params(self):
|
||||||
|
@deprecate_posargs(RemovedAfterNextVersionWarning, ["c"])
|
||||||
|
def some_func(a, /, b, *, c=3):
|
||||||
|
return a, b, c
|
||||||
|
|
||||||
|
with self.assertDeprecated("'c'", "some_func"):
|
||||||
|
result = some_func(10, 20, 30)
|
||||||
|
self.assertEqual(result, (10, 20, 30))
|
||||||
|
|
||||||
|
def test_class_methods(self):
|
||||||
|
"""
|
||||||
|
Deprecations for class methods should be bound properly and should
|
||||||
|
omit the `self` or `cls` argument from the suggested replacement.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class SomeClass:
|
||||||
|
@deprecate_posargs(RemovedAfterNextVersionWarning, ["a", "b"])
|
||||||
|
def __init__(self, *, a=0, b=1):
|
||||||
|
self.a = a
|
||||||
|
self.b = b
|
||||||
|
|
||||||
|
@deprecate_posargs(RemovedAfterNextVersionWarning, ["a", "b"])
|
||||||
|
def some_method(self, *, a, b=1):
|
||||||
|
return self.a, self.b, a, b
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@deprecate_posargs(RemovedAfterNextVersionWarning, ["a", "b"])
|
||||||
|
def some_static_method(*, a, b=1):
|
||||||
|
return a, b
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@deprecate_posargs(RemovedAfterNextVersionWarning, ["a", "b"])
|
||||||
|
def some_class_method(cls, *, a, b=1):
|
||||||
|
return cls.__name__, a, b
|
||||||
|
|
||||||
|
with (
|
||||||
|
self.subTest("Constructor"),
|
||||||
|
# Warning should use the class name, not `__init__()`.
|
||||||
|
self.assertDeprecated("'a', 'b'", "SomeClass"),
|
||||||
|
):
|
||||||
|
instance = SomeClass(10, 20)
|
||||||
|
self.assertEqual(instance.a, 10)
|
||||||
|
self.assertEqual(instance.b, 20)
|
||||||
|
|
||||||
|
with (
|
||||||
|
self.subTest("Instance method"),
|
||||||
|
self.assertDeprecated("'a', 'b'", "some_method"),
|
||||||
|
):
|
||||||
|
result = SomeClass().some_method(10, 20)
|
||||||
|
self.assertEqual(result, (0, 1, 10, 20))
|
||||||
|
|
||||||
|
with (
|
||||||
|
self.subTest("Static method on instance"),
|
||||||
|
self.assertDeprecated("'a', 'b'", "some_static_method"),
|
||||||
|
):
|
||||||
|
result = SomeClass().some_static_method(10, 20)
|
||||||
|
self.assertEqual(result, (10, 20))
|
||||||
|
|
||||||
|
with (
|
||||||
|
self.subTest("Static method on class"),
|
||||||
|
self.assertDeprecated("'a', 'b'", "some_static_method"),
|
||||||
|
):
|
||||||
|
result = SomeClass.some_static_method(10, 20)
|
||||||
|
self.assertEqual(result, (10, 20))
|
||||||
|
|
||||||
|
with (
|
||||||
|
self.subTest("Class method on instance"),
|
||||||
|
self.assertDeprecated("'a', 'b'", "some_class_method"),
|
||||||
|
):
|
||||||
|
result = SomeClass().some_class_method(10, 20)
|
||||||
|
self.assertEqual(result, ("SomeClass", 10, 20))
|
||||||
|
|
||||||
|
with (
|
||||||
|
self.subTest("Class method on class"),
|
||||||
|
self.assertDeprecated("'a', 'b'", "some_class_method"),
|
||||||
|
):
|
||||||
|
result = SomeClass.some_class_method(10, 20)
|
||||||
|
self.assertEqual(result, ("SomeClass", 10, 20))
|
||||||
|
|
||||||
|
def test_incorrect_classmethod_order(self):
|
||||||
|
"""Catch classmethod applied in wrong order."""
|
||||||
|
with self.assertRaisesMessage(
|
||||||
|
TypeError, "Apply @classmethod before @deprecate_posargs."
|
||||||
|
):
|
||||||
|
|
||||||
|
class SomeClass:
|
||||||
|
@deprecate_posargs(RemovedAfterNextVersionWarning, ["a"])
|
||||||
|
@classmethod
|
||||||
|
def some_class_method(cls, *, a):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_incorrect_staticmethod_order(self):
|
||||||
|
"""Catch staticmethod applied in wrong order."""
|
||||||
|
with self.assertRaisesMessage(
|
||||||
|
TypeError, "Apply @staticmethod before @deprecate_posargs."
|
||||||
|
):
|
||||||
|
|
||||||
|
class SomeClass:
|
||||||
|
@deprecate_posargs(RemovedAfterNextVersionWarning, ["a"])
|
||||||
|
@staticmethod
|
||||||
|
def some_static_method(*, a):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def test_async(self):
|
||||||
|
"""A decorated async function is still async."""
|
||||||
|
|
||||||
|
@deprecate_posargs(RemovedAfterNextVersionWarning, ["a", "b"])
|
||||||
|
async def some_func(*, a, b=1):
|
||||||
|
return a, b
|
||||||
|
|
||||||
|
self.assertTrue(inspect.iscoroutinefunction(some_func.__wrapped__))
|
||||||
|
self.assertTrue(inspect.iscoroutinefunction(some_func))
|
||||||
|
|
||||||
|
with (
|
||||||
|
self.subTest("With deprecation warning"),
|
||||||
|
self.assertDeprecated("'a', 'b'", "some_func"),
|
||||||
|
):
|
||||||
|
result = await some_func(10, 20)
|
||||||
|
self.assertEqual(result, (10, 20))
|
||||||
|
|
||||||
|
with (
|
||||||
|
self.subTest("Without deprecation warning"),
|
||||||
|
self.assertNoLogs(level="WARNING"),
|
||||||
|
):
|
||||||
|
result = await some_func(a=10, b=20)
|
||||||
|
self.assertEqual(result, (10, 20))
|
||||||
|
|
||||||
|
def test_applied_to_lambda(self):
|
||||||
|
"""
|
||||||
|
Please don't try to deprecate lambda args! What does that even mean?!
|
||||||
|
(But if it happens, the decorator should do something reasonable.)
|
||||||
|
"""
|
||||||
|
lambda_func = deprecate_posargs(RemovedAfterNextVersionWarning, ["b"])(
|
||||||
|
lambda a, *, b=1: (a, b)
|
||||||
|
)
|
||||||
|
with self.assertDeprecated("'b'", "<lambda>"):
|
||||||
|
result = lambda_func(10, 20)
|
||||||
|
self.assertEqual(result, (10, 20))
|
||||||
|
|
||||||
|
def test_bare_init(self):
|
||||||
|
"""Can't replace '__init__' with class name if not in a class."""
|
||||||
|
|
||||||
|
@deprecate_posargs(RemovedAfterNextVersionWarning, ["a"])
|
||||||
|
def __init__(*, a):
|
||||||
|
pass
|
||||||
|
|
||||||
|
with self.assertDeprecated("'a'", "__init__"):
|
||||||
|
__init__(10)
|
||||||
|
|
||||||
|
def test_warning_source_location(self):
|
||||||
|
"""The warning points to caller, not the decorator implementation."""
|
||||||
|
|
||||||
|
@deprecate_posargs(RemovedAfterNextVersionWarning, "a")
|
||||||
|
def some_func(*, a):
|
||||||
|
return a
|
||||||
|
|
||||||
|
with self.assertWarns(RemovedAfterNextVersionWarning) as cm:
|
||||||
|
some_func(10)
|
||||||
|
self.assertEqual(cm.filename, __file__)
|
||||||
|
self.assertEqual(cm.lineno, inspect.currentframe().f_lineno - 2)
|
||||||
|
|
||||||
|
def test_decorator_requires_keyword_only_params(self):
|
||||||
|
with self.assertRaisesMessage(
|
||||||
|
TypeError,
|
||||||
|
"@deprecate_posargs() requires at least one keyword-only parameter "
|
||||||
|
"(after a `*` entry in the parameters list).",
|
||||||
|
):
|
||||||
|
|
||||||
|
@deprecate_posargs(RemovedAfterNextVersionWarning, ["b"])
|
||||||
|
def func(a, b=1):
|
||||||
|
return a, b
|
||||||
|
|
||||||
|
def test_decorator_rejects_var_positional_param(self):
|
||||||
|
with self.assertRaisesMessage(
|
||||||
|
TypeError,
|
||||||
|
"@deprecate_posargs() cannot be used with variable positional `*args`.",
|
||||||
|
):
|
||||||
|
|
||||||
|
@deprecate_posargs(RemovedAfterNextVersionWarning, ["b"])
|
||||||
|
def func(*args, b=1):
|
||||||
|
return args, b
|
||||||
|
|
||||||
|
def test_decorator_does_not_apply_to_class(self):
|
||||||
|
with self.assertRaisesMessage(
|
||||||
|
TypeError,
|
||||||
|
"@deprecate_posargs cannot be applied to a class. (Apply it to the "
|
||||||
|
"__init__ method.)",
|
||||||
|
):
|
||||||
|
|
||||||
|
@deprecate_posargs(RemovedAfterNextVersionWarning, ["b"])
|
||||||
|
class NotThisClass:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_decorator_requires_remappable_names_be_keyword_only(self):
|
||||||
|
"""remappable_names cannot refer to positional-or-keyword params."""
|
||||||
|
with self.assertRaisesMessage(
|
||||||
|
TypeError,
|
||||||
|
"@deprecate_posargs() requires all remappable_names to be keyword-only "
|
||||||
|
"parameters.",
|
||||||
|
):
|
||||||
|
|
||||||
|
@deprecate_posargs(RemovedAfterNextVersionWarning, ["a", "b"])
|
||||||
|
def func(a, *, b=1):
|
||||||
|
return a, b
|
||||||
|
|
||||||
|
def test_decorator_requires_remappable_names_exist(self):
|
||||||
|
"""remappable_names cannot refer to variable kwargs."""
|
||||||
|
with self.assertRaisesMessage(
|
||||||
|
TypeError,
|
||||||
|
"@deprecate_posargs() requires all remappable_names to be keyword-only "
|
||||||
|
"parameters.",
|
||||||
|
):
|
||||||
|
|
||||||
|
@deprecate_posargs(RemovedAfterNextVersionWarning, ["b", "c"])
|
||||||
|
def func(a, *, b=1, **kwargs):
|
||||||
|
c = kwargs.get("c")
|
||||||
|
return a, b, c
|
||||||
|
|
||||||
|
def test_decorator_preserves_signature_and_metadata(self):
|
||||||
|
|
||||||
|
def original(a, b=1, *, c=2):
|
||||||
|
"""Docstring."""
|
||||||
|
return a, b, c
|
||||||
|
|
||||||
|
decorated = deprecate_posargs(RemovedAfterNextVersionWarning, ["c"])(original)
|
||||||
|
self.assertEqual(original.__name__, decorated.__name__)
|
||||||
|
self.assertEqual(original.__qualname__, decorated.__qualname__)
|
||||||
|
self.assertEqual(original.__doc__, decorated.__doc__)
|
||||||
|
self.assertEqual(inspect.signature(original), inspect.signature(decorated))
|
Loading…
x
Reference in New Issue
Block a user