pex 2.59.4__py2.py3-none-any.whl → 2.60.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 (114) hide show
  1. pex/build_backend/wrap.py +25 -4
  2. pex/cache/dirs.py +14 -4
  3. pex/cli/commands/lock.py +8 -5
  4. pex/common.py +57 -7
  5. pex/compatibility.py +1 -1
  6. pex/dist_metadata.py +48 -6
  7. pex/docs/html/_pagefind/fragment/en_39c0488.pf_fragment +0 -0
  8. pex/docs/html/_pagefind/fragment/en_3eeaaf4.pf_fragment +0 -0
  9. pex/docs/html/_pagefind/fragment/en_a1dde36.pf_fragment +0 -0
  10. pex/docs/html/_pagefind/fragment/en_a755644.pf_fragment +0 -0
  11. pex/docs/html/_pagefind/fragment/en_b16e3bd.pf_fragment +0 -0
  12. pex/docs/html/_pagefind/fragment/{en_e323b0a.pf_fragment → en_c5d35a7.pf_fragment} +0 -0
  13. pex/docs/html/_pagefind/fragment/en_ec62bd2.pf_fragment +0 -0
  14. pex/docs/html/_pagefind/fragment/en_f32628f.pf_fragment +0 -0
  15. pex/docs/html/_pagefind/index/{en_9894162.pf_index → en_b211695.pf_index} +0 -0
  16. pex/docs/html/_pagefind/pagefind-entry.json +1 -1
  17. pex/docs/html/_pagefind/pagefind.en_e8a49380e5.pf_meta +0 -0
  18. pex/docs/html/_static/documentation_options.js +1 -1
  19. pex/docs/html/api/vars.html +5 -5
  20. pex/docs/html/buildingpex.html +5 -5
  21. pex/docs/html/genindex.html +5 -5
  22. pex/docs/html/index.html +5 -5
  23. pex/docs/html/recipes.html +5 -5
  24. pex/docs/html/scie.html +5 -5
  25. pex/docs/html/search.html +5 -5
  26. pex/docs/html/whatispex.html +5 -5
  27. pex/entry_points_txt.py +98 -0
  28. pex/environment.py +13 -10
  29. pex/finders.py +1 -1
  30. pex/installed_wheel.py +127 -0
  31. pex/interpreter.py +17 -5
  32. pex/interpreter_constraints.py +4 -4
  33. pex/pep_376.py +37 -385
  34. pex/pep_427.py +736 -248
  35. pex/pex_builder.py +4 -4
  36. pex/pex_info.py +8 -3
  37. pex/resolve/venv_resolver.py +98 -23
  38. pex/resolver.py +10 -3
  39. pex/sysconfig.py +5 -3
  40. pex/third_party/__init__.py +1 -1
  41. pex/tools/commands/repository.py +47 -24
  42. pex/vendor/__init__.py +4 -9
  43. pex/vendor/__main__.py +62 -41
  44. pex/vendor/_vendored/ansicolors/.layout.json +1 -1
  45. pex/vendor/_vendored/ansicolors/ansicolors-1.1.8.dist-info/RECORD +11 -0
  46. pex/vendor/_vendored/ansicolors/ansicolors-1.1.8.pex-info/original-whl-info.json +1 -0
  47. pex/vendor/_vendored/appdirs/.layout.json +1 -1
  48. pex/vendor/_vendored/appdirs/appdirs-1.4.4.dist-info/RECORD +7 -0
  49. pex/vendor/_vendored/appdirs/appdirs-1.4.4.pex-info/original-whl-info.json +1 -0
  50. pex/vendor/_vendored/attrs/.layout.json +1 -1
  51. pex/vendor/_vendored/attrs/attrs-21.5.0.dev0.dist-info/RECORD +37 -0
  52. pex/vendor/_vendored/attrs/attrs-21.5.0.dev0.pex-info/original-whl-info.json +1 -0
  53. pex/vendor/_vendored/packaging_20_9/.layout.json +1 -1
  54. pex/vendor/_vendored/packaging_20_9/packaging-20.9.dist-info/RECORD +20 -0
  55. pex/vendor/_vendored/packaging_20_9/packaging-20.9.pex-info/original-whl-info.json +1 -0
  56. pex/vendor/_vendored/packaging_20_9/pyparsing-2.4.7.dist-info/RECORD +7 -0
  57. pex/vendor/_vendored/packaging_20_9/pyparsing-2.4.7.pex-info/original-whl-info.json +1 -0
  58. pex/vendor/_vendored/packaging_21_3/.layout.json +1 -1
  59. pex/vendor/_vendored/packaging_21_3/packaging-21.3.dist-info/RECORD +20 -0
  60. pex/vendor/_vendored/packaging_21_3/packaging-21.3.pex-info/original-whl-info.json +1 -0
  61. pex/vendor/_vendored/packaging_21_3/pyparsing-3.0.7.dist-info/RECORD +18 -0
  62. pex/vendor/_vendored/packaging_21_3/pyparsing-3.0.7.pex-info/original-whl-info.json +1 -0
  63. pex/vendor/_vendored/packaging_24_0/.layout.json +1 -1
  64. pex/vendor/_vendored/packaging_24_0/packaging-24.0.dist-info/RECORD +22 -0
  65. pex/vendor/_vendored/packaging_24_0/packaging-24.0.pex-info/original-whl-info.json +1 -0
  66. pex/vendor/_vendored/packaging_25_0/.layout.json +1 -1
  67. pex/vendor/_vendored/packaging_25_0/packaging-25.0.dist-info/RECORD +24 -0
  68. pex/vendor/_vendored/packaging_25_0/packaging-25.0.pex-info/original-whl-info.json +1 -0
  69. pex/vendor/_vendored/pip/.layout.json +1 -1
  70. pex/vendor/_vendored/pip/pip/_vendor/certifi/cacert.pem +63 -1
  71. pex/vendor/_vendored/pip/pip-20.3.4.dist-info/RECORD +388 -0
  72. pex/vendor/_vendored/pip/pip-20.3.4.pex-info/original-whl-info.json +1 -0
  73. pex/vendor/_vendored/setuptools/.layout.json +1 -1
  74. pex/vendor/_vendored/setuptools/setuptools-44.0.0+3acb925dd708430aeaf197ea53ac8a752f7c1863.dist-info/RECORD +107 -0
  75. pex/vendor/_vendored/setuptools/setuptools-44.0.0+3acb925dd708430aeaf197ea53ac8a752f7c1863.pex-info/original-whl-info.json +1 -0
  76. pex/vendor/_vendored/toml/.layout.json +1 -1
  77. pex/vendor/_vendored/toml/toml-0.10.2.dist-info/RECORD +11 -0
  78. pex/vendor/_vendored/toml/toml-0.10.2.pex-info/original-whl-info.json +1 -0
  79. pex/vendor/_vendored/tomli/.layout.json +1 -1
  80. pex/vendor/_vendored/tomli/tomli-2.0.1.dist-info/RECORD +10 -0
  81. pex/vendor/_vendored/tomli/tomli-2.0.1.pex-info/original-whl-info.json +1 -0
  82. pex/venv/installer.py +9 -5
  83. pex/version.py +1 -1
  84. pex/wheel.py +79 -15
  85. pex/whl.py +67 -0
  86. pex/windows/__init__.py +14 -11
  87. {pex-2.59.4.dist-info → pex-2.60.0.dist-info}/METADATA +4 -4
  88. {pex-2.59.4.dist-info → pex-2.60.0.dist-info}/RECORD +93 -77
  89. pex/docs/html/_pagefind/fragment/en_144b803.pf_fragment +0 -0
  90. pex/docs/html/_pagefind/fragment/en_1df1379.pf_fragment +0 -0
  91. pex/docs/html/_pagefind/fragment/en_2c6c6cb.pf_fragment +0 -0
  92. pex/docs/html/_pagefind/fragment/en_a916b1c.pf_fragment +0 -0
  93. pex/docs/html/_pagefind/fragment/en_b33e5d4.pf_fragment +0 -0
  94. pex/docs/html/_pagefind/fragment/en_c1c571a.pf_fragment +0 -0
  95. pex/docs/html/_pagefind/fragment/en_fda06e7.pf_fragment +0 -0
  96. pex/docs/html/_pagefind/pagefind.en_84c8322e7a.pf_meta +0 -0
  97. pex/vendor/_vendored/ansicolors/ansicolors-1.1.8.dist-info/INSTALLER +0 -1
  98. pex/vendor/_vendored/appdirs/appdirs-1.4.4.dist-info/INSTALLER +0 -1
  99. pex/vendor/_vendored/attrs/attrs-21.5.0.dev0.dist-info/INSTALLER +0 -1
  100. pex/vendor/_vendored/packaging_20_9/packaging-20.9.dist-info/INSTALLER +0 -1
  101. pex/vendor/_vendored/packaging_20_9/pyparsing-2.4.7.dist-info/INSTALLER +0 -1
  102. pex/vendor/_vendored/packaging_21_3/packaging-21.3.dist-info/INSTALLER +0 -1
  103. pex/vendor/_vendored/packaging_21_3/pyparsing-3.0.7.dist-info/INSTALLER +0 -1
  104. pex/vendor/_vendored/packaging_24_0/packaging-24.0.dist-info/INSTALLER +0 -1
  105. pex/vendor/_vendored/packaging_25_0/packaging-25.0.dist-info/INSTALLER +0 -1
  106. pex/vendor/_vendored/pip/pip-20.3.4.dist-info/INSTALLER +0 -1
  107. pex/vendor/_vendored/setuptools/setuptools-44.0.0+3acb925dd708430aeaf197ea53ac8a752f7c1863.dist-info/INSTALLER +0 -1
  108. pex/vendor/_vendored/toml/toml-0.10.2.dist-info/INSTALLER +0 -1
  109. pex/vendor/_vendored/tomli/tomli-2.0.1.dist-info/INSTALLER +0 -1
  110. {pex-2.59.4.dist-info → pex-2.60.0.dist-info}/WHEEL +0 -0
  111. {pex-2.59.4.dist-info → pex-2.60.0.dist-info}/entry_points.txt +0 -0
  112. {pex-2.59.4.dist-info → pex-2.60.0.dist-info}/licenses/LICENSE +0 -0
  113. {pex-2.59.4.dist-info → pex-2.60.0.dist-info}/pylock/pylock.toml +0 -0
  114. {pex-2.59.4.dist-info → pex-2.60.0.dist-info}/top_level.txt +0 -0
pex/pep_376.py CHANGED
@@ -5,32 +5,17 @@ from __future__ import absolute_import
5
5
 
6
6
  import base64
7
7
  import csv
8
- import errno
9
8
  import hashlib
10
- import itertools
11
- import json
12
9
  import os
13
- import shutil
14
10
  from fileinput import FileInput
15
11
 
16
12
  from pex import hashing
17
- from pex.common import (
18
- CopyMode,
19
- is_pyc_dir,
20
- is_pyc_file,
21
- safe_mkdir,
22
- safe_open,
23
- safe_relative_symlink,
24
- )
25
- from pex.fs import safe_link
26
- from pex.interpreter import PythonInterpreter
27
- from pex.typing import TYPE_CHECKING, cast
28
- from pex.util import CacheHelper
29
- from pex.venv.virtualenv import Virtualenv
30
- from pex.wheel import WHEEL, Wheel, WheelMetadataLoadError
13
+ from pex.common import safe_open
14
+ from pex.compatibility import PY2
15
+ from pex.typing import TYPE_CHECKING
31
16
 
32
17
  if TYPE_CHECKING:
33
- from typing import Callable, Iterable, Iterator, Optional, Protocol, Text, Tuple, Union
18
+ from typing import IO, Callable, Iterable, Iterator, Optional, Protocol, Text, Union
34
19
 
35
20
  import attr # vendor:skip
36
21
 
@@ -66,7 +51,12 @@ class Hash(object):
66
51
  # + https://peps.python.org/pep-0376/#record
67
52
  # + https://peps.python.org/pep-0427/#appendix
68
53
  fingerprint = base64.urlsafe_b64encode(hasher.digest()).rstrip(b"=")
69
- return cls(value="{alg}={hash}".format(alg=hasher.name, hash=fingerprint.decode("ascii")))
54
+
55
+ # N.B.: The algorithm is all caps under Python 2.7, but lower case under Python 3; so we
56
+ # normalize.
57
+ alg = hasher.name.lower()
58
+
59
+ return cls(value="{alg}={hash}".format(alg=alg, hash=fingerprint.decode("ascii")))
70
60
 
71
61
  value = attr.ib() # type: str
72
62
 
@@ -75,39 +65,6 @@ class Hash(object):
75
65
  return self.value
76
66
 
77
67
 
78
- def find_and_replace_path_components(
79
- path, # type: Text
80
- find, # type: str
81
- replace, # type: str
82
- ):
83
- # type: (...) -> Text
84
- """Replace components of `path` that are exactly `find` with `replace`.
85
-
86
- >>> find_and_replace_path_components("foo/bar/baz", "bar", "spam")
87
- foo/spam/baz
88
- >>>
89
- """
90
- if not find or not replace:
91
- raise ValueError(
92
- "Both find and replace must be non-empty strings. Given find={find!r} "
93
- "replace={replace!r}".format(find=find, replace=replace)
94
- )
95
- if not path:
96
- return path
97
-
98
- components = []
99
- head = path
100
- while head:
101
- new_head, tail = os.path.split(head)
102
- if new_head == head:
103
- components.append(head)
104
- break
105
- components.append(tail)
106
- head = new_head
107
- components.reverse()
108
- return os.path.join(*(replace if component == find else component for component in components))
109
-
110
-
111
68
  @attr.s(frozen=True)
112
69
  class InstalledFile(object):
113
70
  """The record of a single installed file from a PEP 376 RECORD file.
@@ -115,336 +72,23 @@ class InstalledFile(object):
115
72
  See: https://peps.python.org/pep-0376/#record
116
73
  """
117
74
 
118
- _PYTHON_VER_PLACEHOLDER = "pythonX.Y"
119
-
120
- @staticmethod
121
- def _python_ver(interpreter=None):
122
- # type: (Optional[PythonInterpreter]) -> str
123
- python = interpreter or PythonInterpreter.get()
124
- return "python{major}.{minor}".format(major=python.version[0], minor=python.version[1])
125
-
126
- @classmethod
127
- def normalized_path(
128
- cls,
129
- path, # type: Text
130
- interpreter=None, # type: Optional[PythonInterpreter]
131
- ):
132
- # type: (...) -> Text
133
- return find_and_replace_path_components(
134
- path, cls._python_ver(interpreter=interpreter), cls._PYTHON_VER_PLACEHOLDER
135
- )
136
-
137
- @classmethod
138
- def denormalized_path(
139
- cls,
140
- path, # type: str
141
- interpreter=None, # type: Optional[PythonInterpreter]
142
- ):
143
- # type: (...) -> Text
144
- return find_and_replace_path_components(
145
- path, cls._PYTHON_VER_PLACEHOLDER, cls._python_ver(interpreter=interpreter)
146
- )
147
-
148
75
  path = attr.ib() # type: Text
149
76
  hash = attr.ib(default=None) # type: Optional[Hash]
150
77
  size = attr.ib(default=None) # type: Optional[int]
151
78
 
152
79
 
153
- @attr.s(frozen=True)
154
- class InstalledWheel(object):
155
- class LoadError(Exception):
156
- """Indicates an installed wheel was not loadable at a particular path."""
157
-
158
- _LAYOUT_JSON_FILENAME = ".layout.json"
159
-
160
- @classmethod
161
- def layout_file(cls, prefix_dir):
162
- # type: (str) -> str
163
- return os.path.join(prefix_dir, cls._LAYOUT_JSON_FILENAME)
164
-
165
- @classmethod
166
- def save(
167
- cls,
168
- prefix_dir, # type: str
169
- stash_dir, # type: str
170
- record_relpath, # type: Text
171
- root_is_purelib, # type: bool
172
- ):
173
- # type: (...) -> InstalledWheel
174
-
175
- # We currently need the installed wheel chroot hash for PEX-INFO / boot purposes. It is
176
- # expensive to calculate; so we do it here 1 time when saving the installed wheel.
177
- fingerprint = CacheHelper.dir_hash(prefix_dir, hasher=hashlib.sha256)
178
-
179
- layout = {
180
- "stash_dir": stash_dir,
181
- "record_relpath": record_relpath,
182
- "fingerprint": fingerprint,
183
- "root_is_purelib": root_is_purelib,
184
- }
185
- with open(cls.layout_file(prefix_dir), "w") as fp:
186
- json.dump(layout, fp, sort_keys=True)
187
- return cls(
188
- prefix_dir=prefix_dir,
189
- stash_dir=stash_dir,
190
- record_relpath=record_relpath,
191
- fingerprint=fingerprint,
192
- root_is_purelib=root_is_purelib,
193
- )
194
-
195
- @classmethod
196
- def load(cls, prefix_dir):
197
- # type: (str) -> InstalledWheel
198
- layout_file = cls.layout_file(prefix_dir)
199
- try:
200
- with open(layout_file) as fp:
201
- layout = json.load(fp)
202
- except (IOError, OSError) as e:
203
- raise cls.LoadError(
204
- "Failed to load an installed wheel layout from {layout_file}: {err}".format(
205
- layout_file=layout_file, err=e
206
- )
207
- )
208
- if not isinstance(layout, dict):
209
- raise cls.LoadError(
210
- "The installed wheel layout file at {layout_file} must contain a single top-level "
211
- "object, found: {value}.".format(layout_file=layout_file, value=layout)
212
- )
213
- stash_dir = layout.get("stash_dir")
214
- record_relpath = layout.get("record_relpath")
215
- if not stash_dir or not record_relpath:
216
- raise cls.LoadError(
217
- "The installed wheel layout file at {layout_file} must contain an object with both "
218
- "`stash_dir` and `record_relpath` attributes, found: {value}".format(
219
- layout_file=layout_file, value=layout
220
- )
221
- )
222
-
223
- fingerprint = layout.get("fingerprint")
224
-
225
- # N.B.: Caching root_is_purelib was not part of the original InstalledWheel layout data; so
226
- # we materialize the property if needed to support older installed wheel chroots.
227
- root_is_purelib = layout.get("root_is_purelib")
228
- if root_is_purelib is None:
229
- try:
230
- wheel = WHEEL.load(prefix_dir)
231
- except WheelMetadataLoadError as e:
232
- raise cls.LoadError(
233
- "Failed to determine if installed wheel at {location} is platform-specific: "
234
- "{err}".format(location=prefix_dir, err=e)
235
- )
236
- root_is_purelib = wheel.root_is_purelib
237
-
238
- return cls(
239
- prefix_dir=prefix_dir,
240
- stash_dir=cast(str, stash_dir),
241
- record_relpath=cast(str, record_relpath),
242
- fingerprint=cast("Optional[str]", fingerprint),
243
- root_is_purelib=root_is_purelib,
244
- )
245
-
246
- prefix_dir = attr.ib() # type: str
247
- stash_dir = attr.ib() # type: str
248
- record_relpath = attr.ib() # type: Text
249
- fingerprint = attr.ib() # type: Optional[str]
250
- root_is_purelib = attr.ib() # type: bool
251
-
252
- def wheel_file_name(self):
253
- # type: () -> str
254
- return Wheel.load(self.prefix_dir).wheel_file_name
255
-
256
- def stashed_path(self, *components):
257
- # type: (*str) -> str
258
- return os.path.join(self.prefix_dir, self.stash_dir, *components)
259
-
260
- @staticmethod
261
- def create_installed_file(
262
- path, # type: Text
263
- dest_dir, # type: str
264
- ):
265
- # type: (...) -> InstalledFile
266
- hasher = hashlib.sha256()
267
- hashing.file_hash(path, digest=hasher)
268
- return InstalledFile(
269
- path=os.path.relpath(path, dest_dir),
270
- hash=Hash.create(hasher),
271
- size=os.stat(path).st_size,
272
- )
273
-
274
- def _create_record(
275
- self,
276
- dst, # type: Text
277
- installed_files, # type: Iterable[InstalledFile]
278
- ):
279
- # type: (...) -> None
280
- Record.write(
281
- dst=os.path.join(dst, self.record_relpath),
282
- installed_files=[
283
- # The RECORD entry should never include hash or size; so we replace any such entry
284
- # with an un-hashed and un-sized one.
285
- InstalledFile(self.record_relpath, hash=None, size=None)
286
- if installed_file.path == self.record_relpath
287
- else installed_file
288
- for installed_file in installed_files
289
- ],
290
- )
291
-
292
- def reinstall_flat(
293
- self,
294
- target_dir, # type: str
295
- copy_mode=CopyMode.LINK, # type: CopyMode.Value
296
- ):
297
- # type: (...) -> Iterator[Tuple[Text, Text]]
298
- """Re-installs the installed wheel in a flat target directory.
299
-
300
- N.B.: A record of reinstalled files is returned in the form of an iterator that must be
301
- consumed to drive the installation to completion.
302
-
303
- If there is an error re-installing a file due to it already existing in the target
304
- directory, the error is suppressed, and it's expected that the caller detects this by
305
- comparing the record of installed files against those installed previously.
306
-
307
- :return: An iterator over src -> dst pairs.
308
- """
309
- installed_files = [InstalledFile(self.record_relpath)]
310
- for src, dst in itertools.chain(
311
- self._reinstall_stash(dest_dir=target_dir, link=copy_mode is not CopyMode.COPY),
312
- self._reinstall_site_packages(target_dir, copy_mode=copy_mode),
313
- ):
314
- installed_files.append(self.create_installed_file(path=dst, dest_dir=target_dir))
315
- yield src, dst
316
-
317
- self._create_record(target_dir, installed_files)
318
-
319
- def reinstall_venv(
320
- self,
321
- venv, # type: Virtualenv
322
- copy_mode=CopyMode.LINK, # type: CopyMode.Value
323
- rel_extra_path=None, # type: Optional[str]
324
- ):
325
- # type: (...) -> Iterator[Tuple[Text, Text]]
326
- """Re-installs the installed wheel in a venv.
327
-
328
- N.B.: A record of reinstalled files is returned in the form of an iterator that must be
329
- consumed to drive the installation to completion.
330
-
331
- If there is an error re-installing a file due to it already existing in the destination
332
- venv, the error is suppressed, and it's expected that the caller detects this by comparing
333
- the record of installed files against those installed previously.
334
-
335
- :return: An iterator over src -> dst pairs.
336
- """
337
-
338
- site_packages_dir = venv.purelib if self.root_is_purelib else venv.platlib
339
- site_packages_dir = (
340
- os.path.join(site_packages_dir, rel_extra_path) if rel_extra_path else site_packages_dir
341
- )
342
-
343
- installed_files = [InstalledFile(self.record_relpath)]
344
- for src, dst in itertools.chain(
345
- self._reinstall_stash(
346
- dest_dir=venv.venv_dir,
347
- interpreter=venv.interpreter,
348
- link=copy_mode is not CopyMode.COPY,
349
- ),
350
- self._reinstall_site_packages(site_packages_dir, copy_mode=copy_mode),
351
- ):
352
- installed_files.append(self.create_installed_file(path=dst, dest_dir=site_packages_dir))
353
- yield src, dst
354
-
355
- self._create_record(site_packages_dir, installed_files)
356
-
357
- def _reinstall_stash(
358
- self,
359
- dest_dir, # type: str
360
- interpreter=None, # type: Optional[PythonInterpreter]
361
- link=True, # type: bool
362
- ):
363
- # type: (...) -> Iterator[Tuple[Text, Text]]
364
-
365
- stash_abs_path = os.path.join(self.prefix_dir, self.stash_dir)
366
- for root, dirs, files in os.walk(stash_abs_path, topdown=True, followlinks=True):
367
- dir_created = False
368
- for f in files:
369
- src = os.path.join(root, f)
370
- src_relpath = os.path.relpath(src, stash_abs_path)
371
- dst = InstalledFile.denormalized_path(
372
- path=os.path.join(dest_dir, src_relpath), interpreter=interpreter
373
- )
374
- if not dir_created:
375
- safe_mkdir(os.path.dirname(dst))
376
- dir_created = True
377
- try:
378
- # We only try to link regular files since linking a symlink on Linux can produce
379
- # another symlink, which leaves open the possibility the src target could later
380
- # go missing leaving the dst dangling.
381
- if link and not os.path.islink(src):
382
- try:
383
- safe_link(src, dst)
384
- continue
385
- except OSError as e:
386
- if e.errno != errno.EXDEV:
387
- raise e
388
- link = False
389
- shutil.copy(src, dst)
390
- except (IOError, OSError) as e:
391
- if e.errno != errno.EEXIST:
392
- raise e
393
- finally:
394
- yield src, dst
395
-
396
- def _reinstall_site_packages(
397
- self,
398
- site_packages_dir, # type: str
399
- copy_mode=CopyMode.LINK, # type: CopyMode.Value
400
- ):
401
- # type: (...) -> Iterator[Tuple[Text, Text]]
402
-
403
- link = copy_mode is CopyMode.LINK
404
- for root, dirs, files in os.walk(self.prefix_dir, topdown=True, followlinks=True):
405
- if root == self.prefix_dir:
406
- dirs[:] = [d for d in dirs if not is_pyc_dir(d) and d != self.stash_dir]
407
- files[:] = [
408
- f for f in files if not is_pyc_file(f) and f != self._LAYOUT_JSON_FILENAME
409
- ]
410
-
411
- traverse = set(dirs)
412
- for path, is_dir in itertools.chain(
413
- zip(dirs, itertools.repeat(True)), zip(files, itertools.repeat(False))
414
- ):
415
- src_entry = os.path.join(root, path)
416
- dst_entry = os.path.join(
417
- site_packages_dir, os.path.relpath(src_entry, self.prefix_dir)
418
- )
419
- try:
420
- if copy_mode is CopyMode.SYMLINK and not (
421
- src_entry.endswith(".dist-info") and os.path.isdir(src_entry)
422
- ):
423
- safe_relative_symlink(src_entry, dst_entry)
424
- traverse.discard(path)
425
- elif is_dir:
426
- safe_mkdir(dst_entry)
427
- else:
428
- # We only try to link regular files since linking a symlink on Linux can
429
- # produce another symlink, which leaves open the possibility the src_entry
430
- # target could later go missing leaving the dst_entry dangling.
431
- if link and not os.path.islink(src_entry):
432
- try:
433
- safe_link(src_entry, dst_entry)
434
- continue
435
- except OSError as e:
436
- if e.errno != errno.EXDEV:
437
- raise e
438
- link = False
439
- shutil.copy(src_entry, dst_entry)
440
- except (IOError, OSError) as e:
441
- if e.errno != errno.EEXIST:
442
- raise e
443
- finally:
444
- if not is_dir:
445
- yield src_entry, dst_entry
446
-
447
- dirs[:] = list(traverse)
80
+ def create_installed_file(
81
+ path, # type: Text
82
+ dest_dir, # type: str
83
+ ):
84
+ # type: (...) -> InstalledFile
85
+ hasher = hashlib.sha256()
86
+ hashing.file_hash(path, digest=hasher)
87
+ return InstalledFile(
88
+ path=os.path.relpath(path, dest_dir),
89
+ hash=Hash.create(hasher),
90
+ size=os.stat(path).st_size,
91
+ )
448
92
 
449
93
 
450
94
  class RecordError(Exception):
@@ -472,23 +116,31 @@ class Record(object):
472
116
  See: https://peps.python.org/pep-0376/#record
473
117
  """
474
118
 
119
+ @classmethod
120
+ def write_fp(
121
+ cls,
122
+ fp, # type: IO
123
+ installed_files, # type: Iterable[InstalledFile]
124
+ eol="\n", # type: str
125
+ ):
126
+ # type: (...) -> None
127
+ csv_writer = csv.writer(fp, delimiter=",", quotechar='"', lineterminator=eol)
128
+ for installed_file in installed_files:
129
+ csv_writer.writerow(attr.astuple(installed_file, recurse=False))
130
+
475
131
  @classmethod
476
132
  def write(
477
133
  cls,
478
134
  dst, # type: Text
479
135
  installed_files, # type: Iterable[InstalledFile]
136
+ eol="\n", # type: str
480
137
  ):
481
138
  # type: (...) -> None
482
139
 
483
140
  # The RECORD is a csv file with the path to each installed file in the 1st column.
484
141
  # See: https://peps.python.org/pep-0376/#record
485
- with safe_open(dst, "w") as fp:
486
- csv_writer = cast(
487
- "CSVWriter",
488
- csv.writer(fp, delimiter=",", quotechar='"', lineterminator="\n"),
489
- )
490
- for installed_file in sorted(installed_files, key=lambda installed: installed.path):
491
- csv_writer.writerow(attr.astuple(installed_file, recurse=False))
142
+ with safe_open(dst, "wb" if PY2 else "w") as fp:
143
+ cls.write_fp(fp, installed_files, eol=eol)
492
144
 
493
145
  @classmethod
494
146
  def read(