python-discovery 1.2.2__tar.gz → 1.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. {python_discovery-1.2.2 → python_discovery-1.3.1}/.github/workflows/check.yaml +1 -1
  2. python_discovery-1.3.1/.github/workflows/prepare-release.yaml +100 -0
  3. {python_discovery-1.2.2 → python_discovery-1.3.1}/.github/workflows/release.yaml +3 -3
  4. {python_discovery-1.2.2 → python_discovery-1.3.1}/.pre-commit-config.yaml +5 -5
  5. {python_discovery-1.2.2 → python_discovery-1.3.1}/PKG-INFO +5 -2
  6. {python_discovery-1.2.2 → python_discovery-1.3.1}/README.md +2 -1
  7. python_discovery-1.3.1/docs/changelog/template.jinja2 +38 -0
  8. python_discovery-1.3.1/docs/changelog.rst +125 -0
  9. {python_discovery-1.2.2 → python_discovery-1.3.1}/docs/conf.py +7 -1
  10. {python_discovery-1.2.2 → python_discovery-1.3.1}/docs/explanation.rst +107 -4
  11. {python_discovery-1.2.2 → python_discovery-1.3.1}/docs/how-to/standalone-usage.rst +54 -0
  12. {python_discovery-1.2.2 → python_discovery-1.3.1}/docs/index.rst +6 -0
  13. {python_discovery-1.2.2 → python_discovery-1.3.1}/docs/tutorial/getting-started.rst +45 -1
  14. {python_discovery-1.2.2 → python_discovery-1.3.1}/pyproject.toml +35 -0
  15. {python_discovery-1.2.2 → python_discovery-1.3.1}/src/python_discovery/__init__.py +4 -2
  16. {python_discovery-1.2.2 → python_discovery-1.3.1}/src/python_discovery/_discovery.py +99 -11
  17. {python_discovery-1.2.2 → python_discovery-1.3.1}/src/python_discovery/_py_info.py +2 -0
  18. {python_discovery-1.2.2 → python_discovery-1.3.1}/src/python_discovery/_py_spec.py +20 -4
  19. python_discovery-1.3.1/tasks/release.py +167 -0
  20. python_discovery-1.3.1/tests/test_iter_interpreters.py +218 -0
  21. {python_discovery-1.2.2 → python_discovery-1.3.1}/tests/test_py_info_extra.py +6 -0
  22. {python_discovery-1.2.2 → python_discovery-1.3.1}/tests/test_py_spec.py +13 -0
  23. {python_discovery-1.2.2 → python_discovery-1.3.1}/tox.toml +9 -2
  24. python_discovery-1.2.2/docs/changelog/59.bugfix.rst +0 -1
  25. {python_discovery-1.2.2 → python_discovery-1.3.1}/.github/FUNDING.yaml +0 -0
  26. {python_discovery-1.2.2 → python_discovery-1.3.1}/.github/SECURITY.md +0 -0
  27. {python_discovery-1.2.2 → python_discovery-1.3.1}/.github/dependabot.yaml +0 -0
  28. {python_discovery-1.2.2 → python_discovery-1.3.1}/.github/release.yaml +0 -0
  29. {python_discovery-1.2.2 → python_discovery-1.3.1}/.gitignore +0 -0
  30. {python_discovery-1.2.2 → python_discovery-1.3.1}/.readthedocs.yaml +0 -0
  31. {python_discovery-1.2.2 → python_discovery-1.3.1}/.readthedocs.yml +0 -0
  32. {python_discovery-1.2.2 → python_discovery-1.3.1}/CODE_OF_CONDUCT.md +0 -0
  33. {python_discovery-1.2.2 → python_discovery-1.3.1}/LICENSE +0 -0
  34. {python_discovery-1.2.2 → python_discovery-1.3.1}/docs/_static/custom.css +0 -0
  35. {python_discovery-1.2.2 → python_discovery-1.3.1}/docs/_static/logo.svg +0 -0
  36. {python_discovery-1.2.2 → python_discovery-1.3.1}/docs/reference/api.rst +0 -0
  37. {python_discovery-1.2.2 → python_discovery-1.3.1}/docs/reference/environment-variables.rst +0 -0
  38. {python_discovery-1.2.2 → python_discovery-1.3.1}/src/python_discovery/_cache.py +0 -0
  39. {python_discovery-1.2.2 → python_discovery-1.3.1}/src/python_discovery/_cached_py_info.py +0 -0
  40. {python_discovery-1.2.2 → python_discovery-1.3.1}/src/python_discovery/_compat.py +0 -0
  41. {python_discovery-1.2.2 → python_discovery-1.3.1}/src/python_discovery/_specifier.py +0 -0
  42. {python_discovery-1.2.2 → python_discovery-1.3.1}/src/python_discovery/_windows/__init__.py +0 -0
  43. {python_discovery-1.2.2 → python_discovery-1.3.1}/src/python_discovery/_windows/_pep514.py +0 -0
  44. {python_discovery-1.2.2 → python_discovery-1.3.1}/src/python_discovery/_windows/_propose.py +0 -0
  45. {python_discovery-1.2.2 → python_discovery-1.3.1}/src/python_discovery/py.typed +0 -0
  46. {python_discovery-1.2.2 → python_discovery-1.3.1}/tests/conftest.py +0 -0
  47. {python_discovery-1.2.2 → python_discovery-1.3.1}/tests/py_info/test_py_info.py +0 -0
  48. {python_discovery-1.2.2 → python_discovery-1.3.1}/tests/py_info/test_py_info_exe_based_of.py +0 -0
  49. {python_discovery-1.2.2 → python_discovery-1.3.1}/tests/test_cache.py +0 -0
  50. {python_discovery-1.2.2 → python_discovery-1.3.1}/tests/test_cached_py_info.py +0 -0
  51. {python_discovery-1.2.2 → python_discovery-1.3.1}/tests/test_discovery.py +0 -0
  52. {python_discovery-1.2.2 → python_discovery-1.3.1}/tests/test_discovery_extra.py +0 -0
  53. {python_discovery-1.2.2 → python_discovery-1.3.1}/tests/test_py_spec_extra.py +0 -0
  54. {python_discovery-1.2.2 → python_discovery-1.3.1}/tests/test_specifier.py +0 -0
  55. {python_discovery-1.2.2 → python_discovery-1.3.1}/tests/windows/conftest.py +0 -0
  56. {python_discovery-1.2.2 → python_discovery-1.3.1}/tests/windows/test_windows.py +0 -0
  57. {python_discovery-1.2.2 → python_discovery-1.3.1}/tests/windows/test_windows_pep514.py +0 -0
  58. {python_discovery-1.2.2 → python_discovery-1.3.1}/tests/windows/winreg_mock_values.py +0 -0
@@ -39,7 +39,7 @@ jobs:
39
39
  fetch-depth: 0
40
40
  persist-credentials: false
41
41
  - name: Install the latest version of uv
42
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
42
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
43
43
  with:
44
44
  enable-cache: false
45
45
  cache-dependency-glob: "pyproject.toml"
@@ -0,0 +1,100 @@
1
+ name: Prepare Release
2
+ on:
3
+ workflow_dispatch:
4
+ inputs:
5
+ bump:
6
+ description: "Version bump type"
7
+ required: true
8
+ type: choice
9
+ options:
10
+ - auto
11
+ - major
12
+ - minor
13
+ - patch
14
+ default: auto
15
+
16
+ permissions:
17
+ contents: read
18
+
19
+ env:
20
+ FORCE_COLOR: 1
21
+
22
+ jobs:
23
+ prepare-release:
24
+ runs-on: ubuntu-24.04
25
+ environment: release-auth
26
+ permissions:
27
+ contents: write
28
+ steps:
29
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
30
+ with:
31
+ fetch-depth: 0
32
+ token: ${{ secrets.RELEASE_PAT }}
33
+ persist-credentials: true
34
+
35
+ - name: Install the latest version of uv
36
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
37
+ with:
38
+ enable-cache: true
39
+ cache-dependency-glob: "pyproject.toml"
40
+ github-token: ${{ secrets.GITHUB_TOKEN }}
41
+
42
+ - name: Configure git
43
+ run: |
44
+ git config user.name "${GITHUB_ACTOR}"
45
+ git config user.email "${GITHUB_ACTOR}@users.noreply.github.com"
46
+
47
+ - name: Calculate next version
48
+ id: version
49
+ env:
50
+ BUMP: ${{ inputs.bump }}
51
+ run: |
52
+ current=$(git describe --tags --abbrev=0 2>/dev/null || echo "0.0.0")
53
+ echo "Current version: $current"
54
+
55
+ IFS='.' read -r major minor patch <<< "$current"
56
+
57
+ bump_type="$BUMP"
58
+ if [ "$bump_type" = "auto" ]; then
59
+ feature_count=$(find docs/changelog -name "*.feature.rst" 2>/dev/null | wc -l)
60
+ breaking_count=$(find docs/changelog -name "*.breaking.rst" 2>/dev/null | wc -l)
61
+ if [ "$breaking_count" -gt 0 ]; then
62
+ bump_type="major"
63
+ echo "Auto-detected: major bump (found $breaking_count breaking changelog(s))"
64
+ elif [ "$feature_count" -gt 0 ]; then
65
+ bump_type="minor"
66
+ echo "Auto-detected: minor bump (found $feature_count feature changelog(s))"
67
+ else
68
+ bump_type="patch"
69
+ echo "Auto-detected: patch bump (no feature changelogs)"
70
+ fi
71
+ fi
72
+
73
+ case "$bump_type" in
74
+ major)
75
+ next="$((major + 1)).0.0"
76
+ ;;
77
+ minor)
78
+ next="$major.$((minor + 1)).0"
79
+ ;;
80
+ patch)
81
+ next="$major.$minor.$((patch + 1))"
82
+ ;;
83
+ esac
84
+
85
+ echo "Next version: $next"
86
+ echo "version=$next" >> $GITHUB_OUTPUT
87
+
88
+ - name: Install tox
89
+ run: uv tool install --python-preference only-managed --python 3.14 tox@latest --with tox-uv
90
+
91
+ - name: Run release process
92
+ run: tox run -e release -- ${STEPS_VERSION_OUTPUTS_VERSION}
93
+ env:
94
+ GH_TOKEN: ${{ secrets.RELEASE_PAT }}
95
+ STEPS_VERSION_OUTPUTS_VERSION: ${{ steps.version.outputs.version }}
96
+
97
+ - name: Display completion message
98
+ run: echo "Release ${STEPS_VERSION_OUTPUTS_VERSION} prepared and pushed successfully!"
99
+ env:
100
+ STEPS_VERSION_OUTPUTS_VERSION: ${{ steps.version.outputs.version }}
@@ -18,7 +18,7 @@ jobs:
18
18
  fetch-depth: 0
19
19
  persist-credentials: false
20
20
  - name: Install the latest version of uv
21
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
21
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
22
22
  with:
23
23
  enable-cache: false
24
24
  cache-dependency-glob: "pyproject.toml"
@@ -26,7 +26,7 @@ jobs:
26
26
  - name: Build package
27
27
  run: uv build --python 3.14 --python-preference only-managed --sdist --wheel . --out-dir dist
28
28
  - name: Store the distribution packages
29
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
29
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
30
30
  with:
31
31
  name: ${{ env.dists-artifact-name }}
32
32
  path: dist/*
@@ -47,6 +47,6 @@ jobs:
47
47
  name: ${{ env.dists-artifact-name }}
48
48
  path: dist/
49
49
  - name: Publish to PyPI
50
- uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
50
+ uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
51
51
  with:
52
52
  attestations: true
@@ -5,7 +5,7 @@ repos:
5
5
  - id: end-of-file-fixer
6
6
  - id: trailing-whitespace
7
7
  - repo: https://github.com/python-jsonschema/check-jsonschema
8
- rev: 0.37.1
8
+ rev: 0.37.2
9
9
  hooks:
10
10
  - id: check-github-workflows
11
11
  args: ["--verbose"]
@@ -15,24 +15,24 @@ repos:
15
15
  - id: codespell
16
16
  additional_dependencies: ["tomli>=2.4"]
17
17
  - repo: https://github.com/tox-dev/pyproject-fmt
18
- rev: "v2.21.0"
18
+ rev: "v2.21.2"
19
19
  hooks:
20
20
  - id: pyproject-fmt
21
21
  - repo: https://github.com/astral-sh/ruff-pre-commit
22
- rev: "v0.15.9"
22
+ rev: "v0.15.12"
23
23
  hooks:
24
24
  - id: ruff-format
25
25
  - id: ruff
26
26
  args: ["--fix", "--unsafe-fixes", "--exit-non-zero-on-fix"]
27
27
  - repo: https://github.com/rbubley/mirrors-prettier
28
- rev: "v3.8.1"
28
+ rev: "v3.8.3"
29
29
  hooks:
30
30
  - id: prettier
31
31
  additional_dependencies:
32
32
  - prettier@3.8.1
33
33
  - "@prettier/plugin-xml@3.4.2"
34
34
  - repo: https://github.com/zizmorcore/zizmor-pre-commit
35
- rev: v1.23.1
35
+ rev: v1.24.1
36
36
  hooks:
37
37
  - id: zizmor
38
38
  - repo: meta
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-discovery
3
- Version: 1.2.2
3
+ Version: 1.3.1
4
4
  Summary: Python interpreter discovery
5
5
  Project-URL: Changelog, https://github.com/tox-dev/python-discovery/releases
6
6
  Project-URL: Documentation, https://python-discovery.readthedocs.io
@@ -53,6 +53,8 @@ Requires-Dist: furo>=2025.12.19; extra == 'docs'
53
53
  Requires-Dist: sphinx-autodoc-typehints>=3.6.3; extra == 'docs'
54
54
  Requires-Dist: sphinx>=9.1; extra == 'docs'
55
55
  Requires-Dist: sphinxcontrib-mermaid>=2; extra == 'docs'
56
+ Requires-Dist: sphinxcontrib-towncrier>=0.4; extra == 'docs'
57
+ Requires-Dist: towncrier>=25.8; extra == 'docs'
56
58
  Provides-Extra: testing
57
59
  Requires-Dist: covdefaults>=2.3; extra == 'testing'
58
60
  Requires-Dist: coverage>=7.5.4; extra == 'testing'
@@ -99,7 +101,8 @@ The `get_interpreter()` function accepts various specification formats:
99
101
  - Absolute path: `/usr/bin/python3.12`
100
102
  - Version: `3.12`
101
103
  - Implementation prefix: `cpython3.12`
102
- - PEP 440 specifier: `>=3.10`, `>=3.11,<3.13`
104
+ - [Version specifier](https://packaging.python.org/en/latest/specifications/version-specifiers/): `>=3.10`,
105
+ `>=3.11,<3.13`
103
106
 
104
107
  ## Documentation
105
108
 
@@ -36,7 +36,8 @@ The `get_interpreter()` function accepts various specification formats:
36
36
  - Absolute path: `/usr/bin/python3.12`
37
37
  - Version: `3.12`
38
38
  - Implementation prefix: `cpython3.12`
39
- - PEP 440 specifier: `>=3.10`, `>=3.11,<3.13`
39
+ - [Version specifier](https://packaging.python.org/en/latest/specifications/version-specifiers/): `>=3.10`,
40
+ `>=3.11,<3.13`
40
41
 
41
42
  ## Documentation
42
43
 
@@ -0,0 +1,38 @@
1
+ {% set top_underline = "*" %}
2
+ {% set underline = "=" %}
3
+ {% if versiondata.name %}
4
+ {{ top_underline * ((versiondata.version + versiondata.date)|length + 5)}}
5
+ v{{ versiondata.version }} ({{ versiondata.date }})
6
+ {{ top_underline * ((versiondata.version + versiondata.date)|length + 5)}}
7
+ {% else %}
8
+ {{ top_underline * ((versiondata.version + versiondata.date)|length + 4)}}
9
+ {{ versiondata.version }} ({{ versiondata.date }})
10
+ {{ top_underline * ((versiondata.version + versiondata.date)|length + 4)}}
11
+ {% endif %}
12
+
13
+ {% for section, _ in sections.items() %}
14
+ {% if sections[section] %}
15
+ {% for category, val in definitions.items() if category in sections[section]%}
16
+ {{ definitions[category]['name'] }} - {{ versiondata.version }}
17
+ {{ underline * ((definitions[category]['name'] + versiondata.version)|length + 3)}}
18
+ {% if definitions[category]['showcontent'] %}
19
+ {% for text, values in sections[section][category].items() %}
20
+ - {{ text }} ({{ values|join(', ') }})
21
+ {% endfor %}
22
+
23
+ {% else %}
24
+ - {{ sections[section][category]['']|join(', ') }}
25
+
26
+ {% endif %}
27
+ {% if sections[section][category]|length == 0 %}
28
+ No significant changes.
29
+
30
+ {% else %}
31
+ {% endif %}
32
+ {% endfor %}
33
+ {% else %}
34
+ No significant changes.
35
+
36
+
37
+ {% endif %}
38
+ {% endfor %}
@@ -0,0 +1,125 @@
1
+ #################
2
+ Release History
3
+ #################
4
+
5
+ .. towncrier-draft-entries::
6
+
7
+ .. towncrier release notes start
8
+
9
+ ********************
10
+ v1.3.1 (2026-05-12)
11
+ ********************
12
+
13
+ Bug fixes - 1.3.1
14
+ =================
15
+ - export normalize_isa and deprecate KNOWN_ARCHITECTURES - by :user:`rahuldevikar`. (:issue:`59`)
16
+ - discover uv-managed Pythons on Windows. Previously the glob assumed Unix layout (``<root>/<key>/bin/python``) and
17
+ silently found nothing on Windows, where uv places ``python.exe`` directly under the install root - by
18
+ :user:`gaborbernat`. (:issue:`65`)
19
+ - Canonicalize GraalVM to match GraalPy Python interpreter in PythonSpec and PythonInfo. - by :user:`timfel`. (:issue:`73`)
20
+
21
+ ********************
22
+ v1.3.0 (2026-05-05)
23
+ ********************
24
+
25
+ Features - 1.3.0
26
+ ================
27
+
28
+ - Add :func:`~python_discovery.iter_interpreters` for enumerating every discovered interpreter, with PATH and
29
+ UV-install support for non-CPython implementations listed in :data:`~python_discovery.KNOWN_IMPLEMENTATIONS`
30
+ (:pull:`71`)
31
+
32
+ ********************
33
+ v1.2.2 (2026-04-06)
34
+ ********************
35
+
36
+ Features - 1.2.2
37
+ ================
38
+
39
+ - Export ``normalize_isa`` and deprecate ``KNOWN_ARCHITECTURES`` (:pull:`62`)
40
+
41
+ ********************
42
+ v1.2.1 (2026-03-26)
43
+ ********************
44
+
45
+ Features - 1.2.1
46
+ ================
47
+
48
+ - Expose ``KNOWN_ARCHITECTURES`` as public API (:pull:`56`)
49
+
50
+ Contributor-facing changes - 1.2.1
51
+ ==================================
52
+
53
+ - Add zizmor security auditing for workflows (:pull:`55`)
54
+
55
+ ********************
56
+ v1.2.0 (2026-03-18)
57
+ ********************
58
+
59
+ Features - 1.2.0
60
+ ================
61
+
62
+ - Increase interpreter query timeout to 15s, with an override (:pull:`53`)
63
+
64
+ ********************
65
+ v1.1.3 (2026-03-10)
66
+ ********************
67
+
68
+ Bug fixes - 1.1.3
69
+ =================
70
+
71
+ - Add ``loongarch64`` to known ISAs (:pull:`50`)
72
+
73
+ ********************
74
+ v1.1.2 (2026-03-09)
75
+ ********************
76
+
77
+ Bug fixes - 1.1.2
78
+ =================
79
+
80
+ - Match prerelease versions against ``major.minor`` specs (:pull:`48`)
81
+ - Drain pipes after killing a timed-out interpreter probe (:pull:`49`)
82
+
83
+ Improved documentation - 1.1.2
84
+ ==============================
85
+
86
+ - Add package description and usage to the README (:pull:`46`)
87
+
88
+ ********************
89
+ v1.1.1 (2026-03-06)
90
+ ********************
91
+
92
+ Bug fixes - 1.1.1
93
+ =================
94
+
95
+ - Add a timeout to interpreter probing (:pull:`42`)
96
+ - Add ``i686`` to known ISAs (:pull:`43`)
97
+
98
+ Contributor-facing changes - 1.1.1
99
+ ==================================
100
+
101
+ - Add a security policy and workflow permissions hardening (:pull:`33`)
102
+
103
+ ********************
104
+ v1.1.0 (2026-02-26)
105
+ ********************
106
+
107
+ Features - 1.1.0
108
+ ================
109
+
110
+ - Add a ``predicate`` parameter to :func:`~python_discovery.get_interpreter` (:pull:`31`)
111
+
112
+ Improved documentation - 1.1.0
113
+ ==============================
114
+
115
+ - Fix the ReadTheDocs build (:pull:`29`)
116
+ - Add ``:param:`` descriptions to all public APIs (:pull:`32`)
117
+
118
+ ********************
119
+ v1.0.0 (2026-02-25)
120
+ ********************
121
+
122
+ Features - 1.0.0
123
+ ================
124
+
125
+ - Initial release as a standalone package, extracted from ``virtualenv`` (:pull:`28`)
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from datetime import datetime, timezone
6
+ from pathlib import Path
6
7
 
7
8
  from python_discovery import __version__
8
9
 
@@ -19,8 +20,13 @@ extensions = [
19
20
  "sphinx.ext.intersphinx",
20
21
  "sphinx_autodoc_typehints",
21
22
  "sphinxcontrib.mermaid",
23
+ "sphinxcontrib.towncrier.ext",
22
24
  ]
23
25
 
26
+ towncrier_draft_autoversion_mode = "draft"
27
+ towncrier_draft_include_empty = True
28
+ towncrier_draft_working_directory = Path(__file__).parent.parent
29
+
24
30
  extlinks = {
25
31
  "issue": ("https://github.com/tox-dev/python-discovery/issues/%s", "#%s"),
26
32
  "pull": ("https://github.com/tox-dev/python-discovery/pull/%s", "PR #%s"),
@@ -33,7 +39,7 @@ intersphinx_mapping = {
33
39
 
34
40
  templates_path = []
35
41
  source_suffix = ".rst"
36
- exclude_patterns = ["_build", "changelog/*.rst"]
42
+ exclude_patterns = ["_build", "changelog/*"]
37
43
 
38
44
  main_doc = "index"
39
45
  pygments_style = "default"
@@ -65,6 +65,106 @@ detects these shims and resolves them to the actual binary.
65
65
  `mise <https://mise.jdx.dev/>`_ and `asdf <https://asdf-vm.com/>`_ work similarly, using the
66
66
  ``MISE_DATA_DIR`` and ``ASDF_DATA_DIR`` environment variables to locate their installations.
67
67
 
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
+
68
168
  How caching works
69
169
  -------------------
70
170
 
@@ -165,9 +265,12 @@ A spec string follows the pattern ``[impl][version][t][-arch][-machine]``. Every
165
265
  * - ``/usr/bin/python3``
166
266
  - Absolute path, used directly (no search)
167
267
  * - ``>=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)
169
270
  * - ``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
171
273
 
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``.
@@ -62,6 +62,60 @@ the ``PY_DISCOVERY_TIMEOUT`` environment variable.
62
62
  The timeout value should be a number in seconds. Each interpreter candidate is given this much time
63
63
  to respond. If a timeout occurs, the candidate is skipped and the search continues with the next one.
64
64
 
65
+ List every interpreter on the system
66
+ --------------------------------------
67
+
68
+ Use :func:`~python_discovery.iter_interpreters` to enumerate every Python python-discovery can find. With no spec
69
+ it yields all known implementations (CPython, PyPy, GraalPy -- see
70
+ :data:`~python_discovery.KNOWN_IMPLEMENTATIONS`). Pass a spec to filter, exactly like
71
+ :func:`~python_discovery.get_interpreter`.
72
+
73
+ .. code-block:: python
74
+
75
+ from pathlib import Path
76
+
77
+ from python_discovery import DiskCache, iter_interpreters
78
+
79
+ cache = DiskCache(root=Path("~/.cache/python-discovery").expanduser())
80
+
81
+ # Every interpreter, no filter
82
+ for info in iter_interpreters(cache=cache):
83
+ print(info.executable, info.version_str, info.implementation)
84
+
85
+ # Every CPython 3.10 or newer, newest first
86
+ newest_first = sorted(
87
+ iter_interpreters("cpython>=3.10", cache=cache),
88
+ key=lambda info: info.version_info,
89
+ reverse=True,
90
+ )
91
+
92
+ Enumeration interrogates every candidate as a subprocess on a cold cache. Always pass a
93
+ :class:`~python_discovery.DiskCache` if you call this more than once.
94
+
95
+ Pick an interpreter from a range, preferring newer
96
+ ----------------------------------------------------
97
+
98
+ A common need: search a version range and prefer newer interpreters when more than one matches. Sort the result of
99
+ :func:`~python_discovery.iter_interpreters` and take the first.
100
+
101
+ .. code-block:: python
102
+
103
+ from pathlib import Path
104
+
105
+ from python_discovery import DiskCache, iter_interpreters
106
+
107
+ cache = DiskCache(root=Path("~/.cache/python-discovery").expanduser())
108
+
109
+ matches = sorted(
110
+ iter_interpreters("cpython>=3.10,<3.15", cache=cache),
111
+ key=lambda info: info.version_info,
112
+ reverse=True,
113
+ )
114
+ info = matches[0] if matches else None
115
+
116
+ Use :func:`get_interpreter` instead when you only need the first PATH-priority hit; use the sort-and-take pattern
117
+ when *your* ordering differs from PATH order (newest version, smallest install size, preferred install root, etc.).
118
+
65
119
  Read interpreter metadata
66
120
  ---------------------------
67
121
 
@@ -47,3 +47,9 @@ repeated lookups are fast.
47
47
  :hidden:
48
48
 
49
49
  explanation
50
+
51
+ .. toctree::
52
+ :caption: Project
53
+ :hidden:
54
+
55
+ changelog
@@ -87,6 +87,49 @@ You can pass multiple specs as a list -- the library tries each one in order and
87
87
 
88
88
  result = get_interpreter(["python3.12", "python3.11"], cache=cache)
89
89
 
90
+ Listing every interpreter
91
+ ---------------------------
92
+
93
+ When you need *every* interpreter rather than just the first match -- for example, to show the user a chooser, or
94
+ to apply your own ranking -- use :func:`~python_discovery.iter_interpreters`. Pass no arguments to enumerate every
95
+ implementation python-discovery knows about, or pass a spec to filter.
96
+
97
+ .. mermaid::
98
+
99
+ flowchart TD
100
+ Call["iter_interpreters(spec, cache)"] --> Yield["yields PythonInfo"]
101
+ Yield --> A["1. try_first_with paths"]
102
+ Yield --> B["2. running interpreter"]
103
+ Yield --> C["3. PATH (left to right)"]
104
+ Yield --> D["4. uv-managed installs"]
105
+
106
+ style Call fill:#4a90d9,stroke:#2a5f8f,color:#fff
107
+ style Yield fill:#4a9f4a,stroke:#2a6f2a,color:#fff
108
+
109
+ .. code-block:: python
110
+
111
+ from pathlib import Path
112
+
113
+ from python_discovery import DiskCache, iter_interpreters
114
+
115
+ cache = DiskCache(root=Path("~/.cache/python-discovery").expanduser())
116
+ for info in iter_interpreters(cache=cache):
117
+ print(info.executable, info.version_str, info.implementation)
118
+
119
+ The result is an iterator, so :func:`list`, :func:`sorted`, generator expressions and early ``break`` all work as you
120
+ would expect. Symlinked aliases (``/bin/python3`` and ``/usr/bin/python3``, or a virtualenv and the base it points at)
121
+ collapse to a single entry, so you do not see the same install twice.
122
+
123
+ To prefer newer interpreters in a range, sort the result by ``version_info`` after filtering:
124
+
125
+ .. code-block:: python
126
+
127
+ newest_first = sorted(
128
+ iter_interpreters(">=3.10,<3.15", cache=cache),
129
+ key=lambda info: info.version_info,
130
+ reverse=True,
131
+ )
132
+
90
133
  Writing specs
91
134
  -------------
92
135
 
@@ -131,7 +174,8 @@ Common examples:
131
174
  * - ``/usr/bin/python3``
132
175
  - An absolute path, used directly without searching
133
176
  * - ``>=3.11,<3.13``
134
- - Any Python in the 3.11--3.12 range (:pep:`440` syntax)
177
+ - Any Python in the 3.11--3.12 range
178
+ (`version specifier <https://packaging.python.org/en/latest/specifications/version-specifiers/>`_ syntax)
135
179
 
136
180
  See the :doc:`full spec reference </explanation>` for all options.
137
181