[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,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()