From b40da5452f3bbf702ebde2489af1e8db90f5edb7 Mon Sep 17 00:00:00 2001 From: Xucheng Zhou Date: Sun, 11 Oct 2026 08:00:35 +0800 Subject: [PATCH 1/6] Deliver optional positional-only hook arguments positionally Fixes #774 Assisted-by: OpenAI Codex Signed-off-by: Xucheng Zhou --- changelog/774.bugfix.rst | 3 + src/pluggy/_execution.py | 12 +++ src/pluggy/_impl.py | 20 +++++ testing/test_posonly_defaults.py | 122 +++++++++++++++++++++++++++++++ 4 files changed, 157 insertions(+) create mode 100644 changelog/774.bugfix.rst create mode 100644 testing/test_posonly_defaults.py diff --git a/changelog/774.bugfix.rst b/changelog/774.bugfix.rst new file mode 100644 index 00000000..1d5a2219 --- /dev/null +++ b/changelog/774.bugfix.rst @@ -0,0 +1,3 @@ +Deliver defaulted positional-only hook implementation arguments positionally, +including in hook wrappers and ``call_extra``. Explicit hook call values and +hookspec defaults keep their precedence over implementation defaults. diff --git a/src/pluggy/_execution.py b/src/pluggy/_execution.py index 0e62b4d3..c7756212 100644 --- a/src/pluggy/_execution.py +++ b/src/pluggy/_execution.py @@ -106,10 +106,22 @@ def _multicall( try: args = [caller_kwargs[argname] for argname in hook_impl.argnames] if hook_impl.kwargnames: + if hook_impl._posonlydefaults: + last_supplied = None + for argname in hook_impl._posonlydefaults: + if argname in caller_kwargs: + last_supplied = argname + if last_supplied is not None: + # Fill gaps, but leave trailing defaults implicit. + for argname, default in hook_impl._posonlydefaults.items(): + args.append(caller_kwargs.get(argname, default)) + if argname == last_supplied: + break kwargs = { argname: caller_kwargs[argname] for argname in hook_impl.kwargnames if argname in caller_kwargs + and argname not in hook_impl._posonlydefaults } or None else: kwargs = None diff --git a/src/pluggy/_impl.py b/src/pluggy/_impl.py index 1b7182f1..f96b5d89 100644 --- a/src/pluggy/_impl.py +++ b/src/pluggy/_impl.py @@ -6,6 +6,7 @@ from collections.abc import Callable from collections.abc import Generator +import inspect from typing import Final from typing import final from typing import TypeAlias @@ -27,6 +28,7 @@ class HookImpl: """A hook implementation in a :class:`HookCaller`.""" __slots__ = ( + "_posonlydefaults", "argnames", "function", "hookwrapper", @@ -56,6 +58,24 @@ def __init__( #: The keyword parameter names of ``function`` which declare defaults. #: Keyword-only parameters are *not* included. self.kwargnames: Final = kwargnames + # Optional positional-only arguments still need positional delivery. + # Inspect the same underlying callable as varnames, without evaluating + # annotations or requiring an inspect.Signature on the hot call path. + func = self.function + if inspect.isclass(func): + func = func.__init__ + elif not inspect.isroutine(func): + func = getattr(func, "__call__", func) # noqa: B004 + func = inspect.unwrap(func) + code = getattr(func, "__code__", None) + posonly = () if code is None else code.co_varnames[: code.co_posonlyargcount] + self._posonlydefaults: Final = { + name: default + for name, default in zip( + kwargnames, getattr(func, "__defaults__", ()) or () + ) + if name in posonly + } #: The plugin which defined this hook implementation. self.plugin: Final = plugin #: The :class:`HookimplOpts` used to configure this hook implementation. diff --git a/testing/test_posonly_defaults.py b/testing/test_posonly_defaults.py new file mode 100644 index 00000000..1eaf64a2 --- /dev/null +++ b/testing/test_posonly_defaults.py @@ -0,0 +1,122 @@ +from collections.abc import Callable +from functools import wraps + +import pytest + +from pluggy import HookimplMarker +from pluggy import HookspecMarker +from pluggy import PluginManager + + +hookspec = HookspecMarker("posonly") +hookimpl = HookimplMarker("posonly") + + +@pytest.mark.parametrize("style", ["plain", "wrapper", "hookwrapper"]) +@pytest.mark.parametrize("supplied", [False, True]) +def test_defaulted_positional_only_delivery(style, supplied): + class Spec: + @hookspec + def hello(self, value): + pass + + class Plain: + @hookimpl + def hello(self, value="fallback", /): + return value + + class Wrapper: + @hookimpl(wrapper=True) + def hello(self, value="fallback", /): + yield + return value + + class Hookwrapper: + @hookimpl(hookwrapper=True) + def hello(self, value="fallback", /): + outcome = yield + outcome.force_result(value) + + manager = PluginManager("posonly") + manager.add_hookspecs(Spec) + manager.register( + {"plain": Plain, "wrapper": Wrapper, "hookwrapper": Hookwrapper}[style]() + ) + if supplied: + result = manager.hook.hello(value="caller") + assert result == (["caller"] if style == "plain" else "caller") + else: + with pytest.warns(UserWarning): + result = manager.hook.hello() + assert result == (["fallback"] if style == "plain" else "fallback") + + +@pytest.mark.parametrize("kind", ["method", "callable", "decorated"]) +@pytest.mark.parametrize( + "kwargs, expected", + [ + ({}, (1, 2, 3)), + ({"first": 10}, (10, 2, 3)), + ({"second": 20}, (1, 20, 3)), + ({"tail": 30}, (1, 2, 30)), + ({"first": None, "second": 0, "tail": False}, (None, 0, False)), + ], +) +def test_positional_only_default_gaps(kind, kwargs, expected): + positional_calls = [] + + class Spec: + @hookspec + def hello(self): + pass + + def decorate(function: Callable[..., object]) -> Callable[..., object]: + @wraps(function) + def wrapped(*args, **kwargs): + positional_calls.append(args) + return function(*args, **kwargs) + + return wrapped + + class Plugin: + @hookimpl + def hello(self, first=1, second=2, /, tail=3): + return first, second, tail + + class CallablePlugin: + def __call__(self, first=1, second=2, /, tail=3): + return first, second, tail + + manager = PluginManager("posonly") + manager.add_hookspecs(Spec) + if kind == "method": + manager.register(Plugin()) + result = manager.hook.hello(**kwargs) + else: + function = CallablePlugin() if kind == "callable" else decorate(Plugin().hello) + result = manager.hook.hello.call_extra([function], kwargs) + assert result == [expected] + if kind == "decorated": + if "second" in kwargs: + assert positional_calls == [expected[:2]] + elif "first" in kwargs: + assert positional_calls == [expected[:1]] + else: + assert positional_calls == [()] + + +def test_required_positional_only_control(): + class Spec: + @hookspec + def hello(self, first, second=20): + pass + + class Plugin: + @hookimpl + def hello(self, first, second=2, /, tail=3): + return first, second, tail + + manager = PluginManager("posonly") + manager.add_hookspecs(Spec) + manager.register(Plugin()) + assert manager.hook.hello(first=10) == [(10, 20, 3)] From dcfb4601a1a11d6ce03ce946fb5174146afab1fa Mon Sep 17 00:00:00 2001 From: Xucheng Zhou Date: Sun, 11 Oct 2026 08:07:55 +0800 Subject: [PATCH 2/6] Cover class call_extra and avoid unreachable binding branch Assisted-by: OpenAI Codex Signed-off-by: Xucheng Zhou --- src/pluggy/_execution.py | 19 ++++++++++--------- testing/test_posonly_defaults.py | 11 ++++++++++- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/pluggy/_execution.py b/src/pluggy/_execution.py index c7756212..b11d0282 100644 --- a/src/pluggy/_execution.py +++ b/src/pluggy/_execution.py @@ -107,16 +107,17 @@ def _multicall( args = [caller_kwargs[argname] for argname in hook_impl.argnames] if hook_impl.kwargnames: if hook_impl._posonlydefaults: - last_supplied = None - for argname in hook_impl._posonlydefaults: + last_supplied = 0 + for index, argname in enumerate(hook_impl._posonlydefaults, 1): if argname in caller_kwargs: - last_supplied = argname - if last_supplied is not None: - # Fill gaps, but leave trailing defaults implicit. - for argname, default in hook_impl._posonlydefaults.items(): - args.append(caller_kwargs.get(argname, default)) - if argname == last_supplied: - break + last_supplied = index + # Fill gaps, but leave trailing defaults implicit. + args.extend( + caller_kwargs.get(argname, default) + for argname, default in list( + hook_impl._posonlydefaults.items() + )[:last_supplied] + ) kwargs = { argname: caller_kwargs[argname] for argname in hook_impl.kwargnames diff --git a/testing/test_posonly_defaults.py b/testing/test_posonly_defaults.py index 1eaf64a2..3b2bca7d 100644 --- a/testing/test_posonly_defaults.py +++ b/testing/test_posonly_defaults.py @@ -51,7 +51,7 @@ def hello(self, value="fallback", /): assert result == (["fallback"] if style == "plain" else "fallback") -@pytest.mark.parametrize("kind", ["method", "callable", "decorated"]) +@pytest.mark.parametrize("kind", ["method", "callable", "decorated", "class"]) @pytest.mark.parametrize( "kwargs, expected", [ @@ -87,11 +87,20 @@ class CallablePlugin: def __call__(self, first=1, second=2, /, tail=3): return first, second, tail + class ClassPlugin: + def __init__(self, first=1, second=2, /, tail=3): + self.values = first, second, tail + manager = PluginManager("posonly") manager.add_hookspecs(Spec) if kind == "method": manager.register(Plugin()) result = manager.hook.hello(**kwargs) + elif kind == "class": + result = manager.hook.hello.call_extra([ClassPlugin], kwargs) + assert isinstance(result, list) + assert isinstance(result[0], ClassPlugin) + result = [result[0].values] else: function = CallablePlugin() if kind == "callable" else decorate(Plugin().hello) result = manager.hook.hello.call_extra([function], kwargs) From fd0298f87b576bb2354ea00a235b90208d138fe8 Mon Sep 17 00:00:00 2001 From: Xucheng Zhou Date: Thu, 15 Oct 2026 15:47:28 +0800 Subject: [PATCH 3/6] Deprecate positional-only hook implementation parameters Keep the existing call behavior as requested in #774. Remove the previous binding-path change; warn only for actual implementation parameters, not a bound self or cls receiver. Assisted-by: OpenAI Codex Signed-off-by: Xucheng Zhou --- changelog/774.bugfix.rst | 3 - changelog/774.deprecation.rst | 3 + src/pluggy/_decorators.py | 22 +++- src/pluggy/_execution.py | 13 --- src/pluggy/_impl.py | 22 +--- testing/test_posonly_defaults.py | 131 ---------------------- testing/test_posonly_deprecation.py | 164 ++++++++++++++++++++++++++++ 7 files changed, 189 insertions(+), 169 deletions(-) delete mode 100644 changelog/774.bugfix.rst create mode 100644 changelog/774.deprecation.rst delete mode 100644 testing/test_posonly_defaults.py create mode 100644 testing/test_posonly_deprecation.py diff --git a/changelog/774.bugfix.rst b/changelog/774.bugfix.rst deleted file mode 100644 index 1d5a2219..00000000 --- a/changelog/774.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -Deliver defaulted positional-only hook implementation arguments positionally, -including in hook wrappers and ``call_extra``. Explicit hook call values and -hookspec defaults keep their precedence over implementation defaults. diff --git a/changelog/774.deprecation.rst b/changelog/774.deprecation.rst new file mode 100644 index 00000000..7382395b --- /dev/null +++ b/changelog/774.deprecation.rst @@ -0,0 +1,3 @@ +Deprecate positional-only hook implementation parameters, without changing +their current call behavior. Use positional-or-keyword parameters instead. +Positional-only implementation parameters will become an error in pluggy 2.0. diff --git a/src/pluggy/_decorators.py b/src/pluggy/_decorators.py index c5574a67..dd2630c9 100644 --- a/src/pluggy/_decorators.py +++ b/src/pluggy/_decorators.py @@ -246,7 +246,7 @@ def setattr_hookimpl_opts(func: _F) -> _F: def varnames( - func: object, *, legacy_noself: bool = False + func: object, *, legacy_noself: bool = False, warn_posonly: bool = False ) -> tuple[tuple[str, ...], tuple[str, ...]]: """Return tuple of positional and keyword parameter names for a callable. @@ -260,6 +260,9 @@ def varnames( If ``True``, support hookspec classes whose methods omit ``self``. When the function looks like a class method but has no implicit first parameter, a :class:`DeprecationWarning` is emitted. + :param warn_posonly: + If ``True``, warn about positional-only hook implementation parameters, + excluding the already-bound instance/class parameter. """ is_bound = False if inspect.isclass(func): @@ -322,6 +325,23 @@ def varnames( stacklevel=2, ) + if warn_posonly: + positional_only = tuple( + name + for name in code.co_varnames[: code.co_posonlyargcount] + if name in args or name in kwargs + ) + if positional_only: + warnings.warn_explicit( + f"Positional-only parameters {positional_only!r} in {qualname} " + "are deprecated. Use positional-or-keyword parameters instead. " + "Positional-only hook implementation parameters will be an error " + "in pluggy 2.0.", + DeprecationWarning, + filename=code.co_filename, + lineno=code.co_firstlineno, + ) + return args, kwargs diff --git a/src/pluggy/_execution.py b/src/pluggy/_execution.py index b11d0282..0e62b4d3 100644 --- a/src/pluggy/_execution.py +++ b/src/pluggy/_execution.py @@ -106,23 +106,10 @@ def _multicall( try: args = [caller_kwargs[argname] for argname in hook_impl.argnames] if hook_impl.kwargnames: - if hook_impl._posonlydefaults: - last_supplied = 0 - for index, argname in enumerate(hook_impl._posonlydefaults, 1): - if argname in caller_kwargs: - last_supplied = index - # Fill gaps, but leave trailing defaults implicit. - args.extend( - caller_kwargs.get(argname, default) - for argname, default in list( - hook_impl._posonlydefaults.items() - )[:last_supplied] - ) kwargs = { argname: caller_kwargs[argname] for argname in hook_impl.kwargnames if argname in caller_kwargs - and argname not in hook_impl._posonlydefaults } or None else: kwargs = None diff --git a/src/pluggy/_impl.py b/src/pluggy/_impl.py index f96b5d89..c4e36f37 100644 --- a/src/pluggy/_impl.py +++ b/src/pluggy/_impl.py @@ -6,7 +6,6 @@ from collections.abc import Callable from collections.abc import Generator -import inspect from typing import Final from typing import final from typing import TypeAlias @@ -28,7 +27,6 @@ class HookImpl: """A hook implementation in a :class:`HookCaller`.""" __slots__ = ( - "_posonlydefaults", "argnames", "function", "hookwrapper", @@ -52,30 +50,12 @@ def __init__( """:meta private:""" #: The hook implementation function. self.function: Final = function - argnames, kwargnames = varnames(self.function) + argnames, kwargnames = varnames(self.function, warn_posonly=True) #: The positional parameter names of ``function``. self.argnames: Final = argnames #: The keyword parameter names of ``function`` which declare defaults. #: Keyword-only parameters are *not* included. self.kwargnames: Final = kwargnames - # Optional positional-only arguments still need positional delivery. - # Inspect the same underlying callable as varnames, without evaluating - # annotations or requiring an inspect.Signature on the hot call path. - func = self.function - if inspect.isclass(func): - func = func.__init__ - elif not inspect.isroutine(func): - func = getattr(func, "__call__", func) # noqa: B004 - func = inspect.unwrap(func) - code = getattr(func, "__code__", None) - posonly = () if code is None else code.co_varnames[: code.co_posonlyargcount] - self._posonlydefaults: Final = { - name: default - for name, default in zip( - kwargnames, getattr(func, "__defaults__", ()) or () - ) - if name in posonly - } #: The plugin which defined this hook implementation. self.plugin: Final = plugin #: The :class:`HookimplOpts` used to configure this hook implementation. diff --git a/testing/test_posonly_defaults.py b/testing/test_posonly_defaults.py deleted file mode 100644 index 3b2bca7d..00000000 --- a/testing/test_posonly_defaults.py +++ /dev/null @@ -1,131 +0,0 @@ -from collections.abc import Callable -from functools import wraps - -import pytest - -from pluggy import HookimplMarker -from pluggy import HookspecMarker -from pluggy import PluginManager - - -hookspec = HookspecMarker("posonly") -hookimpl = HookimplMarker("posonly") - - -@pytest.mark.parametrize("style", ["plain", "wrapper", "hookwrapper"]) -@pytest.mark.parametrize("supplied", [False, True]) -def test_defaulted_positional_only_delivery(style, supplied): - class Spec: - @hookspec - def hello(self, value): - pass - - class Plain: - @hookimpl - def hello(self, value="fallback", /): - return value - - class Wrapper: - @hookimpl(wrapper=True) - def hello(self, value="fallback", /): - yield - return value - - class Hookwrapper: - @hookimpl(hookwrapper=True) - def hello(self, value="fallback", /): - outcome = yield - outcome.force_result(value) - - manager = PluginManager("posonly") - manager.add_hookspecs(Spec) - manager.register( - {"plain": Plain, "wrapper": Wrapper, "hookwrapper": Hookwrapper}[style]() - ) - if supplied: - result = manager.hook.hello(value="caller") - assert result == (["caller"] if style == "plain" else "caller") - else: - with pytest.warns(UserWarning): - result = manager.hook.hello() - assert result == (["fallback"] if style == "plain" else "fallback") - - -@pytest.mark.parametrize("kind", ["method", "callable", "decorated", "class"]) -@pytest.mark.parametrize( - "kwargs, expected", - [ - ({}, (1, 2, 3)), - ({"first": 10}, (10, 2, 3)), - ({"second": 20}, (1, 20, 3)), - ({"tail": 30}, (1, 2, 30)), - ({"first": None, "second": 0, "tail": False}, (None, 0, False)), - ], -) -def test_positional_only_default_gaps(kind, kwargs, expected): - positional_calls = [] - - class Spec: - @hookspec - def hello(self): - pass - - def decorate(function: Callable[..., object]) -> Callable[..., object]: - @wraps(function) - def wrapped(*args, **kwargs): - positional_calls.append(args) - return function(*args, **kwargs) - - return wrapped - - class Plugin: - @hookimpl - def hello(self, first=1, second=2, /, tail=3): - return first, second, tail - - class CallablePlugin: - def __call__(self, first=1, second=2, /, tail=3): - return first, second, tail - - class ClassPlugin: - def __init__(self, first=1, second=2, /, tail=3): - self.values = first, second, tail - - manager = PluginManager("posonly") - manager.add_hookspecs(Spec) - if kind == "method": - manager.register(Plugin()) - result = manager.hook.hello(**kwargs) - elif kind == "class": - result = manager.hook.hello.call_extra([ClassPlugin], kwargs) - assert isinstance(result, list) - assert isinstance(result[0], ClassPlugin) - result = [result[0].values] - else: - function = CallablePlugin() if kind == "callable" else decorate(Plugin().hello) - result = manager.hook.hello.call_extra([function], kwargs) - assert result == [expected] - if kind == "decorated": - if "second" in kwargs: - assert positional_calls == [expected[:2]] - elif "first" in kwargs: - assert positional_calls == [expected[:1]] - else: - assert positional_calls == [()] - - -def test_required_positional_only_control(): - class Spec: - @hookspec - def hello(self, first, second=20): - pass - - class Plugin: - @hookimpl - def hello(self, first, second=2, /, tail=3): - return first, second, tail - - manager = PluginManager("posonly") - manager.add_hookspecs(Spec) - manager.register(Plugin()) - assert manager.hook.hello(first=10) == [(10, 20, 3)] diff --git a/testing/test_posonly_deprecation.py b/testing/test_posonly_deprecation.py new file mode 100644 index 00000000..180ebb54 --- /dev/null +++ b/testing/test_posonly_deprecation.py @@ -0,0 +1,164 @@ +from collections.abc import Callable +from functools import wraps +import inspect +import warnings + +import pytest + +from pluggy import HookimplMarker +from pluggy import HookspecMarker +from pluggy import PluginManager + + +hookspec = HookspecMarker("posonly") +hookimpl = HookimplMarker("posonly") + + +@pytest.mark.parametrize("style", ["plain", "wrapper", "hookwrapper"]) +@pytest.mark.parametrize("defaulted", [False, True]) +def test_positional_only_deprecation_preserves_calls(style, defaulted): + class Spec: + @hookspec + def hello(self, value): + pass + + def plain_required(self, value, /): + return value + + def plain_defaulted(self, value="fallback", /): + return value + + def wrapper_required(self, value, /): + yield + return value + + def wrapper_defaulted(self, value="fallback", /): + yield + return value + + def hookwrapper_required(self, value, /): + outcome = yield + outcome.force_result([value]) + + def hookwrapper_defaulted(self, value="fallback", /): + outcome = yield + outcome.force_result([value]) + + variants: dict[tuple[str, bool], Callable[..., object]] = { + ("plain", False): plain_required, + ("plain", True): plain_defaulted, + ("wrapper", False): wrapper_required, + ("wrapper", True): wrapper_defaulted, + ("hookwrapper", False): hookwrapper_required, + ("hookwrapper", True): hookwrapper_defaulted, + } + hello = variants[style, defaulted] + marked = hookimpl(wrapper=style == "wrapper", hookwrapper=style == "hookwrapper")( + hello + ) + plugin = type("Plugin", (), {"hello": marked})() + manager = PluginManager("posonly") + manager.add_hookspecs(Spec) + with pytest.warns(DeprecationWarning, match="Positional-only.*pluggy 2.0") as recorded: + manager.register(plugin) + assert len(recorded) == 1 + assert recorded[0].filename == plain_required.__code__.co_filename + assert recorded[0].lineno == inspect.getsourcelines(hello)[1] + if defaulted: + # Preserve the pre-existing failure rather than adding new binding semantics. + with pytest.raises(TypeError, match="positional-only"): + manager.hook.hello(value="caller") + with pytest.warns(UserWarning, match="cannot be found"): + result = manager.hook.hello() + assert result == (["fallback"] if style != "wrapper" else "fallback") + else: + result = manager.hook.hello(value="caller") + assert result == (["caller"] if style != "wrapper" else "caller") + + +@pytest.mark.parametrize( + "kind", ["method", "classmethod", "staticmethod", "decorated", "callable", "class"] +) +def test_positional_only_callable_shapes(kind): + class Spec: + @hookspec + def hello(self, value): + pass + + class Plugin: + @hookimpl + def hello(self, value, /): + return value + + class ClassPlugin: + @classmethod + @hookimpl + def hello(cls, value, /): + return value + + class StaticPlugin: + @staticmethod + @hookimpl + def hello(value, /): + return value + + class CallablePlugin: + def __call__(self, value, /): + return value + + class ExtraClass: + def __init__(self, value, /): + self.value = value + + def decorate(function: Callable[..., object]) -> Callable[..., object]: + @wraps(function) + def wrapped(*args, **kwargs): + return function(*args, **kwargs) + + return wrapped + + manager = PluginManager("posonly") + manager.add_hookspecs(Spec) + with pytest.warns(DeprecationWarning, match="Positional-only") as recorded: + if kind in ("method", "classmethod", "staticmethod"): + plugin = { + "method": Plugin, + "classmethod": ClassPlugin, + "staticmethod": StaticPlugin, + }[kind]() + manager.register(plugin) + result = manager.hook.hello(value="caller") + else: + extras: dict[str, Callable[..., object]] = { + "decorated": decorate(Plugin().hello), + "callable": CallablePlugin(), + "class": ExtraClass, + } + result = manager.hook.hello.call_extra([extras[kind]], {"value": "caller"}) + assert len(recorded) == 1 + assert "'self'" not in str(recorded[0].message) + assert "'cls'" not in str(recorded[0].message) + if kind == "class": + assert result[0].value == "caller" + else: + assert result == ["caller"] + + +def test_implicit_positional_only_receiver_does_not_warn(): + class Spec: + @hookspec + def hello(self, value, /): + pass + + class Plugin: + @hookimpl + def hello(self, /, value="fallback"): + return value + + manager = PluginManager("posonly") + with warnings.catch_warnings(record=True) as recorded: + warnings.simplefilter("always") + manager.add_hookspecs(Spec) + manager.register(Plugin()) + assert manager.hook.hello(value="caller") == ["caller"] + assert not recorded From 65a202f1c135d5ecc193dd56416ede8f099b4b87 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 15 Oct 2026 07:47:44 +0000 Subject: [PATCH 4/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- testing/test_posonly_deprecation.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/testing/test_posonly_deprecation.py b/testing/test_posonly_deprecation.py index 180ebb54..471335e8 100644 --- a/testing/test_posonly_deprecation.py +++ b/testing/test_posonly_deprecation.py @@ -59,7 +59,9 @@ def hookwrapper_defaulted(self, value="fallback", /): plugin = type("Plugin", (), {"hello": marked})() manager = PluginManager("posonly") manager.add_hookspecs(Spec) - with pytest.warns(DeprecationWarning, match="Positional-only.*pluggy 2.0") as recorded: + with pytest.warns( + DeprecationWarning, match="Positional-only.*pluggy 2.0" + ) as recorded: manager.register(plugin) assert len(recorded) == 1 assert recorded[0].filename == plain_required.__code__.co_filename From 9d304d05447e80dadeaa3a5478ecbbb7d05878c2 Mon Sep 17 00:00:00 2001 From: Xucheng Zhou Date: Thu, 15 Oct 2026 15:49:15 +0800 Subject: [PATCH 5/6] Use the configured deprecation changelog category Assisted-by: OpenAI Codex Signed-off-by: Xucheng Zhou --- changelog/{774.deprecation.rst => 774.removal.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog/{774.deprecation.rst => 774.removal.rst} (100%) diff --git a/changelog/774.deprecation.rst b/changelog/774.removal.rst similarity index 100% rename from changelog/774.deprecation.rst rename to changelog/774.removal.rst From cd600f4ae0e83a61319af996383513c4c798d222 Mon Sep 17 00:00:00 2001 From: Xucheng Zhou Date: Thu, 15 Oct 2026 15:52:11 +0800 Subject: [PATCH 6/6] Address current test lint requirements Assisted-by: OpenAI Codex Signed-off-by: Xucheng Zhou --- testing/test_posonly_deprecation.py | 33 +++++++++++++++-------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/testing/test_posonly_deprecation.py b/testing/test_posonly_deprecation.py index 471335e8..8d5099b2 100644 --- a/testing/test_posonly_deprecation.py +++ b/testing/test_posonly_deprecation.py @@ -95,13 +95,13 @@ def hello(self, value, /): class ClassPlugin: @classmethod @hookimpl - def hello(cls, value, /): + def hello(cls, value, /) -> object: return value class StaticPlugin: @staticmethod @hookimpl - def hello(value, /): + def hello(value, /) -> object: return value class CallablePlugin: @@ -121,21 +121,22 @@ def wrapped(*args, **kwargs): manager = PluginManager("posonly") manager.add_hookspecs(Spec) - with pytest.warns(DeprecationWarning, match="Positional-only") as recorded: - if kind in ("method", "classmethod", "staticmethod"): - plugin = { - "method": Plugin, - "classmethod": ClassPlugin, - "staticmethod": StaticPlugin, - }[kind]() + if kind in ("method", "classmethod", "staticmethod"): + plugin = { + "method": Plugin, + "classmethod": ClassPlugin, + "staticmethod": StaticPlugin, + }[kind]() + with pytest.warns(DeprecationWarning, match="Positional-only") as recorded: manager.register(plugin) - result = manager.hook.hello(value="caller") - else: - extras: dict[str, Callable[..., object]] = { - "decorated": decorate(Plugin().hello), - "callable": CallablePlugin(), - "class": ExtraClass, - } + result = manager.hook.hello(value="caller") + else: + extras: dict[str, Callable[..., object]] = { + "decorated": decorate(Plugin().hello), + "callable": CallablePlugin(), + "class": ExtraClass, + } + with pytest.warns(DeprecationWarning, match="Positional-only") as recorded: result = manager.hook.hello.call_extra([extras[kind]], {"value": "caller"}) assert len(recorded) == 1 assert "'self'" not in str(recorded[0].message)