From 11ce4b9008769cdb17454278cab68af900d1a06e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 21:33:17 +0000 Subject: [PATCH 01/14] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.15.22 → v0.16.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.15.22...v0.16.2) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5b9f521a..8098b46b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.15.22" + rev: "v0.16.2" hooks: - id: ruff-check args: ["--fix"] From 1a2a8f4eb1df1be118a41caa2fb34b2b4cbe3286 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 21:33:54 +0000 Subject: [PATCH 02/14] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/pluggy/__init__.py | 18 ++++++++--------- src/pluggy/_callers.py | 2 +- src/pluggy/_hooks.py | 38 +++++++++++++++++------------------ src/pluggy/_result.py | 2 +- testing/test_pluginmanager.py | 5 ++--- testing/test_warnings.py | 14 +++++++------ 6 files changed, 39 insertions(+), 40 deletions(-) diff --git a/src/pluggy/__init__.py b/src/pluggy/__init__.py index 3d81d0a3..32c7eae5 100644 --- a/src/pluggy/__init__.py +++ b/src/pluggy/__init__.py @@ -1,18 +1,18 @@ __all__ = [ - "__version__", - "PluginManager", - "PluginValidationError", - "HookCaller", "HookCallError", - "HookspecOpts", - "HookimplOpts", + "HookCaller", "HookImpl", "HookRelay", - "HookspecMarker", "HookimplMarker", - "Result", - "PluggyWarning", + "HookimplOpts", + "HookspecMarker", + "HookspecOpts", "PluggyTeardownRaisedWarning", + "PluggyWarning", + "PluginManager", + "PluginValidationError", + "Result", + "__version__", ] from ._hooks import HookCaller from ._hooks import HookImpl diff --git a/src/pluggy/_callers.py b/src/pluggy/_callers.py index 450db1a7..8b4b1477 100644 --- a/src/pluggy/_callers.py +++ b/src/pluggy/_callers.py @@ -74,7 +74,7 @@ def _warn_teardown_exception( f"A plugin raised an exception during an old-style hookwrapper teardown.\n" f"Plugin: {hook_impl.plugin_name}, Hook: {hook_name}\n" f"{type(e).__name__}: {e}\n" - f"For more information see https://pluggy.readthedocs.io/en/stable/api_reference.html#pluggy.PluggyTeardownRaisedWarning" # noqa: E501 + f"For more information see https://pluggy.readthedocs.io/en/stable/api_reference.html#pluggy.PluggyTeardownRaisedWarning" ) warnings.warn(PluggyTeardownRaisedWarning(msg), stacklevel=6) diff --git a/src/pluggy/_hooks.py b/src/pluggy/_hooks.py index f079d3b7..700fe67d 100644 --- a/src/pluggy/_hooks.py +++ b/src/pluggy/_hooks.py @@ -99,8 +99,8 @@ def __call__( warn_on_impl_args: Mapping[str, Warning] | None = None, ) -> _F: ... - @overload # noqa: F811 - def __call__( # noqa: F811 + @overload + def __call__( self, function: None = ..., firstresult: bool = ..., @@ -109,7 +109,7 @@ def __call__( # noqa: F811 warn_on_impl_args: Mapping[str, Warning] | None = ..., ) -> Callable[[_F], _F]: ... - def __call__( # noqa: F811 + def __call__( self, function: _F | None = None, firstresult: bool = False, @@ -188,8 +188,8 @@ def __call__( wrapper: bool = ..., ) -> _F: ... - @overload # noqa: F811 - def __call__( # noqa: F811 + @overload + def __call__( self, function: None = ..., hookwrapper: bool = ..., @@ -200,7 +200,7 @@ def __call__( # noqa: F811 wrapper: bool = ..., ) -> Callable[[_F], _F]: ... - def __call__( # noqa: F811 + def __call__( self, function: _F | None = None, hookwrapper: bool = False, @@ -366,9 +366,7 @@ def varnames( _tail = qualname.rsplit(".", maxsplit=1)[-1] _is_class_method = "." in _tail if args: - if is_bound: - args = args[1:] - elif _is_class_method and args[0] in _IMPLICIT_NAMES: + if is_bound or _is_class_method and args[0] in _IMPLICIT_NAMES: args = args[1:] elif _is_class_method and legacy_noself: if _tail not in _NOSELF_WARN_SUPPRESS: @@ -412,11 +410,11 @@ class HookCaller: """A caller of all registered implementations of a hook specification.""" __slots__ = ( - "name", - "spec", + "_call_history", "_hookexec", "_hookimpls", - "_call_history", + "name", + "spec", ) def __init__( @@ -669,17 +667,17 @@ class HookImpl: """A hook implementation in a :class:`HookCaller`.""" __slots__ = ( - "function", "argnames", + "function", + "hookwrapper", "kwargnames", - "plugin", + "optionalhook", "opts", + "plugin", "plugin_name", - "wrapper", - "hookwrapper", - "optionalhook", "tryfirst", "trylast", + "wrapper", ) def __init__( @@ -725,11 +723,11 @@ def __repr__(self) -> str: @final class HookSpec: __slots__ = ( - "namespace", - "function", - "name", "argnames", + "function", "kwargnames", + "name", + "namespace", "opts", "warn_on_impl", "warn_on_impl_args", diff --git a/src/pluggy/_result.py b/src/pluggy/_result.py index f8020b51..d9d5dbe8 100644 --- a/src/pluggy/_result.py +++ b/src/pluggy/_result.py @@ -26,7 +26,7 @@ class Result(Generic[ResultType]): """An object used to inspect and set the result in a :ref:`hook wrapper `.""" - __slots__ = ("_result", "_exception", "_traceback") + __slots__ = ("_exception", "_result", "_traceback") def __init__( self, diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index 7924068a..4c5f95b9 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -441,9 +441,8 @@ def he_method1(self, arg): with pytest.raises(ZeroDivisionError): pm.hook.he_method1(arg="works") - with pytest.raises(HookCallError): - with pytest.warns(UserWarning): - pm.hook.he_method1() + with pytest.raises(HookCallError), pytest.warns(UserWarning): + pm.hook.he_method1() def test_subset_hook_caller(pm: PluginManager) -> None: diff --git a/testing/test_warnings.py b/testing/test_warnings.py index b84cdca6..0bee1549 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -40,12 +40,14 @@ def my_hook(self): pm.register(Plugin1(), "plugin1") pm.register(Plugin2(), "plugin2") pm.register(Plugin3(), "plugin3") - with pytest.warns( - PluggyTeardownRaisedWarning, - match=r"\bplugin2\b.*\bmy_hook\b.*\n.*ZeroDivisionError", - ) as wc: - with pytest.raises(ZeroDivisionError): - pm.hook.my_hook() + with ( + pytest.warns( + PluggyTeardownRaisedWarning, + match=r"\bplugin2\b.*\bmy_hook\b.*\n.*ZeroDivisionError", + ) as wc, + pytest.raises(ZeroDivisionError), + ): + pm.hook.my_hook() assert len(wc.list) == 1 assert Path(wc.list[0].filename).name == "test_warnings.py" From 107f8750bf3ba55885995992f7781fd1c3db5a92 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Wed, 12 Aug 2026 07:19:34 +0200 Subject: [PATCH 03/14] test: use dict literals instead of dict() calls Fixes the flake8-comprehensions C408 violations that ruff 0.16 reports now that it enables the rule by default. Co-Authored-By: Claude Opus 5 (1M context) --- testing/test_details.py | 4 ++-- testing/test_multicall.py | 4 ++-- testing/test_pluginmanager.py | 12 ++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/testing/test_details.py b/testing/test_details.py index 237b7de1..dd3dad0f 100644 --- a/testing/test_details.py +++ b/testing/test_details.py @@ -175,10 +175,10 @@ def herstory(self, arg1, arg2): pm.hook.hello(arg2=2) with pytest.warns(UserWarning, match=r"'arg1', 'arg2'.*cannot be found.*$"): - pm.hook.hello.call_extra([], kwargs=dict()) + pm.hook.hello.call_extra([], kwargs={}) with pytest.warns(UserWarning, match=r"'arg1', 'arg2'.*cannot be found.*$"): - pm.hook.herstory.call_historic(kwargs=dict()) + pm.hook.herstory.call_historic(kwargs={}) def test_repr() -> None: diff --git a/testing/test_multicall.py b/testing/test_multicall.py index 93f394c8..e400e85a 100644 --- a/testing/test_multicall.py +++ b/testing/test_multicall.py @@ -38,7 +38,7 @@ class A: def f(self, x, y): return x + y - reslist = MC([f, A().f], dict(x=23, y=24)) + reslist = MC([f, A().f], {"x": 23, "y": 24}) assert reslist == [24 + 23, 24] @@ -47,7 +47,7 @@ def test_keyword_args_with_defaultargs() -> None: def f(x, z=1): return x + z - reslist = MC([f], dict(x=23, y=24)) + reslist = MC([f], {"x": 23, "y": 24}) assert reslist == [24] diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index 4c5f95b9..7be6e974 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -242,7 +242,7 @@ def he_method1(self, arg): ... pm.add_hookspecs(Hooks) - pm.hook.he_method1.call_historic(kwargs=dict(arg=1)) + pm.hook.he_method1.call_historic(kwargs={"arg": 1}) out = [] class Plugin: @@ -260,7 +260,7 @@ def he_method1(self, arg): pm.register(Plugin2()) assert out == [1, 10] - pm.hook.he_method1.call_historic(kwargs=dict(arg=12)) + pm.hook.he_method1.call_historic(kwargs={"arg": 12}) assert out == [1, 10, 120, 12] @@ -287,7 +287,7 @@ def he_method1(self, arg): out.append(arg * 10) shc = pm.subset_hook_caller("he_method1", remove_plugins=[plugin]) - shc.call_historic(kwargs=dict(arg=1)) + shc.call_historic(kwargs={"arg": 1}) pm.register(Plugin2()) assert out == [10] @@ -325,7 +325,7 @@ def he_method1(self, arg): pm.register(Plugin1()) he_method1 = pm.hook.he_method1 - he_method1.call_historic(result_callback=callback, kwargs=dict(arg=1)) + he_method1.call_historic(result_callback=callback, kwargs={"arg": 1}) class Plugin2: @hookimpl @@ -367,7 +367,7 @@ def he_method1(self, arg): pm.register(Plugin2()) he_method1 = pm.hook.he_method1 - he_method1.call_historic(lambda res: out.append(res), dict(arg=1)) + he_method1.call_historic(lambda res: out.append(res), {"arg": 1}) assert out == [20, 10] pm.register(Plugin3()) assert out == [20, 10, 30] @@ -420,7 +420,7 @@ def he_method1(self, arg): def he_method1(arg): return arg * 10 - out = pm.hook.he_method1.call_extra([he_method1], dict(arg=1)) + out = pm.hook.he_method1.call_extra([he_method1], {"arg": 1}) assert out == [10] From 237edb6e8e3067c46f91ae620a652e2fb20bf68e Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Wed, 12 Aug 2026 07:20:16 +0200 Subject: [PATCH 04/14] test: raise explicitly instead of leaving a bare expression ``0 / 0`` as a statement is a deliberate "blow up here", and ``module.x.broken`` a deliberate attribute access, but both read as dead code, which is what flake8-bugbear B018 (enabled by default as of ruff 0.16) reports. Say what is meant instead: raise the error, and assign the attribute access that is only there for its side effect. Co-Authored-By: Claude Opus 5 (1M context) --- testing/test_details.py | 2 +- testing/test_pluginmanager.py | 4 ++-- testing/test_result.py | 2 +- testing/test_warnings.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/testing/test_details.py b/testing/test_details.py index dd3dad0f..430d192d 100644 --- a/testing/test_details.py +++ b/testing/test_details.py @@ -144,7 +144,7 @@ class Module: module = Module() module.x = DontTouchMe() with pytest.raises(Exception, match="touch me"): - module.x.broken + _ = module.x.broken pm = PluginManager(hookspec.project_name) # register() would raise an error diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index 7be6e974..dd395950 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -69,7 +69,7 @@ def __getattr__(self, name): raise AttributeError(name) a = A() - a.test + _ = a.test he_pm.register(a) assert not he_pm.get_hookcallers(a) @@ -435,7 +435,7 @@ def he_method1(self, arg): class Plugin1: @hookimpl def he_method1(self, arg): - 0 / 0 + raise ZeroDivisionError pm.register(Plugin1()) with pytest.raises(ZeroDivisionError): diff --git a/testing/test_result.py b/testing/test_result.py index c4a33920..0568d255 100644 --- a/testing/test_result.py +++ b/testing/test_result.py @@ -5,7 +5,7 @@ def test_exceptions_traceback_doesnt_get_longer_and_longer() -> None: def bad() -> None: - 1 / 0 + raise ZeroDivisionError result = Result.from_call(bad) diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 0bee1549..72fd8817 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -30,7 +30,7 @@ class Plugin2: @hookimpl(hookwrapper=True) def my_hook(self): yield - 1 / 0 + raise ZeroDivisionError class Plugin3: @hookimpl(hookwrapper=True) From 54127a334d52a49d02c77b001ee998d36f7d6037 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Wed, 12 Aug 2026 07:21:02 +0200 Subject: [PATCH 05/14] refactor: simplify conditionals flagged by flake8-simplify ruff 0.16 enables flake8-simplify by default: * SIM102 in ``varnames()`` and in a test plugin manager: flatten a nested ``if`` into the branch condition. * SIM103 in the Sphinx warning filter: return the negated condition instead of branching to two literals. * SIM118 in ``HookCaller._verify_all_args_are_provided()``: membership on the mapping itself, no ``.keys()`` needed. Also parenthesizes the ``or``/``and`` condition ruff collapsed in the previous commit, which relied on operator precedence to be read right. Co-Authored-By: Claude Opus 5 (1M context) --- docs/conf.py | 6 ++---- src/pluggy/_hooks.py | 23 +++++++++++------------ testing/test_details.py | 5 ++--- 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 3f621bd6..f72d7416 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -114,12 +114,10 @@ def filter(self, record: logging.LogRecord) -> bool: """Ignore warnings about missing include with "only" directive. Ref: https://github.com/sphinx-doc/sphinx/issues/2150.""" - if ( + return not ( record.msg.startswith('Problems with "include" directive path:') and "_changelog_towncrier_draft.rst" in record.msg - ): - return False - return True + ) logger = logging.getLogger(sphinx.util.logging.NAMESPACE) warn_handler = [x for x in logger.handlers if x.level == logging.WARNING] diff --git a/src/pluggy/_hooks.py b/src/pluggy/_hooks.py index 700fe67d..3dcc368d 100644 --- a/src/pluggy/_hooks.py +++ b/src/pluggy/_hooks.py @@ -366,18 +366,17 @@ def varnames( _tail = qualname.rsplit(".", maxsplit=1)[-1] _is_class_method = "." in _tail if args: - if is_bound or _is_class_method and args[0] in _IMPLICIT_NAMES: + if is_bound or (_is_class_method and args[0] in _IMPLICIT_NAMES): args = args[1:] - elif _is_class_method and legacy_noself: - if _tail not in _NOSELF_WARN_SUPPRESS: - warnings.warn( - f"{qualname} is a method but its first parameter" - f" {args[0]!r} is not 'self'." - f" Add 'self' as the first parameter or use @staticmethod." - f" This will become an error in a future version of pluggy.", - DeprecationWarning, - stacklevel=2, - ) + elif _is_class_method and legacy_noself and _tail not in _NOSELF_WARN_SUPPRESS: + warnings.warn( + f"{qualname} is a method but its first parameter" + f" {args[0]!r} is not 'self'." + f" Add 'self' as the first parameter or use @staticmethod." + f" This will become an error in a future version of pluggy.", + DeprecationWarning, + stacklevel=2, + ) return args, kwargs @@ -514,7 +513,7 @@ def _verify_all_args_are_provided(self, kwargs: Mapping[str, object]) -> None: for argname in self.spec.argnames # Avoid self.spec.argnames - kwargs.keys() # it doesn't preserve order. - if argname not in kwargs.keys() + if argname not in kwargs ) warnings.warn( f"Argument(s) {notincall} which are declared in the hookspec " diff --git a/testing/test_details.py b/testing/test_details.py index 430d192d..e5b5077a 100644 --- a/testing/test_details.py +++ b/testing/test_details.py @@ -16,9 +16,8 @@ def test_parse_hookimpl_override() -> None: class MyPluginManager(PluginManager): def parse_hookimpl_opts(self, module_or_class, name): opts = PluginManager.parse_hookimpl_opts(self, module_or_class, name) - if opts is None: - if name.startswith("x1"): - opts = {} # type: ignore[assignment] + if opts is None and name.startswith("x1"): + opts = {} # type: ignore[assignment] return opts class Plugin: From c7d1880c0a37358e3c60c514226ef0b27f7e47b0 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Wed, 12 Aug 2026 07:21:20 +0200 Subject: [PATCH 06/14] refactor: import Set as AbstractSet ``from collections.abc import Set`` shadows the reading of the ``set`` builtin at a glance, which is what flake8-pyi PYI025 (default as of ruff 0.16) points out. Alias it to ``AbstractSet``. Co-Authored-By: Claude Opus 5 (1M context) --- src/pluggy/_hooks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pluggy/_hooks.py b/src/pluggy/_hooks.py index 3dcc368d..6cd62cd6 100644 --- a/src/pluggy/_hooks.py +++ b/src/pluggy/_hooks.py @@ -8,7 +8,7 @@ from collections.abc import Generator from collections.abc import Mapping from collections.abc import Sequence -from collections.abc import Set +from collections.abc import Set as AbstractSet import inspect import sys import types @@ -635,7 +635,7 @@ class _SubsetHookCaller(HookCaller): "_remove_plugins", ) - def __init__(self, orig: HookCaller, remove_plugins: Set[_Plugin]) -> None: + def __init__(self, orig: HookCaller, remove_plugins: AbstractSet[_Plugin]) -> None: self._orig = orig self._remove_plugins = remove_plugins self.name = orig.name # type: ignore[misc] From 85db9956e0fc5693d8c40847901f0022ca279852 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Wed, 12 Aug 2026 07:21:47 +0200 Subject: [PATCH 07/14] refactor: stop shadowing the register() name argument The hookimpl loop in ``PluginManager.register()`` reused ``name``, the method's own argument, for two different things: the attribute name being inspected on the plugin, and then the hook name it maps to. Give each its own name, as PLR1704 (default as of ruff 0.16) asks. Co-Authored-By: Claude Opus 5 (1M context) --- src/pluggy/_manager.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pluggy/_manager.py b/src/pluggy/_manager.py index 426e0a3b..325388a8 100644 --- a/src/pluggy/_manager.py +++ b/src/pluggy/_manager.py @@ -141,17 +141,17 @@ def register(self, plugin: _Plugin, name: str | None = None) -> str | None: self._name2plugin[plugin_name] = plugin # register matching hook implementations of the plugin - for name in dir(plugin): - hookimpl_opts = self.parse_hookimpl_opts(plugin, name) + for attr_name in dir(plugin): + hookimpl_opts = self.parse_hookimpl_opts(plugin, attr_name) if hookimpl_opts is not None: normalize_hookimpl_opts(hookimpl_opts) - method: _HookImplFunction[object] = getattr(plugin, name) + method: _HookImplFunction[object] = getattr(plugin, attr_name) hookimpl = HookImpl(plugin, plugin_name, method, hookimpl_opts) - name = hookimpl_opts.get("specname") or name - hook: HookCaller | None = getattr(self.hook, name, None) + hook_name = hookimpl_opts.get("specname") or attr_name + hook: HookCaller | None = getattr(self.hook, hook_name, None) if hook is None: - hook = HookCaller(name, self._hookexec) - setattr(self.hook, name, hook) + hook = HookCaller(hook_name, self._hookexec) + setattr(self.hook, hook_name, hook) elif hook.has_spec(): self._verify_hook(hook, hookimpl) hook._maybe_apply_history(hookimpl) From 55893b6e262e27021e26b6e9c7d29b3650034b89 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Wed, 12 Aug 2026 07:22:00 +0200 Subject: [PATCH 08/14] refactor: extend the argument list instead of appending in a loop PERF402, enabled by default as of ruff 0.16. Co-Authored-By: Claude Opus 5 (1M context) --- downstream/run_downstream.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/downstream/run_downstream.py b/downstream/run_downstream.py index da3b3931..03b43e96 100644 --- a/downstream/run_downstream.py +++ b/downstream/run_downstream.py @@ -201,8 +201,7 @@ def build_uv_install_argv(*, venv_home: Path, env: EnvironmentUv) -> list[str]: args.extend(["--group", g]) for spec in env.editables: args.extend(["-e", spec]) - for pkg in env.packages: - args.append(pkg) + args.extend(env.packages) return args From e7454bace8e3d4ea81e6841ff18e6572aedaa9f6 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Wed, 12 Aug 2026 07:22:27 +0200 Subject: [PATCH 09/14] lint: silence B004 where the __call__ attribute is the point ``varnames()`` and its benchmark counterpart fetch ``__call__`` to inspect the signature of a callable object. B004 (default as of ruff 0.16) reads that as an unreliable callable test and suggests ``callable()``, which returns a bool and would break both. Mark it, and say why in the code. Co-Authored-By: Claude Opus 5 (1M context) --- src/pluggy/_hooks.py | 4 +++- testing/benchmark.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/pluggy/_hooks.py b/src/pluggy/_hooks.py index 6cd62cd6..eaa006cc 100644 --- a/src/pluggy/_hooks.py +++ b/src/pluggy/_hooks.py @@ -328,7 +328,9 @@ def varnames( is_bound = True elif not inspect.isroutine(func): # callable object? try: - func = getattr(func, "__call__", func) + # Not a `callable()` check: the `__call__` attribute itself is + # wanted, so that its signature can be inspected below. + func = getattr(func, "__call__", func) # noqa: B004 except Exception: # pragma: no cover - pypy special case return (), () diff --git a/testing/benchmark.py b/testing/benchmark.py index 81823edd..0ca52ad2 100644 --- a/testing/benchmark.py +++ b/testing/benchmark.py @@ -26,7 +26,9 @@ def _varnames_legacy(func: object) -> tuple[tuple[str, ...], tuple[str, ...]]: return (), () elif not inspect.isroutine(func): try: - func = getattr(func, "__call__", func) + # Not a `callable()` check: the `__call__` attribute itself is + # wanted, so that its signature can be inspected below. + func = getattr(func, "__call__", func) # noqa: B004 except Exception: return (), () From ce8bdb922971a658b32247df9f03d40e56f60416 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Wed, 12 Aug 2026 07:22:43 +0200 Subject: [PATCH 10/14] lint: silence PLR0124 on the deliberate self-equality assertion ``test_dist_facade_identity_equality_and_hash`` asserts that a facade equals itself, which is exactly the identity semantics under test. PLR0124 (default as of ruff 0.16) flags the self-comparison. Co-Authored-By: Claude Opus 5 (1M context) --- testing/test_details.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/testing/test_details.py b/testing/test_details.py index e5b5077a..fa4c7076 100644 --- a/testing/test_details.py +++ b/testing/test_details.py @@ -222,7 +222,8 @@ def test_dist_facade_identity_equality_and_hash() -> None: dist = distribution("pluggy") fc1 = DistFacade(dist) fc2 = DistFacade(dist) - assert fc1 == fc1 + # Comparing fc1 with itself is the point: DistFacade equality is identity. + assert fc1 == fc1 # noqa: PLR0124 assert fc1 is not fc2 assert fc1 != fc2 assert hash(fc1) == hash(fc1) From 041e9caf4ad886ecad9d087265b7c9a8000fa1a4 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Wed, 12 Aug 2026 07:23:04 +0200 Subject: [PATCH 11/14] lint: ignore BLE001, catching anything is pluggy's job The multicall engine and ``Result.from_call()`` must capture whatever a hook implementation raised -- BaseException included -- and hand it to the hook wrappers; narrowing those handlers would change pluggy's contract. ``varnames()`` and ``parse_hookimpl_opts()`` likewise read attributes off arbitrary plugin objects, which may raise anything (see ``test_plugin_getattr_raises_errors``). flake8-blind-except is enabled by default as of ruff 0.16 and flags all thirteen sites. Co-Authored-By: Claude Opus 5 (1M context) --- pyproject.toml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3adc4454..52fc0cf0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,13 @@ extend-select = [ "F","E", "W", "UP", "ANN", ] -extend-ignore = ["ANN401"] +extend-ignore = [ + "ANN401", + # Catching whatever a plugin raised, BaseException included, and handing it + # to the hook wrappers is what pluggy is for. The same goes for reading + # attributes off arbitrary plugin objects, which may raise anything at all. + "BLE001", +] [tool.ruff.lint.extend-per-file-ignores] "testing/*.py" = ["ANN001", "ANN002", "ANN003", "ANN201", "ANN202","ANN204" ,] From 83c8fb10d3014092024532b971d3ec9ffc5c1eb7 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Wed, 12 Aug 2026 07:23:41 +0200 Subject: [PATCH 12/14] lint: ignore TRY002 in the test suite ``test_wrapper_exception_chaining`` and ``test_plugin_getattr_raises_errors`` raise plain ``Exception`` deliberately: the point is that pluggy carries and chains exception types it knows nothing about. tryceratops is enabled by default as of ruff 0.16. Co-Authored-By: Claude Opus 5 (1M context) --- pyproject.toml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 52fc0cf0..755e3591 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,12 @@ extend-ignore = [ ] [tool.ruff.lint.extend-per-file-ignores] -"testing/*.py" = ["ANN001", "ANN002", "ANN003", "ANN201", "ANN202","ANN204" ,] +"testing/*.py" = [ + "ANN001", "ANN002", "ANN003", "ANN201", "ANN202","ANN204", + # Tests raise plain Exception on purpose, to check that pluggy propagates + # and chains exceptions it knows nothing about. + "TRY002", +] "docs/*.py" = ["ANN001", "ANN002", "ANN003", "ANN201", "ANN202","ANN204" ,] [tool.ruff.lint.isort] From 9564b0a1699e46adeea015b62d4fc5e2b721e701 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Wed, 12 Aug 2026 07:40:05 +0200 Subject: [PATCH 13/14] lint: drop flake8, ruff covers it ruff already selects E, W and F, and ruff format owns line length, so flake8 only duplicated it -- and the two now disagree: ruff 0.16 enables RUF100, which removed the ``# noqa: E501`` on the documentation URL in ``_warn_teardown_exception()`` because ruff's own E501 does not flag a line that has nothing left to wrap, while flake8's E501 still does. The URL is 93 characters, so no indentation fits it under 99 either. The only hook-specific check lost is flake8-typing-imports, which warns about typing features newer than the minimum Python. pluggy requires 3.10, ruff infers the same target version from ``requires-python``, and mypy runs on every commit, so it had nothing left to catch. This also unpins pre-commit-hooks, which dropped its flake8 hook in v3.0.0 and could not be autoupdated past v2.1.0 while it was in use. Co-Authored-By: Claude Opus 5 (1M context) --- .pre-commit-config.yaml | 2 -- CLAUDE.md | 2 +- tox.ini | 4 ---- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8098b46b..982db86d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,8 +18,6 @@ repos: hooks: - id: trailing-whitespace - id: end-of-file-fixer - - id: flake8 - additional_dependencies: [flake8-typing-imports] - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.10.0 hooks: diff --git a/CLAUDE.md b/CLAUDE.md index 2b43aae5..1c18b81c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -48,4 +48,4 @@ All commands use `uv run` for consistent environments. ## Configuration Files - `pyproject.toml` - Project metadata, build system, tool configuration (ruff, mypy, setuptools-scm) - `tox.ini` - Multi-environment testing configuration -- `.pre-commit-config.yaml` - Code quality automation (ruff, mypy, flake8, etc.) +- `.pre-commit-config.yaml` - Code quality automation (ruff, mypy, etc.) diff --git a/tox.ini b/tox.ini index a09b09cc..40e9b0e2 100644 --- a/tox.ini +++ b/tox.ini @@ -39,10 +39,6 @@ addopts=-r a filterwarnings = error -[flake8] -max-line-length=99 -min-python-version = 3.10 - [testenv:release] description = do a release, required posarg of the version number basepython = python3 From faadc7192a7cee656ef0350a781f4d398537d579 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Wed, 12 Aug 2026 08:12:07 +0200 Subject: [PATCH 14/14] lint: pin mypy to the oldest supported Python The previous commit claimed mypy already covered what flake8-typing-imports checked. That was only half true: ``[tool.mypy]`` set no ``python_version``, so mypy checked against whichever interpreter the hook happened to run on -- 3.14 in the pre-commit environment -- and a ``typing`` member added after 3.10 passed unnoticed. Pin it to 3.10, matching ``requires-python``. mypy then reads typeshed's ``sys.version_info`` guards and reports, for instance, ``Module "typing" has no attribute "Self"``, which is what TYP001 used to catch. It covers more than the plugin did, too: every guarded stdlib attribute rather than only ``typing`` imports, and uses as well as import lines. Co-Authored-By: Claude Opus 5 (1M context) --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 755e3591..ad36c808 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -133,6 +133,9 @@ disallow_untyped_decorators = true ignore_missing_imports = true implicit_reexport = false no_implicit_optional = true +# Without this mypy checks against whatever interpreter it happens to run on, +# and typing features newer than the oldest supported Python slip through. +python_version = "3.10" show_error_codes = true strict_equality = true strict_optional = true