python-discovery 1.6.2__py3-none-any.whl → 1.6.3__py3-none-any.whl

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.
@@ -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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: python-discovery
3
- Version: 1.6.2
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
@@ -1,17 +1,17 @@
1
1
  python_discovery/__init__.py,sha256=Hi4j3pMvC8Fo8KoNL-wOHsPSCUHwY4c0qvv5sjnFzME,808
2
2
  python_discovery/_cache.py,sha256=svmG2GzUQzd2xTjAbm2eX1CKxxGAglfv4kxaPuxwPIA,5147
3
- python_discovery/_cached_py_info.py,sha256=d-46_ydj-tJNUwaJK5CHp5OKooJkW2t7lWVNBg9Fa4U,11151
3
+ python_discovery/_cached_py_info.py,sha256=15P5_TODxkZCkNfD2Uk5dtPxGf0Nea_aWm2N7uY9zbY,12059
4
4
  python_discovery/_compat.py,sha256=iqxGuhK9rPaiui80G1H1KrweoMJ16gyLYEmUrSXrp5c,709
5
5
  python_discovery/_discovery.py,sha256=BDHC1XEx0XNStEIbXjujpHHm0nkBpe26bl26YVHSWhM,22488
6
- python_discovery/_py_info.py,sha256=dov0UFDbcbKlNW765puRgLQuAvbc_ze0Mpsdb7Tixgs,25797
6
+ python_discovery/_py_info.py,sha256=rRyixfRnqyrmudr7SSSKLxKfsCjdfSezH30YE6n773U,26672
7
7
  python_discovery/_py_info_collect.py,sha256=3PNlySbxNSHDjBw8KJ_olVpOXww1qhGdY28YoLfF0Z8,16160
8
- python_discovery/_py_spec.py,sha256=O1RMC9JQ7m5SRzF6cEELxnIWZpadXYWO4bWDD7TemUs,10965
8
+ python_discovery/_py_spec.py,sha256=1L7uVqpLdTVvHX6ZcXiUICFAfnOcJaThfdCGIntp7l4,11073
9
9
  python_discovery/_specifier.py,sha256=o7MHb0z_mFSLz9gXqMwJoNkHYjgi2biq3Ibvd0_slvc,10457
10
10
  python_discovery/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  python_discovery/_windows/__init__.py,sha256=qthcyGh36b3WkeSbZ4ECRRYLbCOCaQu5X_DRZ98tXfk,315
12
- python_discovery/_windows/_pep514.py,sha256=1TqLTdofSCPu3VFE7D4u2fXweyaqhKJR7b48VmJ9AQQ,7787
13
- python_discovery/_windows/_propose.py,sha256=iMJf6l0TU4SvuV8UsCHHINAe9L5dUT949xyeyMVdong,1565
14
- python_discovery-1.6.2.dist-info/METADATA,sha256=siAqGU63m_LkmFYyYUsdDKAFl5m9ur1upsL1mNvKrJs,5082
15
- python_discovery-1.6.2.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
16
- python_discovery-1.6.2.dist-info/licenses/LICENSE,sha256=kOJeH68qSq6o2V7o5_VziwUqGswMnFgGgQH5Mahr1Yg,1023
17
- python_discovery-1.6.2.dist-info/RECORD,,
12
+ python_discovery/_windows/_pep514.py,sha256=Vhxe-3hETFIXQrekLqOHmzG5eq5moeR8b0dCS80zbBg,8026
13
+ python_discovery/_windows/_propose.py,sha256=EE-N-BiNgrFU9mQ34SkoF5gRaNjtspghssQxT9uRQi0,1756
14
+ python_discovery-1.6.3.dist-info/METADATA,sha256=WsZZH1Wr81WH9g-ea1Ftj0PuktCQbt6fsIK3TQqzUU8,5082
15
+ python_discovery-1.6.3.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
16
+ python_discovery-1.6.3.dist-info/licenses/LICENSE,sha256=kOJeH68qSq6o2V7o5_VziwUqGswMnFgGgQH5Mahr1Yg,1023
17
+ python_discovery-1.6.3.dist-info/RECORD,,