diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5b9f521a..982db86d 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"] @@ -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/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/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 diff --git a/pyproject.toml b/pyproject.toml index 3adc4454..ad36c808 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,10 +52,21 @@ 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" ,] +"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] @@ -122,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 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..eaa006cc 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 @@ -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, @@ -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 (), () @@ -366,20 +368,17 @@ def varnames( _tail = qualname.rsplit(".", maxsplit=1)[-1] _is_class_method = "." in _tail if args: - if is_bound: + if is_bound or (_is_class_method and args[0] in _IMPLICIT_NAMES): args = args[1:] - elif _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 @@ -412,11 +411,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__( @@ -516,7 +515,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 " @@ -638,7 +637,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] @@ -669,17 +668,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 +724,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/_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) 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/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 (), () diff --git a/testing/test_details.py b/testing/test_details.py index 237b7de1..fa4c7076 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: @@ -144,7 +143,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 @@ -175,10 +174,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: @@ -223,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) 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 7924068a..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) @@ -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] @@ -435,15 +435,14 @@ def he_method1(self, arg): class Plugin1: @hookimpl def he_method1(self, arg): - 0 / 0 + raise ZeroDivisionError pm.register(Plugin1()) 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_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 b84cdca6..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) @@ -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" 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