Skip to content

Commit b050578

Browse files
committed
pytester: split asserts to a separate plugin, don't rewrite pytester itself
An upcoming commit wants to import from `_pytest.pytester` in the public `pytest` module. This means that `_pytest.pytester` would start to get imported during import time, which it hasn't up to now -- it was imported by the plugin loader (if requested). When a plugin is loaded, it is subjected to assertion rewriting, but only if the module isn't imported yet, it issues a warning "Module already imported so cannot be rewritten" and skips the rewriting. So we'd end up with the pytester plugin not being rewritten, but it wants to be. Absent better ideas, the solution here is to split the pytester assertions to their own plugin (which will always only be imported by the plugin loader) and exclude pytester itself from plugin rewriting.
1 parent e986d84 commit b050578

File tree

3 files changed

+90
-28
lines changed

3 files changed

+90
-28
lines changed

src/_pytest/config/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ def directory_arg(path: str, optname: str) -> str:
256256

257257
builtin_plugins = set(default_plugins)
258258
builtin_plugins.add("pytester")
259+
builtin_plugins.add("pytester_assertions")
259260

260261

261262
def get_config(

src/_pytest/pytester.py

Lines changed: 23 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
"""(Disabled by default) support for testing pytest and pytest plugins."""
1+
"""(Disabled by default) support for testing pytest and pytest plugins.
2+
3+
PYTEST_DONT_REWRITE
4+
"""
25
import collections.abc
36
import contextlib
47
import gc
@@ -66,6 +69,9 @@
6669
import pexpect
6770

6871

72+
pytest_plugins = ["pytester_assertions"]
73+
74+
6975
IGNORE_PAM = [ # filenames added when obtaining details about the current user
7076
"/var/lib/sss/mc/passwd"
7177
]
@@ -408,16 +414,12 @@ def countoutcomes(self) -> List[int]:
408414

409415
def assertoutcome(self, passed: int = 0, skipped: int = 0, failed: int = 0) -> None:
410416
__tracebackhide__ = True
417+
from _pytest.pytester_assertions import assertoutcome
411418

412419
outcomes = self.listoutcomes()
413-
realpassed, realskipped, realfailed = outcomes
414-
obtained = {
415-
"passed": len(realpassed),
416-
"skipped": len(realskipped),
417-
"failed": len(realfailed),
418-
}
419-
expected = {"passed": passed, "skipped": skipped, "failed": failed}
420-
assert obtained == expected, outcomes
420+
assertoutcome(
421+
outcomes, passed=passed, skipped=skipped, failed=failed,
422+
)
421423

422424
def clear(self) -> None:
423425
self.calls[:] = []
@@ -574,25 +576,18 @@ def assert_outcomes(
574576
"""Assert that the specified outcomes appear with the respective
575577
numbers (0 means it didn't occur) in the text output from a test run."""
576578
__tracebackhide__ = True
577-
578-
d = self.parseoutcomes()
579-
obtained = {
580-
"passed": d.get("passed", 0),
581-
"skipped": d.get("skipped", 0),
582-
"failed": d.get("failed", 0),
583-
"errors": d.get("errors", 0),
584-
"xpassed": d.get("xpassed", 0),
585-
"xfailed": d.get("xfailed", 0),
586-
}
587-
expected = {
588-
"passed": passed,
589-
"skipped": skipped,
590-
"failed": failed,
591-
"errors": errors,
592-
"xpassed": xpassed,
593-
"xfailed": xfailed,
594-
}
595-
assert obtained == expected
579+
from _pytest.pytester_assertions import assert_outcomes
580+
581+
outcomes = self.parseoutcomes()
582+
assert_outcomes(
583+
outcomes,
584+
passed=passed,
585+
skipped=skipped,
586+
failed=failed,
587+
errors=errors,
588+
xpassed=xpassed,
589+
xfailed=xfailed,
590+
)
596591

597592

598593
class CwdSnapshot:

src/_pytest/pytester_assertions.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""Helper plugin for pytester; should not be loaded on its own."""
2+
# This plugin contains assertions used by pytester. pytester cannot
3+
# contain them itself, since it is imported by the `pytest` module,
4+
# hence cannot be subject to assertion rewriting, which requires a
5+
# module to not be already imported.
6+
from typing import Dict
7+
from typing import Sequence
8+
from typing import Tuple
9+
from typing import Union
10+
11+
from _pytest.reports import CollectReport
12+
from _pytest.reports import TestReport
13+
14+
15+
def assertoutcome(
16+
outcomes: Tuple[
17+
Sequence[TestReport],
18+
Sequence[Union[CollectReport, TestReport]],
19+
Sequence[Union[CollectReport, TestReport]],
20+
],
21+
passed: int = 0,
22+
skipped: int = 0,
23+
failed: int = 0,
24+
) -> None:
25+
__tracebackhide__ = True
26+
27+
realpassed, realskipped, realfailed = outcomes
28+
obtained = {
29+
"passed": len(realpassed),
30+
"skipped": len(realskipped),
31+
"failed": len(realfailed),
32+
}
33+
expected = {"passed": passed, "skipped": skipped, "failed": failed}
34+
assert obtained == expected, outcomes
35+
36+
37+
def assert_outcomes(
38+
outcomes: Dict[str, int],
39+
passed: int = 0,
40+
skipped: int = 0,
41+
failed: int = 0,
42+
errors: int = 0,
43+
xpassed: int = 0,
44+
xfailed: int = 0,
45+
) -> None:
46+
"""Assert that the specified outcomes appear with the respective
47+
numbers (0 means it didn't occur) in the text output from a test run."""
48+
__tracebackhide__ = True
49+
50+
obtained = {
51+
"passed": outcomes.get("passed", 0),
52+
"skipped": outcomes.get("skipped", 0),
53+
"failed": outcomes.get("failed", 0),
54+
"errors": outcomes.get("errors", 0),
55+
"xpassed": outcomes.get("xpassed", 0),
56+
"xfailed": outcomes.get("xfailed", 0),
57+
}
58+
expected = {
59+
"passed": passed,
60+
"skipped": skipped,
61+
"failed": failed,
62+
"errors": errors,
63+
"xpassed": xpassed,
64+
"xfailed": xfailed,
65+
}
66+
assert obtained == expected

0 commit comments

Comments
 (0)