Voice et bot modif

This commit is contained in:
pi 2026-06-16 17:09:34 +00:00
parent 189d56026b
commit 7333a22bcd
10774 changed files with 634644 additions and 933308 deletions

View file

@ -1,11 +1,7 @@
from __future__ import annotations
import collections
import logging
from collections.abc import Generator
from dataclasses import dataclass
from typing import Generator, List, Optional, Sequence, Tuple
from pip._internal.cli.progress_bars import BarType, get_install_progress_renderer
from pip._internal.utils.logging import indent_log
from .req_file import parse_requirements
@ -22,29 +18,32 @@ __all__ = [
logger = logging.getLogger(__name__)
@dataclass(frozen=True)
class InstallationResult:
name: str
def __init__(self, name: str) -> None:
self.name = name
def __repr__(self) -> str:
return f"InstallationResult(name={self.name!r})"
def _validate_requirements(
requirements: list[InstallRequirement],
) -> Generator[tuple[str, InstallRequirement], None, None]:
requirements: List[InstallRequirement],
) -> Generator[Tuple[str, InstallRequirement], None, None]:
for req in requirements:
assert req.name, f"invalid to-be-installed requirement: {req}"
yield req.name, req
def install_given_reqs(
requirements: list[InstallRequirement],
root: str | None,
home: str | None,
prefix: str | None,
requirements: List[InstallRequirement],
global_options: Sequence[str],
root: Optional[str],
home: Optional[str],
prefix: Optional[str],
warn_script_location: bool,
use_user_site: bool,
pycompile: bool,
progress_bar: BarType,
) -> list[InstallationResult]:
) -> List[InstallationResult]:
"""
Install everything in the given list.
@ -60,19 +59,8 @@ def install_given_reqs(
installed = []
show_progress = logger.isEnabledFor(logging.INFO) and len(to_install) > 1
items = iter(to_install.values())
if show_progress:
renderer = get_install_progress_renderer(
bar_type=progress_bar, total=len(to_install)
)
items = renderer(items)
with indent_log():
for requirement in items:
req_name = requirement.name
assert req_name is not None
for req_name, requirement in to_install.items():
if requirement.should_reinstall:
logger.info("Attempting uninstall: %s", req_name)
with indent_log():
@ -82,6 +70,7 @@ def install_given_reqs(
try:
requirement.install(
global_options,
root=root,
home=home,
prefix=prefix,

View file

@ -8,14 +8,11 @@ These are meant to be used elsewhere within pip to create instances of
InstallRequirement.
"""
from __future__ import annotations
import copy
import logging
import os
import re
from collections.abc import Collection
from dataclasses import dataclass
from typing import Collection, Dict, List, Optional, Set, Tuple, Union
from pip._vendor.packaging.markers import Marker
from pip._vendor.packaging.requirements import InvalidRequirement, Requirement
@ -43,11 +40,11 @@ logger = logging.getLogger(__name__)
operators = Specifier._operators.keys()
def _strip_extras(path: str) -> tuple[str, str | None]:
def _strip_extras(path: str) -> Tuple[str, Optional[str]]:
m = re.match(r"^(.+)(\[[^\]]+\])$", path)
extras = None
if m:
path_no_extras = m.group(1).rstrip()
path_no_extras = m.group(1)
extras = m.group(2)
else:
path_no_extras = path
@ -55,19 +52,19 @@ def _strip_extras(path: str) -> tuple[str, str | None]:
return path_no_extras, extras
def convert_extras(extras: str | None) -> set[str]:
def convert_extras(extras: Optional[str]) -> Set[str]:
if not extras:
return set()
return get_requirement("placeholder" + extras.lower()).extras
def _set_requirement_extras(req: Requirement, new_extras: set[str]) -> Requirement:
def _set_requirement_extras(req: Requirement, new_extras: Set[str]) -> Requirement:
"""
Returns a new requirement based on the given one, with the supplied extras. If the
given requirement already has extras those are replaced (or dropped if no new extras
are given).
"""
match: re.Match[str] | None = re.fullmatch(
match: Optional[re.Match[str]] = re.fullmatch(
# see https://peps.python.org/pep-0508/#complete-grammar
r"([\w\t .-]+)(\[[^\]]*\])?(.*)",
str(req),
@ -77,34 +74,26 @@ def _set_requirement_extras(req: Requirement, new_extras: set[str]) -> Requireme
assert (
match is not None
), f"regex match on requirement {req} failed, this should never happen"
pre: str | None = match.group(1)
post: str | None = match.group(3)
pre: Optional[str] = match.group(1)
post: Optional[str] = match.group(3)
assert (
pre is not None and post is not None
), f"regex group selection for requirement {req} failed, this should never happen"
extras: str = "[{}]".format(",".join(sorted(new_extras)) if new_extras else "")
return get_requirement(f"{pre}{extras}{post}")
extras: str = "[%s]" % ",".join(sorted(new_extras)) if new_extras else ""
return Requirement(f"{pre}{extras}{post}")
def _parse_direct_url_editable(editable_req: str) -> tuple[str | None, str, set[str]]:
try:
req = Requirement(editable_req)
except InvalidRequirement:
pass
else:
if req.url:
# Join the marker back into the name part. This will be parsed out
# later into a Requirement again.
if req.marker:
name = f"{req.name} ; {req.marker}"
else:
name = req.name
return (name, req.url, req.extras)
def parse_editable(editable_req: str) -> Tuple[Optional[str], str, Set[str]]:
"""Parses an editable requirement into:
- a requirement name
- an URL
- extras
- editable options
Accepted requirements:
svn+http://blahblah@rev#egg=Foobar[baz]&subdirectory=version_subdir
.[some_extra]
"""
raise ValueError
def _parse_pip_syntax_editable(editable_req: str) -> tuple[str | None, str, set[str]]:
url = editable_req
# If a file path is specified with extras, strip off the extras.
@ -130,27 +119,9 @@ def _parse_pip_syntax_editable(editable_req: str) -> tuple[str | None, str, set[
url = f"{version_control}+{url}"
break
return Link(url).egg_fragment, url, set()
def parse_editable(editable_req: str) -> tuple[str | None, str, set[str]]:
"""Parses an editable requirement into:
- a requirement name with environment markers
- an URL
- extras
Accepted requirements:
- svn+http://blahblah@rev#egg=Foobar[baz]&subdirectory=version_subdir
- local_path[some_extra]
- Foobar[extra] @ svn+http://blahblah@rev#subdirectory=subdir ; markers
"""
try:
package_name, url, extras = _parse_direct_url_editable(editable_req)
except ValueError:
package_name, url, extras = _parse_pip_syntax_editable(editable_req)
link = Link(url)
if not link.is_vcs and not link.url.startswith("file:"):
if not link.is_vcs:
backends = ", ".join(vcs.all_schemes)
raise InstallationError(
f"{editable_req} is not a valid editable requirement. "
@ -158,13 +129,13 @@ def parse_editable(editable_req: str) -> tuple[str | None, str, set[str]]:
f"(beginning with {backends})."
)
# The project name can be inferred from local file URIs easily.
if not package_name and not link.url.startswith("file:"):
package_name = link.egg_fragment
if not package_name:
raise InstallationError(
f"Could not detect requirement name for '{editable_req}', "
"please specify one with your_package_name @ URL"
"Could not detect requirement name for '{}', please specify one "
"with #egg=your_package_name".format(editable_req)
)
return package_name, url, extras
return package_name, url, set()
def check_first_requirement_in_file(filename: str) -> None:
@ -191,7 +162,7 @@ def check_first_requirement_in_file(filename: str) -> None:
# If there is a line continuation, drop it, and append the next line.
if line.endswith("\\"):
line = line[:-2].strip() + next(lines, "")
get_requirement(line)
Requirement(line)
return
@ -220,12 +191,18 @@ def deduce_helpful_msg(req: str) -> str:
return msg
@dataclass(frozen=True)
class RequirementParts:
requirement: Requirement | None
link: Link | None
markers: Marker | None
extras: set[str]
def __init__(
self,
requirement: Optional[Requirement],
link: Optional[Link],
markers: Optional[Marker],
extras: Set[str],
):
self.requirement = requirement
self.link = link
self.markers = markers
self.extras = extras
def parse_req_from_editable(editable_req: str) -> RequirementParts:
@ -233,9 +210,9 @@ def parse_req_from_editable(editable_req: str) -> RequirementParts:
if name is not None:
try:
req: Requirement | None = get_requirement(name)
except InvalidRequirement as exc:
raise InstallationError(f"Invalid requirement: {name!r}: {exc}")
req: Optional[Requirement] = Requirement(name)
except InvalidRequirement:
raise InstallationError(f"Invalid requirement: '{name}'")
else:
req = None
@ -249,14 +226,16 @@ def parse_req_from_editable(editable_req: str) -> RequirementParts:
def install_req_from_editable(
editable_req: str,
comes_from: InstallRequirement | str | None = None,
comes_from: Optional[Union[InstallRequirement, str]] = None,
*,
use_pep517: Optional[bool] = None,
isolated: bool = False,
hash_options: dict[str, list[str]] | None = None,
global_options: Optional[List[str]] = None,
hash_options: Optional[Dict[str, List[str]]] = None,
constraint: bool = False,
user_supplied: bool = False,
permit_editable_wheels: bool = False,
config_settings: dict[str, str | list[str]] | None = None,
config_settings: Optional[Dict[str, Union[str, List[str]]]] = None,
) -> InstallRequirement:
parts = parse_req_from_editable(editable_req)
@ -268,7 +247,9 @@ def install_req_from_editable(
permit_editable_wheels=permit_editable_wheels,
link=parts.link,
constraint=constraint,
use_pep517=use_pep517,
isolated=isolated,
global_options=global_options,
hash_options=hash_options,
config_settings=config_settings,
extras=parts.extras,
@ -294,7 +275,7 @@ def _looks_like_path(name: str) -> bool:
return False
def _get_url_from_path(path: str, name: str) -> str | None:
def _get_url_from_path(path: str, name: str) -> Optional[str]:
"""
First, it checks whether a provided path is an installable directory. If it
is, returns the path.
@ -328,7 +309,7 @@ def _get_url_from_path(path: str, name: str) -> str | None:
return path_to_url(path)
def parse_req_from_line(name: str, line_source: str | None) -> RequirementParts:
def parse_req_from_line(name: str, line_source: Optional[str]) -> RequirementParts:
if is_url(name):
marker_sep = "; "
else:
@ -383,8 +364,8 @@ def parse_req_from_line(name: str, line_source: str | None) -> RequirementParts:
def _parse_req_string(req_as_string: str) -> Requirement:
try:
return get_requirement(req_as_string)
except InvalidRequirement as exc:
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 += deduce_helpful_msg(req_as_string)
@ -394,13 +375,24 @@ def parse_req_from_line(name: str, line_source: str | None) -> RequirementParts:
add_msg = "= is not a valid operator. Did you mean == ?"
else:
add_msg = ""
msg = with_source(f"Invalid requirement: {req_as_string!r}: {exc}")
msg = with_source(f"Invalid requirement: {req_as_string!r}")
if add_msg:
msg += f"\nHint: {add_msg}"
raise InstallationError(msg)
else:
# Deprecate extras after specifiers: "name>=1.0[extras]"
# This currently works by accident because _strip_extras() parses
# any extras in the end of the string and those are saved in
# RequirementParts
for spec in req.specifier:
spec_str = str(spec)
if spec_str.endswith("]"):
msg = f"Extras after version '{spec_str}'."
raise InstallationError(msg)
return req
if req_as_string is not None:
req: Requirement | None = _parse_req_string(req_as_string)
req: Optional[Requirement] = _parse_req_string(req_as_string)
else:
req = None
@ -409,14 +401,16 @@ def parse_req_from_line(name: str, line_source: str | None) -> RequirementParts:
def install_req_from_line(
name: str,
comes_from: str | InstallRequirement | None = None,
comes_from: Optional[Union[str, InstallRequirement]] = None,
*,
use_pep517: Optional[bool] = None,
isolated: bool = False,
hash_options: dict[str, list[str]] | None = None,
global_options: Optional[List[str]] = None,
hash_options: Optional[Dict[str, List[str]]] = None,
constraint: bool = False,
line_source: str | None = None,
line_source: Optional[str] = None,
user_supplied: bool = False,
config_settings: dict[str, str | list[str]] | None = None,
config_settings: Optional[Dict[str, Union[str, List[str]]]] = None,
) -> InstallRequirement:
"""Creates an InstallRequirement from a name, which might be a
requirement, directory containing 'setup.py', filename, or URL.
@ -431,7 +425,9 @@ def install_req_from_line(
comes_from,
link=parts.link,
markers=parts.markers,
use_pep517=use_pep517,
isolated=isolated,
global_options=global_options,
hash_options=hash_options,
config_settings=config_settings,
constraint=constraint,
@ -442,14 +438,15 @@ def install_req_from_line(
def install_req_from_req_string(
req_string: str,
comes_from: InstallRequirement | None = None,
comes_from: Optional[InstallRequirement] = None,
isolated: bool = False,
use_pep517: Optional[bool] = None,
user_supplied: bool = False,
) -> InstallRequirement:
try:
req = get_requirement(req_string)
except InvalidRequirement as exc:
raise InstallationError(f"Invalid requirement: {req_string!r}: {exc}")
except InvalidRequirement:
raise InstallationError(f"Invalid requirement: '{req_string}'")
domains_not_allowed = [
PyPI.file_storage_domain,
@ -472,6 +469,7 @@ def install_req_from_req_string(
req,
comes_from,
isolated=isolated,
use_pep517=use_pep517,
user_supplied=user_supplied,
)
@ -479,13 +477,15 @@ def install_req_from_req_string(
def install_req_from_parsed_requirement(
parsed_req: ParsedRequirement,
isolated: bool = False,
use_pep517: Optional[bool] = None,
user_supplied: bool = False,
config_settings: dict[str, str | list[str]] | None = None,
config_settings: Optional[Dict[str, Union[str, List[str]]]] = None,
) -> InstallRequirement:
if parsed_req.is_editable:
req = install_req_from_editable(
parsed_req.requirement,
comes_from=parsed_req.comes_from,
use_pep517=use_pep517,
constraint=parsed_req.constraint,
isolated=isolated,
user_supplied=user_supplied,
@ -496,7 +496,13 @@ def install_req_from_parsed_requirement(
req = install_req_from_line(
parsed_req.requirement,
comes_from=parsed_req.comes_from,
use_pep517=use_pep517,
isolated=isolated,
global_options=(
parsed_req.options.get("global_options", [])
if parsed_req.options
else []
),
hash_options=(
parsed_req.options.get("hashes", {}) if parsed_req.options else {}
),
@ -517,7 +523,9 @@ def install_req_from_link_and_ireq(
editable=ireq.editable,
link=link,
markers=ireq.markers,
use_pep517=ireq.use_pep517,
isolated=ireq.isolated,
global_options=ireq.global_options,
hash_options=ireq.hash_options,
config_settings=ireq.config_settings,
user_supplied=ireq.user_supplied,
@ -538,7 +546,9 @@ def install_req_drop_extras(ireq: InstallRequirement) -> InstallRequirement:
editable=ireq.editable,
link=ireq.link,
markers=ireq.markers,
use_pep517=ireq.use_pep517,
isolated=ireq.isolated,
global_options=ireq.global_options,
hash_options=ireq.hash_options,
constraint=ireq.constraint,
extras=[],

View file

@ -1,75 +0,0 @@
from collections.abc import Iterable, Iterator
from typing import Any
from pip._vendor.dependency_groups import DependencyGroupResolver
from pip._internal.exceptions import InstallationError
from pip._internal.utils.compat import tomllib
def parse_dependency_groups(groups: list[tuple[str, str]]) -> list[str]:
"""
Parse dependency groups data as provided via the CLI, in a `[path:]group` syntax.
Raises InstallationErrors if anything goes wrong.
"""
resolvers = _build_resolvers(path for (path, _) in groups)
return list(_resolve_all_groups(resolvers, groups))
def _resolve_all_groups(
resolvers: dict[str, DependencyGroupResolver], groups: list[tuple[str, str]]
) -> Iterator[str]:
"""
Run all resolution, converting any error from `DependencyGroupResolver` into
an InstallationError.
"""
for path, groupname in groups:
resolver = resolvers[path]
try:
yield from (str(req) for req in resolver.resolve(groupname))
except (ValueError, TypeError, LookupError) as e:
raise InstallationError(
f"[dependency-groups] resolution failed for '{groupname}' "
f"from '{path}': {e}"
) from e
def _build_resolvers(paths: Iterable[str]) -> dict[str, Any]:
resolvers = {}
for path in paths:
if path in resolvers:
continue
pyproject = _load_pyproject(path)
if "dependency-groups" not in pyproject:
raise InstallationError(
f"[dependency-groups] table was missing from '{path}'. "
"Cannot resolve '--group' option."
)
raw_dependency_groups = pyproject["dependency-groups"]
if not isinstance(raw_dependency_groups, dict):
raise InstallationError(
f"[dependency-groups] table was malformed in {path}. "
"Cannot resolve '--group' option."
)
resolvers[path] = DependencyGroupResolver(raw_dependency_groups)
return resolvers
def _load_pyproject(path: str) -> dict[str, Any]:
"""
This helper loads a pyproject.toml as TOML.
It raises an InstallationError if the operation fails.
"""
try:
with open(path, "rb") as fp:
return tomllib.load(fp)
except FileNotFoundError:
raise InstallationError(f"{path} not found. Cannot resolve '--group' option.")
except tomllib.TOMLDecodeError as e:
raise InstallationError(f"Error parsing {path}: {e}") from e
except OSError as e:
raise InstallationError(f"Error reading {path}: {e}") from e

View file

@ -2,40 +2,45 @@
Requirements file parsing
"""
from __future__ import annotations
import codecs
import locale
import logging
import optparse
import os
import re
import shlex
import sys
import urllib.parse
from collections.abc import Generator, Iterable
from dataclasses import dataclass
from optparse import Values
from typing import (
TYPE_CHECKING,
Any,
Callable,
NoReturn,
Dict,
Generator,
Iterable,
List,
Optional,
Tuple,
)
from pip._internal.cli import cmdoptions
from pip._internal.exceptions import InstallationError, 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
from pip._internal.utils.encoding import auto_decode
from pip._internal.utils.urls import get_url_scheme
if TYPE_CHECKING:
# NoReturn introduced in 3.6.2; imported only for type checking to maintain
# pip compatibility with older patch versions of Python 3.6
from typing import NoReturn
from pip._internal.index.package_finder import PackageFinder
from pip._internal.network.session import PipSession
__all__ = ["parse_requirements"]
ReqFileLines = Iterable[tuple[int, str]]
ReqFileLines = Iterable[Tuple[int, str]]
LineParser = Callable[[str], tuple[str, Values]]
LineParser = Callable[[str], Tuple[str, Values]]
SCHEME_RE = re.compile(r"^(http|https|file):", re.I)
COMMENT_RE = re.compile(r"(^|\s+)#.*$")
@ -46,7 +51,7 @@ COMMENT_RE = re.compile(r"(^|\s+)#.*$")
# 2013 Edition.
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,
@ -64,12 +69,13 @@ 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.global_options,
cmdoptions.hash,
cmdoptions.config_settings,
]
SUPPORTED_OPTIONS_EDITABLE_REQ: list[Callable[..., optparse.Option]] = [
SUPPORTED_OPTIONS_EDITABLE_REQ: List[Callable[..., optparse.Option]] = [
cmdoptions.config_settings,
]
@ -80,73 +86,59 @@ SUPPORTED_OPTIONS_EDITABLE_REQ_DEST = [
str(o().dest) for o in SUPPORTED_OPTIONS_EDITABLE_REQ
]
# order of BOMS is important: codecs.BOM_UTF16_LE is a prefix of codecs.BOM_UTF32_LE
# so data.startswith(BOM_UTF16_LE) would be true for UTF32_LE data
BOMS: list[tuple[bytes, str]] = [
(codecs.BOM_UTF8, "utf-8"),
(codecs.BOM_UTF32, "utf-32"),
(codecs.BOM_UTF32_BE, "utf-32-be"),
(codecs.BOM_UTF32_LE, "utf-32-le"),
(codecs.BOM_UTF16, "utf-16"),
(codecs.BOM_UTF16_BE, "utf-16-be"),
(codecs.BOM_UTF16_LE, "utf-16-le"),
]
PEP263_ENCODING_RE = re.compile(rb"coding[:=]\s*([-\w.]+)")
DEFAULT_ENCODING = "utf-8"
logger = logging.getLogger(__name__)
@dataclass(frozen=True)
class ParsedRequirement:
# TODO: replace this with slots=True when dropping Python 3.9 support.
__slots__ = (
"requirement",
"is_editable",
"comes_from",
"constraint",
"options",
"line_source",
)
requirement: str
is_editable: bool
comes_from: str
constraint: bool
options: dict[str, Any] | None
line_source: str | None
def __init__(
self,
requirement: str,
is_editable: bool,
comes_from: str,
constraint: bool,
options: Optional[Dict[str, Any]] = None,
line_source: Optional[str] = None,
) -> None:
self.requirement = requirement
self.is_editable = is_editable
self.comes_from = comes_from
self.options = options
self.constraint = constraint
self.line_source = line_source
@dataclass(frozen=True)
class ParsedLine:
__slots__ = ("filename", "lineno", "args", "opts", "constraint")
def __init__(
self,
filename: str,
lineno: int,
args: str,
opts: Values,
constraint: bool,
) -> None:
self.filename = filename
self.lineno = lineno
self.opts = opts
self.constraint = constraint
filename: str
lineno: int
args: str
opts: Values
constraint: bool
@property
def is_editable(self) -> bool:
return bool(self.opts.editables)
@property
def requirement(self) -> str | None:
if self.args:
return self.args
elif self.is_editable:
if args:
self.is_requirement = True
self.is_editable = False
self.requirement = args
elif opts.editables:
self.is_requirement = True
self.is_editable = True
# We don't support multiple -e on one line
return self.opts.editables[0]
return None
self.requirement = opts.editables[0]
else:
self.is_requirement = False
def parse_requirements(
filename: str,
session: PipSession,
finder: PackageFinder | None = None,
options: optparse.Values | None = None,
finder: Optional["PackageFinder"] = None,
options: Optional[optparse.Values] = None,
constraint: bool = False,
) -> Generator[ParsedRequirement, None, None]:
"""Parse a requirements file and yield ParsedRequirement instances.
@ -183,7 +175,7 @@ def preprocess(content: str) -> ReqFileLines:
def handle_requirement_line(
line: ParsedLine,
options: optparse.Values | None = None,
options: Optional[optparse.Values] = None,
) -> ParsedRequirement:
# preserve for the nested code path
line_comes_from = "{} {} (line {})".format(
@ -192,7 +184,7 @@ def handle_requirement_line(
line.lineno,
)
assert line.requirement is not None
assert line.is_requirement
# get the options that apply to requirements
if line.is_editable:
@ -219,9 +211,9 @@ def handle_option_line(
opts: Values,
filename: str,
lineno: int,
finder: PackageFinder | None = None,
options: optparse.Values | None = None,
session: PipSession | None = None,
finder: Optional["PackageFinder"] = None,
options: Optional[optparse.Values] = None,
session: Optional[PipSession] = None,
) -> None:
if opts.hashes:
logger.warning(
@ -287,10 +279,10 @@ def handle_option_line(
def handle_line(
line: ParsedLine,
options: optparse.Values | None = None,
finder: PackageFinder | None = None,
session: PipSession | None = None,
) -> ParsedRequirement | None:
options: Optional[optparse.Values] = None,
finder: Optional["PackageFinder"] = None,
session: Optional[PipSession] = None,
) -> Optional[ParsedRequirement]:
"""Handle a single parsed requirements line; This can result in
creating/yielding requirements, or updating the finder.
@ -314,7 +306,7 @@ def handle_line(
affect the finder.
"""
if line.requirement is not None:
if line.is_requirement:
parsed_req = handle_requirement_line(line, options)
return parsed_req
else:
@ -342,18 +334,13 @@ class RequirementsFileParser:
self, filename: str, constraint: bool
) -> Generator[ParsedLine, None, None]:
"""Parse a given file, yielding parsed lines."""
yield from self._parse_and_recurse(
filename, constraint, [{os.path.abspath(filename): None}]
)
yield from self._parse_and_recurse(filename, constraint)
def _parse_and_recurse(
self,
filename: str,
constraint: bool,
parsed_files_stack: list[dict[str, str | None]],
self, filename: str, constraint: bool
) -> Generator[ParsedLine, None, None]:
for line in self._parse_file(filename, constraint):
if line.requirement is None and (
if not line.is_requirement and (
line.opts.requirements or line.opts.constraints
):
# parse a nested requirements file
@ -371,30 +358,12 @@ class RequirementsFileParser:
# original file and nested file are paths
elif not SCHEME_RE.search(req_path):
# do a join so relative paths work
# and then abspath so that we can identify recursive references
req_path = os.path.abspath(
os.path.join(
os.path.dirname(filename),
req_path,
)
req_path = os.path.join(
os.path.dirname(filename),
req_path,
)
parsed_files = parsed_files_stack[0]
if req_path in parsed_files:
initial_file = parsed_files[req_path]
tail = (
f" and again in {initial_file}"
if initial_file is not None
else ""
)
raise RequirementsFileParseError(
f"{req_path} recursively references itself in {filename}{tail}"
)
# Keeping a track where was each file first included in
new_parsed_files = parsed_files.copy()
new_parsed_files[req_path] = filename
yield from self._parse_and_recurse(
req_path, nested_constraint, [new_parsed_files, *parsed_files_stack]
)
yield from self._parse_and_recurse(req_path, nested_constraint)
else:
yield line
@ -422,8 +391,8 @@ class RequirementsFileParser:
)
def get_line_parser(finder: PackageFinder | None) -> LineParser:
def parse_line(line: str) -> tuple[str, Values]:
def get_line_parser(finder: Optional["PackageFinder"]) -> LineParser:
def parse_line(line: str) -> Tuple[str, Values]:
# Build new parser for each line since it accumulates appendable
# options.
parser = build_parser()
@ -446,7 +415,7 @@ def get_line_parser(finder: PackageFinder | None) -> 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.
@ -455,7 +424,7 @@ def break_args_options(line: str) -> tuple[str, str]:
args = []
options = tokens[:]
for token in tokens:
if token.startswith(("-", "--")):
if token.startswith("-") or token.startswith("--"):
break
else:
args.append(token)
@ -481,7 +450,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
@ -496,7 +465,7 @@ 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 COMMENT_RE.match(line):
@ -560,7 +529,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.
@ -568,12 +537,10 @@ def get_file_content(url: str, session: PipSession) -> tuple[str, str]:
:param url: File path or url.
:param session: PipSession instance.
"""
scheme = urllib.parse.urlsplit(url).scheme
scheme = get_url_scheme(url)
# Pip has special support for file:// URLs (LocalFSAdapter).
if scheme in ["http", "https", "file"]:
# Delay importing heavy network modules until absolutely necessary.
from pip._internal.network.utils import raise_for_status
resp = session.get(url)
raise_for_status(resp)
return resp.url, resp.text
@ -581,39 +548,7 @@ def get_file_content(url: str, session: PipSession) -> tuple[str, str]:
# Assume this is a bare path.
try:
with open(url, "rb") as f:
raw_content = f.read()
content = auto_decode(f.read())
except OSError as exc:
raise InstallationError(f"Could not open requirements file: {exc}")
content = _decode_req_file(raw_content, url)
return url, content
def _decode_req_file(data: bytes, url: str) -> str:
for bom, encoding in BOMS:
if data.startswith(bom):
return data[len(bom) :].decode(encoding)
for line in data.split(b"\n")[:2]:
if line[0:1] == b"#":
result = PEP263_ENCODING_RE.search(line)
if result is not None:
encoding = result.groups()[0].decode("ascii")
return data.decode(encoding)
try:
return data.decode(DEFAULT_ENCODING)
except UnicodeDecodeError:
locale_encoding = locale.getpreferredencoding(False) or sys.getdefaultencoding()
logging.warning(
"unable to decode data from %s with default encoding %s, "
"falling back to encoding from locale: %s. "
"If this is intentional you should specify the encoding with a "
"PEP-263 style comment, e.g. '# -*- coding: %s -*-'",
url,
DEFAULT_ENCODING,
locale_encoding,
locale_encoding,
)
return data.decode(locale_encoding)

View file

@ -1,5 +1,3 @@
from __future__ import annotations
import functools
import logging
import os
@ -7,10 +5,9 @@ import shutil
import sys
import uuid
import zipfile
from collections.abc import Collection, Iterable
from optparse import Values
from pathlib import Path
from typing import Any
from typing import Any, Collection, Dict, Iterable, List, Optional, Sequence, Union
from pip._vendor.packaging.markers import Marker
from pip._vendor.packaging.requirements import Requirement
@ -34,6 +31,12 @@ from pip._internal.models.direct_url import DirectUrl
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
from pip._internal.operations.build.metadata_legacy import (
generate_metadata as generate_metadata_legacy,
)
from pip._internal.operations.install.editable_legacy import (
install_editable as install_editable_legacy,
)
from pip._internal.operations.install.wheel import install_wheel
from pip._internal.pyproject import load_pyproject_toml, make_pyproject_path
from pip._internal.req.req_uninstall import UninstallPathSet
@ -49,7 +52,7 @@ from pip._internal.utils.misc import (
redact_auth_from_requirement,
redact_auth_from_url,
)
from pip._internal.utils.packaging import get_requirement
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.unpacking import unpack_file
@ -68,15 +71,17 @@ class InstallRequirement:
def __init__(
self,
req: Requirement | None,
comes_from: str | InstallRequirement | None,
req: Optional[Requirement],
comes_from: Optional[Union[str, "InstallRequirement"]],
editable: bool = False,
link: Link | None = None,
markers: Marker | None = None,
link: Optional[Link] = None,
markers: Optional[Marker] = None,
use_pep517: Optional[bool] = None,
isolated: bool = False,
*,
hash_options: dict[str, list[str]] | None = None,
config_settings: dict[str, str | list[str]] | None = None,
global_options: Optional[List[str]] = None,
hash_options: Optional[Dict[str, List[str]]] = None,
config_settings: Optional[Dict[str, Union[str, List[str]]]] = None,
constraint: bool = False,
extras: Collection[str] = (),
user_supplied: bool = False,
@ -94,7 +99,7 @@ class InstallRequirement:
# 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: str | None = None
self.source_dir: Optional[str] = None
if self.editable:
assert link
if link.is_file:
@ -110,14 +115,14 @@ class InstallRequirement:
# When this InstallRequirement is a wheel obtained from the cache of locally
# built wheels, this is the source link corresponding to the cache entry, which
# was used to download and build the cached wheel.
self.cached_wheel_source_link: Link | None = None
self.cached_wheel_source_link: Optional[Link] = None
# Information about the location of the artifact that was downloaded . This
# property is guaranteed to be set in resolver results.
self.download_info: DirectUrl | None = None
self.download_info: Optional[DirectUrl] = None
# Path to any downloaded or already-existing package.
self.local_file_path: str | None = None
self.local_file_path: Optional[str] = None
if self.link and self.link.is_file:
self.local_file_path = self.link.file_path
@ -132,15 +137,16 @@ class InstallRequirement:
self.markers = markers
# This holds the Distribution object if this requirement is already installed.
self.satisfied_by: BaseDistribution | None = None
self.satisfied_by: Optional[BaseDistribution] = 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: TempDirectory | None = None
self._temp_build_dir: Optional[TempDirectory] = None
# Set to True after successful installation
self.install_succeeded: bool | None = None
self.install_succeeded: Optional[bool] = None
# Supplied options
self.global_options = global_options if global_options else []
self.hash_options = hash_options if hash_options else {}
self.config_settings = config_settings
# Set to True after successful preparation of this requirement
@ -157,26 +163,39 @@ 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: str | None = None
# The cached metadata distribution that this requirement represents.
# See get_dist / set_dist.
self._distribution: BaseDistribution | None = None
self.metadata_directory: Optional[str] = None
# The static build requirements (from pyproject.toml)
self.pyproject_requires: list[str] | None = None
self.pyproject_requires: Optional[List[str]] = 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: BuildBackendHookCaller | None = None
self.pep517_backend: Optional[BuildBackendHookCaller] = None
# Are we using PEP 517 for this requirement?
# After pyproject.toml has been loaded, the only valid values are True
# and False. Before loading, None is valid (meaning "use the default").
# Setting an explicit value before loading pyproject.toml is supported,
# but after loading this flag should be treated as read only.
self.use_pep517 = use_pep517
# If config settings are provided, enforce PEP 517.
if self.config_settings:
if self.use_pep517 is False:
logger.warning(
"--no-use-pep517 ignored for %s "
"because --config-settings are specified.",
self,
)
self.use_pep517 = True
# This requirement needs more preparation before it can be built
self.needs_more_preparation = False
# This requirement needs to be unpacked before it can be installed.
self._archive_source: Path | None = None
self._archive_source: Optional[Path] = None
def __str__(self) -> str:
if self.req:
@ -195,7 +214,7 @@ class InstallRequirement:
s += f" in {location}"
if self.comes_from:
if isinstance(self.comes_from, str):
comes_from: str | None = self.comes_from
comes_from: Optional[str] = self.comes_from
else:
comes_from = self.comes_from.from_path()
if comes_from:
@ -203,9 +222,8 @@ class InstallRequirement:
return s
def __repr__(self) -> str:
return (
f"<{self.__class__.__name__} object: "
f"{str(self)} editable={self.editable!r}>"
return "<{} object: {} editable={!r}>".format(
self.__class__.__name__, str(self), self.editable
)
def format_debug(self) -> str:
@ -221,13 +239,15 @@ class InstallRequirement:
# Things that are valid for all kinds of requirements?
@property
def name(self) -> str | None:
def name(self) -> Optional[str]:
if self.req is None:
return None
return self.req.name
@functools.cached_property
@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(
@ -256,14 +276,19 @@ class InstallRequirement:
specifiers = self.req.specifier
return len(specifiers) == 1 and next(iter(specifiers)).operator in {"==", "==="}
def match_markers(self, extras_requested: Iterable[str] | None = None) -> bool:
def match_markers(self, extras_requested: Optional[Iterable[str]] = None) -> bool:
if not extras_requested:
# Provide an extra to safely evaluate the markers
# without matching any extra
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})
# TODO: Remove these two variants when packaging is upgraded to
# support the marker comparison logic specified in PEP 685.
or self.markers.evaluate({"extra": safe_extra(extra)})
or self.markers.evaluate({"extra": canonicalize_name(extra)})
for extra in extras_requested
)
else:
return True
@ -305,13 +330,13 @@ class InstallRequirement:
good_hashes.setdefault(link.hash_name, []).append(link.hash)
return Hashes(good_hashes)
def from_path(self) -> str | None:
def from_path(self) -> Optional[str]:
"""Format a nice indicator to show where this "comes from" """
if self.req is None:
return None
s = str(self.req)
if self.comes_from:
comes_from: str | None
comes_from: Optional[str]
if isinstance(self.comes_from, str):
comes_from = self.comes_from
else:
@ -375,7 +400,7 @@ class InstallRequirement:
else:
op = "==="
self.req = get_requirement(
self.req = Requirement(
"".join(
[
self.metadata["Name"],
@ -401,7 +426,7 @@ class InstallRequirement:
metadata_name,
self.name,
)
self.req = get_requirement(metadata_name)
self.req = Requirement(metadata_name)
def check_if_exists(self, use_user_site: bool) -> None:
"""Find an installed distribution that satisfies or conflicts
@ -468,6 +493,13 @@ class InstallRequirement:
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")
return setup_cfg
@property
def pyproject_toml_path(self) -> str:
assert self.source_dir, f"No source dir for {self}"
@ -477,12 +509,20 @@ class InstallRequirement:
"""Load the pyproject.toml file.
After calling this routine, all of the attributes related to PEP 517
processing for this requirement have been set.
processing for this requirement have been set. In particular, the
use_pep517 attribute can be used to determine whether we should
follow the PEP 517 or legacy (setup.py) code path.
"""
pyproject_toml_data = load_pyproject_toml(
self.pyproject_toml_path, self.setup_py_path, str(self)
self.use_pep517, self.pyproject_toml_path, self.setup_py_path, str(self)
)
assert pyproject_toml_data
if pyproject_toml_data is None:
assert not self.config_settings
self.use_pep517 = False
return
self.use_pep517 = True
requires, backend, check, backend_path = pyproject_toml_data
self.requirements_to_check = check
self.pyproject_requires = requires
@ -493,15 +533,23 @@ class InstallRequirement:
backend_path=backend_path,
)
def editable_sanity_check(self) -> None:
def isolated_editable_sanity_check(self) -> None:
"""Check that an editable requirement if valid for use with PEP 517/518.
This verifies that an editable has a build backend that supports PEP 660.
This verifies that an editable that has a pyproject.toml either supports PEP 660
or as a setup.py or a setup.cfg
"""
if self.editable and not self.supports_pyproject_editable:
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)
):
raise InstallationError(
f"Project {self} uses a build backend "
f"that is missing the 'build_editable' hook, so "
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."
)
@ -515,21 +563,30 @@ class InstallRequirement:
assert self.source_dir, f"No source dir for {self}"
details = self.name or f"from {self.link}"
assert self.pep517_backend is not None
if (
self.editable
and self.permit_editable_wheels
and self.supports_pyproject_editable
):
self.metadata_directory = generate_editable_metadata(
build_env=self.build_env,
backend=self.pep517_backend,
details=details,
)
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.metadata_directory = generate_editable_metadata(
build_env=self.build_env,
backend=self.pep517_backend,
details=details,
)
else:
self.metadata_directory = generate_metadata(
build_env=self.build_env,
backend=self.pep517_backend,
details=details,
)
else:
self.metadata_directory = generate_metadata(
self.metadata_directory = generate_metadata_legacy(
build_env=self.build_env,
backend=self.pep517_backend,
setup_py_path=self.setup_py_path,
source_dir=self.unpacked_source_directory,
isolated=self.isolated,
details=details,
)
@ -548,13 +605,8 @@ class InstallRequirement:
return self._metadata
def set_dist(self, distribution: BaseDistribution) -> None:
self._distribution = distribution
def get_dist(self) -> BaseDistribution:
if self._distribution is not None:
return self._distribution
elif self.metadata_directory:
if self.metadata_directory:
return get_directory_distribution(self.metadata_directory)
elif self.local_file_path and self.is_wheel:
assert self.req is not None
@ -651,7 +703,7 @@ class InstallRequirement:
# Top-level Actions
def uninstall(
self, auto_confirm: bool = False, verbose: bool = False
) -> UninstallPathSet | None:
) -> Optional[UninstallPathSet]:
"""
Uninstall the distribution currently satisfying this requirement.
@ -689,7 +741,7 @@ class InstallRequirement:
name = _clean_zip_name(path, rootdir)
return self.req.name + "/" + name
def archive(self, build_dir: str | None) -> None:
def archive(self, build_dir: Optional[str]) -> None:
"""Saves archive to provided build_dir.
Used for saving downloaded VCS requirements as part of `pip download`.
@ -758,9 +810,10 @@ class InstallRequirement:
def install(
self,
root: str | None = None,
home: str | None = None,
prefix: str | None = None,
global_options: Optional[Sequence[str]] = None,
root: Optional[str] = None,
home: Optional[str] = None,
prefix: Optional[str] = None,
warn_script_location: bool = True,
use_user_site: bool = False,
pycompile: bool = True,
@ -775,6 +828,28 @@ class InstallRequirement:
prefix=prefix,
)
if self.editable and not self.is_wheel:
if self.config_settings:
logger.warning(
"--config-settings ignored for legacy editable install of %s. "
"Consider upgrading to a version of setuptools "
"that supports PEP 660 (>= 64).",
self,
)
install_editable_legacy(
global_options=global_options if global_options is not None else [],
prefix=prefix,
home=home,
use_user_site=use_user_site,
name=self.req.name,
setup_py_path=self.setup_py_path,
isolated=self.isolated,
build_env=self.build_env,
unpacked_source_directory=self.unpacked_source_directory,
)
self.install_succeeded = True
return
assert self.is_wheel
assert self.local_file_path
@ -819,10 +894,30 @@ def check_invalid_constraint_type(req: InstallRequirement) -> str:
return problem
def _has_option(options: Values, reqs: list[InstallRequirement], option: str) -> bool:
def _has_option(options: Values, reqs: List[InstallRequirement], option: str) -> bool:
if getattr(options, option, None):
return True
for req in reqs:
if getattr(req, option, None):
return True
return False
def check_legacy_setup_py_options(
options: Values,
reqs: List[InstallRequirement],
) -> None:
has_build_options = _has_option(options, reqs, "build_options")
has_global_options = _has_option(options, reqs, "global_options")
if has_build_options or has_global_options:
deprecated(
reason="--build-option and --global-option are deprecated.",
issue=11859,
replacement="to use --config-settings",
gone_in="24.2",
)
logger.warning(
"Implying --no-binary=:all: due to the presence of "
"--build-option / --global-option. "
)
options.format_control.disallow_binaries()

View file

@ -1,9 +1,13 @@
import logging
from collections import OrderedDict
from typing import Dict, List
from pip._vendor.packaging.specifiers import LegacySpecifier
from pip._vendor.packaging.utils import canonicalize_name
from pip._vendor.packaging.version import LegacyVersion
from pip._internal.req.req_install import InstallRequirement
from pip._internal.utils.deprecation import deprecated
logger = logging.getLogger(__name__)
@ -12,10 +16,10 @@ 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(
@ -64,11 +68,11 @@ class RequirementSet:
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())
@property
def requirements_to_install(self) -> list[InstallRequirement]:
def requirements_to_install(self) -> List[InstallRequirement]:
"""Return the list of requirements that need to be installed.
TODO remove this property together with the legacy resolver, since the new
@ -79,3 +83,37 @@ class RequirementSet:
for install_req in self.all_requirements
if not install_req.constraint and not install_req.satisfied_by
]
def warn_legacy_versions_and_specifiers(self) -> None:
for req in self.requirements_to_install:
version = req.get_dist().version
if isinstance(version, LegacyVersion):
deprecated(
reason=(
f"pip has selected the non standard version {version} "
f"of {req}. In the future this version will be "
f"ignored as it isn't standard compliant."
),
replacement=(
"set or update constraints to select another version "
"or contact the package author to fix the version number"
),
issue=12063,
gone_in="24.1",
)
for dep in req.get_dist().iter_dependencies():
if any(isinstance(spec, LegacySpecifier) for spec in dep.specifier):
deprecated(
reason=(
f"pip has selected {req} {version} which has non "
f"standard dependency specifier {dep}. "
f"In the future this version of {req} will be "
f"ignored as it isn't standard compliant."
),
replacement=(
"set or update constraints to select another version "
"or contact the package author to fix the version number"
),
issue=12063,
gone_in="24.1",
)

View file

@ -1,14 +1,11 @@
from __future__ import annotations
import functools
import os
import sys
import sysconfig
from collections.abc import Generator, Iterable
from importlib.util import cache_from_source
from typing import Any, Callable
from typing import Any, Callable, Dict, Generator, Iterable, List, Optional, Set, Tuple
from pip._internal.exceptions import LegacyDistutilsInstall, UninstallMissingRecord
from pip._internal.exceptions import UninstallationError
from pip._internal.locations import get_bin_prefix, get_bin_user
from pip._internal.metadata import BaseDistribution
from pip._internal.utils.compat import WINDOWS
@ -41,11 +38,11 @@ def _script_names(
def _unique(
fn: Callable[..., Generator[Any, None, None]],
fn: Callable[..., Generator[Any, None, None]]
) -> Callable[..., Generator[Any, None, None]]:
@functools.wraps(fn)
def unique(*args: Any, **kw: Any) -> Generator[Any, None, None]:
seen: set[Any] = set()
seen: Set[Any] = set()
for item in fn(*args, **kw):
if item not in seen:
seen.add(item)
@ -64,7 +61,7 @@ def uninstallation_paths(dist: BaseDistribution) -> Generator[str, None, None]:
UninstallPathSet.add() takes care of the __pycache__ .py[co].
If RECORD is not found, raises an error,
If RECORD is not found, raises UninstallationError,
with possible information from the INSTALLER file.
https://packaging.python.org/specifications/recording-installed-packages/
@ -74,7 +71,17 @@ def uninstallation_paths(dist: BaseDistribution) -> Generator[str, None, None]:
entries = dist.iter_declared_entries()
if entries is None:
raise UninstallMissingRecord(distribution=dist)
msg = f"Cannot uninstall {dist}, RECORD file not found."
installer = dist.installer
if not installer or installer == "pip":
dep = f"{dist.raw_name}=={dist.version}"
msg += (
" You might be able to recover from this via: "
f"'pip install --force-reinstall --no-deps {dep}'."
)
else:
msg += f" Hint: The package was installed by {installer}."
raise UninstallationError(msg)
for entry in entries:
path = os.path.join(location, entry)
@ -88,14 +95,14 @@ def uninstallation_paths(dist: BaseDistribution) -> Generator[str, None, None]:
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("*"))
@ -107,7 +114,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
@ -116,7 +123,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))
@ -126,8 +133,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)
@ -141,7 +148,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
@ -197,10 +204,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.
@ -300,15 +307,15 @@ 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()
# Create local cache of normalize_path results. Creating an UninstallPathSet
# can result in hundreds/thousands of redundant calls to normalize_path with
# the same args, which hurts performance.
self._normalize_path_cached = functools.lru_cache(normalize_path)
self._normalize_path_cached = functools.lru_cache()(normalize_path)
def _permitted(self, path: str) -> bool:
"""
@ -360,7 +367,7 @@ class UninstallPathSet:
)
return
dist_name_version = f"{self._dist.raw_name}-{self._dist.raw_version}"
dist_name_version = f"{self._dist.raw_name}-{self._dist.version}"
logger.info("Uninstalling %s:", dist_name_version)
with indent_log():
@ -424,7 +431,7 @@ 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:
@ -502,19 +509,22 @@ class UninstallPathSet:
paths_to_remove.add(f"{path}.pyo")
elif dist.installed_by_distutils:
raise LegacyDistutilsInstall(distribution=dist)
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(
dist.raw_name,
)
)
elif dist.installed_as_egg:
# package installed by easy_install
# We cannot match on dist.egg_name because it can slightly vary
# i.e. setuptools-0.6c11-py2.6.egg vs setuptools-0.6rc11-py2.6.egg
# XXX We use normalized_dist_location because dist_location my contain
# a trailing / if the distribution is a zipped egg
# (which is not a directory).
paths_to_remove.add(normalized_dist_location)
easy_install_egg = os.path.split(normalized_dist_location)[1]
paths_to_remove.add(dist_location)
easy_install_egg = os.path.split(dist_location)[1]
easy_install_pth = os.path.join(
os.path.dirname(normalized_dist_location),
os.path.dirname(dist_location),
"easy-install.pth",
)
paths_to_remove.add_pth(easy_install_pth, "./" + easy_install_egg)
@ -584,8 +594,8 @@ class UninstallPathSet:
class UninstallPthEntries:
def __init__(self, pth_file: str) -> None:
self.file = pth_file
self.entries: set[str] = set()
self._saved_lines: list[bytes] | None = None
self.entries: Set[str] = set()
self._saved_lines: Optional[List[bytes]] = None
def add(self, entry: str) -> None:
entry = os.path.normcase(entry)