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
@@ -10,6 +10,7 @@ from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
10
10
  from pip._vendor.packaging.version import Version
11
11
 
12
12
  from pip._internal.exceptions import (
13
+ FailedToPrepareCandidate,
13
14
  HashError,
14
15
  InstallationSubprocessError,
15
16
  InvalidInstalledPackage,
@@ -68,10 +69,8 @@ def make_install_req_from_link(
68
69
  line,
69
70
  user_supplied=template.user_supplied,
70
71
  comes_from=template.comes_from,
71
- use_pep517=template.use_pep517,
72
72
  isolated=template.isolated,
73
73
  constraint=template.constraint,
74
- global_options=template.global_options,
75
74
  hash_options=template.hash_options,
76
75
  config_settings=template.config_settings,
77
76
  )
@@ -85,15 +84,17 @@ def make_install_req_from_editable(
85
84
  link: Link, template: InstallRequirement
86
85
  ) -> InstallRequirement:
87
86
  assert template.editable, "template not editable"
87
+ if template.name:
88
+ req_string = f"{template.name} @ {link.url}"
89
+ else:
90
+ req_string = link.url
88
91
  ireq = install_req_from_editable(
89
- link.url,
92
+ req_string,
90
93
  user_supplied=template.user_supplied,
91
94
  comes_from=template.comes_from,
92
- use_pep517=template.use_pep517,
93
95
  isolated=template.isolated,
94
96
  constraint=template.constraint,
95
97
  permit_editable_wheels=template.permit_editable_wheels,
96
- global_options=template.global_options,
97
98
  hash_options=template.hash_options,
98
99
  config_settings=template.config_settings,
99
100
  )
@@ -114,10 +115,8 @@ def _make_install_req_from_dist(
114
115
  line,
115
116
  user_supplied=template.user_supplied,
116
117
  comes_from=template.comes_from,
117
- use_pep517=template.use_pep517,
118
118
  isolated=template.isolated,
119
119
  constraint=template.constraint,
120
- global_options=template.global_options,
121
120
  hash_options=template.hash_options,
122
121
  config_settings=template.config_settings,
123
122
  )
@@ -244,9 +243,19 @@ class _InstallRequirementBackedCandidate(Candidate):
244
243
  e.req = self._ireq
245
244
  raise
246
245
  except InstallationSubprocessError as exc:
247
- # The output has been presented already, so don't duplicate it.
248
- exc.context = "See above for output."
249
- raise
246
+ if isinstance(self._ireq.comes_from, InstallRequirement):
247
+ request_chain = self._ireq.comes_from.from_path()
248
+ else:
249
+ request_chain = self._ireq.comes_from
250
+
251
+ if request_chain is None:
252
+ request_chain = "directly requested"
253
+
254
+ raise FailedToPrepareCandidate(
255
+ package_name=self._ireq.name or str(self._link),
256
+ requirement_chain=request_chain,
257
+ failed_step=exc.command_description,
258
+ )
250
259
 
251
260
  self._check_metadata_consistency(dist)
252
261
  return dist
@@ -283,7 +292,7 @@ class LinkCandidate(_InstallRequirementBackedCandidate):
283
292
  assert ireq.link == link
284
293
  if ireq.link.is_wheel and not ireq.link.is_file:
285
294
  wheel = Wheel(ireq.link.filename)
286
- wheel_name = canonicalize_name(wheel.name)
295
+ wheel_name = wheel.name
287
296
  assert name == wheel_name, f"{name!r} != {wheel_name!r} for wheel"
288
297
  # Version may not be present for PEP 508 direct URLs
289
298
  if version is not None:
@@ -695,9 +695,20 @@ class Factory:
695
695
  "version: %s",
696
696
  "; ".join(skipped_by_requires_python) or "none",
697
697
  )
698
+
699
+ # Check if only final releases are allowed for this package
700
+ version_type = "version"
701
+ if self._finder.release_control is not None:
702
+ allows_pre = self._finder.release_control.allows_prereleases(
703
+ canonicalize_name(req.project_name)
704
+ )
705
+ if allows_pre is False:
706
+ version_type = "final version"
707
+
698
708
  logger.critical(
699
- "Could not find a version that satisfies the requirement %s "
709
+ "Could not find a %s that satisfies the requirement %s "
700
710
  "(from versions: %s)",
711
+ version_type,
701
712
  req_disp,
702
713
  ", ".join(versions) or "none",
703
714
  )
@@ -711,6 +722,21 @@ class Factory:
711
722
 
712
723
  return DistributionNotFound(f"No matching distribution found for {req}")
713
724
 
725
+ def _has_any_candidates(self, project_name: str) -> bool:
726
+ """
727
+ Check if there are any candidates available for the project name.
728
+ """
729
+ return any(
730
+ self.find_candidates(
731
+ project_name,
732
+ requirements={project_name: []},
733
+ incompatibilities={},
734
+ constraint=Constraint.empty(),
735
+ prefers_installed=True,
736
+ is_satisfied_by=lambda r, c: True,
737
+ )
738
+ )
739
+
714
740
  def get_installation_error(
715
741
  self,
716
742
  e: ResolutionImpossible[Requirement, Candidate],
@@ -796,6 +822,22 @@ class Factory:
796
822
  spec = constraints[key].specifier
797
823
  msg += f"\n The user requested (constraint) {key}{spec}"
798
824
 
825
+ # Check for causes that had no candidates
826
+ causes = set()
827
+ for req, _ in e.causes:
828
+ causes.add(req.name)
829
+
830
+ no_candidates = {c for c in causes if not self._has_any_candidates(c)}
831
+ if no_candidates:
832
+ msg = (
833
+ msg
834
+ + "\n\n"
835
+ + "Additionally, some packages in these conflicts have no "
836
+ + "matching distributions available for your environment:"
837
+ + "\n "
838
+ + "\n ".join(sorted(no_candidates))
839
+ )
840
+
799
841
  msg = (
800
842
  msg
801
843
  + "\n\n"
@@ -100,6 +100,15 @@ class PipProvider(_ProviderBase):
100
100
  self._upgrade_strategy = upgrade_strategy
101
101
  self._user_requested = user_requested
102
102
 
103
+ @property
104
+ def constraints(self) -> dict[str, Constraint]:
105
+ """Public view of user-specified constraints.
106
+
107
+ Exposes the provider's constraints mapping without encouraging
108
+ external callers to reach into private attributes.
109
+ """
110
+ return self._constraints
111
+
103
112
  def identify(self, requirement_or_candidate: Requirement | Candidate) -> str:
104
113
  return requirement_or_candidate.name
105
114
 
@@ -1,19 +1,21 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from collections import defaultdict
4
+ from collections.abc import Mapping
4
5
  from logging import getLogger
5
6
  from typing import Any
6
7
 
7
8
  from pip._vendor.resolvelib.reporters import BaseReporter
8
9
 
9
- from .base import Candidate, Requirement
10
+ from .base import Candidate, Constraint, Requirement
10
11
 
11
12
  logger = getLogger(__name__)
12
13
 
13
14
 
14
15
  class PipReporter(BaseReporter[Requirement, Candidate, str]):
15
- def __init__(self) -> None:
16
+ def __init__(self, constraints: Mapping[str, Constraint] | None = None) -> None:
16
17
  self.reject_count_by_package: defaultdict[str, int] = defaultdict(int)
18
+ self._constraints = constraints or {}
17
19
 
18
20
  self._messages_at_reject_count = {
19
21
  1: (
@@ -35,25 +37,36 @@ class PipReporter(BaseReporter[Requirement, Candidate, str]):
35
37
  }
36
38
 
37
39
  def rejecting_candidate(self, criterion: Any, candidate: Candidate) -> None:
40
+ """Report a candidate being rejected.
41
+
42
+ Logs both the rejection count message (if applicable) and details about
43
+ the requirements and constraints that caused the rejection.
44
+ """
38
45
  self.reject_count_by_package[candidate.name] += 1
39
46
 
40
47
  count = self.reject_count_by_package[candidate.name]
41
- if count not in self._messages_at_reject_count:
42
- return
43
-
44
- message = self._messages_at_reject_count[count]
45
- logger.info("INFO: %s", message.format(package_name=candidate.name))
48
+ if count in self._messages_at_reject_count:
49
+ message = self._messages_at_reject_count[count]
50
+ logger.info("INFO: %s", message.format(package_name=candidate.name))
46
51
 
47
52
  msg = "Will try a different candidate, due to conflict:"
48
53
  for req_info in criterion.information:
49
54
  req, parent = req_info.requirement, req_info.parent
50
- # Inspired by Factory.get_installation_error
51
55
  msg += "\n "
52
56
  if parent:
53
57
  msg += f"{parent.name} {parent.version} depends on "
54
58
  else:
55
59
  msg += "The user requested "
56
60
  msg += req.format_for_error()
61
+
62
+ # Add any relevant constraints
63
+ if self._constraints:
64
+ name = candidate.name
65
+ constraint = self._constraints.get(name)
66
+ if constraint and constraint.specifier:
67
+ constraint_text = f"{name}{constraint.specifier}"
68
+ msg += f"\n The user requested (constraint) {constraint_text}"
69
+
57
70
  logger.debug(msg)
58
71
 
59
72
 
@@ -164,6 +164,12 @@ class RequiresPythonRequirement(Requirement):
164
164
  self._hash: int | None = None
165
165
  self._candidate = match
166
166
 
167
+ # Pre-compute candidate lookup to avoid repeated specifier checks
168
+ if specifier.contains(match.version, prereleases=True):
169
+ self._candidate_lookup: CandidateLookup = (match, None)
170
+ else:
171
+ self._candidate_lookup = (None, None)
172
+
167
173
  def __str__(self) -> str:
168
174
  return f"Python {self.specifier}"
169
175
 
@@ -197,9 +203,7 @@ class RequiresPythonRequirement(Requirement):
197
203
  return str(self)
198
204
 
199
205
  def get_candidate_lookup(self) -> CandidateLookup:
200
- if self.specifier.contains(self._candidate.version, prereleases=True):
201
- return self._candidate, None
202
- return None, None
206
+ return self._candidate_lookup
203
207
 
204
208
  def is_satisfied_by(self, candidate: Candidate) -> bool:
205
209
  assert candidate.name == self._candidate.name, "Not Python candidate"
@@ -87,7 +87,8 @@ class Resolver(BaseResolver):
87
87
  if "PIP_RESOLVER_DEBUG" in os.environ:
88
88
  reporter: BaseReporter[Requirement, Candidate, str] = PipDebuggingReporter()
89
89
  else:
90
- reporter = PipReporter()
90
+ reporter = PipReporter(constraints=provider.constraints)
91
+
91
92
  resolver: RLResolver[Requirement, Candidate, str] = RLResolver(
92
93
  provider,
93
94
  reporter,
@@ -180,11 +181,6 @@ class Resolver(BaseResolver):
180
181
 
181
182
  req_set.add_named_requirement(ireq)
182
183
 
183
- reqs = req_set.all_requirements
184
- self.factory.preparer.prepare_linked_requirements_more(reqs)
185
- for req in reqs:
186
- req.prepared = True
187
- req.needs_more_preparation = False
188
184
  return req_set
189
185
 
190
186
  def get_installation_order(
@@ -9,7 +9,7 @@ import optparse
9
9
  import os.path
10
10
  import sys
11
11
  from dataclasses import dataclass
12
- from typing import Any, Callable
12
+ from typing import Callable
13
13
 
14
14
  from pip._vendor.packaging.version import Version
15
15
  from pip._vendor.packaging.version import parse as parse_version
@@ -20,14 +20,21 @@ from pip._vendor.rich.text import Text
20
20
  from pip._internal.index.collector import LinkCollector
21
21
  from pip._internal.index.package_finder import PackageFinder
22
22
  from pip._internal.metadata import get_default_environment
23
+ from pip._internal.models.release_control import ReleaseControl
23
24
  from pip._internal.models.selection_prefs import SelectionPreferences
24
25
  from pip._internal.network.session import PipSession
25
26
  from pip._internal.utils.compat import WINDOWS
27
+ from pip._internal.utils.datetime import parse_iso_datetime
26
28
  from pip._internal.utils.entrypoints import (
27
29
  get_best_invocation_for_this_pip,
28
30
  get_best_invocation_for_this_python,
29
31
  )
30
- from pip._internal.utils.filesystem import adjacent_tmp_file, check_path_owner, replace
32
+ from pip._internal.utils.filesystem import (
33
+ adjacent_tmp_file,
34
+ check_path_owner,
35
+ copy_directory_permissions,
36
+ replace,
37
+ )
31
38
  from pip._internal.utils.misc import (
32
39
  ExternallyManagedEnvironment,
33
40
  check_externally_managed,
@@ -45,18 +52,9 @@ def _get_statefile_name(key: str) -> str:
45
52
  return name
46
53
 
47
54
 
48
- def _convert_date(isodate: str) -> datetime.datetime:
49
- """Convert an ISO format string to a date.
50
-
51
- Handles the format 2020-01-22T14:24:01Z (trailing Z)
52
- which is not supported by older versions of fromisoformat.
53
- """
54
- return datetime.datetime.fromisoformat(isodate.replace("Z", "+00:00"))
55
-
56
-
57
55
  class SelfCheckState:
58
56
  def __init__(self, cache_dir: str) -> None:
59
- self._state: dict[str, Any] = {}
57
+ self._state: dict[str, str] = {}
60
58
  self._statefile_path = None
61
59
 
62
60
  # Try to load the existing state
@@ -88,7 +86,7 @@ class SelfCheckState:
88
86
  return None
89
87
 
90
88
  # Determine if we need to refresh the state
91
- last_check = _convert_date(self._state["last_check"])
89
+ last_check = parse_iso_datetime(self._state["last_check"])
92
90
  time_since_last_check = current_time - last_check
93
91
  if time_since_last_check > _WEEK:
94
92
  return None
@@ -100,13 +98,15 @@ class SelfCheckState:
100
98
  if not self._statefile_path:
101
99
  return
102
100
 
101
+ statefile_directory = os.path.dirname(self._statefile_path)
102
+
103
103
  # Check to make sure that we own the directory
104
- if not check_path_owner(os.path.dirname(self._statefile_path)):
104
+ if not check_path_owner(statefile_directory):
105
105
  return
106
106
 
107
107
  # Now that we've ensured the directory is owned by this user, we'll go
108
108
  # ahead and make sure that all our directories are created.
109
- ensure_dir(os.path.dirname(self._statefile_path))
109
+ ensure_dir(statefile_directory)
110
110
 
111
111
  state = {
112
112
  # Include the key so it's easy to tell which pip wrote the
@@ -120,6 +120,7 @@ class SelfCheckState:
120
120
 
121
121
  with adjacent_tmp_file(self._statefile_path) as f:
122
122
  f.write(text.encode())
123
+ copy_directory_permissions(statefile_directory, f)
123
124
 
124
125
  try:
125
126
  # Since we have a prefix-specific state file, we can just
@@ -179,7 +180,7 @@ def _get_current_remote_pip_version(
179
180
  # yanked version.
180
181
  selection_prefs = SelectionPreferences(
181
182
  allow_yanked=False,
182
- allow_all_prereleases=False, # Explicitly set to False
183
+ release_control=ReleaseControl(only_final={"pip"}),
183
184
  )
184
185
 
185
186
  finder = PackageFinder.create(
@@ -1,6 +1,7 @@
1
1
  """For when pip wants to check the date or time."""
2
2
 
3
3
  import datetime
4
+ import sys
4
5
 
5
6
 
6
7
  def today_is_later_than(year: int, month: int, day: int) -> bool:
@@ -8,3 +9,20 @@ def today_is_later_than(year: int, month: int, day: int) -> bool:
8
9
  given = datetime.date(year, month, day)
9
10
 
10
11
  return today > given
12
+
13
+
14
+ def parse_iso_datetime(isodate: str) -> datetime.datetime:
15
+ """Convert an ISO format string to a datetime.
16
+
17
+ Handles the format 2020-01-22T14:24:01Z (trailing Z)
18
+ which is not supported by older versions of fromisoformat.
19
+ """
20
+ # Python 3.11+ supports Z suffix natively in fromisoformat
21
+ if sys.version_info >= (3, 11):
22
+ return datetime.datetime.fromisoformat(isodate)
23
+ else:
24
+ return datetime.datetime.fromisoformat(
25
+ isodate.replace("Z", "+00:00")
26
+ if isodate.endswith("Z") and ("T" in isodate or " " in isodate.strip())
27
+ else isodate
28
+ )
@@ -7,8 +7,9 @@ import random
7
7
  import sys
8
8
  from collections.abc import Generator
9
9
  from contextlib import contextmanager
10
+ from pathlib import Path
10
11
  from tempfile import NamedTemporaryFile
11
- from typing import Any, BinaryIO, cast
12
+ from typing import Any, BinaryIO, Callable, cast
12
13
 
13
14
  from pip._internal.utils.compat import get_path_uid
14
15
  from pip._internal.utils.misc import format_size
@@ -150,3 +151,53 @@ def directory_size(path: str) -> int | float:
150
151
 
151
152
  def format_directory_size(path: str) -> str:
152
153
  return format_size(directory_size(path))
154
+
155
+
156
+ def copy_directory_permissions(directory: str, target_file: BinaryIO) -> None:
157
+ mode = (
158
+ os.stat(directory).st_mode & 0o666 # select read/write permissions of directory
159
+ | 0o600 # set owner read/write permissions
160
+ )
161
+ # Change permissions only if there is no risk of following a symlink.
162
+ if os.chmod in os.supports_fd:
163
+ os.chmod(target_file.fileno(), mode)
164
+ elif os.chmod in os.supports_follow_symlinks:
165
+ os.chmod(target_file.name, mode, follow_symlinks=False)
166
+
167
+
168
+ def _subdirs_without_generic(
169
+ path: str, predicate: Callable[[str, list[str]], bool]
170
+ ) -> Generator[Path]:
171
+ """Yields every subdirectory of +path+ that has no files matching the
172
+ predicate under it."""
173
+
174
+ directories = []
175
+ excluded = set()
176
+
177
+ for root_str, _, filenames in os.walk(Path(path).resolve()):
178
+ root = Path(root_str)
179
+ if predicate(root_str, filenames):
180
+ # This directory should be excluded, so exclude it and all of its
181
+ # parent directories.
182
+ # The last item in root.parents is ".", so we ignore it.
183
+ #
184
+ # Wrapping this in `list()` is only needed for Python 3.9.
185
+ excluded.update(list(root.parents)[:-1])
186
+ excluded.add(root)
187
+ directories.append(root)
188
+
189
+ for d in sorted(directories, reverse=True):
190
+ if d not in excluded:
191
+ yield d
192
+
193
+
194
+ def subdirs_without_files(path: str) -> Generator[Path]:
195
+ """Yields every subdirectory of +path+ that has no files under it."""
196
+ return _subdirs_without_generic(path, lambda root, filenames: len(filenames) > 0)
197
+
198
+
199
+ def subdirs_without_wheels(path: str) -> Generator[Path]:
200
+ """Yields every subdirectory of +path+ that has no .whl files under it."""
201
+ return _subdirs_without_generic(
202
+ path, lambda root, filenames: any(x.endswith(".whl") for x in filenames)
203
+ )
@@ -9,7 +9,7 @@ import sys
9
9
  import threading
10
10
  from collections.abc import Generator
11
11
  from dataclasses import dataclass
12
- from io import TextIOWrapper
12
+ from io import StringIO, TextIOWrapper
13
13
  from logging import Filter
14
14
  from typing import Any, ClassVar
15
15
 
@@ -29,7 +29,7 @@ from pip._vendor.rich.style import Style
29
29
  from pip._internal.utils._log import VERBOSE, getLogger
30
30
  from pip._internal.utils.compat import WINDOWS
31
31
  from pip._internal.utils.deprecation import DEPRECATION_MSG_PREFIX
32
- from pip._internal.utils.misc import ensure_dir
32
+ from pip._internal.utils.misc import StreamWrapper, ensure_dir
33
33
 
34
34
  _log_state = threading.local()
35
35
  _stdout_console = None
@@ -56,6 +56,38 @@ def _is_broken_pipe_error(exc_class: type[BaseException], exc: BaseException) ->
56
56
  return isinstance(exc, OSError) and exc.errno in (errno.EINVAL, errno.EPIPE)
57
57
 
58
58
 
59
+ @contextlib.contextmanager
60
+ def capture_logging() -> Generator[StringIO, None, None]:
61
+ """Capture all pip logs in a buffer temporarily."""
62
+ # Patching sys.std(out|err) directly is not viable as the caller
63
+ # may want to emit non-logging output (e.g. a rich spinner). To
64
+ # avoid capturing that, temporarily patch the root logging handlers
65
+ # to use new rich consoles that write to a StringIO.
66
+ handlers = {}
67
+ for handler in logging.getLogger().handlers:
68
+ if isinstance(handler, RichPipStreamHandler):
69
+ # Also store the handler's original console so it can be
70
+ # restored on context exit.
71
+ handlers[handler] = handler.console
72
+
73
+ fake_stream = StreamWrapper.from_stream(sys.stdout)
74
+ if not handlers:
75
+ yield fake_stream
76
+ return
77
+
78
+ # HACK: grab no_color attribute from a random handler console since
79
+ # it's a global option anyway.
80
+ no_color = next(iter(handlers.values())).no_color
81
+ fake_console = PipConsole(file=fake_stream, no_color=no_color, soft_wrap=True)
82
+ try:
83
+ for handler in handlers:
84
+ handler.console = fake_console
85
+ yield fake_stream
86
+ finally:
87
+ for handler, original_console in handlers.items():
88
+ handler.console = original_console
89
+
90
+
59
91
  @contextlib.contextmanager
60
92
  def indent_log(num: int = 2) -> Generator[None, None, None]:
61
93
  """
@@ -182,10 +182,12 @@ def rmtree_errorhandler(
182
182
  def display_path(path: str) -> str:
183
183
  """Gives the display value for a given path, making it relative to cwd
184
184
  if possible."""
185
- path = os.path.normcase(os.path.abspath(path))
186
- if path.startswith(os.getcwd() + os.path.sep):
187
- path = "." + path[len(os.getcwd()) :]
188
- return path
185
+ try:
186
+ relative = Path(path).relative_to(Path.cwd())
187
+ except ValueError:
188
+ # If the path isn't relative to the CWD, leave it alone
189
+ return path
190
+ return os.path.join(".", relative)
189
191
 
190
192
 
191
193
  def backup_dir(dir: str, ext: str = ".bak") -> str:
@@ -541,14 +543,18 @@ class HiddenText:
541
543
  def __str__(self) -> str:
542
544
  return self.redacted
543
545
 
544
- # This is useful for testing.
545
- def __eq__(self, other: Any) -> bool:
546
- if type(self) is not type(other):
547
- return False
548
-
549
- # The string being used for redaction doesn't also have to match,
550
- # just the raw, original string.
551
- return self.secret == other.secret
546
+ def __eq__(self, other: object) -> bool:
547
+ # Equality is particularly useful for testing.
548
+ if type(self) is type(other):
549
+ # The string being used for redaction doesn't also have to match,
550
+ # just the raw, original string.
551
+ return self.secret == cast(HiddenText, other).secret
552
+ return NotImplemented
553
+
554
+ # Disable hashing, since we have a custom __eq__ and don't need hash-ability
555
+ # (yet). The only required property of hashing is that objects which compare
556
+ # equal have the same hash value.
557
+ __hash__ = None # type: ignore[assignment]
552
558
 
553
559
 
554
560
  def hide_value(value: str) -> HiddenText:
@@ -0,0 +1,116 @@
1
+ from collections.abc import Iterable
2
+ from pathlib import Path
3
+
4
+ from pip._vendor.packaging.pylock import (
5
+ Package,
6
+ PackageArchive,
7
+ PackageDirectory,
8
+ PackageSdist,
9
+ PackageVcs,
10
+ PackageWheel,
11
+ Pylock,
12
+ )
13
+ from pip._vendor.packaging.version import Version
14
+
15
+ from pip._internal.models.direct_url import ArchiveInfo, DirInfo, VcsInfo
16
+ from pip._internal.models.link import Link
17
+ from pip._internal.req.req_install import InstallRequirement
18
+ from pip._internal.utils.urls import url_to_path
19
+
20
+
21
+ def _pylock_package_from_install_requirement(
22
+ ireq: InstallRequirement, base_dir: Path
23
+ ) -> Package:
24
+ base_dir = base_dir.resolve()
25
+ dist = ireq.get_dist()
26
+ download_info = ireq.download_info
27
+ assert download_info
28
+ package_version = None
29
+ package_vcs = None
30
+ package_directory = None
31
+ package_archive = None
32
+ package_sdist = None
33
+ package_wheels = None
34
+ if ireq.is_direct:
35
+ if isinstance(download_info.info, VcsInfo):
36
+ package_vcs = PackageVcs(
37
+ type=download_info.info.vcs,
38
+ url=download_info.url,
39
+ path=None,
40
+ requested_revision=download_info.info.requested_revision,
41
+ commit_id=download_info.info.commit_id,
42
+ subdirectory=download_info.subdirectory,
43
+ )
44
+ elif isinstance(download_info.info, DirInfo):
45
+ package_directory = PackageDirectory(
46
+ path=(
47
+ Path(url_to_path(download_info.url))
48
+ .resolve()
49
+ .relative_to(base_dir)
50
+ .as_posix()
51
+ ),
52
+ editable=(
53
+ download_info.info.editable if download_info.info.editable else None
54
+ ),
55
+ subdirectory=download_info.subdirectory,
56
+ )
57
+ elif isinstance(download_info.info, ArchiveInfo):
58
+ if not download_info.info.hashes:
59
+ raise NotImplementedError()
60
+ package_archive = PackageArchive(
61
+ url=download_info.url,
62
+ path=None,
63
+ hashes=download_info.info.hashes,
64
+ subdirectory=download_info.subdirectory,
65
+ )
66
+ else:
67
+ # should never happen
68
+ raise NotImplementedError()
69
+ else:
70
+ package_version = dist.version
71
+ if isinstance(download_info.info, ArchiveInfo):
72
+ if not download_info.info.hashes:
73
+ raise NotImplementedError()
74
+ link = Link(download_info.url)
75
+ if link.is_wheel:
76
+ package_wheels = [
77
+ PackageWheel(
78
+ name=link.filename,
79
+ url=download_info.url,
80
+ hashes=download_info.info.hashes,
81
+ )
82
+ ]
83
+ else:
84
+ package_sdist = PackageSdist(
85
+ name=link.filename,
86
+ url=download_info.url,
87
+ hashes=download_info.info.hashes,
88
+ )
89
+ else:
90
+ # should never happen
91
+ raise NotImplementedError()
92
+ return Package(
93
+ name=dist.canonical_name,
94
+ version=package_version,
95
+ vcs=package_vcs,
96
+ directory=package_directory,
97
+ archive=package_archive,
98
+ sdist=package_sdist,
99
+ wheels=package_wheels,
100
+ )
101
+
102
+
103
+ def pylock_from_install_requirements(
104
+ install_requirements: Iterable[InstallRequirement], base_dir: Path
105
+ ) -> Pylock:
106
+ return Pylock(
107
+ lock_version=Version("1.0"),
108
+ created_by="pip",
109
+ packages=sorted(
110
+ (
111
+ _pylock_package_from_install_requirement(ireq, base_dir)
112
+ for ireq in install_requirements
113
+ ),
114
+ key=lambda p: p.name,
115
+ ),
116
+ )