pip 25.1__py3-none-any.whl → 25.2__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 (203) hide show
  1. pip/__init__.py +3 -3
  2. pip/_internal/__init__.py +2 -2
  3. pip/_internal/build_env.py +118 -94
  4. pip/_internal/cache.py +16 -14
  5. pip/_internal/cli/autocompletion.py +13 -4
  6. pip/_internal/cli/base_command.py +18 -7
  7. pip/_internal/cli/cmdoptions.py +14 -9
  8. pip/_internal/cli/command_context.py +4 -3
  9. pip/_internal/cli/index_command.py +11 -9
  10. pip/_internal/cli/main.py +3 -2
  11. pip/_internal/cli/main_parser.py +4 -3
  12. pip/_internal/cli/parser.py +26 -22
  13. pip/_internal/cli/progress_bars.py +19 -12
  14. pip/_internal/cli/req_command.py +16 -12
  15. pip/_internal/cli/spinners.py +81 -5
  16. pip/_internal/commands/__init__.py +5 -3
  17. pip/_internal/commands/cache.py +18 -15
  18. pip/_internal/commands/check.py +1 -2
  19. pip/_internal/commands/completion.py +1 -2
  20. pip/_internal/commands/configuration.py +26 -18
  21. pip/_internal/commands/debug.py +8 -6
  22. pip/_internal/commands/download.py +2 -3
  23. pip/_internal/commands/freeze.py +2 -3
  24. pip/_internal/commands/hash.py +1 -2
  25. pip/_internal/commands/help.py +1 -2
  26. pip/_internal/commands/index.py +15 -9
  27. pip/_internal/commands/inspect.py +4 -4
  28. pip/_internal/commands/install.py +45 -40
  29. pip/_internal/commands/list.py +35 -26
  30. pip/_internal/commands/lock.py +1 -2
  31. pip/_internal/commands/search.py +14 -12
  32. pip/_internal/commands/show.py +14 -11
  33. pip/_internal/commands/uninstall.py +1 -2
  34. pip/_internal/commands/wheel.py +2 -3
  35. pip/_internal/configuration.py +39 -25
  36. pip/_internal/distributions/base.py +6 -4
  37. pip/_internal/distributions/installed.py +8 -4
  38. pip/_internal/distributions/sdist.py +20 -13
  39. pip/_internal/distributions/wheel.py +6 -4
  40. pip/_internal/exceptions.py +58 -39
  41. pip/_internal/index/collector.py +24 -29
  42. pip/_internal/index/package_finder.py +70 -61
  43. pip/_internal/index/sources.py +17 -14
  44. pip/_internal/locations/__init__.py +18 -16
  45. pip/_internal/locations/_distutils.py +12 -11
  46. pip/_internal/locations/_sysconfig.py +5 -4
  47. pip/_internal/locations/base.py +4 -3
  48. pip/_internal/main.py +2 -2
  49. pip/_internal/metadata/__init__.py +8 -6
  50. pip/_internal/metadata/_json.py +5 -4
  51. pip/_internal/metadata/base.py +22 -27
  52. pip/_internal/metadata/importlib/_compat.py +6 -4
  53. pip/_internal/metadata/importlib/_dists.py +12 -17
  54. pip/_internal/metadata/importlib/_envs.py +9 -6
  55. pip/_internal/metadata/pkg_resources.py +11 -14
  56. pip/_internal/models/direct_url.py +24 -21
  57. pip/_internal/models/format_control.py +5 -5
  58. pip/_internal/models/installation_report.py +4 -3
  59. pip/_internal/models/link.py +39 -34
  60. pip/_internal/models/pylock.py +27 -22
  61. pip/_internal/models/search_scope.py +6 -7
  62. pip/_internal/models/selection_prefs.py +3 -3
  63. pip/_internal/models/target_python.py +10 -9
  64. pip/_internal/models/wheel.py +7 -5
  65. pip/_internal/network/auth.py +20 -22
  66. pip/_internal/network/cache.py +22 -6
  67. pip/_internal/network/download.py +169 -141
  68. pip/_internal/network/lazy_wheel.py +10 -7
  69. pip/_internal/network/session.py +32 -27
  70. pip/_internal/network/utils.py +2 -2
  71. pip/_internal/network/xmlrpc.py +2 -2
  72. pip/_internal/operations/build/build_tracker.py +10 -8
  73. pip/_internal/operations/build/wheel.py +3 -2
  74. pip/_internal/operations/build/wheel_editable.py +3 -2
  75. pip/_internal/operations/build/wheel_legacy.py +9 -8
  76. pip/_internal/operations/check.py +21 -26
  77. pip/_internal/operations/freeze.py +12 -9
  78. pip/_internal/operations/install/editable_legacy.py +5 -3
  79. pip/_internal/operations/install/wheel.py +53 -44
  80. pip/_internal/operations/prepare.py +35 -30
  81. pip/_internal/pyproject.py +7 -10
  82. pip/_internal/req/__init__.py +12 -10
  83. pip/_internal/req/constructors.py +33 -31
  84. pip/_internal/req/req_dependency_group.py +9 -8
  85. pip/_internal/req/req_file.py +32 -35
  86. pip/_internal/req/req_install.py +37 -34
  87. pip/_internal/req/req_set.py +4 -5
  88. pip/_internal/req/req_uninstall.py +20 -17
  89. pip/_internal/resolution/base.py +3 -3
  90. pip/_internal/resolution/legacy/resolver.py +21 -20
  91. pip/_internal/resolution/resolvelib/base.py +16 -13
  92. pip/_internal/resolution/resolvelib/candidates.py +29 -26
  93. pip/_internal/resolution/resolvelib/factory.py +41 -50
  94. pip/_internal/resolution/resolvelib/found_candidates.py +11 -9
  95. pip/_internal/resolution/resolvelib/provider.py +15 -20
  96. pip/_internal/resolution/resolvelib/reporter.py +5 -3
  97. pip/_internal/resolution/resolvelib/requirements.py +8 -6
  98. pip/_internal/resolution/resolvelib/resolver.py +39 -23
  99. pip/_internal/self_outdated_check.py +8 -6
  100. pip/_internal/utils/appdirs.py +1 -2
  101. pip/_internal/utils/compat.py +7 -1
  102. pip/_internal/utils/compatibility_tags.py +17 -16
  103. pip/_internal/utils/deprecation.py +11 -9
  104. pip/_internal/utils/direct_url_helpers.py +2 -2
  105. pip/_internal/utils/egg_link.py +6 -5
  106. pip/_internal/utils/entrypoints.py +3 -2
  107. pip/_internal/utils/filesystem.py +8 -5
  108. pip/_internal/utils/filetypes.py +4 -6
  109. pip/_internal/utils/glibc.py +6 -5
  110. pip/_internal/utils/hashes.py +9 -6
  111. pip/_internal/utils/logging.py +8 -5
  112. pip/_internal/utils/misc.py +54 -44
  113. pip/_internal/utils/packaging.py +3 -2
  114. pip/_internal/utils/retry.py +7 -4
  115. pip/_internal/utils/setuptools_build.py +12 -10
  116. pip/_internal/utils/subprocess.py +20 -17
  117. pip/_internal/utils/temp_dir.py +10 -12
  118. pip/_internal/utils/unpacking.py +6 -4
  119. pip/_internal/utils/urls.py +1 -1
  120. pip/_internal/utils/virtualenv.py +3 -2
  121. pip/_internal/utils/wheel.py +3 -4
  122. pip/_internal/vcs/bazaar.py +26 -8
  123. pip/_internal/vcs/git.py +59 -24
  124. pip/_internal/vcs/mercurial.py +34 -11
  125. pip/_internal/vcs/subversion.py +27 -16
  126. pip/_internal/vcs/versioncontrol.py +56 -51
  127. pip/_internal/wheel_builder.py +14 -12
  128. pip/_vendor/cachecontrol/__init__.py +1 -1
  129. pip/_vendor/certifi/__init__.py +1 -1
  130. pip/_vendor/certifi/cacert.pem +102 -221
  131. pip/_vendor/certifi/core.py +1 -32
  132. pip/_vendor/dependency_groups/_implementation.py +7 -11
  133. pip/_vendor/distlib/__init__.py +2 -2
  134. pip/_vendor/distlib/scripts.py +1 -1
  135. pip/_vendor/msgpack/__init__.py +2 -2
  136. pip/_vendor/pkg_resources/__init__.py +1 -1
  137. pip/_vendor/platformdirs/version.py +2 -2
  138. pip/_vendor/pygments/__init__.py +1 -1
  139. pip/_vendor/requests/__version__.py +2 -2
  140. pip/_vendor/requests/compat.py +12 -0
  141. pip/_vendor/requests/models.py +3 -1
  142. pip/_vendor/requests/utils.py +6 -16
  143. pip/_vendor/resolvelib/__init__.py +3 -3
  144. pip/_vendor/resolvelib/reporters.py +1 -1
  145. pip/_vendor/resolvelib/resolvers/__init__.py +4 -4
  146. pip/_vendor/resolvelib/resolvers/resolution.py +91 -10
  147. pip/_vendor/rich/__main__.py +12 -40
  148. pip/_vendor/rich/_inspect.py +1 -1
  149. pip/_vendor/rich/_ratio.py +1 -7
  150. pip/_vendor/rich/align.py +1 -7
  151. pip/_vendor/rich/box.py +1 -7
  152. pip/_vendor/rich/console.py +25 -20
  153. pip/_vendor/rich/control.py +1 -7
  154. pip/_vendor/rich/diagnose.py +1 -0
  155. pip/_vendor/rich/emoji.py +1 -6
  156. pip/_vendor/rich/live.py +32 -7
  157. pip/_vendor/rich/live_render.py +1 -7
  158. pip/_vendor/rich/logging.py +1 -1
  159. pip/_vendor/rich/panel.py +3 -4
  160. pip/_vendor/rich/progress.py +15 -15
  161. pip/_vendor/rich/spinner.py +7 -13
  162. pip/_vendor/rich/syntax.py +24 -5
  163. pip/_vendor/rich/traceback.py +32 -17
  164. pip/_vendor/truststore/_api.py +1 -1
  165. pip/_vendor/vendor.txt +10 -11
  166. {pip-25.1.dist-info → pip-25.2.dist-info}/METADATA +26 -4
  167. {pip-25.1.dist-info → pip-25.2.dist-info}/RECORD +194 -181
  168. {pip-25.1.dist-info → pip-25.2.dist-info}/WHEEL +1 -1
  169. {pip-25.1.dist-info → pip-25.2.dist-info}/licenses/AUTHORS.txt +12 -0
  170. pip-25.2.dist-info/licenses/src/pip/_vendor/cachecontrol/LICENSE.txt +13 -0
  171. pip-25.2.dist-info/licenses/src/pip/_vendor/certifi/LICENSE +20 -0
  172. pip-25.2.dist-info/licenses/src/pip/_vendor/dependency_groups/LICENSE.txt +9 -0
  173. pip-25.2.dist-info/licenses/src/pip/_vendor/distlib/LICENSE.txt +284 -0
  174. pip-25.2.dist-info/licenses/src/pip/_vendor/distro/LICENSE +202 -0
  175. pip-25.2.dist-info/licenses/src/pip/_vendor/idna/LICENSE.md +31 -0
  176. pip-25.2.dist-info/licenses/src/pip/_vendor/msgpack/COPYING +14 -0
  177. pip-25.2.dist-info/licenses/src/pip/_vendor/packaging/LICENSE +3 -0
  178. pip-25.2.dist-info/licenses/src/pip/_vendor/packaging/LICENSE.APACHE +177 -0
  179. pip-25.2.dist-info/licenses/src/pip/_vendor/packaging/LICENSE.BSD +23 -0
  180. pip-25.2.dist-info/licenses/src/pip/_vendor/pkg_resources/LICENSE +17 -0
  181. pip-25.2.dist-info/licenses/src/pip/_vendor/platformdirs/LICENSE +21 -0
  182. pip-25.2.dist-info/licenses/src/pip/_vendor/pygments/LICENSE +25 -0
  183. pip-25.2.dist-info/licenses/src/pip/_vendor/pyproject_hooks/LICENSE +21 -0
  184. pip-25.2.dist-info/licenses/src/pip/_vendor/requests/LICENSE +175 -0
  185. pip-25.2.dist-info/licenses/src/pip/_vendor/resolvelib/LICENSE +13 -0
  186. pip-25.2.dist-info/licenses/src/pip/_vendor/rich/LICENSE +19 -0
  187. pip-25.2.dist-info/licenses/src/pip/_vendor/tomli/LICENSE +21 -0
  188. pip-25.2.dist-info/licenses/src/pip/_vendor/tomli/LICENSE-HEADER +3 -0
  189. pip-25.2.dist-info/licenses/src/pip/_vendor/tomli_w/LICENSE +21 -0
  190. pip-25.2.dist-info/licenses/src/pip/_vendor/truststore/LICENSE +21 -0
  191. pip-25.2.dist-info/licenses/src/pip/_vendor/urllib3/LICENSE.txt +21 -0
  192. pip/_vendor/distlib/database.py +0 -1329
  193. pip/_vendor/distlib/index.py +0 -508
  194. pip/_vendor/distlib/locators.py +0 -1295
  195. pip/_vendor/distlib/manifest.py +0 -384
  196. pip/_vendor/distlib/markers.py +0 -162
  197. pip/_vendor/distlib/metadata.py +0 -1031
  198. pip/_vendor/distlib/version.py +0 -750
  199. pip/_vendor/distlib/wheel.py +0 -1100
  200. pip/_vendor/typing_extensions.py +0 -4584
  201. {pip-25.1.dist-info → pip-25.2.dist-info}/entry_points.txt +0 -0
  202. {pip-25.1.dist-info → pip-25.2.dist-info}/licenses/LICENSE.txt +0 -0
  203. {pip-25.1.dist-info → pip-25.2.dist-info}/top_level.txt +0 -0
@@ -1,10 +1,13 @@
1
1
  """PEP 610"""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  import json
4
6
  import re
5
7
  import urllib.parse
8
+ from collections.abc import Iterable
6
9
  from dataclasses import dataclass
7
- from typing import Any, ClassVar, Dict, Iterable, Optional, Type, TypeVar, Union
10
+ from typing import Any, ClassVar, TypeVar, Union
8
11
 
9
12
  __all__ = [
10
13
  "DirectUrl",
@@ -25,8 +28,8 @@ class DirectUrlValidationError(Exception):
25
28
 
26
29
 
27
30
  def _get(
28
- d: Dict[str, Any], expected_type: Type[T], key: str, default: Optional[T] = None
29
- ) -> Optional[T]:
31
+ d: dict[str, Any], expected_type: type[T], key: str, default: T | None = None
32
+ ) -> T | None:
30
33
  """Get value from dictionary and verify expected type."""
31
34
  if key not in d:
32
35
  return default
@@ -39,7 +42,7 @@ def _get(
39
42
 
40
43
 
41
44
  def _get_required(
42
- d: Dict[str, Any], expected_type: Type[T], key: str, default: Optional[T] = None
45
+ d: dict[str, Any], expected_type: type[T], key: str, default: T | None = None
43
46
  ) -> T:
44
47
  value = _get(d, expected_type, key, default)
45
48
  if value is None:
@@ -47,7 +50,7 @@ def _get_required(
47
50
  return value
48
51
 
49
52
 
50
- def _exactly_one_of(infos: Iterable[Optional["InfoType"]]) -> "InfoType":
53
+ def _exactly_one_of(infos: Iterable[InfoType | None]) -> InfoType:
51
54
  infos = [info for info in infos if info is not None]
52
55
  if not infos:
53
56
  raise DirectUrlValidationError(
@@ -61,7 +64,7 @@ def _exactly_one_of(infos: Iterable[Optional["InfoType"]]) -> "InfoType":
61
64
  return infos[0]
62
65
 
63
66
 
64
- def _filter_none(**kwargs: Any) -> Dict[str, Any]:
67
+ def _filter_none(**kwargs: Any) -> dict[str, Any]:
65
68
  """Make dict excluding None values."""
66
69
  return {k: v for k, v in kwargs.items() if v is not None}
67
70
 
@@ -72,10 +75,10 @@ class VcsInfo:
72
75
 
73
76
  vcs: str
74
77
  commit_id: str
75
- requested_revision: Optional[str] = None
78
+ requested_revision: str | None = None
76
79
 
77
80
  @classmethod
78
- def _from_dict(cls, d: Optional[Dict[str, Any]]) -> Optional["VcsInfo"]:
81
+ def _from_dict(cls, d: dict[str, Any] | None) -> VcsInfo | None:
79
82
  if d is None:
80
83
  return None
81
84
  return cls(
@@ -84,7 +87,7 @@ class VcsInfo:
84
87
  requested_revision=_get(d, str, "requested_revision"),
85
88
  )
86
89
 
87
- def _to_dict(self) -> Dict[str, Any]:
90
+ def _to_dict(self) -> dict[str, Any]:
88
91
  return _filter_none(
89
92
  vcs=self.vcs,
90
93
  requested_revision=self.requested_revision,
@@ -97,19 +100,19 @@ class ArchiveInfo:
97
100
 
98
101
  def __init__(
99
102
  self,
100
- hash: Optional[str] = None,
101
- hashes: Optional[Dict[str, str]] = None,
103
+ hash: str | None = None,
104
+ hashes: dict[str, str] | None = None,
102
105
  ) -> None:
103
106
  # set hashes before hash, since the hash setter will further populate hashes
104
107
  self.hashes = hashes
105
108
  self.hash = hash
106
109
 
107
110
  @property
108
- def hash(self) -> Optional[str]:
111
+ def hash(self) -> str | None:
109
112
  return self._hash
110
113
 
111
114
  @hash.setter
112
- def hash(self, value: Optional[str]) -> None:
115
+ def hash(self, value: str | None) -> None:
113
116
  if value is not None:
114
117
  # Auto-populate the hashes key to upgrade to the new format automatically.
115
118
  # We don't back-populate the legacy hash key from hashes.
@@ -127,12 +130,12 @@ class ArchiveInfo:
127
130
  self._hash = value
128
131
 
129
132
  @classmethod
130
- def _from_dict(cls, d: Optional[Dict[str, Any]]) -> Optional["ArchiveInfo"]:
133
+ def _from_dict(cls, d: dict[str, Any] | None) -> ArchiveInfo | None:
131
134
  if d is None:
132
135
  return None
133
136
  return cls(hash=_get(d, str, "hash"), hashes=_get(d, dict, "hashes"))
134
137
 
135
- def _to_dict(self) -> Dict[str, Any]:
138
+ def _to_dict(self) -> dict[str, Any]:
136
139
  return _filter_none(hash=self.hash, hashes=self.hashes)
137
140
 
138
141
 
@@ -143,12 +146,12 @@ class DirInfo:
143
146
  editable: bool = False
144
147
 
145
148
  @classmethod
146
- def _from_dict(cls, d: Optional[Dict[str, Any]]) -> Optional["DirInfo"]:
149
+ def _from_dict(cls, d: dict[str, Any] | None) -> DirInfo | None:
147
150
  if d is None:
148
151
  return None
149
152
  return cls(editable=_get_required(d, bool, "editable", default=False))
150
153
 
151
- def _to_dict(self) -> Dict[str, Any]:
154
+ def _to_dict(self) -> dict[str, Any]:
152
155
  return _filter_none(editable=self.editable or None)
153
156
 
154
157
 
@@ -159,7 +162,7 @@ InfoType = Union[ArchiveInfo, DirInfo, VcsInfo]
159
162
  class DirectUrl:
160
163
  url: str
161
164
  info: InfoType
162
- subdirectory: Optional[str] = None
165
+ subdirectory: str | None = None
163
166
 
164
167
  def _remove_auth_from_netloc(self, netloc: str) -> str:
165
168
  if "@" not in netloc:
@@ -192,7 +195,7 @@ class DirectUrl:
192
195
  self.from_dict(self.to_dict())
193
196
 
194
197
  @classmethod
195
- def from_dict(cls, d: Dict[str, Any]) -> "DirectUrl":
198
+ def from_dict(cls, d: dict[str, Any]) -> DirectUrl:
196
199
  return DirectUrl(
197
200
  url=_get_required(d, str, "url"),
198
201
  subdirectory=_get(d, str, "subdirectory"),
@@ -205,7 +208,7 @@ class DirectUrl:
205
208
  ),
206
209
  )
207
210
 
208
- def to_dict(self) -> Dict[str, Any]:
211
+ def to_dict(self) -> dict[str, Any]:
209
212
  res = _filter_none(
210
213
  url=self.redacted_url,
211
214
  subdirectory=self.subdirectory,
@@ -214,7 +217,7 @@ class DirectUrl:
214
217
  return res
215
218
 
216
219
  @classmethod
217
- def from_json(cls, s: str) -> "DirectUrl":
220
+ def from_json(cls, s: str) -> DirectUrl:
218
221
  return cls.from_dict(json.loads(s))
219
222
 
220
223
  def to_json(self) -> str:
@@ -1,4 +1,4 @@
1
- from typing import FrozenSet, Optional, Set
1
+ from __future__ import annotations
2
2
 
3
3
  from pip._vendor.packaging.utils import canonicalize_name
4
4
 
@@ -12,8 +12,8 @@ class FormatControl:
12
12
 
13
13
  def __init__(
14
14
  self,
15
- no_binary: Optional[Set[str]] = None,
16
- only_binary: Optional[Set[str]] = None,
15
+ no_binary: set[str] | None = None,
16
+ only_binary: set[str] | None = None,
17
17
  ) -> None:
18
18
  if no_binary is None:
19
19
  no_binary = set()
@@ -36,7 +36,7 @@ class FormatControl:
36
36
  return f"{self.__class__.__name__}({self.no_binary}, {self.only_binary})"
37
37
 
38
38
  @staticmethod
39
- def handle_mutual_excludes(value: str, target: Set[str], other: Set[str]) -> None:
39
+ def handle_mutual_excludes(value: str, target: set[str], other: set[str]) -> None:
40
40
  if value.startswith("-"):
41
41
  raise CommandError(
42
42
  "--no-binary / --only-binary option requires 1 argument."
@@ -58,7 +58,7 @@ class FormatControl:
58
58
  other.discard(name)
59
59
  target.add(name)
60
60
 
61
- def get_allowed_formats(self, canonical_name: str) -> FrozenSet[str]:
61
+ def get_allowed_formats(self, canonical_name: str) -> frozenset[str]:
62
62
  result = {"binary", "source"}
63
63
  if canonical_name in self.only_binary:
64
64
  result.discard("source")
@@ -1,4 +1,5 @@
1
- from typing import Any, Dict, Sequence
1
+ from collections.abc import Sequence
2
+ from typing import Any
2
3
 
3
4
  from pip._vendor.packaging.markers import default_environment
4
5
 
@@ -11,7 +12,7 @@ class InstallationReport:
11
12
  self._install_requirements = install_requirements
12
13
 
13
14
  @classmethod
14
- def _install_req_to_dict(cls, ireq: InstallRequirement) -> Dict[str, Any]:
15
+ def _install_req_to_dict(cls, ireq: InstallRequirement) -> dict[str, Any]:
15
16
  assert ireq.download_info, f"No download_info for {ireq}"
16
17
  res = {
17
18
  # PEP 610 json for the download URL. download_info.archive_info.hashes may
@@ -39,7 +40,7 @@ class InstallationReport:
39
40
  res["requested_extras"] = sorted(ireq.extras)
40
41
  return res
41
42
 
42
- def to_dict(self) -> Dict[str, Any]:
43
+ def to_dict(self) -> dict[str, Any]:
43
44
  return {
44
45
  "version": "1",
45
46
  "pip_version": __version__,
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import functools
2
4
  import itertools
3
5
  import logging
@@ -5,17 +7,12 @@ import os
5
7
  import posixpath
6
8
  import re
7
9
  import urllib.parse
10
+ from collections.abc import Mapping
8
11
  from dataclasses import dataclass
9
12
  from typing import (
10
13
  TYPE_CHECKING,
11
14
  Any,
12
- Dict,
13
- List,
14
- Mapping,
15
15
  NamedTuple,
16
- Optional,
17
- Tuple,
18
- Union,
19
16
  )
20
17
 
21
18
  from pip._internal.utils.deprecation import deprecated
@@ -69,8 +66,8 @@ class LinkHash:
69
66
  assert self.name in _SUPPORTED_HASHES
70
67
 
71
68
  @classmethod
72
- @functools.lru_cache(maxsize=None)
73
- def find_hash_url_fragment(cls, url: str) -> Optional["LinkHash"]:
69
+ @functools.cache
70
+ def find_hash_url_fragment(cls, url: str) -> LinkHash | None:
74
71
  """Search a string for a checksum algorithm name and encoded output value."""
75
72
  match = cls._hash_url_fragment_re.search(url)
76
73
  if match is None:
@@ -78,14 +75,14 @@ class LinkHash:
78
75
  name, value = match.groups()
79
76
  return cls(name=name, value=value)
80
77
 
81
- def as_dict(self) -> Dict[str, str]:
78
+ def as_dict(self) -> dict[str, str]:
82
79
  return {self.name: self.value}
83
80
 
84
81
  def as_hashes(self) -> Hashes:
85
82
  """Return a Hashes instance which checks only for the current hash."""
86
83
  return Hashes({self.name: [self.value]})
87
84
 
88
- def is_hash_allowed(self, hashes: Optional[Hashes]) -> bool:
85
+ def is_hash_allowed(self, hashes: Hashes | None) -> bool:
89
86
  """
90
87
  Return True if the current hash is allowed by `hashes`.
91
88
  """
@@ -98,14 +95,14 @@ class LinkHash:
98
95
  class MetadataFile:
99
96
  """Information about a core metadata file associated with a distribution."""
100
97
 
101
- hashes: Optional[Dict[str, str]]
98
+ hashes: dict[str, str] | None
102
99
 
103
100
  def __post_init__(self) -> None:
104
101
  if self.hashes is not None:
105
102
  assert all(name in _SUPPORTED_HASHES for name in self.hashes)
106
103
 
107
104
 
108
- def supported_hashes(hashes: Optional[Dict[str, str]]) -> Optional[Dict[str, str]]:
105
+ def supported_hashes(hashes: dict[str, str] | None) -> dict[str, str] | None:
109
106
  # Remove any unsupported hash types from the mapping. If this leaves no
110
107
  # supported hashes, return None
111
108
  if hashes is None:
@@ -134,7 +131,11 @@ def _clean_file_url_path(part: str) -> str:
134
131
  # should not be quoted. On Linux where drive letters do not
135
132
  # exist, the colon should be quoted. We rely on urllib.request
136
133
  # to do the right thing here.
137
- return urllib.request.pathname2url(urllib.request.url2pathname(part))
134
+ ret = urllib.request.pathname2url(urllib.request.url2pathname(part))
135
+ if ret.startswith("///"):
136
+ # Remove any URL authority section, leaving only the URL path.
137
+ ret = ret.removeprefix("//")
138
+ return ret
138
139
 
139
140
 
140
141
  # percent-encoded: /
@@ -175,7 +176,11 @@ def _ensure_quoted_url(url: str) -> str:
175
176
  # If the netloc is empty, then the URL refers to a local filesystem path.
176
177
  is_local_path = not result.netloc
177
178
  path = _clean_url_path(result.path, is_local_path=is_local_path)
178
- return urllib.parse.urlunsplit(result._replace(path=path))
179
+ # Temporarily replace scheme with file to ensure the URL generated by
180
+ # urlunsplit() contains an empty netloc (file://) as per RFC 1738.
181
+ ret = urllib.parse.urlunsplit(result._replace(scheme="file", path=path))
182
+ ret = result.scheme + ret[4:] # Restore original scheme.
183
+ return ret
179
184
 
180
185
 
181
186
  def _absolute_link_url(base_url: str, url: str) -> str:
@@ -209,12 +214,12 @@ class Link:
209
214
  def __init__(
210
215
  self,
211
216
  url: str,
212
- comes_from: Optional[Union[str, "IndexContent"]] = None,
213
- requires_python: Optional[str] = None,
214
- yanked_reason: Optional[str] = None,
215
- metadata_file_data: Optional[MetadataFile] = None,
217
+ comes_from: str | IndexContent | None = None,
218
+ requires_python: str | None = None,
219
+ yanked_reason: str | None = None,
220
+ metadata_file_data: MetadataFile | None = None,
216
221
  cache_link_parsing: bool = True,
217
- hashes: Optional[Mapping[str, str]] = None,
222
+ hashes: Mapping[str, str] | None = None,
218
223
  ) -> None:
219
224
  """
220
225
  :param url: url of the resource pointed to (href of the link)
@@ -274,9 +279,9 @@ class Link:
274
279
  @classmethod
275
280
  def from_json(
276
281
  cls,
277
- file_data: Dict[str, Any],
282
+ file_data: dict[str, Any],
278
283
  page_url: str,
279
- ) -> Optional["Link"]:
284
+ ) -> Link | None:
280
285
  """
281
286
  Convert an pypi json document from a simple repository page into a Link.
282
287
  """
@@ -325,10 +330,10 @@ class Link:
325
330
  @classmethod
326
331
  def from_element(
327
332
  cls,
328
- anchor_attribs: Dict[str, Optional[str]],
333
+ anchor_attribs: dict[str, str | None],
329
334
  page_url: str,
330
335
  base_url: str,
331
- ) -> Optional["Link"]:
336
+ ) -> Link | None:
332
337
  """
333
338
  Convert an anchor element's attributes in a simple repository page to a Link.
334
339
  """
@@ -441,7 +446,7 @@ class Link:
441
446
  def path(self) -> str:
442
447
  return self._path
443
448
 
444
- def splitext(self) -> Tuple[str, str]:
449
+ def splitext(self) -> tuple[str, str]:
445
450
  return splitext(posixpath.basename(self.path.rstrip("/")))
446
451
 
447
452
  @property
@@ -460,7 +465,7 @@ class Link:
460
465
  r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$", re.IGNORECASE
461
466
  )
462
467
 
463
- def _egg_fragment(self) -> Optional[str]:
468
+ def _egg_fragment(self) -> str | None:
464
469
  match = self._egg_fragment_re.search(self._url)
465
470
  if not match:
466
471
  return None
@@ -472,7 +477,7 @@ class Link:
472
477
  deprecated(
473
478
  reason=f"{self} contains an egg fragment with a non-PEP 508 name.",
474
479
  replacement="to use the req @ url syntax, and remove the egg fragment",
475
- gone_in="25.2",
480
+ gone_in="25.3",
476
481
  issue=13157,
477
482
  )
478
483
 
@@ -481,13 +486,13 @@ class Link:
481
486
  _subdirectory_fragment_re = re.compile(r"[#&]subdirectory=([^&]*)")
482
487
 
483
488
  @property
484
- def subdirectory_fragment(self) -> Optional[str]:
489
+ def subdirectory_fragment(self) -> str | None:
485
490
  match = self._subdirectory_fragment_re.search(self._url)
486
491
  if not match:
487
492
  return None
488
493
  return match.group(1)
489
494
 
490
- def metadata_link(self) -> Optional["Link"]:
495
+ def metadata_link(self) -> Link | None:
491
496
  """Return a link to the associated core metadata file (if any)."""
492
497
  if self.metadata_file_data is None:
493
498
  return None
@@ -500,11 +505,11 @@ class Link:
500
505
  return Hashes({k: [v] for k, v in self._hashes.items()})
501
506
 
502
507
  @property
503
- def hash(self) -> Optional[str]:
508
+ def hash(self) -> str | None:
504
509
  return next(iter(self._hashes.values()), None)
505
510
 
506
511
  @property
507
- def hash_name(self) -> Optional[str]:
512
+ def hash_name(self) -> str | None:
508
513
  return next(iter(self._hashes), None)
509
514
 
510
515
  @property
@@ -536,7 +541,7 @@ class Link:
536
541
  def has_hash(self) -> bool:
537
542
  return bool(self._hashes)
538
543
 
539
- def is_hash_allowed(self, hashes: Optional[Hashes]) -> bool:
544
+ def is_hash_allowed(self, hashes: Hashes | None) -> bool:
540
545
  """
541
546
  Return True if the link has a hash and it is allowed by `hashes`.
542
547
  """
@@ -572,9 +577,9 @@ class _CleanResult(NamedTuple):
572
577
  """
573
578
 
574
579
  parsed: urllib.parse.SplitResult
575
- query: Dict[str, List[str]]
580
+ query: dict[str, list[str]]
576
581
  subdirectory: str
577
- hashes: Dict[str, str]
582
+ hashes: dict[str, str]
578
583
 
579
584
 
580
585
  def _clean_link(link: Link) -> _CleanResult:
@@ -603,6 +608,6 @@ def _clean_link(link: Link) -> _CleanResult:
603
608
  )
604
609
 
605
610
 
606
- @functools.lru_cache(maxsize=None)
611
+ @functools.cache
607
612
  def links_equivalent(link1: Link, link2: Link) -> bool:
608
613
  return _clean_link(link1) == _clean_link(link2)
@@ -1,17 +1,22 @@
1
+ from __future__ import annotations
2
+
1
3
  import dataclasses
2
4
  import re
5
+ from collections.abc import Iterable
3
6
  from dataclasses import dataclass
4
7
  from pathlib import Path
5
- from typing import Any, Dict, Iterable, List, Optional, Tuple
8
+ from typing import TYPE_CHECKING, Any
6
9
 
7
10
  from pip._vendor import tomli_w
8
- from pip._vendor.typing_extensions import Self
9
11
 
10
12
  from pip._internal.models.direct_url import ArchiveInfo, DirInfo, VcsInfo
11
13
  from pip._internal.models.link import Link
12
14
  from pip._internal.req.req_install import InstallRequirement
13
15
  from pip._internal.utils.urls import url_to_path
14
16
 
17
+ if TYPE_CHECKING:
18
+ from typing_extensions import Self
19
+
15
20
  PYLOCK_FILE_NAME_RE = re.compile(r"^pylock\.([^.]+)\.toml$")
16
21
 
17
22
 
@@ -19,70 +24,70 @@ def is_valid_pylock_file_name(path: Path) -> bool:
19
24
  return path.name == "pylock.toml" or bool(re.match(PYLOCK_FILE_NAME_RE, path.name))
20
25
 
21
26
 
22
- def _toml_dict_factory(data: List[Tuple[str, Any]]) -> Dict[str, Any]:
27
+ def _toml_dict_factory(data: list[tuple[str, Any]]) -> dict[str, Any]:
23
28
  return {key.replace("_", "-"): value for key, value in data if value is not None}
24
29
 
25
30
 
26
31
  @dataclass
27
32
  class PackageVcs:
28
33
  type: str
29
- url: Optional[str]
34
+ url: str | None
30
35
  # (not supported) path: Optional[str]
31
- requested_revision: Optional[str]
36
+ requested_revision: str | None
32
37
  commit_id: str
33
- subdirectory: Optional[str]
38
+ subdirectory: str | None
34
39
 
35
40
 
36
41
  @dataclass
37
42
  class PackageDirectory:
38
43
  path: str
39
- editable: Optional[bool]
40
- subdirectory: Optional[str]
44
+ editable: bool | None
45
+ subdirectory: str | None
41
46
 
42
47
 
43
48
  @dataclass
44
49
  class PackageArchive:
45
- url: Optional[str]
50
+ url: str | None
46
51
  # (not supported) path: Optional[str]
47
52
  # (not supported) size: Optional[int]
48
53
  # (not supported) upload_time: Optional[datetime]
49
- hashes: Dict[str, str]
50
- subdirectory: Optional[str]
54
+ hashes: dict[str, str]
55
+ subdirectory: str | None
51
56
 
52
57
 
53
58
  @dataclass
54
59
  class PackageSdist:
55
60
  name: str
56
61
  # (not supported) upload_time: Optional[datetime]
57
- url: Optional[str]
62
+ url: str | None
58
63
  # (not supported) path: Optional[str]
59
64
  # (not supported) size: Optional[int]
60
- hashes: Dict[str, str]
65
+ hashes: dict[str, str]
61
66
 
62
67
 
63
68
  @dataclass
64
69
  class PackageWheel:
65
70
  name: str
66
71
  # (not supported) upload_time: Optional[datetime]
67
- url: Optional[str]
72
+ url: str | None
68
73
  # (not supported) path: Optional[str]
69
74
  # (not supported) size: Optional[int]
70
- hashes: Dict[str, str]
75
+ hashes: dict[str, str]
71
76
 
72
77
 
73
78
  @dataclass
74
79
  class Package:
75
80
  name: str
76
- version: Optional[str] = None
81
+ version: str | None = None
77
82
  # (not supported) marker: Optional[str]
78
83
  # (not supported) requires_python: Optional[str]
79
84
  # (not supported) dependencies
80
- vcs: Optional[PackageVcs] = None
81
- directory: Optional[PackageDirectory] = None
82
- archive: Optional[PackageArchive] = None
85
+ vcs: PackageVcs | None = None
86
+ directory: PackageDirectory | None = None
87
+ archive: PackageArchive | None = None
83
88
  # (not supported) index: Optional[str]
84
- sdist: Optional[PackageSdist] = None
85
- wheels: Optional[List[PackageWheel]] = None
89
+ sdist: PackageSdist | None = None
90
+ wheels: list[PackageWheel] | None = None
86
91
  # (not supported) attestation_identities: Optional[List[Dict[str, Any]]]
87
92
  # (not supported) tool: Optional[Dict[str, Any]]
88
93
 
@@ -162,7 +167,7 @@ class Pylock:
162
167
  # (not supported) extras: List[str] = []
163
168
  # (not supported) dependency_groups: List[str] = []
164
169
  created_by: str = "pip"
165
- packages: List[Package] = dataclasses.field(default_factory=list)
170
+ packages: list[Package] = dataclasses.field(default_factory=list)
166
171
  # (not supported) tool: Optional[Dict[str, Any]]
167
172
 
168
173
  def as_toml(self) -> str:
@@ -4,7 +4,6 @@ import os
4
4
  import posixpath
5
5
  import urllib.parse
6
6
  from dataclasses import dataclass
7
- from typing import List
8
7
 
9
8
  from pip._vendor.packaging.utils import canonicalize_name
10
9
 
@@ -23,15 +22,15 @@ class SearchScope:
23
22
 
24
23
  __slots__ = ["find_links", "index_urls", "no_index"]
25
24
 
26
- find_links: List[str]
27
- index_urls: List[str]
25
+ find_links: list[str]
26
+ index_urls: list[str]
28
27
  no_index: bool
29
28
 
30
29
  @classmethod
31
30
  def create(
32
31
  cls,
33
- find_links: List[str],
34
- index_urls: List[str],
32
+ find_links: list[str],
33
+ index_urls: list[str],
35
34
  no_index: bool,
36
35
  ) -> "SearchScope":
37
36
  """
@@ -42,7 +41,7 @@ class SearchScope:
42
41
  # it and if it exists, use the normalized version.
43
42
  # This is deliberately conservative - it might be fine just to
44
43
  # blindly normalize anything starting with a ~...
45
- built_find_links: List[str] = []
44
+ built_find_links: list[str] = []
46
45
  for link in find_links:
47
46
  if link.startswith("~"):
48
47
  new_link = normalize_path(link)
@@ -104,7 +103,7 @@ class SearchScope:
104
103
  )
105
104
  return "\n".join(lines)
106
105
 
107
- def get_index_urls_locations(self, project_name: str) -> List[str]:
106
+ def get_index_urls_locations(self, project_name: str) -> list[str]:
108
107
  """Returns the locations found via self.index_urls
109
108
 
110
109
  Checks the url_name on the main (first in the list) index and
@@ -1,4 +1,4 @@
1
- from typing import Optional
1
+ from __future__ import annotations
2
2
 
3
3
  from pip._internal.models.format_control import FormatControl
4
4
 
@@ -27,9 +27,9 @@ class SelectionPreferences:
27
27
  self,
28
28
  allow_yanked: bool,
29
29
  allow_all_prereleases: bool = False,
30
- format_control: Optional[FormatControl] = None,
30
+ format_control: FormatControl | None = None,
31
31
  prefer_binary: bool = False,
32
- ignore_requires_python: Optional[bool] = None,
32
+ ignore_requires_python: bool | None = None,
33
33
  ) -> None:
34
34
  """Create a SelectionPreferences object.
35
35
 
@@ -1,5 +1,6 @@
1
+ from __future__ import annotations
2
+
1
3
  import sys
2
- from typing import List, Optional, Set, Tuple
3
4
 
4
5
  from pip._vendor.packaging.tags import Tag
5
6
 
@@ -26,10 +27,10 @@ class TargetPython:
26
27
 
27
28
  def __init__(
28
29
  self,
29
- platforms: Optional[List[str]] = None,
30
- py_version_info: Optional[Tuple[int, ...]] = None,
31
- abis: Optional[List[str]] = None,
32
- implementation: Optional[str] = None,
30
+ platforms: list[str] | None = None,
31
+ py_version_info: tuple[int, ...] | None = None,
32
+ abis: list[str] | None = None,
33
+ implementation: str | None = None,
33
34
  ) -> None:
34
35
  """
35
36
  :param platforms: A list of strings or None. If None, searches for
@@ -62,8 +63,8 @@ class TargetPython:
62
63
  self.py_version_info = py_version_info
63
64
 
64
65
  # This is used to cache the return value of get_(un)sorted_tags.
65
- self._valid_tags: Optional[List[Tag]] = None
66
- self._valid_tags_set: Optional[Set[Tag]] = None
66
+ self._valid_tags: list[Tag] | None = None
67
+ self._valid_tags_set: set[Tag] | None = None
67
68
 
68
69
  def format_given(self) -> str:
69
70
  """
@@ -85,7 +86,7 @@ class TargetPython:
85
86
  f"{key}={value!r}" for key, value in key_values if value is not None
86
87
  )
87
88
 
88
- def get_sorted_tags(self) -> List[Tag]:
89
+ def get_sorted_tags(self) -> list[Tag]:
89
90
  """
90
91
  Return the supported PEP 425 tags to check wheel candidates against.
91
92
 
@@ -110,7 +111,7 @@ class TargetPython:
110
111
 
111
112
  return self._valid_tags
112
113
 
113
- def get_unsorted_tags(self) -> Set[Tag]:
114
+ def get_unsorted_tags(self) -> set[Tag]:
114
115
  """Exactly the same as get_sorted_tags, but returns a set.
115
116
 
116
117
  This is important for performance.