python-discovery 1.6.2__tar.gz → 1.6.4__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.
- {python_discovery-1.6.2 → python_discovery-1.6.4}/.github/dependabot.yaml +4 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/.github/workflows/check.yaml +32 -2
- python_discovery-1.6.4/.github/workflows/prepare-release.yaml +156 -0
- python_discovery-1.6.4/.github/workflows/release.yaml +105 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/.pre-commit-config.yaml +2 -2
- {python_discovery-1.6.2 → python_discovery-1.6.4}/PKG-INFO +1 -1
- {python_discovery-1.6.2 → python_discovery-1.6.4}/docs/changelog.rst +28 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/docs/how-to/standalone-usage.rst +4 -4
- {python_discovery-1.6.2 → python_discovery-1.6.4}/pyproject.toml +0 -1
- {python_discovery-1.6.2 → python_discovery-1.6.4}/src/python_discovery/_cached_py_info.py +33 -18
- {python_discovery-1.6.2 → python_discovery-1.6.4}/src/python_discovery/_py_info.py +32 -1
- {python_discovery-1.6.2 → python_discovery-1.6.4}/src/python_discovery/_py_spec.py +3 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/src/python_discovery/_windows/_pep514.py +4 -1
- {python_discovery-1.6.2 → python_discovery-1.6.4}/src/python_discovery/_windows/_propose.py +8 -2
- python_discovery-1.6.4/tasks/release.py +82 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/tests/test_cached_py_info.py +67 -2
- {python_discovery-1.6.2 → python_discovery-1.6.4}/tests/test_discovery.py +3 -2
- {python_discovery-1.6.2 → python_discovery-1.6.4}/tests/test_py_spec.py +2 -2
- {python_discovery-1.6.2 → python_discovery-1.6.4}/tests/windows/conftest.py +1 -1
- {python_discovery-1.6.2 → python_discovery-1.6.4}/tests/windows/test_windows_pep514.py +75 -0
- python_discovery-1.6.2/.github/workflows/prepare-release.yaml +0 -92
- python_discovery-1.6.2/.github/workflows/release.yaml +0 -49
- python_discovery-1.6.2/tasks/release.py +0 -173
- {python_discovery-1.6.2 → python_discovery-1.6.4}/.github/FUNDING.yaml +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/.github/SECURITY.md +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/.github/release.yaml +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/.gitignore +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/.readthedocs.yaml +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/.readthedocs.yml +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/CODE_OF_CONDUCT.md +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/LICENSE +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/README.md +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/docs/_static/custom.css +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/docs/_static/logo.svg +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/docs/changelog/template.jinja2 +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/docs/conf.py +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/docs/explanation.rst +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/docs/index.rst +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/docs/reference/api.rst +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/docs/reference/environment-variables.rst +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/docs/tutorial/getting-started.rst +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/src/python_discovery/__init__.py +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/src/python_discovery/_cache.py +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/src/python_discovery/_compat.py +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/src/python_discovery/_discovery.py +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/src/python_discovery/_py_info_collect.py +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/src/python_discovery/_specifier.py +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/src/python_discovery/_windows/__init__.py +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/src/python_discovery/py.typed +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/tasks/old_target_check.py +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/tasks/old_target_check.sh +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/tests/conftest.py +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/tests/py_info/test_py_info.py +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/tests/py_info/test_py_info_exe_based_of.py +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/tests/test_cache.py +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/tests/test_cache_recovery.py +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/tests/test_discovery_extra.py +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/tests/test_iter_interpreters.py +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/tests/test_py_info_collect.py +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/tests/test_py_info_extra.py +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/tests/test_py_spec_extra.py +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/tests/test_query_timeout.py +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/tests/test_specifier.py +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/tests/windows/test_windows.py +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/tests/windows/winreg_mock_values.py +0 -0
- {python_discovery-1.6.2 → python_discovery-1.6.4}/tox.toml +0 -0
|
@@ -5,6 +5,7 @@ on:
|
|
|
5
5
|
branches: ["main"]
|
|
6
6
|
tags-ignore: ["**"]
|
|
7
7
|
pull_request:
|
|
8
|
+
types: [opened, synchronize, reopened, labeled, unlabeled]
|
|
8
9
|
schedule:
|
|
9
10
|
- cron: "0 8 * * *"
|
|
10
11
|
concurrency:
|
|
@@ -37,7 +38,7 @@ jobs:
|
|
|
37
38
|
fetch-depth: 0
|
|
38
39
|
persist-credentials: false
|
|
39
40
|
- name: Install the latest version of uv
|
|
40
|
-
uses: astral-sh/setup-uv@
|
|
41
|
+
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
|
41
42
|
with:
|
|
42
43
|
python-version: "3.15"
|
|
43
44
|
enable-cache: false
|
|
@@ -63,7 +64,7 @@ jobs:
|
|
|
63
64
|
fetch-depth: 0
|
|
64
65
|
persist-credentials: false
|
|
65
66
|
- name: Install the latest version of uv
|
|
66
|
-
uses: astral-sh/setup-uv@
|
|
67
|
+
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
|
67
68
|
with:
|
|
68
69
|
python-version: "3.15"
|
|
69
70
|
enable-cache: false
|
|
@@ -80,3 +81,32 @@ jobs:
|
|
|
80
81
|
run: tasks/old_target_check.sh 3.5 rejected
|
|
81
82
|
- name: Reject Python 2.7
|
|
82
83
|
run: tasks/old_target_check.sh 2.7 rejected
|
|
84
|
+
changelog:
|
|
85
|
+
if: github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'skip news')
|
|
86
|
+
runs-on: ubuntu-24.04
|
|
87
|
+
steps:
|
|
88
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
89
|
+
with:
|
|
90
|
+
fetch-depth: 0
|
|
91
|
+
persist-credentials: false
|
|
92
|
+
- uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
|
93
|
+
with:
|
|
94
|
+
python-version: "3.15"
|
|
95
|
+
enable-cache: false
|
|
96
|
+
- run: uvx towncrier check --compare-with "origin/${GITHUB_BASE_REF}"
|
|
97
|
+
gate:
|
|
98
|
+
name: Required checks pass
|
|
99
|
+
if: always()
|
|
100
|
+
needs: [test, old-target, changelog]
|
|
101
|
+
runs-on: ubuntu-24.04
|
|
102
|
+
steps:
|
|
103
|
+
- env:
|
|
104
|
+
NEEDS_JSON: ${{ toJSON(needs) }}
|
|
105
|
+
run: |
|
|
106
|
+
jq -e '
|
|
107
|
+
all(
|
|
108
|
+
to_entries[];
|
|
109
|
+
.value.result == "success" or
|
|
110
|
+
(.key == "changelog" and .value.result == "skipped")
|
|
111
|
+
)
|
|
112
|
+
' <<< "$NEEDS_JSON"
|
|
@@ -0,0 +1,156 @@
|
|
|
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
|
+
permissions:
|
|
16
|
+
contents: read
|
|
17
|
+
env:
|
|
18
|
+
FORCE_COLOR: 1
|
|
19
|
+
jobs:
|
|
20
|
+
assemble:
|
|
21
|
+
if: github.ref == 'refs/heads/main'
|
|
22
|
+
runs-on: ubuntu-24.04
|
|
23
|
+
permissions:
|
|
24
|
+
contents: read
|
|
25
|
+
outputs:
|
|
26
|
+
version: ${{ steps.version.outputs.version }}
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
29
|
+
with:
|
|
30
|
+
fetch-depth: 0 # The next version comes from the tags.
|
|
31
|
+
persist-credentials: false
|
|
32
|
+
- name: Install uv
|
|
33
|
+
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
|
34
|
+
with:
|
|
35
|
+
python-version: "3.15"
|
|
36
|
+
enable-cache: false
|
|
37
|
+
- name: Require a news fragment
|
|
38
|
+
run: |
|
|
39
|
+
if ! compgen -G 'docs/changelog/*.*.rst' > /dev/null; then
|
|
40
|
+
echo '::error::add a docs/changelog/*.rst fragment before preparing a release'
|
|
41
|
+
exit 1
|
|
42
|
+
fi
|
|
43
|
+
- name: Set up remote tracking
|
|
44
|
+
run: |
|
|
45
|
+
git remote -v
|
|
46
|
+
git fetch origin
|
|
47
|
+
git branch -a
|
|
48
|
+
- name: Calculate next version
|
|
49
|
+
id: version
|
|
50
|
+
env:
|
|
51
|
+
BUMP: ${{ inputs.bump }}
|
|
52
|
+
run: |
|
|
53
|
+
current=$(git describe --tags --abbrev=0 2>/dev/null || echo "0.0.0")
|
|
54
|
+
echo "Current version: $current"
|
|
55
|
+
|
|
56
|
+
IFS='.' read -r major minor patch <<< "$current"
|
|
57
|
+
|
|
58
|
+
bump_type="$BUMP"
|
|
59
|
+
if [ "$bump_type" = "auto" ]; then
|
|
60
|
+
feature_count=$(find docs/changelog -name "*.feature.rst" 2>/dev/null | wc -l)
|
|
61
|
+
breaking_count=$(find docs/changelog -name "*.breaking.rst" 2>/dev/null | wc -l)
|
|
62
|
+
if [ "$breaking_count" -gt 0 ]; then
|
|
63
|
+
bump_type="major"
|
|
64
|
+
echo "Auto-detected: major bump (found $breaking_count breaking changelog(s))"
|
|
65
|
+
elif [ "$feature_count" -gt 0 ]; then
|
|
66
|
+
bump_type="minor"
|
|
67
|
+
echo "Auto-detected: minor bump (found $feature_count feature changelog(s))"
|
|
68
|
+
else
|
|
69
|
+
bump_type="patch"
|
|
70
|
+
echo "Auto-detected: patch bump (no feature changelogs)"
|
|
71
|
+
fi
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
case "$bump_type" in
|
|
75
|
+
major)
|
|
76
|
+
next="$((major + 1)).0.0"
|
|
77
|
+
;;
|
|
78
|
+
minor)
|
|
79
|
+
next="$major.$((minor + 1)).0"
|
|
80
|
+
;;
|
|
81
|
+
patch)
|
|
82
|
+
next="$major.$minor.$((patch + 1))"
|
|
83
|
+
;;
|
|
84
|
+
esac
|
|
85
|
+
|
|
86
|
+
echo "Next version: $next"
|
|
87
|
+
echo "version=$next" >> "$GITHUB_OUTPUT"
|
|
88
|
+
- name: Build release patch
|
|
89
|
+
env:
|
|
90
|
+
VERSION: ${{ steps.version.outputs.version }}
|
|
91
|
+
run: |
|
|
92
|
+
set -euo pipefail
|
|
93
|
+
uvx towncrier build --yes --version "$VERSION"
|
|
94
|
+
uvx --with pre-commit-uv pre-commit run --files docs/changelog.rst ||
|
|
95
|
+
uvx --with pre-commit-uv pre-commit run --files docs/changelog.rst
|
|
96
|
+
git diff HEAD --binary > "$RUNNER_TEMP/release.patch"
|
|
97
|
+
test -s "$RUNNER_TEMP/release.patch"
|
|
98
|
+
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
99
|
+
with:
|
|
100
|
+
name: release-patch
|
|
101
|
+
path: ${{ runner.temp }}/release.patch
|
|
102
|
+
if-no-files-found: error
|
|
103
|
+
push:
|
|
104
|
+
needs: assemble
|
|
105
|
+
runs-on: ubuntu-24.04
|
|
106
|
+
environment: release-auth
|
|
107
|
+
permissions:
|
|
108
|
+
contents: read
|
|
109
|
+
steps:
|
|
110
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
111
|
+
with:
|
|
112
|
+
fetch-depth: 0
|
|
113
|
+
persist-credentials: false
|
|
114
|
+
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
115
|
+
with:
|
|
116
|
+
name: release-patch
|
|
117
|
+
path: ${{ runner.temp }}/release-patch
|
|
118
|
+
- name: Validate release patch
|
|
119
|
+
env:
|
|
120
|
+
VERSION: ${{ needs.assemble.outputs.version }}
|
|
121
|
+
run: |
|
|
122
|
+
set -euo pipefail
|
|
123
|
+
git fetch origin main
|
|
124
|
+
git reset --hard origin/main
|
|
125
|
+
git apply --check "$RUNNER_TEMP/release-patch/release.patch"
|
|
126
|
+
git apply --index "$RUNNER_TEMP/release-patch/release.patch"
|
|
127
|
+
while IFS= read -r path; do
|
|
128
|
+
case "$path" in
|
|
129
|
+
docs/changelog.rst|docs/changelog/*.rst) ;;
|
|
130
|
+
*) echo "::error::unexpected release path: $path"; exit 1 ;;
|
|
131
|
+
esac
|
|
132
|
+
done < <(git diff --cached --name-only)
|
|
133
|
+
grep -qE "^ v${VERSION//./\\.} \(" docs/changelog.rst
|
|
134
|
+
git diff --cached --check
|
|
135
|
+
- id: token
|
|
136
|
+
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
|
|
137
|
+
with:
|
|
138
|
+
client-id: ${{ vars.RELEASE_APP_CLIENT_ID }}
|
|
139
|
+
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
|
|
140
|
+
owner: ${{ github.repository_owner }}
|
|
141
|
+
repositories: ${{ github.event.repository.name }}
|
|
142
|
+
permission-contents: write
|
|
143
|
+
- name: Commit and tag the release
|
|
144
|
+
env:
|
|
145
|
+
VERSION: ${{ needs.assemble.outputs.version }}
|
|
146
|
+
GH_TOKEN: ${{ steps.token.outputs.token }}
|
|
147
|
+
APP_SLUG: ${{ steps.token.outputs.app-slug }}
|
|
148
|
+
run: |
|
|
149
|
+
set -euo pipefail
|
|
150
|
+
app_user_id=$(gh api "/users/${APP_SLUG}[bot]" --jq .id)
|
|
151
|
+
git config user.name "${APP_SLUG}[bot]"
|
|
152
|
+
git config user.email "${app_user_id}+${APP_SLUG}[bot]@users.noreply.github.com"
|
|
153
|
+
git commit -m "release $VERSION"
|
|
154
|
+
git tag -a "$VERSION" -m "release $VERSION"
|
|
155
|
+
gh auth setup-git --hostname github.com --force
|
|
156
|
+
git push --atomic origin HEAD:main "refs/tags/$VERSION"
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
name: Release to PyPI
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
tags: ["*"]
|
|
5
|
+
permissions:
|
|
6
|
+
contents: read
|
|
7
|
+
env:
|
|
8
|
+
dists-artifact-name: python-package-distributions
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
runs-on: ubuntu-26.04
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
14
|
+
with:
|
|
15
|
+
fetch-depth: 0
|
|
16
|
+
persist-credentials: false
|
|
17
|
+
- name: Require a changelog section for the tag
|
|
18
|
+
env:
|
|
19
|
+
TAG: ${{ github.ref_name }}
|
|
20
|
+
run: |
|
|
21
|
+
if [[ ! "$TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
22
|
+
echo "::error::unexpected release tag $TAG"
|
|
23
|
+
exit 1
|
|
24
|
+
fi
|
|
25
|
+
if ! grep -qE "^ v${TAG//./\\.} \\(" docs/changelog.rst; then
|
|
26
|
+
echo "::error::docs/changelog.rst has no section for $TAG"
|
|
27
|
+
exit 1
|
|
28
|
+
fi
|
|
29
|
+
- name: Install the latest version of uv
|
|
30
|
+
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
|
31
|
+
with:
|
|
32
|
+
python-version: "3.15"
|
|
33
|
+
enable-cache: false
|
|
34
|
+
cache-dependency-glob: "pyproject.toml"
|
|
35
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
36
|
+
- name: Build package
|
|
37
|
+
run: uv build --python 3.15 --python-preference only-managed --sdist --wheel . --out-dir dist
|
|
38
|
+
- name: Store the distribution packages
|
|
39
|
+
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
40
|
+
with:
|
|
41
|
+
name: ${{ env.dists-artifact-name }}
|
|
42
|
+
path: dist/*
|
|
43
|
+
release:
|
|
44
|
+
needs:
|
|
45
|
+
- build
|
|
46
|
+
runs-on: ubuntu-26.04
|
|
47
|
+
environment:
|
|
48
|
+
name: release
|
|
49
|
+
url: https://pypi.org/project/python-discovery/${{ github.ref_name }}
|
|
50
|
+
permissions:
|
|
51
|
+
id-token: write
|
|
52
|
+
steps:
|
|
53
|
+
- name: Download all the dists
|
|
54
|
+
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
55
|
+
with:
|
|
56
|
+
name: ${{ env.dists-artifact-name }}
|
|
57
|
+
path: dist/
|
|
58
|
+
- name: Publish to PyPI
|
|
59
|
+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
|
60
|
+
with:
|
|
61
|
+
attestations: true
|
|
62
|
+
github-release:
|
|
63
|
+
needs: release
|
|
64
|
+
runs-on: ubuntu-26.04
|
|
65
|
+
permissions:
|
|
66
|
+
contents: write
|
|
67
|
+
steps:
|
|
68
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
69
|
+
with:
|
|
70
|
+
fetch-depth: 0
|
|
71
|
+
persist-credentials: false
|
|
72
|
+
- name: Publish GitHub Release
|
|
73
|
+
env:
|
|
74
|
+
GH_TOKEN: ${{ github.token }}
|
|
75
|
+
TAG: ${{ github.ref_name }}
|
|
76
|
+
run: |
|
|
77
|
+
set -euo pipefail
|
|
78
|
+
awk -v tag="$TAG" '
|
|
79
|
+
index($0, " v" tag " (") == 1 { found = 1; next }
|
|
80
|
+
found && /^ v[0-9]+\.[0-9]+\.[0-9]+ \(/ { exit }
|
|
81
|
+
found && $0 !~ /^\*+$/ { print }
|
|
82
|
+
' docs/changelog.rst > "$RUNNER_TEMP/release-notes.md"
|
|
83
|
+
python3 - "$RUNNER_TEMP/release-notes.md" <<'PY'
|
|
84
|
+
import re
|
|
85
|
+
import sys
|
|
86
|
+
from pathlib import Path
|
|
87
|
+
|
|
88
|
+
path = Path(sys.argv[1])
|
|
89
|
+
body = path.read_text().strip()
|
|
90
|
+
if not body:
|
|
91
|
+
raise SystemExit("release notes are empty")
|
|
92
|
+
body = re.sub(r":user:`([^`]+)`", r"\1", body)
|
|
93
|
+
body = re.sub(r":(?:issue|pull):`(\d+)`", r"#\1", body)
|
|
94
|
+
body = re.sub(r":[a-z]+:`~?([^`]+)`", lambda match: f"`{match.group(1).split('.')[-1]}`", body)
|
|
95
|
+
path.write_text(body.replace("``", "`") + "\n")
|
|
96
|
+
PY
|
|
97
|
+
test -s "$RUNNER_TEMP/release-notes.md"
|
|
98
|
+
printf '\n[PyPI release](https://pypi.org/project/python-discovery/%s/)\n' "$TAG" >> "$RUNNER_TEMP/release-notes.md"
|
|
99
|
+
previous=$(git tag --list '[0-9]*' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | awk -v tag="$TAG" '$0 == tag { print previous; exit } { previous = $0 }')
|
|
100
|
+
if [[ -n "$previous" ]]; then
|
|
101
|
+
printf '\n**Full Changelog**: https://github.com/%s/compare/%s...%s\n' "$GITHUB_REPOSITORY" "$previous" "$TAG" >> "$RUNNER_TEMP/release-notes.md"
|
|
102
|
+
else
|
|
103
|
+
printf '\n**Full Changelog**: https://github.com/%s/commits/%s\n' "$GITHUB_REPOSITORY" "$TAG" >> "$RUNNER_TEMP/release-notes.md"
|
|
104
|
+
fi
|
|
105
|
+
gh release create "$TAG" --verify-tag --title "v$TAG" --notes-file "$RUNNER_TEMP/release-notes.md"
|
|
@@ -16,11 +16,11 @@ repos:
|
|
|
16
16
|
hooks:
|
|
17
17
|
- id: codespell
|
|
18
18
|
- repo: https://github.com/tox-dev/pyproject-fmt
|
|
19
|
-
rev: "v2.30.
|
|
19
|
+
rev: "v2.30.3"
|
|
20
20
|
hooks:
|
|
21
21
|
- id: pyproject-fmt
|
|
22
22
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
23
|
-
rev: "v0.
|
|
23
|
+
rev: "v0.17.0"
|
|
24
24
|
hooks:
|
|
25
25
|
- id: ruff-format
|
|
26
26
|
- id: ruff
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: python-discovery
|
|
3
|
-
Version: 1.6.
|
|
3
|
+
Version: 1.6.4
|
|
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
|
|
@@ -6,6 +6,26 @@
|
|
|
6
6
|
|
|
7
7
|
.. towncrier release notes start
|
|
8
8
|
|
|
9
|
+
********************
|
|
10
|
+
v1.6.4 (2026-10-19)
|
|
11
|
+
********************
|
|
12
|
+
|
|
13
|
+
Improved documentation - 1.6.4
|
|
14
|
+
==============================
|
|
15
|
+
- Clarify the executable paths accepted by ``try_first_with`` in the standalone usage example. (:issue:`150`)
|
|
16
|
+
|
|
17
|
+
********************
|
|
18
|
+
v1.6.3 (2026-10-16)
|
|
19
|
+
********************
|
|
20
|
+
|
|
21
|
+
Bug fixes - 1.6.3
|
|
22
|
+
=================
|
|
23
|
+
- Treat specs with dot-only version components, such as ``python.``, as literal executable paths instead of raising
|
|
24
|
+
``IndexError`` - by :user:`harbinresearcher`. (:issue:`147`)
|
|
25
|
+
- Pass PEP 514 ``ExecutableArguments`` to registered Windows interpreters when querying them, so a registration that
|
|
26
|
+
points at a launcher such as ``py.exe -3.11`` reports the interpreter it selects - by :user:`blouirts123`.
|
|
27
|
+
(:issue:`148`)
|
|
28
|
+
|
|
9
29
|
********************
|
|
10
30
|
v1.6.2 (2026-10-10)
|
|
11
31
|
********************
|
|
@@ -61,6 +81,14 @@ Improved documentation - 1.5.3
|
|
|
61
81
|
==============================
|
|
62
82
|
- Document the version floors: runs on Python 3.8+, discovers interpreters down to 3.6 - by :user:`gaborbernat`. (:issue:`116`)
|
|
63
83
|
|
|
84
|
+
********************
|
|
85
|
+
v1.5.2 (2026-08-12)
|
|
86
|
+
********************
|
|
87
|
+
|
|
88
|
+
Bug fixes - 1.5.2
|
|
89
|
+
=================
|
|
90
|
+
- Move interpreter interrogation into a Python 3.6-compatible collector. (:issue:`117`)
|
|
91
|
+
|
|
64
92
|
********************
|
|
65
93
|
v1.5.1 (2026-07-31)
|
|
66
94
|
********************
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
How-to guides
|
|
2
2
|
=============
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
Try specific executables first
|
|
5
|
+
------------------------------
|
|
6
6
|
|
|
7
|
-
If you know
|
|
7
|
+
If you know the path to an interpreter executable, pass it via ``try_first_with`` to check it
|
|
8
8
|
before the normal search. This is useful when you have a custom Python install outside the
|
|
9
9
|
standard locations.
|
|
10
10
|
|
|
@@ -12,7 +12,7 @@ standard locations.
|
|
|
12
12
|
|
|
13
13
|
from python_discovery import get_interpreter
|
|
14
14
|
|
|
15
|
-
info = get_interpreter("python3.12", try_first_with=["/opt/python/bin"])
|
|
15
|
+
info = get_interpreter("python3.12", try_first_with=["/opt/python/bin/python3.12"])
|
|
16
16
|
if info is not None:
|
|
17
17
|
print(info.executable)
|
|
18
18
|
|
|
@@ -228,5 +228,4 @@ type = [
|
|
|
228
228
|
{ directory = "doc", name = "Improved documentation", showcontent = true },
|
|
229
229
|
{ directory = "packaging", name = "Packaging updates and notes for downstreams", showcontent = true },
|
|
230
230
|
{ directory = "contrib", name = "Contributor-facing changes", showcontent = true },
|
|
231
|
-
{ directory = "misc", name = "Miscellaneous internal changes", showcontent = true },
|
|
232
231
|
]
|
|
@@ -30,15 +30,16 @@ if TYPE_CHECKING:
|
|
|
30
30
|
from ._cache import ContentStore, PyInfoCache
|
|
31
31
|
|
|
32
32
|
class CacheEntryMeta(TypedDict):
|
|
33
|
-
"""Identity of a cached interrogation result: the queried binary and the script
|
|
33
|
+
"""Identity of a cached interrogation result: the queried binary, its arguments and the script."""
|
|
34
34
|
|
|
35
35
|
st_mtime: float
|
|
36
36
|
path: str
|
|
37
37
|
hash: str | None
|
|
38
|
+
arguments: str | None
|
|
38
39
|
|
|
39
40
|
|
|
40
|
-
_CACHE: OrderedDict[Path, PythonInfo | Exception] = OrderedDict()
|
|
41
|
-
_CACHE[Path(sys.executable)] = PythonInfo()
|
|
41
|
+
_CACHE: OrderedDict[tuple[Path, str | None], PythonInfo | Exception] = OrderedDict()
|
|
42
|
+
_CACHE[Path(sys.executable), None] = PythonInfo()
|
|
42
43
|
_LOGGER: Final[logging.Logger] = logging.getLogger(__name__)
|
|
43
44
|
_PY_INFO_SCRIPT: Final[Path] = Path(__file__).resolve().parent / "_py_info_collect.py"
|
|
44
45
|
_DEFAULT_QUERY_TIMEOUT: Final[float] = 15.0
|
|
@@ -58,9 +59,10 @@ def from_exe( # ruff:ignore[too-many-arguments]
|
|
|
58
59
|
*,
|
|
59
60
|
raise_on_error: bool = True,
|
|
60
61
|
ignore_cache: bool = False,
|
|
62
|
+
executable_args: str | None = None,
|
|
61
63
|
) -> PythonInfo | None:
|
|
62
64
|
env = os.environ if env is None else env
|
|
63
|
-
result = _get_from_cache(cls, cache, exe, env, ignore_cache=ignore_cache)
|
|
65
|
+
result = _get_from_cache(cls, cache, exe, env, ignore_cache=ignore_cache, executable_args=executable_args)
|
|
64
66
|
if isinstance(result, Exception):
|
|
65
67
|
if raise_on_error:
|
|
66
68
|
raise result
|
|
@@ -69,31 +71,34 @@ def from_exe( # ruff:ignore[too-many-arguments]
|
|
|
69
71
|
return result
|
|
70
72
|
|
|
71
73
|
|
|
72
|
-
def _get_from_cache(
|
|
74
|
+
def _get_from_cache( # ruff:ignore[too-many-arguments]
|
|
73
75
|
cls: type[PythonInfo],
|
|
74
76
|
cache: PyInfoCache | None,
|
|
75
77
|
exe: str,
|
|
76
78
|
env: Mapping[str, str],
|
|
77
79
|
*,
|
|
78
80
|
ignore_cache: bool = True,
|
|
81
|
+
executable_args: str | None = None,
|
|
79
82
|
) -> PythonInfo | Exception:
|
|
80
|
-
|
|
81
|
-
if not ignore_cache and
|
|
82
|
-
result = _CACHE[
|
|
83
|
+
key = Path(exe), executable_args
|
|
84
|
+
if not ignore_cache and key in _CACHE:
|
|
85
|
+
result = _CACHE[key]
|
|
83
86
|
else:
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if isinstance(result, PythonInfo):
|
|
87
|
+
result = _CACHE[key] = _get_via_file_cache(cls, cache, key[0], exe, env, executable_args=executable_args)
|
|
88
|
+
# with launcher arguments the queried binary is not the interpreter, so keep the one the query reported
|
|
89
|
+
if isinstance(result, PythonInfo) and executable_args is None:
|
|
87
90
|
result.executable = exe
|
|
88
91
|
return result
|
|
89
92
|
|
|
90
93
|
|
|
91
|
-
def _get_via_file_cache(
|
|
94
|
+
def _get_via_file_cache( # ruff:ignore[too-many-arguments]
|
|
92
95
|
cls: type[PythonInfo],
|
|
93
96
|
cache: PyInfoCache | None,
|
|
94
97
|
path: Path,
|
|
95
98
|
exe: str,
|
|
96
99
|
env: Mapping[str, str],
|
|
100
|
+
*,
|
|
101
|
+
executable_args: str | None = None,
|
|
97
102
|
) -> PythonInfo | Exception:
|
|
98
103
|
path_text = str(path)
|
|
99
104
|
try:
|
|
@@ -107,17 +112,22 @@ def _get_via_file_cache(
|
|
|
107
112
|
|
|
108
113
|
resolved_cache = cache if cache is not None else NoOpCache()
|
|
109
114
|
py_info_store = resolved_cache.py_info(path)
|
|
110
|
-
entry_meta: CacheEntryMeta = {
|
|
115
|
+
entry_meta: CacheEntryMeta = {
|
|
116
|
+
"st_mtime": path_modified,
|
|
117
|
+
"path": path_text,
|
|
118
|
+
"hash": py_info_hash,
|
|
119
|
+
"arguments": executable_args,
|
|
120
|
+
}
|
|
111
121
|
with py_info_store.locked():
|
|
112
122
|
cached = _read_cache_entry(cls, py_info_store, entry_meta)
|
|
113
123
|
if isinstance(cached, _UnsupportedInterpreterError):
|
|
114
124
|
return cached
|
|
115
125
|
py_info = cached
|
|
116
126
|
if py_info is None:
|
|
117
|
-
failure, py_info = _run_subprocess(cls, exe, env)
|
|
127
|
+
failure, py_info = _run_subprocess(cls, exe, env, executable_args)
|
|
118
128
|
if failure is not None and not isinstance(failure, _UnsupportedInterpreterError):
|
|
119
129
|
_LOGGER.debug("first subprocess attempt failed for %s (%s), retrying", exe, failure)
|
|
120
|
-
failure, py_info = _run_subprocess(cls, exe, env)
|
|
130
|
+
failure, py_info = _run_subprocess(cls, exe, env, executable_args)
|
|
121
131
|
if isinstance(failure, _UnsupportedInterpreterError):
|
|
122
132
|
_LOGGER.warning("%s", failure) # the verdict is permanent, warn once and remember it
|
|
123
133
|
py_info_store.write({**entry_meta, "unsupported": str(failure)})
|
|
@@ -214,16 +224,20 @@ def _run_subprocess(
|
|
|
214
224
|
cls: type[PythonInfo],
|
|
215
225
|
exe: str,
|
|
216
226
|
env: Mapping[str, str],
|
|
227
|
+
executable_args: str | None = None,
|
|
217
228
|
) -> tuple[Exception | None, PythonInfo | None]:
|
|
218
229
|
start_cookie = gen_cookie()
|
|
219
230
|
end_cookie = gen_cookie()
|
|
220
231
|
timeout = _query_timeout(env.get("PY_DISCOVERY_TIMEOUT"))
|
|
221
232
|
with _resolve_py_info_script() as py_info_script:
|
|
222
|
-
cmd = [exe, str(py_info_script), start_cookie, end_cookie]
|
|
233
|
+
cmd: list[str] | str = [exe, str(py_info_script), start_cookie, end_cookie]
|
|
234
|
+
if executable_args is not None:
|
|
235
|
+
# PEP 514 ExecutableArguments is raw command line text, placed before the script per the PEP
|
|
236
|
+
cmd = f"{subprocess.list2cmdline(cmd[:1])} {executable_args} {subprocess.list2cmdline(cmd[1:])}"
|
|
223
237
|
env = dict(env)
|
|
224
238
|
env.pop("__PYVENV_LAUNCHER__", None)
|
|
225
239
|
env["PYTHONUTF8"] = "1"
|
|
226
|
-
_LOGGER.debug("get interpreter info via cmd: %s", LogCmd(cmd))
|
|
240
|
+
_LOGGER.debug("get interpreter info via cmd: %s", cmd if isinstance(cmd, str) else LogCmd(cmd))
|
|
227
241
|
try:
|
|
228
242
|
process = Popen( # ruff:ignore[subprocess-without-shell-equals-true]
|
|
229
243
|
cmd,
|
|
@@ -248,7 +262,8 @@ def _run_subprocess(
|
|
|
248
262
|
out, raw_out, out_starts, out_ends = _extract_between_cookies(out, start_cookie, end_cookie)
|
|
249
263
|
try:
|
|
250
264
|
result = cls.from_json(out)
|
|
251
|
-
|
|
265
|
+
if executable_args is None:
|
|
266
|
+
result.executable = exe
|
|
252
267
|
except json.JSONDecodeError as exc:
|
|
253
268
|
_LOGGER.warning(
|
|
254
269
|
"subprocess %s returned invalid JSON; raw stdout %d chars, start cookie %s, end cookie %s, "
|
|
@@ -378,10 +378,41 @@ class PythonInfo: # ruff:ignore[too-many-public-methods]
|
|
|
378
378
|
:param resolve_to_host: resolve through virtualenv layers to the system interpreter.
|
|
379
379
|
:param env: environment mapping; defaults to :data:`os.environ`.
|
|
380
380
|
"""
|
|
381
|
+
return cls._from_exe_with_args(
|
|
382
|
+
exe,
|
|
383
|
+
cache,
|
|
384
|
+
executable_args=None,
|
|
385
|
+
raise_on_error=raise_on_error,
|
|
386
|
+
ignore_cache=ignore_cache,
|
|
387
|
+
resolve_to_host=resolve_to_host,
|
|
388
|
+
env=env,
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
@classmethod
|
|
392
|
+
def _from_exe_with_args( # ruff:ignore[too-many-arguments]
|
|
393
|
+
cls,
|
|
394
|
+
exe: str,
|
|
395
|
+
cache: PyInfoCache | None = None,
|
|
396
|
+
*,
|
|
397
|
+
executable_args: str | None,
|
|
398
|
+
raise_on_error: bool = True,
|
|
399
|
+
ignore_cache: bool = False,
|
|
400
|
+
resolve_to_host: bool = True,
|
|
401
|
+
env: Mapping[str, str] | None = None,
|
|
402
|
+
) -> PythonInfo | None:
|
|
403
|
+
"""Interrogate an executable with optional platform-specific leading arguments."""
|
|
381
404
|
from ._cached_py_info import from_exe # ruff:ignore[import-outside-top-level]
|
|
382
405
|
|
|
383
406
|
env = os.environ if env is None else env
|
|
384
|
-
proposed = from_exe(
|
|
407
|
+
proposed = from_exe(
|
|
408
|
+
cls,
|
|
409
|
+
cache,
|
|
410
|
+
exe,
|
|
411
|
+
env=env,
|
|
412
|
+
raise_on_error=raise_on_error,
|
|
413
|
+
ignore_cache=ignore_cache,
|
|
414
|
+
executable_args=executable_args,
|
|
415
|
+
)
|
|
385
416
|
|
|
386
417
|
if isinstance(proposed, PythonInfo) and resolve_to_host:
|
|
387
418
|
try:
|
|
@@ -50,6 +50,9 @@ def _int_or_none(val: str | None) -> int | None:
|
|
|
50
50
|
|
|
51
51
|
def _parse_version_parts(version: str) -> tuple[int | None, int | None, int | None]:
|
|
52
52
|
versions = tuple(int(i) for i in version.split(".") if i)
|
|
53
|
+
if not versions:
|
|
54
|
+
msg = "version must contain a numeric component"
|
|
55
|
+
raise ValueError(msg)
|
|
53
56
|
if len(versions) > _MAX_VERSION_PARTS:
|
|
54
57
|
msg = "too many version parts"
|
|
55
58
|
raise ValueError(msg)
|
|
@@ -125,7 +125,10 @@ def load_exe(hive_name: str, company: str, company_key: Any, tag: str) -> tuple[
|
|
|
125
125
|
try:
|
|
126
126
|
with winreg.OpenKeyEx(company_key, rf"{tag}\InstallPath") as ip_key, ip_key: # ty: ignore[unresolved-attribute]
|
|
127
127
|
if (exe := _resolve_exe(ip_key, key_path)) is not None and os.path.exists(exe):
|
|
128
|
-
|
|
128
|
+
if (args := get_value(ip_key, "ExecutableArguments")) is None or isinstance(args, str):
|
|
129
|
+
return exe, args if args and args.strip() else None
|
|
130
|
+
msg(f"{key_path}/ExecutableArguments", f"expected a string, got {type(args).__name__}")
|
|
131
|
+
return None
|
|
129
132
|
msg(key_path, f"could not load exe with value {exe}")
|
|
130
133
|
except OSError:
|
|
131
134
|
msg(f"{key_path}/InstallPath", "missing")
|
|
@@ -36,13 +36,19 @@ def propose_interpreters(
|
|
|
36
36
|
reverse=True,
|
|
37
37
|
)
|
|
38
38
|
|
|
39
|
-
for name, major, minor, arch, threaded, exe,
|
|
39
|
+
for name, major, minor, arch, threaded, exe, executable_args in existing:
|
|
40
40
|
implementation = _IMPLEMENTATION_BY_ORG.get(name, name)
|
|
41
41
|
|
|
42
42
|
skip_pre_filter = implementation.lower() != "cpython"
|
|
43
43
|
registry_spec = PythonSpec("", implementation, major, minor, None, arch, exe, free_threaded=threaded)
|
|
44
44
|
if skip_pre_filter or registry_spec.satisfies(spec):
|
|
45
|
-
interpreter = Pep514PythonInfo.
|
|
45
|
+
interpreter = Pep514PythonInfo._from_exe_with_args( # ruff:ignore[private-member-access]
|
|
46
|
+
exe,
|
|
47
|
+
cache,
|
|
48
|
+
env=env,
|
|
49
|
+
executable_args=executable_args,
|
|
50
|
+
raise_on_error=False,
|
|
51
|
+
)
|
|
46
52
|
if interpreter is not None and interpreter.satisfies(spec, impl_must_match=True):
|
|
47
53
|
yield interpreter
|
|
48
54
|
|