Voice et bot modif
This commit is contained in:
parent
189d56026b
commit
7333a22bcd
10774 changed files with 634644 additions and 933308 deletions
|
|
@ -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,
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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=[],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue