@@ -65,6 +65,106 @@ detects these shims and resolves them to the actual binary.
6565`mise <https://mise.jdx.dev/ >`_ and `asdf <https://asdf-vm.com/ >`_ work similarly, using the
6666``MISE_DATA_DIR `` and ``ASDF_DATA_DIR `` environment variables to locate their installations.
6767
68+ How uv-managed Pythons are discovered
69+ ---------------------------------------
70+
71+ `uv <https://docs.astral.sh/uv/ >`_ installs Python interpreters under a single root directory (configurable via
72+ ``UV_PYTHON_INSTALL_DIR ``, otherwise defaulting under ``XDG_DATA_HOME `` or the platform user-data path). Each
73+ install lives in its own subdirectory, but the actual binary location varies by OS and implementation:
74+
75+ .. list-table ::
76+ :header-rows: 1
77+ :widths: 25 35 40
78+
79+ * - Implementation
80+ - Unix layout
81+ - Windows layout
82+ * - CPython
83+ - ``<root>/<key>/bin/python ``
84+ - ``<root>/<key>/python.exe ``
85+ * - PyPy
86+ - ``<root>/<key>/bin/pypy* ``
87+ - ``<root>/<key>/pypy*.exe ``
88+ * - GraalPy
89+ - ``<root>/<key>/bin/graalpy ``
90+ - ``<root>/<key>/bin/graalpy.exe ``
91+
92+ .. mermaid ::
93+
94+ flowchart LR
95+ Call(["iter_interpreters(key)"]) --> Mode{"key is None?"}
96+ Mode -->|"narrow"| N1["*/bin/python"]
97+ Mode -->|"narrow"| N2[" */python.exe"]
98+ Mode -->|"wide"| W1["*/bin/pypy *"]
99+ Mode -->|"wide"| W2["*/bin/graalpy"]
100+ Mode -->|"wide"| W3[" */pypy*.exe"]
101+ Mode -->|"wide"| W4["*/bin/graalpy.exe"]
102+
103+ N1 --> Dedup[/"realpath dedup"/]
104+ N2 --> Dedup
105+ W1 --> Dedup
106+ W2 --> Dedup
107+ W3 --> Dedup
108+ W4 --> Dedup
109+
110+ Dedup --> Interrogate(["subprocess interrogation"])
111+
112+ style Call fill:#4a90d9,stroke:#2a5f8f,color:#fff
113+ style Mode fill:#d9904a,stroke:#8f5f2a,color:#fff
114+ style N1 fill:#3a7fc2,stroke:#1f4d7a,color:#fff
115+ style N2 fill:#3a7fc2,stroke:#1f4d7a,color:#fff
116+ style W1 fill:#9f4ad9,stroke:#5f2a8f,color:#fff
117+ style W2 fill:#9f4ad9,stroke:#5f2a8f,color:#fff
118+ style W3 fill:#9f4ad9,stroke:#5f2a8f,color:#fff
119+ style W4 fill:#9f4ad9,stroke:#5f2a8f,color:#fff
120+ style Dedup fill:#c2873a,stroke:#7a4c1f,color:#fff
121+ style Interrogate fill:#4a9f4a,stroke:#2a6f2a,color:#fff
122+
123+ GraalPy keeps its ``bin/ `` segment on Windows (an upstream choice in uv); PyPy and CPython do not. python-discovery
124+ globs all of these patterns regardless of the host OS, because globs that do not match anything are essentially
125+ free, and the cross-platform list is short. Symlinked aliases inside an install (``bin/python ``,
126+ ``bin/python3 ``, ``bin/python3.14 `` all pointing at the same real file) are deduplicated by resolved path before
127+ the subprocess interrogation, so each install is interrogated once.
128+
129+ Selecting one interpreter vs. enumerating all of them
130+ -------------------------------------------------------
131+
132+ :func: `~python_discovery.get_interpreter ` and :func: `~python_discovery.iter_interpreters ` walk the same candidate
133+ sources, but they answer different questions and behave differently in three ways.
134+
135+ .. mermaid ::
136+
137+ flowchart LR
138+ Sources["candidate sources<br>(try_first_with → current →<br>PEP 514 → PATH → uv)"]
139+ Sources --> Get["get_interpreter()<br>first match wins, returns one"]
140+ Sources --> Iter["iter_interpreters()<br>yields every match"]
141+
142+ style Get fill:#4a9f4a,stroke:#2a6f2a,color:#fff
143+ style Iter fill:#4a90d9,stroke:#2a5f8f,color:#fff
144+
145+ **Implementation coverage on PATH. ** :func: `~python_discovery.get_interpreter ` matches only ``python* `` filenames on
146+ PATH unless the spec names another implementation explicitly (``pypy3.12 ``, ``graalpy3.11 ``). This keeps backwards
147+ compatibility with tools that have always read "no implementation in the spec" as "give me CPython."
148+ :func: `~python_discovery.iter_interpreters ` with no spec broadens the search to every name in
149+ :data: `~python_discovery.KNOWN_IMPLEMENTATIONS ` -- otherwise an "all interpreters" call would silently miss every
150+ PyPy and GraalPy on the system. When you pass a spec to :func: `~python_discovery.iter_interpreters `, it falls back
151+ to the same narrow regex as :func: `~python_discovery.get_interpreter `, so behavior is consistent across the two
152+ APIs whenever a spec is given.
153+
154+ **Deduplication. ** :func: `~python_discovery.get_interpreter ` deduplicates per call so it does not interrogate the
155+ same binary twice while searching, and stops as soon as a match is found. :func: `~python_discovery.iter_interpreters `
156+ deduplicates by the resolved real path of each candidate's ``system_executable `` (falling back to ``executable ``).
157+ That means symlinked aliases like ``/bin/python3 `` and ``/usr/bin/python3 ``, or a virtualenv whose ``python ``
158+ symlinks to its base interpreter, collapse to a single yield. The semantic is "one entry per distinct install,"
159+ which is what callers building choosers or version-range pickers usually want.
160+
161+ **Iteration order. ** Yields come back in *priority order *: ``try_first_with `` first, then the running interpreter,
162+ then :pep: `514 ` entries on Windows, then PATH left-to-right, then UV-managed installs. This matches what
163+ :func: `~python_discovery.get_interpreter ` would have returned at each step. If your ordering differs (newest
164+ version first, smallest install root, etc.), wrap the call in :func: `sorted ` -- the API deliberately does not
165+ include a ``sort_by `` parameter because keeping discovery order preserves the priority signal for callers who
166+ want it.
167+
68168How caching works
69169-------------------
70170
@@ -165,9 +265,12 @@ A spec string follows the pattern ``[impl][version][t][-arch][-machine]``. Every
165265 * - ``/usr/bin/python3 ``
166266 - Absolute path, used directly (no search)
167267 * - ``>=3.11,<3.13 ``
168- - :pep: `440 ` version specifier (any Python in range)
268+ - `Version specifier <https://packaging.python.org/en/latest/specifications/version-specifiers/ >`_
269+ (any Python in range)
169270 * - ``cpython>=3.11 ``
170- - :pep: `440 ` specifier restricted to CPython
271+ - `Version specifier <https://packaging.python.org/en/latest/specifications/version-specifiers/ >`_
272+ restricted to CPython
171273
172- :pep: `440 ` specifiers (``>= ``, ``<= ``, ``~= ``, ``!= ``, ``== ``, ``=== ``) are supported. Multiple
173- specifiers can be comma-separated, for example ``>=3.11,<3.13 ``.
274+ `Version specifiers <https://packaging.python.org/en/latest/specifications/version-specifiers/ >`_
275+ (``>= ``, ``<= ``, ``~= ``, ``!= ``, ``== ``, ``=== ``) are supported. Multiple specifiers can be comma-separated,
276+ for example ``>=3.11,<3.13 ``.
0 commit comments