Repository navigation
Description
A version-manager shim cached as an unsupported Python in one directory remains rejected in another directory where it selects a supported Python.
Affected code
src/python_discovery/_cached_py_info.py:133 at the reported commit. The reproducer also returns BUG on current main 004294957ed227fe7e52b2864952ba09fd28b220.
Mechanism
The DiskCache entry is keyed by the shim path although its target changes with the current directory's version selection.
Reproduction
Check out the reported commit or current main. Save the files below as named, then build and run:
docker build -t python-discovery-audit -f - . <<'DOCKERFILE'
FROM python:3.15-bookworm
RUN python -m venv /opt/venv
ENV PATH=/opt/venv/bin:$PATH
COPY . /src
WORKDIR /src
ENV SETUPTOOLS_SCM_PRETEND_VERSION=1.9.1
RUN pip install -e . --group test
DOCKERFILE
docker run --rm --network=none --memory=2g -u 0 -w /src \
-v "$PWD/repro:/tmp/repro:ro" -v "$PWD/repro.d:/tmp/repro.d:ro" \
python-discovery-audit /tmp/reprorepro
#!/usr/bin/env python3
import json
import shutil
import subprocess
from pathlib import Path
scene = Path("/tmp/shim-verdict-repro")
home = scene / "home"
pyenv_root = home / ".pyenv"
shim = pyenv_root / "shims" / "python3"
shim_log = pyenv_root / "shim.log"
user_cache = home / ".local" / "share" / "virtualenv"
project = scene / "project"
checkout = scene / "untrusted_checkout"
def install_executable(source, destination):
destination.parent.mkdir(parents=True, exist_ok=True)
shutil.copy(source, destination)
destination.chmod(0o755)
def find_interpreter(cwd, cache_dir):
env = {"PATH": f"{shim.parent}:/usr/bin", "HOME": str(home), "PYENV_ROOT": str(pyenv_root)}
tool = ["/opt/venv/bin/python", "/tmp/repro.d/find_interpreter.py", str(shim), str(cache_dir)]
return subprocess.run(tool, cwd=cwd, env=env, stdout=subprocess.PIPE, text=True, check=True).stdout.strip()
# set up: pyenv with CPython 3.11.2 and 3.5.10 installed, and the user's project pinned to 3.11.2
shutil.rmtree(scene, ignore_errors=True)
install_executable("/tmp/repro.d/pyenv_shim.py", shim)
(pyenv_root / "versions" / "3.11.2" / "bin").mkdir(parents=True)
(pyenv_root / "versions" / "3.11.2" / "bin" / "python3").symlink_to("/usr/bin/python3.11")
install_executable("/tmp/repro.d/python3_5_stand_in.py", pyenv_root / "versions" / "3.5.10" / "bin" / "python3")
project.mkdir()
(project / ".python-version").write_text("3.11.2\n")
# control: from the project, with a fresh cache, the shim is Python 3.11.2
control = find_interpreter(project, scene / "fresh_cache")
assert control == "Python 3.11.2 (/usr/bin/python3.11)", control
# trigger: the checkout's author controls only this file; the shim is probed once from there
checkout.mkdir()
(checkout / ".python-version").write_text("3.5.10\n")
in_checkout = find_interpreter(checkout, user_cache)
assert "is Python 3.5.10, older than the minimum 3.6" in in_checkout, in_checkout
# the control's call again, in the project, now with the user's cache
shim_log.write_text("")
in_project = find_interpreter(project, user_cache)
shim_runs_in_project = shim_log.read_text()
# check: in the project the shim runs 3.11.2, so the answer must be the control's, not the checkout's
print("project, fresh cache :", control)
print("untrusted_checkout, user cache :", in_checkout)
print("project, user cache :", in_project)
print("shim runs during that last call :", shim_runs_in_project.strip() or "none")
print(
"user cache :",
[json.loads(entry.read_text()) for entry in user_cache.glob("py_info/*/*.json")],
)
print("BUG" if in_project == in_checkout and not shim_runs_in_project else "OK")repro.d/find_interpreter.py
"""A tool built on python-discovery: look up the interpreter at a given path, using the user's on-disk cache."""
import sys
from pathlib import Path
from python_discovery import DiskCache, get_interpreter
interpreter_path, cache_dir = sys.argv[1:]
try:
info = get_interpreter(interpreter_path, cache=DiskCache(Path(cache_dir)))
except RuntimeError as error: # documented for an absolute path to a Python older than 3.6
print(f"RuntimeError: {error}")
else:
print(f"Python {info.version_str} ({info.system_executable})")repro.d/pyenv_shim.py
#!/usr/bin/python3.11 -I
"""Stand-in for a pyenv shim: exec the same command from the version pinned by the nearest .python-version."""
import os
import sys
directory = os.getcwd()
while not os.path.isfile(os.path.join(directory, ".python-version")):
if directory == os.path.dirname(directory):
sys.exit("pyenv: no .python-version found")
directory = os.path.dirname(directory)
with open(os.path.join(directory, ".python-version")) as file:
version = file.read().strip()
pyenv_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
target = os.path.join(pyenv_root, "versions", version, "bin", os.path.basename(__file__))
# not something pyenv does: lets the reproducer see each time the shim is actually run
with open(os.path.join(pyenv_root, "shim.log"), "a") as log:
log.write(f"run from {os.getcwd()} -> {target}\n")
os.execv(target, [target, *sys.argv[1:]])repro.d/python3_5_stand_in.py
#!/usr/bin/python3.11 -I
"""Stand-in for an installed CPython 3.5.10, which this image does not have: run the given script as if on 3.5.10."""
import runpy
import sys
# the version gate at the top of python-discovery's _py_info_collect.py reads only this
sys.version_info = (3, 5, 10, "final", 0)
runpy.run_path(sys.argv[1], run_name="__main__")Output on 004294957ed227fe7e52b2864952ba09fd28b220:
/tmp/shim-verdict-repro/home/.pyenv/shims/python3 is Python 3.5.10, older than the minimum 3.6 python-discovery can query
project, fresh cache : Python 3.11.2 (/usr/bin/python3.11)
untrusted_checkout, user cache : RuntimeError: /tmp/shim-verdict-repro/home/.pyenv/shims/python3 is Python 3.5.10, older than the minimum 3.6 python-discovery can query
project, user cache : RuntimeError: /tmp/shim-verdict-repro/home/.pyenv/shims/python3 is Python 3.5.10, older than the minimum 3.6 python-discovery can query
shim runs during that last call : none
user cache : [{'arguments': None, 'hash': '93d3f8273364bd6f62c37eed79ebbb2fa636ef2b564e7bd688ae3b077826e718', 'path': '/tmp/shim-verdict-repro/home/.pyenv/shims/python3', 'st_mtime': 1793658667.5352418, 'unsupported': '/tmp/shim-verdict-repro/home/.pyenv/shims/python3 is Python 3.5.10, older than the minimum 3.6 python-discovery can query'}]
BUG
Expected: A shim should be probed again when its selected version changes with the directory.
Impact
Reproducibility is moderate because the setup requires a specific candidate or configuration. Exposure is niche: a pyenv, asdf, or mise shim must select different versions across directories while sharing a cache. The reproduced effect stays within the caller's configured cache, search path, or interpreter.
Credits
Reported by Anthropic's OSS Scanner: ANT-2026-HK25S1M7.
Activity