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