pex 2.54.2__py2.py3-none-any.whl → 2.69.0__py2.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.

Potentially problematic release.


This version of pex might be problematic. Click here for more details.

Files changed (180) hide show
  1. pex/auth.py +1 -1
  2. pex/bin/pex.py +15 -2
  3. pex/build_backend/configuration.py +5 -5
  4. pex/build_backend/wrap.py +27 -23
  5. pex/build_system/pep_517.py +4 -1
  6. pex/cache/dirs.py +17 -12
  7. pex/cli/commands/lock.py +302 -165
  8. pex/cli/commands/pip/core.py +4 -12
  9. pex/cli/commands/pip/wheel.py +1 -1
  10. pex/cli/commands/run.py +13 -20
  11. pex/cli/commands/venv.py +85 -16
  12. pex/cli/pex.py +11 -4
  13. pex/common.py +57 -7
  14. pex/compatibility.py +1 -1
  15. pex/dependency_configuration.py +87 -15
  16. pex/dist_metadata.py +143 -25
  17. pex/docs/html/_pagefind/fragment/en_4250138.pf_fragment +0 -0
  18. pex/docs/html/_pagefind/fragment/en_7125dad.pf_fragment +0 -0
  19. pex/docs/html/_pagefind/fragment/en_785d562.pf_fragment +0 -0
  20. pex/docs/html/_pagefind/fragment/en_8e94bb8.pf_fragment +0 -0
  21. pex/docs/html/_pagefind/fragment/en_a0396bb.pf_fragment +0 -0
  22. pex/docs/html/_pagefind/fragment/en_a8a3588.pf_fragment +0 -0
  23. pex/docs/html/_pagefind/fragment/en_c07d988.pf_fragment +0 -0
  24. pex/docs/html/_pagefind/fragment/en_d718411.pf_fragment +0 -0
  25. pex/docs/html/_pagefind/index/en_a2e3c5e.pf_index +0 -0
  26. pex/docs/html/_pagefind/pagefind-entry.json +1 -1
  27. pex/docs/html/_pagefind/pagefind.en_4ce1afa9e3.pf_meta +0 -0
  28. pex/docs/html/_static/documentation_options.js +1 -1
  29. pex/docs/html/_static/pygments.css +164 -146
  30. pex/docs/html/_static/styles/furo.css +1 -1
  31. pex/docs/html/_static/styles/furo.css.map +1 -1
  32. pex/docs/html/api/vars.html +25 -34
  33. pex/docs/html/buildingpex.html +25 -34
  34. pex/docs/html/genindex.html +24 -33
  35. pex/docs/html/index.html +25 -34
  36. pex/docs/html/recipes.html +25 -34
  37. pex/docs/html/scie.html +25 -34
  38. pex/docs/html/search.html +24 -33
  39. pex/docs/html/whatispex.html +25 -34
  40. pex/entry_points_txt.py +98 -0
  41. pex/environment.py +54 -33
  42. pex/finders.py +1 -1
  43. pex/hashing.py +71 -9
  44. pex/installed_wheel.py +141 -0
  45. pex/interpreter.py +41 -38
  46. pex/interpreter_constraints.py +25 -25
  47. pex/interpreter_implementation.py +40 -0
  48. pex/jobs.py +13 -6
  49. pex/pep_376.py +68 -384
  50. pex/pep_425.py +11 -2
  51. pex/pep_427.py +937 -205
  52. pex/pep_508.py +4 -5
  53. pex/pex_builder.py +5 -8
  54. pex/pex_info.py +14 -9
  55. pex/pip/dependencies/__init__.py +85 -13
  56. pex/pip/dependencies/requires.py +38 -3
  57. pex/pip/foreign_platform/__init__.py +4 -3
  58. pex/pip/installation.py +2 -2
  59. pex/pip/local_project.py +6 -14
  60. pex/pip/package_repositories/__init__.py +78 -0
  61. pex/pip/package_repositories/link_collector.py +96 -0
  62. pex/pip/tool.py +139 -33
  63. pex/pip/vcs.py +109 -43
  64. pex/pip/version.py +8 -1
  65. pex/requirements.py +121 -16
  66. pex/resolve/config.py +5 -1
  67. pex/resolve/configured_resolve.py +32 -10
  68. pex/resolve/configured_resolver.py +10 -39
  69. pex/resolve/downloads.py +4 -3
  70. pex/resolve/lock_downloader.py +16 -23
  71. pex/resolve/lock_resolver.py +41 -51
  72. pex/resolve/locked_resolve.py +89 -32
  73. pex/resolve/locker.py +145 -101
  74. pex/resolve/locker_patches.py +123 -197
  75. pex/resolve/lockfile/create.py +232 -87
  76. pex/resolve/lockfile/download_manager.py +5 -1
  77. pex/resolve/lockfile/json_codec.py +103 -28
  78. pex/resolve/lockfile/model.py +13 -35
  79. pex/resolve/lockfile/pep_751.py +117 -98
  80. pex/resolve/lockfile/requires_dist.py +17 -262
  81. pex/resolve/lockfile/subset.py +11 -0
  82. pex/resolve/lockfile/targets.py +445 -0
  83. pex/resolve/lockfile/updater.py +22 -10
  84. pex/resolve/package_repository.py +406 -0
  85. pex/resolve/pex_repository_resolver.py +1 -1
  86. pex/resolve/pre_resolved_resolver.py +19 -16
  87. pex/resolve/project.py +233 -47
  88. pex/resolve/requirement_configuration.py +28 -10
  89. pex/resolve/resolver_configuration.py +18 -32
  90. pex/resolve/resolver_options.py +234 -28
  91. pex/resolve/resolvers.py +3 -12
  92. pex/resolve/target_options.py +18 -2
  93. pex/resolve/target_system.py +908 -0
  94. pex/resolve/venv_resolver.py +670 -0
  95. pex/resolver.py +673 -209
  96. pex/scie/__init__.py +40 -1
  97. pex/scie/model.py +2 -0
  98. pex/scie/science.py +25 -3
  99. pex/sdist.py +219 -0
  100. pex/sh_boot.py +24 -21
  101. pex/sysconfig.py +5 -3
  102. pex/targets.py +31 -10
  103. pex/third_party/__init__.py +1 -1
  104. pex/tools/commands/repository.py +48 -25
  105. pex/vendor/__init__.py +4 -9
  106. pex/vendor/__main__.py +65 -41
  107. pex/vendor/_vendored/ansicolors/.layout.json +1 -1
  108. pex/vendor/_vendored/ansicolors/ansicolors-1.1.8.dist-info/RECORD +11 -0
  109. pex/vendor/_vendored/ansicolors/ansicolors-1.1.8.pex-info/original-whl-info.json +1 -0
  110. pex/vendor/_vendored/appdirs/.layout.json +1 -1
  111. pex/vendor/_vendored/appdirs/appdirs-1.4.4.dist-info/RECORD +7 -0
  112. pex/vendor/_vendored/appdirs/appdirs-1.4.4.pex-info/original-whl-info.json +1 -0
  113. pex/vendor/_vendored/attrs/.layout.json +1 -1
  114. pex/vendor/_vendored/attrs/attrs-21.5.0.dev0.dist-info/RECORD +37 -0
  115. pex/vendor/_vendored/attrs/attrs-21.5.0.dev0.pex-info/original-whl-info.json +1 -0
  116. pex/vendor/_vendored/packaging_20_9/.layout.json +1 -1
  117. pex/vendor/_vendored/packaging_20_9/packaging-20.9.dist-info/RECORD +20 -0
  118. pex/vendor/_vendored/packaging_20_9/packaging-20.9.pex-info/original-whl-info.json +1 -0
  119. pex/vendor/_vendored/packaging_20_9/pyparsing-2.4.7.dist-info/RECORD +7 -0
  120. pex/vendor/_vendored/packaging_20_9/pyparsing-2.4.7.pex-info/original-whl-info.json +1 -0
  121. pex/vendor/_vendored/packaging_21_3/.layout.json +1 -1
  122. pex/vendor/_vendored/packaging_21_3/packaging-21.3.dist-info/RECORD +20 -0
  123. pex/vendor/_vendored/packaging_21_3/packaging-21.3.pex-info/original-whl-info.json +1 -0
  124. pex/vendor/_vendored/packaging_21_3/pyparsing-3.0.7.dist-info/RECORD +18 -0
  125. pex/vendor/_vendored/packaging_21_3/pyparsing-3.0.7.pex-info/original-whl-info.json +1 -0
  126. pex/vendor/_vendored/packaging_24_0/.layout.json +1 -1
  127. pex/vendor/_vendored/packaging_24_0/packaging-24.0.dist-info/RECORD +22 -0
  128. pex/vendor/_vendored/packaging_24_0/packaging-24.0.pex-info/original-whl-info.json +1 -0
  129. pex/vendor/_vendored/packaging_25_0/.layout.json +1 -1
  130. pex/vendor/_vendored/packaging_25_0/packaging-25.0.dist-info/RECORD +24 -0
  131. pex/vendor/_vendored/packaging_25_0/packaging-25.0.pex-info/original-whl-info.json +1 -0
  132. pex/vendor/_vendored/pip/.layout.json +1 -1
  133. pex/vendor/_vendored/pip/pip/_vendor/certifi/cacert.pem +63 -1
  134. pex/vendor/_vendored/pip/pip-20.3.4.dist-info/RECORD +388 -0
  135. pex/vendor/_vendored/pip/pip-20.3.4.pex-info/original-whl-info.json +1 -0
  136. pex/vendor/_vendored/setuptools/.layout.json +1 -1
  137. pex/vendor/_vendored/setuptools/setuptools-44.0.0+3acb925dd708430aeaf197ea53ac8a752f7c1863.dist-info/RECORD +107 -0
  138. pex/vendor/_vendored/setuptools/setuptools-44.0.0+3acb925dd708430aeaf197ea53ac8a752f7c1863.pex-info/original-whl-info.json +1 -0
  139. pex/vendor/_vendored/toml/.layout.json +1 -1
  140. pex/vendor/_vendored/toml/toml-0.10.2.dist-info/RECORD +11 -0
  141. pex/vendor/_vendored/toml/toml-0.10.2.pex-info/original-whl-info.json +1 -0
  142. pex/vendor/_vendored/tomli/.layout.json +1 -1
  143. pex/vendor/_vendored/tomli/tomli-2.0.1.dist-info/RECORD +10 -0
  144. pex/vendor/_vendored/tomli/tomli-2.0.1.pex-info/original-whl-info.json +1 -0
  145. pex/venv/installer.py +46 -19
  146. pex/venv/venv_pex.py +6 -3
  147. pex/version.py +1 -1
  148. pex/wheel.py +188 -40
  149. pex/whl.py +67 -0
  150. pex/windows/__init__.py +14 -11
  151. {pex-2.54.2.dist-info → pex-2.69.0.dist-info}/METADATA +6 -5
  152. {pex-2.54.2.dist-info → pex-2.69.0.dist-info}/RECORD +157 -133
  153. {pex-2.54.2.dist-info → pex-2.69.0.dist-info}/entry_points.txt +1 -0
  154. {pex-2.54.2.dist-info → pex-2.69.0.dist-info}/pylock/pylock.toml +1 -1
  155. pex/docs/html/_pagefind/fragment/en_42c9d8c.pf_fragment +0 -0
  156. pex/docs/html/_pagefind/fragment/en_45dd5a2.pf_fragment +0 -0
  157. pex/docs/html/_pagefind/fragment/en_4ca74d2.pf_fragment +0 -0
  158. pex/docs/html/_pagefind/fragment/en_77273d5.pf_fragment +0 -0
  159. pex/docs/html/_pagefind/fragment/en_87a59c5.pf_fragment +0 -0
  160. pex/docs/html/_pagefind/fragment/en_8dc89b5.pf_fragment +0 -0
  161. pex/docs/html/_pagefind/fragment/en_9d1319b.pf_fragment +0 -0
  162. pex/docs/html/_pagefind/fragment/en_e55df9d.pf_fragment +0 -0
  163. pex/docs/html/_pagefind/index/en_1e98c6f.pf_index +0 -0
  164. pex/docs/html/_pagefind/pagefind.en_d1c488ecae.pf_meta +0 -0
  165. pex/vendor/_vendored/ansicolors/ansicolors-1.1.8.dist-info/INSTALLER +0 -1
  166. pex/vendor/_vendored/appdirs/appdirs-1.4.4.dist-info/INSTALLER +0 -1
  167. pex/vendor/_vendored/attrs/attrs-21.5.0.dev0.dist-info/INSTALLER +0 -1
  168. pex/vendor/_vendored/packaging_20_9/packaging-20.9.dist-info/INSTALLER +0 -1
  169. pex/vendor/_vendored/packaging_20_9/pyparsing-2.4.7.dist-info/INSTALLER +0 -1
  170. pex/vendor/_vendored/packaging_21_3/packaging-21.3.dist-info/INSTALLER +0 -1
  171. pex/vendor/_vendored/packaging_21_3/pyparsing-3.0.7.dist-info/INSTALLER +0 -1
  172. pex/vendor/_vendored/packaging_24_0/packaging-24.0.dist-info/INSTALLER +0 -1
  173. pex/vendor/_vendored/packaging_25_0/packaging-25.0.dist-info/INSTALLER +0 -1
  174. pex/vendor/_vendored/pip/pip-20.3.4.dist-info/INSTALLER +0 -1
  175. pex/vendor/_vendored/setuptools/setuptools-44.0.0+3acb925dd708430aeaf197ea53ac8a752f7c1863.dist-info/INSTALLER +0 -1
  176. pex/vendor/_vendored/toml/toml-0.10.2.dist-info/INSTALLER +0 -1
  177. pex/vendor/_vendored/tomli/tomli-2.0.1.dist-info/INSTALLER +0 -1
  178. {pex-2.54.2.dist-info → pex-2.69.0.dist-info}/WHEEL +0 -0
  179. {pex-2.54.2.dist-info → pex-2.69.0.dist-info}/licenses/LICENSE +0 -0
  180. {pex-2.54.2.dist-info → pex-2.69.0.dist-info}/top_level.txt +0 -0
pex/scie/__init__.py CHANGED
@@ -222,6 +222,32 @@ def register_options(parser):
222
222
  "to the patch level."
223
223
  ),
224
224
  )
225
+ parser.add_argument(
226
+ "--scie-pbs-free-threaded",
227
+ "--no-scie-pbs-free-threaded",
228
+ dest="scie_pbs_free_threaded",
229
+ default=False,
230
+ type=bool,
231
+ action=HandleBoolAction,
232
+ help=(
233
+ "Should the Python Standalone Builds CPython distributions be free-threaded. If left "
234
+ "unspecified or otherwise turned off, creating a scie from a PEX with free-threaded "
235
+ "abi wheels will automatically turn this option on. Note that this option is not "
236
+ "compatible with `--scie-pbs-stripped`."
237
+ ),
238
+ )
239
+ parser.add_argument(
240
+ "--scie-pbs-debug",
241
+ "--no-scie-pbs-debug",
242
+ dest="scie_pbs_debug",
243
+ default=False,
244
+ type=bool,
245
+ action=HandleBoolAction,
246
+ help=(
247
+ "Should the Python Standalone Builds CPython distributions be debug builds. Note that "
248
+ "this option is not compatible with `--scie-pbs-stripped`."
249
+ ),
250
+ )
225
251
  parser.add_argument(
226
252
  "--scie-pbs-stripped",
227
253
  "--no-scie-pbs-stripped",
@@ -232,7 +258,8 @@ def register_options(parser):
232
258
  help=(
233
259
  "Should the Python Standalone Builds CPython distributions used be stripped of debug "
234
260
  "symbols or not. For Linux and Windows particularly, the stripped distributions are "
235
- "less than half the size of the distributions that ship with debug symbols."
261
+ "less than half the size of the distributions that ship with debug symbols. Note that"
262
+ "this option is not compatible with `--scie-pbs-free-threaded` or `--scie-pbs-debug`."
236
263
  ),
237
264
  )
238
265
  parser.add_argument(
@@ -318,6 +345,10 @@ def render_options(options):
318
345
  if options.python_version:
319
346
  args.append("--scie-python-version")
320
347
  args.append(".".join(map(str, options.python_version)))
348
+ if options.pbs_free_threaded:
349
+ args.append("--scie-pbs-free-threaded")
350
+ if options.pbs_debug:
351
+ args.append("--scie-pbs-debug")
321
352
  if options.pbs_stripped:
322
353
  args.append("--scie-pbs-stripped")
323
354
  for hash_algorithm in options.hash_algorithms:
@@ -398,6 +429,12 @@ def extract_options(options):
398
429
  )
399
430
  )
400
431
 
432
+ if options.scie_pbs_stripped and (options.scie_pbs_free_threaded or options.scie_pbs_debug):
433
+ raise ValueError(
434
+ "Python Standalone Builds does not release stripped distributions for debug or "
435
+ "free-threaded builds."
436
+ )
437
+
401
438
  science_binary = None # type: Optional[Union[File, Url]]
402
439
  if options.scie_science_binary:
403
440
  url_info = urlparse.urlparse(options.scie_science_binary)
@@ -420,6 +457,8 @@ def extract_options(options):
420
457
  pbs_release=options.scie_pbs_release,
421
458
  pypy_release=options.scie_pypy_release,
422
459
  python_version=python_version,
460
+ pbs_free_threaded=options.scie_pbs_free_threaded,
461
+ pbs_debug=options.scie_pbs_debug,
423
462
  pbs_stripped=options.scie_pbs_stripped,
424
463
  hash_algorithms=tuple(options.scie_hash_algorithms),
425
464
  science_binary=science_binary,
pex/scie/model.py CHANGED
@@ -303,6 +303,8 @@ class ScieOptions(object):
303
303
  python_version = attr.ib(
304
304
  default=None
305
305
  ) # type: Optional[Union[Tuple[int, int], Tuple[int, int, int]]]
306
+ pbs_free_threaded = attr.ib(default=False) # type: bool
307
+ pbs_debug = attr.ib(default=False) # type: bool
306
308
  pbs_stripped = attr.ib(default=False) # type: bool
307
309
  hash_algorithms = attr.ib(default=()) # type: Tuple[str, ...]
308
310
  science_binary = attr.ib(default=None) # type: Optional[Union[File, Url]]
pex/scie/science.py CHANGED
@@ -24,6 +24,7 @@ from pex.hashing import Sha256
24
24
  from pex.os import is_exe
25
25
  from pex.pep_440 import Version
26
26
  from pex.pex import PEX
27
+ from pex.pex_info import PexInfo
27
28
  from pex.result import Error, try_
28
29
  from pex.scie.model import (
29
30
  File,
@@ -37,6 +38,7 @@ from pex.scie.model import (
37
38
  )
38
39
  from pex.sysconfig import SysPlatform
39
40
  from pex.third_party.packaging.specifiers import SpecifierSet
41
+ from pex.third_party.packaging.utils import parse_wheel_filename
40
42
  from pex.third_party.packaging.version import InvalidVersion
41
43
  from pex.tracer import TRACER
42
44
  from pex.typing import TYPE_CHECKING
@@ -66,7 +68,7 @@ class Manifest(object):
66
68
 
67
69
 
68
70
  SCIENCE_RELEASES_URL = "https://github.com/a-scie/lift/releases"
69
- MIN_SCIENCE_VERSION = Version("0.13.0")
71
+ MIN_SCIENCE_VERSION = Version("0.15.1")
70
72
  SCIENCE_REQUIREMENT = SpecifierSet("~={min_version}".format(min_version=MIN_SCIENCE_VERSION))
71
73
 
72
74
 
@@ -104,6 +106,16 @@ class Filenames(Enum["Filenames.Value"]):
104
106
  Filenames.seal()
105
107
 
106
108
 
109
+ def _is_free_threaded_pex(pex_info):
110
+ # type: (PexInfo) -> bool
111
+ for distribution in pex_info.distributions:
112
+ _, _, _, tags = parse_wheel_filename(os.path.basename(distribution))
113
+ for tag in tags:
114
+ if tag.abi.startswith(("cp", "abi3")) and "t" in tag.abi:
115
+ return True
116
+ return False
117
+
118
+
107
119
  def create_manifests(
108
120
  configuration, # type: ScieConfiguration
109
121
  name, # type: str
@@ -270,6 +282,7 @@ def create_manifests(
270
282
  }
271
283
 
272
284
  configure_binding_args = [Filenames.PEX.placeholder, Filenames.CONFIGURE_BINDING.placeholder]
285
+ pbs_free_threaded = _is_free_threaded_pex(pex_info) or configuration.options.pbs_free_threaded
273
286
  for interpreter in configuration.interpreters:
274
287
  lift = lift_template.copy()
275
288
 
@@ -285,10 +298,17 @@ def create_manifests(
285
298
  interpreter.platform.qualified_file_name("{name}-lift.toml".format(name=name)),
286
299
  )
287
300
 
301
+ version_str = interpreter.version_str
302
+ if Provider.PythonBuildStandalone is interpreter.provider:
303
+ if configuration.options.pbs_debug:
304
+ version_str += "d"
305
+ if pbs_free_threaded:
306
+ version_str += "t"
307
+
288
308
  interpreter_config = {
289
309
  "id": "python-distribution",
290
310
  "provider": interpreter.provider.value,
291
- "version": interpreter.version_str,
311
+ "version": version_str,
292
312
  "lazy": configuration.options.style is ScieStyle.LAZY,
293
313
  }
294
314
  if interpreter.release:
@@ -297,7 +317,9 @@ def create_manifests(
297
317
  interpreter_config["base_url"] = "/".join(
298
318
  (configuration.options.assets_base_url, "providers", str(interpreter.provider))
299
319
  )
300
- if Provider.PythonBuildStandalone is interpreter.provider:
320
+ if Provider.PythonBuildStandalone is interpreter.provider and not (
321
+ configuration.options.pbs_debug or pbs_free_threaded
322
+ ):
301
323
  interpreter_config.update(
302
324
  flavor=(
303
325
  "install_only_stripped"
pex/sdist.py ADDED
@@ -0,0 +1,219 @@
1
+ # Copyright 2025 Pex project contributors.
2
+ # Licensed under the Apache License, Version 2.0 (see LICENSE).
3
+
4
+ from __future__ import absolute_import
5
+
6
+ import copy
7
+ import os.path
8
+ import sys
9
+ import tarfile
10
+ from tarfile import TarInfo
11
+
12
+ from pex.compatibility import commonpath
13
+ from pex.typing import TYPE_CHECKING, cast
14
+
15
+ if TYPE_CHECKING:
16
+ from typing import Any, Dict, Optional, Text, TypeVar
17
+
18
+
19
+ class FilterError(tarfile.TarError):
20
+ pass
21
+
22
+
23
+ class AbsolutePathError(FilterError):
24
+ pass
25
+
26
+
27
+ class OutsideDestinationError(FilterError):
28
+ pass
29
+
30
+
31
+ class SpecialFileError(FilterError):
32
+ pass
33
+
34
+
35
+ class AbsoluteLinkError(FilterError):
36
+ pass
37
+
38
+
39
+ class LinkOutsideDestinationError(FilterError):
40
+ pass
41
+
42
+
43
+ _REALPATH_KWARGS = (
44
+ {"strict": getattr(os.path, "ALLOW_MISSING", False)} if sys.version_info[:2] >= (3, 10) else {}
45
+ ) # type: Dict[str, Any]
46
+
47
+
48
+ if TYPE_CHECKING:
49
+ _Text = TypeVar("_Text", str, Text)
50
+
51
+
52
+ def _realpath(path):
53
+ # type: (_Text) -> _Text
54
+ return os.path.realpath(path, **_REALPATH_KWARGS)
55
+
56
+
57
+ def _get_filtered_attrs(
58
+ member, # type: TarInfo
59
+ dest_path, # type: Text
60
+ for_data=True, # type: bool
61
+ ):
62
+ # type: (...) -> Dict[str, Any]
63
+
64
+ # N.B.: Copied from CPython 3.14 stdlib tarfile.py
65
+ # Modifications:
66
+ # + Exception types replicated with error messages placed at call site.
67
+ # + `os.path.realpath` -> `_realpath` to deal with `strict` parameter.
68
+ # + `os.path.commonpath` -> `pex.compatibility.commonpath`
69
+ # + `mode = None` guarded by `sys.version_info[:2] >= (3, 12)` with commentary.
70
+ # + `{uid,gid,uname,gname} = None` guarded by `sys.version_info[:2] >= (3, 12)` with commentary.
71
+
72
+ new_attrs = {} # type: Dict[str, Any]
73
+ name = member.name
74
+ dest_path = _realpath(dest_path)
75
+ # Strip leading / (tar's directory separator) from filenames.
76
+ # Include os.sep (target OS directory separator) as well.
77
+ if name.startswith(("/", os.sep)):
78
+ name = new_attrs["name"] = member.path.lstrip("/" + os.sep)
79
+ if os.path.isabs(name):
80
+ # Path is absolute even after stripping.
81
+ # For example, 'C:/foo' on Windows.
82
+ raise AbsolutePathError("member {name!r} has an absolute path".format(name=member.name))
83
+ # Ensure we stay in the destination
84
+ target_path = _realpath(os.path.join(dest_path, name))
85
+ if commonpath([target_path, dest_path]) != dest_path:
86
+ raise OutsideDestinationError(
87
+ "{name!r} would be extracted to {path!r}, which is outside the destination".format(
88
+ name=member.name, path=target_path
89
+ )
90
+ )
91
+ # Limit permissions (no high bits, and go-w)
92
+ mode = member.mode # type: Optional[int]
93
+ if mode is not None:
94
+ # Strip high bits & group/other write bits
95
+ mode = mode & 0o755
96
+ if for_data:
97
+ # For data, handle permissions & file types
98
+ if member.isreg() or member.islnk():
99
+ if not mode & 0o100:
100
+ # Clear executable bits if not executable by user
101
+ mode &= ~0o111
102
+ # Ensure owner can read & write
103
+ mode |= 0o600
104
+ elif member.isdir() or member.issym():
105
+ if sys.version_info[:2] >= (3, 12):
106
+ # Ignore mode for directories & symlinks
107
+ mode = None
108
+ else:
109
+ # Retain stripped mode since older Pythons do not support None.
110
+ pass
111
+ else:
112
+ # Reject special files
113
+ raise SpecialFileError("{name!r} is a special file".format(name=member.name))
114
+ if mode != member.mode:
115
+ new_attrs["mode"] = mode
116
+ if for_data:
117
+ if sys.version_info[:2] >= (3, 12):
118
+ # Ignore ownership for 'data'
119
+ if member.uid is not None:
120
+ new_attrs["uid"] = None
121
+ if member.gid is not None:
122
+ new_attrs["gid"] = None
123
+ if member.uname is not None:
124
+ new_attrs["uname"] = None
125
+ if member.gname is not None:
126
+ new_attrs["gname"] = None
127
+ else:
128
+ # Retain uid/gid/uname/gname since older Pythons do not support None.
129
+ pass
130
+
131
+ # Check link destination for 'data'
132
+ if member.islnk() or member.issym():
133
+ if os.path.isabs(member.linkname):
134
+ raise AbsoluteLinkError(
135
+ "{name!r} is a link to an absolute path".format(name=member.name)
136
+ )
137
+ normalized = os.path.normpath(member.linkname)
138
+ if normalized != member.linkname:
139
+ new_attrs["linkname"] = normalized
140
+ if member.issym():
141
+ target_path = os.path.join(dest_path, os.path.dirname(name), member.linkname)
142
+ else:
143
+ target_path = os.path.join(dest_path, member.linkname)
144
+ target_path = _realpath(target_path)
145
+ if commonpath([target_path, dest_path]) != dest_path:
146
+ raise LinkOutsideDestinationError(
147
+ "{name!r} would link to {path!r}, which is outside the destination".format(
148
+ name=member.name, path=target_path
149
+ )
150
+ )
151
+ return new_attrs
152
+
153
+
154
+ def _replace(
155
+ member, # type: TarInfo
156
+ attrs, # type: Dict[str, Any]
157
+ ):
158
+ # type: (...) -> TarInfo
159
+
160
+ replace = getattr(member, "replace", None)
161
+ if replace:
162
+ attrs["deep"] = False
163
+ return cast(TarInfo, replace(**attrs))
164
+
165
+ result = copy.copy(member)
166
+ for attr, value in attrs.items():
167
+ setattr(result, attr, value)
168
+ return result
169
+
170
+
171
+ def _data_filter(
172
+ member, # type: TarInfo
173
+ dest_path, # type: Text
174
+ ):
175
+ # type: (...) -> TarInfo
176
+ new_attrs = _get_filtered_attrs(member, dest_path, True)
177
+ if new_attrs:
178
+ return _replace(member, new_attrs)
179
+ return member
180
+
181
+
182
+ _EXTRACTALL_DATA_FILTER_KWARGS = {"filter": "data"} # type: Dict[str, Any]
183
+
184
+
185
+ class InvalidSourceDistributionError(ValueError):
186
+ pass
187
+
188
+
189
+ def extract_tarball(
190
+ tarball_path, # type: Text
191
+ dest_dir, # type: _Text
192
+ ):
193
+ # type: (...) -> _Text
194
+
195
+ with tarfile.open(tarball_path) as tf:
196
+ if sys.version_info[:2] >= (3, 12):
197
+ tf.extractall(dest_dir, **_EXTRACTALL_DATA_FILTER_KWARGS)
198
+ else:
199
+ for tar_info in tf: # type: ignore[unreachable]
200
+ tar_info = _data_filter(tar_info, dest_dir)
201
+ tf.extract(tar_info, dest_dir)
202
+
203
+ listing = os.listdir(dest_dir)
204
+ if len(listing) != 1:
205
+ raise InvalidSourceDistributionError(
206
+ "Expected one top-level project directory to be extracted from {project}, "
207
+ "found {count}: {listing}".format(
208
+ project=tarball_path, count=len(listing), listing=", ".join(listing)
209
+ )
210
+ )
211
+
212
+ project_dir = os.path.join(dest_dir, listing[0])
213
+ if not os.path.isdir(project_dir):
214
+ raise InvalidSourceDistributionError(
215
+ "Expected one top-level project directory to be extracted from {project}, "
216
+ "found file: {path}".format(project=tarball_path, path=listing[0])
217
+ )
218
+
219
+ return project_dir
pex/sh_boot.py CHANGED
@@ -11,14 +11,16 @@ from textwrap import dedent
11
11
  from pex import dist_metadata, variables
12
12
  from pex.compatibility import shlex_quote
13
13
  from pex.dist_metadata import Distribution
14
- from pex.interpreter import PythonInterpreter, calculate_binary_name
14
+ from pex.interpreter import PythonInterpreter
15
15
  from pex.interpreter_constraints import InterpreterConstraints, iter_compatible_versions
16
+ from pex.interpreter_implementation import InterpreterImplementation
16
17
  from pex.layout import Layout
17
18
  from pex.orderedset import OrderedSet
18
19
  from pex.os import WINDOWS
19
20
  from pex.pep_440 import Version
20
21
  from pex.pex_info import PexInfo
21
22
  from pex.targets import Targets
23
+ from pex.third_party.packaging.specifiers import SpecifierSet
22
24
  from pex.typing import TYPE_CHECKING
23
25
  from pex.version import __version__
24
26
 
@@ -32,14 +34,12 @@ else:
32
34
 
33
35
  @attr.s(frozen=True)
34
36
  class PythonBinaryName(object):
35
- name = attr.ib() # type: str
37
+ implementation = attr.ib() # type: InterpreterImplementation.Value
36
38
  version = attr.ib() # type: Tuple[int, ...]
37
39
 
38
40
  def render(self, version_components=2):
39
41
  # type: (int) -> str
40
- return "{name}{version}".format(
41
- name=self.name, version=".".join(map(str, self.version[:version_components]))
42
- )
42
+ return self.implementation.calculate_binary_name(self.version[:version_components])
43
43
 
44
44
 
45
45
  def _calculate_applicable_binary_names(
@@ -55,17 +55,15 @@ def _calculate_applicable_binary_names(
55
55
  ic_majors_minors = OrderedSet() # type: OrderedSet[PythonBinaryName]
56
56
  if interpreter_constraints:
57
57
  ic_majors_minors.update(
58
- PythonBinaryName(
59
- name=calculate_binary_name(platform_python_implementation=name), version=version
60
- )
58
+ PythonBinaryName(implementation=implementation, version=version)
61
59
  for interpreter_constraint in interpreter_constraints
62
60
  for version in iter_compatible_versions(
63
- requires_python=[str(interpreter_constraint.requires_python)]
61
+ requires_python=[interpreter_constraint.specifier]
64
62
  )
65
- for name in (
66
- (interpreter_constraint.name,)
67
- if interpreter_constraint.name
68
- else ("CPython", "PyPy")
63
+ for implementation in (
64
+ (interpreter_constraint.implementation,)
65
+ if interpreter_constraint.implementation
66
+ else InterpreterImplementation.values()
69
67
  )
70
68
  )
71
69
  # If we get targets from ICs, we only want explicitly specified local interpreter targets;
@@ -75,10 +73,10 @@ def _calculate_applicable_binary_names(
75
73
  names = OrderedSet() # type: OrderedSet[PythonBinaryName]
76
74
  # 1. Explicit targets 1st.
77
75
  for target in targets.unique_targets(only_explicit=only_explicit):
78
- if target.python_version is not None:
76
+ if target.implementation and target.python_version is not None:
79
77
  names.add(
80
78
  PythonBinaryName(
81
- name=target.binary_name(version_components=0), version=target.python_version
79
+ implementation=target.implementation, version=target.python_version
82
80
  )
83
81
  )
84
82
 
@@ -89,10 +87,14 @@ def _calculate_applicable_binary_names(
89
87
  # more sophisticated detection and re-direction from these during its own bootstrap. When doing
90
88
  # so, select these interpreters from newest to oldest since it more likely any given machine
91
89
  # will have Python 3 at this point than it will Python 2.
92
- pex_requires_python = ">=2.7"
93
- dist = dist_metadata.find_distribution("pex") # type: Optional[Distribution]
94
- if dist and dist.metadata.version == Version(__version__):
95
- pex_requires_python = str(dist.metadata.requires_python)
90
+ pex_requires_python_override = os.environ.get("_PEX_REQUIRES_PYTHON", None)
91
+ if pex_requires_python_override:
92
+ pex_requires_python = SpecifierSet(pex_requires_python_override)
93
+ else:
94
+ pex_requires_python = SpecifierSet(">=2.7")
95
+ dist = dist_metadata.find_distribution("pex") # type: Optional[Distribution]
96
+ if dist and dist.metadata.version == Version(__version__):
97
+ pex_requires_python = dist.metadata.requires_python
96
98
  pex_supported_python_versions = tuple(
97
99
  reversed(list(iter_compatible_versions(requires_python=[pex_requires_python])))
98
100
  )
@@ -102,11 +104,12 @@ def _calculate_applicable_binary_names(
102
104
  # for CPython end targets and for PyPy it need not be quite as fast since it inherently asks you
103
105
  # to trade startup latency for longer term jit performance.
104
106
  names.update(
105
- PythonBinaryName(name="python", version=version)
107
+ PythonBinaryName(implementation=InterpreterImplementation.CPYTHON, version=version)
106
108
  for version in pex_supported_python_versions
107
109
  )
108
110
  names.update(
109
- PythonBinaryName(name="pypy", version=version) for version in pex_supported_python_versions
111
+ PythonBinaryName(implementation=InterpreterImplementation.PYPY, version=version)
112
+ for version in pex_supported_python_versions
110
113
  )
111
114
 
112
115
  # Favor more specific interpreter names since these should need re-direction less often.
pex/sysconfig.py CHANGED
@@ -82,7 +82,7 @@ class _PlatformValue(Enum.Value):
82
82
  self.arch = arch
83
83
 
84
84
  @property
85
- def extension(self):
85
+ def exe_extension(self):
86
86
  # type: () -> str
87
87
  return ".exe" if self.os is Os.WINDOWS else ""
88
88
 
@@ -93,12 +93,14 @@ class _PlatformValue(Enum.Value):
93
93
 
94
94
  def binary_name(self, binary_name):
95
95
  # type: (_Text) -> _Text
96
- return "{binary_name}{extension}".format(binary_name=binary_name, extension=self.extension)
96
+ return "{binary_name}{extension}".format(
97
+ binary_name=binary_name, extension=self.exe_extension
98
+ )
97
99
 
98
100
  def qualified_binary_name(self, binary_name):
99
101
  # type: (_Text) -> _Text
100
102
  return "{binary_name}-{platform}{extension}".format(
101
- binary_name=binary_name, platform=self, extension=self.extension
103
+ binary_name=binary_name, platform=self, extension=self.exe_extension
102
104
  )
103
105
 
104
106
  def qualified_file_name(self, file_name):
pex/targets.py CHANGED
@@ -5,8 +5,9 @@ from __future__ import absolute_import
5
5
 
6
6
  import os
7
7
 
8
- from pex.dist_metadata import Distribution, Requirement
9
- from pex.interpreter import PythonInterpreter, calculate_binary_name
8
+ from pex.dist_metadata import Constraint, Distribution
9
+ from pex.interpreter import PythonInterpreter
10
+ from pex.interpreter_implementation import InterpreterImplementation
10
11
  from pex.orderedset import OrderedSet
11
12
  from pex.pep_425 import CompatibilityTags, RankedTag
12
13
  from pex.pep_508 import MarkerEnvironment
@@ -48,14 +49,25 @@ class Target(object):
48
49
  id = attr.ib() # type: str
49
50
  platform = attr.ib() # type: Platform
50
51
  marker_environment = attr.ib() # type: MarkerEnvironment
52
+ implementation = attr.ib(init=False) # type: Optional[InterpreterImplementation.Value]
53
+
54
+ def __attrs_post_init__(self):
55
+ interpreter_implementation = None # type: Optional[InterpreterImplementation.Value]
56
+ for interpreter_impl in InterpreterImplementation.values():
57
+ if interpreter_impl.value == self.marker_environment.platform_python_implementation:
58
+ interpreter_implementation = interpreter_impl
59
+ break
60
+ object.__setattr__(self, "implementation", interpreter_implementation)
51
61
 
52
62
  def binary_name(self, version_components=2):
53
63
  # type: (int) -> str
54
- return calculate_binary_name(
55
- platform_python_implementation=self.marker_environment.platform_python_implementation,
56
- python_version=self.python_version[:version_components]
57
- if self.python_version and version_components > 0
58
- else None,
64
+ interpreter_implementation = self.implementation or InterpreterImplementation.CPYTHON
65
+ return interpreter_implementation.calculate_binary_name(
66
+ version=(
67
+ self.python_version[:version_components]
68
+ if self.python_version and version_components > 0
69
+ else None
70
+ )
59
71
  )
60
72
 
61
73
  @property
@@ -145,7 +157,7 @@ class Target(object):
145
157
 
146
158
  def requirement_applies(
147
159
  self,
148
- requirement, # type: Requirement
160
+ requirement, # type: Constraint
149
161
  extras=(), # type: Iterable[str]
150
162
  ):
151
163
  # type: (...) -> bool
@@ -171,7 +183,7 @@ class Target(object):
171
183
 
172
184
  def wheel_applies(self, wheel):
173
185
  # type: (Distribution) -> WheelEvaluation
174
- wheel_tags = CompatibilityTags.from_wheel(wheel.location)
186
+ wheel_tags = CompatibilityTags.from_wheel(wheel)
175
187
  ranked_tag = self.supported_tags.best_match(wheel_tags)
176
188
  return WheelEvaluation(
177
189
  tags=tuple(wheel_tags),
@@ -193,6 +205,7 @@ class Target(object):
193
205
  return str(self.platform.tag)
194
206
 
195
207
  def render_description(self):
208
+ # type: () -> str
196
209
  raise NotImplementedError()
197
210
 
198
211
  def __repr__(self):
@@ -247,6 +260,7 @@ class LocalInterpreter(Target):
247
260
  return self.interpreter.binary
248
261
 
249
262
  def render_description(self):
263
+ # type: () -> str
250
264
  return "{platform} interpreter at {path}".format(
251
265
  platform=self.interpreter.platform.tag, path=self.interpreter.binary
252
266
  )
@@ -269,6 +283,7 @@ class AbbreviatedPlatform(Target):
269
283
  return self.platform.supported_tags
270
284
 
271
285
  def render_description(self):
286
+ # type: () -> str
272
287
  return "abbreviated platform {platform}".format(platform=self.platform.tag)
273
288
 
274
289
 
@@ -311,6 +326,7 @@ class CompletePlatform(Target):
311
326
  return self._supported_tags
312
327
 
313
328
  def render_description(self):
329
+ # type: () -> str
314
330
  return "complete platform {platform}".format(platform=self.platform.tag)
315
331
 
316
332
 
@@ -330,6 +346,11 @@ class Targets(object):
330
346
  complete_platforms = attr.ib(default=()) # type: Tuple[CompletePlatform, ...]
331
347
  platforms = attr.ib(default=()) # type: Tuple[Optional[Platform], ...]
332
348
 
349
+ @property
350
+ def is_empty(self):
351
+ # type: () -> bool
352
+ return not self.interpreters and not self.complete_platforms and not self.platforms
353
+
333
354
  @property
334
355
  def interpreter(self):
335
356
  # type: () -> Optional[PythonInterpreter]
@@ -391,7 +412,7 @@ class Targets(object):
391
412
 
392
413
  def require_at_most_one_target(self, purpose):
393
414
  # type: (str) -> Union[Optional[Target], Error]
394
- resolved_targets = self.unique_targets(only_explicit=False)
415
+ resolved_targets = self.unique_targets(only_explicit=True)
395
416
  if len(resolved_targets) > 1:
396
417
  return Error(
397
418
  "At most a single target is required for {purpose}.\n"
@@ -639,7 +639,7 @@ def expose_installed_wheels(
639
639
 
640
640
  from pex.atomic_directory import atomic_directory
641
641
  from pex.cache.dirs import InstalledWheelDir
642
- from pex.pep_376 import InstalledWheel
642
+ from pex.installed_wheel import InstalledWheel
643
643
 
644
644
  for path in expose(dists, interpreter=interpreter):
645
645
  # TODO(John Sirois): Maybe consolidate with pex.resolver.BuildAndInstallRequest.