This commit is contained in:
ton
2024-10-07 10:13:40 +07:00
parent aa1631742f
commit 3a7d696db6
9729 changed files with 1832837 additions and 161742 deletions

View File

@@ -1,5 +1,6 @@
import collections
import logging
from dataclasses import dataclass
from typing import Generator, List, Optional, Sequence, Tuple
from pip._internal.utils.logging import indent_log
@@ -18,12 +19,9 @@ __all__ = [
logger = logging.getLogger(__name__)
@dataclass(frozen=True)
class InstallationResult:
def __init__(self, name: str) -> None:
self.name = name
def __repr__(self) -> str:
return f"InstallationResult(name={self.name!r})"
name: str
def _validate_requirements(

View File

@@ -8,10 +8,12 @@ These are meant to be used elsewhere within pip to create instances of
InstallRequirement.
"""
import copy
import logging
import os
import re
from typing import Dict, List, Optional, Set, Tuple, Union
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
@@ -57,6 +59,31 @@ def convert_extras(extras: Optional[str]) -> Set[str]:
return get_requirement("placeholder" + extras.lower()).extras
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: Optional[re.Match[str]] = re.fullmatch(
# see https://peps.python.org/pep-0508/#complete-grammar
r"([\w\t .-]+)(\[[^\]]*\])?(.*)",
str(req),
flags=re.ASCII,
)
# ireq.req is a valid requirement so the regex should always match
assert (
match is not None
), f"regex match on requirement {req} failed, this should never happen"
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 = "[%s]" % ",".join(sorted(new_extras)) if new_extras else ""
return get_requirement(f"{pre}{extras}{post}")
def parse_editable(editable_req: str) -> Tuple[Optional[str], str, Set[str]]:
"""Parses an editable requirement into:
- a requirement name
@@ -106,8 +133,8 @@ def parse_editable(editable_req: str) -> Tuple[Optional[str], str, Set[str]]:
package_name = link.egg_fragment
if not package_name:
raise InstallationError(
"Could not detect requirement name for '{}', please specify one "
"with #egg=your_package_name".format(editable_req)
f"Could not detect requirement name for '{editable_req}', "
"please specify one with #egg=your_package_name"
)
return package_name, url, set()
@@ -136,7 +163,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, "")
Requirement(line)
get_requirement(line)
return
@@ -165,18 +192,12 @@ def deduce_helpful_msg(req: str) -> str:
return msg
@dataclass(frozen=True)
class RequirementParts:
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
requirement: Optional[Requirement]
link: Optional[Link]
markers: Optional[Marker]
extras: Set[str]
def parse_req_from_editable(editable_req: str) -> RequirementParts:
@@ -184,9 +205,9 @@ def parse_req_from_editable(editable_req: str) -> RequirementParts:
if name is not None:
try:
req: Optional[Requirement] = Requirement(name)
except InvalidRequirement:
raise InstallationError(f"Invalid requirement: '{name}'")
req: Optional[Requirement] = get_requirement(name)
except InvalidRequirement as exc:
raise InstallationError(f"Invalid requirement: {name!r}: {exc}")
else:
req = None
@@ -338,8 +359,8 @@ def parse_req_from_line(name: str, line_source: Optional[str]) -> RequirementPar
def _parse_req_string(req_as_string: str) -> Requirement:
try:
req = get_requirement(req_as_string)
except InvalidRequirement:
return get_requirement(req_as_string)
except InvalidRequirement as exc:
if os.path.sep in req_as_string:
add_msg = "It looks like a path."
add_msg += deduce_helpful_msg(req_as_string)
@@ -349,21 +370,10 @@ def parse_req_from_line(name: str, line_source: Optional[str]) -> RequirementPar
add_msg = "= is not a valid operator. Did you mean == ?"
else:
add_msg = ""
msg = with_source(f"Invalid requirement: {req_as_string!r}")
msg = with_source(f"Invalid requirement: {req_as_string!r}: {exc}")
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: Optional[Requirement] = _parse_req_string(req_as_string)
@@ -419,8 +429,8 @@ def install_req_from_req_string(
) -> InstallRequirement:
try:
req = get_requirement(req_string)
except InvalidRequirement:
raise InstallationError(f"Invalid requirement: '{req_string}'")
except InvalidRequirement as exc:
raise InstallationError(f"Invalid requirement: {req_string!r}: {exc}")
domains_not_allowed = [
PyPI.file_storage_domain,
@@ -436,7 +446,7 @@ def install_req_from_req_string(
raise InstallationError(
"Packages installed from PyPI cannot depend on packages "
"which are not also hosted on PyPI.\n"
"{} depends on {} ".format(comes_from.name, req)
f"{comes_from.name} depends on {req} "
)
return InstallRequirement(
@@ -504,3 +514,47 @@ def install_req_from_link_and_ireq(
config_settings=ireq.config_settings,
user_supplied=ireq.user_supplied,
)
def install_req_drop_extras(ireq: InstallRequirement) -> InstallRequirement:
"""
Creates a new InstallationRequirement using the given template but without
any extras. Sets the original requirement as the new one's parent
(comes_from).
"""
return InstallRequirement(
req=(
_set_requirement_extras(ireq.req, set()) if ireq.req is not None else None
),
comes_from=ireq,
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=[],
config_settings=ireq.config_settings,
user_supplied=ireq.user_supplied,
permit_editable_wheels=ireq.permit_editable_wheels,
)
def install_req_extend_extras(
ireq: InstallRequirement,
extras: Collection[str],
) -> InstallRequirement:
"""
Returns a copy of an installation requirement with some additional extras.
Makes a shallow copy of the ireq object.
"""
result = copy.copy(ireq)
result.extras = {*ireq.extras, *extras}
result.req = (
_set_requirement_extras(ireq.req, result.extras)
if ireq.req is not None
else None
)
return result

View File

@@ -17,6 +17,7 @@ from typing import (
Generator,
Iterable,
List,
NoReturn,
Optional,
Tuple,
)
@@ -24,17 +25,11 @@ from typing import (
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"]
@@ -75,8 +70,16 @@ SUPPORTED_OPTIONS_REQ: List[Callable[..., optparse.Option]] = [
cmdoptions.config_settings,
]
SUPPORTED_OPTIONS_EDITABLE_REQ: List[Callable[..., optparse.Option]] = [
cmdoptions.config_settings,
]
# the 'dest' string values
SUPPORTED_OPTIONS_REQ_DEST = [str(o().dest) for o in SUPPORTED_OPTIONS_REQ]
SUPPORTED_OPTIONS_EDITABLE_REQ_DEST = [
str(o().dest) for o in SUPPORTED_OPTIONS_EDITABLE_REQ
]
logger = logging.getLogger(__name__)
@@ -128,7 +131,7 @@ class ParsedLine:
def parse_requirements(
filename: str,
session: PipSession,
session: "PipSession",
finder: Optional["PackageFinder"] = None,
options: Optional[optparse.Values] = None,
constraint: bool = False,
@@ -178,31 +181,25 @@ def handle_requirement_line(
assert line.is_requirement
# get the options that apply to requirements
if line.is_editable:
# For editable requirements, we don't support per-requirement
# options, so just return the parsed requirement.
return ParsedRequirement(
requirement=line.requirement,
is_editable=line.is_editable,
comes_from=line_comes_from,
constraint=line.constraint,
)
supported_dest = SUPPORTED_OPTIONS_EDITABLE_REQ_DEST
else:
# get the options that apply to requirements
req_options = {}
for dest in SUPPORTED_OPTIONS_REQ_DEST:
if dest in line.opts.__dict__ and line.opts.__dict__[dest]:
req_options[dest] = line.opts.__dict__[dest]
supported_dest = SUPPORTED_OPTIONS_REQ_DEST
req_options = {}
for dest in supported_dest:
if dest in line.opts.__dict__ and line.opts.__dict__[dest]:
req_options[dest] = line.opts.__dict__[dest]
line_source = f"line {line.lineno} of {line.filename}"
return ParsedRequirement(
requirement=line.requirement,
is_editable=line.is_editable,
comes_from=line_comes_from,
constraint=line.constraint,
options=req_options,
line_source=line_source,
)
line_source = f"line {line.lineno} of {line.filename}"
return ParsedRequirement(
requirement=line.requirement,
is_editable=line.is_editable,
comes_from=line_comes_from,
constraint=line.constraint,
options=req_options,
line_source=line_source,
)
def handle_option_line(
@@ -211,7 +208,7 @@ def handle_option_line(
lineno: int,
finder: Optional["PackageFinder"] = None,
options: Optional[optparse.Values] = None,
session: Optional[PipSession] = None,
session: Optional["PipSession"] = None,
) -> None:
if opts.hashes:
logger.warning(
@@ -279,7 +276,7 @@ def handle_line(
line: ParsedLine,
options: Optional[optparse.Values] = None,
finder: Optional["PackageFinder"] = None,
session: Optional[PipSession] = None,
session: Optional["PipSession"] = None,
) -> Optional[ParsedRequirement]:
"""Handle a single parsed requirements line; This can result in
creating/yielding requirements, or updating the finder.
@@ -322,7 +319,7 @@ def handle_line(
class RequirementsFileParser:
def __init__(
self,
session: PipSession,
session: "PipSession",
line_parser: LineParser,
) -> None:
self._session = session
@@ -527,7 +524,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.
@@ -535,10 +532,12 @@ def get_file_content(url: str, session: PipSession) -> Tuple[str, str]:
:param url: File path or url.
:param session: PipSession instance.
"""
scheme = get_url_scheme(url)
scheme = urllib.parse.urlsplit(url).scheme
# 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

View File

@@ -1,6 +1,3 @@
# The following comment should be removed at some point in the future.
# mypy: strict-optional=False
import functools
import logging
import os
@@ -9,6 +6,7 @@ import sys
import uuid
import zipfile
from optparse import Values
from pathlib import Path
from typing import Any, Collection, Dict, Iterable, List, Optional, Sequence, Union
from pip._vendor.packaging.markers import Marker
@@ -20,7 +18,7 @@ from pip._vendor.packaging.version import parse as parse_version
from pip._vendor.pyproject_hooks import BuildBackendHookCaller
from pip._internal.build_env import BuildEnvironment, NoOpBuildEnvironment
from pip._internal.exceptions import InstallationError
from pip._internal.exceptions import InstallationError, PreviousBuildDirError
from pip._internal.locations import get_scheme
from pip._internal.metadata import (
BaseDistribution,
@@ -50,11 +48,14 @@ from pip._internal.utils.misc import (
backup_dir,
display_path,
hide_url,
is_installable_dir,
redact_auth_from_requirement,
redact_auth_from_url,
)
from pip._internal.utils.packaging import safe_extra
from pip._internal.utils.packaging import get_requirement
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
from pip._internal.utils.virtualenv import running_under_virtualenv
from pip._internal.vcs import vcs
@@ -128,7 +129,7 @@ class InstallRequirement:
if extras:
self.extras = extras
elif req:
self.extras = {safe_extra(extra) for extra in req.extras}
self.extras = req.extras
else:
self.extras = set()
if markers is None and req:
@@ -180,14 +181,27 @@ class InstallRequirement:
# 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: Optional[Path] = None
def __str__(self) -> str:
if self.req:
s = str(self.req)
s = redact_auth_from_requirement(self.req)
if self.link:
s += " from {}".format(redact_auth_from_url(self.link.url))
s += f" from {redact_auth_from_url(self.link.url)}"
elif self.link:
s = redact_auth_from_url(self.link.url)
else:
@@ -208,8 +222,9 @@ class InstallRequirement:
return s
def __repr__(self) -> str:
return "<{} object: {} editable={!r}>".format(
self.__class__.__name__, str(self), self.editable
return (
f"<{self.__class__.__name__} object: "
f"{str(self)} editable={self.editable!r}>"
)
def format_debug(self) -> str:
@@ -217,7 +232,7 @@ class InstallRequirement:
attributes = vars(self)
names = sorted(attributes)
state = ("{}={!r}".format(attr, attributes[attr]) for attr in sorted(names))
state = (f"{attr}={attributes[attr]!r}" for attr in sorted(names))
return "<{name} object: {{{state}}}>".format(
name=self.__class__.__name__,
state=", ".join(state),
@@ -230,7 +245,7 @@ class InstallRequirement:
return None
return self.req.name
@functools.lru_cache() # use cached_property in python 3.8+
@functools.cached_property
def supports_pyproject_editable(self) -> bool:
if not self.use_pep517:
return False
@@ -244,6 +259,7 @@ class InstallRequirement:
@property
def specifier(self) -> SpecifierSet:
assert self.req is not None
return self.req.specifier
@property
@@ -257,7 +273,8 @@ class InstallRequirement:
For example, some-package==1.2 is pinned; some-package>1.2 is not.
"""
specifiers = self.specifier
assert self.req is not None
specifiers = self.req.specifier
return len(specifiers) == 1 and next(iter(specifiers)).operator in {"==", "==="}
def match_markers(self, extras_requested: Optional[Iterable[str]] = None) -> bool:
@@ -305,6 +322,7 @@ class InstallRequirement:
else:
link = None
if link and link.hash:
assert link.hash_name is not None
good_hashes.setdefault(link.hash_name, []).append(link.hash)
return Hashes(good_hashes)
@@ -314,6 +332,7 @@ class InstallRequirement:
return None
s = str(self.req)
if self.comes_from:
comes_from: Optional[str]
if isinstance(self.comes_from, str):
comes_from = self.comes_from
else:
@@ -345,7 +364,7 @@ class InstallRequirement:
# When parallel builds are enabled, add a UUID to the build directory
# name so multiple builds do not interfere with each other.
dir_name: str = canonicalize_name(self.name)
dir_name: str = canonicalize_name(self.req.name)
if parallel_builds:
dir_name = f"{dir_name}_{uuid.uuid4().hex}"
@@ -377,7 +396,7 @@ class InstallRequirement:
else:
op = "==="
self.req = Requirement(
self.req = get_requirement(
"".join(
[
self.metadata["Name"],
@@ -388,6 +407,7 @@ class InstallRequirement:
)
def warn_on_mismatching_name(self) -> None:
assert self.req is not None
metadata_name = canonicalize_name(self.metadata["Name"])
if canonicalize_name(self.req.name) == metadata_name:
# Everything is fine.
@@ -402,7 +422,7 @@ class InstallRequirement:
metadata_name,
self.name,
)
self.req = Requirement(metadata_name)
self.req = get_requirement(metadata_name)
def check_if_exists(self, use_user_site: bool) -> None:
"""Find an installed distribution that satisfies or conflicts
@@ -457,6 +477,7 @@ class InstallRequirement:
# Things valid for sdists
@property
def unpacked_source_directory(self) -> str:
assert self.source_dir, f"No source dir for {self}"
return os.path.join(
self.source_dir, self.link and self.link.subdirectory_fragment or ""
)
@@ -493,15 +514,7 @@ class InstallRequirement:
)
if pyproject_toml_data is None:
if self.config_settings:
deprecated(
reason=f"Config settings are ignored for project {self}.",
replacement=(
"to use --use-pep517 or add a "
"pyproject.toml file to the project"
),
gone_in="23.3",
)
assert not self.config_settings
self.use_pep517 = False
return
@@ -525,7 +538,7 @@ class InstallRequirement:
if (
self.editable
and self.use_pep517
and not self.supports_pyproject_editable()
and not self.supports_pyproject_editable
and not os.path.isfile(self.setup_py_path)
and not os.path.isfile(self.setup_cfg_path)
):
@@ -543,7 +556,7 @@ class InstallRequirement:
Under PEP 517 and PEP 660, call the backend hook to prepare the metadata.
Under legacy processing, call setup.py egg-info.
"""
assert self.source_dir
assert self.source_dir, f"No source dir for {self}"
details = self.name or f"from {self.link}"
if self.use_pep517:
@@ -551,7 +564,7 @@ class InstallRequirement:
if (
self.editable
and self.permit_editable_wheels
and self.supports_pyproject_editable()
and self.supports_pyproject_editable
):
self.metadata_directory = generate_editable_metadata(
build_env=self.build_env,
@@ -592,8 +605,10 @@ class InstallRequirement:
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
return get_wheel_distribution(
FilesystemWheel(self.local_file_path), canonicalize_name(self.name)
FilesystemWheel(self.local_file_path),
canonicalize_name(self.req.name),
)
raise AssertionError(
f"InstallRequirement {self} has no metadata directory and no wheel: "
@@ -601,9 +616,9 @@ class InstallRequirement:
)
def assert_source_matches_version(self) -> None:
assert self.source_dir
assert self.source_dir, f"No source dir for {self}"
version = self.metadata["version"]
if self.req.specifier and version not in self.req.specifier:
if self.req and self.req.specifier and version not in self.req.specifier:
logger.warning(
"Requested %s, but installing version %s",
self,
@@ -640,6 +655,27 @@ class InstallRequirement:
parallel_builds=parallel_builds,
)
def needs_unpacked_archive(self, archive_source: Path) -> None:
assert self._archive_source is None
self._archive_source = archive_source
def ensure_pristine_source_checkout(self) -> None:
"""Ensure the source directory has not yet been built in."""
assert self.source_dir is not None
if self._archive_source is not None:
unpack_file(str(self._archive_source), self.source_dir)
elif is_installable_dir(self.source_dir):
# If a checkout exists, it's unwise to keep going.
# version inconsistencies are logged later, but do not fail
# the installation.
raise PreviousBuildDirError(
f"pip can't proceed with requirements '{self}' due to a "
f"pre-existing build directory ({self.source_dir}). This is likely "
"due to a previous installation that failed . pip is "
"being responsible and not assuming it can delete this. "
"Please delete it and try again."
)
# For editable installations
def update_editable(self) -> None:
if not self.link:
@@ -696,9 +732,10 @@ class InstallRequirement:
name = name.replace(os.path.sep, "/")
return name
assert self.req is not None
path = os.path.join(parentdir, path)
name = _clean_zip_name(path, rootdir)
return self.name + "/" + name
return self.req.name + "/" + name
def archive(self, build_dir: Optional[str]) -> None:
"""Saves archive to provided build_dir.
@@ -715,8 +752,8 @@ class InstallRequirement:
if os.path.exists(archive_path):
response = ask_path_exists(
"The file {} exists. (i)gnore, (w)ipe, "
"(b)ackup, (a)bort ".format(display_path(archive_path)),
f"The file {display_path(archive_path)} exists. (i)gnore, (w)ipe, "
"(b)ackup, (a)bort ",
("i", "w", "b", "a"),
)
if response == "i":
@@ -777,8 +814,9 @@ class InstallRequirement:
use_user_site: bool = False,
pycompile: bool = True,
) -> None:
assert self.req is not None
scheme = get_scheme(
self.name,
self.req.name,
user=use_user_site,
home=home,
root=root,
@@ -787,12 +825,34 @@ class InstallRequirement:
)
if self.editable and not self.is_wheel:
deprecated(
reason=(
f"Legacy editable install of {self} (setup.py develop) "
"is deprecated."
),
replacement=(
"to add a pyproject.toml or enable --use-pep517, "
"and use setuptools >= 64. "
"If the resulting installation is not behaving as expected, "
"try using --config-settings editable_mode=compat. "
"Please consult the setuptools documentation for more information"
),
gone_in="25.0",
issue=11457,
)
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.name,
name=self.req.name,
setup_py_path=self.setup_py_path,
isolated=self.isolated,
build_env=self.build_env,
@@ -805,7 +865,7 @@ class InstallRequirement:
assert self.local_file_path
install_wheel(
self.name,
self.req.name,
self.local_file_path,
scheme=scheme,
req_description=str(self.req),
@@ -865,7 +925,7 @@ def check_legacy_setup_py_options(
reason="--build-option and --global-option are deprecated.",
issue=11859,
replacement="to use --config-settings",
gone_in="23.3",
gone_in="25.0",
)
logger.warning(
"Implying --no-binary=:all: due to the presence of "

View File

@@ -2,12 +2,9 @@ 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__)
@@ -83,37 +80,3 @@ 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="23.3",
)
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="23.3",
)

View File

@@ -5,7 +5,7 @@ import sysconfig
from importlib.util import cache_from_source
from typing import Any, Callable, Dict, Generator, Iterable, List, Optional, Set, Tuple
from pip._internal.exceptions import UninstallationError
from pip._internal.exceptions import LegacyDistutilsInstall, UninstallMissingRecord
from pip._internal.locations import get_bin_prefix, get_bin_user
from pip._internal.metadata import BaseDistribution
from pip._internal.utils.compat import WINDOWS
@@ -61,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 UninstallationError,
If RECORD is not found, raises an error,
with possible information from the INSTALLER file.
https://packaging.python.org/specifications/recording-installed-packages/
@@ -71,17 +71,7 @@ def uninstallation_paths(dist: BaseDistribution) -> Generator[str, None, None]:
entries = dist.iter_declared_entries()
if entries is None:
msg = "Cannot uninstall {dist}, RECORD file not found.".format(dist=dist)
installer = dist.installer
if not installer or installer == "pip":
dep = "{}=={}".format(dist.raw_name, dist.version)
msg += (
" You might be able to recover from this via: "
"'pip install --force-reinstall --no-deps {}'.".format(dep)
)
else:
msg += " Hint: The package was installed by {}.".format(installer)
raise UninstallationError(msg)
raise UninstallMissingRecord(distribution=dist)
for entry in entries:
path = os.path.join(location, entry)
@@ -172,8 +162,7 @@ def compress_for_output_listing(paths: Iterable[str]) -> Tuple[Set[str], Set[str
folders.add(os.path.dirname(path))
files.add(path)
# probably this one https://github.com/python/mypy/issues/390
_normcased_files = set(map(os.path.normcase, files)) # type: ignore
_normcased_files = set(map(os.path.normcase, files))
folders = compact(folders)
@@ -274,7 +263,7 @@ class StashedUninstallPathSet:
def commit(self) -> None:
"""Commits the uninstall by removing stashed files."""
for _, save_dir in self._save_dirs.items():
for save_dir in self._save_dirs.values():
save_dir.cleanup()
self._moves = []
self._save_dirs = {}
@@ -316,7 +305,7 @@ class UninstallPathSet:
# 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:
"""
@@ -368,7 +357,7 @@ class UninstallPathSet:
)
return
dist_name_version = f"{self._dist.raw_name}-{self._dist.version}"
dist_name_version = f"{self._dist.raw_name}-{self._dist.raw_version}"
logger.info("Uninstalling %s:", dist_name_version)
with indent_log():
@@ -510,13 +499,7 @@ class UninstallPathSet:
paths_to_remove.add(f"{path}.pyo")
elif dist.installed_by_distutils:
raise UninstallationError(
"Cannot uninstall {!r}. It is a distutils installed project "
"and thus we cannot accurately determine which files belong "
"to it which would lead to only a partial uninstall.".format(
dist.raw_name,
)
)
raise LegacyDistutilsInstall(distribution=dist)
elif dist.installed_as_egg:
# package installed by easy_install