python-discovery 1.4.3__tar.gz → 1.4.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 (58) hide show
  1. python_discovery-1.4.4/.github/FUNDING.yaml +2 -0
  2. {python_discovery-1.4.3 → python_discovery-1.4.4}/.pre-commit-config.yaml +2 -2
  3. {python_discovery-1.4.3 → python_discovery-1.4.4}/PKG-INFO +1 -1
  4. {python_discovery-1.4.3 → python_discovery-1.4.4}/docs/changelog.rst +11 -0
  5. {python_discovery-1.4.3 → python_discovery-1.4.4}/docs/explanation.rst +16 -2
  6. {python_discovery-1.4.3 → python_discovery-1.4.4}/docs/how-to/standalone-usage.rst +21 -0
  7. {python_discovery-1.4.3 → python_discovery-1.4.4}/docs/tutorial/getting-started.rst +11 -5
  8. {python_discovery-1.4.3 → python_discovery-1.4.4}/src/python_discovery/_py_info.py +6 -2
  9. {python_discovery-1.4.3 → python_discovery-1.4.4}/src/python_discovery/_py_spec.py +11 -2
  10. {python_discovery-1.4.3 → python_discovery-1.4.4}/tests/py_info/test_py_info.py +40 -0
  11. {python_discovery-1.4.3 → python_discovery-1.4.4}/tests/test_py_spec.py +26 -0
  12. {python_discovery-1.4.3 → python_discovery-1.4.4}/tests/test_py_spec_extra.py +9 -0
  13. python_discovery-1.4.3/.github/FUNDING.yaml +0 -1
  14. {python_discovery-1.4.3 → python_discovery-1.4.4}/.github/SECURITY.md +0 -0
  15. {python_discovery-1.4.3 → python_discovery-1.4.4}/.github/dependabot.yaml +0 -0
  16. {python_discovery-1.4.3 → python_discovery-1.4.4}/.github/release.yaml +0 -0
  17. {python_discovery-1.4.3 → python_discovery-1.4.4}/.github/workflows/check.yaml +0 -0
  18. {python_discovery-1.4.3 → python_discovery-1.4.4}/.github/workflows/prepare-release.yaml +0 -0
  19. {python_discovery-1.4.3 → python_discovery-1.4.4}/.github/workflows/release.yaml +0 -0
  20. {python_discovery-1.4.3 → python_discovery-1.4.4}/.gitignore +0 -0
  21. {python_discovery-1.4.3 → python_discovery-1.4.4}/.readthedocs.yaml +0 -0
  22. {python_discovery-1.4.3 → python_discovery-1.4.4}/.readthedocs.yml +0 -0
  23. {python_discovery-1.4.3 → python_discovery-1.4.4}/CODE_OF_CONDUCT.md +0 -0
  24. {python_discovery-1.4.3 → python_discovery-1.4.4}/LICENSE +0 -0
  25. {python_discovery-1.4.3 → python_discovery-1.4.4}/README.md +0 -0
  26. {python_discovery-1.4.3 → python_discovery-1.4.4}/docs/_static/custom.css +0 -0
  27. {python_discovery-1.4.3 → python_discovery-1.4.4}/docs/_static/logo.svg +0 -0
  28. {python_discovery-1.4.3 → python_discovery-1.4.4}/docs/changelog/template.jinja2 +0 -0
  29. {python_discovery-1.4.3 → python_discovery-1.4.4}/docs/conf.py +0 -0
  30. {python_discovery-1.4.3 → python_discovery-1.4.4}/docs/index.rst +0 -0
  31. {python_discovery-1.4.3 → python_discovery-1.4.4}/docs/reference/api.rst +0 -0
  32. {python_discovery-1.4.3 → python_discovery-1.4.4}/docs/reference/environment-variables.rst +0 -0
  33. {python_discovery-1.4.3 → python_discovery-1.4.4}/pyproject.toml +0 -0
  34. {python_discovery-1.4.3 → python_discovery-1.4.4}/src/python_discovery/__init__.py +0 -0
  35. {python_discovery-1.4.3 → python_discovery-1.4.4}/src/python_discovery/_cache.py +0 -0
  36. {python_discovery-1.4.3 → python_discovery-1.4.4}/src/python_discovery/_cached_py_info.py +0 -0
  37. {python_discovery-1.4.3 → python_discovery-1.4.4}/src/python_discovery/_compat.py +0 -0
  38. {python_discovery-1.4.3 → python_discovery-1.4.4}/src/python_discovery/_discovery.py +0 -0
  39. {python_discovery-1.4.3 → python_discovery-1.4.4}/src/python_discovery/_specifier.py +0 -0
  40. {python_discovery-1.4.3 → python_discovery-1.4.4}/src/python_discovery/_windows/__init__.py +0 -0
  41. {python_discovery-1.4.3 → python_discovery-1.4.4}/src/python_discovery/_windows/_pep514.py +0 -0
  42. {python_discovery-1.4.3 → python_discovery-1.4.4}/src/python_discovery/_windows/_propose.py +0 -0
  43. {python_discovery-1.4.3 → python_discovery-1.4.4}/src/python_discovery/py.typed +0 -0
  44. {python_discovery-1.4.3 → python_discovery-1.4.4}/tasks/release.py +0 -0
  45. {python_discovery-1.4.3 → python_discovery-1.4.4}/tests/conftest.py +0 -0
  46. {python_discovery-1.4.3 → python_discovery-1.4.4}/tests/py_info/test_py_info_exe_based_of.py +0 -0
  47. {python_discovery-1.4.3 → python_discovery-1.4.4}/tests/test_cache.py +0 -0
  48. {python_discovery-1.4.3 → python_discovery-1.4.4}/tests/test_cached_py_info.py +0 -0
  49. {python_discovery-1.4.3 → python_discovery-1.4.4}/tests/test_discovery.py +0 -0
  50. {python_discovery-1.4.3 → python_discovery-1.4.4}/tests/test_discovery_extra.py +0 -0
  51. {python_discovery-1.4.3 → python_discovery-1.4.4}/tests/test_iter_interpreters.py +0 -0
  52. {python_discovery-1.4.3 → python_discovery-1.4.4}/tests/test_py_info_extra.py +0 -0
  53. {python_discovery-1.4.3 → python_discovery-1.4.4}/tests/test_specifier.py +0 -0
  54. {python_discovery-1.4.3 → python_discovery-1.4.4}/tests/windows/conftest.py +0 -0
  55. {python_discovery-1.4.3 → python_discovery-1.4.4}/tests/windows/test_windows.py +0 -0
  56. {python_discovery-1.4.3 → python_discovery-1.4.4}/tests/windows/test_windows_pep514.py +0 -0
  57. {python_discovery-1.4.3 → python_discovery-1.4.4}/tests/windows/winreg_mock_values.py +0 -0
  58. {python_discovery-1.4.3 → python_discovery-1.4.4}/tox.toml +0 -0
@@ -0,0 +1,2 @@
1
+ github: gaborbernat
2
+ thanks_dev: u/gh/gaborbernat
@@ -5,7 +5,7 @@ repos:
5
5
  - id: end-of-file-fixer
6
6
  - id: trailing-whitespace
7
7
  - repo: https://github.com/python-jsonschema/check-jsonschema
8
- rev: 0.37.3
8
+ rev: 0.37.4
9
9
  hooks:
10
10
  - id: check-github-workflows
11
11
  args: ["--verbose"]
@@ -25,7 +25,7 @@ repos:
25
25
  - id: ruff
26
26
  args: ["--fix", "--unsafe-fixes", "--exit-non-zero-on-fix"]
27
27
  - repo: https://github.com/rbubley/mirrors-prettier
28
- rev: "v3.9.1"
28
+ rev: "v3.9.4"
29
29
  hooks:
30
30
  - id: prettier
31
31
  additional_dependencies:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-discovery
3
- Version: 1.4.3
3
+ Version: 1.4.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,17 @@
6
6
 
7
7
  .. towncrier release notes start
8
8
 
9
+ ********************
10
+ v1.4.4 (2026-07-08)
11
+ ********************
12
+
13
+ Bug fixes - 1.4.4
14
+ =================
15
+ - Parse the debug build flag in interpreter specs - ``python3.13d`` and Debian's ``python3.13-dbg`` /
16
+ ``python3.13-debug`` now select a ``Py_DEBUG`` interpreter instead of being misread as an ISA named ``dbg``.
17
+ Resolving a virtualenv to its base interpreter also checks the free-threaded and debug ABI flags, so a debug or
18
+ free-threaded environment no longer resolves to a release build of the same version - by :user:`gaborbernat`. (:issue:`96`)
19
+
9
20
  ********************
10
21
  v1.4.3 (2026-07-03)
11
22
  ********************
@@ -213,7 +213,7 @@ when encountering problematic interpreters.
213
213
  Spec format reference
214
214
  -----------------------
215
215
 
216
- A spec string follows the pattern ``[impl][version][t][-arch][-machine]``. Every part is optional.
216
+ A spec string follows the pattern ``[impl][version][t][d][-arch][-machine]``. Every part is optional.
217
217
 
218
218
  .. mermaid::
219
219
 
@@ -221,12 +221,14 @@ A spec string follows the pattern ``[impl][version][t][-arch][-machine]``. Every
221
221
  Spec["Spec string"] --> Impl["impl<br>(optional)"]
222
222
  Impl --> Version["version<br>(optional)"]
223
223
  Version --> T["t<br>(optional)"]
224
- T --> Arch["-arch<br>(optional)"]
224
+ T --> D["d / -dbg / -debug<br>(optional)"]
225
+ D --> Arch["-arch<br>(optional)"]
225
226
  Arch --> Machine["-machine<br>(optional)"]
226
227
 
227
228
  style Impl fill:#4a90d9,stroke:#2a5f8f,color:#fff
228
229
  style Version fill:#4a9f4a,stroke:#2a6f2a,color:#fff
229
230
  style T fill:#d9904a,stroke:#8f5f2a,color:#fff
231
+ style D fill:#4ad9c4,stroke:#2a8f7f,color:#fff
230
232
  style Arch fill:#d94a4a,stroke:#8f2a2a,color:#fff
231
233
  style Machine fill:#904ad9,stroke:#5f2a8f,color:#fff
232
234
 
@@ -237,9 +239,17 @@ A spec string follows the pattern ``[impl][version][t][-arch][-machine]``. Every
237
239
  - **version** -- dotted version number (``3``, ``3.12``, or ``3.12.1``). You can also write
238
240
  ``312`` as shorthand for ``3.12``.
239
241
  - **t** -- appended directly after the version. Matches free-threaded (no-GIL) builds only.
242
+ - **d** -- require a debug (``Py_DEBUG``) build. Written as ``d`` right after the version (the ABI flag
243
+ form, as in ``python3.13d``) or as Debian's ``-dbg`` / ``-debug`` suffix (``python3.13-dbg``). When
244
+ omitted the build type is unconstrained.
240
245
  - **-arch** -- ``-32`` or ``-64`` for 32-bit or 64-bit interpreters.
241
246
  - **-machine** -- the CPU instruction set: ``-arm64``, ``-x86_64``, ``-aarch64``, ``-riscv64``, etc.
242
247
 
248
+ Order matters: the parts appear in the sequence shown above. Combine the ABI flags as ``python3.13td``
249
+ (free-threaded then debug, the order CPython uses in ``sys.abiflags``), and keep ``-arch`` before
250
+ ``-machine`` as in ``python3.13d-64-arm64``. Mixing ``d`` with ``-dbg`` in one spec, or swapping the arch
251
+ and machine segments, leaves the string unrecognized, so it falls back to a literal executable-name match.
252
+
243
253
  **Full examples:**
244
254
 
245
255
  .. list-table::
@@ -258,6 +268,10 @@ A spec string follows the pattern ``[impl][version][t][-arch][-machine]``. Every
258
268
  - PyPy 3.9
259
269
  * - ``python3.13t``
260
270
  - Free-threaded (no-GIL) CPython 3.13
271
+ * - ``python3.13-dbg``
272
+ - Debug (``Py_DEBUG``) CPython 3.13 (``python3.13d`` also works)
273
+ * - ``python3.13td``
274
+ - Free-threaded debug CPython 3.13
261
275
  * - ``python3.12-64``
262
276
  - 64-bit CPython 3.12
263
277
  * - ``python3.12-64-arm64``
@@ -16,6 +16,25 @@ standard locations.
16
16
  if info is not None:
17
17
  print(info.executable)
18
18
 
19
+ Select a debug build
20
+ ----------------------
21
+
22
+ To require a debug (``Py_DEBUG``) interpreter, add the debug marker to the spec. Both the ABI-flag form
23
+ ``python3.13d`` and the Debian package name ``python3.13-dbg`` work, and each resolves to the same
24
+ interpreter (Debian ships ``/usr/bin/python3.13d`` and ``/usr/bin/python3.13-dbg`` side by side). A spec with no
25
+ marker keeps matching either build, so reach for the marker only when you need the debug build.
26
+
27
+ .. code-block:: python
28
+
29
+ from python_discovery import get_interpreter
30
+
31
+ info = get_interpreter("python3.13-dbg")
32
+ if info is not None:
33
+ assert info.debug_build
34
+
35
+ Combine the marker with the free-threaded ``t`` flag to select a free-threaded debug build, written
36
+ ``python3.13td`` (ABI-flag order is ``t`` then ``d``) or ``python3.13t-dbg``.
37
+
19
38
  Restrict the search environment
20
39
  ---------------------------------
21
40
 
@@ -135,6 +154,7 @@ Once you have a :class:`~python_discovery.PythonInfo`, you can inspect everythin
135
154
  +sysconfig_paths: dict
136
155
  +machine: str
137
156
  +free_threaded: bool
157
+ +debug_build: bool
138
158
  }
139
159
 
140
160
  .. code-block:: python
@@ -154,6 +174,7 @@ Once you have a :class:`~python_discovery.PythonInfo`, you can inspect everythin
154
174
  info.platform # sys.platform value ("linux", "darwin", "win32").
155
175
  info.machine # ISA: "arm64", "x86_64", etc.
156
176
  info.free_threaded # True if this is a no-GIL build.
177
+ info.debug_build # True if this is a Py_DEBUG build.
157
178
  info.sysconfig_vars # All sysconfig.get_config_vars() values.
158
179
  info.sysconfig_paths # All sysconfig.get_paths() values.
159
180
 
@@ -142,12 +142,14 @@ You can add more constraints to narrow the search.
142
142
  Spec["Spec string"] --> Impl["impl<br>(optional)"]
143
143
  Impl --> Version["version<br>(optional)"]
144
144
  Version --> T["t<br>(optional)"]
145
- T --> Arch["-arch<br>(optional)"]
145
+ T --> D["d / -dbg / -debug<br>(optional)"]
146
+ D --> Arch["-arch<br>(optional)"]
146
147
  Arch --> Machine["-machine<br>(optional)"]
147
148
 
148
149
  style Impl fill:#4a90d9,stroke:#2a5f8f,color:#fff
149
150
  style Version fill:#4a9f4a,stroke:#2a6f2a,color:#fff
150
151
  style T fill:#d9904a,stroke:#8f5f2a,color:#fff
152
+ style D fill:#4ad9c4,stroke:#2a8f7f,color:#fff
151
153
  style Arch fill:#d94a4a,stroke:#8f2a2a,color:#fff
152
154
  style Machine fill:#904ad9,stroke:#5f2a8f,color:#fff
153
155
 
@@ -167,6 +169,8 @@ Common examples:
167
169
  - PyPy 3.9
168
170
  * - ``python3.13t``
169
171
  - Free-threaded (no-GIL) CPython 3.13
172
+ * - ``python3.13-dbg``
173
+ - Debug (``Py_DEBUG``) CPython 3.13 (``python3.13d`` also works)
170
174
  * - ``python3.12-64``
171
175
  - 64-bit CPython 3.12
172
176
  * - ``python3.12-64-arm64``
@@ -188,11 +192,12 @@ inspecting what a spec means or for building tools on top of python-discovery.
188
192
  .. mermaid::
189
193
 
190
194
  flowchart TD
191
- Input["cpython3.12t-64-arm64"] --> Parse["PythonSpec.from_string_spec()"]
195
+ Input["cpython3.13td-64-arm64"] --> Parse["PythonSpec.from_string_spec()"]
192
196
  Parse --> Spec["PythonSpec"]
193
197
  Spec --> impl["implementation: cpython"]
194
- Spec --> ver["major: 3, minor: 12"]
198
+ Spec --> ver["major: 3, minor: 13"]
195
199
  Spec --> ft["free_threaded: True"]
200
+ Spec --> dbg["debug: True"]
196
201
  Spec --> arch["architecture: 64"]
197
202
  Spec --> mach["machine: arm64"]
198
203
 
@@ -203,11 +208,12 @@ inspecting what a spec means or for building tools on top of python-discovery.
203
208
 
204
209
  from python_discovery import PythonSpec
205
210
 
206
- spec = PythonSpec.from_string_spec("cpython3.12t-64-arm64")
211
+ spec = PythonSpec.from_string_spec("cpython3.13td-64-arm64")
207
212
  spec.implementation # "cpython"
208
213
  spec.major # 3
209
- spec.minor # 12
214
+ spec.minor # 13
210
215
  spec.free_threaded # True
216
+ spec.debug # True
211
217
  spec.architecture # 64
212
218
  spec.machine # "arm64"
213
219
 
@@ -481,6 +481,8 @@ class PythonInfo: # noqa: PLR0904
481
481
  return False
482
482
  if spec.free_threaded is not None and spec.free_threaded != self.free_threaded:
483
483
  return False
484
+ if spec.debug is not None and spec.debug != self.debug_build:
485
+ return False
484
486
  if spec.version_specifier is not None and not self._satisfies_version_specifier(spec):
485
487
  return False
486
488
  return all(
@@ -717,7 +719,7 @@ class PythonInfo: # noqa: PLR0904
717
719
  info = self.from_exe(exe_path, cache, resolve_to_host=False, raise_on_error=False, env=env)
718
720
  if info is None: # ignore if for some reason we can't query
719
721
  return None
720
- for item in ["implementation", "architecture", "machine", "version_info"]:
722
+ for item in ["implementation", "architecture", "machine", "version_info", "free_threaded", "debug_build"]:
721
723
  found = getattr(info, item)
722
724
  searched = getattr(self, item)
723
725
  if found != searched:
@@ -772,7 +774,9 @@ class PythonInfo: # noqa: PLR0904
772
774
  def _find_possible_exe_names(self) -> list[str]:
773
775
  name_candidate = OrderedDict()
774
776
  mods = ["", "t"] if self.free_threaded else [""]
775
- debug_suffixes = ["_d", ""] if self.debug_build else [""]
777
+ # _d (Windows python_d.exe), -dbg (Debian's own name), d (the abiflags form used by
778
+ # upstream/Fedora and also shipped by Debian, so pythonX.Yd / pythonX.Ytd resolve too)
779
+ debug_suffixes = ["_d", "-dbg", "d", ""] if self.debug_build else [""]
776
780
  archs = [f"-{self.architecture}", ""]
777
781
  for name in self._possible_base():
778
782
  for at in (3, 2, 1, 0):
@@ -16,6 +16,7 @@ PATTERN = re.compile(
16
16
  (?P<impl>[a-zA-Z]+)? # implementation (e.g. cpython, pypy)
17
17
  (?P<version>[0-9.]+)? # version (e.g. 3.12, 3.12.1)
18
18
  (?P<threaded>t)? # free-threaded flag
19
+ (?P<debug>d|(?:-dbg|-debug)(?=-|$))? # debug build flag (d, -dbg or -debug)
19
20
  (?:-(?P<arch>32|64))? # architecture bitness
20
21
  (?:-(?P<machine>[a-zA-Z0-9_.]+))? # ISA (e.g. arm64, x86_64, i86pc.64bit)
21
22
  $
@@ -68,6 +69,7 @@ def _parse_spec_pattern(string_spec: str) -> PythonSpec | None:
68
69
  groups = match.groupdict()
69
70
  version = groups["version"]
70
71
  major, minor, micro, threaded = None, None, None, None
72
+ debug = True if groups["debug"] else None # unconstrained unless an explicit d/-dbg/-debug marker is present
71
73
  if version is not None:
72
74
  try:
73
75
  major, minor, micro = _parse_version_parts(version)
@@ -83,7 +85,9 @@ def _parse_spec_pattern(string_spec: str) -> PythonSpec | None:
83
85
  machine = groups.get("machine")
84
86
  if machine is not None:
85
87
  machine = normalize_isa(machine)
86
- return PythonSpec(string_spec, impl, major, minor, micro, arch, None, free_threaded=threaded, machine=machine)
88
+ return PythonSpec(
89
+ string_spec, impl, major, minor, micro, arch, None, free_threaded=threaded, machine=machine, debug=debug
90
+ )
87
91
 
88
92
 
89
93
  def _parse_specifier(string_spec: str) -> PythonSpec | None:
@@ -117,6 +121,7 @@ class PythonSpec:
117
121
  :param path: filesystem path to a specific interpreter, or ``None``.
118
122
  :param free_threaded: whether a free-threaded build is required, or ``None`` for any.
119
123
  :param machine: required ISA (e.g. ``"arm64"``), or ``None`` for any.
124
+ :param debug: whether a debug (``Py_DEBUG``) build is required, or ``None`` for any.
120
125
  :param version_specifier:
121
126
  `version specifier <https://packaging.python.org/en/latest/specifications/version-specifiers/>`_
122
127
  constraints, or ``None``.
@@ -134,6 +139,7 @@ class PythonSpec:
134
139
  *,
135
140
  free_threaded: bool | None = None,
136
141
  machine: str | None = None,
142
+ debug: bool | None = None,
137
143
  version_specifier: SpecifierSet | None = None,
138
144
  ) -> None:
139
145
  self.str_spec = str_spec
@@ -144,6 +150,7 @@ class PythonSpec:
144
150
  self.free_threaded = free_threaded
145
151
  self.architecture = architecture
146
152
  self.machine = machine
153
+ self.debug = debug
147
154
  self.path = path
148
155
  self.version_specifier = version_specifier
149
156
 
@@ -181,10 +188,11 @@ class PythonSpec:
181
188
  else:
182
189
  impl = "python"
183
190
  mod = "t?" if self.free_threaded else ""
191
+ dbg = "(?:d|-dbg|-debug)?" if self.debug else ""
184
192
  suffix = r"\.exe" if windows else ""
185
193
  version_conditional = "?" if windows or self.major is None else ""
186
194
  return re.compile(
187
- rf"(?P<impl>{impl})(?P<v>{version}{mod}){version_conditional}{suffix}$",
195
+ rf"(?P<impl>{impl})(?P<v>{version}{mod}){version_conditional}{dbg}{suffix}$",
188
196
  flags=re.IGNORECASE,
189
197
  )
190
198
 
@@ -263,6 +271,7 @@ class PythonSpec:
263
271
  "machine",
264
272
  "path",
265
273
  "free_threaded",
274
+ "debug",
266
275
  "version_specifier",
267
276
  )
268
277
  return f"{name}({', '.join(f'{k}={getattr(self, k)}' for k in params if getattr(self, k) is not None)})"
@@ -259,6 +259,31 @@ def test_system_executable_no_exact_match(
259
259
  assert warn_similar.msg.startswith("no exact match found, chosen most similar")
260
260
 
261
261
 
262
+ @pytest.mark.parametrize("attr", ["free_threaded", "debug_build"])
263
+ def test_discover_exe_validates_abi_flag(
264
+ attr: str, tmp_path: Path, mocker: MockerFixture, session_cache: DiskCache
265
+ ) -> None:
266
+ def candidate(*, flag: bool, name: str) -> PythonInfo:
267
+ info = copy.deepcopy(CURRENT)
268
+ setattr(info, attr, flag)
269
+ exe = tmp_path / name
270
+ exe.write_text("", encoding="utf-8")
271
+ info.executable = str(exe)
272
+ return info
273
+
274
+ target = candidate(flag=True, name="target")
275
+ mismatch = candidate(flag=False, name="release")
276
+ match = candidate(flag=True, name="debug")
277
+ by_path = {info.executable: info for info in (mismatch, match)}
278
+
279
+ mocker.patch.object(target, "_find_possible_exe_names", return_value=["release", "debug"])
280
+ mocker.patch.object(target, "_find_possible_folders", return_value=[str(tmp_path)])
281
+ mocker.patch.object(target, "from_exe", side_effect=lambda exe, *_a, **_k: by_path[exe])
282
+
283
+ found = target.discover_exe(session_cache, prefix=str(tmp_path), exact=True)
284
+ assert found is match # the release build is refused even though version, impl and arch match
285
+
286
+
262
287
  def test_py_info_ignores_distutils_config(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
263
288
  raw = f"""
264
289
  [install]
@@ -287,6 +312,18 @@ def test_discover_exe_on_path_non_spec_name_match(mocker: MockerFixture) -> None
287
312
  assert CURRENT.satisfies(spec, impl_must_match=True) is True
288
313
 
289
314
 
315
+ def test_satisfies_debug_spec_requires_debug_build(mocker: MockerFixture) -> None:
316
+ spec = PythonSpec.from_string_spec(f"python{CURRENT.version_info.major}.{CURRENT.version_info.minor}-dbg")
317
+ assert spec.debug is True
318
+ mocker.patch.object(CURRENT, "free_threaded", spec.free_threaded)
319
+
320
+ mocker.patch.object(CURRENT, "debug_build", False)
321
+ assert CURRENT.satisfies(spec, impl_must_match=True) is False
322
+
323
+ mocker.patch.object(CURRENT, "debug_build", True)
324
+ assert CURRENT.satisfies(spec, impl_must_match=True) is True
325
+
326
+
290
327
  def test_discover_exe_on_path_non_spec_name_not_match(mocker: MockerFixture) -> None:
291
328
  suffixed_name = f"python{CURRENT.version_info.major}.{CURRENT.version_info.minor}m"
292
329
  if sys.platform == "win32": # pragma: win32 cover
@@ -431,7 +468,10 @@ def test_py_info_debug_build_exe_names(*, debug: bool) -> None:
431
468
  info = copy.deepcopy(CURRENT)
432
469
  info.debug_build = debug
433
470
  names = info._find_possible_exe_names()
471
+ abiflag_name = f"python{info.version_info.major}.{info.version_info.minor}d"
434
472
  assert any("_d" in n for n in names) is debug
473
+ assert any(n.endswith("-dbg") for n in names) is debug
474
+ assert (abiflag_name in names) is debug
435
475
 
436
476
 
437
477
  def test_py_info_debug_build_json_round_trip() -> None:
@@ -186,6 +186,7 @@ def test_specifier_satisfies_with_partial_information() -> None:
186
186
  def test_spec_parse_machine(spec_str: str, expected_machine: str | None) -> None:
187
187
  spec = PythonSpec.from_string_spec(spec_str)
188
188
  assert spec.machine == expected_machine
189
+ assert spec.debug is None # the debug marker sits before -arch/-machine and must not swallow the ISA
189
190
 
190
191
 
191
192
  @pytest.mark.parametrize(
@@ -202,6 +203,31 @@ def test_spec_parse_arch_and_machine_together(spec_str: str, expected_arch: int,
202
203
  assert spec.machine == expected_machine
203
204
 
204
205
 
206
+ @pytest.mark.parametrize(
207
+ "spec_str",
208
+ [
209
+ pytest.param("python3.13-dbg", id="debian-dbg"),
210
+ pytest.param("python3.13-debug", id="debian-debug"),
211
+ pytest.param("python3.13d", id="abi-flag"),
212
+ pytest.param("cpython3.13d", id="impl-abi-flag"),
213
+ pytest.param("python3.13td", id="free-threaded-debug"),
214
+ ],
215
+ )
216
+ def test_spec_parse_debug(spec_str: str) -> None:
217
+ spec = PythonSpec.from_string_spec(spec_str)
218
+
219
+ assert spec.debug is True
220
+ assert spec.major == 3
221
+ assert spec.minor == 13
222
+ assert spec.machine is None
223
+ assert spec.path is None
224
+
225
+
226
+ @pytest.mark.parametrize("spec_str", ["python3.13", "python"])
227
+ def test_spec_parse_debug_unconstrained_without_marker(spec_str: str) -> None:
228
+ assert PythonSpec.from_string_spec(spec_str).debug is None
229
+
230
+
205
231
  @pytest.mark.parametrize(
206
232
  ("left", "right", "expected"),
207
233
  [
@@ -2,6 +2,8 @@ from __future__ import annotations
2
2
 
3
3
  from unittest.mock import MagicMock
4
4
 
5
+ import pytest
6
+
5
7
  from python_discovery import PythonSpec
6
8
 
7
9
 
@@ -86,6 +88,13 @@ def test_generate_re_with_threaded() -> None:
86
88
  assert pat.fullmatch("python3.12t") is not None
87
89
 
88
90
 
91
+ @pytest.mark.parametrize("name", ["python3.12-dbg", "python3.12d", "python3.12"])
92
+ def test_generate_re_with_debug(name: str) -> None:
93
+ spec = PythonSpec.from_string_spec("python3.12-dbg")
94
+ pat = spec.generate_re(windows=False)
95
+ assert pat.fullmatch(name) is not None
96
+
97
+
89
98
  def test_single_digit_version() -> None:
90
99
  spec = PythonSpec.from_string_spec("python3")
91
100
  assert spec.major == 3
@@ -1 +0,0 @@
1
- github: gaborbernat