@@ -6,25 +6,123 @@ Discovery strategy
66
77The :class: `~py_discovery.Builtin ` discover searches for Python interpreters in the following order:
88
9- 1. **Absolute paths ** -- if the spec is an absolute path, try it directly.
10- 2. **``try_first_with`` ** -- explicit paths to try before anything else.
11- 3. **Relative paths ** -- resolve relative to the current directory.
12- 4. **Current interpreter ** -- the running Python itself.
13- 5. **Windows registry ** (PEP 514) -- on Windows, enumerate registered interpreters.
14- 6. **PATH search ** -- walk ``$PATH `` entries, matching filenames against the spec pattern.
15- 7. **Version-manager shims ** -- resolve pyenv, mise, and asdf shims to real binaries.
16- 8. **uv-managed Pythons ** -- check ``UV_PYTHON_INSTALL_DIR ``, ``XDG_DATA_HOME/uv/python ``, or the platform default.
17-
18- Cache sharing
19- -------------
20-
21- ``py-discovery `` uses the same on-disk cache layout as virtualenv (``py_info/4/<sha256>.json ``), so they share
22- cached interpreter metadata. The :class: `~py_discovery.DiskCache ` stores results under a configurable root directory
23- with file locking via ``filelock ``.
24-
25- Protocol design
9+ .. mermaid ::
10+
11+ flowchart TD
12+ Start["Builtin.run()"] --> AbsPath{Is spec an\n absolute path?}
13+ AbsPath -->|Yes| TryAbs["Try path directly"]
14+ AbsPath -->|No| TryFirst["try_first_with paths"]
15+ TryFirst --> RelPath{Is spec a\n relative path?}
16+ RelPath -->|Yes| TryRel["Resolve relative to cwd"]
17+ RelPath -->|No| Current["Current interpreter"]
18+ Current --> Win{Windows?}
19+ Win -->|Yes| PEP514["PEP 514 registry"]
20+ Win -->|No| PATH
21+ PEP514 --> PATH["PATH search"]
22+ PATH --> Shims["Version-manager shims\n (pyenv / mise / asdf)"]
23+ Shims --> UV["uv-managed Pythons"]
24+
25+ TryAbs --> Verify
26+ TryRel --> Verify
27+ UV --> Verify
28+
29+ Verify{{"Verify candidate\n (subprocess call)"}}
30+ Verify -->|Satisfies spec| Cache["Cache & return"]
31+ Verify -->|No match| Next["Try next candidate"]
32+
33+ Each candidate is verified by running it as a subprocess to collect its metadata (version, architecture, platform,
34+ sysconfig paths, etc.). Verified interpreters are cached to avoid repeating this subprocess call.
35+
36+ Shim resolution
2637---------------
2738
28- The cache layer is defined as a :class: `typing.Protocol ` (:class: `~py_discovery.PyInfoCache `), allowing custom
29- backends without inheriting from any base class. The built-in :class: `~py_discovery.DiskCache ` implements this protocol
30- using JSON files and ``filelock ``.
39+ When a spec like ``python3.12 `` resolves to a version-manager shim (e.g., ``~/.pyenv/shims/python3.12 ``),
40+ py-discovery detects this and resolves it to the real binary:
41+
42+ .. mermaid ::
43+
44+ flowchart LR
45+ Shim["Shim detected"] --> EnvVar{"PYENV_VERSION\n set?"}
46+ EnvVar -->|Yes| Use["Use that version"]
47+ EnvVar -->|No| File{".python-version\n file exists?"}
48+ File -->|Yes| Use
49+ File -->|No| Global{"$(PYENV_ROOT)/version\n exists?"}
50+ Global -->|Yes| Use
51+ Global -->|No| Skip["Skip shim"]
52+
53+ For mise and asdf, the ``MISE_DATA_DIR `` / ``ASDF_DATA_DIR `` directories are searched. The shim directory is
54+ identified via ``PYENV_ROOT ``, ``MISE_DATA_DIR ``, or ``ASDF_DATA_DIR `` environment variables.
55+
56+ Cache design
57+ ------------
58+
59+ The :class: `~py_discovery.DiskCache ` stores interpreter metadata as JSON files under
60+ ``<root>/py_info/4/<sha256>.json ``, where the hash is derived from the interpreter path. File locking via ``filelock ``
61+ ensures safe concurrent access.
62+
63+ .. mermaid ::
64+
65+ flowchart LR
66+ Lookup["py_info(path)"] --> Exists{Cache hit?}
67+ Exists -->|Yes| Read["Read JSON"]
68+ Exists -->|No| Run["Run subprocess"]
69+ Run --> Write["Write JSON\n (with filelock)"]
70+ Write --> Return["Return PythonInfo"]
71+ Read --> Return
72+
73+ The cache layer uses a :class: `typing.Protocol ` (:class: `~py_discovery.PyInfoCache `), so any object with the right
74+ method signatures works as a cache backend -- no inheritance required. Two built-in implementations are provided:
75+
76+ - :class: `~py_discovery.DiskCache ` -- persistent JSON + filelock storage
77+ - :class: `~py_discovery.NoOpCache ` -- no-op, useful for one-shot scripts or testing
78+
79+ Spec format
80+ -----------
81+
82+ A spec string follows the pattern ``[impl][version][t][-arch][-machine] ``:
83+
84+ .. mermaid ::
85+
86+ flowchart LR
87+ Spec["Spec string"] --> Impl["impl\n (optional)"]
88+ Impl --> Version["version\n (optional)"]
89+ Version --> T["t\n (optional)"]
90+ T --> Arch["-arch\n (optional)"]
91+ Arch --> Machine["-machine\n (optional)"]
92+
93+ style Impl fill:#e0f0ff,stroke:#4a90d9
94+ style Version fill:#e0ffe0,stroke:#4a9f4a
95+ style T fill:#fff0e0,stroke:#d9904a
96+ style Arch fill:#ffe0e0,stroke:#d94a4a
97+ style Machine fill:#f0e0ff,stroke:#904ad9
98+
99+ +------------------------------+-----------------------------------------------+
100+ | Spec | Meaning |
101+ +==============================+===============================================+
102+ | ``python3.12 `` | CPython 3.12 (any architecture) |
103+ +------------------------------+-----------------------------------------------+
104+ | ``cpython3.12 `` | Explicitly CPython 3.12 |
105+ +------------------------------+-----------------------------------------------+
106+ | ``pypy3.9 `` | PyPy 3.9 |
107+ +------------------------------+-----------------------------------------------+
108+ | ``3.12 `` | Any implementation, version 3.12 |
109+ +------------------------------+-----------------------------------------------+
110+ | ``python3.12t `` | Free-threaded (no-GIL) CPython 3.12 |
111+ +------------------------------+-----------------------------------------------+
112+ | ``python3.12-64 `` | 64-bit CPython 3.12 |
113+ +------------------------------+-----------------------------------------------+
114+ | ``python3.12-64-arm64 `` | 64-bit CPython 3.12 on ARM64 |
115+ +------------------------------+-----------------------------------------------+
116+ | ``/usr/bin/python3 `` | Absolute path, used directly |
117+ +------------------------------+-----------------------------------------------+
118+ | ``>=3.11,<3.13 `` | PEP 440 version specifier |
119+ +------------------------------+-----------------------------------------------+
120+ | ``cpython>=3.11 `` | PEP 440 specifier with implementation filter |
121+ +------------------------------+-----------------------------------------------+
122+
123+ The ``impl `` prefix is optional; ``python `` and ``py `` are treated as "any implementation". The ``t `` suffix matches
124+ free-threaded (no-GIL) builds. Architecture (``-32 `` or ``-64 ``) and ISA (``-arm64 ``, ``-x86_64 ``, etc.) are optional
125+ suffixes separated by dashes.
126+
127+ PEP 440 specifiers (``>= ``, ``<= ``, ``~= ``, ``!= ``, ``== ``, ``=== ``) are supported for flexible version matching.
128+ Multiple specifiers can be comma-separated: ``>=3.11,<3.13 ``.
0 commit comments