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]

Reply via email to