Skip to content

tmpdir: hand out numbered dirs from a per-prefix counter - #15255

Open
RonnyPfannschmidt wants to merge 6 commits into
mainfrom
claude/project-thread-eoi3d6
Open

RonnyPfannschmidt wants to merge 6 commits into
mainfrom
claude/project-thread-eoi3d6

Conversation

@RonnyPfannschmidt

@RonnyPfannschmidt RonnyPfannschmidt commented Oct 24, 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 #15254.

TempPathFactory.mktemp() and tmp_path used make_numbered_dir(), which lists the whole basetemp on every call to find the next free number. basetemp gains entries with every test (two directories and a *current symlink per test in the issue's reproducer), so a session does O(N²) work. At N=3000, profiling put 41 of 54 s in find_prefixed.

This PR is a simpler alternative to the stat-validated listing cache proposed in the issue. The factory keeps a per-prefix counter (_next_number, keyed by the lower-cased prefix):

  • mktemp(numbered=True), and through it tmp_path and pytester, counts from 0 without listing basetemp. The factory always creates basetemp itself (a fresh pytest-N, or a --basetemp it has just wiped).
  • mkdir decides uniqueness. A name it fails on (a directory, file or dangling symlink already there, or a PermissionError on Windows for a name still being deleted) is skipped. After 10 failures in a row, basetemp is listed once to jump past the run. After 20, an OSError chained to the last error is raised, so a filesystem that reports every name as taken cannot make it loop forever.
  • make_numbered_dir() and its other callers (the pytest-of-<user> session root) are unchanged.

Behaviour differences

  • mktemp numbers are no longer "greater than any existing one". Existing names are skipped instead, and the docstring now says so. With a basetemp the factory owns, that only matters for directories something else creates there.
  • Numbers never go down. With main, a number freed by the tmp_path retention removal could be reused.
  • On a case-sensitive filesystem, a foreign X2 no longer pushes x past 2, because mkdir decides the collision. Main matched the listing case-insensitively.

Covered by tests: no listing in mktemp, unexpected entries (directory, file, dangling symlink), a run of 50 taken names (one listing), overlapping prefixes (test_a and test_a1 both produce test_a10), the probing limit, and retries on other OSErrors.

Numbers

Reproducer from the issue (pytest -q -p no:cacheprovider, noisy cloud box, wall-clock):

tests main this PR
1000 3.9 s ~3–4 s
2000 10.9 s ~3.5 s
4000 37.3 s ~7.5 s

Run time now grows linearly. What remains is mkdir/symlink syscalls.

Checklist

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

Full test suite passed locally (4662 passed). pre-commit is clean.

TempPathFactory.mktemp() and tmp_path listed the whole basetemp on every
call to find the next free number. basetemp gains entries with every test,
so a session did O(N^2) work.

The factory now keeps a per-prefix counter. tmp_path starts at 0 without
listing, as the factory always creates basetemp itself. mktemp() lists
basetemp once per prefix so the number stays greater than any directory
already there. mkdir decides uniqueness: on FileExistsError the counter
moves on. make_numbered_dir() and its other callers are unchanged.

Closes #15254

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@RonnyPfannschmidt RonnyPfannschmidt self-assigned this Oct 24, 2026
Names taken by something else (a directory, a file or a dangling symlink)
are skipped. After 10 consecutive collisions basetemp is listed once to
jump past a long run, and after 20 an OSError is raised instead of looping
forever on a filesystem that reports every name as taken.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@nicoddemus nicoddemus left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, it seems this fixes the original issue, but left a comment for your consideration to further improve things.

Comment thread src/_pytest/tmpdir.py Outdated
Comment thread src/_pytest/tmpdir.py Outdated
On Windows mkdir can raise PermissionError for a name that is still being
deleted. Suggested in review.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
pytester calls mktemp() with a different prefix for every test, so listing
basetemp on the first use of each prefix kept the quadratic cost there.
The factory owns basetemp; mkdir already skips names taken by anything
else, so mktemp now counts from 0 like tmp_path. Suggested in review.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tmp_path / tmp_path_factory.mktemp list the whole basetemp on every call: O(N^2) over a session

3 participants