Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -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"]
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.)
6 changes: 2 additions & 4 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
3 changes: 1 addition & 2 deletions downstream/run_downstream.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
18 changes: 16 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand Down
18 changes: 9 additions & 9 deletions src/pluggy/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/pluggy/_callers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
67 changes: 33 additions & 34 deletions src/pluggy/_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = ...,
Expand All @@ -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,
Expand Down Expand Up @@ -188,8 +188,8 @@ def __call__(
wrapper: bool = ...,
) -> _F: ...

@overload # noqa: F811
def __call__( # noqa: F811
@overload
def __call__(
self,
function: None = ...,
hookwrapper: bool = ...,
Expand All @@ -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,
Expand Down Expand Up @@ -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 (), ()

Expand Down Expand Up @@ -366,20 +368,17 @@ def varnames(
_tail = qualname.rsplit("<locals>.", 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

Expand Down Expand Up @@ -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__(
Expand Down Expand Up @@ -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 "
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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__(
Expand Down Expand Up @@ -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",
Expand Down
14 changes: 7 additions & 7 deletions src/pluggy/_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/pluggy/_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class Result(Generic[ResultType]):
"""An object used to inspect and set the result in a :ref:`hook wrapper
<hookwrappers>`."""

__slots__ = ("_result", "_exception", "_traceback")
__slots__ = ("_exception", "_result", "_traceback")

def __init__(
self,
Expand Down
4 changes: 3 additions & 1 deletion testing/benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (), ()

Expand Down
14 changes: 7 additions & 7 deletions testing/test_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions testing/test_multicall.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]


Expand All @@ -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]


Expand Down
Loading