jenkins-bot has submitted this change. (
https://gerrit.wikimedia.org/r/c/pywikibot/core/+/1193468?usp=email )
Change subject: IMPR: rename deprecate_positional to deprecated_signature
......................................................................
IMPR: rename deprecate_positional to deprecated_signature
Also
- deprecate keyword arguments used for positional only parameters (PEP 570)
- added some tests
Change-Id: I3d54e0e26d17aea0ff8c973869cf9fc57f63b2c2
---
M pywikibot/_wbtypes.py
M pywikibot/daemonize.py
M pywikibot/page/_basepage.py
M pywikibot/site/_apisite.py
M pywikibot/site/_generators.py
M pywikibot/throttle.py
M pywikibot/tools/__init__.py
M pywikibot/tools/_deprecate.py
M tests/tools_deprecate_tests.py
9 files changed, 174 insertions(+), 83 deletions(-)
Approvals:
jenkins-bot: Verified
Xqt: Looks good to me, approved
diff --git a/pywikibot/_wbtypes.py b/pywikibot/_wbtypes.py
index 3713931..89dea48 100644
--- a/pywikibot/_wbtypes.py
+++ b/pywikibot/_wbtypes.py
@@ -21,7 +21,7 @@
from pywikibot.backports import Iterator
from pywikibot.time import Timestamp
from pywikibot.tools import (
- deprecate_positionals,
+ deprecated_signature,
issue_deprecation_warning,
remove_last_args,
)
@@ -113,7 +113,7 @@
_items = ('lat', 'lon', 'entity')
- @deprecate_positionals(since='10.4.0')
+ @deprecated_signature(since='10.4.0')
def __init__(
self,
lat: float,
@@ -326,7 +326,7 @@
)
return self._dim
- @deprecate_positionals(since='10.4.0')
+ @deprecated_signature(since='10.4.0')
def get_globe_item(self, repo: DataSite | None = None, *,
lazy_load: bool = False) -> pywikibot.ItemPage:
"""Return the ItemPage corresponding to the globe.
@@ -436,7 +436,7 @@
_timestr_re = re.compile(
r'([-+]?\d{1,16})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z')
- @deprecate_positionals(since='10.4.0')
+ @deprecated_signature(since='10.4.0')
def __init__(
self,
year: int,
@@ -651,7 +651,7 @@
return self._getSecondsAdjusted() == other._getSecondsAdjusted()
@classmethod
- @deprecate_positionals(since='10.4.0')
+ @deprecated_signature(since='10.4.0')
def fromTimestr(
cls,
datetimestr: str,
@@ -703,7 +703,7 @@
timezone=timezone, calendarmodel=calendarmodel, site=site)
@classmethod
- @deprecate_positionals(since='10.4.0')
+ @deprecated_signature(since='10.4.0')
def fromTimestamp(
cls,
timestamp: Timestamp,
diff --git a/pywikibot/daemonize.py b/pywikibot/daemonize.py
index 9bdc9e5..a00c131 100644
--- a/pywikibot/daemonize.py
+++ b/pywikibot/daemonize.py
@@ -64,7 +64,7 @@
from enum import IntEnum
from pathlib import Path
-from pywikibot.tools import deprecate_positionals
+from pywikibot.tools import deprecated_signature
class StandardFD(IntEnum):
@@ -79,7 +79,7 @@
is_daemon = False
-@deprecate_positionals(since='10.6.0')
+@deprecated_signature(since='10.6.0')
def daemonize(*,
close_fd: bool = True,
chdir: bool = True,
diff --git a/pywikibot/page/_basepage.py b/pywikibot/page/_basepage.py
index f10c1d1..93af7cf 100644
--- a/pywikibot/page/_basepage.py
+++ b/pywikibot/page/_basepage.py
@@ -38,9 +38,9 @@
from pywikibot.tools import (
ComparableMixin,
cached,
- deprecate_positionals,
deprecated,
deprecated_args,
+ deprecated_signature,
first_upper,
)
@@ -1457,7 +1457,7 @@
force=force, asynchronous=asynchronous, callback=callback,
**kwargs)
- @deprecate_positionals(since='10.4.0')
+ @deprecated_signature(since='10.4.0')
def watch(
self, *,
unwatch: bool = False,
@@ -1660,7 +1660,7 @@
"""Convenience function to get the Wikibase item of a page."""
return pywikibot.ItemPage.fromPage(self)
- @deprecate_positionals(since='9.2')
+ @deprecated_signature(since='9.2')
def templates(self,
*,
content: bool = False,
@@ -1705,7 +1705,7 @@
return list(self._templates)
- @deprecate_positionals(since='9.2')
+ @deprecated_signature(since='9.2')
def itertemplates(
self,
total: int | None = None,
diff --git a/pywikibot/site/_apisite.py b/pywikibot/site/_apisite.py
index ec61c45..a796814 100644
--- a/pywikibot/site/_apisite.py
+++ b/pywikibot/site/_apisite.py
@@ -73,8 +73,8 @@
MediaWikiVersion,
cached,
deprecate_arg,
- deprecate_positionals,
deprecated,
+ deprecated_signature,
issue_deprecation_warning,
merge_unique_dicts,
normalize_username,
@@ -2995,7 +2995,7 @@
return req.submit()
@need_right('editmywatchlist')
- @deprecate_positionals(since='10.4.0')
+ @deprecated_signature(since='10.4.0')
def watch(
self,
pages: BasePage | str | list[BasePage | str],
diff --git a/pywikibot/site/_generators.py b/pywikibot/site/_generators.py
index 9901495..8bdc721 100644
--- a/pywikibot/site/_generators.py
+++ b/pywikibot/site/_generators.py
@@ -26,7 +26,7 @@
)
from pywikibot.site._decorators import need_right
from pywikibot.site._namespace import NamespaceArgType
-from pywikibot.tools import deprecate_arg, deprecate_positionals, is_ip_address
+from pywikibot.tools import deprecate_arg, deprecated_signature, is_ip_address
from pywikibot.tools.itertools import filter_unique
@@ -925,7 +925,7 @@
for linkdata in pageitem['extlinks']:
yield linkdata['*']
- @deprecate_positionals(since='10.4.0')
+ @deprecated_signature(since='10.4.0')
def allpages(
self,
start: str = '!', *,
diff --git a/pywikibot/throttle.py b/pywikibot/throttle.py
index 9d8c7da..eb270fb 100644
--- a/pywikibot/throttle.py
+++ b/pywikibot/throttle.py
@@ -27,7 +27,7 @@
import pywikibot
from pywikibot import config
from pywikibot.backports import Counter as CounterType
-from pywikibot.tools import deprecate_positionals, deprecated, deprecated_args
+from pywikibot.tools import deprecated, deprecated_args, deprecated_signature
FORMAT_LINE = '{module_id} {pid} {time} {site}\n'
@@ -343,7 +343,7 @@
time.sleep(seconds)
@deprecated_args(requestsize=None) # since: 10.3.0
- @deprecate_positionals(since='10.3.0')
+ @deprecated_signature(since='10.3.0')
def __call__(self, *, requestsize: int = 1, write: bool = False) -> None:
"""Apply throttling based on delay rules and request type.
diff --git a/pywikibot/tools/__init__.py b/pywikibot/tools/__init__.py
index a39cb51..bfc8da1 100644
--- a/pywikibot/tools/__init__.py
+++ b/pywikibot/tools/__init__.py
@@ -29,9 +29,9 @@
add_decorated_full_name,
add_full_name,
deprecate_arg,
- deprecate_positionals,
deprecated,
deprecated_args,
+ deprecated_signature,
get_wrapper_depth,
issue_deprecation_warning,
manage_wrapping,
@@ -55,9 +55,9 @@
'add_decorated_full_name',
'add_full_name',
'deprecate_arg',
- 'deprecate_positionals',
'deprecated',
'deprecated_args',
+ 'deprecated_signature',
'get_wrapper_depth',
'issue_deprecation_warning',
'manage_wrapping',
diff --git a/pywikibot/tools/_deprecate.py b/pywikibot/tools/_deprecate.py
index 4f6ffe9..e72f514 100644
--- a/pywikibot/tools/_deprecate.py
+++ b/pywikibot/tools/_deprecate.py
@@ -443,28 +443,35 @@
return decorator
-def deprecate_positionals(since: str = ''):
- """Decorator for methods that issues warnings for positional arguments.
+def deprecated_signature(since: str = ''):
+ """Decorator handling deprecated changes in function or method signatures.
- This decorator allows positional arguments after keyword-only
- argument syntax (:pep:`3102`) but throws a ``FutureWarning``. It
- automatically maps the provided positional arguments to their
- corresponding keyword-only parameters before invoking the decorated
- method.
+ This decorator supports:
- The intended use is during a deprecation period in which certain
- parameters should be passed as keyword-only, allowing legacy calls
- to continue working with a warning rather than immediately raising a
- ``TypeError``.
+ - Deprecation of positional arguments that have been converted to
+ keyword-only parameters.
+ - Detection of invalid keyword usage for positional-only parameters.
- .. important:: This decorator is only supported for instance or
- class methods. It does not work for standalone functions.
+ Positional-only parameters (introduced in :pep:`570`) must be passed
+ positionally. If such parameters are passed as keyword arguments,
+ this decorator will emit a ``FutureWarning`` and automatically remap
+ them to positional arguments for backward compatibility.
+
+ It allows positional arguments after keyword-only syntax (:pep:`3102`)
+ but emits a ``FutureWarning``. Positional arguments that are now
+ keyword-only are automatically mapped to their corresponding
+ keyword parameters before the decorated function or method is
+ invoked.
+
+ The intended use is during a deprecation period, allowing legacy
+ calls to continue working with a warning instead of raising a
+ ``TypeError`` immediately.
Example:
.. code-block:: python
- @deprecate_positionals(since='9.2.0')
+ @deprecated_signature(since='10.6.0')
def f(posarg, *, kwarg):
...
@@ -473,18 +480,27 @@
This function call passes but throws a ``FutureWarning``.
Without the decorator, a ``TypeError`` would be raised.
- .. caution:: The decorated function must not accept ``*args``. The
- sequence of keyword-only arguments must match the sequence of the
- old positional parameters, otherwise argument assignment will
- fail.
+ .. note::
+ If the parameter name was changed, use :func:`deprecated_args`
+ first.
+
+ .. caution::
+ The decorated function must not accept ``*args``. The order of
+ keyword-only arguments must match the order of the old positional
+ parameters; otherwise, argument assignment may fail.
.. versionadded:: 9.2
.. versionchanged:: 10.4
Raises ``ValueError`` if method has a ``*args`` parameter.
+ .. versionchanged:: 10.6
+ Renamed from ``deprecate_positionals``. Adds handling of
+ positional-only parameters and emits warnings if they are passed
+ as keyword arguments.
- :param since: Mandatory version string indicating when certain
- positional parameters were deprecated
- :raises ValueError: If the method has an *args parameter.
+ :param since: Mandatory version string indicating when signature
+ changed.
+ :raises TypeError: If required positional arguments are missing.
+ :raises ValueError: If the method has an ``*args`` parameter.
"""
def decorator(func):
"""Outer wrapper. Inspect the parameters of *func*.
@@ -502,10 +518,64 @@
:return: the value returned by the decorated function or
method
"""
+ # 1. fix deprecated positional-only usage
+ pos_only_in_kwargs = {
+ name: kwargs[name]
+ for name, p in params.items()
+ if p.kind == const.POSITIONAL_ONLY and name in kwargs
+ }
+
+ if pos_only_in_kwargs:
+ new_args: list[Any] = []
+ args_repr = [] # build representation for deprecation warning
+ idx = 0 # index for args
+
+ for name in arg_keys:
+ param = params[name]
+
+ if param.kind != const.POSITIONAL_ONLY:
+ # append remaining POSITIONAL_OR_KEYWORD arguments
+ new_args.extend(args[idx:])
+ break
+
+ if name in pos_only_in_kwargs:
+ # Value was passed as keyword → use it
+ value = kwargs.pop(name)
+ args_repr.append(repr(value))
+ elif idx < len(args):
+ # Value from original args
+ value = args[idx]
+ idx += 1
+ # Add ellipsis once for original args
+ if name not in ('cls', 'self') and (
+ not args_repr or args_repr[-1] != '...'):
+ args_repr.append('...')
+ elif param.default is not param.empty:
+ # Value from default → show actual value
+ value = param.default
+ args_repr.append(repr(value))
+ else:
+ raise TypeError(
+ f'Missing required positional argument: {name}'
+ )
+
+ new_args.append(value)
+
+ args = tuple(new_args)
+
+ args_str = ', '.join(args_repr)
+ issue_deprecation_warning(
+ f'Passing positional-only arguments as keywords to '
+ f"{func.__qualname__}(): {', '.join(pos_only_in_kwargs)}",
+ f'positional arguments like {func.__name__}({args_str})',
+ since=since
+ )
+
+ # 2. warn for deprecated keyword-only usage as positional
if len(args) > positionals:
replace_args = list(zip(arg_keys[positionals:],
args[positionals:]))
- pos_args = "', '".join(name for name, arg in replace_args)
+ pos_args = "', '".join(name for name, _ in replace_args)
keyw_args = ', '.join(f'{name}={arg!r}'
for name, arg in replace_args)
issue_deprecation_warning(
@@ -520,17 +590,25 @@
return func(*args, **kwargs)
sig = inspect.signature(func)
+ params = sig.parameters
arg_keys = list(sig.parameters)
+ const = inspect.Parameter
# find the first KEYWORD_ONLY index
+ positionals = 0
for positionals, key in enumerate(arg_keys):
- if sig.parameters[key].kind == inspect.Parameter.VAR_POSITIONAL:
+ kind = params[key].kind
+
+ # disallow *args entirely
+ if kind == const.VAR_POSITIONAL:
raise ValueError(
f'{func.__qualname__} must not have *{key} parameter')
- if sig.parameters[key].kind in (inspect.Parameter.KEYWORD_ONLY,
- inspect.Parameter.VAR_KEYWORD):
+ # stop counting when we reach keyword-only or **kwargs
+ if kind in (const.KEYWORD_ONLY, const.VAR_KEYWORD):
break
+ else:
+ positionals += 1 # all were positional, no keyword found
return wrapper
diff --git a/tests/tools_deprecate_tests.py b/tests/tools_deprecate_tests.py
index 728bb6f..5657ea3 100755
--- a/tests/tools_deprecate_tests.py
+++ b/tests/tools_deprecate_tests.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""Tests for deprecation tools."""
#
-# (C) Pywikibot team, 2014-2024
+# (C) Pywikibot team, 2014-2025
#
# Distributed under the terms of the MIT license.
#
@@ -15,9 +15,9 @@
PYTHON_VERSION,
add_full_name,
deprecate_arg,
- deprecate_positionals,
deprecated,
deprecated_args,
+ deprecated_signature,
remove_last_args,
)
from tests.aspects import DeprecationTestCase
@@ -154,9 +154,9 @@
return foo
-@deprecate_positionals()
-def positionals_test_function(foo: str, *,
- bar: int, baz: str = '') -> tuple[int, str]:
+@deprecated_signature()
+def positionals_test_function(foo: str, /, *,
+ bar: int, baz: str = '') -> tuple[str, int]:
"""Deprecating positional parameters."""
return foo + baz, bar ** 2
@@ -240,9 +240,9 @@
"""Deprecating last positional parameter."""
return foo
- @deprecate_positionals()
- def test_method(self, foo: str, *,
- bar: int = 5, baz: str = '') -> tuple[int, str]:
+ @deprecated_signature()
+ def test_method(self, foo: str, /, *,
+ bar: int = 5, baz: str = '') -> tuple[str, int]:
"""Deprecating positional parameters."""
return foo + baz, bar ** 2
@@ -613,81 +613,94 @@
" The value(s) provided for 'bar' have been dropped."
)
- def test_deprecate_positionals(self) -> None:
- """Test deprecation of positional parameters."""
- msg = ('Passing {param} as positional argument(s) to {func}() is '
- 'deprecated; use keyword arguments like {instead} instead.')
+ def test_deprecated_signature(self) -> None:
+ """Test deprecation of parameters signature."""
+ msg1 = ('Passing {param} as positional argument(s) to {func}() is '
+ 'deprecated; use keyword arguments like {instead} instead.')
+ msg2 = (
+ 'Passing positional-only arguments as keywords to {qual}(): '
+ 'foo is deprecated; '
+ "use positional arguments like {func}('Pywiki') instead."
+ )
f = DeprecatedMethodClass().test_method
- func = 'DeprecatedMethodClass.test_method'
+ qual = f.__qualname__
+ func = f.__name__
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'"))
+ self.assertOneDeprecation(msg1.format(param="'bar', 'baz'",
+ func=qual,
+ 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'))
+ self.assertOneDeprecation(msg1.format(param="'bar'",
+ func=qual,
+ 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'))
+ self.assertOneDeprecation(msg1.format(param="'bar'",
+ func=qual,
+ instead='bar=3'))
with self.subTest(test=4):
- rv1, rv2 = f('Pywiki', bar=4)
+ rv1, rv2 = f(foo='Pywiki')
self.assertEqual(rv1, 'Pywiki')
- self.assertEqual(rv2, 16)
- self.assertNoDeprecation()
+ self.assertEqual(rv2, 25)
+ self.assertOneDeprecation(msg2.format(qual=qual, func=func))
with self.subTest(test=5):
- rv1, rv2 = f(foo='Pywiki')
+ rv1, rv2 = f('Pywiki', bar=5)
self.assertEqual(rv1, 'Pywiki')
self.assertEqual(rv2, 25)
self.assertNoDeprecation()
f = positionals_test_function
- func = 'positionals_test_function'
+ func = f.__name__
+ qual = f.__qualname__
- with self.subTest(test=6):
+ with self.subTest(test=1):
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'"))
+ self.assertOneDeprecation(msg1.format(param="'bar', 'baz'",
+ func=qual,
+ 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'))
+ self.assertOneDeprecation(msg1.format(param="'bar'",
+ func=qual,
+ 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'))
+ self.assertOneDeprecation(msg1.format(param="'bar'",
+ func=qual,
+ instead='bar=8'))
with self.subTest(test=9):
- rv1, rv2 = f('Pywiki', bar=9)
+ rv1, rv2 = f(foo='Pywiki', bar=9)
self.assertEqual(rv1, 'Pywiki')
self.assertEqual(rv2, 81)
+ self.assertOneDeprecation(msg2.format(qual=qual, func=func))
+
+ with self.subTest(test=10):
+ rv1, rv2 = f('Pywiki', bar=10)
+ self.assertEqual(rv1, 'Pywiki')
+ self.assertEqual(rv2, 100)
self.assertNoDeprecation()
def test_remove_last_args_invalid(self) -> None:
--
To view, visit
https://gerrit.wikimedia.org/r/c/pywikibot/core/+/1193468?usp=email
To unsubscribe, or for help writing mail filters, visit
https://gerrit.wikimedia.org/r/settings?usp=email
Gerrit-MessageType: merged
Gerrit-Project: pywikibot/core
Gerrit-Branch: master
Gerrit-Change-Id: I3d54e0e26d17aea0ff8c973869cf9fc57f63b2c2
Gerrit-Change-Number: 1193468
Gerrit-PatchSet: 9
Gerrit-Owner: Xqt <[email protected]>
Gerrit-Reviewer: Xqt <[email protected]>
Gerrit-Reviewer: jenkins-bot
_______________________________________________
Pywikibot-commits mailing list -- [email protected]
To unsubscribe send an email to [email protected]