Skip to content

Commit 7186d47

Browse files
committed
Code review
1 parent ec3144f commit 7186d47

File tree

5 files changed

+52
-51
lines changed

5 files changed

+52
-51
lines changed

changelog/1367.feature.rst

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,18 @@
66

77
.. code-block:: python
88
9-
def test(subtests):
10-
for i in range(5):
11-
with subtests.test(msg="custom message", i=i):
12-
assert i % 2 == 0
9+
def contains_docstring(p: Path) -> bool:
10+
"""Return True if the given Python file contains a top-level docstring."""
11+
...
1312
1413
15-
Each assert failure or error is caught by the context manager and reported individually.
14+
def test_py_files_contain_docstring(subtests: pytest.Subtests) -> None:
15+
for path in Path.cwd().glob("*.py"):
16+
with subtests.test(path=str(path)):
17+
assert contains_docstring(path)
18+
19+
20+
Each assert failure or error is caught by the context manager and reported individually, giving a clear picture of all files that are missing a docstring.
1621

1722
In addition, :meth:`unittest.TestCase.subTest` is now also supported.
1823

doc/en/how-to/subtests.rst

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@ Subtests are an alternative to parametrization, particularly useful when the exa
1616

1717
.. code-block:: python
1818
19-
# content of test_subtest.py
19+
def contains_docstring(p: Path) -> bool:
20+
"""Return True if the given Python file contains a top-level docstring."""
21+
...
2022
2123
22-
def test(subtests):
23-
for i in range(5):
24-
with subtests.test(msg="custom message", i=i):
25-
assert i % 2 == 0
24+
def test_py_files_contain_docstring(subtests: pytest.Subtests) -> None:
25+
for path in Path.cwd().glob("*.py"):
26+
with subtests.test(path=str(path)):
27+
assert contains_docstring(path)
2628
2729
Each assertion failure or error is caught by the context manager and reported individually:
2830

@@ -38,13 +40,13 @@ outside the ``subtests.test`` block:
3840
3941
def test(subtests):
4042
for i in range(5):
41-
with subtests.test(msg="stage 1", i=i):
43+
with subtests.test("stage 1", i=i):
4244
assert i % 2 == 0
4345
4446
assert func() == 10
4547
4648
for i in range(10, 20):
47-
with subtests.test(msg="stage 2", i=i):
49+
with subtests.test("stage 2", i=i):
4850
assert i % 2 == 0
4951
5052
.. note::

src/_pytest/subtests.py

Lines changed: 25 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@
1313
import time
1414
from types import TracebackType
1515
from typing import Any
16-
from typing import Literal
1716
from typing import TYPE_CHECKING
1817

1918
import pluggy
2019

2120
from _pytest._code import ExceptionInfo
21+
from _pytest._io.saferepr import saferepr
2222
from _pytest.capture import CaptureFixture
2323
from _pytest.capture import FDCapture
2424
from _pytest.capture import SysCapture
@@ -83,11 +83,11 @@ def head_line(self) -> str:
8383

8484
def _sub_test_description(self) -> str:
8585
parts = []
86-
if isinstance(self.context.msg, str):
86+
if self.context.msg is not None:
8787
parts.append(f"[{self.context.msg}]")
8888
if self.context.kwargs:
8989
params_desc = ", ".join(
90-
f"{k}={v!r}" for (k, v) in sorted(self.context.kwargs.items())
90+
f"{k}={saferepr(v)}" for (k, v) in self.context.kwargs.items()
9191
)
9292
parts.append(f"({params_desc})")
9393
return " ".join(parts) or "(<subtest>)"
@@ -106,8 +106,12 @@ def _from_json(cls, reportdict: dict[str, Any]) -> SubtestReport:
106106
return report
107107

108108
@classmethod
109-
def _from_test_report(cls, test_report: TestReport) -> SubtestReport:
110-
return super()._from_json(test_report._to_json())
109+
def _from_test_report(
110+
cls, test_report: TestReport, context: SubtestContext
111+
) -> Self:
112+
result = super()._from_json(test_report._to_json())
113+
result.context = context
114+
return result
111115

112116

113117
@fixture
@@ -121,8 +125,6 @@ def subtests(request: SubRequest) -> Subtests:
121125
return Subtests(request.node.ihook, suspend_capture_ctx, request, _ispytest=True)
122126

123127

124-
# Note: cannot use a dataclass here because Sphinx insists on showing up the __init__ method in the documentation,
125-
# even if we explicitly use :exclude-members: __init__.
126128
class Subtests:
127129
"""Subtests fixture, enables declaring subtests inside test functions via the :meth:`test` method."""
128130

@@ -178,12 +180,13 @@ class _SubTestContextManager:
178180
"""
179181
Context manager for subtests, capturing exceptions raised inside the subtest scope and handling
180182
them through the pytest machinery.
181-
182-
Note: initially this logic was implemented directly in Subtests.test() as a @contextmanager, however
183-
it is not possible to control the output fully when exiting from it due to an exception when
184-
in --exitfirst mode, so this was refactored into an explicit context manager class (pytest-dev/pytest-subtests#134).
185183
"""
186184

185+
# Note: initially the logic for this context manager was implemented directly
186+
# in Subtests.test() as a @contextmanager, however, it is not possible to control the output fully when
187+
# exiting from it due to an exception when in `--exitfirst` mode, so this was refactored into an
188+
# explicit context manager class (pytest-dev/pytest-subtests#134).
189+
187190
ihook: pluggy.HookRelay
188191
msg: str | None
189192
kwargs: dict[str, Any]
@@ -224,14 +227,21 @@ def __exit__(
224227
duration = precise_stop - self._precise_start
225228
stop = time.time()
226229

227-
call_info = make_call_info(
228-
exc_info, start=self._start, stop=stop, duration=duration, when="call"
230+
call_info = CallInfo[None](
231+
None,
232+
exc_info,
233+
start=self._start,
234+
stop=stop,
235+
duration=duration,
236+
when="call",
237+
_ispytest=True,
229238
)
230239
report = self.ihook.pytest_runtest_makereport(
231240
item=self.request.node, call=call_info
232241
)
233-
sub_report = SubtestReport._from_test_report(report)
234-
sub_report.context = SubtestContext(msg=self.msg, kwargs=self.kwargs.copy())
242+
sub_report = SubtestReport._from_test_report(
243+
report, SubtestContext(msg=self.msg, kwargs=self.kwargs.copy())
244+
)
235245

236246
self._captured_output.update_report(sub_report)
237247
self._captured_logs.update_report(sub_report)
@@ -250,25 +260,6 @@ def __exit__(
250260
return True
251261

252262

253-
def make_call_info(
254-
exc_info: ExceptionInfo[BaseException] | None,
255-
*,
256-
start: float,
257-
stop: float,
258-
duration: float,
259-
when: Literal["collect", "setup", "call", "teardown"],
260-
) -> CallInfo[Any]:
261-
return CallInfo(
262-
None,
263-
exc_info,
264-
start=start,
265-
stop=stop,
266-
duration=duration,
267-
when=when,
268-
_ispytest=True,
269-
)
270-
271-
272263
@contextmanager
273264
def capturing_output(request: SubRequest) -> Iterator[Captured]:
274265
option = request.config.getoption("capture", None)

src/_pytest/unittest.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
from _pytest.python import Module
3636
from _pytest.runner import CallInfo
3737
from _pytest.runner import check_interactive_exception
38-
from _pytest.subtests import make_call_info
3938
from _pytest.subtests import SubtestContext
4039
from _pytest.subtests import SubtestReport
4140

@@ -419,17 +418,21 @@ def addSubTest(
419418
case unreachable:
420419
assert_never(unreachable)
421420

422-
call_info = make_call_info(
421+
call_info = CallInfo[None](
422+
None,
423423
exception_info,
424424
start=0,
425425
stop=0,
426426
duration=0,
427427
when="call",
428+
_ispytest=True,
428429
)
429430
msg = test._message if isinstance(test._message, str) else None # type: ignore[attr-defined]
430431
report = self.ihook.pytest_runtest_makereport(item=self, call=call_info)
431-
sub_report = SubtestReport._from_test_report(report)
432-
sub_report.context = SubtestContext(msg=msg, kwargs=dict(test.params)) # type: ignore[attr-defined]
432+
sub_report = SubtestReport._from_test_report(
433+
report,
434+
SubtestContext(msg=msg, kwargs=dict(test.params)), # type: ignore[attr-defined]
435+
)
433436
self.ihook.pytest_runtest_logreport(report=sub_report)
434437
if check_interactive_exception(call_info, sub_report):
435438
self.ihook.pytest_exception_interact(

testing/test_subtests.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ def test_subtests_and_parametrization(pytester: pytest.Pytester) -> None:
220220
@pytest.mark.parametrize("x", [0, 1])
221221
def test_foo(subtests, x):
222222
for i in range(3):
223-
with subtests.test(msg="custom", i=i):
223+
with subtests.test("custom", i=i):
224224
assert i % 2 == 0
225225
assert x == 0
226226
"""

0 commit comments

Comments
 (0)