Skip to content

Commit 741314a

Browse files
nicoddemuspatchback[bot]
authored andcommitted
Fix quadratic-time behavior when handling unittest subtests in Python 3.10 (#13993)
Fix #13965 Close #13970 (cherry picked from commit 342dba6)
1 parent 73d9b01 commit 741314a

File tree

2 files changed

+32
-17
lines changed

2 files changed

+32
-17
lines changed

changelog/13965.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed quadratic-time behavior when handling ``unittest`` subtests in Python 3.10.

src/_pytest/unittest.py

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,10 @@ def setup(self) -> None:
225225
# A bound method to be called during teardown() if set (see 'runtest()').
226226
self._explicit_tearDown: Callable[[], None] | None = None
227227
super().setup()
228+
if sys.version_info < (3, 11):
229+
# A cache of the subTest errors and non-subtest skips in self._outcome.
230+
# Compute and cache these lists once, instead of computing them again and again for each subtest (#13965).
231+
self._cached_errors_and_skips: tuple[list[Any], list[Any]] | None = None
228232

229233
def teardown(self) -> None:
230234
if self._explicit_tearDown is not None:
@@ -313,11 +317,7 @@ def add_skip() -> None:
313317
# We also need to check if `self.instance._outcome` is `None` (this happens if the test
314318
# class/method is decorated with `unittest.skip`, see pytest-dev/pytest-subtests#173).
315319
if sys.version_info < (3, 11) and self.instance._outcome is not None:
316-
subtest_errors = [
317-
x
318-
for x, y in self.instance._outcome.errors
319-
if isinstance(x, _SubTest) and y is not None
320-
]
320+
subtest_errors, _ = self._obtain_errors_and_skips()
321321
if len(subtest_errors) == 0:
322322
add_skip()
323323
else:
@@ -443,18 +443,8 @@ def addSubTest(
443443

444444
# For python < 3.11: add non-subtest skips once all subtest failures are processed by # `_addSubTest`.
445445
if sys.version_info < (3, 11):
446-
from unittest.case import _SubTest # type: ignore[attr-defined]
447-
448-
non_subtest_skip = [
449-
(x, y)
450-
for x, y in self.instance._outcome.skipped
451-
if not isinstance(x, _SubTest)
452-
]
453-
subtest_errors = [
454-
(x, y)
455-
for x, y in self.instance._outcome.errors
456-
if isinstance(x, _SubTest) and y is not None
457-
]
446+
subtest_errors, non_subtest_skip = self._obtain_errors_and_skips()
447+
458448
# Check if we have non-subtest skips: if there are also sub failures, non-subtest skips are not treated in
459449
# `_addSubTest` and have to be added using `add_skip` after all subtest failures are processed.
460450
if len(non_subtest_skip) > 0 and len(subtest_errors) > 0:
@@ -465,6 +455,30 @@ def addSubTest(
465455
for testcase, reason in non_subtest_skip:
466456
self.addSkip(testcase, reason, handle_subtests=False)
467457

458+
def _obtain_errors_and_skips(self) -> tuple[list[Any], list[Any]]:
459+
"""Compute or obtain the cached values for subtest errors and non-subtest skips."""
460+
from unittest.case import _SubTest # type: ignore[attr-defined]
461+
462+
assert sys.version_info < (3, 11), (
463+
"This workaround only should be used in Python 3.10"
464+
)
465+
if self._cached_errors_and_skips is not None:
466+
return self._cached_errors_and_skips
467+
468+
subtest_errors = [
469+
(x, y)
470+
for x, y in self.instance._outcome.errors
471+
if isinstance(x, _SubTest) and y is not None
472+
]
473+
474+
non_subtest_skips = [
475+
(x, y)
476+
for x, y in self.instance._outcome.skipped
477+
if not isinstance(x, _SubTest)
478+
]
479+
self._cached_errors_and_skips = (subtest_errors, non_subtest_skips)
480+
return subtest_errors, non_subtest_skips
481+
468482

469483
@hookimpl(tryfirst=True)
470484
def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> None:

0 commit comments

Comments
 (0)