pex 2.64.1__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 (69) hide show
  1. pex/bin/pex.py +2 -1
  2. pex/build_backend/configuration.py +5 -5
  3. pex/build_backend/wrap.py +2 -19
  4. pex/cli/commands/lock.py +4 -2
  5. pex/cli/commands/run.py +10 -11
  6. pex/cli/pex.py +11 -4
  7. pex/dist_metadata.py +29 -2
  8. pex/docs/html/_pagefind/fragment/en_4250138.pf_fragment +0 -0
  9. pex/docs/html/_pagefind/fragment/en_7125dad.pf_fragment +0 -0
  10. pex/docs/html/_pagefind/fragment/en_785d562.pf_fragment +0 -0
  11. pex/docs/html/_pagefind/fragment/en_8e94bb8.pf_fragment +0 -0
  12. pex/docs/html/_pagefind/fragment/{en_17782b6.pf_fragment → en_a0396bb.pf_fragment} +0 -0
  13. pex/docs/html/_pagefind/fragment/en_a8a3588.pf_fragment +0 -0
  14. pex/docs/html/_pagefind/fragment/en_c07d988.pf_fragment +0 -0
  15. pex/docs/html/_pagefind/fragment/en_d718411.pf_fragment +0 -0
  16. pex/docs/html/_pagefind/index/en_a2e3c5e.pf_index +0 -0
  17. pex/docs/html/_pagefind/pagefind-entry.json +1 -1
  18. pex/docs/html/_pagefind/pagefind.en_4ce1afa9e3.pf_meta +0 -0
  19. pex/docs/html/_static/documentation_options.js +1 -1
  20. pex/docs/html/api/vars.html +5 -5
  21. pex/docs/html/buildingpex.html +5 -5
  22. pex/docs/html/genindex.html +5 -5
  23. pex/docs/html/index.html +5 -5
  24. pex/docs/html/recipes.html +5 -5
  25. pex/docs/html/scie.html +5 -5
  26. pex/docs/html/search.html +5 -5
  27. pex/docs/html/whatispex.html +5 -5
  28. pex/hashing.py +71 -9
  29. pex/interpreter_constraints.py +1 -1
  30. pex/jobs.py +13 -6
  31. pex/pep_376.py +21 -6
  32. pex/pep_427.py +30 -8
  33. pex/pex_builder.py +1 -4
  34. pex/pip/local_project.py +6 -14
  35. pex/pip/tool.py +3 -3
  36. pex/pip/vcs.py +93 -44
  37. pex/pip/version.py +7 -0
  38. pex/resolve/configured_resolve.py +13 -5
  39. pex/resolve/lock_downloader.py +1 -0
  40. pex/resolve/locker.py +30 -14
  41. pex/resolve/lockfile/create.py +2 -7
  42. pex/resolve/pre_resolved_resolver.py +1 -7
  43. pex/resolve/project.py +233 -47
  44. pex/resolve/resolver_configuration.py +1 -1
  45. pex/resolve/resolver_options.py +14 -9
  46. pex/resolve/venv_resolver.py +221 -65
  47. pex/resolver.py +59 -55
  48. pex/scie/__init__.py +40 -1
  49. pex/scie/model.py +2 -0
  50. pex/scie/science.py +25 -3
  51. pex/sdist.py +219 -0
  52. pex/version.py +1 -1
  53. pex/wheel.py +16 -12
  54. {pex-2.64.1.dist-info → pex-2.69.0.dist-info}/METADATA +4 -4
  55. {pex-2.64.1.dist-info → pex-2.69.0.dist-info}/RECORD +60 -59
  56. {pex-2.64.1.dist-info → pex-2.69.0.dist-info}/entry_points.txt +1 -0
  57. pex/docs/html/_pagefind/fragment/en_1048255.pf_fragment +0 -0
  58. pex/docs/html/_pagefind/fragment/en_3f7efc3.pf_fragment +0 -0
  59. pex/docs/html/_pagefind/fragment/en_40667cd.pf_fragment +0 -0
  60. pex/docs/html/_pagefind/fragment/en_55ee2f4.pf_fragment +0 -0
  61. pex/docs/html/_pagefind/fragment/en_d6d92dd.pf_fragment +0 -0
  62. pex/docs/html/_pagefind/fragment/en_d834316.pf_fragment +0 -0
  63. pex/docs/html/_pagefind/fragment/en_ec2ce54.pf_fragment +0 -0
  64. pex/docs/html/_pagefind/index/en_17effb2.pf_index +0 -0
  65. pex/docs/html/_pagefind/pagefind.en_49ec86cf86.pf_meta +0 -0
  66. {pex-2.64.1.dist-info → pex-2.69.0.dist-info}/WHEEL +0 -0
  67. {pex-2.64.1.dist-info → pex-2.69.0.dist-info}/licenses/LICENSE +0 -0
  68. {pex-2.64.1.dist-info → pex-2.69.0.dist-info}/pylock/pylock.toml +0 -0
  69. {pex-2.64.1.dist-info → pex-2.69.0.dist-info}/top_level.txt +0 -0
@@ -8,7 +8,7 @@
8
8
  <link rel="prefetch" href="_static/pex-logo-dark.png" as="image">
9
9
 
10
10
  <link rel="shortcut icon" href="_static/pex-icon.png"><!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25 -->
11
- <title>What are .pex files? - Pex Docs (v2.64.1)</title>
11
+ <title>What are .pex files? - Pex Docs (v2.69.0)</title>
12
12
  <link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
13
13
  <link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
14
14
  <link rel="stylesheet" type="text/css" href="_static/styles/furo-extensions.css?v=8dab3a3b" />
@@ -163,7 +163,7 @@
163
163
  </label>
164
164
  </div>
165
165
  <div class="header-center">
166
- <a href="index.html"><div class="brand">Pex Docs (v2.64.1)</div></a>
166
+ <a href="index.html"><div class="brand">Pex Docs (v2.69.0)</div></a>
167
167
  </div>
168
168
  <div class="header-right">
169
169
  <div class="theme-toggle-container theme-toggle-header">
@@ -322,12 +322,12 @@ build executable .pex files. This is described more thoroughly in
322
322
  </div>
323
323
  <div class="right-details">
324
324
  <div class="icons">
325
- <a class="muted-link " href="https://pypi.org/project/pex/2.64.1/" aria-label="PyPI"><svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 448 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg">
325
+ <a class="muted-link " href="https://pypi.org/project/pex/2.69.0/" aria-label="PyPI"><svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 448 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg">
326
326
  <path d="M439.8 200.5c-7.7-30.9-22.3-54.2-53.4-54.2h-40.1v47.4c0 36.8-31.2 67.8-66.8 67.8H172.7c-29.2 0-53.4 25-53.4 54.3v101.8c0 29 25.2 46 53.4 54.3 33.8 9.9 66.3 11.7 106.8 0 26.9-7.8 53.4-23.5 53.4-54.3v-40.7H226.2v-13.6h160.2c31.1 0 42.6-21.7 53.4-54.2 11.2-33.5 10.7-65.7 0-108.6zM286.2 404c11.1 0 20.1 9.1 20.1 20.3 0 11.3-9 20.4-20.1 20.4-11 0-20.1-9.2-20.1-20.4.1-11.3 9.1-20.3 20.1-20.3zM167.8 248.1h106.8c29.7 0 53.4-24.5 53.4-54.3V91.9c0-29-24.4-50.7-53.4-55.6-35.8-5.9-74.7-5.6-106.8.1-45.2 8-53.4 24.7-53.4 55.6v40.7h106.9v13.6h-147c-31.1 0-58.3 18.7-66.8 54.2-9.8 40.7-10.2 66.1 0 108.6 7.6 31.6 25.7 54.2 56.8 54.2H101v-48.8c0-35.3 30.5-66.4 66.8-66.4zm-6.7-142.6c-11.1 0-20.1-9.1-20.1-20.3.1-11.3 9-20.4 20.1-20.4 11 0 20.1 9.2 20.1 20.4s-9 20.3-20.1 20.3z">
327
327
  </path>
328
328
  </svg>
329
329
  </a>
330
- <a class="muted-link " href="https://github.com/pex-tool/pex/releases/download/v2.64.1/pex" aria-label="Download"><svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 640 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg">
330
+ <a class="muted-link " href="https://github.com/pex-tool/pex/releases/download/v2.69.0/pex" aria-label="Download"><svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 640 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg">
331
331
  <path d="M537.6 226.6c4.1-10.7 6.4-22.4 6.4-34.6 0-53-43-96-96-96-19.7 0-38.1 6-53.3 16.2C367 64.2 315.3 32 256 32c-88.4 0-160 71.6-160 160 0 2.7.1 5.4.2 8.1C40.2 219.8 0 273.2 0 336c0 79.5 64.5 144 144 144h368c70.7 0 128-57.3 128-128 0-61.9-44-113.6-102.4-125.4zm-132.9 88.7L299.3 420.7c-6.2 6.2-16.4 6.2-22.6 0L171.3 315.3c-10.1-10.1-2.9-27.3 11.3-27.3H248V176c0-8.8 7.2-16 16-16h48c8.8 0 16 7.2 16 16v112h65.4c14.2 0 21.4 17.2 11.3 27.3z">
332
332
  </path>
333
333
  </svg>
@@ -371,7 +371,7 @@ build executable .pex files. This is described more thoroughly in
371
371
 
372
372
  </aside>
373
373
  </div>
374
- </div><script src="_static/documentation_options.js?v=a39c154c"></script>
374
+ </div><script src="_static/documentation_options.js?v=525daa63"></script>
375
375
  <script src="_static/doctools.js?v=9bcbadda"></script>
376
376
  <script src="_static/sphinx_highlight.js?v=dc90522c"></script>
377
377
  <script src="_static/scripts/furo.js?v=46bd48cc"></script>
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)
@@ -375,7 +375,7 @@ COMPATIBLE_PYTHON_VERSIONS = (
375
375
  PythonVersion(Lifecycle.EOL, 3, 6, 15),
376
376
  PythonVersion(Lifecycle.EOL, 3, 7, 17),
377
377
  PythonVersion(Lifecycle.EOL, 3, 8, 20),
378
- PythonVersion(Lifecycle.STABLE, 3, 9, 24),
378
+ PythonVersion(Lifecycle.EOL, 3, 9, 25),
379
379
  PythonVersion(Lifecycle.STABLE, 3, 10, 19),
380
380
  PythonVersion(Lifecycle.STABLE, 3, 11, 14),
381
381
  PythonVersion(Lifecycle.STABLE, 3, 12, 12),
pex/jobs.py CHANGED
@@ -724,6 +724,19 @@ def iter_map_parallel(
724
724
  if not input_items:
725
725
  return
726
726
 
727
+ # We want each of the job slots to process MULTIPROCESSING_DEFAULT_MIN_AVERAGE_LOAD on
728
+ # average in order to overcome multiprocessing overheads. If we don't need at least 2 slots, we
729
+ # are unlikely to get any boost from multiprocessing.
730
+ needed_slots = len(input_items) // min_average_load
731
+ if needed_slots < 2:
732
+ for item in input_items:
733
+ yield function(item)
734
+ return
735
+
736
+ # Of course, if there are fewer available cores than that or the user has pinned max jobs lower,
737
+ # we clamp to that.
738
+ pool_size = min(needed_slots, _sanitize_max_jobs(max_jobs))
739
+
727
740
  if costing_function is not None:
728
741
  # We ensure no job slot is so unlucky as to get all the biggest jobs and thus become an
729
742
  # un-necessarily long pole by sorting based on cost. Some examples to illustrate the effect
@@ -740,12 +753,6 @@ def iter_map_parallel(
740
753
  #
741
754
  input_items.sort(key=costing_function, reverse=True)
742
755
 
743
- # We want each of the job slots above to process MULTIPROCESSING_DEFAULT_MIN_AVERAGE_LOAD on
744
- # average in order to overcome multiprocessing overheads. Of course, if there are fewer
745
- # available cores than that or the user has pinned max jobs lower, we clamp to that. Finally, we
746
- # always want at least two slots to ensure we process input items in parallel.
747
- pool_size = max(2, min(len(input_items) // min_average_load, _sanitize_max_jobs(max_jobs)))
748
-
749
756
  apply_function = functools.partial(_apply_function, function)
750
757
 
751
758
  slots = defaultdict(list) # type: DefaultDict[int, List[float]]
pex/pep_376.py CHANGED
@@ -78,6 +78,14 @@ class InstalledFile(object):
78
78
  size = attr.ib(default=None) # type: Optional[int]
79
79
 
80
80
 
81
+ @attr.s(frozen=True)
82
+ class InstalledDirectory(object):
83
+ # N.B.: Although directory entries should not exist in a RECORD, they have been seen in the
84
+ # wild; so we're forced to deal with them. See: https://github.com/pex-tool/pex/issues/2998
85
+
86
+ dir_info = attr.ib() # type: InstalledFile
87
+
88
+
81
89
  def create_installed_file(
82
90
  path, # type: Text
83
91
  dest_dir, # type: str
@@ -121,18 +129,21 @@ class Record(object):
121
129
  def write_fp(
122
130
  cls,
123
131
  fp, # type: IO
124
- installed_files, # type: Iterable[InstalledFile]
132
+ installed_files, # type: Iterable[Union[InstalledFile, InstalledDirectory]]
125
133
  eol="\n", # type: str
126
134
  ):
127
135
  # type: (...) -> None
128
136
  csv_writer = csv.writer(fp, delimiter=",", quotechar='"', lineterminator=eol)
129
137
  for installed_file in installed_files:
130
- csv_writer.writerow(attr.astuple(installed_file, recurse=False))
138
+ if isinstance(installed_file, InstalledDirectory):
139
+ csv_writer.writerow(attr.astuple(installed_file.dir_info, recurse=False))
140
+ else:
141
+ csv_writer.writerow(attr.astuple(installed_file, recurse=False))
131
142
 
132
143
  @classmethod
133
144
  def write_bytes(
134
145
  cls,
135
- installed_files, # type: Iterable[InstalledFile]
146
+ installed_files, # type: Iterable[Union[InstalledFile, InstalledDirectory]]
136
147
  eol="\n", # type: str
137
148
  ):
138
149
  # type: (...) -> bytes
@@ -149,7 +160,7 @@ class Record(object):
149
160
  def write(
150
161
  cls,
151
162
  dst, # type: Text
152
- installed_files, # type: Iterable[InstalledFile]
163
+ installed_files, # type: Iterable[Union[InstalledFile, InstalledDirectory]]
153
164
  eol="\n", # type: str
154
165
  ):
155
166
  # type: (...) -> None
@@ -165,7 +176,7 @@ class Record(object):
165
176
  lines, # type: Union[FileInput[Text], Iterator[Text]]
166
177
  exclude=None, # type: Optional[Callable[[Text], bool]]
167
178
  ):
168
- # type: (...) -> Iterator[InstalledFile]
179
+ # type: (...) -> Iterator[Union[InstalledFile, InstalledDirectory]]
169
180
 
170
181
  # The RECORD is a csv file with the path to each installed file in the 1st column.
171
182
  # See: https://peps.python.org/pep-0376/#record
@@ -177,7 +188,11 @@ class Record(object):
177
188
  continue
178
189
  file_hash = Hash(fingerprint) if fingerprint else None
179
190
  size = int(file_size) if file_size else None
180
- yield InstalledFile(path=path, hash=file_hash, size=size)
191
+ installed_file = InstalledFile(path=path, hash=file_hash, size=size)
192
+ if path.endswith("/"):
193
+ yield InstalledDirectory(dir_info=installed_file)
194
+ else:
195
+ yield installed_file
181
196
 
182
197
  project_name = attr.ib() # type: str
183
198
  version = attr.ib() # type: str
pex/pep_427.py CHANGED
@@ -36,7 +36,7 @@ from pex.exceptions import production_assert, reportable_unexpected_error_msg
36
36
  from pex.executables import chmod_plus_x, is_python_script
37
37
  from pex.installed_wheel import InstalledWheel
38
38
  from pex.interpreter import PythonInterpreter
39
- from pex.pep_376 import InstalledFile, Record, create_installed_file
39
+ from pex.pep_376 import InstalledDirectory, InstalledFile, Record, create_installed_file
40
40
  from pex.pep_440 import Version
41
41
  from pex.pep_503 import ProjectName
42
42
  from pex.sysconfig import SCRIPT_DIR, SysPlatform
@@ -775,11 +775,24 @@ def create_whl(
775
775
  )
776
776
  else:
777
777
  for installed_file in Record.read(lines=iter(record_data.decode("utf-8").splitlines())):
778
- src = os.path.join(whl_chroot, installed_file.path)
778
+ path = (
779
+ installed_file.dir_info.path
780
+ if isinstance(installed_file, InstalledDirectory)
781
+ else installed_file.path
782
+ )
783
+ src = os.path.join(whl_chroot, path)
784
+ if not os.path.exists(src):
785
+ production_assert(
786
+ isinstance(installed_file, InstalledDirectory),
787
+ "The wheel entry {filename} is unexpectedly missing from {source}.",
788
+ filename=path,
789
+ source=wheel_to_create.source,
790
+ )
791
+ safe_mkdir(src)
779
792
  if use_system_time:
780
- zip_fp.write(src, installed_file.path)
793
+ zip_fp.write(src, path)
781
794
  else:
782
- zip_fp.write_deterministic(src, installed_file.path)
795
+ zip_fp.write_deterministic(src, path)
783
796
  return wheel_path
784
797
 
785
798
 
@@ -869,7 +882,11 @@ def install_wheel(
869
882
  eol = "\r\n" if record_lines[0].endswith("\r\n") else "\n"
870
883
 
871
884
  if not record_data or any(
872
- os.path.isabs(installed_file.path)
885
+ os.path.isabs(
886
+ installed_file.dir_info.path
887
+ if isinstance(installed_file, InstalledDirectory)
888
+ else installed_file.path
889
+ )
873
890
  for installed_file in Record.read(lines=iter(record_lines))
874
891
  ):
875
892
  prefix = "The RECORD in {whl}".format(whl=os.path.basename(whl))
@@ -935,10 +952,15 @@ def install_wheel(
935
952
  requested_relpath = wheel.metadata_path("REQUESTED")
936
953
  zip_metadata_relpath = wheel.pex_metadata_path(ZipMetadata.FILENAME)
937
954
 
938
- installed_files = [] # type: List[InstalledFile]
955
+ installed_files = [] # type: List[Union[InstalledFile, InstalledDirectory]]
939
956
  provenance = [] # type: List[Tuple[Text, Text]]
940
957
  symlinked = set() # type: Set[Text]
941
- for installed_file in Record.read(lines=iter(record_data.decode("utf-8").splitlines())):
958
+ for installed_file_or_dir in Record.read(lines=iter(record_data.decode("utf-8").splitlines())):
959
+ if isinstance(installed_file_or_dir, InstalledDirectory):
960
+ installed_files.append(installed_file_or_dir)
961
+ continue
962
+
963
+ installed_file = installed_file_or_dir
942
964
  if installed_file.path == record_relpath:
943
965
  record_eol = _detect_record_eol(os.path.join(wheel.location, installed_file.path))
944
966
  installed_files.append(InstalledFile(path=record_relpath, hash=None, size=None))
@@ -1051,7 +1073,7 @@ def install_wheel(
1051
1073
  py_files = [
1052
1074
  os.path.join(dest, installed_file.path)
1053
1075
  for installed_file in installed_files
1054
- if installed_file.path.endswith(".py")
1076
+ if isinstance(installed_file, InstalledFile) and installed_file.path.endswith(".py")
1055
1077
  ]
1056
1078
  process = subprocess.Popen(
1057
1079
  args=args + py_files, stdout=subprocess.PIPE, stderr=subprocess.PIPE
pex/pex_builder.py CHANGED
@@ -470,10 +470,7 @@ class PEXBuilder(object):
470
470
  def _prepare_code(self):
471
471
  chroot_path = self._chroot.path()
472
472
  self._pex_info.code_hash = CacheHelper.pex_code_hash(
473
- chroot_path,
474
- exclude_dirs=tuple(
475
- os.path.join(chroot_path, d) for d in (layout.BOOTSTRAP_DIR, layout.DEPS_DIR)
476
- ),
473
+ chroot_path, exclude_dirs=(layout.BOOTSTRAP_DIR, layout.DEPS_DIR)
477
474
  )
478
475
  self._pex_info.pex_hash = hashlib.sha1(self._pex_info.dump().encode("utf-8")).hexdigest()
479
476
  self._chroot.write(self._pex_info.dump().encode("utf-8"), PexInfo.PATH, label="manifest")
pex/pip/local_project.py CHANGED
@@ -4,9 +4,8 @@
4
4
  from __future__ import absolute_import
5
5
 
6
6
  import os.path
7
- import tarfile
8
7
 
9
- from pex import hashing
8
+ from pex import hashing, sdist
10
9
  from pex.build_system import pep_517
11
10
  from pex.common import temporary_dir
12
11
  from pex.pip.version import PipVersionValue
@@ -33,25 +32,18 @@ def digest_local_project(
33
32
  # type: (...) -> Union[str, Error]
34
33
  with TRACER.timed("Fingerprinting local project at {directory}".format(directory=directory)):
35
34
  with temporary_dir() as td:
36
- sdist_or_error = pep_517.build_sdist(
35
+ sdist_path_or_error = pep_517.build_sdist(
37
36
  project_directory=directory,
38
37
  dist_dir=os.path.join(td, "dists"),
39
38
  pip_version=pip_version,
40
39
  target=target,
41
40
  resolver=resolver,
42
41
  )
43
- if isinstance(sdist_or_error, Error):
44
- return sdist_or_error
45
- sdist = sdist_or_error
42
+ if isinstance(sdist_path_or_error, Error):
43
+ return sdist_path_or_error
44
+ sdist_path = sdist_path_or_error
46
45
 
47
46
  extract_dir = dest_dir or os.path.join(td, "extracted")
48
- with tarfile.open(sdist) as tf:
49
- tf.extractall(extract_dir)
50
- listing = os.listdir(extract_dir)
51
- assert len(listing) == 1, (
52
- "Expected sdist generated for {directory} to contain one top-level directory, "
53
- "found:\n{listing}".format(directory=directory, listing="\n".join(listing))
54
- )
55
- project_dir = os.path.join(extract_dir, listing[0])
47
+ project_dir = sdist.extract_tarball(sdist_path, dest_dir=extract_dir)
56
48
  hashing.dir_hash(directory=project_dir, digest=digest)
57
49
  return os.path.join(extract_dir, project_dir)
pex/pip/tool.py CHANGED
@@ -552,8 +552,7 @@ class Pip(object):
552
552
  )
553
553
  return Job(command=command, process=process, finalizer=finalizer, context="pip")
554
554
 
555
- @staticmethod
556
- def _iter_build_configuration_options(build_configuration):
555
+ def _iter_build_configuration_options(self, build_configuration):
557
556
  # type: (BuildConfiguration) -> Iterator[str]
558
557
 
559
558
  # N.B.: BuildConfiguration maintains invariants that ensure --only-binary, --no-binary,
@@ -576,7 +575,8 @@ class Pip(object):
576
575
  if build_configuration.prefer_older_binary:
577
576
  yield "--prefer-binary"
578
577
 
579
- if build_configuration.use_pep517 is not None:
578
+ # N.B.: In 25.3 `--use-pep517` became the default and only option.
579
+ if build_configuration.use_pep517 is not None and self.version < PipVersion.v25_3:
580
580
  yield "--use-pep517" if build_configuration.use_pep517 else "--no-use-pep517"
581
581
 
582
582
  if not build_configuration.build_isolation:
pex/pip/vcs.py CHANGED
@@ -3,26 +3,42 @@
3
3
 
4
4
  from __future__ import absolute_import
5
5
 
6
- import glob
7
6
  import os
8
7
  import re
9
8
 
10
9
  from pex import hashing
11
10
  from pex.artifact_url import VCS, Fingerprint
12
- from pex.common import is_pyc_dir, is_pyc_file, open_zip, temporary_dir
11
+ from pex.common import is_pyc_dir, is_pyc_file
12
+ from pex.exceptions import reportable_unexpected_error_msg
13
13
  from pex.hashing import Sha256
14
14
  from pex.pep_440 import Version
15
15
  from pex.pep_503 import ProjectName
16
16
  from pex.result import Error, try_
17
- from pex.tracer import TRACER
18
17
  from pex.typing import TYPE_CHECKING
19
18
 
20
19
  if TYPE_CHECKING:
21
- from typing import Optional, Tuple, Union
20
+ # N.B.: The `re.Pattern` type is not available in all Python versions Pex supports.
21
+ from re import Pattern # type: ignore[attr-defined]
22
+ from typing import Callable, Optional, Text, Tuple, Union
22
23
 
23
24
  from pex.hashing import HintedDigest
24
25
 
25
26
 
27
+ def _project_name_re(project_name):
28
+ # type: (ProjectName) -> str
29
+ return project_name.normalized.replace("-", "[-_.]+")
30
+
31
+
32
+ def _built_source_dist_pattern(project_name):
33
+ # type: (ProjectName) -> Pattern
34
+ return re.compile(
35
+ r"(?P<project_name>{project_name_re})-(?P<version>.+)\.zip".format(
36
+ project_name_re=_project_name_re(project_name)
37
+ ),
38
+ re.IGNORECASE,
39
+ )
40
+
41
+
26
42
  def _find_built_source_dist(
27
43
  build_dir, # type: str
28
44
  project_name, # type: ProjectName
@@ -34,12 +50,7 @@ def _find_built_source_dist(
34
50
  # encoded in: `pip._internal.req.req_install.InstallRequirement.archive`.
35
51
 
36
52
  listing = os.listdir(build_dir)
37
- pattern = re.compile(
38
- r"{project_name}-(?P<version>.+)\.zip".format(
39
- project_name=project_name.normalized.replace("-", "[-_.]+")
40
- ),
41
- re.IGNORECASE,
42
- )
53
+ pattern = _built_source_dist_pattern(project_name)
43
54
  for name in listing:
44
55
  match = pattern.match(name)
45
56
  if match and Version(match.group("version")) == version:
@@ -58,23 +69,66 @@ def _find_built_source_dist(
58
69
 
59
70
  def fingerprint_downloaded_vcs_archive(
60
71
  download_dir, # type: str
61
- project_name, # type: str
62
- version, # type: str
72
+ project_name, # type: ProjectName
73
+ version, # type: Version
63
74
  vcs, # type: VCS.Value
64
75
  ):
65
76
  # type: (...) -> Tuple[Fingerprint, str]
66
77
 
67
78
  archive_path = try_(
68
- _find_built_source_dist(
69
- build_dir=download_dir, project_name=ProjectName(project_name), version=Version(version)
70
- )
79
+ _find_built_source_dist(build_dir=download_dir, project_name=project_name, version=version)
71
80
  )
72
81
  digest = Sha256()
73
- digest_vcs_archive(archive_path=archive_path, vcs=vcs, digest=digest)
82
+ digest_vcs_archive(project_name=project_name, archive_path=archive_path, vcs=vcs, digest=digest)
74
83
  return Fingerprint.from_digest(digest), archive_path
75
84
 
76
85
 
86
+ def _vcs_dir_filter(
87
+ vcs, # type: VCS.Value
88
+ project_name, # type: ProjectName
89
+ ):
90
+ # type: (...) -> Callable[[Text], bool]
91
+
92
+ # Ignore VCS control directories for the purposes of fingerprinting the version controlled
93
+ # source tree. VCS control directories can contain non-reproducible content (Git at least
94
+ # has files that contain timestamps).
95
+ #
96
+ # We cannot prune these directories from the source archive directly unfortunately since
97
+ # some build processes use VCS version information to derive their version numbers (C.F.:
98
+ # https://pypi.org/project/setuptools-scm/). As such, we'll get a stable fingerprint, but be
99
+ # forced to re-build a wheel each time the VCS requirement is re-locked later, even when it
100
+ # hashes the same.
101
+ vcs_control_dir = ".{vcs}".format(vcs=vcs)
102
+
103
+ # N.B.: If the VCS project uses setuptools as its build backend, depending on the version of
104
+ # Pip used, the VCS checkout can have a `<project name>.egg-info/` directory littering its root
105
+ # left over from Pip generating project metadata to determine version and dependencies. No other
106
+ # well known build-backend has this problem at this time (checked hatchling, poetry-core,
107
+ # pdm-backend and uv_build).
108
+ # C.F.: https://github.com/pypa/pip/pull/13602
109
+ egg_info_dir_re = re.compile(
110
+ r"^{project_name_re}\.egg-info$".format(project_name_re=_project_name_re(project_name)),
111
+ re.IGNORECASE,
112
+ )
113
+
114
+ def vcs_dir_filter(dir_path):
115
+ # type: (Text) -> bool
116
+ if is_pyc_dir(dir_path):
117
+ return False
118
+
119
+ base_dir_name = dir_path.split(os.sep)[0]
120
+ return base_dir_name != vcs_control_dir and not egg_info_dir_re.match(base_dir_name)
121
+
122
+ return vcs_dir_filter
123
+
124
+
125
+ def _vcs_file_filter(vcs):
126
+ # type: (VCS.Value) -> Callable[[Text], bool]
127
+ return lambda f: not is_pyc_file(f)
128
+
129
+
77
130
  def digest_vcs_archive(
131
+ project_name, # type: ProjectName
78
132
  archive_path, # type: str
79
133
  vcs, # type: VCS.Value
80
134
  digest, # type: HintedDigest
@@ -84,22 +138,32 @@ def digest_vcs_archive(
84
138
  # All VCS requirements are prepared as zip archives as encoded in:
85
139
  # `pip._internal.req.req_install.InstallRequirement.archive` and the archive is already offset
86
140
  # by a subdirectory (if any).
87
- with TRACER.timed(
88
- "Digesting {archive} {vcs} archive".format(archive=os.path.basename(archive_path), vcs=vcs)
89
- ), temporary_dir() as chroot, open_zip(archive_path) as archive:
90
- # TODO(John Sirois): Consider implementing zip_hash to avoid the extractall.
91
- archive.extractall(chroot)
92
141
 
93
- # The zip archives created by Pip have a single project name top-level directory housing
94
- # the full clone. We look for that to get a consistent clone hash with a bare clone.
95
- listing = glob.glob(os.path.join(chroot, "*"))
96
- if len(listing) == 1 and os.path.isdir(listing[0]):
97
- chroot = listing[0]
142
+ # The zip archives created by Pip have a single project name top-level directory housing
143
+ # the full clone. We look for that to get a consistent clone hash with a bare clone.
144
+ match = _built_source_dist_pattern(project_name).match(os.path.basename(archive_path))
145
+ if match is None:
146
+ raise AssertionError(
147
+ reportable_unexpected_error_msg(
148
+ "Failed to determine the project name prefix for the VCS zip {zip} with expected "
149
+ "canonical project name {project_name}".format(
150
+ zip=archive_path, project_name=project_name
151
+ )
152
+ )
153
+ )
154
+ top_dir = match.group("project_name")
98
155
 
99
- digest_vcs_repo(repo_path=chroot, vcs=vcs, digest=digest)
156
+ hashing.zip_hash(
157
+ zip_path=archive_path,
158
+ digest=digest,
159
+ relpath=top_dir,
160
+ dir_filter=_vcs_dir_filter(vcs, project_name),
161
+ file_filter=_vcs_file_filter(vcs),
162
+ )
100
163
 
101
164
 
102
165
  def digest_vcs_repo(
166
+ project_name, # type: ProjectName
103
167
  repo_path, # type: str
104
168
  vcs, # type: VCS.Value
105
169
  digest, # type: HintedDigest
@@ -107,24 +171,9 @@ def digest_vcs_repo(
107
171
  ):
108
172
  # type: (...) -> None
109
173
 
110
- # Ignore VCS control directories for the purposes of fingerprinting the version controlled
111
- # source tree. VCS control directories can contain non-reproducible content (Git at least
112
- # has files that contain timestamps).
113
- #
114
- # We cannot prune these directories from the source archive directly unfortunately since
115
- # some build processes use VCS version information to derive their version numbers (C.F.:
116
- # https://pypi.org/project/setuptools-scm/). As such, we'll get a stable fingerprint, but be
117
- # forced to re-build a wheel each time the VCS requirement is re-locked later, even when it
118
- # hashes the same.
119
- vcs_control_dir = ".{vcs}".format(vcs=vcs)
120
-
121
174
  hashing.dir_hash(
122
175
  directory=os.path.join(repo_path, subdirectory) if subdirectory else repo_path,
123
176
  digest=digest,
124
- dir_filter=(
125
- lambda dir_path: (
126
- not is_pyc_dir(dir_path) and os.path.basename(dir_path) != vcs_control_dir
127
- )
128
- ),
129
- file_filter=lambda f: not is_pyc_file(f),
177
+ dir_filter=_vcs_dir_filter(vcs, project_name),
178
+ file_filter=_vcs_file_filter(vcs),
130
179
  )
pex/pip/version.py CHANGED
@@ -369,6 +369,13 @@ class PipVersion(Enum["PipVersionValue"]):
369
369
  requires_python=">=3.9,<3.16",
370
370
  )
371
371
 
372
+ v25_3 = PipVersionValue(
373
+ version="25.3",
374
+ setuptools_version="80.9.0",
375
+ wheel_version="0.45.1",
376
+ requires_python=">=3.9,<3.16",
377
+ )
378
+
372
379
  VENDORED = v20_3_4_patched
373
380
  LATEST = LatestPipVersion()
374
381
  LATEST_COMPATIBLE = LatestCompatiblePipVersion()