From 136bbe8c98b66cbcefdc2438bf3c59705678249b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 5 Sep 2023 08:22:41 +0000 Subject: [PATCH 01/23] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/autoflake: v2.2.0 → v2.2.1](https://github.com/PyCQA/autoflake/compare/v2.2.0...v2.2.1) --- .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 f75ac6ba..0994390a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/PyCQA/autoflake - rev: v2.2.0 + rev: v2.2.1 hooks: - id: autoflake name: autoflake From e9490ff64d0c838c1ec3e63e197be2dbf1614236 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 12 Sep 2023 09:32:06 +0000 Subject: [PATCH 02/23] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 23.7.0 → 23.9.1](https://github.com/psf/black/compare/23.7.0...23.9.1) --- .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 0994390a..890d8491 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black - rev: 23.7.0 + rev: 23.9.1 hooks: - id: black args: [--safe, --quiet] From 8dadaf63b881e506af5e04ca439a90e625221dd0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 19 Sep 2023 08:39:40 +0000 Subject: [PATCH 03/23] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/reorder-python-imports: v3.10.0 → v3.11.0](https://github.com/asottile/reorder-python-imports/compare/v3.10.0...v3.11.0) - [github.com/asottile/pyupgrade: v3.10.1 → v3.11.0](https://github.com/asottile/pyupgrade/compare/v3.10.1...v3.11.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 890d8491..ba9844e0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ repos: language: python files: \.py$ - repo: https://github.com/asottile/reorder-python-imports - rev: v3.10.0 + rev: v3.11.0 hooks: - id: reorder-python-imports args: ['--application-directories=.:src', --py38-plus] @@ -24,7 +24,7 @@ repos: hooks: - id: rst-backticks - repo: https://github.com/asottile/pyupgrade - rev: v3.10.1 + rev: v3.11.0 hooks: - id: pyupgrade args: [--py38-plus] From 681cd107636504587eb23775a79be1724b5fb9a9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 26 Sep 2023 08:57:24 +0000 Subject: [PATCH 04/23] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.11.0 → v3.13.0](https://github.com/asottile/pyupgrade/compare/v3.11.0...v3.13.0) --- .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 ba9844e0..013ea126 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: rst-backticks - repo: https://github.com/asottile/pyupgrade - rev: v3.11.0 + rev: v3.13.0 hooks: - id: pyupgrade args: [--py38-plus] From e8bc94c772040e138f6acdf0fdfffb22fa8eba46 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 3 Oct 2023 10:48:14 +0000 Subject: [PATCH 05/23] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/reorder-python-imports: v3.11.0 → v3.12.0](https://github.com/asottile/reorder-python-imports/compare/v3.11.0...v3.12.0) - [github.com/asottile/pyupgrade: v3.13.0 → v3.14.0](https://github.com/asottile/pyupgrade/compare/v3.13.0...v3.14.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 013ea126..b594627d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ repos: language: python files: \.py$ - repo: https://github.com/asottile/reorder-python-imports - rev: v3.11.0 + rev: v3.12.0 hooks: - id: reorder-python-imports args: ['--application-directories=.:src', --py38-plus] @@ -24,7 +24,7 @@ repos: hooks: - id: rst-backticks - repo: https://github.com/asottile/pyupgrade - rev: v3.13.0 + rev: v3.14.0 hooks: - id: pyupgrade args: [--py38-plus] From 718c27e757723c371ebf8b2dbc1a1b5374887d91 Mon Sep 17 00:00:00 2001 From: Bill Huneke Date: Thu, 28 Sep 2023 19:19:19 -0400 Subject: [PATCH 06/23] Fix a few more git branch renames (master -> main) --- docs/conf.py | 2 +- docs/index.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index ece874a2..aaa6c966 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -45,7 +45,7 @@ "github_button": "true", "github_banner": "true", "github_type": "star", - "badge_branch": "master", + "badge_branch": "main", "page_width": "1080px", "sidebar_width": "300px", "fixed_sidebar": "false", diff --git a/docs/index.rst b/docs/index.rst index d527a507..5bc90b79 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1028,11 +1028,11 @@ Table of contents .. _callbacks: https://en.wikipedia.org/wiki/Callback_(computer_programming) .. _tox test suite: - https://github.com/pytest-dev/pluggy/blob/master/tox.ini + https://github.com/pytest-dev/pluggy/blob/main/tox.ini .. _Semantic Versioning: https://semver.org/ .. _Python interpreters: - https://github.com/pytest-dev/pluggy/blob/master/tox.ini#L2 + https://github.com/pytest-dev/pluggy/blob/main/tox.ini#L2 .. _500+ plugins: https://docs.pytest.org/en/latest/reference/plugin_list.html .. _pre-commit: From 1c906c93492f9632b23f7ad6f5bb7ef9cceb44cb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 9 Oct 2023 20:59:11 +0000 Subject: [PATCH 07/23] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.14.0 → v3.15.0](https://github.com/asottile/pyupgrade/compare/v3.14.0...v3.15.0) --- .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 b594627d..9f5b533d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: rst-backticks - repo: https://github.com/asottile/pyupgrade - rev: v3.14.0 + rev: v3.15.0 hooks: - id: pyupgrade args: [--py38-plus] From c73880f31409af3fe0d0c38368b5a5baaebdb1b8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 20:24:09 +0000 Subject: [PATCH 08/23] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.5.1 → v1.6.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.5.1...v1.6.0) --- .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 9f5b533d..e0d96135 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -47,7 +47,7 @@ repos: language: python additional_dependencies: [pygments, restructuredtext_lint] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.5.1 + rev: v1.6.0 hooks: - id: mypy files: ^(src/|testing/) From 46fec3f17869dafa21accab7835bdd36871ecd04 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 20:52:30 +0000 Subject: [PATCH 09/23] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 23.9.1 → 23.10.1](https://github.com/psf/black/compare/23.9.1...23.10.1) - [github.com/pre-commit/mirrors-mypy: v1.6.0 → v1.6.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.6.0...v1.6.1) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e0d96135..71235f9b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black - rev: 23.9.1 + rev: 23.10.1 hooks: - id: black args: [--safe, --quiet] @@ -47,7 +47,7 @@ repos: language: python additional_dependencies: [pygments, restructuredtext_lint] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.6.0 + rev: v1.6.1 hooks: - id: mypy files: ^(src/|testing/) From 3396f7bf88ed2a7dcbfc44abf1647b135cee1282 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 21:00:46 +0000 Subject: [PATCH 10/23] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 23.10.1 → 23.11.0](https://github.com/psf/black/compare/23.10.1...23.11.0) - [github.com/pre-commit/mirrors-mypy: v1.6.1 → v1.7.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.6.1...v1.7.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 71235f9b..3d7e429f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black - rev: 23.10.1 + rev: 23.11.0 hooks: - id: black args: [--safe, --quiet] @@ -47,7 +47,7 @@ repos: language: python additional_dependencies: [pygments, restructuredtext_lint] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.6.1 + rev: v1.7.0 hooks: - id: mypy files: ^(src/|testing/) From 94ec1b5125ebb9bec2dd05e2b6210f53e83b0c2c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 27 Nov 2023 20:21:10 +0000 Subject: [PATCH 11/23] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.7.0 → v1.7.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.7.0...v1.7.1) --- .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 3d7e429f..90d4b6a3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -47,7 +47,7 @@ repos: language: python additional_dependencies: [pygments, restructuredtext_lint] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.7.0 + rev: v1.7.1 hooks: - id: mypy files: ^(src/|testing/) From e474438efb2306cca47162a7f115009d8ade9e7f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 21:01:07 +0000 Subject: [PATCH 12/23] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 23.11.0 → 23.12.0](https://github.com/psf/black/compare/23.11.0...23.12.0) --- .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 90d4b6a3..d32d3d85 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black - rev: 23.11.0 + rev: 23.12.0 hooks: - id: black args: [--safe, --quiet] From d3877d47a0cd2a142c9d63c97c46d73b97d1b423 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 21 Dec 2023 12:31:52 +0200 Subject: [PATCH 13/23] Move `_raise_wrapfail` to callers.py It's only used there. --- src/pluggy/_callers.py | 15 ++++++++++++++- src/pluggy/_result.py | 15 --------------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/pluggy/_callers.py b/src/pluggy/_callers.py index 6498eaed..f9bad2d2 100644 --- a/src/pluggy/_callers.py +++ b/src/pluggy/_callers.py @@ -6,12 +6,12 @@ from typing import cast from typing import Generator from typing import Mapping +from typing import NoReturn from typing import Sequence from typing import Tuple from typing import Union from ._hooks import HookImpl -from ._result import _raise_wrapfail from ._result import HookCallError from ._result import Result @@ -24,6 +24,19 @@ ] +def _raise_wrapfail( + wrap_controller: ( + Generator[None, Result[object], None] | Generator[None, object, object] + ), + msg: str, +) -> NoReturn: + co = wrap_controller.gi_code + raise RuntimeError( + "wrap_controller at %r %s:%d %s" + % (co.co_name, co.co_filename, co.co_firstlineno, msg) + ) + + def _multicall( hook_name: str, hook_impls: Sequence[HookImpl], diff --git a/src/pluggy/_result.py b/src/pluggy/_result.py index 29859eb9..aa21fa13 100644 --- a/src/pluggy/_result.py +++ b/src/pluggy/_result.py @@ -7,9 +7,7 @@ from typing import Callable from typing import cast from typing import final -from typing import Generator from typing import Generic -from typing import NoReturn from typing import Optional from typing import Tuple from typing import Type @@ -20,19 +18,6 @@ ResultType = TypeVar("ResultType") -def _raise_wrapfail( - wrap_controller: ( - Generator[None, Result[ResultType], None] | Generator[None, object, object] - ), - msg: str, -) -> NoReturn: - co = wrap_controller.gi_code - raise RuntimeError( - "wrap_controller at %r %s:%d %s" - % (co.co_name, co.co_filename, co.co_firstlineno, msg) - ) - - class HookCallError(Exception): """Hook was called incorrectly.""" From 3bc3aab681d8c17536fe0a952ad571e3fac5c27e Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 21 Dec 2023 12:32:27 +0200 Subject: [PATCH 14/23] Warn when old-style hookwrapper raises during teardown Fix #463. --- changelog/463.feature.rst | 2 ++ docs/api_reference.rst | 12 ++++++++++ src/pluggy/__init__.py | 6 +++++ src/pluggy/_callers.py | 24 +++++++++++++++---- src/pluggy/_warnings.py | 27 +++++++++++++++++++++ testing/test_warnings.py | 49 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 changelog/463.feature.rst create mode 100644 src/pluggy/_warnings.py create mode 100644 testing/test_warnings.py diff --git a/changelog/463.feature.rst b/changelog/463.feature.rst new file mode 100644 index 00000000..8340cc94 --- /dev/null +++ b/changelog/463.feature.rst @@ -0,0 +1,2 @@ +A warning :class:`~pluggy.PluggyTeardownRaisedWarning` is now issued when an old-style hookwrapper raises an exception during teardown. +See the warning documentation for more details. diff --git a/docs/api_reference.rst b/docs/api_reference.rst index 40b27bfe..b14d725d 100644 --- a/docs/api_reference.rst +++ b/docs/api_reference.rst @@ -47,3 +47,15 @@ API Reference .. autoclass:: pluggy.HookimplOpts() :show-inheritance: :members: + + +Warnings +-------- + +Custom warnings generated in some situations such as improper usage or deprecated features. + +.. autoclass:: pluggy.PluggyWarning() + :show-inheritance: + +.. autoclass:: pluggy.PluggyTeardownRaisedWarning() + :show-inheritance: diff --git a/src/pluggy/__init__.py b/src/pluggy/__init__.py index 9d9e873b..2adf9454 100644 --- a/src/pluggy/__init__.py +++ b/src/pluggy/__init__.py @@ -18,6 +18,8 @@ "HookspecMarker", "HookimplMarker", "Result", + "PluggyWarning", + "PluggyTeardownRaisedWarning", ] from ._manager import PluginManager, PluginValidationError @@ -31,3 +33,7 @@ HookimplOpts, HookImpl, ) +from ._warnings import ( + PluggyWarning, + PluggyTeardownRaisedWarning, +) diff --git a/src/pluggy/_callers.py b/src/pluggy/_callers.py index f9bad2d2..787f56ba 100644 --- a/src/pluggy/_callers.py +++ b/src/pluggy/_callers.py @@ -3,6 +3,7 @@ """ from __future__ import annotations +import warnings from typing import cast from typing import Generator from typing import Mapping @@ -14,12 +15,13 @@ from ._hooks import HookImpl from ._result import HookCallError from ._result import Result +from ._warnings import PluggyTeardownRaisedWarning # Need to distinguish between old- and new-style hook wrappers. -# Wrapping one a singleton tuple is the fastest type-safe way I found to do it. +# Wrapping with a tuple is the fastest type-safe way I found to do it. Teardown = Union[ - Tuple[Generator[None, Result[object], None]], + Tuple[Generator[None, Result[object], None], HookImpl], Generator[None, object, object], ] @@ -37,6 +39,16 @@ def _raise_wrapfail( ) +def _warn_teardown_exception( + hook_name: str, hook_impl: HookImpl, e: BaseException +) -> None: + msg = "A plugin raised an exception during an old-style hookwrapper teardown.\n" + msg += f"Plugin: {hook_impl.plugin_name}, Hook: {hook_name}\n" + msg += f"{type(e).__name__}: {e}\n" + msg += "For more information see https://pluggy.readthedocs.io/en/stable/api_reference.html#pluggy.PluggyTeardownRaisedWarning" # noqa: E501 + warnings.warn(PluggyTeardownRaisedWarning(msg), stacklevel=5) + + def _multicall( hook_name: str, hook_impls: Sequence[HookImpl], @@ -73,7 +85,7 @@ def _multicall( res = hook_impl.function(*args) wrapper_gen = cast(Generator[None, Result[object], None], res) next(wrapper_gen) # first yield - teardowns.append((wrapper_gen,)) + teardowns.append((wrapper_gen, hook_impl)) except StopIteration: _raise_wrapfail(wrapper_gen, "did not yield") elif hook_impl.wrapper: @@ -141,9 +153,13 @@ def _multicall( if isinstance(teardown, tuple): try: teardown[0].send(outcome) - _raise_wrapfail(teardown[0], "has second yield") except StopIteration: pass + except BaseException as e: + _warn_teardown_exception(hook_name, teardown[1], e) + raise + else: + _raise_wrapfail(teardown[0], "has second yield") else: try: if outcome._exception is not None: diff --git a/src/pluggy/_warnings.py b/src/pluggy/_warnings.py new file mode 100644 index 00000000..6356c770 --- /dev/null +++ b/src/pluggy/_warnings.py @@ -0,0 +1,27 @@ +from typing import final + + +class PluggyWarning(UserWarning): + """Base class for all warnings emitted by pluggy.""" + + __module__ = "pluggy" + + +@final +class PluggyTeardownRaisedWarning(PluggyWarning): + """A plugin raised an exception during an :ref:`old-style hookwrapper + ` teardown. + + Such exceptions are not handled by pluggy, and may cause subsequent + teardowns to be executed at unexpected times, or be skipped entirely. + + This is an issue in the plugin implementation. + + If the exception is unintended, fix the underlying cause. + + If the exception is intended, switch to :ref:`new-style hook wrappers + `, or use :func:`result.force_exception() + ` to set the exception instead of raising. + """ + + __module__ = "pluggy" diff --git a/testing/test_warnings.py b/testing/test_warnings.py new file mode 100644 index 00000000..4f5454be --- /dev/null +++ b/testing/test_warnings.py @@ -0,0 +1,49 @@ +from pathlib import Path + +import pytest + +from pluggy import HookimplMarker +from pluggy import HookspecMarker +from pluggy import PluggyTeardownRaisedWarning +from pluggy import PluginManager + + +hookspec = HookspecMarker("example") +hookimpl = HookimplMarker("example") + + +def test_teardown_raised_warning(pm: PluginManager) -> None: + class Api: + @hookspec + def my_hook(self): + raise NotImplementedError() + + pm.add_hookspecs(Api) + + class Plugin1: + @hookimpl + def my_hook(self): + pass + + class Plugin2: + @hookimpl(hookwrapper=True) + def my_hook(self): + yield + 1 / 0 + + class Plugin3: + @hookimpl(hookwrapper=True) + def my_hook(self): + yield + + 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() + assert len(wc.list) == 1 + assert Path(wc.list[0].filename).name == "test_warnings.py" From 59e0ca3d7a35e60bdd769cfd97678b801b9472a4 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 24 Dec 2023 12:53:52 +0200 Subject: [PATCH 15/23] Defer loading of importtlib.metadata This package is needed for `load_setuptools_entrypoints`, but it is quite slow to import. While most users do use `load_setuptools_entrypoints`, maybe some don't, so let's let them avoid the cost. For `import pluggy`: Before: 36ms After: 11ms --- src/pluggy/_manager.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/pluggy/_manager.py b/src/pluggy/_manager.py index 84717e6e..a984d1d7 100644 --- a/src/pluggy/_manager.py +++ b/src/pluggy/_manager.py @@ -1,6 +1,5 @@ from __future__ import annotations -import importlib.metadata import inspect import types import warnings @@ -11,6 +10,7 @@ from typing import Iterable from typing import Mapping from typing import Sequence +from typing import TYPE_CHECKING from . import _tracing from ._callers import _multicall @@ -26,6 +26,10 @@ from ._hooks import normalize_hookimpl_opts from ._result import Result +if TYPE_CHECKING: + # importtlib.metadata import is slow, defer it. + import importlib.metadata + _BeforeTrace = Callable[[str, Sequence[HookImpl], Mapping[str, Any]], None] _AfterTrace = Callable[[Result[Any], str, Sequence[HookImpl], Mapping[str, Any]], None] @@ -384,6 +388,8 @@ def load_setuptools_entrypoints(self, group: str, name: str | None = None) -> in :return: The number of plugins loaded by this call. """ + import importlib.metadata + count = 0 for dist in list(importlib.metadata.distributions()): for ep in dist.entry_points: From 29912d77ba24aa4ddfb3d9ff1e9d8a4a783e56b6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 25 Dec 2023 20:43:34 +0000 Subject: [PATCH 16/23] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 23.12.0 → 23.12.1](https://github.com/psf/black/compare/23.12.0...23.12.1) - [github.com/pre-commit/mirrors-mypy: v1.7.1 → v1.8.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.7.1...v1.8.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d32d3d85..078703be 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black - rev: 23.12.0 + rev: 23.12.1 hooks: - id: black args: [--safe, --quiet] @@ -47,7 +47,7 @@ repos: language: python additional_dependencies: [pygments, restructuredtext_lint] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.7.1 + rev: v1.8.0 hooks: - id: mypy files: ^(src/|testing/) From 0abac7520e060f479dbf84c0336b0dff23a14c9c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 11 Jan 2024 13:59:00 -0300 Subject: [PATCH 17/23] Fix formatting in wrappers docs Noticed that part of the text is not being formatted correctly in readthedocs. --- docs/index.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 5bc90b79..42fb8f49 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -416,9 +416,11 @@ of the hook thus far, or, if the previous calls raised an exception, it is :py:meth:`thrown ` the exception. The function should do one of two things: -- Return a value, which can be the same value as received from the ``yield``, or -something else entirely. + +- Return a value, which can be the same value as received from the ``yield``, or something else entirely. + - Raise an exception. + The return value or exception propagate to further hook wrappers, and finally to the hook caller. From 4b5b2d4ede425742d99ebb6539c42517a4401dc0 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 11 Jan 2024 19:25:30 +0200 Subject: [PATCH 18/23] CHANGELOG: fix errors in 1.0.0 entry Refs #468. --- CHANGELOG.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2ce57326..b7ae00f4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -148,7 +148,7 @@ Features -------- - `#282 `_: When registering a hookimpl which is declared as ``hookwrapper=True`` but whose - function is not a generator function, a ``PluggyValidationError`` exception is + function is not a generator function, a :class:`~pluggy.PluginValidationError` exception is now raised. Previously this problem would cause an error only later, when calling the hook. @@ -158,7 +158,7 @@ Features .. code-block:: python - def my_hook_real_implementation(arg): + def my_hook_implementation(arg): print("before") yield print("after") From 13b3661080cd3f6dcae1dedbceac6814b740c6dc Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 13 Jan 2024 10:39:11 +0200 Subject: [PATCH 19/23] Add `PluginManager.unblock` method to unblock a name Pytest currently accesses internals to do this: https://github.com/pytest-dev/pytest/blob/1b78de4e21d55983422e7327a51304ef0fc61679/src/_pytest/config/__init__.py#L740-L746 Let's provide a proper API for this. --- src/pluggy/_manager.py | 10 ++++++++++ testing/test_pluginmanager.py | 7 +++++++ 2 files changed, 17 insertions(+) diff --git a/src/pluggy/_manager.py b/src/pluggy/_manager.py index a984d1d7..ce1e107a 100644 --- a/src/pluggy/_manager.py +++ b/src/pluggy/_manager.py @@ -235,6 +235,16 @@ def is_blocked(self, name: str) -> bool: """Return whether the given plugin name is blocked.""" return name in self._name2plugin and self._name2plugin[name] is None + def unblock(self, name: str) -> bool: + """Unblocks a name. + + Returns whether the name was actually blocked. + """ + if self._name2plugin.get(name, -1) is None: + del self._name2plugin[name] + return True + return False + def add_hookspecs(self, module_or_class: _Namespace) -> None: """Add new hook specifications defined in the given ``module_or_class``. diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index 81b86b65..7f09d9b4 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -110,6 +110,13 @@ class A: pm.unregister(name="somename") assert pm.is_blocked("somename") + # Unblock. + assert not pm.unblock("someothername") + assert pm.unblock("somename") + assert not pm.is_blocked("somename") + assert not pm.unblock("somename") + assert pm.register(A(), "somename") + def test_register_mismatch_method(he_pm: PluginManager) -> None: class hello: From 4577b459a9043ba283e0d7a39beeddd627cc54a0 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 20 Jan 2024 12:58:24 +0200 Subject: [PATCH 20/23] hooks: add comment describing `_hookimpls`'s format/invariants. --- src/pluggy/_hooks.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/pluggy/_hooks.py b/src/pluggy/_hooks.py index 916ca704..ed72ed2f 100644 --- a/src/pluggy/_hooks.py +++ b/src/pluggy/_hooks.py @@ -389,6 +389,13 @@ def __init__( #: Name of the hook getting called. self.name: Final = name self._hookexec: Final = hook_execute + # The hookimpls list. The caller iterates it *in reverse*. Format: + # 1. trylast nonwrappers + # 2. nonwrappers + # 3. tryfirst nonwrappers + # 4. trylast wrappers + # 5. wrappers + # 6. tryfirst wrappers self._hookimpls: Final[list[HookImpl]] = [] self._call_history: _CallHistory | None = None # TODO: Document, or make private. From 443fee6d7a30ca694aa2eec77bde4aea9356efa0 Mon Sep 17 00:00:00 2001 From: Vishu Tupili Date: Wed, 13 Dec 2023 13:37:09 -0500 Subject: [PATCH 21/23] hooks: fix `call_extra` extra methods getting ordered before everything else when there is at least one wrapper impl. Fix #441. Regressed in 63b7e908b4b22c30d86cd2cff240b3b7aa6da596 - pluggy 1.1.0. [ran: reworked] --- src/pluggy/_hooks.py | 9 ++--- testing/test_hookcaller.py | 72 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/src/pluggy/_hooks.py b/src/pluggy/_hooks.py index ed72ed2f..6623c147 100644 --- a/src/pluggy/_hooks.py +++ b/src/pluggy/_hooks.py @@ -548,10 +548,11 @@ def call_extra( hookimpl = HookImpl(None, "", method, opts) # Find last non-tryfirst nonwrapper method. i = len(hookimpls) - 1 - while ( - i >= 0 - and hookimpls[i].tryfirst - and not (hookimpls[i].hookwrapper or hookimpls[i].wrapper) + while i >= 0 and ( + # Skip wrappers. + (hookimpls[i].hookwrapper or hookimpls[i].wrapper) + # Skip tryfirst nonwrappers. + or hookimpls[i].tryfirst ): i -= 1 hookimpls.insert(i + 1, hookimpl) diff --git a/testing/test_hookcaller.py b/testing/test_hookcaller.py index 88ed1316..9eb6f979 100644 --- a/testing/test_hookcaller.py +++ b/testing/test_hookcaller.py @@ -1,4 +1,5 @@ from typing import Callable +from typing import Generator from typing import List from typing import Sequence from typing import TypeVar @@ -448,3 +449,74 @@ def conflict(self) -> None: "Hook 'conflict' is already registered within namespace " ".Api1'>" ) + + +def test_call_extra_hook_order(hc: HookCaller, addmeth: AddMeth) -> None: + """Ensure that call_extra is calling hooks in the right order.""" + order = [] + + @addmeth(tryfirst=True) + def method1() -> str: + order.append("1") + return "1" + + @addmeth() + def method2() -> str: + order.append("2") + return "2" + + @addmeth(trylast=True) + def method3() -> str: + order.append("3") + return "3" + + @addmeth(wrapper=True, tryfirst=True) + def method4() -> Generator[None, str, str]: + order.append("4pre") + result = yield + order.append("4post") + return result + + @addmeth(wrapper=True) + def method5() -> Generator[None, str, str]: + order.append("5pre") + result = yield + order.append("5post") + return result + + @addmeth(wrapper=True, trylast=True) + def method6() -> Generator[None, str, str]: + order.append("6pre") + result = yield + order.append("6post") + return result + + def extra1() -> str: + order.append("extra1") + return "extra1" + + def extra2() -> str: + order.append("extra2") + return "extra2" + + result = hc.call_extra([extra1, extra2], {"arg": "test"}) + assert order == [ + "4pre", + "5pre", + "6pre", + "1", + "extra2", + "extra1", + "2", + "3", + "6post", + "5post", + "4post", + ] + assert result == [ + "1", + "extra2", + "extra1", + "2", + "3", + ] From 7aef3e608fddc57648daffaec23432e6893f26ff Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 20 Jan 2024 18:47:13 +0200 Subject: [PATCH 22/23] hooks: fix plugins registering other plugins in a hook when the other plugins implement the same hook itself. Fix #438. Regressed in 63b7e908b4b22c30d86cd2cff240b3b7aa6da596 - pluggy 1.1.0. Went with the simple solution described in the issue for now, basically undoing most of the rationale for 63b7e908b4b22c30d86cd2cf (though I think it's still better to have a single `_hookimpls` list). --- src/pluggy/_hooks.py | 6 ++- testing/test_pluginmanager.py | 76 +++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/src/pluggy/_hooks.py b/src/pluggy/_hooks.py index 6623c147..7c8420f4 100644 --- a/src/pluggy/_hooks.py +++ b/src/pluggy/_hooks.py @@ -497,7 +497,8 @@ def __call__(self, **kwargs: object) -> Any: ), "Cannot directly call a historic hook - use call_historic instead." self._verify_all_args_are_provided(kwargs) firstresult = self.spec.opts.get("firstresult", False) if self.spec else False - return self._hookexec(self.name, self._hookimpls, kwargs, firstresult) + # Copy because plugins may register other plugins during iteration (#438). + return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult) def call_historic( self, @@ -518,7 +519,8 @@ def call_historic( self._call_history.append((kwargs, result_callback)) # Historizing hooks don't return results. # Remember firstresult isn't compatible with historic. - res = self._hookexec(self.name, self._hookimpls, kwargs, False) + # Copy because plugins may register other plugins during iteration (#438). + res = self._hookexec(self.name, self._hookimpls.copy(), kwargs, False) if result_callback is None: return if isinstance(res, list): diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index 81b86b65..08e2f37e 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -675,3 +675,79 @@ def he_method1(self): assert saveindent[0] > indent finally: undo() + + +@pytest.mark.parametrize("historic", [False, True]) +def test_register_while_calling( + pm: PluginManager, + historic: bool, +) -> None: + """Test that registering an impl of a hook while it is being called does + not affect the call itself, only later calls. + + For historic hooks however, the hook is called immediately on registration. + + Regression test for #438. + """ + hookspec = HookspecMarker("example") + + class Hooks: + @hookspec(historic=historic) + def configure(self) -> int: + raise NotImplementedError() + + class Plugin1: + @hookimpl + def configure(self) -> int: + return 1 + + class Plugin2: + def __init__(self) -> None: + self.already_registered = False + + @hookimpl + def configure(self) -> int: + if not self.already_registered: + pm.register(Plugin4()) + pm.register(Plugin5()) + pm.register(Plugin6()) + self.already_registered = True + return 2 + + class Plugin3: + @hookimpl + def configure(self) -> int: + return 3 + + class Plugin4: + @hookimpl(tryfirst=True) + def configure(self) -> int: + return 4 + + class Plugin5: + @hookimpl() + def configure(self) -> int: + return 5 + + class Plugin6: + @hookimpl(trylast=True) + def configure(self) -> int: + return 6 + + pm.add_hookspecs(Hooks) + pm.register(Plugin1()) + pm.register(Plugin2()) + pm.register(Plugin3()) + + if not historic: + result = pm.hook.configure() + assert result == [3, 2, 1] + result = pm.hook.configure() + assert result == [4, 5, 3, 2, 1, 6] + else: + result = [] + pm.hook.configure.call_historic(result.append) + assert result == [4, 5, 6, 3, 2, 1] + result = [] + pm.hook.configure.call_historic(result.append) + assert result == [4, 5, 3, 2, 1, 6] From 2efd28ef5dcd8b7ad44a41777ce640debbbf9c39 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 24 Jan 2024 10:48:53 +0200 Subject: [PATCH 23/23] Preparing release 1.4.0 --- CHANGELOG.rst | 19 +++++++++++++++++++ changelog/463.feature.rst | 2 -- 2 files changed, 19 insertions(+), 2 deletions(-) delete mode 100644 changelog/463.feature.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b7ae00f4..d7af1746 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -21,6 +21,25 @@ Versions follow `Semantic Versioning `_ (``.. .. towncrier release notes start +pluggy 1.4.0 (2024-01-24) +========================= + +Features +-------- + +- `#463 `_: A warning :class:`~pluggy.PluggyTeardownRaisedWarning` is now issued when an old-style hookwrapper raises an exception during teardown. + See the warning documentation for more details. + +- `#471 `_: Add :func:`PluginManager.unblock ` method to unblock a plugin by plugin name. + +Bug Fixes +--------- + +- `#441 `_: Fix :func:`~pluggy.HookCaller.call_extra()` extra methods getting ordered before everything else in some circumstances. Regressed in pluggy 1.1.0. + +- `#438 `_: Fix plugins registering other plugins in a hook when the other plugins implement the same hook itself. Regressed in pluggy 1.1.0. + + pluggy 1.3.0 (2023-08-26) ========================= diff --git a/changelog/463.feature.rst b/changelog/463.feature.rst deleted file mode 100644 index 8340cc94..00000000 --- a/changelog/463.feature.rst +++ /dev/null @@ -1,2 +0,0 @@ -A warning :class:`~pluggy.PluggyTeardownRaisedWarning` is now issued when an old-style hookwrapper raises an exception during teardown. -See the warning documentation for more details.