From 8740f26e76ff1c53ad469f46da4e9dc346e73a7b Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 1 Feb 2020 03:46:10 +0100 Subject: [PATCH 1/7] assertion: save/restore hooks via stack This fixes the hook(s) not being used after `testdir.runpytest` etc. --- src/_pytest/assertion/__init__.py | 14 ++++++++++++-- testing/test_assertion.py | 17 +++++++++++++---- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/_pytest/assertion/__init__.py b/src/_pytest/assertion/__init__.py index a060723a76b..d2ef43a00ee 100644 --- a/src/_pytest/assertion/__init__.py +++ b/src/_pytest/assertion/__init__.py @@ -13,6 +13,9 @@ from _pytest.main import Session +saved_hooks = [] + + def pytest_addoption(parser): group = parser.getgroup("debugconfig") group.addoption( @@ -37,6 +40,14 @@ def pytest_addoption(parser): ) +def pytest_configure(): + saved_hooks.append((util._reprcompare, util._assertion_pass)) + + +def pytest_unconfigure(): + util._reprcompare, util._assertion_pass = saved_hooks.pop() + + def register_assert_rewrite(*names) -> None: """Register one or more module names to be rewritten on import. @@ -156,8 +167,7 @@ def call_assertion_pass_hook(lineno, orig, expl): def pytest_runtest_teardown(item): - util._reprcompare = None - util._assertion_pass = None + util._reprcompare, util._assertion_pass = saved_hooks[-1] def pytest_sessionfinish(session): diff --git a/testing/test_assertion.py b/testing/test_assertion.py index e975a3fea2b..dc260b39f9e 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -72,10 +72,19 @@ def test_dummy_failure(testdir): # how meta! result = testdir.runpytest_subprocess() result.stdout.fnmatch_lines( [ - "E * AssertionError: ([[][]], [[][]], [[][]])*", - "E * assert" - " {'failed': 1, 'passed': 0, 'skipped': 0} ==" - " {'failed': 0, 'passed': 1, 'skipped': 0}", + "> r.assertoutcome(passed=1)", + "E AssertionError: ([[][]], [[][]], [[][]])*", + "E assert {'failed': 1,... 'skipped': 0} == {'failed': 0,... 'skipped': 0}", + "E Omitting 1 identical items, use -vv to show", + "E Differing items:", + "E Use -v to get the full diff", + ] + ) + # XXX: unstable output. + result.stdout.fnmatch_lines_random( + [ + "E {'failed': 1} != {'failed': 0}", + "E {'passed': 0} != {'passed': 1}", ] ) From fb7a0a885f07b103e86a6150eac554169f638f7f Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 1 Feb 2020 06:43:53 +0100 Subject: [PATCH 2/7] fixup! assertion: save/restore hooks via stack --- changelog/6646.bugfix.rst | 1 + src/_pytest/assertion/__init__.py | 15 +++------------ 2 files changed, 4 insertions(+), 12 deletions(-) create mode 100644 changelog/6646.bugfix.rst diff --git a/changelog/6646.bugfix.rst b/changelog/6646.bugfix.rst new file mode 100644 index 00000000000..4dba3ed0723 --- /dev/null +++ b/changelog/6646.bugfix.rst @@ -0,0 +1 @@ +Assertion rewriting hooks are (re)stored for the current item, which fixes them being still used after e.g. pytester's :func:`testdir.runpytest <_pytest.pytester.Testdir.runpytest>` etc. diff --git a/src/_pytest/assertion/__init__.py b/src/_pytest/assertion/__init__.py index d2ef43a00ee..d8c58f0a52d 100644 --- a/src/_pytest/assertion/__init__.py +++ b/src/_pytest/assertion/__init__.py @@ -13,9 +13,6 @@ from _pytest.main import Session -saved_hooks = [] - - def pytest_addoption(parser): group = parser.getgroup("debugconfig") group.addoption( @@ -40,14 +37,6 @@ def pytest_addoption(parser): ) -def pytest_configure(): - saved_hooks.append((util._reprcompare, util._assertion_pass)) - - -def pytest_unconfigure(): - util._reprcompare, util._assertion_pass = saved_hooks.pop() - - def register_assert_rewrite(*names) -> None: """Register one or more module names to be rewritten on import. @@ -154,6 +143,7 @@ def callbinrepr(op, left, right): return res return None + item._saved_assert_hooks = util._reprcompare, util._assertion_pass util._reprcompare = callbinrepr if item.ihook.pytest_assertion_pass.get_hookimpls(): @@ -167,7 +157,8 @@ def call_assertion_pass_hook(lineno, orig, expl): def pytest_runtest_teardown(item): - util._reprcompare, util._assertion_pass = saved_hooks[-1] + util._reprcompare, util._assertion_pass = item._saved_assert_hooks + del item._saved_assert_hooks def pytest_sessionfinish(session): From 8eb0d683f75283b902b8b03d54ef77312042b65e Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 1 Feb 2020 06:54:45 +0100 Subject: [PATCH 3/7] fixup! fixup! assertion: save/restore hooks via stack --- src/_pytest/assertion/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/_pytest/assertion/__init__.py b/src/_pytest/assertion/__init__.py index d8c58f0a52d..51a8969c762 100644 --- a/src/_pytest/assertion/__init__.py +++ b/src/_pytest/assertion/__init__.py @@ -157,8 +157,11 @@ def call_assertion_pass_hook(lineno, orig, expl): def pytest_runtest_teardown(item): - util._reprcompare, util._assertion_pass = item._saved_assert_hooks - del item._saved_assert_hooks + if hasattr(item, "_saved_assert_hooks"): + util._reprcompare, util._assertion_pass = item._saved_assert_hooks + del item._saved_assert_hooks + else: + util._reprcompare, util._assertion_pass = None, None def pytest_sessionfinish(session): From caa056f4398af9df37bdfdd38e3358de0d4c861f Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 1 Feb 2020 16:06:25 +0100 Subject: [PATCH 4/7] use pytest_runtest_protocol --- src/_pytest/assertion/__init__.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/_pytest/assertion/__init__.py b/src/_pytest/assertion/__init__.py index 51a8969c762..cdb0347034f 100644 --- a/src/_pytest/assertion/__init__.py +++ b/src/_pytest/assertion/__init__.py @@ -8,6 +8,7 @@ from _pytest.assertion import truncate from _pytest.assertion import util from _pytest.compat import TYPE_CHECKING +from _pytest.config import hookimpl if TYPE_CHECKING: from _pytest.main import Session @@ -105,7 +106,8 @@ def pytest_collection(session: "Session") -> None: assertstate.hook.set_session(session) -def pytest_runtest_setup(item): +@hookimpl(tryfirst=True, hookwrapper=True) +def pytest_runtest_protocol(item): """Setup the pytest_assertrepr_compare and pytest_assertion_pass hooks The newinterpret and rewrite modules will use util._reprcompare if @@ -143,7 +145,7 @@ def callbinrepr(op, left, right): return res return None - item._saved_assert_hooks = util._reprcompare, util._assertion_pass + saved_assert_hooks = util._reprcompare, util._assertion_pass util._reprcompare = callbinrepr if item.ihook.pytest_assertion_pass.get_hookimpls(): @@ -155,13 +157,9 @@ def call_assertion_pass_hook(lineno, orig, expl): util._assertion_pass = call_assertion_pass_hook + yield -def pytest_runtest_teardown(item): - if hasattr(item, "_saved_assert_hooks"): - util._reprcompare, util._assertion_pass = item._saved_assert_hooks - del item._saved_assert_hooks - else: - util._reprcompare, util._assertion_pass = None, None + util._reprcompare, util._assertion_pass = saved_assert_hooks def pytest_sessionfinish(session): From 34bf992806cc59d4eb0f2e542e6e42bc442f3f65 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 1 Feb 2020 17:00:09 +0100 Subject: [PATCH 5/7] Move hookimpl/hookspec to _pytest.config.plugin This allows for more easily importing it without circular imports. --- src/_pytest/assertion/__init__.py | 2 +- src/_pytest/config/__init__.py | 8 ++------ src/_pytest/config/plugin.py | 6 ++++++ 3 files changed, 9 insertions(+), 7 deletions(-) create mode 100644 src/_pytest/config/plugin.py diff --git a/src/_pytest/assertion/__init__.py b/src/_pytest/assertion/__init__.py index cdb0347034f..9fee81e5ecf 100644 --- a/src/_pytest/assertion/__init__.py +++ b/src/_pytest/assertion/__init__.py @@ -8,7 +8,7 @@ from _pytest.assertion import truncate from _pytest.assertion import util from _pytest.compat import TYPE_CHECKING -from _pytest.config import hookimpl +from _pytest.config.plugin import hookimpl if TYPE_CHECKING: from _pytest.main import Session diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index ed3334e5fc4..b8e9f3c1bd5 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -22,8 +22,6 @@ import attr import py from packaging.version import Version -from pluggy import HookimplMarker -from pluggy import HookspecMarker from pluggy import PluginManager import _pytest._code @@ -34,6 +32,8 @@ from .exceptions import UsageError from .findpaths import determine_setup from .findpaths import exists +from .plugin import hookimpl +from .plugin import hookspec # noqa: F401 from _pytest._code import ExceptionInfo from _pytest._code import filter_traceback from _pytest._io import TerminalWriter @@ -55,10 +55,6 @@ Ideally this type would be provided by pluggy itself.""" -hookimpl = HookimplMarker("pytest") -hookspec = HookspecMarker("pytest") - - class ConftestImportFailure(Exception): def __init__(self, path, excinfo): Exception.__init__(self, path, excinfo) diff --git a/src/_pytest/config/plugin.py b/src/_pytest/config/plugin.py new file mode 100644 index 00000000000..d4705a04695 --- /dev/null +++ b/src/_pytest/config/plugin.py @@ -0,0 +1,6 @@ +from pluggy import HookimplMarker +from pluggy import HookspecMarker + + +hookimpl = HookimplMarker("pytest") +hookspec = HookspecMarker("pytest") From 88c63ecf61a1d07fef107f0f62f21397a791ddb2 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 1 Feb 2020 17:19:21 +0100 Subject: [PATCH 6/7] Revert "Move hookimpl/hookspec to _pytest.config.plugin" This reverts commit 34bf992806cc59d4eb0f2e542e6e42bc442f3f65. --- src/_pytest/assertion/__init__.py | 2 +- src/_pytest/config/__init__.py | 8 ++++++-- src/_pytest/config/plugin.py | 6 ------ 3 files changed, 7 insertions(+), 9 deletions(-) delete mode 100644 src/_pytest/config/plugin.py diff --git a/src/_pytest/assertion/__init__.py b/src/_pytest/assertion/__init__.py index 9fee81e5ecf..cdb0347034f 100644 --- a/src/_pytest/assertion/__init__.py +++ b/src/_pytest/assertion/__init__.py @@ -8,7 +8,7 @@ from _pytest.assertion import truncate from _pytest.assertion import util from _pytest.compat import TYPE_CHECKING -from _pytest.config.plugin import hookimpl +from _pytest.config import hookimpl if TYPE_CHECKING: from _pytest.main import Session diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index b8e9f3c1bd5..ed3334e5fc4 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -22,6 +22,8 @@ import attr import py from packaging.version import Version +from pluggy import HookimplMarker +from pluggy import HookspecMarker from pluggy import PluginManager import _pytest._code @@ -32,8 +34,6 @@ from .exceptions import UsageError from .findpaths import determine_setup from .findpaths import exists -from .plugin import hookimpl -from .plugin import hookspec # noqa: F401 from _pytest._code import ExceptionInfo from _pytest._code import filter_traceback from _pytest._io import TerminalWriter @@ -55,6 +55,10 @@ Ideally this type would be provided by pluggy itself.""" +hookimpl = HookimplMarker("pytest") +hookspec = HookspecMarker("pytest") + + class ConftestImportFailure(Exception): def __init__(self, path, excinfo): Exception.__init__(self, path, excinfo) diff --git a/src/_pytest/config/plugin.py b/src/_pytest/config/plugin.py deleted file mode 100644 index d4705a04695..00000000000 --- a/src/_pytest/config/plugin.py +++ /dev/null @@ -1,6 +0,0 @@ -from pluggy import HookimplMarker -from pluggy import HookspecMarker - - -hookimpl = HookimplMarker("pytest") -hookspec = HookspecMarker("pytest") From c82846fbeb07876ddf86a5df0437985794b3667a Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 1 Feb 2020 17:21:19 +0100 Subject: [PATCH 7/7] fix/address circular import with _pytest.assertion --- src/_pytest/config/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index ed3334e5fc4..d4477ba81d8 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -27,7 +27,6 @@ from pluggy import PluginManager import _pytest._code -import _pytest.assertion import _pytest.deprecated import _pytest.hookspec # the extension point definitions from .exceptions import PrintHelp @@ -260,6 +259,8 @@ class PytestPluginManager(PluginManager): """ def __init__(self): + import _pytest.assertion + super().__init__("pytest") # The objects are module objects, only used generically. self._conftest_plugins = set() # type: Set[object] @@ -891,6 +892,8 @@ def _consider_importhook(self, args): ns, unknown_args = self._parser.parse_known_and_unknown_args(args) mode = getattr(ns, "assertmode", "plain") if mode == "rewrite": + import _pytest.assertion + try: hook = _pytest.assertion.install_importhook(self) except SystemError: