python-discovery 1.3.0__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 (60) hide show
  1. python_discovery-1.3.1/.github/workflows/prepare-release.yaml +100 -0
  2. {python_discovery-1.3.0 → python_discovery-1.3.1}/.pre-commit-config.yaml +1 -1
  3. {python_discovery-1.3.0 → python_discovery-1.3.1}/PKG-INFO +3 -1
  4. python_discovery-1.3.1/docs/changelog/template.jinja2 +38 -0
  5. python_discovery-1.3.1/docs/changelog.rst +125 -0
  6. {python_discovery-1.3.0 → python_discovery-1.3.1}/docs/conf.py +7 -1
  7. {python_discovery-1.3.0 → python_discovery-1.3.1}/docs/index.rst +6 -0
  8. {python_discovery-1.3.0 → python_discovery-1.3.1}/pyproject.toml +34 -0
  9. {python_discovery-1.3.0 → python_discovery-1.3.1}/src/python_discovery/_py_info.py +2 -0
  10. {python_discovery-1.3.0 → python_discovery-1.3.1}/src/python_discovery/_py_spec.py +4 -0
  11. python_discovery-1.3.1/tasks/release.py +167 -0
  12. {python_discovery-1.3.0 → python_discovery-1.3.1}/tests/test_py_info_extra.py +6 -0
  13. {python_discovery-1.3.0 → python_discovery-1.3.1}/tests/test_py_spec.py +13 -0
  14. {python_discovery-1.3.0 → python_discovery-1.3.1}/tox.toml +9 -2
  15. python_discovery-1.3.0/docs/changelog/59.bugfix.rst +0 -1
  16. python_discovery-1.3.0/docs/changelog/65.bugfix.rst +0 -3
  17. python_discovery-1.3.0/docs/changelog/65.feature.rst +0 -3
  18. {python_discovery-1.3.0 → python_discovery-1.3.1}/.github/FUNDING.yaml +0 -0
  19. {python_discovery-1.3.0 → python_discovery-1.3.1}/.github/SECURITY.md +0 -0
  20. {python_discovery-1.3.0 → python_discovery-1.3.1}/.github/dependabot.yaml +0 -0
  21. {python_discovery-1.3.0 → python_discovery-1.3.1}/.github/release.yaml +0 -0
  22. {python_discovery-1.3.0 → python_discovery-1.3.1}/.github/workflows/check.yaml +0 -0
  23. {python_discovery-1.3.0 → python_discovery-1.3.1}/.github/workflows/release.yaml +0 -0
  24. {python_discovery-1.3.0 → python_discovery-1.3.1}/.gitignore +0 -0
  25. {python_discovery-1.3.0 → python_discovery-1.3.1}/.readthedocs.yaml +0 -0
  26. {python_discovery-1.3.0 → python_discovery-1.3.1}/.readthedocs.yml +0 -0
  27. {python_discovery-1.3.0 → python_discovery-1.3.1}/CODE_OF_CONDUCT.md +0 -0
  28. {python_discovery-1.3.0 → python_discovery-1.3.1}/LICENSE +0 -0
  29. {python_discovery-1.3.0 → python_discovery-1.3.1}/README.md +0 -0
  30. {python_discovery-1.3.0 → python_discovery-1.3.1}/docs/_static/custom.css +0 -0
  31. {python_discovery-1.3.0 → python_discovery-1.3.1}/docs/_static/logo.svg +0 -0
  32. {python_discovery-1.3.0 → python_discovery-1.3.1}/docs/explanation.rst +0 -0
  33. {python_discovery-1.3.0 → python_discovery-1.3.1}/docs/how-to/standalone-usage.rst +0 -0
  34. {python_discovery-1.3.0 → python_discovery-1.3.1}/docs/reference/api.rst +0 -0
  35. {python_discovery-1.3.0 → python_discovery-1.3.1}/docs/reference/environment-variables.rst +0 -0
  36. {python_discovery-1.3.0 → python_discovery-1.3.1}/docs/tutorial/getting-started.rst +0 -0
  37. {python_discovery-1.3.0 → python_discovery-1.3.1}/src/python_discovery/__init__.py +0 -0
  38. {python_discovery-1.3.0 → python_discovery-1.3.1}/src/python_discovery/_cache.py +0 -0
  39. {python_discovery-1.3.0 → python_discovery-1.3.1}/src/python_discovery/_cached_py_info.py +0 -0
  40. {python_discovery-1.3.0 → python_discovery-1.3.1}/src/python_discovery/_compat.py +0 -0
  41. {python_discovery-1.3.0 → python_discovery-1.3.1}/src/python_discovery/_discovery.py +0 -0
  42. {python_discovery-1.3.0 → python_discovery-1.3.1}/src/python_discovery/_specifier.py +0 -0
  43. {python_discovery-1.3.0 → python_discovery-1.3.1}/src/python_discovery/_windows/__init__.py +0 -0
  44. {python_discovery-1.3.0 → python_discovery-1.3.1}/src/python_discovery/_windows/_pep514.py +0 -0
  45. {python_discovery-1.3.0 → python_discovery-1.3.1}/src/python_discovery/_windows/_propose.py +0 -0
  46. {python_discovery-1.3.0 → python_discovery-1.3.1}/src/python_discovery/py.typed +0 -0
  47. {python_discovery-1.3.0 → python_discovery-1.3.1}/tests/conftest.py +0 -0
  48. {python_discovery-1.3.0 → python_discovery-1.3.1}/tests/py_info/test_py_info.py +0 -0
  49. {python_discovery-1.3.0 → python_discovery-1.3.1}/tests/py_info/test_py_info_exe_based_of.py +0 -0
  50. {python_discovery-1.3.0 → python_discovery-1.3.1}/tests/test_cache.py +0 -0
  51. {python_discovery-1.3.0 → python_discovery-1.3.1}/tests/test_cached_py_info.py +0 -0
  52. {python_discovery-1.3.0 → python_discovery-1.3.1}/tests/test_discovery.py +0 -0
  53. {python_discovery-1.3.0 → python_discovery-1.3.1}/tests/test_discovery_extra.py +0 -0
  54. {python_discovery-1.3.0 → python_discovery-1.3.1}/tests/test_iter_interpreters.py +0 -0
  55. {python_discovery-1.3.0 → python_discovery-1.3.1}/tests/test_py_spec_extra.py +0 -0
  56. {python_discovery-1.3.0 → python_discovery-1.3.1}/tests/test_specifier.py +0 -0
  57. {python_discovery-1.3.0 → python_discovery-1.3.1}/tests/windows/conftest.py +0 -0
  58. {python_discovery-1.3.0 → python_discovery-1.3.1}/tests/windows/test_windows.py +0 -0
  59. {python_discovery-1.3.0 → python_discovery-1.3.1}/tests/windows/test_windows_pep514.py +0 -0
  60. {python_discovery-1.3.0 → python_discovery-1.3.1}/tests/windows/winreg_mock_values.py +0 -0
@@ -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 }}
@@ -15,7 +15,7 @@ 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.1"
18
+ rev: "v2.21.2"
19
19
  hooks:
20
20
  - id: pyproject-fmt
21
21
  - repo: https://github.com/astral-sh/ruff-pre-commit
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-discovery
3
- Version: 1.3.0
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'
@@ -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"
@@ -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
@@ -50,6 +50,8 @@ optional-dependencies.docs = [
50
50
  "sphinx>=9.1",
51
51
  "sphinx-autodoc-typehints>=3.6.3",
52
52
  "sphinxcontrib-mermaid>=2",
53
+ "sphinxcontrib-towncrier>=0.4",
54
+ "towncrier>=25.8",
53
55
  ]
54
56
  optional-dependencies.testing = [
55
57
  "covdefaults>=2.3",
@@ -64,6 +66,14 @@ urls.Homepage = "https://github.com/tox-dev/python-discovery"
64
66
  urls.Source = "https://github.com/tox-dev/python-discovery"
65
67
  urls.Tracker = "https://github.com/tox-dev/python-discovery/issues"
66
68
 
69
+ [dependency-groups]
70
+ release = [
71
+ "gitpython>=3.1.46",
72
+ "packaging>=26",
73
+ "pre-commit>=4.5.1",
74
+ "towncrier>=25.8",
75
+ ]
76
+
67
77
  [tool.hatch]
68
78
  version.source = "vcs"
69
79
 
@@ -98,6 +108,12 @@ lint.per-file-ignores."src/python_discovery/_py_info.py" = [
98
108
  lint.per-file-ignores."src/python_discovery/_windows/_pep514.py" = [
99
109
  "PTH", # os.path.exists is monkeypatched in tests; pathlib.Path.exists bypasses the mock
100
110
  ]
111
+ lint.per-file-ignores."tasks/**/*.py" = [
112
+ "D", # release helper scripts, not API
113
+ "INP001", # no __init__.py in tasks directory
114
+ "S404", # subprocess import (release tooling)
115
+ "S603", # `subprocess` call: argv built from trusted inputs (version, gh CLI)
116
+ ]
101
117
  lint.per-file-ignores."tests/**/*.py" = [
102
118
  "D", # don't care about documentation in tests
103
119
  "FBT", # don't care about booleans as positional arguments in tests
@@ -180,3 +196,21 @@ report.partial_branches = [
180
196
  report.show_missing = true
181
197
  html.show_contexts = true
182
198
  html.skip_covered = false
199
+
200
+ [tool.towncrier]
201
+ name = "python-discovery"
202
+ filename = "docs/changelog.rst"
203
+ directory = "docs/changelog"
204
+ title_format = false
205
+ issue_format = ":issue:`{issue}`"
206
+ template = "docs/changelog/template.jinja2"
207
+ type = [
208
+ { directory = "breaking", name = "Backward incompatible changes", showcontent = true },
209
+ { directory = "deprecation", name = "Deprecations (removal in next major release)", showcontent = true },
210
+ { directory = "feature", name = "Features", showcontent = true },
211
+ { directory = "bugfix", name = "Bug fixes", showcontent = true },
212
+ { directory = "doc", name = "Improved documentation", showcontent = true },
213
+ { directory = "packaging", name = "Packaging updates and notes for downstreams", showcontent = true },
214
+ { directory = "contrib", name = "Contributor-facing changes", showcontent = true },
215
+ { directory = "misc", name = "Miscellaneous internal changes", showcontent = true },
216
+ ]
@@ -59,6 +59,8 @@ class PythonInfo: # noqa: PLR0904
59
59
  def _init_identity(self) -> None:
60
60
  self.platform = sys.platform
61
61
  self.implementation = platform.python_implementation()
62
+ if self.implementation == "GraalVM":
63
+ self.implementation = "GraalPy"
62
64
  if self.implementation == "PyPy":
63
65
  self.pypy_version_info = tuple(sys.pypy_version_info) # ty: ignore[unresolved-attribute] # pypy only
64
66
 
@@ -77,6 +77,8 @@ def _parse_spec_pattern(string_spec: str) -> PythonSpec | None:
77
77
  impl = groups["impl"]
78
78
  if impl in {"py", "python"}:
79
79
  impl = None
80
+ if impl == "graalvm":
81
+ impl = "graalpy"
80
82
  arch = _int_or_none(groups["arch"])
81
83
  machine = groups.get("machine")
82
84
  if machine is not None:
@@ -97,6 +99,8 @@ def _parse_specifier(string_spec: str) -> PythonSpec | None:
97
99
  return None
98
100
  if impl in {"py", "python"}:
99
101
  impl = None
102
+ if impl == "graalvm":
103
+ impl = "graalpy"
100
104
  return PythonSpec(string_spec, impl, None, None, None, None, None, version_specifier=version_specifier)
101
105
 
102
106
 
@@ -0,0 +1,167 @@
1
+ """Handles creating a release commit and tag, then publishes the GitHub release."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from subprocess import CalledProcessError, check_call, run
7
+
8
+ from git import Commit, Head, Remote, Repo, TagReference
9
+ from packaging.version import Version
10
+
11
+ ROOT_SRC_DIR = Path(__file__).parents[1]
12
+
13
+
14
+ def main(version_str: str) -> None:
15
+ version = Version(version_str)
16
+ repo = Repo(str(ROOT_SRC_DIR))
17
+
18
+ if repo.is_dirty():
19
+ msg = "Current repository is dirty. Please commit any changes and try again."
20
+ raise RuntimeError(msg)
21
+ upstream, release_branch = create_release_branch(repo, version)
22
+ main_pushed = False
23
+ tag_pushed = False
24
+ release_created = False
25
+ original_main_sha = upstream.refs.main.commit.hexsha
26
+ try:
27
+ release_commit = release_changelog(repo, version)
28
+ tag = tag_release_commit(release_commit, repo, version)
29
+ print("push release commit") # noqa: T201
30
+ repo.git.push(upstream.name, f"{release_branch}:main", "-f")
31
+ main_pushed = True
32
+ print("push release tag") # noqa: T201
33
+ repo.git.push(upstream.name, tag, "-f")
34
+ tag_pushed = True
35
+ create_github_release(version)
36
+ release_created = True
37
+ print("checkout main to new release and delete release branch") # noqa: T201
38
+ repo.heads.main.checkout()
39
+ repo.delete_head(release_branch, force=True)
40
+ print("delete remote release branch") # noqa: T201
41
+ repo.git.push(upstream.name, f":{release_branch}", "--no-verify")
42
+ upstream.fetch()
43
+ repo.git.reset("--hard", f"{upstream.name}/main")
44
+ print("All done!") # noqa: T201
45
+ except Exception:
46
+ cleanup_failed_release(
47
+ repo,
48
+ upstream,
49
+ version,
50
+ release_branch,
51
+ original_main_sha,
52
+ release_created=release_created,
53
+ tag_pushed=tag_pushed,
54
+ main_pushed=main_pushed,
55
+ )
56
+ raise
57
+
58
+
59
+ def create_release_branch(repo: Repo, version: Version) -> tuple[Remote, Head]:
60
+ print("create release branch from upstream main") # noqa: T201
61
+ upstream = get_upstream(repo)
62
+ upstream.fetch()
63
+ branch_name = f"release-{version}"
64
+ release_branch = repo.create_head(branch_name, upstream.refs.main, force=True)
65
+ upstream.push(refspec=f"{branch_name}:{branch_name}", force=True)
66
+ release_branch.set_tracking_branch(upstream.refs[branch_name])
67
+ release_branch.checkout()
68
+ return upstream, release_branch
69
+
70
+
71
+ def get_upstream(repo: Repo) -> Remote:
72
+ for remote in repo.remotes:
73
+ if any("tox-dev/python-discovery" in url for url in remote.urls):
74
+ return remote
75
+ msg = "could not find tox-dev/python-discovery remote"
76
+ raise RuntimeError(msg)
77
+
78
+
79
+ def release_changelog(repo: Repo, version: Version) -> Commit:
80
+ print("generate release commit") # noqa: T201
81
+ check_call(["towncrier", "build", "--yes", "--version", version.public], cwd=str(ROOT_SRC_DIR)) # noqa: S607
82
+ print("format changelog with pre-commit") # noqa: T201
83
+ changelog_path = ROOT_SRC_DIR / "docs" / "changelog.rst"
84
+ try:
85
+ check_call(["pre-commit", "run", "--files", str(changelog_path)], cwd=str(ROOT_SRC_DIR)) # noqa: S607
86
+ except CalledProcessError:
87
+ print("pre-commit made formatting changes, staging them") # noqa: T201
88
+ repo.index.add([str(changelog_path)])
89
+ return repo.index.commit(f"release {version}")
90
+
91
+
92
+ def tag_release_commit(release_commit: Commit, repo: Repo, version: Version) -> TagReference:
93
+ print("tag release commit") # noqa: T201
94
+ version_str = str(version)
95
+ if version_str in {tag.name for tag in repo.tags}:
96
+ print(f"delete existing tag {version_str}") # noqa: T201
97
+ repo.delete_tag(repo.tags[version_str])
98
+ print(f"create tag {version_str}") # noqa: T201
99
+ return repo.create_tag(version_str, ref=release_commit.hexsha, force=True)
100
+
101
+
102
+ def create_github_release(version: Version) -> None:
103
+ print("create github release") # noqa: T201
104
+ version_str = str(version)
105
+ try:
106
+ result = run(
107
+ ["gh", "release", "create", version_str, "--title", f"v{version_str}", "--generate-notes"], # noqa: S607
108
+ cwd=str(ROOT_SRC_DIR),
109
+ capture_output=True,
110
+ text=True,
111
+ check=True,
112
+ )
113
+ if result.stdout:
114
+ print(result.stdout) # noqa: T201
115
+ except CalledProcessError as e:
116
+ print(f"gh release create failed with exit code {e.returncode}") # noqa: T201
117
+ if e.stdout:
118
+ print(f"stdout: {e.stdout}") # noqa: T201
119
+ if e.stderr:
120
+ print(f"stderr: {e.stderr}") # noqa: T201
121
+ raise
122
+
123
+
124
+ def cleanup_failed_release( # noqa: PLR0913
125
+ repo: Repo,
126
+ upstream: Remote,
127
+ version: Version,
128
+ release_branch: Head,
129
+ original_main_sha: str,
130
+ *,
131
+ release_created: bool,
132
+ tag_pushed: bool,
133
+ main_pushed: bool,
134
+ ) -> None:
135
+ print("Release failed! Cleaning up...") # noqa: T201
136
+ if release_created:
137
+ print(f"Deleting GitHub release {version}") # noqa: T201
138
+ try:
139
+ check_call(["gh", "release", "delete", str(version), "--yes"], cwd=str(ROOT_SRC_DIR)) # noqa: S607
140
+ except Exception as cleanup_error: # noqa: BLE001
141
+ print(f"Warning: Failed to delete GitHub release: {cleanup_error}") # noqa: T201
142
+ if tag_pushed:
143
+ print(f"Deleting remote tag {version}") # noqa: T201
144
+ try:
145
+ repo.git.push(upstream.name, f":refs/tags/{version}", "--no-verify")
146
+ except Exception as cleanup_error: # noqa: BLE001
147
+ print(f"Warning: Failed to delete remote tag: {cleanup_error}") # noqa: T201
148
+ if main_pushed:
149
+ print(f"Reverting main to {original_main_sha[:8]}") # noqa: T201
150
+ try:
151
+ repo.git.push(upstream.name, f"{original_main_sha}:main", "-f", "--no-verify")
152
+ except Exception as cleanup_error: # noqa: BLE001
153
+ print(f"Warning: Failed to revert main: {cleanup_error}") # noqa: T201
154
+ print("Deleting remote release branch") # noqa: T201
155
+ try:
156
+ repo.git.push(upstream.name, f":{release_branch}", "--no-verify")
157
+ except Exception as cleanup_error: # noqa: BLE001
158
+ print(f"Warning: Failed to delete remote branch: {cleanup_error}") # noqa: T201
159
+
160
+
161
+ if __name__ == "__main__":
162
+ import argparse
163
+
164
+ parser = argparse.ArgumentParser(prog="release")
165
+ parser.add_argument("--version", required=True)
166
+ options = parser.parse_args()
167
+ main(options.version)
@@ -32,6 +32,12 @@ def test_py_info_pypy_version(mocker: MockerFixture) -> None:
32
32
  assert info.pypy_version_info == (7, 3, 11, "final", 0)
33
33
 
34
34
 
35
+ def test_py_info_canonicalizes_graalvm(mocker: MockerFixture) -> None:
36
+ mocker.patch("platform.python_implementation", return_value="GraalVM")
37
+ info = PythonInfo()
38
+ assert info.implementation == "GraalPy"
39
+
40
+
35
41
  def test_has_venv_attribute() -> None:
36
42
  info = PythonInfo()
37
43
  assert isinstance(info.has_venv, bool)
@@ -155,6 +155,12 @@ def test_specifier_with_implementation() -> None:
155
155
  assert spec.version_specifier == SpecifierSet.from_string(">=3.12")
156
156
 
157
157
 
158
+ def test_specifier_with_graalpy_implementation_alias() -> None:
159
+ spec = PythonSpec.from_string_spec("graalvm>=3.12")
160
+ assert spec.implementation == "graalpy"
161
+ assert spec.version_specifier == SpecifierSet.from_string(">=3.12")
162
+
163
+
158
164
  def test_specifier_satisfies_with_partial_information() -> None:
159
165
  spec = PythonSpec.from_string_spec(">=3.12")
160
166
  candidate = PythonSpec.from_string_spec("python3.12")
@@ -245,3 +251,10 @@ def test_normalize_isa(isa: str, normalized: str) -> None:
245
251
  )
246
252
  def test_spec_repr_machine(spec_str: str, in_repr: str) -> None:
247
253
  assert in_repr in repr(PythonSpec.from_string_spec(spec_str))
254
+
255
+
256
+ @pytest.mark.parametrize(("left", "right"), [("graalpy", "graalvm"), ("graalvm", "graalpy")])
257
+ def test_spec_satisfies_graalpy_implementation_aliases(left: str, right: str) -> None:
258
+ spec_1 = PythonSpec.from_string_spec(left)
259
+ spec_2 = PythonSpec.from_string_spec(right)
260
+ assert spec_1.satisfies(spec_2) is True
@@ -36,12 +36,12 @@ commands = [["pre-commit", "run", "--all-files", "--show-diff-on-failure"]]
36
36
 
37
37
  [env."type-3.8"]
38
38
  description = "run type check on code base (3.8)"
39
- deps = ["ty==0.0.17"]
39
+ deps = ["gitpython>=3.1.46", "packaging>=26", "ty==0.0.17"]
40
40
  commands = [["ty", "check", "--output-format", "concise", "--error-on-warning", "--python-version", "3.8", "."]]
41
41
 
42
42
  [env."type-3.14"]
43
43
  description = "run type check on code base (3.14)"
44
- deps = ["ty==0.0.17"]
44
+ deps = ["gitpython>=3.1.46", "packaging>=26", "ty==0.0.17"]
45
45
  commands = [["ty", "check", "--output-format", "concise", "--error-on-warning", "--python-version", "3.14", "."]]
46
46
 
47
47
  [env.docs]
@@ -71,6 +71,13 @@ commands = [
71
71
  ["check-wheel-contents", "--no-config", "{env_tmp_dir}"],
72
72
  ]
73
73
 
74
+ [env.release]
75
+ description = "do a release, required posargs of the version number"
76
+ skip_install = true
77
+ dependency_groups = ["release"]
78
+ pass_env = ["GH_TOKEN"]
79
+ commands = [["python", "{tox_root}/tasks/release.py", "--version", "{posargs}"]]
80
+
74
81
  [env.dev]
75
82
  description = "generate a DEV environment"
76
83
  package = "editable"
@@ -1 +0,0 @@
1
- export normalize_isa and deprecate KNOWN_ARCHITECTURES - by :user:`rahuldevikar`.
@@ -1,3 +0,0 @@
1
- discover uv-managed Pythons on Windows. Previously the glob assumed Unix layout (``<root>/<key>/bin/python``) and
2
- silently found nothing on Windows, where uv places ``python.exe`` directly under the install root - by
3
- :user:`gaborbernat`.
@@ -1,3 +0,0 @@
1
- add :func:`~python_discovery.iter_interpreters` for enumerating every discovered interpreter, with PATH and
2
- UV-install support for non-CPython implementations listed in :data:`~python_discovery.KNOWN_IMPLEMENTATIONS`
3
- - by :user:`gaborbernat`.