pip 25.3__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 (104) hide show
  1. pip/__init__.py +1 -1
  2. pip/_internal/build_env.py +194 -5
  3. pip/_internal/cli/base_command.py +11 -0
  4. pip/_internal/cli/cmdoptions.py +157 -0
  5. pip/_internal/cli/index_command.py +20 -0
  6. pip/_internal/cli/main.py +11 -6
  7. pip/_internal/cli/main_parser.py +3 -1
  8. pip/_internal/cli/parser.py +93 -33
  9. pip/_internal/cli/progress_bars.py +4 -2
  10. pip/_internal/cli/req_command.py +99 -23
  11. pip/_internal/commands/cache.py +24 -0
  12. pip/_internal/commands/completion.py +2 -1
  13. pip/_internal/commands/download.py +8 -4
  14. pip/_internal/commands/index.py +13 -6
  15. pip/_internal/commands/install.py +36 -29
  16. pip/_internal/commands/list.py +14 -16
  17. pip/_internal/commands/lock.py +16 -8
  18. pip/_internal/commands/wheel.py +8 -13
  19. pip/_internal/exceptions.py +76 -3
  20. pip/_internal/index/collector.py +2 -3
  21. pip/_internal/index/package_finder.py +84 -18
  22. pip/_internal/locations/__init__.py +1 -2
  23. pip/_internal/locations/_sysconfig.py +4 -1
  24. pip/_internal/models/link.py +18 -14
  25. pip/_internal/models/release_control.py +92 -0
  26. pip/_internal/models/selection_prefs.py +6 -3
  27. pip/_internal/network/auth.py +6 -2
  28. pip/_internal/network/download.py +4 -5
  29. pip/_internal/network/session.py +14 -10
  30. pip/_internal/operations/install/wheel.py +1 -2
  31. pip/_internal/operations/prepare.py +2 -3
  32. pip/_internal/req/constructors.py +3 -1
  33. pip/_internal/req/pep723.py +41 -0
  34. pip/_internal/req/req_file.py +10 -1
  35. pip/_internal/resolution/resolvelib/factory.py +12 -1
  36. pip/_internal/resolution/resolvelib/requirements.py +7 -3
  37. pip/_internal/self_outdated_check.py +6 -13
  38. pip/_internal/utils/datetime.py +18 -0
  39. pip/_internal/utils/filesystem.py +40 -1
  40. pip/_internal/utils/logging.py +34 -2
  41. pip/_internal/utils/misc.py +18 -12
  42. pip/_internal/utils/pylock.py +116 -0
  43. pip/_internal/utils/unpacking.py +1 -1
  44. pip/_internal/vcs/versioncontrol.py +3 -1
  45. pip/_vendor/cachecontrol/__init__.py +6 -3
  46. pip/_vendor/cachecontrol/adapter.py +0 -1
  47. pip/_vendor/cachecontrol/controller.py +1 -1
  48. pip/_vendor/cachecontrol/filewrapper.py +3 -1
  49. pip/_vendor/certifi/__init__.py +1 -1
  50. pip/_vendor/certifi/cacert.pem +0 -332
  51. pip/_vendor/idna/LICENSE.md +1 -1
  52. pip/_vendor/idna/codec.py +1 -1
  53. pip/_vendor/idna/core.py +1 -1
  54. pip/_vendor/idna/idnadata.py +72 -6
  55. pip/_vendor/idna/package_data.py +1 -1
  56. pip/_vendor/idna/uts46data.py +891 -731
  57. pip/_vendor/packaging/__init__.py +1 -1
  58. pip/_vendor/packaging/_elffile.py +0 -1
  59. pip/_vendor/packaging/_manylinux.py +36 -36
  60. pip/_vendor/packaging/_musllinux.py +1 -1
  61. pip/_vendor/packaging/_parser.py +22 -10
  62. pip/_vendor/packaging/_structures.py +8 -0
  63. pip/_vendor/packaging/_tokenizer.py +23 -25
  64. pip/_vendor/packaging/licenses/__init__.py +13 -11
  65. pip/_vendor/packaging/licenses/_spdx.py +41 -1
  66. pip/_vendor/packaging/markers.py +64 -38
  67. pip/_vendor/packaging/metadata.py +143 -27
  68. pip/_vendor/packaging/pylock.py +635 -0
  69. pip/_vendor/packaging/requirements.py +5 -10
  70. pip/_vendor/packaging/specifiers.py +219 -170
  71. pip/_vendor/packaging/tags.py +15 -20
  72. pip/_vendor/packaging/utils.py +19 -24
  73. pip/_vendor/packaging/version.py +315 -105
  74. pip/_vendor/platformdirs/version.py +2 -2
  75. pip/_vendor/platformdirs/windows.py +7 -1
  76. pip/_vendor/vendor.txt +5 -5
  77. {pip-25.3.dist-info → pip-26.0.dist-info}/METADATA +2 -2
  78. {pip-25.3.dist-info → pip-26.0.dist-info}/RECORD +103 -100
  79. {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/AUTHORS.txt +18 -0
  80. {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/idna/LICENSE.md +1 -1
  81. pip/_internal/models/pylock.py +0 -188
  82. {pip-25.3.dist-info → pip-26.0.dist-info}/WHEEL +0 -0
  83. {pip-25.3.dist-info → pip-26.0.dist-info}/entry_points.txt +0 -0
  84. {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/LICENSE.txt +0 -0
  85. {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/cachecontrol/LICENSE.txt +0 -0
  86. {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/certifi/LICENSE +0 -0
  87. {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/dependency_groups/LICENSE.txt +0 -0
  88. {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/distlib/LICENSE.txt +0 -0
  89. {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/distro/LICENSE +0 -0
  90. {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/msgpack/COPYING +0 -0
  91. {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/packaging/LICENSE +0 -0
  92. {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/packaging/LICENSE.APACHE +0 -0
  93. {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/packaging/LICENSE.BSD +0 -0
  94. {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/pkg_resources/LICENSE +0 -0
  95. {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/platformdirs/LICENSE +0 -0
  96. {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/pygments/LICENSE +0 -0
  97. {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/pyproject_hooks/LICENSE +0 -0
  98. {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/requests/LICENSE +0 -0
  99. {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/resolvelib/LICENSE +0 -0
  100. {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/rich/LICENSE +0 -0
  101. {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/tomli/LICENSE +0 -0
  102. {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/tomli_w/LICENSE +0 -0
  103. {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/truststore/LICENSE +0 -0
  104. {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/urllib3/LICENSE.txt +0 -0
@@ -50,8 +50,8 @@ from pip._internal.utils.urls import url_to_path
50
50
  if TYPE_CHECKING:
51
51
  from ssl import SSLContext
52
52
 
53
+ from pip._vendor.urllib3 import ProxyManager
53
54
  from pip._vendor.urllib3.poolmanager import PoolManager
54
- from pip._vendor.urllib3.proxymanager import ProxyManager
55
55
 
56
56
 
57
57
  logger = logging.getLogger(__name__)
@@ -212,11 +212,12 @@ class LocalFSAdapter(BaseAdapter):
212
212
  self,
213
213
  request: PreparedRequest,
214
214
  stream: bool = False,
215
- timeout: float | tuple[float, float] | None = None,
215
+ timeout: float | tuple[float, float] | tuple[float, None] | None = None,
216
216
  verify: bool | str = True,
217
- cert: str | tuple[str, str] | None = None,
217
+ cert: bytes | str | tuple[bytes | str, bytes | str] | None = None,
218
218
  proxies: Mapping[str, str] | None = None,
219
219
  ) -> Response:
220
+ assert request.url is not None
220
221
  pathname = url_to_path(request.url)
221
222
 
222
223
  resp = Response()
@@ -237,13 +238,13 @@ class LocalFSAdapter(BaseAdapter):
237
238
  resp.headers = CaseInsensitiveDict(
238
239
  {
239
240
  "Content-Type": content_type,
240
- "Content-Length": stats.st_size,
241
+ "Content-Length": str(stats.st_size),
241
242
  "Last-Modified": modified,
242
243
  }
243
244
  )
244
245
 
245
246
  resp.raw = open(pathname, "rb")
246
- resp.close = resp.raw.close
247
+ resp.close = resp.raw.close # type: ignore[method-assign]
247
248
 
248
249
  return resp
249
250
 
@@ -277,7 +278,7 @@ class _SSLContextAdapterMixin:
277
278
  ) -> PoolManager:
278
279
  if self._ssl_context is not None:
279
280
  pool_kwargs.setdefault("ssl_context", self._ssl_context)
280
- return super().init_poolmanager( # type: ignore[misc]
281
+ return super().init_poolmanager( # type: ignore[misc, no-any-return]
281
282
  connections=connections,
282
283
  maxsize=maxsize,
283
284
  block=block,
@@ -289,7 +290,7 @@ class _SSLContextAdapterMixin:
289
290
  # context here too. https://github.com/pypa/pip/issues/13288
290
291
  if self._ssl_context is not None:
291
292
  proxy_kwargs.setdefault("ssl_context", self._ssl_context)
292
- return super().proxy_manager_for(proxy, **proxy_kwargs) # type: ignore[misc]
293
+ return super().proxy_manager_for(proxy, **proxy_kwargs) # type: ignore[misc, no-any-return]
293
294
 
294
295
 
295
296
  class HTTPAdapter(_SSLContextAdapterMixin, _BaseHTTPAdapter):
@@ -329,6 +330,7 @@ class PipSession(requests.Session):
329
330
  self,
330
331
  *args: Any,
331
332
  retries: int = 0,
333
+ resume_retries: int = 0,
332
334
  cache: str | None = None,
333
335
  trusted_hosts: Sequence[str] = (),
334
336
  index_urls: list[str] | None = None,
@@ -350,7 +352,7 @@ class PipSession(requests.Session):
350
352
  self.headers["User-Agent"] = user_agent()
351
353
 
352
354
  # Attach our Authentication handler to the session
353
- self.auth = MultiDomainBasicAuth(index_urls=index_urls)
355
+ self.auth: MultiDomainBasicAuth = MultiDomainBasicAuth(index_urls=index_urls)
354
356
 
355
357
  # Create our urllib3.Retry instance which will allow us to customize
356
358
  # how we handle retries.
@@ -370,6 +372,7 @@ class PipSession(requests.Session):
370
372
  # order to prevent hammering the service.
371
373
  backoff_factor=0.25,
372
374
  ) # type: ignore
375
+ self.resume_retries = resume_retries
373
376
 
374
377
  # Our Insecure HTTPAdapter disables HTTPS validation. It does not
375
378
  # support caching so we'll use it for all http:// URLs.
@@ -383,8 +386,9 @@ class PipSession(requests.Session):
383
386
  # we can't validate the response of an insecurely/untrusted fetched
384
387
  # origin, and we don't want someone to be able to poison the cache and
385
388
  # require manual eviction from the cache to fix it.
389
+ self._trusted_host_adapter: InsecureCacheControlAdapter | InsecureHTTPAdapter
386
390
  if cache:
387
- secure_adapter = CacheControlAdapter(
391
+ secure_adapter: _BaseHTTPAdapter = CacheControlAdapter(
388
392
  cache=SafeFileCache(cache),
389
393
  max_retries=retries,
390
394
  ssl_context=ssl_context,
@@ -518,7 +522,7 @@ class PipSession(requests.Session):
518
522
 
519
523
  return False
520
524
 
521
- def request(self, method: str, url: str, *args: Any, **kwargs: Any) -> Response:
525
+ def request(self, method: str, url: str, *args: Any, **kwargs: Any) -> Response: # type: ignore[override]
522
526
  # Allow setting a default timeout on a session
523
527
  kwargs.setdefault("timeout", self.timeout)
524
528
  # Allow setting a default proxies on a session
@@ -411,8 +411,7 @@ class PipScriptMaker(ScriptMaker):
411
411
  import sys
412
412
  from %(module)s import %(import_name)s
413
413
  if __name__ == '__main__':
414
- if sys.argv[0].endswith('.exe'):
415
- sys.argv[0] = sys.argv[0][:-4]
414
+ sys.argv[0] = sys.argv[0].removesuffix('.exe')
416
415
  sys.exit(%(func)s())
417
416
  """
418
417
  )
@@ -225,7 +225,7 @@ def _check_download_dir(
225
225
  class RequirementPreparer:
226
226
  """Prepares a Requirement"""
227
227
 
228
- def __init__( # noqa: PLR0913 (too many parameters)
228
+ def __init__(
229
229
  self,
230
230
  *,
231
231
  build_dir: str,
@@ -243,7 +243,6 @@ class RequirementPreparer:
243
243
  lazy_wheel: bool,
244
244
  verbosity: int,
245
245
  legacy_resolver: bool,
246
- resume_retries: int,
247
246
  ) -> None:
248
247
  super().__init__()
249
248
 
@@ -251,7 +250,7 @@ class RequirementPreparer:
251
250
  self.build_dir = build_dir
252
251
  self.build_tracker = build_tracker
253
252
  self._session = session
254
- self._download = Downloader(session, progress_bar, resume_retries)
253
+ self._download = Downloader(session, progress_bar)
255
254
  self.finder = finder
256
255
 
257
256
  # Where still-packed archives should be written to. If None, they are
@@ -258,8 +258,10 @@ def install_req_from_editable(
258
258
  permit_editable_wheels: bool = False,
259
259
  config_settings: dict[str, str | list[str]] | None = None,
260
260
  ) -> InstallRequirement:
261
- parts = parse_req_from_editable(editable_req)
261
+ if constraint:
262
+ raise InstallationError("Editable requirements are not allowed as constraints")
262
263
 
264
+ parts = parse_req_from_editable(editable_req)
263
265
  return InstallRequirement(
264
266
  parts.requirement,
265
267
  comes_from=comes_from,
@@ -0,0 +1,41 @@
1
+ import re
2
+ from typing import Any
3
+
4
+ from pip._internal.utils.compat import tomllib
5
+
6
+ REGEX = r"(?m)^# /// (?P<type>[a-zA-Z0-9-]+)$\s(?P<content>(^#(| .*)$\s)+)^# ///$"
7
+
8
+
9
+ class PEP723Exception(ValueError):
10
+ """Raised to indicate a problem when parsing PEP 723 metadata from a script"""
11
+
12
+ def __init__(self, msg: str) -> None:
13
+ self.msg = msg
14
+
15
+
16
+ def pep723_metadata(scriptfile: str) -> dict[str, Any]:
17
+ with open(scriptfile) as f:
18
+ script = f.read()
19
+
20
+ name = "script"
21
+ matches = list(
22
+ filter(lambda m: m.group("type") == name, re.finditer(REGEX, script))
23
+ )
24
+
25
+ if len(matches) > 1:
26
+ raise PEP723Exception(f"Multiple {name!r} blocks found in {scriptfile!r}")
27
+ elif len(matches) == 1:
28
+ content = "".join(
29
+ line[2:] if line.startswith("# ") else line[1:]
30
+ for line in matches[0].group("content").splitlines(keepends=True)
31
+ )
32
+ try:
33
+ metadata = tomllib.loads(content)
34
+ except Exception as exc:
35
+ raise PEP723Exception(f"Failed to parse TOML in {scriptfile!r}") from exc
36
+ else:
37
+ raise PEP723Exception(
38
+ f"File does not contain {name!r} metadata: {scriptfile!r}"
39
+ )
40
+
41
+ return metadata
@@ -25,6 +25,7 @@ from typing import (
25
25
 
26
26
  from pip._internal.cli import cmdoptions
27
27
  from pip._internal.exceptions import InstallationError, RequirementsFileParseError
28
+ from pip._internal.models.release_control import ReleaseControl
28
29
  from pip._internal.models.search_scope import SearchScope
29
30
 
30
31
  if TYPE_CHECKING:
@@ -59,6 +60,8 @@ SUPPORTED_OPTIONS: list[Callable[..., optparse.Option]] = [
59
60
  cmdoptions.prefer_binary,
60
61
  cmdoptions.require_hashes,
61
62
  cmdoptions.pre,
63
+ cmdoptions.all_releases,
64
+ cmdoptions.only_final,
62
65
  cmdoptions.trusted_host,
63
66
  cmdoptions.use_new_feature,
64
67
  ]
@@ -273,8 +276,14 @@ def handle_option_line(
273
276
  )
274
277
  finder.search_scope = search_scope
275
278
 
279
+ # Transform --pre into --all-releases :all:
276
280
  if opts.pre:
277
- finder.set_allow_all_prereleases()
281
+ if not opts.release_control:
282
+ opts.release_control = ReleaseControl()
283
+ opts.release_control.all_releases.add(":all:")
284
+
285
+ if opts.release_control:
286
+ finder.set_release_control(opts.release_control)
278
287
 
279
288
  if opts.prefer_binary:
280
289
  finder.set_prefer_binary()
@@ -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
  )
@@ -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"
@@ -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,9 +20,11 @@ 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,
@@ -50,18 +52,9 @@ def _get_statefile_name(key: str) -> str:
50
52
  return name
51
53
 
52
54
 
53
- def _convert_date(isodate: str) -> datetime.datetime:
54
- """Convert an ISO format string to a date.
55
-
56
- Handles the format 2020-01-22T14:24:01Z (trailing Z)
57
- which is not supported by older versions of fromisoformat.
58
- """
59
- return datetime.datetime.fromisoformat(isodate.replace("Z", "+00:00"))
60
-
61
-
62
55
  class SelfCheckState:
63
56
  def __init__(self, cache_dir: str) -> None:
64
- self._state: dict[str, Any] = {}
57
+ self._state: dict[str, str] = {}
65
58
  self._statefile_path = None
66
59
 
67
60
  # Try to load the existing state
@@ -93,7 +86,7 @@ class SelfCheckState:
93
86
  return None
94
87
 
95
88
  # Determine if we need to refresh the state
96
- last_check = _convert_date(self._state["last_check"])
89
+ last_check = parse_iso_datetime(self._state["last_check"])
97
90
  time_since_last_check = current_time - last_check
98
91
  if time_since_last_check > _WEEK:
99
92
  return None
@@ -187,7 +180,7 @@ def _get_current_remote_pip_version(
187
180
  # yanked version.
188
181
  selection_prefs = SelectionPreferences(
189
182
  allow_yanked=False,
190
- allow_all_prereleases=False, # Explicitly set to False
183
+ release_control=ReleaseControl(only_final={"pip"}),
191
184
  )
192
185
 
193
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
@@ -162,3 +163,41 @@ def copy_directory_permissions(directory: str, target_file: BinaryIO) -> None:
162
163
  os.chmod(target_file.fileno(), mode)
163
164
  elif os.chmod in os.supports_follow_symlinks:
164
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
+ )
@@ -83,7 +83,7 @@ def is_within_directory(directory: str, target: str) -> bool:
83
83
  abs_directory = os.path.abspath(directory)
84
84
  abs_target = os.path.abspath(target)
85
85
 
86
- prefix = os.path.commonprefix([abs_directory, abs_target])
86
+ prefix = os.path.commonpath([abs_directory, abs_target])
87
87
  return prefix == abs_directory
88
88
 
89
89
 
@@ -62,8 +62,9 @@ def make_vcs_requirement_url(
62
62
  repo_url: the remote VCS url, with any needed VCS prefix (e.g. "git+").
63
63
  project_name: the (unescaped) project name.
64
64
  """
65
+ quoted_rev = urllib.parse.quote(rev, "/")
65
66
  egg_project_name = project_name.replace("-", "_")
66
- req = f"{repo_url}@{rev}#egg={egg_project_name}"
67
+ req = f"{repo_url}@{quoted_rev}#egg={egg_project_name}"
67
68
  if subdir:
68
69
  req += f"&subdirectory={subdir}"
69
70
 
@@ -397,6 +398,7 @@ class VersionControl:
397
398
  "which is not supported. Include a revision after @ "
398
399
  "or remove @ from the URL."
399
400
  )
401
+ rev = urllib.parse.unquote(rev)
400
402
  url = urllib.parse.urlunsplit((scheme, netloc, path, query, ""))
401
403
  return url, rev, user_pass
402
404