pip 25.2__py3-none-any.whl → 26.0__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.
Files changed (167) hide show
  1. pip/__init__.py +1 -1
  2. pip/_internal/__init__.py +0 -0
  3. pip/_internal/build_env.py +265 -8
  4. pip/_internal/cache.py +1 -1
  5. pip/_internal/cli/base_command.py +11 -0
  6. pip/_internal/cli/cmdoptions.py +200 -71
  7. pip/_internal/cli/index_command.py +20 -0
  8. pip/_internal/cli/main.py +11 -6
  9. pip/_internal/cli/main_parser.py +3 -1
  10. pip/_internal/cli/parser.py +96 -36
  11. pip/_internal/cli/progress_bars.py +4 -2
  12. pip/_internal/cli/req_command.py +126 -30
  13. pip/_internal/commands/cache.py +24 -0
  14. pip/_internal/commands/completion.py +2 -1
  15. pip/_internal/commands/download.py +12 -11
  16. pip/_internal/commands/index.py +13 -6
  17. pip/_internal/commands/install.py +55 -43
  18. pip/_internal/commands/list.py +14 -16
  19. pip/_internal/commands/lock.py +19 -14
  20. pip/_internal/commands/wheel.py +13 -23
  21. pip/_internal/configuration.py +1 -2
  22. pip/_internal/distributions/sdist.py +13 -14
  23. pip/_internal/exceptions.py +96 -6
  24. pip/_internal/index/collector.py +2 -3
  25. pip/_internal/index/package_finder.py +87 -21
  26. pip/_internal/locations/__init__.py +1 -2
  27. pip/_internal/locations/_sysconfig.py +4 -1
  28. pip/_internal/metadata/__init__.py +7 -2
  29. pip/_internal/metadata/importlib/_dists.py +8 -2
  30. pip/_internal/models/link.py +18 -14
  31. pip/_internal/models/release_control.py +92 -0
  32. pip/_internal/models/selection_prefs.py +6 -3
  33. pip/_internal/models/wheel.py +5 -66
  34. pip/_internal/network/auth.py +6 -2
  35. pip/_internal/network/cache.py +6 -11
  36. pip/_internal/network/download.py +4 -5
  37. pip/_internal/network/lazy_wheel.py +5 -3
  38. pip/_internal/network/session.py +14 -10
  39. pip/_internal/operations/build/wheel.py +4 -4
  40. pip/_internal/operations/build/wheel_editable.py +4 -4
  41. pip/_internal/operations/install/wheel.py +1 -2
  42. pip/_internal/operations/prepare.py +9 -4
  43. pip/_internal/pyproject.py +2 -61
  44. pip/_internal/req/__init__.py +1 -3
  45. pip/_internal/req/constructors.py +45 -39
  46. pip/_internal/req/pep723.py +41 -0
  47. pip/_internal/req/req_file.py +10 -2
  48. pip/_internal/req/req_install.py +32 -141
  49. pip/_internal/resolution/resolvelib/candidates.py +20 -11
  50. pip/_internal/resolution/resolvelib/factory.py +43 -1
  51. pip/_internal/resolution/resolvelib/provider.py +9 -0
  52. pip/_internal/resolution/resolvelib/reporter.py +21 -8
  53. pip/_internal/resolution/resolvelib/requirements.py +7 -3
  54. pip/_internal/resolution/resolvelib/resolver.py +2 -6
  55. pip/_internal/self_outdated_check.py +17 -16
  56. pip/_internal/utils/datetime.py +18 -0
  57. pip/_internal/utils/filesystem.py +52 -1
  58. pip/_internal/utils/logging.py +34 -2
  59. pip/_internal/utils/misc.py +18 -12
  60. pip/_internal/utils/pylock.py +116 -0
  61. pip/_internal/utils/unpacking.py +26 -1
  62. pip/_internal/vcs/versioncontrol.py +3 -1
  63. pip/_internal/wheel_builder.py +23 -96
  64. pip/_vendor/README.rst +180 -0
  65. pip/_vendor/cachecontrol/LICENSE.txt +13 -0
  66. pip/_vendor/cachecontrol/__init__.py +6 -3
  67. pip/_vendor/cachecontrol/adapter.py +0 -1
  68. pip/_vendor/cachecontrol/controller.py +1 -1
  69. pip/_vendor/cachecontrol/filewrapper.py +3 -1
  70. pip/_vendor/certifi/LICENSE +20 -0
  71. pip/_vendor/certifi/__init__.py +1 -1
  72. pip/_vendor/certifi/cacert.pem +62 -372
  73. pip/_vendor/dependency_groups/LICENSE.txt +9 -0
  74. pip/_vendor/distlib/LICENSE.txt +284 -0
  75. pip/_vendor/distro/LICENSE +202 -0
  76. pip/_vendor/idna/LICENSE.md +31 -0
  77. pip/_vendor/idna/codec.py +1 -1
  78. pip/_vendor/idna/core.py +1 -1
  79. pip/_vendor/idna/idnadata.py +72 -6
  80. pip/_vendor/idna/package_data.py +1 -1
  81. pip/_vendor/idna/uts46data.py +891 -731
  82. pip/_vendor/msgpack/COPYING +14 -0
  83. pip/_vendor/msgpack/__init__.py +2 -2
  84. pip/_vendor/packaging/LICENSE +3 -0
  85. pip/_vendor/packaging/LICENSE.APACHE +177 -0
  86. pip/_vendor/packaging/LICENSE.BSD +23 -0
  87. pip/_vendor/packaging/__init__.py +1 -1
  88. pip/_vendor/packaging/_elffile.py +0 -1
  89. pip/_vendor/packaging/_manylinux.py +36 -36
  90. pip/_vendor/packaging/_musllinux.py +1 -1
  91. pip/_vendor/packaging/_parser.py +22 -10
  92. pip/_vendor/packaging/_structures.py +8 -0
  93. pip/_vendor/packaging/_tokenizer.py +23 -25
  94. pip/_vendor/packaging/licenses/__init__.py +13 -11
  95. pip/_vendor/packaging/licenses/_spdx.py +41 -1
  96. pip/_vendor/packaging/markers.py +64 -38
  97. pip/_vendor/packaging/metadata.py +143 -27
  98. pip/_vendor/packaging/pylock.py +635 -0
  99. pip/_vendor/packaging/requirements.py +5 -10
  100. pip/_vendor/packaging/specifiers.py +219 -170
  101. pip/_vendor/packaging/tags.py +15 -20
  102. pip/_vendor/packaging/utils.py +19 -24
  103. pip/_vendor/packaging/version.py +315 -105
  104. pip/_vendor/pkg_resources/LICENSE +17 -0
  105. pip/_vendor/platformdirs/LICENSE +21 -0
  106. pip/_vendor/platformdirs/api.py +1 -1
  107. pip/_vendor/platformdirs/macos.py +10 -8
  108. pip/_vendor/platformdirs/version.py +16 -3
  109. pip/_vendor/platformdirs/windows.py +7 -1
  110. pip/_vendor/pygments/LICENSE +25 -0
  111. pip/_vendor/pyproject_hooks/LICENSE +21 -0
  112. pip/_vendor/requests/LICENSE +175 -0
  113. pip/_vendor/requests/__version__.py +2 -2
  114. pip/_vendor/requests/adapters.py +17 -40
  115. pip/_vendor/requests/sessions.py +1 -1
  116. pip/_vendor/resolvelib/LICENSE +13 -0
  117. pip/_vendor/resolvelib/__init__.py +1 -1
  118. pip/_vendor/resolvelib/resolvers/abstract.py +3 -3
  119. pip/_vendor/resolvelib/resolvers/resolution.py +5 -0
  120. pip/_vendor/rich/LICENSE +19 -0
  121. pip/_vendor/rich/style.py +7 -11
  122. pip/_vendor/tomli/LICENSE +21 -0
  123. pip/_vendor/tomli/__init__.py +1 -1
  124. pip/_vendor/tomli/_parser.py +28 -21
  125. pip/_vendor/tomli/_re.py +8 -5
  126. pip/_vendor/tomli_w/LICENSE +21 -0
  127. pip/_vendor/truststore/LICENSE +21 -0
  128. pip/_vendor/truststore/__init__.py +1 -1
  129. pip/_vendor/truststore/_api.py +14 -6
  130. pip/_vendor/truststore/_openssl.py +3 -1
  131. pip/_vendor/urllib3/LICENSE.txt +21 -0
  132. pip/_vendor/vendor.txt +11 -11
  133. {pip-25.2.dist-info → pip-26.0.dist-info}/METADATA +10 -11
  134. {pip-25.2.dist-info → pip-26.0.dist-info}/RECORD +158 -139
  135. {pip-25.2.dist-info → pip-26.0.dist-info}/WHEEL +1 -2
  136. pip-26.0.dist-info/entry_points.txt +4 -0
  137. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/AUTHORS.txt +27 -0
  138. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/idna/LICENSE.md +1 -1
  139. pip/_internal/models/pylock.py +0 -188
  140. pip/_internal/operations/build/metadata_legacy.py +0 -73
  141. pip/_internal/operations/build/wheel_legacy.py +0 -119
  142. pip/_internal/operations/install/editable_legacy.py +0 -48
  143. pip/_internal/utils/setuptools_build.py +0 -149
  144. pip-25.2.dist-info/entry_points.txt +0 -3
  145. pip-25.2.dist-info/licenses/src/pip/_vendor/tomli/LICENSE-HEADER +0 -3
  146. pip-25.2.dist-info/top_level.txt +0 -1
  147. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/LICENSE.txt +0 -0
  148. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/cachecontrol/LICENSE.txt +0 -0
  149. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/certifi/LICENSE +0 -0
  150. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/dependency_groups/LICENSE.txt +0 -0
  151. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/distlib/LICENSE.txt +0 -0
  152. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/distro/LICENSE +0 -0
  153. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/msgpack/COPYING +0 -0
  154. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/packaging/LICENSE +0 -0
  155. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/packaging/LICENSE.APACHE +0 -0
  156. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/packaging/LICENSE.BSD +0 -0
  157. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/pkg_resources/LICENSE +0 -0
  158. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/platformdirs/LICENSE +0 -0
  159. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/pygments/LICENSE +0 -0
  160. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/pyproject_hooks/LICENSE +0 -0
  161. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/requests/LICENSE +0 -0
  162. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/resolvelib/LICENSE +0 -0
  163. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/rich/LICENSE +0 -0
  164. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/tomli/LICENSE +0 -0
  165. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/tomli_w/LICENSE +0 -0
  166. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/truststore/LICENSE +0 -0
  167. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/urllib3/LICENSE.txt +0 -0
@@ -14,7 +14,8 @@ import logging
14
14
  import pathlib
15
15
  import re
16
16
  import sys
17
- from collections.abc import Iterator
17
+ import traceback
18
+ from collections.abc import Iterable, Iterator
18
19
  from itertools import chain, groupby, repeat
19
20
  from typing import TYPE_CHECKING, Literal
20
21
 
@@ -27,9 +28,10 @@ from pip._vendor.rich.text import Text
27
28
  if TYPE_CHECKING:
28
29
  from hashlib import _Hash
29
30
 
30
- from pip._vendor.requests.models import Request, Response
31
+ from pip._vendor.requests.models import PreparedRequest, Request, Response
31
32
 
32
33
  from pip._internal.metadata import BaseDistribution
34
+ from pip._internal.models.link import Link
33
35
  from pip._internal.network.download import _FileDownload
34
36
  from pip._internal.req.req_install import InstallRequirement
35
37
 
@@ -190,6 +192,23 @@ class InstallationError(PipError):
190
192
  """General exception during installation"""
191
193
 
192
194
 
195
+ class FailedToPrepareCandidate(InstallationError):
196
+ """Raised when we fail to prepare a candidate (i.e. fetch and generate metadata).
197
+
198
+ This is intentionally not a diagnostic error, since the output will be presented
199
+ above this error, when this occurs. This should instead present information to the
200
+ user.
201
+ """
202
+
203
+ def __init__(
204
+ self, *, package_name: str, requirement_chain: str, failed_step: str
205
+ ) -> None:
206
+ super().__init__(f"Failed to build '{package_name}' when {failed_step.lower()}")
207
+ self.package_name = package_name
208
+ self.requirement_chain = requirement_chain
209
+ self.failed_step = failed_step
210
+
211
+
193
212
  class MissingPyProjectBuildRequires(DiagnosticPipError):
194
213
  """Raised when pyproject.toml has `build-system`, but no `build-system.requires`."""
195
214
 
@@ -297,7 +316,7 @@ class NetworkConnectionError(PipError):
297
316
  self,
298
317
  error_msg: str,
299
318
  response: Response | None = None,
300
- request: Request | None = None,
319
+ request: Request | PreparedRequest | None = None,
301
320
  ) -> None:
302
321
  """
303
322
  Initialize NetworkConnectionError with `request` and `response`
@@ -384,7 +403,7 @@ class InstallationSubprocessError(DiagnosticPipError, InstallationError):
384
403
  output_lines: list[str] | None,
385
404
  ) -> None:
386
405
  if output_lines is None:
387
- output_prompt = Text("See above for output.")
406
+ output_prompt = Text("No available output.")
388
407
  else:
389
408
  output_prompt = (
390
409
  Text.from_markup(f"[red][{len(output_lines)} lines of output][/]\n")
@@ -412,7 +431,7 @@ class InstallationSubprocessError(DiagnosticPipError, InstallationError):
412
431
  return f"{self.command_description} exited with {self.exit_code}"
413
432
 
414
433
 
415
- class MetadataGenerationFailed(InstallationSubprocessError, InstallationError):
434
+ class MetadataGenerationFailed(DiagnosticPipError, InstallationError):
416
435
  reference = "metadata-generation-failed"
417
436
 
418
437
  def __init__(
@@ -420,7 +439,7 @@ class MetadataGenerationFailed(InstallationSubprocessError, InstallationError):
420
439
  *,
421
440
  package_details: str,
422
441
  ) -> None:
423
- super(InstallationSubprocessError, self).__init__(
442
+ super().__init__(
424
443
  message="Encountered error while generating package metadata.",
425
444
  context=escape(package_details),
426
445
  hint_stmt="See above for details.",
@@ -879,3 +898,74 @@ class InstallWheelBuildError(DiagnosticPipError):
879
898
  context=", ".join(r.name for r in failed), # type: ignore
880
899
  hint_stmt=None,
881
900
  )
901
+
902
+
903
+ class InvalidEggFragment(DiagnosticPipError):
904
+ reference = "invalid-egg-fragment"
905
+
906
+ def __init__(self, link: Link, fragment: str) -> None:
907
+ hint = ""
908
+ if ">" in fragment or "=" in fragment or "<" in fragment:
909
+ hint = (
910
+ "Version specifiers are silently ignored for URL references. "
911
+ "Remove them. "
912
+ )
913
+ if "[" in fragment and "]" in fragment:
914
+ hint += "Try using the Direct URL requirement syntax: 'name[extra] @ URL'"
915
+
916
+ if not hint:
917
+ hint = "Egg fragments can only be a valid project name."
918
+
919
+ super().__init__(
920
+ message=f"The '{escape(fragment)}' egg fragment is invalid",
921
+ context=f"from '{escape(str(link))}'",
922
+ hint_stmt=escape(hint),
923
+ )
924
+
925
+
926
+ class BuildDependencyInstallError(DiagnosticPipError):
927
+ """Raised when build dependencies cannot be installed."""
928
+
929
+ reference = "failed-build-dependency-install"
930
+
931
+ def __init__(
932
+ self,
933
+ req: InstallRequirement | None,
934
+ build_reqs: Iterable[str],
935
+ *,
936
+ cause: Exception,
937
+ log_lines: list[str] | None,
938
+ ) -> None:
939
+ if isinstance(cause, PipError):
940
+ note = "This is likely not a problem with pip."
941
+ else:
942
+ note = (
943
+ "pip crashed unexpectedly. Please file an issue on pip's issue "
944
+ "tracker: https://github.com/pypa/pip/issues/new"
945
+ )
946
+
947
+ if log_lines is None:
948
+ # No logs are available, they must have been printed earlier.
949
+ context = Text("See above for more details.")
950
+ else:
951
+ if isinstance(cause, PipError):
952
+ log_lines.append(f"ERROR: {cause}")
953
+ else:
954
+ # Split rendered error into real lines without trailing newlines.
955
+ log_lines.extend(
956
+ "".join(traceback.format_exception(cause)).splitlines()
957
+ )
958
+
959
+ context = Text.assemble(
960
+ f"Installing {' '.join(build_reqs)}\n",
961
+ (f"[{len(log_lines)} lines of output]\n", "red"),
962
+ "\n".join(log_lines),
963
+ ("\n[end of output]", "red"),
964
+ )
965
+
966
+ message = Text("Cannot install build dependencies", "green")
967
+ if req:
968
+ message += Text(f" for {req}")
969
+ super().__init__(
970
+ message=message, context=context, hint_stmt=None, note_stmt=note
971
+ )
@@ -12,7 +12,6 @@ import json
12
12
  import logging
13
13
  import os
14
14
  import urllib.parse
15
- import urllib.request
16
15
  from collections.abc import Iterable, MutableMapping, Sequence
17
16
  from dataclasses import dataclass
18
17
  from html.parser import HTMLParser
@@ -34,6 +33,7 @@ from pip._internal.network.session import PipSession
34
33
  from pip._internal.network.utils import raise_for_status
35
34
  from pip._internal.utils.filetypes import is_archive_file
36
35
  from pip._internal.utils.misc import redact_auth_from_url
36
+ from pip._internal.utils.urls import url_to_path
37
37
  from pip._internal.vcs import vcs
38
38
 
39
39
  from .sources import CandidatesFromPage, LinkSource, build_source
@@ -330,8 +330,7 @@ def _get_index_content(link: Link, *, session: PipSession) -> IndexContent | Non
330
330
  return None
331
331
 
332
332
  # Tack index.html onto file:// URLs that point to directories
333
- scheme, _, path, _, _, _ = urllib.parse.urlparse(url)
334
- if scheme == "file" and os.path.isdir(urllib.request.url2pathname(path)):
333
+ if url.startswith("file:") and os.path.isdir(url_to_path(url)):
335
334
  # add trailing slash if not present so urljoin doesn't trim
336
335
  # final segment
337
336
  if not url.endswith("/"):
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import datetime
5
6
  import enum
6
7
  import functools
7
8
  import itertools
@@ -17,20 +18,23 @@ from typing import (
17
18
 
18
19
  from pip._vendor.packaging import specifiers
19
20
  from pip._vendor.packaging.tags import Tag
20
- from pip._vendor.packaging.utils import canonicalize_name
21
- from pip._vendor.packaging.version import InvalidVersion, _BaseVersion
21
+ from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
22
+ from pip._vendor.packaging.version import InvalidVersion, Version, _BaseVersion
22
23
  from pip._vendor.packaging.version import parse as parse_version
23
24
 
24
25
  from pip._internal.exceptions import (
25
26
  BestVersionAlreadyInstalled,
26
27
  DistributionNotFound,
28
+ InstallationError,
27
29
  InvalidWheelFilename,
28
30
  UnsupportedWheel,
29
31
  )
30
32
  from pip._internal.index.collector import LinkCollector, parse_links
33
+ from pip._internal.metadata import select_backend
31
34
  from pip._internal.models.candidate import InstallationCandidate
32
35
  from pip._internal.models.format_control import FormatControl
33
36
  from pip._internal.models.link import Link
37
+ from pip._internal.models.release_control import ReleaseControl
34
38
  from pip._internal.models.search_scope import SearchScope
35
39
  from pip._internal.models.selection_prefs import SelectionPreferences
36
40
  from pip._internal.models.target_python import TargetPython
@@ -111,6 +115,8 @@ class LinkType(enum.Enum):
111
115
  format_invalid = enum.auto()
112
116
  platform_mismatch = enum.auto()
113
117
  requires_python_mismatch = enum.auto()
118
+ upload_too_late = enum.auto()
119
+ upload_time_missing = enum.auto()
114
120
 
115
121
 
116
122
  class LinkEvaluator:
@@ -127,11 +133,12 @@ class LinkEvaluator:
127
133
  def __init__(
128
134
  self,
129
135
  project_name: str,
130
- canonical_name: str,
136
+ canonical_name: NormalizedName,
131
137
  formats: frozenset[str],
132
138
  target_python: TargetPython,
133
139
  allow_yanked: bool,
134
140
  ignore_requires_python: bool | None = None,
141
+ uploaded_prior_to: datetime.datetime | None = None,
135
142
  ) -> None:
136
143
  """
137
144
  :param project_name: The user supplied package name.
@@ -149,6 +156,8 @@ class LinkEvaluator:
149
156
  :param ignore_requires_python: Whether to ignore incompatible
150
157
  PEP 503 "data-requires-python" values in HTML links. Defaults
151
158
  to False.
159
+ :param uploaded_prior_to: If set, only allow links uploaded prior to
160
+ the given datetime.
152
161
  """
153
162
  if ignore_requires_python is None:
154
163
  ignore_requires_python = False
@@ -158,6 +167,7 @@ class LinkEvaluator:
158
167
  self._ignore_requires_python = ignore_requires_python
159
168
  self._formats = formats
160
169
  self._target_python = target_python
170
+ self._uploaded_prior_to = uploaded_prior_to
161
171
 
162
172
  self.project_name = project_name
163
173
 
@@ -201,7 +211,7 @@ class LinkEvaluator:
201
211
  LinkType.format_invalid,
202
212
  "invalid wheel filename",
203
213
  )
204
- if canonicalize_name(wheel.name) != self._canonical_name:
214
+ if wheel.name != self._canonical_name:
205
215
  reason = f"wrong project name (not {self.project_name})"
206
216
  return (LinkType.different_project, reason)
207
217
 
@@ -218,6 +228,27 @@ class LinkEvaluator:
218
228
 
219
229
  version = wheel.version
220
230
 
231
+ # Check upload-time filter after verifying the link is a package file.
232
+ # Skip this check for local files, as --uploaded-prior-to only applies
233
+ # to packages from indexes.
234
+ if self._uploaded_prior_to is not None and not link.is_file:
235
+ if link.upload_time is None:
236
+ if link.comes_from:
237
+ index_info = f"Index {link.comes_from}"
238
+ else:
239
+ index_info = "Index"
240
+
241
+ return (
242
+ LinkType.upload_time_missing,
243
+ f"{index_info} does not provide upload-time metadata.",
244
+ )
245
+ elif link.upload_time >= self._uploaded_prior_to:
246
+ return (
247
+ LinkType.upload_too_late,
248
+ f"Upload time {link.upload_time} not "
249
+ f"prior to {self._uploaded_prior_to}",
250
+ )
251
+
221
252
  # This should be up by the self.ok_binary check, but see issue 2700.
222
253
  if "source" not in self._formats and ext != WHEEL_EXTENSION:
223
254
  reason = f"No sources permitted for {self.project_name}"
@@ -350,7 +381,7 @@ class CandidatePreferences:
350
381
  """
351
382
 
352
383
  prefer_binary: bool = False
353
- allow_all_prereleases: bool = False
384
+ release_control: ReleaseControl | None = None
354
385
 
355
386
 
356
387
  @dataclass(frozen=True)
@@ -391,7 +422,7 @@ class CandidateEvaluator:
391
422
  project_name: str,
392
423
  target_python: TargetPython | None = None,
393
424
  prefer_binary: bool = False,
394
- allow_all_prereleases: bool = False,
425
+ release_control: ReleaseControl | None = None,
395
426
  specifier: specifiers.BaseSpecifier | None = None,
396
427
  hashes: Hashes | None = None,
397
428
  ) -> CandidateEvaluator:
@@ -417,7 +448,7 @@ class CandidateEvaluator:
417
448
  supported_tags=supported_tags,
418
449
  specifier=specifier,
419
450
  prefer_binary=prefer_binary,
420
- allow_all_prereleases=allow_all_prereleases,
451
+ release_control=release_control,
421
452
  hashes=hashes,
422
453
  )
423
454
 
@@ -427,14 +458,14 @@ class CandidateEvaluator:
427
458
  supported_tags: list[Tag],
428
459
  specifier: specifiers.BaseSpecifier,
429
460
  prefer_binary: bool = False,
430
- allow_all_prereleases: bool = False,
461
+ release_control: ReleaseControl | None = None,
431
462
  hashes: Hashes | None = None,
432
463
  ) -> None:
433
464
  """
434
465
  :param supported_tags: The PEP 425 tags supported by the target
435
466
  Python in order of preference (most preferred first).
436
467
  """
437
- self._allow_all_prereleases = allow_all_prereleases
468
+ self._release_control = release_control
438
469
  self._hashes = hashes
439
470
  self._prefer_binary = prefer_binary
440
471
  self._project_name = project_name
@@ -455,17 +486,27 @@ class CandidateEvaluator:
455
486
  Return the applicable candidates from a list of candidates.
456
487
  """
457
488
  # Using None infers from the specifier instead.
458
- allow_prereleases = self._allow_all_prereleases or None
489
+ if self._release_control is not None:
490
+ allow_prereleases = self._release_control.allows_prereleases(
491
+ canonicalize_name(self._project_name)
492
+ )
493
+ else:
494
+ allow_prereleases = None
459
495
  specifier = self._specifier
460
496
 
461
- # We turn the version object into a str here because otherwise
462
- # when we're debundled but setuptools isn't, Python will see
463
- # packaging.version.Version and
497
+ # When using the pkg_resources backend we turn the version object into
498
+ # a str here because otherwise when we're debundled but setuptools isn't,
499
+ # Python will see packaging.version.Version and
464
500
  # pkg_resources._vendor.packaging.version.Version as different
465
501
  # types. This way we'll use a str as a common data interchange
466
502
  # format. If we stop using the pkg_resources provided specifier
467
503
  # and start using our own, we can drop the cast to str().
468
- candidates_and_versions = [(c, str(c.version)) for c in candidates]
504
+ if select_backend().NAME == "pkg_resources":
505
+ candidates_and_versions: list[
506
+ tuple[InstallationCandidate, str | Version]
507
+ ] = [(c, str(c.version)) for c in candidates]
508
+ else:
509
+ candidates_and_versions = [(c, c.version) for c in candidates]
469
510
  versions = set(
470
511
  specifier.filter(
471
512
  (v for _, v in candidates_and_versions),
@@ -593,6 +634,7 @@ class PackageFinder:
593
634
  format_control: FormatControl | None = None,
594
635
  candidate_prefs: CandidatePreferences | None = None,
595
636
  ignore_requires_python: bool | None = None,
637
+ uploaded_prior_to: datetime.datetime | None = None,
596
638
  ) -> None:
597
639
  """
598
640
  This constructor is primarily meant to be used by the create() class
@@ -614,6 +656,7 @@ class PackageFinder:
614
656
  self._ignore_requires_python = ignore_requires_python
615
657
  self._link_collector = link_collector
616
658
  self._target_python = target_python
659
+ self._uploaded_prior_to = uploaded_prior_to
617
660
 
618
661
  self.format_control = format_control
619
662
 
@@ -637,6 +680,7 @@ class PackageFinder:
637
680
  link_collector: LinkCollector,
638
681
  selection_prefs: SelectionPreferences,
639
682
  target_python: TargetPython | None = None,
683
+ uploaded_prior_to: datetime.datetime | None = None,
640
684
  ) -> PackageFinder:
641
685
  """Create a PackageFinder.
642
686
 
@@ -645,13 +689,15 @@ class PackageFinder:
645
689
  :param target_python: The target Python interpreter to use when
646
690
  checking compatibility. If None (the default), a TargetPython
647
691
  object will be constructed from the running Python.
692
+ :param uploaded_prior_to: If set, only find links uploaded prior
693
+ to the given datetime.
648
694
  """
649
695
  if target_python is None:
650
696
  target_python = TargetPython()
651
697
 
652
698
  candidate_prefs = CandidatePreferences(
653
699
  prefer_binary=selection_prefs.prefer_binary,
654
- allow_all_prereleases=selection_prefs.allow_all_prereleases,
700
+ release_control=selection_prefs.release_control,
655
701
  )
656
702
 
657
703
  return cls(
@@ -661,6 +707,7 @@ class PackageFinder:
661
707
  allow_yanked=selection_prefs.allow_yanked,
662
708
  format_control=selection_prefs.format_control,
663
709
  ignore_requires_python=selection_prefs.ignore_requires_python,
710
+ uploaded_prior_to=uploaded_prior_to,
664
711
  )
665
712
 
666
713
  @property
@@ -707,11 +754,11 @@ class PackageFinder:
707
754
  return cert
708
755
 
709
756
  @property
710
- def allow_all_prereleases(self) -> bool:
711
- return self._candidate_prefs.allow_all_prereleases
757
+ def release_control(self) -> ReleaseControl | None:
758
+ return self._candidate_prefs.release_control
712
759
 
713
- def set_allow_all_prereleases(self) -> None:
714
- self._candidate_prefs.allow_all_prereleases = True
760
+ def set_release_control(self, release_control: ReleaseControl) -> None:
761
+ self._candidate_prefs.release_control = release_control
715
762
 
716
763
  @property
717
764
  def prefer_binary(self) -> bool:
@@ -720,6 +767,10 @@ class PackageFinder:
720
767
  def set_prefer_binary(self) -> None:
721
768
  self._candidate_prefs.prefer_binary = True
722
769
 
770
+ @property
771
+ def uploaded_prior_to(self) -> datetime.datetime | None:
772
+ return self._uploaded_prior_to
773
+
723
774
  def requires_python_skipped_reasons(self) -> list[str]:
724
775
  reasons = {
725
776
  detail
@@ -739,6 +790,7 @@ class PackageFinder:
739
790
  target_python=self._target_python,
740
791
  allow_yanked=self._allow_yanked,
741
792
  ignore_requires_python=self._ignore_requires_python,
793
+ uploaded_prior_to=self._uploaded_prior_to,
742
794
  )
743
795
 
744
796
  def _sort_links(self, links: Iterable[Link]) -> list[Link]:
@@ -773,6 +825,10 @@ class PackageFinder:
773
825
  InstallationCandidate and return it. Otherwise, return None.
774
826
  """
775
827
  result, detail = link_evaluator.evaluate_link(link)
828
+ if result == LinkType.upload_time_missing:
829
+ # Fail immediately if the index doesn't provide upload-time
830
+ # when --uploaded-prior-to is specified
831
+ raise InstallationError(detail)
776
832
  if result != LinkType.candidate:
777
833
  self._log_skipped_link(link, result, detail)
778
834
  return None
@@ -890,7 +946,7 @@ class PackageFinder:
890
946
  project_name=project_name,
891
947
  target_python=self._target_python,
892
948
  prefer_binary=candidate_prefs.prefer_binary,
893
- allow_all_prereleases=candidate_prefs.allow_all_prereleases,
949
+ release_control=candidate_prefs.release_control,
894
950
  specifier=specifier,
895
951
  hashes=hashes,
896
952
  )
@@ -964,9 +1020,19 @@ class PackageFinder:
964
1020
  )
965
1021
 
966
1022
  if installed_version is None and best_candidate is None:
1023
+ # Check if only final releases are allowed for this package
1024
+ version_type = "version"
1025
+ if self.release_control is not None:
1026
+ allows_pre = self.release_control.allows_prereleases(
1027
+ canonicalize_name(name)
1028
+ )
1029
+ if allows_pre is False:
1030
+ version_type = "final version"
1031
+
967
1032
  logger.critical(
968
- "Could not find a version that satisfies the requirement %s "
1033
+ "Could not find a %s that satisfies the requirement %s "
969
1034
  "(from versions: %s)",
1035
+ version_type,
970
1036
  req,
971
1037
  _format_versions(best_candidate_result.all_candidates),
972
1038
  )
@@ -6,7 +6,6 @@ import os
6
6
  import pathlib
7
7
  import sys
8
8
  import sysconfig
9
- from typing import Any
10
9
 
11
10
  from pip._internal.models.scheme import SCHEME_KEYS, Scheme
12
11
  from pip._internal.utils.compat import WINDOWS
@@ -134,7 +133,7 @@ def _looks_like_red_hat_scheme() -> bool:
134
133
  from distutils.command.install import install
135
134
  from distutils.dist import Distribution
136
135
 
137
- cmd: Any = install(Distribution())
136
+ cmd = install(Distribution())
138
137
  cmd.finalize_options()
139
138
  return (
140
139
  cmd.exec_prefix == f"{os.path.normpath(sys.exec_prefix)}/local"
@@ -4,6 +4,7 @@ import logging
4
4
  import os
5
5
  import sys
6
6
  import sysconfig
7
+ from typing import Callable
7
8
 
8
9
  from pip._internal.exceptions import InvalidSchemeCombination, UserInstallationInvalid
9
10
  from pip._internal.models.scheme import SCHEME_KEYS, Scheme
@@ -24,7 +25,9 @@ logger = logging.getLogger(__name__)
24
25
 
25
26
  _AVAILABLE_SCHEMES = set(sysconfig.get_scheme_names())
26
27
 
27
- _PREFERRED_SCHEME_API = getattr(sysconfig, "get_preferred_scheme", None)
28
+ _PREFERRED_SCHEME_API: Callable[[str], str] | None = getattr(
29
+ sysconfig, "get_preferred_scheme", None
30
+ )
28
31
 
29
32
 
30
33
  def _should_use_osx_framework_prefix() -> bool:
@@ -4,13 +4,16 @@ import contextlib
4
4
  import functools
5
5
  import os
6
6
  import sys
7
- from typing import Literal, Protocol, cast
7
+ from typing import TYPE_CHECKING, Literal, Protocol, cast
8
8
 
9
9
  from pip._internal.utils.deprecation import deprecated
10
10
  from pip._internal.utils.misc import strtobool
11
11
 
12
12
  from .base import BaseDistribution, BaseEnvironment, FilesystemWheel, MemoryWheel, Wheel
13
13
 
14
+ if TYPE_CHECKING:
15
+ from pip._vendor.packaging.utils import NormalizedName
16
+
14
17
  __all__ = [
15
18
  "BaseDistribution",
16
19
  "BaseEnvironment",
@@ -131,7 +134,9 @@ def get_directory_distribution(directory: str) -> BaseDistribution:
131
134
  return select_backend().Distribution.from_directory(directory)
132
135
 
133
136
 
134
- def get_wheel_distribution(wheel: Wheel, canonical_name: str) -> BaseDistribution:
137
+ def get_wheel_distribution(
138
+ wheel: Wheel, canonical_name: NormalizedName
139
+ ) -> BaseDistribution:
135
140
  """Get the representation of the specified wheel's distribution metadata.
136
141
 
137
142
  This returns a Distribution instance from the chosen backend based on
@@ -28,6 +28,7 @@ from pip._internal.utils.temp_dir import TempDirectory
28
28
  from pip._internal.utils.wheel import parse_wheel, read_wheel_metadata_file
29
29
 
30
30
  from ._compat import (
31
+ BadMetadata,
31
32
  BasePath,
32
33
  get_dist_canonical_name,
33
34
  parse_name_and_version_from_info_directory,
@@ -165,9 +166,14 @@ class Distribution(BaseDistribution):
165
166
 
166
167
  @property
167
168
  def version(self) -> Version:
168
- if version := parse_name_and_version_from_info_directory(self._dist)[1]:
169
+ try:
170
+ version = (
171
+ parse_name_and_version_from_info_directory(self._dist)[1]
172
+ or self._dist.version
173
+ )
169
174
  return parse_version(version)
170
- return parse_version(self._dist.version)
175
+ except TypeError:
176
+ raise BadMetadata(self._dist, reason="invalid metadata entry `version`")
171
177
 
172
178
  @property
173
179
  def raw_version(self) -> str: