jenkins-bot has submitted this change. (
https://gerrit.wikimedia.org/r/c/pywikibot/core/+/1033627?usp=email )
Change subject: [IMPR] Add a new decorator to deprecate positional arguments
......................................................................
[IMPR] Add a new decorator to deprecate positional arguments
The new deprecate_positionals decorator can be used to circumvent a
TypeError when using positional arguments if a keyword-only argument is
required but throws a FutureWarning. The advantage is to have a
deprecation period when introducing keyword-only syntax for methods
and functions.
Change-Id: I549a2e951e5b0c1ed10f2a1d656b27b226d03c63
---
M pywikibot/tools/__init__.py
M pywikibot/tools/_deprecate.py
M tests/tools_deprecate_tests.py
3 files changed, 173 insertions(+), 1 deletion(-)
Approvals:
Xqt: Looks good to me, approved
jenkins-bot: Verified
diff --git a/pywikibot/tools/__init__.py b/pywikibot/tools/__init__.py
index 586684e..32c0bf2 100644
--- a/pywikibot/tools/__init__.py
+++ b/pywikibot/tools/__init__.py
@@ -52,6 +52,7 @@
add_decorated_full_name,
add_full_name,
deprecate_arg,
+ deprecate_positionals,
deprecated,
deprecated_args,
get_wrapper_depth,
@@ -69,6 +70,7 @@
'add_decorated_full_name',
'add_full_name',
'deprecate_arg',
+ 'deprecate_positionals',
'deprecated',
'deprecated_args',
'get_wrapper_depth',
diff --git a/pywikibot/tools/_deprecate.py b/pywikibot/tools/_deprecate.py
index 9bbc133..05d5bc3 100644
--- a/pywikibot/tools/_deprecate.py
+++ b/pywikibot/tools/_deprecate.py
@@ -19,7 +19,7 @@
deprecation decorators moved to _deprecate submodule
"""
#
-# (C) Pywikibot team, 2008-2023
+# (C) Pywikibot team, 2008-2024
#
# Distributed under the terms of the MIT license.
#
@@ -31,6 +31,7 @@
import sys
import types
from contextlib import suppress
+from functools import wraps
from importlib import import_module
from inspect import getfullargspec
from typing import Any
@@ -431,6 +432,84 @@
return decorator
+def deprecate_positionals(since: str = ''):
+ """Decorator for methods that issues warnings for positional arguments.
+
+ This decorator allowes positional arguments after keyword-only
+ argument syntax (:pep:`3102`) but throws a FutureWarning. The
+ decorator makes the needed argument updates before passing them to
+ the called function or method. This decorator may be used for a
+ deprecation period when require keyword-only arguments.
+
+ Example:
+
+ .. code-block:: python
+
+ @deprecate_positionals(since='9.2.0')
+ def f(posarg, *, kwarg):
+ ...
+
+ f('foo', 'bar')
+
+ This function call passes but throws a FutureWarning. Without
+ decorator a TypeError would be raised.
+
+ .. caution:: The decorated function may not use ``*args`` or
+ ``**kwargs``. The sequence of keyword-only arguments must match
+ the sequence of the old positional arguments, otherwise the
+ assignment of the arguments to the keyworded arguments will fail.
+ .. versionadded:: 9.2
+
+ :param since: a version string when some positional arguments were
+ deprecated
+ """
+ def decorator(func):
+ """Outer wrapper. Inspect the parameters of *func*.
+
+ :param func: function or method beeing wrapped.
+ """
+
+ @wraps(func)
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
+ """Throws the warning and makes the argument fixing.
+
+ :param args: args passed to the decorated functoin or method
+ :param kwargs: kwargs passed to the decorated function or
+ method
+ :return: the value returned by the decorated function or
+ method
+ """
+ if len(args) > positionals:
+ replace_args = list(zip(arg_keys[positionals:],
+ args[positionals:]))
+ pos_args = "', '".join(name for name, arg in replace_args)
+ keyw_args = ', '.join('{}={!r}'.format(name, arg)
+ for name, arg in replace_args)
+ issue_deprecation_warning(
+ f"Passing '{pos_args}' as positional "
+ f'argument(s) to {func.__qualname__}()',
+ f'keyword arguments like {keyw_args}',
+ since=since)
+
+ args = args[:positionals]
+ kwargs.update(replace_args)
+
+ return func(*args, **kwargs)
+
+ sig = inspect.signature(func)
+ arg_keys = list(sig.parameters)
+
+ # find the first KEYWORD_ONLY index
+ for positionals, key in enumerate(arg_keys):
+ if sig.parameters[key].kind in (inspect.Parameter.KEYWORD_ONLY,
+ inspect.Parameter.VAR_KEYWORD):
+ break
+
+ return wrapper
+
+ return decorator
+
+
def remove_last_args(arg_names):
"""
Decorator to declare all args additionally provided deprecated.
diff --git a/tests/tools_deprecate_tests.py b/tests/tools_deprecate_tests.py
index fd5cc21..ebdd248 100755
--- a/tests/tools_deprecate_tests.py
+++ b/tests/tools_deprecate_tests.py
@@ -14,6 +14,7 @@
PYTHON_VERSION,
add_full_name,
deprecate_arg,
+ deprecate_positionals,
deprecated,
deprecated_args,
remove_last_args,
@@ -153,6 +154,13 @@
return foo
+@deprecate_positionals()
+def positionals_test_function(foo: str, *,
+ bar: int, baz: str = '') -> tuple[int, str]:
+ """Deprecating positional parameters."""
+ return foo + baz, bar ** 2
+
+
class DeprecatedMethodClass:
"""Class with methods deprecated."""
@@ -232,6 +240,12 @@
"""Deprecating last positional parameter."""
return foo
+ @deprecate_positionals()
+ def test_method(self, foo: str, *,
+ bar: int = 5, baz: str = '') -> tuple[int, str]:
+ """Deprecating positional parameters."""
+ return foo + baz, bar ** 2
+
@deprecated()
class DeprecatedClassNoInit:
@@ -606,6 +620,83 @@
"The value(s) provided for 'bar' have been dropped."
.format(__name__))
+ def test_deprecate_positionals(self):
+ """Test deprecation of positional parameters."""
+ msg = ('Passing {param} as positional argument(s) to {func}() is '
+ 'deprecated; use keyword arguments like {instead} instead.')
+
+ f = DeprecatedMethodClass().test_method
+ func = 'DeprecatedMethodClass.test_method'
+
+ with self.subTest(test=1):
+ rv1, rv2 = f('Pywiki', 1, 'bot')
+ self.assertEqual(rv1, 'Pywikibot')
+ self.assertEqual(rv2, 1)
+ self.assertOneDeprecation(msg.format(param="'bar', 'baz'",
+ func=func,
+ instead="bar=1, baz='bot'"))
+
+ with self.subTest(test=2):
+ rv1, rv2 = f('Pywiki', 2)
+ self.assertEqual(rv1, 'Pywiki')
+ self.assertEqual(rv2, 4)
+ self.assertOneDeprecation(msg.format(param="'bar'",
+ func=func,
+ instead='bar=2'))
+
+ with self.subTest(test=3):
+ rv1, rv2 = f('Pywiki', 3, baz='bot')
+ self.assertEqual(rv1, 'Pywikibot')
+ self.assertEqual(rv2, 9)
+ self.assertOneDeprecation(msg.format(param="'bar'",
+ func=func,
+ instead='bar=3'))
+
+ with self.subTest(test=4):
+ rv1, rv2 = f('Pywiki', bar=4)
+ self.assertEqual(rv1, 'Pywiki')
+ self.assertEqual(rv2, 16)
+ self.assertNoDeprecation()
+
+ with self.subTest(test=5):
+ rv1, rv2 = f(foo='Pywiki')
+ self.assertEqual(rv1, 'Pywiki')
+ self.assertEqual(rv2, 25)
+ self.assertNoDeprecation()
+
+ f = positionals_test_function
+ func = 'positionals_test_function'
+
+ with self.subTest(test=6):
+ rv1, rv2 = f('Pywiki', 6, 'bot')
+ self.assertEqual(rv1, 'Pywikibot')
+ self.assertEqual(rv2, 36)
+ self.assertOneDeprecation(msg.format(param="'bar', 'baz'",
+ func=func,
+ instead="bar=6, baz='bot'"))
+
+ with self.subTest(test=7):
+ rv1, rv2 = f('Pywiki', 7)
+ self.assertEqual(rv1, 'Pywiki')
+ self.assertEqual(rv2, 49)
+ self.assertOneDeprecation(msg.format(param="'bar'",
+ func=func,
+ instead='bar=7'))
+
+ with self.subTest(test=8):
+ rv1, rv2 = f('Pywiki', 8, baz='bot')
+ self.assertEqual(rv1, 'Pywikibot')
+ self.assertEqual(rv2, 64)
+ self.assertOneDeprecation(msg.format(param="'bar'",
+ func=func,
+ instead='bar=8'))
+
+ with self.subTest(test=9):
+ rv1, rv2 = f('Pywiki', bar=9)
+ self.assertEqual(rv1, 'Pywiki')
+ self.assertEqual(rv2, 81)
+ self.assertNoDeprecation()
+
def test_remove_last_args_invalid(self):
"""Test invalid @remove_last_args on functions."""
with self.assertRaisesRegex(
--
To view, visit
https://gerrit.wikimedia.org/r/c/pywikibot/core/+/1033627?usp=email
To unsubscribe, or for help writing mail filters, visit
https://gerrit.wikimedia.org/r/settings
Gerrit-Project: pywikibot/core
Gerrit-Branch: master
Gerrit-Change-Id: I549a2e951e5b0c1ed10f2a1d656b27b226d03c63
Gerrit-Change-Number: 1033627
Gerrit-PatchSet: 6
Gerrit-Owner: Xqt <[email protected]>
Gerrit-Reviewer: Xqt <[email protected]>
Gerrit-Reviewer: jenkins-bot
Gerrit-MessageType: merged
_______________________________________________
Pywikibot-commits mailing list -- [email protected]
To unsubscribe send an email to [email protected]