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/environment.py CHANGED
@@ -15,6 +15,7 @@ from pex.dependency_configuration import DependencyConfiguration
15
15
  from pex.dist_metadata import Distribution, Requirement, is_wheel
16
16
  from pex.fingerprinted_distribution import FingerprintedDistribution
17
17
  from pex.inherit_path import InheritPath
18
+ from pex.installed_wheel import InstalledWheel
18
19
  from pex.interpreter import PythonInterpreter
19
20
  from pex.layout import ensure_installed, identify_layout
20
21
  from pex.orderedset import OrderedSet
@@ -26,6 +27,7 @@ from pex.third_party.packaging import specifiers
26
27
  from pex.third_party.packaging.tags import Tag
27
28
  from pex.tracer import TRACER
28
29
  from pex.typing import TYPE_CHECKING
30
+ from pex.whl import repacked_whl
29
31
 
30
32
  if TYPE_CHECKING:
31
33
  from typing import (
@@ -66,6 +68,22 @@ def _import_pkg_resources():
66
68
  return None, False
67
69
 
68
70
 
71
+ def _fd_lt(
72
+ self, # type: FingerprintedDistribution
73
+ other, # type: FingerprintedDistribution
74
+ ):
75
+ # type: (...) -> bool
76
+ if self.project_name.normalized < other.project_name.normalized:
77
+ return True
78
+
79
+ # Since we want to rank higher versions higher (earlier) we need to reverse the natural
80
+ # ordering of Version in Distribution which is least to greatest.
81
+ if self.distribution.metadata.version >= other.distribution.metadata.version:
82
+ return True
83
+
84
+ return self.fingerprint < other.fingerprint
85
+
86
+
69
87
  @attr.s(frozen=True)
70
88
  class _RankedDistribution(object):
71
89
  # N.B.: A distribution implements rich comparison with the leading component being the
@@ -77,9 +95,7 @@ class _RankedDistribution(object):
77
95
  # The attr project type stub file simply misses this.
78
96
  _fd_cmp = attr.cmp_using( # type: ignore[attr-defined]
79
97
  eq=FingerprintedDistribution.__eq__,
80
- # Since we want to rank higher versions higher (earlier) we need to reverse the natural
81
- # ordering of Version in Distribution which is least to greatest.
82
- lt=FingerprintedDistribution.__ge__,
98
+ lt=_fd_lt,
83
99
  )
84
100
 
85
101
  @classmethod
@@ -291,12 +307,7 @@ class PEXEnvironment(object):
291
307
 
292
308
  def iter_distributions(self, result_type_wheel_file=False):
293
309
  # type: (bool) -> Iterator[FingerprintedDistribution]
294
- if result_type_wheel_file:
295
- if not self._pex_info.deps_are_wheel_files:
296
- raise ResolveError(
297
- "Cannot resolve .whl files from PEX at {pex}; its dependencies are in the "
298
- "form of pre-installed wheel chroots.".format(pex=self.source_pex)
299
- )
310
+ if result_type_wheel_file and self._pex_info.deps_are_wheel_files:
300
311
  with TRACER.timed(
301
312
  "Searching dependency cache: {cache}".format(
302
313
  cache=os.path.join(self.source_pex, self._pex_info.internal_cache)
@@ -320,10 +331,17 @@ class PEXEnvironment(object):
320
331
  ):
321
332
  for distribution_name, fingerprint in self._pex_info.distributions.items():
322
333
  dist_path = os.path.join(internal_cache, distribution_name)
323
- yield FingerprintedDistribution(
324
- distribution=Distribution.load(dist_path),
325
- fingerprint=fingerprint,
326
- )
334
+ if result_type_wheel_file:
335
+ yield repacked_whl(
336
+ installed_wheel=dist_path,
337
+ distribution_name=distribution_name,
338
+ fingerprint=fingerprint,
339
+ use_system_time=True,
340
+ )
341
+ else:
342
+ yield FingerprintedDistribution(
343
+ distribution=Distribution.load(dist_path), fingerprint=fingerprint
344
+ )
327
345
 
328
346
  def _update_candidate_distributions(self, distribution_iter):
329
347
  # type: (Iterable[FingerprintedDistribution]) -> None
@@ -768,10 +786,10 @@ class PEXEnvironment(object):
768
786
  current_interpreter = PythonInterpreter.get()
769
787
  pex_warnings.warn(
770
788
  "The legacy `pkg_resources` package cannot be imported by the "
771
- "{interpreter} {version} interpreter at {path}.\n"
789
+ "{implementation} {version} interpreter at {path}.\n"
772
790
  "The following distributions need `pkg_resources` to load some legacy "
773
791
  "namespace packages and may fail to work properly:\n{dists}".format(
774
- interpreter=current_interpreter.identity.interpreter,
792
+ implementation=current_interpreter.identity.implementation,
775
793
  version=current_interpreter.python,
776
794
  path=current_interpreter.binary,
777
795
  dists=dists,
@@ -803,22 +821,25 @@ class PEXEnvironment(object):
803
821
  if dist.location in sys.path:
804
822
  continue
805
823
  with TRACER.timed("Activating %s" % dist, V=2):
806
- if self._pex_info.inherit_path == InheritPath.FALLBACK:
807
- # Prepend location to sys.path.
808
- #
809
- # This ensures that bundled versions of libraries will be used before system-installed
810
- # versions, in case something is installed in both, helping to favor hermeticity in
811
- # the case of non-hermetic PEX files (i.e. those with inherit_path=True).
812
- #
813
- # If the path is not already in sys.path, site.addsitedir will append (not prepend)
814
- # the path to sys.path. But if the path is already in sys.path, site.addsitedir will
815
- # leave sys.path unmodified, but will do everything else it would do. This is not part
816
- # of its advertised contract (which is very vague), but has been verified to be the
817
- # case by inspecting its source for both cpython 2.7 and cpython 3.7.
818
- sys.path.insert(0, dist.location)
819
- else:
820
- sys.path.append(dist.location)
821
-
822
- with TRACER.timed("Adding sitedir", V=2):
823
- site.addsitedir(dist.location)
824
+ for entry in InstalledWheel.load(dist.location).iter_sys_path_entries():
825
+ if self._pex_info.inherit_path == InheritPath.FALLBACK:
826
+ # Prepend location to sys.path.
827
+ #
828
+ # This ensures that bundled versions of libraries will be used before
829
+ # system-installed versions, in case something is installed in both, helping
830
+ # to favor hermeticity in the case of non-hermetic PEX files (i.e. those
831
+ # with inherit_path=True).
832
+ #
833
+ # If the path is not already in sys.path, site.addsitedir will append (not
834
+ # prepend) the path to sys.path. But if the path is already in sys.path,
835
+ # site.addsitedir will leave sys.path unmodified, but will do everything
836
+ # else it would do. This is not part of its advertised contract (which is
837
+ # very vague), but has been verified to be the case by inspecting its source
838
+ # for both cpython 2.7 and cpython 3.7.
839
+ sys.path.insert(0, entry)
840
+ else:
841
+ sys.path.append(entry)
842
+
843
+ with TRACER.timed("Adding sitedir", V=2):
844
+ site.addsitedir(entry)
824
845
  return resolved
pex/finders.py CHANGED
@@ -15,7 +15,7 @@ from pex.dist_metadata import (
15
15
  NamedEntryPoint,
16
16
  )
17
17
  from pex.executables import is_python_script
18
- from pex.pep_376 import InstalledWheel
18
+ from pex.installed_wheel import InstalledWheel
19
19
  from pex.pep_503 import ProjectName
20
20
  from pex.sysconfig import SCRIPT_DIR, script_name
21
21
  from pex.typing import TYPE_CHECKING, cast
pex/hashing.py CHANGED
@@ -1,7 +1,7 @@
1
1
  # Copyright 2022 Pex project contributors.
2
2
  # Licensed under the Apache License, Version 2.0 (see LICENSE).
3
3
 
4
- from __future__ import absolute_import
4
+ from __future__ import absolute_import, print_function
5
5
 
6
6
  import hashlib
7
7
  import os
@@ -241,10 +241,12 @@ def dir_hash(
241
241
  def iter_files():
242
242
  # type: () -> Iterator[Text]
243
243
  for root, dirs, files in os.walk(top, followlinks=True):
244
- dirs[:] = [d for d in dirs if dir_filter(os.path.join(root, d))]
244
+ dirs[:] = [
245
+ d for d in dirs if dir_filter(os.path.relpath(os.path.join(root, d), top))
246
+ ]
245
247
  for f in files:
246
248
  path = os.path.join(root, f)
247
- if file_filter(path):
249
+ if file_filter(os.path.relpath(path, top)):
248
250
  yield path
249
251
 
250
252
  file_paths = sorted(iter_files())
@@ -278,16 +280,26 @@ def zip_hash(
278
280
  else zf.namelist()
279
281
  )
280
282
 
281
- dirs = frozenset(name.rstrip("/") for name in namelist if name.endswith("/"))
282
- accept_dirs = frozenset(d for d in dirs if dir_filter(os.path.basename(d)))
283
- reject_dirs = dirs - accept_dirs
284
-
283
+ dirs = set()
284
+ for name in namelist:
285
+ if name.endswith("/"):
286
+ dirname = name.rstrip("/")
287
+ else:
288
+ dirname = os.path.dirname(name)
289
+ while dirname:
290
+ dirs.add(dirname)
291
+ dirname = os.path.dirname(dirname)
292
+
293
+ accept_dirs = frozenset(
294
+ d for d in dirs if dir_filter(os.path.relpath(d, relpath) if relpath else d)
295
+ )
296
+ reject_dirs = tuple("{dir}/".format(dir=path) for path in (dirs - accept_dirs))
285
297
  accept_files = sorted(
286
298
  name
287
299
  for name in namelist
288
300
  if not name.endswith("/")
289
- and not any(name.startswith(reject_dir) for reject_dir in reject_dirs)
290
- and file_filter(os.path.basename(name))
301
+ and not name.startswith(reject_dirs)
302
+ and file_filter(os.path.relpath(name, relpath) if relpath else name)
291
303
  )
292
304
 
293
305
  hashed_names = (
@@ -297,3 +309,53 @@ def zip_hash(
297
309
 
298
310
  for filename in accept_files:
299
311
  update_hash(zf.open(filename, "r"), digest)
312
+
313
+
314
+ if __name__ == "__main__":
315
+ import sys
316
+ import zipfile
317
+ from argparse import ArgumentParser
318
+
319
+ from pex.common import is_pyc_dir, is_pyc_file
320
+
321
+ parser = ArgumentParser()
322
+ parser.add_argument(
323
+ "--exclude-dir",
324
+ dest="exclude_dirs",
325
+ action="append",
326
+ default=[],
327
+ )
328
+ parser.add_argument("--zip-relpath")
329
+ parser.add_argument("paths", nargs="+")
330
+
331
+ options = parser.parse_args()
332
+ exclude_dirs = frozenset(options.exclude_dirs)
333
+
334
+ for path in options.paths:
335
+ digest = Sha256()
336
+ if zipfile.is_zipfile(path):
337
+ zip_hash(
338
+ path,
339
+ digest=digest,
340
+ relpath=options.zip_relpath,
341
+ dir_filter=(
342
+ lambda dir_path: (
343
+ not is_pyc_dir(dir_path) and os.path.basename(dir_path) not in exclude_dirs
344
+ )
345
+ ),
346
+ file_filter=lambda f: not is_pyc_file(f),
347
+ )
348
+ elif os.path.isdir(path):
349
+ dir_hash(
350
+ path,
351
+ digest=digest,
352
+ dir_filter=(
353
+ lambda dir_path: (
354
+ not is_pyc_dir(dir_path) and os.path.basename(dir_path) not in exclude_dirs
355
+ )
356
+ ),
357
+ file_filter=lambda f: not is_pyc_file(f),
358
+ )
359
+ else:
360
+ print("Can only hash zip files or directories. Skipping file", path, file=sys.stderr)
361
+ print(path, digest.hexdigest(), file=sys.stdout)
pex/installed_wheel.py ADDED
@@ -0,0 +1,141 @@
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 hashlib
7
+ import json
8
+ import os
9
+
10
+ from pex.typing import TYPE_CHECKING, cast
11
+ from pex.util import CacheHelper
12
+ from pex.wheel import WHEEL, Wheel, WheelMetadataLoadError
13
+
14
+ if TYPE_CHECKING:
15
+ from typing import Iterator, Optional, Text, Tuple
16
+
17
+ import attr # vendor:skip
18
+ else:
19
+ import pex.third_party.attr as attr
20
+
21
+
22
+ @attr.s(frozen=True)
23
+ class InstalledWheel(object):
24
+ class LoadError(Exception):
25
+ """Indicates an installed wheel was not loadable at a particular path."""
26
+
27
+ _LAYOUT_JSON_FILENAME = ".layout.json"
28
+
29
+ @classmethod
30
+ def layout_file(cls, prefix_dir):
31
+ # type: (str) -> str
32
+ return os.path.join(prefix_dir, cls._LAYOUT_JSON_FILENAME)
33
+
34
+ @classmethod
35
+ def save(
36
+ cls,
37
+ prefix_dir, # type: str
38
+ stash_dir, # type: str
39
+ record_relpath, # type: Text
40
+ root_is_purelib, # type: bool
41
+ sys_path_entries, # type: Tuple[str, ...]
42
+ ):
43
+ # type: (...) -> InstalledWheel
44
+
45
+ # We currently need the installed wheel chroot hash for PEX-INFO / boot purposes. It is
46
+ # expensive to calculate; so we do it here 1 time when saving the installed wheel.
47
+ fingerprint = CacheHelper.dir_hash(prefix_dir, hasher=hashlib.sha256)
48
+
49
+ layout = {
50
+ "stash_dir": stash_dir,
51
+ "record_relpath": record_relpath,
52
+ "fingerprint": fingerprint,
53
+ "root_is_purelib": root_is_purelib,
54
+ "sys_path_entries": sys_path_entries,
55
+ }
56
+ with open(cls.layout_file(prefix_dir), "w") as fp:
57
+ json.dump(layout, fp, sort_keys=True, separators=(",", ":"))
58
+ return cls(
59
+ prefix_dir=prefix_dir,
60
+ stash_dir=stash_dir,
61
+ record_relpath=record_relpath,
62
+ fingerprint=fingerprint,
63
+ root_is_purelib=root_is_purelib,
64
+ sys_path_entries=sys_path_entries,
65
+ )
66
+
67
+ @classmethod
68
+ def load(cls, prefix_dir):
69
+ # type: (str) -> InstalledWheel
70
+ layout_file = cls.layout_file(prefix_dir)
71
+ try:
72
+ with open(layout_file) as fp:
73
+ layout = json.load(fp)
74
+ except (IOError, OSError) as e:
75
+ raise cls.LoadError(
76
+ "Failed to load an installed wheel layout from {layout_file}: {err}".format(
77
+ layout_file=layout_file, err=e
78
+ )
79
+ )
80
+ if not isinstance(layout, dict):
81
+ raise cls.LoadError(
82
+ "The installed wheel layout file at {layout_file} must contain a single top-level "
83
+ "object, found: {value}.".format(layout_file=layout_file, value=layout)
84
+ )
85
+ stash_dir = layout.get("stash_dir")
86
+ record_relpath = layout.get("record_relpath")
87
+ if not stash_dir or not record_relpath:
88
+ raise cls.LoadError(
89
+ "The installed wheel layout file at {layout_file} must contain an object with both "
90
+ "`stash_dir` and `record_relpath` attributes, found: {value}".format(
91
+ layout_file=layout_file, value=layout
92
+ )
93
+ )
94
+
95
+ fingerprint = layout.get("fingerprint")
96
+
97
+ # N.B.: Caching root_is_purelib was not part of the original InstalledWheel layout data; so
98
+ # we materialize the property if needed to support older installed wheel chroots.
99
+ root_is_purelib = layout.get("root_is_purelib")
100
+ if root_is_purelib is None:
101
+ try:
102
+ wheel = WHEEL.load(prefix_dir)
103
+ except WheelMetadataLoadError as e:
104
+ raise cls.LoadError(
105
+ "Failed to determine if installed wheel at {location} is platform-specific: "
106
+ "{err}".format(location=prefix_dir, err=e)
107
+ )
108
+ root_is_purelib = wheel.root_is_purelib
109
+
110
+ # N.B.: Older versions of Pex installed wheel chroots did not have this field since the
111
+ # `sys.path` entry was always just the prefix_dir for those.
112
+ sys_path_entries = layout.get("sys_path_entries", [""])
113
+
114
+ return cls(
115
+ prefix_dir=prefix_dir,
116
+ stash_dir=cast(str, stash_dir),
117
+ record_relpath=cast(str, record_relpath),
118
+ fingerprint=cast("Optional[str]", fingerprint),
119
+ root_is_purelib=root_is_purelib,
120
+ sys_path_entries=tuple(sys_path_entries),
121
+ )
122
+
123
+ prefix_dir = attr.ib() # type: str
124
+ stash_dir = attr.ib() # type: str
125
+ record_relpath = attr.ib() # type: Text
126
+ fingerprint = attr.ib() # type: Optional[str]
127
+ root_is_purelib = attr.ib() # type: bool
128
+ sys_path_entries = attr.ib() # type: Tuple[str, ...]
129
+
130
+ def wheel_file_name(self):
131
+ # type: () -> str
132
+ return Wheel.load(self.prefix_dir).wheel_file_name
133
+
134
+ def stashed_path(self, *components):
135
+ # type: (*str) -> str
136
+ return os.path.join(self.prefix_dir, self.stash_dir, *components)
137
+
138
+ def iter_sys_path_entries(self):
139
+ # type: () -> Iterator[str]
140
+ for sys_path_entry in self.sys_path_entries:
141
+ yield os.path.normpath(os.path.join(self.prefix_dir, sys_path_entry))
pex/interpreter.py CHANGED
@@ -18,7 +18,9 @@ from textwrap import dedent
18
18
  from pex import third_party
19
19
  from pex.cache.dirs import InterpreterDir
20
20
  from pex.common import safe_mkdtemp, safe_rmtree
21
+ from pex.exceptions import production_assert
21
22
  from pex.executor import Executor
23
+ from pex.interpreter_implementation import InterpreterImplementation
22
24
  from pex.jobs import Job, Retain, SpawnedJob, execute_parallel
23
25
  from pex.orderedset import OrderedSet
24
26
  from pex.os import WINDOWS, is_exe
@@ -61,18 +63,6 @@ if TYPE_CHECKING:
61
63
  InterpreterOrError = Union["PythonInterpreter", InterpreterIdentificationError]
62
64
 
63
65
 
64
- def calculate_binary_name(
65
- platform_python_implementation, python_version=None # type: Optional[Tuple[int, ...]]
66
- ):
67
- # type: (...) -> str
68
- name = "python"
69
- if platform_python_implementation == "PyPy":
70
- name = "pypy"
71
- if not python_version:
72
- return name
73
- return "{name}{version}".format(name=name, version=".".join(map(str, python_version)))
74
-
75
-
76
66
  class SitePackagesDir(object):
77
67
  def __init__(self, path):
78
68
  # type: (str) -> None
@@ -404,11 +394,11 @@ class PythonIdentity(object):
404
394
  )
405
395
 
406
396
  @classmethod
407
- def _find_interpreter_name(cls, python_tag):
408
- # type: (str) -> str
409
- for abbr, interpreter in cls.ABBR_TO_INTERPRETER_NAME.items():
410
- if python_tag.startswith(abbr):
411
- return interpreter
397
+ def _find_implementation(cls, python_tag):
398
+ # type: (str) -> InterpreterImplementation.Value
399
+ for implementation in InterpreterImplementation.values():
400
+ if python_tag.startswith(implementation.abbr):
401
+ return implementation
412
402
  raise ValueError("Unknown interpreter: {}".format(python_tag))
413
403
 
414
404
  def __init__(
@@ -431,9 +421,12 @@ class PythonIdentity(object):
431
421
  configured_macosx_deployment_target, # type: Optional[str]
432
422
  ):
433
423
  # type: (...) -> None
434
- # N.B.: We keep this mapping to support historical values for `distribution` and
435
- # `requirement` properties.
436
- self._interpreter_name = self._find_interpreter_name(python_tag)
424
+
425
+ self._implementation = self._find_implementation(python_tag)
426
+ production_assert(
427
+ not pypy_version or self._implementation is InterpreterImplementation.PYPY
428
+ )
429
+ self._pypy_version = pypy_version
437
430
 
438
431
  self._binary = binary
439
432
  self._prefix = prefix
@@ -447,7 +440,6 @@ class PythonIdentity(object):
447
440
  self._abi_tag = abi_tag
448
441
  self._platform_tag = platform_tag
449
442
  self._version = version
450
- self._pypy_version = pypy_version
451
443
  self._supported_tags = CompatibilityTags(tags=supported_tags)
452
444
  self._env_markers = env_markers
453
445
  self._configured_macosx_deployment_target = configured_macosx_deployment_target
@@ -491,7 +483,7 @@ class PythonIdentity(object):
491
483
  env_markers=self._env_markers.as_dict(),
492
484
  configured_macosx_deployment_target=self._configured_macosx_deployment_target,
493
485
  )
494
- return json.dumps(values, sort_keys=True)
486
+ return json.dumps(values, sort_keys=True, separators=(",", ":"))
495
487
 
496
488
  @property
497
489
  def binary(self):
@@ -570,7 +562,7 @@ class PythonIdentity(object):
570
562
  @property
571
563
  def is_pypy(self):
572
564
  # type: () -> bool
573
- return bool(self._pypy_version)
565
+ return self._implementation is InterpreterImplementation.PYPY
574
566
 
575
567
  @property
576
568
  def version_str(self):
@@ -593,9 +585,9 @@ class PythonIdentity(object):
593
585
  return self._configured_macosx_deployment_target
594
586
 
595
587
  @property
596
- def interpreter(self):
597
- # type: () -> str
598
- return self._interpreter_name
588
+ def implementation(self):
589
+ # type: () -> InterpreterImplementation.Value
590
+ return self._implementation
599
591
 
600
592
  def iter_supported_platforms(self):
601
593
  # type: () -> Iterator[Platform]
@@ -614,9 +606,8 @@ class PythonIdentity(object):
614
606
 
615
607
  def binary_name(self, version_components=2):
616
608
  # type: (int) -> str
617
- return calculate_binary_name(
618
- platform_python_implementation=self._interpreter_name,
619
- python_version=self._version[:version_components] if version_components > 0 else None,
609
+ return self._implementation.calculate_binary_name(
610
+ version=self._version[:version_components] if version_components > 0 else None
620
611
  )
621
612
 
622
613
  def hashbang(self):
@@ -636,8 +627,8 @@ class PythonIdentity(object):
636
627
  # type: () -> str
637
628
  # N.B.: Kept as distinct from __repr__ to support legacy str(identity) used by Pants v1 when
638
629
  # forming cache locations.
639
- return "{interpreter_name}-{major}.{minor}.{patch}".format(
640
- interpreter_name=self._interpreter_name,
630
+ return "{implementation}-{major}.{minor}.{patch}".format(
631
+ implementation=self._implementation,
641
632
  major=self._version[0],
642
633
  minor=self._version[1],
643
634
  patch=self._version[2],
@@ -1438,13 +1429,13 @@ class PythonInterpreter(object):
1438
1429
  # python<major>.<minor> is present in any given <prefix>/bin/ directory; so the algorithm
1439
1430
  # gets a hit on 1st try for CPython binaries incurring ~no extra overhead.
1440
1431
 
1432
+ implementation = self._identity.implementation
1441
1433
  version = self._identity.version
1442
1434
  abi_tag = self._identity.abi_tag
1443
1435
 
1444
- prefix = "pypy" if self.is_pypy else "python"
1445
- suffixes = ("{}.{}".format(version[0], version[1]), str(version[0]), "")
1436
+ versions = version[:2], version[:1], None
1446
1437
  candidate_binaries = tuple(
1447
- script_name("{}{}".format(prefix, suffix)) for suffix in suffixes
1438
+ script_name(implementation.calculate_binary_name(version)) for version in versions
1448
1439
  )
1449
1440
 
1450
1441
  def iter_base_candidate_binary_paths(interpreter):
@@ -1534,13 +1525,19 @@ class PythonInterpreter(object):
1534
1525
  self._supported_platforms = frozenset(self._identity.iter_supported_platforms())
1535
1526
  return self._supported_platforms
1536
1527
 
1537
- def shebang(self, args=None):
1538
- # type: (Optional[Text]) -> Text
1528
+ def shebang(
1529
+ self,
1530
+ args=None, # type: Optional[Text]
1531
+ encoding_line="", # type: str
1532
+ ):
1533
+ # type: (...) -> Text
1539
1534
  """Return the contents of an appropriate shebang for this interpreter and args.
1540
1535
 
1541
1536
  The shebang will include the leading `#!` but will not include a trailing new line character.
1542
1537
  """
1543
- return create_shebang(self._binary, python_args=args)
1538
+ return create_shebang(
1539
+ adjust_to_final_path(self._binary), python_args=args, encoding_line=encoding_line
1540
+ )
1544
1541
 
1545
1542
  def create_isolated_cmd(
1546
1543
  self,
@@ -1646,6 +1643,7 @@ def create_shebang(
1646
1643
  python_exe, # type: Text
1647
1644
  python_args=None, # type: Optional[Text]
1648
1645
  max_shebang_length=MAX_SHEBANG_LENGTH, # type: int
1646
+ encoding_line="", # type: str
1649
1647
  ):
1650
1648
  # type: (...) -> Text
1651
1649
  """Return the contents of an appropriate shebang for the given Python interpreter and args.
@@ -1671,6 +1669,7 @@ def create_shebang(
1671
1669
  dedent(
1672
1670
  """\
1673
1671
  #!/bin/sh
1672
+ {encoding_line}
1674
1673
  # N.B.: This python script executes via a /bin/sh re-exec as a hack to work around a
1675
1674
  # potential maximum shebang length of {max_shebang_length} bytes on this system which
1676
1675
  # the python interpreter `exec`ed below would violate.
@@ -1678,6 +1677,10 @@ def create_shebang(
1678
1677
  '''
1679
1678
  """
1680
1679
  )
1681
- .format(max_shebang_length=max_shebang_length, python=python)
1680
+ .format(
1681
+ encoding_line=encoding_line.rstrip(),
1682
+ max_shebang_length=max_shebang_length,
1683
+ python=python,
1684
+ )
1682
1685
  .strip()
1683
1686
  )