From 61b24c978df2e93574721b594df107de094a320f Mon Sep 17 00:00:00 2001 From: Patrick Date: Sun, 1 Nov 2026 23:29:58 +0700 Subject: [PATCH 1/4] fix(fixtures): skip mock objects with _mock_name None in wrapped lookup unittest.mock.call is a _Call whose _mock_name is None and whose __wrapped__ returns a new object on every access, so inspect.unwrap only stopped at the recursion limit (slow collection + huge memory with a raised limit). Treat any object with a _mock_name attribute as a mock. Fixes pytest-dev/pytest#15281 --- src/_pytest/fixtures.py | 7 +++++-- testing/python/fixtures.py | 26 ++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index e3d6300b599..3e873411401 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -2166,8 +2166,11 @@ def _find_wrapped_fixture_def( and objects that raise on attribute access are treated as not found. """ # Skip mock objects to avoid false positives when traversing - # their wrapper chains (they have a _mock_name attribute). - if safe_getattr(obj, "_mock_name", None) is not None: + # their wrapper chains. Any object with a `_mock_name` attribute is + # a mock — including `unittest.mock.call`, whose `_mock_name` is None + # and whose `__wrapped__` returns a new object on every access, so + # unwrapping it only stops at the recursion limit (see pytest#15281). + if safe_getattr(obj, "_mock_name", NOTSET) is not NOTSET: return None try: diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index cb2f1daab63..04dda1ea933 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -6294,6 +6294,32 @@ def test_fixt(self, fixt): result.assert_outcomes(passed=1) +def test_find_wrapped_fixture_def_skips_mock_call_without_unwrap( + monkeypatch: MonkeyPatch, +) -> None: + """`unittest.mock.call` must be skipped before `inspect.unwrap` (see #15281). + + ``call`` is a ``_Call`` whose ``_mock_name`` is ``None`` and whose + ``__wrapped__`` returns a new ``_Call`` on every access, so unwrapping it + only stops at the recursion limit (slow + huge memory with a raised limit). + """ + import inspect + from unittest.mock import call + + from _pytest.fixtures import FixtureManager + + calls = [] + real_unwrap = inspect.unwrap + + def counting_unwrap(*args, **kwargs): + calls.append(args) + return real_unwrap(*args, **kwargs) + + monkeypatch.setattr(inspect, "unwrap", counting_unwrap) + assert FixtureManager._find_wrapped_fixture_def(None, call) is None + assert calls == [] + + @pytest.mark.filterwarnings("default:cannot discover fixture *:pytest.PytestWarning") def test_classmethod_above_fixture_warning_inherited(pytester: Pytester) -> None: """MRO ``__dict__`` lookup finds @classmethod wrappers on a base class.""" From afbf69573041b6cc159d9371af609b47cefd18d4 Mon Sep 17 00:00:00 2001 From: Patrick Date: Sun, 1 Nov 2026 23:30:16 +0700 Subject: [PATCH 2/4] docs(changelog): add newsfragment for 15281 --- changelog/15281.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/15281.bugfix.rst diff --git a/changelog/15281.bugfix.rst b/changelog/15281.bugfix.rst new file mode 100644 index 00000000000..6d103524caf --- /dev/null +++ b/changelog/15281.bugfix.rst @@ -0,0 +1 @@ +Fixed collection hanging and using gigabytes of memory when a collected module contains ``unittest.mock.call`` (or similar mock objects) at module level while the recursion limit is raised: mock objects are now skipped before traversing their ``__wrapped__`` chain. From c4f1e56b89d2a513eb95935f9e2befaa094389da Mon Sep 17 00:00:00 2001 From: Patrick Date: Sun, 1 Nov 2026 23:32:22 +0700 Subject: [PATCH 3/4] docs(authors): add rinopatrick --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 136ffd6c5ee..aa9ce45e90a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -426,6 +426,7 @@ Raquel Alegre Ravi Chandra Reagan Lee Reilly Brogan +rinopatrick Rob Arrow Robert Holt Roberto Aldera From bf4bc53e5626956d8f4d91b2bc4fa1b92ebb3ce0 Mon Sep 17 00:00:00 2001 From: Patrick Date: Sun, 1 Nov 2026 23:35:05 +0700 Subject: [PATCH 4/4] fix(test): call wrapped fixture lookup unbound with dummy manager --- testing/python/fixtures.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 04dda1ea933..ac53adcab0e 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -6304,6 +6304,7 @@ def test_find_wrapped_fixture_def_skips_mock_call_without_unwrap( only stops at the recursion limit (slow + huge memory with a raised limit). """ import inspect + from typing import cast from unittest.mock import call from _pytest.fixtures import FixtureManager @@ -6316,7 +6317,10 @@ def counting_unwrap(*args, **kwargs): return real_unwrap(*args, **kwargs) monkeypatch.setattr(inspect, "unwrap", counting_unwrap) - assert FixtureManager._find_wrapped_fixture_def(None, call) is None + # `_find_wrapped_fixture_def` does not use `self`; call it on the class + # with a dummy instance to avoid constructing a full `FixtureManager`. + manager = cast(FixtureManager, object()) + assert FixtureManager._find_wrapped_fixture_def(manager, call) is None assert calls == []