99from contextlib import ExitStack
1010from contextlib import nullcontext
1111import dataclasses
12- import sys
1312import time
1413from typing import Any
1514from typing import TYPE_CHECKING
16- from unittest import TestCase
1715
1816import pluggy
1917
3129from _pytest .reports import TestReport
3230from _pytest .runner import CallInfo
3331from _pytest .runner import check_interactive_exception
34- from _pytest .unittest import TestCaseFunction
35- from _pytest .warning_types import PytestDeprecationWarning
3632
3733
3834if TYPE_CHECKING :
@@ -60,12 +56,14 @@ def pytest_addoption(parser: Parser) -> None:
6056
6157@dataclasses .dataclass
6258class SubTestContext :
59+ """The values passed to SubTests.test() that are included in the test report."""
60+
6361 msg : str | None
6462 kwargs : dict [str , Any ]
6563
6664
6765@dataclasses .dataclass (init = False )
68- class SubTestReport (TestReport ): # type: ignore[misc]
66+ class SubTestReport (TestReport ):
6967 context : SubTestContext
7068
7169 @property
@@ -105,122 +103,6 @@ def _from_test_report(cls, test_report: TestReport) -> SubTestReport:
105103 return super ()._from_json (test_report ._to_json ())
106104
107105
108- def _addSkip (self : TestCaseFunction , testcase : TestCase , reason : str ) -> None :
109- from unittest .case import _SubTest # type: ignore[attr-defined]
110-
111- if isinstance (testcase , _SubTest ):
112- self ._originaladdSkip (testcase , reason ) # type: ignore[attr-defined]
113- if self ._excinfo is not None :
114- exc_info = self ._excinfo [- 1 ]
115- self .addSubTest (testcase .test_case , testcase , exc_info ) # type: ignore[attr-defined]
116- else :
117- # For python < 3.11: the non-subtest skips have to be added by `_originaladdSkip` only after all subtest
118- # failures are processed by `_addSubTest`. (`self.instance._outcome` has no attribute `skipped/errors` anymore.)
119- # For python < 3.11, we also need to check if `self.instance._outcome` is `None` (this happens if the test
120- # class/method is decorated with `unittest.skip`, see #173).
121- if sys .version_info < (3 , 11 ) and self .instance ._outcome is not None :
122- subtest_errors = [
123- x
124- for x , y in self .instance ._outcome .errors
125- if isinstance (x , _SubTest ) and y is not None
126- ]
127- if len (subtest_errors ) == 0 :
128- self ._originaladdSkip (testcase , reason ) # type: ignore[attr-defined]
129- else :
130- self ._originaladdSkip (testcase , reason ) # type: ignore[attr-defined]
131-
132-
133- def _addSubTest (
134- self : TestCaseFunction ,
135- test_case : Any ,
136- test : TestCase ,
137- exc_info : tuple [type [BaseException ], BaseException , TracebackType ] | None ,
138- ) -> None :
139- msg = test ._message if isinstance (test ._message , str ) else None # type: ignore[attr-defined]
140- call_info = make_call_info (
141- ExceptionInfo (exc_info , _ispytest = True ) if exc_info else None ,
142- start = 0 ,
143- stop = 0 ,
144- duration = 0 ,
145- when = "call" ,
146- )
147- report = self .ihook .pytest_runtest_makereport (item = self , call = call_info )
148- sub_report = SubTestReport ._from_test_report (report )
149- sub_report .context = SubTestContext (msg , dict (test .params )) # type: ignore[attr-defined]
150- self .ihook .pytest_runtest_logreport (report = sub_report )
151- if check_interactive_exception (call_info , sub_report ):
152- self .ihook .pytest_exception_interact (
153- node = self , call = call_info , report = sub_report
154- )
155-
156- # For python < 3.11: add non-subtest skips once all subtest failures are processed by # `_addSubTest`.
157- if sys .version_info < (3 , 11 ):
158- from unittest .case import _SubTest # type: ignore[attr-defined]
159-
160- non_subtest_skip = [
161- (x , y )
162- for x , y in self .instance ._outcome .skipped
163- if not isinstance (x , _SubTest )
164- ]
165- subtest_errors = [
166- (x , y )
167- for x , y in self .instance ._outcome .errors
168- if isinstance (x , _SubTest ) and y is not None
169- ]
170- # Check if we have non-subtest skips: if there are also sub failures, non-subtest skips are not treated in
171- # `_addSubTest` and have to be added using `_originaladdSkip` after all subtest failures are processed.
172- if len (non_subtest_skip ) > 0 and len (subtest_errors ) > 0 :
173- # Make sure we have processed the last subtest failure
174- last_subset_error = subtest_errors [- 1 ]
175- if exc_info is last_subset_error [- 1 ]:
176- # Add non-subtest skips (as they could not be treated in `_addSkip`)
177- for testcase , reason in non_subtest_skip :
178- self ._originaladdSkip (testcase , reason ) # type: ignore[attr-defined]
179-
180-
181- def pytest_configure (config : Config ) -> None :
182- TestCaseFunction .addSubTest = _addSubTest # type: ignore[attr-defined]
183- TestCaseFunction .failfast = False # type: ignore[attr-defined]
184- # This condition is to prevent `TestCaseFunction._originaladdSkip` being assigned again in a subprocess from a
185- # parent python process where `addSkip` is already `_addSkip`. A such case is when running tests in
186- # `test_subtests.py` where `pytester.runpytest` is used. Without this guard condition, `_originaladdSkip` is
187- # assigned to `_addSkip` which is wrong as well as causing an infinite recursion in some cases.
188- if not hasattr (TestCaseFunction , "_originaladdSkip" ):
189- TestCaseFunction ._originaladdSkip = TestCaseFunction .addSkip # type: ignore[attr-defined]
190- TestCaseFunction .addSkip = _addSkip # type: ignore[method-assign]
191-
192- # Hack (#86): the terminal does not know about the "subtests"
193- # status, so it will by default turn the output to yellow.
194- # This forcibly adds the new 'subtests' status.
195- import _pytest .terminal
196-
197- new_types = tuple (
198- f"subtests { outcome } " for outcome in ("passed" , "failed" , "skipped" )
199- )
200- # We need to check if we are not re-adding because we run our own tests
201- # with pytester in-process mode, so this will be called multiple times.
202- if new_types [0 ] not in _pytest .terminal .KNOWN_TYPES :
203- _pytest .terminal .KNOWN_TYPES = _pytest .terminal .KNOWN_TYPES + new_types # type: ignore[assignment]
204-
205- _pytest .terminal ._color_for_type .update (
206- {
207- f"subtests { outcome } " : _pytest .terminal ._color_for_type [outcome ]
208- for outcome in ("passed" , "failed" , "skipped" )
209- if outcome in _pytest .terminal ._color_for_type
210- }
211- )
212-
213-
214- def pytest_unconfigure () -> None :
215- if hasattr (TestCaseFunction , "addSubTest" ):
216- del TestCaseFunction .addSubTest
217- if hasattr (TestCaseFunction , "failfast" ):
218- del TestCaseFunction .failfast
219- if hasattr (TestCaseFunction , "_originaladdSkip" ):
220- TestCaseFunction .addSkip = TestCaseFunction ._originaladdSkip # type: ignore[method-assign]
221- del TestCaseFunction ._originaladdSkip
222-
223-
224106@fixture
225107def subtests (request : SubRequest ) -> Generator [SubTests , None , None ]:
226108 """Provides subtests functionality."""
@@ -293,7 +175,7 @@ class _SubTestContextManager:
293175
294176 Note: initially this logic was implemented directly in SubTests.test() as a @contextmanager, however
295177 it is not possible to control the output fully when exiting from it due to an exception when
296- in --exitfirst mode, so this was refactored into an explicit context manager class (#134).
178+ in --exitfirst mode, so this was refactored into an explicit context manager class (pytest-dev/pytest-subtests #134).
297179 """
298180
299181 ihook : pluggy .HookRelay
@@ -390,11 +272,9 @@ def capturing_output(request: SubRequest) -> Iterator[Captured]:
390272 capture_fixture_active = getattr (capman , "_capture_fixture" , None )
391273
392274 if option == "sys" and not capture_fixture_active :
393- with ignore_pytest_private_warning ():
394- fixture = CaptureFixture (SysCapture , request )
275+ fixture = CaptureFixture (SysCapture , request , _ispytest = True )
395276 elif option == "fd" and not capture_fixture_active :
396- with ignore_pytest_private_warning ():
397- fixture = CaptureFixture (FDCapture , request )
277+ fixture = CaptureFixture (FDCapture , request , _ispytest = True )
398278 else :
399279 fixture = None
400280
@@ -428,20 +308,7 @@ def capturing_logs(
428308 yield captured_logs
429309
430310
431- @contextmanager
432- def ignore_pytest_private_warning () -> Generator [None , None , None ]:
433- import warnings
434-
435- with warnings .catch_warnings ():
436- warnings .filterwarnings (
437- "ignore" ,
438- "A private pytest class or function was used." ,
439- category = PytestDeprecationWarning ,
440- )
441- yield
442-
443-
444- @dataclasses .dataclass ()
311+ @dataclasses .dataclass
445312class Captured :
446313 out : str = ""
447314 err : str = ""
@@ -453,12 +320,12 @@ def update_report(self, report: TestReport) -> None:
453320 report .sections .append (("Captured stderr call" , self .err ))
454321
455322
323+ @dataclasses .dataclass
456324class CapturedLogs :
457- def __init__ (self , handler : LogCaptureHandler ) -> None :
458- self ._handler = handler
325+ handler : LogCaptureHandler
459326
460327 def update_report (self , report : TestReport ) -> None :
461- report .sections .append (("Captured log call" , self ._handler .stream .getvalue ()))
328+ report .sections .append (("Captured log call" , self .handler .stream .getvalue ()))
462329
463330
464331class NullCapturedLogs :
0 commit comments