diff --git a/docs/changelog/128.doc.rst b/docs/changelog/128.doc.rst new file mode 100644 index 0000000..ad7dd8a --- /dev/null +++ b/docs/changelog/128.doc.rst @@ -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`. diff --git a/docs/explanation.rst b/docs/explanation.rst index d32ffd6..72dfe2f 100644 --- a/docs/explanation.rst +++ b/docs/explanation.rst @@ -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. @@ -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
system_executable = None"] --> Walk["resolve_to_system()
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 ------------------- diff --git a/docs/how-to/standalone-usage.rst b/docs/how-to/standalone-usage.rst index 0715d24..0e2e996 100644 --- a/docs/how-to/standalone-usage.rst +++ b/docs/how-to/standalone-usage.rst @@ -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 @@ -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. @@ -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 ----------------------------------- diff --git a/docs/tutorial/getting-started.rst b/docs/tutorial/getting-started.rst index 3ced1d4..712fc49 100644 --- a/docs/tutorial/getting-started.rst +++ b/docs/tutorial/getting-started.rst @@ -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 ------------------------------------ @@ -45,6 +46,7 @@ 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) @@ -52,6 +54,11 @@ The simplest use case: get information about the Python that is running right no 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 --------------------------------