python-discovery 1.6.1__tar.gz → 1.6.3__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.1 → python_discovery-1.6.3}/.github/workflows/check.yaml +19 -17
- {python_discovery-1.6.1 → python_discovery-1.6.3}/.github/workflows/prepare-release.yaml +3 -2
- {python_discovery-1.6.1 → python_discovery-1.6.3}/.github/workflows/release.yaml +7 -6
- {python_discovery-1.6.1 → python_discovery-1.6.3}/.pre-commit-config.yaml +5 -4
- {python_discovery-1.6.1 → python_discovery-1.6.3}/.readthedocs.yaml +1 -1
- python_discovery-1.6.3/.readthedocs.yml +8 -0
- {python_discovery-1.6.1 → python_discovery-1.6.3}/PKG-INFO +4 -5
- {python_discovery-1.6.1 → python_discovery-1.6.3}/README.md +1 -1
- python_discovery-1.6.3/docs/changelog/147.bugfix.rst +2 -0
- python_discovery-1.6.3/docs/changelog/148.bugfix.rst +2 -0
- {python_discovery-1.6.1 → python_discovery-1.6.3}/docs/changelog.rst +11 -0
- {python_discovery-1.6.1 → python_discovery-1.6.3}/docs/explanation.rst +3 -2
- {python_discovery-1.6.1 → python_discovery-1.6.3}/docs/reference/environment-variables.rst +4 -0
- {python_discovery-1.6.1 → python_discovery-1.6.3}/pyproject.toml +11 -10
- {python_discovery-1.6.1 → python_discovery-1.6.3}/src/python_discovery/_cached_py_info.py +58 -24
- {python_discovery-1.6.1 → python_discovery-1.6.3}/src/python_discovery/_py_info.py +32 -1
- {python_discovery-1.6.1 → python_discovery-1.6.3}/src/python_discovery/_py_info_collect.py +1 -1
- {python_discovery-1.6.1 → python_discovery-1.6.3}/src/python_discovery/_py_spec.py +3 -0
- {python_discovery-1.6.1 → python_discovery-1.6.3}/src/python_discovery/_windows/_pep514.py +4 -1
- {python_discovery-1.6.1 → python_discovery-1.6.3}/src/python_discovery/_windows/_propose.py +8 -2
- python_discovery-1.6.3/tests/test_cache_recovery.py +74 -0
- {python_discovery-1.6.1 → python_discovery-1.6.3}/tests/test_cached_py_info.py +67 -13
- {python_discovery-1.6.1 → python_discovery-1.6.3}/tests/test_discovery.py +3 -2
- {python_discovery-1.6.1 → python_discovery-1.6.3}/tests/test_py_info_collect.py +2 -2
- {python_discovery-1.6.1 → python_discovery-1.6.3}/tests/test_py_info_extra.py +1 -1
- {python_discovery-1.6.1 → python_discovery-1.6.3}/tests/test_py_spec.py +2 -2
- python_discovery-1.6.3/tests/test_query_timeout.py +88 -0
- {python_discovery-1.6.1 → python_discovery-1.6.3}/tests/windows/conftest.py +3 -3
- {python_discovery-1.6.1 → python_discovery-1.6.3}/tests/windows/test_windows_pep514.py +75 -0
- python_discovery-1.6.3/tox.toml +89 -0
- python_discovery-1.6.1/.readthedocs.yml +0 -8
- python_discovery-1.6.1/tox.toml +0 -85
- {python_discovery-1.6.1 → python_discovery-1.6.3}/.github/FUNDING.yaml +0 -0
- {python_discovery-1.6.1 → python_discovery-1.6.3}/.github/SECURITY.md +0 -0
- {python_discovery-1.6.1 → python_discovery-1.6.3}/.github/dependabot.yaml +0 -0
- {python_discovery-1.6.1 → python_discovery-1.6.3}/.github/release.yaml +0 -0
- {python_discovery-1.6.1 → python_discovery-1.6.3}/.gitignore +0 -0
- {python_discovery-1.6.1 → python_discovery-1.6.3}/CODE_OF_CONDUCT.md +0 -0
- {python_discovery-1.6.1 → python_discovery-1.6.3}/LICENSE +0 -0
- {python_discovery-1.6.1 → python_discovery-1.6.3}/docs/_static/custom.css +0 -0
- {python_discovery-1.6.1 → python_discovery-1.6.3}/docs/_static/logo.svg +0 -0
- {python_discovery-1.6.1 → python_discovery-1.6.3}/docs/changelog/template.jinja2 +0 -0
- {python_discovery-1.6.1 → python_discovery-1.6.3}/docs/conf.py +0 -0
- {python_discovery-1.6.1 → python_discovery-1.6.3}/docs/how-to/standalone-usage.rst +0 -0
- {python_discovery-1.6.1 → python_discovery-1.6.3}/docs/index.rst +0 -0
- {python_discovery-1.6.1 → python_discovery-1.6.3}/docs/reference/api.rst +0 -0
- {python_discovery-1.6.1 → python_discovery-1.6.3}/docs/tutorial/getting-started.rst +0 -0
- {python_discovery-1.6.1 → python_discovery-1.6.3}/src/python_discovery/__init__.py +0 -0
- {python_discovery-1.6.1 → python_discovery-1.6.3}/src/python_discovery/_cache.py +0 -0
- {python_discovery-1.6.1 → python_discovery-1.6.3}/src/python_discovery/_compat.py +0 -0
- {python_discovery-1.6.1 → python_discovery-1.6.3}/src/python_discovery/_discovery.py +0 -0
- {python_discovery-1.6.1 → python_discovery-1.6.3}/src/python_discovery/_specifier.py +0 -0
- {python_discovery-1.6.1 → python_discovery-1.6.3}/src/python_discovery/_windows/__init__.py +0 -0
- {python_discovery-1.6.1 → python_discovery-1.6.3}/src/python_discovery/py.typed +0 -0
- {python_discovery-1.6.1 → python_discovery-1.6.3}/tasks/old_target_check.py +0 -0
- {python_discovery-1.6.1 → python_discovery-1.6.3}/tasks/old_target_check.sh +0 -0
- {python_discovery-1.6.1 → python_discovery-1.6.3}/tasks/release.py +0 -0
- {python_discovery-1.6.1 → python_discovery-1.6.3}/tests/conftest.py +0 -0
- {python_discovery-1.6.1 → python_discovery-1.6.3}/tests/py_info/test_py_info.py +0 -0
- {python_discovery-1.6.1 → python_discovery-1.6.3}/tests/py_info/test_py_info_exe_based_of.py +0 -0
- {python_discovery-1.6.1 → python_discovery-1.6.3}/tests/test_cache.py +0 -0
- {python_discovery-1.6.1 → python_discovery-1.6.3}/tests/test_discovery_extra.py +0 -0
- {python_discovery-1.6.1 → python_discovery-1.6.3}/tests/test_iter_interpreters.py +0 -0
- {python_discovery-1.6.1 → python_discovery-1.6.3}/tests/test_py_spec_extra.py +0 -0
- {python_discovery-1.6.1 → python_discovery-1.6.3}/tests/test_specifier.py +0 -0
- {python_discovery-1.6.1 → python_discovery-1.6.3}/tests/windows/test_windows.py +0 -0
- {python_discovery-1.6.1 → python_discovery-1.6.3}/tests/windows/winreg_mock_values.py +0 -0
|
@@ -14,7 +14,7 @@ permissions:
|
|
|
14
14
|
contents: read
|
|
15
15
|
jobs:
|
|
16
16
|
test:
|
|
17
|
-
runs-on: ubuntu-
|
|
17
|
+
runs-on: ubuntu-26.04
|
|
18
18
|
strategy:
|
|
19
19
|
fail-fast: false
|
|
20
20
|
matrix:
|
|
@@ -27,9 +27,8 @@ jobs:
|
|
|
27
27
|
- "3.11"
|
|
28
28
|
- "3.10"
|
|
29
29
|
- "3.9"
|
|
30
|
-
-
|
|
31
|
-
- type-3.
|
|
32
|
-
- type-3.14
|
|
30
|
+
- type-3.9
|
|
31
|
+
- type-3.15
|
|
33
32
|
- dev
|
|
34
33
|
- pkg_meta
|
|
35
34
|
steps:
|
|
@@ -38,15 +37,16 @@ jobs:
|
|
|
38
37
|
fetch-depth: 0
|
|
39
38
|
persist-credentials: false
|
|
40
39
|
- name: Install the latest version of uv
|
|
41
|
-
uses: astral-sh/setup-uv@
|
|
40
|
+
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
|
42
41
|
with:
|
|
42
|
+
python-version: "3.15"
|
|
43
43
|
enable-cache: false
|
|
44
44
|
cache-dependency-glob: "pyproject.toml"
|
|
45
45
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
46
46
|
- name: Install tox
|
|
47
|
-
run: uv tool install --python-preference only-managed --python 3.
|
|
47
|
+
run: uv tool install --python-preference only-managed --python 3.15 tox --with tox-uv
|
|
48
48
|
- name: Install Python
|
|
49
|
-
if: startsWith(matrix.env, '3.') && matrix.env != '3.
|
|
49
|
+
if: startsWith(matrix.env, '3.') && matrix.env != '3.15'
|
|
50
50
|
run: uv python install --python-preference only-managed ${{ matrix.env }}
|
|
51
51
|
- name: Setup test suite
|
|
52
52
|
run: tox run -vv --notest --skip-missing-interpreters false -e ${{ matrix.env }}
|
|
@@ -56,25 +56,27 @@ jobs:
|
|
|
56
56
|
PYTEST_ADDOPTS: "-vv --durations=20"
|
|
57
57
|
DIFF_AGAINST: HEAD
|
|
58
58
|
old-target:
|
|
59
|
-
runs-on: ubuntu-
|
|
59
|
+
runs-on: ubuntu-26.04
|
|
60
60
|
steps:
|
|
61
61
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
62
62
|
with:
|
|
63
63
|
fetch-depth: 0
|
|
64
64
|
persist-credentials: false
|
|
65
65
|
- name: Install the latest version of uv
|
|
66
|
-
uses: astral-sh/setup-uv@
|
|
66
|
+
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
|
67
67
|
with:
|
|
68
|
+
python-version: "3.15"
|
|
68
69
|
enable-cache: false
|
|
69
70
|
cache-dependency-glob: "pyproject.toml"
|
|
70
71
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
71
72
|
- name: Build the wheel
|
|
72
73
|
run: uv build --wheel --out-dir dist .
|
|
73
|
-
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
74
|
+
- parallel:
|
|
75
|
+
- name: Discover Python 3.6
|
|
76
|
+
run: tasks/old_target_check.sh 3.6 discover
|
|
77
|
+
- name: Discover Python 3.7
|
|
78
|
+
run: tasks/old_target_check.sh 3.7 discover
|
|
79
|
+
- name: Reject Python 3.5
|
|
80
|
+
run: tasks/old_target_check.sh 3.5 rejected
|
|
81
|
+
- name: Reject Python 2.7
|
|
82
|
+
run: tasks/old_target_check.sh 2.7 rejected
|
|
@@ -29,8 +29,9 @@ jobs:
|
|
|
29
29
|
token: ${{ secrets.RELEASE_PAT }}
|
|
30
30
|
persist-credentials: true
|
|
31
31
|
- name: Install the latest version of uv
|
|
32
|
-
uses: astral-sh/setup-uv@
|
|
32
|
+
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
|
33
33
|
with:
|
|
34
|
+
python-version: "3.15"
|
|
34
35
|
enable-cache: true
|
|
35
36
|
cache-dependency-glob: "pyproject.toml"
|
|
36
37
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -79,7 +80,7 @@ jobs:
|
|
|
79
80
|
echo "Next version: $next"
|
|
80
81
|
echo "version=$next" >> $GITHUB_OUTPUT
|
|
81
82
|
- name: Install tox
|
|
82
|
-
run: uv tool install --python-preference only-managed --python 3.
|
|
83
|
+
run: uv tool install --python-preference only-managed --python 3.15 tox@latest --with tox-uv
|
|
83
84
|
- name: Run release process
|
|
84
85
|
run: tox run -e release -- ${STEPS_VERSION_OUTPUTS_VERSION}
|
|
85
86
|
env:
|
|
@@ -8,29 +8,30 @@ env:
|
|
|
8
8
|
dists-artifact-name: python-package-distributions
|
|
9
9
|
jobs:
|
|
10
10
|
build:
|
|
11
|
-
runs-on: ubuntu-
|
|
11
|
+
runs-on: ubuntu-26.04
|
|
12
12
|
steps:
|
|
13
13
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
14
14
|
with:
|
|
15
15
|
fetch-depth: 0
|
|
16
16
|
persist-credentials: false
|
|
17
17
|
- name: Install the latest version of uv
|
|
18
|
-
uses: astral-sh/setup-uv@
|
|
18
|
+
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
|
19
19
|
with:
|
|
20
|
+
python-version: "3.15"
|
|
20
21
|
enable-cache: false
|
|
21
22
|
cache-dependency-glob: "pyproject.toml"
|
|
22
23
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
23
24
|
- name: Build package
|
|
24
|
-
run: uv build --python 3.
|
|
25
|
+
run: uv build --python 3.15 --python-preference only-managed --sdist --wheel . --out-dir dist
|
|
25
26
|
- name: Store the distribution packages
|
|
26
|
-
uses: actions/upload-artifact@
|
|
27
|
+
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
27
28
|
with:
|
|
28
29
|
name: ${{ env.dists-artifact-name }}
|
|
29
30
|
path: dist/*
|
|
30
31
|
release:
|
|
31
32
|
needs:
|
|
32
33
|
- build
|
|
33
|
-
runs-on: ubuntu-
|
|
34
|
+
runs-on: ubuntu-26.04
|
|
34
35
|
environment:
|
|
35
36
|
name: release
|
|
36
37
|
url: https://pypi.org/project/python-discovery/${{ github.ref_name }}
|
|
@@ -38,7 +39,7 @@ jobs:
|
|
|
38
39
|
id-token: write
|
|
39
40
|
steps:
|
|
40
41
|
- name: Download all the dists
|
|
41
|
-
uses: actions/download-artifact@
|
|
42
|
+
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
42
43
|
with:
|
|
43
44
|
name: ${{ env.dists-artifact-name }}
|
|
44
45
|
path: dist/
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
default_language_version:
|
|
2
|
+
python: python3.15
|
|
1
3
|
repos:
|
|
2
4
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
3
5
|
rev: v6.0.0
|
|
@@ -5,7 +7,7 @@ repos:
|
|
|
5
7
|
- id: end-of-file-fixer
|
|
6
8
|
- id: trailing-whitespace
|
|
7
9
|
- repo: https://github.com/python-jsonschema/check-jsonschema
|
|
8
|
-
rev: 0.38.
|
|
10
|
+
rev: 0.38.2
|
|
9
11
|
hooks:
|
|
10
12
|
- id: check-github-workflows
|
|
11
13
|
args: ["--verbose"]
|
|
@@ -13,13 +15,12 @@ repos:
|
|
|
13
15
|
rev: v2.4.3
|
|
14
16
|
hooks:
|
|
15
17
|
- id: codespell
|
|
16
|
-
additional_dependencies: ["tomli>=2.4"]
|
|
17
18
|
- repo: https://github.com/tox-dev/pyproject-fmt
|
|
18
|
-
rev: "v2.
|
|
19
|
+
rev: "v2.30.3"
|
|
19
20
|
hooks:
|
|
20
21
|
- id: pyproject-fmt
|
|
21
22
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
22
|
-
rev: "v0.
|
|
23
|
+
rev: "v0.17.0"
|
|
23
24
|
hooks:
|
|
24
25
|
- id: ruff-format
|
|
25
26
|
- id: ruff
|
|
@@ -4,5 +4,5 @@ build:
|
|
|
4
4
|
tools: {}
|
|
5
5
|
commands:
|
|
6
6
|
- curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
7
|
-
- ~/.local/bin/uv tool install tox --with tox-uv -p 3.
|
|
7
|
+
- ~/.local/bin/uv tool install tox --with tox-uv -p 3.15 --managed-python
|
|
8
8
|
- ~/.local/bin/tox run -e docs -- "$READTHEDOCS_OUTPUT/html"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: python-discovery
|
|
3
|
-
Version: 1.6.
|
|
3
|
+
Version: 1.6.3
|
|
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
|
|
@@ -35,7 +35,6 @@ Classifier: Operating System :: MacOS :: MacOS X
|
|
|
35
35
|
Classifier: Operating System :: Microsoft :: Windows
|
|
36
36
|
Classifier: Operating System :: POSIX
|
|
37
37
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
38
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
39
38
|
Classifier: Programming Language :: Python :: 3.9
|
|
40
39
|
Classifier: Programming Language :: Python :: 3.10
|
|
41
40
|
Classifier: Programming Language :: Python :: 3.11
|
|
@@ -46,8 +45,8 @@ Classifier: Programming Language :: Python :: 3.15
|
|
|
46
45
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
47
46
|
Classifier: Topic :: Software Development :: Libraries
|
|
48
47
|
Classifier: Topic :: Utilities
|
|
49
|
-
Requires-Python: >=3.
|
|
50
|
-
Requires-Dist: filelock>=3.
|
|
48
|
+
Requires-Python: >=3.9
|
|
49
|
+
Requires-Dist: filelock>=3.16.1
|
|
51
50
|
Description-Content-Type: text/markdown
|
|
52
51
|
|
|
53
52
|
# [`python-discovery`](https://python-discovery.readthedocs.io/en/latest/)
|
|
@@ -68,7 +67,7 @@ the right one for you.
|
|
|
68
67
|
Give it a requirement like `python3.12` or `>=3.11,<3.13`, and it searches all known locations, verifies each candidate,
|
|
69
68
|
and returns detailed metadata about the match. Results are cached to disk so repeated lookups are fast.
|
|
70
69
|
|
|
71
|
-
The library runs on Python 3.
|
|
70
|
+
The library runs on Python 3.9+ and discovers interpreters as old as Python 3.6; anything older is skipped with a
|
|
72
71
|
warning naming the version it found.
|
|
73
72
|
|
|
74
73
|
## Usage
|
|
@@ -16,7 +16,7 @@ the right one for you.
|
|
|
16
16
|
Give it a requirement like `python3.12` or `>=3.11,<3.13`, and it searches all known locations, verifies each candidate,
|
|
17
17
|
and returns detailed metadata about the match. Results are cached to disk so repeated lookups are fast.
|
|
18
18
|
|
|
19
|
-
The library runs on Python 3.
|
|
19
|
+
The library runs on Python 3.9+ and discovers interpreters as old as Python 3.6; anything older is skipped with a
|
|
20
20
|
warning naming the version it found.
|
|
21
21
|
|
|
22
22
|
## Usage
|
|
@@ -6,6 +6,17 @@
|
|
|
6
6
|
|
|
7
7
|
.. towncrier release notes start
|
|
8
8
|
|
|
9
|
+
********************
|
|
10
|
+
v1.6.2 (2026-10-10)
|
|
11
|
+
********************
|
|
12
|
+
|
|
13
|
+
Bug fixes - 1.6.2
|
|
14
|
+
=================
|
|
15
|
+
- Discard malformed interpreter cache entries and query the interpreter again, including entries with missing,
|
|
16
|
+
non-string or unusable ``system_executable`` paths - by :user:`darrenhuai`. (:issue:`141`)
|
|
17
|
+
- Use the default interpreter query timeout and warn for invalid ``PY_DISCOVERY_TIMEOUT`` values. Accept ``inf``
|
|
18
|
+
or values above the subprocess wait limit to disable the timeout - by :user:`darrenhuai`. (:issue:`143`)
|
|
19
|
+
|
|
9
20
|
********************
|
|
10
21
|
v1.6.1 (2026-09-18)
|
|
11
22
|
********************
|
|
@@ -275,12 +275,13 @@ If your system consistently hits timeouts, you can customize the timeout via the
|
|
|
275
275
|
|
|
276
276
|
The timeout applies to each individual interpreter being queried. If you set a value that is too low,
|
|
277
277
|
legitimate interpreters may be skipped; if too high, the discovery process may take longer to fail
|
|
278
|
-
when encountering problematic interpreters.
|
|
278
|
+
when encountering problematic interpreters. For invalid values, python-discovery logs a warning and uses
|
|
279
|
+
the default of 15 seconds. Set ``inf`` to disable the timeout.
|
|
279
280
|
|
|
280
281
|
Supported Python versions
|
|
281
282
|
-------------------------
|
|
282
283
|
|
|
283
|
-
Two floors apply: python-discovery itself runs on Python 3.
|
|
284
|
+
Two floors apply: python-discovery itself runs on Python 3.9+ (``requires-python``), while discovery
|
|
284
285
|
reaches down to Python 3.6 - old enough to cover RHEL 8's system Python.
|
|
285
286
|
|
|
286
287
|
python-discovery skips candidates below 3.6 with one warning naming the executable, the version
|
|
@@ -36,3 +36,7 @@ Setting this variable extends the allowed time for each interpreter query.
|
|
|
36
36
|
- Setting the value too low may skip legitimate interpreters
|
|
37
37
|
- Setting it too high increases discovery time when encountering problematic interpreters
|
|
38
38
|
- The value is read from the environment dict passed to :func:`~python_discovery.get_interpreter`
|
|
39
|
+
- For invalid values, including empty strings, ``nan``, ``0`` and negative numbers, python-discovery logs a warning
|
|
40
|
+
and uses the default of 15 seconds
|
|
41
|
+
- ``inf`` or a value above 2,147,483.647 seconds (about 24.9 days) disables the timeout. This limit applies on all
|
|
42
|
+
platforms because some subprocess waits accept a signed 32-bit count of milliseconds
|
|
@@ -19,7 +19,7 @@ license.file = "LICENSE"
|
|
|
19
19
|
maintainers = [
|
|
20
20
|
{ name = "Bernát Gábor", email = "gaborjbernat@gmail.com" },
|
|
21
21
|
]
|
|
22
|
-
requires-python = ">=3.
|
|
22
|
+
requires-python = ">=3.9"
|
|
23
23
|
classifiers = [
|
|
24
24
|
"Development Status :: 5 - Production/Stable",
|
|
25
25
|
"Intended Audience :: Developers",
|
|
@@ -28,7 +28,6 @@ classifiers = [
|
|
|
28
28
|
"Operating System :: Microsoft :: Windows",
|
|
29
29
|
"Operating System :: POSIX",
|
|
30
30
|
"Programming Language :: Python :: 3 :: Only",
|
|
31
|
-
"Programming Language :: Python :: 3.8",
|
|
32
31
|
"Programming Language :: Python :: 3.9",
|
|
33
32
|
"Programming Language :: Python :: 3.10",
|
|
34
33
|
"Programming Language :: Python :: 3.11",
|
|
@@ -44,7 +43,7 @@ dynamic = [
|
|
|
44
43
|
"version",
|
|
45
44
|
]
|
|
46
45
|
dependencies = [
|
|
47
|
-
"filelock>=3.
|
|
46
|
+
"filelock>=3.16.1",
|
|
48
47
|
]
|
|
49
48
|
urls.Changelog = "https://github.com/tox-dev/python-discovery/releases"
|
|
50
49
|
urls.Documentation = "https://python-discovery.readthedocs.io"
|
|
@@ -55,24 +54,25 @@ urls.Tracker = "https://github.com/tox-dev/python-discovery/issues"
|
|
|
55
54
|
[dependency-groups]
|
|
56
55
|
test = [
|
|
57
56
|
"covdefaults>=2.3",
|
|
58
|
-
"coverage>=7.
|
|
57
|
+
"coverage>=7.6.1",
|
|
59
58
|
"pytest>=8.3.5",
|
|
60
|
-
"pytest-mock>=3.14",
|
|
61
|
-
"setuptools>=75.
|
|
62
|
-
"
|
|
59
|
+
"pytest-mock>=3.14.1",
|
|
60
|
+
"setuptools>=75.3.4",
|
|
61
|
+
"typing-extensions>=4.12",
|
|
62
|
+
"vermin>=1.8",
|
|
63
63
|
]
|
|
64
64
|
docs = [
|
|
65
65
|
"furo>=2025.12.19",
|
|
66
66
|
"sphinx>=9.1",
|
|
67
67
|
"sphinx-autodoc-typehints>=3.6.3",
|
|
68
|
-
"sphinx-llm>=0.4.1",
|
|
68
|
+
"sphinx-llm>=0.4.1,!=1.1",
|
|
69
69
|
"sphinxcontrib-mermaid>=2",
|
|
70
70
|
"sphinxcontrib-towncrier>=0.4",
|
|
71
71
|
"towncrier>=25.8",
|
|
72
72
|
]
|
|
73
73
|
release = [
|
|
74
|
-
"gitpython>=3.
|
|
75
|
-
"packaging>=26",
|
|
74
|
+
"gitpython>=3.2",
|
|
75
|
+
"packaging>=26.2",
|
|
76
76
|
"pre-commit>=4.5.1",
|
|
77
77
|
"towncrier>=25.8",
|
|
78
78
|
]
|
|
@@ -81,6 +81,7 @@ release = [
|
|
|
81
81
|
version.source = "vcs"
|
|
82
82
|
|
|
83
83
|
[tool.ruff]
|
|
84
|
+
target-version = "py39"
|
|
84
85
|
line-length = 120
|
|
85
86
|
format.preview = true
|
|
86
87
|
format.docstring-code-line-length = 100
|
|
@@ -30,17 +30,21 @@ 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"
|
|
45
|
+
_DEFAULT_QUERY_TIMEOUT: Final[float] = 15.0
|
|
46
|
+
# poll(2) limits milliseconds to a signed C int.
|
|
47
|
+
_MAX_QUERY_TIMEOUT: Final[float] = (2**31 - 1) / 1000
|
|
44
48
|
|
|
45
49
|
|
|
46
50
|
class _UnsupportedInterpreterError(RuntimeError):
|
|
@@ -55,9 +59,10 @@ def from_exe( # ruff:ignore[too-many-arguments]
|
|
|
55
59
|
*,
|
|
56
60
|
raise_on_error: bool = True,
|
|
57
61
|
ignore_cache: bool = False,
|
|
62
|
+
executable_args: str | None = None,
|
|
58
63
|
) -> PythonInfo | None:
|
|
59
64
|
env = os.environ if env is None else env
|
|
60
|
-
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)
|
|
61
66
|
if isinstance(result, Exception):
|
|
62
67
|
if raise_on_error:
|
|
63
68
|
raise result
|
|
@@ -66,31 +71,34 @@ def from_exe( # ruff:ignore[too-many-arguments]
|
|
|
66
71
|
return result
|
|
67
72
|
|
|
68
73
|
|
|
69
|
-
def _get_from_cache(
|
|
74
|
+
def _get_from_cache( # ruff:ignore[too-many-arguments]
|
|
70
75
|
cls: type[PythonInfo],
|
|
71
76
|
cache: PyInfoCache | None,
|
|
72
77
|
exe: str,
|
|
73
78
|
env: Mapping[str, str],
|
|
74
79
|
*,
|
|
75
80
|
ignore_cache: bool = True,
|
|
81
|
+
executable_args: str | None = None,
|
|
76
82
|
) -> PythonInfo | Exception:
|
|
77
|
-
|
|
78
|
-
if not ignore_cache and
|
|
79
|
-
result = _CACHE[
|
|
83
|
+
key = Path(exe), executable_args
|
|
84
|
+
if not ignore_cache and key in _CACHE:
|
|
85
|
+
result = _CACHE[key]
|
|
80
86
|
else:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
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:
|
|
84
90
|
result.executable = exe
|
|
85
91
|
return result
|
|
86
92
|
|
|
87
93
|
|
|
88
|
-
def _get_via_file_cache(
|
|
94
|
+
def _get_via_file_cache( # ruff:ignore[too-many-arguments]
|
|
89
95
|
cls: type[PythonInfo],
|
|
90
96
|
cache: PyInfoCache | None,
|
|
91
97
|
path: Path,
|
|
92
98
|
exe: str,
|
|
93
99
|
env: Mapping[str, str],
|
|
100
|
+
*,
|
|
101
|
+
executable_args: str | None = None,
|
|
94
102
|
) -> PythonInfo | Exception:
|
|
95
103
|
path_text = str(path)
|
|
96
104
|
try:
|
|
@@ -104,17 +112,22 @@ def _get_via_file_cache(
|
|
|
104
112
|
|
|
105
113
|
resolved_cache = cache if cache is not None else NoOpCache()
|
|
106
114
|
py_info_store = resolved_cache.py_info(path)
|
|
107
|
-
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
|
+
}
|
|
108
121
|
with py_info_store.locked():
|
|
109
122
|
cached = _read_cache_entry(cls, py_info_store, entry_meta)
|
|
110
123
|
if isinstance(cached, _UnsupportedInterpreterError):
|
|
111
124
|
return cached
|
|
112
125
|
py_info = cached
|
|
113
126
|
if py_info is None:
|
|
114
|
-
failure, py_info = _run_subprocess(cls, exe, env)
|
|
127
|
+
failure, py_info = _run_subprocess(cls, exe, env, executable_args)
|
|
115
128
|
if failure is not None and not isinstance(failure, _UnsupportedInterpreterError):
|
|
116
129
|
_LOGGER.debug("first subprocess attempt failed for %s (%s), retrying", exe, failure)
|
|
117
|
-
failure, py_info = _run_subprocess(cls, exe, env)
|
|
130
|
+
failure, py_info = _run_subprocess(cls, exe, env, executable_args)
|
|
118
131
|
if isinstance(failure, _UnsupportedInterpreterError):
|
|
119
132
|
_LOGGER.warning("%s", failure) # the verdict is permanent, warn once and remember it
|
|
120
133
|
py_info_store.write({**entry_meta, "unsupported": str(failure)})
|
|
@@ -136,7 +149,7 @@ def _read_cache_entry(
|
|
|
136
149
|
) -> PythonInfo | _UnsupportedInterpreterError | None:
|
|
137
150
|
if not py_info_store.exists() or (data := py_info_store.read()) is None:
|
|
138
151
|
return None
|
|
139
|
-
if all(data.get(key) == value for key, value in entry_meta.items()):
|
|
152
|
+
if isinstance(data, dict) and all(data.get(key) == value for key, value in entry_meta.items()):
|
|
140
153
|
if isinstance(unsupported := data.get("unsupported"), str):
|
|
141
154
|
return _UnsupportedInterpreterError(unsupported)
|
|
142
155
|
if isinstance(content := data.get("content"), dict):
|
|
@@ -152,10 +165,10 @@ def _load_cached_py_info(
|
|
|
152
165
|
) -> PythonInfo | None:
|
|
153
166
|
try:
|
|
154
167
|
py_info = cls.from_dict(content.copy())
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if
|
|
168
|
+
stale = (sys_exe := py_info.system_executable) is not None and not Path(sys_exe).is_file()
|
|
169
|
+
except (AttributeError, KeyError, OSError, TypeError):
|
|
170
|
+
stale = True
|
|
171
|
+
if stale:
|
|
159
172
|
py_info_store.remove()
|
|
160
173
|
return None
|
|
161
174
|
return py_info
|
|
@@ -211,16 +224,20 @@ def _run_subprocess(
|
|
|
211
224
|
cls: type[PythonInfo],
|
|
212
225
|
exe: str,
|
|
213
226
|
env: Mapping[str, str],
|
|
227
|
+
executable_args: str | None = None,
|
|
214
228
|
) -> tuple[Exception | None, PythonInfo | None]:
|
|
215
229
|
start_cookie = gen_cookie()
|
|
216
230
|
end_cookie = gen_cookie()
|
|
217
|
-
timeout =
|
|
231
|
+
timeout = _query_timeout(env.get("PY_DISCOVERY_TIMEOUT"))
|
|
218
232
|
with _resolve_py_info_script() as py_info_script:
|
|
219
|
-
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:])}"
|
|
220
237
|
env = dict(env)
|
|
221
238
|
env.pop("__PYVENV_LAUNCHER__", None)
|
|
222
239
|
env["PYTHONUTF8"] = "1"
|
|
223
|
-
_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))
|
|
224
241
|
try:
|
|
225
242
|
process = Popen( # ruff:ignore[subprocess-without-shell-equals-true]
|
|
226
243
|
cmd,
|
|
@@ -245,7 +262,8 @@ def _run_subprocess(
|
|
|
245
262
|
out, raw_out, out_starts, out_ends = _extract_between_cookies(out, start_cookie, end_cookie)
|
|
246
263
|
try:
|
|
247
264
|
result = cls.from_json(out)
|
|
248
|
-
|
|
265
|
+
if executable_args is None:
|
|
266
|
+
result.executable = exe
|
|
249
267
|
except json.JSONDecodeError as exc:
|
|
250
268
|
_LOGGER.warning(
|
|
251
269
|
"subprocess %s returned invalid JSON; raw stdout %d chars, start cookie %s, end cookie %s, "
|
|
@@ -264,6 +282,22 @@ def _run_subprocess(
|
|
|
264
282
|
return None, result
|
|
265
283
|
|
|
266
284
|
|
|
285
|
+
@lru_cache(maxsize=128)
|
|
286
|
+
def _query_timeout(raw: str | None) -> float | None:
|
|
287
|
+
if raw is None:
|
|
288
|
+
return _DEFAULT_QUERY_TIMEOUT
|
|
289
|
+
try:
|
|
290
|
+
timeout = float(raw)
|
|
291
|
+
except ValueError:
|
|
292
|
+
timeout = 0.0
|
|
293
|
+
if timeout > 0:
|
|
294
|
+
return timeout if timeout <= _MAX_QUERY_TIMEOUT else None
|
|
295
|
+
_LOGGER.warning(
|
|
296
|
+
"ignoring PY_DISCOVERY_TIMEOUT=%r, not a positive number of seconds; using %s", raw, _DEFAULT_QUERY_TIMEOUT
|
|
297
|
+
)
|
|
298
|
+
return _DEFAULT_QUERY_TIMEOUT
|
|
299
|
+
|
|
300
|
+
|
|
267
301
|
def _query_failure(exe: str, out: str, err: str | None, code: int | None) -> RuntimeError:
|
|
268
302
|
# the gate's exit code and stderr marker must both be present: either alone can collide, an errno of 79 from a
|
|
269
303
|
# failed exec or a shim that echoes the marker phrase, but only the gate produces the combination
|
|
@@ -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:
|
|
@@ -321,7 +321,7 @@ class PythonInfoCollector:
|
|
|
321
321
|
"script_args": "--no-user-cfg",
|
|
322
322
|
}) # conf files not parsed so they do not hijack paths
|
|
323
323
|
if hasattr(sys, "_framework"): # pragma: no cover # macOS framework builds only
|
|
324
|
-
sys._framework = None # ruff:ignore[private-member-access] # disable macOS static paths for framework
|
|
324
|
+
sys._framework = None # ty: ignore[invalid-assignment] # ruff:ignore[private-member-access] # disable macOS static paths for framework
|
|
325
325
|
|
|
326
326
|
with warnings.catch_warnings(): # disable warning for PEP-632
|
|
327
327
|
warnings.simplefilter("ignore")
|
|
@@ -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
|
|