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/pep_376.py CHANGED
@@ -5,32 +5,18 @@ 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
9
+ import io
12
10
  import os
13
- import shutil
14
11
  from fileinput import FileInput
15
12
 
16
13
  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
14
+ from pex.common import safe_open
15
+ from pex.compatibility import PY2
16
+ from pex.typing import TYPE_CHECKING
31
17
 
32
18
  if TYPE_CHECKING:
33
- from typing import Callable, Iterable, Iterator, Optional, Protocol, Text, Tuple, Union
19
+ from typing import IO, Callable, Iterable, Iterator, Optional, Protocol, Text, Union
34
20
 
35
21
  import attr # vendor:skip
36
22
 
@@ -66,7 +52,12 @@ class Hash(object):
66
52
  # + https://peps.python.org/pep-0376/#record
67
53
  # + https://peps.python.org/pep-0427/#appendix
68
54
  fingerprint = base64.urlsafe_b64encode(hasher.digest()).rstrip(b"=")
69
- return cls(value="{alg}={hash}".format(alg=hasher.name, hash=fingerprint.decode("ascii")))
55
+
56
+ # N.B.: The algorithm is all caps under Python 2.7, but lower case under Python 3; so we
57
+ # normalize.
58
+ alg = hasher.name.lower()
59
+
60
+ return cls(value="{alg}={hash}".format(alg=alg, hash=fingerprint.decode("ascii")))
70
61
 
71
62
  value = attr.ib() # type: str
72
63
 
@@ -75,39 +66,6 @@ class Hash(object):
75
66
  return self.value
76
67
 
77
68
 
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
69
  @attr.s(frozen=True)
112
70
  class InstalledFile(object):
113
71
  """The record of a single installed file from a PEP 376 RECORD file.
@@ -115,336 +73,31 @@ class InstalledFile(object):
115
73
  See: https://peps.python.org/pep-0376/#record
116
74
  """
117
75
 
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
76
  path = attr.ib() # type: Text
149
77
  hash = attr.ib(default=None) # type: Optional[Hash]
150
78
  size = attr.ib(default=None) # type: Optional[int]
151
79
 
152
80
 
153
81
  @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
- )
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
194
85
 
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
- )
86
+ dir_info = attr.ib() # type: InstalledFile
245
87
 
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
88
 
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)
89
+ def create_installed_file(
90
+ path, # type: Text
91
+ dest_dir, # type: str
92
+ ):
93
+ # type: (...) -> InstalledFile
94
+ hasher = hashlib.sha256()
95
+ hashing.file_hash(path, digest=hasher)
96
+ return InstalledFile(
97
+ path=os.path.relpath(path, dest_dir),
98
+ hash=Hash.create(hasher),
99
+ size=os.stat(path).st_size,
100
+ )
448
101
 
449
102
 
450
103
  class RecordError(Exception):
@@ -472,23 +125,50 @@ class Record(object):
472
125
  See: https://peps.python.org/pep-0376/#record
473
126
  """
474
127
 
128
+ @classmethod
129
+ def write_fp(
130
+ cls,
131
+ fp, # type: IO
132
+ installed_files, # type: Iterable[Union[InstalledFile, InstalledDirectory]]
133
+ eol="\n", # type: str
134
+ ):
135
+ # type: (...) -> None
136
+ csv_writer = csv.writer(fp, delimiter=",", quotechar='"', lineterminator=eol)
137
+ for installed_file in installed_files:
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))
142
+
143
+ @classmethod
144
+ def write_bytes(
145
+ cls,
146
+ installed_files, # type: Iterable[Union[InstalledFile, InstalledDirectory]]
147
+ eol="\n", # type: str
148
+ ):
149
+ # type: (...) -> bytes
150
+ if PY2:
151
+ record_fp = io.BytesIO()
152
+ cls.write_fp(fp=record_fp, installed_files=installed_files, eol=eol)
153
+ return record_fp.getvalue()
154
+ else:
155
+ record_fp = io.StringIO()
156
+ cls.write_fp(fp=record_fp, installed_files=installed_files, eol=eol)
157
+ return record_fp.getvalue().encode("utf-8")
158
+
475
159
  @classmethod
476
160
  def write(
477
161
  cls,
478
162
  dst, # type: Text
479
- installed_files, # type: Iterable[InstalledFile]
163
+ installed_files, # type: Iterable[Union[InstalledFile, InstalledDirectory]]
164
+ eol="\n", # type: str
480
165
  ):
481
166
  # type: (...) -> None
482
167
 
483
168
  # The RECORD is a csv file with the path to each installed file in the 1st column.
484
169
  # 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))
170
+ with safe_open(dst, "wb" if PY2 else "w") as fp:
171
+ cls.write_fp(fp, installed_files, eol=eol)
492
172
 
493
173
  @classmethod
494
174
  def read(
@@ -496,7 +176,7 @@ class Record(object):
496
176
  lines, # type: Union[FileInput[Text], Iterator[Text]]
497
177
  exclude=None, # type: Optional[Callable[[Text], bool]]
498
178
  ):
499
- # type: (...) -> Iterator[InstalledFile]
179
+ # type: (...) -> Iterator[Union[InstalledFile, InstalledDirectory]]
500
180
 
501
181
  # The RECORD is a csv file with the path to each installed file in the 1st column.
502
182
  # See: https://peps.python.org/pep-0376/#record
@@ -508,7 +188,11 @@ class Record(object):
508
188
  continue
509
189
  file_hash = Hash(fingerprint) if fingerprint else None
510
190
  size = int(file_size) if file_size else None
511
- 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
512
196
 
513
197
  project_name = attr.ib() # type: str
514
198
  version = attr.ib() # type: str
pex/pep_425.py CHANGED
@@ -6,11 +6,12 @@ from __future__ import absolute_import
6
6
  import itertools
7
7
  import os.path
8
8
 
9
- from pex.dist_metadata import is_wheel
9
+ from pex.dist_metadata import Distribution, is_wheel
10
10
  from pex.orderedset import OrderedSet
11
11
  from pex.rank import Rank
12
12
  from pex.third_party.packaging.tags import Tag, parse_tag
13
13
  from pex.typing import TYPE_CHECKING, cast, overload
14
+ from pex.wheel import WHEEL
14
15
 
15
16
  if TYPE_CHECKING:
16
17
  from typing import (
@@ -66,7 +67,13 @@ class CompatibilityTags(object):
66
67
 
67
68
  @classmethod
68
69
  def from_wheel(cls, wheel):
69
- # type: (Text) -> CompatibilityTags
70
+ # type: (Union[Text, Distribution]) -> CompatibilityTags
71
+
72
+ if isinstance(wheel, Distribution):
73
+ if not is_wheel(wheel.location):
74
+ return cls(tags=WHEEL.from_distribution(wheel).tags)
75
+ wheel = wheel.location
76
+
70
77
  if not is_wheel(wheel):
71
78
  raise ValueError(
72
79
  "Can only calculate wheel tags from a filename that ends in .whl per "
@@ -74,6 +81,7 @@ class CompatibilityTags(object):
74
81
  wheel=wheel
75
82
  )
76
83
  )
84
+
77
85
  wheel_stem, _ = os.path.splitext(os.path.basename(wheel))
78
86
  # Wheel filename format: https://peps.python.org/pep-0427/#file-name-convention
79
87
  # `{distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl`
@@ -86,6 +94,7 @@ class CompatibilityTags(object):
86
94
  pattern=pattern, wheel=wheel
87
95
  )
88
96
  )
97
+
89
98
  return cls(tags=tuple(parse_tag("-".join(wheel_components[-3:]))))
90
99
 
91
100
  @classmethod