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