pip 24.3__py3-none-any.whl → 25.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 (110) hide show
  1. pip/__init__.py +1 -1
  2. pip/_internal/build_env.py +6 -2
  3. pip/_internal/cli/base_command.py +9 -0
  4. pip/_internal/cli/cmdoptions.py +2 -2
  5. pip/_internal/cli/index_command.py +1 -0
  6. pip/_internal/cli/progress_bars.py +1 -1
  7. pip/_internal/commands/cache.py +4 -1
  8. pip/_internal/commands/install.py +8 -7
  9. pip/_internal/commands/show.py +8 -1
  10. pip/_internal/configuration.py +1 -1
  11. pip/_internal/index/package_finder.py +41 -32
  12. pip/_internal/metadata/__init__.py +2 -2
  13. pip/_internal/metadata/_json.py +2 -0
  14. pip/_internal/metadata/importlib/_dists.py +7 -1
  15. pip/_internal/models/link.py +23 -9
  16. pip/_internal/network/cache.py +12 -0
  17. pip/_internal/network/session.py +1 -0
  18. pip/_internal/operations/build/metadata_editable.py +1 -0
  19. pip/_internal/operations/freeze.py +11 -13
  20. pip/_internal/pyproject.py +1 -1
  21. pip/_internal/req/req_file.py +106 -52
  22. pip/_internal/req/req_install.py +2 -2
  23. pip/_internal/resolution/resolvelib/factory.py +1 -1
  24. pip/_internal/self_outdated_check.py +9 -1
  25. pip/_internal/utils/logging.py +8 -1
  26. pip/_internal/utils/misc.py +15 -14
  27. pip/_internal/utils/packaging.py +1 -0
  28. pip/_internal/utils/unpacking.py +1 -1
  29. pip/_vendor/cachecontrol/__init__.py +2 -1
  30. pip/_vendor/cachecontrol/adapter.py +2 -2
  31. pip/_vendor/cachecontrol/cache.py +1 -0
  32. pip/_vendor/cachecontrol/caches/file_cache.py +1 -1
  33. pip/_vendor/cachecontrol/controller.py +1 -0
  34. pip/_vendor/cachecontrol/filewrapper.py +2 -2
  35. pip/_vendor/cachecontrol/heuristics.py +4 -1
  36. pip/_vendor/idna/__init__.py +2 -1
  37. pip/_vendor/idna/codec.py +31 -27
  38. pip/_vendor/idna/compat.py +6 -4
  39. pip/_vendor/idna/core.py +161 -119
  40. pip/_vendor/idna/idnadata.py +3537 -3539
  41. pip/_vendor/idna/intranges.py +7 -4
  42. pip/_vendor/idna/package_data.py +1 -2
  43. pip/_vendor/idna/uts46data.py +8261 -8178
  44. pip/_vendor/msgpack/__init__.py +8 -8
  45. pip/_vendor/msgpack/ext.py +5 -3
  46. pip/_vendor/msgpack/fallback.py +29 -51
  47. pip/_vendor/packaging/__init__.py +2 -2
  48. pip/_vendor/packaging/_elffile.py +4 -4
  49. pip/_vendor/packaging/_manylinux.py +1 -0
  50. pip/_vendor/packaging/licenses/__init__.py +145 -0
  51. pip/_vendor/packaging/licenses/_spdx.py +759 -0
  52. pip/_vendor/packaging/markers.py +15 -9
  53. pip/_vendor/packaging/metadata.py +83 -24
  54. pip/_vendor/packaging/specifiers.py +19 -8
  55. pip/_vendor/packaging/tags.py +15 -25
  56. pip/_vendor/packaging/utils.py +33 -44
  57. pip/_vendor/packaging/version.py +26 -7
  58. pip/_vendor/platformdirs/__init__.py +14 -10
  59. pip/_vendor/platformdirs/android.py +1 -1
  60. pip/_vendor/platformdirs/api.py +6 -0
  61. pip/_vendor/platformdirs/macos.py +14 -0
  62. pip/_vendor/platformdirs/unix.py +0 -6
  63. pip/_vendor/platformdirs/version.py +2 -2
  64. pip/_vendor/pyproject_hooks/__init__.py +17 -9
  65. pip/_vendor/pyproject_hooks/_impl.py +181 -101
  66. pip/_vendor/pyproject_hooks/_in_process/__init__.py +5 -2
  67. pip/_vendor/pyproject_hooks/_in_process/_in_process.py +113 -77
  68. pip/_vendor/pyproject_hooks/py.typed +0 -0
  69. pip/_vendor/requests/certs.py +1 -8
  70. pip/_vendor/rich/_inspect.py +0 -2
  71. pip/_vendor/rich/_null_file.py +1 -1
  72. pip/_vendor/rich/_win32_console.py +3 -4
  73. pip/_vendor/rich/align.py +1 -0
  74. pip/_vendor/rich/ansi.py +1 -0
  75. pip/_vendor/rich/cells.py +30 -23
  76. pip/_vendor/rich/color.py +2 -2
  77. pip/_vendor/rich/console.py +55 -27
  78. pip/_vendor/rich/default_styles.py +2 -1
  79. pip/_vendor/rich/filesize.py +1 -2
  80. pip/_vendor/rich/highlighter.py +1 -1
  81. pip/_vendor/rich/live.py +1 -1
  82. pip/_vendor/rich/logging.py +8 -0
  83. pip/_vendor/rich/padding.py +5 -5
  84. pip/_vendor/rich/panel.py +13 -7
  85. pip/_vendor/rich/pretty.py +46 -25
  86. pip/_vendor/rich/progress.py +25 -9
  87. pip/_vendor/rich/progress_bar.py +1 -1
  88. pip/_vendor/rich/prompt.py +29 -4
  89. pip/_vendor/rich/segment.py +33 -19
  90. pip/_vendor/rich/spinner.py +1 -0
  91. pip/_vendor/rich/style.py +1 -1
  92. pip/_vendor/rich/syntax.py +16 -8
  93. pip/_vendor/rich/table.py +15 -8
  94. pip/_vendor/rich/text.py +10 -6
  95. pip/_vendor/rich/theme.py +2 -2
  96. pip/_vendor/rich/traceback.py +70 -26
  97. pip/_vendor/rich/tree.py +16 -8
  98. pip/_vendor/tomli/__init__.py +1 -4
  99. pip/_vendor/tomli/_parser.py +158 -79
  100. pip/_vendor/tomli/_re.py +10 -5
  101. pip/_vendor/vendor.txt +8 -8
  102. {pip-24.3.dist-info → pip-25.0.dist-info}/AUTHORS.txt +7 -0
  103. {pip-24.3.dist-info → pip-25.0.dist-info}/METADATA +2 -2
  104. {pip-24.3.dist-info → pip-25.0.dist-info}/RECORD +108 -107
  105. {pip-24.3.dist-info → pip-25.0.dist-info}/WHEEL +1 -1
  106. pip/_internal/utils/encoding.py +0 -36
  107. pip/_vendor/pyproject_hooks/_compat.py +0 -8
  108. {pip-24.3.dist-info → pip-25.0.dist-info}/LICENSE.txt +0 -0
  109. {pip-24.3.dist-info → pip-25.0.dist-info}/entry_points.txt +0 -0
  110. {pip-24.3.dist-info → pip-25.0.dist-info}/top_level.txt +0 -0
pip/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from typing import List, Optional
2
2
 
3
- __version__ = "24.3"
3
+ __version__ = "25.0"
4
4
 
5
5
 
6
6
  def main(args: Optional[List[str]] = None) -> int:
@@ -246,6 +246,8 @@ class BuildEnvironment:
246
246
  # target from config file or env var should be ignored
247
247
  "--target",
248
248
  "",
249
+ "--cert",
250
+ finder.custom_cert or where(),
249
251
  ]
250
252
  if logger.getEffectiveLevel() <= logging.DEBUG:
251
253
  args.append("-vv")
@@ -270,21 +272,23 @@ class BuildEnvironment:
270
272
  for link in finder.find_links:
271
273
  args.extend(["--find-links", link])
272
274
 
275
+ if finder.proxy:
276
+ args.extend(["--proxy", finder.proxy])
273
277
  for host in finder.trusted_hosts:
274
278
  args.extend(["--trusted-host", host])
279
+ if finder.client_cert:
280
+ args.extend(["--client-cert", finder.client_cert])
275
281
  if finder.allow_all_prereleases:
276
282
  args.append("--pre")
277
283
  if finder.prefer_binary:
278
284
  args.append("--prefer-binary")
279
285
  args.append("--")
280
286
  args.extend(requirements)
281
- extra_environ = {"_PIP_STANDALONE_CERT": where()}
282
287
  with open_spinner(f"Installing {kind}") as spinner:
283
288
  call_subprocess(
284
289
  args,
285
290
  command_desc=f"pip subprocess to install {kind}",
286
291
  spinner=spinner,
287
- extra_environ=extra_environ,
288
292
  )
289
293
 
290
294
 
@@ -29,6 +29,7 @@ from pip._internal.exceptions import (
29
29
  NetworkConnectionError,
30
30
  PreviousBuildDirError,
31
31
  )
32
+ from pip._internal.utils.deprecation import deprecated
32
33
  from pip._internal.utils.filesystem import check_path_owner
33
34
  from pip._internal.utils.logging import BrokenStdoutLoggingError, setup_logging
34
35
  from pip._internal.utils.misc import get_prog, normalize_path
@@ -228,4 +229,12 @@ class Command(CommandContextMixIn):
228
229
  )
229
230
  options.cache_dir = None
230
231
 
232
+ if options.no_python_version_warning:
233
+ deprecated(
234
+ reason="--no-python-version-warning is deprecated.",
235
+ replacement="to remove the flag as it's a no-op",
236
+ gone_in="25.1",
237
+ issue=13154,
238
+ )
239
+
231
240
  return self._run_wrapper(level_number, options, args)
@@ -260,8 +260,8 @@ keyring_provider: Callable[..., Option] = partial(
260
260
  default="auto",
261
261
  help=(
262
262
  "Enable the credential lookup via the keyring library if user input is allowed."
263
- " Specify which mechanism to use [disabled, import, subprocess]."
264
- " (default: disabled)"
263
+ " Specify which mechanism to use [auto, disabled, import, subprocess]."
264
+ " (default: %default)"
265
265
  ),
266
266
  )
267
267
 
@@ -123,6 +123,7 @@ class SessionCommandMixin(CommandContextMixIn):
123
123
  "https": options.proxy,
124
124
  }
125
125
  session.trust_env = False
126
+ session.pip_proxy = options.proxy
126
127
 
127
128
  # Determine if we can prompt the user for authentication or not
128
129
  session.auth.prompting = not options.no_input
@@ -63,7 +63,7 @@ def _raw_progress_bar(
63
63
  size: Optional[int],
64
64
  ) -> Generator[bytes, None, None]:
65
65
  def write_progress(current: int, total: int) -> None:
66
- sys.stdout.write("Progress %d of %d\n" % (current, total))
66
+ sys.stdout.write(f"Progress {current} of {total}\n")
67
67
  sys.stdout.flush()
68
68
 
69
69
  current = 0
@@ -8,6 +8,7 @@ from pip._internal.cli.status_codes import ERROR, SUCCESS
8
8
  from pip._internal.exceptions import CommandError, PipError
9
9
  from pip._internal.utils import filesystem
10
10
  from pip._internal.utils.logging import getLogger
11
+ from pip._internal.utils.misc import format_size
11
12
 
12
13
  logger = getLogger(__name__)
13
14
 
@@ -180,10 +181,12 @@ class CacheCommand(Command):
180
181
  if not files:
181
182
  logger.warning(no_matching_msg)
182
183
 
184
+ bytes_removed = 0
183
185
  for filename in files:
186
+ bytes_removed += os.stat(filename).st_size
184
187
  os.unlink(filename)
185
188
  logger.verbose("Removed %s", filename)
186
- logger.info("Files removed: %s", len(files))
189
+ logger.info("Files removed: %s (%s)", len(files), format_size(bytes_removed))
187
190
 
188
191
  def purge_cache(self, options: Values, args: List[Any]) -> None:
189
192
  if args:
@@ -10,6 +10,13 @@ from typing import List, Optional
10
10
  from pip._vendor.packaging.utils import canonicalize_name
11
11
  from pip._vendor.rich import print_json
12
12
 
13
+ # Eagerly import self_outdated_check to avoid crashes. Otherwise,
14
+ # this module would be imported *after* pip was replaced, resulting
15
+ # in crashes if the new self_outdated_check module was incompatible
16
+ # with the rest of pip that's already imported, or allowing a
17
+ # wheel to execute arbitrary code on install by replacing
18
+ # self_outdated_check.
19
+ import pip._internal.self_outdated_check # noqa: F401
13
20
  from pip._internal.cache import WheelCache
14
21
  from pip._internal.cli import cmdoptions
15
22
  from pip._internal.cli.cmdoptions import make_target_python
@@ -408,12 +415,6 @@ class InstallCommand(RequirementCommand):
408
415
  # If we're not replacing an already installed pip,
409
416
  # we're not modifying it.
410
417
  modifying_pip = pip_req.satisfied_by is None
411
- if modifying_pip:
412
- # Eagerly import this module to avoid crashes. Otherwise, this
413
- # module would be imported *after* pip was replaced, resulting in
414
- # crashes if the new self_outdated_check module was incompatible
415
- # with the rest of pip that's already imported.
416
- import pip._internal.self_outdated_check # noqa: F401
417
418
  protect_pip_from_modification_on_windows(modifying_pip=modifying_pip)
418
419
 
419
420
  reqs_to_build = [
@@ -432,7 +433,7 @@ class InstallCommand(RequirementCommand):
432
433
 
433
434
  if build_failures:
434
435
  raise InstallationError(
435
- "ERROR: Failed to build installable wheels for some "
436
+ "Failed to build installable wheels for some "
436
437
  "pyproject.toml based projects ({})".format(
437
438
  ", ".join(r.name for r in build_failures) # type: ignore
438
439
  )
@@ -66,6 +66,7 @@ class _PackageInfo(NamedTuple):
66
66
  author: str
67
67
  author_email: str
68
68
  license: str
69
+ license_expression: str
69
70
  entry_points: List[str]
70
71
  files: Optional[List[str]]
71
72
 
@@ -161,6 +162,7 @@ def search_packages_info(query: List[str]) -> Generator[_PackageInfo, None, None
161
162
  author=metadata.get("Author", ""),
162
163
  author_email=metadata.get("Author-email", ""),
163
164
  license=metadata.get("License", ""),
165
+ license_expression=metadata.get("License-Expression", ""),
164
166
  entry_points=entry_points,
165
167
  files=files,
166
168
  )
@@ -180,13 +182,18 @@ def print_results(
180
182
  if i > 0:
181
183
  write_output("---")
182
184
 
185
+ metadata_version_tuple = tuple(map(int, dist.metadata_version.split(".")))
186
+
183
187
  write_output("Name: %s", dist.name)
184
188
  write_output("Version: %s", dist.version)
185
189
  write_output("Summary: %s", dist.summary)
186
190
  write_output("Home-page: %s", dist.homepage)
187
191
  write_output("Author: %s", dist.author)
188
192
  write_output("Author-email: %s", dist.author_email)
189
- write_output("License: %s", dist.license)
193
+ if metadata_version_tuple >= (2, 4) and dist.license_expression:
194
+ write_output("License-Expression: %s", dist.license_expression)
195
+ else:
196
+ write_output("License: %s", dist.license)
190
197
  write_output("Location: %s", dist.location)
191
198
  if dist.editable_project_location is not None:
192
199
  write_output(
@@ -330,7 +330,7 @@ class Configuration:
330
330
  This should be treated like items of a dictionary. The order
331
331
  here doesn't affect what gets overridden. That is controlled
332
332
  by OVERRIDE_ORDER. However this does control the order they are
333
- displayed to the user. It's probably most ergononmic to display
333
+ displayed to the user. It's probably most ergonomic to display
334
334
  things in the same order as OVERRIDE_ORDER
335
335
  """
336
336
  # SMELL: Move the conditions out of this function
@@ -334,44 +334,30 @@ class CandidatePreferences:
334
334
  allow_all_prereleases: bool = False
335
335
 
336
336
 
337
+ @dataclass(frozen=True)
337
338
  class BestCandidateResult:
338
339
  """A collection of candidates, returned by `PackageFinder.find_best_candidate`.
339
340
 
340
341
  This class is only intended to be instantiated by CandidateEvaluator's
341
342
  `compute_best_candidate()` method.
342
- """
343
-
344
- def __init__(
345
- self,
346
- candidates: List[InstallationCandidate],
347
- applicable_candidates: List[InstallationCandidate],
348
- best_candidate: Optional[InstallationCandidate],
349
- ) -> None:
350
- """
351
- :param candidates: A sequence of all available candidates found.
352
- :param applicable_candidates: The applicable candidates.
353
- :param best_candidate: The most preferred candidate found, or None
354
- if no applicable candidates were found.
355
- """
356
- assert set(applicable_candidates) <= set(candidates)
357
-
358
- if best_candidate is None:
359
- assert not applicable_candidates
360
- else:
361
- assert best_candidate in applicable_candidates
362
343
 
363
- self._applicable_candidates = applicable_candidates
364
- self._candidates = candidates
344
+ :param all_candidates: A sequence of all available candidates found.
345
+ :param applicable_candidates: The applicable candidates.
346
+ :param best_candidate: The most preferred candidate found, or None
347
+ if no applicable candidates were found.
348
+ """
365
349
 
366
- self.best_candidate = best_candidate
350
+ all_candidates: List[InstallationCandidate]
351
+ applicable_candidates: List[InstallationCandidate]
352
+ best_candidate: Optional[InstallationCandidate]
367
353
 
368
- def iter_all(self) -> Iterable[InstallationCandidate]:
369
- """Iterate through all candidates."""
370
- return iter(self._candidates)
354
+ def __post_init__(self) -> None:
355
+ assert set(self.applicable_candidates) <= set(self.all_candidates)
371
356
 
372
- def iter_applicable(self) -> Iterable[InstallationCandidate]:
373
- """Iterate through the applicable candidates."""
374
- return iter(self._applicable_candidates)
357
+ if self.best_candidate is None:
358
+ assert not self.applicable_candidates
359
+ else:
360
+ assert self.best_candidate in self.applicable_candidates
375
361
 
376
362
 
377
363
  class CandidateEvaluator:
@@ -675,11 +661,29 @@ class PackageFinder:
675
661
  def index_urls(self) -> List[str]:
676
662
  return self.search_scope.index_urls
677
663
 
664
+ @property
665
+ def proxy(self) -> Optional[str]:
666
+ return self._link_collector.session.pip_proxy
667
+
678
668
  @property
679
669
  def trusted_hosts(self) -> Iterable[str]:
680
670
  for host_port in self._link_collector.session.pip_trusted_origins:
681
671
  yield build_netloc(*host_port)
682
672
 
673
+ @property
674
+ def custom_cert(self) -> Optional[str]:
675
+ # session.verify is either a boolean (use default bundle/no SSL
676
+ # verification) or a string path to a custom CA bundle to use. We only
677
+ # care about the latter.
678
+ verify = self._link_collector.session.verify
679
+ return verify if isinstance(verify, str) else None
680
+
681
+ @property
682
+ def client_cert(self) -> Optional[str]:
683
+ cert = self._link_collector.session.cert
684
+ assert not isinstance(cert, tuple), "pip only supports PEM client certs"
685
+ return cert
686
+
683
687
  @property
684
688
  def allow_all_prereleases(self) -> bool:
685
689
  return self._candidate_prefs.allow_all_prereleases
@@ -732,6 +736,11 @@ class PackageFinder:
732
736
  return no_eggs + eggs
733
737
 
734
738
  def _log_skipped_link(self, link: Link, result: LinkType, detail: str) -> None:
739
+ # This is a hot method so don't waste time hashing links unless we're
740
+ # actually going to log 'em.
741
+ if not logger.isEnabledFor(logging.DEBUG):
742
+ return
743
+
735
744
  entry = (link, result, detail)
736
745
  if entry not in self._logged_links:
737
746
  # Put the link at the end so the reason is more visible and because
@@ -929,7 +938,7 @@ class PackageFinder:
929
938
  "Could not find a version that satisfies the requirement %s "
930
939
  "(from versions: %s)",
931
940
  req,
932
- _format_versions(best_candidate_result.iter_all()),
941
+ _format_versions(best_candidate_result.all_candidates),
933
942
  )
934
943
 
935
944
  raise DistributionNotFound(f"No matching distribution found for {req}")
@@ -963,7 +972,7 @@ class PackageFinder:
963
972
  logger.debug(
964
973
  "Using version %s (newest of versions: %s)",
965
974
  best_candidate.version,
966
- _format_versions(best_candidate_result.iter_applicable()),
975
+ _format_versions(best_candidate_result.applicable_candidates),
967
976
  )
968
977
  return best_candidate
969
978
 
@@ -971,7 +980,7 @@ class PackageFinder:
971
980
  logger.debug(
972
981
  "Installed version (%s) is most up-to-date (past versions: %s)",
973
982
  installed_version,
974
- _format_versions(best_candidate_result.iter_applicable()),
983
+ _format_versions(best_candidate_result.applicable_candidates),
975
984
  )
976
985
  raise BestVersionAlreadyInstalled
977
986
 
@@ -30,7 +30,7 @@ def _should_use_importlib_metadata() -> bool:
30
30
  """Whether to use the ``importlib.metadata`` or ``pkg_resources`` backend.
31
31
 
32
32
  By default, pip uses ``importlib.metadata`` on Python 3.11+, and
33
- ``pkg_resourcess`` otherwise. This can be overridden by a couple of ways:
33
+ ``pkg_resources`` otherwise. This can be overridden by a couple of ways:
34
34
 
35
35
  * If environment variable ``_PIP_USE_IMPORTLIB_METADATA`` is set, it
36
36
  dictates whether ``importlib.metadata`` is used, regardless of Python
@@ -71,7 +71,7 @@ def get_default_environment() -> BaseEnvironment:
71
71
 
72
72
  This returns an Environment instance from the chosen backend. The default
73
73
  Environment instance should be built from ``sys.path`` and may use caching
74
- to share instance state accorss calls.
74
+ to share instance state across calls.
75
75
  """
76
76
  return select_backend().Environment.default()
77
77
 
@@ -23,6 +23,8 @@ METADATA_FIELDS = [
23
23
  ("Maintainer", False),
24
24
  ("Maintainer-email", False),
25
25
  ("License", False),
26
+ ("License-Expression", False),
27
+ ("License-File", True),
26
28
  ("Classifier", True),
27
29
  ("Requires-Dist", True),
28
30
  ("Requires-Python", False),
@@ -2,6 +2,7 @@ import email.message
2
2
  import importlib.metadata
3
3
  import pathlib
4
4
  import zipfile
5
+ from os import PathLike
5
6
  from typing import (
6
7
  Collection,
7
8
  Dict,
@@ -95,6 +96,11 @@ class WheelDistribution(importlib.metadata.Distribution):
95
96
  raise UnsupportedWheel(error)
96
97
  return text
97
98
 
99
+ def locate_file(self, path: str | PathLike[str]) -> pathlib.Path:
100
+ # This method doesn't make sense for our in-memory wheel, but the API
101
+ # requires us to define it.
102
+ raise NotImplementedError
103
+
98
104
 
99
105
  class Distribution(BaseDistribution):
100
106
  def __init__(
@@ -190,7 +196,7 @@ class Distribution(BaseDistribution):
190
196
  return content
191
197
 
192
198
  def iter_entry_points(self) -> Iterable[BaseEntryPoint]:
193
- # importlib.metadata's EntryPoint structure sasitfies BaseEntryPoint.
199
+ # importlib.metadata's EntryPoint structure satisfies BaseEntryPoint.
194
200
  return self._dist.entry_points
195
201
 
196
202
  def _metadata_impl(self) -> email.message.Message:
@@ -170,12 +170,23 @@ def _ensure_quoted_url(url: str) -> str:
170
170
  and without double-quoting other characters.
171
171
  """
172
172
  # Split the URL into parts according to the general structure
173
- # `scheme://netloc/path;parameters?query#fragment`.
174
- result = urllib.parse.urlparse(url)
173
+ # `scheme://netloc/path?query#fragment`.
174
+ result = urllib.parse.urlsplit(url)
175
175
  # If the netloc is empty, then the URL refers to a local filesystem path.
176
176
  is_local_path = not result.netloc
177
177
  path = _clean_url_path(result.path, is_local_path=is_local_path)
178
- return urllib.parse.urlunparse(result._replace(path=path))
178
+ return urllib.parse.urlunsplit(result._replace(path=path))
179
+
180
+
181
+ def _absolute_link_url(base_url: str, url: str) -> str:
182
+ """
183
+ A faster implementation of urllib.parse.urljoin with a shortcut
184
+ for absolute http/https URLs.
185
+ """
186
+ if url.startswith(("https://", "http://")):
187
+ return url
188
+ else:
189
+ return urllib.parse.urljoin(base_url, url)
179
190
 
180
191
 
181
192
  @functools.total_ordering
@@ -185,6 +196,7 @@ class Link:
185
196
  __slots__ = [
186
197
  "_parsed_url",
187
198
  "_url",
199
+ "_path",
188
200
  "_hashes",
189
201
  "comes_from",
190
202
  "requires_python",
@@ -241,6 +253,8 @@ class Link:
241
253
  # Store the url as a private attribute to prevent accidentally
242
254
  # trying to set a new value.
243
255
  self._url = url
256
+ # The .path property is hot, so calculate its value ahead of time.
257
+ self._path = urllib.parse.unquote(self._parsed_url.path)
244
258
 
245
259
  link_hash = LinkHash.find_hash_url_fragment(url)
246
260
  hashes_from_link = {} if link_hash is None else link_hash.as_dict()
@@ -270,7 +284,7 @@ class Link:
270
284
  if file_url is None:
271
285
  return None
272
286
 
273
- url = _ensure_quoted_url(urllib.parse.urljoin(page_url, file_url))
287
+ url = _ensure_quoted_url(_absolute_link_url(page_url, file_url))
274
288
  pyrequire = file_data.get("requires-python")
275
289
  yanked_reason = file_data.get("yanked")
276
290
  hashes = file_data.get("hashes", {})
@@ -322,7 +336,7 @@ class Link:
322
336
  if not href:
323
337
  return None
324
338
 
325
- url = _ensure_quoted_url(urllib.parse.urljoin(base_url, href))
339
+ url = _ensure_quoted_url(_absolute_link_url(base_url, href))
326
340
  pyrequire = anchor_attribs.get("data-requires-python")
327
341
  yanked_reason = anchor_attribs.get("data-yanked")
328
342
 
@@ -421,7 +435,7 @@ class Link:
421
435
 
422
436
  @property
423
437
  def path(self) -> str:
424
- return urllib.parse.unquote(self._parsed_url.path)
438
+ return self._path
425
439
 
426
440
  def splitext(self) -> Tuple[str, str]:
427
441
  return splitext(posixpath.basename(self.path.rstrip("/")))
@@ -452,10 +466,10 @@ class Link:
452
466
  project_name = match.group(1)
453
467
  if not self._project_name_re.match(project_name):
454
468
  deprecated(
455
- reason=f"{self} contains an egg fragment with a non-PEP 508 name",
469
+ reason=f"{self} contains an egg fragment with a non-PEP 508 name.",
456
470
  replacement="to use the req @ url syntax, and remove the egg fragment",
457
- gone_in="25.0",
458
- issue=11617,
471
+ gone_in="25.1",
472
+ issue=13157,
459
473
  )
460
474
 
461
475
  return project_name
@@ -76,6 +76,18 @@ class SafeFileCache(SeparateBodyBaseCache):
76
76
 
77
77
  with adjacent_tmp_file(path) as f:
78
78
  f.write(data)
79
+ # Inherit the read/write permissions of the cache directory
80
+ # to enable multi-user cache use-cases.
81
+ mode = (
82
+ os.stat(self.directory).st_mode
83
+ & 0o666 # select read/write permissions of cache directory
84
+ | 0o600 # set owner read/write permissions
85
+ )
86
+ # Change permissions only if there is no risk of following a symlink.
87
+ if os.chmod in os.supports_fd:
88
+ os.chmod(f.fileno(), mode)
89
+ elif os.chmod in os.supports_follow_symlinks:
90
+ os.chmod(f.name, mode, follow_symlinks=False)
79
91
 
80
92
  replace(f.name, path)
81
93
 
@@ -339,6 +339,7 @@ class PipSession(requests.Session):
339
339
  # Namespace the attribute with "pip_" just in case to prevent
340
340
  # possible conflicts with the base class.
341
341
  self.pip_trusted_origins: List[Tuple[str, Optional[int]]] = []
342
+ self.pip_proxy = None
342
343
 
343
344
  # Attach our User Agent to the request
344
345
  self.headers["User-Agent"] = user_agent()
@@ -38,4 +38,5 @@ def generate_editable_metadata(
38
38
  except InstallationSubprocessError as error:
39
39
  raise MetadataGenerationFailed(package_details=details) from error
40
40
 
41
+ assert distinfo_dir is not None
41
42
  return os.path.join(metadata_dir, distinfo_dir)
@@ -1,9 +1,10 @@
1
1
  import collections
2
2
  import logging
3
3
  import os
4
+ from dataclasses import dataclass, field
4
5
  from typing import Container, Dict, Generator, Iterable, List, NamedTuple, Optional, Set
5
6
 
6
- from pip._vendor.packaging.utils import canonicalize_name
7
+ from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
7
8
  from pip._vendor.packaging.version import InvalidVersion
8
9
 
9
10
  from pip._internal.exceptions import BadCommand, InstallationError
@@ -220,19 +221,16 @@ def _get_editable_info(dist: BaseDistribution) -> _EditableInfo:
220
221
  )
221
222
 
222
223
 
224
+ @dataclass(frozen=True)
223
225
  class FrozenRequirement:
224
- def __init__(
225
- self,
226
- name: str,
227
- req: str,
228
- editable: bool,
229
- comments: Iterable[str] = (),
230
- ) -> None:
231
- self.name = name
232
- self.canonical_name = canonicalize_name(name)
233
- self.req = req
234
- self.editable = editable
235
- self.comments = comments
226
+ name: str
227
+ req: str
228
+ editable: bool
229
+ comments: Iterable[str] = field(default_factory=tuple)
230
+
231
+ @property
232
+ def canonical_name(self) -> NormalizedName:
233
+ return canonicalize_name(self.name)
236
234
 
237
235
  @classmethod
238
236
  def from_dist(cls, dist: BaseDistribution) -> "FrozenRequirement":
@@ -73,7 +73,7 @@ def load_pyproject_toml(
73
73
  build_system = None
74
74
 
75
75
  # The following cases must use PEP 517
76
- # We check for use_pep517 being non-None and falsey because that means
76
+ # We check for use_pep517 being non-None and falsy because that means
77
77
  # the user explicitly requested --no-use-pep517. The value 0 as
78
78
  # opposed to False can occur when the value is provided via an
79
79
  # environment variable or config file option (due to the quirk of