Skip to content

Commit d2c54c9

Browse files
committed
📝 docs: rewrite for clarity and accessibility
The documentation assumed familiarity with Python packaging internals. Rewrote all pages with plain language, added concept definitions before using them, and reorganized content to follow a logical learning path.
1 parent 6a1a6fc commit d2c54c9

4 files changed

Lines changed: 228 additions & 191 deletions

File tree

docs/explanation.rst

Lines changed: 51 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
1-
How py-discovery works
2-
======================
1+
How it works
2+
============
33

4-
Discovery strategy
5-
------------------
4+
Where does py-discovery look?
5+
-------------------------------
66

7-
The :class:`~py_discovery.Builtin` class searches for Python interpreters in the following order.
7+
When you call :class:`~py_discovery.Builtin` ``.run()``, the library checks several locations in
8+
order. It stops as soon as it finds an interpreter that matches your spec.
89

910
.. mermaid::
1011

1112
flowchart TD
1213
Start["Builtin.run()"] --> AbsPath{"Is spec an<br>absolute path?"}
13-
AbsPath -->|Yes| TryAbs["Try path directly"]
14+
AbsPath -->|Yes| TryAbs["Use path directly"]
1415
AbsPath -->|No| TryFirst["try_first_with paths"]
1516
TryFirst --> RelPath{"Is spec a<br>relative path?"}
1617
RelPath -->|Yes| TryRel["Resolve relative to cwd"]
@@ -27,22 +28,24 @@ The :class:`~py_discovery.Builtin` class searches for Python interpreters in the
2728
UV --> Verify
2829

2930
Verify{{"Verify candidate<br>(subprocess call)"}}
30-
Verify -->|Satisfies spec| Cache["Cache and return"]
31+
Verify -->|Matches spec| Cache["Cache and return"]
3132
Verify -->|No match| Next["Try next candidate"]
3233

3334
style Start fill:#4a90d9,stroke:#2a5f8f,color:#fff
3435
style Verify fill:#d9904a,stroke:#8f5f2a,color:#fff
3536
style Cache fill:#4a9f4a,stroke:#2a6f2a,color:#fff
3637
style Next fill:#d94a4a,stroke:#8f2a2a,color:#fff
3738

38-
Each candidate is verified by running it as a subprocess to collect its metadata (version, architecture, platform,
39-
sysconfig paths, and more). Verified interpreters are cached to avoid repeating this subprocess call.
39+
Each candidate is verified by running it as a subprocess and collecting its metadata (version,
40+
architecture, platform, sysconfig values, etc.). This subprocess call is the expensive part, which
41+
is why results are cached.
4042

41-
Shim resolution
42-
---------------
43+
How version-manager shims are handled
44+
-----------------------------------------
4345

44-
When a spec like ``python3.12`` resolves to a version-manager shim (e.g., ``~/.pyenv/shims/python3.12``),
45-
py-discovery detects this and resolves it to the real binary.
46+
Version managers like `pyenv <https://github.com/pyenv/pyenv>`_ install thin wrapper scripts called
47+
**shims** (e.g., ``~/.pyenv/shims/python3.12``) that redirect to the real interpreter. py-discovery
48+
detects these shims and resolves them to the actual binary.
4649

4750
.. mermaid::
4851

@@ -59,17 +62,14 @@ py-discovery detects this and resolves it to the real binary.
5962
style Use fill:#4a9f4a,stroke:#2a6f2a,color:#fff
6063
style Skip fill:#d94a4a,stroke:#8f2a2a,color:#fff
6164

62-
For `mise <https://mise.jdx.dev/>`_ and `asdf <https://asdf-vm.com/>`_, the ``MISE_DATA_DIR`` and ``ASDF_DATA_DIR``
63-
directories are searched. The shim directory is identified via the ``PYENV_ROOT``, ``MISE_DATA_DIR``, or
64-
``ASDF_DATA_DIR`` environment variables.
65+
`mise <https://mise.jdx.dev/>`_ and `asdf <https://asdf-vm.com/>`_ work similarly, using the
66+
``MISE_DATA_DIR`` and ``ASDF_DATA_DIR`` environment variables to locate their installations.
6567

66-
Cache design
67-
------------
68+
How caching works
69+
-------------------
6870

69-
The :class:`~py_discovery.DiskCache` stores interpreter metadata as JSON files under
70-
``<root>/py_info/4/<sha256>.json``, where the hash is derived from the interpreter path. File locking via
71-
`filelock <https://py-filelock.readthedocs.io/>`_
72-
ensures safe concurrent access.
71+
Querying an interpreter requires a subprocess call, which is slow. The cache avoids repeating this
72+
work by storing the result as a JSON file keyed by the interpreter's path.
7373

7474
.. mermaid::
7575

@@ -85,16 +85,15 @@ ensures safe concurrent access.
8585
style Return fill:#4a9f4a,stroke:#2a6f2a,color:#fff
8686
style Run fill:#d9904a,stroke:#8f5f2a,color:#fff
8787

88-
The cache layer uses a :class:`typing.Protocol` (:class:`~py_discovery.PyInfoCache`), so any object with the right
89-
method signatures works as a cache backend -- no inheritance required. Two built-in implementations are provided:
88+
The built-in :class:`~py_discovery.DiskCache` stores files under ``<root>/py_info/4/<sha256>.json``
89+
with `filelock <https://py-filelock.readthedocs.io/>`_-based locking for safe concurrent access. You
90+
can also pass ``cache=None`` to disable caching, or implement your own backend (see
91+
:doc:`/how-to/standalone-usage`).
9092

91-
- :class:`~py_discovery.DiskCache` -- persistent JSON + `filelock <https://py-filelock.readthedocs.io/>`_ storage.
92-
- ``cache=None`` -- disables caching, useful for one-shot scripts or testing.
93+
Spec format reference
94+
-----------------------
9395

94-
Spec format
95-
-----------
96-
97-
A spec string follows the pattern ``[impl][version][t][-arch][-machine]``.
96+
A spec string follows the pattern ``[impl][version][t][-arch][-machine]``. Every part is optional.
9897

9998
.. mermaid::
10099

@@ -111,36 +110,44 @@ A spec string follows the pattern ``[impl][version][t][-arch][-machine]``.
111110
style Arch fill:#d94a4a,stroke:#8f2a2a,color:#fff
112111
style Machine fill:#904ad9,stroke:#5f2a8f,color:#fff
113112

113+
**Parts explained:**
114+
115+
- **impl** -- the Python implementation name. ``python`` and ``py`` both mean "any implementation"
116+
(usually CPython). Use ``cpython``, ``pypy``, or ``graalpy`` to be explicit.
117+
- **version** -- dotted version number (``3``, ``3.12``, or ``3.12.1``). You can also write
118+
``312`` as shorthand for ``3.12``.
119+
- **t** -- appended directly after the version. Matches free-threaded (no-GIL) builds only.
120+
- **-arch** -- ``-32`` or ``-64`` for 32-bit or 64-bit interpreters.
121+
- **-machine** -- the CPU instruction set: ``-arm64``, ``-x86_64``, ``-aarch64``, ``-riscv64``, etc.
122+
123+
**Full examples:**
124+
114125
.. list-table::
115126
:header-rows: 1
116127
:widths: 30 70
117128

118129
* - Spec
119130
- Meaning
131+
* - ``3.12``
132+
- Any Python 3.12
120133
* - ``python3.12``
121-
- CPython 3.12 (any architecture)
134+
- CPython 3.12
122135
* - ``cpython3.12``
123136
- Explicitly CPython 3.12
124137
* - ``pypy3.9``
125138
- PyPy 3.9
126-
* - ``3.12``
127-
- Any implementation, version 3.12
128-
* - ``python3.12t``
129-
- Free-threaded (no-GIL) CPython 3.12
139+
* - ``python3.13t``
140+
- Free-threaded (no-GIL) CPython 3.13
130141
* - ``python3.12-64``
131142
- 64-bit CPython 3.12
132143
* - ``python3.12-64-arm64``
133144
- 64-bit CPython 3.12 on ARM64
134145
* - ``/usr/bin/python3``
135-
- Absolute path, used directly
146+
- Absolute path, used directly (no search)
136147
* - ``>=3.11,<3.13``
137-
- :pep:`440` version specifier
148+
- :pep:`440` version specifier (any Python in range)
138149
* - ``cpython>=3.11``
139-
- :pep:`440` specifier with implementation filter
140-
141-
The ``impl`` prefix is optional; ``python`` and ``py`` are treated as "any implementation." The ``t`` suffix matches
142-
free-threaded (no-GIL) builds. Architecture (``-32`` or ``-64``) and ISA (``-arm64``, ``-x86_64``, and others) are
143-
optional suffixes separated by dashes.
150+
- :pep:`440` specifier restricted to CPython
144151

145-
:pep:`440` specifiers (``>=``, ``<=``, ``~=``, ``!=``, ``==``, ``===``) are supported for flexible version matching.
146-
Multiple specifiers can be comma-separated, for example ``>=3.11,<3.13``.
152+
:pep:`440` specifiers (``>=``, ``<=``, ``~=``, ``!=``, ``==``, ``===``) are supported. Multiple
153+
specifiers can be comma-separated, for example ``>=3.11,<3.13``.

docs/how-to/standalone-usage.rst

Lines changed: 80 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,12 @@
1-
Advanced usage
2-
==============
1+
How-to guides
2+
=============
33

4-
Custom cache backends
5-
---------------------
6-
7-
Implement the :class:`~py_discovery.PyInfoCache` protocol to provide your own storage.
8-
9-
.. mermaid::
10-
11-
classDiagram
12-
class PyInfoCache {
13-
<<Protocol>>
14-
+py_info(path) ContentStore
15-
+py_info_clear() None
16-
}
17-
class ContentStore {
18-
<<Protocol>>
19-
+exists() bool
20-
+read() dict | None
21-
+write(content) None
22-
+remove() None
23-
+locked() context
24-
}
25-
class DiskCache {
26-
+root: Path
27-
}
28-
PyInfoCache <|.. DiskCache
29-
PyInfoCache --> ContentStore
30-
31-
.. code-block:: python
32-
33-
from pathlib import Path
34-
35-
from py_discovery import ContentStore, PyInfoCache
36-
37-
38-
class MyContentStore:
39-
def __init__(self, path: Path) -> None:
40-
self._path = path
41-
42-
def exists(self) -> bool: ...
43-
44-
def read(self) -> dict | None: ...
45-
46-
def write(self, content: dict) -> None: ...
47-
48-
def remove(self) -> None: ...
49-
50-
def locked(self): ...
51-
52-
53-
class MyCache:
54-
def py_info(self, path: Path) -> MyContentStore: ...
55-
56-
def py_info_clear(self) -> None: ...
57-
58-
The built-in :class:`~py_discovery.DiskCache` stores JSON files under ``<root>/py_info/4/<sha256>.json`` with
59-
`filelock <https://py-filelock.readthedocs.io/>`_-based locking. Pass ``cache=None`` to disable caching entirely.
60-
61-
Using ``get_interpreter`` directly
4+
Search specific directories first
625
-----------------------------------
636

64-
For lower-level control, use :func:`~py_discovery.get_interpreter` instead of the :class:`~py_discovery.Builtin` class.
65-
66-
.. mermaid::
67-
68-
flowchart TD
69-
Call["get_interpreter(spec, try_first_with)"] --> First["Check try_first_with paths"]
70-
First --> Normal["Normal discovery"]
71-
Normal --> Result{"Found?"}
72-
Result -->|Yes| Info["PythonInfo"]
73-
Result -->|No| Nil["None"]
74-
75-
style Call fill:#4a90d9,stroke:#2a5f8f,color:#fff
76-
style Info fill:#4a9f4a,stroke:#2a6f2a,color:#fff
77-
style Nil fill:#d94a4a,stroke:#8f2a2a,color:#fff
7+
If you know a likely location for the interpreter, pass it via ``try_first_with`` to check there
8+
before the normal search. This is useful when you have a custom Python install outside the
9+
standard locations.
7810

7911
.. code-block:: python
8012
@@ -84,12 +16,11 @@ For lower-level control, use :func:`~py_discovery.get_interpreter` instead of th
8416
if info is not None:
8517
print(info.executable)
8618
87-
The ``try_first_with`` parameter takes paths to check before the normal discovery strategy runs.
88-
89-
Controlling the environment
90-
---------------------------
19+
Restrict the search environment
20+
---------------------------------
9121

92-
Pass a custom environment mapping to isolate discovery from the system environment.
22+
By default, py-discovery reads environment variables like ``PATH`` and ``PYENV_ROOT`` from your
23+
shell. You can override these to control exactly where the library looks.
9324

9425
.. mermaid::
9526

@@ -111,13 +42,10 @@ Pass a custom environment mapping to isolate discovery from the system environme
11142
env = {**os.environ, "PATH": "/usr/local/bin:/usr/bin"}
11243
discover = Builtin(python_spec=["python3.12"], env=env)
11344
114-
This controls which ``PATH``, ``PYENV_ROOT``, ``UV_PYTHON_INSTALL_DIR``, and other environment variables are used
115-
during discovery.
116-
117-
Inspecting interpreter metadata
118-
--------------------------------
45+
Read interpreter metadata
46+
---------------------------
11947

120-
A :class:`~py_discovery.PythonInfo` object exposes detailed interpreter metadata.
48+
Once you have a :class:`~py_discovery.PythonInfo`, you can inspect everything about the interpreter.
12149

12250
.. mermaid::
12351

@@ -137,17 +65,79 @@ A :class:`~py_discovery.PythonInfo` object exposes detailed interpreter metadata
13765

13866
.. code-block:: python
13967
140-
from py_discovery import Builtin, DiskCache
14168
from pathlib import Path
14269
70+
from py_discovery import Builtin, DiskCache
71+
14372
cache = DiskCache(root=Path("~/.cache/py-discovery").expanduser())
14473
info = Builtin(python_spec=["python3.12"], cache=cache).run()
14574
14675
info.executable # Resolved path to the binary.
147-
info.system_executable # The system (non-venv) executable.
148-
info.implementation # "CPython", "PyPy", etc.
76+
info.system_executable # The underlying system interpreter (outside any venv).
77+
info.implementation # "CPython", "PyPy", "GraalPy", etc.
14978
info.version_info # VersionInfo(major, minor, micro, releaselevel, serial).
15079
info.architecture # 64 or 32.
15180
info.platform # sys.platform value ("linux", "darwin", "win32").
152-
info.sysconfig_vars # Dict of sysconfig variables.
153-
info.sysconfig_paths # Dict of sysconfig paths.
81+
info.machine # ISA: "arm64", "x86_64", etc.
82+
info.free_threaded # True if this is a no-GIL build.
83+
info.sysconfig_vars # All sysconfig.get_config_vars() values.
84+
info.sysconfig_paths # All sysconfig.get_paths() values.
85+
86+
Implement a custom cache backend
87+
-----------------------------------
88+
89+
The built-in :class:`~py_discovery.DiskCache` stores results as JSON files with
90+
`filelock <https://py-filelock.readthedocs.io/>`_-based locking. If you need a different storage
91+
strategy (e.g., in-memory, database-backed), implement the :class:`~py_discovery.PyInfoCache`
92+
protocol.
93+
94+
.. mermaid::
95+
96+
classDiagram
97+
class PyInfoCache {
98+
<<Protocol>>
99+
+py_info(path) ContentStore
100+
+py_info_clear() None
101+
}
102+
class ContentStore {
103+
<<Protocol>>
104+
+exists() bool
105+
+read() dict | None
106+
+write(content) None
107+
+remove() None
108+
+locked() context
109+
}
110+
class DiskCache {
111+
+root: Path
112+
}
113+
PyInfoCache <|.. DiskCache
114+
PyInfoCache --> ContentStore
115+
116+
.. code-block:: python
117+
118+
from pathlib import Path
119+
120+
from py_discovery import ContentStore, PyInfoCache
121+
122+
123+
class MyContentStore:
124+
def __init__(self, path: Path) -> None:
125+
self._path = path
126+
127+
def exists(self) -> bool: ...
128+
129+
def read(self) -> dict | None: ...
130+
131+
def write(self, content: dict) -> None: ...
132+
133+
def remove(self) -> None: ...
134+
135+
def locked(self): ...
136+
137+
138+
class MyCache:
139+
def py_info(self, path: Path) -> MyContentStore: ...
140+
141+
def py_info_clear(self) -> None: ...
142+
143+
Any object that matches the protocol works -- no inheritance required.

docs/index.rst

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,26 @@
11
py-discovery
22
============
33

4-
A Python interpreter discovery library. ``py-discovery`` finds Python interpreters on your system by searching PATH,
5-
version managers (`pyenv <https://github.com/pyenv/pyenv>`_, `mise <https://mise.jdx.dev/>`_,
6-
`asdf <https://asdf-vm.com/>`_), the Windows registry (:pep:`514`), and `uv <https://docs.astral.sh/uv/>`_-managed
7-
installations. Results are cached to disk for fast repeated lookups.
4+
You may have multiple Python versions installed on your machine -- system Python, versions from
5+
`pyenv <https://github.com/pyenv/pyenv>`_, `mise <https://mise.jdx.dev/>`_,
6+
`asdf <https://asdf-vm.com/>`_, `uv <https://docs.astral.sh/uv/>`_, or the Windows registry
7+
(:pep:`514`). ``py-discovery`` finds the right one for you.
8+
9+
Give it a requirement like ``python3.12`` or ``>=3.11,<3.13``, and it searches all known locations,
10+
verifies each candidate, and returns detailed metadata about the match. Results are cached to disk so
11+
repeated lookups are fast.
12+
13+
.. code-block:: python
14+
15+
from py_discovery import Builtin, DiskCache
16+
from pathlib import Path
17+
18+
cache = DiskCache(root=Path("~/.cache/py-discovery").expanduser())
19+
result = Builtin(python_spec=["python3.12"], cache=cache).run()
20+
if result is not None:
21+
print(result.executable) # /usr/bin/python3.12
22+
print(result.implementation) # CPython
23+
print(result.version_info[:3]) # (3, 12, 1)
824
925
.. toctree::
1026
:caption: Tutorials

0 commit comments

Comments
 (0)