Skip to content

Stop wrapped-fixture unwrapping at mock-like objects - #15283

Open
RonnyPfannschmidt wants to merge 4 commits into
mainfrom
claude/project-thread-twy9z6
Open

RonnyPfannschmidt wants to merge 4 commits into
mainfrom
claude/project-thread-twy9z6

Conversation

@RonnyPfannschmidt

@RonnyPfannschmidt RonnyPfannschmidt commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

Requested by Ronny · project thread

🤖 Written by Claude Opus 5.5 via Claude Code for the pytest maintainers; I prompted it, it did the work, I read it.

closes #15281

_find_wrapped_fixture_def (added in #13745) calls inspect.unwrap, whose depth limit is sys.getrecursionlimit(). unittest.mock.call returns a new _Call for every attribute, including __wrapped__, and it got past the mock guard because its _mock_name is None. Once a conftest or library raised the recursion limit (to 100 000, for example), collection followed that chain until memory ran out.

Changes (after review, see f68dbb4):

  • The inspect.unwrap call now also stops at mock-like objects, via the _is_mocked check that doctest.py already uses for the same mock.call problem. This replaces the _mock_name guard, which missed mock.call.
  • _is_mocked moved from _pytest.doctest to _pytest.compat so both modules can share it.

The first commits replaced inspect.unwrap with a capped hand-written loop. The later commit drops that loop in favour of the smaller change above.

Tests:

  • An end-to-end subprocess test using the issue's reproducer, which fails without the fix.
  • A unit test that mock.call and Mock() are not unwrapped.

A related, pre-existing case under --doctest-modules is tracked separately.

  • Include new tests or update existing tests when applicable.
  • Allow maintainers to push and squash when merging my commits.
  • If AI agents were used, they are credited in Co-authored-by commit trailers.
  • Create a new changelog file in the changelog directory.

🤖 Generated with Claude Code

https://claude.ai/code/session_014GZsRF9tm1CrxGUNz1r4UC

Fixes #15281.

`_find_wrapped_fixture_def` used `inspect.unwrap`, whose depth limit is
`sys.getrecursionlimit()`. `unittest.mock.call` returns a new `_Call`
for every attribute, including `__wrapped__`, and slipped through the
mock guard because its `_mock_name` is `None`. With a raised recursion
limit, collection walked that chain until it exhausted memory.

- Treat any `_mock_name` attribute as a mock by using `NOTSET` as the
  sentinel instead of `None`.
- Walk `__wrapped__` with a local loop capped at 100 steps, with
  id-based loop detection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GZsRF9tm1CrxGUNz1r4UC
@RonnyPfannschmidt RonnyPfannschmidt self-assigned this Oct 28, 2026
@pyba-chronographer pyba-chronographer Bot added the bot:chronographer:provided (automation) changelog entry is part of PR label Oct 28, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GZsRF9tm1CrxGUNz1r4UC
@bluetech

Copy link
Copy Markdown
Member

I haven't reviewed 264846d but it seems pretty complex, and this seems to make it even more complex. Have you considered reverting the change instead? 9.2.0 is still new so I think a revert would be fine.

@RonnyPfannschmidt

Copy link
Copy Markdown
Member Author

i'd prefer keeping - i checking if we can simplify a bit

Review feedback: the hand-rolled __wrapped__ walk added complexity on
top of #13745. Go back to inspect.unwrap and stop it at mock-like
objects instead, reusing the _is_mocked check that doctest already uses
for the same unittest.mock.call problem (moved to compat so fixtures
can share it). This also replaces the _mock_name guard, which missed
mock.call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GZsRF9tm1CrxGUNz1r4UC

Copy link
Copy Markdown
Member Author

🤖 Written by Claude Opus 5.5 via Claude Code for the pytest maintainers; I prompted it, it did the work, I read it.

@bluetech I pushed f68dbb4 as a separate commit so the change is visible. It drops the custom __wrapped__ loop and goes back to plain inspect.unwrap. The unwrap now also stops at mock-like objects, using the _is_mocked check that doctest.py already relies on for the same mock.call problem. That check moved to compat so both modules share it. The old _mock_name guard missed mock.call, so this replaces it too.

In fixtures.py the net change against main is now a few lines. The repro from #15281 collects in about 0.1 s with the recursion limit at 100 000. The regression test still fails without the fix.


Generated by Claude Code

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GZsRF9tm1CrxGUNz1r4UC
@RonnyPfannschmidt RonnyPfannschmidt changed the title Bound wrapped-fixture unwrapping independently of the recursion limit Stop wrapped-fixture unwrapping at mock-like objects Nov 4, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot:chronographer:provided (automation) changelog entry is part of PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9.2.0: collection of a module importing unittest.mock.call uses GBs of memory when recursionlimit is raised (regression from 9.1.1)

3 participants