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.
Files changed (66) hide show
  1. {python_discovery-1.6.2 → python_discovery-1.6.4}/.github/dependabot.yaml +4 -0
  2. {python_discovery-1.6.2 → python_discovery-1.6.4}/.github/workflows/check.yaml +32 -2
  3. python_discovery-1.6.4/.github/workflows/prepare-release.yaml +156 -0
  4. python_discovery-1.6.4/.github/workflows/release.yaml +105 -0
  5. {python_discovery-1.6.2 → python_discovery-1.6.4}/.pre-commit-config.yaml +2 -2
  6. {python_discovery-1.6.2 → python_discovery-1.6.4}/PKG-INFO +1 -1
  7. {python_discovery-1.6.2 → python_discovery-1.6.4}/docs/changelog.rst +28 -0
  8. {python_discovery-1.6.2 → python_discovery-1.6.4}/docs/how-to/standalone-usage.rst +4 -4
  9. {python_discovery-1.6.2 → python_discovery-1.6.4}/pyproject.toml +0 -1
  10. {python_discovery-1.6.2 → python_discovery-1.6.4}/src/python_discovery/_cached_py_info.py +33 -18
  11. {python_discovery-1.6.2 → python_discovery-1.6.4}/src/python_discovery/_py_info.py +32 -1
  12. {python_discovery-1.6.2 → python_discovery-1.6.4}/src/python_discovery/_py_spec.py +3 -0
  13. {python_discovery-1.6.2 → python_discovery-1.6.4}/src/python_discovery/_windows/_pep514.py +4 -1
  14. {python_discovery-1.6.2 → python_discovery-1.6.4}/src/python_discovery/_windows/_propose.py +8 -2
  15. python_discovery-1.6.4/tasks/release.py +82 -0
  16. {python_discovery-1.6.2 → python_discovery-1.6.4}/tests/test_cached_py_info.py +67 -2
  17. {python_discovery-1.6.2 → python_discovery-1.6.4}/tests/test_discovery.py +3 -2
  18. {python_discovery-1.6.2 → python_discovery-1.6.4}/tests/test_py_spec.py +2 -2
  19. {python_discovery-1.6.2 → python_discovery-1.6.4}/tests/windows/conftest.py +1 -1
  20. {python_discovery-1.6.2 → python_discovery-1.6.4}/tests/windows/test_windows_pep514.py +75 -0
  21. python_discovery-1.6.2/.github/workflows/prepare-release.yaml +0 -92
  22. python_discovery-1.6.2/.github/workflows/release.yaml +0 -49
  23. python_discovery-1.6.2/tasks/release.py +0 -173
  24. {python_discovery-1.6.2 → python_discovery-1.6.4}/.github/FUNDING.yaml +0 -0
  25. {python_discovery-1.6.2 → python_discovery-1.6.4}/.github/SECURITY.md +0 -0
  26. {python_discovery-1.6.2 → python_discovery-1.6.4}/.github/release.yaml +0 -0
  27. {python_discovery-1.6.2 → python_discovery-1.6.4}/.gitignore +0 -0
  28. {python_discovery-1.6.2 → python_discovery-1.6.4}/.readthedocs.yaml +0 -0
  29. {python_discovery-1.6.2 → python_discovery-1.6.4}/.readthedocs.yml +0 -0
  30. {python_discovery-1.6.2 → python_discovery-1.6.4}/CODE_OF_CONDUCT.md +0 -0
  31. {python_discovery-1.6.2 → python_discovery-1.6.4}/LICENSE +0 -0
  32. {python_discovery-1.6.2 → python_discovery-1.6.4}/README.md +0 -0
  33. {python_discovery-1.6.2 → python_discovery-1.6.4}/docs/_static/custom.css +0 -0
  34. {python_discovery-1.6.2 → python_discovery-1.6.4}/docs/_static/logo.svg +0 -0
  35. {python_discovery-1.6.2 → python_discovery-1.6.4}/docs/changelog/template.jinja2 +0 -0
  36. {python_discovery-1.6.2 → python_discovery-1.6.4}/docs/conf.py +0 -0
  37. {python_discovery-1.6.2 → python_discovery-1.6.4}/docs/explanation.rst +0 -0
  38. {python_discovery-1.6.2 → python_discovery-1.6.4}/docs/index.rst +0 -0
  39. {python_discovery-1.6.2 → python_discovery-1.6.4}/docs/reference/api.rst +0 -0
  40. {python_discovery-1.6.2 → python_discovery-1.6.4}/docs/reference/environment-variables.rst +0 -0
  41. {python_discovery-1.6.2 → python_discovery-1.6.4}/docs/tutorial/getting-started.rst +0 -0
  42. {python_discovery-1.6.2 → python_discovery-1.6.4}/src/python_discovery/__init__.py +0 -0
  43. {python_discovery-1.6.2 → python_discovery-1.6.4}/src/python_discovery/_cache.py +0 -0
  44. {python_discovery-1.6.2 → python_discovery-1.6.4}/src/python_discovery/_compat.py +0 -0
  45. {python_discovery-1.6.2 → python_discovery-1.6.4}/src/python_discovery/_discovery.py +0 -0
  46. {python_discovery-1.6.2 → python_discovery-1.6.4}/src/python_discovery/_py_info_collect.py +0 -0
  47. {python_discovery-1.6.2 → python_discovery-1.6.4}/src/python_discovery/_specifier.py +0 -0
  48. {python_discovery-1.6.2 → python_discovery-1.6.4}/src/python_discovery/_windows/__init__.py +0 -0
  49. {python_discovery-1.6.2 → python_discovery-1.6.4}/src/python_discovery/py.typed +0 -0
  50. {python_discovery-1.6.2 → python_discovery-1.6.4}/tasks/old_target_check.py +0 -0
  51. {python_discovery-1.6.2 → python_discovery-1.6.4}/tasks/old_target_check.sh +0 -0
  52. {python_discovery-1.6.2 → python_discovery-1.6.4}/tests/conftest.py +0 -0
  53. {python_discovery-1.6.2 → python_discovery-1.6.4}/tests/py_info/test_py_info.py +0 -0
  54. {python_discovery-1.6.2 → python_discovery-1.6.4}/tests/py_info/test_py_info_exe_based_of.py +0 -0
  55. {python_discovery-1.6.2 → python_discovery-1.6.4}/tests/test_cache.py +0 -0
  56. {python_discovery-1.6.2 → python_discovery-1.6.4}/tests/test_cache_recovery.py +0 -0
  57. {python_discovery-1.6.2 → python_discovery-1.6.4}/tests/test_discovery_extra.py +0 -0
  58. {python_discovery-1.6.2 → python_discovery-1.6.4}/tests/test_iter_interpreters.py +0 -0
  59. {python_discovery-1.6.2 → python_discovery-1.6.4}/tests/test_py_info_collect.py +0 -0
  60. {python_discovery-1.6.2 → python_discovery-1.6.4}/tests/test_py_info_extra.py +0 -0
  61. {python_discovery-1.6.2 → python_discovery-1.6.4}/tests/test_py_spec_extra.py +0 -0
  62. {python_discovery-1.6.2 → python_discovery-1.6.4}/tests/test_query_timeout.py +0 -0
  63. {python_discovery-1.6.2 → python_discovery-1.6.4}/tests/test_specifier.py +0 -0
  64. {python_discovery-1.6.2 → python_discovery-1.6.4}/tests/windows/test_windows.py +0 -0
  65. {python_discovery-1.6.2 → python_discovery-1.6.4}/tests/windows/winreg_mock_values.py +0 -0
  66. {python_discovery-1.6.2 → python_discovery-1.6.4}/tox.toml +0 -0
@@ -2,6 +2,10 @@ version: 2
2
2
  updates:
3
3
  - package-ecosystem: "github-actions"
4
4
  directory: "/"
5
+ labels:
6
+ - dependencies
7
+ - github_actions
8
+ - skip news
5
9
  schedule:
6
10
  interval: "weekly"
7
11
  day: "tuesday"
@@ -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@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
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@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
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.1"
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.16.10"
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.2
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
- Search specific directories first
5
- -----------------------------------
4
+ Try specific executables first
5
+ ------------------------------
6
6
 
7
- If you know a likely location for the interpreter, pass it via ``try_first_with`` to check there
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 that produced it."""
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
- exe_path = Path(exe)
81
- if not ignore_cache and exe_path in _CACHE:
82
- result = _CACHE[exe_path]
83
+ key = Path(exe), executable_args
84
+ if not ignore_cache and key in _CACHE:
85
+ result = _CACHE[key]
83
86
  else:
84
- py_info = _get_via_file_cache(cls, cache, exe_path, exe, env)
85
- result = _CACHE[exe_path] = py_info
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 = {"st_mtime": path_modified, "path": path_text, "hash": py_info_hash}
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
- result.executable = exe
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(cls, cache, exe, env=env, raise_on_error=raise_on_error, ignore_cache=ignore_cache)
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
- return exe, get_value(ip_key, "ExecutableArguments")
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, _ in existing:
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.from_exe(exe, cache, env=env, raise_on_error=False)
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