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/resolver.py CHANGED
@@ -8,15 +8,14 @@ import functools
8
8
  import glob
9
9
  import hashlib
10
10
  import itertools
11
+ import json
11
12
  import os
12
- import tarfile
13
13
  import zipfile
14
14
  from abc import abstractmethod
15
15
  from collections import OrderedDict, defaultdict
16
16
 
17
- from pex import targets
17
+ from pex import sdist, targets
18
18
  from pex.atomic_directory import AtomicDirectory, atomic_directory
19
- from pex.auth import PasswordEntry
20
19
  from pex.cache.dirs import BuiltWheelDir, CacheDir
21
20
  from pex.common import (
22
21
  open_zip,
@@ -31,24 +30,29 @@ from pex.dependency_configuration import DependencyConfiguration
31
30
  from pex.dist_metadata import (
32
31
  DistMetadata,
33
32
  Distribution,
33
+ ProjectMetadata,
34
34
  Requirement,
35
35
  is_tar_sdist,
36
36
  is_wheel,
37
37
  is_zip_sdist,
38
38
  )
39
+ from pex.exceptions import production_assert
39
40
  from pex.fingerprinted_distribution import FingerprintedDistribution
41
+ from pex.installed_wheel import InstalledWheel
40
42
  from pex.jobs import Raise, SpawnedJob, execute_parallel, iter_map_parallel
41
43
  from pex.network_configuration import NetworkConfiguration
42
44
  from pex.orderedset import OrderedSet
43
- from pex.pep_376 import InstalledWheel
44
45
  from pex.pep_425 import CompatibilityTags
45
46
  from pex.pep_427 import InstallableType, WheelError, install_wheel_chroot
47
+ from pex.pep_440 import Version
46
48
  from pex.pep_503 import ProjectName
47
49
  from pex.pip.download_observer import DownloadObserver
48
50
  from pex.pip.installation import get_pip
51
+ from pex.pip.local_project import digest_local_project
49
52
  from pex.pip.tool import PackageIndexConfiguration
50
53
  from pex.pip.version import PipVersionValue
51
54
  from pex.requirements import LocalProjectRequirement, URLRequirement
55
+ from pex.resolve.package_repository import ReposConfiguration
52
56
  from pex.resolve.requirement_configuration import RequirementConfiguration
53
57
  from pex.resolve.resolver_configuration import BuildConfiguration, PipLog, ResolverVersion
54
58
  from pex.resolve.resolvers import (
@@ -59,7 +63,10 @@ from pex.resolve.resolvers import (
59
63
  Untranslatable,
60
64
  check_resolve,
61
65
  )
66
+ from pex.resolve.target_system import TargetSystem, UniversalTarget
67
+ from pex.result import Error
62
68
  from pex.targets import AbbreviatedPlatform, CompletePlatform, LocalInterpreter, Target, Targets
69
+ from pex.third_party.packaging.specifiers import SpecifierSet
63
70
  from pex.third_party.packaging.tags import Tag
64
71
  from pex.tracer import TRACER
65
72
  from pex.typing import TYPE_CHECKING
@@ -79,6 +86,7 @@ if TYPE_CHECKING:
79
86
  Sequence,
80
87
  Set,
81
88
  Tuple,
89
+ Union,
82
90
  )
83
91
 
84
92
  import attr # vendor:skip
@@ -88,59 +96,135 @@ else:
88
96
  from pex.third_party import attr
89
97
 
90
98
 
91
- def _uniqued_targets(targets=None):
92
- # type: (Optional[Iterable[Target]]) -> Tuple[Target, ...]
93
- return tuple(OrderedSet(targets)) if targets is not None else ()
99
+ @attr.s(frozen=True)
100
+ class DownloadTarget(object):
101
+ @classmethod
102
+ def current(cls):
103
+ # type: () -> DownloadTarget
104
+ return cls(target=targets.current())
105
+
106
+ target = attr.ib() # type: Target
107
+ universal_target = attr.ib(default=None) # type: Optional[UniversalTarget]
108
+
109
+ def render_description(self):
110
+ # type: () -> str
111
+ target_description = self.target.render_description()
112
+ if self.universal_target:
113
+ description_components = ["universal resolve"]
114
+ if self.universal_target.systems and frozenset(
115
+ self.universal_target.systems
116
+ ) != frozenset(TargetSystem.values()):
117
+ description_components.append(
118
+ "targeting {systems}".format(
119
+ systems=" and ".join(map(str, self.universal_target.systems))
120
+ )
121
+ )
122
+ if self.universal_target.implementation:
123
+ description_components.append(
124
+ "for {impl}".format(impl=self.universal_target.implementation)
125
+ )
126
+ description_components.append("using {target}".format(target=target_description))
127
+ return " ".join(description_components)
128
+ return target_description
129
+
130
+ def id(self, complete=False):
131
+ # type: (bool) -> str
132
+
133
+ if self.universal_target:
134
+ id_components = ["universal"]
135
+ if self.universal_target.implementation:
136
+ id_components.append(str(self.universal_target.implementation))
137
+ id_components.append(
138
+ "-and-".join(map(str, self.universal_target.systems or TargetSystem.values()))
139
+ )
140
+ if complete:
141
+ id_components.append(
142
+ hashlib.sha1(str(self.universal_target.marker()).encode("utf-8")).hexdigest()
143
+ )
144
+ return "-".join(id_components)
145
+
146
+ if isinstance(self.target, LocalInterpreter):
147
+ # e.g.: CPython 2.7.18
148
+ return self.target.interpreter.version_string
149
+ if isinstance(self.target, AbbreviatedPlatform):
150
+ return str(self.target.platform)
151
+ if isinstance(self.target, CompletePlatform):
152
+ return str(self.target.platform.tag)
153
+
154
+ return self.target.id
155
+
156
+
157
+ def _uniqued_download_requests(requests=None):
158
+ # type: (Optional[Iterable[DownloadRequest]]) -> Tuple[DownloadRequest, ...]
159
+ return tuple(OrderedSet(requests)) if requests is not None else ()
94
160
 
95
161
 
96
162
  @attr.s(frozen=True)
97
163
  class PipLogManager(object):
164
+ @staticmethod
165
+ def _target_id(download_target):
166
+ # type: (DownloadTarget) -> str
167
+
168
+ universal_target = download_target.universal_target
169
+ if universal_target:
170
+ id_components = ["universal"]
171
+ if universal_target.implementation:
172
+ id_components.append(str(universal_target.implementation))
173
+ id_components.extend(map(str, universal_target.systems or TargetSystem.values()))
174
+ return "-and-".join(id_components)
175
+
176
+ target = download_target.target
177
+ if isinstance(target, LocalInterpreter):
178
+ # e.g.: CPython 2.7.18
179
+ return target.interpreter.version_string
180
+ if isinstance(target, AbbreviatedPlatform):
181
+ return str(target.platform)
182
+ if isinstance(target, CompletePlatform):
183
+ return str(target.platform.tag)
184
+ return target.id
185
+
98
186
  @classmethod
99
187
  def create(
100
188
  cls,
101
189
  log, # type: Optional[PipLog]
102
- targets, # type: Sequence[Target]
190
+ download_targets, # type: Sequence[DownloadTarget]
103
191
  ):
104
192
  # type: (...) -> PipLogManager
105
- log_by_target = {} # type: Dict[Target, str]
106
- if log and len(targets) == 1:
107
- log_by_target[targets[0]] = log.path
193
+ log_by_download_target = {} # type: Dict[DownloadTarget, str]
194
+ if log and len(download_targets) == 1:
195
+ log_by_download_target[download_targets[0]] = log.path
108
196
  elif log:
109
197
  log_dir = safe_mkdtemp(prefix="pex-pip-log.")
110
- log_by_target.update(
111
- (target, os.path.join(log_dir, "pip.{target}.log".format(target=target.id)))
112
- for target in targets
198
+ log_by_download_target.update(
199
+ (
200
+ download_target,
201
+ os.path.join(
202
+ log_dir,
203
+ "pip.{target}.log".format(target=download_target.id(complete=True)),
204
+ ),
205
+ )
206
+ for download_target in download_targets
113
207
  )
114
- return cls(log=log, log_by_target=log_by_target)
208
+ return cls(log=log, log_by_download_target=log_by_download_target)
115
209
 
116
210
  log = attr.ib() # type: Optional[PipLog]
117
- _log_by_target = attr.ib() # type: Mapping[Target, str]
118
-
119
- @staticmethod
120
- def _target_id(target):
121
- # type: (Target) -> str
122
- if isinstance(target, LocalInterpreter):
123
- # e.g.: CPython 2.7.18
124
- return target.interpreter.version_string
125
- if isinstance(target, AbbreviatedPlatform):
126
- return str(target.platform)
127
- if isinstance(target, CompletePlatform):
128
- return str(target.platform.tag)
129
- return target.id
211
+ _log_by_download_target = attr.ib() # type: Mapping[DownloadTarget, str]
130
212
 
131
213
  def finalize_log(self):
132
214
  # type: () -> None
133
215
  if not self.log:
134
216
  return
135
217
 
136
- target_count = len(self._log_by_target)
218
+ target_count = len(self._log_by_download_target)
137
219
  if target_count <= 1:
138
220
  return
139
221
 
140
222
  with safe_open(self.log.path, "a") as out_fp:
141
- for index, (target, log) in enumerate(self._log_by_target.items(), start=1):
223
+ for index, (download_target, log) in enumerate(
224
+ self._log_by_download_target.items(), start=1
225
+ ):
142
226
  prefix = "{index}/{count}]{target}".format(
143
- index=index, count=target_count, target=self._target_id(target)
227
+ index=index, count=target_count, target=download_target.id()
144
228
  )
145
229
  if not os.path.exists(log):
146
230
  print(
@@ -153,18 +237,60 @@ class PipLogManager(object):
153
237
  for line in in_fp:
154
238
  out_fp.write("{prefix}: {line}".format(prefix=prefix, line=line))
155
239
 
156
- def get_log(self, target):
157
- # type: (Target) -> Optional[str]
158
- return self._log_by_target.get(target)
240
+ def get_log(self, download_target):
241
+ # type: (DownloadTarget) -> Optional[str]
242
+ return self._log_by_download_target.get(download_target)
159
243
 
160
244
 
161
245
  @attr.s(frozen=True)
162
- class DownloadRequest(object):
163
- targets = attr.ib(converter=_uniqued_targets) # type: Tuple[Target, ...]
246
+ class Report(object):
247
+ @classmethod
248
+ def parse(
249
+ cls,
250
+ download_target, # type: DownloadTarget
251
+ report, # type: str
252
+ ):
253
+ # type: (...) -> Report
254
+
255
+ with open(report) as fp:
256
+ data = json.load(fp)
257
+
258
+ project_metadata = [] # type: List[ProjectMetadata]
259
+ for distribution in data["install"]:
260
+ metadata = distribution["metadata"]
261
+ project_metadata.append(
262
+ ProjectMetadata(
263
+ project_name=ProjectName(metadata["name"]),
264
+ version=Version(metadata["version"]),
265
+ requires_python=SpecifierSet(metadata.get("requires_python", "")),
266
+ requires_dists=tuple(
267
+ Requirement.parse(requirement)
268
+ for requirement in metadata.get("requires_dist", ())
269
+ ),
270
+ )
271
+ )
272
+ return cls(
273
+ download_target=download_target,
274
+ metadata={metadata.project_name: metadata for metadata in project_metadata},
275
+ )
276
+
277
+ download_target = attr.ib() # type: DownloadTarget
278
+ metadata = attr.ib() # type: Mapping[ProjectName, ProjectMetadata]
279
+
280
+
281
+ @attr.s(frozen=True)
282
+ class Reports(object):
283
+ reports = attr.ib(default=()) # type: Tuple[Report, ...]
284
+
285
+ def __iter__(self):
286
+ # type: () -> Iterator[Report]
287
+ return iter(self.reports)
288
+
289
+
290
+ @attr.s(frozen=True)
291
+ class _PipSession(object):
292
+ requests = attr.ib(converter=_uniqued_download_requests) # type: Tuple[DownloadRequest, ...]
164
293
  direct_requirements = attr.ib() # type: Iterable[ParsedRequirement]
165
- requirements = attr.ib(default=None) # type: Optional[Iterable[str]]
166
- requirement_files = attr.ib(default=None) # type: Optional[Iterable[str]]
167
- constraint_files = attr.ib(default=None) # type: Optional[Iterable[str]]
168
294
  allow_prereleases = attr.ib(default=False) # type: bool
169
295
  transitive = attr.ib(default=True) # type: bool
170
296
  package_index_configuration = attr.ib(default=None) # type: Optional[PackageIndexConfiguration]
@@ -181,12 +307,114 @@ class DownloadRequest(object):
181
307
  # type: () -> Iterator[BuildRequest]
182
308
  for requirement in self.direct_requirements:
183
309
  if isinstance(requirement, LocalProjectRequirement):
184
- for target in self.targets:
185
- yield BuildRequest.create(target=target, source_path=requirement.path)
310
+ for request in self.requests:
311
+ yield BuildRequest.for_directory(
312
+ target=request.target, source_path=requirement.path
313
+ )
314
+
315
+ def generate_reports(self, max_parallel_jobs=None):
316
+ # type: (Optional[int]) -> Reports
317
+
318
+ if not self.requests or not any(request.has_requirements for request in self.requests):
319
+ # Nothing to report.
320
+ return Reports()
321
+
322
+ dest = safe_mkdtemp(
323
+ prefix="resolver_report.", dir=safe_mkdir(CacheDir.DOWNLOADS.path(".tmp"))
324
+ )
325
+
326
+ log_manager = PipLogManager.create(
327
+ self.pip_log,
328
+ download_targets=tuple(request.download_target for request in self.requests),
329
+ )
330
+ if self.pip_log and not self.pip_log.user_specified:
331
+ TRACER.log(
332
+ "Preserving `pip install --dry-run` log at {log_path}".format(
333
+ log_path=self.pip_log.path
334
+ ),
335
+ V=ENV.PEX_VERBOSE,
336
+ )
337
+
338
+ spawn_report = functools.partial(
339
+ self._spawn_report, resolved_target_dir=dest, log_manager=log_manager
340
+ )
341
+ with TRACER.timed(
342
+ "Resolving for:\n {}".format(
343
+ "\n ".join(request.render_description() for request in self.requests)
344
+ )
345
+ ):
346
+ try:
347
+ return Reports(
348
+ reports=tuple(
349
+ Report.parse(download_request.download_target, report)
350
+ for download_request, report in zip(
351
+ self.requests,
352
+ execute_parallel(
353
+ inputs=self.requests,
354
+ spawn_func=spawn_report,
355
+ error_handler=Raise[DownloadRequest, str](Unsatisfiable),
356
+ max_jobs=max_parallel_jobs,
357
+ ),
358
+ )
359
+ )
360
+ )
361
+ finally:
362
+ log_manager.finalize_log()
363
+
364
+ def _spawn_report(
365
+ self,
366
+ request, # type: DownloadRequest
367
+ resolved_target_dir, # type: str
368
+ log_manager, # type: PipLogManager
369
+ ):
370
+ # type: (...) -> SpawnedJob[str]
371
+
372
+ report_dir = safe_mkdir(
373
+ os.path.join(resolved_target_dir, request.download_target.id(complete=True))
374
+ )
375
+ report = os.path.join(report_dir, "pip-report.json")
376
+ download_target = request.download_target
377
+ observer = (
378
+ self.observer.observe_download(
379
+ download_target=download_target, download_dir=resolved_target_dir
380
+ )
381
+ if self.observer
382
+ else None
383
+ )
384
+
385
+ target = download_target.target
386
+
387
+ download_job = get_pip(
388
+ interpreter=target.get_interpreter(),
389
+ version=self.pip_version,
390
+ resolver=self.resolver,
391
+ extra_requirements=(
392
+ self.package_index_configuration.extra_pip_requirements
393
+ if self.package_index_configuration
394
+ else ()
395
+ ),
396
+ ).spawn_report(
397
+ report_path=report,
398
+ requirements=request.requirements,
399
+ requirement_files=request.requirement_files,
400
+ constraint_files=request.constraint_files,
401
+ allow_prereleases=self.allow_prereleases,
402
+ transitive=self.transitive,
403
+ target=target,
404
+ package_index_configuration=self.package_index_configuration,
405
+ build_configuration=self.build_configuration,
406
+ observer=observer,
407
+ dependency_configuration=self.dependency_configuration,
408
+ universal_target=download_target.universal_target,
409
+ log=log_manager.get_log(download_target),
410
+ )
411
+
412
+ return SpawnedJob.wait(job=download_job, result=report)
186
413
 
187
414
  def download_distributions(self, dest=None, max_parallel_jobs=None):
188
415
  # type: (...) -> List[DownloadResult]
189
- if not self.requirements and not self.requirement_files:
416
+
417
+ if not self.requests or not any(request.has_requirements for request in self.requests):
190
418
  # Nothing to resolve.
191
419
  return []
192
420
 
@@ -194,48 +422,32 @@ class DownloadRequest(object):
194
422
  prefix="resolver_download.", dir=safe_mkdir(CacheDir.DOWNLOADS.path(".tmp"))
195
423
  )
196
424
 
197
- log_manager = PipLogManager.create(self.pip_log, self.targets)
425
+ log_manager = PipLogManager.create(
426
+ self.pip_log,
427
+ download_targets=tuple(request.download_target for request in self.requests),
428
+ )
198
429
  if self.pip_log and not self.pip_log.user_specified:
199
430
  TRACER.log(
200
431
  "Preserving `pip download` log at {log_path}".format(log_path=self.pip_log.path),
201
432
  V=ENV.PEX_VERBOSE,
202
433
  )
203
434
 
204
- requirement_config = RequirementConfiguration(
205
- requirements=self.requirements,
206
- requirement_files=self.requirement_files,
207
- constraint_files=self.constraint_files,
208
- )
209
- network_configuration = (
210
- self.package_index_configuration.network_configuration
211
- if self.package_index_configuration
212
- else None
213
- )
214
- subdirectory_by_filename = {} # type: Dict[str, str]
215
- for parsed_requirement in requirement_config.parse_requirements(network_configuration):
216
- if not isinstance(parsed_requirement, URLRequirement):
217
- continue
218
- subdirectory = parsed_requirement.subdirectory
219
- if subdirectory:
220
- subdirectory_by_filename[parsed_requirement.filename] = subdirectory
221
-
222
435
  spawn_download = functools.partial(
223
436
  self._spawn_download,
224
437
  resolved_dists_dir=dest,
225
438
  log_manager=log_manager,
226
- subdirectory_by_filename=subdirectory_by_filename,
227
439
  )
228
440
  with TRACER.timed(
229
441
  "Resolving for:\n {}".format(
230
- "\n ".join(target.render_description() for target in self.targets)
442
+ "\n ".join(request.render_description() for request in self.requests)
231
443
  )
232
444
  ):
233
445
  try:
234
446
  return list(
235
447
  execute_parallel(
236
- inputs=self.targets,
448
+ inputs=self.requests,
237
449
  spawn_func=spawn_download,
238
- error_handler=Raise[Target, DownloadResult](Unsatisfiable),
450
+ error_handler=Raise[DownloadRequest, DownloadResult](Unsatisfiable),
239
451
  max_jobs=max_parallel_jobs,
240
452
  )
241
453
  )
@@ -244,20 +456,42 @@ class DownloadRequest(object):
244
456
 
245
457
  def _spawn_download(
246
458
  self,
247
- target, # type: Target
459
+ request, # type: DownloadRequest
248
460
  resolved_dists_dir, # type: str
249
461
  log_manager, # type: PipLogManager
250
- subdirectory_by_filename, # type: Mapping[str, str]
251
462
  ):
252
463
  # type: (...) -> SpawnedJob[DownloadResult]
253
- download_dir = os.path.join(resolved_dists_dir, target.id)
464
+
465
+ requirement_config = RequirementConfiguration(
466
+ requirements=request.requirements,
467
+ requirement_files=request.requirement_files,
468
+ constraint_files=request.constraint_files,
469
+ )
470
+ network_configuration = (
471
+ self.package_index_configuration.network_configuration
472
+ if self.package_index_configuration
473
+ else None
474
+ )
475
+ subdirectory_by_filename = {} # type: Dict[str, str]
476
+ for parsed_requirement in requirement_config.parse_requirements(network_configuration):
477
+ if not isinstance(parsed_requirement, URLRequirement):
478
+ continue
479
+ subdirectory = parsed_requirement.subdirectory
480
+ if subdirectory:
481
+ subdirectory_by_filename[parsed_requirement.filename] = subdirectory
482
+
483
+ download_target = request.download_target
484
+ download_dir = os.path.join(resolved_dists_dir, download_target.id(complete=True))
254
485
  observer = (
255
- self.observer.observe_download(target=target, download_dir=download_dir)
486
+ self.observer.observe_download(
487
+ download_target=download_target, download_dir=download_dir
488
+ )
256
489
  if self.observer
257
490
  else None
258
491
  )
259
492
 
260
- download_result = DownloadResult(target, download_dir, subdirectory_by_filename)
493
+ download_result = DownloadResult(download_target, download_dir, subdirectory_by_filename)
494
+ target = download_target.target
261
495
  download_job = get_pip(
262
496
  interpreter=target.get_interpreter(),
263
497
  version=self.pip_version,
@@ -269,9 +503,9 @@ class DownloadRequest(object):
269
503
  ),
270
504
  ).spawn_download_distributions(
271
505
  download_dir=download_dir,
272
- requirements=self.requirements,
273
- requirement_files=self.requirement_files,
274
- constraint_files=self.constraint_files,
506
+ requirements=request.requirements,
507
+ requirement_files=request.requirement_files,
508
+ constraint_files=request.constraint_files,
275
509
  allow_prereleases=self.allow_prereleases,
276
510
  transitive=self.transitive,
277
511
  target=target,
@@ -279,7 +513,8 @@ class DownloadRequest(object):
279
513
  build_configuration=self.build_configuration,
280
514
  observer=observer,
281
515
  dependency_configuration=self.dependency_configuration,
282
- log=log_manager.get_log(target),
516
+ universal_target=download_target.universal_target,
517
+ log=log_manager.get_log(download_target),
283
518
  )
284
519
 
285
520
  return SpawnedJob.wait(job=download_job, result=download_result)
@@ -292,7 +527,7 @@ class DownloadResult(object):
292
527
  # type: (str) -> bool
293
528
  return is_wheel(path) and zipfile.is_zipfile(path)
294
529
 
295
- target = attr.ib() # type: Target
530
+ download_target = attr.ib() # type: DownloadTarget
296
531
  download_dir = attr.ib() # type: str
297
532
  subdirectory_by_filename = attr.ib() # type: Mapping[str, str]
298
533
 
@@ -310,70 +545,135 @@ class DownloadResult(object):
310
545
  subdirectory = self.subdirectory_by_filename.get(
311
546
  os.path.basename(distribution_path)
312
547
  )
313
- yield BuildRequest.create(
314
- target=self.target, source_path=distribution_path, subdirectory=subdirectory
548
+ production_assert(
549
+ os.path.isfile(distribution_path),
550
+ (
551
+ "Pip download results should always be files and never local project "
552
+ "directories."
553
+ ),
554
+ )
555
+ yield BuildRequest.for_file(
556
+ target=self.download_target,
557
+ source_path=distribution_path,
558
+ subdirectory=subdirectory,
315
559
  )
316
560
 
317
561
  def install_requests(self):
318
562
  # type: () -> Iterator[InstallRequest]
319
563
  for distribution_path in self._iter_distribution_paths():
320
564
  if self._is_wheel(distribution_path):
321
- yield InstallRequest.create(target=self.target, wheel_path=distribution_path)
565
+ yield InstallRequest.create(
566
+ target=self.download_target, wheel_path=distribution_path
567
+ )
322
568
 
323
569
 
324
570
  class IntegrityError(Exception):
325
571
  pass
326
572
 
327
573
 
328
- def fingerprint_path(path):
574
+ if TYPE_CHECKING:
575
+ from pex.hashing import Hasher
576
+
577
+
578
+ def _hasher():
579
+ # type: () -> Hasher
580
+
581
+ return hashlib.sha256()
582
+
583
+
584
+ def _fingerprint_file(path):
329
585
  # type: (str) -> str
586
+ return CacheHelper.hash(path, digest=_hasher())
330
587
 
331
- # We switched from sha1 to sha256 at the transition from using `pip install --target` to
332
- # `pip install --prefix` to serve two purposes:
333
- # 1. Insulate the new installation scheme from the old.
334
- # 2. Move past sha1 which was shown to have practical collision attacks in 2019.
335
- #
336
- # The installation scheme switch was the primary purpose and switching hashes proved a pragmatic
337
- # insulation. If the `pip install --prefix` re-arrangement scheme evolves, then some other
338
- # option than switching hashing algorithms will be needed, like post-fixing a running version
339
- # integer or just mixing one into the hashed content.
340
- #
341
- # See: https://github.com/pex-tool/pex/issues/1655 for a general overview of these cache
342
- # structure concerns.
343
- hasher = hashlib.sha256
344
588
 
345
- if os.path.isdir(path):
346
- return CacheHelper.dir_hash(path, hasher=hasher)
347
- return CacheHelper.hash(path, hasher=hasher)
589
+ def _fingerprint_directory(path):
590
+ # type: (str) -> str
591
+ return CacheHelper.dir_hash(path, digest=_hasher())
348
592
 
349
593
 
350
594
  class BuildError(Exception):
351
595
  pass
352
596
 
353
597
 
598
+ def _fingerprint_local_project(
599
+ path, # type: str
600
+ target, # type: Target
601
+ resolver=None, # type: Optional[Resolver]
602
+ pip_version=None, # type: Optional[PipVersionValue]
603
+ ):
604
+ # type: (...) -> str
605
+ if resolver:
606
+ build_system_resolver = resolver
607
+ else:
608
+ from pex.resolve.configured_resolver import ConfiguredResolver
609
+
610
+ build_system_resolver = ConfiguredResolver.default()
611
+
612
+ hasher = _hasher()
613
+ result = digest_local_project(
614
+ directory=path,
615
+ digest=hasher,
616
+ target=target,
617
+ resolver=build_system_resolver,
618
+ pip_version=pip_version,
619
+ )
620
+ if isinstance(result, Error):
621
+ raise BuildError(
622
+ "Failed to create an sdist for hashing from the local project at {path}: "
623
+ "{err}".format(path=path, err=result)
624
+ )
625
+ return hasher.hexdigest()
626
+
627
+
628
+ def _as_download_target(target):
629
+ # type: (Union[DownloadTarget, Target]) -> DownloadTarget
630
+ return target if isinstance(target, DownloadTarget) else DownloadTarget(target)
631
+
632
+
354
633
  @attr.s(frozen=True)
355
634
  class BuildRequest(object):
356
635
  @classmethod
357
- def create(
636
+ def for_file(
358
637
  cls,
359
- target, # type: Target
638
+ target, # type: Union[DownloadTarget, Target]
360
639
  source_path, # type: str
361
640
  subdirectory=None, # type: Optional[str]
362
641
  ):
363
642
  # type: (...) -> BuildRequest
364
- fingerprint = fingerprint_path(source_path)
643
+ fingerprint = _fingerprint_file(source_path)
365
644
  return cls(
366
- target=target,
645
+ download_target=_as_download_target(target),
367
646
  source_path=source_path,
368
647
  fingerprint=fingerprint,
369
648
  subdirectory=subdirectory,
370
649
  )
371
650
 
372
- target = attr.ib() # type: Target
651
+ @classmethod
652
+ def for_directory(
653
+ cls,
654
+ target, # type: Union[DownloadTarget, Target]
655
+ source_path, # type: str
656
+ subdirectory=None, # type: Optional[str]
657
+ ):
658
+ # type: (...) -> BuildRequest
659
+ download_target = _as_download_target(target)
660
+ return cls(
661
+ download_target=download_target,
662
+ source_path=source_path,
663
+ fingerprint=None,
664
+ subdirectory=subdirectory,
665
+ )
666
+
667
+ download_target = attr.ib(converter=_as_download_target) # type: DownloadTarget
373
668
  source_path = attr.ib() # type: str
374
- fingerprint = attr.ib() # type: str
669
+ fingerprint = attr.ib() # type: Optional[str]
375
670
  subdirectory = attr.ib() # type: Optional[str]
376
671
 
672
+ @property
673
+ def target(self):
674
+ # type: () -> Target
675
+ return self.download_target.target
676
+
377
677
  def prepare(self):
378
678
  # type: () -> str
379
679
 
@@ -386,26 +686,26 @@ class BuildRequest(object):
386
686
  if is_zip_sdist(self.source_path):
387
687
  with open_zip(self.source_path) as zf:
388
688
  zf.extractall(extract_dir)
389
- elif is_tar_sdist(self.source_path):
390
- with tarfile.open(self.source_path) as tf:
391
- tf.extractall(extract_dir)
392
- else:
393
- raise BuildError(
394
- "Unexpected archive type for sdist {project}".format(project=self.source_path)
395
- )
396
689
 
397
- listing = os.listdir(extract_dir)
398
- if len(listing) != 1:
399
- raise BuildError(
400
- "Expected one top-level project directory to be extracted from {project}, "
401
- "found {count}: {listing}".format(
402
- project=self.source_path, count=len(listing), listing=", ".join(listing)
690
+ listing = os.listdir(extract_dir)
691
+ if len(listing) != 1:
692
+ raise BuildError(
693
+ "Expected one top-level project directory to be extracted from {project}, "
694
+ "found {count}: {listing}".format(
695
+ project=self.source_path, count=len(listing), listing=", ".join(listing)
696
+ )
403
697
  )
404
- )
405
- project_directory = os.path.join(extract_dir, listing[0])
406
- if self.subdirectory:
407
- project_directory = os.path.join(project_directory, self.subdirectory)
408
- return project_directory
698
+ project_directory = os.path.join(extract_dir, listing[0])
699
+ if self.subdirectory:
700
+ project_directory = os.path.join(project_directory, self.subdirectory)
701
+ return project_directory
702
+
703
+ if is_tar_sdist(self.source_path):
704
+ return sdist.extract_tarball(self.source_path, dest_dir=extract_dir)
705
+
706
+ raise BuildError(
707
+ "Unexpected archive type for sdist {project}".format(project=self.source_path)
708
+ )
409
709
 
410
710
  def result(self, source_path=None):
411
711
  # type: (Optional[str]) -> BuildResult
@@ -421,12 +721,16 @@ class BuildResult(object):
421
721
  source_path=None, # type: Optional[str]
422
722
  ):
423
723
  # type: (...) -> BuildResult
424
- built_wheel = BuiltWheelDir.create(
425
- sdist=source_path or build_request.source_path,
426
- fingerprint=build_request.fingerprint,
427
- target=build_request.target,
428
- )
429
- return cls(request=build_request, atomic_dir=AtomicDirectory(built_wheel.dist_dir))
724
+ if build_request.fingerprint:
725
+ built_wheel = BuiltWheelDir.create(
726
+ sdist=source_path or build_request.source_path,
727
+ fingerprint=build_request.fingerprint,
728
+ target=build_request.target,
729
+ )
730
+ target_dir = built_wheel.dist_dir
731
+ else:
732
+ target_dir = os.path.join(safe_mkdtemp(), "build")
733
+ return cls(request=build_request, atomic_dir=AtomicDirectory(target_dir))
430
734
 
431
735
  request = attr.ib() # type: BuildRequest
432
736
  _atomic_dir = attr.ib() # type: AtomicDirectory
@@ -483,7 +787,7 @@ class BuildResult(object):
483
787
  return frozenset(platforms), is_linux
484
788
 
485
789
  wheel_platform_tags, is_linux_wheel = collect_platforms(
486
- CompatibilityTags.from_wheel(wheel.location)
790
+ CompatibilityTags.from_wheel(wheel)
487
791
  )
488
792
  abbreviated_target_platform_tags, is_linux_abbreviated_target = collect_platforms(
489
793
  self.request.target.supported_tags
@@ -522,7 +826,7 @@ class BuildResult(object):
522
826
  target=self.request.target.render_description(),
523
827
  )
524
828
  )
525
- return InstallRequest.create(self.request.target, wheel_path)
829
+ return InstallRequest.create(self.request.target, wheel_path, was_built_locally=True)
526
830
 
527
831
 
528
832
  @attr.s(frozen=True)
@@ -530,16 +834,28 @@ class InstallRequest(object):
530
834
  @classmethod
531
835
  def create(
532
836
  cls,
533
- target, # type: Target
837
+ target, # type: Union[DownloadTarget, Target]
534
838
  wheel_path, # type: str
839
+ was_built_locally=False, # type: bool
535
840
  ):
536
841
  # type: (...) -> InstallRequest
537
- fingerprint = fingerprint_path(wheel_path)
538
- return cls(target=target, wheel_path=wheel_path, fingerprint=fingerprint)
842
+ fingerprint = _fingerprint_file(wheel_path)
843
+ return cls(
844
+ download_target=_as_download_target(target),
845
+ wheel_path=wheel_path,
846
+ fingerprint=fingerprint,
847
+ was_built_locally=was_built_locally,
848
+ )
539
849
 
540
- target = attr.ib() # type: Target
850
+ download_target = attr.ib(converter=_as_download_target) # type: DownloadTarget
541
851
  wheel_path = attr.ib() # type: str
542
852
  fingerprint = attr.ib() # type: str
853
+ was_built_locally = attr.ib(default=False) # type: bool
854
+
855
+ @property
856
+ def target(self):
857
+ # type: () -> Target
858
+ return self.download_target.target
543
859
 
544
860
  @property
545
861
  def wheel_file(self):
@@ -656,7 +972,7 @@ class InstallResult(object):
656
972
  else:
657
973
  cached_fingerprint = installed_wheel.fingerprint
658
974
 
659
- wheel_dir_hash = cached_fingerprint or fingerprint_path(self.install_chroot)
975
+ wheel_dir_hash = cached_fingerprint or _fingerprint_directory(self.install_chroot)
660
976
  runtime_key_dir = os.path.join(self._installation_root, wheel_dir_hash)
661
977
  with atomic_directory(runtime_key_dir) as atomic_dir:
662
978
  if not atomic_dir.is_finalized():
@@ -879,7 +1195,9 @@ def _perform_install(
879
1195
  # type: (...) -> InstallResult
880
1196
  install_result = install_request.result(installed_wheels_dir)
881
1197
  install_wheel_chroot(
882
- wheel_path=install_request.wheel_path, destination=install_result.build_chroot
1198
+ wheel=install_request.wheel_path,
1199
+ destination=install_result.build_chroot,
1200
+ normalize_file_stat=install_request.was_built_locally,
883
1201
  )
884
1202
  return install_result
885
1203
 
@@ -972,8 +1290,10 @@ class BuildAndInstallRequest(object):
972
1290
  )
973
1291
  if is_wheel(dist_path):
974
1292
  to_install.add(InstallRequest.create(install_request.target, dist_path))
1293
+ elif os.path.isdir(dist_path):
1294
+ to_build.add(BuildRequest.for_directory(install_request.target, dist_path))
975
1295
  else:
976
- to_build.add(BuildRequest.create(install_request.target, dist_path))
1296
+ to_build.add(BuildRequest.for_file(install_request.target, dist_path))
977
1297
  already_analyzed.add(metadata.project_name)
978
1298
 
979
1299
  all_install_requests = OrderedSet(install_requests)
@@ -1137,7 +1457,7 @@ def _parse_reqs(
1137
1457
  requirement_files=None, # type: Optional[Iterable[str]]
1138
1458
  network_configuration=None, # type: Optional[NetworkConfiguration]
1139
1459
  ):
1140
- # type: (...) -> Iterable[ParsedRequirement]
1460
+ # type: (...) -> Tuple[ParsedRequirement, ...]
1141
1461
  requirement_configuration = RequirementConfiguration(
1142
1462
  requirements=requirements, requirement_files=requirement_files
1143
1463
  )
@@ -1151,11 +1471,9 @@ def resolve(
1151
1471
  constraint_files=None, # type: Optional[Iterable[str]]
1152
1472
  allow_prereleases=False, # type: bool
1153
1473
  transitive=True, # type: bool
1154
- indexes=None, # type: Optional[Sequence[str]]
1155
- find_links=None, # type: Optional[Sequence[str]]
1474
+ repos_configuration=ReposConfiguration(), # type: ReposConfiguration
1156
1475
  resolver_version=None, # type: Optional[ResolverVersion.Value]
1157
1476
  network_configuration=None, # type: Optional[NetworkConfiguration]
1158
- password_entries=(), # type: Iterable[PasswordEntry]
1159
1477
  build_configuration=BuildConfiguration(), # type: BuildConfiguration
1160
1478
  compile=False, # type: bool
1161
1479
  max_parallel_jobs=None, # type: Optional[int]
@@ -1182,19 +1500,13 @@ def resolve(
1182
1500
  :keyword constraint_files: A sequence of constraint file paths.
1183
1501
  :keyword allow_prereleases: Whether to include pre-release and development versions when
1184
1502
  resolving requirements. Defaults to ``False``, but any requirements that explicitly request
1185
- prerelease or development versions will override this setting.
1503
+ pre-release or development versions will override this setting.
1186
1504
  :keyword transitive: Whether to resolve transitive dependencies of requirements.
1187
1505
  Defaults to ``True``.
1188
- :keyword indexes: A list of urls or paths pointing to PEP 503 compliant repositories to search for
1189
- distributions. Defaults to ``None`` which indicates to use the default pypi index. To turn off
1190
- use of all indexes, pass an empty list.
1191
- :keyword find_links: A list or URLs, paths to local html files or directory paths. If URLs or
1192
- local html file paths, these are parsed for links to distributions. If a local directory path,
1193
- its listing is used to discover distributions.
1506
+ :keyword repos_configuration: Configuration for package repositories to resolve packages from.
1194
1507
  :keyword resolver_version: The resolver version to use.
1195
1508
  :keyword network_configuration: Configuration for network requests made downloading and building
1196
1509
  distributions.
1197
- :keyword password_entries: Any known authentication information needed for resolving.
1198
1510
  :keyword build_configuration: The configuration for building resolved projects.
1199
1511
  :keyword compile: Whether to pre-compile resolved distribution python sources.
1200
1512
  Defaults to ``False``.
@@ -1212,6 +1524,23 @@ def resolve(
1212
1524
  :raises ValueError: If `build=False` and `use_wheel=False`.
1213
1525
  """
1214
1526
 
1527
+ if not build_configuration.allow_wheels:
1528
+ foreign_targets = [
1529
+ target
1530
+ for target in targets.unique_targets()
1531
+ if not isinstance(target, LocalInterpreter)
1532
+ ]
1533
+ if foreign_targets:
1534
+ raise ValueError(
1535
+ "Cannot ignore wheels (use_wheel=False) when resolving for foreign {platforms}: "
1536
+ "{foreign_platforms}".format(
1537
+ platforms=pluralize(foreign_targets, "platform"),
1538
+ foreign_platforms=", ".join(
1539
+ target.render_description() for target in foreign_targets
1540
+ ),
1541
+ )
1542
+ )
1543
+
1215
1544
  # A resolve happens in four stages broken into two phases:
1216
1545
  # 1. Download phase: resolves sdists and wheels in a single operation per distribution target.
1217
1546
  # 2. Install phase:
@@ -1246,38 +1575,26 @@ def resolve(
1246
1575
  package_index_configuration = PackageIndexConfiguration.create(
1247
1576
  pip_version=pip_version,
1248
1577
  resolver_version=resolver_version,
1249
- indexes=indexes,
1250
- find_links=find_links,
1578
+ repos_configuration=repos_configuration,
1251
1579
  network_configuration=network_configuration,
1252
- password_entries=password_entries,
1253
1580
  use_pip_config=use_pip_config,
1254
1581
  extra_pip_requirements=extra_pip_requirements,
1255
1582
  keyring_provider=keyring_provider,
1256
1583
  )
1257
1584
 
1258
- if not build_configuration.allow_wheels:
1259
- foreign_targets = [
1260
- target
1261
- for target in targets.unique_targets()
1262
- if not isinstance(target, LocalInterpreter)
1263
- ]
1264
- if foreign_targets:
1265
- raise ValueError(
1266
- "Cannot ignore wheels (use_wheel=False) when resolving for foreign {platforms}: "
1267
- "{foreign_platforms}".format(
1268
- platforms=pluralize(foreign_targets, "platform"),
1269
- foreign_platforms=", ".join(
1270
- target.render_description() for target in foreign_targets
1271
- ),
1272
- )
1273
- )
1585
+ requests = tuple(
1586
+ DownloadRequest(
1587
+ download_target=DownloadTarget(target=target),
1588
+ requirements=requirements,
1589
+ requirement_files=requirement_files,
1590
+ constraint_files=constraint_files,
1591
+ )
1592
+ for target in targets.unique_targets()
1593
+ )
1274
1594
 
1275
1595
  build_requests, download_results = _download_internal(
1276
- targets=targets,
1596
+ requests=requests,
1277
1597
  direct_requirements=direct_requirements,
1278
- requirements=requirements,
1279
- requirement_files=requirement_files,
1280
- constraint_files=constraint_files,
1281
1598
  allow_prereleases=allow_prereleases,
1282
1599
  transitive=transitive,
1283
1600
  package_index_configuration=package_index_configuration,
@@ -1325,11 +1642,8 @@ def resolve(
1325
1642
 
1326
1643
 
1327
1644
  def _download_internal(
1328
- targets, # type: Targets
1645
+ requests, # type: Tuple[DownloadRequest, ...]
1329
1646
  direct_requirements, # type: Iterable[ParsedRequirement]
1330
- requirements=None, # type: Optional[Iterable[str]]
1331
- requirement_files=None, # type: Optional[Iterable[str]]
1332
- constraint_files=None, # type: Optional[Iterable[str]]
1333
1647
  allow_prereleases=False, # type: bool
1334
1648
  transitive=True, # type: bool
1335
1649
  package_index_configuration=None, # type: Optional[PackageIndexConfiguration]
@@ -1344,13 +1658,9 @@ def _download_internal(
1344
1658
  ):
1345
1659
  # type: (...) -> Tuple[List[BuildRequest], List[DownloadResult]]
1346
1660
 
1347
- unique_targets = targets.unique_targets()
1348
- download_request = DownloadRequest(
1349
- targets=unique_targets,
1661
+ pip_session = _PipSession(
1662
+ requests=requests,
1350
1663
  direct_requirements=direct_requirements,
1351
- requirements=requirements,
1352
- requirement_files=requirement_files,
1353
- constraint_files=constraint_files,
1354
1664
  allow_prereleases=allow_prereleases,
1355
1665
  transitive=transitive,
1356
1666
  package_index_configuration=package_index_configuration,
@@ -1362,8 +1672,8 @@ def _download_internal(
1362
1672
  dependency_configuration=dependency_configuration,
1363
1673
  )
1364
1674
 
1365
- local_projects = list(download_request.iter_local_projects())
1366
- download_results = download_request.download_distributions(
1675
+ local_projects = list(pip_session.iter_local_projects())
1676
+ download_results = pip_session.download_distributions(
1367
1677
  dest=dest, max_parallel_jobs=max_parallel_jobs
1368
1678
  )
1369
1679
  return local_projects, download_results
@@ -1373,12 +1683,13 @@ def _download_internal(
1373
1683
  class LocalDistribution(object):
1374
1684
  path = attr.ib() # type: str
1375
1685
  fingerprint = attr.ib() # type: str
1376
- target = attr.ib(factory=targets.current) # type: Target
1686
+ download_target = attr.ib(factory=DownloadTarget.current) # type: DownloadTarget
1377
1687
  subdirectory = attr.ib(default=None) # type: Optional[str]
1378
1688
 
1379
- @fingerprint.default
1380
- def _calculate_fingerprint(self):
1381
- return fingerprint_path(self.path)
1689
+ @property
1690
+ def target(self):
1691
+ # type: () -> Target
1692
+ return self.download_target.target
1382
1693
 
1383
1694
  @property
1384
1695
  def is_wheel(self):
@@ -1394,8 +1705,8 @@ class ResolveObserver(object):
1394
1705
  @abstractmethod
1395
1706
  def observe_download(
1396
1707
  self,
1397
- target,
1398
- download_dir,
1708
+ download_target, # type: DownloadTarget
1709
+ download_dir, # type: str
1399
1710
  ):
1400
1711
  # type: (...) -> DownloadObserver
1401
1712
  raise NotImplementedError()
@@ -1408,11 +1719,9 @@ def download(
1408
1719
  constraint_files=None, # type: Optional[Iterable[str]]
1409
1720
  allow_prereleases=False, # type: bool
1410
1721
  transitive=True, # type: bool
1411
- indexes=None, # type: Optional[Sequence[str]]
1412
- find_links=None, # type: Optional[Sequence[str]]
1722
+ repos_configuration=ReposConfiguration(), # type: ReposConfiguration
1413
1723
  resolver_version=None, # type: Optional[ResolverVersion.Value]
1414
1724
  network_configuration=None, # type: Optional[NetworkConfiguration]
1415
- password_entries=(), # type: Iterable[PasswordEntry]
1416
1725
  build_configuration=BuildConfiguration(), # type: BuildConfiguration
1417
1726
  dest=None, # type: Optional[str]
1418
1727
  max_parallel_jobs=None, # type: Optional[int]
@@ -1434,19 +1743,13 @@ def download(
1434
1743
  :keyword constraint_files: A sequence of constraint file paths.
1435
1744
  :keyword allow_prereleases: Whether to include pre-release and development versions when
1436
1745
  resolving requirements. Defaults to ``False``, but any requirements that explicitly request
1437
- prerelease or development versions will override this setting.
1746
+ pre-release or development versions will override this setting.
1438
1747
  :keyword transitive: Whether to resolve transitive dependencies of requirements.
1439
1748
  Defaults to ``True``.
1440
- :keyword indexes: A list of urls or paths pointing to PEP 503 compliant repositories to search
1441
- for distributions. Defaults to ``None`` which indicates to use the default pypi index. To turn
1442
- off use of all indexes, pass an empty list.
1443
- :keyword find_links: A list of URLs, paths to local html files or directory paths. If URLs or
1444
- local html file paths, these are parsed for links to distributions. If a local directory path,
1445
- its listing is used to discover distributions.
1749
+ :keyword repos_configuration: Configuration for package repositories to resolve packages from.
1446
1750
  :keyword resolver_version: The resolver version to use.
1447
1751
  :keyword network_configuration: Configuration for network requests made downloading and building
1448
1752
  distributions.
1449
- :keyword password_entries: Any known authentication information needed for downloading.
1450
1753
  :keyword build_configuration: The configuration for building resolved projects.
1451
1754
  :keyword dest: A directory path to download distributions to.
1452
1755
  :keyword max_parallel_jobs: The maximum number of parallel jobs to use when resolving,
@@ -1459,24 +1762,127 @@ def download(
1459
1762
  :raises ValueError: If a foreign platform was provided in `platforms`, and `use_wheel=False`.
1460
1763
  :raises ValueError: If `build=False` and `use_wheel=False`.
1461
1764
  """
1462
- direct_requirements = _parse_reqs(requirements, requirement_files, network_configuration)
1765
+ return download_requests(
1766
+ requests=tuple(
1767
+ DownloadRequest(
1768
+ download_target=DownloadTarget(target),
1769
+ requirements=requirements,
1770
+ requirement_files=requirement_files,
1771
+ constraint_files=constraint_files,
1772
+ )
1773
+ for target in targets.unique_targets()
1774
+ ),
1775
+ direct_requirements=_parse_reqs(requirements, requirement_files, network_configuration),
1776
+ allow_prereleases=allow_prereleases,
1777
+ transitive=transitive,
1778
+ repos_configuration=repos_configuration,
1779
+ resolver_version=resolver_version,
1780
+ network_configuration=network_configuration,
1781
+ build_configuration=build_configuration,
1782
+ dest=dest,
1783
+ max_parallel_jobs=max_parallel_jobs,
1784
+ observer=observer,
1785
+ pip_log=pip_log,
1786
+ pip_version=pip_version,
1787
+ resolver=resolver,
1788
+ use_pip_config=use_pip_config,
1789
+ extra_pip_requirements=extra_pip_requirements,
1790
+ keyring_provider=keyring_provider,
1791
+ dependency_configuration=dependency_configuration,
1792
+ )
1793
+
1794
+
1795
+ def _as_str_tuple(items):
1796
+ # type: (Optional[Iterable[str]]) -> Tuple[str, ...]
1797
+ if not items:
1798
+ return ()
1799
+ return items if isinstance(items, tuple) else tuple(items)
1800
+
1801
+
1802
+ @attr.s(frozen=True)
1803
+ class DownloadRequest(object):
1804
+ @classmethod
1805
+ def create(
1806
+ cls,
1807
+ target, # type: Target
1808
+ universal_target=None, # type: Optional[UniversalTarget]
1809
+ requirement_configuration=RequirementConfiguration(), # type: RequirementConfiguration
1810
+ provenance=None, # type: Optional[str]
1811
+ ):
1812
+ # type: (...) -> DownloadRequest
1813
+ return cls(
1814
+ download_target=DownloadTarget(target=target, universal_target=universal_target),
1815
+ requirements=requirement_configuration.requirements,
1816
+ requirement_files=requirement_configuration.requirement_files,
1817
+ constraint_files=requirement_configuration.constraint_files,
1818
+ provenance=provenance,
1819
+ )
1820
+
1821
+ download_target = attr.ib() # type: DownloadTarget
1822
+ requirements = attr.ib(default=(), converter=_as_str_tuple) # type: Tuple[str, ...]
1823
+ requirement_files = attr.ib(default=(), converter=_as_str_tuple) # type: Tuple[str, ...]
1824
+ constraint_files = attr.ib(default=(), converter=_as_str_tuple) # type: Tuple[str, ...]
1825
+ provenance = attr.ib(default=None) # type: Optional[str]
1826
+
1827
+ def render_description(self):
1828
+ # type: () -> str
1829
+ description = self.download_target.render_description()
1830
+ if not self.provenance:
1831
+ return description
1832
+ return "{description} from {provenance}".format(
1833
+ description=description, provenance=self.provenance
1834
+ )
1835
+
1836
+ @property
1837
+ def target(self):
1838
+ # type: () -> Target
1839
+ return self.download_target.target
1840
+
1841
+ @property
1842
+ def universal_target(self):
1843
+ # type: () -> Optional[UniversalTarget]
1844
+ return self.download_target.universal_target
1845
+
1846
+ @property
1847
+ def has_requirements(self):
1848
+ return bool(self.requirements) or bool(self.requirement_files)
1849
+
1850
+
1851
+ def download_requests(
1852
+ requests, # type: Tuple[DownloadRequest, ...]
1853
+ direct_requirements, # type: Tuple[ParsedRequirement, ...]
1854
+ allow_prereleases=False, # type: bool
1855
+ transitive=True, # type: bool
1856
+ repos_configuration=ReposConfiguration(), # type: ReposConfiguration
1857
+ resolver_version=None, # type: Optional[ResolverVersion.Value]
1858
+ network_configuration=None, # type: Optional[NetworkConfiguration]
1859
+ build_configuration=BuildConfiguration(), # type: BuildConfiguration
1860
+ dest=None, # type: Optional[str]
1861
+ max_parallel_jobs=None, # type: Optional[int]
1862
+ observer=None, # type: Optional[ResolveObserver]
1863
+ pip_log=None, # type: Optional[PipLog]
1864
+ pip_version=None, # type: Optional[PipVersionValue]
1865
+ resolver=None, # type: Optional[Resolver]
1866
+ use_pip_config=False, # type: bool
1867
+ extra_pip_requirements=(), # type: Tuple[Requirement, ...]
1868
+ keyring_provider=None, # type: Optional[str]
1869
+ dependency_configuration=DependencyConfiguration(), # type: DependencyConfiguration
1870
+ ):
1871
+ # type: (...) -> Downloaded
1872
+
1463
1873
  package_index_configuration = PackageIndexConfiguration.create(
1464
1874
  pip_version=pip_version,
1465
1875
  resolver_version=resolver_version,
1466
- indexes=indexes,
1467
- find_links=find_links,
1876
+ repos_configuration=repos_configuration,
1468
1877
  network_configuration=network_configuration,
1469
- password_entries=password_entries,
1470
1878
  use_pip_config=use_pip_config,
1471
1879
  extra_pip_requirements=extra_pip_requirements,
1472
1880
  keyring_provider=keyring_provider,
1473
1881
  )
1882
+
1474
1883
  build_requests, download_results = _download_internal(
1475
- targets=targets,
1884
+ requests=requests,
1476
1885
  direct_requirements=direct_requirements,
1477
- requirements=requirements,
1478
- requirement_files=requirement_files,
1479
- constraint_files=constraint_files,
1480
1886
  allow_prereleases=allow_prereleases,
1481
1887
  transitive=transitive,
1482
1888
  package_index_configuration=package_index_configuration,
@@ -1495,11 +1901,21 @@ def download(
1495
1901
  def add_build_requests(requests):
1496
1902
  # type: (Iterable[BuildRequest]) -> None
1497
1903
  for request in requests:
1904
+ if request.fingerprint:
1905
+ fingerprint = request.fingerprint
1906
+ else:
1907
+ production_assert(os.path.isdir(request.source_path))
1908
+ fingerprint = _fingerprint_local_project(
1909
+ path=request.source_path,
1910
+ target=request.target,
1911
+ resolver=resolver,
1912
+ pip_version=pip_version,
1913
+ )
1498
1914
  local_distributions.append(
1499
1915
  LocalDistribution(
1500
- target=request.target,
1916
+ download_target=request.download_target,
1501
1917
  path=request.source_path,
1502
- fingerprint=request.fingerprint,
1918
+ fingerprint=fingerprint,
1503
1919
  subdirectory=request.subdirectory,
1504
1920
  )
1505
1921
  )
@@ -1510,10 +1926,58 @@ def download(
1510
1926
  for install_request in download_result.install_requests():
1511
1927
  local_distributions.append(
1512
1928
  LocalDistribution(
1513
- target=install_request.target,
1929
+ download_target=install_request.download_target,
1514
1930
  path=install_request.wheel_path,
1515
1931
  fingerprint=install_request.fingerprint,
1516
1932
  )
1517
1933
  )
1518
1934
 
1519
1935
  return Downloaded(local_distributions=tuple(local_distributions))
1936
+
1937
+
1938
+ def reports(
1939
+ requests, # type: Tuple[DownloadRequest, ...]
1940
+ direct_requirements, # type: Tuple[ParsedRequirement, ...]
1941
+ allow_prereleases=False, # type: bool
1942
+ transitive=True, # type: bool
1943
+ repos_configuration=ReposConfiguration(), # type: ReposConfiguration
1944
+ resolver_version=None, # type: Optional[ResolverVersion.Value]
1945
+ network_configuration=None, # type: Optional[NetworkConfiguration]
1946
+ build_configuration=BuildConfiguration(), # type: BuildConfiguration
1947
+ max_parallel_jobs=None, # type: Optional[int]
1948
+ observer=None, # type: Optional[ResolveObserver]
1949
+ pip_log=None, # type: Optional[PipLog]
1950
+ pip_version=None, # type: Optional[PipVersionValue]
1951
+ resolver=None, # type: Optional[Resolver]
1952
+ use_pip_config=False, # type: bool
1953
+ extra_pip_requirements=(), # type: Tuple[Requirement, ...]
1954
+ keyring_provider=None, # type: Optional[str]
1955
+ dependency_configuration=DependencyConfiguration(), # type: DependencyConfiguration
1956
+ ):
1957
+ # type: (...) -> Reports
1958
+
1959
+ package_index_configuration = PackageIndexConfiguration.create(
1960
+ pip_version=pip_version,
1961
+ resolver_version=resolver_version,
1962
+ repos_configuration=repos_configuration,
1963
+ network_configuration=network_configuration,
1964
+ use_pip_config=use_pip_config,
1965
+ extra_pip_requirements=extra_pip_requirements,
1966
+ keyring_provider=keyring_provider,
1967
+ )
1968
+
1969
+ pip_session = _PipSession(
1970
+ requests=requests,
1971
+ direct_requirements=direct_requirements,
1972
+ allow_prereleases=allow_prereleases,
1973
+ transitive=transitive,
1974
+ package_index_configuration=package_index_configuration,
1975
+ build_configuration=build_configuration,
1976
+ observer=observer,
1977
+ pip_log=pip_log,
1978
+ pip_version=pip_version,
1979
+ resolver=resolver,
1980
+ dependency_configuration=dependency_configuration,
1981
+ )
1982
+
1983
+ return pip_session.generate_reports(max_parallel_jobs=max_parallel_jobs)