[pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci
This commit is contained in:
pre-commit-ci[bot] 2024-04-13 00:00:18 +00:00
parent 72ad6dc953
commit f4cd1ba0d6
813 changed files with 66015 additions and 58839 deletions

View file

@ -1,9 +1,12 @@
from typing import List, Optional
from __future__ import annotations
__version__ = "22.0.4"
from typing import List
from typing import Optional
__version__ = '22.0.4'
def main(args: Optional[List[str]] = None) -> int:
def main(args: list[str] | None = None) -> int:
"""This is an internal API only meant for use by pip's own console scripts.
For additional details, see https://github.com/pypa/pip/issues/7498.

View file

@ -1,3 +1,5 @@
from __future__ import annotations
import os
import sys
import warnings
@ -6,12 +8,12 @@ import warnings
# of sys.path, if present to avoid using current directory
# in pip commands check, freeze, install, list and show,
# when invoked as python -m pip <command>
if sys.path[0] in ("", os.getcwd()):
if sys.path[0] in ('', os.getcwd()):
sys.path.pop(0)
# If we are running from a wheel, add the wheel to sys.path
# This allows the usage python pip-*.whl/pip install pip-*.whl
if __package__ == "":
if __package__ == '':
# __file__ is pip-*.whl/pip/__main__.py
# first dirname call strips of '/__main__.py', second strips off '/pip'
# Resulting path is the name of the wheel itself
@ -19,12 +21,12 @@ if __package__ == "":
path = os.path.dirname(os.path.dirname(__file__))
sys.path.insert(0, path)
if __name__ == "__main__":
if __name__ == '__main__':
# Work around the error reported in #9540, pending a proper fix.
# Note: It is essential the warning filter is set *before* importing
# pip, as the deprecation happens at import time, not runtime.
warnings.filterwarnings(
"ignore", category=DeprecationWarning, module=".*packaging\\.version"
'ignore', category=DeprecationWarning, module='.*packaging\\.version',
)
from pip._internal.cli.main import main as _main

View file

@ -1,4 +1,7 @@
from typing import List, Optional
from __future__ import annotations
from typing import List
from typing import Optional
import pip._internal.utils.inject_securetransport # noqa
from pip._internal.utils import _log
@ -8,7 +11,7 @@ from pip._internal.utils import _log
_log.init_logging()
def main(args: (Optional[List[str]]) = None) -> int:
def main(args: (list[str] | None) = None) -> int:
"""This is preserved for old console scripts that may still be referencing
it.

View file

@ -1,5 +1,6 @@
"""Build Environment used for isolation during sdist building
"""
from __future__ import annotations
import contextlib
import logging
@ -11,18 +12,27 @@ import zipfile
from collections import OrderedDict
from sysconfig import get_paths
from types import TracebackType
from typing import TYPE_CHECKING, Iterable, Iterator, List, Optional, Set, Tuple, Type
from pip._vendor.certifi import where
from pip._vendor.packaging.requirements import Requirement
from pip._vendor.packaging.version import Version
from typing import Iterable
from typing import Iterator
from typing import List
from typing import Optional
from typing import Set
from typing import Tuple
from typing import Type
from typing import TYPE_CHECKING
from pip import __file__ as pip_location
from pip._internal.cli.spinners import open_spinner
from pip._internal.locations import get_platlib, get_prefixed_libs, get_purelib
from pip._internal.locations import get_platlib
from pip._internal.locations import get_prefixed_libs
from pip._internal.locations import get_purelib
from pip._internal.metadata import get_environment
from pip._internal.utils.subprocess import call_subprocess
from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds
from pip._internal.utils.temp_dir import tempdir_kinds
from pip._internal.utils.temp_dir import TempDirectory
from pip._vendor.certifi import where
from pip._vendor.packaging.requirements import Requirement
from pip._vendor.packaging.version import Version
if TYPE_CHECKING:
from pip._internal.index.package_finder import PackageFinder
@ -35,9 +45,9 @@ class _Prefix:
self.path = path
self.setup = False
self.bin_dir = get_paths(
"nt" if os.name == "nt" else "posix_prefix",
vars={"base": path, "platbase": path},
)["scripts"]
'nt' if os.name == 'nt' else 'posix_prefix',
vars={'base': path, 'platbase': path},
)['scripts']
self.lib_dirs = get_prefixed_libs(path)
@ -56,15 +66,14 @@ def _create_standalone_pip() -> Iterator[str]:
yield str(source)
return
with TempDirectory(kind="standalone-pip") as tmp_dir:
pip_zip = os.path.join(tmp_dir.path, "__env_pip__.zip")
with TempDirectory(kind='standalone-pip') as tmp_dir:
pip_zip = os.path.join(tmp_dir.path, '__env_pip__.zip')
kwargs = {}
if sys.version_info >= (3, 8):
kwargs["strict_timestamps"] = False
with zipfile.ZipFile(pip_zip, "w", **kwargs) as zf:
for child in source.rglob("*"):
kwargs['strict_timestamps'] = False
with zipfile.ZipFile(pip_zip, 'w', **kwargs) as zf:
for child in source.rglob('*'):
zf.write(child, child.relative_to(source.parent).as_posix())
yield os.path.join(pip_zip, "pip")
yield os.path.join(pip_zip, 'pip')
class BuildEnvironment:
@ -75,11 +84,11 @@ class BuildEnvironment:
self._prefixes = OrderedDict(
(name, _Prefix(os.path.join(temp_dir.path, name)))
for name in ("normal", "overlay")
for name in ('normal', 'overlay')
)
self._bin_dirs: List[str] = []
self._lib_dirs: List[str] = []
self._bin_dirs: list[str] = []
self._lib_dirs: list[str] = []
for prefix in reversed(list(self._prefixes.values())):
self._bin_dirs.append(prefix.bin_dir)
self._lib_dirs.extend(prefix.lib_dirs)
@ -90,11 +99,11 @@ class BuildEnvironment:
system_sites = {
os.path.normcase(site) for site in (get_purelib(), get_platlib())
}
self._site_dir = os.path.join(temp_dir.path, "site")
self._site_dir = os.path.join(temp_dir.path, 'site')
if not os.path.exists(self._site_dir):
os.mkdir(self._site_dir)
with open(
os.path.join(self._site_dir, "sitecustomize.py"), "w", encoding="utf-8"
os.path.join(self._site_dir, 'sitecustomize.py'), 'w', encoding='utf-8',
) as fp:
fp.write(
textwrap.dedent(
@ -121,18 +130,18 @@ class BuildEnvironment:
for path in {lib_dirs!r}:
assert not path in sys.path
site.addsitedir(path)
"""
).format(system_sites=system_sites, lib_dirs=self._lib_dirs)
""",
).format(system_sites=system_sites, lib_dirs=self._lib_dirs),
)
def __enter__(self) -> None:
self._save_env = {
name: os.environ.get(name, None)
for name in ("PATH", "PYTHONNOUSERSITE", "PYTHONPATH")
for name in ('PATH', 'PYTHONNOUSERSITE', 'PYTHONPATH')
}
path = self._bin_dirs[:]
old_path = self._save_env["PATH"]
old_path = self._save_env['PATH']
if old_path:
path.extend(old_path.split(os.pathsep))
@ -140,17 +149,17 @@ class BuildEnvironment:
os.environ.update(
{
"PATH": os.pathsep.join(path),
"PYTHONNOUSERSITE": "1",
"PYTHONPATH": os.pathsep.join(pythonpath),
}
'PATH': os.pathsep.join(path),
'PYTHONNOUSERSITE': '1',
'PYTHONPATH': os.pathsep.join(pythonpath),
},
)
def __exit__(
self,
exc_type: Optional[Type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[TracebackType],
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
for varname, old_value in self._save_env.items():
if old_value is None:
@ -159,8 +168,8 @@ class BuildEnvironment:
os.environ[varname] = old_value
def check_requirements(
self, reqs: Iterable[str]
) -> Tuple[Set[Tuple[str, str]], Set[str]]:
self, reqs: Iterable[str],
) -> tuple[set[tuple[str, str]], set[str]]:
"""Return 2 sets:
- conflicting requirements: set of (installed, wanted) reqs tuples
- missing requirements: set of reqs
@ -176,9 +185,9 @@ class BuildEnvironment:
missing.add(req_str)
continue
if isinstance(dist.version, Version):
installed_req_str = f"{req.name}=={dist.version}"
installed_req_str = f'{req.name}=={dist.version}'
else:
installed_req_str = f"{req.name}==={dist.version}"
installed_req_str = f'{req.name}==={dist.version}'
if dist.version not in req.specifier:
conflicting.add((installed_req_str, req_str))
# FIXME: Consider direct URL?
@ -186,7 +195,7 @@ class BuildEnvironment:
def install_requirements(
self,
finder: "PackageFinder",
finder: PackageFinder,
requirements: Iterable[str],
prefix_as_string: str,
*,
@ -210,56 +219,56 @@ class BuildEnvironment:
@staticmethod
def _install_requirements(
pip_runnable: str,
finder: "PackageFinder",
finder: PackageFinder,
requirements: Iterable[str],
prefix: _Prefix,
*,
kind: str,
) -> None:
args: List[str] = [
args: list[str] = [
sys.executable,
pip_runnable,
"install",
"--ignore-installed",
"--no-user",
"--prefix",
'install',
'--ignore-installed',
'--no-user',
'--prefix',
prefix.path,
"--no-warn-script-location",
'--no-warn-script-location',
]
if logger.getEffectiveLevel() <= logging.DEBUG:
args.append("-v")
for format_control in ("no_binary", "only_binary"):
args.append('-v')
for format_control in ('no_binary', 'only_binary'):
formats = getattr(finder.format_control, format_control)
args.extend(
(
"--" + format_control.replace("_", "-"),
",".join(sorted(formats or {":none:"})),
)
'--' + format_control.replace('_', '-'),
','.join(sorted(formats or {':none:'})),
),
)
index_urls = finder.index_urls
if index_urls:
args.extend(["-i", index_urls[0]])
args.extend(['-i', index_urls[0]])
for extra_index in index_urls[1:]:
args.extend(["--extra-index-url", extra_index])
args.extend(['--extra-index-url', extra_index])
else:
args.append("--no-index")
args.append('--no-index')
for link in finder.find_links:
args.extend(["--find-links", link])
args.extend(['--find-links', link])
for host in finder.trusted_hosts:
args.extend(["--trusted-host", host])
args.extend(['--trusted-host', host])
if finder.allow_all_prereleases:
args.append("--pre")
args.append('--pre')
if finder.prefer_binary:
args.append("--prefer-binary")
args.append("--")
args.append('--prefer-binary')
args.append('--')
args.extend(requirements)
extra_environ = {"_PIP_STANDALONE_CERT": where()}
with open_spinner(f"Installing {kind}") as spinner:
extra_environ = {'_PIP_STANDALONE_CERT': where()}
with open_spinner(f'Installing {kind}') as spinner:
call_subprocess(
args,
command_desc=f"pip subprocess to install {kind}",
command_desc=f'pip subprocess to install {kind}',
spinner=spinner,
extra_environ=extra_environ,
)
@ -276,9 +285,9 @@ class NoOpBuildEnvironment(BuildEnvironment):
def __exit__(
self,
exc_type: Optional[Type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[TracebackType],
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
pass
@ -287,7 +296,7 @@ class NoOpBuildEnvironment(BuildEnvironment):
def install_requirements(
self,
finder: "PackageFinder",
finder: PackageFinder,
requirements: Iterable[str],
prefix_as_string: str,
*,

View file

@ -1,29 +1,36 @@
"""Cache Management
"""
from __future__ import annotations
import hashlib
import json
import logging
import os
from typing import Any, Dict, List, Optional, Set
from pip._vendor.packaging.tags import Tag, interpreter_name, interpreter_version
from pip._vendor.packaging.utils import canonicalize_name
from typing import Any
from typing import Dict
from typing import List
from typing import Optional
from typing import Set
from pip._internal.exceptions import InvalidWheelFilename
from pip._internal.models.format_control import FormatControl
from pip._internal.models.link import Link
from pip._internal.models.wheel import Wheel
from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds
from pip._internal.utils.temp_dir import tempdir_kinds
from pip._internal.utils.temp_dir import TempDirectory
from pip._internal.utils.urls import path_to_url
from pip._vendor.packaging.tags import interpreter_name
from pip._vendor.packaging.tags import interpreter_version
from pip._vendor.packaging.tags import Tag
from pip._vendor.packaging.utils import canonicalize_name
logger = logging.getLogger(__name__)
def _hash_dict(d: Dict[str, str]) -> str:
def _hash_dict(d: dict[str, str]) -> str:
"""Return a stable sha224 of a dictionary."""
s = json.dumps(d, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
return hashlib.sha224(s.encode("ascii")).hexdigest()
s = json.dumps(d, sort_keys=True, separators=(',', ':'), ensure_ascii=True)
return hashlib.sha224(s.encode('ascii')).hexdigest()
class Cache:
@ -38,7 +45,7 @@ class Cache:
"""
def __init__(
self, cache_dir: str, format_control: FormatControl, allowed_formats: Set[str]
self, cache_dir: str, format_control: FormatControl, allowed_formats: set[str],
) -> None:
super().__init__()
assert not cache_dir or os.path.isabs(cache_dir)
@ -46,28 +53,28 @@ class Cache:
self.format_control = format_control
self.allowed_formats = allowed_formats
_valid_formats = {"source", "binary"}
_valid_formats = {'source', 'binary'}
assert self.allowed_formats.union(_valid_formats) == _valid_formats
def _get_cache_path_parts(self, link: Link) -> List[str]:
def _get_cache_path_parts(self, link: Link) -> list[str]:
"""Get parts of part that must be os.path.joined with cache_dir"""
# We want to generate an url to use as our cache key, we don't want to
# just re-use the URL because it might have other items in the fragment
# and we don't care about those.
key_parts = {"url": link.url_without_fragment}
key_parts = {'url': link.url_without_fragment}
if link.hash_name is not None and link.hash is not None:
key_parts[link.hash_name] = link.hash
if link.subdirectory_fragment:
key_parts["subdirectory"] = link.subdirectory_fragment
key_parts['subdirectory'] = link.subdirectory_fragment
# Include interpreter name, major and minor version in cache key
# to cope with ill-behaved sdists that build a different wheel
# depending on the python version their setup.py is being run on,
# and don't encode the difference in compatibility tags.
# https://github.com/pypa/pip/issues/7296
key_parts["interpreter_name"] = interpreter_name()
key_parts["interpreter_version"] = interpreter_version()
key_parts['interpreter_name'] = interpreter_name()
key_parts['interpreter_version'] = interpreter_version()
# Encode our key url with sha224, we'll use this because it has similar
# security properties to sha256, but with a shorter total output (and
@ -82,7 +89,7 @@ class Cache:
return parts
def _get_candidates(self, link: Link, canonical_package_name: str) -> List[Any]:
def _get_candidates(self, link: Link, canonical_package_name: str) -> list[Any]:
can_not_cache = not self.cache_dir or not canonical_package_name or not link
if can_not_cache:
return []
@ -105,8 +112,8 @@ class Cache:
def get(
self,
link: Link,
package_name: Optional[str],
supported_tags: List[Tag],
package_name: str | None,
supported_tags: list[Tag],
) -> Link:
"""Returns a link to a cached item if it exists, otherwise returns the
passed link.
@ -118,7 +125,7 @@ class SimpleWheelCache(Cache):
"""A cache of wheels for future installs."""
def __init__(self, cache_dir: str, format_control: FormatControl) -> None:
super().__init__(cache_dir, format_control, {"binary"})
super().__init__(cache_dir, format_control, {'binary'})
def get_path_for_link(self, link: Link) -> str:
"""Return a directory to store cached wheels for link
@ -138,13 +145,13 @@ class SimpleWheelCache(Cache):
parts = self._get_cache_path_parts(link)
assert self.cache_dir
# Store wheels within the root cache_dir
return os.path.join(self.cache_dir, "wheels", *parts)
return os.path.join(self.cache_dir, 'wheels', *parts)
def get(
self,
link: Link,
package_name: Optional[str],
supported_tags: List[Tag],
package_name: str | None,
supported_tags: list[Tag],
) -> Link:
candidates = []
@ -159,8 +166,8 @@ class SimpleWheelCache(Cache):
continue
if canonicalize_name(wheel.name) != canonical_package_name:
logger.debug(
"Ignoring cached wheel %s for %s as it "
"does not match the expected distribution name %s.",
'Ignoring cached wheel %s for %s as it '
'does not match the expected distribution name %s.',
wheel_name,
link,
package_name,
@ -174,7 +181,7 @@ class SimpleWheelCache(Cache):
wheel.support_index_min(supported_tags),
wheel_name,
wheel_dir,
)
),
)
if not candidates:
@ -214,7 +221,7 @@ class WheelCache(Cache):
"""
def __init__(self, cache_dir: str, format_control: FormatControl) -> None:
super().__init__(cache_dir, format_control, {"binary"})
super().__init__(cache_dir, format_control, {'binary'})
self._wheel_cache = SimpleWheelCache(cache_dir, format_control)
self._ephem_cache = EphemWheelCache(format_control)
@ -227,8 +234,8 @@ class WheelCache(Cache):
def get(
self,
link: Link,
package_name: Optional[str],
supported_tags: List[Tag],
package_name: str | None,
supported_tags: list[Tag],
) -> Link:
cache_entry = self.get_cache_entry(link, package_name, supported_tags)
if cache_entry is None:
@ -238,9 +245,9 @@ class WheelCache(Cache):
def get_cache_entry(
self,
link: Link,
package_name: Optional[str],
supported_tags: List[Tag],
) -> Optional[CacheEntry]:
package_name: str | None,
supported_tags: list[Tag],
) -> CacheEntry | None:
"""Returns a CacheEntry with a link to a cached item if it exists or
None. The cache entry indicates if the item was found in the persistent
or ephemeral cache.

View file

@ -1,4 +1,4 @@
"""Subpackage containing all of pip's command line interface related code
"""
# This file intentionally does not import submodules
from __future__ import annotations

View file

@ -1,35 +1,40 @@
"""Logic that powers autocompletion installed by ``pip completion``.
"""
from __future__ import annotations
import optparse
import os
import sys
from itertools import chain
from typing import Any, Iterable, List, Optional
from typing import Any
from typing import Iterable
from typing import List
from typing import Optional
from pip._internal.cli.main_parser import create_main_parser
from pip._internal.commands import commands_dict, create_command
from pip._internal.commands import commands_dict
from pip._internal.commands import create_command
from pip._internal.metadata import get_default_environment
def autocomplete() -> None:
"""Entry Point for completion of main and subcommand options."""
# Don't complete if user hasn't sourced bash_completion file.
if "PIP_AUTO_COMPLETE" not in os.environ:
if 'PIP_AUTO_COMPLETE' not in os.environ:
return
cwords = os.environ["COMP_WORDS"].split()[1:]
cword = int(os.environ["COMP_CWORD"])
cwords = os.environ['COMP_WORDS'].split()[1:]
cword = int(os.environ['COMP_CWORD'])
try:
current = cwords[cword - 1]
except IndexError:
current = ""
current = ''
parser = create_main_parser()
subcommands = list(commands_dict)
options = []
# subcommand
subcommand_name: Optional[str] = None
subcommand_name: str | None = None
for word in cwords:
if word in subcommands:
subcommand_name = word
@ -37,12 +42,12 @@ def autocomplete() -> None:
# subcommand options
if subcommand_name is not None:
# special case: 'help' subcommand has no options
if subcommand_name == "help":
if subcommand_name == 'help':
sys.exit(1)
# special case: list locally installed dists for show and uninstall
should_list_installed = not current.startswith("-") and subcommand_name in [
"show",
"uninstall",
should_list_installed = not current.startswith('-') and subcommand_name in [
'show',
'uninstall',
]
if should_list_installed:
env = get_default_environment()
@ -50,8 +55,8 @@ def autocomplete() -> None:
installed = [
dist.canonical_name
for dist in env.iter_installed_distributions(local_only=True)
if dist.canonical_name.startswith(lc)
and dist.canonical_name not in cwords[1:]
if dist.canonical_name.startswith(lc) and
dist.canonical_name not in cwords[1:]
]
# if there are no dists installed, fall back to option completion
if installed:
@ -60,10 +65,10 @@ def autocomplete() -> None:
sys.exit(1)
should_list_installables = (
not current.startswith("-") and subcommand_name == "install"
not current.startswith('-') and subcommand_name == 'install'
)
if should_list_installables:
for path in auto_complete_paths(current, "path"):
for path in auto_complete_paths(current, 'path'):
print(path)
sys.exit(1)
@ -75,7 +80,7 @@ def autocomplete() -> None:
options.append((opt_str, opt.nargs))
# filter out previously specified options from available options
prev_opts = [x.split("=")[0] for x in cwords[1 : cword - 1]]
prev_opts = [x.split('=')[0] for x in cwords[1: cword - 1]]
options = [(x, v) for (x, v) in options if x not in prev_opts]
# filter options by current input
options = [(k, v) for k, v in options if k.startswith(current)]
@ -93,8 +98,8 @@ def autocomplete() -> None:
for option in options:
opt_label = option[0]
# append '=' to options which require args
if option[1] and option[0][:2] == "--":
opt_label += "="
if option[1] and option[0][:2] == '--':
opt_label += '='
print(opt_label)
else:
# show main parser options only when necessary
@ -102,7 +107,7 @@ def autocomplete() -> None:
opts = [i.option_list for i in parser.option_groups]
opts.append(parser.option_list)
flattened_opts = chain.from_iterable(opts)
if current.startswith("-"):
if current.startswith('-'):
for opt in flattened_opts:
if opt.help != optparse.SUPPRESS_HELP:
subcommands += opt._long_opts + opt._short_opts
@ -112,13 +117,13 @@ def autocomplete() -> None:
if completion_type:
subcommands = list(auto_complete_paths(current, completion_type))
print(" ".join([x for x in subcommands if x.startswith(current)]))
print(' '.join([x for x in subcommands if x.startswith(current)]))
sys.exit(1)
def get_path_completion_type(
cwords: List[str], cword: int, opts: Iterable[Any]
) -> Optional[str]:
cwords: list[str], cword: int, opts: Iterable[Any],
) -> str | None:
"""Get the type of path completion (``file``, ``dir``, ``path`` or None)
:param cwords: same as the environmental variable ``COMP_WORDS``
@ -126,15 +131,15 @@ def get_path_completion_type(
:param opts: The available options to check
:return: path completion type (``file``, ``dir``, ``path`` or None)
"""
if cword < 2 or not cwords[cword - 2].startswith("-"):
if cword < 2 or not cwords[cword - 2].startswith('-'):
return None
for opt in opts:
if opt.help == optparse.SUPPRESS_HELP:
continue
for o in str(opt).split("/"):
if cwords[cword - 2].split("=")[0] == o:
for o in str(opt).split('/'):
if cwords[cword - 2].split('=')[0] == o:
if not opt.metavar or any(
x in ("path", "file", "dir") for x in opt.metavar.split("/")
x in ('path', 'file', 'dir') for x in opt.metavar.split('/')
):
return opt.metavar
return None
@ -165,7 +170,7 @@ def auto_complete_paths(current: str, completion_type: str) -> Iterable[str]:
# complete regular files when there is not ``<dir>`` after option
# complete directories when there is ``<file>``, ``<path>`` or
# ``<dir>``after option
if completion_type != "dir" and os.path.isfile(opt):
if completion_type != 'dir' and os.path.isfile(opt):
yield comp_file
elif os.path.isdir(opt):
yield os.path.join(comp_file, "")
yield os.path.join(comp_file, '')

View file

@ -1,49 +1,52 @@
"""Base Command class, and related routines"""
from __future__ import annotations
import functools
import logging
import logging.config
import optparse
import os
import sys
import traceback
from optparse import Values
from typing import Any, Callable, List, Optional, Tuple
from pip._vendor.rich import traceback as rich_traceback
from typing import Any
from typing import Callable
from typing import List
from typing import Optional
from typing import Tuple
from pip._internal.cli import cmdoptions
from pip._internal.cli.command_context import CommandContextMixIn
from pip._internal.cli.parser import ConfigOptionParser, UpdatingDefaultsHelpFormatter
from pip._internal.cli.status_codes import (
ERROR,
PREVIOUS_BUILD_DIR_ERROR,
UNKNOWN_ERROR,
VIRTUALENV_NOT_FOUND,
)
from pip._internal.exceptions import (
BadCommand,
CommandError,
DiagnosticPipError,
InstallationError,
NetworkConnectionError,
PreviousBuildDirError,
UninstallationError,
)
from pip._internal.cli.parser import ConfigOptionParser
from pip._internal.cli.parser import UpdatingDefaultsHelpFormatter
from pip._internal.cli.status_codes import ERROR
from pip._internal.cli.status_codes import PREVIOUS_BUILD_DIR_ERROR
from pip._internal.cli.status_codes import UNKNOWN_ERROR
from pip._internal.cli.status_codes import VIRTUALENV_NOT_FOUND
from pip._internal.exceptions import BadCommand
from pip._internal.exceptions import CommandError
from pip._internal.exceptions import DiagnosticPipError
from pip._internal.exceptions import InstallationError
from pip._internal.exceptions import NetworkConnectionError
from pip._internal.exceptions import PreviousBuildDirError
from pip._internal.exceptions import UninstallationError
from pip._internal.utils.filesystem import check_path_owner
from pip._internal.utils.logging import BrokenStdoutLoggingError, setup_logging
from pip._internal.utils.misc import get_prog, normalize_path
from pip._internal.utils.logging import BrokenStdoutLoggingError
from pip._internal.utils.logging import setup_logging
from pip._internal.utils.misc import get_prog
from pip._internal.utils.misc import normalize_path
from pip._internal.utils.temp_dir import global_tempdir_manager
from pip._internal.utils.temp_dir import tempdir_registry
from pip._internal.utils.temp_dir import TempDirectoryTypeRegistry as TempDirRegistry
from pip._internal.utils.temp_dir import global_tempdir_manager, tempdir_registry
from pip._internal.utils.virtualenv import running_under_virtualenv
from pip._vendor.rich import traceback as rich_traceback
__all__ = ["Command"]
__all__ = ['Command']
logger = logging.getLogger(__name__)
class Command(CommandContextMixIn):
usage: str = ""
usage: str = ''
ignore_require_venv: bool = False
def __init__(self, name: str, summary: str, isolated: bool = False) -> None:
@ -53,7 +56,7 @@ class Command(CommandContextMixIn):
self.summary = summary
self.parser = ConfigOptionParser(
usage=self.usage,
prog=f"{get_prog()} {name}",
prog=f'{get_prog()} {name}',
formatter=UpdatingDefaultsHelpFormatter(),
add_help_option=False,
name=name,
@ -61,10 +64,10 @@ class Command(CommandContextMixIn):
isolated=isolated,
)
self.tempdir_registry: Optional[TempDirRegistry] = None
self.tempdir_registry: TempDirRegistry | None = None
# Commands should add options to this option group
optgroup_name = f"{self.name.capitalize()} Options"
optgroup_name = f'{self.name.capitalize()} Options'
self.cmd_opts = optparse.OptionGroup(self.parser, optgroup_name)
# Add the general options
@ -86,23 +89,23 @@ class Command(CommandContextMixIn):
"""
# Make sure we do the pip version check if the index_group options
# are present.
assert not hasattr(options, "no_index")
assert not hasattr(options, 'no_index')
def run(self, options: Values, args: List[str]) -> int:
def run(self, options: Values, args: list[str]) -> int:
raise NotImplementedError
def parse_args(self, args: List[str]) -> Tuple[Values, List[str]]:
def parse_args(self, args: list[str]) -> tuple[Values, list[str]]:
# factored out for testability
return self.parser.parse_args(args)
def main(self, args: List[str]) -> int:
def main(self, args: list[str]) -> int:
try:
with self.main_context():
return self._main(args)
finally:
logging.shutdown()
def _main(self, args: List[str]) -> int:
def _main(self, args: list[str]) -> int:
# We must initialize this before the tempdir manager, otherwise the
# configuration would not be accessible by the time we clean up the
# tempdir manager.
@ -127,15 +130,15 @@ class Command(CommandContextMixIn):
# This also affects isolated builds and it should.
if options.no_input:
os.environ["PIP_NO_INPUT"] = "1"
os.environ['PIP_NO_INPUT'] = '1'
if options.exists_action:
os.environ["PIP_EXISTS_ACTION"] = " ".join(options.exists_action)
os.environ['PIP_EXISTS_ACTION'] = ' '.join(options.exists_action)
if options.require_venv and not self.ignore_require_venv:
# If a venv is required check if it can really be found
if not running_under_virtualenv():
logger.critical("Could not find an activated virtualenv (required).")
logger.critical('Could not find an activated virtualenv (required).')
sys.exit(VIRTUALENV_NOT_FOUND)
if options.cache_dir:
@ -143,23 +146,23 @@ class Command(CommandContextMixIn):
if not check_path_owner(options.cache_dir):
logger.warning(
"The directory '%s' or its parent directory is not owned "
"or is not writable by the current user. The cache "
"has been disabled. Check the permissions and owner of "
"that directory. If executing pip with sudo, you should "
'or is not writable by the current user. The cache '
'has been disabled. Check the permissions and owner of '
'that directory. If executing pip with sudo, you should '
"use sudo's -H flag.",
options.cache_dir,
)
options.cache_dir = None
if "2020-resolver" in options.features_enabled:
if '2020-resolver' in options.features_enabled:
logger.warning(
"--use-feature=2020-resolver no longer has any effect, "
"since it is now the default dependency resolver in pip. "
"This will become an error in pip 21.0."
'--use-feature=2020-resolver no longer has any effect, '
'since it is now the default dependency resolver in pip. '
'This will become an error in pip 21.0.',
)
def intercepts_unhandled_exc(
run_func: Callable[..., int]
run_func: Callable[..., int],
) -> Callable[..., int]:
@functools.wraps(run_func)
def exc_logging_wrapper(*args: Any) -> int:
@ -168,13 +171,13 @@ class Command(CommandContextMixIn):
assert isinstance(status, int)
return status
except DiagnosticPipError as exc:
logger.error("[present-diagnostic] %s", exc)
logger.debug("Exception information:", exc_info=True)
logger.error('[present-diagnostic] %s', exc)
logger.debug('Exception information:', exc_info=True)
return ERROR
except PreviousBuildDirError as exc:
logger.critical(str(exc))
logger.debug("Exception information:", exc_info=True)
logger.debug('Exception information:', exc_info=True)
return PREVIOUS_BUILD_DIR_ERROR
except (
@ -184,29 +187,29 @@ class Command(CommandContextMixIn):
NetworkConnectionError,
) as exc:
logger.critical(str(exc))
logger.debug("Exception information:", exc_info=True)
logger.debug('Exception information:', exc_info=True)
return ERROR
except CommandError as exc:
logger.critical("%s", exc)
logger.debug("Exception information:", exc_info=True)
logger.critical('%s', exc)
logger.debug('Exception information:', exc_info=True)
return ERROR
except BrokenStdoutLoggingError:
# Bypass our logger and write any remaining messages to
# stderr because stdout no longer works.
print("ERROR: Pipe to stdout was broken", file=sys.stderr)
print('ERROR: Pipe to stdout was broken', file=sys.stderr)
if level_number <= logging.DEBUG:
traceback.print_exc(file=sys.stderr)
return ERROR
except KeyboardInterrupt:
logger.critical("Operation cancelled by user")
logger.debug("Exception information:", exc_info=True)
logger.critical('Operation cancelled by user')
logger.debug('Exception information:', exc_info=True)
return ERROR
except BaseException:
logger.critical("Exception:", exc_info=True)
logger.critical('Exception:', exc_info=True)
return UNKNOWN_ERROR

View file

@ -1,7 +1,12 @@
from contextlib import ExitStack, contextmanager
from typing import ContextManager, Iterator, TypeVar
from __future__ import annotations
_T = TypeVar("_T", covariant=True)
from contextlib import contextmanager
from contextlib import ExitStack
from typing import ContextManager
from typing import Iterator
from typing import TypeVar
_T = TypeVar('_T', covariant=True)
class CommandContextMixIn:

View file

@ -1,10 +1,13 @@
"""Primary application entrypoint.
"""
from __future__ import annotations
import locale
import logging
import os
import sys
from typing import List, Optional
from typing import List
from typing import Optional
from pip._internal.cli.autocompletion import autocomplete
from pip._internal.cli.main_parser import parse_command
@ -42,7 +45,7 @@ logger = logging.getLogger(__name__)
# main, this should not be an issue in practice.
def main(args: Optional[List[str]] = None) -> int:
def main(args: list[str] | None = None) -> int:
if args is None:
args = sys.argv[1:]
@ -54,17 +57,17 @@ def main(args: Optional[List[str]] = None) -> int:
try:
cmd_name, cmd_args = parse_command(args)
except PipError as exc:
sys.stderr.write(f"ERROR: {exc}")
sys.stderr.write(f'ERROR: {exc}')
sys.stderr.write(os.linesep)
sys.exit(1)
# Needed for locale.getpreferredencoding(False) to work
# in pip._internal.utils.encoding.auto_decode
try:
locale.setlocale(locale.LC_ALL, "")
locale.setlocale(locale.LC_ALL, '')
except locale.Error as e:
# setlocale can apparently crash if locale are uninitialized
logger.debug("Ignoring error %s when setting locale", e)
command = create_command(cmd_name, isolated=("--isolated" in cmd_args))
logger.debug('Ignoring error %s when setting locale', e)
command = create_command(cmd_name, isolated=('--isolated' in cmd_args))
return command.main(cmd_args)

View file

@ -1,27 +1,32 @@
"""A single place for constructing and exposing the main parser
"""
from __future__ import annotations
import os
import sys
from typing import List, Tuple
from typing import List
from typing import Tuple
from pip._internal.cli import cmdoptions
from pip._internal.cli.parser import ConfigOptionParser, UpdatingDefaultsHelpFormatter
from pip._internal.commands import commands_dict, get_similar_commands
from pip._internal.cli.parser import ConfigOptionParser
from pip._internal.cli.parser import UpdatingDefaultsHelpFormatter
from pip._internal.commands import commands_dict
from pip._internal.commands import get_similar_commands
from pip._internal.exceptions import CommandError
from pip._internal.utils.misc import get_pip_version, get_prog
from pip._internal.utils.misc import get_pip_version
from pip._internal.utils.misc import get_prog
__all__ = ["create_main_parser", "parse_command"]
__all__ = ['create_main_parser', 'parse_command']
def create_main_parser() -> ConfigOptionParser:
"""Creates and returns the main parser for pip's CLI"""
parser = ConfigOptionParser(
usage="\n%prog <command> [options]",
usage='\n%prog <command> [options]',
add_help_option=False,
formatter=UpdatingDefaultsHelpFormatter(),
name="global",
name='global',
prog=get_prog(),
)
parser.disable_interspersed_args()
@ -36,16 +41,16 @@ def create_main_parser() -> ConfigOptionParser:
parser.main = True # type: ignore
# create command listing for description
description = [""] + [
f"{name:27} {command_info.summary}"
description = [''] + [
f'{name:27} {command_info.summary}'
for name, command_info in commands_dict.items()
]
parser.description = "\n".join(description)
parser.description = '\n'.join(description)
return parser
def parse_command(args: List[str]) -> Tuple[str, List[str]]:
def parse_command(args: list[str]) -> tuple[str, list[str]]:
parser = create_main_parser()
# Note: parser calls disable_interspersed_args(), so the result of this
@ -64,7 +69,7 @@ def parse_command(args: List[str]) -> Tuple[str, List[str]]:
sys.exit()
# pip || pip help -> print_help()
if not args_else or (args_else[0] == "help" and len(args_else) == 1):
if not args_else or (args_else[0] == 'help' and len(args_else) == 1):
parser.print_help()
sys.exit()
@ -78,7 +83,7 @@ def parse_command(args: List[str]) -> Tuple[str, List[str]]:
if guess:
msg.append(f'maybe you meant "{guess}"')
raise CommandError(" - ".join(msg))
raise CommandError(' - '.join(msg))
# all the args without the subcommand
cmd_args = args[:]

View file

@ -1,4 +1,5 @@
"""Base option parser setup"""
from __future__ import annotations
import logging
import optparse
@ -6,11 +7,17 @@ import shutil
import sys
import textwrap
from contextlib import suppress
from typing import Any, Dict, Iterator, List, Tuple
from typing import Any
from typing import Dict
from typing import Iterator
from typing import List
from typing import Tuple
from pip._internal.cli.status_codes import UNKNOWN_ERROR
from pip._internal.configuration import Configuration, ConfigurationError
from pip._internal.utils.misc import redact_auth_from_url, strtobool
from pip._internal.configuration import Configuration
from pip._internal.configuration import ConfigurationError
from pip._internal.utils.misc import redact_auth_from_url
from pip._internal.utils.misc import strtobool
logger = logging.getLogger(__name__)
@ -20,16 +27,16 @@ class PrettyHelpFormatter(optparse.IndentedHelpFormatter):
def __init__(self, *args: Any, **kwargs: Any) -> None:
# help position must be aligned with __init__.parseopts.description
kwargs["max_help_position"] = 30
kwargs["indent_increment"] = 1
kwargs["width"] = shutil.get_terminal_size()[0] - 2
kwargs['max_help_position'] = 30
kwargs['indent_increment'] = 1
kwargs['width'] = shutil.get_terminal_size()[0] - 2
super().__init__(*args, **kwargs)
def format_option_strings(self, option: optparse.Option) -> str:
return self._format_option_strings(option)
def _format_option_strings(
self, option: optparse.Option, mvarfmt: str = " <{}>", optsep: str = ", "
self, option: optparse.Option, mvarfmt: str = ' <{}>', optsep: str = ', ',
) -> str:
"""
Return a comma-separated list of option strings and metavars.
@ -52,49 +59,49 @@ class PrettyHelpFormatter(optparse.IndentedHelpFormatter):
metavar = option.metavar or option.dest.lower()
opts.append(mvarfmt.format(metavar.lower()))
return "".join(opts)
return ''.join(opts)
def format_heading(self, heading: str) -> str:
if heading == "Options":
return ""
return heading + ":\n"
if heading == 'Options':
return ''
return heading + ':\n'
def format_usage(self, usage: str) -> str:
"""
Ensure there is only one newline between usage and the first heading
if there is no description.
"""
msg = "\nUsage: {}\n".format(self.indent_lines(textwrap.dedent(usage), " "))
msg = '\nUsage: {}\n'.format(self.indent_lines(textwrap.dedent(usage), ' '))
return msg
def format_description(self, description: str) -> str:
# leave full control over description to us
if description:
if hasattr(self.parser, "main"):
label = "Commands"
if hasattr(self.parser, 'main'):
label = 'Commands'
else:
label = "Description"
label = 'Description'
# some doc strings have initial newlines, some don't
description = description.lstrip("\n")
description = description.lstrip('\n')
# some doc strings have final newlines and spaces, some don't
description = description.rstrip()
# dedent, then reindent
description = self.indent_lines(textwrap.dedent(description), " ")
description = f"{label}:\n{description}\n"
description = self.indent_lines(textwrap.dedent(description), ' ')
description = f'{label}:\n{description}\n'
return description
else:
return ""
return ''
def format_epilog(self, epilog: str) -> str:
# leave full control over epilog to us
if epilog:
return epilog
else:
return ""
return ''
def indent_lines(self, text: str, indent: str) -> str:
new_lines = [indent + line for line in text.split("\n")]
return "\n".join(new_lines)
new_lines = [indent + line for line in text.split('\n')]
return '\n'.join(new_lines)
class UpdatingDefaultsHelpFormatter(PrettyHelpFormatter):
@ -115,7 +122,7 @@ class UpdatingDefaultsHelpFormatter(PrettyHelpFormatter):
default_values = self.parser.defaults.get(option.dest)
help_text = super().expand_default(option)
if default_values and option.metavar == "URL":
if default_values and option.metavar == 'URL':
if isinstance(default_values, str):
default_values = [default_values]
@ -131,7 +138,7 @@ class UpdatingDefaultsHelpFormatter(PrettyHelpFormatter):
class CustomOptionParser(optparse.OptionParser):
def insert_option_group(
self, idx: int, *args: Any, **kwargs: Any
self, idx: int, *args: Any, **kwargs: Any,
) -> optparse.OptionGroup:
"""Insert an OptionGroup at a given position."""
group = self.add_option_group(*args, **kwargs)
@ -142,7 +149,7 @@ class CustomOptionParser(optparse.OptionParser):
return group
@property
def option_list_all(self) -> List[optparse.Option]:
def option_list_all(self) -> list[optparse.Option]:
"""Get a list of all options, including those in option groups."""
res = self.option_list[:]
for i in self.option_groups:
@ -172,15 +179,15 @@ class ConfigOptionParser(CustomOptionParser):
try:
return option.check_value(key, val)
except optparse.OptionValueError as exc:
print(f"An error occurred during configuration: {exc}")
print(f'An error occurred during configuration: {exc}')
sys.exit(3)
def _get_ordered_configuration_items(self) -> Iterator[Tuple[str, Any]]:
def _get_ordered_configuration_items(self) -> Iterator[tuple[str, Any]]:
# Configuration gives keys in an unordered manner. Order them.
override_order = ["global", self.name, ":env:"]
override_order = ['global', self.name, ':env:']
# Pool the options into different groups
section_items: Dict[str, List[Tuple[str, Any]]] = {
section_items: dict[str, list[tuple[str, Any]]] = {
name: [] for name in override_order
}
for section_key, val in self.config.items():
@ -192,7 +199,7 @@ class ConfigOptionParser(CustomOptionParser):
)
continue
section, key = section_key.split(".", 1)
section, key = section_key.split('.', 1)
if section in override_order:
section_items[section].append((key, val))
@ -201,7 +208,7 @@ class ConfigOptionParser(CustomOptionParser):
for key, val in section_items[section]:
yield key, val
def _update_defaults(self, defaults: Dict[str, Any]) -> Dict[str, Any]:
def _update_defaults(self, defaults: dict[str, Any]) -> dict[str, Any]:
"""Updates the given defaults with values from the config files and
the environ. Does a little special handling for certain types of
options (lists)."""
@ -212,7 +219,7 @@ class ConfigOptionParser(CustomOptionParser):
# Then set the options with those values
for key, val in self._get_ordered_configuration_items():
# '--' because configuration supports only long names
option = self.get_option("--" + key)
option = self.get_option('--' + key)
# Ignore options not present in this parser. E.g. non-globals put
# in [global] by users that want them to apply to all applicable
@ -222,31 +229,31 @@ class ConfigOptionParser(CustomOptionParser):
assert option.dest is not None
if option.action in ("store_true", "store_false"):
if option.action in ('store_true', 'store_false'):
try:
val = strtobool(val)
except ValueError:
self.error(
"{} is not a valid value for {} option, " # noqa
"please specify a boolean value like yes/no, "
"true/false or 1/0 instead.".format(val, key)
'{} is not a valid value for {} option, ' # noqa
'please specify a boolean value like yes/no, '
'true/false or 1/0 instead.'.format(val, key),
)
elif option.action == "count":
elif option.action == 'count':
with suppress(ValueError):
val = strtobool(val)
with suppress(ValueError):
val = int(val)
if not isinstance(val, int) or val < 0:
self.error(
"{} is not a valid value for {} option, " # noqa
"please instead specify either a non-negative integer "
"or a boolean value like yes/no or false/true "
"which is equivalent to 1/0.".format(val, key)
'{} is not a valid value for {} option, ' # noqa
'please instead specify either a non-negative integer '
'or a boolean value like yes/no or false/true '
'which is equivalent to 1/0.'.format(val, key),
)
elif option.action == "append":
elif option.action == 'append':
val = val.split()
val = [self.check_default(option, key, v) for v in val]
elif option.action == "callback":
elif option.action == 'callback':
assert option.callback is not None
late_eval.add(option.dest)
opt_str = option.get_opt_string()
@ -289,4 +296,4 @@ class ConfigOptionParser(CustomOptionParser):
def error(self, msg: str) -> None:
self.print_usage(sys.stderr)
self.exit(UNKNOWN_ERROR, f"{msg}\n")
self.exit(UNKNOWN_ERROR, f'{msg}\n')

View file

@ -1,27 +1,34 @@
from __future__ import annotations
import functools
import itertools
import sys
from signal import SIGINT, default_int_handler, signal
from typing import Any, Callable, Iterator, Optional, Tuple
from pip._vendor.progress.bar import Bar, FillingCirclesBar, IncrementalBar
from pip._vendor.progress.spinner import Spinner
from pip._vendor.rich.progress import (
BarColumn,
DownloadColumn,
FileSizeColumn,
Progress,
ProgressColumn,
SpinnerColumn,
TextColumn,
TimeElapsedColumn,
TimeRemainingColumn,
TransferSpeedColumn,
)
from signal import default_int_handler
from signal import SIGINT
from signal import signal
from typing import Any
from typing import Callable
from typing import Iterator
from typing import Optional
from typing import Tuple
from pip._internal.utils.compat import WINDOWS
from pip._internal.utils.logging import get_indentation
from pip._internal.utils.misc import format_size
from pip._vendor.progress.bar import Bar
from pip._vendor.progress.bar import FillingCirclesBar
from pip._vendor.progress.bar import IncrementalBar
from pip._vendor.progress.spinner import Spinner
from pip._vendor.rich.progress import BarColumn
from pip._vendor.rich.progress import DownloadColumn
from pip._vendor.rich.progress import FileSizeColumn
from pip._vendor.rich.progress import Progress
from pip._vendor.rich.progress import ProgressColumn
from pip._vendor.rich.progress import SpinnerColumn
from pip._vendor.rich.progress import TextColumn
from pip._vendor.rich.progress import TimeElapsedColumn
from pip._vendor.rich.progress import TimeRemainingColumn
from pip._vendor.rich.progress import TransferSpeedColumn
try:
from pip._vendor import colorama
@ -34,7 +41,7 @@ DownloadProgressRenderer = Callable[[Iterator[bytes]], Iterator[bytes]]
def _select_progress_class(preferred: Bar, fallback: Bar) -> Bar:
encoding = getattr(preferred.file, "encoding", None)
encoding = getattr(preferred.file, 'encoding', None)
# If we don't know what encoding this file is in, then we'll just assume
# that it doesn't support unicode and use the ASCII bar.
@ -44,16 +51,16 @@ def _select_progress_class(preferred: Bar, fallback: Bar) -> Bar:
# Collect all of the possible characters we want to use with the preferred
# bar.
characters = [
getattr(preferred, "empty_fill", ""),
getattr(preferred, "fill", ""),
getattr(preferred, 'empty_fill', ''),
getattr(preferred, 'fill', ''),
]
characters += list(getattr(preferred, "phases", []))
characters += list(getattr(preferred, 'phases', []))
# Try to decode the characters we're using for the bar using the encoding
# of the given file, if this works then we'll assume that we can use the
# fancier bar and if not we'll fall back to the plaintext bar.
try:
"".join(characters).encode(encoding)
''.join(characters).encode(encoding)
except UnicodeEncodeError:
return fallback
else:
@ -126,17 +133,17 @@ class SilentBar(Bar):
class BlueEmojiBar(IncrementalBar):
suffix = "%(percent)d%%"
bar_prefix = " "
bar_suffix = " "
phases = ("\U0001F539", "\U0001F537", "\U0001F535")
suffix = '%(percent)d%%'
bar_prefix = ' '
bar_suffix = ' '
phases = ('\U0001F539', '\U0001F537', '\U0001F535')
class DownloadProgressMixin:
def __init__(self, *args: Any, **kwargs: Any) -> None:
# https://github.com/python/mypy/issues/5887
super().__init__(*args, **kwargs) # type: ignore
self.message: str = (" " * (get_indentation() + 2)) + self.message
self.message: str = (' ' * (get_indentation() + 2)) + self.message
@property
def downloaded(self) -> str:
@ -146,14 +153,14 @@ class DownloadProgressMixin:
def download_speed(self) -> str:
# Avoid zero division errors...
if self.avg == 0.0: # type: ignore
return "..."
return format_size(1 / self.avg) + "/s" # type: ignore
return '...'
return format_size(1 / self.avg) + '/s' # type: ignore
@property
def pretty_eta(self) -> str:
if self.eta: # type: ignore
return f"eta {self.eta_td}" # type: ignore
return ""
return f'eta {self.eta_td}' # type: ignore
return ''
def iter(self, it): # type: ignore
for x in it:
@ -196,8 +203,8 @@ class WindowsMixin:
class BaseDownloadProgressBar(WindowsMixin, InterruptibleMixin, DownloadProgressMixin):
file = sys.stdout
message = "%(percent)d%%"
suffix = "%(downloaded)s %(download_speed)s %(pretty_eta)s"
message = '%(percent)d%%'
suffix = '%(downloaded)s %(download_speed)s %(pretty_eta)s'
class DefaultDownloadProgressBar(BaseDownloadProgressBar, _BaseBar):
@ -221,14 +228,14 @@ class DownloadBlueEmojiProgressBar(BaseDownloadProgressBar, BlueEmojiBar):
class DownloadProgressSpinner(
WindowsMixin, InterruptibleMixin, DownloadProgressMixin, Spinner
WindowsMixin, InterruptibleMixin, DownloadProgressMixin, Spinner,
):
file = sys.stdout
suffix = "%(downloaded)s %(download_speed)s"
suffix = '%(downloaded)s %(download_speed)s'
def next_phase(self) -> str:
if not hasattr(self, "_phaser"):
if not hasattr(self, '_phaser'):
self._phaser = itertools.cycle(self.phases)
return next(self._phaser)
@ -236,30 +243,30 @@ class DownloadProgressSpinner(
message = self.message % self
phase = self.next_phase()
suffix = self.suffix % self
line = "".join(
line = ''.join(
[
message,
" " if message else "",
' ' if message else '',
phase,
" " if suffix else "",
' ' if suffix else '',
suffix,
]
],
)
self.writeln(line)
BAR_TYPES = {
"off": (DownloadSilentBar, DownloadSilentBar),
"on": (DefaultDownloadProgressBar, DownloadProgressSpinner),
"ascii": (DownloadBar, DownloadProgressSpinner),
"pretty": (DownloadFillingCirclesBar, DownloadProgressSpinner),
"emoji": (DownloadBlueEmojiProgressBar, DownloadProgressSpinner),
'off': (DownloadSilentBar, DownloadSilentBar),
'on': (DefaultDownloadProgressBar, DownloadProgressSpinner),
'ascii': (DownloadBar, DownloadProgressSpinner),
'pretty': (DownloadFillingCirclesBar, DownloadProgressSpinner),
'emoji': (DownloadBlueEmojiProgressBar, DownloadProgressSpinner),
}
def _legacy_progress_bar(
progress_bar: str, max: Optional[int]
progress_bar: str, max: int | None,
) -> DownloadProgressRenderer:
if max is None or max == 0:
return BAR_TYPES[progress_bar][1]().iter # type: ignore
@ -276,13 +283,13 @@ def _rich_progress_bar(
bar_type: str,
size: int,
) -> Iterator[bytes]:
assert bar_type == "on", "This should only be used in the default mode."
assert bar_type == 'on', 'This should only be used in the default mode.'
if not size:
total = float("inf")
columns: Tuple[ProgressColumn, ...] = (
TextColumn("[progress.description]{task.description}"),
SpinnerColumn("line", speed=1.5),
total = float('inf')
columns: tuple[ProgressColumn, ...] = (
TextColumn('[progress.description]{task.description}'),
SpinnerColumn('line', speed=1.5),
FileSizeColumn(),
TransferSpeedColumn(),
TimeElapsedColumn(),
@ -290,16 +297,16 @@ def _rich_progress_bar(
else:
total = size
columns = (
TextColumn("[progress.description]{task.description}"),
TextColumn('[progress.description]{task.description}'),
BarColumn(),
DownloadColumn(),
TransferSpeedColumn(),
TextColumn("eta"),
TextColumn('eta'),
TimeRemainingColumn(),
)
progress = Progress(*columns, refresh_per_second=30)
task_id = progress.add_task(" " * (get_indentation() + 2), total=total)
task_id = progress.add_task(' ' * (get_indentation() + 2), total=total)
with progress:
for chunk in iterable:
yield chunk
@ -307,15 +314,15 @@ def _rich_progress_bar(
def get_download_progress_renderer(
*, bar_type: str, size: Optional[int] = None
*, bar_type: str, size: int | None = None,
) -> DownloadProgressRenderer:
"""Get an object that can be used to render the download progress.
Returns a callable, that takes an iterable to "wrap".
"""
if bar_type == "on":
if bar_type == 'on':
return functools.partial(_rich_progress_bar, bar_type=bar_type, size=size)
elif bar_type == "off":
elif bar_type == 'off':
return iter # no-op, when passed an iterator
else:
return _legacy_progress_bar(bar_type, size)

View file

@ -4,42 +4,43 @@ The classes in this module are in a separate module so the commands not
needing download / PackageFinder capability don't unnecessarily import the
PackageFinder machinery and all its vendored dependencies, etc.
"""
from __future__ import annotations
import logging
import os
import sys
from functools import partial
from optparse import Values
from typing import Any, List, Optional, Tuple
from typing import Any
from typing import List
from typing import Optional
from typing import Tuple
from pip._internal.cache import WheelCache
from pip._internal.cli import cmdoptions
from pip._internal.cli.base_command import Command
from pip._internal.cli.command_context import CommandContextMixIn
from pip._internal.exceptions import CommandError, PreviousBuildDirError
from pip._internal.exceptions import CommandError
from pip._internal.exceptions import PreviousBuildDirError
from pip._internal.index.collector import LinkCollector
from pip._internal.index.package_finder import PackageFinder
from pip._internal.models.selection_prefs import SelectionPreferences
from pip._internal.models.target_python import TargetPython
from pip._internal.network.session import PipSession
from pip._internal.operations.prepare import RequirementPreparer
from pip._internal.req.constructors import (
install_req_from_editable,
install_req_from_line,
install_req_from_parsed_requirement,
install_req_from_req_string,
)
from pip._internal.req.constructors import install_req_from_editable
from pip._internal.req.constructors import install_req_from_line
from pip._internal.req.constructors import install_req_from_parsed_requirement
from pip._internal.req.constructors import install_req_from_req_string
from pip._internal.req.req_file import parse_requirements
from pip._internal.req.req_install import InstallRequirement
from pip._internal.req.req_tracker import RequirementTracker
from pip._internal.resolution.base import BaseResolver
from pip._internal.self_outdated_check import pip_self_version_check
from pip._internal.utils.deprecation import deprecated
from pip._internal.utils.temp_dir import (
TempDirectory,
TempDirectoryTypeRegistry,
tempdir_kinds,
)
from pip._internal.utils.temp_dir import tempdir_kinds
from pip._internal.utils.temp_dir import TempDirectory
from pip._internal.utils.temp_dir import TempDirectoryTypeRegistry
from pip._internal.utils.virtualenv import running_under_virtualenv
logger = logging.getLogger(__name__)
@ -53,17 +54,17 @@ class SessionCommandMixin(CommandContextMixIn):
def __init__(self) -> None:
super().__init__()
self._session: Optional[PipSession] = None
self._session: PipSession | None = None
@classmethod
def _get_index_urls(cls, options: Values) -> Optional[List[str]]:
def _get_index_urls(cls, options: Values) -> list[str] | None:
"""Return a list of index urls from user-provided options."""
index_urls = []
if not getattr(options, "no_index", False):
url = getattr(options, "index_url", None)
if not getattr(options, 'no_index', False):
url = getattr(options, 'index_url', None)
if url:
index_urls.append(url)
urls = getattr(options, "extra_index_urls", None)
urls = getattr(options, 'extra_index_urls', None)
if urls:
index_urls.extend(urls)
# Return None rather than an empty list
@ -82,13 +83,13 @@ class SessionCommandMixin(CommandContextMixIn):
def _build_session(
self,
options: Values,
retries: Optional[int] = None,
timeout: Optional[int] = None,
retries: int | None = None,
timeout: int | None = None,
) -> PipSession:
assert not options.cache_dir or os.path.isabs(options.cache_dir)
session = PipSession(
cache=(
os.path.join(options.cache_dir, "http") if options.cache_dir else None
os.path.join(options.cache_dir, 'http') if options.cache_dir else None
),
retries=retries if retries is not None else options.retries,
trusted_hosts=options.trusted_hosts,
@ -110,8 +111,8 @@ class SessionCommandMixin(CommandContextMixIn):
# Handle configured proxies
if options.proxy:
session.proxies = {
"http": options.proxy,
"https": options.proxy,
'http': options.proxy,
'https': options.proxy,
}
# Determine if we can prompt the user for authentication or not
@ -135,14 +136,14 @@ class IndexGroupCommand(Command, SessionCommandMixin):
This overrides the default behavior of not doing the check.
"""
# Make sure the index_group options are present.
assert hasattr(options, "no_index")
assert hasattr(options, 'no_index')
if options.disable_pip_version_check or options.no_index:
return
# Otherwise, check if we're using the latest version of pip available.
session = self._build_session(
options, retries=0, timeout=min(5, options.timeout)
options, retries=0, timeout=min(5, options.timeout),
)
with session:
pip_self_version_check(session, options)
@ -164,14 +165,14 @@ def warn_if_run_as_root() -> None:
"""
if running_under_virtualenv():
return
if not hasattr(os, "getuid"):
if not hasattr(os, 'getuid'):
return
# On Windows, there are no "system managed" Python packages. Installing as
# Administrator via pip is the correct way of updating system environments.
#
# We choose sys.platform over utils.compat.WINDOWS here to enable Mypy platform
# checks: https://mypy.readthedocs.io/en/stable/common_issues.html
if sys.platform == "win32" or sys.platform == "cygwin":
if sys.platform == 'win32' or sys.platform == 'cygwin':
return
if os.getuid() != 0:
@ -179,9 +180,9 @@ def warn_if_run_as_root() -> None:
logger.warning(
"Running pip as the 'root' user can result in broken permissions and "
"conflicting behaviour with the system package manager. "
"It is recommended to use a virtual environment instead: "
"https://pip.pypa.io/warnings/venv"
'conflicting behaviour with the system package manager. '
'It is recommended to use a virtual environment instead: '
'https://pip.pypa.io/warnings/venv',
)
@ -195,8 +196,8 @@ def with_cleanup(func: Any) -> Any:
registry.set_delete(t, False)
def wrapper(
self: RequirementCommand, options: Values, args: List[Any]
) -> Optional[int]:
self: RequirementCommand, options: Values, args: list[Any],
) -> int | None:
assert self.tempdir_registry is not None
if options.no_clean:
configure_tempdir_registry(self.tempdir_registry)
@ -222,30 +223,30 @@ class RequirementCommand(IndexGroupCommand):
@staticmethod
def determine_resolver_variant(options: Values) -> str:
"""Determines which resolver should be used, based on the given options."""
if "legacy-resolver" in options.deprecated_features_enabled:
return "legacy"
if 'legacy-resolver' in options.deprecated_features_enabled:
return 'legacy'
return "2020-resolver"
return '2020-resolver'
@staticmethod
def determine_build_failure_suppression(options: Values) -> bool:
"""Determines whether build failures should be suppressed and backtracked on."""
if "backtrack-on-build-failures" not in options.deprecated_features_enabled:
if 'backtrack-on-build-failures' not in options.deprecated_features_enabled:
return False
if "legacy-resolver" in options.deprecated_features_enabled:
raise CommandError("Cannot backtrack with legacy resolver.")
if 'legacy-resolver' in options.deprecated_features_enabled:
raise CommandError('Cannot backtrack with legacy resolver.')
deprecated(
reason=(
"Backtracking on build failures can mask issues related to how "
"a package generates metadata or builds a wheel. This flag will "
"be removed in pip 22.2."
'Backtracking on build failures can mask issues related to how '
'a package generates metadata or builds a wheel. This flag will '
'be removed in pip 22.2.'
),
gone_in=None,
replacement=(
"avoiding known-bad versions by explicitly telling pip to ignore them "
"(either directly as requirements, or via a constraints file)"
'avoiding known-bad versions by explicitly telling pip to ignore them '
'(either directly as requirements, or via a constraints file)'
),
feature_flag=None,
issue=10655,
@ -261,7 +262,7 @@ class RequirementCommand(IndexGroupCommand):
session: PipSession,
finder: PackageFinder,
use_user_site: bool,
download_dir: Optional[str] = None,
download_dir: str | None = None,
verbosity: int = 0,
) -> RequirementPreparer:
"""
@ -271,42 +272,42 @@ class RequirementCommand(IndexGroupCommand):
assert temp_build_dir_path is not None
resolver_variant = cls.determine_resolver_variant(options)
if resolver_variant == "2020-resolver":
lazy_wheel = "fast-deps" in options.features_enabled
if resolver_variant == '2020-resolver':
lazy_wheel = 'fast-deps' in options.features_enabled
if lazy_wheel:
logger.warning(
"pip is using lazily downloaded wheels using HTTP "
"range requests to obtain dependency information. "
"This experimental feature is enabled through "
"--use-feature=fast-deps and it is not ready for "
"production."
'pip is using lazily downloaded wheels using HTTP '
'range requests to obtain dependency information. '
'This experimental feature is enabled through '
'--use-feature=fast-deps and it is not ready for '
'production.',
)
else:
lazy_wheel = False
if "fast-deps" in options.features_enabled:
if 'fast-deps' in options.features_enabled:
logger.warning(
"fast-deps has no effect when used with the legacy resolver."
'fast-deps has no effect when used with the legacy resolver.',
)
in_tree_build = "out-of-tree-build" not in options.deprecated_features_enabled
if "in-tree-build" in options.features_enabled:
in_tree_build = 'out-of-tree-build' not in options.deprecated_features_enabled
if 'in-tree-build' in options.features_enabled:
deprecated(
reason="In-tree builds are now the default.",
replacement="to remove the --use-feature=in-tree-build flag",
gone_in="22.1",
reason='In-tree builds are now the default.',
replacement='to remove the --use-feature=in-tree-build flag',
gone_in='22.1',
)
if "out-of-tree-build" in options.deprecated_features_enabled:
if 'out-of-tree-build' in options.deprecated_features_enabled:
deprecated(
reason="Out-of-tree builds are deprecated.",
reason='Out-of-tree builds are deprecated.',
replacement=None,
gone_in="22.1",
gone_in='22.1',
)
if options.progress_bar not in {"on", "off"}:
if options.progress_bar not in {'on', 'off'}:
deprecated(
reason="Custom progress bar styles are deprecated",
replacement="to use the default progress bar style.",
gone_in="22.1",
reason='Custom progress bar styles are deprecated',
replacement='to use the default progress bar style.',
gone_in='22.1',
)
return RequirementPreparer(
@ -331,14 +332,14 @@ class RequirementCommand(IndexGroupCommand):
preparer: RequirementPreparer,
finder: PackageFinder,
options: Values,
wheel_cache: Optional[WheelCache] = None,
wheel_cache: WheelCache | None = None,
use_user_site: bool = False,
ignore_installed: bool = True,
ignore_requires_python: bool = False,
force_reinstall: bool = False,
upgrade_strategy: str = "to-satisfy-only",
use_pep517: Optional[bool] = None,
py_version_info: Optional[Tuple[int, ...]] = None,
upgrade_strategy: str = 'to-satisfy-only',
use_pep517: bool | None = None,
py_version_info: tuple[int, ...] | None = None,
) -> BaseResolver:
"""
Create a Resolver instance for the given parameters.
@ -353,7 +354,7 @@ class RequirementCommand(IndexGroupCommand):
# The long import name and duplicated invocation is needed to convince
# Mypy into correctly typechecking. Otherwise it would complain the
# "Resolver" class being redefined.
if resolver_variant == "2020-resolver":
if resolver_variant == '2020-resolver':
import pip._internal.resolution.resolvelib.resolver
return pip._internal.resolution.resolvelib.resolver.Resolver(
@ -388,15 +389,15 @@ class RequirementCommand(IndexGroupCommand):
def get_requirements(
self,
args: List[str],
args: list[str],
options: Values,
finder: PackageFinder,
session: PipSession,
) -> List[InstallRequirement]:
) -> list[InstallRequirement]:
"""
Parse command-line arguments into the corresponding requirements.
"""
requirements: List[InstallRequirement] = []
requirements: list[InstallRequirement] = []
for filename in options.constraints:
for parsed_req in parse_requirements(
filename,
@ -434,7 +435,7 @@ class RequirementCommand(IndexGroupCommand):
# NOTE: options.require_hashes may be set if --require-hashes is True
for filename in options.requirements:
for parsed_req in parse_requirements(
filename, finder=finder, options=options, session=session
filename, finder=finder, options=options, session=session,
):
req_to_add = install_req_from_parsed_requirement(
parsed_req,
@ -449,18 +450,18 @@ class RequirementCommand(IndexGroupCommand):
options.require_hashes = True
if not (args or options.editables or options.requirements):
opts = {"name": self.name}
opts = {'name': self.name}
if options.find_links:
raise CommandError(
"You must give at least one requirement to {name} "
'You must give at least one requirement to {name} '
'(maybe you meant "pip {name} {links}"?)'.format(
**dict(opts, links=" ".join(options.find_links))
)
**dict(opts, links=' '.join(options.find_links)),
),
)
else:
raise CommandError(
"You must give at least one requirement to {name} "
'(see "pip help {name}")'.format(**opts)
'You must give at least one requirement to {name} '
'(see "pip help {name}")'.format(**opts),
)
return requirements
@ -480,8 +481,8 @@ class RequirementCommand(IndexGroupCommand):
self,
options: Values,
session: PipSession,
target_python: Optional[TargetPython] = None,
ignore_requires_python: Optional[bool] = None,
target_python: TargetPython | None = None,
ignore_requires_python: bool | None = None,
) -> PackageFinder:
"""
Create a package finder appropriate to this requirement command.
@ -502,5 +503,5 @@ class RequirementCommand(IndexGroupCommand):
link_collector=link_collector,
selection_prefs=selection_prefs,
target_python=target_python,
use_deprecated_html5lib="html5lib" in options.deprecated_features_enabled,
use_deprecated_html5lib='html5lib' in options.deprecated_features_enabled,
)

View file

@ -1,14 +1,17 @@
from __future__ import annotations
import contextlib
import itertools
import logging
import sys
import time
from typing import IO, Iterator
from pip._vendor.progress import HIDE_CURSOR, SHOW_CURSOR
from typing import IO
from typing import Iterator
from pip._internal.utils.compat import WINDOWS
from pip._internal.utils.logging import get_indentation
from pip._vendor.progress import HIDE_CURSOR
from pip._vendor.progress import SHOW_CURSOR
logger = logging.getLogger(__name__)
@ -26,7 +29,7 @@ class InteractiveSpinner(SpinnerInterface):
self,
message: str,
file: IO[str] = None,
spin_chars: str = "-\\|/",
spin_chars: str = '-\\|/',
# Empirically, 8 updates/second looks nice
min_update_interval_seconds: float = 0.125,
):
@ -39,15 +42,15 @@ class InteractiveSpinner(SpinnerInterface):
self._spin_cycle = itertools.cycle(spin_chars)
self._file.write(" " * get_indentation() + self._message + " ... ")
self._file.write(' ' * get_indentation() + self._message + ' ... ')
self._width = 0
def _write(self, status: str) -> None:
assert not self._finished
# Erase what we wrote before by backspacing to the beginning, writing
# spaces to overwrite the old text, and then backspacing again
backup = "\b" * self._width
self._file.write(backup + " " * self._width + backup)
backup = '\b' * self._width
self._file.write(backup + ' ' * self._width + backup)
# Now we have a blank slate to add our status
self._file.write(status)
self._width = len(status)
@ -65,7 +68,7 @@ class InteractiveSpinner(SpinnerInterface):
if self._finished:
return
self._write(final_status)
self._file.write("\n")
self._file.write('\n')
self._file.flush()
self._finished = True
@ -79,19 +82,19 @@ class NonInteractiveSpinner(SpinnerInterface):
self._message = message
self._finished = False
self._rate_limiter = RateLimiter(min_update_interval_seconds)
self._update("started")
self._update('started')
def _update(self, status: str) -> None:
assert not self._finished
self._rate_limiter.reset()
logger.info("%s: %s", self._message, status)
logger.info('%s: %s', self._message, status)
def spin(self) -> None:
if self._finished:
return
if not self._rate_limiter.ready():
return
self._update("still running...")
self._update('still running...')
def finish(self, final_status: str) -> None:
if self._finished:
@ -129,13 +132,13 @@ def open_spinner(message: str) -> Iterator[SpinnerInterface]:
with hidden_cursor(sys.stdout):
yield spinner
except KeyboardInterrupt:
spinner.finish("canceled")
spinner.finish('canceled')
raise
except Exception:
spinner.finish("error")
spinner.finish('error')
raise
else:
spinner.finish("done")
spinner.finish('done')
@contextlib.contextmanager

View file

@ -1,3 +1,4 @@
from __future__ import annotations
SUCCESS = 0
ERROR = 1
UNKNOWN_ERROR = 2

View file

@ -1,14 +1,17 @@
"""
Package containing all pip commands
"""
from __future__ import annotations
import importlib
from collections import namedtuple
from typing import Any, Dict, Optional
from typing import Any
from typing import Dict
from typing import Optional
from pip._internal.cli.base_command import Command
CommandInfo = namedtuple("CommandInfo", "module_path, class_name, summary")
CommandInfo = namedtuple('CommandInfo', 'module_path, class_name, summary')
# This dictionary does a bunch of heavy lifting for help output:
# - Enables avoiding additional (costly) imports for presenting `--help`.
@ -17,86 +20,86 @@ CommandInfo = namedtuple("CommandInfo", "module_path, class_name, summary")
# Even though the module path starts with the same "pip._internal.commands"
# prefix, the full path makes testing easier (specifically when modifying
# `commands_dict` in test setup / teardown).
commands_dict: Dict[str, CommandInfo] = {
"install": CommandInfo(
"pip._internal.commands.install",
"InstallCommand",
"Install packages.",
commands_dict: dict[str, CommandInfo] = {
'install': CommandInfo(
'pip._internal.commands.install',
'InstallCommand',
'Install packages.',
),
"download": CommandInfo(
"pip._internal.commands.download",
"DownloadCommand",
"Download packages.",
'download': CommandInfo(
'pip._internal.commands.download',
'DownloadCommand',
'Download packages.',
),
"uninstall": CommandInfo(
"pip._internal.commands.uninstall",
"UninstallCommand",
"Uninstall packages.",
'uninstall': CommandInfo(
'pip._internal.commands.uninstall',
'UninstallCommand',
'Uninstall packages.',
),
"freeze": CommandInfo(
"pip._internal.commands.freeze",
"FreezeCommand",
"Output installed packages in requirements format.",
'freeze': CommandInfo(
'pip._internal.commands.freeze',
'FreezeCommand',
'Output installed packages in requirements format.',
),
"list": CommandInfo(
"pip._internal.commands.list",
"ListCommand",
"List installed packages.",
'list': CommandInfo(
'pip._internal.commands.list',
'ListCommand',
'List installed packages.',
),
"show": CommandInfo(
"pip._internal.commands.show",
"ShowCommand",
"Show information about installed packages.",
'show': CommandInfo(
'pip._internal.commands.show',
'ShowCommand',
'Show information about installed packages.',
),
"check": CommandInfo(
"pip._internal.commands.check",
"CheckCommand",
"Verify installed packages have compatible dependencies.",
'check': CommandInfo(
'pip._internal.commands.check',
'CheckCommand',
'Verify installed packages have compatible dependencies.',
),
"config": CommandInfo(
"pip._internal.commands.configuration",
"ConfigurationCommand",
"Manage local and global configuration.",
'config': CommandInfo(
'pip._internal.commands.configuration',
'ConfigurationCommand',
'Manage local and global configuration.',
),
"search": CommandInfo(
"pip._internal.commands.search",
"SearchCommand",
"Search PyPI for packages.",
'search': CommandInfo(
'pip._internal.commands.search',
'SearchCommand',
'Search PyPI for packages.',
),
"cache": CommandInfo(
"pip._internal.commands.cache",
"CacheCommand",
'cache': CommandInfo(
'pip._internal.commands.cache',
'CacheCommand',
"Inspect and manage pip's wheel cache.",
),
"index": CommandInfo(
"pip._internal.commands.index",
"IndexCommand",
"Inspect information available from package indexes.",
'index': CommandInfo(
'pip._internal.commands.index',
'IndexCommand',
'Inspect information available from package indexes.',
),
"wheel": CommandInfo(
"pip._internal.commands.wheel",
"WheelCommand",
"Build wheels from your requirements.",
'wheel': CommandInfo(
'pip._internal.commands.wheel',
'WheelCommand',
'Build wheels from your requirements.',
),
"hash": CommandInfo(
"pip._internal.commands.hash",
"HashCommand",
"Compute hashes of package archives.",
'hash': CommandInfo(
'pip._internal.commands.hash',
'HashCommand',
'Compute hashes of package archives.',
),
"completion": CommandInfo(
"pip._internal.commands.completion",
"CompletionCommand",
"A helper command used for command completion.",
'completion': CommandInfo(
'pip._internal.commands.completion',
'CompletionCommand',
'A helper command used for command completion.',
),
"debug": CommandInfo(
"pip._internal.commands.debug",
"DebugCommand",
"Show information useful for debugging.",
'debug': CommandInfo(
'pip._internal.commands.debug',
'DebugCommand',
'Show information useful for debugging.',
),
"help": CommandInfo(
"pip._internal.commands.help",
"HelpCommand",
"Show help for commands.",
'help': CommandInfo(
'pip._internal.commands.help',
'HelpCommand',
'Show help for commands.',
),
}
@ -113,7 +116,7 @@ def create_command(name: str, **kwargs: Any) -> Command:
return command
def get_similar_commands(name: str) -> Optional[str]:
def get_similar_commands(name: str) -> str | None:
"""Command name auto-correct."""
from difflib import get_close_matches

View file

@ -1,12 +1,17 @@
from __future__ import annotations
import os
import textwrap
from optparse import Values
from typing import Any, List
from typing import Any
from typing import List
import pip._internal.utils.filesystem as filesystem
from pip._internal.cli.base_command import Command
from pip._internal.cli.status_codes import ERROR, SUCCESS
from pip._internal.exceptions import CommandError, PipError
from pip._internal.cli.status_codes import ERROR
from pip._internal.cli.status_codes import SUCCESS
from pip._internal.exceptions import CommandError
from pip._internal.exceptions import PipError
from pip._internal.utils.logging import getLogger
logger = getLogger(__name__)
@ -39,34 +44,34 @@ class CacheCommand(Command):
def add_options(self) -> None:
self.cmd_opts.add_option(
"--format",
action="store",
dest="list_format",
default="human",
choices=("human", "abspath"),
help="Select the output format among: human (default) or abspath",
'--format',
action='store',
dest='list_format',
default='human',
choices=('human', 'abspath'),
help='Select the output format among: human (default) or abspath',
)
self.parser.insert_option_group(0, self.cmd_opts)
def run(self, options: Values, args: List[str]) -> int:
def run(self, options: Values, args: list[str]) -> int:
handlers = {
"dir": self.get_cache_dir,
"info": self.get_cache_info,
"list": self.list_cache_items,
"remove": self.remove_cache_items,
"purge": self.purge_cache,
'dir': self.get_cache_dir,
'info': self.get_cache_info,
'list': self.list_cache_items,
'remove': self.remove_cache_items,
'purge': self.purge_cache,
}
if not options.cache_dir:
logger.error("pip cache commands can not function since cache is disabled.")
logger.error('pip cache commands can not function since cache is disabled.')
return ERROR
# Determine action
if not args or args[0] not in handlers:
logger.error(
"Need an action (%s) to perform.",
", ".join(sorted(handlers)),
'Need an action (%s) to perform.',
', '.join(sorted(handlers)),
)
return ERROR
@ -81,21 +86,21 @@ class CacheCommand(Command):
return SUCCESS
def get_cache_dir(self, options: Values, args: List[Any]) -> None:
def get_cache_dir(self, options: Values, args: list[Any]) -> None:
if args:
raise CommandError("Too many arguments")
raise CommandError('Too many arguments')
logger.info(options.cache_dir)
def get_cache_info(self, options: Values, args: List[Any]) -> None:
def get_cache_info(self, options: Values, args: list[Any]) -> None:
if args:
raise CommandError("Too many arguments")
raise CommandError('Too many arguments')
num_http_files = len(self._find_http_files(options))
num_packages = len(self._find_wheels(options, "*"))
num_packages = len(self._find_wheels(options, '*'))
http_cache_location = self._cache_dir(options, "http")
wheels_cache_location = self._cache_dir(options, "wheels")
http_cache_location = self._cache_dir(options, 'http')
wheels_cache_location = self._cache_dir(options, 'wheels')
http_cache_size = filesystem.format_directory_size(http_cache_location)
wheels_cache_size = filesystem.format_directory_size(wheels_cache_location)
@ -108,7 +113,7 @@ class CacheCommand(Command):
Wheels location: {wheels_cache_location}
Wheels size: {wheels_cache_size}
Number of wheels: {package_count}
"""
""",
)
.format(
http_cache_location=http_cache_location,
@ -123,35 +128,35 @@ class CacheCommand(Command):
logger.info(message)
def list_cache_items(self, options: Values, args: List[Any]) -> None:
def list_cache_items(self, options: Values, args: list[Any]) -> None:
if len(args) > 1:
raise CommandError("Too many arguments")
raise CommandError('Too many arguments')
if args:
pattern = args[0]
else:
pattern = "*"
pattern = '*'
files = self._find_wheels(options, pattern)
if options.list_format == "human":
if options.list_format == 'human':
self.format_for_human(files)
else:
self.format_for_abspath(files)
def format_for_human(self, files: List[str]) -> None:
def format_for_human(self, files: list[str]) -> None:
if not files:
logger.info("Nothing cached.")
logger.info('Nothing cached.')
return
results = []
for filename in files:
wheel = os.path.basename(filename)
size = filesystem.format_file_size(filename)
results.append(f" - {wheel} ({size})")
logger.info("Cache contents:\n")
logger.info("\n".join(sorted(results)))
results.append(f' - {wheel} ({size})')
logger.info('Cache contents:\n')
logger.info('\n'.join(sorted(results)))
def format_for_abspath(self, files: List[str]) -> None:
def format_for_abspath(self, files: list[str]) -> None:
if not files:
return
@ -159,48 +164,48 @@ class CacheCommand(Command):
for filename in files:
results.append(filename)
logger.info("\n".join(sorted(results)))
logger.info('\n'.join(sorted(results)))
def remove_cache_items(self, options: Values, args: List[Any]) -> None:
def remove_cache_items(self, options: Values, args: list[Any]) -> None:
if len(args) > 1:
raise CommandError("Too many arguments")
raise CommandError('Too many arguments')
if not args:
raise CommandError("Please provide a pattern")
raise CommandError('Please provide a pattern')
files = self._find_wheels(options, args[0])
no_matching_msg = "No matching packages"
if args[0] == "*":
no_matching_msg = 'No matching packages'
if args[0] == '*':
# Only fetch http files if no specific pattern given
files += self._find_http_files(options)
else:
# Add the pattern to the log message
no_matching_msg += ' for pattern "{}"'.format(args[0])
no_matching_msg += f' for pattern "{args[0]}"'
if not files:
logger.warning(no_matching_msg)
for filename in files:
os.unlink(filename)
logger.verbose("Removed %s", filename)
logger.info("Files removed: %s", len(files))
logger.verbose('Removed %s', filename)
logger.info('Files removed: %s', len(files))
def purge_cache(self, options: Values, args: List[Any]) -> None:
def purge_cache(self, options: Values, args: list[Any]) -> None:
if args:
raise CommandError("Too many arguments")
raise CommandError('Too many arguments')
return self.remove_cache_items(options, ["*"])
return self.remove_cache_items(options, ['*'])
def _cache_dir(self, options: Values, subdir: str) -> str:
return os.path.join(options.cache_dir, subdir)
def _find_http_files(self, options: Values) -> List[str]:
http_dir = self._cache_dir(options, "http")
return filesystem.find_files(http_dir, "*")
def _find_http_files(self, options: Values) -> list[str]:
http_dir = self._cache_dir(options, 'http')
return filesystem.find_files(http_dir, '*')
def _find_wheels(self, options: Values, pattern: str) -> List[str]:
wheel_dir = self._cache_dir(options, "wheels")
def _find_wheels(self, options: Values, pattern: str) -> list[str]:
wheel_dir = self._cache_dir(options, 'wheels')
# The wheel filename format, as specified in PEP 427, is:
# {distribution}-{version}(-{build})?-{python}-{abi}-{platform}.whl
@ -218,6 +223,6 @@ class CacheCommand(Command):
# match the hyphen before the version, followed by anything else.
#
# PEP 427: https://www.python.org/dev/peps/pep-0427/
pattern = pattern + ("*.whl" if "-" in pattern else "-*.whl")
pattern = pattern + ('*.whl' if '-' in pattern else '-*.whl')
return filesystem.find_files(wheel_dir, pattern)

View file

@ -1,13 +1,14 @@
from __future__ import annotations
import logging
from optparse import Values
from typing import List
from pip._internal.cli.base_command import Command
from pip._internal.cli.status_codes import ERROR, SUCCESS
from pip._internal.operations.check import (
check_package_set,
create_package_set_from_installed,
)
from pip._internal.cli.status_codes import ERROR
from pip._internal.cli.status_codes import SUCCESS
from pip._internal.operations.check import check_package_set
from pip._internal.operations.check import create_package_set_from_installed
from pip._internal.utils.misc import write_output
logger = logging.getLogger(__name__)
@ -19,7 +20,7 @@ class CheckCommand(Command):
usage = """
%prog [options]"""
def run(self, options: Values, args: List[str]) -> int:
def run(self, options: Values, args: list[str]) -> int:
package_set, parsing_probs = create_package_set_from_installed()
missing, conflicting = check_package_set(package_set)
@ -28,7 +29,7 @@ class CheckCommand(Command):
version = package_set[project_name].version
for dependency in missing[project_name]:
write_output(
"%s %s requires %s, which is not installed.",
'%s %s requires %s, which is not installed.',
project_name,
version,
dependency[0],
@ -38,7 +39,7 @@ class CheckCommand(Command):
version = package_set[project_name].version
for dep_name, dep_version, req in conflicting[project_name]:
write_output(
"%s %s has requirement %s, but you have %s %s.",
'%s %s has requirement %s, but you have %s %s.',
project_name,
version,
req,
@ -49,5 +50,5 @@ class CheckCommand(Command):
if missing or conflicting or parsing_probs:
return ERROR
else:
write_output("No broken requirements found.")
write_output('No broken requirements found.')
return SUCCESS

View file

@ -1,3 +1,5 @@
from __future__ import annotations
import sys
import textwrap
from optparse import Values
@ -12,7 +14,7 @@ BASE_COMPLETION = """
"""
COMPLETION_SCRIPTS = {
"bash": """
'bash': """
_pip_completion()
{{
COMPREPLY=( $( COMP_WORDS="${{COMP_WORDS[*]}}" \\
@ -21,7 +23,7 @@ COMPLETION_SCRIPTS = {
}}
complete -o default -F _pip_completion {prog}
""",
"zsh": """
'zsh': """
function _pip_completion {{
local words cword
read -Ac words
@ -32,7 +34,7 @@ COMPLETION_SCRIPTS = {
}}
compctl -K _pip_completion {prog}
""",
"fish": """
'fish': """
function __fish_complete_pip
set -lx COMP_WORDS (commandline -o) ""
set -lx COMP_CWORD ( \\
@ -53,44 +55,44 @@ class CompletionCommand(Command):
def add_options(self) -> None:
self.cmd_opts.add_option(
"--bash",
"-b",
action="store_const",
const="bash",
dest="shell",
help="Emit completion code for bash",
'--bash',
'-b',
action='store_const',
const='bash',
dest='shell',
help='Emit completion code for bash',
)
self.cmd_opts.add_option(
"--zsh",
"-z",
action="store_const",
const="zsh",
dest="shell",
help="Emit completion code for zsh",
'--zsh',
'-z',
action='store_const',
const='zsh',
dest='shell',
help='Emit completion code for zsh',
)
self.cmd_opts.add_option(
"--fish",
"-f",
action="store_const",
const="fish",
dest="shell",
help="Emit completion code for fish",
'--fish',
'-f',
action='store_const',
const='fish',
dest='shell',
help='Emit completion code for fish',
)
self.parser.insert_option_group(0, self.cmd_opts)
def run(self, options: Values, args: List[str]) -> int:
def run(self, options: Values, args: list[str]) -> int:
"""Prints the completion code of the given shell"""
shells = COMPLETION_SCRIPTS.keys()
shell_options = ["--" + shell for shell in sorted(shells)]
shell_options = ['--' + shell for shell in sorted(shells)]
if options.shell in shells:
script = textwrap.dedent(
COMPLETION_SCRIPTS.get(options.shell, "").format(prog=get_prog())
COMPLETION_SCRIPTS.get(options.shell, '').format(prog=get_prog()),
)
print(BASE_COMPLETION.format(script=script, shell=options.shell))
return SUCCESS
else:
sys.stderr.write(
"ERROR: You must pass {}\n".format(" or ".join(shell_options))
'ERROR: You must pass {}\n'.format(' or '.join(shell_options)),
)
return SUCCESS

View file

@ -1,20 +1,24 @@
from __future__ import annotations
import logging
import os
import subprocess
from optparse import Values
from typing import Any, List, Optional
from typing import Any
from typing import List
from typing import Optional
from pip._internal.cli.base_command import Command
from pip._internal.cli.status_codes import ERROR, SUCCESS
from pip._internal.configuration import (
Configuration,
Kind,
get_configuration_files,
kinds,
)
from pip._internal.cli.status_codes import ERROR
from pip._internal.cli.status_codes import SUCCESS
from pip._internal.configuration import Configuration
from pip._internal.configuration import get_configuration_files
from pip._internal.configuration import Kind
from pip._internal.configuration import kinds
from pip._internal.exceptions import PipError
from pip._internal.utils.logging import indent_log
from pip._internal.utils.misc import get_prog, write_output
from pip._internal.utils.misc import get_prog
from pip._internal.utils.misc import write_output
logger = logging.getLogger(__name__)
@ -51,57 +55,57 @@ class ConfigurationCommand(Command):
def add_options(self) -> None:
self.cmd_opts.add_option(
"--editor",
dest="editor",
action="store",
'--editor',
dest='editor',
action='store',
default=None,
help=(
"Editor to use to edit the file. Uses VISUAL or EDITOR "
"environment variables if not provided."
'Editor to use to edit the file. Uses VISUAL or EDITOR '
'environment variables if not provided.'
),
)
self.cmd_opts.add_option(
"--global",
dest="global_file",
action="store_true",
'--global',
dest='global_file',
action='store_true',
default=False,
help="Use the system-wide configuration file only",
help='Use the system-wide configuration file only',
)
self.cmd_opts.add_option(
"--user",
dest="user_file",
action="store_true",
'--user',
dest='user_file',
action='store_true',
default=False,
help="Use the user configuration file only",
help='Use the user configuration file only',
)
self.cmd_opts.add_option(
"--site",
dest="site_file",
action="store_true",
'--site',
dest='site_file',
action='store_true',
default=False,
help="Use the current environment configuration file only",
help='Use the current environment configuration file only',
)
self.parser.insert_option_group(0, self.cmd_opts)
def run(self, options: Values, args: List[str]) -> int:
def run(self, options: Values, args: list[str]) -> int:
handlers = {
"list": self.list_values,
"edit": self.open_in_editor,
"get": self.get_name,
"set": self.set_name_value,
"unset": self.unset_name,
"debug": self.list_config_values,
'list': self.list_values,
'edit': self.open_in_editor,
'get': self.get_name,
'set': self.set_name_value,
'unset': self.unset_name,
'debug': self.list_config_values,
}
# Determine action
if not args or args[0] not in handlers:
logger.error(
"Need an action (%s) to perform.",
", ".join(sorted(handlers)),
'Need an action (%s) to perform.',
', '.join(sorted(handlers)),
)
return ERROR
@ -111,7 +115,7 @@ class ConfigurationCommand(Command):
# Depends on whether the command is modifying.
try:
load_only = self._determine_file(
options, need_value=(action in ["get", "set", "unset", "edit"])
options, need_value=(action in ['get', 'set', 'unset', 'edit']),
)
except PipError as e:
logger.error(e.args[0])
@ -119,7 +123,7 @@ class ConfigurationCommand(Command):
# Load a new configuration
self.configuration = Configuration(
isolated=options.isolated_mode, load_only=load_only
isolated=options.isolated_mode, load_only=load_only,
)
self.configuration.load()
@ -132,7 +136,7 @@ class ConfigurationCommand(Command):
return SUCCESS
def _determine_file(self, options: Values, need_value: bool) -> Optional[Kind]:
def _determine_file(self, options: Values, need_value: bool) -> Kind | None:
file_options = [
key
for key, value in (
@ -158,47 +162,47 @@ class ConfigurationCommand(Command):
return file_options[0]
raise PipError(
"Need exactly one file to operate upon "
"(--user, --site, --global) to perform."
'Need exactly one file to operate upon '
'(--user, --site, --global) to perform.',
)
def list_values(self, options: Values, args: List[str]) -> None:
self._get_n_args(args, "list", n=0)
def list_values(self, options: Values, args: list[str]) -> None:
self._get_n_args(args, 'list', n=0)
for key, value in sorted(self.configuration.items()):
write_output("%s=%r", key, value)
write_output('%s=%r', key, value)
def get_name(self, options: Values, args: List[str]) -> None:
key = self._get_n_args(args, "get [name]", n=1)
def get_name(self, options: Values, args: list[str]) -> None:
key = self._get_n_args(args, 'get [name]', n=1)
value = self.configuration.get_value(key)
write_output("%s", value)
write_output('%s', value)
def set_name_value(self, options: Values, args: List[str]) -> None:
key, value = self._get_n_args(args, "set [name] [value]", n=2)
def set_name_value(self, options: Values, args: list[str]) -> None:
key, value = self._get_n_args(args, 'set [name] [value]', n=2)
self.configuration.set_value(key, value)
self._save_configuration()
def unset_name(self, options: Values, args: List[str]) -> None:
key = self._get_n_args(args, "unset [name]", n=1)
def unset_name(self, options: Values, args: list[str]) -> None:
key = self._get_n_args(args, 'unset [name]', n=1)
self.configuration.unset_value(key)
self._save_configuration()
def list_config_values(self, options: Values, args: List[str]) -> None:
def list_config_values(self, options: Values, args: list[str]) -> None:
"""List config key-value pairs across different config files"""
self._get_n_args(args, "debug", n=0)
self._get_n_args(args, 'debug', n=0)
self.print_env_var_values()
# Iterate over config files and print if they exist, and the
# key-value pairs present in them if they do
for variant, files in sorted(self.configuration.iter_config_files()):
write_output("%s:", variant)
write_output('%s:', variant)
for fname in files:
with indent_log():
file_exists = os.path.exists(fname)
write_output("%s, exists: %r", fname, file_exists)
write_output('%s, exists: %r', fname, file_exists)
if file_exists:
self.print_config_file_values(variant)
@ -206,35 +210,35 @@ class ConfigurationCommand(Command):
"""Get key-value pairs from the file of a variant"""
for name, value in self.configuration.get_values_in_config(variant).items():
with indent_log():
write_output("%s: %s", name, value)
write_output('%s: %s', name, value)
def print_env_var_values(self) -> None:
"""Get key-values pairs present as environment variables"""
write_output("%s:", "env_var")
write_output('%s:', 'env_var')
with indent_log():
for key, value in sorted(self.configuration.get_environ_vars()):
env_var = f"PIP_{key.upper()}"
write_output("%s=%r", env_var, value)
env_var = f'PIP_{key.upper()}'
write_output('%s=%r', env_var, value)
def open_in_editor(self, options: Values, args: List[str]) -> None:
def open_in_editor(self, options: Values, args: list[str]) -> None:
editor = self._determine_editor(options)
fname = self.configuration.get_file_to_edit()
if fname is None:
raise PipError("Could not determine appropriate file.")
raise PipError('Could not determine appropriate file.')
try:
subprocess.check_call([editor, fname])
except subprocess.CalledProcessError as e:
raise PipError(
"Editor Subprocess exited with exit code {}".format(e.returncode)
f'Editor Subprocess exited with exit code {e.returncode}',
)
def _get_n_args(self, args: List[str], example: str, n: int) -> Any:
def _get_n_args(self, args: list[str], example: str, n: int) -> Any:
"""Helper to make sure the command got the right number of arguments"""
if len(args) != n:
msg = (
"Got unexpected number of arguments, expected {}. "
'Got unexpected number of arguments, expected {}. '
'(example: "{} config {}")'
).format(n, get_prog(), example)
raise PipError(msg)
@ -251,16 +255,16 @@ class ConfigurationCommand(Command):
self.configuration.save()
except Exception:
logger.exception(
"Unable to save configuration. Please report this as a bug."
'Unable to save configuration. Please report this as a bug.',
)
raise PipError("Internal Error.")
raise PipError('Internal Error.')
def _determine_editor(self, options: Values) -> str:
if options.editor is not None:
return options.editor
elif "VISUAL" in os.environ:
return os.environ["VISUAL"]
elif "EDITOR" in os.environ:
return os.environ["EDITOR"]
elif 'VISUAL' in os.environ:
return os.environ['VISUAL']
elif 'EDITOR' in os.environ:
return os.environ['EDITOR']
else:
raise PipError("Could not determine editor to use.")
raise PipError('Could not determine editor to use.')

View file

@ -1,15 +1,17 @@
from __future__ import annotations
import locale
import logging
import os
import sys
from optparse import Values
from types import ModuleType
from typing import Any, Dict, List, Optional
from typing import Any
from typing import Dict
from typing import List
from typing import Optional
import pip._vendor
from pip._vendor.certifi import where
from pip._vendor.packaging.version import parse as parse_version
from pip import __file__ as pip_location
from pip._internal.cli import cmdoptions
from pip._internal.cli.base_command import Command
@ -19,51 +21,53 @@ from pip._internal.configuration import Configuration
from pip._internal.metadata import get_environment
from pip._internal.utils.logging import indent_log
from pip._internal.utils.misc import get_pip_version
from pip._vendor.certifi import where
from pip._vendor.packaging.version import parse as parse_version
logger = logging.getLogger(__name__)
def show_value(name: str, value: Any) -> None:
logger.info("%s: %s", name, value)
logger.info('%s: %s', name, value)
def show_sys_implementation() -> None:
logger.info("sys.implementation:")
logger.info('sys.implementation:')
implementation_name = sys.implementation.name
with indent_log():
show_value("name", implementation_name)
show_value('name', implementation_name)
def create_vendor_txt_map() -> Dict[str, str]:
def create_vendor_txt_map() -> dict[str, str]:
vendor_txt_path = os.path.join(
os.path.dirname(pip_location), "_vendor", "vendor.txt"
os.path.dirname(pip_location), '_vendor', 'vendor.txt',
)
with open(vendor_txt_path) as f:
# Purge non version specifying lines.
# Also, remove any space prefix or suffixes (including comments).
lines = [
line.strip().split(" ", 1)[0] for line in f.readlines() if "==" in line
line.strip().split(' ', 1)[0] for line in f.readlines() if '==' in line
]
# Transform into "module" -> version dict.
return dict(line.split("==", 1) for line in lines) # type: ignore
return dict(line.split('==', 1) for line in lines) # type: ignore
def get_module_from_module_name(module_name: str) -> ModuleType:
# Module name can be uppercase in vendor.txt for some reason...
module_name = module_name.lower()
# PATCH: setuptools is actually only pkg_resources.
if module_name == "setuptools":
module_name = "pkg_resources"
if module_name == 'setuptools':
module_name = 'pkg_resources'
__import__(f"pip._vendor.{module_name}", globals(), locals(), level=0)
__import__(f'pip._vendor.{module_name}', globals(), locals(), level=0)
return getattr(pip._vendor, module_name)
def get_vendor_version_from_module(module_name: str) -> Optional[str]:
def get_vendor_version_from_module(module_name: str) -> str | None:
module = get_module_from_module_name(module_name)
version = getattr(module, "__version__", None)
version = getattr(module, '__version__', None)
if not version:
# Try to find version in debundled module info.
@ -75,29 +79,29 @@ def get_vendor_version_from_module(module_name: str) -> Optional[str]:
return version
def show_actual_vendor_versions(vendor_txt_versions: Dict[str, str]) -> None:
def show_actual_vendor_versions(vendor_txt_versions: dict[str, str]) -> None:
"""Log the actual version and print extra info if there is
a conflict or if the actual version could not be imported.
"""
for module_name, expected_version in vendor_txt_versions.items():
extra_message = ""
extra_message = ''
actual_version = get_vendor_version_from_module(module_name)
if not actual_version:
extra_message = (
" (Unable to locate actual module version, using"
" vendor.txt specified version)"
' (Unable to locate actual module version, using'
' vendor.txt specified version)'
)
actual_version = expected_version
elif parse_version(actual_version) != parse_version(expected_version):
extra_message = (
" (CONFLICT: vendor.txt suggests version should"
" be {})".format(expected_version)
' (CONFLICT: vendor.txt suggests version should'
' be {})'.format(expected_version)
)
logger.info("%s==%s%s", module_name, actual_version, extra_message)
logger.info('%s==%s%s', module_name, actual_version, extra_message)
def show_vendor_versions() -> None:
logger.info("vendored library versions:")
logger.info('vendored library versions:')
vendor_txt_versions = create_vendor_txt_map()
with indent_log():
@ -112,11 +116,11 @@ def show_tags(options: Values) -> None:
# Display the target options that were explicitly provided.
formatted_target = target_python.format_given()
suffix = ""
suffix = ''
if formatted_target:
suffix = f" (target: {formatted_target})"
suffix = f' (target: {formatted_target})'
msg = "Compatible tags: {}{}".format(len(tags), suffix)
msg = f'Compatible tags: {len(tags)}{suffix}'
logger.info(msg)
if options.verbose < 1 and len(tags) > tag_limit:
@ -131,7 +135,7 @@ def show_tags(options: Values) -> None:
if tags_limited:
msg = (
"...\n[First {tag_limit} tags shown. Pass --verbose to show all.]"
'...\n[First {tag_limit} tags shown. Pass --verbose to show all.]'
).format(tag_limit=tag_limit)
logger.info(msg)
@ -139,21 +143,21 @@ def show_tags(options: Values) -> None:
def ca_bundle_info(config: Configuration) -> str:
levels = set()
for key, _ in config.items():
levels.add(key.split(".")[0])
levels.add(key.split('.')[0])
if not levels:
return "Not specified"
return 'Not specified'
levels_that_override_global = ["install", "wheel", "download"]
levels_that_override_global = ['install', 'wheel', 'download']
global_overriding_level = [
level for level in levels if level in levels_that_override_global
]
if not global_overriding_level:
return "global"
return 'global'
if "global" in levels:
levels.remove("global")
return ", ".join(levels)
if 'global' in levels:
levels.remove('global')
return ', '.join(levels)
class DebugCommand(Command):
@ -170,30 +174,30 @@ class DebugCommand(Command):
self.parser.insert_option_group(0, self.cmd_opts)
self.parser.config.load()
def run(self, options: Values, args: List[str]) -> int:
def run(self, options: Values, args: list[str]) -> int:
logger.warning(
"This command is only meant for debugging. "
"Do not use this with automation for parsing and getting these "
"details, since the output and options of this command may "
"change without notice."
'This command is only meant for debugging. '
'Do not use this with automation for parsing and getting these '
'details, since the output and options of this command may '
'change without notice.',
)
show_value("pip version", get_pip_version())
show_value("sys.version", sys.version)
show_value("sys.executable", sys.executable)
show_value("sys.getdefaultencoding", sys.getdefaultencoding())
show_value("sys.getfilesystemencoding", sys.getfilesystemencoding())
show_value('pip version', get_pip_version())
show_value('sys.version', sys.version)
show_value('sys.executable', sys.executable)
show_value('sys.getdefaultencoding', sys.getdefaultencoding())
show_value('sys.getfilesystemencoding', sys.getfilesystemencoding())
show_value(
"locale.getpreferredencoding",
'locale.getpreferredencoding',
locale.getpreferredencoding(),
)
show_value("sys.platform", sys.platform)
show_value('sys.platform', sys.platform)
show_sys_implementation()
show_value("'cert' config value", ca_bundle_info(self.parser.config))
show_value("REQUESTS_CA_BUNDLE", os.environ.get("REQUESTS_CA_BUNDLE"))
show_value("CURL_CA_BUNDLE", os.environ.get("CURL_CA_BUNDLE"))
show_value("pip._vendor.certifi.where()", where())
show_value("pip._vendor.DEBUNDLED", pip._vendor.DEBUNDLED)
show_value('REQUESTS_CA_BUNDLE', os.environ.get('REQUESTS_CA_BUNDLE'))
show_value('CURL_CA_BUNDLE', os.environ.get('CURL_CA_BUNDLE'))
show_value('pip._vendor.certifi.where()', where())
show_value('pip._vendor.DEBUNDLED', pip._vendor.DEBUNDLED)
show_vendor_versions()

View file

@ -1,3 +1,5 @@
from __future__ import annotations
import logging
import os
from optparse import Values
@ -5,10 +7,13 @@ from typing import List
from pip._internal.cli import cmdoptions
from pip._internal.cli.cmdoptions import make_target_python
from pip._internal.cli.req_command import RequirementCommand, with_cleanup
from pip._internal.cli.req_command import RequirementCommand
from pip._internal.cli.req_command import with_cleanup
from pip._internal.cli.status_codes import SUCCESS
from pip._internal.req.req_tracker import get_requirement_tracker
from pip._internal.utils.misc import ensure_dir, normalize_path, write_output
from pip._internal.utils.misc import ensure_dir
from pip._internal.utils.misc import normalize_path
from pip._internal.utils.misc import write_output
from pip._internal.utils.temp_dir import TempDirectory
logger = logging.getLogger(__name__)
@ -52,14 +57,14 @@ class DownloadCommand(RequirementCommand):
self.cmd_opts.add_option(cmdoptions.ignore_requires_python())
self.cmd_opts.add_option(
"-d",
"--dest",
"--destination-dir",
"--destination-directory",
dest="download_dir",
metavar="dir",
'-d',
'--dest',
'--destination-dir',
'--destination-directory',
dest='download_dir',
metavar='dir',
default=os.curdir,
help="Download packages into <dir>.",
help='Download packages into <dir>.',
)
cmdoptions.add_target_python_options(self.cmd_opts)
@ -73,7 +78,7 @@ class DownloadCommand(RequirementCommand):
self.parser.insert_option_group(0, self.cmd_opts)
@with_cleanup
def run(self, options: Values, args: List[str]) -> int:
def run(self, options: Values, args: list[str]) -> int:
options.ignore_installed = True
# editable doesn't really make sense for `pip download`, but the bowels
@ -99,7 +104,7 @@ class DownloadCommand(RequirementCommand):
directory = TempDirectory(
delete=not options.no_clean,
kind="download",
kind='download',
globally_managed=True,
)
@ -128,13 +133,13 @@ class DownloadCommand(RequirementCommand):
requirement_set = resolver.resolve(reqs, check_supported_wheels=True)
downloaded: List[str] = []
downloaded: list[str] = []
for req in requirement_set.requirements.values():
if req.satisfied_by is None:
assert req.name is not None
preparer.save_linked_requirement(req)
downloaded.append(req.name)
if downloaded:
write_output("Successfully downloaded %s", " ".join(downloaded))
write_output('Successfully downloaded %s', ' '.join(downloaded))
return SUCCESS

View file

@ -1,3 +1,5 @@
from __future__ import annotations
import sys
from optparse import Values
from typing import List
@ -8,7 +10,7 @@ from pip._internal.cli.status_codes import SUCCESS
from pip._internal.operations.freeze import freeze
from pip._internal.utils.compat import stdlib_pkgs
DEV_PKGS = {"pip", "setuptools", "distribute", "wheel"}
DEV_PKGS = {'pip', 'setuptools', 'distribute', 'wheel'}
class FreezeCommand(Command):
@ -20,61 +22,61 @@ class FreezeCommand(Command):
usage = """
%prog [options]"""
log_streams = ("ext://sys.stderr", "ext://sys.stderr")
log_streams = ('ext://sys.stderr', 'ext://sys.stderr')
def add_options(self) -> None:
self.cmd_opts.add_option(
"-r",
"--requirement",
dest="requirements",
action="append",
'-r',
'--requirement',
dest='requirements',
action='append',
default=[],
metavar="file",
metavar='file',
help=(
"Use the order in the given requirements file and its "
"comments when generating output. This option can be "
"used multiple times."
'Use the order in the given requirements file and its '
'comments when generating output. This option can be '
'used multiple times.'
),
)
self.cmd_opts.add_option(
"-l",
"--local",
dest="local",
action="store_true",
'-l',
'--local',
dest='local',
action='store_true',
default=False,
help=(
"If in a virtualenv that has global access, do not output "
"globally-installed packages."
'If in a virtualenv that has global access, do not output '
'globally-installed packages.'
),
)
self.cmd_opts.add_option(
"--user",
dest="user",
action="store_true",
'--user',
dest='user',
action='store_true',
default=False,
help="Only output packages installed in user-site.",
help='Only output packages installed in user-site.',
)
self.cmd_opts.add_option(cmdoptions.list_path())
self.cmd_opts.add_option(
"--all",
dest="freeze_all",
action="store_true",
'--all',
dest='freeze_all',
action='store_true',
help=(
"Do not skip these packages in the output:"
" {}".format(", ".join(DEV_PKGS))
'Do not skip these packages in the output:'
' {}'.format(', '.join(DEV_PKGS))
),
)
self.cmd_opts.add_option(
"--exclude-editable",
dest="exclude_editable",
action="store_true",
help="Exclude editable package from output.",
'--exclude-editable',
dest='exclude_editable',
action='store_true',
help='Exclude editable package from output.',
)
self.cmd_opts.add_option(cmdoptions.list_exclude())
self.parser.insert_option_group(0, self.cmd_opts)
def run(self, options: Values, args: List[str]) -> int:
def run(self, options: Values, args: list[str]) -> int:
skip = set(stdlib_pkgs)
if not options.freeze_all:
skip.update(DEV_PKGS)
@ -93,5 +95,5 @@ class FreezeCommand(Command):
skip=skip,
exclude_editable=options.exclude_editable,
):
sys.stdout.write(line + "\n")
sys.stdout.write(line + '\n')
return SUCCESS

View file

@ -1,3 +1,5 @@
from __future__ import annotations
import hashlib
import logging
import sys
@ -5,9 +7,12 @@ from optparse import Values
from typing import List
from pip._internal.cli.base_command import Command
from pip._internal.cli.status_codes import ERROR, SUCCESS
from pip._internal.utils.hashes import FAVORITE_HASH, STRONG_HASHES
from pip._internal.utils.misc import read_chunks, write_output
from pip._internal.cli.status_codes import ERROR
from pip._internal.cli.status_codes import SUCCESS
from pip._internal.utils.hashes import FAVORITE_HASH
from pip._internal.utils.hashes import STRONG_HASHES
from pip._internal.utils.misc import read_chunks
from pip._internal.utils.misc import write_output
logger = logging.getLogger(__name__)
@ -20,24 +25,24 @@ class HashCommand(Command):
installs.
"""
usage = "%prog [options] <file> ..."
usage = '%prog [options] <file> ...'
ignore_require_venv = True
def add_options(self) -> None:
self.cmd_opts.add_option(
"-a",
"--algorithm",
dest="algorithm",
'-a',
'--algorithm',
dest='algorithm',
choices=STRONG_HASHES,
action="store",
action='store',
default=FAVORITE_HASH,
help="The hash algorithm to use: one of {}".format(
", ".join(STRONG_HASHES)
help='The hash algorithm to use: one of {}'.format(
', '.join(STRONG_HASHES),
),
)
self.parser.insert_option_group(0, self.cmd_opts)
def run(self, options: Values, args: List[str]) -> int:
def run(self, options: Values, args: list[str]) -> int:
if not args:
self.parser.print_usage(sys.stderr)
return ERROR
@ -45,14 +50,14 @@ class HashCommand(Command):
algorithm = options.algorithm
for path in args:
write_output(
"%s:\n--hash=%s:%s", path, algorithm, _hash_of_file(path, algorithm)
'%s:\n--hash=%s:%s', path, algorithm, _hash_of_file(path, algorithm),
)
return SUCCESS
def _hash_of_file(path: str, algorithm: str) -> str:
"""Return the hash digest of a file."""
with open(path, "rb") as archive:
with open(path, 'rb') as archive:
hash = hashlib.new(algorithm)
for chunk in read_chunks(archive):
hash.update(chunk)

View file

@ -1,3 +1,5 @@
from __future__ import annotations
from optparse import Values
from typing import List
@ -13,7 +15,7 @@ class HelpCommand(Command):
%prog <command>"""
ignore_require_venv = True
def run(self, options: Values, args: List[str]) -> int:
def run(self, options: Values, args: list[str]) -> int:
from pip._internal.commands import (
commands_dict,
create_command,
@ -33,7 +35,7 @@ class HelpCommand(Command):
if guess:
msg.append(f'maybe you meant "{guess}"')
raise CommandError(" - ".join(msg))
raise CommandError(' - '.join(msg))
command = create_command(cmd_name)
command.parser.print_help()

View file

@ -1,20 +1,29 @@
from __future__ import annotations
import logging
from optparse import Values
from typing import Any, Iterable, List, Optional, Union
from pip._vendor.packaging.version import LegacyVersion, Version
from typing import Any
from typing import Iterable
from typing import List
from typing import Optional
from typing import Union
from pip._internal.cli import cmdoptions
from pip._internal.cli.req_command import IndexGroupCommand
from pip._internal.cli.status_codes import ERROR, SUCCESS
from pip._internal.cli.status_codes import ERROR
from pip._internal.cli.status_codes import SUCCESS
from pip._internal.commands.search import print_dist_installation_info
from pip._internal.exceptions import CommandError, DistributionNotFound, PipError
from pip._internal.exceptions import CommandError
from pip._internal.exceptions import DistributionNotFound
from pip._internal.exceptions import PipError
from pip._internal.index.collector import LinkCollector
from pip._internal.index.package_finder import PackageFinder
from pip._internal.models.selection_prefs import SelectionPreferences
from pip._internal.models.target_python import TargetPython
from pip._internal.network.session import PipSession
from pip._internal.utils.misc import write_output
from pip._vendor.packaging.version import LegacyVersion
from pip._vendor.packaging.version import Version
logger = logging.getLogger(__name__)
@ -44,22 +53,22 @@ class IndexCommand(IndexGroupCommand):
self.parser.insert_option_group(0, index_opts)
self.parser.insert_option_group(0, self.cmd_opts)
def run(self, options: Values, args: List[str]) -> int:
def run(self, options: Values, args: list[str]) -> int:
handlers = {
"versions": self.get_available_package_versions,
'versions': self.get_available_package_versions,
}
logger.warning(
"pip index is currently an experimental command. "
"It may be removed/changed in a future release "
"without prior warning."
'pip index is currently an experimental command. '
'It may be removed/changed in a future release '
'without prior warning.',
)
# Determine action
if not args or args[0] not in handlers:
logger.error(
"Need an action (%s) to perform.",
", ".join(sorted(handlers)),
'Need an action (%s) to perform.',
', '.join(sorted(handlers)),
)
return ERROR
@ -78,8 +87,8 @@ class IndexCommand(IndexGroupCommand):
self,
options: Values,
session: PipSession,
target_python: Optional[TargetPython] = None,
ignore_requires_python: Optional[bool] = None,
target_python: TargetPython | None = None,
ignore_requires_python: bool | None = None,
) -> PackageFinder:
"""
Create a package finder appropriate to the index command.
@ -97,12 +106,12 @@ class IndexCommand(IndexGroupCommand):
link_collector=link_collector,
selection_prefs=selection_prefs,
target_python=target_python,
use_deprecated_html5lib="html5lib" in options.deprecated_features_enabled,
use_deprecated_html5lib='html5lib' in options.deprecated_features_enabled,
)
def get_available_package_versions(self, options: Values, args: List[Any]) -> None:
def get_available_package_versions(self, options: Values, args: list[Any]) -> None:
if len(args) != 1:
raise CommandError("You need to specify exactly one argument")
raise CommandError('You need to specify exactly one argument')
target_python = cmdoptions.make_target_python(options)
query = args[0]
@ -115,7 +124,7 @@ class IndexCommand(IndexGroupCommand):
ignore_requires_python=options.ignore_requires_python,
)
versions: Iterable[Union[LegacyVersion, Version]] = (
versions: Iterable[LegacyVersion | Version] = (
candidate.version for candidate in finder.find_all_candidates(query)
)
@ -128,12 +137,12 @@ class IndexCommand(IndexGroupCommand):
if not versions:
raise DistributionNotFound(
"No matching distribution found for {}".format(query)
f'No matching distribution found for {query}',
)
formatted_versions = [str(ver) for ver in sorted(versions, reverse=True)]
latest = formatted_versions[0]
write_output("{} ({})".format(query, latest))
write_output("Available versions: {}".format(", ".join(formatted_versions)))
write_output(f'{query} ({latest})')
write_output('Available versions: {}'.format(', '.join(formatted_versions)))
print_dist_installation_info(query, latest)

View file

@ -1,27 +1,31 @@
from __future__ import annotations
import errno
import operator
import os
import shutil
import site
from optparse import SUPPRESS_HELP, Values
from typing import Iterable, List, Optional
from pip._vendor.packaging.utils import canonicalize_name
from optparse import SUPPRESS_HELP
from optparse import Values
from typing import Iterable
from typing import List
from typing import Optional
from pip._internal.cache import WheelCache
from pip._internal.cli import cmdoptions
from pip._internal.cli.cmdoptions import make_target_python
from pip._internal.cli.req_command import (
RequirementCommand,
warn_if_run_as_root,
with_cleanup,
)
from pip._internal.cli.status_codes import ERROR, SUCCESS
from pip._internal.exceptions import CommandError, InstallationError
from pip._internal.cli.req_command import RequirementCommand
from pip._internal.cli.req_command import warn_if_run_as_root
from pip._internal.cli.req_command import with_cleanup
from pip._internal.cli.status_codes import ERROR
from pip._internal.cli.status_codes import SUCCESS
from pip._internal.exceptions import CommandError
from pip._internal.exceptions import InstallationError
from pip._internal.locations import get_scheme
from pip._internal.metadata import get_environment
from pip._internal.models.format_control import FormatControl
from pip._internal.operations.check import ConflictDetails, check_install_conflicts
from pip._internal.operations.check import check_install_conflicts
from pip._internal.operations.check import ConflictDetails
from pip._internal.req import install_given_reqs
from pip._internal.req.req_install import InstallRequirement
from pip._internal.req.req_tracker import get_requirement_tracker
@ -29,31 +33,26 @@ from pip._internal.utils.compat import WINDOWS
from pip._internal.utils.distutils_args import parse_distutils_args
from pip._internal.utils.filesystem import test_writable_dir
from pip._internal.utils.logging import getLogger
from pip._internal.utils.misc import (
ensure_dir,
get_pip_version,
protect_pip_from_modification_on_windows,
write_output,
)
from pip._internal.utils.misc import ensure_dir
from pip._internal.utils.misc import get_pip_version
from pip._internal.utils.misc import protect_pip_from_modification_on_windows
from pip._internal.utils.misc import write_output
from pip._internal.utils.temp_dir import TempDirectory
from pip._internal.utils.virtualenv import (
running_under_virtualenv,
virtualenv_no_global,
)
from pip._internal.wheel_builder import (
BinaryAllowedPredicate,
build,
should_build_for_install_command,
)
from pip._internal.utils.virtualenv import running_under_virtualenv
from pip._internal.utils.virtualenv import virtualenv_no_global
from pip._internal.wheel_builder import BinaryAllowedPredicate
from pip._internal.wheel_builder import build
from pip._internal.wheel_builder import should_build_for_install_command
from pip._vendor.packaging.utils import canonicalize_name
logger = getLogger(__name__)
def get_check_binary_allowed(format_control: FormatControl) -> BinaryAllowedPredicate:
def check_binary_allowed(req: InstallRequirement) -> bool:
canonical_name = canonicalize_name(req.name or "")
canonical_name = canonicalize_name(req.name or '')
allowed_formats = format_control.get_allowed_formats(canonical_name)
return "binary" in allowed_formats
return 'binary' in allowed_formats
return check_binary_allowed
@ -86,102 +85,102 @@ class InstallCommand(RequirementCommand):
self.cmd_opts.add_option(cmdoptions.editable())
self.cmd_opts.add_option(
"-t",
"--target",
dest="target_dir",
metavar="dir",
'-t',
'--target',
dest='target_dir',
metavar='dir',
default=None,
help=(
"Install packages into <dir>. "
"By default this will not replace existing files/folders in "
"<dir>. Use --upgrade to replace existing packages in <dir> "
"with new versions."
'Install packages into <dir>. '
'By default this will not replace existing files/folders in '
'<dir>. Use --upgrade to replace existing packages in <dir> '
'with new versions.'
),
)
cmdoptions.add_target_python_options(self.cmd_opts)
self.cmd_opts.add_option(
"--user",
dest="use_user_site",
action="store_true",
'--user',
dest='use_user_site',
action='store_true',
help=(
"Install to the Python user install directory for your "
"platform. Typically ~/.local/, or %APPDATA%\\Python on "
"Windows. (See the Python documentation for site.USER_BASE "
"for full details.)"
'Install to the Python user install directory for your '
'platform. Typically ~/.local/, or %APPDATA%\\Python on '
'Windows. (See the Python documentation for site.USER_BASE '
'for full details.)'
),
)
self.cmd_opts.add_option(
"--no-user",
dest="use_user_site",
action="store_false",
'--no-user',
dest='use_user_site',
action='store_false',
help=SUPPRESS_HELP,
)
self.cmd_opts.add_option(
"--root",
dest="root_path",
metavar="dir",
'--root',
dest='root_path',
metavar='dir',
default=None,
help="Install everything relative to this alternate root directory.",
help='Install everything relative to this alternate root directory.',
)
self.cmd_opts.add_option(
"--prefix",
dest="prefix_path",
metavar="dir",
'--prefix',
dest='prefix_path',
metavar='dir',
default=None,
help=(
"Installation prefix where lib, bin and other top-level "
"folders are placed"
'Installation prefix where lib, bin and other top-level '
'folders are placed'
),
)
self.cmd_opts.add_option(cmdoptions.src())
self.cmd_opts.add_option(
"-U",
"--upgrade",
dest="upgrade",
action="store_true",
'-U',
'--upgrade',
dest='upgrade',
action='store_true',
help=(
"Upgrade all specified packages to the newest available "
"version. The handling of dependencies depends on the "
"upgrade-strategy used."
'Upgrade all specified packages to the newest available '
'version. The handling of dependencies depends on the '
'upgrade-strategy used.'
),
)
self.cmd_opts.add_option(
"--upgrade-strategy",
dest="upgrade_strategy",
default="only-if-needed",
choices=["only-if-needed", "eager"],
'--upgrade-strategy',
dest='upgrade_strategy',
default='only-if-needed',
choices=['only-if-needed', 'eager'],
help=(
"Determines how dependency upgrading should be handled "
"[default: %default]. "
'Determines how dependency upgrading should be handled '
'[default: %default]. '
'"eager" - dependencies are upgraded regardless of '
"whether the currently installed version satisfies the "
"requirements of the upgraded package(s). "
'whether the currently installed version satisfies the '
'requirements of the upgraded package(s). '
'"only-if-needed" - are upgraded only when they do not '
"satisfy the requirements of the upgraded package(s)."
'satisfy the requirements of the upgraded package(s).'
),
)
self.cmd_opts.add_option(
"--force-reinstall",
dest="force_reinstall",
action="store_true",
help="Reinstall all packages even if they are already up-to-date.",
'--force-reinstall',
dest='force_reinstall',
action='store_true',
help='Reinstall all packages even if they are already up-to-date.',
)
self.cmd_opts.add_option(
"-I",
"--ignore-installed",
dest="ignore_installed",
action="store_true",
'-I',
'--ignore-installed',
dest='ignore_installed',
action='store_true',
help=(
"Ignore the installed packages, overwriting them. "
"This can break your system if the existing package "
"is of a different version or was installed "
"with a different package manager!"
'Ignore the installed packages, overwriting them. '
'This can break your system if the existing package '
'is of a different version or was installed '
'with a different package manager!'
),
)
@ -194,33 +193,33 @@ class InstallCommand(RequirementCommand):
self.cmd_opts.add_option(cmdoptions.global_options())
self.cmd_opts.add_option(
"--compile",
action="store_true",
dest="compile",
'--compile',
action='store_true',
dest='compile',
default=True,
help="Compile Python source files to bytecode",
help='Compile Python source files to bytecode',
)
self.cmd_opts.add_option(
"--no-compile",
action="store_false",
dest="compile",
help="Do not compile Python source files to bytecode",
'--no-compile',
action='store_false',
dest='compile',
help='Do not compile Python source files to bytecode',
)
self.cmd_opts.add_option(
"--no-warn-script-location",
action="store_false",
dest="warn_script_location",
'--no-warn-script-location',
action='store_false',
dest='warn_script_location',
default=True,
help="Do not warn when installing scripts outside PATH",
help='Do not warn when installing scripts outside PATH',
)
self.cmd_opts.add_option(
"--no-warn-conflicts",
action="store_false",
dest="warn_about_conflicts",
'--no-warn-conflicts',
action='store_false',
dest='warn_about_conflicts',
default=True,
help="Do not warn about broken dependencies",
help='Do not warn about broken dependencies',
)
self.cmd_opts.add_option(cmdoptions.no_binary())
@ -238,12 +237,12 @@ class InstallCommand(RequirementCommand):
self.parser.insert_option_group(0, self.cmd_opts)
@with_cleanup
def run(self, options: Values, args: List[str]) -> int:
def run(self, options: Values, args: list[str]) -> int:
if options.use_user_site and options.target_dir is not None:
raise CommandError("Can not combine '--user' and '--target'")
cmdoptions.check_install_build_global(options)
upgrade_strategy = "to-satisfy-only"
upgrade_strategy = 'to-satisfy-only'
if options.upgrade:
upgrade_strategy = options.upgrade_strategy
@ -251,7 +250,7 @@ class InstallCommand(RequirementCommand):
install_options = options.install_options or []
logger.verbose("Using %s", get_pip_version())
logger.verbose('Using %s', get_pip_version())
options.use_user_site = decide_user_install(
options.use_user_site,
prefix_path=options.prefix_path,
@ -260,8 +259,8 @@ class InstallCommand(RequirementCommand):
isolated_mode=options.isolated_mode,
)
target_temp_dir: Optional[TempDirectory] = None
target_temp_dir_path: Optional[str] = None
target_temp_dir: TempDirectory | None = None
target_temp_dir_path: str | None = None
if options.target_dir:
options.ignore_installed = True
options.target_dir = os.path.abspath(options.target_dir)
@ -272,11 +271,11 @@ class InstallCommand(RequirementCommand):
# fmt: on
):
raise CommandError(
"Target path exists but is not a directory, will not continue."
'Target path exists but is not a directory, will not continue.',
)
# Create a target directory for using with the target option
target_temp_dir = TempDirectory(kind="target")
target_temp_dir = TempDirectory(kind='target')
target_temp_dir_path = target_temp_dir.path
self.enter_context(target_temp_dir)
@ -297,7 +296,7 @@ class InstallCommand(RequirementCommand):
directory = TempDirectory(
delete=not options.no_clean,
kind="install",
kind='install',
globally_managed=True,
)
@ -337,11 +336,11 @@ class InstallCommand(RequirementCommand):
self.trace_basic_info(finder)
requirement_set = resolver.resolve(
reqs, check_supported_wheels=not options.target_dir
reqs, check_supported_wheels=not options.target_dir,
)
try:
pip_req = requirement_set.get_requirement("pip")
pip_req = requirement_set.get_requirement('pip')
except KeyError:
modifying_pip = False
else:
@ -368,15 +367,15 @@ class InstallCommand(RequirementCommand):
# If we're using PEP 517, we cannot do a legacy setup.py install
# so we fail here.
pep517_build_failure_names: List[str] = [
pep517_build_failure_names: list[str] = [
r.name for r in build_failures if r.use_pep517 # type: ignore
]
if pep517_build_failure_names:
raise InstallationError(
"Could not build wheels for {}, which is required to "
"install pyproject.toml-based projects".format(
", ".join(pep517_build_failure_names)
)
'Could not build wheels for {}, which is required to '
'install pyproject.toml-based projects'.format(
', '.join(pep517_build_failure_names),
),
)
# For now, we just warn about failures building legacy
@ -389,7 +388,7 @@ class InstallCommand(RequirementCommand):
to_install = resolver.get_installation_order(requirement_set)
# Check for conflicts in the package set we're installing.
conflicts: Optional[ConflictDetails] = None
conflicts: ConflictDetails | None = None
should_warn_about_conflicts = (
not options.ignore_dependencies and options.warn_about_conflicts
)
@ -423,14 +422,14 @@ class InstallCommand(RequirementCommand):
)
env = get_environment(lib_locations)
installed.sort(key=operator.attrgetter("name"))
installed.sort(key=operator.attrgetter('name'))
items = []
for result in installed:
item = result.name
try:
installed_dist = env.get_distribution(item)
if installed_dist is not None:
item = f"{item}-{installed_dist.version}"
item = f'{item}-{installed_dist.version}'
except Exception:
pass
items.append(item)
@ -441,10 +440,10 @@ class InstallCommand(RequirementCommand):
resolver_variant=self.determine_resolver_variant(options),
)
installed_desc = " ".join(items)
installed_desc = ' '.join(items)
if installed_desc:
write_output(
"Successfully installed %s",
'Successfully installed %s',
installed_desc,
)
except OSError as error:
@ -462,14 +461,14 @@ class InstallCommand(RequirementCommand):
if options.target_dir:
assert target_temp_dir
self._handle_target_dir(
options.target_dir, target_temp_dir, options.upgrade
options.target_dir, target_temp_dir, options.upgrade,
)
warn_if_run_as_root()
return SUCCESS
def _handle_target_dir(
self, target_dir: str, target_temp_dir: TempDirectory, upgrade: bool
self, target_dir: str, target_temp_dir: TempDirectory, upgrade: bool,
) -> None:
ensure_dir(target_dir)
@ -479,7 +478,7 @@ class InstallCommand(RequirementCommand):
# Checking both purelib and platlib directories for installed
# packages to be moved to target directory
scheme = get_scheme("", home=target_temp_dir.path)
scheme = get_scheme('', home=target_temp_dir.path)
purelib_dir = scheme.purelib
platlib_dir = scheme.platlib
data_dir = scheme.data
@ -501,17 +500,17 @@ class InstallCommand(RequirementCommand):
if os.path.exists(target_item_dir):
if not upgrade:
logger.warning(
"Target directory %s already exists. Specify "
"--upgrade to force replacement.",
'Target directory %s already exists. Specify '
'--upgrade to force replacement.',
target_item_dir,
)
continue
if os.path.islink(target_item_dir):
logger.warning(
"Target directory %s already exists and is "
"a link. pip will not automatically replace "
"links, please remove if replacement is "
"desired.",
'Target directory %s already exists and is '
'a link. pip will not automatically replace '
'links, please remove if replacement is '
'desired.',
target_item_dir,
)
continue
@ -523,37 +522,37 @@ class InstallCommand(RequirementCommand):
shutil.move(os.path.join(lib_dir, item), target_item_dir)
def _determine_conflicts(
self, to_install: List[InstallRequirement]
) -> Optional[ConflictDetails]:
self, to_install: list[InstallRequirement],
) -> ConflictDetails | None:
try:
return check_install_conflicts(to_install)
except Exception:
logger.exception(
"Error while checking for conflicts. Please file an issue on "
"pip's issue tracker: https://github.com/pypa/pip/issues/new"
'Error while checking for conflicts. Please file an issue on '
"pip's issue tracker: https://github.com/pypa/pip/issues/new",
)
return None
def _warn_about_conflicts(
self, conflict_details: ConflictDetails, resolver_variant: str
self, conflict_details: ConflictDetails, resolver_variant: str,
) -> None:
package_set, (missing, conflicting) = conflict_details
if not missing and not conflicting:
return
parts: List[str] = []
if resolver_variant == "legacy":
parts: list[str] = []
if resolver_variant == 'legacy':
parts.append(
"pip's legacy dependency resolver does not consider dependency "
"conflicts when selecting packages. This behaviour is the "
"source of the following dependency conflicts."
'conflicts when selecting packages. This behaviour is the '
'source of the following dependency conflicts.',
)
else:
assert resolver_variant == "2020-resolver"
assert resolver_variant == '2020-resolver'
parts.append(
"pip's dependency resolver does not currently take into account "
"all the packages that are installed. This behaviour is the "
"source of the following dependency conflicts."
'all the packages that are installed. This behaviour is the '
'source of the following dependency conflicts.',
)
# NOTE: There is some duplication here, with commands/check.py
@ -561,8 +560,8 @@ class InstallCommand(RequirementCommand):
version = package_set[project_name][0]
for dependency in missing[project_name]:
message = (
"{name} {version} requires {requirement}, "
"which is not installed."
'{name} {version} requires {requirement}, '
'which is not installed.'
).format(
name=project_name,
version=version,
@ -574,30 +573,30 @@ class InstallCommand(RequirementCommand):
version = package_set[project_name][0]
for dep_name, dep_version, req in conflicting[project_name]:
message = (
"{name} {version} requires {requirement}, but {you} have "
"{dep_name} {dep_version} which is incompatible."
'{name} {version} requires {requirement}, but {you} have '
'{dep_name} {dep_version} which is incompatible.'
).format(
name=project_name,
version=version,
requirement=req,
dep_name=dep_name,
dep_version=dep_version,
you=("you" if resolver_variant == "2020-resolver" else "you'll"),
you=('you' if resolver_variant == '2020-resolver' else "you'll"),
)
parts.append(message)
logger.critical("\n".join(parts))
logger.critical('\n'.join(parts))
def get_lib_location_guesses(
user: bool = False,
home: Optional[str] = None,
root: Optional[str] = None,
home: str | None = None,
root: str | None = None,
isolated: bool = False,
prefix: Optional[str] = None,
) -> List[str]:
prefix: str | None = None,
) -> list[str]:
scheme = get_scheme(
"",
'',
user=user,
home=home,
root=root,
@ -607,7 +606,7 @@ def get_lib_location_guesses(
return [scheme.purelib, scheme.platlib]
def site_packages_writable(root: Optional[str], isolated: bool) -> bool:
def site_packages_writable(root: str | None, isolated: bool) -> bool:
return all(
test_writable_dir(d)
for d in set(get_lib_location_guesses(root=root, isolated=isolated))
@ -615,10 +614,10 @@ def site_packages_writable(root: Optional[str], isolated: bool) -> bool:
def decide_user_install(
use_user_site: Optional[bool],
prefix_path: Optional[str] = None,
target_dir: Optional[str] = None,
root_path: Optional[str] = None,
use_user_site: bool | None,
prefix_path: str | None = None,
target_dir: str | None = None,
root_path: str | None = None,
isolated_mode: bool = False,
) -> bool:
"""Determine whether to do a user install based on the input options.
@ -632,21 +631,21 @@ def decide_user_install(
# In some cases (config from tox), use_user_site can be set to an integer
# rather than a bool, which 'use_user_site is False' wouldn't catch.
if (use_user_site is not None) and (not use_user_site):
logger.debug("Non-user install by explicit request")
logger.debug('Non-user install by explicit request')
return False
if use_user_site:
if prefix_path:
raise CommandError(
"Can not combine '--user' and '--prefix' as they imply "
"different installation locations"
'different installation locations',
)
if virtualenv_no_global():
raise InstallationError(
"Can not perform a '--user' install. User site-packages "
"are not visible in this virtualenv."
'are not visible in this virtualenv.',
)
logger.debug("User install by explicit request")
logger.debug('User install by explicit request')
return True
# If we are here, user installs have not been explicitly requested/avoided
@ -654,36 +653,36 @@ def decide_user_install(
# user install incompatible with --prefix/--target
if prefix_path or target_dir:
logger.debug("Non-user install due to --prefix or --target option")
logger.debug('Non-user install due to --prefix or --target option')
return False
# If user installs are not enabled, choose a non-user install
if not site.ENABLE_USER_SITE:
logger.debug("Non-user install because user site-packages disabled")
logger.debug('Non-user install because user site-packages disabled')
return False
# If we have permission for a non-user install, do that,
# otherwise do a user install.
if site_packages_writable(root=root_path, isolated=isolated_mode):
logger.debug("Non-user install because site-packages writeable")
logger.debug('Non-user install because site-packages writeable')
return False
logger.info(
"Defaulting to user installation because normal site-packages "
"is not writeable"
'Defaulting to user installation because normal site-packages '
'is not writeable',
)
return True
def reject_location_related_install_options(
requirements: List[InstallRequirement], options: Optional[List[str]]
requirements: list[InstallRequirement], options: list[str] | None,
) -> None:
"""If any location-changing --install-option arguments were passed for
requirements or on the command-line, then show a deprecation warning.
"""
def format_options(option_names: Iterable[str]) -> List[str]:
return ["--{}".format(name.replace("_", "-")) for name in option_names]
def format_options(option_names: Iterable[str]) -> list[str]:
return ['--{}'.format(name.replace('_', '-')) for name in option_names]
offenders = []
@ -692,30 +691,30 @@ def reject_location_related_install_options(
location_options = parse_distutils_args(install_options)
if location_options:
offenders.append(
"{!r} from {}".format(
format_options(location_options.keys()), requirement
)
'{!r} from {}'.format(
format_options(location_options.keys()), requirement,
),
)
if options:
location_options = parse_distutils_args(options)
if location_options:
offenders.append(
"{!r} from command line".format(format_options(location_options.keys()))
f'{format_options(location_options.keys())!r} from command line',
)
if not offenders:
return
raise CommandError(
"Location-changing options found in --install-option: {}."
" This is unsupported, use pip-level options like --user,"
" --prefix, --root, and --target instead.".format("; ".join(offenders))
'Location-changing options found in --install-option: {}.'
' This is unsupported, use pip-level options like --user,'
' --prefix, --root, and --target instead.'.format('; '.join(offenders)),
)
def create_os_error_message(
error: OSError, show_traceback: bool, using_user_site: bool
error: OSError, show_traceback: bool, using_user_site: bool,
) -> str:
"""Format an error message for an OSError
@ -724,48 +723,48 @@ def create_os_error_message(
parts = []
# Mention the error if we are not going to show a traceback
parts.append("Could not install packages due to an OSError")
parts.append('Could not install packages due to an OSError')
if not show_traceback:
parts.append(": ")
parts.append(': ')
parts.append(str(error))
else:
parts.append(".")
parts.append('.')
# Spilt the error indication from a helper message (if any)
parts[-1] += "\n"
parts[-1] += '\n'
# Suggest useful actions to the user:
# (1) using user site-packages or (2) verifying the permissions
if error.errno == errno.EACCES:
user_option_part = "Consider using the `--user` option"
permissions_part = "Check the permissions"
user_option_part = 'Consider using the `--user` option'
permissions_part = 'Check the permissions'
if not running_under_virtualenv() and not using_user_site:
parts.extend(
[
user_option_part,
" or ",
' or ',
permissions_part.lower(),
]
],
)
else:
parts.append(permissions_part)
parts.append(".\n")
parts.append('.\n')
# Suggest the user to enable Long Paths if path length is
# more than 260
if (
WINDOWS
and error.errno == errno.ENOENT
and error.filename
and len(error.filename) > 260
WINDOWS and
error.errno == errno.ENOENT and
error.filename and
len(error.filename) > 260
):
parts.append(
"HINT: This error might have occurred since "
"this system does not have Windows Long Path "
"support enabled. You can find information on "
"how to enable this at "
"https://pip.pypa.io/warnings/enable-long-paths\n"
'HINT: This error might have occurred since '
'this system does not have Windows Long Path '
'support enabled. You can find information on '
'how to enable this at '
'https://pip.pypa.io/warnings/enable-long-paths\n',
)
return "".join(parts).strip() + "\n"
return ''.join(parts).strip() + '\n'

View file

@ -1,9 +1,15 @@
from __future__ import annotations
import json
import logging
from optparse import Values
from typing import TYPE_CHECKING, Iterator, List, Optional, Sequence, Tuple, cast
from pip._vendor.packaging.utils import canonicalize_name
from typing import cast
from typing import Iterator
from typing import List
from typing import Optional
from typing import Sequence
from typing import Tuple
from typing import TYPE_CHECKING
from pip._internal.cli import cmdoptions
from pip._internal.cli.req_command import IndexGroupCommand
@ -11,11 +17,14 @@ from pip._internal.cli.status_codes import SUCCESS
from pip._internal.exceptions import CommandError
from pip._internal.index.collector import LinkCollector
from pip._internal.index.package_finder import PackageFinder
from pip._internal.metadata import BaseDistribution, get_environment
from pip._internal.metadata import BaseDistribution
from pip._internal.metadata import get_environment
from pip._internal.models.selection_prefs import SelectionPreferences
from pip._internal.network.session import PipSession
from pip._internal.utils.compat import stdlib_pkgs
from pip._internal.utils.misc import tabulate, write_output
from pip._internal.utils.misc import tabulate
from pip._internal.utils.misc import write_output
from pip._vendor.packaging.utils import canonicalize_name
if TYPE_CHECKING:
from pip._internal.metadata.base import DistributionVersion
@ -49,81 +58,81 @@ class ListCommand(IndexGroupCommand):
def add_options(self) -> None:
self.cmd_opts.add_option(
"-o",
"--outdated",
action="store_true",
'-o',
'--outdated',
action='store_true',
default=False,
help="List outdated packages",
help='List outdated packages',
)
self.cmd_opts.add_option(
"-u",
"--uptodate",
action="store_true",
'-u',
'--uptodate',
action='store_true',
default=False,
help="List uptodate packages",
help='List uptodate packages',
)
self.cmd_opts.add_option(
"-e",
"--editable",
action="store_true",
'-e',
'--editable',
action='store_true',
default=False,
help="List editable projects.",
help='List editable projects.',
)
self.cmd_opts.add_option(
"-l",
"--local",
action="store_true",
'-l',
'--local',
action='store_true',
default=False,
help=(
"If in a virtualenv that has global access, do not list "
"globally-installed packages."
'If in a virtualenv that has global access, do not list '
'globally-installed packages.'
),
)
self.cmd_opts.add_option(
"--user",
dest="user",
action="store_true",
'--user',
dest='user',
action='store_true',
default=False,
help="Only output packages installed in user-site.",
help='Only output packages installed in user-site.',
)
self.cmd_opts.add_option(cmdoptions.list_path())
self.cmd_opts.add_option(
"--pre",
action="store_true",
'--pre',
action='store_true',
default=False,
help=(
"Include pre-release and development versions. By default, "
"pip only finds stable versions."
'Include pre-release and development versions. By default, '
'pip only finds stable versions.'
),
)
self.cmd_opts.add_option(
"--format",
action="store",
dest="list_format",
default="columns",
choices=("columns", "freeze", "json"),
help="Select the output format among: columns (default), freeze, or json",
'--format',
action='store',
dest='list_format',
default='columns',
choices=('columns', 'freeze', 'json'),
help='Select the output format among: columns (default), freeze, or json',
)
self.cmd_opts.add_option(
"--not-required",
action="store_true",
dest="not_required",
help="List packages that are not dependencies of installed packages.",
'--not-required',
action='store_true',
dest='not_required',
help='List packages that are not dependencies of installed packages.',
)
self.cmd_opts.add_option(
"--exclude-editable",
action="store_false",
dest="include_editable",
help="Exclude editable package from output.",
'--exclude-editable',
action='store_false',
dest='include_editable',
help='Exclude editable package from output.',
)
self.cmd_opts.add_option(
"--include-editable",
action="store_true",
dest="include_editable",
help="Include editable package from output.",
'--include-editable',
action='store_true',
dest='include_editable',
help='Include editable package from output.',
default=True,
)
self.cmd_opts.add_option(cmdoptions.list_exclude())
@ -133,7 +142,7 @@ class ListCommand(IndexGroupCommand):
self.parser.insert_option_group(0, self.cmd_opts)
def _build_package_finder(
self, options: Values, session: PipSession
self, options: Values, session: PipSession,
) -> PackageFinder:
"""
Create a package finder appropriate to this list command.
@ -149,12 +158,12 @@ class ListCommand(IndexGroupCommand):
return PackageFinder.create(
link_collector=link_collector,
selection_prefs=selection_prefs,
use_deprecated_html5lib="html5lib" in options.deprecated_features_enabled,
use_deprecated_html5lib='html5lib' in options.deprecated_features_enabled,
)
def run(self, options: Values, args: List[str]) -> int:
def run(self, options: Values, args: list[str]) -> int:
if options.outdated and options.uptodate:
raise CommandError("Options --outdated and --uptodate cannot be combined.")
raise CommandError('Options --outdated and --uptodate cannot be combined.')
cmdoptions.check_list_path_option(options)
@ -162,8 +171,8 @@ class ListCommand(IndexGroupCommand):
if options.excludes:
skip.update(canonicalize_name(n) for n in options.excludes)
packages: "_ProcessedDists" = [
cast("_DistWithLatestInfo", d)
packages: _ProcessedDists = [
cast('_DistWithLatestInfo', d)
for d in get_environment(options.path).iter_installed_distributions(
local_only=options.local,
user_only=options.user,
@ -189,8 +198,8 @@ class ListCommand(IndexGroupCommand):
return SUCCESS
def get_outdated(
self, packages: "_ProcessedDists", options: Values
) -> "_ProcessedDists":
self, packages: _ProcessedDists, options: Values,
) -> _ProcessedDists:
return [
dist
for dist in self.iter_packages_latest_infos(packages, options)
@ -198,8 +207,8 @@ class ListCommand(IndexGroupCommand):
]
def get_uptodate(
self, packages: "_ProcessedDists", options: Values
) -> "_ProcessedDists":
self, packages: _ProcessedDists, options: Values,
) -> _ProcessedDists:
return [
dist
for dist in self.iter_packages_latest_infos(packages, options)
@ -207,8 +216,8 @@ class ListCommand(IndexGroupCommand):
]
def get_not_required(
self, packages: "_ProcessedDists", options: Values
) -> "_ProcessedDists":
self, packages: _ProcessedDists, options: Values,
) -> _ProcessedDists:
dep_keys = {
canonicalize_name(dep.name)
for dist in packages
@ -221,14 +230,14 @@ class ListCommand(IndexGroupCommand):
return list({pkg for pkg in packages if pkg.canonical_name not in dep_keys})
def iter_packages_latest_infos(
self, packages: "_ProcessedDists", options: Values
) -> Iterator["_DistWithLatestInfo"]:
self, packages: _ProcessedDists, options: Values,
) -> Iterator[_DistWithLatestInfo]:
with self._build_session(options) as session:
finder = self._build_package_finder(options, session)
def latest_info(
dist: "_DistWithLatestInfo",
) -> Optional["_DistWithLatestInfo"]:
dist: _DistWithLatestInfo,
) -> _DistWithLatestInfo | None:
all_candidates = finder.find_all_candidates(dist.canonical_name)
if not options.pre:
# Remove prereleases
@ -247,9 +256,9 @@ class ListCommand(IndexGroupCommand):
remote_version = best_candidate.version
if best_candidate.link.is_wheel:
typ = "wheel"
typ = 'wheel'
else:
typ = "sdist"
typ = 'sdist'
dist.latest_version = remote_version
dist.latest_filetype = typ
return dist
@ -259,28 +268,28 @@ class ListCommand(IndexGroupCommand):
yield dist
def output_package_listing(
self, packages: "_ProcessedDists", options: Values
self, packages: _ProcessedDists, options: Values,
) -> None:
packages = sorted(
packages,
key=lambda dist: dist.canonical_name,
)
if options.list_format == "columns" and packages:
if options.list_format == 'columns' and packages:
data, header = format_for_columns(packages, options)
self.output_package_listing_columns(data, header)
elif options.list_format == "freeze":
elif options.list_format == 'freeze':
for dist in packages:
if options.verbose >= 1:
write_output(
"%s==%s (%s)", dist.raw_name, dist.version, dist.location
'%s==%s (%s)', dist.raw_name, dist.version, dist.location,
)
else:
write_output("%s==%s", dist.raw_name, dist.version)
elif options.list_format == "json":
write_output('%s==%s', dist.raw_name, dist.version)
elif options.list_format == 'json':
write_output(format_for_json(packages, options))
def output_package_listing_columns(
self, data: List[List[str]], header: List[str]
self, data: list[list[str]], header: list[str],
) -> None:
# insert the header first: we need to know the size of column names
if len(data) > 0:
@ -290,33 +299,33 @@ class ListCommand(IndexGroupCommand):
# Create and add a separator.
if len(data) > 0:
pkg_strings.insert(1, " ".join(map(lambda x: "-" * x, sizes)))
pkg_strings.insert(1, ' '.join(map(lambda x: '-' * x, sizes)))
for val in pkg_strings:
write_output(val)
def format_for_columns(
pkgs: "_ProcessedDists", options: Values
) -> Tuple[List[List[str]], List[str]]:
pkgs: _ProcessedDists, options: Values,
) -> tuple[list[list[str]], list[str]]:
"""
Convert the package data into something usable
by output_package_listing_columns.
"""
header = ["Package", "Version"]
header = ['Package', 'Version']
running_outdated = options.outdated
if running_outdated:
header.extend(["Latest", "Type"])
header.extend(['Latest', 'Type'])
has_editables = any(x.editable for x in pkgs)
if has_editables:
header.append("Editable project location")
header.append('Editable project location')
if options.verbose >= 1:
header.append("Location")
header.append('Location')
if options.verbose >= 1:
header.append("Installer")
header.append('Installer')
data = []
for proj in pkgs:
@ -329,10 +338,10 @@ def format_for_columns(
row.append(proj.latest_filetype)
if has_editables:
row.append(proj.editable_project_location or "")
row.append(proj.editable_project_location or '')
if options.verbose >= 1:
row.append(proj.location or "")
row.append(proj.location or '')
if options.verbose >= 1:
row.append(proj.installer)
@ -341,21 +350,21 @@ def format_for_columns(
return data, header
def format_for_json(packages: "_ProcessedDists", options: Values) -> str:
def format_for_json(packages: _ProcessedDists, options: Values) -> str:
data = []
for dist in packages:
info = {
"name": dist.raw_name,
"version": str(dist.version),
'name': dist.raw_name,
'version': str(dist.version),
}
if options.verbose >= 1:
info["location"] = dist.location or ""
info["installer"] = dist.installer
info['location'] = dist.location or ''
info['installer'] = dist.installer
if options.outdated:
info["latest_version"] = str(dist.latest_version)
info["latest_filetype"] = dist.latest_filetype
info['latest_version'] = str(dist.latest_version)
info['latest_filetype'] = dist.latest_filetype
editable_project_location = dist.editable_project_location
if editable_project_location:
info["editable_project_location"] = editable_project_location
info['editable_project_location'] = editable_project_location
data.append(info)
return json.dumps(data)

View file

@ -1,3 +1,5 @@
from __future__ import annotations
import logging
import shutil
import sys
@ -5,19 +7,22 @@ import textwrap
import xmlrpc.client
from collections import OrderedDict
from optparse import Values
from typing import TYPE_CHECKING, Dict, List, Optional
from pip._vendor.packaging.version import parse as parse_version
from typing import Dict
from typing import List
from typing import Optional
from typing import TYPE_CHECKING
from pip._internal.cli.base_command import Command
from pip._internal.cli.req_command import SessionCommandMixin
from pip._internal.cli.status_codes import NO_MATCHES_FOUND, SUCCESS
from pip._internal.cli.status_codes import NO_MATCHES_FOUND
from pip._internal.cli.status_codes import SUCCESS
from pip._internal.exceptions import CommandError
from pip._internal.metadata import get_default_environment
from pip._internal.models.index import PyPI
from pip._internal.network.xmlrpc import PipXmlrpcTransport
from pip._internal.utils.logging import indent_log
from pip._internal.utils.misc import write_output
from pip._vendor.packaging.version import parse as parse_version
if TYPE_CHECKING:
from typing import TypedDict
@ -25,7 +30,7 @@ if TYPE_CHECKING:
class TransformedHit(TypedDict):
name: str
summary: str
versions: List[str]
versions: list[str]
logger = logging.getLogger(__name__)
@ -40,19 +45,19 @@ class SearchCommand(Command, SessionCommandMixin):
def add_options(self) -> None:
self.cmd_opts.add_option(
"-i",
"--index",
dest="index",
metavar="URL",
'-i',
'--index',
dest='index',
metavar='URL',
default=PyPI.pypi_url,
help="Base URL of Python Package Index (default %default)",
help='Base URL of Python Package Index (default %default)',
)
self.parser.insert_option_group(0, self.cmd_opts)
def run(self, options: Values, args: List[str]) -> int:
def run(self, options: Values, args: list[str]) -> int:
if not args:
raise CommandError("Missing required argument (search query).")
raise CommandError('Missing required argument (search query).')
query = args
pypi_hits = self.search(query, options)
hits = transform_hits(pypi_hits)
@ -66,7 +71,7 @@ class SearchCommand(Command, SessionCommandMixin):
return SUCCESS
return NO_MATCHES_FOUND
def search(self, query: List[str], options: Values) -> List[Dict[str, str]]:
def search(self, query: list[str], options: Values) -> list[dict[str, str]]:
index_url = options.index
session = self.get_default_session(options)
@ -74,9 +79,9 @@ class SearchCommand(Command, SessionCommandMixin):
transport = PipXmlrpcTransport(index_url, session)
pypi = xmlrpc.client.ServerProxy(index_url, transport)
try:
hits = pypi.search({"name": query, "summary": query}, "or")
hits = pypi.search({'name': query, 'summary': query}, 'or')
except xmlrpc.client.Fault as fault:
message = "XMLRPC request failed [code: {code}]\n{string}".format(
message = 'XMLRPC request failed [code: {code}]\n{string}'.format(
code=fault.faultCode,
string=fault.faultString,
)
@ -85,30 +90,30 @@ class SearchCommand(Command, SessionCommandMixin):
return hits
def transform_hits(hits: List[Dict[str, str]]) -> List["TransformedHit"]:
def transform_hits(hits: list[dict[str, str]]) -> list[TransformedHit]:
"""
The list from pypi is really a list of versions. We want a list of
packages with the list of versions stored inline. This converts the
list from pypi into one we can use.
"""
packages: Dict[str, "TransformedHit"] = OrderedDict()
packages: dict[str, TransformedHit] = OrderedDict()
for hit in hits:
name = hit["name"]
summary = hit["summary"]
version = hit["version"]
name = hit['name']
summary = hit['summary']
version = hit['version']
if name not in packages.keys():
packages[name] = {
"name": name,
"summary": summary,
"versions": [version],
'name': name,
'summary': summary,
'versions': [version],
}
else:
packages[name]["versions"].append(version)
packages[name]['versions'].append(version)
# if this is the highest version, replace summary and score
if version == highest_version(packages[name]["versions"]):
packages[name]["summary"] = summary
if version == highest_version(packages[name]['versions']):
packages[name]['summary'] = summary
return list(packages.values())
@ -119,23 +124,23 @@ def print_dist_installation_info(name: str, latest: str) -> None:
if dist is not None:
with indent_log():
if dist.version == latest:
write_output("INSTALLED: %s (latest)", dist.version)
write_output('INSTALLED: %s (latest)', dist.version)
else:
write_output("INSTALLED: %s", dist.version)
write_output('INSTALLED: %s', dist.version)
if parse_version(latest).pre:
write_output(
"LATEST: %s (pre-release; install"
" with `pip install --pre`)",
'LATEST: %s (pre-release; install'
' with `pip install --pre`)',
latest,
)
else:
write_output("LATEST: %s", latest)
write_output('LATEST: %s', latest)
def print_results(
hits: List["TransformedHit"],
name_column_width: Optional[int] = None,
terminal_width: Optional[int] = None,
hits: list[TransformedHit],
name_column_width: int | None = None,
terminal_width: int | None = None,
) -> None:
if not hits:
return
@ -143,26 +148,26 @@ def print_results(
name_column_width = (
max(
[
len(hit["name"]) + len(highest_version(hit.get("versions", ["-"])))
len(hit['name']) + len(highest_version(hit.get('versions', ['-'])))
for hit in hits
]
)
+ 4
],
) +
4
)
for hit in hits:
name = hit["name"]
summary = hit["summary"] or ""
latest = highest_version(hit.get("versions", ["-"]))
name = hit['name']
summary = hit['summary'] or ''
latest = highest_version(hit.get('versions', ['-']))
if terminal_width is not None:
target_width = terminal_width - name_column_width - 5
if target_width > 10:
# wrap and indent summary to fit terminal
summary_lines = textwrap.wrap(summary, target_width)
summary = ("\n" + " " * (name_column_width + 3)).join(summary_lines)
summary = ('\n' + ' ' * (name_column_width + 3)).join(summary_lines)
name_latest = f"{name} ({latest})"
line = f"{name_latest:{name_column_width}} - {summary}"
name_latest = f'{name} ({latest})'
line = f'{name_latest:{name_column_width}} - {summary}'
try:
write_output(line)
print_dist_installation_info(name, latest)
@ -170,5 +175,5 @@ def print_results(
pass
def highest_version(versions: List[str]) -> str:
def highest_version(versions: list[str]) -> str:
return max(versions, key=parse_version)

View file

@ -1,13 +1,19 @@
from __future__ import annotations
import logging
from optparse import Values
from typing import Iterator, List, NamedTuple, Optional
from pip._vendor.packaging.utils import canonicalize_name
from typing import Iterator
from typing import List
from typing import NamedTuple
from typing import Optional
from pip._internal.cli.base_command import Command
from pip._internal.cli.status_codes import ERROR, SUCCESS
from pip._internal.metadata import BaseDistribution, get_default_environment
from pip._internal.cli.status_codes import ERROR
from pip._internal.cli.status_codes import SUCCESS
from pip._internal.metadata import BaseDistribution
from pip._internal.metadata import get_default_environment
from pip._internal.utils.misc import write_output
from pip._vendor.packaging.utils import canonicalize_name
logger = logging.getLogger(__name__)
@ -25,25 +31,25 @@ class ShowCommand(Command):
def add_options(self) -> None:
self.cmd_opts.add_option(
"-f",
"--files",
dest="files",
action="store_true",
'-f',
'--files',
dest='files',
action='store_true',
default=False,
help="Show the full list of installed files for each package.",
help='Show the full list of installed files for each package.',
)
self.parser.insert_option_group(0, self.cmd_opts)
def run(self, options: Values, args: List[str]) -> int:
def run(self, options: Values, args: list[str]) -> int:
if not args:
logger.warning("ERROR: Please provide a package name or names.")
logger.warning('ERROR: Please provide a package name or names.')
return ERROR
query = args
results = search_packages_info(query)
if not print_results(
results, list_files=options.files, verbose=options.verbose
results, list_files=options.files, verbose=options.verbose,
):
return ERROR
return SUCCESS
@ -53,21 +59,21 @@ class _PackageInfo(NamedTuple):
name: str
version: str
location: str
requires: List[str]
required_by: List[str]
requires: list[str]
required_by: list[str]
installer: str
metadata_version: str
classifiers: List[str]
classifiers: list[str]
summary: str
homepage: str
author: str
author_email: str
license: str
entry_points: List[str]
files: Optional[List[str]]
entry_points: list[str]
files: list[str] | None
def search_packages_info(query: List[str]) -> Iterator[_PackageInfo]:
def search_packages_info(query: list[str]) -> Iterator[_PackageInfo]:
"""
Gather details from installed distributions. Print distribution name,
version, location, and installed files. Installed files requires a
@ -79,14 +85,14 @@ def search_packages_info(query: List[str]) -> Iterator[_PackageInfo]:
installed = {dist.canonical_name: dist for dist in env.iter_distributions()}
query_names = [canonicalize_name(name) for name in query]
missing = sorted(
[name for name, pkg in zip(query, query_names) if pkg not in installed]
[name for name, pkg in zip(query, query_names) if pkg not in installed],
)
if missing:
logger.warning("Package(s) not found: %s", ", ".join(missing))
logger.warning('Package(s) not found: %s', ', '.join(missing))
def _get_requiring_packages(current_dist: BaseDistribution) -> Iterator[str]:
return (
dist.metadata["Name"] or "UNKNOWN"
dist.metadata['Name'] or 'UNKNOWN'
for dist in installed.values()
if current_dist.canonical_name
in {canonicalize_name(d.name) for d in dist.iter_dependencies()}
@ -102,14 +108,14 @@ def search_packages_info(query: List[str]) -> Iterator[_PackageInfo]:
required_by = sorted(_get_requiring_packages(dist), key=str.lower)
try:
entry_points_text = dist.read_text("entry_points.txt")
entry_points_text = dist.read_text('entry_points.txt')
entry_points = entry_points_text.splitlines(keepends=False)
except FileNotFoundError:
entry_points = []
files_iter = dist.iter_declared_entries()
if files_iter is None:
files: Optional[List[str]] = None
files: list[str] | None = None
else:
files = sorted(files_iter)
@ -118,17 +124,17 @@ def search_packages_info(query: List[str]) -> Iterator[_PackageInfo]:
yield _PackageInfo(
name=dist.raw_name,
version=str(dist.version),
location=dist.location or "",
location=dist.location or '',
requires=requires,
required_by=required_by,
installer=dist.installer,
metadata_version=dist.metadata_version or "",
classifiers=metadata.get_all("Classifier", []),
summary=metadata.get("Summary", ""),
homepage=metadata.get("Home-page", ""),
author=metadata.get("Author", ""),
author_email=metadata.get("Author-email", ""),
license=metadata.get("License", ""),
metadata_version=dist.metadata_version or '',
classifiers=metadata.get_all('Classifier', []),
summary=metadata.get('Summary', ''),
homepage=metadata.get('Home-page', ''),
author=metadata.get('Author', ''),
author_email=metadata.get('Author-email', ''),
license=metadata.get('License', ''),
entry_points=entry_points,
files=files,
)
@ -146,33 +152,33 @@ def print_results(
for i, dist in enumerate(distributions):
results_printed = True
if i > 0:
write_output("---")
write_output('---')
write_output("Name: %s", dist.name)
write_output("Version: %s", dist.version)
write_output("Summary: %s", dist.summary)
write_output("Home-page: %s", dist.homepage)
write_output("Author: %s", dist.author)
write_output("Author-email: %s", dist.author_email)
write_output("License: %s", dist.license)
write_output("Location: %s", dist.location)
write_output("Requires: %s", ", ".join(dist.requires))
write_output("Required-by: %s", ", ".join(dist.required_by))
write_output('Name: %s', dist.name)
write_output('Version: %s', dist.version)
write_output('Summary: %s', dist.summary)
write_output('Home-page: %s', dist.homepage)
write_output('Author: %s', dist.author)
write_output('Author-email: %s', dist.author_email)
write_output('License: %s', dist.license)
write_output('Location: %s', dist.location)
write_output('Requires: %s', ', '.join(dist.requires))
write_output('Required-by: %s', ', '.join(dist.required_by))
if verbose:
write_output("Metadata-Version: %s", dist.metadata_version)
write_output("Installer: %s", dist.installer)
write_output("Classifiers:")
write_output('Metadata-Version: %s', dist.metadata_version)
write_output('Installer: %s', dist.installer)
write_output('Classifiers:')
for classifier in dist.classifiers:
write_output(" %s", classifier)
write_output("Entry-points:")
write_output(' %s', classifier)
write_output('Entry-points:')
for entry in dist.entry_points:
write_output(" %s", entry.strip())
write_output(' %s', entry.strip())
if list_files:
write_output("Files:")
write_output('Files:')
if dist.files is None:
write_output("Cannot locate RECORD or installed-files.txt")
write_output('Cannot locate RECORD or installed-files.txt')
else:
for line in dist.files:
write_output(" %s", line.strip())
write_output(' %s', line.strip())
return results_printed

View file

@ -1,19 +1,19 @@
from __future__ import annotations
import logging
from optparse import Values
from typing import List
from pip._vendor.packaging.utils import canonicalize_name
from pip._internal.cli.base_command import Command
from pip._internal.cli.req_command import SessionCommandMixin, warn_if_run_as_root
from pip._internal.cli.req_command import SessionCommandMixin
from pip._internal.cli.req_command import warn_if_run_as_root
from pip._internal.cli.status_codes import SUCCESS
from pip._internal.exceptions import InstallationError
from pip._internal.req import parse_requirements
from pip._internal.req.constructors import (
install_req_from_line,
install_req_from_parsed_requirement,
)
from pip._internal.req.constructors import install_req_from_line
from pip._internal.req.constructors import install_req_from_parsed_requirement
from pip._internal.utils.misc import protect_pip_from_modification_on_windows
from pip._vendor.packaging.utils import canonicalize_name
logger = logging.getLogger(__name__)
@ -35,28 +35,28 @@ class UninstallCommand(Command, SessionCommandMixin):
def add_options(self) -> None:
self.cmd_opts.add_option(
"-r",
"--requirement",
dest="requirements",
action="append",
'-r',
'--requirement',
dest='requirements',
action='append',
default=[],
metavar="file",
metavar='file',
help=(
"Uninstall all the packages listed in the given requirements "
"file. This option can be used multiple times."
'Uninstall all the packages listed in the given requirements '
'file. This option can be used multiple times.'
),
)
self.cmd_opts.add_option(
"-y",
"--yes",
dest="yes",
action="store_true",
'-y',
'--yes',
dest='yes',
action='store_true',
help="Don't ask for confirmation of uninstall deletions.",
)
self.parser.insert_option_group(0, self.cmd_opts)
def run(self, options: Values, args: List[str]) -> int:
def run(self, options: Values, args: list[str]) -> int:
session = self.get_default_session(options)
reqs_to_uninstall = {}
@ -69,28 +69,28 @@ class UninstallCommand(Command, SessionCommandMixin):
reqs_to_uninstall[canonicalize_name(req.name)] = req
else:
logger.warning(
"Invalid requirement: %r ignored -"
" the uninstall command expects named"
" requirements.",
'Invalid requirement: %r ignored -'
' the uninstall command expects named'
' requirements.',
name,
)
for filename in options.requirements:
for parsed_req in parse_requirements(
filename, options=options, session=session
filename, options=options, session=session,
):
req = install_req_from_parsed_requirement(
parsed_req, isolated=options.isolated_mode
parsed_req, isolated=options.isolated_mode,
)
if req.name:
reqs_to_uninstall[canonicalize_name(req.name)] = req
if not reqs_to_uninstall:
raise InstallationError(
f"You must give at least one requirement to {self.name} (see "
f'"pip help {self.name}")'
f'You must give at least one requirement to {self.name} (see '
f'"pip help {self.name}")',
)
protect_pip_from_modification_on_windows(
modifying_pip="pip" in reqs_to_uninstall
modifying_pip='pip' in reqs_to_uninstall,
)
for req in reqs_to_uninstall.values():

View file

@ -1,3 +1,5 @@
from __future__ import annotations
import logging
import os
import shutil
@ -6,14 +8,17 @@ from typing import List
from pip._internal.cache import WheelCache
from pip._internal.cli import cmdoptions
from pip._internal.cli.req_command import RequirementCommand, with_cleanup
from pip._internal.cli.req_command import RequirementCommand
from pip._internal.cli.req_command import with_cleanup
from pip._internal.cli.status_codes import SUCCESS
from pip._internal.exceptions import CommandError
from pip._internal.req.req_install import InstallRequirement
from pip._internal.req.req_tracker import get_requirement_tracker
from pip._internal.utils.misc import ensure_dir, normalize_path
from pip._internal.utils.misc import ensure_dir
from pip._internal.utils.misc import normalize_path
from pip._internal.utils.temp_dir import TempDirectory
from pip._internal.wheel_builder import build, should_build_for_wheel_command
from pip._internal.wheel_builder import build
from pip._internal.wheel_builder import should_build_for_wheel_command
logger = logging.getLogger(__name__)
@ -43,14 +48,14 @@ class WheelCommand(RequirementCommand):
def add_options(self) -> None:
self.cmd_opts.add_option(
"-w",
"--wheel-dir",
dest="wheel_dir",
metavar="dir",
'-w',
'--wheel-dir',
dest='wheel_dir',
metavar='dir',
default=os.curdir,
help=(
"Build wheels into <dir>, where the default is the "
"current working directory."
'Build wheels into <dir>, where the default is the '
'current working directory.'
),
)
self.cmd_opts.add_option(cmdoptions.no_binary())
@ -68,9 +73,9 @@ class WheelCommand(RequirementCommand):
self.cmd_opts.add_option(cmdoptions.progress_bar())
self.cmd_opts.add_option(
"--no-verify",
dest="no_verify",
action="store_true",
'--no-verify',
dest='no_verify',
action='store_true',
default=False,
help="Don't verify if built wheel is valid.",
)
@ -79,12 +84,12 @@ class WheelCommand(RequirementCommand):
self.cmd_opts.add_option(cmdoptions.global_options())
self.cmd_opts.add_option(
"--pre",
action="store_true",
'--pre',
action='store_true',
default=False,
help=(
"Include pre-release and development versions. By default, "
"pip only finds stable versions."
'Include pre-release and development versions. By default, '
'pip only finds stable versions.'
),
)
@ -99,7 +104,7 @@ class WheelCommand(RequirementCommand):
self.parser.insert_option_group(0, self.cmd_opts)
@with_cleanup
def run(self, options: Values, args: List[str]) -> int:
def run(self, options: Values, args: list[str]) -> int:
cmdoptions.check_install_build_global(options)
session = self.get_default_session(options)
@ -114,7 +119,7 @@ class WheelCommand(RequirementCommand):
directory = TempDirectory(
delete=not options.no_clean,
kind="wheel",
kind='wheel',
globally_managed=True,
)
@ -144,7 +149,7 @@ class WheelCommand(RequirementCommand):
requirement_set = resolver.resolve(reqs, check_supported_wheels=True)
reqs_to_build: List[InstallRequirement] = []
reqs_to_build: list[InstallRequirement] = []
for req in requirement_set.requirements.values():
if req.is_wheel:
preparer.save_linked_requirement(req)
@ -167,12 +172,12 @@ class WheelCommand(RequirementCommand):
shutil.copy(req.local_file_path, options.wheel_dir)
except OSError as e:
logger.warning(
"Building wheel for %s failed: %s",
'Building wheel for %s failed: %s',
req.name,
e,
)
build_failures.append(req)
if len(build_failures) != 0:
raise CommandError("Failed to build one or more wheels")
raise CommandError('Failed to build one or more wheels')
return SUCCESS

View file

@ -10,35 +10,41 @@ Some terminology:
- variant
A single word describing where the configuration key-value pair came from
"""
from __future__ import annotations
import configparser
import locale
import os
import sys
from typing import Any, Dict, Iterable, List, NewType, Optional, Tuple
from typing import Any
from typing import Dict
from typing import Iterable
from typing import List
from typing import NewType
from typing import Optional
from typing import Tuple
from pip._internal.exceptions import (
ConfigurationError,
ConfigurationFileCouldNotBeLoaded,
)
from pip._internal.exceptions import ConfigurationError
from pip._internal.exceptions import ConfigurationFileCouldNotBeLoaded
from pip._internal.utils import appdirs
from pip._internal.utils.compat import WINDOWS
from pip._internal.utils.logging import getLogger
from pip._internal.utils.misc import ensure_dir, enum
from pip._internal.utils.misc import ensure_dir
from pip._internal.utils.misc import enum
RawConfigParser = configparser.RawConfigParser # Shorthand
Kind = NewType("Kind", str)
Kind = NewType('Kind', str)
CONFIG_BASENAME = "pip.ini" if WINDOWS else "pip.conf"
ENV_NAMES_IGNORED = "version", "help"
CONFIG_BASENAME = 'pip.ini' if WINDOWS else 'pip.conf'
ENV_NAMES_IGNORED = 'version', 'help'
# The kinds of configurations there are.
kinds = enum(
USER="user", # User Specific
GLOBAL="global", # System Wide
SITE="site", # [Virtual] Environment Specific
ENV="env", # from PIP_CONFIG_FILE
ENV_VAR="env-var", # from Environment Variables
USER='user', # User Specific
GLOBAL='global', # System Wide
SITE='site', # [Virtual] Environment Specific
ENV='env', # from PIP_CONFIG_FILE
ENV_VAR='env-var', # from Environment Variables
)
OVERRIDE_ORDER = kinds.GLOBAL, kinds.USER, kinds.SITE, kinds.ENV, kinds.ENV_VAR
VALID_LOAD_ONLY = kinds.USER, kinds.GLOBAL, kinds.SITE
@ -49,34 +55,34 @@ logger = getLogger(__name__)
# NOTE: Maybe use the optionx attribute to normalize keynames.
def _normalize_name(name: str) -> str:
"""Make a name consistent regardless of source (environment or file)"""
name = name.lower().replace("_", "-")
if name.startswith("--"):
name = name.lower().replace('_', '-')
if name.startswith('--'):
name = name[2:] # only prefer long opts
return name
def _disassemble_key(name: str) -> List[str]:
if "." not in name:
def _disassemble_key(name: str) -> list[str]:
if '.' not in name:
error_message = (
"Key does not contain dot separated section and key. "
'Key does not contain dot separated section and key. '
"Perhaps you wanted to use 'global.{}' instead?"
).format(name)
raise ConfigurationError(error_message)
return name.split(".", 1)
return name.split('.', 1)
def get_configuration_files() -> Dict[Kind, List[str]]:
def get_configuration_files() -> dict[Kind, list[str]]:
global_config_files = [
os.path.join(path, CONFIG_BASENAME) for path in appdirs.site_config_dirs("pip")
os.path.join(path, CONFIG_BASENAME) for path in appdirs.site_config_dirs('pip')
]
site_config_file = os.path.join(sys.prefix, CONFIG_BASENAME)
legacy_config_file = os.path.join(
os.path.expanduser("~"),
"pip" if WINDOWS else ".pip",
os.path.expanduser('~'),
'pip' if WINDOWS else '.pip',
CONFIG_BASENAME,
)
new_config_file = os.path.join(appdirs.user_config_dir("pip"), CONFIG_BASENAME)
new_config_file = os.path.join(appdirs.user_config_dir('pip'), CONFIG_BASENAME)
return {
kinds.GLOBAL: global_config_files,
kinds.SITE: [site_config_file],
@ -98,26 +104,26 @@ class Configuration:
and the data stored is also nice.
"""
def __init__(self, isolated: bool, load_only: Optional[Kind] = None) -> None:
def __init__(self, isolated: bool, load_only: Kind | None = None) -> None:
super().__init__()
if load_only is not None and load_only not in VALID_LOAD_ONLY:
raise ConfigurationError(
"Got invalid value for load_only - should be one of {}".format(
", ".join(map(repr, VALID_LOAD_ONLY))
)
'Got invalid value for load_only - should be one of {}'.format(
', '.join(map(repr, VALID_LOAD_ONLY)),
),
)
self.isolated = isolated
self.load_only = load_only
# Because we keep track of where we got the data from
self._parsers: Dict[Kind, List[Tuple[str, RawConfigParser]]] = {
self._parsers: dict[Kind, list[tuple[str, RawConfigParser]]] = {
variant: [] for variant in OVERRIDE_ORDER
}
self._config: Dict[Kind, Dict[str, Any]] = {
self._config: dict[Kind, dict[str, Any]] = {
variant: {} for variant in OVERRIDE_ORDER
}
self._modified_parsers: List[Tuple[str, RawConfigParser]] = []
self._modified_parsers: list[tuple[str, RawConfigParser]] = []
def load(self) -> None:
"""Loads configuration from configuration files and environment"""
@ -125,16 +131,16 @@ class Configuration:
if not self.isolated:
self._load_environment_vars()
def get_file_to_edit(self) -> Optional[str]:
def get_file_to_edit(self) -> str | None:
"""Returns the file with highest priority in configuration"""
assert self.load_only is not None, "Need to be specified a file to be editing"
assert self.load_only is not None, 'Need to be specified a file to be editing'
try:
return self._get_parser_to_modify()[0]
except IndexError:
return None
def items(self) -> Iterable[Tuple[str, Any]]:
def items(self) -> Iterable[tuple[str, Any]]:
"""Returns key-value pairs like dict.items() representing the loaded
configuration
"""
@ -145,7 +151,7 @@ class Configuration:
try:
return self._dictionary[key]
except KeyError:
raise ConfigurationError(f"No such key - {key}")
raise ConfigurationError(f'No such key - {key}')
def set_value(self, key: str, value: Any) -> None:
"""Modify a value in the configuration."""
@ -171,7 +177,7 @@ class Configuration:
assert self.load_only
if key not in self._config[self.load_only]:
raise ConfigurationError(f"No such key - {key}")
raise ConfigurationError(f'No such key - {key}')
fname, parser = self._get_parser_to_modify()
@ -182,7 +188,7 @@ class Configuration:
):
# The option was not removed.
raise ConfigurationError(
"Fatal Internal error [id=1]. Please report as a bug."
'Fatal Internal error [id=1]. Please report as a bug.',
)
# The section may be empty after the option was removed.
@ -197,12 +203,12 @@ class Configuration:
self._ensure_have_load_only()
for fname, parser in self._modified_parsers:
logger.info("Writing to %s", fname)
logger.info('Writing to %s', fname)
# Ensure directory exists.
ensure_dir(os.path.dirname(fname))
with open(fname, "w") as f:
with open(fname, 'w') as f:
parser.write(f)
#
@ -211,11 +217,11 @@ class Configuration:
def _ensure_have_load_only(self) -> None:
if self.load_only is None:
raise ConfigurationError("Needed a specific file to be modifying.")
logger.debug("Will be working with %s variant only", self.load_only)
raise ConfigurationError('Needed a specific file to be modifying.')
logger.debug('Will be working with %s variant only', self.load_only)
@property
def _dictionary(self) -> Dict[str, Any]:
def _dictionary(self) -> dict[str, Any]:
"""A dictionary representing the loaded configuration."""
# NOTE: Dictionaries are not populated if not loaded. So, conditionals
# are not needed here.
@ -231,8 +237,8 @@ class Configuration:
config_files = dict(self.iter_config_files())
if config_files[kinds.ENV][0:1] == [os.devnull]:
logger.debug(
"Skipping loading configuration files due to "
"environment's PIP_CONFIG_FILE being os.devnull"
'Skipping loading configuration files due to '
"environment's PIP_CONFIG_FILE being os.devnull",
)
return
@ -272,7 +278,7 @@ class Configuration:
except UnicodeDecodeError:
# See https://github.com/pypa/pip/issues/4963
raise ConfigurationFileCouldNotBeLoaded(
reason=f"contains invalid {locale_encoding} characters",
reason=f'contains invalid {locale_encoding} characters',
fname=fname,
)
except configparser.Error as error:
@ -283,12 +289,12 @@ class Configuration:
def _load_environment_vars(self) -> None:
"""Loads configuration from environment variables"""
self._config[kinds.ENV_VAR].update(
self._normalized_keys(":env:", self.get_environ_vars())
self._normalized_keys(':env:', self.get_environ_vars()),
)
def _normalized_keys(
self, section: str, items: Iterable[Tuple[str, Any]]
) -> Dict[str, Any]:
self, section: str, items: Iterable[tuple[str, Any]],
) -> dict[str, Any]:
"""Normalizes items to construct a dictionary with normalized keys.
This routine is where the names become keys and are made the same
@ -296,20 +302,20 @@ class Configuration:
"""
normalized = {}
for name, val in items:
key = section + "." + _normalize_name(name)
key = section + '.' + _normalize_name(name)
normalized[key] = val
return normalized
def get_environ_vars(self) -> Iterable[Tuple[str, str]]:
def get_environ_vars(self) -> Iterable[tuple[str, str]]:
"""Returns a generator with all environmental vars with prefix PIP_"""
for key, val in os.environ.items():
if key.startswith("PIP_"):
if key.startswith('PIP_'):
name = key[4:].lower()
if name not in ENV_NAMES_IGNORED:
yield name, val
# XXX: This is patched in the tests.
def iter_config_files(self) -> Iterable[Tuple[Kind, List[str]]]:
def iter_config_files(self) -> Iterable[tuple[Kind, list[str]]]:
"""Yields variant and configuration files associated with it.
This should be treated like items of a dictionary.
@ -317,7 +323,7 @@ class Configuration:
# SMELL: Move the conditions out of this function
# environment variables have the lowest priority
config_file = os.environ.get("PIP_CONFIG_FILE", None)
config_file = os.environ.get('PIP_CONFIG_FILE', None)
if config_file is not None:
yield kinds.ENV, [config_file]
else:
@ -339,18 +345,18 @@ class Configuration:
# finally virtualenv configuration first trumping others
yield kinds.SITE, config_files[kinds.SITE]
def get_values_in_config(self, variant: Kind) -> Dict[str, Any]:
def get_values_in_config(self, variant: Kind) -> dict[str, Any]:
"""Get values present in a config file"""
return self._config[variant]
def _get_parser_to_modify(self) -> Tuple[str, RawConfigParser]:
def _get_parser_to_modify(self) -> tuple[str, RawConfigParser]:
# Determine which parser to modify
assert self.load_only
parsers = self._parsers[self.load_only]
if not parsers:
# This should not happen if everything works correctly.
raise ConfigurationError(
"Fatal Internal error [id=2]. Please report as a bug."
'Fatal Internal error [id=2]. Please report as a bug.',
)
# Use the highest priority parser.
@ -363,4 +369,4 @@ class Configuration:
self._modified_parsers.append(file_parser_tuple)
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self._dictionary!r})"
return f'{self.__class__.__name__}({self._dictionary!r})'

View file

@ -1,3 +1,5 @@
from __future__ import annotations
from pip._internal.distributions.base import AbstractDistribution
from pip._internal.distributions.sdist import SourceDistribution
from pip._internal.distributions.wheel import WheelDistribution

View file

@ -1,3 +1,5 @@
from __future__ import annotations
import abc
from pip._internal.index.package_finder import PackageFinder
@ -31,6 +33,6 @@ class AbstractDistribution(metaclass=abc.ABCMeta):
@abc.abstractmethod
def prepare_distribution_metadata(
self, finder: PackageFinder, build_isolation: bool
self, finder: PackageFinder, build_isolation: bool,
) -> None:
raise NotImplementedError()

View file

@ -1,3 +1,5 @@
from __future__ import annotations
from pip._internal.distributions.base import AbstractDistribution
from pip._internal.index.package_finder import PackageFinder
from pip._internal.metadata import BaseDistribution
@ -11,10 +13,10 @@ class InstalledDistribution(AbstractDistribution):
"""
def get_metadata_distribution(self) -> BaseDistribution:
assert self.req.satisfied_by is not None, "not actually installed"
assert self.req.satisfied_by is not None, 'not actually installed'
return self.req.satisfied_by
def prepare_distribution_metadata(
self, finder: PackageFinder, build_isolation: bool
self, finder: PackageFinder, build_isolation: bool,
) -> None:
pass

View file

@ -1,5 +1,9 @@
from __future__ import annotations
import logging
from typing import Iterable, Set, Tuple
from typing import Iterable
from typing import Set
from typing import Tuple
from pip._internal.build_env import BuildEnvironment
from pip._internal.distributions.base import AbstractDistribution
@ -22,7 +26,7 @@ class SourceDistribution(AbstractDistribution):
return self.req.get_dist()
def prepare_distribution_metadata(
self, finder: PackageFinder, build_isolation: bool
self, finder: PackageFinder, build_isolation: bool,
) -> None:
# Load pyproject.toml, to determine whether PEP 517 is to be used
self.req.load_pyproject_toml()
@ -54,27 +58,27 @@ class SourceDistribution(AbstractDistribution):
self.req.build_env = BuildEnvironment()
self.req.build_env.install_requirements(
finder, pyproject_requires, "overlay", kind="build dependencies"
finder, pyproject_requires, 'overlay', kind='build dependencies',
)
conflicting, missing = self.req.build_env.check_requirements(
self.req.requirements_to_check
self.req.requirements_to_check,
)
if conflicting:
self._raise_conflicts("PEP 517/518 supported requirements", conflicting)
self._raise_conflicts('PEP 517/518 supported requirements', conflicting)
if missing:
logger.warning(
"Missing build requirements in pyproject.toml for %s.",
'Missing build requirements in pyproject.toml for %s.',
self.req,
)
logger.warning(
"The project does not specify a build backend, and "
"pip cannot fall back to setuptools without %s.",
" and ".join(map(repr, sorted(missing))),
'The project does not specify a build backend, and '
'pip cannot fall back to setuptools without %s.',
' and '.join(map(repr, sorted(missing))),
)
def _get_build_requires_wheel(self) -> Iterable[str]:
with self.req.build_env:
runner = runner_with_spinner_message("Getting requirements to build wheel")
runner = runner_with_spinner_message('Getting requirements to build wheel')
backend = self.req.pep517_backend
assert backend is not None
with backend.subprocess_runner(runner):
@ -83,7 +87,7 @@ class SourceDistribution(AbstractDistribution):
def _get_build_requires_editable(self) -> Iterable[str]:
with self.req.build_env:
runner = runner_with_spinner_message(
"Getting requirements to build editable"
'Getting requirements to build editable',
)
backend = self.req.pep517_backend
assert backend is not None
@ -95,32 +99,32 @@ class SourceDistribution(AbstractDistribution):
# This must be done in a second pass, as the pyproject.toml
# dependencies must be installed before we can call the backend.
if (
self.req.editable
and self.req.permit_editable_wheels
and self.req.supports_pyproject_editable()
self.req.editable and
self.req.permit_editable_wheels and
self.req.supports_pyproject_editable()
):
build_reqs = self._get_build_requires_editable()
else:
build_reqs = self._get_build_requires_wheel()
conflicting, missing = self.req.build_env.check_requirements(build_reqs)
if conflicting:
self._raise_conflicts("the backend dependencies", conflicting)
self._raise_conflicts('the backend dependencies', conflicting)
self.req.build_env.install_requirements(
finder, missing, "normal", kind="backend dependencies"
finder, missing, 'normal', kind='backend dependencies',
)
def _raise_conflicts(
self, conflicting_with: str, conflicting_reqs: Set[Tuple[str, str]]
self, conflicting_with: str, conflicting_reqs: set[tuple[str, str]],
) -> None:
format_string = (
"Some build dependencies for {requirement} "
"conflict with {conflicting_with}: {description}."
'Some build dependencies for {requirement} '
'conflict with {conflicting_with}: {description}.'
)
error_message = format_string.format(
requirement=self.req,
conflicting_with=conflicting_with,
description=", ".join(
f"{installed} is incompatible with {wanted}"
description=', '.join(
f'{installed} is incompatible with {wanted}'
for installed, wanted in sorted(conflicting_reqs)
),
)

View file

@ -1,12 +1,11 @@
from pip._vendor.packaging.utils import canonicalize_name
from __future__ import annotations
from pip._internal.distributions.base import AbstractDistribution
from pip._internal.index.package_finder import PackageFinder
from pip._internal.metadata import (
BaseDistribution,
FilesystemWheel,
get_wheel_distribution,
)
from pip._internal.metadata import BaseDistribution
from pip._internal.metadata import FilesystemWheel
from pip._internal.metadata import get_wheel_distribution
from pip._vendor.packaging.utils import canonicalize_name
class WheelDistribution(AbstractDistribution):
@ -20,12 +19,12 @@ class WheelDistribution(AbstractDistribution):
Distribution that uses it, not relying on the wheel file or
requirement.
"""
assert self.req.local_file_path, "Set as part of preparation during download"
assert self.req.name, "Wheels are never unnamed"
assert self.req.local_file_path, 'Set as part of preparation during download'
assert self.req.name, 'Wheels are never unnamed'
wheel = FilesystemWheel(self.req.local_file_path)
return get_wheel_distribution(wheel, canonicalize_name(self.req.name))
def prepare_distribution_metadata(
self, finder: PackageFinder, build_isolation: bool
self, finder: PackageFinder, build_isolation: bool,
) -> None:
pass

View file

@ -4,14 +4,24 @@ This module MUST NOT try to import from anything within `pip._internal` to
operate. This is expected to be importable from any/all files within the
subpackage and, thus, should not depend on them.
"""
from __future__ import annotations
import configparser
import re
from itertools import chain, groupby, repeat
from typing import TYPE_CHECKING, Dict, List, Optional, Union
from itertools import chain
from itertools import groupby
from itertools import repeat
from typing import Dict
from typing import List
from typing import Optional
from typing import TYPE_CHECKING
from typing import Union
from pip._vendor.requests.models import Request, Response
from pip._vendor.rich.console import Console, ConsoleOptions, RenderResult
from pip._vendor.requests.models import Request
from pip._vendor.requests.models import Response
from pip._vendor.rich.console import Console
from pip._vendor.rich.console import ConsoleOptions
from pip._vendor.rich.console import RenderResult
from pip._vendor.rich.markup import escape
from pip._vendor.rich.text import Text
@ -27,11 +37,11 @@ if TYPE_CHECKING:
# Scaffolding
#
def _is_kebab_case(s: str) -> bool:
return re.match(r"^[a-z]+(-[a-z]+)*$", s) is not None
return re.match(r'^[a-z]+(-[a-z]+)*$', s) is not None
def _prefix_with_indent(
s: Union[Text, str],
s: Text | str,
console: Console,
*,
prefix: str,
@ -42,8 +52,8 @@ def _prefix_with_indent(
else:
text = console.render_str(s)
return console.render_str(prefix, overflow="ignore") + console.render_str(
f"\n{indent}", overflow="ignore"
return console.render_str(prefix, overflow='ignore') + console.render_str(
f'\n{indent}', overflow='ignore',
).join(text.split(allow_blank=True))
@ -67,19 +77,19 @@ class DiagnosticPipError(PipError):
def __init__(
self,
*,
kind: 'Literal["error", "warning"]' = "error",
reference: Optional[str] = None,
message: Union[str, Text],
context: Optional[Union[str, Text]],
hint_stmt: Optional[Union[str, Text]],
note_stmt: Optional[Union[str, Text]] = None,
link: Optional[str] = None,
kind: Literal["error", "warning"] = 'error',
reference: str | None = None,
message: str | Text,
context: str | Text | None,
hint_stmt: str | Text | None,
note_stmt: str | Text | None = None,
link: str | None = None,
) -> None:
# Ensure a proper reference is provided.
if reference is None:
assert hasattr(self, "reference"), "error reference not provided!"
assert hasattr(self, 'reference'), 'error reference not provided!'
reference = self.reference
assert _is_kebab_case(reference), "error reference must be kebab-case!"
assert _is_kebab_case(reference), 'error reference must be kebab-case!'
self.kind = kind
self.reference = reference
@ -92,17 +102,17 @@ class DiagnosticPipError(PipError):
self.link = link
super().__init__(f"<{self.__class__.__name__}: {self.reference}>")
super().__init__(f'<{self.__class__.__name__}: {self.reference}>')
def __repr__(self) -> str:
return (
f"<{self.__class__.__name__}("
f"reference={self.reference!r}, "
f"message={self.message!r}, "
f"context={self.context!r}, "
f"note_stmt={self.note_stmt!r}, "
f"hint_stmt={self.hint_stmt!r}"
")>"
f'<{self.__class__.__name__}('
f'reference={self.reference!r}, '
f'message={self.message!r}, '
f'context={self.context!r}, '
f'note_stmt={self.note_stmt!r}, '
f'hint_stmt={self.hint_stmt!r}'
')>'
)
def __rich_console__(
@ -110,10 +120,10 @@ class DiagnosticPipError(PipError):
console: Console,
options: ConsoleOptions,
) -> RenderResult:
colour = "red" if self.kind == "error" else "yellow"
colour = 'red' if self.kind == 'error' else 'yellow'
yield f"[{colour} bold]{self.kind}[/]: [bold]{self.reference}[/]"
yield ""
yield f'[{colour} bold]{self.kind}[/]: [bold]{self.reference}[/]'
yield ''
if not options.ascii_only:
# Present the main message, with relevant context indented.
@ -121,49 +131,49 @@ class DiagnosticPipError(PipError):
yield _prefix_with_indent(
self.message,
console,
prefix=f"[{colour}]×[/] ",
indent=f"[{colour}]│[/] ",
prefix=f'[{colour}]×[/] ',
indent=f'[{colour}]│[/] ',
)
yield _prefix_with_indent(
self.context,
console,
prefix=f"[{colour}]╰─>[/] ",
indent=f"[{colour}] [/] ",
prefix=f'[{colour}]╰─>[/] ',
indent=f'[{colour}] [/] ',
)
else:
yield _prefix_with_indent(
self.message,
console,
prefix="[red]×[/] ",
indent=" ",
prefix='[red]×[/] ',
indent=' ',
)
else:
yield self.message
if self.context is not None:
yield ""
yield ''
yield self.context
if self.note_stmt is not None or self.hint_stmt is not None:
yield ""
yield ''
if self.note_stmt is not None:
yield _prefix_with_indent(
self.note_stmt,
console,
prefix="[magenta bold]note[/]: ",
indent=" ",
prefix='[magenta bold]note[/]: ',
indent=' ',
)
if self.hint_stmt is not None:
yield _prefix_with_indent(
self.hint_stmt,
console,
prefix="[cyan bold]hint[/]: ",
indent=" ",
prefix='[cyan bold]hint[/]: ',
indent=' ',
)
if self.link is not None:
yield ""
yield f"Link: {self.link}"
yield ''
yield f'Link: {self.link}'
#
@ -184,34 +194,34 @@ class UninstallationError(PipError):
class MissingPyProjectBuildRequires(DiagnosticPipError):
"""Raised when pyproject.toml has `build-system`, but no `build-system.requires`."""
reference = "missing-pyproject-build-system-requires"
reference = 'missing-pyproject-build-system-requires'
def __init__(self, *, package: str) -> None:
super().__init__(
message=f"Can not process {escape(package)}",
message=f'Can not process {escape(package)}',
context=Text(
"This package has an invalid pyproject.toml file.\n"
"The [build-system] table is missing the mandatory `requires` key."
'This package has an invalid pyproject.toml file.\n'
'The [build-system] table is missing the mandatory `requires` key.',
),
note_stmt="This is an issue with the package mentioned above, not pip.",
hint_stmt=Text("See PEP 518 for the detailed specification."),
note_stmt='This is an issue with the package mentioned above, not pip.',
hint_stmt=Text('See PEP 518 for the detailed specification.'),
)
class InvalidPyProjectBuildRequires(DiagnosticPipError):
"""Raised when pyproject.toml an invalid `build-system.requires`."""
reference = "invalid-pyproject-build-system-requires"
reference = 'invalid-pyproject-build-system-requires'
def __init__(self, *, package: str, reason: str) -> None:
super().__init__(
message=f"Can not process {escape(package)}",
message=f'Can not process {escape(package)}',
context=Text(
"This package has an invalid `build-system.requires` key in "
f"pyproject.toml.\n{reason}"
'This package has an invalid `build-system.requires` key in '
f'pyproject.toml.\n{reason}',
),
note_stmt="This is an issue with the package mentioned above, not pip.",
hint_stmt=Text("See PEP 518 for the detailed specification."),
note_stmt='This is an issue with the package mentioned above, not pip.',
hint_stmt=Text('See PEP 518 for the detailed specification.'),
)
@ -226,7 +236,7 @@ class NoneMetadataError(PipError):
def __init__(
self,
dist: "BaseDistribution",
dist: BaseDistribution,
metadata_name: str,
) -> None:
"""
@ -240,7 +250,7 @@ class NoneMetadataError(PipError):
def __str__(self) -> str:
# Use `dist` in the error message because its stringification
# includes more information, like the version and location.
return "None {} metadata found for distribution: {}".format(
return 'None {} metadata found for distribution: {}'.format(
self.metadata_name,
self.dist,
)
@ -250,13 +260,13 @@ class UserInstallationInvalid(InstallationError):
"""A --user install is requested on an environment without user site."""
def __str__(self) -> str:
return "User base directory is not specified"
return 'User base directory is not specified'
class InvalidSchemeCombination(InstallationError):
def __str__(self) -> str:
before = ", ".join(str(a) for a in self.args[:-1])
return f"Cannot set {before} and {self.args[-1]} together"
before = ', '.join(str(a) for a in self.args[:-1])
return f'Cannot set {before} and {self.args[-1]} together'
class DistributionNotFound(InstallationError):
@ -288,7 +298,7 @@ class NetworkConnectionError(PipError):
"""HTTP connection error"""
def __init__(
self, error_msg: str, response: Response = None, request: Request = None
self, error_msg: str, response: Response = None, request: Request = None,
) -> None:
"""
Initialize NetworkConnectionError with `request` and `response`
@ -298,9 +308,9 @@ class NetworkConnectionError(PipError):
self.request = request
self.error_msg = error_msg
if (
self.response is not None
and not self.request
and hasattr(response, "request")
self.response is not None and
not self.request and
hasattr(response, 'request')
):
self.request = self.response.request
super().__init__(error_msg, response, request)
@ -337,7 +347,7 @@ class MetadataInconsistent(InstallationError):
"""
def __init__(
self, ireq: "InstallRequirement", field: str, f_val: str, m_val: str
self, ireq: InstallRequirement, field: str, f_val: str, m_val: str,
) -> None:
self.ireq = ireq
self.field = field
@ -346,8 +356,8 @@ class MetadataInconsistent(InstallationError):
def __str__(self) -> str:
template = (
"Requested {} has inconsistent {}: "
"filename has {!r}, but metadata has {!r}"
'Requested {} has inconsistent {}: '
'filename has {!r}, but metadata has {!r}'
)
return template.format(self.ireq, self.field, self.f_val, self.m_val)
@ -355,48 +365,48 @@ class MetadataInconsistent(InstallationError):
class LegacyInstallFailure(DiagnosticPipError):
"""Error occurred while executing `setup.py install`"""
reference = "legacy-install-failure"
reference = 'legacy-install-failure'
def __init__(self, package_details: str) -> None:
super().__init__(
message="Encountered error while trying to install package.",
message='Encountered error while trying to install package.',
context=package_details,
hint_stmt="See above for output from the failure.",
note_stmt="This is an issue with the package mentioned above, not pip.",
hint_stmt='See above for output from the failure.',
note_stmt='This is an issue with the package mentioned above, not pip.',
)
class InstallationSubprocessError(DiagnosticPipError, InstallationError):
"""A subprocess call failed."""
reference = "subprocess-exited-with-error"
reference = 'subprocess-exited-with-error'
def __init__(
self,
*,
command_description: str,
exit_code: int,
output_lines: Optional[List[str]],
output_lines: list[str] | None,
) -> None:
if output_lines is None:
output_prompt = Text("See above for output.")
output_prompt = Text('See above for output.')
else:
output_prompt = (
Text.from_markup(f"[red][{len(output_lines)} lines of output][/]\n")
+ Text("".join(output_lines))
+ Text.from_markup(R"[red]\[end of output][/]")
Text.from_markup(f'[red][{len(output_lines)} lines of output][/]\n') +
Text(''.join(output_lines)) +
Text.from_markup(R'[red]\[end of output][/]')
)
super().__init__(
message=(
f"[green]{escape(command_description)}[/] did not run successfully.\n"
f"exit code: {exit_code}"
f'[green]{escape(command_description)}[/] did not run successfully.\n'
f'exit code: {exit_code}'
),
context=output_prompt,
hint_stmt=None,
note_stmt=(
"This error originates from a subprocess, and is likely not a "
"problem with pip."
'This error originates from a subprocess, and is likely not a '
'problem with pip.'
),
)
@ -404,11 +414,11 @@ class InstallationSubprocessError(DiagnosticPipError, InstallationError):
self.exit_code = exit_code
def __str__(self) -> str:
return f"{self.command_description} exited with {self.exit_code}"
return f'{self.command_description} exited with {self.exit_code}'
class MetadataGenerationFailed(InstallationSubprocessError, InstallationError):
reference = "metadata-generation-failed"
reference = 'metadata-generation-failed'
def __init__(
self,
@ -416,23 +426,23 @@ class MetadataGenerationFailed(InstallationSubprocessError, InstallationError):
package_details: str,
) -> None:
super(InstallationSubprocessError, self).__init__(
message="Encountered error while generating package metadata.",
message='Encountered error while generating package metadata.',
context=escape(package_details),
hint_stmt="See above for details.",
note_stmt="This is an issue with the package mentioned above, not pip.",
hint_stmt='See above for details.',
note_stmt='This is an issue with the package mentioned above, not pip.',
)
def __str__(self) -> str:
return "metadata generation failed"
return 'metadata generation failed'
class HashErrors(InstallationError):
"""Multiple HashError instances rolled into one for reporting"""
def __init__(self) -> None:
self.errors: List["HashError"] = []
self.errors: list[HashError] = []
def append(self, error: "HashError") -> None:
def append(self, error: HashError) -> None:
self.errors.append(error)
def __str__(self) -> str:
@ -442,8 +452,8 @@ class HashErrors(InstallationError):
lines.append(cls.head)
lines.extend(e.body() for e in errors_of_cls)
if lines:
return "\n".join(lines)
return ""
return '\n'.join(lines)
return ''
def __bool__(self) -> bool:
return bool(self.errors)
@ -466,8 +476,8 @@ class HashError(InstallationError):
"""
req: Optional["InstallRequirement"] = None
head = ""
req: InstallRequirement | None = None
head = ''
order: int = -1
def body(self) -> str:
@ -480,10 +490,10 @@ class HashError(InstallationError):
its link already populated by the resolver's _populate_link().
"""
return f" {self._requirement_name()}"
return f' {self._requirement_name()}'
def __str__(self) -> str:
return f"{self.head}\n{self.body()}"
return f'{self.head}\n{self.body()}'
def _requirement_name(self) -> str:
"""Return a description of the requirement that triggered me.
@ -492,7 +502,7 @@ class HashError(InstallationError):
line numbers
"""
return str(self.req) if self.req else "unknown package"
return str(self.req) if self.req else 'unknown package'
class VcsHashUnsupported(HashError):
@ -502,7 +512,7 @@ class VcsHashUnsupported(HashError):
order = 0
head = (
"Can't verify hashes for these requirements because we don't "
"have a way to hash version control repositories:"
'have a way to hash version control repositories:'
)
@ -513,7 +523,7 @@ class DirectoryUrlHashUnsupported(HashError):
order = 1
head = (
"Can't verify hashes for these file:// requirements because they "
"point to directories:"
'point to directories:'
)
@ -522,13 +532,13 @@ class HashMissing(HashError):
order = 2
head = (
"Hashes are required in --require-hashes mode, but they are "
"missing from some requirements. Here is a list of those "
"requirements along with the hashes their downloaded archives "
"actually had. Add lines like these to your requirements files to "
"prevent tampering. (If you did not enable --require-hashes "
"manually, note that it turns on automatically when any package "
"has a hash.)"
'Hashes are required in --require-hashes mode, but they are '
'missing from some requirements. Here is a list of those '
'requirements along with the hashes their downloaded archives '
'actually had. Add lines like these to your requirements files to '
'prevent tampering. (If you did not enable --require-hashes '
'manually, note that it turns on automatically when any package '
'has a hash.)'
)
def __init__(self, gotten_hash: str) -> None:
@ -552,10 +562,10 @@ class HashMissing(HashError):
if self.req.original_link
# In case someone feeds something downright stupid
# to InstallRequirement's constructor.
else getattr(self.req, "req", None)
else getattr(self.req, 'req', None)
)
return " {} --hash={}:{}".format(
package or "unknown package", FAVORITE_HASH, self.gotten_hash
return ' {} --hash={}:{}'.format(
package or 'unknown package', FAVORITE_HASH, self.gotten_hash,
)
@ -565,8 +575,8 @@ class HashUnpinned(HashError):
order = 3
head = (
"In --require-hashes mode, all requirements must have their "
"versions pinned with ==. These do not:"
'In --require-hashes mode, all requirements must have their '
'versions pinned with ==. These do not:'
)
@ -582,13 +592,13 @@ class HashMismatch(HashError):
order = 4
head = (
"THESE PACKAGES DO NOT MATCH THE HASHES FROM THE REQUIREMENTS "
"FILE. If you have updated the package versions, please update "
"the hashes. Otherwise, examine the package contents carefully; "
"someone may have tampered with them."
'THESE PACKAGES DO NOT MATCH THE HASHES FROM THE REQUIREMENTS '
'FILE. If you have updated the package versions, please update '
'the hashes. Otherwise, examine the package contents carefully; '
'someone may have tampered with them.'
)
def __init__(self, allowed: Dict[str, List[str]], gots: Dict[str, "_Hash"]) -> None:
def __init__(self, allowed: dict[str, list[str]], gots: dict[str, _Hash]) -> None:
"""
:param allowed: A dict of algorithm names pointing to lists of allowed
hex digests
@ -599,7 +609,7 @@ class HashMismatch(HashError):
self.gots = gots
def body(self) -> str:
return " {}:\n{}".format(self._requirement_name(), self._hash_comparison())
return f' {self._requirement_name()}:\n{self._hash_comparison()}'
def _hash_comparison(self) -> str:
"""
@ -613,21 +623,21 @@ class HashMismatch(HashError):
"""
def hash_then_or(hash_name: str) -> "chain[str]":
def hash_then_or(hash_name: str) -> chain[str]:
# For now, all the decent hashes have 6-char names, so we can get
# away with hard-coding space literals.
return chain([hash_name], repeat(" or"))
return chain([hash_name], repeat(' or'))
lines: List[str] = []
lines: list[str] = []
for hash_name, expecteds in self.allowed.items():
prefix = hash_then_or(hash_name)
lines.extend(
(" Expected {} {}".format(next(prefix), e)) for e in expecteds
(f' Expected {next(prefix)} {e}') for e in expecteds
)
lines.append(
" Got {}\n".format(self.gots[hash_name].hexdigest())
f' Got {self.gots[hash_name].hexdigest()}\n',
)
return "\n".join(lines)
return '\n'.join(lines)
class UnsupportedPythonVersion(InstallationError):
@ -640,9 +650,9 @@ class ConfigurationFileCouldNotBeLoaded(ConfigurationError):
def __init__(
self,
reason: str = "could not be loaded",
fname: Optional[str] = None,
error: Optional[configparser.Error] = None,
reason: str = 'could not be loaded',
fname: str | None = None,
error: configparser.Error | None = None,
) -> None:
super().__init__(error)
self.reason = reason
@ -651,8 +661,8 @@ class ConfigurationFileCouldNotBeLoaded(ConfigurationError):
def __str__(self) -> str:
if self.fname is not None:
message_part = f" in {self.fname}."
message_part = f' in {self.fname}.'
else:
assert self.error is not None
message_part = f".\n{self.error}\n"
return f"Configuration file {self.reason}{message_part}"
message_part = f'.\n{self.error}\n'
return f'Configuration file {self.reason}{message_part}'

View file

@ -1,2 +1,3 @@
"""Index interaction code
"""
from __future__ import annotations

View file

@ -1,6 +1,7 @@
"""
The main purpose of this module is to expose LinkCollector.collect_sources().
"""
from __future__ import annotations
import cgi
import collections
@ -14,23 +15,17 @@ import urllib.request
import xml.etree.ElementTree
from html.parser import HTMLParser
from optparse import Values
from typing import (
TYPE_CHECKING,
Callable,
Dict,
Iterable,
List,
MutableMapping,
NamedTuple,
Optional,
Sequence,
Tuple,
Union,
)
from pip._vendor import html5lib, requests
from pip._vendor.requests import Response
from pip._vendor.requests.exceptions import RetryError, SSLError
from typing import Callable
from typing import Dict
from typing import Iterable
from typing import List
from typing import MutableMapping
from typing import NamedTuple
from typing import Optional
from typing import Sequence
from typing import Tuple
from typing import TYPE_CHECKING
from typing import Union
from pip._internal.exceptions import NetworkConnectionError
from pip._internal.models.link import Link
@ -38,10 +33,18 @@ from pip._internal.models.search_scope import SearchScope
from pip._internal.network.session import PipSession
from pip._internal.network.utils import raise_for_status
from pip._internal.utils.filetypes import is_archive_file
from pip._internal.utils.misc import pairwise, redact_auth_from_url
from pip._internal.utils.misc import pairwise
from pip._internal.utils.misc import redact_auth_from_url
from pip._internal.vcs import vcs
from pip._vendor import html5lib
from pip._vendor import requests
from pip._vendor.requests import Response
from pip._vendor.requests.exceptions import RetryError
from pip._vendor.requests.exceptions import SSLError
from .sources import CandidatesFromPage, LinkSource, build_source
from .sources import build_source
from .sources import CandidatesFromPage
from .sources import LinkSource
if TYPE_CHECKING:
from typing import Protocol
@ -54,13 +57,13 @@ HTMLElement = xml.etree.ElementTree.Element
ResponseHeaders = MutableMapping[str, str]
def _match_vcs_scheme(url: str) -> Optional[str]:
def _match_vcs_scheme(url: str) -> str | None:
"""Look for VCS schemes in the URL.
Returns the matched VCS scheme, or None if there's no match.
"""
for scheme in vcs.schemes:
if url.lower().startswith(scheme) and url[len(scheme)] in "+:":
if url.lower().startswith(scheme) and url[len(scheme)] in '+:':
return scheme
return None
@ -77,8 +80,8 @@ def _ensure_html_header(response: Response) -> None:
Raises `_NotHTML` if the content type is not text/html.
"""
content_type = response.headers.get("Content-Type", "")
if not content_type.lower().startswith("text/html"):
content_type = response.headers.get('Content-Type', '')
if not content_type.lower().startswith('text/html'):
raise _NotHTML(content_type, response.request.method)
@ -93,7 +96,7 @@ def _ensure_html_response(url: str, session: PipSession) -> None:
`_NotHTML` if the content type is not text/html.
"""
scheme, netloc, path, query, fragment = urllib.parse.urlsplit(url)
if scheme not in {"http", "https"}:
if scheme not in {'http', 'https'}:
raise _NotHTTP()
resp = session.head(url, allow_redirects=True)
@ -118,12 +121,12 @@ def _get_html_response(url: str, session: PipSession) -> Response:
if is_archive_file(Link(url).filename):
_ensure_html_response(url, session=session)
logger.debug("Getting page %s", redact_auth_from_url(url))
logger.debug('Getting page %s', redact_auth_from_url(url))
resp = session.get(
url,
headers={
"Accept": "text/html",
'Accept': 'text/html',
# We don't want to blindly returned cached data for
# /simple/, because authors generally expecting that
# twine upload && pip install will function, but if
@ -137,7 +140,7 @@ def _get_html_response(url: str, session: PipSession) -> Response:
# trip for the conditional GET now instead of only
# once per 10 minutes.
# For more information, please see pypa/pip#5670.
"Cache-Control": "max-age=0",
'Cache-Control': 'max-age=0',
},
)
raise_for_status(resp)
@ -152,12 +155,12 @@ def _get_html_response(url: str, session: PipSession) -> Response:
return resp
def _get_encoding_from_headers(headers: ResponseHeaders) -> Optional[str]:
def _get_encoding_from_headers(headers: ResponseHeaders) -> str | None:
"""Determine if we have any encoding information in our headers."""
if headers and "Content-Type" in headers:
content_type, params = cgi.parse_header(headers["Content-Type"])
if "charset" in params:
return params["charset"]
if headers and 'Content-Type' in headers:
content_type, params = cgi.parse_header(headers['Content-Type'])
if 'charset' in params:
return params['charset']
return None
@ -175,8 +178,8 @@ def _determine_base_url(document: HTMLElement, page_url: str) -> str:
TODO: Remove when `html5lib` is dropped.
"""
for base in document.findall(".//base"):
href = base.get("href")
for base in document.findall('.//base'):
href = base.get('href')
if href is not None:
return href
return page_url
@ -204,7 +207,7 @@ def _clean_file_url_path(part: str) -> str:
# percent-encoded: /
_reserved_chars_re = re.compile("(@|%2F)", re.IGNORECASE)
_reserved_chars_re = re.compile('(@|%2F)', re.IGNORECASE)
def _clean_url_path(path: str, is_local_path: bool) -> str:
@ -221,12 +224,12 @@ def _clean_url_path(path: str, is_local_path: bool) -> str:
parts = _reserved_chars_re.split(path)
cleaned_parts = []
for to_clean, reserved in pairwise(itertools.chain(parts, [""])):
for to_clean, reserved in pairwise(itertools.chain(parts, [''])):
cleaned_parts.append(clean_func(to_clean))
# Normalize %xx escapes (e.g. %2f -> %2F)
cleaned_parts.append(reserved.upper())
return "".join(cleaned_parts)
return ''.join(cleaned_parts)
def _clean_link(url: str) -> str:
@ -245,20 +248,20 @@ def _clean_link(url: str) -> str:
def _create_link_from_element(
element_attribs: Dict[str, Optional[str]],
element_attribs: dict[str, str | None],
page_url: str,
base_url: str,
) -> Optional[Link]:
) -> Link | None:
"""
Convert an anchor element's attributes in a simple repository page to a Link.
"""
href = element_attribs.get("href")
href = element_attribs.get('href')
if not href:
return None
url = _clean_link(urllib.parse.urljoin(base_url, href))
pyrequire = element_attribs.get("data-requires-python")
yanked_reason = element_attribs.get("data-yanked")
pyrequire = element_attribs.get('data-requires-python')
yanked_reason = element_attribs.get('data-yanked')
link = Link(
url,
@ -271,7 +274,7 @@ def _create_link_from_element(
class CacheablePageContent:
def __init__(self, page: "HTMLPage") -> None:
def __init__(self, page: HTMLPage) -> None:
assert page.cache_link_parsing
self.page = page
@ -284,7 +287,7 @@ class CacheablePageContent:
class ParseLinks(Protocol):
def __call__(
self, page: "HTMLPage", use_deprecated_html5lib: bool
self, page: HTMLPage, use_deprecated_html5lib: bool,
) -> Iterable[Link]:
...
@ -298,12 +301,12 @@ def with_cached_html_pages(fn: ParseLinks) -> ParseLinks:
@functools.lru_cache(maxsize=None)
def wrapper(
cacheable_page: CacheablePageContent, use_deprecated_html5lib: bool
) -> List[Link]:
cacheable_page: CacheablePageContent, use_deprecated_html5lib: bool,
) -> list[Link]:
return list(fn(cacheable_page.page, use_deprecated_html5lib))
@functools.wraps(fn)
def wrapper_wrapper(page: "HTMLPage", use_deprecated_html5lib: bool) -> List[Link]:
def wrapper_wrapper(page: HTMLPage, use_deprecated_html5lib: bool) -> list[Link]:
if page.cache_link_parsing:
return wrapper(CacheablePageContent(page), use_deprecated_html5lib)
return list(fn(page, use_deprecated_html5lib))
@ -311,7 +314,7 @@ def with_cached_html_pages(fn: ParseLinks) -> ParseLinks:
return wrapper_wrapper
def _parse_links_html5lib(page: "HTMLPage") -> Iterable[Link]:
def _parse_links_html5lib(page: HTMLPage) -> Iterable[Link]:
"""
Parse an HTML document, and yield its anchor elements as Link objects.
@ -325,7 +328,7 @@ def _parse_links_html5lib(page: "HTMLPage") -> Iterable[Link]:
url = page.url
base_url = _determine_base_url(document, url)
for anchor in document.findall(".//a"):
for anchor in document.findall('.//a'):
link = _create_link_from_element(
anchor.attrib,
page_url=url,
@ -337,7 +340,7 @@ def _parse_links_html5lib(page: "HTMLPage") -> Iterable[Link]:
@with_cached_html_pages
def parse_links(page: "HTMLPage", use_deprecated_html5lib: bool) -> Iterable[Link]:
def parse_links(page: HTMLPage, use_deprecated_html5lib: bool) -> Iterable[Link]:
"""
Parse an HTML document, and yield its anchor elements as Link objects.
"""
@ -347,7 +350,7 @@ def parse_links(page: "HTMLPage", use_deprecated_html5lib: bool) -> Iterable[Lin
return
parser = HTMLLinkParser(page.url)
encoding = page.encoding or "utf-8"
encoding = page.encoding or 'utf-8'
parser.feed(page.content.decode(encoding))
url = page.url
@ -369,7 +372,7 @@ class HTMLPage:
def __init__(
self,
content: bytes,
encoding: Optional[str],
encoding: str | None,
url: str,
cache_link_parsing: bool = True,
) -> None:
@ -399,32 +402,32 @@ class HTMLLinkParser(HTMLParser):
super().__init__(convert_charrefs=True)
self.url: str = url
self.base_url: Optional[str] = None
self.anchors: List[Dict[str, Optional[str]]] = []
self.base_url: str | None = None
self.anchors: list[dict[str, str | None]] = []
def handle_starttag(self, tag: str, attrs: List[Tuple[str, Optional[str]]]) -> None:
if tag == "base" and self.base_url is None:
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
if tag == 'base' and self.base_url is None:
href = self.get_href(attrs)
if href is not None:
self.base_url = href
elif tag == "a":
elif tag == 'a':
self.anchors.append(dict(attrs))
def get_href(self, attrs: List[Tuple[str, Optional[str]]]) -> Optional[str]:
def get_href(self, attrs: list[tuple[str, str | None]]) -> str | None:
for name, value in attrs:
if name == "href":
if name == 'href':
return value
return None
def _handle_get_page_fail(
link: Link,
reason: Union[str, Exception],
meth: Optional[Callable[..., None]] = None,
reason: str | Exception,
meth: Callable[..., None] | None = None,
) -> None:
if meth is None:
meth = logger.debug
meth("Could not fetch URL %s: %s - skipping", link, reason)
meth('Could not fetch URL %s: %s - skipping', link, reason)
def _make_html_page(response: Response, cache_link_parsing: bool = True) -> HTMLPage:
@ -438,20 +441,20 @@ def _make_html_page(response: Response, cache_link_parsing: bool = True) -> HTML
def _get_html_page(
link: Link, session: Optional[PipSession] = None
) -> Optional["HTMLPage"]:
link: Link, session: PipSession | None = None,
) -> HTMLPage | None:
if session is None:
raise TypeError(
"_get_html_page() missing 1 required keyword argument: 'session'"
"_get_html_page() missing 1 required keyword argument: 'session'",
)
url = link.url.split("#", 1)[0]
url = link.url.split('#', 1)[0]
# Check for VCS schemes that do not support lookup as web pages.
vcs_scheme = _match_vcs_scheme(url)
if vcs_scheme:
logger.warning(
"Cannot look at %s URL %s because it does not support lookup as web pages.",
'Cannot look at %s URL %s because it does not support lookup as web pages.',
vcs_scheme,
link,
)
@ -459,26 +462,26 @@ def _get_html_page(
# Tack index.html onto file:// URLs that point to directories
scheme, _, path, _, _, _ = urllib.parse.urlparse(url)
if scheme == "file" and os.path.isdir(urllib.request.url2pathname(path)):
if scheme == 'file' and os.path.isdir(urllib.request.url2pathname(path)):
# add trailing slash if not present so urljoin doesn't trim
# final segment
if not url.endswith("/"):
url += "/"
url = urllib.parse.urljoin(url, "index.html")
logger.debug(" file: URL is directory, getting %s", url)
if not url.endswith('/'):
url += '/'
url = urllib.parse.urljoin(url, 'index.html')
logger.debug(' file: URL is directory, getting %s', url)
try:
resp = _get_html_response(url, session=session)
except _NotHTTP:
logger.warning(
"Skipping page %s because it looks like an archive, and cannot "
"be checked by a HTTP HEAD request.",
'Skipping page %s because it looks like an archive, and cannot '
'be checked by a HTTP HEAD request.',
link,
)
except _NotHTML as exc:
logger.warning(
"Skipping page %s because the %s request got Content-Type: %s."
"The only supported Content-Type is text/html",
'Skipping page %s because the %s request got Content-Type: %s.'
'The only supported Content-Type is text/html',
link,
exc.request_desc,
exc.content_type,
@ -488,21 +491,21 @@ def _get_html_page(
except RetryError as exc:
_handle_get_page_fail(link, exc)
except SSLError as exc:
reason = "There was a problem confirming the ssl certificate: "
reason = 'There was a problem confirming the ssl certificate: '
reason += str(exc)
_handle_get_page_fail(link, reason, meth=logger.info)
except requests.ConnectionError as exc:
_handle_get_page_fail(link, f"connection error: {exc}")
_handle_get_page_fail(link, f'connection error: {exc}')
except requests.Timeout:
_handle_get_page_fail(link, "timed out")
_handle_get_page_fail(link, 'timed out')
else:
return _make_html_page(resp, cache_link_parsing=link.cache_link_parsing)
return None
class CollectedSources(NamedTuple):
find_links: Sequence[Optional[LinkSource]]
index_urls: Sequence[Optional[LinkSource]]
find_links: Sequence[LinkSource | None]
index_urls: Sequence[LinkSource | None]
class LinkCollector:
@ -528,7 +531,7 @@ class LinkCollector:
session: PipSession,
options: Values,
suppress_no_index: bool = False,
) -> "LinkCollector":
) -> LinkCollector:
"""
:param session: The Session to use to make requests.
:param suppress_no_index: Whether to ignore the --no-index option
@ -537,8 +540,8 @@ class LinkCollector:
index_urls = [options.index_url] + options.extra_index_urls
if options.no_index and not suppress_no_index:
logger.debug(
"Ignoring indexes: %s",
",".join(redact_auth_from_url(url) for url in index_urls),
'Ignoring indexes: %s',
','.join(redact_auth_from_url(url) for url in index_urls),
)
index_urls = []
@ -556,10 +559,10 @@ class LinkCollector:
return link_collector
@property
def find_links(self) -> List[str]:
def find_links(self) -> list[str]:
return self.search_scope.find_links
def fetch_page(self, location: Link) -> Optional[HTMLPage]:
def fetch_page(self, location: Link) -> HTMLPage | None:
"""
Fetch an HTML page containing package links.
"""
@ -594,15 +597,15 @@ class LinkCollector:
if logger.isEnabledFor(logging.DEBUG):
lines = [
f"* {s.link}"
f'* {s.link}'
for s in itertools.chain(find_links_sources, index_url_sources)
if s is not None and s.link is not None
]
lines = [
f"{len(lines)} location(s) to search "
f"for versions of {project_name}:"
f'{len(lines)} location(s) to search '
f'for versions of {project_name}:',
] + lines
logger.debug("\n".join(lines))
logger.debug('\n'.join(lines))
return CollectedSources(
find_links=list(find_links_sources),

View file

@ -1,27 +1,26 @@
"""Routines related to PyPI, indexes"""
# The following comment should be removed at some point in the future.
# mypy: strict-optional=False
from __future__ import annotations
import functools
import itertools
import logging
import re
from typing import FrozenSet, Iterable, List, Optional, Set, Tuple, Union
from typing import FrozenSet
from typing import Iterable
from typing import List
from typing import Optional
from typing import Set
from typing import Tuple
from typing import Union
from pip._vendor.packaging import specifiers
from pip._vendor.packaging.tags import Tag
from pip._vendor.packaging.utils import canonicalize_name
from pip._vendor.packaging.version import _BaseVersion
from pip._vendor.packaging.version import parse as parse_version
from pip._internal.exceptions import (
BestVersionAlreadyInstalled,
DistributionNotFound,
InvalidWheelFilename,
UnsupportedWheel,
)
from pip._internal.index.collector import LinkCollector, parse_links
from pip._internal.exceptions import BestVersionAlreadyInstalled
from pip._internal.exceptions import DistributionNotFound
from pip._internal.exceptions import InvalidWheelFilename
from pip._internal.exceptions import UnsupportedWheel
from pip._internal.index.collector import LinkCollector
from pip._internal.index.collector import parse_links
from pip._internal.models.candidate import InstallationCandidate
from pip._internal.models.format_control import FormatControl
from pip._internal.models.link import Link
@ -37,8 +36,13 @@ from pip._internal.utils.logging import indent_log
from pip._internal.utils.misc import build_netloc
from pip._internal.utils.packaging import check_requires_python
from pip._internal.utils.unpacking import SUPPORTED_EXTENSIONS
from pip._vendor.packaging import specifiers
from pip._vendor.packaging.tags import Tag
from pip._vendor.packaging.utils import canonicalize_name
from pip._vendor.packaging.version import _BaseVersion
from pip._vendor.packaging.version import parse as parse_version
__all__ = ["FormatControl", "BestCandidateResult", "PackageFinder"]
__all__ = ['FormatControl', 'BestCandidateResult', 'PackageFinder']
logger = getLogger(__name__)
@ -49,7 +53,7 @@ CandidateSortingKey = Tuple[int, int, int, _BaseVersion, Optional[int], BuildTag
def _check_link_requires_python(
link: Link,
version_info: Tuple[int, int, int],
version_info: tuple[int, int, int],
ignore_requires_python: bool = False,
) -> bool:
"""
@ -68,16 +72,16 @@ def _check_link_requires_python(
)
except specifiers.InvalidSpecifier:
logger.debug(
"Ignoring invalid Requires-Python (%r) for link: %s",
'Ignoring invalid Requires-Python (%r) for link: %s',
link.requires_python,
link,
)
else:
if not is_compatible:
version = ".".join(map(str, version_info))
version = '.'.join(map(str, version_info))
if not ignore_requires_python:
logger.verbose(
"Link requires a different Python (%s not in: %r): %s",
'Link requires a different Python (%s not in: %r): %s',
version,
link.requires_python,
link,
@ -85,7 +89,7 @@ def _check_link_requires_python(
return False
logger.debug(
"Ignoring failed Requires-Python check (%s not in: %r) for link: %s",
'Ignoring failed Requires-Python check (%s not in: %r) for link: %s',
version,
link.requires_python,
link,
@ -100,7 +104,7 @@ class LinkEvaluator:
Responsible for evaluating links for a particular project.
"""
_py_version_re = re.compile(r"-py([123]\.?[0-9]?)$")
_py_version_re = re.compile(r'-py([123]\.?[0-9]?)$')
# Don't include an allow_yanked default value to make sure each call
# site considers whether yanked releases are allowed. This also causes
@ -110,10 +114,10 @@ class LinkEvaluator:
self,
project_name: str,
canonical_name: str,
formats: FrozenSet[str],
formats: frozenset[str],
target_python: TargetPython,
allow_yanked: bool,
ignore_requires_python: Optional[bool] = None,
ignore_requires_python: bool | None = None,
) -> None:
"""
:param project_name: The user supplied package name.
@ -143,7 +147,7 @@ class LinkEvaluator:
self.project_name = project_name
def evaluate_link(self, link: Link) -> Tuple[bool, Optional[str]]:
def evaluate_link(self, link: Link) -> tuple[bool, str | None]:
"""
Determine whether a link is a candidate for installation.
@ -154,8 +158,8 @@ class LinkEvaluator:
"""
version = None
if link.is_yanked and not self._allow_yanked:
reason = link.yanked_reason or "<none given>"
return (False, f"yanked for reason: {reason}")
reason = link.yanked_reason or '<none given>'
return (False, f'yanked for reason: {reason}')
if link.egg_fragment:
egg_info = link.egg_fragment
@ -163,21 +167,21 @@ class LinkEvaluator:
else:
egg_info, ext = link.splitext()
if not ext:
return (False, "not a file")
return (False, 'not a file')
if ext not in SUPPORTED_EXTENSIONS:
return (False, f"unsupported archive format: {ext}")
if "binary" not in self._formats and ext == WHEEL_EXTENSION:
reason = "No binaries permitted for {}".format(self.project_name)
return (False, f'unsupported archive format: {ext}')
if 'binary' not in self._formats and ext == WHEEL_EXTENSION:
reason = f'No binaries permitted for {self.project_name}'
return (False, reason)
if "macosx10" in link.path and ext == ".zip":
return (False, "macosx10 one")
if 'macosx10' in link.path and ext == '.zip':
return (False, 'macosx10 one')
if ext == WHEEL_EXTENSION:
try:
wheel = Wheel(link.filename)
except InvalidWheelFilename:
return (False, "invalid wheel filename")
return (False, 'invalid wheel filename')
if canonicalize_name(wheel.name) != self._canonical_name:
reason = "wrong project name (not {})".format(self.project_name)
reason = f'wrong project name (not {self.project_name})'
return (False, reason)
supported_tags = self._target_python.get_tags()
@ -187,8 +191,8 @@ class LinkEvaluator:
file_tags = wheel.get_formatted_file_tags()
reason = (
"none of the wheel's tags ({}) are compatible "
"(run pip debug --verbose to show compatible tags)".format(
", ".join(file_tags)
'(run pip debug --verbose to show compatible tags)'.format(
', '.join(file_tags),
)
)
return (False, reason)
@ -196,8 +200,8 @@ class LinkEvaluator:
version = wheel.version
# This should be up by the self.ok_binary check, but see issue 2700.
if "source" not in self._formats and ext != WHEEL_EXTENSION:
reason = f"No sources permitted for {self.project_name}"
if 'source' not in self._formats and ext != WHEEL_EXTENSION:
reason = f'No sources permitted for {self.project_name}'
return (False, reason)
if not version:
@ -206,7 +210,7 @@ class LinkEvaluator:
self._canonical_name,
)
if not version:
reason = f"Missing project version for {self.project_name}"
reason = f'Missing project version for {self.project_name}'
return (False, reason)
match = self._py_version_re.search(version)
@ -214,7 +218,7 @@ class LinkEvaluator:
version = version[: match.start()]
py_version = match.group(1)
if py_version != self._target_python.py_version:
return (False, "Python version is incorrect")
return (False, 'Python version is incorrect')
supports_python = _check_link_requires_python(
link,
@ -226,16 +230,16 @@ class LinkEvaluator:
# _log_skipped_link().
return (False, None)
logger.debug("Found link %s, version: %s", link, version)
logger.debug('Found link %s, version: %s', link, version)
return (True, version)
def filter_unallowed_hashes(
candidates: List[InstallationCandidate],
candidates: list[InstallationCandidate],
hashes: Hashes,
project_name: str,
) -> List[InstallationCandidate]:
) -> list[InstallationCandidate]:
"""
Filter out candidates whose hashes aren't allowed, and return a new
list of candidates.
@ -253,8 +257,8 @@ def filter_unallowed_hashes(
"""
if not hashes:
logger.debug(
"Given no hashes to check %s links for project %r: "
"discarding no candidates",
'Given no hashes to check %s links for project %r: '
'discarding no candidates',
len(candidates),
project_name,
)
@ -284,16 +288,16 @@ def filter_unallowed_hashes(
filtered = list(candidates)
if len(filtered) == len(candidates):
discard_message = "discarding no candidates"
discard_message = 'discarding no candidates'
else:
discard_message = "discarding {} non-matches:\n {}".format(
discard_message = 'discarding {} non-matches:\n {}'.format(
len(non_matches),
"\n ".join(str(candidate.link) for candidate in non_matches),
'\n '.join(str(candidate.link) for candidate in non_matches),
)
logger.debug(
"Checked %s links for project %r against %s hashes "
"(%s matches, %s no digest): %s",
'Checked %s links for project %r against %s hashes '
'(%s matches, %s no digest): %s',
len(candidates),
project_name,
hashes.digest_count,
@ -333,9 +337,9 @@ class BestCandidateResult:
def __init__(
self,
candidates: List[InstallationCandidate],
applicable_candidates: List[InstallationCandidate],
best_candidate: Optional[InstallationCandidate],
candidates: list[InstallationCandidate],
applicable_candidates: list[InstallationCandidate],
best_candidate: InstallationCandidate | None,
) -> None:
"""
:param candidates: A sequence of all available candidates found.
@ -375,12 +379,12 @@ class CandidateEvaluator:
def create(
cls,
project_name: str,
target_python: Optional[TargetPython] = None,
target_python: TargetPython | None = None,
prefer_binary: bool = False,
allow_all_prereleases: bool = False,
specifier: Optional[specifiers.BaseSpecifier] = None,
hashes: Optional[Hashes] = None,
) -> "CandidateEvaluator":
specifier: specifiers.BaseSpecifier | None = None,
hashes: Hashes | None = None,
) -> CandidateEvaluator:
"""Create a CandidateEvaluator object.
:param target_python: The target Python interpreter to use when
@ -410,11 +414,11 @@ class CandidateEvaluator:
def __init__(
self,
project_name: str,
supported_tags: List[Tag],
supported_tags: list[Tag],
specifier: specifiers.BaseSpecifier,
prefer_binary: bool = False,
allow_all_prereleases: bool = False,
hashes: Optional[Hashes] = None,
hashes: Hashes | None = None,
) -> None:
"""
:param supported_tags: The PEP 425 tags supported by the target
@ -435,8 +439,8 @@ class CandidateEvaluator:
def get_applicable_candidates(
self,
candidates: List[InstallationCandidate],
) -> List[InstallationCandidate]:
candidates: list[InstallationCandidate],
) -> list[InstallationCandidate]:
"""
Return the applicable candidates from a list of candidates.
"""
@ -510,18 +514,18 @@ class CandidateEvaluator:
try:
pri = -(
wheel.find_most_preferred_tag(
valid_tags, self._wheel_tag_preferences
valid_tags, self._wheel_tag_preferences,
)
)
except ValueError:
raise UnsupportedWheel(
"{} is not a supported wheel for this platform. It "
"can't be sorted.".format(wheel.filename)
'{} is not a supported wheel for this platform. It '
"can't be sorted.".format(wheel.filename),
)
if self._prefer_binary:
binary_preference = 1
if wheel.build_tag is not None:
match = re.match(r"^(\d+)(.*)$", wheel.build_tag)
match = re.match(r'^(\d+)(.*)$', wheel.build_tag)
build_tag_groups = match.groups()
build_tag = (int(build_tag_groups[0]), build_tag_groups[1])
else: # sdist
@ -539,8 +543,8 @@ class CandidateEvaluator:
def sort_best_candidate(
self,
candidates: List[InstallationCandidate],
) -> Optional[InstallationCandidate]:
candidates: list[InstallationCandidate],
) -> InstallationCandidate | None:
"""
Return the best candidate per the instance's sort order, or None if
no candidate is acceptable.
@ -552,7 +556,7 @@ class CandidateEvaluator:
def compute_best_candidate(
self,
candidates: List[InstallationCandidate],
candidates: list[InstallationCandidate],
) -> BestCandidateResult:
"""
Compute and return a `BestCandidateResult` instance.
@ -581,9 +585,9 @@ class PackageFinder:
target_python: TargetPython,
allow_yanked: bool,
use_deprecated_html5lib: bool,
format_control: Optional[FormatControl] = None,
candidate_prefs: Optional[CandidatePreferences] = None,
ignore_requires_python: Optional[bool] = None,
format_control: FormatControl | None = None,
candidate_prefs: CandidatePreferences | None = None,
ignore_requires_python: bool | None = None,
) -> None:
"""
This constructor is primarily meant to be used by the create() class
@ -610,7 +614,7 @@ class PackageFinder:
self.format_control = format_control
# These are boring links that have already been logged somehow.
self._logged_links: Set[Link] = set()
self._logged_links: set[Link] = set()
# Don't include an allow_yanked default value to make sure each call
# site considers whether yanked releases are allowed. This also causes
@ -621,10 +625,10 @@ class PackageFinder:
cls,
link_collector: LinkCollector,
selection_prefs: SelectionPreferences,
target_python: Optional[TargetPython] = None,
target_python: TargetPython | None = None,
*,
use_deprecated_html5lib: bool,
) -> "PackageFinder":
) -> PackageFinder:
"""Create a PackageFinder.
:param selection_prefs: The candidate selection preferences, as a
@ -664,11 +668,11 @@ class PackageFinder:
self._link_collector.search_scope = search_scope
@property
def find_links(self) -> List[str]:
def find_links(self) -> list[str]:
return self._link_collector.find_links
@property
def index_urls(self) -> List[str]:
def index_urls(self) -> list[str]:
return self.search_scope.index_urls
@property
@ -703,13 +707,13 @@ class PackageFinder:
ignore_requires_python=self._ignore_requires_python,
)
def _sort_links(self, links: Iterable[Link]) -> List[Link]:
def _sort_links(self, links: Iterable[Link]) -> list[Link]:
"""
Returns elements of links in order, non-egg links first, egg links
second, while eliminating duplicates
"""
eggs, no_eggs = [], []
seen: Set[Link] = set()
seen: set[Link] = set()
for link in links:
if link not in seen:
seen.add(link)
@ -723,12 +727,12 @@ class PackageFinder:
if link not in self._logged_links:
# Put the link at the end so the reason is more visible and because
# the link string is usually very long.
logger.debug("Skipping link: %s: %s", reason, link)
logger.debug('Skipping link: %s: %s', reason, link)
self._logged_links.add(link)
def get_install_candidate(
self, link_evaluator: LinkEvaluator, link: Link
) -> Optional[InstallationCandidate]:
self, link_evaluator: LinkEvaluator, link: Link,
) -> InstallationCandidate | None:
"""
If the link is a candidate for install, convert it to an
InstallationCandidate and return it. Otherwise, return None.
@ -746,8 +750,8 @@ class PackageFinder:
)
def evaluate_links(
self, link_evaluator: LinkEvaluator, links: Iterable[Link]
) -> List[InstallationCandidate]:
self, link_evaluator: LinkEvaluator, links: Iterable[Link],
) -> list[InstallationCandidate]:
"""
Convert links that are candidates to InstallationCandidate objects.
"""
@ -760,10 +764,10 @@ class PackageFinder:
return candidates
def process_project_url(
self, project_url: Link, link_evaluator: LinkEvaluator
) -> List[InstallationCandidate]:
self, project_url: Link, link_evaluator: LinkEvaluator,
) -> list[InstallationCandidate]:
logger.debug(
"Fetching project page and analyzing links: %s",
'Fetching project page and analyzing links: %s',
project_url,
)
html_page = self._link_collector.fetch_page(project_url)
@ -781,7 +785,7 @@ class PackageFinder:
return package_links
@functools.lru_cache(maxsize=None)
def find_all_candidates(self, project_name: str) -> List[InstallationCandidate]:
def find_all_candidates(self, project_name: str) -> list[InstallationCandidate]:
"""Find all available InstallationCandidate for project_name
This checks index_urls and find_links.
@ -828,7 +832,7 @@ class PackageFinder:
except Exception:
paths.append(candidate.link.url) # it's not a local file
logger.debug("Local files found: %s", ", ".join(paths))
logger.debug('Local files found: %s', ', '.join(paths))
# This is an intentional priority ordering
return file_candidates + page_candidates
@ -836,8 +840,8 @@ class PackageFinder:
def make_candidate_evaluator(
self,
project_name: str,
specifier: Optional[specifiers.BaseSpecifier] = None,
hashes: Optional[Hashes] = None,
specifier: specifiers.BaseSpecifier | None = None,
hashes: Hashes | None = None,
) -> CandidateEvaluator:
"""Create a CandidateEvaluator object to use."""
candidate_prefs = self._candidate_prefs
@ -854,8 +858,8 @@ class PackageFinder:
def find_best_candidate(
self,
project_name: str,
specifier: Optional[specifiers.BaseSpecifier] = None,
hashes: Optional[Hashes] = None,
specifier: specifiers.BaseSpecifier | None = None,
hashes: Hashes | None = None,
) -> BestCandidateResult:
"""Find matches for the given project and specifier.
@ -874,8 +878,8 @@ class PackageFinder:
return candidate_evaluator.compute_best_candidate(candidates)
def find_requirement(
self, req: InstallRequirement, upgrade: bool
) -> Optional[InstallationCandidate]:
self, req: InstallRequirement, upgrade: bool,
) -> InstallationCandidate | None:
"""Try to find a Link matching req
Expects req, an InstallRequirement and upgrade, a boolean
@ -890,7 +894,7 @@ class PackageFinder:
)
best_candidate = best_candidate_result.best_candidate
installed_version: Optional[_BaseVersion] = None
installed_version: _BaseVersion | None = None
if req.satisfied_by is not None:
installed_version = req.satisfied_by.version
@ -900,25 +904,25 @@ class PackageFinder:
# If we stop using the pkg_resources provided specifier and start
# using our own, we can drop the cast to str().
return (
", ".join(
', '.join(
sorted(
{str(c.version) for c in cand_iter},
key=parse_version,
)
)
or "none"
),
) or
'none'
)
if installed_version is None and best_candidate is None:
logger.critical(
"Could not find a version that satisfies the requirement %s "
"(from versions: %s)",
'Could not find a version that satisfies the requirement %s '
'(from versions: %s)',
req,
_format_versions(best_candidate_result.iter_all()),
)
raise DistributionNotFound(
"No matching distribution found for {}".format(req)
f'No matching distribution found for {req}',
)
best_installed = False
@ -930,14 +934,14 @@ class PackageFinder:
if not upgrade and installed_version is not None:
if best_installed:
logger.debug(
"Existing installed version (%s) is most up-to-date and "
"satisfies requirement",
'Existing installed version (%s) is most up-to-date and '
'satisfies requirement',
installed_version,
)
else:
logger.debug(
"Existing installed version (%s) satisfies requirement "
"(most up-to-date version is %s)",
'Existing installed version (%s) satisfies requirement '
'(most up-to-date version is %s)',
installed_version,
best_candidate.version,
)
@ -946,14 +950,14 @@ class PackageFinder:
if best_installed:
# We have an existing version, and its the best version
logger.debug(
"Installed version (%s) is most up-to-date (past versions: %s)",
'Installed version (%s) is most up-to-date (past versions: %s)',
installed_version,
_format_versions(best_candidate_result.iter_applicable()),
)
raise BestVersionAlreadyInstalled
logger.debug(
"Using version %s (newest of versions: %s)",
'Using version %s (newest of versions: %s)',
best_candidate.version,
_format_versions(best_candidate_result.iter_applicable()),
)
@ -979,14 +983,14 @@ def _find_name_version_sep(fragment: str, canonical_name: str) -> int:
# occurrences of dashes; if the string in front of it matches the canonical
# name, this is the one separating the name and version parts.
for i, c in enumerate(fragment):
if c != "-":
if c != '-':
continue
if canonicalize_name(fragment[:i]) == canonical_name:
return i
raise ValueError(f"{fragment} does not match {canonical_name}")
raise ValueError(f'{fragment} does not match {canonical_name}')
def _extract_version_from_fragment(fragment: str, canonical_name: str) -> Optional[str]:
def _extract_version_from_fragment(fragment: str, canonical_name: str) -> str | None:
"""Parse the version string from a <package>+<version> filename
"fragment" (stem) or egg fragment.

View file

@ -1,12 +1,18 @@
from __future__ import annotations
import logging
import mimetypes
import os
import pathlib
from typing import Callable, Iterable, Optional, Tuple
from typing import Callable
from typing import Iterable
from typing import Optional
from typing import Tuple
from pip._internal.models.candidate import InstallationCandidate
from pip._internal.models.link import Link
from pip._internal.utils.urls import path_to_url, url_to_path
from pip._internal.utils.urls import path_to_url
from pip._internal.utils.urls import url_to_path
from pip._internal.vcs import is_url
logger = logging.getLogger(__name__)
@ -19,7 +25,7 @@ PageValidator = Callable[[Link], bool]
class LinkSource:
@property
def link(self) -> Optional[Link]:
def link(self) -> Link | None:
"""Returns the underlying link, if there's one."""
raise NotImplementedError()
@ -33,7 +39,7 @@ class LinkSource:
def _is_html_file(file_url: str) -> bool:
return mimetypes.guess_type(file_url, strict=False)[0] == "text/html"
return mimetypes.guess_type(file_url, strict=False)[0] == 'text/html'
class _FlatDirectorySource(LinkSource):
@ -54,7 +60,7 @@ class _FlatDirectorySource(LinkSource):
self._path = pathlib.Path(os.path.realpath(path))
@property
def link(self) -> Optional[Link]:
def link(self) -> Link | None:
return None
def page_candidates(self) -> FoundCandidates:
@ -91,7 +97,7 @@ class _LocalFileSource(LinkSource):
self._link = link
@property
def link(self) -> Optional[Link]:
def link(self) -> Link | None:
return self._link
def page_candidates(self) -> FoundCandidates:
@ -125,7 +131,7 @@ class _RemoteFileSource(LinkSource):
self._link = link
@property
def link(self) -> Optional[Link]:
def link(self) -> Link | None:
return self._link
def page_candidates(self) -> FoundCandidates:
@ -153,7 +159,7 @@ class _IndexDirectorySource(LinkSource):
self._link = link
@property
def link(self) -> Optional[Link]:
def link(self) -> Link | None:
return self._link
def page_candidates(self) -> FoundCandidates:
@ -170,14 +176,14 @@ def build_source(
page_validator: PageValidator,
expand_dir: bool,
cache_link_parsing: bool,
) -> Tuple[Optional[str], Optional[LinkSource]]:
) -> tuple[str | None, LinkSource | None]:
path: Optional[str] = None
url: Optional[str] = None
path: str | None = None
url: str | None = None
if os.path.exists(location): # Is a local path.
url = path_to_url(location)
path = location
elif location.startswith("file:"): # A file: URL.
elif location.startswith('file:'): # A file: URL.
url = location
path = url_to_path(location)
elif is_url(location):
@ -186,7 +192,7 @@ def build_source(
if url is None:
msg = (
"Location '%s' is ignored: "
"it is either a non-existing path or lacks a specific scheme."
'it is either a non-existing path or lacks a specific scheme.'
)
logger.warning(msg, location)
return (None, None)

View file

@ -1,45 +1,52 @@
from __future__ import annotations
import functools
import logging
import os
import pathlib
import sys
import sysconfig
from typing import Any, Dict, Iterator, List, Optional, Tuple
from typing import Any
from typing import Dict
from typing import Iterator
from typing import List
from typing import Optional
from typing import Tuple
from pip._internal.models.scheme import SCHEME_KEYS, Scheme
from pip._internal.models.scheme import Scheme
from pip._internal.models.scheme import SCHEME_KEYS
from pip._internal.utils.compat import WINDOWS
from pip._internal.utils.deprecation import deprecated
from pip._internal.utils.virtualenv import running_under_virtualenv
from . import _distutils, _sysconfig
from .base import (
USER_CACHE_DIR,
get_major_minor_version,
get_src_prefix,
is_osx_framework,
site_packages,
user_site,
)
from . import _distutils
from . import _sysconfig
from .base import get_major_minor_version
from .base import get_src_prefix
from .base import is_osx_framework
from .base import site_packages
from .base import USER_CACHE_DIR
from .base import user_site
__all__ = [
"USER_CACHE_DIR",
"get_bin_prefix",
"get_bin_user",
"get_major_minor_version",
"get_platlib",
"get_prefixed_libs",
"get_purelib",
"get_scheme",
"get_src_prefix",
"site_packages",
"user_site",
'USER_CACHE_DIR',
'get_bin_prefix',
'get_bin_user',
'get_major_minor_version',
'get_platlib',
'get_prefixed_libs',
'get_purelib',
'get_scheme',
'get_src_prefix',
'site_packages',
'user_site',
]
logger = logging.getLogger(__name__)
_PLATLIBDIR: str = getattr(sys, "platlibdir", "lib")
_PLATLIBDIR: str = getattr(sys, 'platlibdir', 'lib')
_USE_SYSCONFIG_DEFAULT = sys.version_info >= (3, 10)
@ -55,7 +62,7 @@ def _should_use_sysconfig() -> bool:
This is a function for testability, but should be constant during any one
run.
"""
return bool(getattr(sysconfig, "_PIP_USE_SYSCONFIG", _USE_SYSCONFIG_DEFAULT))
return bool(getattr(sysconfig, '_PIP_USE_SYSCONFIG', _USE_SYSCONFIG_DEFAULT))
_USE_SYSCONFIG = _should_use_sysconfig()
@ -76,20 +83,20 @@ def _looks_like_bpo_44860() -> bool:
from distutils.command.install import INSTALL_SCHEMES # type: ignore
try:
unix_user_platlib = INSTALL_SCHEMES["unix_user"]["platlib"]
unix_user_platlib = INSTALL_SCHEMES['unix_user']['platlib']
except KeyError:
return False
return unix_user_platlib == "$usersite"
return unix_user_platlib == '$usersite'
def _looks_like_red_hat_patched_platlib_purelib(scheme: Dict[str, str]) -> bool:
platlib = scheme["platlib"]
if "/$platlibdir/" in platlib:
platlib = platlib.replace("/$platlibdir/", f"/{_PLATLIBDIR}/")
if "/lib64/" not in platlib:
def _looks_like_red_hat_patched_platlib_purelib(scheme: dict[str, str]) -> bool:
platlib = scheme['platlib']
if '/$platlibdir/' in platlib:
platlib = platlib.replace('/$platlibdir/', f'/{_PLATLIBDIR}/')
if '/lib64/' not in platlib:
return False
unpatched = platlib.replace("/lib64/", "/lib/")
return unpatched.replace("$platbase/", "$base/") == scheme["purelib"]
unpatched = platlib.replace('/lib64/', '/lib/')
return unpatched.replace('$platbase/', '$base/') == scheme['purelib']
@functools.lru_cache(maxsize=None)
@ -101,9 +108,9 @@ def _looks_like_red_hat_lib() -> bool:
from distutils.command.install import INSTALL_SCHEMES # type: ignore
return all(
k in INSTALL_SCHEMES
and _looks_like_red_hat_patched_platlib_purelib(INSTALL_SCHEMES[k])
for k in ("unix_prefix", "unix_home")
k in INSTALL_SCHEMES and
_looks_like_red_hat_patched_platlib_purelib(INSTALL_SCHEMES[k])
for k in ('unix_prefix', 'unix_home')
)
@ -112,7 +119,7 @@ def _looks_like_debian_scheme() -> bool:
"""Debian adds two additional schemes."""
from distutils.command.install import INSTALL_SCHEMES # type: ignore
return "deb_system" in INSTALL_SCHEMES and "unix_local" in INSTALL_SCHEMES
return 'deb_system' in INSTALL_SCHEMES and 'unix_local' in INSTALL_SCHEMES
@functools.lru_cache(maxsize=None)
@ -130,8 +137,8 @@ def _looks_like_red_hat_scheme() -> bool:
cmd: Any = install(Distribution())
cmd.finalize_options()
return (
cmd.exec_prefix == f"{os.path.normpath(sys.exec_prefix)}/local"
and cmd.prefix == f"{os.path.normpath(sys.prefix)}/local"
cmd.exec_prefix == f'{os.path.normpath(sys.exec_prefix)}/local' and
cmd.prefix == f'{os.path.normpath(sys.prefix)}/local'
)
@ -145,10 +152,10 @@ def _looks_like_slackware_scheme() -> bool:
if user_site is None: # User-site not available.
return False
try:
paths = sysconfig.get_paths(scheme="posix_user", expand=False)
paths = sysconfig.get_paths(scheme='posix_user', expand=False)
except KeyError: # User-site not available.
return False
return "/lib64/" in paths["purelib"] and "/lib64/" not in user_site
return '/lib64/' in paths['purelib'] and '/lib64/' not in user_site
@functools.lru_cache(maxsize=None)
@ -162,16 +169,16 @@ def _looks_like_msys2_mingw_scheme() -> bool:
MSYS2 MINGW's patch uses lowercase ``"lib"`` instead of the usual uppercase,
and is missing the final ``"site-packages"``.
"""
paths = sysconfig.get_paths("nt", expand=False)
paths = sysconfig.get_paths('nt', expand=False)
return all(
"Lib" not in p and "lib" in p and not p.endswith("site-packages")
for p in (paths[key] for key in ("platlib", "purelib"))
'Lib' not in p and 'lib' in p and not p.endswith('site-packages')
for p in (paths[key] for key in ('platlib', 'purelib'))
)
def _fix_abiflags(parts: Tuple[str]) -> Iterator[str]:
ldversion = sysconfig.get_config_var("LDVERSION")
abiflags: str = getattr(sys, "abiflags", None)
def _fix_abiflags(parts: tuple[str]) -> Iterator[str]:
ldversion = sysconfig.get_config_var('LDVERSION')
abiflags: str = getattr(sys, 'abiflags', None)
# LDVERSION does not end with sys.abiflags. Just return the path unchanged.
if not ldversion or not abiflags or not ldversion.endswith(abiflags):
@ -187,11 +194,11 @@ def _fix_abiflags(parts: Tuple[str]) -> Iterator[str]:
@functools.lru_cache(maxsize=None)
def _warn_mismatched(old: pathlib.Path, new: pathlib.Path, *, key: str) -> None:
issue_url = "https://github.com/pypa/pip/issues/10151"
issue_url = 'https://github.com/pypa/pip/issues/10151'
message = (
"Value for %s does not match. Please report this to <%s>"
"\ndistutils: %s"
"\nsysconfig: %s"
'Value for %s does not match. Please report this to <%s>'
'\ndistutils: %s'
'\nsysconfig: %s'
)
logger.log(_MISMATCH_LEVEL, message, key, issue_url, old, new)
@ -207,28 +214,28 @@ def _warn_if_mismatch(old: pathlib.Path, new: pathlib.Path, *, key: str) -> bool
def _log_context(
*,
user: bool = False,
home: Optional[str] = None,
root: Optional[str] = None,
prefix: Optional[str] = None,
home: str | None = None,
root: str | None = None,
prefix: str | None = None,
) -> None:
parts = [
"Additional context:",
"user = %r",
"home = %r",
"root = %r",
"prefix = %r",
'Additional context:',
'user = %r',
'home = %r',
'root = %r',
'prefix = %r',
]
logger.log(_MISMATCH_LEVEL, "\n".join(parts), user, home, root, prefix)
logger.log(_MISMATCH_LEVEL, '\n'.join(parts), user, home, root, prefix)
def get_scheme(
dist_name: str,
user: bool = False,
home: Optional[str] = None,
root: Optional[str] = None,
home: str | None = None,
root: str | None = None,
isolated: bool = False,
prefix: Optional[str] = None,
prefix: str | None = None,
) -> Scheme:
new = _sysconfig.get_scheme(
dist_name,
@ -263,12 +270,12 @@ def get_scheme(
# directory name to be ``pypy`` instead. So we treat this as a bug fix
# and not warn about it. See bpo-43307 and python/cpython#24628.
skip_pypy_special_case = (
sys.implementation.name == "pypy"
and home is not None
and k in ("platlib", "purelib")
and old_v.parent == new_v.parent
and old_v.name.startswith("python")
and new_v.name.startswith("pypy")
sys.implementation.name == 'pypy' and
home is not None and
k in ('platlib', 'purelib') and
old_v.parent == new_v.parent and
old_v.name.startswith('python') and
new_v.name.startswith('pypy')
)
if skip_pypy_special_case:
continue
@ -277,18 +284,18 @@ def get_scheme(
# the ``include`` value, but distutils's ``headers`` does. We'll let
# CPython decide whether this is a bug or feature. See bpo-43948.
skip_osx_framework_user_special_case = (
user
and is_osx_framework()
and k == "headers"
and old_v.parent.parent == new_v.parent
and old_v.parent.name.startswith("python")
user and
is_osx_framework() and
k == 'headers' and
old_v.parent.parent == new_v.parent and
old_v.parent.name.startswith('python')
)
if skip_osx_framework_user_special_case:
continue
# On Red Hat and derived Linux distributions, distutils is patched to
# use "lib64" instead of "lib" for platlib.
if k == "platlib" and _looks_like_red_hat_lib():
if k == 'platlib' and _looks_like_red_hat_lib():
continue
# On Python 3.9+, sysconfig's posix_user scheme sets platlib against
@ -296,12 +303,12 @@ def get_scheme(
# using the same $usersite for both platlib and purelib. This creates a
# mismatch when sys.platlibdir is not "lib".
skip_bpo_44860 = (
user
and k == "platlib"
and not WINDOWS
and sys.version_info >= (3, 9)
and _PLATLIBDIR != "lib"
and _looks_like_bpo_44860()
user and
k == 'platlib' and
not WINDOWS and
sys.version_info >= (3, 9) and
_PLATLIBDIR != 'lib' and
_looks_like_bpo_44860()
)
if skip_bpo_44860:
continue
@ -309,10 +316,10 @@ def get_scheme(
# Slackware incorrectly patches posix_user to use lib64 instead of lib,
# but not usersite to match the location.
skip_slackware_user_scheme = (
user
and k in ("platlib", "purelib")
and not WINDOWS
and _looks_like_slackware_scheme()
user and
k in ('platlib', 'purelib') and
not WINDOWS and
_looks_like_slackware_scheme()
)
if skip_slackware_user_scheme:
continue
@ -321,12 +328,12 @@ def get_scheme(
# /usr/local instead of /usr. Debian also places lib in dist-packages
# instead of site-packages, but the /usr/local check should cover it.
skip_linux_system_special_case = (
not (user or home or prefix or running_under_virtualenv())
and old_v.parts[1:3] == ("usr", "local")
and len(new_v.parts) > 1
and new_v.parts[1] == "usr"
and (len(new_v.parts) < 3 or new_v.parts[2] != "local")
and (_looks_like_red_hat_scheme() or _looks_like_debian_scheme())
not (user or home or prefix or running_under_virtualenv()) and
old_v.parts[1:3] == ('usr', 'local') and
len(new_v.parts) > 1 and
new_v.parts[1] == 'usr' and
(len(new_v.parts) < 3 or new_v.parts[2] != 'local') and
(_looks_like_red_hat_scheme() or _looks_like_debian_scheme())
)
if skip_linux_system_special_case:
continue
@ -334,10 +341,10 @@ def get_scheme(
# On Python 3.7 and earlier, sysconfig does not include sys.abiflags in
# the "pythonX.Y" part of the path, but distutils does.
skip_sysconfig_abiflag_bug = (
sys.version_info < (3, 8)
and not WINDOWS
and k in ("headers", "platlib", "purelib")
and tuple(_fix_abiflags(old_v.parts)) == new_v.parts
sys.version_info < (3, 8) and
not WINDOWS and
k in ('headers', 'platlib', 'purelib') and
tuple(_fix_abiflags(old_v.parts)) == new_v.parts
)
if skip_sysconfig_abiflag_bug:
continue
@ -345,7 +352,7 @@ def get_scheme(
# MSYS2 MINGW's sysconfig patch does not include the "site-packages"
# part of the path. This is incorrect and will be fixed in MSYS.
skip_msys2_mingw_bug = (
WINDOWS and k in ("platlib", "purelib") and _looks_like_msys2_mingw_scheme()
WINDOWS and k in ('platlib', 'purelib') and _looks_like_msys2_mingw_scheme()
)
if skip_msys2_mingw_bug:
continue
@ -355,14 +362,14 @@ def get_scheme(
# triggers special logic in sysconfig that's not present in distutils.
# https://github.com/python/cpython/blob/8c21941ddaf/Lib/sysconfig.py#L178-L194
skip_cpython_build = (
sysconfig.is_python_build(check_home=True)
and not WINDOWS
and k in ("headers", "include", "platinclude")
sysconfig.is_python_build(check_home=True) and
not WINDOWS and
k in ('headers', 'include', 'platinclude')
)
if skip_cpython_build:
continue
warning_contexts.append((old_v, new_v, f"scheme.{k}"))
warning_contexts.append((old_v, new_v, f'scheme.{k}'))
if not warning_contexts:
return old
@ -382,10 +389,10 @@ def get_scheme(
if any(default_old[k] != getattr(old, k) for k in SCHEME_KEYS):
deprecated(
reason=(
"Configuring installation scheme with distutils config files "
"is deprecated and will no longer work in the near future. If you "
"are using a Homebrew or Linuxbrew Python, please see discussion "
"at https://github.com/Homebrew/homebrew-core/issues/76621"
'Configuring installation scheme with distutils config files '
'is deprecated and will no longer work in the near future. If you '
'are using a Homebrew or Linuxbrew Python, please see discussion '
'at https://github.com/Homebrew/homebrew-core/issues/76621'
),
replacement=None,
gone_in=None,
@ -406,13 +413,13 @@ def get_bin_prefix() -> str:
return new
old = _distutils.get_bin_prefix()
if _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="bin_prefix"):
if _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key='bin_prefix'):
_log_context()
return old
def get_bin_user() -> str:
return _sysconfig.get_scheme("", user=True).scripts
return _sysconfig.get_scheme('', user=True).scripts
def _looks_like_deb_system_dist_packages(value: str) -> bool:
@ -427,7 +434,7 @@ def _looks_like_deb_system_dist_packages(value: str) -> bool:
"""
if not _looks_like_debian_scheme():
return False
if value == "/usr/lib/python3/dist-packages":
if value == '/usr/lib/python3/dist-packages':
return True
return False
@ -441,7 +448,7 @@ def get_purelib() -> str:
old = _distutils.get_purelib()
if _looks_like_deb_system_dist_packages(old):
return old
if _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="purelib"):
if _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key='purelib'):
_log_context()
return old
@ -455,12 +462,12 @@ def get_platlib() -> str:
old = _distutils.get_platlib()
if _looks_like_deb_system_dist_packages(old):
return old
if _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="platlib"):
if _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key='platlib'):
_log_context()
return old
def _deduplicated(v1: str, v2: str) -> List[str]:
def _deduplicated(v1: str, v2: str) -> list[str]:
"""Deduplicate values from a list."""
if v1 == v2:
return [v1]
@ -469,12 +476,12 @@ def _deduplicated(v1: str, v2: str) -> List[str]:
def _looks_like_apple_library(path: str) -> bool:
"""Apple patches sysconfig to *always* look under */Library/Python*."""
if sys.platform[:6] != "darwin":
if sys.platform[:6] != 'darwin':
return False
return path == f"/Library/Python/{get_major_minor_version()}/site-packages"
return path == f'/Library/Python/{get_major_minor_version()}/site-packages'
def get_prefixed_libs(prefix: str) -> List[str]:
def get_prefixed_libs(prefix: str) -> list[str]:
"""Return the lib locations under ``prefix``."""
new_pure, new_plat = _sysconfig.get_prefixed_libs(prefix)
if _USE_SYSCONFIG:
@ -493,9 +500,9 @@ def get_prefixed_libs(prefix: str) -> List[str]:
reason=(
"Python distributed by Apple's Command Line Tools incorrectly "
"patches sysconfig to always point to '/Library/Python'. This "
"will cause build isolation to operate incorrectly on Python "
"3.10 or later. Please help report this to Apple so they can "
"fix this. https://developer.apple.com/bug-reporting/"
'will cause build isolation to operate incorrectly on Python '
'3.10 or later. Please help report this to Apple so they can '
'fix this. https://developer.apple.com/bug-reporting/'
),
replacement=None,
gone_in=None,
@ -506,12 +513,12 @@ def get_prefixed_libs(prefix: str) -> List[str]:
_warn_if_mismatch(
pathlib.Path(old_pure),
pathlib.Path(new_pure),
key="prefixed-purelib",
key='prefixed-purelib',
),
_warn_if_mismatch(
pathlib.Path(old_plat),
pathlib.Path(new_plat),
key="prefixed-platlib",
key='prefixed-platlib',
),
]
if any(warned):

View file

@ -1,17 +1,22 @@
"""Locations where we look for configs, install stuff, etc"""
# The following comment should be removed at some point in the future.
# mypy: strict-optional=False
from __future__ import annotations
import logging
import os
import sys
from distutils.cmd import Command as DistutilsCommand
from distutils.command.install import SCHEME_KEYS
from distutils.command.install import install as distutils_install_command
from distutils.sysconfig import get_python_lib
from typing import Dict, List, Optional, Tuple, Union, cast
from typing import cast
from typing import Dict
from typing import List
from typing import Optional
from typing import Tuple
from typing import Union
from distutils.cmd import Command as DistutilsCommand
from distutils.command.install import install as distutils_install_command
from distutils.command.install import SCHEME_KEYS
from distutils.sysconfig import get_python_lib
from pip._internal.models.scheme import Scheme
from pip._internal.utils.compat import WINDOWS
from pip._internal.utils.virtualenv import running_under_virtualenv
@ -30,15 +35,15 @@ def distutils_scheme(
prefix: str = None,
*,
ignore_config_files: bool = False,
) -> Dict[str, str]:
) -> dict[str, str]:
"""
Return a distutils install scheme
"""
from distutils.dist import Distribution
dist_args: Dict[str, Union[str, List[str]]] = {"name": dist_name}
dist_args: dict[str, str | list[str]] = {'name': dist_name}
if isolated:
dist_args["script_args"] = ["--no-user-cfg"]
dist_args['script_args'] = ['--no-user-cfg']
d = Distribution(dist_args)
if not ignore_config_files:
@ -48,21 +53,21 @@ def distutils_scheme(
# Typeshed does not include find_config_files() for some reason.
paths = d.find_config_files() # type: ignore
logger.warning(
"Ignore distutils configs in %s due to encoding errors.",
", ".join(os.path.basename(p) for p in paths),
'Ignore distutils configs in %s due to encoding errors.',
', '.join(os.path.basename(p) for p in paths),
)
obj: Optional[DistutilsCommand] = None
obj = d.get_command_obj("install", create=True)
obj: DistutilsCommand | None = None
obj = d.get_command_obj('install', create=True)
assert obj is not None
i = cast(distutils_install_command, obj)
# NOTE: setting user or home has the side-effect of creating the home dir
# or user base for installations during finalize_options()
# ideally, we'd prefer a scheme class that has no side-effects.
assert not (user and prefix), f"user={user} prefix={prefix}"
assert not (home and prefix), f"home={home} prefix={prefix}"
assert not (user and prefix), f'user={user} prefix={prefix}'
assert not (home and prefix), f'home={home} prefix={prefix}'
i.user = user or i.user
if user or home:
i.prefix = ""
i.prefix = ''
i.prefix = prefix or i.prefix
i.home = home or i.home
i.root = root or i.root
@ -70,14 +75,14 @@ def distutils_scheme(
scheme = {}
for key in SCHEME_KEYS:
scheme[key] = getattr(i, "install_" + key)
scheme[key] = getattr(i, 'install_' + key)
# install_lib specified in setup.cfg should install *everything*
# into there (i.e. it takes precedence over both purelib and
# platlib). Note, i.install_lib is *always* set after
# finalize_options(); we only want to override here if the user
# has explicitly requested it hence going back to the config
if "install_lib" in d.get_option_dict("install"):
if 'install_lib' in d.get_option_dict('install'):
scheme.update(dict(purelib=i.install_lib, platlib=i.install_lib))
if running_under_virtualenv():
@ -87,17 +92,17 @@ def distutils_scheme(
prefix = i.install_userbase # type: ignore
else:
prefix = i.prefix
scheme["headers"] = os.path.join(
scheme['headers'] = os.path.join(
prefix,
"include",
"site",
f"python{get_major_minor_version()}",
'include',
'site',
f'python{get_major_minor_version()}',
dist_name,
)
if root is not None:
path_no_drive = os.path.splitdrive(os.path.abspath(scheme["headers"]))[1]
scheme["headers"] = os.path.join(root, path_no_drive[1:])
path_no_drive = os.path.splitdrive(os.path.abspath(scheme['headers']))[1]
scheme['headers'] = os.path.join(root, path_no_drive[1:])
return scheme
@ -105,10 +110,10 @@ def distutils_scheme(
def get_scheme(
dist_name: str,
user: bool = False,
home: Optional[str] = None,
root: Optional[str] = None,
home: str | None = None,
root: str | None = None,
isolated: bool = False,
prefix: Optional[str] = None,
prefix: str | None = None,
) -> Scheme:
"""
Get the "scheme" corresponding to the input parameters. The distutils
@ -129,11 +134,11 @@ def get_scheme(
"""
scheme = distutils_scheme(dist_name, user, home, root, isolated, prefix)
return Scheme(
platlib=scheme["platlib"],
purelib=scheme["purelib"],
headers=scheme["headers"],
scripts=scheme["scripts"],
data=scheme["data"],
platlib=scheme['platlib'],
purelib=scheme['purelib'],
headers=scheme['headers'],
scripts=scheme['scripts'],
data=scheme['data'],
)
@ -142,16 +147,16 @@ def get_bin_prefix() -> str:
# so we need to call normpath to eliminate them.
prefix = os.path.normpath(sys.prefix)
if WINDOWS:
bin_py = os.path.join(prefix, "Scripts")
bin_py = os.path.join(prefix, 'Scripts')
# buildout uses 'bin' on Windows too?
if not os.path.exists(bin_py):
bin_py = os.path.join(prefix, "bin")
bin_py = os.path.join(prefix, 'bin')
return bin_py
# Forcing to use /usr/local/bin for standard macOS framework installs
# Also log to ~/Library/Logs/ for use with the Console.app log viewer
if sys.platform[:6] == "darwin" and prefix[:16] == "/System/Library/":
return "/usr/local/bin"
return os.path.join(prefix, "bin")
if sys.platform[:6] == 'darwin' and prefix[:16] == '/System/Library/':
return '/usr/local/bin'
return os.path.join(prefix, 'bin')
def get_purelib() -> str:
@ -162,7 +167,7 @@ def get_platlib() -> str:
return get_python_lib(plat_specific=True)
def get_prefixed_libs(prefix: str) -> Tuple[str, str]:
def get_prefixed_libs(prefix: str) -> tuple[str, str]:
return (
get_python_lib(plat_specific=False, prefix=prefix),
get_python_lib(plat_specific=True, prefix=prefix),

View file

@ -1,15 +1,20 @@
import distutils.util # FIXME: For change_root.
from __future__ import annotations
import logging
import os
import sys
import sysconfig
import typing
from pip._internal.exceptions import InvalidSchemeCombination, UserInstallationInvalid
from pip._internal.models.scheme import SCHEME_KEYS, Scheme
import distutils.util # FIXME: For change_root.
from pip._internal.exceptions import InvalidSchemeCombination
from pip._internal.exceptions import UserInstallationInvalid
from pip._internal.models.scheme import Scheme
from pip._internal.models.scheme import SCHEME_KEYS
from pip._internal.utils.virtualenv import running_under_virtualenv
from .base import get_major_minor_version, is_osx_framework
from .base import get_major_minor_version
from .base import is_osx_framework
logger = logging.getLogger(__name__)
@ -24,7 +29,7 @@ logger = logging.getLogger(__name__)
_AVAILABLE_SCHEMES = set(sysconfig.get_scheme_names())
_PREFERRED_SCHEME_API = getattr(sysconfig, "get_preferred_scheme", None)
_PREFERRED_SCHEME_API = getattr(sysconfig, 'get_preferred_scheme', None)
def _should_use_osx_framework_prefix() -> bool:
@ -47,9 +52,9 @@ def _should_use_osx_framework_prefix() -> bool:
or our own, and we deal with this special case in ``get_scheme()`` instead.
"""
return (
"osx_framework_library" in _AVAILABLE_SCHEMES
and not running_under_virtualenv()
and is_osx_framework()
'osx_framework_library' in _AVAILABLE_SCHEMES and
not running_under_virtualenv() and
is_osx_framework()
)
@ -68,67 +73,67 @@ def _infer_prefix() -> str:
If none of the above works, fall back to ``posix_prefix``.
"""
if _PREFERRED_SCHEME_API:
return _PREFERRED_SCHEME_API("prefix")
return _PREFERRED_SCHEME_API('prefix')
if _should_use_osx_framework_prefix():
return "osx_framework_library"
implementation_suffixed = f"{sys.implementation.name}_{os.name}"
return 'osx_framework_library'
implementation_suffixed = f'{sys.implementation.name}_{os.name}'
if implementation_suffixed in _AVAILABLE_SCHEMES:
return implementation_suffixed
if sys.implementation.name in _AVAILABLE_SCHEMES:
return sys.implementation.name
suffixed = f"{os.name}_prefix"
suffixed = f'{os.name}_prefix'
if suffixed in _AVAILABLE_SCHEMES:
return suffixed
if os.name in _AVAILABLE_SCHEMES: # On Windows, prefx is just called "nt".
return os.name
return "posix_prefix"
return 'posix_prefix'
def _infer_user() -> str:
"""Try to find a user scheme for the current platform."""
if _PREFERRED_SCHEME_API:
return _PREFERRED_SCHEME_API("user")
return _PREFERRED_SCHEME_API('user')
if is_osx_framework() and not running_under_virtualenv():
suffixed = "osx_framework_user"
suffixed = 'osx_framework_user'
else:
suffixed = f"{os.name}_user"
suffixed = f'{os.name}_user'
if suffixed in _AVAILABLE_SCHEMES:
return suffixed
if "posix_user" not in _AVAILABLE_SCHEMES: # User scheme unavailable.
if 'posix_user' not in _AVAILABLE_SCHEMES: # User scheme unavailable.
raise UserInstallationInvalid()
return "posix_user"
return 'posix_user'
def _infer_home() -> str:
"""Try to find a home for the current platform."""
if _PREFERRED_SCHEME_API:
return _PREFERRED_SCHEME_API("home")
suffixed = f"{os.name}_home"
return _PREFERRED_SCHEME_API('home')
suffixed = f'{os.name}_home'
if suffixed in _AVAILABLE_SCHEMES:
return suffixed
return "posix_home"
return 'posix_home'
# Update these keys if the user sets a custom home.
_HOME_KEYS = [
"installed_base",
"base",
"installed_platbase",
"platbase",
"prefix",
"exec_prefix",
'installed_base',
'base',
'installed_platbase',
'platbase',
'prefix',
'exec_prefix',
]
if sysconfig.get_config_var("userbase") is not None:
_HOME_KEYS.append("userbase")
if sysconfig.get_config_var('userbase') is not None:
_HOME_KEYS.append('userbase')
def get_scheme(
dist_name: str,
user: bool = False,
home: typing.Optional[str] = None,
root: typing.Optional[str] = None,
home: str | None = None,
root: str | None = None,
isolated: bool = False,
prefix: typing.Optional[str] = None,
prefix: str | None = None,
) -> Scheme:
"""
Get the "scheme" corresponding to the input parameters.
@ -144,9 +149,9 @@ def get_scheme(
base directory for the same
"""
if user and prefix:
raise InvalidSchemeCombination("--user", "--prefix")
raise InvalidSchemeCombination('--user', '--prefix')
if home and prefix:
raise InvalidSchemeCombination("--home", "--prefix")
raise InvalidSchemeCombination('--home', '--prefix')
if home is not None:
scheme_name = _infer_home()
@ -158,8 +163,8 @@ def get_scheme(
# Special case: When installing into a custom prefix, use posix_prefix
# instead of osx_framework_library. See _should_use_osx_framework_prefix()
# docstring for details.
if prefix is not None and scheme_name == "osx_framework_library":
scheme_name = "posix_prefix"
if prefix is not None and scheme_name == 'osx_framework_library':
scheme_name = 'posix_prefix'
if home is not None:
variables = {k: home for k in _HOME_KEYS}
@ -177,20 +182,20 @@ def get_scheme(
# pip's historical header path logic (see point 1) did not do this.
if running_under_virtualenv():
if user:
base = variables.get("userbase", sys.prefix)
base = variables.get('userbase', sys.prefix)
else:
base = variables.get("base", sys.prefix)
python_xy = f"python{get_major_minor_version()}"
paths["include"] = os.path.join(base, "include", "site", python_xy)
base = variables.get('base', sys.prefix)
python_xy = f'python{get_major_minor_version()}'
paths['include'] = os.path.join(base, 'include', 'site', python_xy)
elif not dist_name:
dist_name = "UNKNOWN"
dist_name = 'UNKNOWN'
scheme = Scheme(
platlib=paths["platlib"],
purelib=paths["purelib"],
headers=os.path.join(paths["include"], dist_name),
scripts=paths["scripts"],
data=paths["data"],
platlib=paths['platlib'],
purelib=paths['purelib'],
headers=os.path.join(paths['include'], dist_name),
scripts=paths['scripts'],
data=paths['data'],
)
if root is not None:
for key in SCHEME_KEYS:
@ -201,19 +206,19 @@ def get_scheme(
def get_bin_prefix() -> str:
# Forcing to use /usr/local/bin for standard macOS framework installs.
if sys.platform[:6] == "darwin" and sys.prefix[:16] == "/System/Library/":
return "/usr/local/bin"
return sysconfig.get_paths()["scripts"]
if sys.platform[:6] == 'darwin' and sys.prefix[:16] == '/System/Library/':
return '/usr/local/bin'
return sysconfig.get_paths()['scripts']
def get_purelib() -> str:
return sysconfig.get_paths()["purelib"]
return sysconfig.get_paths()['purelib']
def get_platlib() -> str:
return sysconfig.get_paths()["platlib"]
return sysconfig.get_paths()['platlib']
def get_prefixed_libs(prefix: str) -> typing.Tuple[str, str]:
paths = sysconfig.get_paths(vars={"base": prefix, "platbase": prefix})
return (paths["purelib"], paths["platlib"])
def get_prefixed_libs(prefix: str) -> tuple[str, str]:
paths = sysconfig.get_paths(vars={'base': prefix, 'platbase': prefix})
return (paths['purelib'], paths['platlib'])

View file

@ -1,3 +1,5 @@
from __future__ import annotations
import functools
import os
import site
@ -9,10 +11,10 @@ from pip._internal.utils import appdirs
from pip._internal.utils.virtualenv import running_under_virtualenv
# Application Directories
USER_CACHE_DIR = appdirs.user_cache_dir("pip")
USER_CACHE_DIR = appdirs.user_cache_dir('pip')
# FIXME doesn't account for venv linked to global site-packages
site_packages: typing.Optional[str] = sysconfig.get_path("purelib")
site_packages: str | None = sysconfig.get_path('purelib')
def get_major_minor_version() -> str:
@ -20,19 +22,19 @@ def get_major_minor_version() -> str:
Return the major-minor version of the current Python as a string, e.g.
"3.7" or "3.10".
"""
return "{}.{}".format(*sys.version_info)
return '{}.{}'.format(*sys.version_info)
def get_src_prefix() -> str:
if running_under_virtualenv():
src_prefix = os.path.join(sys.prefix, "src")
src_prefix = os.path.join(sys.prefix, 'src')
else:
# FIXME: keep src in cwd for now (it is not a temporary folder)
try:
src_prefix = os.path.join(os.getcwd(), "src")
src_prefix = os.path.join(os.getcwd(), 'src')
except OSError:
# In case the current working directory has been renamed or deleted
sys.exit("The folder you are executing pip from can no longer be found.")
sys.exit('The folder you are executing pip from can no longer be found.')
# under macOS + virtualenv sys.prefix is not properly resolved
# it is something like /path/to/python/bin/..
@ -42,11 +44,11 @@ def get_src_prefix() -> str:
try:
# Use getusersitepackages if this is present, as it ensures that the
# value is initialised properly.
user_site: typing.Optional[str] = site.getusersitepackages()
user_site: str | None = site.getusersitepackages()
except AttributeError:
user_site = site.USER_SITE
@functools.lru_cache(maxsize=None)
def is_osx_framework() -> bool:
return bool(sysconfig.get_config_var("PYTHONFRAMEWORK"))
return bool(sysconfig.get_config_var('PYTHONFRAMEWORK'))

View file

@ -1,7 +1,10 @@
from typing import List, Optional
from __future__ import annotations
from typing import List
from typing import Optional
def main(args: Optional[List[str]] = None) -> int:
def main(args: list[str] | None = None) -> int:
"""This is preserved for old console scripts that may still be referencing
it.

View file

@ -1,16 +1,23 @@
from typing import List, Optional
from __future__ import annotations
from .base import BaseDistribution, BaseEnvironment, FilesystemWheel, MemoryWheel, Wheel
from typing import List
from typing import Optional
from .base import BaseDistribution
from .base import BaseEnvironment
from .base import FilesystemWheel
from .base import MemoryWheel
from .base import Wheel
__all__ = [
"BaseDistribution",
"BaseEnvironment",
"FilesystemWheel",
"MemoryWheel",
"Wheel",
"get_default_environment",
"get_environment",
"get_wheel_distribution",
'BaseDistribution',
'BaseEnvironment',
'FilesystemWheel',
'MemoryWheel',
'Wheel',
'get_default_environment',
'get_environment',
'get_wheel_distribution',
]
@ -26,7 +33,7 @@ def get_default_environment() -> BaseEnvironment:
return Environment.default()
def get_environment(paths: Optional[List[str]]) -> BaseEnvironment:
def get_environment(paths: list[str] | None) -> BaseEnvironment:
"""Get a representation of the environment specified by ``paths``.
This returns an Environment instance from the chosen backend based on the

View file

@ -1,3 +1,5 @@
from __future__ import annotations
import csv
import email.message
import json
@ -5,38 +7,35 @@ import logging
import pathlib
import re
import zipfile
from typing import (
IO,
TYPE_CHECKING,
Collection,
Container,
Iterable,
Iterator,
List,
Optional,
Tuple,
Union,
)
from pip._vendor.packaging.requirements import Requirement
from pip._vendor.packaging.specifiers import InvalidSpecifier, SpecifierSet
from pip._vendor.packaging.utils import NormalizedName
from pip._vendor.packaging.version import LegacyVersion, Version
from typing import Collection
from typing import Container
from typing import IO
from typing import Iterable
from typing import Iterator
from typing import List
from typing import Optional
from typing import Tuple
from typing import TYPE_CHECKING
from typing import Union
from pip._internal.exceptions import NoneMetadataError
from pip._internal.locations import site_packages, user_site
from pip._internal.models.direct_url import (
DIRECT_URL_METADATA_NAME,
DirectUrl,
DirectUrlValidationError,
)
from pip._internal.locations import site_packages
from pip._internal.locations import user_site
from pip._internal.models.direct_url import DIRECT_URL_METADATA_NAME
from pip._internal.models.direct_url import DirectUrl
from pip._internal.models.direct_url import DirectUrlValidationError
from pip._internal.utils.compat import stdlib_pkgs # TODO: Move definition here.
from pip._internal.utils.egg_link import (
egg_link_path_from_location,
egg_link_path_from_sys_path,
)
from pip._internal.utils.misc import is_local, normalize_path
from pip._internal.utils.egg_link import egg_link_path_from_location
from pip._internal.utils.egg_link import egg_link_path_from_sys_path
from pip._internal.utils.misc import is_local
from pip._internal.utils.misc import normalize_path
from pip._internal.utils.urls import url_to_path
from pip._vendor.packaging.requirements import Requirement
from pip._vendor.packaging.specifiers import InvalidSpecifier
from pip._vendor.packaging.specifiers import SpecifierSet
from pip._vendor.packaging.utils import NormalizedName
from pip._vendor.packaging.version import LegacyVersion
from pip._vendor.packaging.version import Version
if TYPE_CHECKING:
from typing import Protocol
@ -65,8 +64,8 @@ class BaseEntryPoint(Protocol):
def _convert_installed_files_path(
entry: Tuple[str, ...],
info: Tuple[str, ...],
entry: tuple[str, ...],
info: tuple[str, ...],
) -> str:
"""Convert a legacy installed-files.txt path into modern RECORD path.
@ -85,9 +84,9 @@ def _convert_installed_files_path(
from ``info``; if ``info`` is empty, start appending ``..`` instead.
2. Join the two directly.
"""
while entry and entry[0] == "..":
if not info or info[-1] == "..":
info += ("..",)
while entry and entry[0] == '..':
if not info or info[-1] == '..':
info += ('..',)
else:
info = info[:-1]
entry = entry[1:]
@ -96,13 +95,13 @@ def _convert_installed_files_path(
class BaseDistribution(Protocol):
def __repr__(self) -> str:
return f"{self.raw_name} {self.version} ({self.location})"
return f'{self.raw_name} {self.version} ({self.location})'
def __str__(self) -> str:
return f"{self.raw_name} {self.version}"
return f'{self.raw_name} {self.version}'
@property
def location(self) -> Optional[str]:
def location(self) -> str | None:
"""Where the distribution is loaded from.
A string value is not necessarily a filesystem path, since distributions
@ -116,7 +115,7 @@ class BaseDistribution(Protocol):
raise NotImplementedError()
@property
def editable_project_location(self) -> Optional[str]:
def editable_project_location(self) -> str | None:
"""The project location for editable distributions.
This is the directory where pyproject.toml or setup.py is located.
@ -138,7 +137,7 @@ class BaseDistribution(Protocol):
return None
@property
def installed_location(self) -> Optional[str]:
def installed_location(self) -> str | None:
"""The distribution's "installed" location.
This should generally be a ``site-packages`` directory. This is
@ -158,7 +157,7 @@ class BaseDistribution(Protocol):
return normalize_path(location)
@property
def info_location(self) -> Optional[str]:
def info_location(self) -> str | None:
"""Location of the .[egg|dist]-info directory or file.
Similarly to ``location``, a string value is not necessarily a
@ -196,7 +195,7 @@ class BaseDistribution(Protocol):
location = self.location
if not location:
return False
return location.endswith(".egg")
return location.endswith('.egg')
@property
def installed_with_setuptools_egg_info(self) -> bool:
@ -212,7 +211,7 @@ class BaseDistribution(Protocol):
info_location = self.info_location
if not info_location:
return False
if not info_location.endswith(".egg-info"):
if not info_location.endswith('.egg-info'):
return False
return pathlib.Path(info_location).is_dir()
@ -228,7 +227,7 @@ class BaseDistribution(Protocol):
info_location = self.info_location
if not info_location:
return False
if not info_location.endswith(".dist-info"):
if not info_location.endswith('.dist-info'):
return False
return pathlib.Path(info_location).is_dir()
@ -246,10 +245,10 @@ class BaseDistribution(Protocol):
This is a copy of ``pkg_resources.to_filename()`` for compatibility.
"""
return self.raw_name.replace("-", "_")
return self.raw_name.replace('-', '_')
@property
def direct_url(self) -> Optional[DirectUrl]:
def direct_url(self) -> DirectUrl | None:
"""Obtain a DirectUrl from this distribution.
Returns None if the distribution has no `direct_url.json` metadata,
@ -267,7 +266,7 @@ class BaseDistribution(Protocol):
DirectUrlValidationError,
) as e:
logger.warning(
"Error parsing %s for %s: %s",
'Error parsing %s for %s: %s',
DIRECT_URL_METADATA_NAME,
self.canonical_name,
e,
@ -277,14 +276,14 @@ class BaseDistribution(Protocol):
@property
def installer(self) -> str:
try:
installer_text = self.read_text("INSTALLER")
installer_text = self.read_text('INSTALLER')
except (OSError, ValueError, NoneMetadataError):
return "" # Fail silently if the installer file cannot be read.
return '' # Fail silently if the installer file cannot be read.
for line in installer_text.splitlines():
cleaned_line = line.strip()
if cleaned_line:
return cleaned_line
return ""
return ''
@property
def editable(self) -> bool:
@ -350,16 +349,16 @@ class BaseDistribution(Protocol):
raise NotImplementedError()
@property
def metadata_version(self) -> Optional[str]:
def metadata_version(self) -> str | None:
"""Value of "Metadata-Version:" in distribution metadata, if available."""
return self.metadata.get("Metadata-Version")
return self.metadata.get('Metadata-Version')
@property
def raw_name(self) -> str:
"""Value of "Name:" in distribution metadata."""
# The metadata should NEVER be missing the Name: key, but if it somehow
# does, fall back to the known canonical name.
return self.metadata.get("Name", self.canonical_name)
return self.metadata.get('Name', self.canonical_name)
@property
def requires_python(self) -> SpecifierSet:
@ -368,14 +367,14 @@ class BaseDistribution(Protocol):
If the key does not exist or contains an invalid value, an empty
SpecifierSet should be returned.
"""
value = self.metadata.get("Requires-Python")
value = self.metadata.get('Requires-Python')
if value is None:
return SpecifierSet()
try:
# Convert to str to satisfy the type checker; this can be a Header object.
spec = SpecifierSet(str(value))
except InvalidSpecifier as e:
message = "Package %r has an invalid Requires-Python: %s"
message = 'Package %r has an invalid Requires-Python: %s'
logger.warning(message, self.raw_name, e)
return SpecifierSet()
return spec
@ -396,17 +395,17 @@ class BaseDistribution(Protocol):
"""
raise NotImplementedError()
def _iter_declared_entries_from_record(self) -> Optional[Iterator[str]]:
def _iter_declared_entries_from_record(self) -> Iterator[str] | None:
try:
text = self.read_text("RECORD")
text = self.read_text('RECORD')
except FileNotFoundError:
return None
# This extra Path-str cast normalizes entries.
return (str(pathlib.Path(row[0])) for row in csv.reader(text.splitlines()))
def _iter_declared_entries_from_legacy(self) -> Optional[Iterator[str]]:
def _iter_declared_entries_from_legacy(self) -> Iterator[str] | None:
try:
text = self.read_text("installed-files.txt")
text = self.read_text('installed-files.txt')
except FileNotFoundError:
return None
paths = (p for p in text.splitlines(keepends=False) if p)
@ -425,7 +424,7 @@ class BaseDistribution(Protocol):
for p in paths
)
def iter_declared_entries(self) -> Optional[Iterator[str]]:
def iter_declared_entries(self) -> Iterator[str] | None:
"""Iterate through file entires declared in this distribution.
For modern .dist-info distributions, this is the files listed in the
@ -437,8 +436,8 @@ class BaseDistribution(Protocol):
contains neither ``RECORD`` nor ``installed-files.txt``.
"""
return (
self._iter_declared_entries_from_record()
or self._iter_declared_entries_from_legacy()
self._iter_declared_entries_from_record() or
self._iter_declared_entries_from_legacy()
)
@ -446,14 +445,14 @@ class BaseEnvironment:
"""An environment containing distributions to introspect."""
@classmethod
def default(cls) -> "BaseEnvironment":
def default(cls) -> BaseEnvironment:
raise NotImplementedError()
@classmethod
def from_paths(cls, paths: Optional[List[str]]) -> "BaseEnvironment":
def from_paths(cls, paths: list[str] | None) -> BaseEnvironment:
raise NotImplementedError()
def get_distribution(self, name: str) -> Optional["BaseDistribution"]:
def get_distribution(self, name: str) -> BaseDistribution | None:
"""Given a requirement name, return the installed distributions.
The name may not be normalized. The implementation must canonicalize
@ -461,7 +460,7 @@ class BaseEnvironment:
"""
raise NotImplementedError()
def _iter_distributions(self) -> Iterator["BaseDistribution"]:
def _iter_distributions(self) -> Iterator[BaseDistribution]:
"""Iterate through installed distributions.
This function should be implemented by subclass, but never called
@ -470,7 +469,7 @@ class BaseEnvironment:
"""
raise NotImplementedError()
def iter_distributions(self) -> Iterator["BaseDistribution"]:
def iter_distributions(self) -> Iterator[BaseDistribution]:
"""Iterate through installed distributions."""
for dist in self._iter_distributions():
# Make sure the distribution actually comes from a valid Python
@ -478,13 +477,13 @@ class BaseEnvironment:
# e.g. ``~atplotlib.dist-info`` if cleanup was interrupted. The
# valid project name pattern is taken from PEP 508.
project_name_valid = re.match(
r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$",
r'^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$',
dist.canonical_name,
flags=re.IGNORECASE,
)
if not project_name_valid:
logger.warning(
"Ignoring invalid distribution %s (%s)",
'Ignoring invalid distribution %s (%s)',
dist.canonical_name,
dist.location,
)

View file

@ -1,28 +1,37 @@
from __future__ import annotations
import email.message
import email.parser
import logging
import os
import pathlib
import zipfile
from typing import Collection, Iterable, Iterator, List, Mapping, NamedTuple, Optional
from typing import Collection
from typing import Iterable
from typing import Iterator
from typing import List
from typing import Mapping
from typing import NamedTuple
from typing import Optional
from pip._internal.exceptions import InvalidWheel
from pip._internal.exceptions import NoneMetadataError
from pip._internal.exceptions import UnsupportedWheel
from pip._internal.utils.misc import display_path
from pip._internal.utils.wheel import parse_wheel
from pip._internal.utils.wheel import read_wheel_metadata_file
from pip._vendor import pkg_resources
from pip._vendor.packaging.requirements import Requirement
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
from pip._vendor.packaging.utils import canonicalize_name
from pip._vendor.packaging.utils import NormalizedName
from pip._vendor.packaging.version import parse as parse_version
from pip._internal.exceptions import InvalidWheel, NoneMetadataError, UnsupportedWheel
from pip._internal.utils.misc import display_path
from pip._internal.utils.wheel import parse_wheel, read_wheel_metadata_file
from .base import (
BaseDistribution,
BaseEntryPoint,
BaseEnvironment,
DistributionVersion,
InfoPath,
Wheel,
)
from .base import BaseDistribution
from .base import BaseEntryPoint
from .base import BaseEnvironment
from .base import DistributionVersion
from .base import InfoPath
from .base import Wheel
logger = logging.getLogger(__name__)
@ -52,7 +61,7 @@ class WheelMetadata:
except UnicodeDecodeError as e:
# Augment the default error with the origin of the file.
raise UnsupportedWheel(
f"Error decoding metadata for {self._wheel_name}: {e} in {name} file"
f'Error decoding metadata for {self._wheel_name}: {e} in {name} file',
)
def get_metadata_lines(self, name: str) -> Iterable[str]:
@ -61,7 +70,7 @@ class WheelMetadata:
def metadata_isdir(self, name: str) -> bool:
return False
def metadata_listdir(self, name: str) -> List[str]:
def metadata_listdir(self, name: str) -> list[str]:
return []
def run_script(self, script_name: str, namespace: str) -> None:
@ -73,7 +82,7 @@ class Distribution(BaseDistribution):
self._dist = dist
@classmethod
def from_directory(cls, directory: str) -> "Distribution":
def from_directory(cls, directory: str) -> Distribution:
dist_dir = directory.rstrip(os.sep)
# Build a PathMetadata object, from path to metadata. :wink:
@ -81,19 +90,19 @@ class Distribution(BaseDistribution):
metadata = pkg_resources.PathMetadata(base_dir, dist_dir)
# Determine the correct Distribution object type.
if dist_dir.endswith(".egg-info"):
if dist_dir.endswith('.egg-info'):
dist_cls = pkg_resources.Distribution
dist_name = os.path.splitext(dist_dir_name)[0]
else:
assert dist_dir.endswith(".dist-info")
assert dist_dir.endswith('.dist-info')
dist_cls = pkg_resources.DistInfoDistribution
dist_name = os.path.splitext(dist_dir_name)[0].split("-")[0]
dist_name = os.path.splitext(dist_dir_name)[0].split('-')[0]
dist = dist_cls(base_dir, project_name=dist_name, metadata=metadata)
return cls(dist)
@classmethod
def from_wheel(cls, wheel: Wheel, name: str) -> "Distribution":
def from_wheel(cls, wheel: Wheel, name: str) -> Distribution:
"""Load the distribution from a given wheel.
:raises InvalidWheel: Whenever loading of the wheel causes a
@ -105,14 +114,14 @@ class Distribution(BaseDistribution):
with wheel.as_zipfile() as zf:
info_dir, _ = parse_wheel(zf, name)
metadata_text = {
path.split("/", 1)[-1]: read_wheel_metadata_file(zf, path)
path.split('/', 1)[-1]: read_wheel_metadata_file(zf, path)
for path in zf.namelist()
if path.startswith(f"{info_dir}/")
if path.startswith(f'{info_dir}/')
}
except zipfile.BadZipFile as e:
raise InvalidWheel(wheel.location, name) from e
except UnsupportedWheel as e:
raise UnsupportedWheel(f"{name} has an invalid wheel, {e}")
raise UnsupportedWheel(f'{name} has an invalid wheel, {e}')
dist = pkg_resources.DistInfoDistribution(
location=wheel.location,
metadata=WheelMetadata(metadata_text, wheel.location),
@ -121,11 +130,11 @@ class Distribution(BaseDistribution):
return cls(dist)
@property
def location(self) -> Optional[str]:
def location(self) -> str | None:
return self._dist.location
@property
def info_location(self) -> Optional[str]:
def info_location(self) -> str | None:
return self._dist.egg_info
@property
@ -170,7 +179,7 @@ class Distribution(BaseDistribution):
def iter_entry_points(self) -> Iterable[BaseEntryPoint]:
for group, entries in self._dist.get_entry_map().items():
for name, entry_point in entries.items():
name, _, value = str(entry_point).partition("=")
name, _, value = str(entry_point).partition('=')
yield EntryPoint(name=name.strip(), value=value.strip(), group=group)
@property
@ -180,9 +189,9 @@ class Distribution(BaseDistribution):
True but `get_metadata()` returns None.
"""
if isinstance(self._dist, pkg_resources.DistInfoDistribution):
metadata_name = "METADATA"
metadata_name = 'METADATA'
else:
metadata_name = "PKG-INFO"
metadata_name = 'PKG-INFO'
try:
metadata = self.read_text(metadata_name)
except FileNotFoundError:
@ -190,8 +199,8 @@ class Distribution(BaseDistribution):
displaying_path = display_path(self.location)
else:
displaying_path = repr(self.location)
logger.warning("No metadata found in %s", displaying_path)
metadata = ""
logger.warning('No metadata found in %s', displaying_path)
metadata = ''
feed_parser = email.parser.FeedParser()
feed_parser.feed(metadata)
return feed_parser.close()
@ -214,10 +223,10 @@ class Environment(BaseEnvironment):
return cls(pkg_resources.working_set)
@classmethod
def from_paths(cls, paths: Optional[List[str]]) -> BaseEnvironment:
def from_paths(cls, paths: list[str] | None) -> BaseEnvironment:
return cls(pkg_resources.WorkingSet(paths))
def _search_distribution(self, name: str) -> Optional[BaseDistribution]:
def _search_distribution(self, name: str) -> BaseDistribution | None:
"""Find a distribution matching the ``name`` in the environment.
This searches from *all* distributions available in the environment, to
@ -229,7 +238,7 @@ class Environment(BaseEnvironment):
return dist
return None
def get_distribution(self, name: str) -> Optional[BaseDistribution]:
def get_distribution(self, name: str) -> BaseDistribution | None:
# Search the distribution by looking through the working set.
dist = self._search_distribution(name)
if dist:

View file

@ -1,2 +1,3 @@
"""A package that contains models that represent entities.
"""
from __future__ import annotations

View file

@ -1,13 +1,14 @@
from pip._vendor.packaging.version import parse as parse_version
from __future__ import annotations
from pip._internal.models.link import Link
from pip._internal.utils.models import KeyBasedCompareMixin
from pip._vendor.packaging.version import parse as parse_version
class InstallationCandidate(KeyBasedCompareMixin):
"""Represents a potential "candidate" for installation."""
__slots__ = ["name", "version", "link"]
__slots__ = ['name', 'version', 'link']
def __init__(self, name: str, version: str, link: Link) -> None:
self.name = name
@ -20,14 +21,14 @@ class InstallationCandidate(KeyBasedCompareMixin):
)
def __repr__(self) -> str:
return "<InstallationCandidate({!r}, {!r}, {!r})>".format(
return '<InstallationCandidate({!r}, {!r}, {!r})>'.format(
self.name,
self.version,
self.link,
)
def __str__(self) -> str:
return "{!r} candidate (version {} at {})".format(
return '{!r} candidate (version {} at {})'.format(
self.name,
self.version,
self.link,

View file

@ -1,21 +1,29 @@
""" PEP 610 """
from __future__ import annotations
import json
import re
import urllib.parse
from typing import Any, Dict, Iterable, Optional, Type, TypeVar, Union
from typing import Any
from typing import Dict
from typing import Iterable
from typing import Optional
from typing import Type
from typing import TypeVar
from typing import Union
__all__ = [
"DirectUrl",
"DirectUrlValidationError",
"DirInfo",
"ArchiveInfo",
"VcsInfo",
'DirectUrl',
'DirectUrlValidationError',
'DirInfo',
'ArchiveInfo',
'VcsInfo',
]
T = TypeVar("T")
T = TypeVar('T')
DIRECT_URL_METADATA_NAME = "direct_url.json"
ENV_VAR_RE = re.compile(r"^\$\{[A-Za-z0-9-_]+\}(:\$\{[A-Za-z0-9-_]+\})?$")
DIRECT_URL_METADATA_NAME = 'direct_url.json'
ENV_VAR_RE = re.compile(r'^\$\{[A-Za-z0-9-_]+\}(:\$\{[A-Za-z0-9-_]+\})?$')
class DirectUrlValidationError(Exception):
@ -23,59 +31,59 @@ class DirectUrlValidationError(Exception):
def _get(
d: Dict[str, Any], expected_type: Type[T], key: str, default: Optional[T] = None
) -> Optional[T]:
d: dict[str, Any], expected_type: type[T], key: str, default: T | None = None,
) -> T | None:
"""Get value from dictionary and verify expected type."""
if key not in d:
return default
value = d[key]
if not isinstance(value, expected_type):
raise DirectUrlValidationError(
"{!r} has unexpected type for {} (expected {})".format(
value, key, expected_type
)
'{!r} has unexpected type for {} (expected {})'.format(
value, key, expected_type,
),
)
return value
def _get_required(
d: Dict[str, Any], expected_type: Type[T], key: str, default: Optional[T] = None
d: dict[str, Any], expected_type: type[T], key: str, default: T | None = None,
) -> T:
value = _get(d, expected_type, key, default)
if value is None:
raise DirectUrlValidationError(f"{key} must have a value")
raise DirectUrlValidationError(f'{key} must have a value')
return value
def _exactly_one_of(infos: Iterable[Optional["InfoType"]]) -> "InfoType":
def _exactly_one_of(infos: Iterable[InfoType | None]) -> InfoType:
infos = [info for info in infos if info is not None]
if not infos:
raise DirectUrlValidationError(
"missing one of archive_info, dir_info, vcs_info"
'missing one of archive_info, dir_info, vcs_info',
)
if len(infos) > 1:
raise DirectUrlValidationError(
"more than one of archive_info, dir_info, vcs_info"
'more than one of archive_info, dir_info, vcs_info',
)
assert infos[0] is not None
return infos[0]
def _filter_none(**kwargs: Any) -> Dict[str, Any]:
def _filter_none(**kwargs: Any) -> dict[str, Any]:
"""Make dict excluding None values."""
return {k: v for k, v in kwargs.items() if v is not None}
class VcsInfo:
name = "vcs_info"
name = 'vcs_info'
def __init__(
self,
vcs: str,
commit_id: str,
requested_revision: Optional[str] = None,
resolved_revision: Optional[str] = None,
resolved_revision_type: Optional[str] = None,
requested_revision: str | None = None,
resolved_revision: str | None = None,
resolved_revision_type: str | None = None,
) -> None:
self.vcs = vcs
self.requested_revision = requested_revision
@ -84,18 +92,18 @@ class VcsInfo:
self.resolved_revision_type = resolved_revision_type
@classmethod
def _from_dict(cls, d: Optional[Dict[str, Any]]) -> Optional["VcsInfo"]:
def _from_dict(cls, d: dict[str, Any] | None) -> VcsInfo | None:
if d is None:
return None
return cls(
vcs=_get_required(d, str, "vcs"),
commit_id=_get_required(d, str, "commit_id"),
requested_revision=_get(d, str, "requested_revision"),
resolved_revision=_get(d, str, "resolved_revision"),
resolved_revision_type=_get(d, str, "resolved_revision_type"),
vcs=_get_required(d, str, 'vcs'),
commit_id=_get_required(d, str, 'commit_id'),
requested_revision=_get(d, str, 'requested_revision'),
resolved_revision=_get(d, str, 'resolved_revision'),
resolved_revision_type=_get(d, str, 'resolved_revision_type'),
)
def _to_dict(self) -> Dict[str, Any]:
def _to_dict(self) -> dict[str, Any]:
return _filter_none(
vcs=self.vcs,
requested_revision=self.requested_revision,
@ -106,26 +114,26 @@ class VcsInfo:
class ArchiveInfo:
name = "archive_info"
name = 'archive_info'
def __init__(
self,
hash: Optional[str] = None,
hash: str | None = None,
) -> None:
self.hash = hash
@classmethod
def _from_dict(cls, d: Optional[Dict[str, Any]]) -> Optional["ArchiveInfo"]:
def _from_dict(cls, d: dict[str, Any] | None) -> ArchiveInfo | None:
if d is None:
return None
return cls(hash=_get(d, str, "hash"))
return cls(hash=_get(d, str, 'hash'))
def _to_dict(self) -> Dict[str, Any]:
def _to_dict(self) -> dict[str, Any]:
return _filter_none(hash=self.hash)
class DirInfo:
name = "dir_info"
name = 'dir_info'
def __init__(
self,
@ -134,12 +142,12 @@ class DirInfo:
self.editable = editable
@classmethod
def _from_dict(cls, d: Optional[Dict[str, Any]]) -> Optional["DirInfo"]:
def _from_dict(cls, d: dict[str, Any] | None) -> DirInfo | None:
if d is None:
return None
return cls(editable=_get_required(d, bool, "editable", default=False))
return cls(editable=_get_required(d, bool, 'editable', default=False))
def _to_dict(self) -> Dict[str, Any]:
def _to_dict(self) -> dict[str, Any]:
return _filter_none(editable=self.editable or None)
@ -151,20 +159,20 @@ class DirectUrl:
self,
url: str,
info: InfoType,
subdirectory: Optional[str] = None,
subdirectory: str | None = None,
) -> None:
self.url = url
self.info = info
self.subdirectory = subdirectory
def _remove_auth_from_netloc(self, netloc: str) -> str:
if "@" not in netloc:
if '@' not in netloc:
return netloc
user_pass, netloc_no_user_pass = netloc.split("@", 1)
user_pass, netloc_no_user_pass = netloc.split('@', 1)
if (
isinstance(self.info, VcsInfo)
and self.info.vcs == "git"
and user_pass == "git"
isinstance(self.info, VcsInfo) and
self.info.vcs == 'git' and
user_pass == 'git'
):
return netloc
if ENV_VAR_RE.match(user_pass):
@ -180,7 +188,7 @@ class DirectUrl:
purl = urllib.parse.urlsplit(self.url)
netloc = self._remove_auth_from_netloc(purl.netloc)
surl = urllib.parse.urlunsplit(
(purl.scheme, netloc, purl.path, purl.query, purl.fragment)
(purl.scheme, netloc, purl.path, purl.query, purl.fragment),
)
return surl
@ -188,20 +196,20 @@ class DirectUrl:
self.from_dict(self.to_dict())
@classmethod
def from_dict(cls, d: Dict[str, Any]) -> "DirectUrl":
def from_dict(cls, d: dict[str, Any]) -> DirectUrl:
return DirectUrl(
url=_get_required(d, str, "url"),
subdirectory=_get(d, str, "subdirectory"),
url=_get_required(d, str, 'url'),
subdirectory=_get(d, str, 'subdirectory'),
info=_exactly_one_of(
[
ArchiveInfo._from_dict(_get(d, dict, "archive_info")),
DirInfo._from_dict(_get(d, dict, "dir_info")),
VcsInfo._from_dict(_get(d, dict, "vcs_info")),
]
ArchiveInfo._from_dict(_get(d, dict, 'archive_info')),
DirInfo._from_dict(_get(d, dict, 'dir_info')),
VcsInfo._from_dict(_get(d, dict, 'vcs_info')),
],
),
)
def to_dict(self) -> Dict[str, Any]:
def to_dict(self) -> dict[str, Any]:
res = _filter_none(
url=self.redacted_url,
subdirectory=self.subdirectory,
@ -210,7 +218,7 @@ class DirectUrl:
return res
@classmethod
def from_json(cls, s: str) -> "DirectUrl":
def from_json(cls, s: str) -> DirectUrl:
return cls.from_dict(json.loads(s))
def to_json(self) -> str:

View file

@ -1,19 +1,22 @@
from typing import FrozenSet, Optional, Set
from __future__ import annotations
from pip._vendor.packaging.utils import canonicalize_name
from typing import FrozenSet
from typing import Optional
from typing import Set
from pip._internal.exceptions import CommandError
from pip._vendor.packaging.utils import canonicalize_name
class FormatControl:
"""Helper for managing formats from which a package can be installed."""
__slots__ = ["no_binary", "only_binary"]
__slots__ = ['no_binary', 'only_binary']
def __init__(
self,
no_binary: Optional[Set[str]] = None,
only_binary: Optional[Set[str]] = None,
no_binary: set[str] | None = None,
only_binary: set[str] | None = None,
) -> None:
if no_binary is None:
no_binary = set()
@ -33,48 +36,48 @@ class FormatControl:
return all(getattr(self, k) == getattr(other, k) for k in self.__slots__)
def __repr__(self) -> str:
return "{}({}, {})".format(
self.__class__.__name__, self.no_binary, self.only_binary
return '{}({}, {})'.format(
self.__class__.__name__, self.no_binary, self.only_binary,
)
@staticmethod
def handle_mutual_excludes(value: str, target: Set[str], other: Set[str]) -> None:
if value.startswith("-"):
def handle_mutual_excludes(value: str, target: set[str], other: set[str]) -> None:
if value.startswith('-'):
raise CommandError(
"--no-binary / --only-binary option requires 1 argument."
'--no-binary / --only-binary option requires 1 argument.',
)
new = value.split(",")
while ":all:" in new:
new = value.split(',')
while ':all:' in new:
other.clear()
target.clear()
target.add(":all:")
del new[: new.index(":all:") + 1]
target.add(':all:')
del new[: new.index(':all:') + 1]
# Without a none, we want to discard everything as :all: covers it
if ":none:" not in new:
if ':none:' not in new:
return
for name in new:
if name == ":none:":
if name == ':none:':
target.clear()
continue
name = canonicalize_name(name)
other.discard(name)
target.add(name)
def get_allowed_formats(self, canonical_name: str) -> FrozenSet[str]:
result = {"binary", "source"}
def get_allowed_formats(self, canonical_name: str) -> frozenset[str]:
result = {'binary', 'source'}
if canonical_name in self.only_binary:
result.discard("source")
result.discard('source')
elif canonical_name in self.no_binary:
result.discard("binary")
elif ":all:" in self.only_binary:
result.discard("source")
elif ":all:" in self.no_binary:
result.discard("binary")
result.discard('binary')
elif ':all:' in self.only_binary:
result.discard('source')
elif ':all:' in self.no_binary:
result.discard('binary')
return frozenset(result)
def disallow_binaries(self) -> None:
self.handle_mutual_excludes(
":all:",
':all:',
self.no_binary,
self.only_binary,
)

View file

@ -1,17 +1,19 @@
from __future__ import annotations
import urllib.parse
class PackageIndex:
"""Represents a Package Index and provides easier access to endpoints"""
__slots__ = ["url", "netloc", "simple_url", "pypi_url", "file_storage_domain"]
__slots__ = ['url', 'netloc', 'simple_url', 'pypi_url', 'file_storage_domain']
def __init__(self, url: str, file_storage_domain: str) -> None:
super().__init__()
self.url = url
self.netloc = urllib.parse.urlsplit(url).netloc
self.simple_url = self._url_for_path("simple")
self.pypi_url = self._url_for_path("pypi")
self.simple_url = self._url_for_path('simple')
self.pypi_url = self._url_for_path('pypi')
# This is part of a temporary hack used to block installs of PyPI
# packages which depend on external urls only necessary until PyPI can
@ -22,7 +24,7 @@ class PackageIndex:
return urllib.parse.urljoin(self.url, path)
PyPI = PackageIndex("https://pypi.org/", file_storage_domain="files.pythonhosted.org")
PyPI = PackageIndex('https://pypi.org/', file_storage_domain='files.pythonhosted.org')
TestPyPI = PackageIndex(
"https://test.pypi.org/", file_storage_domain="test-files.pythonhosted.org"
'https://test.pypi.org/', file_storage_domain='test-files.pythonhosted.org',
)

View file

@ -1,20 +1,27 @@
from __future__ import annotations
import functools
import logging
import os
import posixpath
import re
import urllib.parse
from typing import TYPE_CHECKING, Dict, List, NamedTuple, Optional, Tuple, Union
from typing import Dict
from typing import List
from typing import NamedTuple
from typing import Optional
from typing import Tuple
from typing import TYPE_CHECKING
from typing import Union
from pip._internal.utils.filetypes import WHEEL_EXTENSION
from pip._internal.utils.hashes import Hashes
from pip._internal.utils.misc import (
redact_auth_from_url,
split_auth_from_netloc,
splitext,
)
from pip._internal.utils.misc import redact_auth_from_url
from pip._internal.utils.misc import split_auth_from_netloc
from pip._internal.utils.misc import splitext
from pip._internal.utils.models import KeyBasedCompareMixin
from pip._internal.utils.urls import path_to_url, url_to_path
from pip._internal.utils.urls import path_to_url
from pip._internal.utils.urls import url_to_path
if TYPE_CHECKING:
from pip._internal.index.collector import HTMLPage
@ -22,27 +29,27 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
_SUPPORTED_HASHES = ("sha1", "sha224", "sha384", "sha256", "sha512", "md5")
_SUPPORTED_HASHES = ('sha1', 'sha224', 'sha384', 'sha256', 'sha512', 'md5')
class Link(KeyBasedCompareMixin):
"""Represents a parsed link from a Package Index's simple URL"""
__slots__ = [
"_parsed_url",
"_url",
"comes_from",
"requires_python",
"yanked_reason",
"cache_link_parsing",
'_parsed_url',
'_url',
'comes_from',
'requires_python',
'yanked_reason',
'cache_link_parsing',
]
def __init__(
self,
url: str,
comes_from: Optional[Union[str, "HTMLPage"]] = None,
requires_python: Optional[str] = None,
yanked_reason: Optional[str] = None,
comes_from: str | HTMLPage | None = None,
requires_python: str | None = None,
yanked_reason: str | None = None,
cache_link_parsing: bool = True,
) -> None:
"""
@ -67,7 +74,7 @@ class Link(KeyBasedCompareMixin):
"""
# url can be a UNC windows share
if url.startswith("\\\\"):
if url.startswith('\\\\'):
url = path_to_url(url)
self._parsed_url = urllib.parse.urlsplit(url)
@ -85,18 +92,18 @@ class Link(KeyBasedCompareMixin):
def __str__(self) -> str:
if self.requires_python:
rp = f" (requires-python:{self.requires_python})"
rp = f' (requires-python:{self.requires_python})'
else:
rp = ""
rp = ''
if self.comes_from:
return "{} (from {}){}".format(
redact_auth_from_url(self._url), self.comes_from, rp
return '{} (from {}){}'.format(
redact_auth_from_url(self._url), self.comes_from, rp,
)
else:
return redact_auth_from_url(str(self._url))
def __repr__(self) -> str:
return f"<Link {self}>"
return f'<Link {self}>'
@property
def url(self) -> str:
@ -104,7 +111,7 @@ class Link(KeyBasedCompareMixin):
@property
def filename(self) -> str:
path = self.path.rstrip("/")
path = self.path.rstrip('/')
name = posixpath.basename(path)
if not name:
# Make sure we don't leak auth information if the netloc
@ -113,7 +120,7 @@ class Link(KeyBasedCompareMixin):
return netloc
name = urllib.parse.unquote(name)
assert name, f"URL {self._url!r} produced no filename"
assert name, f'URL {self._url!r} produced no filename'
return name
@property
@ -135,8 +142,8 @@ class Link(KeyBasedCompareMixin):
def path(self) -> str:
return urllib.parse.unquote(self._parsed_url.path)
def splitext(self) -> Tuple[str, str]:
return splitext(posixpath.basename(self.path.rstrip("/")))
def splitext(self) -> tuple[str, str]:
return splitext(posixpath.basename(self.path.rstrip('/')))
@property
def ext(self) -> str:
@ -145,39 +152,39 @@ class Link(KeyBasedCompareMixin):
@property
def url_without_fragment(self) -> str:
scheme, netloc, path, query, fragment = self._parsed_url
return urllib.parse.urlunsplit((scheme, netloc, path, query, ""))
return urllib.parse.urlunsplit((scheme, netloc, path, query, ''))
_egg_fragment_re = re.compile(r"[#&]egg=([^&]*)")
_egg_fragment_re = re.compile(r'[#&]egg=([^&]*)')
@property
def egg_fragment(self) -> Optional[str]:
def egg_fragment(self) -> str | None:
match = self._egg_fragment_re.search(self._url)
if not match:
return None
return match.group(1)
_subdirectory_fragment_re = re.compile(r"[#&]subdirectory=([^&]*)")
_subdirectory_fragment_re = re.compile(r'[#&]subdirectory=([^&]*)')
@property
def subdirectory_fragment(self) -> Optional[str]:
def subdirectory_fragment(self) -> str | None:
match = self._subdirectory_fragment_re.search(self._url)
if not match:
return None
return match.group(1)
_hash_re = re.compile(
r"({choices})=([a-f0-9]+)".format(choices="|".join(_SUPPORTED_HASHES))
r'({choices})=([a-f0-9]+)'.format(choices='|'.join(_SUPPORTED_HASHES)),
)
@property
def hash(self) -> Optional[str]:
def hash(self) -> str | None:
match = self._hash_re.search(self._url)
if match:
return match.group(2)
return None
@property
def hash_name(self) -> Optional[str]:
def hash_name(self) -> str | None:
match = self._hash_re.search(self._url)
if match:
return match.group(1)
@ -185,11 +192,11 @@ class Link(KeyBasedCompareMixin):
@property
def show_url(self) -> str:
return posixpath.basename(self._url.split("#", 1)[0].split("?", 1)[0])
return posixpath.basename(self._url.split('#', 1)[0].split('?', 1)[0])
@property
def is_file(self) -> bool:
return self.scheme == "file"
return self.scheme == 'file'
def is_existing_dir(self) -> bool:
return self.is_file and os.path.isdir(self.file_path)
@ -212,7 +219,7 @@ class Link(KeyBasedCompareMixin):
def has_hash(self) -> bool:
return self.hash_name is not None
def is_hash_allowed(self, hashes: Optional[Hashes]) -> bool:
def is_hash_allowed(self, hashes: Hashes | None) -> bool:
"""
Return True if the link has a hash and it is allowed.
"""
@ -252,31 +259,31 @@ class _CleanResult(NamedTuple):
"""
parsed: urllib.parse.SplitResult
query: Dict[str, List[str]]
query: dict[str, list[str]]
subdirectory: str
hashes: Dict[str, str]
hashes: dict[str, str]
def _clean_link(link: Link) -> _CleanResult:
parsed = link._parsed_url
netloc = parsed.netloc.rsplit("@", 1)[-1]
netloc = parsed.netloc.rsplit('@', 1)[-1]
# According to RFC 8089, an empty host in file: means localhost.
if parsed.scheme == "file" and not netloc:
netloc = "localhost"
if parsed.scheme == 'file' and not netloc:
netloc = 'localhost'
fragment = urllib.parse.parse_qs(parsed.fragment)
if "egg" in fragment:
logger.debug("Ignoring egg= fragment in %s", link)
if 'egg' in fragment:
logger.debug('Ignoring egg= fragment in %s', link)
try:
# If there are multiple subdirectory values, use the first one.
# This matches the behavior of Link.subdirectory_fragment.
subdirectory = fragment["subdirectory"][0]
subdirectory = fragment['subdirectory'][0]
except (IndexError, KeyError):
subdirectory = ""
subdirectory = ''
# If there are multiple hash values under the same algorithm, use the
# first one. This matches the behavior of Link.hash_value.
hashes = {k: fragment[k][0] for k in _SUPPORTED_HASHES if k in fragment}
return _CleanResult(
parsed=parsed._replace(netloc=netloc, query="", fragment=""),
parsed=parsed._replace(netloc=netloc, query='', fragment=''),
query=urllib.parse.parse_qs(parsed.query),
subdirectory=subdirectory,
hashes=hashes,

View file

@ -4,9 +4,10 @@ For types associated with installation schemes.
For a general overview of available schemes and their context, see
https://docs.python.org/3/install/index.html#alternate-installation.
"""
from __future__ import annotations
SCHEME_KEYS = ["platlib", "purelib", "headers", "scripts", "data"]
SCHEME_KEYS = ['platlib', 'purelib', 'headers', 'scripts', 'data']
class Scheme:

View file

@ -1,3 +1,5 @@
from __future__ import annotations
import itertools
import logging
import os
@ -5,11 +7,11 @@ import posixpath
import urllib.parse
from typing import List
from pip._vendor.packaging.utils import canonicalize_name
from pip._internal.models.index import PyPI
from pip._internal.utils.compat import has_tls
from pip._internal.utils.misc import normalize_path, redact_auth_from_url
from pip._internal.utils.misc import normalize_path
from pip._internal.utils.misc import redact_auth_from_url
from pip._vendor.packaging.utils import canonicalize_name
logger = logging.getLogger(__name__)
@ -20,14 +22,14 @@ class SearchScope:
Encapsulates the locations that pip is configured to search.
"""
__slots__ = ["find_links", "index_urls"]
__slots__ = ['find_links', 'index_urls']
@classmethod
def create(
cls,
find_links: List[str],
index_urls: List[str],
) -> "SearchScope":
find_links: list[str],
index_urls: list[str],
) -> SearchScope:
"""
Create a SearchScope object after normalizing the `find_links`.
"""
@ -36,9 +38,9 @@ class SearchScope:
# it and if it exists, use the normalized version.
# This is deliberately conservative - it might be fine just to
# blindly normalize anything starting with a ~...
built_find_links: List[str] = []
built_find_links: list[str] = []
for link in find_links:
if link.startswith("~"):
if link.startswith('~'):
new_link = normalize_path(link)
if os.path.exists(new_link):
link = new_link
@ -49,11 +51,11 @@ class SearchScope:
if not has_tls():
for link in itertools.chain(index_urls, built_find_links):
parsed = urllib.parse.urlparse(link)
if parsed.scheme == "https":
if parsed.scheme == 'https':
logger.warning(
"pip is configured with locations that require "
"TLS/SSL, however the ssl module in Python is not "
"available."
'pip is configured with locations that require '
'TLS/SSL, however the ssl module in Python is not '
'available.',
)
break
@ -64,8 +66,8 @@ class SearchScope:
def __init__(
self,
find_links: List[str],
index_urls: List[str],
find_links: list[str],
index_urls: list[str],
) -> None:
self.find_links = find_links
self.index_urls = index_urls
@ -95,18 +97,18 @@ class SearchScope:
redacted_index_urls.append(redacted_index_url)
lines.append(
"Looking in indexes: {}".format(", ".join(redacted_index_urls))
'Looking in indexes: {}'.format(', '.join(redacted_index_urls)),
)
if self.find_links:
lines.append(
"Looking in links: {}".format(
", ".join(redact_auth_from_url(url) for url in self.find_links)
)
'Looking in links: {}'.format(
', '.join(redact_auth_from_url(url) for url in self.find_links),
),
)
return "\n".join(lines)
return '\n'.join(lines)
def get_index_urls_locations(self, project_name: str) -> List[str]:
def get_index_urls_locations(self, project_name: str) -> list[str]:
"""Returns the locations found via self.index_urls
Checks the url_name on the main (first in the list) index and
@ -115,15 +117,15 @@ class SearchScope:
def mkurl_pypi_url(url: str) -> str:
loc = posixpath.join(
url, urllib.parse.quote(canonicalize_name(project_name))
url, urllib.parse.quote(canonicalize_name(project_name)),
)
# For maximum compatibility with easy_install, ensure the path
# ends in a trailing slash. Although this isn't in the spec
# (and PyPI can handle it without the slash) some other index
# implementations might break if they relied on easy_install's
# behavior.
if not loc.endswith("/"):
loc = loc + "/"
if not loc.endswith('/'):
loc = loc + '/'
return loc
return [mkurl_pypi_url(url) for url in self.index_urls]

View file

@ -1,3 +1,5 @@
from __future__ import annotations
from typing import Optional
from pip._internal.models.format_control import FormatControl
@ -10,11 +12,11 @@ class SelectionPreferences:
"""
__slots__ = [
"allow_yanked",
"allow_all_prereleases",
"format_control",
"prefer_binary",
"ignore_requires_python",
'allow_yanked',
'allow_all_prereleases',
'format_control',
'prefer_binary',
'ignore_requires_python',
]
# Don't include an allow_yanked default value to make sure each call
@ -25,9 +27,9 @@ class SelectionPreferences:
self,
allow_yanked: bool,
allow_all_prereleases: bool = False,
format_control: Optional[FormatControl] = None,
format_control: FormatControl | None = None,
prefer_binary: bool = False,
ignore_requires_python: Optional[bool] = None,
ignore_requires_python: bool | None = None,
) -> None:
"""Create a SelectionPreferences object.

View file

@ -1,10 +1,14 @@
from __future__ import annotations
import sys
from typing import List, Optional, Tuple
from typing import List
from typing import Optional
from typing import Tuple
from pip._vendor.packaging.tags import Tag
from pip._internal.utils.compatibility_tags import get_supported, version_info_to_nodot
from pip._internal.utils.compatibility_tags import get_supported
from pip._internal.utils.compatibility_tags import version_info_to_nodot
from pip._internal.utils.misc import normalize_version_info
from pip._vendor.packaging.tags import Tag
class TargetPython:
@ -15,21 +19,21 @@ class TargetPython:
"""
__slots__ = [
"_given_py_version_info",
"abis",
"implementation",
"platforms",
"py_version",
"py_version_info",
"_valid_tags",
'_given_py_version_info',
'abis',
'implementation',
'platforms',
'py_version',
'py_version_info',
'_valid_tags',
]
def __init__(
self,
platforms: Optional[List[str]] = None,
py_version_info: Optional[Tuple[int, ...]] = None,
abis: Optional[List[str]] = None,
implementation: Optional[str] = None,
platforms: list[str] | None = None,
py_version_info: tuple[int, ...] | None = None,
abis: list[str] | None = None,
implementation: str | None = None,
) -> None:
"""
:param platforms: A list of strings or None. If None, searches for
@ -53,7 +57,7 @@ class TargetPython:
else:
py_version_info = normalize_version_info(py_version_info)
py_version = ".".join(map(str, py_version_info[:2]))
py_version = '.'.join(map(str, py_version_info[:2]))
self.abis = abis
self.implementation = implementation
@ -62,7 +66,7 @@ class TargetPython:
self.py_version_info = py_version_info
# This is used to cache the return value of get_tags().
self._valid_tags: Optional[List[Tag]] = None
self._valid_tags: list[Tag] | None = None
def format_given(self) -> str:
"""
@ -70,21 +74,21 @@ class TargetPython:
"""
display_version = None
if self._given_py_version_info is not None:
display_version = ".".join(
display_version = '.'.join(
str(part) for part in self._given_py_version_info
)
key_values = [
("platforms", self.platforms),
("version_info", display_version),
("abis", self.abis),
("implementation", self.implementation),
('platforms', self.platforms),
('version_info', display_version),
('abis', self.abis),
('implementation', self.implementation),
]
return " ".join(
f"{key}={value!r}" for key, value in key_values if value is not None
return ' '.join(
f'{key}={value!r}' for key, value in key_values if value is not None
)
def get_tags(self) -> List[Tag]:
def get_tags(self) -> list[Tag]:
"""
Return the supported PEP 425 tags to check wheel candidates against.

View file

@ -1,12 +1,15 @@
"""Represents a wheel file and provides access to the various parts of the
name that have meaning.
"""
import re
from typing import Dict, Iterable, List
from __future__ import annotations
from pip._vendor.packaging.tags import Tag
import re
from typing import Dict
from typing import Iterable
from typing import List
from pip._internal.exceptions import InvalidWheelFilename
from pip._vendor.packaging.tags import Tag
class Wheel:
@ -25,27 +28,27 @@ class Wheel:
"""
wheel_info = self.wheel_file_re.match(filename)
if not wheel_info:
raise InvalidWheelFilename(f"{filename} is not a valid wheel filename.")
raise InvalidWheelFilename(f'{filename} is not a valid wheel filename.')
self.filename = filename
self.name = wheel_info.group("name").replace("_", "-")
self.name = wheel_info.group('name').replace('_', '-')
# we'll assume "_" means "-" due to wheel naming scheme
# (https://github.com/pypa/pip/issues/1150)
self.version = wheel_info.group("ver").replace("_", "-")
self.build_tag = wheel_info.group("build")
self.pyversions = wheel_info.group("pyver").split(".")
self.abis = wheel_info.group("abi").split(".")
self.plats = wheel_info.group("plat").split(".")
self.version = wheel_info.group('ver').replace('_', '-')
self.build_tag = wheel_info.group('build')
self.pyversions = wheel_info.group('pyver').split('.')
self.abis = wheel_info.group('abi').split('.')
self.plats = wheel_info.group('plat').split('.')
# All the tag combinations from this file
self.file_tags = {
Tag(x, y, z) for x in self.pyversions for y in self.abis for z in self.plats
}
def get_formatted_file_tags(self) -> List[str]:
def get_formatted_file_tags(self) -> list[str]:
"""Return the wheel's tags as a sorted list of strings."""
return sorted(str(tag) for tag in self.file_tags)
def support_index_min(self, tags: List[Tag]) -> int:
def support_index_min(self, tags: list[Tag]) -> int:
"""Return the lowest index that one of the wheel's file_tag combinations
achieves in the given list of supported tags.
@ -61,7 +64,7 @@ class Wheel:
return min(tags.index(tag) for tag in self.file_tags if tag in tags)
def find_most_preferred_tag(
self, tags: List[Tag], tag_to_priority: Dict[Tag, int]
self, tags: list[Tag], tag_to_priority: dict[Tag, int],
) -> int:
"""Return the priority of the most preferred tag that one of the wheel's file
tag combinations achieves in the given list of supported tags using the given

View file

@ -1,2 +1,3 @@
"""Contains purely network-related utilities.
"""
from __future__ import annotations

View file

@ -3,23 +3,27 @@
Contains interface (MultiDomainBasicAuth) and associated glue code for
providing credentials in the context of network requests.
"""
from __future__ import annotations
import urllib.parse
from typing import Any, Dict, List, Optional, Tuple
from pip._vendor.requests.auth import AuthBase, HTTPBasicAuth
from pip._vendor.requests.models import Request, Response
from pip._vendor.requests.utils import get_netrc_auth
from typing import Any
from typing import Dict
from typing import List
from typing import Optional
from typing import Tuple
from pip._internal.utils.logging import getLogger
from pip._internal.utils.misc import (
ask,
ask_input,
ask_password,
remove_auth_from_url,
split_auth_netloc_from_url,
)
from pip._internal.utils.misc import ask
from pip._internal.utils.misc import ask_input
from pip._internal.utils.misc import ask_password
from pip._internal.utils.misc import remove_auth_from_url
from pip._internal.utils.misc import split_auth_netloc_from_url
from pip._internal.vcs.versioncontrol import AuthInfo
from pip._vendor.requests.auth import AuthBase
from pip._vendor.requests.auth import HTTPBasicAuth
from pip._vendor.requests.models import Request
from pip._vendor.requests.models import Response
from pip._vendor.requests.utils import get_netrc_auth
logger = getLogger(__name__)
@ -31,13 +35,13 @@ except ImportError:
keyring = None # type: ignore[assignment]
except Exception as exc:
logger.warning(
"Keyring is skipped due to an exception: %s",
'Keyring is skipped due to an exception: %s',
str(exc),
)
keyring = None # type: ignore[assignment]
def get_keyring_auth(url: Optional[str], username: Optional[str]) -> Optional[AuthInfo]:
def get_keyring_auth(url: str | None, username: str | None) -> AuthInfo | None:
"""Return the tuple auth for a given url from keyring."""
global keyring
if not url or not keyring:
@ -49,21 +53,21 @@ def get_keyring_auth(url: Optional[str], username: Optional[str]) -> Optional[Au
except AttributeError:
pass
else:
logger.debug("Getting credentials from keyring for %s", url)
logger.debug('Getting credentials from keyring for %s', url)
cred = get_credential(url, username)
if cred is not None:
return cred.username, cred.password
return None
if username:
logger.debug("Getting password from keyring for %s", url)
logger.debug('Getting password from keyring for %s', url)
password = keyring.get_password(url, username)
if password:
return username, password
except Exception as exc:
logger.warning(
"Keyring is skipped due to an exception: %s",
'Keyring is skipped due to an exception: %s',
str(exc),
)
keyring = None # type: ignore[assignment]
@ -72,19 +76,19 @@ def get_keyring_auth(url: Optional[str], username: Optional[str]) -> Optional[Au
class MultiDomainBasicAuth(AuthBase):
def __init__(
self, prompting: bool = True, index_urls: Optional[List[str]] = None
self, prompting: bool = True, index_urls: list[str] | None = None,
) -> None:
self.prompting = prompting
self.index_urls = index_urls
self.passwords: Dict[str, AuthInfo] = {}
self.passwords: dict[str, AuthInfo] = {}
# When the user is prompted to enter credentials and keyring is
# available, we will offer to save them. If the user accepts,
# this value is set to the credentials they entered. After the
# request authenticates, the caller should call
# ``save_credentials`` to save these.
self._credentials_to_save: Optional[Credentials] = None
self._credentials_to_save: Credentials | None = None
def _get_index_url(self, url: str) -> Optional[str]:
def _get_index_url(self, url: str) -> str | None:
"""Return the original index URL matching the requested URL.
Cached or dynamically generated credentials may work against
@ -101,7 +105,7 @@ class MultiDomainBasicAuth(AuthBase):
return None
for u in self.index_urls:
prefix = remove_auth_from_url(u).rstrip("/") + "/"
prefix = remove_auth_from_url(u).rstrip('/') + '/'
if url.startswith(prefix):
return u
return None
@ -121,7 +125,7 @@ class MultiDomainBasicAuth(AuthBase):
# Start with the credentials embedded in the url
username, password = url_user_password
if username is not None and password is not None:
logger.debug("Found credentials in url for %s", netloc)
logger.debug('Found credentials in url for %s', netloc)
return url_user_password
# Find a matching index url for this request
@ -131,20 +135,20 @@ class MultiDomainBasicAuth(AuthBase):
index_info = split_auth_netloc_from_url(index_url)
if index_info:
index_url, _, index_url_user_password = index_info
logger.debug("Found index url %s", index_url)
logger.debug('Found index url %s', index_url)
# If an index URL was found, try its embedded credentials
if index_url and index_url_user_password[0] is not None:
username, password = index_url_user_password
if username is not None and password is not None:
logger.debug("Found credentials in index url for %s", netloc)
logger.debug('Found credentials in index url for %s', netloc)
return index_url_user_password
# Get creds from netrc if we still don't have them
if allow_netrc:
netrc_auth = get_netrc_auth(original_url)
if netrc_auth:
logger.debug("Found credentials in netrc for %s", netloc)
logger.debug('Found credentials in netrc for %s', netloc)
return netrc_auth
# If we don't have a password and keyring is available, use it.
@ -157,14 +161,14 @@ class MultiDomainBasicAuth(AuthBase):
)
# fmt: on
if kr_auth:
logger.debug("Found credentials in keyring for %s", netloc)
logger.debug('Found credentials in keyring for %s', netloc)
return kr_auth
return username, password
def _get_url_and_credentials(
self, original_url: str
) -> Tuple[str, Optional[str], Optional[str]]:
self, original_url: str,
) -> tuple[str, str | None, str | None]:
"""Return the credentials to use for the provided URL.
If allowed, netrc and keyring may be used to obtain the
@ -195,18 +199,18 @@ class MultiDomainBasicAuth(AuthBase):
# this netloc will show up as "cached" in the conditional above.
# Further, HTTPBasicAuth doesn't accept None, so it makes sense to
# cache the value that is going to be used.
username = username or ""
password = password or ""
username = username or ''
password = password or ''
# Store any acquired credentials.
self.passwords[netloc] = (username, password)
assert (
# Credentials were found
(username is not None and password is not None)
(username is not None and password is not None) or
# Credentials were not found
or (username is None and password is None)
), f"Could not load credentials from url: {original_url}"
(username is None and password is None)
), f'Could not load credentials from url: {original_url}'
return url, username, password
@ -222,28 +226,28 @@ class MultiDomainBasicAuth(AuthBase):
req = HTTPBasicAuth(username, password)(req)
# Attach a hook to handle 401 responses
req.register_hook("response", self.handle_401)
req.register_hook('response', self.handle_401)
return req
# Factored out to allow for easy patching in tests
def _prompt_for_password(
self, netloc: str
) -> Tuple[Optional[str], Optional[str], bool]:
username = ask_input(f"User for {netloc}: ")
self, netloc: str,
) -> tuple[str | None, str | None, bool]:
username = ask_input(f'User for {netloc}: ')
if not username:
return None, None, False
auth = get_keyring_auth(netloc, username)
if auth and auth[0] is not None and auth[1] is not None:
return auth[0], auth[1], False
password = ask_password("Password: ")
password = ask_password('Password: ')
return username, password, True
# Factored out to allow for easy patching in tests
def _should_save_password_to_keyring(self) -> bool:
if not keyring:
return False
return ask("Save credentials to keyring [y/N]: ", ["y", "n"]) == "y"
return ask('Save credentials to keyring [y/N]: ', ['y', 'n']) == 'y'
def handle_401(self, resp: Response, **kwargs: Any) -> Response:
# We only care about 401 responses, anything else we want to just
@ -284,14 +288,14 @@ class MultiDomainBasicAuth(AuthBase):
resp.raw.release_conn()
# Add our new username and password to the request
req = HTTPBasicAuth(username or "", password or "")(resp.request)
req.register_hook("response", self.warn_on_401)
req = HTTPBasicAuth(username or '', password or '')(resp.request)
req.register_hook('response', self.warn_on_401)
# On successful request, save the credentials that were used to
# keyring. (Note that if the user responded "no" above, this member
# is not set and nothing will be saved.)
if self._credentials_to_save:
req.register_hook("response", self.save_credentials)
req.register_hook('response', self.save_credentials)
# Send our new request
new_resp = resp.connection.send(req, **kwargs)
@ -303,13 +307,13 @@ class MultiDomainBasicAuth(AuthBase):
"""Response callback to warn about incorrect credentials."""
if resp.status_code == 401:
logger.warning(
"401 Error, Credentials not correct for %s",
'401 Error, Credentials not correct for %s',
resp.request.url,
)
def save_credentials(self, resp: Response, **kwargs: Any) -> None:
"""Response callback to save credentials on success."""
assert keyring is not None, "should never reach here without keyring"
assert keyring is not None, 'should never reach here without keyring'
if not keyring:
return
@ -317,7 +321,7 @@ class MultiDomainBasicAuth(AuthBase):
self._credentials_to_save = None
if creds and resp.status_code < 400:
try:
logger.info("Saving credentials to keyring")
logger.info('Saving credentials to keyring')
keyring.set_password(*creds)
except Exception:
logger.exception("Failed to save credentials")
logger.exception('Failed to save credentials')

View file

@ -1,20 +1,22 @@
"""HTTP cache implementation.
"""
from __future__ import annotations
import os
from contextlib import contextmanager
from typing import Iterator, Optional
from typing import Iterator
from typing import Optional
from pip._internal.utils.filesystem import adjacent_tmp_file
from pip._internal.utils.filesystem import replace
from pip._internal.utils.misc import ensure_dir
from pip._vendor.cachecontrol.cache import BaseCache
from pip._vendor.cachecontrol.caches import FileCache
from pip._vendor.requests.models import Response
from pip._internal.utils.filesystem import adjacent_tmp_file, replace
from pip._internal.utils.misc import ensure_dir
def is_from_cache(response: Response) -> bool:
return getattr(response, "from_cache", False)
return getattr(response, 'from_cache', False)
@contextmanager
@ -35,7 +37,7 @@ class SafeFileCache(BaseCache):
"""
def __init__(self, directory: str) -> None:
assert directory is not None, "Cache directory must not be None."
assert directory is not None, 'Cache directory must not be None.'
super().__init__()
self.directory = directory
@ -47,13 +49,13 @@ class SafeFileCache(BaseCache):
parts = list(hashed[:5]) + [hashed]
return os.path.join(self.directory, *parts)
def get(self, key: str) -> Optional[bytes]:
def get(self, key: str) -> bytes | None:
path = self._get_cache_path(key)
with suppressed_cache_errors():
with open(path, "rb") as f:
with open(path, 'rb') as f:
return f.read()
def set(self, key: str, value: bytes, expires: Optional[int] = None) -> None:
def set(self, key: str, value: bytes, expires: int | None = None) -> None:
path = self._get_cache_path(key)
with suppressed_cache_errors():
ensure_dir(os.path.dirname(path))

View file

@ -1,12 +1,14 @@
"""Download files with progress indicators.
"""
from __future__ import annotations
import cgi
import logging
import mimetypes
import os
from typing import Iterable, Optional, Tuple
from pip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response
from typing import Iterable
from typing import Optional
from typing import Tuple
from pip._internal.cli.progress_bars import get_download_progress_renderer
from pip._internal.exceptions import NetworkConnectionError
@ -14,15 +16,21 @@ from pip._internal.models.index import PyPI
from pip._internal.models.link import Link
from pip._internal.network.cache import is_from_cache
from pip._internal.network.session import PipSession
from pip._internal.network.utils import HEADERS, raise_for_status, response_chunks
from pip._internal.utils.misc import format_size, redact_auth_from_url, splitext
from pip._internal.network.utils import HEADERS
from pip._internal.network.utils import raise_for_status
from pip._internal.network.utils import response_chunks
from pip._internal.utils.misc import format_size
from pip._internal.utils.misc import redact_auth_from_url
from pip._internal.utils.misc import splitext
from pip._vendor.requests.models import CONTENT_CHUNK_SIZE
from pip._vendor.requests.models import Response
logger = logging.getLogger(__name__)
def _get_http_response_size(resp: Response) -> Optional[int]:
def _get_http_response_size(resp: Response) -> int | None:
try:
return int(resp.headers["content-length"])
return int(resp.headers['content-length'])
except (ValueError, KeyError, TypeError):
return None
@ -42,12 +50,12 @@ def _prepare_download(
logged_url = redact_auth_from_url(url)
if total_length:
logged_url = "{} ({})".format(logged_url, format_size(total_length))
logged_url = f'{logged_url} ({format_size(total_length)})'
if is_from_cache(resp):
logger.info("Using cached %s", logged_url)
logger.info('Using cached %s', logged_url)
else:
logger.info("Downloading %s", logged_url)
logger.info('Downloading %s', logged_url)
if logger.getEffectiveLevel() > logging.INFO:
show_progress = False
@ -82,7 +90,7 @@ def parse_content_disposition(content_disposition: str, default_filename: str) -
return the default filename if the result is empty.
"""
_type, params = cgi.parse_header(content_disposition)
filename = params.get("filename")
filename = params.get('filename')
if filename:
# We need to sanitize the filename to prevent directory traversal
# in case the filename contains ".." path parts.
@ -96,12 +104,12 @@ def _get_http_response_filename(resp: Response, link: Link) -> str:
"""
filename = link.filename # fallback
# Have a look at the Content-Disposition header for a better guess
content_disposition = resp.headers.get("content-disposition")
content_disposition = resp.headers.get('content-disposition')
if content_disposition:
filename = parse_content_disposition(content_disposition, filename)
ext: Optional[str] = splitext(filename)[1]
ext: str | None = splitext(filename)[1]
if not ext:
ext = mimetypes.guess_extension(resp.headers.get("content-type", ""))
ext = mimetypes.guess_extension(resp.headers.get('content-type', ''))
if ext:
filename += ext
if not ext and link.url != resp.url:
@ -112,7 +120,7 @@ def _get_http_response_filename(resp: Response, link: Link) -> str:
def _http_get_download(session: PipSession, link: Link) -> Response:
target_url = link.url.split("#", 1)[0]
target_url = link.url.split('#', 1)[0]
resp = session.get(target_url, headers=HEADERS, stream=True)
raise_for_status(resp)
return resp
@ -127,14 +135,14 @@ class Downloader:
self._session = session
self._progress_bar = progress_bar
def __call__(self, link: Link, location: str) -> Tuple[str, str]:
def __call__(self, link: Link, location: str) -> tuple[str, str]:
"""Download the file given by link into location."""
try:
resp = _http_get_download(self._session, link)
except NetworkConnectionError as e:
assert e.response is not None
logger.critical(
"HTTP error %s while getting %s", e.response.status_code, link
'HTTP error %s while getting %s', e.response.status_code, link,
)
raise
@ -142,10 +150,10 @@ class Downloader:
filepath = os.path.join(location, filename)
chunks = _prepare_download(resp, link, self._progress_bar)
with open(filepath, "wb") as content_file:
with open(filepath, 'wb') as content_file:
for chunk in chunks:
content_file.write(chunk)
content_type = resp.headers.get("Content-Type", "")
content_type = resp.headers.get('Content-Type', '')
return filepath, content_type
@ -159,8 +167,8 @@ class BatchDownloader:
self._progress_bar = progress_bar
def __call__(
self, links: Iterable[Link], location: str
) -> Iterable[Tuple[Link, Tuple[str, str]]]:
self, links: Iterable[Link], location: str,
) -> Iterable[tuple[Link, tuple[str, str]]]:
"""Download the files given by links into location."""
for link in links:
try:
@ -168,7 +176,7 @@ class BatchDownloader:
except NetworkConnectionError as e:
assert e.response is not None
logger.critical(
"HTTP error %s while getting %s",
'HTTP error %s while getting %s',
e.response.status_code,
link,
)
@ -178,8 +186,8 @@ class BatchDownloader:
filepath = os.path.join(location, filename)
chunks = _prepare_download(resp, link, self._progress_bar)
with open(filepath, "wb") as content_file:
with open(filepath, 'wb') as content_file:
for chunk in chunks:
content_file.write(chunk)
content_type = resp.headers.get("Content-Type", "")
content_type = resp.headers.get('Content-Type', '')
yield link, (filepath, content_type)

View file

@ -1,6 +1,7 @@
"""Lazy ZIP over HTTP"""
from __future__ import annotations
__all__ = ["HTTPRangeRequestUnsupported", "dist_from_wheel_url"]
__all__ = ['HTTPRangeRequestUnsupported', 'dist_from_wheel_url']
from bisect import bisect_left, bisect_right
from contextlib import contextmanager
@ -47,25 +48,25 @@ class LazyZipOverHTTP:
"""
def __init__(
self, url: str, session: PipSession, chunk_size: int = CONTENT_CHUNK_SIZE
self, url: str, session: PipSession, chunk_size: int = CONTENT_CHUNK_SIZE,
) -> None:
head = session.head(url, headers=HEADERS)
raise_for_status(head)
assert head.status_code == 200
self._session, self._url, self._chunk_size = session, url, chunk_size
self._length = int(head.headers["Content-Length"])
self._length = int(head.headers['Content-Length'])
self._file = NamedTemporaryFile()
self.truncate(self._length)
self._left: List[int] = []
self._right: List[int] = []
if "bytes" not in head.headers.get("Accept-Ranges", "none"):
raise HTTPRangeRequestUnsupported("range request is not supported")
self._left: list[int] = []
self._right: list[int] = []
if 'bytes' not in head.headers.get('Accept-Ranges', 'none'):
raise HTTPRangeRequestUnsupported('range request is not supported')
self._check_zip()
@property
def mode(self) -> str:
"""Opening mode, which is always rb."""
return "rb"
return 'rb'
@property
def name(self) -> str:
@ -117,7 +118,7 @@ class LazyZipOverHTTP:
"""Return the current position."""
return self._file.tell()
def truncate(self, size: Optional[int] = None) -> int:
def truncate(self, size: int | None = None) -> int:
"""Resize the stream to the given size in bytes.
If size is unspecified resize to the current position.
@ -131,11 +132,11 @@ class LazyZipOverHTTP:
"""Return False."""
return False
def __enter__(self) -> "LazyZipOverHTTP":
def __enter__(self) -> LazyZipOverHTTP:
self._file.__enter__()
return self
def __exit__(self, *exc: Any) -> Optional[bool]:
def __exit__(self, *exc: Any) -> bool | None:
return self._file.__exit__(*exc)
@contextmanager
@ -166,18 +167,18 @@ class LazyZipOverHTTP:
break
def _stream_response(
self, start: int, end: int, base_headers: Dict[str, str] = HEADERS
self, start: int, end: int, base_headers: dict[str, str] = HEADERS,
) -> Response:
"""Return HTTP response to a range request from start to end."""
headers = base_headers.copy()
headers["Range"] = f"bytes={start}-{end}"
headers['Range'] = f'bytes={start}-{end}'
# TODO: Get range requests to be correctly cached
headers["Cache-Control"] = "no-cache"
headers['Cache-Control'] = 'no-cache'
return self._session.get(self._url, headers=headers, stream=True)
def _merge(
self, start: int, end: int, left: int, right: int
) -> Iterator[Tuple[int, int]]:
self, start: int, end: int, left: int, right: int,
) -> Iterator[tuple[int, int]]:
"""Return an iterator of intervals to be fetched.
Args:

View file

@ -1,6 +1,7 @@
"""PipSession and supporting code, containing all pip-specific
network request configuration and behavior.
"""
from __future__ import annotations
import email.utils
import io
@ -15,27 +16,37 @@ import subprocess
import sys
import urllib.parse
import warnings
from typing import Any, Dict, Iterator, List, Mapping, Optional, Sequence, Tuple, Union
from pip._vendor import requests, urllib3
from pip._vendor.cachecontrol import CacheControlAdapter
from pip._vendor.requests.adapters import BaseAdapter, HTTPAdapter
from pip._vendor.requests.models import PreparedRequest, Response
from pip._vendor.requests.structures import CaseInsensitiveDict
from pip._vendor.urllib3.connectionpool import ConnectionPool
from pip._vendor.urllib3.exceptions import InsecureRequestWarning
from typing import Any
from typing import Dict
from typing import Iterator
from typing import List
from typing import Mapping
from typing import Optional
from typing import Sequence
from typing import Tuple
from typing import Union
from pip import __version__
from pip._internal.metadata import get_default_environment
from pip._internal.models.link import Link
from pip._internal.network.auth import MultiDomainBasicAuth
from pip._internal.network.cache import SafeFileCache
# Import ssl from compat so the initial import occurs in only one place.
from pip._internal.utils.compat import has_tls
from pip._internal.utils.glibc import libc_ver
from pip._internal.utils.misc import build_url_from_netloc, parse_netloc
from pip._internal.utils.misc import build_url_from_netloc
from pip._internal.utils.misc import parse_netloc
from pip._internal.utils.urls import url_to_path
from pip._vendor import requests
from pip._vendor import urllib3
from pip._vendor.cachecontrol import CacheControlAdapter
from pip._vendor.requests.adapters import BaseAdapter
from pip._vendor.requests.adapters import HTTPAdapter
from pip._vendor.requests.models import PreparedRequest
from pip._vendor.requests.models import Response
from pip._vendor.requests.structures import CaseInsensitiveDict
from pip._vendor.urllib3.connectionpool import ConnectionPool
from pip._vendor.urllib3.exceptions import InsecureRequestWarning
# Import ssl from compat so the initial import occurs in only one place.
logger = logging.getLogger(__name__)
@ -43,19 +54,19 @@ SecureOrigin = Tuple[str, str, Optional[Union[int, str]]]
# Ignore warning raised when using --trusted-host.
warnings.filterwarnings("ignore", category=InsecureRequestWarning)
warnings.filterwarnings('ignore', category=InsecureRequestWarning)
SECURE_ORIGINS: List[SecureOrigin] = [
SECURE_ORIGINS: list[SecureOrigin] = [
# protocol, hostname, port
# Taken from Chrome's list of secure origins (See: http://bit.ly/1qrySKC)
("https", "*", "*"),
("*", "localhost", "*"),
("*", "127.0.0.0/8", "*"),
("*", "::1/128", "*"),
("file", "*", None),
('https', '*', '*'),
('*', 'localhost', '*'),
('*', '127.0.0.0/8', '*'),
('*', '::1/128', '*'),
('file', '*', None),
# ssh is always secure.
("ssh", "*", "*"),
('ssh', '*', '*'),
]
@ -68,13 +79,13 @@ SECURE_ORIGINS: List[SecureOrigin] = [
# For more background, see: https://github.com/pypa/pip/issues/5499
CI_ENVIRONMENT_VARIABLES = (
# Azure Pipelines
"BUILD_BUILDID",
'BUILD_BUILDID',
# Jenkins
"BUILD_ID",
'BUILD_ID',
# AppVeyor, CircleCI, Codeship, Gitlab CI, Shippable, Travis CI
"CI",
'CI',
# Explicit environment variable.
"PIP_IS_CI",
'PIP_IS_CI',
)
@ -92,100 +103,100 @@ def user_agent() -> str:
"""
Return a string representing the user agent.
"""
data: Dict[str, Any] = {
"installer": {"name": "pip", "version": __version__},
"python": platform.python_version(),
"implementation": {
"name": platform.python_implementation(),
data: dict[str, Any] = {
'installer': {'name': 'pip', 'version': __version__},
'python': platform.python_version(),
'implementation': {
'name': platform.python_implementation(),
},
}
if data["implementation"]["name"] == "CPython":
data["implementation"]["version"] = platform.python_version()
elif data["implementation"]["name"] == "PyPy":
if data['implementation']['name'] == 'CPython':
data['implementation']['version'] = platform.python_version()
elif data['implementation']['name'] == 'PyPy':
pypy_version_info = sys.pypy_version_info # type: ignore
if pypy_version_info.releaselevel == "final":
if pypy_version_info.releaselevel == 'final':
pypy_version_info = pypy_version_info[:3]
data["implementation"]["version"] = ".".join(
[str(x) for x in pypy_version_info]
data['implementation']['version'] = '.'.join(
[str(x) for x in pypy_version_info],
)
elif data["implementation"]["name"] == "Jython":
elif data['implementation']['name'] == 'Jython':
# Complete Guess
data["implementation"]["version"] = platform.python_version()
elif data["implementation"]["name"] == "IronPython":
data['implementation']['version'] = platform.python_version()
elif data['implementation']['name'] == 'IronPython':
# Complete Guess
data["implementation"]["version"] = platform.python_version()
data['implementation']['version'] = platform.python_version()
if sys.platform.startswith("linux"):
if sys.platform.startswith('linux'):
from pip._vendor import distro
linux_distribution = distro.name(), distro.version(), distro.codename()
distro_infos: Dict[str, Any] = dict(
distro_infos: dict[str, Any] = dict(
filter(
lambda x: x[1],
zip(["name", "version", "id"], linux_distribution),
)
zip(['name', 'version', 'id'], linux_distribution),
),
)
libc = dict(
filter(
lambda x: x[1],
zip(["lib", "version"], libc_ver()),
)
zip(['lib', 'version'], libc_ver()),
),
)
if libc:
distro_infos["libc"] = libc
distro_infos['libc'] = libc
if distro_infos:
data["distro"] = distro_infos
data['distro'] = distro_infos
if sys.platform.startswith("darwin") and platform.mac_ver()[0]:
data["distro"] = {"name": "macOS", "version": platform.mac_ver()[0]}
if sys.platform.startswith('darwin') and platform.mac_ver()[0]:
data['distro'] = {'name': 'macOS', 'version': platform.mac_ver()[0]}
if platform.system():
data.setdefault("system", {})["name"] = platform.system()
data.setdefault('system', {})['name'] = platform.system()
if platform.release():
data.setdefault("system", {})["release"] = platform.release()
data.setdefault('system', {})['release'] = platform.release()
if platform.machine():
data["cpu"] = platform.machine()
data['cpu'] = platform.machine()
if has_tls():
import _ssl as ssl
data["openssl_version"] = ssl.OPENSSL_VERSION
data['openssl_version'] = ssl.OPENSSL_VERSION
setuptools_dist = get_default_environment().get_distribution("setuptools")
setuptools_dist = get_default_environment().get_distribution('setuptools')
if setuptools_dist is not None:
data["setuptools_version"] = str(setuptools_dist.version)
data['setuptools_version'] = str(setuptools_dist.version)
if shutil.which("rustc") is not None:
if shutil.which('rustc') is not None:
# If for any reason `rustc --version` fails, silently ignore it
try:
rustc_output = subprocess.check_output(
["rustc", "--version"], stderr=subprocess.STDOUT, timeout=0.5
['rustc', '--version'], stderr=subprocess.STDOUT, timeout=0.5,
)
except Exception:
pass
else:
if rustc_output.startswith(b"rustc "):
if rustc_output.startswith(b'rustc '):
# The format of `rustc --version` is:
# `b'rustc 1.52.1 (9bc8c42bb 2021-05-09)\n'`
# We extract just the middle (1.52.1) part
data["rustc_version"] = rustc_output.split(b" ")[1].decode()
data['rustc_version'] = rustc_output.split(b' ')[1].decode()
# Use None rather than False so as not to give the impression that
# pip knows it is not being run under CI. Rather, it is a null or
# inconclusive result. Also, we include some value rather than no
# value to make it easier to know that the check has been run.
data["ci"] = True if looks_like_ci() else None
data['ci'] = True if looks_like_ci() else None
user_data = os.environ.get("PIP_USER_AGENT_USER_DATA")
user_data = os.environ.get('PIP_USER_AGENT_USER_DATA')
if user_data is not None:
data["user_data"] = user_data
data['user_data'] = user_data
return "{data[installer][name]}/{data[installer][version]} {json}".format(
return '{data[installer][name]}/{data[installer][version]} {json}'.format(
data=data,
json=json.dumps(data, separators=(",", ":"), sort_keys=True),
json=json.dumps(data, separators=(',', ':'), sort_keys=True),
)
@ -194,10 +205,10 @@ class LocalFSAdapter(BaseAdapter):
self,
request: PreparedRequest,
stream: bool = False,
timeout: Optional[Union[float, Tuple[float, float]]] = None,
verify: Union[bool, str] = True,
cert: Optional[Union[str, Tuple[str, str]]] = None,
proxies: Optional[Mapping[str, str]] = None,
timeout: float | tuple[float, float] | None = None,
verify: bool | str = True,
cert: str | tuple[str, str] | None = None,
proxies: Mapping[str, str] | None = None,
) -> Response:
pathname = url_to_path(request.url)
@ -212,19 +223,19 @@ class LocalFSAdapter(BaseAdapter):
# to return a better error message:
resp.status_code = 404
resp.reason = type(exc).__name__
resp.raw = io.BytesIO(f"{resp.reason}: {exc}".encode("utf8"))
resp.raw = io.BytesIO(f'{resp.reason}: {exc}'.encode())
else:
modified = email.utils.formatdate(stats.st_mtime, usegmt=True)
content_type = mimetypes.guess_type(pathname)[0] or "text/plain"
content_type = mimetypes.guess_type(pathname)[0] or 'text/plain'
resp.headers = CaseInsensitiveDict(
{
"Content-Type": content_type,
"Content-Length": stats.st_size,
"Last-Modified": modified,
}
'Content-Type': content_type,
'Content-Length': stats.st_size,
'Last-Modified': modified,
},
)
resp.raw = open(pathname, "rb")
resp.raw = open(pathname, 'rb')
resp.close = resp.raw.close
return resp
@ -238,8 +249,8 @@ class InsecureHTTPAdapter(HTTPAdapter):
self,
conn: ConnectionPool,
url: str,
verify: Union[bool, str],
cert: Optional[Union[str, Tuple[str, str]]],
verify: bool | str,
cert: str | tuple[str, str] | None,
) -> None:
super().cert_verify(conn=conn, url=url, verify=False, cert=cert)
@ -249,23 +260,23 @@ class InsecureCacheControlAdapter(CacheControlAdapter):
self,
conn: ConnectionPool,
url: str,
verify: Union[bool, str],
cert: Optional[Union[str, Tuple[str, str]]],
verify: bool | str,
cert: str | tuple[str, str] | None,
) -> None:
super().cert_verify(conn=conn, url=url, verify=False, cert=cert)
class PipSession(requests.Session):
timeout: Optional[int] = None
timeout: int | None = None
def __init__(
self,
*args: Any,
retries: int = 0,
cache: Optional[str] = None,
cache: str | None = None,
trusted_hosts: Sequence[str] = (),
index_urls: Optional[List[str]] = None,
index_urls: list[str] | None = None,
**kwargs: Any,
) -> None:
"""
@ -276,10 +287,10 @@ class PipSession(requests.Session):
# Namespace the attribute with "pip_" just in case to prevent
# possible conflicts with the base class.
self.pip_trusted_origins: List[Tuple[str, Optional[int]]] = []
self.pip_trusted_origins: list[tuple[str, int | None]] = []
# Attach our User Agent to the request
self.headers["User-Agent"] = user_agent()
self.headers['User-Agent'] = user_agent()
# Attach our Authentication handler to the session
self.auth = MultiDomainBasicAuth(index_urls=index_urls)
@ -327,16 +338,16 @@ class PipSession(requests.Session):
secure_adapter = HTTPAdapter(max_retries=retries)
self._trusted_host_adapter = insecure_adapter
self.mount("https://", secure_adapter)
self.mount("http://", insecure_adapter)
self.mount('https://', secure_adapter)
self.mount('http://', insecure_adapter)
# Enable file:// urls
self.mount("file://", LocalFSAdapter())
self.mount('file://', LocalFSAdapter())
for host in trusted_hosts:
self.add_trusted_host(host, suppress_logging=True)
def update_index_urls(self, new_index_urls: List[str]) -> None:
def update_index_urls(self, new_index_urls: list[str]) -> None:
"""
:param new_index_urls: New index urls to update the authentication
handler with.
@ -344,7 +355,7 @@ class PipSession(requests.Session):
self.auth.index_urls = new_index_urls
def add_trusted_host(
self, host: str, source: Optional[str] = None, suppress_logging: bool = False
self, host: str, source: str | None = None, suppress_logging: bool = False,
) -> None:
"""
:param host: It is okay to provide a host that has previously been
@ -353,9 +364,9 @@ class PipSession(requests.Session):
string came from.
"""
if not suppress_logging:
msg = f"adding trusted host: {host!r}"
msg = f'adding trusted host: {host!r}'
if source is not None:
msg += f" (from {source})"
msg += f' (from {source})'
logger.info(msg)
host_port = parse_netloc(host)
@ -363,21 +374,21 @@ class PipSession(requests.Session):
self.pip_trusted_origins.append(host_port)
self.mount(
build_url_from_netloc(host, scheme="http") + "/", self._trusted_host_adapter
build_url_from_netloc(host, scheme='http') + '/', self._trusted_host_adapter,
)
self.mount(build_url_from_netloc(host) + "/", self._trusted_host_adapter)
self.mount(build_url_from_netloc(host) + '/', self._trusted_host_adapter)
if not host_port[1]:
self.mount(
build_url_from_netloc(host, scheme="http") + ":",
build_url_from_netloc(host, scheme='http') + ':',
self._trusted_host_adapter,
)
# Mount wildcard ports for the same host.
self.mount(build_url_from_netloc(host) + ":", self._trusted_host_adapter)
self.mount(build_url_from_netloc(host) + ':', self._trusted_host_adapter)
def iter_secure_origins(self) -> Iterator[SecureOrigin]:
yield from SECURE_ORIGINS
for host, port in self.pip_trusted_origins:
yield ("*", host, "*" if port is None else port)
yield ('*', host, '*' if port is None else port)
def is_secure_origin(self, location: Link) -> bool:
# Determine if this url used a secure transport mechanism
@ -392,14 +403,14 @@ class PipSession(requests.Session):
# Don't count the repository type as part of the protocol: in
# cases such as "git+ssh", only use "ssh". (I.e., Only verify against
# the last scheme.)
origin_protocol = origin_protocol.rsplit("+", 1)[-1]
origin_protocol = origin_protocol.rsplit('+', 1)[-1]
# Determine if our origin is a secure origin by looking through our
# hardcoded list of secure origins, as well as any additional ones
# configured on this PackageFinder instance.
for secure_origin in self.iter_secure_origins():
secure_protocol, secure_host, secure_port = secure_origin
if origin_protocol != secure_protocol and secure_protocol != "*":
if origin_protocol != secure_protocol and secure_protocol != '*':
continue
try:
@ -409,9 +420,9 @@ class PipSession(requests.Session):
# We don't have both a valid address or a valid network, so
# we'll check this origin against hostnames.
if (
origin_host
and origin_host.lower() != secure_host.lower()
and secure_host != "*"
origin_host and
origin_host.lower() != secure_host.lower() and
secure_host != '*'
):
continue
else:
@ -422,9 +433,9 @@ class PipSession(requests.Session):
# Check to see if the port matches.
if (
origin_port != secure_port
and secure_port != "*"
and secure_port is not None
origin_port != secure_port and
secure_port != '*' and
secure_port is not None
):
continue
@ -436,9 +447,9 @@ class PipSession(requests.Session):
# will not accept it as a valid location to search. We will however
# log a warning that we are ignoring it.
logger.warning(
"The repository located at %s is not a trusted or secure host and "
"is being ignored. If this repository is available via HTTPS we "
"recommend you use HTTPS instead, otherwise you may silence "
'The repository located at %s is not a trusted or secure host and '
'is being ignored. If this repository is available via HTTPS we '
'recommend you use HTTPS instead, otherwise you may silence '
"this warning and allow it anyway with '--trusted-host %s'.",
origin_host,
origin_host,
@ -448,7 +459,7 @@ class PipSession(requests.Session):
def request(self, method: str, url: str, *args: Any, **kwargs: Any) -> Response:
# Allow setting a default timeout on a session
kwargs.setdefault("timeout", self.timeout)
kwargs.setdefault('timeout', self.timeout)
# Dispatch the actual request
return super().request(method, url, *args, **kwargs)

View file

@ -1,8 +1,11 @@
from typing import Dict, Iterator
from __future__ import annotations
from pip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response
from typing import Dict
from typing import Iterator
from pip._internal.exceptions import NetworkConnectionError
from pip._vendor.requests.models import CONTENT_CHUNK_SIZE
from pip._vendor.requests.models import Response
# The following comments and HTTP headers were originally added by
# Donald Stufft in git commit 22c562429a61bb77172039e480873fb239dd8c03.
@ -23,31 +26,31 @@ from pip._internal.exceptions import NetworkConnectionError
# you're not asking for a compressed file and will then decompress it
# before sending because if that's the case I don't think it'll ever be
# possible to make this work.
HEADERS: Dict[str, str] = {"Accept-Encoding": "identity"}
HEADERS: dict[str, str] = {'Accept-Encoding': 'identity'}
def raise_for_status(resp: Response) -> None:
http_error_msg = ""
http_error_msg = ''
if isinstance(resp.reason, bytes):
# We attempt to decode utf-8 first because some servers
# choose to localize their reason strings. If the string
# isn't utf-8, we fall back to iso-8859-1 for all other
# encodings.
try:
reason = resp.reason.decode("utf-8")
reason = resp.reason.decode('utf-8')
except UnicodeDecodeError:
reason = resp.reason.decode("iso-8859-1")
reason = resp.reason.decode('iso-8859-1')
else:
reason = resp.reason
if 400 <= resp.status_code < 500:
http_error_msg = (
f"{resp.status_code} Client Error: {reason} for url: {resp.url}"
f'{resp.status_code} Client Error: {reason} for url: {resp.url}'
)
elif 500 <= resp.status_code < 600:
http_error_msg = (
f"{resp.status_code} Server Error: {reason} for url: {resp.url}"
f'{resp.status_code} Server Error: {reason} for url: {resp.url}'
)
if http_error_msg:
@ -55,7 +58,7 @@ def raise_for_status(resp: Response) -> None:
def response_chunks(
response: Response, chunk_size: int = CONTENT_CHUNK_SIZE
response: Response, chunk_size: int = CONTENT_CHUNK_SIZE,
) -> Iterator[bytes]:
"""Given a requests Response, provide the data chunks."""
try:

View file

@ -1,10 +1,12 @@
"""xmlrpclib.Transport implementation
"""
from __future__ import annotations
import logging
import urllib.parse
import xmlrpc.client
from typing import TYPE_CHECKING, Tuple
from typing import Tuple
from typing import TYPE_CHECKING
from pip._internal.exceptions import NetworkConnectionError
from pip._internal.network.session import PipSession
@ -22,7 +24,7 @@ class PipXmlrpcTransport(xmlrpc.client.Transport):
"""
def __init__(
self, index_url: str, session: PipSession, use_datetime: bool = False
self, index_url: str, session: PipSession, use_datetime: bool = False,
) -> None:
super().__init__(use_datetime)
index_parts = urllib.parse.urlparse(index_url)
@ -31,16 +33,16 @@ class PipXmlrpcTransport(xmlrpc.client.Transport):
def request(
self,
host: "_HostType",
host: _HostType,
handler: str,
request_body: bytes,
verbose: bool = False,
) -> Tuple["_Marshallable", ...]:
) -> tuple[_Marshallable, ...]:
assert isinstance(host, str)
parts = (self._scheme, host, handler, None, None, None)
url = urllib.parse.urlunparse(parts)
try:
headers = {"Content-Type": "text/xml"}
headers = {'Content-Type': 'text/xml'}
response = self._session.post(
url,
data=request_body,
@ -53,7 +55,7 @@ class PipXmlrpcTransport(xmlrpc.client.Transport):
except NetworkConnectionError as exc:
assert exc.response
logger.critical(
"HTTP error %s while getting %s",
'HTTP error %s while getting %s',
exc.response.status_code,
url,
)

View file

@ -1,27 +1,25 @@
"""Metadata generation logic for source distributions.
"""
from __future__ import annotations
import os
from pip._vendor.pep517.wrappers import Pep517HookCaller
from pip._internal.build_env import BuildEnvironment
from pip._internal.exceptions import (
InstallationSubprocessError,
MetadataGenerationFailed,
)
from pip._internal.exceptions import InstallationSubprocessError
from pip._internal.exceptions import MetadataGenerationFailed
from pip._internal.utils.subprocess import runner_with_spinner_message
from pip._internal.utils.temp_dir import TempDirectory
from pip._vendor.pep517.wrappers import Pep517HookCaller
def generate_metadata(
build_env: BuildEnvironment, backend: Pep517HookCaller, details: str
build_env: BuildEnvironment, backend: Pep517HookCaller, details: str,
) -> str:
"""Generate metadata using mechanisms described in PEP 517.
Returns the generated metadata directory.
"""
metadata_tmpdir = TempDirectory(kind="modern-metadata", globally_managed=True)
metadata_tmpdir = TempDirectory(kind='modern-metadata', globally_managed=True)
metadata_dir = metadata_tmpdir.path
@ -29,7 +27,7 @@ def generate_metadata(
# Note that Pep517HookCaller implements a fallback for
# prepare_metadata_for_build_wheel, so we don't have to
# consider the possibility that this hook doesn't exist.
runner = runner_with_spinner_message("Preparing metadata (pyproject.toml)")
runner = runner_with_spinner_message('Preparing metadata (pyproject.toml)')
with backend.subprocess_runner(runner):
try:
distinfo_dir = backend.prepare_metadata_for_build_wheel(metadata_dir)

View file

@ -1,27 +1,25 @@
"""Metadata generation logic for source distributions.
"""
from __future__ import annotations
import os
from pip._vendor.pep517.wrappers import Pep517HookCaller
from pip._internal.build_env import BuildEnvironment
from pip._internal.exceptions import (
InstallationSubprocessError,
MetadataGenerationFailed,
)
from pip._internal.exceptions import InstallationSubprocessError
from pip._internal.exceptions import MetadataGenerationFailed
from pip._internal.utils.subprocess import runner_with_spinner_message
from pip._internal.utils.temp_dir import TempDirectory
from pip._vendor.pep517.wrappers import Pep517HookCaller
def generate_editable_metadata(
build_env: BuildEnvironment, backend: Pep517HookCaller, details: str
build_env: BuildEnvironment, backend: Pep517HookCaller, details: str,
) -> str:
"""Generate metadata using mechanisms described in PEP 660.
Returns the generated metadata directory.
"""
metadata_tmpdir = TempDirectory(kind="modern-metadata", globally_managed=True)
metadata_tmpdir = TempDirectory(kind='modern-metadata', globally_managed=True)
metadata_dir = metadata_tmpdir.path
@ -30,7 +28,7 @@ def generate_editable_metadata(
# prepare_metadata_for_build_wheel/editable, so we don't have to
# consider the possibility that this hook doesn't exist.
runner = runner_with_spinner_message(
"Preparing editable metadata (pyproject.toml)"
'Preparing editable metadata (pyproject.toml)',
)
with backend.subprocess_runner(runner):
try:

View file

@ -1,16 +1,15 @@
"""Metadata generation logic for legacy source distributions.
"""
from __future__ import annotations
import logging
import os
from pip._internal.build_env import BuildEnvironment
from pip._internal.cli.spinners import open_spinner
from pip._internal.exceptions import (
InstallationError,
InstallationSubprocessError,
MetadataGenerationFailed,
)
from pip._internal.exceptions import InstallationError
from pip._internal.exceptions import InstallationSubprocessError
from pip._internal.exceptions import MetadataGenerationFailed
from pip._internal.utils.setuptools_build import make_setuptools_egg_info_args
from pip._internal.utils.subprocess import call_subprocess
from pip._internal.utils.temp_dir import TempDirectory
@ -20,14 +19,14 @@ logger = logging.getLogger(__name__)
def _find_egg_info(directory: str) -> str:
"""Find an .egg-info subdirectory in `directory`."""
filenames = [f for f in os.listdir(directory) if f.endswith(".egg-info")]
filenames = [f for f in os.listdir(directory) if f.endswith('.egg-info')]
if not filenames:
raise InstallationError(f"No .egg-info directory found in {directory}")
raise InstallationError(f'No .egg-info directory found in {directory}')
if len(filenames) > 1:
raise InstallationError(
"More than one .egg-info directory found in {}".format(directory)
f'More than one .egg-info directory found in {directory}',
)
return os.path.join(directory, filenames[0])
@ -45,12 +44,12 @@ def generate_metadata(
Returns the generated metadata directory.
"""
logger.debug(
"Running setup.py (path:%s) egg_info for package %s",
'Running setup.py (path:%s) egg_info for package %s',
setup_py_path,
details,
)
egg_info_dir = TempDirectory(kind="pip-egg-info", globally_managed=True).path
egg_info_dir = TempDirectory(kind='pip-egg-info', globally_managed=True).path
args = make_setuptools_egg_info_args(
setup_py_path,
@ -59,12 +58,12 @@ def generate_metadata(
)
with build_env:
with open_spinner("Preparing metadata (setup.py)") as spinner:
with open_spinner('Preparing metadata (setup.py)') as spinner:
try:
call_subprocess(
args,
cwd=source_dir,
command_desc="python setup.py egg_info",
command_desc='python setup.py egg_info',
spinner=spinner,
)
except InstallationSubprocessError as error:

View file

@ -1,10 +1,11 @@
from __future__ import annotations
import logging
import os
from typing import Optional
from pip._vendor.pep517.wrappers import Pep517HookCaller
from pip._internal.utils.subprocess import runner_with_spinner_message
from pip._vendor.pep517.wrappers import Pep517HookCaller
logger = logging.getLogger(__name__)
@ -14,17 +15,17 @@ def build_wheel_pep517(
backend: Pep517HookCaller,
metadata_directory: str,
tempd: str,
) -> Optional[str]:
) -> str | None:
"""Build one InstallRequirement using the PEP 517 build process.
Returns path to wheel if successfully built. Otherwise, returns None.
"""
assert metadata_directory is not None
try:
logger.debug("Destination directory: %s", tempd)
logger.debug('Destination directory: %s', tempd)
runner = runner_with_spinner_message(
f"Building wheel for {name} (pyproject.toml)"
f'Building wheel for {name} (pyproject.toml)',
)
with backend.subprocess_runner(runner):
wheel_name = backend.build_wheel(
@ -32,6 +33,6 @@ def build_wheel_pep517(
metadata_directory=metadata_directory,
)
except Exception:
logger.error("Failed building wheel for %s", name)
logger.error('Failed building wheel for %s', name)
return None
return os.path.join(tempd, wheel_name)

View file

@ -1,10 +1,12 @@
from __future__ import annotations
import logging
import os
from typing import Optional
from pip._vendor.pep517.wrappers import HookMissing, Pep517HookCaller
from pip._internal.utils.subprocess import runner_with_spinner_message
from pip._vendor.pep517.wrappers import HookMissing
from pip._vendor.pep517.wrappers import Pep517HookCaller
logger = logging.getLogger(__name__)
@ -14,17 +16,17 @@ def build_wheel_editable(
backend: Pep517HookCaller,
metadata_directory: str,
tempd: str,
) -> Optional[str]:
) -> str | None:
"""Build one InstallRequirement using the PEP 660 build process.
Returns path to wheel if successfully built. Otherwise, returns None.
"""
assert metadata_directory is not None
try:
logger.debug("Destination directory: %s", tempd)
logger.debug('Destination directory: %s', tempd)
runner = runner_with_spinner_message(
f"Building editable for {name} (pyproject.toml)"
f'Building editable for {name} (pyproject.toml)',
)
with backend.subprocess_runner(runner):
try:
@ -34,13 +36,13 @@ def build_wheel_editable(
)
except HookMissing as e:
logger.error(
"Cannot build editable %s because the build "
"backend does not have the %s hook",
'Cannot build editable %s because the build '
'backend does not have the %s hook',
name,
e,
)
return None
except Exception:
logger.error("Failed building editable for %s", name)
logger.error('Failed building editable for %s', name)
return None
return os.path.join(tempd, wheel_name)

View file

@ -1,54 +1,58 @@
from __future__ import annotations
import logging
import os.path
from typing import List, Optional
from typing import List
from typing import Optional
from pip._internal.cli.spinners import open_spinner
from pip._internal.utils.setuptools_build import make_setuptools_bdist_wheel_args
from pip._internal.utils.subprocess import call_subprocess, format_command_args
from pip._internal.utils.subprocess import call_subprocess
from pip._internal.utils.subprocess import format_command_args
logger = logging.getLogger(__name__)
def format_command_result(
command_args: List[str],
command_args: list[str],
command_output: str,
) -> str:
"""Format command information for logging."""
command_desc = format_command_args(command_args)
text = f"Command arguments: {command_desc}\n"
text = f'Command arguments: {command_desc}\n'
if not command_output:
text += "Command output: None"
text += 'Command output: None'
elif logger.getEffectiveLevel() > logging.DEBUG:
text += "Command output: [use --verbose to show]"
text += 'Command output: [use --verbose to show]'
else:
if not command_output.endswith("\n"):
command_output += "\n"
text += f"Command output:\n{command_output}"
if not command_output.endswith('\n'):
command_output += '\n'
text += f'Command output:\n{command_output}'
return text
def get_legacy_build_wheel_path(
names: List[str],
names: list[str],
temp_dir: str,
name: str,
command_args: List[str],
command_args: list[str],
command_output: str,
) -> Optional[str]:
) -> str | None:
"""Return the path to the wheel in the temporary build directory."""
# Sort for determinism.
names = sorted(names)
if not names:
msg = ("Legacy build of wheel for {!r} created no files.\n").format(name)
msg = ('Legacy build of wheel for {!r} created no files.\n').format(name)
msg += format_command_result(command_args, command_output)
logger.warning(msg)
return None
if len(names) > 1:
msg = (
"Legacy build of wheel for {!r} created more than one file.\n"
"Filenames (choosing first): {}\n"
'Legacy build of wheel for {!r} created more than one file.\n'
'Filenames (choosing first): {}\n'
).format(name, names)
msg += format_command_result(command_args, command_output)
logger.warning(msg)
@ -60,10 +64,10 @@ def build_wheel_legacy(
name: str,
setup_py_path: str,
source_dir: str,
global_options: List[str],
build_options: List[str],
global_options: list[str],
build_options: list[str],
tempd: str,
) -> Optional[str]:
) -> str | None:
"""Build one unpacked package using the "legacy" build process.
Returns path to wheel if successfully built. Otherwise, returns None.
@ -75,20 +79,20 @@ def build_wheel_legacy(
destination_dir=tempd,
)
spin_message = f"Building wheel for {name} (setup.py)"
spin_message = f'Building wheel for {name} (setup.py)'
with open_spinner(spin_message) as spinner:
logger.debug("Destination directory: %s", tempd)
logger.debug('Destination directory: %s', tempd)
try:
output = call_subprocess(
wheel_args,
command_desc="python setup.py bdist_wheel",
command_desc='python setup.py bdist_wheel',
cwd=source_dir,
spinner=spinner,
)
except Exception:
spinner.finish("error")
logger.error("Failed building wheel for %s", name)
spinner.finish('error')
logger.error('Failed building wheel for %s', name)
return None
names = os.listdir(tempd)

View file

@ -1,23 +1,30 @@
"""Validation of dependencies of packages
"""
from __future__ import annotations
import logging
from typing import Callable, Dict, List, NamedTuple, Optional, Set, Tuple
from pip._vendor.packaging.requirements import Requirement
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
from typing import Callable
from typing import Dict
from typing import List
from typing import NamedTuple
from typing import Optional
from typing import Set
from typing import Tuple
from pip._internal.distributions import make_distribution_for_install_requirement
from pip._internal.metadata import get_default_environment
from pip._internal.metadata.base import DistributionVersion
from pip._internal.req.req_install import InstallRequirement
from pip._vendor.packaging.requirements import Requirement
from pip._vendor.packaging.utils import canonicalize_name
from pip._vendor.packaging.utils import NormalizedName
logger = logging.getLogger(__name__)
class PackageDetails(NamedTuple):
version: DistributionVersion
dependencies: List[Requirement]
dependencies: list[Requirement]
# Shorthands
@ -31,7 +38,7 @@ CheckResult = Tuple[MissingDict, ConflictingDict]
ConflictDetails = Tuple[PackageSet, CheckResult]
def create_package_set_from_installed() -> Tuple[PackageSet, bool]:
def create_package_set_from_installed() -> tuple[PackageSet, bool]:
"""Converts a list of distributions into a PackageSet."""
package_set = {}
problems = False
@ -43,13 +50,13 @@ def create_package_set_from_installed() -> Tuple[PackageSet, bool]:
package_set[name] = PackageDetails(dist.version, dependencies)
except (OSError, ValueError) as e:
# Don't crash on unreadable or broken metadata.
logger.warning("Error parsing requirements for %s: %s", name, e)
logger.warning('Error parsing requirements for %s: %s', name, e)
problems = True
return package_set, problems
def check_package_set(
package_set: PackageSet, should_ignore: Optional[Callable[[str], bool]] = None
package_set: PackageSet, should_ignore: Callable[[str], bool] | None = None,
) -> CheckResult:
"""Check if a package set is consistent
@ -62,8 +69,8 @@ def check_package_set(
for package_name, package_detail in package_set.items():
# Info about dependencies of package_name
missing_deps: Set[Missing] = set()
conflicting_deps: Set[Conflicting] = set()
missing_deps: set[Missing] = set()
conflicting_deps: set[Conflicting] = set()
if should_ignore and should_ignore(package_name):
continue
@ -93,7 +100,7 @@ def check_package_set(
return missing, conflicting
def check_install_conflicts(to_install: List[InstallRequirement]) -> ConflictDetails:
def check_install_conflicts(to_install: list[InstallRequirement]) -> ConflictDetails:
"""For checking if the dependency graph would be consistent after \
installing given requirements
"""
@ -108,14 +115,14 @@ def check_install_conflicts(to_install: List[InstallRequirement]) -> ConflictDet
return (
package_set,
check_package_set(
package_set, should_ignore=lambda name: name not in whitelist
package_set, should_ignore=lambda name: name not in whitelist,
),
)
def _simulate_installation_of(
to_install: List[InstallRequirement], package_set: PackageSet
) -> Set[NormalizedName]:
to_install: list[InstallRequirement], package_set: PackageSet,
) -> set[NormalizedName]:
"""Computes the version of packages after installing to_install."""
# Keep track of packages that were installed
installed = set()
@ -133,8 +140,8 @@ def _simulate_installation_of(
def _create_whitelist(
would_be_installed: Set[NormalizedName], package_set: PackageSet
) -> Set[NormalizedName]:
would_be_installed: set[NormalizedName], package_set: PackageSet,
) -> set[NormalizedName]:
packages_affected = set(would_be_installed)
for package_name in package_set:

View file

@ -1,38 +1,46 @@
from __future__ import annotations
import collections
import logging
import os
from typing import Container, Dict, Iterable, Iterator, List, NamedTuple, Optional, Set
from typing import Container
from typing import Dict
from typing import Iterable
from typing import Iterator
from typing import List
from typing import NamedTuple
from typing import Optional
from typing import Set
from pip._vendor.packaging.utils import canonicalize_name
from pip._vendor.packaging.version import Version
from pip._internal.exceptions import BadCommand, InstallationError
from pip._internal.metadata import BaseDistribution, get_environment
from pip._internal.req.constructors import (
install_req_from_editable,
install_req_from_line,
)
from pip._internal.exceptions import BadCommand
from pip._internal.exceptions import InstallationError
from pip._internal.metadata import BaseDistribution
from pip._internal.metadata import get_environment
from pip._internal.req.constructors import install_req_from_editable
from pip._internal.req.constructors import install_req_from_line
from pip._internal.req.req_file import COMMENT_RE
from pip._internal.utils.direct_url_helpers import direct_url_as_pep440_direct_reference
from pip._vendor.packaging.utils import canonicalize_name
from pip._vendor.packaging.version import Version
logger = logging.getLogger(__name__)
class _EditableInfo(NamedTuple):
requirement: str
comments: List[str]
comments: list[str]
def freeze(
requirement: Optional[List[str]] = None,
requirement: list[str] | None = None,
local_only: bool = False,
user_only: bool = False,
paths: Optional[List[str]] = None,
paths: list[str] | None = None,
isolated: bool = False,
exclude_editable: bool = False,
skip: Container[str] = (),
) -> Iterator[str]:
installations: Dict[str, FrozenRequirement] = {}
installations: dict[str, FrozenRequirement] = {}
dists = get_environment(paths).iter_installed_distributions(
local_only=local_only,
@ -50,30 +58,30 @@ def freeze(
# should only be emitted once, even if the same option is in multiple
# requirements files, so we need to keep track of what has been emitted
# so that we don't emit it again if it's seen again
emitted_options: Set[str] = set()
emitted_options: set[str] = set()
# keep track of which files a requirement is in so that we can
# give an accurate warning if a requirement appears multiple times.
req_files: Dict[str, List[str]] = collections.defaultdict(list)
req_files: dict[str, list[str]] = collections.defaultdict(list)
for req_file_path in requirement:
with open(req_file_path) as req_file:
for line in req_file:
if (
not line.strip()
or line.strip().startswith("#")
or line.startswith(
not line.strip() or
line.strip().startswith('#') or
line.startswith(
(
"-r",
"--requirement",
"-f",
"--find-links",
"-i",
"--index-url",
"--pre",
"--trusted-host",
"--process-dependency-links",
"--extra-index-url",
"--use-feature",
)
'-r',
'--requirement',
'-f',
'--find-links',
'-i',
'--index-url',
'--pre',
'--trusted-host',
'--process-dependency-links',
'--extra-index-url',
'--use-feature',
),
)
):
line = line.rstrip()
@ -82,31 +90,31 @@ def freeze(
yield line
continue
if line.startswith("-e") or line.startswith("--editable"):
if line.startswith("-e"):
if line.startswith('-e') or line.startswith('--editable'):
if line.startswith('-e'):
line = line[2:].strip()
else:
line = line[len("--editable") :].strip().lstrip("=")
line = line[len('--editable'):].strip().lstrip('=')
line_req = install_req_from_editable(
line,
isolated=isolated,
)
else:
line_req = install_req_from_line(
COMMENT_RE.sub("", line).strip(),
COMMENT_RE.sub('', line).strip(),
isolated=isolated,
)
if not line_req.name:
logger.info(
"Skipping line in requirement file [%s] because "
'Skipping line in requirement file [%s] because '
"it's not clear what it would install: %s",
req_file_path,
line.strip(),
)
logger.info(
" (add #egg=PackageName to the URL to avoid"
" this warning)"
' (add #egg=PackageName to the URL to avoid'
' this warning)',
)
else:
line_req_canonical_name = canonicalize_name(line_req.name)
@ -115,10 +123,10 @@ def freeze(
# but has been processed already
if not req_files[line_req.name]:
logger.warning(
"Requirement file [%s] contains %s, but "
"package %r is not installed",
'Requirement file [%s] contains %s, but '
'package %r is not installed',
req_file_path,
COMMENT_RE.sub("", line).strip(),
COMMENT_RE.sub('', line).strip(),
line_req.name,
)
else:
@ -133,12 +141,12 @@ def freeze(
for name, files in req_files.items():
if len(files) > 1:
logger.warning(
"Requirement %s included multiple times [%s]",
'Requirement %s included multiple times [%s]',
name,
", ".join(sorted(set(files))),
', '.join(sorted(set(files))),
)
yield ("## The following requirements were added by pip freeze:")
yield ('## The following requirements were added by pip freeze:')
for installation in sorted(installations.values(), key=lambda x: x.name.lower()):
if installation.canonical_name not in skip:
yield str(installation).rstrip()
@ -146,8 +154,8 @@ def freeze(
def _format_as_name_version(dist: BaseDistribution) -> str:
if isinstance(dist.version, Version):
return f"{dist.raw_name}=={dist.version}"
return f"{dist.raw_name}==={dist.version}"
return f'{dist.raw_name}=={dist.version}'
return f'{dist.raw_name}==={dist.version}'
def _get_editable_info(dist: BaseDistribution) -> _EditableInfo:
@ -172,7 +180,7 @@ def _get_editable_info(dist: BaseDistribution) -> _EditableInfo:
)
return _EditableInfo(
requirement=location,
comments=[f"# Editable install with no version control ({display})"],
comments=[f'# Editable install with no version control ({display})'],
)
vcs_name = type(vcs_backend).__name__
@ -183,36 +191,36 @@ def _get_editable_info(dist: BaseDistribution) -> _EditableInfo:
display = _format_as_name_version(dist)
return _EditableInfo(
requirement=location,
comments=[f"# Editable {vcs_name} install with no remote ({display})"],
comments=[f'# Editable {vcs_name} install with no remote ({display})'],
)
except RemoteNotValidError as ex:
display = _format_as_name_version(dist)
return _EditableInfo(
requirement=location,
comments=[
f"# Editable {vcs_name} install ({display}) with either a deleted "
f"local remote or invalid URI:",
f'# Editable {vcs_name} install ({display}) with either a deleted '
f'local remote or invalid URI:',
f"# '{ex.url}'",
],
)
except BadCommand:
logger.warning(
"cannot determine version of editable source in %s "
"(%s command not found in path)",
'cannot determine version of editable source in %s '
'(%s command not found in path)',
location,
vcs_backend.name,
)
return _EditableInfo(requirement=location, comments=[])
except InstallationError as exc:
logger.warning("Error when trying to get requirement for VCS system %s", exc)
logger.warning('Error when trying to get requirement for VCS system %s', exc)
else:
return _EditableInfo(requirement=req, comments=[])
logger.warning("Could not determine repository location of %s", location)
logger.warning('Could not determine repository location of %s', location)
return _EditableInfo(
requirement=location,
comments=["## !! Could not determine repository location"],
comments=['## !! Could not determine repository location'],
)
@ -231,7 +239,7 @@ class FrozenRequirement:
self.comments = comments
@classmethod
def from_dist(cls, dist: BaseDistribution) -> "FrozenRequirement":
def from_dist(cls, dist: BaseDistribution) -> FrozenRequirement:
editable = dist.editable
if editable:
req, comments = _get_editable_info(dist)
@ -250,5 +258,5 @@ class FrozenRequirement:
def __str__(self) -> str:
req = self.req
if self.editable:
req = f"-e {req}"
return "\n".join(list(self.comments) + [str(req)]) + "\n"
req = f'-e {req}'
return '\n'.join(list(self.comments) + [str(req)]) + '\n'

View file

@ -1,2 +1,3 @@
"""For modules related to installing packages.
"""
from __future__ import annotations

View file

@ -1,7 +1,11 @@
"""Legacy editable installation process, i.e. `setup.py develop`.
"""
from __future__ import annotations
import logging
from typing import List, Optional, Sequence
from typing import List
from typing import Optional
from typing import Sequence
from pip._internal.build_env import BuildEnvironment
from pip._internal.utils.logging import indent_log
@ -12,10 +16,10 @@ logger = logging.getLogger(__name__)
def install_editable(
install_options: List[str],
install_options: list[str],
global_options: Sequence[str],
prefix: Optional[str],
home: Optional[str],
prefix: str | None,
home: str | None,
use_user_site: bool,
name: str,
setup_py_path: str,
@ -26,7 +30,7 @@ def install_editable(
"""Install a package in editable mode. Most arguments are pass-through
to setuptools.
"""
logger.info("Running setup.py develop for %s", name)
logger.info('Running setup.py develop for %s', name)
args = make_setuptools_develop_args(
setup_py_path,
@ -42,6 +46,6 @@ def install_editable(
with build_env:
call_subprocess(
args,
command_desc="python setup.py develop",
command_desc='python setup.py develop',
cwd=unpacked_source_directory,
)

View file

@ -1,13 +1,17 @@
"""Legacy installation process, i.e. `setup.py install`.
"""
from __future__ import annotations
import logging
import os
from distutils.util import change_root
from typing import List, Optional, Sequence
from typing import List
from typing import Optional
from typing import Sequence
from distutils.util import change_root
from pip._internal.build_env import BuildEnvironment
from pip._internal.exceptions import InstallationError, LegacyInstallFailure
from pip._internal.exceptions import InstallationError
from pip._internal.exceptions import LegacyInstallFailure
from pip._internal.models.scheme import Scheme
from pip._internal.utils.misc import ensure_dir
from pip._internal.utils.setuptools_build import make_setuptools_install_args
@ -18,8 +22,8 @@ logger = logging.getLogger(__name__)
def write_installed_files_from_setuptools_record(
record_lines: List[str],
root: Optional[str],
record_lines: list[str],
root: str | None,
req_description: str,
) -> None:
def prepend_root(path: str) -> str:
@ -30,14 +34,14 @@ def write_installed_files_from_setuptools_record(
for line in record_lines:
directory = os.path.dirname(line)
if directory.endswith(".egg-info"):
if directory.endswith('.egg-info'):
egg_info_dir = prepend_root(directory)
break
else:
message = (
"{} did not indicate that it installed an "
".egg-info directory. Only setup.py projects "
"generating .egg-info directories are supported."
'{} did not indicate that it installed an '
'.egg-info directory. Only setup.py projects '
'generating .egg-info directories are supported.'
).format(req_description)
raise InstallationError(message)
@ -49,17 +53,17 @@ def write_installed_files_from_setuptools_record(
new_lines.append(os.path.relpath(prepend_root(filename), egg_info_dir))
new_lines.sort()
ensure_dir(egg_info_dir)
inst_files_path = os.path.join(egg_info_dir, "installed-files.txt")
with open(inst_files_path, "w") as f:
f.write("\n".join(new_lines) + "\n")
inst_files_path = os.path.join(egg_info_dir, 'installed-files.txt')
with open(inst_files_path, 'w') as f:
f.write('\n'.join(new_lines) + '\n')
def install(
install_options: List[str],
install_options: list[str],
global_options: Sequence[str],
root: Optional[str],
home: Optional[str],
prefix: Optional[str],
root: str | None,
home: str | None,
prefix: str | None,
use_user_site: bool,
pycompile: bool,
scheme: Scheme,
@ -73,9 +77,9 @@ def install(
header_dir = scheme.headers
with TempDirectory(kind="record") as temp_dir:
with TempDirectory(kind='record') as temp_dir:
try:
record_filename = os.path.join(temp_dir.path, "install-record.txt")
record_filename = os.path.join(temp_dir.path, 'install-record.txt')
install_args = make_setuptools_install_args(
setup_py_path,
global_options=global_options,
@ -91,7 +95,7 @@ def install(
)
runner = runner_with_spinner_message(
f"Running setup.py install for {req_name}"
f'Running setup.py install for {req_name}',
)
with build_env:
runner(
@ -100,7 +104,7 @@ def install(
)
if not os.path.exists(record_filename):
logger.debug("Record file %s not found", record_filename)
logger.debug('Record file %s not found', record_filename)
# Signal to the caller that we didn't install the new package
return False

View file

@ -1,5 +1,6 @@
"""Support for installing and building the "wheel" binary package format.
"""
from __future__ import annotations
import collections
import compileall
@ -14,55 +15,57 @@ import sys
import warnings
from base64 import urlsafe_b64encode
from email.message import Message
from itertools import chain, filterfalse, starmap
from typing import (
IO,
TYPE_CHECKING,
Any,
BinaryIO,
Callable,
Dict,
Iterable,
Iterator,
List,
NewType,
Optional,
Sequence,
Set,
Tuple,
Union,
cast,
)
from zipfile import ZipFile, ZipInfo
from pip._vendor.distlib.scripts import ScriptMaker
from pip._vendor.distlib.util import get_export_entry
from pip._vendor.packaging.utils import canonicalize_name
from itertools import chain
from itertools import filterfalse
from itertools import starmap
from typing import Any
from typing import BinaryIO
from typing import Callable
from typing import cast
from typing import Dict
from typing import IO
from typing import Iterable
from typing import Iterator
from typing import List
from typing import NewType
from typing import Optional
from typing import Sequence
from typing import Set
from typing import Tuple
from typing import TYPE_CHECKING
from typing import Union
from zipfile import ZipFile
from zipfile import ZipInfo
from pip._internal.exceptions import InstallationError
from pip._internal.locations import get_major_minor_version
from pip._internal.metadata import (
BaseDistribution,
FilesystemWheel,
get_wheel_distribution,
)
from pip._internal.models.direct_url import DIRECT_URL_METADATA_NAME, DirectUrl
from pip._internal.models.scheme import SCHEME_KEYS, Scheme
from pip._internal.utils.filesystem import adjacent_tmp_file, replace
from pip._internal.utils.misc import captured_stdout, ensure_dir, hash_file, partition
from pip._internal.utils.unpacking import (
current_umask,
is_within_directory,
set_extracted_file_to_default_mode_plus_executable,
zip_item_is_executable,
)
from pip._internal.metadata import BaseDistribution
from pip._internal.metadata import FilesystemWheel
from pip._internal.metadata import get_wheel_distribution
from pip._internal.models.direct_url import DIRECT_URL_METADATA_NAME
from pip._internal.models.direct_url import DirectUrl
from pip._internal.models.scheme import Scheme
from pip._internal.models.scheme import SCHEME_KEYS
from pip._internal.utils.filesystem import adjacent_tmp_file
from pip._internal.utils.filesystem import replace
from pip._internal.utils.misc import captured_stdout
from pip._internal.utils.misc import ensure_dir
from pip._internal.utils.misc import hash_file
from pip._internal.utils.misc import partition
from pip._internal.utils.unpacking import current_umask
from pip._internal.utils.unpacking import is_within_directory
from pip._internal.utils.unpacking import set_extracted_file_to_default_mode_plus_executable
from pip._internal.utils.unpacking import zip_item_is_executable
from pip._internal.utils.wheel import parse_wheel
from pip._vendor.distlib.scripts import ScriptMaker
from pip._vendor.distlib.util import get_export_entry
from pip._vendor.packaging.utils import canonicalize_name
if TYPE_CHECKING:
from typing import Protocol
class File(Protocol):
src_record_path: "RecordPath"
src_record_path: RecordPath
dest_path: str
changed: bool
@ -72,22 +75,22 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
RecordPath = NewType("RecordPath", str)
RecordPath = NewType('RecordPath', str)
InstalledCSVRow = Tuple[RecordPath, str, Union[int, str]]
def rehash(path: str, blocksize: int = 1 << 20) -> Tuple[str, str]:
def rehash(path: str, blocksize: int = 1 << 20) -> tuple[str, str]:
"""Return (encoded_digest, length) for path using hashlib.sha256()"""
h, length = hash_file(path, blocksize)
digest = "sha256=" + urlsafe_b64encode(h.digest()).decode("latin1").rstrip("=")
digest = 'sha256=' + urlsafe_b64encode(h.digest()).decode('latin1').rstrip('=')
return (digest, str(length))
def csv_io_kwargs(mode: str) -> Dict[str, Any]:
def csv_io_kwargs(mode: str) -> dict[str, Any]:
"""Return keyword arguments to properly open a CSV file
in the given mode.
"""
return {"mode": mode, "newline": "", "encoding": "utf-8"}
return {'mode': mode, 'newline': '', 'encoding': 'utf-8'}
def fix_script(path: str) -> bool:
@ -97,35 +100,35 @@ def fix_script(path: str) -> bool:
# XXX RECORD hashes will need to be updated
assert os.path.isfile(path)
with open(path, "rb") as script:
with open(path, 'rb') as script:
firstline = script.readline()
if not firstline.startswith(b"#!python"):
if not firstline.startswith(b'#!python'):
return False
exename = sys.executable.encode(sys.getfilesystemencoding())
firstline = b"#!" + exename + os.linesep.encode("ascii")
firstline = b'#!' + exename + os.linesep.encode('ascii')
rest = script.read()
with open(path, "wb") as script:
with open(path, 'wb') as script:
script.write(firstline)
script.write(rest)
return True
def wheel_root_is_purelib(metadata: Message) -> bool:
return metadata.get("Root-Is-Purelib", "").lower() == "true"
return metadata.get('Root-Is-Purelib', '').lower() == 'true'
def get_entrypoints(dist: BaseDistribution) -> Tuple[Dict[str, str], Dict[str, str]]:
def get_entrypoints(dist: BaseDistribution) -> tuple[dict[str, str], dict[str, str]]:
console_scripts = {}
gui_scripts = {}
for entry_point in dist.iter_entry_points():
if entry_point.group == "console_scripts":
if entry_point.group == 'console_scripts':
console_scripts[entry_point.name] = entry_point.value
elif entry_point.group == "gui_scripts":
elif entry_point.group == 'gui_scripts':
gui_scripts[entry_point.name] = entry_point.value
return console_scripts, gui_scripts
def message_about_scripts_not_on_PATH(scripts: Sequence[str]) -> Optional[str]:
def message_about_scripts_not_on_PATH(scripts: Sequence[str]) -> str | None:
"""Determine if any scripts are not on PATH and format a warning.
Returns a warning message if one or more scripts are not on PATH,
otherwise None.
@ -134,7 +137,7 @@ def message_about_scripts_not_on_PATH(scripts: Sequence[str]) -> Optional[str]:
return None
# Group scripts by the path they were installed in
grouped_by_dir: Dict[str, Set[str]] = collections.defaultdict(set)
grouped_by_dir: dict[str, set[str]] = collections.defaultdict(set)
for destfile in scripts:
parent_dir = os.path.dirname(destfile)
script_name = os.path.basename(destfile)
@ -143,12 +146,12 @@ def message_about_scripts_not_on_PATH(scripts: Sequence[str]) -> Optional[str]:
# We don't want to warn for directories that are on PATH.
not_warn_dirs = [
os.path.normcase(i).rstrip(os.sep)
for i in os.environ.get("PATH", "").split(os.pathsep)
for i in os.environ.get('PATH', '').split(os.pathsep)
]
# If an executable sits with sys.executable, we don't warn for it.
# This covers the case of venv invocations without activating the venv.
not_warn_dirs.append(os.path.normcase(os.path.dirname(sys.executable)))
warn_for: Dict[str, Set[str]] = {
warn_for: dict[str, set[str]] = {
parent_dir: scripts
for parent_dir, scripts in grouped_by_dir.items()
if os.path.normcase(parent_dir) not in not_warn_dirs
@ -159,47 +162,47 @@ def message_about_scripts_not_on_PATH(scripts: Sequence[str]) -> Optional[str]:
# Format a message
msg_lines = []
for parent_dir, dir_scripts in warn_for.items():
sorted_scripts: List[str] = sorted(dir_scripts)
sorted_scripts: list[str] = sorted(dir_scripts)
if len(sorted_scripts) == 1:
start_text = "script {} is".format(sorted_scripts[0])
start_text = f'script {sorted_scripts[0]} is'
else:
start_text = "scripts {} are".format(
", ".join(sorted_scripts[:-1]) + " and " + sorted_scripts[-1]
start_text = 'scripts {} are'.format(
', '.join(sorted_scripts[:-1]) + ' and ' + sorted_scripts[-1],
)
msg_lines.append(
"The {} installed in '{}' which is not on PATH.".format(
start_text, parent_dir
)
start_text, parent_dir,
),
)
last_line_fmt = (
"Consider adding {} to PATH or, if you prefer "
"to suppress this warning, use --no-warn-script-location."
'Consider adding {} to PATH or, if you prefer '
'to suppress this warning, use --no-warn-script-location.'
)
if len(msg_lines) == 1:
msg_lines.append(last_line_fmt.format("this directory"))
msg_lines.append(last_line_fmt.format('this directory'))
else:
msg_lines.append(last_line_fmt.format("these directories"))
msg_lines.append(last_line_fmt.format('these directories'))
# Add a note if any directory starts with ~
warn_for_tilde = any(
i[0] == "~" for i in os.environ.get("PATH", "").split(os.pathsep) if i
i[0] == '~' for i in os.environ.get('PATH', '').split(os.pathsep) if i
)
if warn_for_tilde:
tilde_warning_msg = (
"NOTE: The current PATH contains path(s) starting with `~`, "
"which may not be expanded by all applications."
'NOTE: The current PATH contains path(s) starting with `~`, '
'which may not be expanded by all applications.'
)
msg_lines.append(tilde_warning_msg)
# Returns the formatted multiline message
return "\n".join(msg_lines)
return '\n'.join(msg_lines)
def _normalized_outrows(
outrows: Iterable[InstalledCSVRow],
) -> List[Tuple[str, str, str]]:
) -> list[tuple[str, str, str]]:
"""Normalize the given rows of a RECORD file.
Items in each row are converted into str. Rows are then sorted to make
@ -227,52 +230,52 @@ def _record_to_fs_path(record_path: RecordPath) -> str:
return record_path
def _fs_to_record_path(path: str, relative_to: Optional[str] = None) -> RecordPath:
def _fs_to_record_path(path: str, relative_to: str | None = None) -> RecordPath:
if relative_to is not None:
# On Windows, do not handle relative paths if they belong to different
# logical disks
if (
os.path.splitdrive(path)[0].lower()
== os.path.splitdrive(relative_to)[0].lower()
os.path.splitdrive(path)[0].lower() ==
os.path.splitdrive(relative_to)[0].lower()
):
path = os.path.relpath(path, relative_to)
path = path.replace(os.path.sep, "/")
return cast("RecordPath", path)
path = path.replace(os.path.sep, '/')
return cast('RecordPath', path)
def get_csv_rows_for_installed(
old_csv_rows: List[List[str]],
installed: Dict[RecordPath, RecordPath],
changed: Set[RecordPath],
generated: List[str],
old_csv_rows: list[list[str]],
installed: dict[RecordPath, RecordPath],
changed: set[RecordPath],
generated: list[str],
lib_dir: str,
) -> List[InstalledCSVRow]:
) -> list[InstalledCSVRow]:
"""
:param installed: A map from archive RECORD path to installation RECORD
path.
"""
installed_rows: List[InstalledCSVRow] = []
installed_rows: list[InstalledCSVRow] = []
for row in old_csv_rows:
if len(row) > 3:
logger.warning("RECORD line has more than three elements: %s", row)
old_record_path = cast("RecordPath", row[0])
logger.warning('RECORD line has more than three elements: %s', row)
old_record_path = cast('RecordPath', row[0])
new_record_path = installed.pop(old_record_path, old_record_path)
if new_record_path in changed:
digest, length = rehash(_record_to_fs_path(new_record_path))
else:
digest = row[1] if len(row) > 1 else ""
length = row[2] if len(row) > 2 else ""
digest = row[1] if len(row) > 1 else ''
length = row[2] if len(row) > 2 else ''
installed_rows.append((new_record_path, digest, length))
for f in generated:
path = _fs_to_record_path(f, lib_dir)
digest, length = rehash(f)
installed_rows.append((path, digest, length))
for installed_record_path in installed.values():
installed_rows.append((installed_record_path, "", ""))
installed_rows.append((installed_record_path, '', ''))
return installed_rows
def get_console_script_specs(console: Dict[str, str]) -> List[str]:
def get_console_script_specs(console: dict[str, str]) -> list[str]:
"""
Given the mapping from entrypoint name to callable, return the relevant
console script specs.
@ -315,47 +318,47 @@ def get_console_script_specs(console: Dict[str, str]) -> List[str]:
# DEFAULT
# - The default behavior is to install pip, pipX, pipX.Y, easy_install
# and easy_install-X.Y.
pip_script = console.pop("pip", None)
pip_script = console.pop('pip', None)
if pip_script:
if "ENSUREPIP_OPTIONS" not in os.environ:
scripts_to_generate.append("pip = " + pip_script)
if 'ENSUREPIP_OPTIONS' not in os.environ:
scripts_to_generate.append('pip = ' + pip_script)
if os.environ.get("ENSUREPIP_OPTIONS", "") != "altinstall":
if os.environ.get('ENSUREPIP_OPTIONS', '') != 'altinstall':
scripts_to_generate.append(
"pip{} = {}".format(sys.version_info[0], pip_script)
f'pip{sys.version_info[0]} = {pip_script}',
)
scripts_to_generate.append(f"pip{get_major_minor_version()} = {pip_script}")
scripts_to_generate.append(f'pip{get_major_minor_version()} = {pip_script}')
# Delete any other versioned pip entry points
pip_ep = [k for k in console if re.match(r"pip(\d(\.\d)?)?$", k)]
pip_ep = [k for k in console if re.match(r'pip(\d(\.\d)?)?$', k)]
for k in pip_ep:
del console[k]
easy_install_script = console.pop("easy_install", None)
easy_install_script = console.pop('easy_install', None)
if easy_install_script:
if "ENSUREPIP_OPTIONS" not in os.environ:
scripts_to_generate.append("easy_install = " + easy_install_script)
if 'ENSUREPIP_OPTIONS' not in os.environ:
scripts_to_generate.append('easy_install = ' + easy_install_script)
scripts_to_generate.append(
"easy_install-{} = {}".format(
get_major_minor_version(), easy_install_script
)
'easy_install-{} = {}'.format(
get_major_minor_version(), easy_install_script,
),
)
# Delete any other versioned easy_install entry points
easy_install_ep = [
k for k in console if re.match(r"easy_install(-\d\.\d)?$", k)
k for k in console if re.match(r'easy_install(-\d\.\d)?$', k)
]
for k in easy_install_ep:
del console[k]
# Generate the console entry points specified in the wheel
scripts_to_generate.extend(starmap("{} = {}".format, console.items()))
scripts_to_generate.extend(starmap('{} = {}'.format, console.items()))
return scripts_to_generate
class ZipBackedFile:
def __init__(
self, src_record_path: RecordPath, dest_path: str, zip_file: ZipFile
self, src_record_path: RecordPath, dest_path: str, zip_file: ZipFile,
) -> None:
self.src_record_path = src_record_path
self.dest_path = dest_path
@ -386,7 +389,7 @@ class ZipBackedFile:
zipinfo = self._getinfo()
with self._zip_file.open(zipinfo) as f:
with open(self.dest_path, "wb") as dest:
with open(self.dest_path, 'wb') as dest:
shutil.copyfileobj(f, dest)
if zip_item_is_executable(zipinfo):
@ -394,7 +397,7 @@ class ZipBackedFile:
class ScriptFile:
def __init__(self, file: "File") -> None:
def __init__(self, file: File) -> None:
self._file = file
self.src_record_path = self._file.src_record_path
self.dest_path = self._file.dest_path
@ -408,10 +411,10 @@ class ScriptFile:
class MissingCallableSuffix(InstallationError):
def __init__(self, entry_point: str) -> None:
super().__init__(
"Invalid script entry point: {} - A callable "
"suffix is required. Cf https://packaging.python.org/"
"specifications/entry-points/#use-for-scripts for more "
"information.".format(entry_point)
'Invalid script entry point: {} - A callable '
'suffix is required. Cf https://packaging.python.org/'
'specifications/entry-points/#use-for-scripts for more '
'information.'.format(entry_point),
)
@ -422,7 +425,7 @@ def _raise_for_invalid_entrypoint(specification: str) -> None:
class PipScriptMaker(ScriptMaker):
def make(self, specification: str, options: Dict[str, Any] = None) -> List[str]:
def make(self, specification: str, options: dict[str, Any] = None) -> list[str]:
_raise_for_invalid_entrypoint(specification)
return super().make(specification, options)
@ -434,7 +437,7 @@ def _install_wheel(
scheme: Scheme,
pycompile: bool = True,
warn_script_location: bool = True,
direct_url: Optional[DirectUrl] = None,
direct_url: DirectUrl | None = None,
requested: bool = False,
) -> None:
"""Install a wheel.
@ -463,12 +466,12 @@ def _install_wheel(
# installed = files copied from the wheel to the destination
# changed = files changed while installing (scripts #! line typically)
# generated = files newly generated during the install (script wrappers)
installed: Dict[RecordPath, RecordPath] = {}
changed: Set[RecordPath] = set()
generated: List[str] = []
installed: dict[RecordPath, RecordPath] = {}
changed: set[RecordPath] = set()
generated: list[str] = []
def record_installed(
srcfile: RecordPath, destfile: str, modified: bool = False
srcfile: RecordPath, destfile: str, modified: bool = False,
) -> None:
"""Map archive RECORD paths to installation RECORD paths."""
newpath = _fs_to_record_path(destfile, lib_dir)
@ -477,22 +480,22 @@ def _install_wheel(
changed.add(_fs_to_record_path(destfile))
def is_dir_path(path: RecordPath) -> bool:
return path.endswith("/")
return path.endswith('/')
def assert_no_path_traversal(dest_dir_path: str, target_path: str) -> None:
if not is_within_directory(dest_dir_path, target_path):
message = (
"The wheel {!r} has a file {!r} trying to install"
" outside the target directory {!r}"
'The wheel {!r} has a file {!r} trying to install'
' outside the target directory {!r}'
)
raise InstallationError(
message.format(wheel_path, target_path, dest_dir_path)
message.format(wheel_path, target_path, dest_dir_path),
)
def root_scheme_file_maker(
zip_file: ZipFile, dest: str
) -> Callable[[RecordPath], "File"]:
def make_root_scheme_file(record_path: RecordPath) -> "File":
zip_file: ZipFile, dest: str,
) -> Callable[[RecordPath], File]:
def make_root_scheme_file(record_path: RecordPath) -> File:
normed_path = os.path.normpath(record_path)
dest_path = os.path.join(dest, normed_path)
assert_no_path_traversal(dest, dest_path)
@ -501,17 +504,17 @@ def _install_wheel(
return make_root_scheme_file
def data_scheme_file_maker(
zip_file: ZipFile, scheme: Scheme
) -> Callable[[RecordPath], "File"]:
zip_file: ZipFile, scheme: Scheme,
) -> Callable[[RecordPath], File]:
scheme_paths = {key: getattr(scheme, key) for key in SCHEME_KEYS}
def make_data_scheme_file(record_path: RecordPath) -> "File":
def make_data_scheme_file(record_path: RecordPath) -> File:
normed_path = os.path.normpath(record_path)
try:
_, scheme_key, dest_subpath = normed_path.split(os.path.sep, 2)
except ValueError:
message = (
"Unexpected file in {}: {!r}. .data directory contents"
'Unexpected file in {}: {!r}. .data directory contents'
" should be named like: '<scheme key>/<path>'."
).format(wheel_path, record_path)
raise InstallationError(message)
@ -519,11 +522,11 @@ def _install_wheel(
try:
scheme_path = scheme_paths[scheme_key]
except KeyError:
valid_scheme_keys = ", ".join(sorted(scheme_paths))
valid_scheme_keys = ', '.join(sorted(scheme_paths))
message = (
"Unknown scheme key used in {}: {} (for file {!r}). .data"
" directory contents should be in subdirectories named"
" with a valid scheme key ({})"
'Unknown scheme key used in {}: {} (for file {!r}). .data'
' directory contents should be in subdirectories named'
' with a valid scheme key ({})'
).format(wheel_path, scheme_key, record_path, valid_scheme_keys)
raise InstallationError(message)
@ -534,7 +537,7 @@ def _install_wheel(
return make_data_scheme_file
def is_data_scheme_path(path: RecordPath) -> bool:
return path.split("/", 1)[0].endswith(".data")
return path.split('/', 1)[0].endswith('.data')
paths = cast(List[RecordPath], wheel_zip.namelist())
file_paths = filterfalse(is_dir_path, paths)
@ -544,11 +547,11 @@ def _install_wheel(
files: Iterator[File] = map(make_root_scheme_file, root_scheme_paths)
def is_script_scheme_path(path: RecordPath) -> bool:
parts = path.split("/", 2)
return len(parts) > 2 and parts[0].endswith(".data") and parts[1] == "scripts"
parts = path.split('/', 2)
return len(parts) > 2 and parts[0].endswith('.data') and parts[1] == 'scripts'
other_scheme_paths, script_scheme_paths = partition(
is_script_scheme_path, data_scheme_paths
is_script_scheme_path, data_scheme_paths,
)
make_data_scheme_file = data_scheme_file_maker(wheel_zip, scheme)
@ -562,16 +565,16 @@ def _install_wheel(
)
console, gui = get_entrypoints(distribution)
def is_entrypoint_wrapper(file: "File") -> bool:
def is_entrypoint_wrapper(file: File) -> bool:
# EP, EP.exe and EP-script.py are scripts generated for
# entry point EP by setuptools
path = file.dest_path
name = os.path.basename(path)
if name.lower().endswith(".exe"):
if name.lower().endswith('.exe'):
matchname = name[:-4]
elif name.lower().endswith("-script.py"):
elif name.lower().endswith('-script.py'):
matchname = name[:-10]
elif name.lower().endswith(".pya"):
elif name.lower().endswith('.pya'):
matchname = name[:-4]
else:
matchname = name
@ -579,7 +582,7 @@ def _install_wheel(
return matchname in console or matchname in gui
script_scheme_files: Iterator[File] = map(
make_data_scheme_file, script_scheme_paths
make_data_scheme_file, script_scheme_paths,
)
script_scheme_files = filterfalse(is_entrypoint_wrapper, script_scheme_files)
script_scheme_files = map(ScriptFile, script_scheme_files)
@ -598,7 +601,7 @@ def _install_wheel(
full_installed_path = os.path.join(lib_dir, installed_path)
if not os.path.isfile(full_installed_path):
continue
if not full_installed_path.endswith(".py"):
if not full_installed_path.endswith('.py'):
continue
yield full_installed_path
@ -610,14 +613,14 @@ def _install_wheel(
if pycompile:
with captured_stdout() as stdout:
with warnings.catch_warnings():
warnings.filterwarnings("ignore")
warnings.filterwarnings('ignore')
for path in pyc_source_file_paths():
success = compileall.compile_file(path, force=True, quiet=True)
if success:
pyc_path = pyc_output_path(path)
assert os.path.exists(pyc_path)
pyc_record_path = cast(
"RecordPath", pyc_path.replace(os.path.sep, "/")
'RecordPath', pyc_path.replace(os.path.sep, '/'),
)
record_installed(pyc_record_path, pyc_path)
logger.debug(stdout.getvalue())
@ -631,7 +634,7 @@ def _install_wheel(
# Ensure we don't generate any variants for scripts because this is almost
# never what somebody wants.
# See https://bitbucket.org/pypa/distlib/issue/35/
maker.variants = {""}
maker.variants = {''}
# This is required because otherwise distlib creates scripts that are not
# executable.
@ -641,12 +644,12 @@ def _install_wheel(
# Generate the console and GUI entry points specified in the wheel
scripts_to_generate = get_console_script_specs(console)
gui_scripts_to_generate = list(starmap("{} = {}".format, gui.items()))
gui_scripts_to_generate = list(starmap('{} = {}'.format, gui.items()))
generated_console_scripts = maker.make_multiple(scripts_to_generate)
generated.extend(generated_console_scripts)
generated.extend(maker.make_multiple(gui_scripts_to_generate, {"gui": True}))
generated.extend(maker.make_multiple(gui_scripts_to_generate, {'gui': True}))
if warn_script_location:
msg = message_about_scripts_not_on_PATH(generated_console_scripts)
@ -665,26 +668,26 @@ def _install_wheel(
dest_info_dir = os.path.join(lib_dir, info_dir)
# Record pip as the installer
installer_path = os.path.join(dest_info_dir, "INSTALLER")
installer_path = os.path.join(dest_info_dir, 'INSTALLER')
with _generate_file(installer_path) as installer_file:
installer_file.write(b"pip\n")
installer_file.write(b'pip\n')
generated.append(installer_path)
# Record the PEP 610 direct URL reference
if direct_url is not None:
direct_url_path = os.path.join(dest_info_dir, DIRECT_URL_METADATA_NAME)
with _generate_file(direct_url_path) as direct_url_file:
direct_url_file.write(direct_url.to_json().encode("utf-8"))
direct_url_file.write(direct_url.to_json().encode('utf-8'))
generated.append(direct_url_path)
# Record the REQUESTED file
if requested:
requested_path = os.path.join(dest_info_dir, "REQUESTED")
with open(requested_path, "wb"):
requested_path = os.path.join(dest_info_dir, 'REQUESTED')
with open(requested_path, 'wb'):
pass
generated.append(requested_path)
record_text = distribution.read_text("RECORD")
record_text = distribution.read_text('RECORD')
record_rows = list(csv.reader(record_text.splitlines()))
rows = get_csv_rows_for_installed(
@ -696,12 +699,12 @@ def _install_wheel(
)
# Record details of all files installed
record_path = os.path.join(dest_info_dir, "RECORD")
record_path = os.path.join(dest_info_dir, 'RECORD')
with _generate_file(record_path, **csv_io_kwargs("w")) as record_file:
with _generate_file(record_path, **csv_io_kwargs('w')) as record_file:
# Explicitly cast to typing.IO[str] as a workaround for the mypy error:
# "writer" has incompatible type "BinaryIO"; expected "_Writer"
writer = csv.writer(cast("IO[str]", record_file))
writer = csv.writer(cast('IO[str]', record_file))
writer.writerows(_normalized_outrows(rows))
@ -710,7 +713,7 @@ def req_error_context(req_description: str) -> Iterator[None]:
try:
yield
except InstallationError as e:
message = "For req: {}. {}".format(req_description, e.args[0])
message = f'For req: {req_description}. {e.args[0]}'
raise InstallationError(message) from e
@ -721,7 +724,7 @@ def install_wheel(
req_description: str,
pycompile: bool = True,
warn_script_location: bool = True,
direct_url: Optional[DirectUrl] = None,
direct_url: DirectUrl | None = None,
requested: bool = False,
) -> None:
with ZipFile(wheel_path, allowZip64=True) as z:

View file

@ -1,47 +1,50 @@
"""Prepares a distribution for installation
"""
# The following comment should be removed at some point in the future.
# mypy: strict-optional=False
from __future__ import annotations
import logging
import mimetypes
import os
import shutil
from typing import Dict, Iterable, List, Optional
from pip._vendor.packaging.utils import canonicalize_name
from typing import Dict
from typing import Iterable
from typing import List
from typing import Optional
from pip._internal.distributions import make_distribution_for_install_requirement
from pip._internal.distributions.installed import InstalledDistribution
from pip._internal.exceptions import (
DirectoryUrlHashUnsupported,
HashMismatch,
HashUnpinned,
InstallationError,
NetworkConnectionError,
PreviousBuildDirError,
VcsHashUnsupported,
)
from pip._internal.exceptions import DirectoryUrlHashUnsupported
from pip._internal.exceptions import HashMismatch
from pip._internal.exceptions import HashUnpinned
from pip._internal.exceptions import InstallationError
from pip._internal.exceptions import NetworkConnectionError
from pip._internal.exceptions import PreviousBuildDirError
from pip._internal.exceptions import VcsHashUnsupported
from pip._internal.index.package_finder import PackageFinder
from pip._internal.metadata import BaseDistribution
from pip._internal.models.link import Link
from pip._internal.models.wheel import Wheel
from pip._internal.network.download import BatchDownloader, Downloader
from pip._internal.network.lazy_wheel import (
HTTPRangeRequestUnsupported,
dist_from_wheel_url,
)
from pip._internal.network.download import BatchDownloader
from pip._internal.network.download import Downloader
from pip._internal.network.lazy_wheel import dist_from_wheel_url
from pip._internal.network.lazy_wheel import HTTPRangeRequestUnsupported
from pip._internal.network.session import PipSession
from pip._internal.req.req_install import InstallRequirement
from pip._internal.req.req_tracker import RequirementTracker
from pip._internal.utils.filesystem import copy2_fixed
from pip._internal.utils.hashes import Hashes, MissingHashes
from pip._internal.utils.hashes import Hashes
from pip._internal.utils.hashes import MissingHashes
from pip._internal.utils.logging import indent_log
from pip._internal.utils.misc import display_path, hide_url, is_installable_dir, rmtree
from pip._internal.utils.misc import display_path
from pip._internal.utils.misc import hide_url
from pip._internal.utils.misc import is_installable_dir
from pip._internal.utils.misc import rmtree
from pip._internal.utils.temp_dir import TempDirectory
from pip._internal.utils.unpacking import unpack_file
from pip._internal.vcs import vcs
from pip._vendor.packaging.utils import canonicalize_name
logger = logging.getLogger(__name__)
@ -66,7 +69,7 @@ def unpack_vcs_link(link: Link, location: str, verbosity: int) -> None:
class File:
def __init__(self, path: str, content_type: Optional[str]) -> None:
def __init__(self, path: str, content_type: str | None) -> None:
self.path = path
if content_type is None:
self.content_type = mimetypes.guess_type(path)[0]
@ -77,10 +80,10 @@ class File:
def get_http_url(
link: Link,
download: Downloader,
download_dir: Optional[str] = None,
hashes: Optional[Hashes] = None,
download_dir: str | None = None,
hashes: Hashes | None = None,
) -> File:
temp_dir = TempDirectory(kind="unpack", globally_managed=True)
temp_dir = TempDirectory(kind='unpack', globally_managed=True)
# If a download dir is specified, is the file already downloaded there?
already_downloaded_path = None
if download_dir:
@ -123,14 +126,14 @@ def _copy_source_tree(source: str, target: str) -> None:
target_basename = os.path.basename(target_abspath)
target_dirname = os.path.dirname(target_abspath)
def ignore(d: str, names: List[str]) -> List[str]:
skipped: List[str] = []
def ignore(d: str, names: list[str]) -> list[str]:
skipped: list[str] = []
if d == source:
# Pulling in those directories can potentially be very slow,
# exclude the following directories if they appear in the top
# level dir (and only it).
# See discussion at https://github.com/pypa/pip/pull/6770
skipped += [".tox", ".nox"]
skipped += ['.tox', '.nox']
if os.path.abspath(d) == target_dirname:
# Prevent an infinite recursion if the target is in source.
# This can happen when TMPDIR is set to ${PWD}/...
@ -148,7 +151,7 @@ def _copy_source_tree(source: str, target: str) -> None:
def get_file_url(
link: Link, download_dir: Optional[str] = None, hashes: Optional[Hashes] = None
link: Link, download_dir: str | None = None, hashes: Hashes | None = None,
) -> File:
"""Get file and optionally check its hash."""
# If a download dir is specified, is the file already there and valid?
@ -176,9 +179,9 @@ def unpack_url(
location: str,
download: Downloader,
verbosity: int,
download_dir: Optional[str] = None,
hashes: Optional[Hashes] = None,
) -> Optional[File]:
download_dir: str | None = None,
hashes: Hashes | None = None,
) -> File | None:
"""Unpack link into location, downloading if required.
:param hashes: A Hashes object, one of whose embedded hashes must match,
@ -227,8 +230,8 @@ def unpack_url(
def _check_download_dir(
link: Link, download_dir: str, hashes: Optional[Hashes]
) -> Optional[str]:
link: Link, download_dir: str, hashes: Hashes | None,
) -> str | None:
"""Check download_dir for previously downloaded file with correct hash
If a correct file is found return its path else None
"""
@ -238,13 +241,13 @@ def _check_download_dir(
return None
# If already downloaded, does its hash match?
logger.info("File was already downloaded %s", download_path)
logger.info('File was already downloaded %s', download_path)
if hashes:
try:
hashes.check_against_path(download_path)
except HashMismatch:
logger.warning(
"Previously-downloaded file %s has bad hash. Re-downloading.",
'Previously-downloaded file %s has bad hash. Re-downloading.',
download_path,
)
os.unlink(download_path)
@ -258,7 +261,7 @@ class RequirementPreparer:
def __init__(
self,
build_dir: str,
download_dir: Optional[str],
download_dir: str | None,
src_dir: str,
build_isolation: bool,
req_tracker: RequirementTracker,
@ -304,18 +307,18 @@ class RequirementPreparer:
self.in_tree_build = in_tree_build
# Memoized downloaded files, as mapping of url: path.
self._downloaded: Dict[str, str] = {}
self._downloaded: dict[str, str] = {}
# Previous "header" printed for a link-based InstallRequirement
self._previous_requirement_header = ("", "")
self._previous_requirement_header = ('', '')
def _log_preparing_link(self, req: InstallRequirement) -> None:
"""Provide context for the requirement being prepared."""
if req.link.is_file and not req.original_link_is_in_wheel_cache:
message = "Processing %s"
message = 'Processing %s'
information = str(display_path(req.link.file_path))
else:
message = "Collecting %s"
message = 'Collecting %s'
information = str(req.req or req)
if (message, information) != self._previous_requirement_header:
@ -324,10 +327,10 @@ class RequirementPreparer:
if req.original_link_is_in_wheel_cache:
with indent_log():
logger.info("Using cached %s", req.link.filename)
logger.info('Using cached %s', req.link.filename)
def _ensure_link_req_src_dir(
self, req: InstallRequirement, parallel_builds: bool
self, req: InstallRequirement, parallel_builds: bool,
) -> None:
"""Ensure source_dir of a linked InstallRequirement."""
# Since source_dir is only set for editable requirements.
@ -357,10 +360,10 @@ class RequirementPreparer:
if is_installable_dir(req.source_dir):
raise PreviousBuildDirError(
"pip can't proceed with requirements '{}' due to a"
"pre-existing build directory ({}). This is likely "
"due to a previous installation that failed . pip is "
"being responsible and not assuming it can delete this. "
"Please delete it and try again.".format(req, req.source_dir)
'pre-existing build directory ({}). This is likely '
'due to a previous installation that failed . pip is '
'being responsible and not assuming it can delete this. '
'Please delete it and try again.'.format(req, req.source_dir),
)
def _get_linked_req_hashes(self, req: InstallRequirement) -> Hashes:
@ -398,16 +401,16 @@ class RequirementPreparer:
def _fetch_metadata_using_lazy_wheel(
self,
link: Link,
) -> Optional[BaseDistribution]:
) -> BaseDistribution | None:
"""Fetch metadata using lazy wheel, if possible."""
if not self.use_lazy_wheel:
return None
if self.require_hashes:
logger.debug("Lazy wheel is not used as hash checking is required")
logger.debug('Lazy wheel is not used as hash checking is required')
return None
if link.is_file or not link.is_wheel:
logger.debug(
"Lazy wheel is not used as %r does not points to a remote wheel",
'Lazy wheel is not used as %r does not points to a remote wheel',
link,
)
return None
@ -415,15 +418,15 @@ class RequirementPreparer:
wheel = Wheel(link.filename)
name = canonicalize_name(wheel.name)
logger.info(
"Obtaining dependency information from %s %s",
'Obtaining dependency information from %s %s',
name,
wheel.version,
)
url = link.url.split("#", 1)[0]
url = link.url.split('#', 1)[0]
try:
return dist_from_wheel_url(name, url, self._session)
except HTTPRangeRequestUnsupported:
logger.debug("%s does not support range requests", url)
logger.debug('%s does not support range requests', url)
return None
def _complete_partial_requirements(
@ -434,12 +437,12 @@ class RequirementPreparer:
"""Download any requirements which were only fetched by metadata."""
# Download to a temporary directory. These will be copied over as
# needed for downstream 'download', 'wheel', and 'install' commands.
temp_dir = TempDirectory(kind="unpack", globally_managed=True).path
temp_dir = TempDirectory(kind='unpack', globally_managed=True).path
# Map each link to the requirement that owns it. This allows us to set
# `req.local_file_path` on the appropriate requirement after passing
# all the links at once into BatchDownloader.
links_to_fully_download: Dict[Link, InstallRequirement] = {}
links_to_fully_download: dict[Link, InstallRequirement] = {}
for req in partially_downloaded_reqs:
assert req.link
links_to_fully_download[req.link] = req
@ -449,7 +452,7 @@ class RequirementPreparer:
temp_dir,
)
for link, (filepath, _) in batch_download:
logger.debug("Downloading link %s to %s", link, filepath)
logger.debug('Downloading link %s to %s', link, filepath)
req = links_to_fully_download[link]
req.local_file_path = filepath
@ -459,7 +462,7 @@ class RequirementPreparer:
self._prepare_linked_requirement(req, parallel_builds)
def prepare_linked_requirement(
self, req: InstallRequirement, parallel_builds: bool = False
self, req: InstallRequirement, parallel_builds: bool = False,
) -> BaseDistribution:
"""Prepare a requirement to be obtained from req.link."""
assert req.link
@ -487,7 +490,7 @@ class RequirementPreparer:
return self._prepare_linked_requirement(req, parallel_builds)
def prepare_linked_requirements_more(
self, reqs: Iterable[InstallRequirement], parallel_builds: bool = False
self, reqs: Iterable[InstallRequirement], parallel_builds: bool = False,
) -> None:
"""Prepare linked requirements more, if needed."""
reqs = [req for req in reqs if req.needs_more_preparation]
@ -502,7 +505,7 @@ class RequirementPreparer:
# Prepare requirements we found were already downloaded for some
# reason. The other downloads will be completed separately.
partially_downloaded_reqs: List[InstallRequirement] = []
partially_downloaded_reqs: list[InstallRequirement] = []
for req in reqs:
if req.needs_more_preparation:
partially_downloaded_reqs.append(req)
@ -517,7 +520,7 @@ class RequirementPreparer:
)
def _prepare_linked_requirement(
self, req: InstallRequirement, parallel_builds: bool
self, req: InstallRequirement, parallel_builds: bool,
) -> BaseDistribution:
assert req.link
link = req.link
@ -539,8 +542,8 @@ class RequirementPreparer:
)
except NetworkConnectionError as exc:
raise InstallationError(
"Could not install requirement {} because of HTTP "
"error {} for URL {}".format(req, exc, link)
'Could not install requirement {} because of HTTP '
'error {} for URL {}'.format(req, exc, link),
)
else:
file_path = self._downloaded[link.url]
@ -572,8 +575,8 @@ class RequirementPreparer:
if link.is_existing_dir():
logger.debug(
"Not copying link to destination directory "
"since it is a directory: %s",
'Not copying link to destination directory '
'since it is a directory: %s',
link,
)
return
@ -585,23 +588,23 @@ class RequirementPreparer:
if not os.path.exists(download_location):
shutil.copy(req.local_file_path, download_location)
download_path = display_path(download_location)
logger.info("Saved %s", download_path)
logger.info('Saved %s', download_path)
def prepare_editable_requirement(
self,
req: InstallRequirement,
) -> BaseDistribution:
"""Prepare an editable requirement."""
assert req.editable, "cannot prepare a non-editable req as editable"
assert req.editable, 'cannot prepare a non-editable req as editable'
logger.info("Obtaining %s", req)
logger.info('Obtaining %s', req)
with indent_log():
if self.require_hashes:
raise InstallationError(
"The editable requirement {} cannot be installed when "
"requiring hashes, because there is no single file to "
"hash.".format(req)
'The editable requirement {} cannot be installed when '
'requiring hashes, because there is no single file to '
'hash.'.format(req),
)
req.ensure_has_source_dir(self.src_dir)
req.update_editable()
@ -625,18 +628,18 @@ class RequirementPreparer:
"""Prepare an already-installed requirement."""
assert req.satisfied_by, "req should have been satisfied but isn't"
assert skip_reason is not None, (
"did not get skip reason skipped but req.satisfied_by "
"is set to {}".format(req.satisfied_by)
'did not get skip reason skipped but req.satisfied_by '
'is set to {}'.format(req.satisfied_by)
)
logger.info(
"Requirement %s: %s (%s)", skip_reason, req, req.satisfied_by.version
'Requirement %s: %s (%s)', skip_reason, req, req.satisfied_by.version,
)
with indent_log():
if self.require_hashes:
logger.debug(
"Since it is already installed, we are trusting this "
"package without checking its hash. To ensure a "
"completely repeatable environment, install into an "
"empty virtualenv."
'Since it is already installed, we are trusting this '
'package without checking its hash. To ensure a '
'completely repeatable environment, install into an '
'empty virtualenv.',
)
return InstalledDistribution(req).get_metadata_distribution()

View file

@ -1,15 +1,17 @@
from __future__ import annotations
import os
from collections import namedtuple
from typing import Any, List, Optional
from typing import Any
from typing import List
from typing import Optional
from pip._internal.exceptions import InstallationError
from pip._internal.exceptions import InvalidPyProjectBuildRequires
from pip._internal.exceptions import MissingPyProjectBuildRequires
from pip._vendor import tomli
from pip._vendor.packaging.requirements import InvalidRequirement, Requirement
from pip._internal.exceptions import (
InstallationError,
InvalidPyProjectBuildRequires,
MissingPyProjectBuildRequires,
)
from pip._vendor.packaging.requirements import InvalidRequirement
from pip._vendor.packaging.requirements import Requirement
def _is_list_of_str(obj: Any) -> bool:
@ -17,17 +19,17 @@ def _is_list_of_str(obj: Any) -> bool:
def make_pyproject_path(unpacked_source_directory: str) -> str:
return os.path.join(unpacked_source_directory, "pyproject.toml")
return os.path.join(unpacked_source_directory, 'pyproject.toml')
BuildSystemDetails = namedtuple(
"BuildSystemDetails", ["requires", "backend", "check", "backend_path"]
'BuildSystemDetails', ['requires', 'backend', 'check', 'backend_path'],
)
def load_pyproject_toml(
use_pep517: Optional[bool], pyproject_toml: str, setup_py: str, req_name: str
) -> Optional[BuildSystemDetails]:
use_pep517: bool | None, pyproject_toml: str, setup_py: str, req_name: str,
) -> BuildSystemDetails | None:
"""Load the pyproject.toml file.
Parameters:
@ -54,14 +56,14 @@ def load_pyproject_toml(
if not has_pyproject and not has_setup:
raise InstallationError(
f"{req_name} does not appear to be a Python project: "
f"neither 'setup.py' nor 'pyproject.toml' found."
f'{req_name} does not appear to be a Python project: '
f"neither 'setup.py' nor 'pyproject.toml' found.",
)
if has_pyproject:
with open(pyproject_toml, encoding="utf-8") as f:
with open(pyproject_toml, encoding='utf-8') as f:
pp_toml = tomli.loads(f.read())
build_system = pp_toml.get("build-system")
build_system = pp_toml.get('build-system')
else:
build_system = None
@ -74,16 +76,16 @@ def load_pyproject_toml(
if has_pyproject and not has_setup:
if use_pep517 is not None and not use_pep517:
raise InstallationError(
"Disabling PEP 517 processing is invalid: "
"project does not have a setup.py"
'Disabling PEP 517 processing is invalid: '
'project does not have a setup.py',
)
use_pep517 = True
elif build_system and "build-backend" in build_system:
elif build_system and 'build-backend' in build_system:
if use_pep517 is not None and not use_pep517:
raise InstallationError(
"Disabling PEP 517 processing is invalid: "
"project specifies a build backend of {} "
"in pyproject.toml".format(build_system["build-backend"])
'Disabling PEP 517 processing is invalid: '
'project specifies a build backend of {} '
'in pyproject.toml'.format(build_system['build-backend']),
)
use_pep517 = True
@ -111,8 +113,8 @@ def load_pyproject_toml(
# a version of setuptools that supports that backend.
build_system = {
"requires": ["setuptools>=40.8.0", "wheel"],
"build-backend": "setuptools.build_meta:__legacy__",
'requires': ['setuptools>=40.8.0', 'wheel'],
'build-backend': 'setuptools.build_meta:__legacy__',
}
# If we're using PEP 517, we have build system information (either
@ -125,15 +127,15 @@ def load_pyproject_toml(
# to PEP 518.
# Specifying the build-system table but not the requires key is invalid
if "requires" not in build_system:
if 'requires' not in build_system:
raise MissingPyProjectBuildRequires(package=req_name)
# Error out if requires is not a list of strings
requires = build_system["requires"]
requires = build_system['requires']
if not _is_list_of_str(requires):
raise InvalidPyProjectBuildRequires(
package=req_name,
reason="It is not a list of strings.",
reason='It is not a list of strings.',
)
# Each requirement must be valid as per PEP 508
@ -143,12 +145,12 @@ def load_pyproject_toml(
except InvalidRequirement as error:
raise InvalidPyProjectBuildRequires(
package=req_name,
reason=f"It contains an invalid requirement: {requirement!r}",
reason=f'It contains an invalid requirement: {requirement!r}',
) from error
backend = build_system.get("build-backend")
backend_path = build_system.get("backend-path", [])
check: List[str] = []
backend = build_system.get('build-backend')
backend_path = build_system.get('backend-path', [])
check: list[str] = []
if backend is None:
# If the user didn't specify a backend, we assume they want to use
# the setuptools backend. But we can't be sure they have included
@ -162,7 +164,7 @@ def load_pyproject_toml(
# execute setup.py, but never considered needing to mention the build
# tools themselves. The original PEP 518 code had a similar check (but
# implemented in a different way).
backend = "setuptools.build_meta:__legacy__"
check = ["setuptools>=40.8.0", "wheel"]
backend = 'setuptools.build_meta:__legacy__'
check = ['setuptools>=40.8.0', 'wheel']
return BuildSystemDetails(requires, backend, check, backend_path)

View file

@ -1,6 +1,12 @@
from __future__ import annotations
import collections
import logging
from typing import Iterator, List, Optional, Sequence, Tuple
from typing import Iterator
from typing import List
from typing import Optional
from typing import Sequence
from typing import Tuple
from pip._internal.utils.logging import indent_log
@ -9,10 +15,10 @@ from .req_install import InstallRequirement
from .req_set import RequirementSet
__all__ = [
"RequirementSet",
"InstallRequirement",
"parse_requirements",
"install_given_reqs",
'RequirementSet',
'InstallRequirement',
'parse_requirements',
'install_given_reqs',
]
logger = logging.getLogger(__name__)
@ -23,28 +29,28 @@ class InstallationResult:
self.name = name
def __repr__(self) -> str:
return f"InstallationResult(name={self.name!r})"
return f'InstallationResult(name={self.name!r})'
def _validate_requirements(
requirements: List[InstallRequirement],
) -> Iterator[Tuple[str, InstallRequirement]]:
requirements: list[InstallRequirement],
) -> Iterator[tuple[str, InstallRequirement]]:
for req in requirements:
assert req.name, f"invalid to-be-installed requirement: {req}"
assert req.name, f'invalid to-be-installed requirement: {req}'
yield req.name, req
def install_given_reqs(
requirements: List[InstallRequirement],
install_options: List[str],
requirements: list[InstallRequirement],
install_options: list[str],
global_options: Sequence[str],
root: Optional[str],
home: Optional[str],
prefix: Optional[str],
root: str | None,
home: str | None,
prefix: str | None,
warn_script_location: bool,
use_user_site: bool,
pycompile: bool,
) -> List[InstallationResult]:
) -> list[InstallationResult]:
"""
Install everything in the given list.
@ -54,8 +60,8 @@ def install_given_reqs(
if to_install:
logger.info(
"Installing collected packages: %s",
", ".join(to_install.keys()),
'Installing collected packages: %s',
', '.join(to_install.keys()),
)
installed = []
@ -63,7 +69,7 @@ def install_given_reqs(
with indent_log():
for req_name, requirement in to_install.items():
if requirement.should_reinstall:
logger.info("Attempting uninstall: %s", req_name)
logger.info('Attempting uninstall: %s', req_name)
with indent_log():
uninstalled_pathset = requirement.uninstall(auto_confirm=True)
else:

View file

@ -7,18 +7,21 @@ helps creates for better understandability for the rest of the code.
These are meant to be used elsewhere within pip to create instances of
InstallRequirement.
"""
from __future__ import annotations
import logging
import os
import re
from typing import Any, Dict, Optional, Set, Tuple, Union
from pip._vendor.packaging.markers import Marker
from pip._vendor.packaging.requirements import InvalidRequirement, Requirement
from pip._vendor.packaging.specifiers import Specifier
from typing import Any
from typing import Dict
from typing import Optional
from typing import Set
from typing import Tuple
from typing import Union
from pip._internal.exceptions import InstallationError
from pip._internal.models.index import PyPI, TestPyPI
from pip._internal.models.index import PyPI
from pip._internal.models.index import TestPyPI
from pip._internal.models.link import Link
from pip._internal.models.wheel import Wheel
from pip._internal.req.req_file import ParsedRequirement
@ -27,20 +30,25 @@ from pip._internal.utils.filetypes import is_archive_file
from pip._internal.utils.misc import is_installable_dir
from pip._internal.utils.packaging import get_requirement
from pip._internal.utils.urls import path_to_url
from pip._internal.vcs import is_url, vcs
from pip._internal.vcs import is_url
from pip._internal.vcs import vcs
from pip._vendor.packaging.markers import Marker
from pip._vendor.packaging.requirements import InvalidRequirement
from pip._vendor.packaging.requirements import Requirement
from pip._vendor.packaging.specifiers import Specifier
__all__ = [
"install_req_from_editable",
"install_req_from_line",
"parse_editable",
'install_req_from_editable',
'install_req_from_line',
'parse_editable',
]
logger = logging.getLogger(__name__)
operators = Specifier._operators.keys()
def _strip_extras(path: str) -> Tuple[str, Optional[str]]:
m = re.match(r"^(.+)(\[[^\]]+\])$", path)
def _strip_extras(path: str) -> tuple[str, str | None]:
m = re.match(r'^(.+)(\[[^\]]+\])$', path)
extras = None
if m:
path_no_extras = m.group(1)
@ -51,13 +59,13 @@ def _strip_extras(path: str) -> Tuple[str, Optional[str]]:
return path_no_extras, extras
def convert_extras(extras: Optional[str]) -> Set[str]:
def convert_extras(extras: str | None) -> set[str]:
if not extras:
return set()
return get_requirement("placeholder" + extras.lower()).extras
return get_requirement('placeholder' + extras.lower()).extras
def parse_editable(editable_req: str) -> Tuple[Optional[str], str, Set[str]]:
def parse_editable(editable_req: str) -> tuple[str | None, str, set[str]]:
"""Parses an editable requirement into:
- a requirement name
- an URL
@ -77,37 +85,37 @@ def parse_editable(editable_req: str) -> Tuple[Optional[str], str, Set[str]]:
# Treating it as code that has already been checked out
url_no_extras = path_to_url(url_no_extras)
if url_no_extras.lower().startswith("file:"):
if url_no_extras.lower().startswith('file:'):
package_name = Link(url_no_extras).egg_fragment
if extras:
return (
package_name,
url_no_extras,
get_requirement("placeholder" + extras.lower()).extras,
get_requirement('placeholder' + extras.lower()).extras,
)
else:
return package_name, url_no_extras, set()
for version_control in vcs:
if url.lower().startswith(f"{version_control}:"):
url = f"{version_control}+{url}"
if url.lower().startswith(f'{version_control}:'):
url = f'{version_control}+{url}'
break
link = Link(url)
if not link.is_vcs:
backends = ", ".join(vcs.all_schemes)
backends = ', '.join(vcs.all_schemes)
raise InstallationError(
f"{editable_req} is not a valid editable requirement. "
f"It should either be a path to a local project or a VCS URL "
f"(beginning with {backends})."
f'{editable_req} is not a valid editable requirement. '
f'It should either be a path to a local project or a VCS URL '
f'(beginning with {backends}).',
)
package_name = link.egg_fragment
if not package_name:
raise InstallationError(
"Could not detect requirement name for '{}', please specify one "
"with #egg=your_package_name".format(editable_req)
'with #egg=your_package_name'.format(editable_req),
)
return package_name, url, set()
@ -121,21 +129,21 @@ def check_first_requirement_in_file(filename: str) -> None:
:raises InvalidRequirement: If the first meaningful line cannot be parsed
as an requirement.
"""
with open(filename, encoding="utf-8", errors="ignore") as f:
with open(filename, encoding='utf-8', errors='ignore') as f:
# Create a steppable iterator, so we can handle \-continuations.
lines = (
line
for line in (line.strip() for line in f)
if line and not line.startswith("#") # Skip blank lines/comments.
if line and not line.startswith('#') # Skip blank lines/comments.
)
for line in lines:
# Drop comments -- a hash without a space may be in a URL.
if " #" in line:
line = line[: line.find(" #")]
if ' #' in line:
line = line[: line.find(' #')]
# If there is a line continuation, drop it, and append the next line.
if line.endswith("\\"):
line = line[:-2].strip() + next(lines, "")
if line.endswith('\\'):
line = line[:-2].strip() + next(lines, '')
Requirement(line)
return
@ -148,7 +156,7 @@ def deduce_helpful_msg(req: str) -> str:
"""
if not os.path.exists(req):
return f" File '{req}' does not exist."
msg = " The path does exist. "
msg = ' The path does exist. '
# Try to parse and check if it is a requirements file.
try:
check_first_requirement_in_file(req)
@ -156,11 +164,11 @@ def deduce_helpful_msg(req: str) -> str:
logger.debug("Cannot parse '%s' as requirements file", req)
else:
msg += (
f"The argument you provided "
f"({req}) appears to be a"
f" requirements file. If that is the"
f'The argument you provided '
f'({req}) appears to be a'
f' requirements file. If that is the'
f" case, use the '-r' flag to install"
f" the packages specified within it."
f' the packages specified within it.'
)
return msg
@ -168,10 +176,10 @@ def deduce_helpful_msg(req: str) -> str:
class RequirementParts:
def __init__(
self,
requirement: Optional[Requirement],
link: Optional[Link],
markers: Optional[Marker],
extras: Set[str],
requirement: Requirement | None,
link: Link | None,
markers: Marker | None,
extras: set[str],
):
self.requirement = requirement
self.link = link
@ -184,7 +192,7 @@ def parse_req_from_editable(editable_req: str) -> RequirementParts:
if name is not None:
try:
req: Optional[Requirement] = Requirement(name)
req: Requirement | None = Requirement(name)
except InvalidRequirement:
raise InstallationError(f"Invalid requirement: '{name}'")
else:
@ -200,10 +208,10 @@ def parse_req_from_editable(editable_req: str) -> RequirementParts:
def install_req_from_editable(
editable_req: str,
comes_from: Optional[Union[InstallRequirement, str]] = None,
use_pep517: Optional[bool] = None,
comes_from: InstallRequirement | str | None = None,
use_pep517: bool | None = None,
isolated: bool = False,
options: Optional[Dict[str, Any]] = None,
options: dict[str, Any] | None = None,
constraint: bool = False,
user_supplied: bool = False,
permit_editable_wheels: bool = False,
@ -221,9 +229,9 @@ def install_req_from_editable(
constraint=constraint,
use_pep517=use_pep517,
isolated=isolated,
install_options=options.get("install_options", []) if options else [],
global_options=options.get("global_options", []) if options else [],
hash_options=options.get("hashes", {}) if options else {},
install_options=options.get('install_options', []) if options else [],
global_options=options.get('global_options', []) if options else [],
hash_options=options.get('hashes', {}) if options else {},
extras=parts.extras,
)
@ -242,12 +250,12 @@ def _looks_like_path(name: str) -> bool:
return True
if os.path.altsep is not None and os.path.altsep in name:
return True
if name.startswith("."):
if name.startswith('.'):
return True
return False
def _get_url_from_path(path: str, name: str) -> Optional[str]:
def _get_url_from_path(path: str, name: str) -> str | None:
"""
First, it checks whether a provided path is an installable directory. If it
is, returns the path.
@ -263,29 +271,29 @@ def _get_url_from_path(path: str, name: str) -> Optional[str]:
# now that it is done in load_pyproject_toml too.
raise InstallationError(
f"Directory {name!r} is not installable. Neither 'setup.py' "
"nor 'pyproject.toml' found."
"nor 'pyproject.toml' found.",
)
if not is_archive_file(path):
return None
if os.path.isfile(path):
return path_to_url(path)
urlreq_parts = name.split("@", 1)
urlreq_parts = name.split('@', 1)
if len(urlreq_parts) >= 2 and not _looks_like_path(urlreq_parts[0]):
# If the path contains '@' and the part before it does not look
# like a path, try to treat it as a PEP 440 URL req instead.
return None
logger.warning(
"Requirement %r looks like a filename, but the file does not exist",
'Requirement %r looks like a filename, but the file does not exist',
name,
)
return path_to_url(path)
def parse_req_from_line(name: str, line_source: Optional[str]) -> RequirementParts:
def parse_req_from_line(name: str, line_source: str | None) -> RequirementParts:
if is_url(name):
marker_sep = "; "
marker_sep = '; '
else:
marker_sep = ";"
marker_sep = ';'
if marker_sep in name:
name, markers_as_string = name.split(marker_sep, 1)
markers_as_string = markers_as_string.strip()
@ -312,12 +320,12 @@ def parse_req_from_line(name: str, line_source: Optional[str]) -> RequirementPar
# it's a local file, dir, or url
if link:
# Handle relative file URLs
if link.scheme == "file" and re.search(r"\.\./", link.url):
if link.scheme == 'file' and re.search(r'\.\./', link.url):
link = Link(path_to_url(os.path.normpath(os.path.abspath(link.path))))
# wheel file
if link.is_wheel:
wheel = Wheel(link.filename) # can raise InvalidWheelFilename
req_as_string = f"{wheel.name}=={wheel.version}"
req_as_string = f'{wheel.name}=={wheel.version}'
else:
# set the req to the egg fragment. when it's not there, this
# will become an 'unnamed' requirement
@ -332,24 +340,24 @@ def parse_req_from_line(name: str, line_source: Optional[str]) -> RequirementPar
def with_source(text: str) -> str:
if not line_source:
return text
return f"{text} (from {line_source})"
return f'{text} (from {line_source})'
def _parse_req_string(req_as_string: str) -> Requirement:
try:
req = get_requirement(req_as_string)
except InvalidRequirement:
if os.path.sep in req_as_string:
add_msg = "It looks like a path."
add_msg = 'It looks like a path.'
add_msg += deduce_helpful_msg(req_as_string)
elif "=" in req_as_string and not any(
elif '=' in req_as_string and not any(
op in req_as_string for op in operators
):
add_msg = "= is not a valid operator. Did you mean == ?"
add_msg = '= is not a valid operator. Did you mean == ?'
else:
add_msg = ""
msg = with_source(f"Invalid requirement: {req_as_string!r}")
add_msg = ''
msg = with_source(f'Invalid requirement: {req_as_string!r}')
if add_msg:
msg += f"\nHint: {add_msg}"
msg += f'\nHint: {add_msg}'
raise InstallationError(msg)
else:
# Deprecate extras after specifiers: "name>=1.0[extras]"
@ -358,13 +366,13 @@ def parse_req_from_line(name: str, line_source: Optional[str]) -> RequirementPar
# RequirementParts
for spec in req.specifier:
spec_str = str(spec)
if spec_str.endswith("]"):
if spec_str.endswith(']'):
msg = f"Extras after version '{spec_str}'."
raise InstallationError(msg)
return req
if req_as_string is not None:
req: Optional[Requirement] = _parse_req_string(req_as_string)
req: Requirement | None = _parse_req_string(req_as_string)
else:
req = None
@ -373,12 +381,12 @@ def parse_req_from_line(name: str, line_source: Optional[str]) -> RequirementPar
def install_req_from_line(
name: str,
comes_from: Optional[Union[str, InstallRequirement]] = None,
use_pep517: Optional[bool] = None,
comes_from: str | InstallRequirement | None = None,
use_pep517: bool | None = None,
isolated: bool = False,
options: Optional[Dict[str, Any]] = None,
options: dict[str, Any] | None = None,
constraint: bool = False,
line_source: Optional[str] = None,
line_source: str | None = None,
user_supplied: bool = False,
) -> InstallRequirement:
"""Creates an InstallRequirement from a name, which might be a
@ -396,9 +404,9 @@ def install_req_from_line(
markers=parts.markers,
use_pep517=use_pep517,
isolated=isolated,
install_options=options.get("install_options", []) if options else [],
global_options=options.get("global_options", []) if options else [],
hash_options=options.get("hashes", {}) if options else {},
install_options=options.get('install_options', []) if options else [],
global_options=options.get('global_options', []) if options else [],
hash_options=options.get('hashes', {}) if options else {},
constraint=constraint,
extras=parts.extras,
user_supplied=user_supplied,
@ -407,9 +415,9 @@ def install_req_from_line(
def install_req_from_req_string(
req_string: str,
comes_from: Optional[InstallRequirement] = None,
comes_from: InstallRequirement | None = None,
isolated: bool = False,
use_pep517: Optional[bool] = None,
use_pep517: bool | None = None,
user_supplied: bool = False,
) -> InstallRequirement:
try:
@ -422,16 +430,16 @@ def install_req_from_req_string(
TestPyPI.file_storage_domain,
]
if (
req.url
and comes_from
and comes_from.link
and comes_from.link.netloc in domains_not_allowed
req.url and
comes_from and
comes_from.link and
comes_from.link.netloc in domains_not_allowed
):
# Explicitly disallow pypi packages that depend on external urls
raise InstallationError(
"Packages installed from PyPI cannot depend on packages "
"which are not also hosted on PyPI.\n"
"{} depends on {} ".format(comes_from.name, req)
'Packages installed from PyPI cannot depend on packages '
'which are not also hosted on PyPI.\n'
'{} depends on {} '.format(comes_from.name, req),
)
return InstallRequirement(
@ -446,7 +454,7 @@ def install_req_from_req_string(
def install_req_from_parsed_requirement(
parsed_req: ParsedRequirement,
isolated: bool = False,
use_pep517: Optional[bool] = None,
use_pep517: bool | None = None,
user_supplied: bool = False,
) -> InstallRequirement:
if parsed_req.is_editable:
@ -474,7 +482,7 @@ def install_req_from_parsed_requirement(
def install_req_from_link_and_ireq(
link: Link, ireq: InstallRequirement
link: Link, ireq: InstallRequirement,
) -> InstallRequirement:
return InstallRequirement(
req=ireq.req,

View file

@ -1,6 +1,7 @@
"""
Requirements file parsing
"""
from __future__ import annotations
import optparse
import os
@ -8,20 +9,19 @@ import re
import shlex
import urllib.parse
from optparse import Values
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
Iterable,
Iterator,
List,
Optional,
Tuple,
)
from typing import Any
from typing import Callable
from typing import Dict
from typing import Iterable
from typing import Iterator
from typing import List
from typing import Optional
from typing import Tuple
from typing import TYPE_CHECKING
from pip._internal.cli import cmdoptions
from pip._internal.exceptions import InstallationError, RequirementsFileParseError
from pip._internal.exceptions import InstallationError
from pip._internal.exceptions import RequirementsFileParseError
from pip._internal.models.search_scope import SearchScope
from pip._internal.network.session import PipSession
from pip._internal.network.utils import raise_for_status
@ -35,22 +35,22 @@ if TYPE_CHECKING:
from pip._internal.index.package_finder import PackageFinder
__all__ = ["parse_requirements"]
__all__ = ['parse_requirements']
ReqFileLines = Iterable[Tuple[int, str]]
LineParser = Callable[[str], Tuple[str, Values]]
SCHEME_RE = re.compile(r"^(http|https|file):", re.I)
COMMENT_RE = re.compile(r"(^|\s+)#.*$")
SCHEME_RE = re.compile(r'^(http|https|file):', re.I)
COMMENT_RE = re.compile(r'(^|\s+)#.*$')
# Matches environment variable-style values in '${MY_VARIABLE_1}' with the
# variable name consisting of only uppercase letters, digits or the '_'
# (underscore). This follows the POSIX standard defined in IEEE Std 1003.1,
# 2013 Edition.
ENV_VAR_RE = re.compile(r"(?P<var>\$\{(?P<name>[A-Z0-9_]+)\})")
ENV_VAR_RE = re.compile(r'(?P<var>\$\{(?P<name>[A-Z0-9_]+)\})')
SUPPORTED_OPTIONS: List[Callable[..., optparse.Option]] = [
SUPPORTED_OPTIONS: list[Callable[..., optparse.Option]] = [
cmdoptions.index_url,
cmdoptions.extra_index_url,
cmdoptions.no_index,
@ -68,7 +68,7 @@ SUPPORTED_OPTIONS: List[Callable[..., optparse.Option]] = [
]
# options to be passed to requirements
SUPPORTED_OPTIONS_REQ: List[Callable[..., optparse.Option]] = [
SUPPORTED_OPTIONS_REQ: list[Callable[..., optparse.Option]] = [
cmdoptions.install_options,
cmdoptions.global_options,
cmdoptions.hash,
@ -85,8 +85,8 @@ class ParsedRequirement:
is_editable: bool,
comes_from: str,
constraint: bool,
options: Optional[Dict[str, Any]] = None,
line_source: Optional[str] = None,
options: dict[str, Any] | None = None,
line_source: str | None = None,
) -> None:
self.requirement = requirement
self.is_editable = is_editable
@ -126,8 +126,8 @@ class ParsedLine:
def parse_requirements(
filename: str,
session: PipSession,
finder: Optional["PackageFinder"] = None,
options: Optional[optparse.Values] = None,
finder: PackageFinder | None = None,
options: optparse.Values | None = None,
constraint: bool = False,
) -> Iterator[ParsedRequirement]:
"""Parse a requirements file and yield ParsedRequirement instances.
@ -144,7 +144,7 @@ def parse_requirements(
for parsed_line in parser.parse(filename, constraint):
parsed_req = handle_line(
parsed_line, options=options, finder=finder, session=session
parsed_line, options=options, finder=finder, session=session,
)
if parsed_req is not None:
yield parsed_req
@ -164,12 +164,12 @@ def preprocess(content: str) -> ReqFileLines:
def handle_requirement_line(
line: ParsedLine,
options: Optional[optparse.Values] = None,
options: optparse.Values | None = None,
) -> ParsedRequirement:
# preserve for the nested code path
line_comes_from = "{} {} (line {})".format(
"-c" if line.constraint else "-r",
line_comes_from = '{} {} (line {})'.format(
'-c' if line.constraint else '-r',
line.filename,
line.lineno,
)
@ -196,7 +196,7 @@ def handle_requirement_line(
if dest in line.opts.__dict__ and line.opts.__dict__[dest]:
req_options[dest] = line.opts.__dict__[dest]
line_source = f"line {line.lineno} of {line.filename}"
line_source = f'line {line.lineno} of {line.filename}'
return ParsedRequirement(
requirement=line.requirement,
is_editable=line.is_editable,
@ -211,9 +211,9 @@ def handle_option_line(
opts: Values,
filename: str,
lineno: int,
finder: Optional["PackageFinder"] = None,
options: Optional[optparse.Values] = None,
session: Optional[PipSession] = None,
finder: PackageFinder | None = None,
options: optparse.Values | None = None,
session: PipSession | None = None,
) -> None:
if options:
@ -264,16 +264,16 @@ def handle_option_line(
if session:
for host in opts.trusted_hosts or []:
source = f"line {lineno} of {filename}"
source = f'line {lineno} of {filename}'
session.add_trusted_host(host, source=source)
def handle_line(
line: ParsedLine,
options: Optional[optparse.Values] = None,
finder: Optional["PackageFinder"] = None,
session: Optional[PipSession] = None,
) -> Optional[ParsedRequirement]:
options: optparse.Values | None = None,
finder: PackageFinder | None = None,
session: PipSession | None = None,
) -> ParsedRequirement | None:
"""Handle a single parsed requirements line; This can result in
creating/yielding requirements, or updating the finder.
@ -326,7 +326,7 @@ class RequirementsFileParser:
yield from self._parse_and_recurse(filename, constraint)
def _parse_and_recurse(
self, filename: str, constraint: bool
self, filename: str, constraint: bool,
) -> Iterator[ParsedLine]:
for line in self._parse_file(filename, constraint):
if not line.is_requirement and (
@ -366,7 +366,7 @@ class RequirementsFileParser:
args_str, opts = self._line_parser(line)
except OptionParsingError as e:
# add offending line
msg = f"Invalid requirement: {line}\n{e.msg}"
msg = f'Invalid requirement: {line}\n{e.msg}'
raise RequirementsFileParseError(msg)
yield ParsedLine(
@ -378,8 +378,8 @@ class RequirementsFileParser:
)
def get_line_parser(finder: Optional["PackageFinder"]) -> LineParser:
def parse_line(line: str) -> Tuple[str, Values]:
def get_line_parser(finder: PackageFinder | None) -> LineParser:
def parse_line(line: str) -> tuple[str, Values]:
# Build new parser for each line since it accumulates appendable
# options.
parser = build_parser()
@ -397,21 +397,21 @@ def get_line_parser(finder: Optional["PackageFinder"]) -> LineParser:
return parse_line
def break_args_options(line: str) -> Tuple[str, str]:
def break_args_options(line: str) -> tuple[str, str]:
"""Break up the line into an args and options string. We only want to shlex
(and then optparse) the options, not the args. args can contain markers
which are corrupted by shlex.
"""
tokens = line.split(" ")
tokens = line.split(' ')
args = []
options = tokens[:]
for token in tokens:
if token.startswith("-") or token.startswith("--"):
if token.startswith('-') or token.startswith('--'):
break
else:
args.append(token)
options.pop(0)
return " ".join(args), " ".join(options)
return ' '.join(args), ' '.join(options)
class OptionParsingError(Exception):
@ -432,7 +432,7 @@ def build_parser() -> optparse.OptionParser:
# By default optparse sys.exits on parsing errors. We want to wrap
# that in our own exception.
def parser_exit(self: Any, msg: str) -> "NoReturn":
def parser_exit(self: Any, msg: str) -> NoReturn:
raise OptionParsingError(msg)
# NOTE: mypy disallows assigning to a method
@ -447,28 +447,28 @@ def join_lines(lines_enum: ReqFileLines) -> ReqFileLines:
comments). The joined line takes on the index of the first line.
"""
primary_line_number = None
new_line: List[str] = []
new_line: list[str] = []
for line_number, line in lines_enum:
if not line.endswith("\\") or COMMENT_RE.match(line):
if not line.endswith('\\') or COMMENT_RE.match(line):
if COMMENT_RE.match(line):
# this ensures comments are always matched later
line = " " + line
line = ' ' + line
if new_line:
new_line.append(line)
assert primary_line_number is not None
yield primary_line_number, "".join(new_line)
yield primary_line_number, ''.join(new_line)
new_line = []
else:
yield line_number, line
else:
if not new_line:
primary_line_number = line_number
new_line.append(line.strip("\\"))
new_line.append(line.strip('\\'))
# last line contains \
if new_line:
assert primary_line_number is not None
yield primary_line_number, "".join(new_line)
yield primary_line_number, ''.join(new_line)
# TODO: handle space after '\'.
@ -478,7 +478,7 @@ def ignore_comments(lines_enum: ReqFileLines) -> ReqFileLines:
Strips comments and filter empty lines.
"""
for line_number, line in lines_enum:
line = COMMENT_RE.sub("", line)
line = COMMENT_RE.sub('', line)
line = line.strip()
if line:
yield line_number, line
@ -511,7 +511,7 @@ def expand_env_variables(lines_enum: ReqFileLines) -> ReqFileLines:
yield line_number, line
def get_file_content(url: str, session: PipSession) -> Tuple[str, str]:
def get_file_content(url: str, session: PipSession) -> tuple[str, str]:
"""Gets the content of a file; it may be a filename, file: URL, or
http: URL. Returns (location, content). Content is unicode.
Respects # -*- coding: declarations on the retrieved files.
@ -522,15 +522,15 @@ def get_file_content(url: str, session: PipSession) -> Tuple[str, str]:
scheme = get_url_scheme(url)
# Pip has special support for file:// URLs (LocalFSAdapter).
if scheme in ["http", "https", "file"]:
if scheme in ['http', 'https', 'file']:
resp = session.get(url)
raise_for_status(resp)
return resp.url, resp.text
# Assume this is a bare path.
try:
with open(url, "rb") as f:
with open(url, 'rb') as f:
content = auto_decode(f.read())
except OSError as exc:
raise InstallationError(f"Could not open requirements file: {exc}")
raise InstallationError(f'Could not open requirements file: {exc}')
return url, content

View file

@ -1,5 +1,6 @@
# The following comment should be removed at some point in the future.
# mypy: strict-optional=False
from __future__ import annotations
import functools
import logging
@ -8,24 +9,23 @@ import shutil
import sys
import uuid
import zipfile
from typing import Any, Collection, Dict, Iterable, List, Optional, Sequence, Union
from typing import Any
from typing import Collection
from typing import Dict
from typing import Iterable
from typing import List
from typing import Optional
from typing import Sequence
from typing import Union
from pip._vendor.packaging.markers import Marker
from pip._vendor.packaging.requirements import Requirement
from pip._vendor.packaging.specifiers import SpecifierSet
from pip._vendor.packaging.utils import canonicalize_name
from pip._vendor.packaging.version import Version
from pip._vendor.packaging.version import parse as parse_version
from pip._vendor.pep517.wrappers import Pep517HookCaller
from pip._internal.build_env import BuildEnvironment, NoOpBuildEnvironment
from pip._internal.exceptions import InstallationError, LegacyInstallFailure
from pip._internal.build_env import BuildEnvironment
from pip._internal.build_env import NoOpBuildEnvironment
from pip._internal.exceptions import InstallationError
from pip._internal.exceptions import LegacyInstallFailure
from pip._internal.locations import get_scheme
from pip._internal.metadata import (
BaseDistribution,
get_default_environment,
get_directory_distribution,
)
from pip._internal.metadata import BaseDistribution
from pip._internal.metadata import get_default_environment
from pip._internal.metadata import get_directory_distribution
from pip._internal.models.link import Link
from pip._internal.operations.build.metadata import generate_metadata
from pip._internal.operations.build.metadata_editable import generate_editable_metadata
@ -37,26 +37,31 @@ from pip._internal.operations.install.editable_legacy import (
)
from pip._internal.operations.install.legacy import install as install_legacy
from pip._internal.operations.install.wheel import install_wheel
from pip._internal.pyproject import load_pyproject_toml, make_pyproject_path
from pip._internal.pyproject import load_pyproject_toml
from pip._internal.pyproject import make_pyproject_path
from pip._internal.req.req_uninstall import UninstallPathSet
from pip._internal.utils.deprecation import deprecated
from pip._internal.utils.direct_url_helpers import (
direct_url_for_editable,
direct_url_from_link,
)
from pip._internal.utils.direct_url_helpers import direct_url_for_editable
from pip._internal.utils.direct_url_helpers import direct_url_from_link
from pip._internal.utils.hashes import Hashes
from pip._internal.utils.misc import (
ask_path_exists,
backup_dir,
display_path,
hide_url,
redact_auth_from_url,
)
from pip._internal.utils.misc import ask_path_exists
from pip._internal.utils.misc import backup_dir
from pip._internal.utils.misc import display_path
from pip._internal.utils.misc import hide_url
from pip._internal.utils.misc import redact_auth_from_url
from pip._internal.utils.packaging import safe_extra
from pip._internal.utils.subprocess import runner_with_spinner_message
from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds
from pip._internal.utils.temp_dir import tempdir_kinds
from pip._internal.utils.temp_dir import TempDirectory
from pip._internal.utils.virtualenv import running_under_virtualenv
from pip._internal.vcs import vcs
from pip._vendor.packaging.markers import Marker
from pip._vendor.packaging.requirements import Requirement
from pip._vendor.packaging.specifiers import SpecifierSet
from pip._vendor.packaging.utils import canonicalize_name
from pip._vendor.packaging.version import parse as parse_version
from pip._vendor.packaging.version import Version
from pip._vendor.pep517.wrappers import Pep517HookCaller
logger = logging.getLogger(__name__)
@ -70,16 +75,16 @@ class InstallRequirement:
def __init__(
self,
req: Optional[Requirement],
comes_from: Optional[Union[str, "InstallRequirement"]],
req: Requirement | None,
comes_from: str | InstallRequirement | None,
editable: bool = False,
link: Optional[Link] = None,
markers: Optional[Marker] = None,
use_pep517: Optional[bool] = None,
link: Link | None = None,
markers: Marker | None = None,
use_pep517: bool | None = None,
isolated: bool = False,
install_options: Optional[List[str]] = None,
global_options: Optional[List[str]] = None,
hash_options: Optional[Dict[str, List[str]]] = None,
install_options: list[str] | None = None,
global_options: list[str] | None = None,
hash_options: dict[str, list[str]] | None = None,
constraint: bool = False,
extras: Collection[str] = (),
user_supplied: bool = False,
@ -91,14 +96,14 @@ class InstallRequirement:
self.constraint = constraint
self.editable = editable
self.permit_editable_wheels = permit_editable_wheels
self.legacy_install_reason: Optional[int] = None
self.legacy_install_reason: int | None = None
# source_dir is the local directory where the linked requirement is
# located, or unpacked. In case unpacking is needed, creating and
# populating source_dir is done by the RequirementPreparer. Note this
# is not necessarily the directory where pyproject.toml or setup.py is
# located - that one is obtained via unpacked_source_directory.
self.source_dir: Optional[str] = None
self.source_dir: str | None = None
if self.editable:
assert link
if link.is_file:
@ -111,7 +116,7 @@ class InstallRequirement:
self.original_link_is_in_wheel_cache = False
# Path to any downloaded or already-existing package.
self.local_file_path: Optional[str] = None
self.local_file_path: str | None = None
if self.link and self.link.is_file:
self.local_file_path = self.link.file_path
@ -126,14 +131,14 @@ class InstallRequirement:
self.markers = markers
# This holds the Distribution object if this requirement is already installed.
self.satisfied_by: Optional[BaseDistribution] = None
self.satisfied_by: BaseDistribution | None = None
# Whether the installation process should try to uninstall an existing
# distribution before installing this requirement.
self.should_reinstall = False
# Temporary build location
self._temp_build_dir: Optional[TempDirectory] = None
self._temp_build_dir: TempDirectory | None = None
# Set to True after successful installation
self.install_succeeded: Optional[bool] = None
self.install_succeeded: bool | None = None
# Supplied options
self.install_options = install_options if install_options else []
self.global_options = global_options if global_options else []
@ -152,16 +157,16 @@ class InstallRequirement:
# gets stored. We need this to pass to build_wheel, so the backend
# can ensure that the wheel matches the metadata (see the PEP for
# details).
self.metadata_directory: Optional[str] = None
self.metadata_directory: str | None = None
# The static build requirements (from pyproject.toml)
self.pyproject_requires: Optional[List[str]] = None
self.pyproject_requires: list[str] | None = None
# Build requirements that we will check are available
self.requirements_to_check: List[str] = []
self.requirements_to_check: list[str] = []
# The PEP 517 backend we should use to build the project
self.pep517_backend: Optional[Pep517HookCaller] = None
self.pep517_backend: Pep517HookCaller | None = None
# Are we using PEP 517 for this requirement?
# After pyproject.toml has been loaded, the only valid values are True
@ -177,25 +182,25 @@ class InstallRequirement:
if self.req:
s = str(self.req)
if self.link:
s += " from {}".format(redact_auth_from_url(self.link.url))
s += f' from {redact_auth_from_url(self.link.url)}'
elif self.link:
s = redact_auth_from_url(self.link.url)
else:
s = "<InstallRequirement>"
s = '<InstallRequirement>'
if self.satisfied_by is not None:
s += " in {}".format(display_path(self.satisfied_by.location))
s += f' in {display_path(self.satisfied_by.location)}'
if self.comes_from:
if isinstance(self.comes_from, str):
comes_from: Optional[str] = self.comes_from
comes_from: str | None = self.comes_from
else:
comes_from = self.comes_from.from_path()
if comes_from:
s += f" (from {comes_from})"
s += f' (from {comes_from})'
return s
def __repr__(self) -> str:
return "<{} object: {} editable={!r}>".format(
self.__class__.__name__, str(self), self.editable
return '<{} object: {} editable={!r}>'.format(
self.__class__.__name__, str(self), self.editable,
)
def format_debug(self) -> str:
@ -203,30 +208,30 @@ class InstallRequirement:
attributes = vars(self)
names = sorted(attributes)
state = ("{}={!r}".format(attr, attributes[attr]) for attr in sorted(names))
return "<{name} object: {{{state}}}>".format(
state = (f'{attr}={attributes[attr]!r}' for attr in sorted(names))
return '<{name} object: {{{state}}}>'.format(
name=self.__class__.__name__,
state=", ".join(state),
state=', '.join(state),
)
# Things that are valid for all kinds of requirements?
@property
def name(self) -> Optional[str]:
def name(self) -> str | None:
if self.req is None:
return None
return self.req.name
@functools.lru_cache() # use cached_property in python 3.8+
@functools.lru_cache # use cached_property in python 3.8+
def supports_pyproject_editable(self) -> bool:
if not self.use_pep517:
return False
assert self.pep517_backend
with self.build_env:
runner = runner_with_spinner_message(
"Checking if build backend supports build_editable"
'Checking if build backend supports build_editable',
)
with self.pep517_backend.subprocess_runner(runner):
return "build_editable" in self.pep517_backend._supported_features()
return 'build_editable' in self.pep517_backend._supported_features()
@property
def specifier(self) -> SpecifierSet:
@ -239,16 +244,16 @@ class InstallRequirement:
For example, some-package==1.2 is pinned; some-package>1.2 is not.
"""
specifiers = self.specifier
return len(specifiers) == 1 and next(iter(specifiers)).operator in {"==", "==="}
return len(specifiers) == 1 and next(iter(specifiers)).operator in {'==', '==='}
def match_markers(self, extras_requested: Optional[Iterable[str]] = None) -> bool:
def match_markers(self, extras_requested: Iterable[str] | None = None) -> bool:
if not extras_requested:
# Provide an extra to safely evaluate the markers
# without matching any extra
extras_requested = ("",)
extras_requested = ('',)
if self.markers is not None:
return any(
self.markers.evaluate({"extra": extra}) for extra in extras_requested
self.markers.evaluate({'extra': extra}) for extra in extras_requested
)
else:
return True
@ -284,7 +289,7 @@ class InstallRequirement:
good_hashes.setdefault(link.hash_name, []).append(link.hash)
return Hashes(good_hashes)
def from_path(self) -> Optional[str]:
def from_path(self) -> str | None:
"""Format a nice indicator to show where this "comes from" """
if self.req is None:
return None
@ -295,11 +300,11 @@ class InstallRequirement:
else:
comes_from = self.comes_from.from_path()
if comes_from:
s += "->" + comes_from
s += '->' + comes_from
return s
def ensure_build_location(
self, build_dir: str, autodelete: bool, parallel_builds: bool
self, build_dir: str, autodelete: bool, parallel_builds: bool,
) -> str:
assert build_dir is not None
if self._temp_build_dir is not None:
@ -310,7 +315,7 @@ class InstallRequirement:
# builds (such as numpy). Thus, we ensure that the real path
# is returned.
self._temp_build_dir = TempDirectory(
kind=tempdir_kinds.REQ_BUILD, globally_managed=True
kind=tempdir_kinds.REQ_BUILD, globally_managed=True,
)
return self._temp_build_dir.path
@ -323,12 +328,12 @@ class InstallRequirement:
# name so multiple builds do not interfere with each other.
dir_name: str = canonicalize_name(self.name)
if parallel_builds:
dir_name = f"{dir_name}_{uuid.uuid4().hex}"
dir_name = f'{dir_name}_{uuid.uuid4().hex}'
# FIXME: Is there a better place to create the build_dir? (hg and bzr
# need this)
if not os.path.exists(build_dir):
logger.debug("Creating directory %s", build_dir)
logger.debug('Creating directory %s', build_dir)
os.makedirs(build_dir)
actual_build_dir = os.path.join(build_dir, dir_name)
# `None` indicates that we respect the globally-configured deletion
@ -348,32 +353,32 @@ class InstallRequirement:
assert self.source_dir is not None
# Construct a Requirement object from the generated metadata
if isinstance(parse_version(self.metadata["Version"]), Version):
op = "=="
if isinstance(parse_version(self.metadata['Version']), Version):
op = '=='
else:
op = "==="
op = '==='
self.req = Requirement(
"".join(
''.join(
[
self.metadata["Name"],
self.metadata['Name'],
op,
self.metadata["Version"],
]
)
self.metadata['Version'],
],
),
)
def warn_on_mismatching_name(self) -> None:
metadata_name = canonicalize_name(self.metadata["Name"])
metadata_name = canonicalize_name(self.metadata['Name'])
if canonicalize_name(self.req.name) == metadata_name:
# Everything is fine.
return
# If we're here, there's a mismatch. Log a warning about it.
logger.warning(
"Generating metadata for package %s "
"produced metadata for project name %s. Fix your "
"#egg=%s fragments.",
'Generating metadata for package %s '
'produced metadata for project name %s. Fix your '
'#egg=%s fragments.',
self.name,
metadata_name,
self.name,
@ -402,9 +407,9 @@ class InstallRequirement:
self.should_reinstall = True
elif running_under_virtualenv() and existing_dist.in_site_packages:
raise InstallationError(
f"Will not install to the user site because it will "
f"lack sys.path precedence to {existing_dist.raw_name} "
f"in {existing_dist.location}"
f'Will not install to the user site because it will '
f'lack sys.path precedence to {existing_dist.raw_name} '
f'in {existing_dist.location}',
)
else:
self.should_reinstall = True
@ -428,26 +433,26 @@ class InstallRequirement:
@property
def unpacked_source_directory(self) -> str:
return os.path.join(
self.source_dir, self.link and self.link.subdirectory_fragment or ""
self.source_dir, self.link and self.link.subdirectory_fragment or '',
)
@property
def setup_py_path(self) -> str:
assert self.source_dir, f"No source dir for {self}"
setup_py = os.path.join(self.unpacked_source_directory, "setup.py")
assert self.source_dir, f'No source dir for {self}'
setup_py = os.path.join(self.unpacked_source_directory, 'setup.py')
return setup_py
@property
def setup_cfg_path(self) -> str:
assert self.source_dir, f"No source dir for {self}"
setup_cfg = os.path.join(self.unpacked_source_directory, "setup.cfg")
assert self.source_dir, f'No source dir for {self}'
setup_cfg = os.path.join(self.unpacked_source_directory, 'setup.cfg')
return setup_cfg
@property
def pyproject_toml_path(self) -> str:
assert self.source_dir, f"No source dir for {self}"
assert self.source_dir, f'No source dir for {self}'
return make_pyproject_path(self.unpacked_source_directory)
def load_pyproject_toml(self) -> None:
@ -459,7 +464,7 @@ class InstallRequirement:
follow the PEP 517 or legacy (setup.py) code path.
"""
pyproject_toml_data = load_pyproject_toml(
self.use_pep517, self.pyproject_toml_path, self.setup_py_path, str(self)
self.use_pep517, self.pyproject_toml_path, self.setup_py_path, str(self),
)
if pyproject_toml_data is None:
@ -483,18 +488,18 @@ class InstallRequirement:
or as a setup.py or a setup.cfg
"""
if (
self.editable
and self.use_pep517
and not self.supports_pyproject_editable()
and not os.path.isfile(self.setup_py_path)
and not os.path.isfile(self.setup_cfg_path)
self.editable and
self.use_pep517 and
not self.supports_pyproject_editable() and
not os.path.isfile(self.setup_py_path) and
not os.path.isfile(self.setup_cfg_path)
):
raise InstallationError(
f"Project {self} has a 'pyproject.toml' and its build "
f"backend is missing the 'build_editable' hook. Since it does not "
f"have a 'setup.py' nor a 'setup.cfg', "
f"it cannot be installed in editable mode. "
f"Consider using a build backend that supports PEP 660."
f'it cannot be installed in editable mode. '
f'Consider using a build backend that supports PEP 660.',
)
def prepare_metadata(self) -> None:
@ -504,14 +509,14 @@ class InstallRequirement:
Under legacy processing, call setup.py egg-info.
"""
assert self.source_dir
details = self.name or f"from {self.link}"
details = self.name or f'from {self.link}'
if self.use_pep517:
assert self.pep517_backend is not None
if (
self.editable
and self.permit_editable_wheels
and self.supports_pyproject_editable()
self.editable and
self.permit_editable_wheels and
self.supports_pyproject_editable()
):
self.metadata_directory = generate_editable_metadata(
build_env=self.build_env,
@ -543,7 +548,7 @@ class InstallRequirement:
@property
def metadata(self) -> Any:
if not hasattr(self, "_metadata"):
if not hasattr(self, '_metadata'):
self._metadata = self.get_dist().metadata
return self._metadata
@ -553,16 +558,16 @@ class InstallRequirement:
def assert_source_matches_version(self) -> None:
assert self.source_dir
version = self.metadata["version"]
version = self.metadata['version']
if self.req.specifier and version not in self.req.specifier:
logger.warning(
"Requested %s, but installing version %s",
'Requested %s, but installing version %s',
self,
version,
)
else:
logger.debug(
"Source in %s has version %s, which satisfies requirement %s",
'Source in %s has version %s, which satisfies requirement %s',
display_path(self.source_dir),
version,
self,
@ -595,26 +600,26 @@ class InstallRequirement:
def update_editable(self) -> None:
if not self.link:
logger.debug(
"Cannot update repository at %s; repository location is unknown",
'Cannot update repository at %s; repository location is unknown',
self.source_dir,
)
return
assert self.editable
assert self.source_dir
if self.link.scheme == "file":
if self.link.scheme == 'file':
# Static paths don't get updated
return
vcs_backend = vcs.get_backend_for_scheme(self.link.scheme)
# Editable requirements are validated in Requirement constructors.
# So here, if it's neither a path nor a valid VCS URL, it's a bug.
assert vcs_backend, f"Unsupported VCS URL {self.link.url}"
assert vcs_backend, f'Unsupported VCS URL {self.link.url}'
hidden_url = hide_url(self.link.url)
vcs_backend.obtain(self.source_dir, url=hidden_url, verbosity=0)
# Top-level Actions
def uninstall(
self, auto_confirm: bool = False, verbose: bool = False
) -> Optional[UninstallPathSet]:
self, auto_confirm: bool = False, verbose: bool = False,
) -> UninstallPathSet | None:
"""
Uninstall the distribution currently satisfying this requirement.
@ -630,9 +635,9 @@ class InstallRequirement:
assert self.req
dist = get_default_environment().get_distribution(self.req.name)
if not dist:
logger.warning("Skipping %s as it is not installed.", self.name)
logger.warning('Skipping %s as it is not installed.', self.name)
return None
logger.info("Found existing installation: %s", dist)
logger.info('Found existing installation: %s', dist)
uninstalled_pathset = UninstallPathSet.from_dist(dist)
uninstalled_pathset.remove(auto_confirm, verbose)
@ -641,17 +646,17 @@ class InstallRequirement:
def _get_archive_name(self, path: str, parentdir: str, rootdir: str) -> str:
def _clean_zip_name(name: str, prefix: str) -> str:
assert name.startswith(
prefix + os.path.sep
prefix + os.path.sep,
), f"name {name!r} doesn't start with prefix {prefix!r}"
name = name[len(prefix) + 1 :]
name = name.replace(os.path.sep, "/")
name = name[len(prefix) + 1:]
name = name.replace(os.path.sep, '/')
return name
path = os.path.join(parentdir, path)
name = _clean_zip_name(path, rootdir)
return self.name + "/" + name
return self.name + '/' + name
def archive(self, build_dir: Optional[str]) -> None:
def archive(self, build_dir: str | None) -> None:
"""Saves archive to provided build_dir.
Used for saving downloaded VCS requirements as part of `pip download`.
@ -661,29 +666,29 @@ class InstallRequirement:
return
create_archive = True
archive_name = "{}-{}.zip".format(self.name, self.metadata["version"])
archive_name = '{}-{}.zip'.format(self.name, self.metadata['version'])
archive_path = os.path.join(build_dir, archive_name)
if os.path.exists(archive_path):
response = ask_path_exists(
"The file {} exists. (i)gnore, (w)ipe, "
"(b)ackup, (a)bort ".format(display_path(archive_path)),
("i", "w", "b", "a"),
'The file {} exists. (i)gnore, (w)ipe, '
'(b)ackup, (a)bort '.format(display_path(archive_path)),
('i', 'w', 'b', 'a'),
)
if response == "i":
if response == 'i':
create_archive = False
elif response == "w":
logger.warning("Deleting %s", display_path(archive_path))
elif response == 'w':
logger.warning('Deleting %s', display_path(archive_path))
os.remove(archive_path)
elif response == "b":
elif response == 'b':
dest_file = backup_dir(archive_path)
logger.warning(
"Backing up %s to %s",
'Backing up %s to %s',
display_path(archive_path),
display_path(dest_file),
)
shutil.move(archive_path, dest_file)
elif response == "a":
elif response == 'a':
sys.exit(-1)
if not create_archive:
@ -691,7 +696,7 @@ class InstallRequirement:
zip_output = zipfile.ZipFile(
archive_path,
"w",
'w',
zipfile.ZIP_DEFLATED,
allowZip64=True,
)
@ -704,9 +709,9 @@ class InstallRequirement:
parentdir=dirpath,
rootdir=dir,
)
zipdir = zipfile.ZipInfo(dir_arcname + "/")
zipdir = zipfile.ZipInfo(dir_arcname + '/')
zipdir.external_attr = 0x1ED << 16 # 0o755
zip_output.writestr(zipdir, "")
zip_output.writestr(zipdir, '')
for filename in filenames:
file_arcname = self._get_archive_name(
filename,
@ -716,15 +721,15 @@ class InstallRequirement:
filename = os.path.join(dirpath, filename)
zip_output.write(filename, file_arcname)
logger.info("Saved %s", display_path(archive_path))
logger.info('Saved %s', display_path(archive_path))
def install(
self,
install_options: List[str],
global_options: Optional[Sequence[str]] = None,
root: Optional[str] = None,
home: Optional[str] = None,
prefix: Optional[str] = None,
install_options: list[str],
global_options: Sequence[str] | None = None,
root: str | None = None,
home: str | None = None,
prefix: str | None = None,
warn_script_location: bool = True,
use_user_site: bool = False,
pycompile: bool = True,
@ -819,11 +824,11 @@ class InstallRequirement:
deprecated(
reason=(
"{} was installed using the legacy 'setup.py install' "
"method, because a wheel could not be built for it.".format(
self.name
'method, because a wheel could not be built for it.'.format(
self.name,
)
),
replacement="to fix the wheel build issue reported above",
replacement='to fix the wheel build issue reported above',
gone_in=None,
issue=8368,
)
@ -832,24 +837,24 @@ class InstallRequirement:
def check_invalid_constraint_type(req: InstallRequirement) -> str:
# Check for unsupported forms
problem = ""
problem = ''
if not req.name:
problem = "Unnamed requirements are not allowed as constraints"
problem = 'Unnamed requirements are not allowed as constraints'
elif req.editable:
problem = "Editable requirements are not allowed as constraints"
problem = 'Editable requirements are not allowed as constraints'
elif req.extras:
problem = "Constraints cannot have extras"
problem = 'Constraints cannot have extras'
if problem:
deprecated(
reason=(
"Constraints are only allowed to take the form of a package "
"name and a version specifier. Other forms were originally "
"permitted as an accident of the implementation, but were "
"undocumented. The new implementation of the resolver no "
"longer supports these forms."
'Constraints are only allowed to take the form of a package '
'name and a version specifier. Other forms were originally '
'permitted as an accident of the implementation, but were '
'undocumented. The new implementation of the resolver no '
'longer supports these forms.'
),
replacement="replacing the constraint with a requirement",
replacement='replacing the constraint with a requirement',
# No plan yet for when the new resolver becomes default
gone_in=None,
issue=8210,

View file

@ -1,13 +1,18 @@
from __future__ import annotations
import logging
from collections import OrderedDict
from typing import Dict, Iterable, List, Optional, Tuple
from pip._vendor.packaging.utils import canonicalize_name
from typing import Dict
from typing import Iterable
from typing import List
from typing import Optional
from typing import Tuple
from pip._internal.exceptions import InstallationError
from pip._internal.models.wheel import Wheel
from pip._internal.req.req_install import InstallRequirement
from pip._internal.utils import compatibility_tags
from pip._vendor.packaging.utils import canonicalize_name
logger = logging.getLogger(__name__)
@ -16,29 +21,29 @@ class RequirementSet:
def __init__(self, check_supported_wheels: bool = True) -> None:
"""Create a RequirementSet."""
self.requirements: Dict[str, InstallRequirement] = OrderedDict()
self.requirements: dict[str, InstallRequirement] = OrderedDict()
self.check_supported_wheels = check_supported_wheels
self.unnamed_requirements: List[InstallRequirement] = []
self.unnamed_requirements: list[InstallRequirement] = []
def __str__(self) -> str:
requirements = sorted(
(req for req in self.requirements.values() if not req.comes_from),
key=lambda req: canonicalize_name(req.name or ""),
key=lambda req: canonicalize_name(req.name or ''),
)
return " ".join(str(req.req) for req in requirements)
return ' '.join(str(req.req) for req in requirements)
def __repr__(self) -> str:
requirements = sorted(
self.requirements.values(),
key=lambda req: canonicalize_name(req.name or ""),
key=lambda req: canonicalize_name(req.name or ''),
)
format_string = "<{classname} object; {count} requirement(s): {reqs}>"
format_string = '<{classname} object; {count} requirement(s): {reqs}>'
return format_string.format(
classname=self.__class__.__name__,
count=len(requirements),
reqs=", ".join(str(req.req) for req in requirements),
reqs=', '.join(str(req.req) for req in requirements),
)
def add_unnamed_requirement(self, install_req: InstallRequirement) -> None:
@ -54,9 +59,9 @@ class RequirementSet:
def add_requirement(
self,
install_req: InstallRequirement,
parent_req_name: Optional[str] = None,
extras_requested: Optional[Iterable[str]] = None,
) -> Tuple[List[InstallRequirement], Optional[InstallRequirement]]:
parent_req_name: str | None = None,
extras_requested: Iterable[str] | None = None,
) -> tuple[list[InstallRequirement], InstallRequirement | None]:
"""Add install_req as a requirement to install.
:param parent_req_name: The name of the requirement that needed this
@ -89,9 +94,9 @@ class RequirementSet:
tags = compatibility_tags.get_supported()
if self.check_supported_wheels and not wheel.supported(tags):
raise InstallationError(
"{} is not a supported wheel on this platform.".format(
wheel.filename
)
'{} is not a supported wheel on this platform.'.format(
wheel.filename,
),
)
# This next bit is really a sanity check.
@ -106,26 +111,26 @@ class RequirementSet:
return [install_req], None
try:
existing_req: Optional[InstallRequirement] = self.get_requirement(
install_req.name
existing_req: InstallRequirement | None = self.get_requirement(
install_req.name,
)
except KeyError:
existing_req = None
has_conflicting_requirement = (
parent_req_name is None
and existing_req
and not existing_req.constraint
and existing_req.extras == install_req.extras
and existing_req.req
and install_req.req
and existing_req.req.specifier != install_req.req.specifier
parent_req_name is None and
existing_req and
not existing_req.constraint and
existing_req.extras == install_req.extras and
existing_req.req and
install_req.req and
existing_req.req.specifier != install_req.req.specifier
)
if has_conflicting_requirement:
raise InstallationError(
"Double requirement given: {} (already in {}, name={!r})".format(
install_req, existing_req, install_req.name
)
'Double requirement given: {} (already in {}, name={!r})'.format(
install_req, existing_req, install_req.name,
),
)
# When no existing requirement exists, add the requirement as a
@ -146,8 +151,8 @@ class RequirementSet:
if does_not_satisfy_constraint:
raise InstallationError(
"Could not satisfy constraints for '{}': "
"installation from path or url cannot be "
"constrained to a version".format(install_req.name)
'installation from path or url cannot be '
'constrained to a version'.format(install_req.name),
)
# If we're now installing a constraint, mark the existing
# object for real installation.
@ -157,10 +162,10 @@ class RequirementSet:
if install_req.user_supplied:
existing_req.user_supplied = True
existing_req.extras = tuple(
sorted(set(existing_req.extras) | set(install_req.extras))
sorted(set(existing_req.extras) | set(install_req.extras)),
)
logger.debug(
"Setting %s extras to: %s",
'Setting %s extras to: %s',
existing_req,
existing_req.extras,
)
@ -172,8 +177,8 @@ class RequirementSet:
project_name = canonicalize_name(name)
return (
project_name in self.requirements
and not self.requirements[project_name].constraint
project_name in self.requirements and
not self.requirements[project_name].constraint
)
def get_requirement(self, name: str) -> InstallRequirement:
@ -182,8 +187,8 @@ class RequirementSet:
if project_name in self.requirements:
return self.requirements[project_name]
raise KeyError(f"No project with the name {name!r}")
raise KeyError(f'No project with the name {name!r}')
@property
def all_requirements(self) -> List[InstallRequirement]:
def all_requirements(self) -> list[InstallRequirement]:
return self.unnamed_requirements + list(self.requirements.values())

View file

@ -1,9 +1,16 @@
from __future__ import annotations
import contextlib
import hashlib
import logging
import os
from types import TracebackType
from typing import Dict, Iterator, Optional, Set, Type, Union
from typing import Dict
from typing import Iterator
from typing import Optional
from typing import Set
from typing import Type
from typing import Union
from pip._internal.models.link import Link
from pip._internal.req.req_install import InstallRequirement
@ -18,7 +25,7 @@ def update_env_context_manager(**changes: str) -> Iterator[None]:
# Save values from the target and change them.
non_existent_marker = object()
saved_values: Dict[str, Union[object, str]] = {}
saved_values: dict[str, object | str] = {}
for name, new_value in changes.items():
try:
saved_values[name] = target[name]
@ -39,13 +46,13 @@ def update_env_context_manager(**changes: str) -> Iterator[None]:
@contextlib.contextmanager
def get_requirement_tracker() -> Iterator["RequirementTracker"]:
root = os.environ.get("PIP_REQ_TRACKER")
def get_requirement_tracker() -> Iterator[RequirementTracker]:
root = os.environ.get('PIP_REQ_TRACKER')
with contextlib.ExitStack() as ctx:
if root is None:
root = ctx.enter_context(TempDirectory(kind="req-tracker")).path
root = ctx.enter_context(TempDirectory(kind='req-tracker')).path
ctx.enter_context(update_env_context_manager(PIP_REQ_TRACKER=root))
logger.debug("Initialized build tracking at %s", root)
logger.debug('Initialized build tracking at %s', root)
with RequirementTracker(root) as tracker:
yield tracker
@ -54,18 +61,18 @@ def get_requirement_tracker() -> Iterator["RequirementTracker"]:
class RequirementTracker:
def __init__(self, root: str) -> None:
self._root = root
self._entries: Set[InstallRequirement] = set()
logger.debug("Created build tracker: %s", self._root)
self._entries: set[InstallRequirement] = set()
logger.debug('Created build tracker: %s', self._root)
def __enter__(self) -> "RequirementTracker":
logger.debug("Entered build tracker: %s", self._root)
def __enter__(self) -> RequirementTracker:
logger.debug('Entered build tracker: %s', self._root)
return self
def __exit__(
self,
exc_type: Optional[Type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[TracebackType],
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
self.cleanup()
@ -88,18 +95,18 @@ class RequirementTracker:
except FileNotFoundError:
pass
else:
message = "{} is already being built: {}".format(req.link, contents)
message = f'{req.link} is already being built: {contents}'
raise LookupError(message)
# If we're here, req should really not be building already.
assert req not in self._entries
# Start tracking this requirement.
with open(entry_path, "w", encoding="utf-8") as fp:
with open(entry_path, 'w', encoding='utf-8') as fp:
fp.write(str(req))
self._entries.add(req)
logger.debug("Added %s to build tracker %r", req, self._root)
logger.debug('Added %s to build tracker %r', req, self._root)
def remove(self, req: InstallRequirement) -> None:
"""Remove an InstallRequirement from build tracking."""
@ -109,13 +116,13 @@ class RequirementTracker:
os.unlink(self._entry_path(req.link))
self._entries.remove(req)
logger.debug("Removed %s from build tracker %r", req, self._root)
logger.debug('Removed %s from build tracker %r', req, self._root)
def cleanup(self) -> None:
for req in set(self._entries):
self.remove(req)
logger.debug("Removed build tracker: %r", self._root)
logger.debug('Removed build tracker: %r', self._root)
@contextlib.contextmanager
def track(self, req: InstallRequirement) -> Iterator[None]:

View file

@ -1,18 +1,35 @@
from __future__ import annotations
import functools
import os
import sys
import sysconfig
from importlib.util import cache_from_source
from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Set, Tuple
from typing import Any
from typing import Callable
from typing import Dict
from typing import Iterable
from typing import Iterator
from typing import List
from typing import Optional
from typing import Set
from typing import Tuple
from pip._internal.exceptions import UninstallationError
from pip._internal.locations import get_bin_prefix, get_bin_user
from pip._internal.locations import get_bin_prefix
from pip._internal.locations import get_bin_user
from pip._internal.metadata import BaseDistribution
from pip._internal.utils.compat import WINDOWS
from pip._internal.utils.egg_link import egg_link_path_from_location
from pip._internal.utils.logging import getLogger, indent_log
from pip._internal.utils.misc import ask, is_local, normalize_path, renames, rmtree
from pip._internal.utils.temp_dir import AdjacentTempDirectory, TempDirectory
from pip._internal.utils.logging import getLogger
from pip._internal.utils.logging import indent_log
from pip._internal.utils.misc import ask
from pip._internal.utils.misc import is_local
from pip._internal.utils.misc import normalize_path
from pip._internal.utils.misc import renames
from pip._internal.utils.misc import rmtree
from pip._internal.utils.temp_dir import AdjacentTempDirectory
from pip._internal.utils.temp_dir import TempDirectory
logger = getLogger(__name__)
@ -26,18 +43,18 @@ def _script_names(bin_dir: str, script_name: str, is_gui: bool) -> Iterator[str]
yield exe_name
if not WINDOWS:
return
yield f"{exe_name}.exe"
yield f"{exe_name}.exe.manifest"
yield f'{exe_name}.exe'
yield f'{exe_name}.exe.manifest'
if is_gui:
yield f"{exe_name}-script.pyw"
yield f'{exe_name}-script.pyw'
else:
yield f"{exe_name}-script.py"
yield f'{exe_name}-script.py'
def _unique(fn: Callable[..., Iterator[Any]]) -> Callable[..., Iterator[Any]]:
@functools.wraps(fn)
def unique(*args: Any, **kw: Any) -> Iterator[Any]:
seen: Set[Any] = set()
seen: set[Any] = set()
for item in fn(*args, **kw):
if item not in seen:
seen.add(item)
@ -62,46 +79,46 @@ def uninstallation_paths(dist: BaseDistribution) -> Iterator[str]:
https://packaging.python.org/specifications/recording-installed-packages/
"""
location = dist.location
assert location is not None, "not installed"
assert location is not None, 'not installed'
entries = dist.iter_declared_entries()
if entries is None:
msg = "Cannot uninstall {dist}, RECORD file not found.".format(dist=dist)
msg = f'Cannot uninstall {dist}, RECORD file not found.'
installer = dist.installer
if not installer or installer == "pip":
dep = "{}=={}".format(dist.raw_name, dist.version)
if not installer or installer == 'pip':
dep = f'{dist.raw_name}=={dist.version}'
msg += (
" You might be able to recover from this via: "
' You might be able to recover from this via: '
"'pip install --force-reinstall --no-deps {}'.".format(dep)
)
else:
msg += " Hint: The package was installed by {}.".format(installer)
msg += f' Hint: The package was installed by {installer}.'
raise UninstallationError(msg)
for entry in entries:
path = os.path.join(location, entry)
yield path
if path.endswith(".py"):
if path.endswith('.py'):
dn, fn = os.path.split(path)
base = fn[:-3]
path = os.path.join(dn, base + ".pyc")
path = os.path.join(dn, base + '.pyc')
yield path
path = os.path.join(dn, base + ".pyo")
path = os.path.join(dn, base + '.pyo')
yield path
def compact(paths: Iterable[str]) -> Set[str]:
def compact(paths: Iterable[str]) -> set[str]:
"""Compact a path set to contain the minimal number of paths
necessary to contain all paths in the set. If /a/path/ and
/a/path/to/a/file.txt are both in the set, leave only the
shorter path."""
sep = os.path.sep
short_paths: Set[str] = set()
short_paths: set[str] = set()
for path in sorted(paths, key=len):
should_skip = any(
path.startswith(shortpath.rstrip("*"))
and path[len(shortpath.rstrip("*").rstrip(sep))] == sep
path.startswith(shortpath.rstrip('*')) and
path[len(shortpath.rstrip('*').rstrip(sep))] == sep
for shortpath in short_paths
)
if not should_skip:
@ -109,7 +126,7 @@ def compact(paths: Iterable[str]) -> Set[str]:
return short_paths
def compress_for_rename(paths: Iterable[str]) -> Set[str]:
def compress_for_rename(paths: Iterable[str]) -> set[str]:
"""Returns a set containing the paths that need to be renamed.
This set may include directories when the original sequence of paths
@ -118,7 +135,7 @@ def compress_for_rename(paths: Iterable[str]) -> Set[str]:
case_map = {os.path.normcase(p): p for p in paths}
remaining = set(case_map)
unchecked = sorted({os.path.split(p)[0] for p in case_map.values()}, key=len)
wildcards: Set[str] = set()
wildcards: set[str] = set()
def norm_join(*a: str) -> str:
return os.path.normcase(os.path.join(*a))
@ -128,8 +145,8 @@ def compress_for_rename(paths: Iterable[str]) -> Set[str]:
# This directory has already been handled.
continue
all_files: Set[str] = set()
all_subdirs: Set[str] = set()
all_files: set[str] = set()
all_subdirs: set[str] = set()
for dirname, subdirs, files in os.walk(root):
all_subdirs.update(norm_join(root, dirname, d) for d in subdirs)
all_files.update(norm_join(root, dirname, f) for f in files)
@ -143,7 +160,7 @@ def compress_for_rename(paths: Iterable[str]) -> Set[str]:
return set(map(case_map.__getitem__, remaining)) | wildcards
def compress_for_output_listing(paths: Iterable[str]) -> Tuple[Set[str], Set[str]]:
def compress_for_output_listing(paths: Iterable[str]) -> tuple[set[str], set[str]]:
"""Returns a tuple of 2 sets of which paths to display to user
The first set contains paths that would be deleted. Files of a package
@ -161,9 +178,9 @@ def compress_for_output_listing(paths: Iterable[str]) -> Tuple[Set[str], Set[str
folders = set()
files = set()
for path in will_remove:
if path.endswith(".pyc"):
if path.endswith('.pyc'):
continue
if path.endswith("__init__.py") or ".dist-info" in path:
if path.endswith('__init__.py') or '.dist-info' in path:
folders.add(os.path.dirname(path))
files.add(path)
@ -177,18 +194,18 @@ def compress_for_output_listing(paths: Iterable[str]) -> Tuple[Set[str], Set[str
for folder in folders:
for dirpath, _, dirfiles in os.walk(folder):
for fname in dirfiles:
if fname.endswith(".pyc"):
if fname.endswith('.pyc'):
continue
file_ = os.path.join(dirpath, fname)
if (
os.path.isfile(file_)
and os.path.normcase(file_) not in _normcased_files
os.path.isfile(file_) and
os.path.normcase(file_) not in _normcased_files
):
# We are skipping this file. Add it to the set.
will_skip.add(file_)
will_remove = files | {os.path.join(folder, "*") for folder in folders}
will_remove = files | {os.path.join(folder, '*') for folder in folders}
return will_remove, will_skip
@ -200,10 +217,10 @@ class StashedUninstallPathSet:
def __init__(self) -> None:
# Mapping from source file root to [Adjacent]TempDirectory
# for files under that directory.
self._save_dirs: Dict[str, TempDirectory] = {}
self._save_dirs: dict[str, TempDirectory] = {}
# (old path, new path) tuples for each move that may need
# to be undone.
self._moves: List[Tuple[str, str]] = []
self._moves: list[tuple[str, str]] = []
def _get_directory_stash(self, path: str) -> str:
"""Stashes a directory.
@ -214,7 +231,7 @@ class StashedUninstallPathSet:
try:
save_dir: TempDirectory = AdjacentTempDirectory(path)
except OSError:
save_dir = TempDirectory(kind="uninstall")
save_dir = TempDirectory(kind='uninstall')
self._save_dirs[os.path.normcase(path)] = save_dir
return save_dir.path
@ -238,7 +255,7 @@ class StashedUninstallPathSet:
else:
# Did not find any suitable root
head = os.path.dirname(path)
save_dir = TempDirectory(kind="uninstall")
save_dir = TempDirectory(kind='uninstall')
self._save_dirs[head] = save_dir
relpath = os.path.relpath(path, head)
@ -277,19 +294,19 @@ class StashedUninstallPathSet:
def rollback(self) -> None:
"""Undoes the uninstall by moving stashed files back."""
for p in self._moves:
logger.info("Moving to %s\n from %s", *p)
logger.info('Moving to %s\n from %s', *p)
for new_path, path in self._moves:
try:
logger.debug("Replacing %s from %s", new_path, path)
logger.debug('Replacing %s from %s', new_path, path)
if os.path.isfile(new_path) or os.path.islink(new_path):
os.unlink(new_path)
elif os.path.isdir(new_path):
rmtree(new_path)
renames(path, new_path)
except OSError as ex:
logger.error("Failed to restore %s", new_path)
logger.debug("Exception: %s", ex)
logger.error('Failed to restore %s', new_path)
logger.debug('Exception: %s', ex)
self.commit()
@ -303,9 +320,9 @@ class UninstallPathSet:
requirement."""
def __init__(self, dist: BaseDistribution) -> None:
self._paths: Set[str] = set()
self._refuse: Set[str] = set()
self._pth: Dict[str, UninstallPthEntries] = {}
self._paths: set[str] = set()
self._refuse: set[str] = set()
self._pth: dict[str, UninstallPthEntries] = {}
self._dist = dist
self._moved_paths = StashedUninstallPathSet()
@ -333,7 +350,7 @@ class UninstallPathSet:
# __pycache__ files can show up after 'installed-files.txt' is created,
# due to imports
if os.path.splitext(path)[1] == ".py":
if os.path.splitext(path)[1] == '.py':
self.add(cache_from_source(path))
def add_pth(self, pth_file: str, entry: str) -> None:
@ -356,8 +373,8 @@ class UninstallPathSet:
)
return
dist_name_version = f"{self._dist.raw_name}-{self._dist.version}"
logger.info("Uninstalling %s:", dist_name_version)
dist_name_version = f'{self._dist.raw_name}-{self._dist.version}'
logger.info('Uninstalling %s:', dist_name_version)
with indent_log():
if auto_confirm or self._allowed_to_proceed(verbose):
@ -367,12 +384,12 @@ class UninstallPathSet:
for path in sorted(compact(for_rename)):
moved.stash(path)
logger.verbose("Removing file or directory %s", path)
logger.verbose('Removing file or directory %s', path)
for pth in self._pth.values():
pth.remove()
logger.info("Successfully uninstalled %s", dist_name_version)
logger.info('Successfully uninstalled %s', dist_name_version)
def _allowed_to_proceed(self, verbose: bool) -> bool:
"""Display which files would be deleted and prompt for confirmation"""
@ -394,13 +411,13 @@ class UninstallPathSet:
will_remove = set(self._paths)
will_skip = set()
_display("Would remove:", will_remove)
_display("Would not remove (might be manually added):", will_skip)
_display("Would not remove (outside of prefix):", self._refuse)
_display('Would remove:', will_remove)
_display('Would not remove (might be manually added):', will_skip)
_display('Would not remove (outside of prefix):', self._refuse)
if verbose:
_display("Will actually move:", compress_for_rename(self._paths))
_display('Will actually move:', compress_for_rename(self._paths))
return ask("Proceed (Y/n)? ", ("y", "n", "")) != "n"
return ask('Proceed (Y/n)? ', ('y', 'n', '')) != 'n'
def rollback(self) -> None:
"""Rollback the changes previously made by remove()."""
@ -410,7 +427,7 @@ class UninstallPathSet:
self._dist.raw_name,
)
return
logger.info("Rolling back uninstall of %s", self._dist.raw_name)
logger.info('Rolling back uninstall of %s', self._dist.raw_name)
self._moved_paths.rollback()
for pth in self._pth.values():
pth.rollback()
@ -420,12 +437,12 @@ class UninstallPathSet:
self._moved_paths.commit()
@classmethod
def from_dist(cls, dist: BaseDistribution) -> "UninstallPathSet":
def from_dist(cls, dist: BaseDistribution) -> UninstallPathSet:
dist_location = dist.location
info_location = dist.info_location
if dist_location is None:
logger.info(
"Not uninstalling %s since it is not installed",
'Not uninstalling %s since it is not installed',
dist.canonical_name,
)
return cls(dist)
@ -433,7 +450,7 @@ class UninstallPathSet:
normalized_dist_location = normalize_path(dist_location)
if not dist.local:
logger.info(
"Not uninstalling %s at %s, outside environment %s",
'Not uninstalling %s at %s, outside environment %s',
dist.canonical_name,
normalized_dist_location,
sys.prefix,
@ -442,11 +459,11 @@ class UninstallPathSet:
if normalized_dist_location in {
p
for p in {sysconfig.get_path("stdlib"), sysconfig.get_path("platstdlib")}
for p in {sysconfig.get_path('stdlib'), sysconfig.get_path('platstdlib')}
if p
}:
logger.info(
"Not uninstalling %s at %s, as it is in the standard library.",
'Not uninstalling %s at %s, as it is in the standard library.',
dist.canonical_name,
normalized_dist_location,
)
@ -459,12 +476,12 @@ class UninstallPathSet:
# directory. This means it is not a modern .dist-info installation, an
# egg, or legacy editable.
setuptools_flat_installation = (
dist.installed_with_setuptools_egg_info
and info_location is not None
and os.path.exists(info_location)
dist.installed_with_setuptools_egg_info and
info_location is not None and
os.path.exists(info_location) and
# If dist is editable and the location points to a ``.egg-info``,
# we are in fact in the legacy editable case.
and not info_location.endswith(f"{dist.setuptools_filename}.egg-info")
not info_location.endswith(f'{dist.setuptools_filename}.egg-info')
)
# Uninstall cases order do matter as in the case of 2 installs of the
@ -479,31 +496,31 @@ class UninstallPathSet:
# FIXME: need a test for this elif block
# occurs with --single-version-externally-managed/--record outside
# of pip
elif dist.is_file("top_level.txt"):
elif dist.is_file('top_level.txt'):
try:
namespace_packages = dist.read_text("namespace_packages.txt")
namespace_packages = dist.read_text('namespace_packages.txt')
except FileNotFoundError:
namespaces = []
else:
namespaces = namespace_packages.splitlines(keepends=False)
for top_level_pkg in [
p
for p in dist.read_text("top_level.txt").splitlines()
for p in dist.read_text('top_level.txt').splitlines()
if p and p not in namespaces
]:
path = os.path.join(dist_location, top_level_pkg)
paths_to_remove.add(path)
paths_to_remove.add(f"{path}.py")
paths_to_remove.add(f"{path}.pyc")
paths_to_remove.add(f"{path}.pyo")
paths_to_remove.add(f'{path}.py')
paths_to_remove.add(f'{path}.pyc')
paths_to_remove.add(f'{path}.pyo')
elif dist.installed_by_distutils:
raise UninstallationError(
"Cannot uninstall {!r}. It is a distutils installed project "
"and thus we cannot accurately determine which files belong "
"to it which would lead to only a partial uninstall.".format(
'Cannot uninstall {!r}. It is a distutils installed project '
'and thus we cannot accurately determine which files belong '
'to it which would lead to only a partial uninstall.'.format(
dist.raw_name,
)
),
)
elif dist.installed_as_egg:
@ -514,9 +531,9 @@ class UninstallPathSet:
easy_install_egg = os.path.split(dist_location)[1]
easy_install_pth = os.path.join(
os.path.dirname(dist_location),
"easy-install.pth",
'easy-install.pth',
)
paths_to_remove.add_pth(easy_install_pth, "./" + easy_install_egg)
paths_to_remove.add_pth(easy_install_pth, './' + easy_install_egg)
elif dist.installed_with_dist_info:
for path in uninstallation_paths(dist):
@ -528,18 +545,18 @@ class UninstallPathSet:
with open(develop_egg_link) as fh:
link_pointer = os.path.normcase(fh.readline().strip())
assert link_pointer == dist_location, (
f"Egg-link {link_pointer} does not match installed location of "
f"{dist.raw_name} (at {dist_location})"
f'Egg-link {link_pointer} does not match installed location of '
f'{dist.raw_name} (at {dist_location})'
)
paths_to_remove.add(develop_egg_link)
easy_install_pth = os.path.join(
os.path.dirname(develop_egg_link), "easy-install.pth"
os.path.dirname(develop_egg_link), 'easy-install.pth',
)
paths_to_remove.add_pth(easy_install_pth, dist_location)
else:
logger.debug(
"Not sure how to uninstall: %s - Check: %s",
'Not sure how to uninstall: %s - Check: %s',
dist,
dist_location,
)
@ -551,10 +568,10 @@ class UninstallPathSet:
# find distutils scripts= scripts
try:
for script in dist.iterdir("scripts"):
for script in dist.iterdir('scripts'):
paths_to_remove.add(os.path.join(bin_dir, script.name))
if WINDOWS:
paths_to_remove.add(os.path.join(bin_dir, f"{script.name}.bat"))
paths_to_remove.add(os.path.join(bin_dir, f'{script.name}.bat'))
except (FileNotFoundError, NotADirectoryError):
pass
@ -564,9 +581,9 @@ class UninstallPathSet:
bin_dir: str,
) -> Iterator[str]:
for entry_point in dist.iter_entry_points():
if entry_point.group == "console_scripts":
if entry_point.group == 'console_scripts':
yield from _script_names(bin_dir, entry_point.name, False)
elif entry_point.group == "gui_scripts":
elif entry_point.group == 'gui_scripts':
yield from _script_names(bin_dir, entry_point.name, True)
for s in iter_scripts_to_remove(dist, bin_dir):
@ -578,8 +595,8 @@ class UninstallPathSet:
class UninstallPthEntries:
def __init__(self, pth_file: str) -> None:
self.file = pth_file
self.entries: Set[str] = set()
self._saved_lines: Optional[List[bytes]] = None
self.entries: set[str] = set()
self._saved_lines: list[bytes] | None = None
def add(self, entry: str) -> None:
entry = os.path.normcase(entry)
@ -593,41 +610,41 @@ class UninstallPthEntries:
# have more than "\\sever\share". Valid examples: "\\server\share\" or
# "\\server\share\folder".
if WINDOWS and not os.path.splitdrive(entry)[0]:
entry = entry.replace("\\", "/")
entry = entry.replace('\\', '/')
self.entries.add(entry)
def remove(self) -> None:
logger.verbose("Removing pth entries from %s:", self.file)
logger.verbose('Removing pth entries from %s:', self.file)
# If the file doesn't exist, log a warning and return
if not os.path.isfile(self.file):
logger.warning("Cannot remove entries from nonexistent file %s", self.file)
logger.warning('Cannot remove entries from nonexistent file %s', self.file)
return
with open(self.file, "rb") as fh:
with open(self.file, 'rb') as fh:
# windows uses '\r\n' with py3k, but uses '\n' with py2.x
lines = fh.readlines()
self._saved_lines = lines
if any(b"\r\n" in line for line in lines):
endline = "\r\n"
if any(b'\r\n' in line for line in lines):
endline = '\r\n'
else:
endline = "\n"
endline = '\n'
# handle missing trailing newline
if lines and not lines[-1].endswith(endline.encode("utf-8")):
lines[-1] = lines[-1] + endline.encode("utf-8")
if lines and not lines[-1].endswith(endline.encode('utf-8')):
lines[-1] = lines[-1] + endline.encode('utf-8')
for entry in self.entries:
try:
logger.verbose("Removing entry: %s", entry)
lines.remove((entry + endline).encode("utf-8"))
logger.verbose('Removing entry: %s', entry)
lines.remove((entry + endline).encode('utf-8'))
except ValueError:
pass
with open(self.file, "wb") as fh:
with open(self.file, 'wb') as fh:
fh.writelines(lines)
def rollback(self) -> bool:
if self._saved_lines is None:
logger.error("Cannot roll back changes to %s, none were made", self.file)
logger.error('Cannot roll back changes to %s, none were made', self.file)
return False
logger.debug("Rolling %s back to previous state", self.file)
with open(self.file, "wb") as fh:
logger.debug('Rolling %s back to previous state', self.file)
with open(self.file, 'wb') as fh:
fh.writelines(self._saved_lines)
return True

View file

@ -1,20 +1,24 @@
from typing import Callable, List, Optional
from __future__ import annotations
from typing import Callable
from typing import List
from typing import Optional
from pip._internal.req.req_install import InstallRequirement
from pip._internal.req.req_set import RequirementSet
InstallRequirementProvider = Callable[
[str, Optional[InstallRequirement]], InstallRequirement
[str, Optional[InstallRequirement]], InstallRequirement,
]
class BaseResolver:
def resolve(
self, root_reqs: List[InstallRequirement], check_supported_wheels: bool
self, root_reqs: list[InstallRequirement], check_supported_wheels: bool,
) -> RequirementSet:
raise NotImplementedError()
def get_installation_order(
self, req_set: RequirementSet
) -> List[InstallRequirement]:
self, req_set: RequirementSet,
) -> list[InstallRequirement]:
raise NotImplementedError()

View file

@ -9,42 +9,43 @@ for top-level requirements:
for sub-dependencies
a. "first found, wins" (where the order is breadth first)
"""
# The following comment should be removed at some point in the future.
# mypy: strict-optional=False
from __future__ import annotations
import logging
import sys
from collections import defaultdict
from itertools import chain
from typing import DefaultDict, Iterable, List, Optional, Set, Tuple
from pip._vendor.packaging import specifiers
from pip._vendor.packaging.requirements import Requirement
from typing import DefaultDict
from typing import Iterable
from typing import List
from typing import Optional
from typing import Set
from typing import Tuple
from pip._internal.cache import WheelCache
from pip._internal.exceptions import (
BestVersionAlreadyInstalled,
DistributionNotFound,
HashError,
HashErrors,
NoneMetadataError,
UnsupportedPythonVersion,
)
from pip._internal.exceptions import BestVersionAlreadyInstalled
from pip._internal.exceptions import DistributionNotFound
from pip._internal.exceptions import HashError
from pip._internal.exceptions import HashErrors
from pip._internal.exceptions import NoneMetadataError
from pip._internal.exceptions import UnsupportedPythonVersion
from pip._internal.index.package_finder import PackageFinder
from pip._internal.metadata import BaseDistribution
from pip._internal.models.link import Link
from pip._internal.operations.prepare import RequirementPreparer
from pip._internal.req.req_install import (
InstallRequirement,
check_invalid_constraint_type,
)
from pip._internal.req.req_install import check_invalid_constraint_type
from pip._internal.req.req_install import InstallRequirement
from pip._internal.req.req_set import RequirementSet
from pip._internal.resolution.base import BaseResolver, InstallRequirementProvider
from pip._internal.resolution.base import BaseResolver
from pip._internal.resolution.base import InstallRequirementProvider
from pip._internal.utils.compatibility_tags import get_supported
from pip._internal.utils.logging import indent_log
from pip._internal.utils.misc import normalize_version_info
from pip._internal.utils.packaging import check_requires_python
from pip._vendor.packaging import specifiers
from pip._vendor.packaging.requirements import Requirement
logger = logging.getLogger(__name__)
@ -53,7 +54,7 @@ DiscoveredDependencies = DefaultDict[str, List[InstallRequirement]]
def _check_dist_requires_python(
dist: BaseDistribution,
version_info: Tuple[int, int, int],
version_info: tuple[int, int, int],
ignore_requires_python: bool = False,
) -> None:
"""
@ -82,17 +83,17 @@ def _check_dist_requires_python(
)
except specifiers.InvalidSpecifier as exc:
logger.warning(
"Package %r has an invalid Requires-Python: %s", dist.raw_name, exc
'Package %r has an invalid Requires-Python: %s', dist.raw_name, exc,
)
return
if is_compatible:
return
version = ".".join(map(str, version_info))
version = '.'.join(map(str, version_info))
if ignore_requires_python:
logger.debug(
"Ignoring failed Requires-Python check for package %r: %s not in %r",
'Ignoring failed Requires-Python check for package %r: %s not in %r',
dist.raw_name,
version,
requires_python,
@ -100,9 +101,9 @@ def _check_dist_requires_python(
return
raise UnsupportedPythonVersion(
"Package {!r} requires a different Python: {} not in {!r}".format(
dist.raw_name, version, requires_python
)
'Package {!r} requires a different Python: {} not in {!r}'.format(
dist.raw_name, version, requires_python,
),
)
@ -111,13 +112,13 @@ class Resolver(BaseResolver):
the requested operation without breaking the requirements of any package.
"""
_allowed_strategies = {"eager", "only-if-needed", "to-satisfy-only"}
_allowed_strategies = {'eager', 'only-if-needed', 'to-satisfy-only'}
def __init__(
self,
preparer: RequirementPreparer,
finder: PackageFinder,
wheel_cache: Optional[WheelCache],
wheel_cache: WheelCache | None,
make_install_req: InstallRequirementProvider,
use_user_site: bool,
ignore_dependencies: bool,
@ -125,7 +126,7 @@ class Resolver(BaseResolver):
ignore_requires_python: bool,
force_reinstall: bool,
upgrade_strategy: str,
py_version_info: Optional[Tuple[int, ...]] = None,
py_version_info: tuple[int, ...] | None = None,
) -> None:
super().__init__()
assert upgrade_strategy in self._allowed_strategies
@ -152,7 +153,7 @@ class Resolver(BaseResolver):
self._discovered_dependencies: DiscoveredDependencies = defaultdict(list)
def resolve(
self, root_reqs: List[InstallRequirement], check_supported_wheels: bool
self, root_reqs: list[InstallRequirement], check_supported_wheels: bool,
) -> RequirementSet:
"""Resolve what operations need to be done
@ -174,7 +175,7 @@ class Resolver(BaseResolver):
# exceptions cannot be checked ahead of time, because
# _populate_link() needs to be called before we can make decisions
# based on link type.
discovered_reqs: List[InstallRequirement] = []
discovered_reqs: list[InstallRequirement] = []
hash_errors = HashErrors()
for req in chain(requirement_set.all_requirements, discovered_reqs):
try:
@ -189,12 +190,12 @@ class Resolver(BaseResolver):
return requirement_set
def _is_upgrade_allowed(self, req: InstallRequirement) -> bool:
if self.upgrade_strategy == "to-satisfy-only":
if self.upgrade_strategy == 'to-satisfy-only':
return False
elif self.upgrade_strategy == "eager":
elif self.upgrade_strategy == 'eager':
return True
else:
assert self.upgrade_strategy == "only-if-needed"
assert self.upgrade_strategy == 'only-if-needed'
return req.user_supplied or req.constraint
def _set_req_to_reinstall(self, req: InstallRequirement) -> None:
@ -208,8 +209,8 @@ class Resolver(BaseResolver):
req.satisfied_by = None
def _check_skip_installed(
self, req_to_install: InstallRequirement
) -> Optional[str]:
self, req_to_install: InstallRequirement,
) -> str | None:
"""Check if req_to_install should be skipped.
This will check if the req is installed, and whether we should upgrade
@ -239,9 +240,9 @@ class Resolver(BaseResolver):
return None
if not self._is_upgrade_allowed(req_to_install):
if self.upgrade_strategy == "only-if-needed":
return "already satisfied, skipping upgrade"
return "already satisfied"
if self.upgrade_strategy == 'only-if-needed':
return 'already satisfied, skipping upgrade'
return 'already satisfied'
# Check for the possibility of an upgrade. For link-based
# requirements we have to pull the tree down and inspect to assess
@ -251,7 +252,7 @@ class Resolver(BaseResolver):
self.finder.find_requirement(req_to_install, upgrade=True)
except BestVersionAlreadyInstalled:
# Then the best version is installed.
return "already up-to-date"
return 'already up-to-date'
except DistributionNotFound:
# No distribution found, so we squash the error. It will
# be raised later when we re-try later to do the install.
@ -261,7 +262,7 @@ class Resolver(BaseResolver):
self._set_req_to_reinstall(req_to_install)
return None
def _find_requirement_link(self, req: InstallRequirement) -> Optional[Link]:
def _find_requirement_link(self, req: InstallRequirement) -> Link | None:
upgrade = self._is_upgrade_allowed(req)
best_candidate = self.finder.find_requirement(req, upgrade)
if not best_candidate:
@ -270,14 +271,14 @@ class Resolver(BaseResolver):
# Log a warning per PEP 592 if necessary before returning.
link = best_candidate.link
if link.is_yanked:
reason = link.yanked_reason or "<none given>"
reason = link.yanked_reason or '<none given>'
msg = (
# Mark this as a unicode string to prevent
# "UnicodeEncodeError: 'ascii' codec can't encode character"
# in Python 2 when the reason contains non-ascii characters.
"The candidate selected for download or install is a "
"yanked version: {candidate}\n"
"Reason for being yanked: {reason}"
'The candidate selected for download or install is a '
'yanked version: {candidate}\n'
'Reason for being yanked: {reason}'
).format(candidate=best_candidate, reason=reason)
logger.warning(msg)
@ -307,7 +308,7 @@ class Resolver(BaseResolver):
supported_tags=get_supported(),
)
if cache_entry is not None:
logger.debug("Using cached wheel link: %s", cache_entry.link)
logger.debug('Using cached wheel link: %s', cache_entry.link)
if req.link is req.original_link and cache_entry.persistent:
req.original_link_is_in_wheel_cache = True
req.link = cache_entry.link
@ -344,16 +345,16 @@ class Resolver(BaseResolver):
if req.satisfied_by:
should_modify = (
self.upgrade_strategy != "to-satisfy-only"
or self.force_reinstall
or self.ignore_installed
or req.link.scheme == "file"
self.upgrade_strategy != 'to-satisfy-only' or
self.force_reinstall or
self.ignore_installed or
req.link.scheme == 'file'
)
if should_modify:
self._set_req_to_reinstall(req)
else:
logger.info(
"Requirement already satisfied (use --upgrade to upgrade): %s",
'Requirement already satisfied (use --upgrade to upgrade): %s',
req,
)
return dist
@ -362,7 +363,7 @@ class Resolver(BaseResolver):
self,
requirement_set: RequirementSet,
req_to_install: InstallRequirement,
) -> List[InstallRequirement]:
) -> list[InstallRequirement]:
"""Prepare a single requirements file.
:return: A list of additional InstallRequirements to also install.
@ -385,7 +386,7 @@ class Resolver(BaseResolver):
ignore_requires_python=self.ignore_requires_python,
)
more_reqs: List[InstallRequirement] = []
more_reqs: list[InstallRequirement] = []
def add_req(subreq: Requirement, extras_requested: Iterable[str]) -> None:
# This idiosyncratically converts the Requirement to str and let
@ -415,11 +416,11 @@ class Resolver(BaseResolver):
if not self.ignore_dependencies:
if req_to_install.extras:
logger.debug(
"Installing extra requirements: %r",
",".join(req_to_install.extras),
'Installing extra requirements: %r',
','.join(req_to_install.extras),
)
missing_requested = sorted(
set(req_to_install.extras) - set(dist.iter_provided_extras())
set(req_to_install.extras) - set(dist.iter_provided_extras()),
)
for missing in missing_requested:
logger.warning(
@ -430,7 +431,7 @@ class Resolver(BaseResolver):
)
available_requested = sorted(
set(dist.iter_provided_extras()) & set(req_to_install.extras)
set(dist.iter_provided_extras()) & set(req_to_install.extras),
)
for subreq in dist.iter_dependencies(available_requested):
add_req(subreq, extras_requested=available_requested)
@ -438,8 +439,8 @@ class Resolver(BaseResolver):
return more_reqs
def get_installation_order(
self, req_set: RequirementSet
) -> List[InstallRequirement]:
self, req_set: RequirementSet,
) -> list[InstallRequirement]:
"""Create the installation order.
The installation order is topological - requirements are installed
@ -450,7 +451,7 @@ class Resolver(BaseResolver):
# installs the user specified things in the order given, except when
# dependencies must come earlier to achieve topological order.
order = []
ordered_reqs: Set[InstallRequirement] = set()
ordered_reqs: set[InstallRequirement] = set()
def schedule(req: InstallRequirement) -> None:
if req.satisfied_by or req in ordered_reqs:

View file

@ -1,45 +1,53 @@
from typing import FrozenSet, Iterable, Optional, Tuple, Union
from __future__ import annotations
from pip._vendor.packaging.specifiers import SpecifierSet
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
from pip._vendor.packaging.version import LegacyVersion, Version
from typing import FrozenSet
from typing import Iterable
from typing import Optional
from typing import Tuple
from typing import Union
from pip._internal.models.link import Link, links_equivalent
from pip._internal.models.link import Link
from pip._internal.models.link import links_equivalent
from pip._internal.req.req_install import InstallRequirement
from pip._internal.utils.hashes import Hashes
from pip._vendor.packaging.specifiers import SpecifierSet
from pip._vendor.packaging.utils import canonicalize_name
from pip._vendor.packaging.utils import NormalizedName
from pip._vendor.packaging.version import LegacyVersion
from pip._vendor.packaging.version import Version
CandidateLookup = Tuple[Optional["Candidate"], Optional[InstallRequirement]]
CandidateLookup = Tuple[Optional['Candidate'], Optional[InstallRequirement]]
CandidateVersion = Union[LegacyVersion, Version]
def format_name(project: str, extras: FrozenSet[str]) -> str:
def format_name(project: str, extras: frozenset[str]) -> str:
if not extras:
return project
canonical_extras = sorted(canonicalize_name(e) for e in extras)
return "{}[{}]".format(project, ",".join(canonical_extras))
return '{}[{}]'.format(project, ','.join(canonical_extras))
class Constraint:
def __init__(
self, specifier: SpecifierSet, hashes: Hashes, links: FrozenSet[Link]
self, specifier: SpecifierSet, hashes: Hashes, links: frozenset[Link],
) -> None:
self.specifier = specifier
self.hashes = hashes
self.links = links
@classmethod
def empty(cls) -> "Constraint":
def empty(cls) -> Constraint:
return Constraint(SpecifierSet(), Hashes(), frozenset())
@classmethod
def from_ireq(cls, ireq: InstallRequirement) -> "Constraint":
def from_ireq(cls, ireq: InstallRequirement) -> Constraint:
links = frozenset([ireq.link]) if ireq.link else frozenset()
return Constraint(ireq.specifier, ireq.hashes(trust_internet=False), links)
def __bool__(self) -> bool:
return bool(self.specifier) or bool(self.hashes) or bool(self.links)
def __and__(self, other: InstallRequirement) -> "Constraint":
def __and__(self, other: InstallRequirement) -> Constraint:
if not isinstance(other, InstallRequirement):
return NotImplemented
specifier = self.specifier & other.specifier
@ -49,7 +57,7 @@ class Constraint:
links = links.union([other.link])
return Constraint(specifier, hashes, links)
def is_satisfied_by(self, candidate: "Candidate") -> bool:
def is_satisfied_by(self, candidate: Candidate) -> bool:
# Reject if there are any mismatched URL constraints on this package.
if self.links and not all(_match_link(link, candidate) for link in self.links):
return False
@ -68,7 +76,7 @@ class Requirement:
in which case ``name`` would contain the ``[...]`` part, while this
refers to the name of the project.
"""
raise NotImplementedError("Subclass should override")
raise NotImplementedError('Subclass should override')
@property
def name(self) -> str:
@ -77,19 +85,19 @@ class Requirement:
This is different from ``project_name`` if this requirement contains
extras, where ``project_name`` would not contain the ``[...]`` part.
"""
raise NotImplementedError("Subclass should override")
raise NotImplementedError('Subclass should override')
def is_satisfied_by(self, candidate: "Candidate") -> bool:
def is_satisfied_by(self, candidate: Candidate) -> bool:
return False
def get_candidate_lookup(self) -> CandidateLookup:
raise NotImplementedError("Subclass should override")
raise NotImplementedError('Subclass should override')
def format_for_error(self) -> str:
raise NotImplementedError("Subclass should override")
raise NotImplementedError('Subclass should override')
def _match_link(link: Link, candidate: "Candidate") -> bool:
def _match_link(link: Link, candidate: Candidate) -> bool:
if candidate.source_link:
return links_equivalent(link, candidate.source_link)
return False
@ -104,7 +112,7 @@ class Candidate:
in which case ``name`` would contain the ``[...]`` part, while this
refers to the name of the project.
"""
raise NotImplementedError("Override in subclass")
raise NotImplementedError('Override in subclass')
@property
def name(self) -> str:
@ -113,29 +121,29 @@ class Candidate:
This is different from ``project_name`` if this candidate contains
extras, where ``project_name`` would not contain the ``[...]`` part.
"""
raise NotImplementedError("Override in subclass")
raise NotImplementedError('Override in subclass')
@property
def version(self) -> CandidateVersion:
raise NotImplementedError("Override in subclass")
raise NotImplementedError('Override in subclass')
@property
def is_installed(self) -> bool:
raise NotImplementedError("Override in subclass")
raise NotImplementedError('Override in subclass')
@property
def is_editable(self) -> bool:
raise NotImplementedError("Override in subclass")
raise NotImplementedError('Override in subclass')
@property
def source_link(self) -> Optional[Link]:
raise NotImplementedError("Override in subclass")
def source_link(self) -> Link | None:
raise NotImplementedError('Override in subclass')
def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requirement]]:
raise NotImplementedError("Override in subclass")
def iter_dependencies(self, with_requires: bool) -> Iterable[Requirement | None]:
raise NotImplementedError('Override in subclass')
def get_install_requirement(self) -> Optional[InstallRequirement]:
raise NotImplementedError("Override in subclass")
def get_install_requirement(self) -> InstallRequirement | None:
raise NotImplementedError('Override in subclass')
def format_for_error(self) -> str:
raise NotImplementedError("Subclass should override")
raise NotImplementedError('Subclass should override')

View file

@ -1,26 +1,35 @@
from __future__ import annotations
import logging
import sys
from typing import TYPE_CHECKING, Any, FrozenSet, Iterable, Optional, Tuple, Union, cast
from typing import Any
from typing import cast
from typing import FrozenSet
from typing import Iterable
from typing import Optional
from typing import Tuple
from typing import TYPE_CHECKING
from typing import Union
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
from pip._vendor.packaging.version import Version
from pip._internal.exceptions import (
HashError,
InstallationSubprocessError,
MetadataInconsistent,
)
from pip._internal.exceptions import HashError
from pip._internal.exceptions import InstallationSubprocessError
from pip._internal.exceptions import MetadataInconsistent
from pip._internal.metadata import BaseDistribution
from pip._internal.models.link import Link, links_equivalent
from pip._internal.models.link import Link
from pip._internal.models.link import links_equivalent
from pip._internal.models.wheel import Wheel
from pip._internal.req.constructors import (
install_req_from_editable,
install_req_from_line,
)
from pip._internal.req.constructors import install_req_from_editable
from pip._internal.req.constructors import install_req_from_line
from pip._internal.req.req_install import InstallRequirement
from pip._internal.utils.misc import normalize_version_info
from pip._vendor.packaging.utils import canonicalize_name
from pip._vendor.packaging.utils import NormalizedName
from pip._vendor.packaging.version import Version
from .base import Candidate, CandidateVersion, Requirement, format_name
from .base import Candidate
from .base import CandidateVersion
from .base import format_name
from .base import Requirement
if TYPE_CHECKING:
from .factory import Factory
@ -28,16 +37,16 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
BaseCandidate = Union[
"AlreadyInstalledCandidate",
"EditableCandidate",
"LinkCandidate",
'AlreadyInstalledCandidate',
'EditableCandidate',
'LinkCandidate',
]
# Avoid conflicting with the PyPI package "Python".
REQUIRES_PYTHON_IDENTIFIER = cast(NormalizedName, "<Python from Requires-Python>")
REQUIRES_PYTHON_IDENTIFIER = cast(NormalizedName, '<Python from Requires-Python>')
def as_base_candidate(candidate: Candidate) -> Optional[BaseCandidate]:
def as_base_candidate(candidate: Candidate) -> BaseCandidate | None:
"""The runtime version of BaseCandidate."""
base_candidate_classes = (
AlreadyInstalledCandidate,
@ -50,9 +59,9 @@ def as_base_candidate(candidate: Candidate) -> Optional[BaseCandidate]:
def make_install_req_from_link(
link: Link, template: InstallRequirement
link: Link, template: InstallRequirement,
) -> InstallRequirement:
assert not template.editable, "template is editable"
assert not template.editable, 'template is editable'
if template.req:
line = str(template.req)
else:
@ -76,9 +85,9 @@ def make_install_req_from_link(
def make_install_req_from_editable(
link: Link, template: InstallRequirement
link: Link, template: InstallRequirement,
) -> InstallRequirement:
assert template.editable, "template not editable"
assert template.editable, 'template not editable'
return install_req_from_editable(
link.url,
user_supplied=template.user_supplied,
@ -96,14 +105,14 @@ def make_install_req_from_editable(
def _make_install_req_from_dist(
dist: BaseDistribution, template: InstallRequirement
dist: BaseDistribution, template: InstallRequirement,
) -> InstallRequirement:
if template.req:
line = str(template.req)
elif template.link:
line = f"{dist.canonical_name} @ {template.link.url}"
line = f'{dist.canonical_name} @ {template.link.url}'
else:
line = f"{dist.canonical_name}=={dist.version}"
line = f'{dist.canonical_name}=={dist.version}'
ireq = install_req_from_line(
line,
user_supplied=template.user_supplied,
@ -145,9 +154,9 @@ class _InstallRequirementBackedCandidate(Candidate):
link: Link,
source_link: Link,
ireq: InstallRequirement,
factory: "Factory",
name: Optional[NormalizedName] = None,
version: Optional[CandidateVersion] = None,
factory: Factory,
name: NormalizedName | None = None,
version: CandidateVersion | None = None,
) -> None:
self._link = link
self._source_link = source_link
@ -158,10 +167,10 @@ class _InstallRequirementBackedCandidate(Candidate):
self.dist = self._prepare()
def __str__(self) -> str:
return f"{self.name} {self.version}"
return f'{self.name} {self.version}'
def __repr__(self) -> str:
return "{class_name}({link!r})".format(
return '{class_name}({link!r})'.format(
class_name=self.__class__.__name__,
link=str(self._link),
)
@ -175,7 +184,7 @@ class _InstallRequirementBackedCandidate(Candidate):
return False
@property
def source_link(self) -> Optional[Link]:
def source_link(self) -> Link | None:
return self._source_link
@property
@ -196,28 +205,28 @@ class _InstallRequirementBackedCandidate(Candidate):
return self._version
def format_for_error(self) -> str:
return "{} {} (from {})".format(
return '{} {} (from {})'.format(
self.name,
self.version,
self._link.file_path if self._link.is_file else self._link,
)
def _prepare_distribution(self) -> BaseDistribution:
raise NotImplementedError("Override in subclass")
raise NotImplementedError('Override in subclass')
def _check_metadata_consistency(self, dist: BaseDistribution) -> None:
"""Check for consistency of project name and version of dist."""
if self._name is not None and self._name != dist.canonical_name:
raise MetadataInconsistent(
self._ireq,
"name",
'name',
self._name,
dist.canonical_name,
)
if self._version is not None and self._version != dist.version:
raise MetadataInconsistent(
self._ireq,
"version",
'version',
str(self._version),
str(dist.version),
)
@ -233,19 +242,19 @@ class _InstallRequirementBackedCandidate(Candidate):
raise
except InstallationSubprocessError as exc:
# The output has been presented already, so don't duplicate it.
exc.context = "See above for output."
exc.context = 'See above for output.'
raise
self._check_metadata_consistency(dist)
return dist
def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requirement]]:
def iter_dependencies(self, with_requires: bool) -> Iterable[Requirement | None]:
requires = self.dist.iter_dependencies() if with_requires else ()
for r in requires:
yield self._factory.make_requirement_from_spec(str(r), self._ireq)
yield self._factory.make_requires_python_requirement(self.dist.requires_python)
def get_install_requirement(self) -> Optional[InstallRequirement]:
def get_install_requirement(self) -> InstallRequirement | None:
return self._ireq
@ -256,32 +265,32 @@ class LinkCandidate(_InstallRequirementBackedCandidate):
self,
link: Link,
template: InstallRequirement,
factory: "Factory",
name: Optional[NormalizedName] = None,
version: Optional[CandidateVersion] = None,
factory: Factory,
name: NormalizedName | None = None,
version: CandidateVersion | None = None,
) -> None:
source_link = link
cache_entry = factory.get_wheel_cache_entry(link, name)
if cache_entry is not None:
logger.debug("Using cached wheel link: %s", cache_entry.link)
logger.debug('Using cached wheel link: %s', cache_entry.link)
link = cache_entry.link
ireq = make_install_req_from_link(link, template)
assert ireq.link == link
if ireq.link.is_wheel and not ireq.link.is_file:
wheel = Wheel(ireq.link.filename)
wheel_name = canonicalize_name(wheel.name)
assert name == wheel_name, f"{name!r} != {wheel_name!r} for wheel"
assert name == wheel_name, f'{name!r} != {wheel_name!r} for wheel'
# Version may not be present for PEP 508 direct URLs
if version is not None:
wheel_version = Version(wheel.version)
assert version == wheel_version, "{!r} != {!r} for wheel {}".format(
version, wheel_version, name
assert version == wheel_version, '{!r} != {!r} for wheel {}'.format(
version, wheel_version, name,
)
if (
cache_entry is not None
and cache_entry.persistent
and template.link is template.original_link
cache_entry is not None and
cache_entry.persistent and
template.link is template.original_link
):
ireq.original_link_is_in_wheel_cache = True
@ -306,9 +315,9 @@ class EditableCandidate(_InstallRequirementBackedCandidate):
self,
link: Link,
template: InstallRequirement,
factory: "Factory",
name: Optional[NormalizedName] = None,
version: Optional[CandidateVersion] = None,
factory: Factory,
name: NormalizedName | None = None,
version: CandidateVersion | None = None,
) -> None:
super().__init__(
link=link,
@ -331,7 +340,7 @@ class AlreadyInstalledCandidate(Candidate):
self,
dist: BaseDistribution,
template: InstallRequirement,
factory: "Factory",
factory: Factory,
) -> None:
self.dist = dist
self._ireq = _make_install_req_from_dist(dist, template)
@ -341,14 +350,14 @@ class AlreadyInstalledCandidate(Candidate):
# The returned dist would be exactly the same as self.dist because we
# set satisfied_by in _make_install_req_from_dist.
# TODO: Supply reason based on force_reinstall and upgrade_strategy.
skip_reason = "already satisfied"
skip_reason = 'already satisfied'
factory.preparer.prepare_installed_requirement(self._ireq, skip_reason)
def __str__(self) -> str:
return str(self.dist)
def __repr__(self) -> str:
return "{class_name}({distribution!r})".format(
return '{class_name}({distribution!r})'.format(
class_name=self.__class__.__name__,
distribution=self.dist,
)
@ -378,15 +387,15 @@ class AlreadyInstalledCandidate(Candidate):
return self.dist.editable
def format_for_error(self) -> str:
return f"{self.name} {self.version} (Installed)"
return f'{self.name} {self.version} (Installed)'
def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requirement]]:
def iter_dependencies(self, with_requires: bool) -> Iterable[Requirement | None]:
if not with_requires:
return
for r in self.dist.iter_dependencies():
yield self._factory.make_requirement_from_spec(str(r), self._ireq)
def get_install_requirement(self) -> Optional[InstallRequirement]:
def get_install_requirement(self) -> InstallRequirement | None:
return None
@ -418,17 +427,17 @@ class ExtrasCandidate(Candidate):
def __init__(
self,
base: BaseCandidate,
extras: FrozenSet[str],
extras: frozenset[str],
) -> None:
self.base = base
self.extras = extras
def __str__(self) -> str:
name, rest = str(self.base).split(" ", 1)
return "{}[{}] {}".format(name, ",".join(self.extras), rest)
name, rest = str(self.base).split(' ', 1)
return '{}[{}] {}'.format(name, ','.join(self.extras), rest)
def __repr__(self) -> str:
return "{class_name}(base={base!r}, extras={extras!r})".format(
return '{class_name}(base={base!r}, extras={extras!r})'.format(
class_name=self.__class__.__name__,
base=self.base,
extras=self.extras,
@ -456,8 +465,8 @@ class ExtrasCandidate(Candidate):
return self.base.version
def format_for_error(self) -> str:
return "{} [{}]".format(
self.base.format_for_error(), ", ".join(sorted(self.extras))
return '{} [{}]'.format(
self.base.format_for_error(), ', '.join(sorted(self.extras)),
)
@property
@ -469,10 +478,10 @@ class ExtrasCandidate(Candidate):
return self.base.is_editable
@property
def source_link(self) -> Optional[Link]:
def source_link(self) -> Link | None:
return self.base.source_link
def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requirement]]:
def iter_dependencies(self, with_requires: bool) -> Iterable[Requirement | None]:
factory = self.base._factory
# Add a dependency on the exact base
@ -495,12 +504,12 @@ class ExtrasCandidate(Candidate):
for r in self.base.dist.iter_dependencies(valid_extras):
requirement = factory.make_requirement_from_spec(
str(r), self.base._ireq, valid_extras
str(r), self.base._ireq, valid_extras,
)
if requirement:
yield requirement
def get_install_requirement(self) -> Optional[InstallRequirement]:
def get_install_requirement(self) -> InstallRequirement | None:
# We don't return anything here, because we always
# depend on the base candidate, and we'll get the
# install requirement from that.
@ -511,19 +520,19 @@ class RequiresPythonCandidate(Candidate):
is_installed = False
source_link = None
def __init__(self, py_version_info: Optional[Tuple[int, ...]]) -> None:
def __init__(self, py_version_info: tuple[int, ...] | None) -> None:
if py_version_info is not None:
version_info = normalize_version_info(py_version_info)
else:
version_info = sys.version_info[:3]
self._version = Version(".".join(str(c) for c in version_info))
self._version = Version('.'.join(str(c) for c in version_info))
# We don't need to implement __eq__() and __ne__() since there is always
# only one RequiresPythonCandidate in a resolution, i.e. the host Python.
# The built-in object.__eq__() and object.__ne__() do exactly what we want.
def __str__(self) -> str:
return f"Python {self._version}"
return f'Python {self._version}'
@property
def project_name(self) -> NormalizedName:
@ -538,10 +547,10 @@ class RequiresPythonCandidate(Candidate):
return self._version
def format_for_error(self) -> str:
return f"Python {self.version}"
return f'Python {self.version}'
def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requirement]]:
def iter_dependencies(self, with_requires: bool) -> Iterable[Requirement | None]:
return ()
def get_install_requirement(self) -> Optional[InstallRequirement]:
def get_install_requirement(self) -> InstallRequirement | None:
return None

View file

@ -1,70 +1,68 @@
from __future__ import annotations
import contextlib
import functools
import logging
from typing import (
TYPE_CHECKING,
Dict,
FrozenSet,
Iterable,
Iterator,
List,
Mapping,
NamedTuple,
Optional,
Sequence,
Set,
Tuple,
TypeVar,
cast,
)
from typing import cast
from typing import Dict
from typing import FrozenSet
from typing import Iterable
from typing import Iterator
from typing import List
from typing import Mapping
from typing import NamedTuple
from typing import Optional
from typing import Sequence
from typing import Set
from typing import Tuple
from typing import TYPE_CHECKING
from typing import TypeVar
from pip._vendor.packaging.requirements import InvalidRequirement
from pip._vendor.packaging.specifiers import SpecifierSet
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
from pip._vendor.resolvelib import ResolutionImpossible
from pip._internal.cache import CacheEntry, WheelCache
from pip._internal.exceptions import (
DistributionNotFound,
InstallationError,
InstallationSubprocessError,
MetadataInconsistent,
UnsupportedPythonVersion,
UnsupportedWheel,
)
from pip._internal.cache import CacheEntry
from pip._internal.cache import WheelCache
from pip._internal.exceptions import DistributionNotFound
from pip._internal.exceptions import InstallationError
from pip._internal.exceptions import InstallationSubprocessError
from pip._internal.exceptions import MetadataInconsistent
from pip._internal.exceptions import UnsupportedPythonVersion
from pip._internal.exceptions import UnsupportedWheel
from pip._internal.index.package_finder import PackageFinder
from pip._internal.metadata import BaseDistribution, get_default_environment
from pip._internal.metadata import BaseDistribution
from pip._internal.metadata import get_default_environment
from pip._internal.models.link import Link
from pip._internal.models.wheel import Wheel
from pip._internal.operations.prepare import RequirementPreparer
from pip._internal.req.constructors import install_req_from_link_and_ireq
from pip._internal.req.req_install import (
InstallRequirement,
check_invalid_constraint_type,
)
from pip._internal.req.req_install import check_invalid_constraint_type
from pip._internal.req.req_install import InstallRequirement
from pip._internal.resolution.base import InstallRequirementProvider
from pip._internal.utils.compatibility_tags import get_supported
from pip._internal.utils.hashes import Hashes
from pip._internal.utils.packaging import get_requirement
from pip._internal.utils.virtualenv import running_under_virtualenv
from pip._vendor.packaging.requirements import InvalidRequirement
from pip._vendor.packaging.specifiers import SpecifierSet
from pip._vendor.packaging.utils import canonicalize_name
from pip._vendor.packaging.utils import NormalizedName
from pip._vendor.resolvelib import ResolutionImpossible
from .base import Candidate, CandidateVersion, Constraint, Requirement
from .candidates import (
AlreadyInstalledCandidate,
BaseCandidate,
EditableCandidate,
ExtrasCandidate,
LinkCandidate,
RequiresPythonCandidate,
as_base_candidate,
)
from .found_candidates import FoundCandidates, IndexCandidateInfo
from .requirements import (
ExplicitRequirement,
RequiresPythonRequirement,
SpecifierRequirement,
UnsatisfiableRequirement,
)
from .base import Candidate
from .base import CandidateVersion
from .base import Constraint
from .base import Requirement
from .candidates import AlreadyInstalledCandidate
from .candidates import as_base_candidate
from .candidates import BaseCandidate
from .candidates import EditableCandidate
from .candidates import ExtrasCandidate
from .candidates import LinkCandidate
from .candidates import RequiresPythonCandidate
from .found_candidates import FoundCandidates
from .found_candidates import IndexCandidateInfo
from .requirements import ExplicitRequirement
from .requirements import RequiresPythonRequirement
from .requirements import SpecifierRequirement
from .requirements import UnsatisfiableRequirement
if TYPE_CHECKING:
from typing import Protocol
@ -76,14 +74,14 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
C = TypeVar("C")
C = TypeVar('C')
Cache = Dict[Link, C]
class CollectedRootRequirements(NamedTuple):
requirements: List[Requirement]
constraints: Dict[str, Constraint]
user_requested: Dict[str, int]
requirements: list[Requirement]
constraints: dict[str, Constraint]
user_requested: dict[str, int]
class Factory:
@ -92,13 +90,13 @@ class Factory:
finder: PackageFinder,
preparer: RequirementPreparer,
make_install_req: InstallRequirementProvider,
wheel_cache: Optional[WheelCache],
wheel_cache: WheelCache | None,
use_user_site: bool,
force_reinstall: bool,
ignore_installed: bool,
ignore_requires_python: bool,
suppress_build_failures: bool,
py_version_info: Optional[Tuple[int, ...]] = None,
py_version_info: tuple[int, ...] | None = None,
) -> None:
self._finder = finder
self.preparer = preparer
@ -113,9 +111,9 @@ class Factory:
self._build_failures: Cache[InstallationError] = {}
self._link_candidate_cache: Cache[LinkCandidate] = {}
self._editable_candidate_cache: Cache[EditableCandidate] = {}
self._installed_candidate_cache: Dict[str, AlreadyInstalledCandidate] = {}
self._extras_candidate_cache: Dict[
Tuple[int, FrozenSet[str]], ExtrasCandidate
self._installed_candidate_cache: dict[str, AlreadyInstalledCandidate] = {}
self._extras_candidate_cache: dict[
tuple[int, frozenset[str]], ExtrasCandidate,
] = {}
if not ignore_installed:
@ -137,11 +135,11 @@ class Factory:
wheel = Wheel(link.filename)
if wheel.supported(self._finder.target_python.get_tags()):
return
msg = f"{link.filename} is not a supported wheel on this platform."
msg = f'{link.filename} is not a supported wheel on this platform.'
raise UnsupportedWheel(msg)
def _make_extras_candidate(
self, base: BaseCandidate, extras: FrozenSet[str]
self, base: BaseCandidate, extras: frozenset[str],
) -> ExtrasCandidate:
cache_key = (id(base), extras)
try:
@ -154,7 +152,7 @@ class Factory:
def _make_candidate_from_dist(
self,
dist: BaseDistribution,
extras: FrozenSet[str],
extras: frozenset[str],
template: InstallRequirement,
) -> Candidate:
try:
@ -169,11 +167,11 @@ class Factory:
def _make_candidate_from_link(
self,
link: Link,
extras: FrozenSet[str],
extras: frozenset[str],
template: InstallRequirement,
name: Optional[NormalizedName],
version: Optional[CandidateVersion],
) -> Optional[Candidate]:
name: NormalizedName | None,
version: CandidateVersion | None,
) -> Candidate | None:
# TODO: Check already installed candidate, and use it if the link and
# editable flag match.
@ -194,17 +192,17 @@ class Factory:
)
except MetadataInconsistent as e:
logger.info(
"Discarding [blue underline]%s[/]: [yellow]%s[reset]",
'Discarding [blue underline]%s[/]: [yellow]%s[reset]',
link,
e,
extra={"markup": True},
extra={'markup': True},
)
self._build_failures[link] = e
return None
except InstallationSubprocessError as e:
if not self._suppress_build_failures:
raise
logger.warning("Discarding %s due to build failure: %s", link, e)
logger.warning('Discarding %s due to build failure: %s', link, e)
self._build_failures[link] = e
return None
@ -221,17 +219,17 @@ class Factory:
)
except MetadataInconsistent as e:
logger.info(
"Discarding [blue underline]%s[/]: [yellow]%s[reset]",
'Discarding [blue underline]%s[/]: [yellow]%s[reset]',
link,
e,
extra={"markup": True},
extra={'markup': True},
)
self._build_failures[link] = e
return None
except InstallationSubprocessError as e:
if not self._suppress_build_failures:
raise
logger.warning("Discarding %s due to build failure: %s", link, e)
logger.warning('Discarding %s due to build failure: %s', link, e)
self._build_failures[link] = e
return None
base = self._link_candidate_cache[link]
@ -246,7 +244,7 @@ class Factory:
specifier: SpecifierSet,
hashes: Hashes,
prefers_installed: bool,
incompatible_ids: Set[int],
incompatible_ids: set[int],
) -> Iterable[Candidate]:
if not ireqs:
return ()
@ -256,17 +254,17 @@ class Factory:
# all of them.
# Hopefully the Project model can correct this mismatch in the future.
template = ireqs[0]
assert template.req, "Candidates found on index must be PEP 508"
assert template.req, 'Candidates found on index must be PEP 508'
name = canonicalize_name(template.req.name)
extras: FrozenSet[str] = frozenset()
extras: frozenset[str] = frozenset()
for ireq in ireqs:
assert ireq.req, "Candidates found on index must be PEP 508"
assert ireq.req, 'Candidates found on index must be PEP 508'
specifier &= ireq.req.specifier
hashes &= ireq.hashes(trust_internet=False)
extras |= frozenset(ireq.extras)
def _get_installed_candidate() -> Optional[Candidate]:
def _get_installed_candidate() -> Candidate | None:
"""Get the candidate for the currently-installed version."""
# If --force-reinstall is set, we want the version from the index
# instead, so we "pretend" there is nothing installed.
@ -305,11 +303,11 @@ class Factory:
def is_pinned(specifier: SpecifierSet) -> bool:
for sp in specifier:
if sp.operator == "===":
if sp.operator == '===':
return True
if sp.operator != "==":
if sp.operator != '==':
continue
if sp.version.endswith(".*"):
if sp.version.endswith('.*'):
continue
return True
return False
@ -340,7 +338,7 @@ class Factory:
def _iter_explicit_candidates_from_base(
self,
base_requirements: Iterable[Requirement],
extras: FrozenSet[str],
extras: frozenset[str],
) -> Iterator[Candidate]:
"""Produce explicit candidates from the base given an extra-ed package.
@ -356,7 +354,7 @@ class Factory:
# We've stripped extras from the identifier, and should always
# get a BaseCandidate here, unless there's a bug elsewhere.
base_cand = as_base_candidate(lookup_cand)
assert base_cand is not None, "no extras here"
assert base_cand is not None, 'no extras here'
yield self._make_extras_candidate(base_cand, extras)
def _iter_candidates_from_constraints(
@ -391,8 +389,8 @@ class Factory:
prefers_installed: bool,
) -> Iterable[Candidate]:
# Collect basic lookup information from the requirements.
explicit_candidates: Set[Candidate] = set()
ireqs: List[InstallRequirement] = []
explicit_candidates: set[Candidate] = set()
ireqs: list[InstallRequirement] = []
for req in requirements[identifier]:
cand, ireq = req.get_candidate_lookup()
if cand is not None:
@ -447,14 +445,14 @@ class Factory:
return (
c
for c in explicit_candidates
if id(c) not in incompat_ids
and constraint.is_satisfied_by(c)
and all(req.is_satisfied_by(c) for req in requirements[identifier])
if id(c) not in incompat_ids and
constraint.is_satisfied_by(c) and
all(req.is_satisfied_by(c) for req in requirements[identifier])
)
def _make_requirement_from_install_req(
self, ireq: InstallRequirement, requested_extras: Iterable[str]
) -> Optional[Requirement]:
self, ireq: InstallRequirement, requested_extras: Iterable[str],
) -> Requirement | None:
if not ireq.match_markers(requested_extras):
logger.info(
"Ignoring %s: markers '%s' don't match your environment",
@ -485,7 +483,7 @@ class Factory:
return self.make_requirement_from_candidate(cand)
def collect_root_requirements(
self, root_ireqs: List[InstallRequirement]
self, root_ireqs: list[InstallRequirement],
) -> CollectedRootRequirements:
collected = CollectedRootRequirements([], {}, {})
for i, ireq in enumerate(root_ireqs):
@ -496,7 +494,7 @@ class Factory:
raise InstallationError(problem)
if not ireq.match_markers():
continue
assert ireq.name, "Constraint must be named"
assert ireq.name, 'Constraint must be named'
name = canonicalize_name(ireq.name)
if name in collected.constraints:
collected.constraints[name] &= ireq
@ -515,23 +513,23 @@ class Factory:
return collected
def make_requirement_from_candidate(
self, candidate: Candidate
self, candidate: Candidate,
) -> ExplicitRequirement:
return ExplicitRequirement(candidate)
def make_requirement_from_spec(
self,
specifier: str,
comes_from: Optional[InstallRequirement],
comes_from: InstallRequirement | None,
requested_extras: Iterable[str] = (),
) -> Optional[Requirement]:
) -> Requirement | None:
ireq = self._make_install_req_from_spec(specifier, comes_from)
return self._make_requirement_from_install_req(ireq, requested_extras)
def make_requires_python_requirement(
self,
specifier: SpecifierSet,
) -> Optional[Requirement]:
) -> Requirement | None:
if self._ignore_requires_python:
return None
# Don't bother creating a dependency for an empty Requires-Python.
@ -540,8 +538,8 @@ class Factory:
return RequiresPythonRequirement(specifier, self._python_candidate)
def get_wheel_cache_entry(
self, link: Link, name: Optional[str]
) -> Optional[CacheEntry]:
self, link: Link, name: str | None,
) -> CacheEntry | None:
"""Look up the link in the wheel cache.
If ``preparer.require_hashes`` is True, don't use the wheel cache,
@ -558,7 +556,7 @@ class Factory:
supported_tags=get_supported(),
)
def get_dist_to_uninstall(self, candidate: Candidate) -> Optional[BaseDistribution]:
def get_dist_to_uninstall(self, candidate: Candidate) -> BaseDistribution | None:
# TODO: Are there more cases this needs to return True? Editable?
dist = self._installed_dists.get(candidate.project_name)
if dist is None: # Not installed, no uninstallation required.
@ -580,82 +578,82 @@ class Factory:
# in virtual environments, so we error out.
if running_under_virtualenv() and dist.in_site_packages:
message = (
f"Will not install to the user site because it will lack "
f"sys.path precedence to {dist.raw_name} in {dist.location}"
f'Will not install to the user site because it will lack '
f'sys.path precedence to {dist.raw_name} in {dist.location}'
)
raise InstallationError(message)
return None
def _report_requires_python_error(
self, causes: Sequence["ConflictCause"]
self, causes: Sequence[ConflictCause],
) -> UnsupportedPythonVersion:
assert causes, "Requires-Python error reported with no cause"
assert causes, 'Requires-Python error reported with no cause'
version = self._python_candidate.version
if len(causes) == 1:
specifier = str(causes[0].requirement.specifier)
message = (
f"Package {causes[0].parent.name!r} requires a different "
f"Python: {version} not in {specifier!r}"
f'Package {causes[0].parent.name!r} requires a different '
f'Python: {version} not in {specifier!r}'
)
return UnsupportedPythonVersion(message)
message = f"Packages require a different Python. {version} not in:"
message = f'Packages require a different Python. {version} not in:'
for cause in causes:
package = cause.parent.format_for_error()
specifier = str(cause.requirement.specifier)
message += f"\n{specifier!r} (required by {package})"
message += f'\n{specifier!r} (required by {package})'
return UnsupportedPythonVersion(message)
def _report_single_requirement_conflict(
self, req: Requirement, parent: Optional[Candidate]
self, req: Requirement, parent: Candidate | None,
) -> DistributionNotFound:
if parent is None:
req_disp = str(req)
else:
req_disp = f"{req} (from {parent.name})"
req_disp = f'{req} (from {parent.name})'
cands = self._finder.find_all_candidates(req.project_name)
versions = [str(v) for v in sorted({c.version for c in cands})]
logger.critical(
"Could not find a version that satisfies the requirement %s "
"(from versions: %s)",
'Could not find a version that satisfies the requirement %s '
'(from versions: %s)',
req_disp,
", ".join(versions) or "none",
', '.join(versions) or 'none',
)
if str(req) == "requirements.txt":
if str(req) == 'requirements.txt':
logger.info(
"HINT: You are attempting to install a package literally "
'HINT: You are attempting to install a package literally '
'named "requirements.txt" (which cannot exist). Consider '
"using the '-r' flag to install the packages listed in "
"requirements.txt"
'requirements.txt',
)
return DistributionNotFound(f"No matching distribution found for {req}")
return DistributionNotFound(f'No matching distribution found for {req}')
def get_installation_error(
self,
e: "ResolutionImpossible[Requirement, Candidate]",
constraints: Dict[str, Constraint],
e: ResolutionImpossible[Requirement, Candidate],
constraints: dict[str, Constraint],
) -> InstallationError:
assert e.causes, "Installation error reported with no cause"
assert e.causes, 'Installation error reported with no cause'
# If one of the things we can't solve is "we need Python X.Y",
# that is what we report.
requires_python_causes = [
cause
for cause in e.causes
if isinstance(cause.requirement, RequiresPythonRequirement)
and not cause.requirement.is_satisfied_by(self._python_candidate)
if isinstance(cause.requirement, RequiresPythonRequirement) and
not cause.requirement.is_satisfied_by(self._python_candidate)
]
if requires_python_causes:
# The comprehension above makes sure all Requirement instances are
# RequiresPythonRequirement, so let's cast for convenience.
return self._report_requires_python_error(
cast("Sequence[ConflictCause]", requires_python_causes),
cast('Sequence[ConflictCause]', requires_python_causes),
)
# Otherwise, we have a set of causes which can't all be satisfied
@ -672,16 +670,16 @@ class Factory:
# satisfied at once.
# A couple of formatting helpers
def text_join(parts: List[str]) -> str:
def text_join(parts: list[str]) -> str:
if len(parts) == 1:
return parts[0]
return ", ".join(parts[:-1]) + " and " + parts[-1]
return ', '.join(parts[:-1]) + ' and ' + parts[-1]
def describe_trigger(parent: Candidate) -> str:
ireq = parent.get_install_requirement()
if not ireq or not ireq.comes_from:
return f"{parent.name}=={parent.version}"
return f'{parent.name}=={parent.version}'
if isinstance(ireq.comes_from, InstallRequirement):
return str(ireq.comes_from.name)
return str(ireq.comes_from)
@ -698,42 +696,42 @@ class Factory:
if triggers:
info = text_join(sorted(triggers))
else:
info = "the requested packages"
info = 'the requested packages'
msg = (
"Cannot install {} because these package versions "
"have conflicting dependencies.".format(info)
'Cannot install {} because these package versions '
'have conflicting dependencies.'.format(info)
)
logger.critical(msg)
msg = "\nThe conflict is caused by:"
msg = '\nThe conflict is caused by:'
relevant_constraints = set()
for req, parent in e.causes:
if req.name in constraints:
relevant_constraints.add(req.name)
msg = msg + "\n "
msg = msg + '\n '
if parent:
msg = msg + f"{parent.name} {parent.version} depends on "
msg = msg + f'{parent.name} {parent.version} depends on '
else:
msg = msg + "The user requested "
msg = msg + 'The user requested '
msg = msg + req.format_for_error()
for key in relevant_constraints:
spec = constraints[key].specifier
msg += f"\n The user requested (constraint) {key}{spec}"
msg += f'\n The user requested (constraint) {key}{spec}'
msg = (
msg
+ "\n\n"
+ "To fix this you could try to:\n"
+ "1. loosen the range of package versions you've specified\n"
+ "2. remove package versions to allow pip attempt to solve "
+ "the dependency conflict\n"
msg +
'\n\n' +
'To fix this you could try to:\n' +
"1. loosen the range of package versions you've specified\n" +
'2. remove package versions to allow pip attempt to solve ' +
'the dependency conflict\n'
)
logger.info(msg)
return DistributionNotFound(
"ResolutionImpossible: for help visit "
"https://pip.pypa.io/en/latest/topics/dependency-resolution/"
"#dealing-with-dependency-conflicts"
'ResolutionImpossible: for help visit '
'https://pip.pypa.io/en/latest/topics/dependency-resolution/'
'#dealing-with-dependency-conflicts',
)

View file

@ -7,10 +7,17 @@ everything here lazy all the way down, so we only touch candidates that we
absolutely need, and not "download the world" when we only need one version of
something.
"""
from __future__ import annotations
import functools
from collections.abc import Sequence
from typing import TYPE_CHECKING, Any, Callable, Iterator, Optional, Set, Tuple
from typing import Any
from typing import Callable
from typing import Iterator
from typing import Optional
from typing import Set
from typing import Tuple
from typing import TYPE_CHECKING
from pip._vendor.packaging.version import _BaseVersion
@ -40,7 +47,7 @@ def _iter_built(infos: Iterator[IndexCandidateInfo]) -> Iterator[Candidate]:
This iterator is used when the package is not already installed. Candidates
from index come later in their normal ordering.
"""
versions_found: Set[_BaseVersion] = set()
versions_found: set[_BaseVersion] = set()
for version, func in infos:
if version in versions_found:
continue
@ -52,7 +59,7 @@ def _iter_built(infos: Iterator[IndexCandidateInfo]) -> Iterator[Candidate]:
def _iter_built_with_prepended(
installed: Candidate, infos: Iterator[IndexCandidateInfo]
installed: Candidate, infos: Iterator[IndexCandidateInfo],
) -> Iterator[Candidate]:
"""Iterator for ``FoundCandidates``.
@ -62,7 +69,7 @@ def _iter_built_with_prepended(
normal ordering, except skipped when the version is already installed.
"""
yield installed
versions_found: Set[_BaseVersion] = {installed.version}
versions_found: set[_BaseVersion] = {installed.version}
for version, func in infos:
if version in versions_found:
continue
@ -74,7 +81,7 @@ def _iter_built_with_prepended(
def _iter_built_with_inserted(
installed: Candidate, infos: Iterator[IndexCandidateInfo]
installed: Candidate, infos: Iterator[IndexCandidateInfo],
) -> Iterator[Candidate]:
"""Iterator for ``FoundCandidates``.
@ -86,7 +93,7 @@ def _iter_built_with_inserted(
the installed candidate exactly once before we start yielding older or
equivalent candidates, or after all other candidates if they are all newer.
"""
versions_found: Set[_BaseVersion] = set()
versions_found: set[_BaseVersion] = set()
for version, func in infos:
if version in versions_found:
continue
@ -117,9 +124,9 @@ class FoundCandidates(SequenceCandidate):
def __init__(
self,
get_infos: Callable[[], Iterator[IndexCandidateInfo]],
installed: Optional[Candidate],
installed: Candidate | None,
prefers_installed: bool,
incompatible_ids: Set[int],
incompatible_ids: set[int],
):
self._get_infos = get_infos
self._installed = installed

View file

@ -1,19 +1,21 @@
from __future__ import annotations
import collections
import math
from typing import (
TYPE_CHECKING,
Dict,
Iterable,
Iterator,
Mapping,
Sequence,
TypeVar,
Union,
)
from typing import Dict
from typing import Iterable
from typing import Iterator
from typing import Mapping
from typing import Sequence
from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
from pip._vendor.resolvelib.providers import AbstractProvider
from .base import Candidate, Constraint, Requirement
from .base import Candidate
from .base import Constraint
from .base import Requirement
from .candidates import REQUIRES_PYTHON_IDENTIFIER
from .factory import Factory
@ -46,15 +48,15 @@ else:
# services to those objects (access to pip's finder and preparer).
D = TypeVar("D")
V = TypeVar("V")
D = TypeVar('D')
V = TypeVar('V')
def _get_with_identifier(
mapping: Mapping[str, V],
identifier: str,
default: D,
) -> Union[D, V]:
) -> D | V:
"""Get item from a package name lookup mapping with a resolver identifier.
This extra logic is needed when the target mapping is keyed by package
@ -69,7 +71,7 @@ def _get_with_identifier(
# some regular expression. But since pip's resolver only spits out three
# kinds of identifiers: normalized PEP 503 names, normalized names plus
# extras, and Requires-Python, we can cheat a bit here.
name, open_bracket, _ = identifier.partition("[")
name, open_bracket, _ = identifier.partition('[')
if open_bracket and name in mapping:
return mapping[name]
return default
@ -89,19 +91,19 @@ class PipProvider(_ProviderBase):
def __init__(
self,
factory: Factory,
constraints: Dict[str, Constraint],
constraints: dict[str, Constraint],
ignore_dependencies: bool,
upgrade_strategy: str,
user_requested: Dict[str, int],
user_requested: dict[str, int],
) -> None:
self._factory = factory
self._constraints = constraints
self._ignore_dependencies = ignore_dependencies
self._upgrade_strategy = upgrade_strategy
self._user_requested = user_requested
self._known_depths: Dict[str, float] = collections.defaultdict(lambda: math.inf)
self._known_depths: dict[str, float] = collections.defaultdict(lambda: math.inf)
def identify(self, requirement_or_candidate: Union[Requirement, Candidate]) -> str:
def identify(self, requirement_or_candidate: Requirement | Candidate) -> str:
return requirement_or_candidate.name
def get_preference( # type: ignore
@ -109,9 +111,9 @@ class PipProvider(_ProviderBase):
identifier: str,
resolutions: Mapping[str, Candidate],
candidates: Mapping[str, Iterator[Candidate]],
information: Mapping[str, Iterable["PreferenceInformation"]],
backtrack_causes: Sequence["PreferenceInformation"],
) -> "Preference":
information: Mapping[str, Iterable[PreferenceInformation]],
backtrack_causes: Sequence[PreferenceInformation],
) -> Preference:
"""Produce a sort key for given requirement based on preference.
The lower the return value is, the more preferred this group of
@ -139,11 +141,11 @@ class PipProvider(_ProviderBase):
]
direct = candidate is not None
pinned = any(op[:2] == "==" for op in operators)
pinned = any(op[:2] == '==' for op in operators)
unfree = bool(operators)
try:
requested_order: Union[int, float] = self._user_requested[identifier]
requested_order: int | float = self._user_requested[identifier]
except KeyError:
requested_order = math.inf
parent_depths = (
@ -169,7 +171,7 @@ class PipProvider(_ProviderBase):
# delaying Setuptools helps reduce branches the resolver has to check.
# This serves as a temporary fix for issues like "apache-airflow[all]"
# while we work on "proper" branch pruning techniques.
delay_this = identifier == "setuptools"
delay_this = identifier == 'setuptools'
# Prefer the causes of backtracking on the assumption that the problem
# resolving the dependency tree is related to the failures that caused
@ -205,9 +207,9 @@ class PipProvider(_ProviderBase):
an upgrade strategy of "to-satisfy-only" means that `--upgrade`
was not specified).
"""
if self._upgrade_strategy == "eager":
if self._upgrade_strategy == 'eager':
return True
elif self._upgrade_strategy == "only-if-needed":
elif self._upgrade_strategy == 'only-if-needed':
user_order = _get_with_identifier(
self._user_requested,
identifier,
@ -238,7 +240,7 @@ class PipProvider(_ProviderBase):
@staticmethod
def is_backtrack_cause(
identifier: str, backtrack_causes: Sequence["PreferenceInformation"]
identifier: str, backtrack_causes: Sequence[PreferenceInformation],
) -> bool:
for backtrack_cause in backtrack_causes:
if identifier == backtrack_cause.requirement.name:

Some files were not shown because too many files have changed in this diff Show more