Skip to content

uv store filtering rejects a matching patch release #195

Closed

Description

@gaborbernat

A spec such as ==3.11 can miss a uv-managed Python 3.11.2, although the same interpreter is accepted from PATH.

Affected code

src/python_discovery/_discovery.py:378 at the reported commit. The reproducer also returns BUG on current main 004294957ed227fe7e52b2864952ba09fd28b220.

Mechanism

The uv directory-name filter compares the full patch version before PythonInfo.satisfies() applies the spec's minor-version semantics.

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/repro

repro

#!/usr/bin/env python3
import json
import os
import subprocess
import tempfile


def find_interpreter(spec, path, uv_python_install_dir):
    # a fresh process per call: python-discovery caches interpreter info in memory
    caller = ["/opt/venv/bin/python", "/tmp/repro.d/find_interpreter.py", spec, path, uv_python_install_dir]
    result = json.loads(subprocess.run(caller, check=True, capture_output=True, text=True).stdout)
    print(f"{spec!r:14} -> {result['version']}  {result['executable']}")
    print(f"{'':14}    spawned: {result['spawned']}")
    return result


# set up: the image's CPython 3.11.2, reachable once through PATH and once as a uv-managed install
scene = tempfile.mkdtemp()
path_dir = os.path.join(scene, "path")
uv_dir = os.path.join(scene, "uv")
empty_dir = os.path.join(scene, "empty")
uv_python = os.path.join(uv_dir, "cpython-3.11.2-linux-x86_64-gnu", "bin", "python")
os.makedirs(path_dir)
os.makedirs(os.path.dirname(uv_python))
os.makedirs(empty_dir)
os.symlink("/usr/bin/python3.11", os.path.join(path_dir, "python3.11"))
os.symlink("/usr/bin/python3.11", uv_python)

# the caller itself runs on 3.15, which no spec below accepts, so any answer comes from PATH or the uv dir
print("uv dir, spec with both bounds written out (control):")
control = find_interpreter(">=3.11,<3.12", path=empty_dir, uv_python_install_dir=uv_dir)
assert control["executable"] == uv_python, "setup: the uv install was not found at all"

# trigger: the same spec, the same interpreter, two sources
print("PATH only:")
from_path = find_interpreter("==3.11", path=path_dir, uv_python_install_dir=empty_dir)
print("uv dir only:")
from_uv = find_interpreter("==3.11", path=empty_dir, uv_python_install_dir=uv_dir)

# check: '==3.11' accepts 3.11.2 from PATH, so it must accept it from the uv dir too
accepted_from_path = from_path["version"] == "3.11.2"
dropped_unrun_from_uv = from_uv["version"] is None and uv_python not in from_uv["spawned"]
print("BUG" if accepted_from_path and dropped_unrun_from_uv else "OK")

repro.d/find_interpreter.py

import json
import sys

from python_discovery import get_interpreter

spec, path, uv_python_install_dir = sys.argv[1:]

spawned = []
sys.addaudithook(lambda event, args: spawned.append(args[0]) if event == "subprocess.Popen" else None)

info = get_interpreter(spec, env={"PATH": path, "UV_PYTHON_INSTALL_DIR": uv_python_install_dir})

print(
    json.dumps({
        "version": None if info is None else "{}.{}.{}".format(*info.version_info),
        "executable": None if info is None else info.executable,
        "spawned": spawned,
    })
)

Output on 004294957ed227fe7e52b2864952ba09fd28b220:

uv dir, spec with both bounds written out (control):
'>=3.11,<3.12' -> 3.11.2  /tmp/tmpt2grrx0q/uv/cpython-3.11.2-linux-x86_64-gnu/bin/python
                  spawned: ['/usr/local/bin/python3.15', '/tmp/tmpt2grrx0q/uv/cpython-3.11.2-linux-x86_64-gnu/bin/python', '/usr/bin/python3.11']
PATH only:
'==3.11'       -> 3.11.2  /tmp/tmpt2grrx0q/path/python3.11
                  spawned: ['/usr/local/bin/python3.15', '/tmp/tmpt2grrx0q/path/python3.11', '/usr/bin/python3.11']
uv dir only:
'==3.11'       -> None  None
                  spawned: ['/usr/local/bin/python3.15']
BUG

Expected: The uv candidate should reach the same spec check used for PATH candidates.

Impact

Reproducibility is moderate because the setup requires a specific candidate or configuration. Exposure is niche: the matching interpreter must be in the uv store and the spec must use a two-part bound. The reproduced effect stays within the caller's configured cache, search path, or interpreter.

Credits

Reported by Anthropic's OSS Scanner: ANT-2026-IQ3HSNNR.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions