Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/changelog/128.doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Document :attr:`~python_discovery.PythonInfo.system_exe` across the tutorial, the how-to guide and the explanation of
how resolution reaches a base interpreter. The class diagram in the how-to guide had
:attr:`~python_discovery.PythonInfo.system_executable` typed ``str`` rather than ``str | None`` - by
:user:`gaborbernat`.
36 changes: 35 additions & 1 deletion docs/explanation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ APIs whenever a spec is given.

**Deduplication.** :func:`~python_discovery.get_interpreter` deduplicates per call so it does not interrogate the
same binary twice while searching, and stops as soon as a match is found. :func:`~python_discovery.iter_interpreters`
deduplicates by the resolved real path of each candidate's ``system_executable`` (falling back to ``executable``).
deduplicates by the resolved real path of each candidate's :attr:`~python_discovery.PythonInfo.system_exe`.
That means symlinked aliases like ``/bin/python3`` and ``/usr/bin/python3``, or a virtualenv whose ``python``
symlinks to its base interpreter, collapse to a single yield. The semantic is "one entry per distinct install,"
which is what callers building choosers or version-range pickers usually want.
Expand All @@ -198,6 +198,40 @@ version first, smallest install root, etc.), wrap the call in :func:`sorted` --
include a ``sort_by`` parameter because keeping discovery order preserves the priority signal for callers who
want it.

Resolving a virtual environment to its base
--------------------------------------------

A :class:`~python_discovery.PythonInfo` reaches you in one of two states, and the difference shows up in a single
field.

Collection fills in :attr:`~python_discovery.PythonInfo.system_executable` by inspection alone, which works whenever
the interpreter is not in a virtual environment or names its base outright. When inspection comes up short the field
holds ``None``, and :meth:`~python_discovery.PythonInfo.resolve_to_system` walks the prefix chain a layer at a time,
interrogating each prefix until it reaches a real install. Every discovery entry point runs that walk, so an
interpreter you got from :func:`~python_discovery.get_interpreter`,
:func:`~python_discovery.iter_interpreters` or :meth:`~python_discovery.PythonInfo.from_exe` is resolved before you
see it.

.. mermaid::

flowchart TD
Collect["collection<br>system_executable = None"] --> Walk["resolve_to_system()<br>walk prefix chain"]
Walk --> Done["system_executable set"]
Collect -- "base named outright" --> Done
Done --> Read["system_exe"]

style Collect fill:#ffe3a3,stroke:#d29200,color:#3a2c00
style Walk fill:#4a90d9,stroke:#2a5f8f,color:#fff
style Done fill:#4a9f4a,stroke:#2a6f2a,color:#fff
style Read fill:#4a9f4a,stroke:#2a6f2a,color:#fff

The annotation cannot describe that ordering, so ``system_executable`` stays ``str | None`` and a type checker asks
every reader to narrow a value resolution has settled.
:attr:`~python_discovery.PythonInfo.system_exe` closes that gap by falling back to
:attr:`~python_discovery.PythonInfo.executable`, which is the value ``resolve_to_system`` writes anyway when a prefix
links back to its own interpreter. Reading the raw field still tells the two states apart, which is what
:meth:`~python_discovery.PythonInfo.from_exe` with ``resolve_to_host=False`` leaves you holding.

How caching works
-------------------

Expand Down
12 changes: 10 additions & 2 deletions docs/how-to/standalone-usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,8 @@ Once you have a :class:`~python_discovery.PythonInfo`, you can inspect everythin
classDiagram
class PythonInfo {
+executable: str
+system_executable: str
+system_exe: str
+system_executable: str | None
+implementation: str
+version_info: VersionInfo
+architecture: int
Expand All @@ -167,7 +168,7 @@ Once you have a :class:`~python_discovery.PythonInfo`, you can inspect everythin
info = get_interpreter("python3.12", cache=cache)

info.executable # Resolved path to the binary.
info.system_executable # The underlying system interpreter (outside any venv).
info.system_exe # The underlying system interpreter (outside any venv).
info.implementation # "CPython", "PyPy", "GraalPy", etc.
info.version_info # VersionInfo(major, minor, micro, releaselevel, serial).
info.architecture # 64 or 32.
Expand All @@ -178,6 +179,13 @@ Once you have a :class:`~python_discovery.PythonInfo`, you can inspect everythin
info.sysconfig_vars # All sysconfig.get_config_vars() values.
info.sysconfig_paths # All sysconfig.get_paths() values.

Prefer :attr:`~python_discovery.PythonInfo.system_exe` over the raw
:attr:`~python_discovery.PythonInfo.system_executable` field it reads. The field is typed ``str | None`` because it
holds nothing until resolution runs, so a type checker makes you narrow it at every use even though discovery has
filled it in by the time you hold a :class:`~python_discovery.PythonInfo`. Read the raw field only when you build a
:class:`~python_discovery.PythonInfo` yourself, or call
:meth:`~python_discovery.PythonInfo.from_exe` with ``resolve_to_host=False``, and want to tell the two states apart.

Implement a custom cache backend
-----------------------------------

Expand Down
7 changes: 7 additions & 0 deletions docs/tutorial/getting-started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Before diving into code, here are the key ideas:
- **Spec** -- a short string describing what you are looking for (e.g., ``python3.12``, ``pypy3.9``, ``>=3.11``).
- **Discovery** -- the process of searching your system for an interpreter that matches a spec.
- **Cache** -- a disk store that remembers previously discovered interpreters so the next lookup is instant.
- **Resolution** -- following a virtual environment back to the base interpreter it was built from.

Inspecting the current interpreter
------------------------------------
Expand Down Expand Up @@ -45,13 +46,19 @@ The simplest use case: get information about the Python that is running right no
info = PythonInfo.current_system(cache)

print(info.executable) # /usr/bin/python3.12
print(info.system_exe) # /usr/bin/python3.12
print(info.version_info[:3]) # (3, 12, 1)
print(info.implementation) # CPython (or PyPy, GraalPy, etc.)
print(info.architecture) # 64 (or 32)

The returned :class:`~python_discovery.PythonInfo` object contains everything the library knows about that interpreter:
paths, version numbers, sysconfig variables, platform details, and more.

Run the same snippet from inside a virtual environment and the two paths part company.
:attr:`~python_discovery.PythonInfo.executable` stays the environment's own ``python``, while
:attr:`~python_discovery.PythonInfo.system_exe` names the base interpreter that environment was built from. Reach for
``system_exe`` whenever you want the real install rather than a link to it.

Finding a different interpreter
--------------------------------

Expand Down