1from __future__ import annotations
2
3import errno
4import getpass
5import hashlib
6import logging
7import os
8import pathlib
9import posixpath
10import shutil
11import stat
12import sys
13import sysconfig
14import urllib.parse
15from collections.abc import Callable, Generator, Iterable, Iterator, Mapping, Sequence
16from dataclasses import dataclass
17from functools import cache, partial, wraps
18from io import StringIO
19from itertools import filterfalse, tee, zip_longest
20from pathlib import Path
21from types import FunctionType, TracebackType
22from typing import (
23 Any,
24 BinaryIO,
25 NoReturn,
26 TextIO,
27 TypeVar,
28 cast,
29)
30
31from pip._vendor.packaging.requirements import Requirement
32from pip._vendor.pyproject_hooks import BackendUnavailable, BuildBackendHookCaller
33
34from pip import __file__ as pip_location
35from pip import __version__
36from pip._internal.exceptions import (
37 BackendUnavailableError,
38 CommandError,
39 ExternallyManagedEnvironment,
40)
41from pip._internal.locations import get_major_minor_version
42from pip._internal.utils.compat import WINDOWS
43from pip._internal.utils.retry import retry
44from pip._internal.utils.virtualenv import running_under_virtualenv
45
46__all__ = [
47 "rmtree",
48 "display_path",
49 "backup_dir",
50 "ask",
51 "splitext",
52 "format_size",
53 "is_installable_dir",
54 "normalize_path",
55 "renames",
56 "get_prog",
57 "ensure_dir",
58 "remove_auth_from_url",
59 "check_externally_managed",
60 "looks_like_ci",
61 "ConfiguredBuildBackendHookCaller",
62]
63
64logger = logging.getLogger(__name__)
65
66T = TypeVar("T")
67F = TypeVar("F", bound=Callable[..., Any])
68ExcInfo = tuple[type[BaseException], BaseException, TracebackType]
69VersionInfo = tuple[int, int, int]
70NetlocTuple = tuple[str, tuple[str | None, str | None]]
71OnExc = Callable[[FunctionType, Path, BaseException], Any]
72OnErr = Callable[[FunctionType, Path, ExcInfo], Any]
73
74FILE_CHUNK_SIZE = 1024 * 1024
75# These are environment variables present when running under various
76# CI systems. For each variable, some CI systems that use the variable
77# are indicated. The collection was chosen so that for each of a number
78# of popular systems, at least one of the environment variables is used.
79# This list is used to provide some indication of and lower bound for
80# CI traffic to PyPI. Thus, it is okay if the list is not comprehensive.
81# For more background, see: https://github.com/pypa/pip/issues/5499
82CI_ENVIRONMENT_VARIABLES = (
83 # Azure Pipelines
84 "BUILD_BUILDID",
85 # Jenkins
86 "BUILD_ID",
87 # AppVeyor, CircleCI, Codeship, Gitlab CI, Shippable, Travis CI
88 "CI",
89 # Explicit environment variable.
90 "PIP_IS_CI",
91)
92
93
94def get_pip_version() -> str:
95 pip_pkg_dir = os.path.join(os.path.dirname(__file__), "..", "..")
96 pip_pkg_dir = os.path.abspath(pip_pkg_dir)
97
98 return f"pip {__version__} from {pip_pkg_dir} (python {get_major_minor_version()})"
99
100
101@cache
102def running_under_zipapp() -> bool:
103 return not pathlib.Path(pip_location).resolve().parent.is_dir()
104
105
106def get_runnable_pip() -> str:
107 """Get a file to pass to a Python executable, to run the currently-running pip.
108
109 This is used to run a pip subprocess, for installing requirements into the build
110 environment.
111 """
112 source = pathlib.Path(pip_location).resolve().parent
113
114 if running_under_zipapp():
115 # This would happen if someone is using pip from inside a zip file. In that
116 # case, we can use that directly.
117 return str(source)
118
119 return os.fsdecode(source / "__pip-runner__.py")
120
121
122def normalize_version_info(py_version_info: tuple[int, ...]) -> tuple[int, int, int]:
123 """
124 Convert a tuple of ints representing a Python version to one of length
125 three.
126
127 :param py_version_info: a tuple of ints representing a Python version,
128 or None to specify no version. The tuple can have any length.
129
130 :return: a tuple of length three if `py_version_info` is non-None.
131 Otherwise, return `py_version_info` unchanged (i.e. None).
132 """
133 if len(py_version_info) < 3:
134 py_version_info += (3 - len(py_version_info)) * (0,)
135 elif len(py_version_info) > 3:
136 py_version_info = py_version_info[:3]
137
138 return cast("VersionInfo", py_version_info)
139
140
141def ensure_dir(path: str) -> None:
142 """os.path.makedirs without EEXIST."""
143 try:
144 os.makedirs(path)
145 except OSError as e:
146 # Windows can raise spurious ENOTEMPTY errors. See #6426.
147 if e.errno != errno.EEXIST and e.errno != errno.ENOTEMPTY:
148 raise
149
150
151def get_prog() -> str:
152 try:
153 prog = os.path.basename(sys.argv[0])
154 if prog in ("__main__.py", "-c"):
155 return f"{sys.executable} -m pip"
156 else:
157 return prog
158 except (AttributeError, TypeError, IndexError):
159 pass
160 return "pip"
161
162
163# Retry every half second for up to 3 seconds
164@retry(stop_after_delay=3, wait=0.5)
165def rmtree(dir: str, ignore_errors: bool = False, onexc: OnExc | None = None) -> None:
166 if ignore_errors:
167 onexc = _onerror_ignore
168 if onexc is None:
169 onexc = _onerror_reraise
170 handler: OnErr = partial(rmtree_errorhandler, onexc=onexc)
171 if sys.version_info >= (3, 12):
172 # See https://docs.python.org/3.12/whatsnew/3.12.html#shutil.
173 shutil.rmtree(dir, onexc=handler) # type: ignore
174 else:
175 shutil.rmtree(dir, onerror=handler) # type: ignore
176
177
178def _onerror_ignore(*_args: Any) -> None:
179 pass
180
181
182def _onerror_reraise(*_args: Any) -> None:
183 raise # noqa: PLE0704 - Bare exception used to reraise existing exception
184
185
186def rmtree_errorhandler(
187 func: FunctionType,
188 path: Path,
189 exc_info: ExcInfo | BaseException,
190 *,
191 onexc: OnExc = _onerror_reraise,
192) -> None:
193 """
194 `rmtree` error handler to 'force' a file remove (i.e. like `rm -f`).
195
196 * If a file is readonly then it's write flag is set and operation is
197 retried.
198
199 * `onerror` is the original callback from `rmtree(... onerror=onerror)`
200 that is chained at the end if the "rm -f" still fails.
201 """
202 try:
203 st_mode = os.stat(path).st_mode
204 except OSError:
205 # it's equivalent to os.path.exists
206 return
207
208 if not st_mode & stat.S_IWRITE:
209 # convert to read/write
210 try:
211 os.chmod(path, st_mode | stat.S_IWRITE)
212 except OSError:
213 pass
214 else:
215 # use the original function to repeat the operation
216 try:
217 func(path)
218 return
219 except OSError:
220 pass
221
222 if not isinstance(exc_info, BaseException):
223 _, exc_info, _ = exc_info
224 onexc(func, path, exc_info)
225
226
227def display_path(path: str) -> str:
228 """Gives the display value for a given path, making it relative to cwd
229 if possible."""
230 try:
231 relative = Path(path).relative_to(Path.cwd())
232 except ValueError:
233 # If the path isn't relative to the CWD, leave it alone
234 return path
235 return os.path.join(".", relative)
236
237
238def backup_dir(dir: str, ext: str = ".bak") -> str:
239 """Figure out the name of a directory to back up the given dir to
240 (adding .bak, .bak2, etc)"""
241 n = 1
242 extension = ext
243 while os.path.exists(dir + extension):
244 n += 1
245 extension = ext + str(n)
246 return dir + extension
247
248
249def ask_path_exists(message: str, options: Iterable[str]) -> str:
250 for action in os.environ.get("PIP_EXISTS_ACTION", "").split():
251 if action in options:
252 return action
253 return ask(message, options)
254
255
256def _check_no_input(message: str) -> None:
257 """Raise an error if no input is allowed."""
258 if os.environ.get("PIP_NO_INPUT"):
259 raise Exception(
260 f"No input was expected ($PIP_NO_INPUT set); question: {message}"
261 )
262
263
264def ask(message: str, options: Iterable[str]) -> str:
265 """Ask the message interactively, with the given possible responses"""
266 while 1:
267 _check_no_input(message)
268 response = input(message)
269 response = response.strip().lower()
270 if response not in options:
271 print(
272 "Your response ({!r}) was not one of the expected responses: "
273 "{}".format(response, ", ".join(options))
274 )
275 else:
276 return response
277
278
279def ask_input(message: str) -> str:
280 """Ask for input interactively."""
281 _check_no_input(message)
282 return input(message)
283
284
285def ask_password(message: str) -> str:
286 """Ask for a password interactively."""
287 _check_no_input(message)
288 return getpass.getpass(message)
289
290
291def strtobool(val: str) -> int:
292 """Convert a string representation of truth to true (1) or false (0).
293
294 True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values
295 are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if
296 'val' is anything else.
297 """
298 val = val.lower()
299 if val in ("y", "yes", "t", "true", "on", "1"):
300 return 1
301 elif val in ("n", "no", "f", "false", "off", "0"):
302 return 0
303 else:
304 raise ValueError(f"invalid truth value {val!r}")
305
306
307def format_size(bytes: float) -> str:
308 if bytes > 1000 * 1000:
309 return f"{bytes / 1000.0 / 1000:.1f} MB"
310 elif bytes > 10 * 1000:
311 return f"{int(bytes / 1000)} kB"
312 elif bytes > 1000:
313 return f"{bytes / 1000.0:.1f} kB"
314 else:
315 return f"{int(bytes)} bytes"
316
317
318def tabulate(rows: Iterable[Iterable[Any]]) -> tuple[list[str], list[int]]:
319 """Return a list of formatted rows and a list of column sizes.
320
321 For example::
322
323 >>> tabulate([['foobar', 2000], [0xdeadbeef]])
324 (['foobar 2000', '3735928559'], [10, 4])
325 """
326 rows = [tuple(map(str, row)) for row in rows]
327 sizes = [max(map(len, col)) for col in zip_longest(*rows, fillvalue="")]
328 table = [" ".join(map(str.ljust, row, sizes)).rstrip() for row in rows]
329 return table, sizes
330
331
332def is_installable_dir(path: str) -> bool:
333 """Is path is a directory containing pyproject.toml or setup.py?
334
335 If pyproject.toml exists, this is a PEP 517 project. Otherwise we look for
336 a legacy setuptools layout by identifying setup.py. We don't check for the
337 setup.cfg because using it without setup.py is only available for PEP 517
338 projects, which are already covered by the pyproject.toml check.
339 """
340 if not os.path.isdir(path):
341 return False
342 if os.path.isfile(os.path.join(path, "pyproject.toml")):
343 return True
344 if os.path.isfile(os.path.join(path, "setup.py")):
345 return True
346 return False
347
348
349def read_chunks(
350 file: BinaryIO, size: int = FILE_CHUNK_SIZE
351) -> Generator[bytes, None, None]:
352 """Yield pieces of data from a file-like object until EOF."""
353 while True:
354 chunk = file.read(size)
355 if not chunk:
356 break
357 yield chunk
358
359
360def normalize_path(path: str, resolve_symlinks: bool = True) -> str:
361 """
362 Convert a path to its canonical, case-normalized, absolute version.
363
364 """
365 path = os.path.expanduser(path)
366 if resolve_symlinks:
367 path = os.path.realpath(path)
368 else:
369 path = os.path.abspath(path)
370 return os.path.normcase(path)
371
372
373def splitext(path: str) -> tuple[str, str]:
374 """Like os.path.splitext, but take off .tar too"""
375 base, ext = posixpath.splitext(path)
376 if base.lower().endswith(".tar"):
377 ext = base[-4:] + ext
378 base = base[:-4]
379 return base, ext
380
381
382def renames(old: str, new: str) -> None:
383 """Like os.renames(), but handles renaming across devices."""
384 # Implementation borrowed from os.renames().
385 head, tail = os.path.split(new)
386 if head and tail and not os.path.exists(head):
387 os.makedirs(head)
388
389 shutil.move(old, new)
390
391 head, tail = os.path.split(old)
392 if head and tail:
393 try:
394 os.removedirs(head)
395 except OSError:
396 pass
397
398
399def is_local(path: str) -> bool:
400 """
401 Return True if path is within sys.prefix, if we're running in a virtualenv.
402
403 If we're not in a virtualenv, all paths are considered "local."
404
405 Caution: this function assumes the head of path has been normalized
406 with normalize_path.
407 """
408 if not running_under_virtualenv():
409 return True
410 return path.startswith(normalize_path(sys.prefix))
411
412
413def write_output(msg: Any, *args: Any) -> None:
414 logger.info(msg, *args)
415
416
417class StreamWrapper(StringIO):
418 orig_stream: TextIO
419
420 @classmethod
421 def from_stream(cls, orig_stream: TextIO) -> StreamWrapper:
422 ret = cls()
423 ret.orig_stream = orig_stream
424 return ret
425
426 # compileall.compile_dir() needs stdout.encoding to print to stdout
427 # type ignore is because TextIOBase.encoding is writeable
428 @property
429 def encoding(self) -> str: # type: ignore
430 return self.orig_stream.encoding
431
432
433# Simulates an enum
434def enum(*sequential: Any, **named: Any) -> type[Any]:
435 enums = dict(zip(sequential, range(len(sequential))), **named)
436 reverse = {value: key for key, value in enums.items()}
437 enums["reverse_mapping"] = reverse
438 return type("Enum", (), enums)
439
440
441def build_netloc(host: str, port: int | None) -> str:
442 """
443 Build a netloc from a host-port pair
444 """
445 if port is None:
446 return host
447 if ":" in host:
448 # Only wrap host with square brackets when it is IPv6
449 host = f"[{host}]"
450 return f"{host}:{port}"
451
452
453def build_url_from_netloc(netloc: str, scheme: str = "https") -> str:
454 """
455 Build a full URL from a netloc.
456 """
457 if netloc.count(":") >= 2 and "@" not in netloc and "[" not in netloc:
458 # It must be a bare IPv6 address, so wrap it with brackets.
459 netloc = f"[{netloc}]"
460 return f"{scheme}://{netloc}"
461
462
463def parse_netloc(netloc: str) -> tuple[str | None, int | None]:
464 """
465 Return the host-port pair from a netloc.
466 """
467 url = build_url_from_netloc(netloc)
468 parsed = urllib.parse.urlparse(url)
469 return parsed.hostname, parsed.port
470
471
472def split_auth_from_netloc(netloc: str) -> NetlocTuple:
473 """
474 Parse out and remove the auth information from a netloc.
475
476 Returns: (netloc, (username, password)).
477 """
478 if "@" not in netloc:
479 return netloc, (None, None)
480
481 # Split from the right because that's how urllib.parse.urlsplit()
482 # behaves if more than one @ is present (which can be checked using
483 # the password attribute of urlsplit()'s return value).
484 auth, netloc = netloc.rsplit("@", 1)
485 pw: str | None = None
486 if ":" in auth:
487 # Split from the left because that's how urllib.parse.urlsplit()
488 # behaves if more than one : is present (which again can be checked
489 # using the password attribute of the return value)
490 user, pw = auth.split(":", 1)
491 else:
492 user, pw = auth, None
493
494 user = urllib.parse.unquote(user)
495 if pw is not None:
496 pw = urllib.parse.unquote(pw)
497
498 return netloc, (user, pw)
499
500
501def redact_netloc(netloc: str) -> str:
502 """
503 Replace the sensitive data in a netloc with "****", if it exists.
504
505 For example:
506 - "user:pass@example.com" returns "user:****@example.com"
507 - "accesstoken@example.com" returns "****@example.com"
508 """
509 netloc, (user, password) = split_auth_from_netloc(netloc)
510 if user is None:
511 return netloc
512 if password is None:
513 user = "****"
514 password = ""
515 else:
516 user = urllib.parse.quote(user)
517 password = ":****"
518 return f"{user}{password}@{netloc}"
519
520
521def _transform_url(
522 url: str, transform_netloc: Callable[[str], tuple[Any, ...]]
523) -> tuple[str, NetlocTuple]:
524 """Transform and replace netloc in a url.
525
526 transform_netloc is a function taking the netloc and returning a
527 tuple. The first element of this tuple is the new netloc. The
528 entire tuple is returned.
529
530 Returns a tuple containing the transformed url as item 0 and the
531 original tuple returned by transform_netloc as item 1.
532 """
533 purl = urllib.parse.urlsplit(url)
534 netloc_tuple = transform_netloc(purl.netloc)
535 # stripped url
536 url_pieces = (purl.scheme, netloc_tuple[0], purl.path, purl.query, purl.fragment)
537 surl = urllib.parse.urlunsplit(url_pieces)
538 return surl, cast("NetlocTuple", netloc_tuple)
539
540
541def _get_netloc(netloc: str) -> NetlocTuple:
542 return split_auth_from_netloc(netloc)
543
544
545def _redact_netloc(netloc: str) -> tuple[str]:
546 return (redact_netloc(netloc),)
547
548
549def split_auth_netloc_from_url(
550 url: str,
551) -> tuple[str, str, tuple[str | None, str | None]]:
552 """
553 Parse a url into separate netloc, auth, and url with no auth.
554
555 Returns: (url_without_auth, netloc, (username, password))
556 """
557 url_without_auth, (netloc, auth) = _transform_url(url, _get_netloc)
558 return url_without_auth, netloc, auth
559
560
561def remove_auth_from_url(url: str) -> str:
562 """Return a copy of url with 'username:password@' removed."""
563 # username/pass params are passed to subversion through flags
564 # and are not recognized in the url.
565 return _transform_url(url, _get_netloc)[0]
566
567
568def redact_auth_from_url(url: str) -> str:
569 """Replace the password in a given url with ****."""
570 return _transform_url(url, _redact_netloc)[0]
571
572
573def redact_auth_from_requirement(req: Requirement) -> str:
574 """Replace the password in a given requirement url with ****."""
575 if not req.url:
576 return str(req)
577 return str(req).replace(req.url, redact_auth_from_url(req.url))
578
579
580@dataclass(frozen=True)
581class HiddenText:
582 secret: str
583 redacted: str
584
585 def __repr__(self) -> str:
586 return f"<HiddenText {str(self)!r}>"
587
588 def __str__(self) -> str:
589 return self.redacted
590
591 def __eq__(self, other: object) -> bool:
592 # Equality is particularly useful for testing.
593 if type(self) is type(other):
594 # The string being used for redaction doesn't also have to match,
595 # just the raw, original string.
596 return self.secret == other.secret
597 return NotImplemented
598
599 # Disable hashing, since we have a custom __eq__ and don't need hash-ability
600 # (yet). The only required property of hashing is that objects which compare
601 # equal have the same hash value.
602 __hash__ = None # type: ignore[assignment]
603
604
605def hide_value(value: str) -> HiddenText:
606 return HiddenText(value, redacted="****")
607
608
609def hide_url(url: str) -> HiddenText:
610 redacted = redact_auth_from_url(url)
611 return HiddenText(url, redacted=redacted)
612
613
614def protect_pip_from_modification_on_windows(modifying_pip: bool) -> None:
615 """Protection of pip.exe from modification on Windows
616
617 On Windows, any operation modifying pip should be run as:
618 python -m pip ...
619 """
620 pip_names = [
621 "pip",
622 f"pip{sys.version_info.major}",
623 f"pip{sys.version_info.major}.{sys.version_info.minor}",
624 ]
625
626 # See https://github.com/pypa/pip/issues/1299 for more discussion
627 should_show_use_python_msg = (
628 modifying_pip and WINDOWS and os.path.basename(sys.argv[0]) in pip_names
629 )
630
631 if should_show_use_python_msg:
632 new_command = [sys.executable, "-m", "pip"] + sys.argv[1:]
633 raise CommandError(
634 "To modify pip, please run the following command:\n{}".format(
635 " ".join(new_command)
636 )
637 )
638
639
640def check_externally_managed() -> None:
641 """Check whether the current environment is externally managed.
642
643 If the ``EXTERNALLY-MANAGED`` config file is found, the current environment
644 is considered externally managed, and an ExternallyManagedEnvironment is
645 raised.
646 """
647 if running_under_virtualenv():
648 return
649 marker = os.path.join(sysconfig.get_path("stdlib"), "EXTERNALLY-MANAGED")
650 if not os.path.isfile(marker):
651 return
652 raise ExternallyManagedEnvironment.from_config(marker)
653
654
655def is_console_interactive() -> bool:
656 """Is this console interactive?"""
657 return sys.stdin is not None and sys.stdin.isatty()
658
659
660def hash_file(path: str, blocksize: int = 1 << 20) -> tuple[Any, int]:
661 """Return (hash, length) for path using hashlib.sha256()"""
662
663 h = hashlib.sha256()
664 length = 0
665 with open(path, "rb") as f:
666 for block in read_chunks(f, size=blocksize):
667 length += len(block)
668 h.update(block)
669 return h, length
670
671
672def pairwise(iterable: Iterable[Any]) -> Iterator[tuple[Any, Any]]:
673 """
674 Return paired elements.
675
676 For example:
677 s -> (s0, s1), (s2, s3), (s4, s5), ...
678 """
679 iterable = iter(iterable)
680 return zip_longest(iterable, iterable)
681
682
683def partition(
684 pred: Callable[[T], bool], iterable: Iterable[T]
685) -> tuple[Iterable[T], Iterable[T]]:
686 """
687 Use a predicate to partition entries into false entries and true entries,
688 like
689
690 partition(is_odd, range(10)) --> 0 2 4 6 8 and 1 3 5 7 9
691 """
692 t1, t2 = tee(iterable)
693 return filterfalse(pred, t1), filter(pred, t2)
694
695
696def _handle_backend_unavailable(func: F) -> F:
697 @wraps(func)
698 def wrapper(
699 self: ConfiguredBuildBackendHookCaller,
700 *args: Any,
701 **kwargs: Any,
702 ) -> Any:
703 try:
704 return func(self, *args, **kwargs)
705 except BackendUnavailable as error:
706 self._raise_backend_unavailable(func.__name__, error)
707
708 return cast(F, wrapper)
709
710
711class ConfiguredBuildBackendHookCaller(BuildBackendHookCaller):
712 def __init__(
713 self,
714 config_holder: Any,
715 source_dir: str,
716 build_backend: str,
717 backend_path: str | None = None,
718 runner: Callable[..., None] | None = None,
719 python_executable: str | None = None,
720 ):
721 super().__init__(
722 source_dir, build_backend, backend_path, runner, python_executable
723 )
724 self.config_holder = config_holder
725
726 def _raise_backend_unavailable(
727 self, hook_name: str, error: BackendUnavailable
728 ) -> NoReturn:
729 backend_error = (
730 error.traceback.strip().splitlines()[-1] if error.traceback else str(error)
731 )
732 exception = BackendUnavailableError(
733 hook_name=hook_name,
734 backend_name=self.build_backend,
735 backend_error=backend_error,
736 )
737 logger.error("%s", exception, extra={"rich": True})
738 raise exception from error
739
740 @_handle_backend_unavailable
741 def supports_feature(self, feature: str) -> bool:
742 return feature in self._supported_features()
743
744 @_handle_backend_unavailable
745 def build_wheel(
746 self,
747 wheel_directory: str,
748 config_settings: Mapping[str, Any] | None = None,
749 metadata_directory: str | None = None,
750 ) -> str:
751 cs = self.config_holder.config_settings
752 return super().build_wheel(
753 wheel_directory, config_settings=cs, metadata_directory=metadata_directory
754 )
755
756 @_handle_backend_unavailable
757 def build_sdist(
758 self,
759 sdist_directory: str,
760 config_settings: Mapping[str, Any] | None = None,
761 ) -> str:
762 cs = self.config_holder.config_settings
763 return super().build_sdist(sdist_directory, config_settings=cs)
764
765 @_handle_backend_unavailable
766 def build_editable(
767 self,
768 wheel_directory: str,
769 config_settings: Mapping[str, Any] | None = None,
770 metadata_directory: str | None = None,
771 ) -> str:
772 cs = self.config_holder.config_settings
773 return super().build_editable(
774 wheel_directory, config_settings=cs, metadata_directory=metadata_directory
775 )
776
777 @_handle_backend_unavailable
778 def get_requires_for_build_wheel(
779 self, config_settings: Mapping[str, Any] | None = None
780 ) -> Sequence[str]:
781 cs = self.config_holder.config_settings
782 return super().get_requires_for_build_wheel(config_settings=cs)
783
784 @_handle_backend_unavailable
785 def get_requires_for_build_sdist(
786 self, config_settings: Mapping[str, Any] | None = None
787 ) -> Sequence[str]:
788 cs = self.config_holder.config_settings
789 return super().get_requires_for_build_sdist(config_settings=cs)
790
791 @_handle_backend_unavailable
792 def get_requires_for_build_editable(
793 self, config_settings: Mapping[str, Any] | None = None
794 ) -> Sequence[str]:
795 cs = self.config_holder.config_settings
796 return super().get_requires_for_build_editable(config_settings=cs)
797
798 @_handle_backend_unavailable
799 def prepare_metadata_for_build_wheel(
800 self,
801 metadata_directory: str,
802 config_settings: Mapping[str, Any] | None = None,
803 _allow_fallback: bool = True,
804 ) -> str:
805 cs = self.config_holder.config_settings
806 return super().prepare_metadata_for_build_wheel(
807 metadata_directory=metadata_directory,
808 config_settings=cs,
809 _allow_fallback=_allow_fallback,
810 )
811
812 @_handle_backend_unavailable
813 def prepare_metadata_for_build_editable(
814 self,
815 metadata_directory: str,
816 config_settings: Mapping[str, Any] | None = None,
817 _allow_fallback: bool = True,
818 ) -> str | None:
819 cs = self.config_holder.config_settings
820 return super().prepare_metadata_for_build_editable(
821 metadata_directory=metadata_directory,
822 config_settings=cs,
823 _allow_fallback=_allow_fallback,
824 )
825
826
827def warn_if_run_as_root() -> None:
828 """Output a warning for sudo users on Unix.
829
830 In a virtual environment, sudo pip still writes to virtualenv.
831 On Windows, users may run pip as Administrator without issues.
832 This warning only applies to Unix root users outside of virtualenv.
833 """
834 if running_under_virtualenv():
835 return
836 if not hasattr(os, "getuid"):
837 return
838 # On Windows, there are no "system managed" Python packages. Installing as
839 # Administrator via pip is the correct way of updating system environments.
840 #
841 # We choose sys.platform over utils.compat.WINDOWS here to enable Mypy platform
842 # checks: https://mypy.readthedocs.io/en/stable/common_issues.html
843 if sys.platform == "win32" or sys.platform == "cygwin":
844 return
845
846 if os.getuid() != 0:
847 return
848
849 logger.warning(
850 "Running pip as the 'root' user can result in broken permissions and "
851 "conflicting behaviour with the system package manager, possibly "
852 "rendering your system unusable. "
853 "It is recommended to use a virtual environment instead: "
854 "https://pip.pypa.io/warnings/venv. "
855 "Use the --root-user-action option if you know what you are doing and "
856 "want to suppress this warning."
857 )
858
859
860def looks_like_ci() -> bool:
861 """
862 Return whether it looks like pip is running under CI.
863 """
864 # We don't use the method of checking for a tty (e.g. using isatty())
865 # because some CI systems mimic a tty (e.g. Travis CI). Thus that
866 # method doesn't provide definitive information in either direction.
867 return any(name in os.environ for name in CI_ENVIRONMENT_VARIABLES)