mirror of
https://github.com/django/django.git
synced 2025-08-23 10:19:13 +00:00
This helper allows marking positional-or-keyword parameters as keyword-only with a deprecation period, in a consistent and correct manner.
405 lines
14 KiB
Python
405 lines
14 KiB
Python
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))
|