py_info.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687
  1. """
  2. The PythonInfo contains information about a concrete instance of a Python interpreter.
  3. Note: this file is also used to query target interpreters, so can only use standard library methods
  4. """
  5. from __future__ import annotations
  6. import json
  7. import logging
  8. import os
  9. import platform
  10. import re
  11. import struct
  12. import sys
  13. import sysconfig
  14. import warnings
  15. from collections import OrderedDict, namedtuple
  16. from string import digits
  17. VersionInfo = namedtuple("VersionInfo", ["major", "minor", "micro", "releaselevel", "serial"]) # noqa: PYI024
  18. LOGGER = logging.getLogger(__name__)
  19. def _get_path_extensions():
  20. return list(OrderedDict.fromkeys(["", *os.environ.get("PATHEXT", "").lower().split(os.pathsep)]))
  21. EXTENSIONS = _get_path_extensions()
  22. _CONF_VAR_RE = re.compile(r"\{\w+}")
  23. class PythonInfo: # noqa: PLR0904
  24. """Contains information for a Python interpreter."""
  25. def __init__(self) -> None: # noqa: PLR0915
  26. def abs_path(v):
  27. return None if v is None else os.path.abspath(v) # unroll relative elements from path (e.g. ..)
  28. # qualifies the python
  29. self.platform = sys.platform
  30. self.implementation = platform.python_implementation()
  31. if self.implementation == "PyPy":
  32. self.pypy_version_info = tuple(sys.pypy_version_info)
  33. # this is a tuple in earlier, struct later, unify to our own named tuple
  34. self.version_info = VersionInfo(*sys.version_info)
  35. # Use the same implementation as found in stdlib platform.architecture
  36. # to account for platforms where the maximum integer is not equal the
  37. # pointer size.
  38. self.architecture = 32 if struct.calcsize("P") == 4 else 64 # noqa: PLR2004
  39. # Used to determine some file names.
  40. # See `CPython3Windows.python_zip()`.
  41. self.version_nodot = sysconfig.get_config_var("py_version_nodot")
  42. self.version = sys.version
  43. self.os = os.name
  44. self.free_threaded = sysconfig.get_config_var("Py_GIL_DISABLED") == 1
  45. # information about the prefix - determines python home
  46. self.prefix = abs_path(getattr(sys, "prefix", None)) # prefix we think
  47. self.base_prefix = abs_path(getattr(sys, "base_prefix", None)) # venv
  48. self.real_prefix = abs_path(getattr(sys, "real_prefix", None)) # old virtualenv
  49. # information about the exec prefix - dynamic stdlib modules
  50. self.base_exec_prefix = abs_path(getattr(sys, "base_exec_prefix", None))
  51. self.exec_prefix = abs_path(getattr(sys, "exec_prefix", None))
  52. self.executable = abs_path(sys.executable) # the executable we were invoked via
  53. self.original_executable = abs_path(self.executable) # the executable as known by the interpreter
  54. self.system_executable = self._fast_get_system_executable() # the executable we are based of (if available)
  55. try:
  56. __import__("venv")
  57. has = True
  58. except ImportError:
  59. has = False
  60. self.has_venv = has
  61. self.path = sys.path
  62. self.file_system_encoding = sys.getfilesystemencoding()
  63. self.stdout_encoding = getattr(sys.stdout, "encoding", None)
  64. scheme_names = sysconfig.get_scheme_names()
  65. if "venv" in scheme_names:
  66. self.sysconfig_scheme = "venv"
  67. self.sysconfig_paths = {
  68. i: sysconfig.get_path(i, expand=False, scheme=self.sysconfig_scheme) for i in sysconfig.get_path_names()
  69. }
  70. # we cannot use distutils at all if "venv" exists, distutils don't know it
  71. self.distutils_install = {}
  72. # debian / ubuntu python 3.10 without `python3-distutils` will report
  73. # mangled `local/bin` / etc. names for the default prefix
  74. # intentionally select `posix_prefix` which is the unaltered posix-like paths
  75. elif sys.version_info[:2] == (3, 10) and "deb_system" in scheme_names:
  76. self.sysconfig_scheme = "posix_prefix"
  77. self.sysconfig_paths = {
  78. i: sysconfig.get_path(i, expand=False, scheme=self.sysconfig_scheme) for i in sysconfig.get_path_names()
  79. }
  80. # we cannot use distutils at all if "venv" exists, distutils don't know it
  81. self.distutils_install = {}
  82. else:
  83. self.sysconfig_scheme = None
  84. self.sysconfig_paths = {i: sysconfig.get_path(i, expand=False) for i in sysconfig.get_path_names()}
  85. self.distutils_install = self._distutils_install().copy()
  86. # https://bugs.python.org/issue22199
  87. makefile = getattr(sysconfig, "get_makefile_filename", getattr(sysconfig, "_get_makefile_filename", None))
  88. self.sysconfig = {
  89. k: v
  90. for k, v in [
  91. # a list of content to store from sysconfig
  92. ("makefile_filename", makefile()),
  93. ]
  94. if k is not None
  95. }
  96. config_var_keys = set()
  97. for element in self.sysconfig_paths.values():
  98. config_var_keys.update(k[1:-1] for k in _CONF_VAR_RE.findall(element))
  99. config_var_keys.add("PYTHONFRAMEWORK")
  100. self.sysconfig_vars = {i: sysconfig.get_config_var(i or "") for i in config_var_keys}
  101. if "TCL_LIBRARY" in os.environ:
  102. self.tcl_lib, self.tk_lib = self._get_tcl_tk_libs()
  103. else:
  104. self.tcl_lib, self.tk_lib = None, None
  105. confs = {
  106. k: (self.system_prefix if v is not None and v.startswith(self.prefix) else v)
  107. for k, v in self.sysconfig_vars.items()
  108. }
  109. self.system_stdlib = self.sysconfig_path("stdlib", confs)
  110. self.system_stdlib_platform = self.sysconfig_path("platstdlib", confs)
  111. self.max_size = getattr(sys, "maxsize", getattr(sys, "maxint", None))
  112. self._creators = None
  113. @staticmethod
  114. def _get_tcl_tk_libs():
  115. """
  116. Detects the tcl and tk libraries using tkinter.
  117. This works reliably but spins up tkinter, which is heavy if you don't need it.
  118. """
  119. tcl_lib, tk_lib = None, None
  120. try:
  121. import tkinter as tk # noqa: PLC0415
  122. except ImportError:
  123. pass
  124. else:
  125. try:
  126. tcl = tk.Tcl()
  127. tcl_lib = tcl.eval("info library")
  128. # Try to get TK library path directly first
  129. try:
  130. tk_lib = tcl.eval("set tk_library")
  131. if tk_lib and os.path.isdir(tk_lib):
  132. pass # We found it directly
  133. else:
  134. tk_lib = None # Reset if invalid
  135. except tk.TclError:
  136. tk_lib = None
  137. # If direct query failed, try constructing the path
  138. if tk_lib is None:
  139. tk_version = tcl.eval("package require Tk")
  140. tcl_parent = os.path.dirname(tcl_lib)
  141. # Try different version formats
  142. version_variants = [
  143. tk_version, # Full version like "8.6.12"
  144. ".".join(tk_version.split(".")[:2]), # Major.minor like "8.6"
  145. tk_version.split(".")[0], # Just major like "8"
  146. ]
  147. for version in version_variants:
  148. tk_lib_path = os.path.join(tcl_parent, f"tk{version}")
  149. if not os.path.isdir(tk_lib_path):
  150. continue
  151. # Validate it's actually a TK directory
  152. if os.path.exists(os.path.join(tk_lib_path, "tk.tcl")):
  153. tk_lib = tk_lib_path
  154. break
  155. except tk.TclError:
  156. pass
  157. return tcl_lib, tk_lib
  158. def _fast_get_system_executable(self):
  159. """Try to get the system executable by just looking at properties."""
  160. # if we're not in a virtual environment, this is already a system python, so return the original executable
  161. # note we must choose the original and not the pure executable as shim scripts might throw us off
  162. if not (self.real_prefix or (self.base_prefix is not None and self.base_prefix != self.prefix)):
  163. return self.original_executable
  164. # if this is NOT a virtual environment, can't determine easily, bail out
  165. if self.real_prefix is not None:
  166. return None
  167. base_executable = getattr(sys, "_base_executable", None) # some platforms may set this to help us
  168. if base_executable is None: # use the saved system executable if present
  169. return None
  170. # we know we're in a virtual environment, can not be us
  171. if sys.executable == base_executable:
  172. return None
  173. # We're not in a venv and base_executable exists; use it directly
  174. if os.path.exists(base_executable):
  175. return base_executable
  176. # Try fallback for POSIX virtual environments
  177. return self._try_posix_fallback_executable(base_executable)
  178. def _try_posix_fallback_executable(self, base_executable):
  179. """
  180. Try to find a versioned Python binary as fallback for POSIX virtual environments.
  181. Python may return "python" because it was invoked from the POSIX virtual environment
  182. however some installs/distributions do not provide a version-less "python" binary in
  183. the system install location (see PEP 394) so try to fallback to a versioned binary.
  184. Gate this to Python 3.11 as `sys._base_executable` path resolution is now relative to
  185. the 'home' key from pyvenv.cfg which often points to the system install location.
  186. """
  187. major, minor = self.version_info.major, self.version_info.minor
  188. if self.os != "posix" or (major, minor) < (3, 11):
  189. return None
  190. # search relative to the directory of sys._base_executable
  191. base_dir = os.path.dirname(base_executable)
  192. candidates = [f"python{major}", f"python{major}.{minor}"]
  193. if self.implementation == "PyPy":
  194. candidates.extend(["pypy", "pypy3", f"pypy{major}", f"pypy{major}.{minor}"])
  195. for candidate in candidates:
  196. full_path = os.path.join(base_dir, candidate)
  197. if os.path.exists(full_path):
  198. return full_path
  199. return None # in this case we just can't tell easily without poking around FS and calling them, bail
  200. def install_path(self, key):
  201. result = self.distutils_install.get(key)
  202. if result is None: # use sysconfig if sysconfig_scheme is set or distutils is unavailable
  203. # set prefixes to empty => result is relative from cwd
  204. prefixes = self.prefix, self.exec_prefix, self.base_prefix, self.base_exec_prefix
  205. config_var = {k: "" if v in prefixes else v for k, v in self.sysconfig_vars.items()}
  206. result = self.sysconfig_path(key, config_var=config_var).lstrip(os.sep)
  207. return result
  208. @staticmethod
  209. def _distutils_install():
  210. # use distutils primarily because that's what pip does
  211. # https://github.com/pypa/pip/blob/main/src/pip/_internal/locations.py#L95
  212. # note here we don't import Distribution directly to allow setuptools to patch it
  213. with warnings.catch_warnings(): # disable warning for PEP-632
  214. warnings.simplefilter("ignore")
  215. try:
  216. from distutils import dist # noqa: PLC0415
  217. from distutils.command.install import SCHEME_KEYS # noqa: PLC0415
  218. except ImportError: # if removed or not installed ignore
  219. return {}
  220. d = dist.Distribution({"script_args": "--no-user-cfg"}) # conf files not parsed so they do not hijack paths
  221. if hasattr(sys, "_framework"):
  222. sys._framework = None # disable macOS static paths for framework # noqa: SLF001
  223. with warnings.catch_warnings(): # disable warning for PEP-632
  224. warnings.simplefilter("ignore")
  225. i = d.get_command_obj("install", create=True)
  226. i.prefix = os.sep # paths generated are relative to prefix that contains the path sep, this makes it relative
  227. i.finalize_options()
  228. return {key: (getattr(i, f"install_{key}")[1:]).lstrip(os.sep) for key in SCHEME_KEYS}
  229. @property
  230. def version_str(self):
  231. return ".".join(str(i) for i in self.version_info[0:3])
  232. @property
  233. def version_release_str(self):
  234. return ".".join(str(i) for i in self.version_info[0:2])
  235. @property
  236. def python_name(self):
  237. version_info = self.version_info
  238. return f"python{version_info.major}.{version_info.minor}"
  239. @property
  240. def is_old_virtualenv(self):
  241. return self.real_prefix is not None
  242. @property
  243. def is_venv(self):
  244. return self.base_prefix is not None
  245. def sysconfig_path(self, key, config_var=None, sep=os.sep):
  246. pattern = self.sysconfig_paths.get(key)
  247. if pattern is None:
  248. return ""
  249. if config_var is None:
  250. config_var = self.sysconfig_vars
  251. else:
  252. base = self.sysconfig_vars.copy()
  253. base.update(config_var)
  254. config_var = base
  255. return pattern.format(**config_var).replace("/", sep)
  256. def creators(self, refresh=False): # noqa: FBT002
  257. if self._creators is None or refresh is True:
  258. from virtualenv.run.plugin.creators import CreatorSelector # noqa: PLC0415
  259. self._creators = CreatorSelector.for_interpreter(self)
  260. return self._creators
  261. @property
  262. def system_include(self):
  263. path = self.sysconfig_path(
  264. "include",
  265. {
  266. k: (self.system_prefix if v is not None and v.startswith(self.prefix) else v)
  267. for k, v in self.sysconfig_vars.items()
  268. },
  269. )
  270. if not os.path.exists(path): # some broken packaging don't respect the sysconfig, fallback to distutils path
  271. # the pattern include the distribution name too at the end, remove that via the parent call
  272. fallback = os.path.join(self.prefix, os.path.dirname(self.install_path("headers")))
  273. if os.path.exists(fallback):
  274. path = fallback
  275. return path
  276. @property
  277. def system_prefix(self):
  278. return self.real_prefix or self.base_prefix or self.prefix
  279. @property
  280. def system_exec_prefix(self):
  281. return self.real_prefix or self.base_exec_prefix or self.exec_prefix
  282. def __repr__(self) -> str:
  283. return "{}({!r})".format(
  284. self.__class__.__name__,
  285. {k: v for k, v in self.__dict__.items() if not k.startswith("_")},
  286. )
  287. def __str__(self) -> str:
  288. return "{}({})".format(
  289. self.__class__.__name__,
  290. ", ".join(
  291. f"{k}={v}"
  292. for k, v in (
  293. ("spec", self.spec),
  294. (
  295. "system"
  296. if self.system_executable is not None and self.system_executable != self.executable
  297. else None,
  298. self.system_executable,
  299. ),
  300. (
  301. "original"
  302. if self.original_executable not in {self.system_executable, self.executable}
  303. else None,
  304. self.original_executable,
  305. ),
  306. ("exe", self.executable),
  307. ("platform", self.platform),
  308. ("version", repr(self.version)),
  309. ("encoding_fs_io", f"{self.file_system_encoding}-{self.stdout_encoding}"),
  310. )
  311. if k is not None
  312. ),
  313. )
  314. @property
  315. def spec(self):
  316. return "{}{}{}-{}".format(
  317. self.implementation,
  318. ".".join(str(i) for i in self.version_info),
  319. "t" if self.free_threaded else "",
  320. self.architecture,
  321. )
  322. @classmethod
  323. def clear_cache(cls, app_data):
  324. # this method is not used by itself, so here and called functions can import stuff locally
  325. from virtualenv.discovery.cached_py_info import clear # noqa: PLC0415
  326. clear(app_data)
  327. cls._cache_exe_discovery.clear()
  328. def satisfies(self, spec, impl_must_match): # noqa: C901, PLR0911, PLR0912
  329. """Check if a given specification can be satisfied by the this python interpreter instance."""
  330. if spec.path:
  331. if self.executable == os.path.abspath(spec.path):
  332. return True # if the path is a our own executable path we're done
  333. if not spec.is_abs:
  334. # if path set, and is not our original executable name, this does not match
  335. basename = os.path.basename(self.original_executable)
  336. spec_path = spec.path
  337. if sys.platform == "win32":
  338. basename, suffix = os.path.splitext(basename)
  339. if spec_path.endswith(suffix):
  340. spec_path = spec_path[: -len(suffix)]
  341. if basename != spec_path:
  342. return False
  343. if (
  344. impl_must_match
  345. and spec.implementation is not None
  346. and spec.implementation.lower() != self.implementation.lower()
  347. ):
  348. return False
  349. if spec.architecture is not None and spec.architecture != self.architecture:
  350. return False
  351. if spec.free_threaded is not None and spec.free_threaded != self.free_threaded:
  352. return False
  353. if spec.version_specifier is not None:
  354. version_info = self.version_info
  355. release = f"{version_info.major}.{version_info.minor}.{version_info.micro}"
  356. if version_info.releaselevel != "final":
  357. suffix = {
  358. "alpha": "a",
  359. "beta": "b",
  360. "candidate": "rc",
  361. }.get(version_info.releaselevel)
  362. if suffix is not None:
  363. release = f"{release}{suffix}{version_info.serial}"
  364. if not spec.version_specifier.contains(release):
  365. return False
  366. for our, req in zip(self.version_info[0:3], (spec.major, spec.minor, spec.micro)):
  367. if req is not None and our is not None and our != req:
  368. return False
  369. return True
  370. _current_system = None
  371. _current = None
  372. @classmethod
  373. def current(cls, app_data=None):
  374. """
  375. This locates the current host interpreter information. This might be different than what we run into in case
  376. the host python has been upgraded from underneath us.
  377. """ # noqa: D205
  378. if cls._current is None:
  379. cls._current = cls.from_exe(sys.executable, app_data, raise_on_error=True, resolve_to_host=False)
  380. return cls._current
  381. @classmethod
  382. def current_system(cls, app_data=None) -> PythonInfo:
  383. """
  384. This locates the current host interpreter information. This might be different than what we run into in case
  385. the host python has been upgraded from underneath us.
  386. """ # noqa: D205
  387. if cls._current_system is None:
  388. cls._current_system = cls.from_exe(sys.executable, app_data, raise_on_error=True, resolve_to_host=True)
  389. return cls._current_system
  390. def _to_json(self):
  391. # don't save calculated paths, as these are non primitive types
  392. return json.dumps(self._to_dict(), indent=2)
  393. def _to_dict(self):
  394. data = {var: (getattr(self, var) if var != "_creators" else None) for var in vars(self)}
  395. data["version_info"] = data["version_info"]._asdict() # namedtuple to dictionary
  396. return data
  397. @classmethod
  398. def from_exe( # noqa: PLR0913
  399. cls,
  400. exe,
  401. app_data=None,
  402. raise_on_error=True, # noqa: FBT002
  403. ignore_cache=False, # noqa: FBT002
  404. resolve_to_host=True, # noqa: FBT002
  405. env=None,
  406. ):
  407. """Given a path to an executable get the python information."""
  408. # this method is not used by itself, so here and called functions can import stuff locally
  409. from virtualenv.discovery.cached_py_info import from_exe # noqa: PLC0415
  410. env = os.environ if env is None else env
  411. proposed = from_exe(cls, app_data, exe, env=env, raise_on_error=raise_on_error, ignore_cache=ignore_cache)
  412. if isinstance(proposed, PythonInfo) and resolve_to_host:
  413. try:
  414. proposed = proposed._resolve_to_system(app_data, proposed) # noqa: SLF001
  415. except Exception as exception:
  416. if raise_on_error:
  417. raise
  418. LOGGER.info("ignore %s due cannot resolve system due to %r", proposed.original_executable, exception)
  419. proposed = None
  420. return proposed
  421. @classmethod
  422. def _from_json(cls, payload):
  423. # the dictionary unroll here is to protect against pypy bug of interpreter crashing
  424. raw = json.loads(payload)
  425. return cls._from_dict(raw.copy())
  426. @classmethod
  427. def _from_dict(cls, data):
  428. data["version_info"] = VersionInfo(**data["version_info"]) # restore this to a named tuple structure
  429. result = cls()
  430. result.__dict__ = data.copy()
  431. return result
  432. @classmethod
  433. def _resolve_to_system(cls, app_data, target):
  434. start_executable = target.executable
  435. prefixes = OrderedDict()
  436. while target.system_executable is None:
  437. prefix = target.real_prefix or target.base_prefix or target.prefix
  438. if prefix in prefixes:
  439. if len(prefixes) == 1:
  440. # if we're linking back to ourselves accept ourselves with a WARNING
  441. LOGGER.info("%r links back to itself via prefixes", target)
  442. target.system_executable = target.executable
  443. break
  444. for at, (p, t) in enumerate(prefixes.items(), start=1):
  445. LOGGER.error("%d: prefix=%s, info=%r", at, p, t)
  446. LOGGER.error("%d: prefix=%s, info=%r", len(prefixes) + 1, prefix, target)
  447. msg = "prefixes are causing a circle {}".format("|".join(prefixes.keys()))
  448. raise RuntimeError(msg)
  449. prefixes[prefix] = target
  450. target = target.discover_exe(app_data, prefix=prefix, exact=False)
  451. if target.executable != target.system_executable:
  452. target = cls.from_exe(target.system_executable, app_data)
  453. target.executable = start_executable
  454. return target
  455. _cache_exe_discovery = {} # noqa: RUF012
  456. def discover_exe(self, app_data, prefix, exact=True, env=None): # noqa: FBT002
  457. key = prefix, exact
  458. if key in self._cache_exe_discovery and prefix:
  459. LOGGER.debug("discover exe from cache %s - exact %s: %r", prefix, exact, self._cache_exe_discovery[key])
  460. return self._cache_exe_discovery[key]
  461. LOGGER.debug("discover exe for %s in %s", self, prefix)
  462. # we don't know explicitly here, do some guess work - our executable name should tell
  463. possible_names = self._find_possible_exe_names()
  464. possible_folders = self._find_possible_folders(prefix)
  465. discovered = []
  466. env = os.environ if env is None else env
  467. for folder in possible_folders:
  468. for name in possible_names:
  469. info = self._check_exe(app_data, folder, name, exact, discovered, env)
  470. if info is not None:
  471. self._cache_exe_discovery[key] = info
  472. return info
  473. if exact is False and discovered:
  474. info = self._select_most_likely(discovered, self)
  475. folders = os.pathsep.join(possible_folders)
  476. self._cache_exe_discovery[key] = info
  477. LOGGER.debug("no exact match found, chosen most similar of %s within base folders %s", info, folders)
  478. return info
  479. msg = "failed to detect {} in {}".format("|".join(possible_names), os.pathsep.join(possible_folders))
  480. raise RuntimeError(msg)
  481. def _check_exe(self, app_data, folder, name, exact, discovered, env): # noqa: PLR0913
  482. exe_path = os.path.join(folder, name)
  483. if not os.path.exists(exe_path):
  484. return None
  485. info = self.from_exe(exe_path, app_data, resolve_to_host=False, raise_on_error=False, env=env)
  486. if info is None: # ignore if for some reason we can't query
  487. return None
  488. for item in ["implementation", "architecture", "version_info"]:
  489. found = getattr(info, item)
  490. searched = getattr(self, item)
  491. if found != searched:
  492. if item == "version_info":
  493. found, searched = ".".join(str(i) for i in found), ".".join(str(i) for i in searched)
  494. executable = info.executable
  495. LOGGER.debug("refused interpreter %s because %s differs %s != %s", executable, item, found, searched)
  496. if exact is False:
  497. discovered.append(info)
  498. break
  499. else:
  500. return info
  501. return None
  502. @staticmethod
  503. def _select_most_likely(discovered, target):
  504. # no exact match found, start relaxing our requirements then to facilitate system package upgrades that
  505. # could cause this (when using copy strategy of the host python)
  506. def sort_by(info):
  507. # we need to setup some priority of traits, this is as follows:
  508. # implementation, major, minor, micro, architecture, tag, serial
  509. matches = [
  510. info.implementation == target.implementation,
  511. info.version_info.major == target.version_info.major,
  512. info.version_info.minor == target.version_info.minor,
  513. info.architecture == target.architecture,
  514. info.version_info.micro == target.version_info.micro,
  515. info.version_info.releaselevel == target.version_info.releaselevel,
  516. info.version_info.serial == target.version_info.serial,
  517. ]
  518. return sum((1 << pos if match else 0) for pos, match in enumerate(reversed(matches)))
  519. sorted_discovered = sorted(discovered, key=sort_by, reverse=True) # sort by priority in decreasing order
  520. return sorted_discovered[0]
  521. def _find_possible_folders(self, inside_folder):
  522. candidate_folder = OrderedDict()
  523. executables = OrderedDict()
  524. executables[os.path.realpath(self.executable)] = None
  525. executables[self.executable] = None
  526. executables[os.path.realpath(self.original_executable)] = None
  527. executables[self.original_executable] = None
  528. for exe in executables:
  529. base = os.path.dirname(exe)
  530. # following path pattern of the current
  531. if base.startswith(self.prefix):
  532. relative = base[len(self.prefix) :]
  533. candidate_folder[f"{inside_folder}{relative}"] = None
  534. # or at root level
  535. candidate_folder[inside_folder] = None
  536. return [i for i in candidate_folder if os.path.exists(i)]
  537. def _find_possible_exe_names(self):
  538. name_candidate = OrderedDict()
  539. for name in self._possible_base():
  540. for at in (3, 2, 1, 0):
  541. version = ".".join(str(i) for i in self.version_info[:at])
  542. mods = [""]
  543. if self.free_threaded:
  544. mods.append("t")
  545. for mod in mods:
  546. for arch in [f"-{self.architecture}", ""]:
  547. for ext in EXTENSIONS:
  548. candidate = f"{name}{version}{mod}{arch}{ext}"
  549. name_candidate[candidate] = None
  550. return list(name_candidate.keys())
  551. def _possible_base(self):
  552. possible_base = OrderedDict()
  553. basename = os.path.splitext(os.path.basename(self.executable))[0].rstrip(digits)
  554. possible_base[basename] = None
  555. possible_base[self.implementation] = None
  556. # python is always the final option as in practice is used by multiple implementation as exe name
  557. if "python" in possible_base:
  558. del possible_base["python"]
  559. possible_base["python"] = None
  560. for base in possible_base:
  561. lower = base.lower()
  562. yield lower
  563. from virtualenv.info import fs_is_case_sensitive # noqa: PLC0415
  564. if fs_is_case_sensitive():
  565. if base != lower:
  566. yield base
  567. upper = base.upper()
  568. if upper != base:
  569. yield upper
  570. if __name__ == "__main__":
  571. # dump a JSON representation of the current python
  572. argv = sys.argv[1:]
  573. if len(argv) >= 1:
  574. start_cookie = argv[0]
  575. argv = argv[1:]
  576. else:
  577. start_cookie = ""
  578. if len(argv) >= 1:
  579. end_cookie = argv[0]
  580. argv = argv[1:]
  581. else:
  582. end_cookie = ""
  583. sys.argv = sys.argv[:1] + argv
  584. info = PythonInfo()._to_json() # noqa: SLF001
  585. sys.stdout.write("".join((start_cookie[::-1], info, end_cookie[::-1])))