python-discovery 1.5.2__tar.gz → 1.6.0__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 (63) hide show
  1. {python_discovery-1.5.2 → python_discovery-1.6.0}/.github/dependabot.yaml +6 -1
  2. {python_discovery-1.5.2 → python_discovery-1.6.0}/.github/workflows/check.yaml +2 -2
  3. {python_discovery-1.5.2 → python_discovery-1.6.0}/.github/workflows/prepare-release.yaml +1 -1
  4. {python_discovery-1.5.2 → python_discovery-1.6.0}/.github/workflows/release.yaml +1 -1
  5. {python_discovery-1.5.2 → python_discovery-1.6.0}/.pre-commit-config.yaml +2 -2
  6. {python_discovery-1.5.2 → python_discovery-1.6.0}/PKG-INFO +1 -1
  7. {python_discovery-1.5.2 → python_discovery-1.6.0}/docs/changelog.rst +29 -0
  8. {python_discovery-1.5.2 → python_discovery-1.6.0}/docs/conf.py +4 -0
  9. {python_discovery-1.5.2 → python_discovery-1.6.0}/pyproject.toml +5 -4
  10. {python_discovery-1.5.2 → python_discovery-1.6.0}/src/python_discovery/_py_info.py +11 -0
  11. {python_discovery-1.5.2 → python_discovery-1.6.0}/src/python_discovery/_py_info_collect.py +37 -12
  12. {python_discovery-1.5.2 → python_discovery-1.6.0}/tests/py_info/test_py_info.py +27 -19
  13. {python_discovery-1.5.2 → python_discovery-1.6.0}/tests/test_py_info_collect.py +29 -1
  14. python_discovery-1.5.2/docs/changelog/116.bugfix.rst +0 -4
  15. python_discovery-1.5.2/docs/changelog/116.doc.rst +0 -1
  16. {python_discovery-1.5.2 → python_discovery-1.6.0}/.github/FUNDING.yaml +0 -0
  17. {python_discovery-1.5.2 → python_discovery-1.6.0}/.github/SECURITY.md +0 -0
  18. {python_discovery-1.5.2 → python_discovery-1.6.0}/.github/release.yaml +0 -0
  19. {python_discovery-1.5.2 → python_discovery-1.6.0}/.gitignore +0 -0
  20. {python_discovery-1.5.2 → python_discovery-1.6.0}/.readthedocs.yaml +0 -0
  21. {python_discovery-1.5.2 → python_discovery-1.6.0}/.readthedocs.yml +0 -0
  22. {python_discovery-1.5.2 → python_discovery-1.6.0}/CODE_OF_CONDUCT.md +0 -0
  23. {python_discovery-1.5.2 → python_discovery-1.6.0}/LICENSE +0 -0
  24. {python_discovery-1.5.2 → python_discovery-1.6.0}/README.md +0 -0
  25. {python_discovery-1.5.2 → python_discovery-1.6.0}/docs/_static/custom.css +0 -0
  26. {python_discovery-1.5.2 → python_discovery-1.6.0}/docs/_static/logo.svg +0 -0
  27. {python_discovery-1.5.2 → python_discovery-1.6.0}/docs/changelog/template.jinja2 +0 -0
  28. {python_discovery-1.5.2 → python_discovery-1.6.0}/docs/explanation.rst +0 -0
  29. {python_discovery-1.5.2 → python_discovery-1.6.0}/docs/how-to/standalone-usage.rst +0 -0
  30. {python_discovery-1.5.2 → python_discovery-1.6.0}/docs/index.rst +0 -0
  31. {python_discovery-1.5.2 → python_discovery-1.6.0}/docs/reference/api.rst +0 -0
  32. {python_discovery-1.5.2 → python_discovery-1.6.0}/docs/reference/environment-variables.rst +0 -0
  33. {python_discovery-1.5.2 → python_discovery-1.6.0}/docs/tutorial/getting-started.rst +0 -0
  34. {python_discovery-1.5.2 → python_discovery-1.6.0}/src/python_discovery/__init__.py +0 -0
  35. {python_discovery-1.5.2 → python_discovery-1.6.0}/src/python_discovery/_cache.py +0 -0
  36. {python_discovery-1.5.2 → python_discovery-1.6.0}/src/python_discovery/_cached_py_info.py +0 -0
  37. {python_discovery-1.5.2 → python_discovery-1.6.0}/src/python_discovery/_compat.py +0 -0
  38. {python_discovery-1.5.2 → python_discovery-1.6.0}/src/python_discovery/_discovery.py +0 -0
  39. {python_discovery-1.5.2 → python_discovery-1.6.0}/src/python_discovery/_py_spec.py +0 -0
  40. {python_discovery-1.5.2 → python_discovery-1.6.0}/src/python_discovery/_specifier.py +0 -0
  41. {python_discovery-1.5.2 → python_discovery-1.6.0}/src/python_discovery/_windows/__init__.py +0 -0
  42. {python_discovery-1.5.2 → python_discovery-1.6.0}/src/python_discovery/_windows/_pep514.py +0 -0
  43. {python_discovery-1.5.2 → python_discovery-1.6.0}/src/python_discovery/_windows/_propose.py +0 -0
  44. {python_discovery-1.5.2 → python_discovery-1.6.0}/src/python_discovery/py.typed +0 -0
  45. {python_discovery-1.5.2 → python_discovery-1.6.0}/tasks/old_target_check.py +0 -0
  46. {python_discovery-1.5.2 → python_discovery-1.6.0}/tasks/old_target_check.sh +0 -0
  47. {python_discovery-1.5.2 → python_discovery-1.6.0}/tasks/release.py +0 -0
  48. {python_discovery-1.5.2 → python_discovery-1.6.0}/tests/conftest.py +0 -0
  49. {python_discovery-1.5.2 → python_discovery-1.6.0}/tests/py_info/test_py_info_exe_based_of.py +0 -0
  50. {python_discovery-1.5.2 → python_discovery-1.6.0}/tests/test_cache.py +0 -0
  51. {python_discovery-1.5.2 → python_discovery-1.6.0}/tests/test_cached_py_info.py +0 -0
  52. {python_discovery-1.5.2 → python_discovery-1.6.0}/tests/test_discovery.py +0 -0
  53. {python_discovery-1.5.2 → python_discovery-1.6.0}/tests/test_discovery_extra.py +0 -0
  54. {python_discovery-1.5.2 → python_discovery-1.6.0}/tests/test_iter_interpreters.py +0 -0
  55. {python_discovery-1.5.2 → python_discovery-1.6.0}/tests/test_py_info_extra.py +0 -0
  56. {python_discovery-1.5.2 → python_discovery-1.6.0}/tests/test_py_spec.py +0 -0
  57. {python_discovery-1.5.2 → python_discovery-1.6.0}/tests/test_py_spec_extra.py +0 -0
  58. {python_discovery-1.5.2 → python_discovery-1.6.0}/tests/test_specifier.py +0 -0
  59. {python_discovery-1.5.2 → python_discovery-1.6.0}/tests/windows/conftest.py +0 -0
  60. {python_discovery-1.5.2 → python_discovery-1.6.0}/tests/windows/test_windows.py +0 -0
  61. {python_discovery-1.5.2 → python_discovery-1.6.0}/tests/windows/test_windows_pep514.py +0 -0
  62. {python_discovery-1.5.2 → python_discovery-1.6.0}/tests/windows/winreg_mock_values.py +0 -0
  63. {python_discovery-1.5.2 → python_discovery-1.6.0}/tox.toml +0 -0
@@ -3,6 +3,11 @@ updates:
3
3
  - package-ecosystem: "github-actions"
4
4
  directory: "/"
5
5
  schedule:
6
- interval: "daily"
6
+ interval: "weekly"
7
+ day: "tuesday"
8
+ groups:
9
+ github-actions:
10
+ patterns:
11
+ - "*"
7
12
  cooldown:
8
13
  default-days: 7
@@ -38,7 +38,7 @@ jobs:
38
38
  fetch-depth: 0
39
39
  persist-credentials: false
40
40
  - name: Install the latest version of uv
41
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
41
+ uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
42
42
  with:
43
43
  enable-cache: false
44
44
  cache-dependency-glob: "pyproject.toml"
@@ -63,7 +63,7 @@ jobs:
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@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
66
+ uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
67
67
  with:
68
68
  enable-cache: false
69
69
  cache-dependency-glob: "pyproject.toml"
@@ -29,7 +29,7 @@ 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@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
32
+ uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
33
33
  with:
34
34
  enable-cache: true
35
35
  cache-dependency-glob: "pyproject.toml"
@@ -15,7 +15,7 @@ jobs:
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@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
18
+ uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
19
19
  with:
20
20
  enable-cache: false
21
21
  cache-dependency-glob: "pyproject.toml"
@@ -15,11 +15,11 @@ repos:
15
15
  - id: codespell
16
16
  additional_dependencies: ["tomli>=2.4"]
17
17
  - repo: https://github.com/tox-dev/pyproject-fmt
18
- rev: "v2.27.0"
18
+ rev: "v2.28.1"
19
19
  hooks:
20
20
  - id: pyproject-fmt
21
21
  - repo: https://github.com/astral-sh/ruff-pre-commit
22
- rev: "v0.16.2"
22
+ rev: "v0.16.4"
23
23
  hooks:
24
24
  - id: ruff-format
25
25
  - id: ruff
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: python-discovery
3
- Version: 1.5.2
3
+ Version: 1.6.0
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,35 @@
6
6
 
7
7
  .. towncrier release notes start
8
8
 
9
+ ********************
10
+ v1.6.0 (2026-08-28)
11
+ ********************
12
+
13
+ Features - 1.6.0
14
+ ================
15
+ - :attr:`~python_discovery.PythonInfo.system_exe` returns the executable of the system Python an interpreter is based on,
16
+ falling back to :attr:`~python_discovery.PythonInfo.executable` the way
17
+ :meth:`~python_discovery.PythonInfo.resolve_to_system` does. :attr:`~python_discovery.PythonInfo.system_executable` is
18
+ ``None`` until resolution runs, so every consumer had to narrow a value discovery has already settled - by
19
+ :user:`gaborbernat`. (:issue:`127`)
20
+
21
+ ********************
22
+ v1.5.3 (2026-08-24)
23
+ ********************
24
+
25
+ Bug fixes - 1.5.3
26
+ =================
27
+ - Restore discovery of Python 3.6 and 3.7 interpreters: the interrogation script had grown 3.8-only syntax and is now
28
+ kept to Python 3.6. Candidates below 3.6 log one warning naming the version found instead of two debug-level query
29
+ attempts, and the verdict is cached so they are queried only once; an absolute-path spec for one raises
30
+ ``RuntimeError`` with the same message - by :user:`gaborbernat`. (:issue:`116`)
31
+ - Prefer the version-matched system executable when CPython reports an existing generic executable for a copied POSIX
32
+ virtual environment. (:issue:`124`)
33
+
34
+ Improved documentation - 1.5.3
35
+ ==============================
36
+ - Document the version floors: runs on Python 3.8+, discovers interpreters down to 3.6 - by :user:`gaborbernat`. (:issue:`116`)
37
+
9
38
  ********************
10
39
  v1.5.1 (2026-07-31)
11
40
  ********************
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import os
5
6
  from datetime import datetime, timezone
6
7
  from pathlib import Path
7
8
 
@@ -14,6 +15,7 @@ release = __version__
14
15
  copyright = f"2026-{datetime.now(tz=timezone.utc).year}, {company}" # ruff:ignore[builtin-variable-shadowing]
15
16
 
16
17
  extensions = [
18
+ "sphinx_llm.txt",
17
19
  "sphinx.ext.autodoc",
18
20
  "sphinx.ext.autosectionlabel",
19
21
  "sphinx.ext.extlinks",
@@ -58,3 +60,5 @@ html_theme_options = {
58
60
  "sidebar_hide_name": True,
59
61
  }
60
62
  html_css_files = ["custom.css"]
63
+
64
+ markdown_http_base = os.environ.get("READTHEDOCS_CANONICAL_URL", "")
@@ -65,6 +65,7 @@ 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
69
  "sphinxcontrib-mermaid>=2",
69
70
  "sphinxcontrib-towncrier>=0.4",
70
71
  "towncrier>=25.8",
@@ -180,16 +181,16 @@ rules.unresolved-attribute = "ignore"
180
181
  ini_options.markers = [ "graalpy" ]
181
182
 
182
183
  [tool.coverage]
184
+ run.source = [
185
+ "python_discovery",
186
+ "tests",
187
+ ]
183
188
  run.branch = true
184
189
  run.dynamic_context = "test_function"
185
190
  run.parallel = true
186
191
  run.plugins = [
187
192
  "covdefaults",
188
193
  ]
189
- run.source = [
190
- "python_discovery",
191
- "tests",
192
- ]
193
194
  paths.source = [
194
195
  "src",
195
196
  ".tox*/*/lib/python*/site-packages",
@@ -167,6 +167,17 @@ class PythonInfo: # ruff:ignore[too-many-public-methods]
167
167
  """The exec prefix of the system Python this interpreter is based on."""
168
168
  return self.real_prefix or self.base_exec_prefix or self.exec_prefix
169
169
 
170
+ @property
171
+ def system_exe(self) -> str:
172
+ """
173
+ The executable of the system Python this interpreter is based on.
174
+
175
+ :attr:`system_executable` stays ``None`` until :meth:`resolve_to_system` walks the prefix chain, so reading it
176
+ forces every caller to narrow a value discovery has already settled. The fallback here matches what resolution
177
+ itself writes when a prefix links back to its own interpreter.
178
+ """
179
+ return self.system_executable or self.executable
180
+
170
181
  def __repr__(self) -> str:
171
182
  return "{}({!r})".format(
172
183
  self.__class__.__name__,
@@ -198,25 +198,30 @@ class PythonInfoCollector:
198
198
 
199
199
  def _fast_get_system_executable(self):
200
200
  """Try to get the system executable by just looking at properties."""
201
- # if we're not in a virtual environment, this is already a system python, so return the original executable
202
- # note we must choose the original and not the pure executable as shim scripts might throw us off
201
+ # Preserve the invoked path because shims may rely on it.
203
202
  if not (self.real_prefix or (self.base_prefix is not None and self.base_prefix != self.prefix)):
204
203
  return self._resolve_executable_symlink(self.original_executable)
205
204
 
206
- # if this is NOT a virtual environment, can't determine easily, bail out
207
205
  if self.real_prefix is not None:
208
206
  return None
209
207
 
210
- base_executable = getattr(sys, "_base_executable", None) # some platforms may set this to help us
211
- if base_executable is None: # use the saved system executable if present
208
+ base_executable = getattr(sys, "_base_executable", None)
209
+ if base_executable is None:
212
210
  return None
213
211
 
214
- # we know we're in a virtual environment, can not be us
215
212
  if sys.executable == base_executable:
216
213
  return None
217
214
 
218
- # We're not in a venv and base_executable exists; use it directly
215
+ # CPython accepts pythonX without checking its version when resolving a copied venv.
219
216
  if os.path.exists(base_executable): # pragma: >=3.11 cover
217
+ major = self.version_info.major
218
+ ambiguous_names = {"python", "python{}".format(major)}
219
+ if self.implementation == "PyPy":
220
+ ambiguous_names.update({"pypy", "pypy3", "pypy{}".format(major)})
221
+ if os.path.basename(base_executable) in ambiguous_names:
222
+ versioned = self._try_posix_versioned_executable(base_executable)
223
+ if versioned is not None and not os.path.samefile(base_executable, versioned):
224
+ base_executable = versioned
220
225
  return self._resolve_executable_symlink(base_executable)
221
226
 
222
227
  # Try fallback for POSIX virtual environments
@@ -259,28 +264,48 @@ class PythonInfoCollector:
259
264
 
260
265
  def _try_posix_fallback_executable(self, base_executable):
261
266
  """Find a versioned Python binary as fallback for POSIX virtual environments."""
267
+ versioned = self._try_posix_versioned_executable(base_executable)
268
+ if versioned is not None:
269
+ return versioned
270
+
271
+ major, minor = self.version_info.major, self.version_info.minor
272
+ if self.os != "posix" or (major, minor) < (3, 11):
273
+ return None
274
+
275
+ base_dir = os.path.dirname(base_executable)
276
+ candidates = ["python{}".format(major)]
277
+ if self.implementation == "PyPy":
278
+ candidates.extend(["pypy", "pypy3", "pypy{}".format(major)])
279
+
280
+ for candidate in candidates:
281
+ full_path = os.path.join(base_dir, candidate)
282
+ if os.path.exists(full_path):
283
+ return full_path
284
+
285
+ return None
286
+
287
+ def _try_posix_versioned_executable(self, base_executable):
262
288
  major, minor = self.version_info.major, self.version_info.minor
263
289
  if self.os != "posix" or (major, minor) < (3, 11):
264
290
  return None
265
291
 
266
- # search relative to the directory of sys._base_executable
267
292
  base_dir = os.path.dirname(base_executable)
268
- candidates = ["python{}".format(major), "python{}.{}".format(major, minor)]
293
+ candidates = ["python{}.{}".format(major, minor)]
269
294
  if self.implementation == "PyPy":
270
- candidates.extend(["pypy", "pypy3", "pypy{}".format(major), "pypy{}.{}".format(major, minor)])
295
+ candidates.insert(0, "pypy{}.{}".format(major, minor))
271
296
 
272
297
  for candidate in candidates:
273
298
  full_path = os.path.join(base_dir, candidate)
274
299
  if os.path.exists(full_path):
275
300
  return full_path
276
301
 
277
- return None # in this case we just can't tell easily without poking around FS and calling them, bail
302
+ return None
278
303
 
279
304
  @staticmethod
280
305
  def _distutils_install(): # pragma: <3.11 cover # 3.11+ uses the "venv" scheme instead
281
306
  # use distutils primarily because that's what pip does
282
307
  # https://github.com/pypa/pip/blob/main/src/pip/_internal/locations.py#L95
283
- # note here we don't import Distribution directly to allow setuptools to patch it
308
+ # Avoid importing Distribution before setuptools can patch it.
284
309
  with warnings.catch_warnings(): # disable warning for PEP-632
285
310
  warnings.simplefilter("ignore")
286
311
  try:
@@ -75,25 +75,19 @@ def test_bad_exe_py_info_no_raise(
75
75
 
76
76
  @pytest.mark.parametrize(
77
77
  "spec",
78
- itertools.chain(
79
- [sys.executable],
80
- [
81
- f"{impl}{'.'.join(str(i) for i in ver)}{'t' if CURRENT.free_threaded else ''}{arch}"
82
- for impl, ver, arch in itertools.product(
83
- (
84
- [CURRENT.implementation]
85
- + (["python"] if CURRENT.implementation == "CPython" else [])
86
- + (
87
- [CURRENT.implementation.lower()]
88
- if CURRENT.implementation != CURRENT.implementation.lower()
89
- else []
90
- )
91
- ),
92
- [sys.version_info[0 : i + 1] for i in range(3)],
93
- ["", f"-{CURRENT.architecture}"],
94
- )
95
- ],
96
- ),
78
+ [sys.executable]
79
+ + [
80
+ f"{impl}{'.'.join(str(i) for i in ver)}{'t' if CURRENT.free_threaded else ''}{arch}"
81
+ for impl, ver, arch in itertools.product(
82
+ (
83
+ [CURRENT.implementation]
84
+ + (["python"] if CURRENT.implementation == "CPython" else [])
85
+ + ([CURRENT.implementation.lower()] if CURRENT.implementation != CURRENT.implementation.lower() else [])
86
+ ),
87
+ [sys.version_info[0 : i + 1] for i in range(3)],
88
+ ["", f"-{CURRENT.architecture}"],
89
+ )
90
+ ],
97
91
  )
98
92
  def test_satisfy_py_info(spec: str) -> None:
99
93
  parsed_spec = PythonSpec.from_string_spec(spec)
@@ -517,3 +511,17 @@ def test_select_most_likely_prefers_machine_match(
517
511
  d.sysconfig_platform = plat
518
512
  result = PythonInfo._select_most_likely(discovered, target)
519
513
  assert result.sysconfig_platform == discovered_platforms[expected_idx]
514
+
515
+
516
+ @pytest.mark.parametrize(
517
+ ("system_executable", "expected"),
518
+ [
519
+ pytest.param("/usr/bin/python3", "/usr/bin/python3", id="resolved"),
520
+ pytest.param(None, "/venv/bin/python", id="unresolved falls back to executable"),
521
+ ],
522
+ )
523
+ def test_py_info_system_exe(system_executable: str | None, expected: str) -> None:
524
+ info = copy.deepcopy(CURRENT)
525
+ info.executable = "/venv/bin/python"
526
+ info.system_executable = system_executable
527
+ assert info.system_exe == expected
@@ -249,10 +249,38 @@ def test_system_executable_from_existing_base(mocker: MockerFixture, tmp_path: P
249
249
  assert PythonInfo().system_executable == str(base)
250
250
 
251
251
 
252
+ @pytest.mark.usefixtures("no_framework")
253
+ @pytest.mark.parametrize(
254
+ "same_file",
255
+ [
256
+ pytest.param(False, id="distinct"),
257
+ pytest.param(True, id="alias"),
258
+ ],
259
+ )
260
+ def test_system_executable_prefers_exact_version(mocker: MockerFixture, tmp_path: Path, *, same_file: bool) -> None:
261
+ bin_dir = tmp_path / "bin"
262
+ bin_dir.mkdir()
263
+ versioned = bin_dir / "python3.13"
264
+ versioned.touch()
265
+ generic = bin_dir / "python3"
266
+ if same_file:
267
+ generic.symlink_to(versioned.name)
268
+ landmark = tmp_path / "lib" / Path(os.__file__).parent.name / "os.py"
269
+ landmark.parent.mkdir(parents=True)
270
+ landmark.touch()
271
+ else:
272
+ generic.touch()
273
+ _venv(mocker)
274
+ mocker.patch.object(sys, "_base_executable", str(generic), create=True)
275
+ mocker.patch.object(sys, "version_info", VersionInfo(3, 13, 0, "final", 0))
276
+ assert PythonInfo().system_executable == str(generic if same_file else versioned)
277
+
278
+
252
279
  def test_system_executable_versioned_fallback(mocker: MockerFixture, tmp_path: Path) -> None:
253
280
  _venv_missing_base_no_candidates(mocker, tmp_path)
254
- versioned = tmp_path / "python3"
281
+ versioned = tmp_path / "python3.12"
255
282
  versioned.touch()
283
+ (tmp_path / "python3").touch()
256
284
  assert PythonInfo().system_executable == str(versioned)
257
285
 
258
286
 
@@ -1,4 +0,0 @@
1
- Restore discovery of Python 3.6 and 3.7 interpreters: the interrogation script had grown 3.8-only syntax and is now
2
- kept to Python 3.6. Candidates below 3.6 log one warning naming the version found instead of two debug-level query
3
- attempts, and the verdict is cached so they are queried only once; an absolute-path spec for one raises
4
- ``RuntimeError`` with the same message - by :user:`gaborbernat`.
@@ -1 +0,0 @@
1
- Document the version floors: runs on Python 3.8+, discovers interpreters down to 3.6 - by :user:`gaborbernat`.