Skip to content

Bound wrapped-fixture unwrapping independently of the recursion limit - #15283

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

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

Conversation

@RonnyPfannschmidt

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) used 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. On main the reproducer from the issue gets OOM-killed.

Changes:

  • The mock guard uses NOTSET as the sentinel instead of None, so any object that has a _mock_name attribute is skipped, mock.call included.
  • __wrapped__ is walked by a local loop capped at 100 steps (_MAX_UNWRAP_DEPTH), with id-based loop detection. This makes the cost independent of the recursion limit for other proxies that grow a fresh __wrapped__ on every access.

Tests:

  • An end-to-end subprocess test using the issue's reproducer.
  • A unit test that checks the depth cap on an endless __wrapped__ chain.
  • A unit test that checks mock.call and Mock() are skipped.

The first two fail without the fix. The full suite and pre-commit pass locally.

  • 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 by Claude Code

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.

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