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