Repository navigation
tmpdir: hand out numbered dirs from a per-prefix counter - #15255
Open
RonnyPfannschmidt wants to merge 6 commits into
Open
RonnyPfannschmidt wants to merge 6 commits into
RonnyPfannschmidt wants to merge 6 commits into
Conversation
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>
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
approved these changes
Oct 24, 2026
nicoddemus
left a comment
Member
There was a problem hiding this comment.
Looks good, it seems this fixes the original issue, but left a comment for your consideration to further improve things.
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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()andtmp_pathusedmake_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*currentsymlink per test in the issue's reproducer), so a session does O(N²) work. At N=3000, profiling put 41 of 54 s infind_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 ittmp_pathand pytester, counts from 0 without listing basetemp. The factory always creates basetemp itself (a freshpytest-N, or a--basetempit has just wiped).mkdirdecides uniqueness. A name it fails on (a directory, file or dangling symlink already there, or aPermissionErroron 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, anOSErrorchained 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 (thepytest-of-<user>session root) are unchanged.Behaviour differences
mktempnumbers 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.tmp_pathretention removal could be reused.X2no longer pushesxpast 2, becausemkdirdecides 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_aandtest_a1both producetest_a10), the probing limit, and retries on otherOSErrors.Numbers
Reproducer from the issue (
pytest -q -p no:cacheprovider, noisy cloud box, wall-clock):Run time now grows linearly. What remains is mkdir/symlink syscalls.
Checklist
closes #XYZWto the PR description and/or commits.Co-authored-bycommit trailers.changelogdirectory.Full test suite passed locally (4662 passed). pre-commit is clean.