Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,7 @@ Raquel Alegre
Ravi Chandra
Reagan Lee
Reilly Brogan
rinopatrick
Rob Arrow
Robert Holt
Roberto Aldera
Expand Down
1 change: 1 addition & 0 deletions changelog/15281.bugfix.rst
Original file line number Diff line number Diff line change
@@ -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.
7 changes: 5 additions & 2 deletions src/_pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
30 changes: 30 additions & 0 deletions testing/python/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -6294,6 +6294,36 @@ 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 typing import cast
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)
# `_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 == []


@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."""
Expand Down
Loading