mirror of
https://github.com/pre-commit/pre-commit-hooks.git
synced 2026-04-16 08:30:20 +00:00
[pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
This commit is contained in:
parent
72ad6dc953
commit
f4cd1ba0d6
813 changed files with 66015 additions and 58839 deletions
|
|
@ -1,6 +1,12 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
import logging
|
||||
from typing import Iterator, List, Optional, Sequence, Tuple
|
||||
from typing import Iterator
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
|
||||
from pip._internal.utils.logging import indent_log
|
||||
|
||||
|
|
@ -9,10 +15,10 @@ from .req_install import InstallRequirement
|
|||
from .req_set import RequirementSet
|
||||
|
||||
__all__ = [
|
||||
"RequirementSet",
|
||||
"InstallRequirement",
|
||||
"parse_requirements",
|
||||
"install_given_reqs",
|
||||
'RequirementSet',
|
||||
'InstallRequirement',
|
||||
'parse_requirements',
|
||||
'install_given_reqs',
|
||||
]
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -23,28 +29,28 @@ class InstallationResult:
|
|||
self.name = name
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"InstallationResult(name={self.name!r})"
|
||||
return f'InstallationResult(name={self.name!r})'
|
||||
|
||||
|
||||
def _validate_requirements(
|
||||
requirements: List[InstallRequirement],
|
||||
) -> Iterator[Tuple[str, InstallRequirement]]:
|
||||
requirements: list[InstallRequirement],
|
||||
) -> Iterator[tuple[str, InstallRequirement]]:
|
||||
for req in requirements:
|
||||
assert req.name, f"invalid to-be-installed requirement: {req}"
|
||||
assert req.name, f'invalid to-be-installed requirement: {req}'
|
||||
yield req.name, req
|
||||
|
||||
|
||||
def install_given_reqs(
|
||||
requirements: List[InstallRequirement],
|
||||
install_options: List[str],
|
||||
requirements: list[InstallRequirement],
|
||||
install_options: list[str],
|
||||
global_options: Sequence[str],
|
||||
root: Optional[str],
|
||||
home: Optional[str],
|
||||
prefix: Optional[str],
|
||||
root: str | None,
|
||||
home: str | None,
|
||||
prefix: str | None,
|
||||
warn_script_location: bool,
|
||||
use_user_site: bool,
|
||||
pycompile: bool,
|
||||
) -> List[InstallationResult]:
|
||||
) -> list[InstallationResult]:
|
||||
"""
|
||||
Install everything in the given list.
|
||||
|
||||
|
|
@ -54,8 +60,8 @@ def install_given_reqs(
|
|||
|
||||
if to_install:
|
||||
logger.info(
|
||||
"Installing collected packages: %s",
|
||||
", ".join(to_install.keys()),
|
||||
'Installing collected packages: %s',
|
||||
', '.join(to_install.keys()),
|
||||
)
|
||||
|
||||
installed = []
|
||||
|
|
@ -63,7 +69,7 @@ def install_given_reqs(
|
|||
with indent_log():
|
||||
for req_name, requirement in to_install.items():
|
||||
if requirement.should_reinstall:
|
||||
logger.info("Attempting uninstall: %s", req_name)
|
||||
logger.info('Attempting uninstall: %s', req_name)
|
||||
with indent_log():
|
||||
uninstalled_pathset = requirement.uninstall(auto_confirm=True)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -7,18 +7,21 @@ helps creates for better understandability for the rest of the code.
|
|||
These are meant to be used elsewhere within pip to create instances of
|
||||
InstallRequirement.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from typing import Any, Dict, Optional, Set, Tuple, Union
|
||||
|
||||
from pip._vendor.packaging.markers import Marker
|
||||
from pip._vendor.packaging.requirements import InvalidRequirement, Requirement
|
||||
from pip._vendor.packaging.specifiers import Specifier
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
from typing import Set
|
||||
from typing import Tuple
|
||||
from typing import Union
|
||||
|
||||
from pip._internal.exceptions import InstallationError
|
||||
from pip._internal.models.index import PyPI, TestPyPI
|
||||
from pip._internal.models.index import PyPI
|
||||
from pip._internal.models.index import TestPyPI
|
||||
from pip._internal.models.link import Link
|
||||
from pip._internal.models.wheel import Wheel
|
||||
from pip._internal.req.req_file import ParsedRequirement
|
||||
|
|
@ -27,20 +30,25 @@ from pip._internal.utils.filetypes import is_archive_file
|
|||
from pip._internal.utils.misc import is_installable_dir
|
||||
from pip._internal.utils.packaging import get_requirement
|
||||
from pip._internal.utils.urls import path_to_url
|
||||
from pip._internal.vcs import is_url, vcs
|
||||
from pip._internal.vcs import is_url
|
||||
from pip._internal.vcs import vcs
|
||||
from pip._vendor.packaging.markers import Marker
|
||||
from pip._vendor.packaging.requirements import InvalidRequirement
|
||||
from pip._vendor.packaging.requirements import Requirement
|
||||
from pip._vendor.packaging.specifiers import Specifier
|
||||
|
||||
__all__ = [
|
||||
"install_req_from_editable",
|
||||
"install_req_from_line",
|
||||
"parse_editable",
|
||||
'install_req_from_editable',
|
||||
'install_req_from_line',
|
||||
'parse_editable',
|
||||
]
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
operators = Specifier._operators.keys()
|
||||
|
||||
|
||||
def _strip_extras(path: str) -> Tuple[str, Optional[str]]:
|
||||
m = re.match(r"^(.+)(\[[^\]]+\])$", path)
|
||||
def _strip_extras(path: str) -> tuple[str, str | None]:
|
||||
m = re.match(r'^(.+)(\[[^\]]+\])$', path)
|
||||
extras = None
|
||||
if m:
|
||||
path_no_extras = m.group(1)
|
||||
|
|
@ -51,13 +59,13 @@ def _strip_extras(path: str) -> Tuple[str, Optional[str]]:
|
|||
return path_no_extras, extras
|
||||
|
||||
|
||||
def convert_extras(extras: Optional[str]) -> Set[str]:
|
||||
def convert_extras(extras: str | None) -> set[str]:
|
||||
if not extras:
|
||||
return set()
|
||||
return get_requirement("placeholder" + extras.lower()).extras
|
||||
return get_requirement('placeholder' + extras.lower()).extras
|
||||
|
||||
|
||||
def parse_editable(editable_req: str) -> Tuple[Optional[str], str, Set[str]]:
|
||||
def parse_editable(editable_req: str) -> tuple[str | None, str, set[str]]:
|
||||
"""Parses an editable requirement into:
|
||||
- a requirement name
|
||||
- an URL
|
||||
|
|
@ -77,37 +85,37 @@ def parse_editable(editable_req: str) -> Tuple[Optional[str], str, Set[str]]:
|
|||
# Treating it as code that has already been checked out
|
||||
url_no_extras = path_to_url(url_no_extras)
|
||||
|
||||
if url_no_extras.lower().startswith("file:"):
|
||||
if url_no_extras.lower().startswith('file:'):
|
||||
package_name = Link(url_no_extras).egg_fragment
|
||||
if extras:
|
||||
return (
|
||||
package_name,
|
||||
url_no_extras,
|
||||
get_requirement("placeholder" + extras.lower()).extras,
|
||||
get_requirement('placeholder' + extras.lower()).extras,
|
||||
)
|
||||
else:
|
||||
return package_name, url_no_extras, set()
|
||||
|
||||
for version_control in vcs:
|
||||
if url.lower().startswith(f"{version_control}:"):
|
||||
url = f"{version_control}+{url}"
|
||||
if url.lower().startswith(f'{version_control}:'):
|
||||
url = f'{version_control}+{url}'
|
||||
break
|
||||
|
||||
link = Link(url)
|
||||
|
||||
if not link.is_vcs:
|
||||
backends = ", ".join(vcs.all_schemes)
|
||||
backends = ', '.join(vcs.all_schemes)
|
||||
raise InstallationError(
|
||||
f"{editable_req} is not a valid editable requirement. "
|
||||
f"It should either be a path to a local project or a VCS URL "
|
||||
f"(beginning with {backends})."
|
||||
f'{editable_req} is not a valid editable requirement. '
|
||||
f'It should either be a path to a local project or a VCS URL '
|
||||
f'(beginning with {backends}).',
|
||||
)
|
||||
|
||||
package_name = link.egg_fragment
|
||||
if not package_name:
|
||||
raise InstallationError(
|
||||
"Could not detect requirement name for '{}', please specify one "
|
||||
"with #egg=your_package_name".format(editable_req)
|
||||
'with #egg=your_package_name'.format(editable_req),
|
||||
)
|
||||
return package_name, url, set()
|
||||
|
||||
|
|
@ -121,21 +129,21 @@ def check_first_requirement_in_file(filename: str) -> None:
|
|||
:raises InvalidRequirement: If the first meaningful line cannot be parsed
|
||||
as an requirement.
|
||||
"""
|
||||
with open(filename, encoding="utf-8", errors="ignore") as f:
|
||||
with open(filename, encoding='utf-8', errors='ignore') as f:
|
||||
# Create a steppable iterator, so we can handle \-continuations.
|
||||
lines = (
|
||||
line
|
||||
for line in (line.strip() for line in f)
|
||||
if line and not line.startswith("#") # Skip blank lines/comments.
|
||||
if line and not line.startswith('#') # Skip blank lines/comments.
|
||||
)
|
||||
|
||||
for line in lines:
|
||||
# Drop comments -- a hash without a space may be in a URL.
|
||||
if " #" in line:
|
||||
line = line[: line.find(" #")]
|
||||
if ' #' in line:
|
||||
line = line[: line.find(' #')]
|
||||
# If there is a line continuation, drop it, and append the next line.
|
||||
if line.endswith("\\"):
|
||||
line = line[:-2].strip() + next(lines, "")
|
||||
if line.endswith('\\'):
|
||||
line = line[:-2].strip() + next(lines, '')
|
||||
Requirement(line)
|
||||
return
|
||||
|
||||
|
|
@ -148,7 +156,7 @@ def deduce_helpful_msg(req: str) -> str:
|
|||
"""
|
||||
if not os.path.exists(req):
|
||||
return f" File '{req}' does not exist."
|
||||
msg = " The path does exist. "
|
||||
msg = ' The path does exist. '
|
||||
# Try to parse and check if it is a requirements file.
|
||||
try:
|
||||
check_first_requirement_in_file(req)
|
||||
|
|
@ -156,11 +164,11 @@ def deduce_helpful_msg(req: str) -> str:
|
|||
logger.debug("Cannot parse '%s' as requirements file", req)
|
||||
else:
|
||||
msg += (
|
||||
f"The argument you provided "
|
||||
f"({req}) appears to be a"
|
||||
f" requirements file. If that is the"
|
||||
f'The argument you provided '
|
||||
f'({req}) appears to be a'
|
||||
f' requirements file. If that is the'
|
||||
f" case, use the '-r' flag to install"
|
||||
f" the packages specified within it."
|
||||
f' the packages specified within it.'
|
||||
)
|
||||
return msg
|
||||
|
||||
|
|
@ -168,10 +176,10 @@ def deduce_helpful_msg(req: str) -> str:
|
|||
class RequirementParts:
|
||||
def __init__(
|
||||
self,
|
||||
requirement: Optional[Requirement],
|
||||
link: Optional[Link],
|
||||
markers: Optional[Marker],
|
||||
extras: Set[str],
|
||||
requirement: Requirement | None,
|
||||
link: Link | None,
|
||||
markers: Marker | None,
|
||||
extras: set[str],
|
||||
):
|
||||
self.requirement = requirement
|
||||
self.link = link
|
||||
|
|
@ -184,7 +192,7 @@ def parse_req_from_editable(editable_req: str) -> RequirementParts:
|
|||
|
||||
if name is not None:
|
||||
try:
|
||||
req: Optional[Requirement] = Requirement(name)
|
||||
req: Requirement | None = Requirement(name)
|
||||
except InvalidRequirement:
|
||||
raise InstallationError(f"Invalid requirement: '{name}'")
|
||||
else:
|
||||
|
|
@ -200,10 +208,10 @@ def parse_req_from_editable(editable_req: str) -> RequirementParts:
|
|||
|
||||
def install_req_from_editable(
|
||||
editable_req: str,
|
||||
comes_from: Optional[Union[InstallRequirement, str]] = None,
|
||||
use_pep517: Optional[bool] = None,
|
||||
comes_from: InstallRequirement | str | None = None,
|
||||
use_pep517: bool | None = None,
|
||||
isolated: bool = False,
|
||||
options: Optional[Dict[str, Any]] = None,
|
||||
options: dict[str, Any] | None = None,
|
||||
constraint: bool = False,
|
||||
user_supplied: bool = False,
|
||||
permit_editable_wheels: bool = False,
|
||||
|
|
@ -221,9 +229,9 @@ def install_req_from_editable(
|
|||
constraint=constraint,
|
||||
use_pep517=use_pep517,
|
||||
isolated=isolated,
|
||||
install_options=options.get("install_options", []) if options else [],
|
||||
global_options=options.get("global_options", []) if options else [],
|
||||
hash_options=options.get("hashes", {}) if options else {},
|
||||
install_options=options.get('install_options', []) if options else [],
|
||||
global_options=options.get('global_options', []) if options else [],
|
||||
hash_options=options.get('hashes', {}) if options else {},
|
||||
extras=parts.extras,
|
||||
)
|
||||
|
||||
|
|
@ -242,12 +250,12 @@ def _looks_like_path(name: str) -> bool:
|
|||
return True
|
||||
if os.path.altsep is not None and os.path.altsep in name:
|
||||
return True
|
||||
if name.startswith("."):
|
||||
if name.startswith('.'):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _get_url_from_path(path: str, name: str) -> Optional[str]:
|
||||
def _get_url_from_path(path: str, name: str) -> str | None:
|
||||
"""
|
||||
First, it checks whether a provided path is an installable directory. If it
|
||||
is, returns the path.
|
||||
|
|
@ -263,29 +271,29 @@ def _get_url_from_path(path: str, name: str) -> Optional[str]:
|
|||
# now that it is done in load_pyproject_toml too.
|
||||
raise InstallationError(
|
||||
f"Directory {name!r} is not installable. Neither 'setup.py' "
|
||||
"nor 'pyproject.toml' found."
|
||||
"nor 'pyproject.toml' found.",
|
||||
)
|
||||
if not is_archive_file(path):
|
||||
return None
|
||||
if os.path.isfile(path):
|
||||
return path_to_url(path)
|
||||
urlreq_parts = name.split("@", 1)
|
||||
urlreq_parts = name.split('@', 1)
|
||||
if len(urlreq_parts) >= 2 and not _looks_like_path(urlreq_parts[0]):
|
||||
# If the path contains '@' and the part before it does not look
|
||||
# like a path, try to treat it as a PEP 440 URL req instead.
|
||||
return None
|
||||
logger.warning(
|
||||
"Requirement %r looks like a filename, but the file does not exist",
|
||||
'Requirement %r looks like a filename, but the file does not exist',
|
||||
name,
|
||||
)
|
||||
return path_to_url(path)
|
||||
|
||||
|
||||
def parse_req_from_line(name: str, line_source: Optional[str]) -> RequirementParts:
|
||||
def parse_req_from_line(name: str, line_source: str | None) -> RequirementParts:
|
||||
if is_url(name):
|
||||
marker_sep = "; "
|
||||
marker_sep = '; '
|
||||
else:
|
||||
marker_sep = ";"
|
||||
marker_sep = ';'
|
||||
if marker_sep in name:
|
||||
name, markers_as_string = name.split(marker_sep, 1)
|
||||
markers_as_string = markers_as_string.strip()
|
||||
|
|
@ -312,12 +320,12 @@ def parse_req_from_line(name: str, line_source: Optional[str]) -> RequirementPar
|
|||
# it's a local file, dir, or url
|
||||
if link:
|
||||
# Handle relative file URLs
|
||||
if link.scheme == "file" and re.search(r"\.\./", link.url):
|
||||
if link.scheme == 'file' and re.search(r'\.\./', link.url):
|
||||
link = Link(path_to_url(os.path.normpath(os.path.abspath(link.path))))
|
||||
# wheel file
|
||||
if link.is_wheel:
|
||||
wheel = Wheel(link.filename) # can raise InvalidWheelFilename
|
||||
req_as_string = f"{wheel.name}=={wheel.version}"
|
||||
req_as_string = f'{wheel.name}=={wheel.version}'
|
||||
else:
|
||||
# set the req to the egg fragment. when it's not there, this
|
||||
# will become an 'unnamed' requirement
|
||||
|
|
@ -332,24 +340,24 @@ def parse_req_from_line(name: str, line_source: Optional[str]) -> RequirementPar
|
|||
def with_source(text: str) -> str:
|
||||
if not line_source:
|
||||
return text
|
||||
return f"{text} (from {line_source})"
|
||||
return f'{text} (from {line_source})'
|
||||
|
||||
def _parse_req_string(req_as_string: str) -> Requirement:
|
||||
try:
|
||||
req = get_requirement(req_as_string)
|
||||
except InvalidRequirement:
|
||||
if os.path.sep in req_as_string:
|
||||
add_msg = "It looks like a path."
|
||||
add_msg = 'It looks like a path.'
|
||||
add_msg += deduce_helpful_msg(req_as_string)
|
||||
elif "=" in req_as_string and not any(
|
||||
elif '=' in req_as_string and not any(
|
||||
op in req_as_string for op in operators
|
||||
):
|
||||
add_msg = "= is not a valid operator. Did you mean == ?"
|
||||
add_msg = '= is not a valid operator. Did you mean == ?'
|
||||
else:
|
||||
add_msg = ""
|
||||
msg = with_source(f"Invalid requirement: {req_as_string!r}")
|
||||
add_msg = ''
|
||||
msg = with_source(f'Invalid requirement: {req_as_string!r}')
|
||||
if add_msg:
|
||||
msg += f"\nHint: {add_msg}"
|
||||
msg += f'\nHint: {add_msg}'
|
||||
raise InstallationError(msg)
|
||||
else:
|
||||
# Deprecate extras after specifiers: "name>=1.0[extras]"
|
||||
|
|
@ -358,13 +366,13 @@ def parse_req_from_line(name: str, line_source: Optional[str]) -> RequirementPar
|
|||
# RequirementParts
|
||||
for spec in req.specifier:
|
||||
spec_str = str(spec)
|
||||
if spec_str.endswith("]"):
|
||||
if spec_str.endswith(']'):
|
||||
msg = f"Extras after version '{spec_str}'."
|
||||
raise InstallationError(msg)
|
||||
return req
|
||||
|
||||
if req_as_string is not None:
|
||||
req: Optional[Requirement] = _parse_req_string(req_as_string)
|
||||
req: Requirement | None = _parse_req_string(req_as_string)
|
||||
else:
|
||||
req = None
|
||||
|
||||
|
|
@ -373,12 +381,12 @@ def parse_req_from_line(name: str, line_source: Optional[str]) -> RequirementPar
|
|||
|
||||
def install_req_from_line(
|
||||
name: str,
|
||||
comes_from: Optional[Union[str, InstallRequirement]] = None,
|
||||
use_pep517: Optional[bool] = None,
|
||||
comes_from: str | InstallRequirement | None = None,
|
||||
use_pep517: bool | None = None,
|
||||
isolated: bool = False,
|
||||
options: Optional[Dict[str, Any]] = None,
|
||||
options: dict[str, Any] | None = None,
|
||||
constraint: bool = False,
|
||||
line_source: Optional[str] = None,
|
||||
line_source: str | None = None,
|
||||
user_supplied: bool = False,
|
||||
) -> InstallRequirement:
|
||||
"""Creates an InstallRequirement from a name, which might be a
|
||||
|
|
@ -396,9 +404,9 @@ def install_req_from_line(
|
|||
markers=parts.markers,
|
||||
use_pep517=use_pep517,
|
||||
isolated=isolated,
|
||||
install_options=options.get("install_options", []) if options else [],
|
||||
global_options=options.get("global_options", []) if options else [],
|
||||
hash_options=options.get("hashes", {}) if options else {},
|
||||
install_options=options.get('install_options', []) if options else [],
|
||||
global_options=options.get('global_options', []) if options else [],
|
||||
hash_options=options.get('hashes', {}) if options else {},
|
||||
constraint=constraint,
|
||||
extras=parts.extras,
|
||||
user_supplied=user_supplied,
|
||||
|
|
@ -407,9 +415,9 @@ def install_req_from_line(
|
|||
|
||||
def install_req_from_req_string(
|
||||
req_string: str,
|
||||
comes_from: Optional[InstallRequirement] = None,
|
||||
comes_from: InstallRequirement | None = None,
|
||||
isolated: bool = False,
|
||||
use_pep517: Optional[bool] = None,
|
||||
use_pep517: bool | None = None,
|
||||
user_supplied: bool = False,
|
||||
) -> InstallRequirement:
|
||||
try:
|
||||
|
|
@ -422,16 +430,16 @@ def install_req_from_req_string(
|
|||
TestPyPI.file_storage_domain,
|
||||
]
|
||||
if (
|
||||
req.url
|
||||
and comes_from
|
||||
and comes_from.link
|
||||
and comes_from.link.netloc in domains_not_allowed
|
||||
req.url and
|
||||
comes_from and
|
||||
comes_from.link and
|
||||
comes_from.link.netloc in domains_not_allowed
|
||||
):
|
||||
# Explicitly disallow pypi packages that depend on external urls
|
||||
raise InstallationError(
|
||||
"Packages installed from PyPI cannot depend on packages "
|
||||
"which are not also hosted on PyPI.\n"
|
||||
"{} depends on {} ".format(comes_from.name, req)
|
||||
'Packages installed from PyPI cannot depend on packages '
|
||||
'which are not also hosted on PyPI.\n'
|
||||
'{} depends on {} '.format(comes_from.name, req),
|
||||
)
|
||||
|
||||
return InstallRequirement(
|
||||
|
|
@ -446,7 +454,7 @@ def install_req_from_req_string(
|
|||
def install_req_from_parsed_requirement(
|
||||
parsed_req: ParsedRequirement,
|
||||
isolated: bool = False,
|
||||
use_pep517: Optional[bool] = None,
|
||||
use_pep517: bool | None = None,
|
||||
user_supplied: bool = False,
|
||||
) -> InstallRequirement:
|
||||
if parsed_req.is_editable:
|
||||
|
|
@ -474,7 +482,7 @@ def install_req_from_parsed_requirement(
|
|||
|
||||
|
||||
def install_req_from_link_and_ireq(
|
||||
link: Link, ireq: InstallRequirement
|
||||
link: Link, ireq: InstallRequirement,
|
||||
) -> InstallRequirement:
|
||||
return InstallRequirement(
|
||||
req=ireq.req,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"""
|
||||
Requirements file parsing
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import optparse
|
||||
import os
|
||||
|
|
@ -8,20 +9,19 @@ import re
|
|||
import shlex
|
||||
import urllib.parse
|
||||
from optparse import Values
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
Tuple,
|
||||
)
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Dict
|
||||
from typing import Iterable
|
||||
from typing import Iterator
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from pip._internal.cli import cmdoptions
|
||||
from pip._internal.exceptions import InstallationError, RequirementsFileParseError
|
||||
from pip._internal.exceptions import InstallationError
|
||||
from pip._internal.exceptions import RequirementsFileParseError
|
||||
from pip._internal.models.search_scope import SearchScope
|
||||
from pip._internal.network.session import PipSession
|
||||
from pip._internal.network.utils import raise_for_status
|
||||
|
|
@ -35,22 +35,22 @@ if TYPE_CHECKING:
|
|||
|
||||
from pip._internal.index.package_finder import PackageFinder
|
||||
|
||||
__all__ = ["parse_requirements"]
|
||||
__all__ = ['parse_requirements']
|
||||
|
||||
ReqFileLines = Iterable[Tuple[int, str]]
|
||||
|
||||
LineParser = Callable[[str], Tuple[str, Values]]
|
||||
|
||||
SCHEME_RE = re.compile(r"^(http|https|file):", re.I)
|
||||
COMMENT_RE = re.compile(r"(^|\s+)#.*$")
|
||||
SCHEME_RE = re.compile(r'^(http|https|file):', re.I)
|
||||
COMMENT_RE = re.compile(r'(^|\s+)#.*$')
|
||||
|
||||
# Matches environment variable-style values in '${MY_VARIABLE_1}' with the
|
||||
# variable name consisting of only uppercase letters, digits or the '_'
|
||||
# (underscore). This follows the POSIX standard defined in IEEE Std 1003.1,
|
||||
# 2013 Edition.
|
||||
ENV_VAR_RE = re.compile(r"(?P<var>\$\{(?P<name>[A-Z0-9_]+)\})")
|
||||
ENV_VAR_RE = re.compile(r'(?P<var>\$\{(?P<name>[A-Z0-9_]+)\})')
|
||||
|
||||
SUPPORTED_OPTIONS: List[Callable[..., optparse.Option]] = [
|
||||
SUPPORTED_OPTIONS: list[Callable[..., optparse.Option]] = [
|
||||
cmdoptions.index_url,
|
||||
cmdoptions.extra_index_url,
|
||||
cmdoptions.no_index,
|
||||
|
|
@ -68,7 +68,7 @@ SUPPORTED_OPTIONS: List[Callable[..., optparse.Option]] = [
|
|||
]
|
||||
|
||||
# options to be passed to requirements
|
||||
SUPPORTED_OPTIONS_REQ: List[Callable[..., optparse.Option]] = [
|
||||
SUPPORTED_OPTIONS_REQ: list[Callable[..., optparse.Option]] = [
|
||||
cmdoptions.install_options,
|
||||
cmdoptions.global_options,
|
||||
cmdoptions.hash,
|
||||
|
|
@ -85,8 +85,8 @@ class ParsedRequirement:
|
|||
is_editable: bool,
|
||||
comes_from: str,
|
||||
constraint: bool,
|
||||
options: Optional[Dict[str, Any]] = None,
|
||||
line_source: Optional[str] = None,
|
||||
options: dict[str, Any] | None = None,
|
||||
line_source: str | None = None,
|
||||
) -> None:
|
||||
self.requirement = requirement
|
||||
self.is_editable = is_editable
|
||||
|
|
@ -126,8 +126,8 @@ class ParsedLine:
|
|||
def parse_requirements(
|
||||
filename: str,
|
||||
session: PipSession,
|
||||
finder: Optional["PackageFinder"] = None,
|
||||
options: Optional[optparse.Values] = None,
|
||||
finder: PackageFinder | None = None,
|
||||
options: optparse.Values | None = None,
|
||||
constraint: bool = False,
|
||||
) -> Iterator[ParsedRequirement]:
|
||||
"""Parse a requirements file and yield ParsedRequirement instances.
|
||||
|
|
@ -144,7 +144,7 @@ def parse_requirements(
|
|||
|
||||
for parsed_line in parser.parse(filename, constraint):
|
||||
parsed_req = handle_line(
|
||||
parsed_line, options=options, finder=finder, session=session
|
||||
parsed_line, options=options, finder=finder, session=session,
|
||||
)
|
||||
if parsed_req is not None:
|
||||
yield parsed_req
|
||||
|
|
@ -164,12 +164,12 @@ def preprocess(content: str) -> ReqFileLines:
|
|||
|
||||
def handle_requirement_line(
|
||||
line: ParsedLine,
|
||||
options: Optional[optparse.Values] = None,
|
||||
options: optparse.Values | None = None,
|
||||
) -> ParsedRequirement:
|
||||
|
||||
# preserve for the nested code path
|
||||
line_comes_from = "{} {} (line {})".format(
|
||||
"-c" if line.constraint else "-r",
|
||||
line_comes_from = '{} {} (line {})'.format(
|
||||
'-c' if line.constraint else '-r',
|
||||
line.filename,
|
||||
line.lineno,
|
||||
)
|
||||
|
|
@ -196,7 +196,7 @@ def handle_requirement_line(
|
|||
if dest in line.opts.__dict__ and line.opts.__dict__[dest]:
|
||||
req_options[dest] = line.opts.__dict__[dest]
|
||||
|
||||
line_source = f"line {line.lineno} of {line.filename}"
|
||||
line_source = f'line {line.lineno} of {line.filename}'
|
||||
return ParsedRequirement(
|
||||
requirement=line.requirement,
|
||||
is_editable=line.is_editable,
|
||||
|
|
@ -211,9 +211,9 @@ def handle_option_line(
|
|||
opts: Values,
|
||||
filename: str,
|
||||
lineno: int,
|
||||
finder: Optional["PackageFinder"] = None,
|
||||
options: Optional[optparse.Values] = None,
|
||||
session: Optional[PipSession] = None,
|
||||
finder: PackageFinder | None = None,
|
||||
options: optparse.Values | None = None,
|
||||
session: PipSession | None = None,
|
||||
) -> None:
|
||||
|
||||
if options:
|
||||
|
|
@ -264,16 +264,16 @@ def handle_option_line(
|
|||
|
||||
if session:
|
||||
for host in opts.trusted_hosts or []:
|
||||
source = f"line {lineno} of {filename}"
|
||||
source = f'line {lineno} of {filename}'
|
||||
session.add_trusted_host(host, source=source)
|
||||
|
||||
|
||||
def handle_line(
|
||||
line: ParsedLine,
|
||||
options: Optional[optparse.Values] = None,
|
||||
finder: Optional["PackageFinder"] = None,
|
||||
session: Optional[PipSession] = None,
|
||||
) -> Optional[ParsedRequirement]:
|
||||
options: optparse.Values | None = None,
|
||||
finder: PackageFinder | None = None,
|
||||
session: PipSession | None = None,
|
||||
) -> ParsedRequirement | None:
|
||||
"""Handle a single parsed requirements line; This can result in
|
||||
creating/yielding requirements, or updating the finder.
|
||||
|
||||
|
|
@ -326,7 +326,7 @@ class RequirementsFileParser:
|
|||
yield from self._parse_and_recurse(filename, constraint)
|
||||
|
||||
def _parse_and_recurse(
|
||||
self, filename: str, constraint: bool
|
||||
self, filename: str, constraint: bool,
|
||||
) -> Iterator[ParsedLine]:
|
||||
for line in self._parse_file(filename, constraint):
|
||||
if not line.is_requirement and (
|
||||
|
|
@ -366,7 +366,7 @@ class RequirementsFileParser:
|
|||
args_str, opts = self._line_parser(line)
|
||||
except OptionParsingError as e:
|
||||
# add offending line
|
||||
msg = f"Invalid requirement: {line}\n{e.msg}"
|
||||
msg = f'Invalid requirement: {line}\n{e.msg}'
|
||||
raise RequirementsFileParseError(msg)
|
||||
|
||||
yield ParsedLine(
|
||||
|
|
@ -378,8 +378,8 @@ class RequirementsFileParser:
|
|||
)
|
||||
|
||||
|
||||
def get_line_parser(finder: Optional["PackageFinder"]) -> LineParser:
|
||||
def parse_line(line: str) -> Tuple[str, Values]:
|
||||
def get_line_parser(finder: PackageFinder | None) -> LineParser:
|
||||
def parse_line(line: str) -> tuple[str, Values]:
|
||||
# Build new parser for each line since it accumulates appendable
|
||||
# options.
|
||||
parser = build_parser()
|
||||
|
|
@ -397,21 +397,21 @@ def get_line_parser(finder: Optional["PackageFinder"]) -> LineParser:
|
|||
return parse_line
|
||||
|
||||
|
||||
def break_args_options(line: str) -> Tuple[str, str]:
|
||||
def break_args_options(line: str) -> tuple[str, str]:
|
||||
"""Break up the line into an args and options string. We only want to shlex
|
||||
(and then optparse) the options, not the args. args can contain markers
|
||||
which are corrupted by shlex.
|
||||
"""
|
||||
tokens = line.split(" ")
|
||||
tokens = line.split(' ')
|
||||
args = []
|
||||
options = tokens[:]
|
||||
for token in tokens:
|
||||
if token.startswith("-") or token.startswith("--"):
|
||||
if token.startswith('-') or token.startswith('--'):
|
||||
break
|
||||
else:
|
||||
args.append(token)
|
||||
options.pop(0)
|
||||
return " ".join(args), " ".join(options)
|
||||
return ' '.join(args), ' '.join(options)
|
||||
|
||||
|
||||
class OptionParsingError(Exception):
|
||||
|
|
@ -432,7 +432,7 @@ def build_parser() -> optparse.OptionParser:
|
|||
|
||||
# By default optparse sys.exits on parsing errors. We want to wrap
|
||||
# that in our own exception.
|
||||
def parser_exit(self: Any, msg: str) -> "NoReturn":
|
||||
def parser_exit(self: Any, msg: str) -> NoReturn:
|
||||
raise OptionParsingError(msg)
|
||||
|
||||
# NOTE: mypy disallows assigning to a method
|
||||
|
|
@ -447,28 +447,28 @@ def join_lines(lines_enum: ReqFileLines) -> ReqFileLines:
|
|||
comments). The joined line takes on the index of the first line.
|
||||
"""
|
||||
primary_line_number = None
|
||||
new_line: List[str] = []
|
||||
new_line: list[str] = []
|
||||
for line_number, line in lines_enum:
|
||||
if not line.endswith("\\") or COMMENT_RE.match(line):
|
||||
if not line.endswith('\\') or COMMENT_RE.match(line):
|
||||
if COMMENT_RE.match(line):
|
||||
# this ensures comments are always matched later
|
||||
line = " " + line
|
||||
line = ' ' + line
|
||||
if new_line:
|
||||
new_line.append(line)
|
||||
assert primary_line_number is not None
|
||||
yield primary_line_number, "".join(new_line)
|
||||
yield primary_line_number, ''.join(new_line)
|
||||
new_line = []
|
||||
else:
|
||||
yield line_number, line
|
||||
else:
|
||||
if not new_line:
|
||||
primary_line_number = line_number
|
||||
new_line.append(line.strip("\\"))
|
||||
new_line.append(line.strip('\\'))
|
||||
|
||||
# last line contains \
|
||||
if new_line:
|
||||
assert primary_line_number is not None
|
||||
yield primary_line_number, "".join(new_line)
|
||||
yield primary_line_number, ''.join(new_line)
|
||||
|
||||
# TODO: handle space after '\'.
|
||||
|
||||
|
|
@ -478,7 +478,7 @@ def ignore_comments(lines_enum: ReqFileLines) -> ReqFileLines:
|
|||
Strips comments and filter empty lines.
|
||||
"""
|
||||
for line_number, line in lines_enum:
|
||||
line = COMMENT_RE.sub("", line)
|
||||
line = COMMENT_RE.sub('', line)
|
||||
line = line.strip()
|
||||
if line:
|
||||
yield line_number, line
|
||||
|
|
@ -511,7 +511,7 @@ def expand_env_variables(lines_enum: ReqFileLines) -> ReqFileLines:
|
|||
yield line_number, line
|
||||
|
||||
|
||||
def get_file_content(url: str, session: PipSession) -> Tuple[str, str]:
|
||||
def get_file_content(url: str, session: PipSession) -> tuple[str, str]:
|
||||
"""Gets the content of a file; it may be a filename, file: URL, or
|
||||
http: URL. Returns (location, content). Content is unicode.
|
||||
Respects # -*- coding: declarations on the retrieved files.
|
||||
|
|
@ -522,15 +522,15 @@ def get_file_content(url: str, session: PipSession) -> Tuple[str, str]:
|
|||
scheme = get_url_scheme(url)
|
||||
|
||||
# Pip has special support for file:// URLs (LocalFSAdapter).
|
||||
if scheme in ["http", "https", "file"]:
|
||||
if scheme in ['http', 'https', 'file']:
|
||||
resp = session.get(url)
|
||||
raise_for_status(resp)
|
||||
return resp.url, resp.text
|
||||
|
||||
# Assume this is a bare path.
|
||||
try:
|
||||
with open(url, "rb") as f:
|
||||
with open(url, 'rb') as f:
|
||||
content = auto_decode(f.read())
|
||||
except OSError as exc:
|
||||
raise InstallationError(f"Could not open requirements file: {exc}")
|
||||
raise InstallationError(f'Could not open requirements file: {exc}')
|
||||
return url, content
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
# The following comment should be removed at some point in the future.
|
||||
# mypy: strict-optional=False
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import logging
|
||||
|
|
@ -8,24 +9,23 @@ import shutil
|
|||
import sys
|
||||
import uuid
|
||||
import zipfile
|
||||
from typing import Any, Collection, Dict, Iterable, List, Optional, Sequence, Union
|
||||
from typing import Any
|
||||
from typing import Collection
|
||||
from typing import Dict
|
||||
from typing import Iterable
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
from typing import Union
|
||||
|
||||
from pip._vendor.packaging.markers import Marker
|
||||
from pip._vendor.packaging.requirements import Requirement
|
||||
from pip._vendor.packaging.specifiers import SpecifierSet
|
||||
from pip._vendor.packaging.utils import canonicalize_name
|
||||
from pip._vendor.packaging.version import Version
|
||||
from pip._vendor.packaging.version import parse as parse_version
|
||||
from pip._vendor.pep517.wrappers import Pep517HookCaller
|
||||
|
||||
from pip._internal.build_env import BuildEnvironment, NoOpBuildEnvironment
|
||||
from pip._internal.exceptions import InstallationError, LegacyInstallFailure
|
||||
from pip._internal.build_env import BuildEnvironment
|
||||
from pip._internal.build_env import NoOpBuildEnvironment
|
||||
from pip._internal.exceptions import InstallationError
|
||||
from pip._internal.exceptions import LegacyInstallFailure
|
||||
from pip._internal.locations import get_scheme
|
||||
from pip._internal.metadata import (
|
||||
BaseDistribution,
|
||||
get_default_environment,
|
||||
get_directory_distribution,
|
||||
)
|
||||
from pip._internal.metadata import BaseDistribution
|
||||
from pip._internal.metadata import get_default_environment
|
||||
from pip._internal.metadata import get_directory_distribution
|
||||
from pip._internal.models.link import Link
|
||||
from pip._internal.operations.build.metadata import generate_metadata
|
||||
from pip._internal.operations.build.metadata_editable import generate_editable_metadata
|
||||
|
|
@ -37,26 +37,31 @@ from pip._internal.operations.install.editable_legacy import (
|
|||
)
|
||||
from pip._internal.operations.install.legacy import install as install_legacy
|
||||
from pip._internal.operations.install.wheel import install_wheel
|
||||
from pip._internal.pyproject import load_pyproject_toml, make_pyproject_path
|
||||
from pip._internal.pyproject import load_pyproject_toml
|
||||
from pip._internal.pyproject import make_pyproject_path
|
||||
from pip._internal.req.req_uninstall import UninstallPathSet
|
||||
from pip._internal.utils.deprecation import deprecated
|
||||
from pip._internal.utils.direct_url_helpers import (
|
||||
direct_url_for_editable,
|
||||
direct_url_from_link,
|
||||
)
|
||||
from pip._internal.utils.direct_url_helpers import direct_url_for_editable
|
||||
from pip._internal.utils.direct_url_helpers import direct_url_from_link
|
||||
from pip._internal.utils.hashes import Hashes
|
||||
from pip._internal.utils.misc import (
|
||||
ask_path_exists,
|
||||
backup_dir,
|
||||
display_path,
|
||||
hide_url,
|
||||
redact_auth_from_url,
|
||||
)
|
||||
from pip._internal.utils.misc import ask_path_exists
|
||||
from pip._internal.utils.misc import backup_dir
|
||||
from pip._internal.utils.misc import display_path
|
||||
from pip._internal.utils.misc import hide_url
|
||||
from pip._internal.utils.misc import redact_auth_from_url
|
||||
from pip._internal.utils.packaging import safe_extra
|
||||
from pip._internal.utils.subprocess import runner_with_spinner_message
|
||||
from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds
|
||||
from pip._internal.utils.temp_dir import tempdir_kinds
|
||||
from pip._internal.utils.temp_dir import TempDirectory
|
||||
from pip._internal.utils.virtualenv import running_under_virtualenv
|
||||
from pip._internal.vcs import vcs
|
||||
from pip._vendor.packaging.markers import Marker
|
||||
from pip._vendor.packaging.requirements import Requirement
|
||||
from pip._vendor.packaging.specifiers import SpecifierSet
|
||||
from pip._vendor.packaging.utils import canonicalize_name
|
||||
from pip._vendor.packaging.version import parse as parse_version
|
||||
from pip._vendor.packaging.version import Version
|
||||
from pip._vendor.pep517.wrappers import Pep517HookCaller
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -70,16 +75,16 @@ class InstallRequirement:
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
req: Optional[Requirement],
|
||||
comes_from: Optional[Union[str, "InstallRequirement"]],
|
||||
req: Requirement | None,
|
||||
comes_from: str | InstallRequirement | None,
|
||||
editable: bool = False,
|
||||
link: Optional[Link] = None,
|
||||
markers: Optional[Marker] = None,
|
||||
use_pep517: Optional[bool] = None,
|
||||
link: Link | None = None,
|
||||
markers: Marker | None = None,
|
||||
use_pep517: bool | None = None,
|
||||
isolated: bool = False,
|
||||
install_options: Optional[List[str]] = None,
|
||||
global_options: Optional[List[str]] = None,
|
||||
hash_options: Optional[Dict[str, List[str]]] = None,
|
||||
install_options: list[str] | None = None,
|
||||
global_options: list[str] | None = None,
|
||||
hash_options: dict[str, list[str]] | None = None,
|
||||
constraint: bool = False,
|
||||
extras: Collection[str] = (),
|
||||
user_supplied: bool = False,
|
||||
|
|
@ -91,14 +96,14 @@ class InstallRequirement:
|
|||
self.constraint = constraint
|
||||
self.editable = editable
|
||||
self.permit_editable_wheels = permit_editable_wheels
|
||||
self.legacy_install_reason: Optional[int] = None
|
||||
self.legacy_install_reason: int | None = None
|
||||
|
||||
# source_dir is the local directory where the linked requirement is
|
||||
# located, or unpacked. In case unpacking is needed, creating and
|
||||
# populating source_dir is done by the RequirementPreparer. Note this
|
||||
# is not necessarily the directory where pyproject.toml or setup.py is
|
||||
# located - that one is obtained via unpacked_source_directory.
|
||||
self.source_dir: Optional[str] = None
|
||||
self.source_dir: str | None = None
|
||||
if self.editable:
|
||||
assert link
|
||||
if link.is_file:
|
||||
|
|
@ -111,7 +116,7 @@ class InstallRequirement:
|
|||
self.original_link_is_in_wheel_cache = False
|
||||
|
||||
# Path to any downloaded or already-existing package.
|
||||
self.local_file_path: Optional[str] = None
|
||||
self.local_file_path: str | None = None
|
||||
if self.link and self.link.is_file:
|
||||
self.local_file_path = self.link.file_path
|
||||
|
||||
|
|
@ -126,14 +131,14 @@ class InstallRequirement:
|
|||
self.markers = markers
|
||||
|
||||
# This holds the Distribution object if this requirement is already installed.
|
||||
self.satisfied_by: Optional[BaseDistribution] = None
|
||||
self.satisfied_by: BaseDistribution | None = None
|
||||
# Whether the installation process should try to uninstall an existing
|
||||
# distribution before installing this requirement.
|
||||
self.should_reinstall = False
|
||||
# Temporary build location
|
||||
self._temp_build_dir: Optional[TempDirectory] = None
|
||||
self._temp_build_dir: TempDirectory | None = None
|
||||
# Set to True after successful installation
|
||||
self.install_succeeded: Optional[bool] = None
|
||||
self.install_succeeded: bool | None = None
|
||||
# Supplied options
|
||||
self.install_options = install_options if install_options else []
|
||||
self.global_options = global_options if global_options else []
|
||||
|
|
@ -152,16 +157,16 @@ class InstallRequirement:
|
|||
# gets stored. We need this to pass to build_wheel, so the backend
|
||||
# can ensure that the wheel matches the metadata (see the PEP for
|
||||
# details).
|
||||
self.metadata_directory: Optional[str] = None
|
||||
self.metadata_directory: str | None = None
|
||||
|
||||
# The static build requirements (from pyproject.toml)
|
||||
self.pyproject_requires: Optional[List[str]] = None
|
||||
self.pyproject_requires: list[str] | None = None
|
||||
|
||||
# Build requirements that we will check are available
|
||||
self.requirements_to_check: List[str] = []
|
||||
self.requirements_to_check: list[str] = []
|
||||
|
||||
# The PEP 517 backend we should use to build the project
|
||||
self.pep517_backend: Optional[Pep517HookCaller] = None
|
||||
self.pep517_backend: Pep517HookCaller | None = None
|
||||
|
||||
# Are we using PEP 517 for this requirement?
|
||||
# After pyproject.toml has been loaded, the only valid values are True
|
||||
|
|
@ -177,25 +182,25 @@ class InstallRequirement:
|
|||
if self.req:
|
||||
s = str(self.req)
|
||||
if self.link:
|
||||
s += " from {}".format(redact_auth_from_url(self.link.url))
|
||||
s += f' from {redact_auth_from_url(self.link.url)}'
|
||||
elif self.link:
|
||||
s = redact_auth_from_url(self.link.url)
|
||||
else:
|
||||
s = "<InstallRequirement>"
|
||||
s = '<InstallRequirement>'
|
||||
if self.satisfied_by is not None:
|
||||
s += " in {}".format(display_path(self.satisfied_by.location))
|
||||
s += f' in {display_path(self.satisfied_by.location)}'
|
||||
if self.comes_from:
|
||||
if isinstance(self.comes_from, str):
|
||||
comes_from: Optional[str] = self.comes_from
|
||||
comes_from: str | None = self.comes_from
|
||||
else:
|
||||
comes_from = self.comes_from.from_path()
|
||||
if comes_from:
|
||||
s += f" (from {comes_from})"
|
||||
s += f' (from {comes_from})'
|
||||
return s
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "<{} object: {} editable={!r}>".format(
|
||||
self.__class__.__name__, str(self), self.editable
|
||||
return '<{} object: {} editable={!r}>'.format(
|
||||
self.__class__.__name__, str(self), self.editable,
|
||||
)
|
||||
|
||||
def format_debug(self) -> str:
|
||||
|
|
@ -203,30 +208,30 @@ class InstallRequirement:
|
|||
attributes = vars(self)
|
||||
names = sorted(attributes)
|
||||
|
||||
state = ("{}={!r}".format(attr, attributes[attr]) for attr in sorted(names))
|
||||
return "<{name} object: {{{state}}}>".format(
|
||||
state = (f'{attr}={attributes[attr]!r}' for attr in sorted(names))
|
||||
return '<{name} object: {{{state}}}>'.format(
|
||||
name=self.__class__.__name__,
|
||||
state=", ".join(state),
|
||||
state=', '.join(state),
|
||||
)
|
||||
|
||||
# Things that are valid for all kinds of requirements?
|
||||
@property
|
||||
def name(self) -> Optional[str]:
|
||||
def name(self) -> str | None:
|
||||
if self.req is None:
|
||||
return None
|
||||
return self.req.name
|
||||
|
||||
@functools.lru_cache() # use cached_property in python 3.8+
|
||||
@functools.lru_cache # use cached_property in python 3.8+
|
||||
def supports_pyproject_editable(self) -> bool:
|
||||
if not self.use_pep517:
|
||||
return False
|
||||
assert self.pep517_backend
|
||||
with self.build_env:
|
||||
runner = runner_with_spinner_message(
|
||||
"Checking if build backend supports build_editable"
|
||||
'Checking if build backend supports build_editable',
|
||||
)
|
||||
with self.pep517_backend.subprocess_runner(runner):
|
||||
return "build_editable" in self.pep517_backend._supported_features()
|
||||
return 'build_editable' in self.pep517_backend._supported_features()
|
||||
|
||||
@property
|
||||
def specifier(self) -> SpecifierSet:
|
||||
|
|
@ -239,16 +244,16 @@ class InstallRequirement:
|
|||
For example, some-package==1.2 is pinned; some-package>1.2 is not.
|
||||
"""
|
||||
specifiers = self.specifier
|
||||
return len(specifiers) == 1 and next(iter(specifiers)).operator in {"==", "==="}
|
||||
return len(specifiers) == 1 and next(iter(specifiers)).operator in {'==', '==='}
|
||||
|
||||
def match_markers(self, extras_requested: Optional[Iterable[str]] = None) -> bool:
|
||||
def match_markers(self, extras_requested: Iterable[str] | None = None) -> bool:
|
||||
if not extras_requested:
|
||||
# Provide an extra to safely evaluate the markers
|
||||
# without matching any extra
|
||||
extras_requested = ("",)
|
||||
extras_requested = ('',)
|
||||
if self.markers is not None:
|
||||
return any(
|
||||
self.markers.evaluate({"extra": extra}) for extra in extras_requested
|
||||
self.markers.evaluate({'extra': extra}) for extra in extras_requested
|
||||
)
|
||||
else:
|
||||
return True
|
||||
|
|
@ -284,7 +289,7 @@ class InstallRequirement:
|
|||
good_hashes.setdefault(link.hash_name, []).append(link.hash)
|
||||
return Hashes(good_hashes)
|
||||
|
||||
def from_path(self) -> Optional[str]:
|
||||
def from_path(self) -> str | None:
|
||||
"""Format a nice indicator to show where this "comes from" """
|
||||
if self.req is None:
|
||||
return None
|
||||
|
|
@ -295,11 +300,11 @@ class InstallRequirement:
|
|||
else:
|
||||
comes_from = self.comes_from.from_path()
|
||||
if comes_from:
|
||||
s += "->" + comes_from
|
||||
s += '->' + comes_from
|
||||
return s
|
||||
|
||||
def ensure_build_location(
|
||||
self, build_dir: str, autodelete: bool, parallel_builds: bool
|
||||
self, build_dir: str, autodelete: bool, parallel_builds: bool,
|
||||
) -> str:
|
||||
assert build_dir is not None
|
||||
if self._temp_build_dir is not None:
|
||||
|
|
@ -310,7 +315,7 @@ class InstallRequirement:
|
|||
# builds (such as numpy). Thus, we ensure that the real path
|
||||
# is returned.
|
||||
self._temp_build_dir = TempDirectory(
|
||||
kind=tempdir_kinds.REQ_BUILD, globally_managed=True
|
||||
kind=tempdir_kinds.REQ_BUILD, globally_managed=True,
|
||||
)
|
||||
|
||||
return self._temp_build_dir.path
|
||||
|
|
@ -323,12 +328,12 @@ class InstallRequirement:
|
|||
# name so multiple builds do not interfere with each other.
|
||||
dir_name: str = canonicalize_name(self.name)
|
||||
if parallel_builds:
|
||||
dir_name = f"{dir_name}_{uuid.uuid4().hex}"
|
||||
dir_name = f'{dir_name}_{uuid.uuid4().hex}'
|
||||
|
||||
# FIXME: Is there a better place to create the build_dir? (hg and bzr
|
||||
# need this)
|
||||
if not os.path.exists(build_dir):
|
||||
logger.debug("Creating directory %s", build_dir)
|
||||
logger.debug('Creating directory %s', build_dir)
|
||||
os.makedirs(build_dir)
|
||||
actual_build_dir = os.path.join(build_dir, dir_name)
|
||||
# `None` indicates that we respect the globally-configured deletion
|
||||
|
|
@ -348,32 +353,32 @@ class InstallRequirement:
|
|||
assert self.source_dir is not None
|
||||
|
||||
# Construct a Requirement object from the generated metadata
|
||||
if isinstance(parse_version(self.metadata["Version"]), Version):
|
||||
op = "=="
|
||||
if isinstance(parse_version(self.metadata['Version']), Version):
|
||||
op = '=='
|
||||
else:
|
||||
op = "==="
|
||||
op = '==='
|
||||
|
||||
self.req = Requirement(
|
||||
"".join(
|
||||
''.join(
|
||||
[
|
||||
self.metadata["Name"],
|
||||
self.metadata['Name'],
|
||||
op,
|
||||
self.metadata["Version"],
|
||||
]
|
||||
)
|
||||
self.metadata['Version'],
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
def warn_on_mismatching_name(self) -> None:
|
||||
metadata_name = canonicalize_name(self.metadata["Name"])
|
||||
metadata_name = canonicalize_name(self.metadata['Name'])
|
||||
if canonicalize_name(self.req.name) == metadata_name:
|
||||
# Everything is fine.
|
||||
return
|
||||
|
||||
# If we're here, there's a mismatch. Log a warning about it.
|
||||
logger.warning(
|
||||
"Generating metadata for package %s "
|
||||
"produced metadata for project name %s. Fix your "
|
||||
"#egg=%s fragments.",
|
||||
'Generating metadata for package %s '
|
||||
'produced metadata for project name %s. Fix your '
|
||||
'#egg=%s fragments.',
|
||||
self.name,
|
||||
metadata_name,
|
||||
self.name,
|
||||
|
|
@ -402,9 +407,9 @@ class InstallRequirement:
|
|||
self.should_reinstall = True
|
||||
elif running_under_virtualenv() and existing_dist.in_site_packages:
|
||||
raise InstallationError(
|
||||
f"Will not install to the user site because it will "
|
||||
f"lack sys.path precedence to {existing_dist.raw_name} "
|
||||
f"in {existing_dist.location}"
|
||||
f'Will not install to the user site because it will '
|
||||
f'lack sys.path precedence to {existing_dist.raw_name} '
|
||||
f'in {existing_dist.location}',
|
||||
)
|
||||
else:
|
||||
self.should_reinstall = True
|
||||
|
|
@ -428,26 +433,26 @@ class InstallRequirement:
|
|||
@property
|
||||
def unpacked_source_directory(self) -> str:
|
||||
return os.path.join(
|
||||
self.source_dir, self.link and self.link.subdirectory_fragment or ""
|
||||
self.source_dir, self.link and self.link.subdirectory_fragment or '',
|
||||
)
|
||||
|
||||
@property
|
||||
def setup_py_path(self) -> str:
|
||||
assert self.source_dir, f"No source dir for {self}"
|
||||
setup_py = os.path.join(self.unpacked_source_directory, "setup.py")
|
||||
assert self.source_dir, f'No source dir for {self}'
|
||||
setup_py = os.path.join(self.unpacked_source_directory, 'setup.py')
|
||||
|
||||
return setup_py
|
||||
|
||||
@property
|
||||
def setup_cfg_path(self) -> str:
|
||||
assert self.source_dir, f"No source dir for {self}"
|
||||
setup_cfg = os.path.join(self.unpacked_source_directory, "setup.cfg")
|
||||
assert self.source_dir, f'No source dir for {self}'
|
||||
setup_cfg = os.path.join(self.unpacked_source_directory, 'setup.cfg')
|
||||
|
||||
return setup_cfg
|
||||
|
||||
@property
|
||||
def pyproject_toml_path(self) -> str:
|
||||
assert self.source_dir, f"No source dir for {self}"
|
||||
assert self.source_dir, f'No source dir for {self}'
|
||||
return make_pyproject_path(self.unpacked_source_directory)
|
||||
|
||||
def load_pyproject_toml(self) -> None:
|
||||
|
|
@ -459,7 +464,7 @@ class InstallRequirement:
|
|||
follow the PEP 517 or legacy (setup.py) code path.
|
||||
"""
|
||||
pyproject_toml_data = load_pyproject_toml(
|
||||
self.use_pep517, self.pyproject_toml_path, self.setup_py_path, str(self)
|
||||
self.use_pep517, self.pyproject_toml_path, self.setup_py_path, str(self),
|
||||
)
|
||||
|
||||
if pyproject_toml_data is None:
|
||||
|
|
@ -483,18 +488,18 @@ class InstallRequirement:
|
|||
or as a setup.py or a setup.cfg
|
||||
"""
|
||||
if (
|
||||
self.editable
|
||||
and self.use_pep517
|
||||
and not self.supports_pyproject_editable()
|
||||
and not os.path.isfile(self.setup_py_path)
|
||||
and not os.path.isfile(self.setup_cfg_path)
|
||||
self.editable and
|
||||
self.use_pep517 and
|
||||
not self.supports_pyproject_editable() and
|
||||
not os.path.isfile(self.setup_py_path) and
|
||||
not os.path.isfile(self.setup_cfg_path)
|
||||
):
|
||||
raise InstallationError(
|
||||
f"Project {self} has a 'pyproject.toml' and its build "
|
||||
f"backend is missing the 'build_editable' hook. Since it does not "
|
||||
f"have a 'setup.py' nor a 'setup.cfg', "
|
||||
f"it cannot be installed in editable mode. "
|
||||
f"Consider using a build backend that supports PEP 660."
|
||||
f'it cannot be installed in editable mode. '
|
||||
f'Consider using a build backend that supports PEP 660.',
|
||||
)
|
||||
|
||||
def prepare_metadata(self) -> None:
|
||||
|
|
@ -504,14 +509,14 @@ class InstallRequirement:
|
|||
Under legacy processing, call setup.py egg-info.
|
||||
"""
|
||||
assert self.source_dir
|
||||
details = self.name or f"from {self.link}"
|
||||
details = self.name or f'from {self.link}'
|
||||
|
||||
if self.use_pep517:
|
||||
assert self.pep517_backend is not None
|
||||
if (
|
||||
self.editable
|
||||
and self.permit_editable_wheels
|
||||
and self.supports_pyproject_editable()
|
||||
self.editable and
|
||||
self.permit_editable_wheels and
|
||||
self.supports_pyproject_editable()
|
||||
):
|
||||
self.metadata_directory = generate_editable_metadata(
|
||||
build_env=self.build_env,
|
||||
|
|
@ -543,7 +548,7 @@ class InstallRequirement:
|
|||
|
||||
@property
|
||||
def metadata(self) -> Any:
|
||||
if not hasattr(self, "_metadata"):
|
||||
if not hasattr(self, '_metadata'):
|
||||
self._metadata = self.get_dist().metadata
|
||||
|
||||
return self._metadata
|
||||
|
|
@ -553,16 +558,16 @@ class InstallRequirement:
|
|||
|
||||
def assert_source_matches_version(self) -> None:
|
||||
assert self.source_dir
|
||||
version = self.metadata["version"]
|
||||
version = self.metadata['version']
|
||||
if self.req.specifier and version not in self.req.specifier:
|
||||
logger.warning(
|
||||
"Requested %s, but installing version %s",
|
||||
'Requested %s, but installing version %s',
|
||||
self,
|
||||
version,
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
"Source in %s has version %s, which satisfies requirement %s",
|
||||
'Source in %s has version %s, which satisfies requirement %s',
|
||||
display_path(self.source_dir),
|
||||
version,
|
||||
self,
|
||||
|
|
@ -595,26 +600,26 @@ class InstallRequirement:
|
|||
def update_editable(self) -> None:
|
||||
if not self.link:
|
||||
logger.debug(
|
||||
"Cannot update repository at %s; repository location is unknown",
|
||||
'Cannot update repository at %s; repository location is unknown',
|
||||
self.source_dir,
|
||||
)
|
||||
return
|
||||
assert self.editable
|
||||
assert self.source_dir
|
||||
if self.link.scheme == "file":
|
||||
if self.link.scheme == 'file':
|
||||
# Static paths don't get updated
|
||||
return
|
||||
vcs_backend = vcs.get_backend_for_scheme(self.link.scheme)
|
||||
# Editable requirements are validated in Requirement constructors.
|
||||
# So here, if it's neither a path nor a valid VCS URL, it's a bug.
|
||||
assert vcs_backend, f"Unsupported VCS URL {self.link.url}"
|
||||
assert vcs_backend, f'Unsupported VCS URL {self.link.url}'
|
||||
hidden_url = hide_url(self.link.url)
|
||||
vcs_backend.obtain(self.source_dir, url=hidden_url, verbosity=0)
|
||||
|
||||
# Top-level Actions
|
||||
def uninstall(
|
||||
self, auto_confirm: bool = False, verbose: bool = False
|
||||
) -> Optional[UninstallPathSet]:
|
||||
self, auto_confirm: bool = False, verbose: bool = False,
|
||||
) -> UninstallPathSet | None:
|
||||
"""
|
||||
Uninstall the distribution currently satisfying this requirement.
|
||||
|
||||
|
|
@ -630,9 +635,9 @@ class InstallRequirement:
|
|||
assert self.req
|
||||
dist = get_default_environment().get_distribution(self.req.name)
|
||||
if not dist:
|
||||
logger.warning("Skipping %s as it is not installed.", self.name)
|
||||
logger.warning('Skipping %s as it is not installed.', self.name)
|
||||
return None
|
||||
logger.info("Found existing installation: %s", dist)
|
||||
logger.info('Found existing installation: %s', dist)
|
||||
|
||||
uninstalled_pathset = UninstallPathSet.from_dist(dist)
|
||||
uninstalled_pathset.remove(auto_confirm, verbose)
|
||||
|
|
@ -641,17 +646,17 @@ class InstallRequirement:
|
|||
def _get_archive_name(self, path: str, parentdir: str, rootdir: str) -> str:
|
||||
def _clean_zip_name(name: str, prefix: str) -> str:
|
||||
assert name.startswith(
|
||||
prefix + os.path.sep
|
||||
prefix + os.path.sep,
|
||||
), f"name {name!r} doesn't start with prefix {prefix!r}"
|
||||
name = name[len(prefix) + 1 :]
|
||||
name = name.replace(os.path.sep, "/")
|
||||
name = name[len(prefix) + 1:]
|
||||
name = name.replace(os.path.sep, '/')
|
||||
return name
|
||||
|
||||
path = os.path.join(parentdir, path)
|
||||
name = _clean_zip_name(path, rootdir)
|
||||
return self.name + "/" + name
|
||||
return self.name + '/' + name
|
||||
|
||||
def archive(self, build_dir: Optional[str]) -> None:
|
||||
def archive(self, build_dir: str | None) -> None:
|
||||
"""Saves archive to provided build_dir.
|
||||
|
||||
Used for saving downloaded VCS requirements as part of `pip download`.
|
||||
|
|
@ -661,29 +666,29 @@ class InstallRequirement:
|
|||
return
|
||||
|
||||
create_archive = True
|
||||
archive_name = "{}-{}.zip".format(self.name, self.metadata["version"])
|
||||
archive_name = '{}-{}.zip'.format(self.name, self.metadata['version'])
|
||||
archive_path = os.path.join(build_dir, archive_name)
|
||||
|
||||
if os.path.exists(archive_path):
|
||||
response = ask_path_exists(
|
||||
"The file {} exists. (i)gnore, (w)ipe, "
|
||||
"(b)ackup, (a)bort ".format(display_path(archive_path)),
|
||||
("i", "w", "b", "a"),
|
||||
'The file {} exists. (i)gnore, (w)ipe, '
|
||||
'(b)ackup, (a)bort '.format(display_path(archive_path)),
|
||||
('i', 'w', 'b', 'a'),
|
||||
)
|
||||
if response == "i":
|
||||
if response == 'i':
|
||||
create_archive = False
|
||||
elif response == "w":
|
||||
logger.warning("Deleting %s", display_path(archive_path))
|
||||
elif response == 'w':
|
||||
logger.warning('Deleting %s', display_path(archive_path))
|
||||
os.remove(archive_path)
|
||||
elif response == "b":
|
||||
elif response == 'b':
|
||||
dest_file = backup_dir(archive_path)
|
||||
logger.warning(
|
||||
"Backing up %s to %s",
|
||||
'Backing up %s to %s',
|
||||
display_path(archive_path),
|
||||
display_path(dest_file),
|
||||
)
|
||||
shutil.move(archive_path, dest_file)
|
||||
elif response == "a":
|
||||
elif response == 'a':
|
||||
sys.exit(-1)
|
||||
|
||||
if not create_archive:
|
||||
|
|
@ -691,7 +696,7 @@ class InstallRequirement:
|
|||
|
||||
zip_output = zipfile.ZipFile(
|
||||
archive_path,
|
||||
"w",
|
||||
'w',
|
||||
zipfile.ZIP_DEFLATED,
|
||||
allowZip64=True,
|
||||
)
|
||||
|
|
@ -704,9 +709,9 @@ class InstallRequirement:
|
|||
parentdir=dirpath,
|
||||
rootdir=dir,
|
||||
)
|
||||
zipdir = zipfile.ZipInfo(dir_arcname + "/")
|
||||
zipdir = zipfile.ZipInfo(dir_arcname + '/')
|
||||
zipdir.external_attr = 0x1ED << 16 # 0o755
|
||||
zip_output.writestr(zipdir, "")
|
||||
zip_output.writestr(zipdir, '')
|
||||
for filename in filenames:
|
||||
file_arcname = self._get_archive_name(
|
||||
filename,
|
||||
|
|
@ -716,15 +721,15 @@ class InstallRequirement:
|
|||
filename = os.path.join(dirpath, filename)
|
||||
zip_output.write(filename, file_arcname)
|
||||
|
||||
logger.info("Saved %s", display_path(archive_path))
|
||||
logger.info('Saved %s', display_path(archive_path))
|
||||
|
||||
def install(
|
||||
self,
|
||||
install_options: List[str],
|
||||
global_options: Optional[Sequence[str]] = None,
|
||||
root: Optional[str] = None,
|
||||
home: Optional[str] = None,
|
||||
prefix: Optional[str] = None,
|
||||
install_options: list[str],
|
||||
global_options: Sequence[str] | None = None,
|
||||
root: str | None = None,
|
||||
home: str | None = None,
|
||||
prefix: str | None = None,
|
||||
warn_script_location: bool = True,
|
||||
use_user_site: bool = False,
|
||||
pycompile: bool = True,
|
||||
|
|
@ -819,11 +824,11 @@ class InstallRequirement:
|
|||
deprecated(
|
||||
reason=(
|
||||
"{} was installed using the legacy 'setup.py install' "
|
||||
"method, because a wheel could not be built for it.".format(
|
||||
self.name
|
||||
'method, because a wheel could not be built for it.'.format(
|
||||
self.name,
|
||||
)
|
||||
),
|
||||
replacement="to fix the wheel build issue reported above",
|
||||
replacement='to fix the wheel build issue reported above',
|
||||
gone_in=None,
|
||||
issue=8368,
|
||||
)
|
||||
|
|
@ -832,24 +837,24 @@ class InstallRequirement:
|
|||
def check_invalid_constraint_type(req: InstallRequirement) -> str:
|
||||
|
||||
# Check for unsupported forms
|
||||
problem = ""
|
||||
problem = ''
|
||||
if not req.name:
|
||||
problem = "Unnamed requirements are not allowed as constraints"
|
||||
problem = 'Unnamed requirements are not allowed as constraints'
|
||||
elif req.editable:
|
||||
problem = "Editable requirements are not allowed as constraints"
|
||||
problem = 'Editable requirements are not allowed as constraints'
|
||||
elif req.extras:
|
||||
problem = "Constraints cannot have extras"
|
||||
problem = 'Constraints cannot have extras'
|
||||
|
||||
if problem:
|
||||
deprecated(
|
||||
reason=(
|
||||
"Constraints are only allowed to take the form of a package "
|
||||
"name and a version specifier. Other forms were originally "
|
||||
"permitted as an accident of the implementation, but were "
|
||||
"undocumented. The new implementation of the resolver no "
|
||||
"longer supports these forms."
|
||||
'Constraints are only allowed to take the form of a package '
|
||||
'name and a version specifier. Other forms were originally '
|
||||
'permitted as an accident of the implementation, but were '
|
||||
'undocumented. The new implementation of the resolver no '
|
||||
'longer supports these forms.'
|
||||
),
|
||||
replacement="replacing the constraint with a requirement",
|
||||
replacement='replacing the constraint with a requirement',
|
||||
# No plan yet for when the new resolver becomes default
|
||||
gone_in=None,
|
||||
issue=8210,
|
||||
|
|
|
|||
|
|
@ -1,13 +1,18 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
from typing import Dict, Iterable, List, Optional, Tuple
|
||||
|
||||
from pip._vendor.packaging.utils import canonicalize_name
|
||||
from typing import Dict
|
||||
from typing import Iterable
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
|
||||
from pip._internal.exceptions import InstallationError
|
||||
from pip._internal.models.wheel import Wheel
|
||||
from pip._internal.req.req_install import InstallRequirement
|
||||
from pip._internal.utils import compatibility_tags
|
||||
from pip._vendor.packaging.utils import canonicalize_name
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -16,29 +21,29 @@ class RequirementSet:
|
|||
def __init__(self, check_supported_wheels: bool = True) -> None:
|
||||
"""Create a RequirementSet."""
|
||||
|
||||
self.requirements: Dict[str, InstallRequirement] = OrderedDict()
|
||||
self.requirements: dict[str, InstallRequirement] = OrderedDict()
|
||||
self.check_supported_wheels = check_supported_wheels
|
||||
|
||||
self.unnamed_requirements: List[InstallRequirement] = []
|
||||
self.unnamed_requirements: list[InstallRequirement] = []
|
||||
|
||||
def __str__(self) -> str:
|
||||
requirements = sorted(
|
||||
(req for req in self.requirements.values() if not req.comes_from),
|
||||
key=lambda req: canonicalize_name(req.name or ""),
|
||||
key=lambda req: canonicalize_name(req.name or ''),
|
||||
)
|
||||
return " ".join(str(req.req) for req in requirements)
|
||||
return ' '.join(str(req.req) for req in requirements)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
requirements = sorted(
|
||||
self.requirements.values(),
|
||||
key=lambda req: canonicalize_name(req.name or ""),
|
||||
key=lambda req: canonicalize_name(req.name or ''),
|
||||
)
|
||||
|
||||
format_string = "<{classname} object; {count} requirement(s): {reqs}>"
|
||||
format_string = '<{classname} object; {count} requirement(s): {reqs}>'
|
||||
return format_string.format(
|
||||
classname=self.__class__.__name__,
|
||||
count=len(requirements),
|
||||
reqs=", ".join(str(req.req) for req in requirements),
|
||||
reqs=', '.join(str(req.req) for req in requirements),
|
||||
)
|
||||
|
||||
def add_unnamed_requirement(self, install_req: InstallRequirement) -> None:
|
||||
|
|
@ -54,9 +59,9 @@ class RequirementSet:
|
|||
def add_requirement(
|
||||
self,
|
||||
install_req: InstallRequirement,
|
||||
parent_req_name: Optional[str] = None,
|
||||
extras_requested: Optional[Iterable[str]] = None,
|
||||
) -> Tuple[List[InstallRequirement], Optional[InstallRequirement]]:
|
||||
parent_req_name: str | None = None,
|
||||
extras_requested: Iterable[str] | None = None,
|
||||
) -> tuple[list[InstallRequirement], InstallRequirement | None]:
|
||||
"""Add install_req as a requirement to install.
|
||||
|
||||
:param parent_req_name: The name of the requirement that needed this
|
||||
|
|
@ -89,9 +94,9 @@ class RequirementSet:
|
|||
tags = compatibility_tags.get_supported()
|
||||
if self.check_supported_wheels and not wheel.supported(tags):
|
||||
raise InstallationError(
|
||||
"{} is not a supported wheel on this platform.".format(
|
||||
wheel.filename
|
||||
)
|
||||
'{} is not a supported wheel on this platform.'.format(
|
||||
wheel.filename,
|
||||
),
|
||||
)
|
||||
|
||||
# This next bit is really a sanity check.
|
||||
|
|
@ -106,26 +111,26 @@ class RequirementSet:
|
|||
return [install_req], None
|
||||
|
||||
try:
|
||||
existing_req: Optional[InstallRequirement] = self.get_requirement(
|
||||
install_req.name
|
||||
existing_req: InstallRequirement | None = self.get_requirement(
|
||||
install_req.name,
|
||||
)
|
||||
except KeyError:
|
||||
existing_req = None
|
||||
|
||||
has_conflicting_requirement = (
|
||||
parent_req_name is None
|
||||
and existing_req
|
||||
and not existing_req.constraint
|
||||
and existing_req.extras == install_req.extras
|
||||
and existing_req.req
|
||||
and install_req.req
|
||||
and existing_req.req.specifier != install_req.req.specifier
|
||||
parent_req_name is None and
|
||||
existing_req and
|
||||
not existing_req.constraint and
|
||||
existing_req.extras == install_req.extras and
|
||||
existing_req.req and
|
||||
install_req.req and
|
||||
existing_req.req.specifier != install_req.req.specifier
|
||||
)
|
||||
if has_conflicting_requirement:
|
||||
raise InstallationError(
|
||||
"Double requirement given: {} (already in {}, name={!r})".format(
|
||||
install_req, existing_req, install_req.name
|
||||
)
|
||||
'Double requirement given: {} (already in {}, name={!r})'.format(
|
||||
install_req, existing_req, install_req.name,
|
||||
),
|
||||
)
|
||||
|
||||
# When no existing requirement exists, add the requirement as a
|
||||
|
|
@ -146,8 +151,8 @@ class RequirementSet:
|
|||
if does_not_satisfy_constraint:
|
||||
raise InstallationError(
|
||||
"Could not satisfy constraints for '{}': "
|
||||
"installation from path or url cannot be "
|
||||
"constrained to a version".format(install_req.name)
|
||||
'installation from path or url cannot be '
|
||||
'constrained to a version'.format(install_req.name),
|
||||
)
|
||||
# If we're now installing a constraint, mark the existing
|
||||
# object for real installation.
|
||||
|
|
@ -157,10 +162,10 @@ class RequirementSet:
|
|||
if install_req.user_supplied:
|
||||
existing_req.user_supplied = True
|
||||
existing_req.extras = tuple(
|
||||
sorted(set(existing_req.extras) | set(install_req.extras))
|
||||
sorted(set(existing_req.extras) | set(install_req.extras)),
|
||||
)
|
||||
logger.debug(
|
||||
"Setting %s extras to: %s",
|
||||
'Setting %s extras to: %s',
|
||||
existing_req,
|
||||
existing_req.extras,
|
||||
)
|
||||
|
|
@ -172,8 +177,8 @@ class RequirementSet:
|
|||
project_name = canonicalize_name(name)
|
||||
|
||||
return (
|
||||
project_name in self.requirements
|
||||
and not self.requirements[project_name].constraint
|
||||
project_name in self.requirements and
|
||||
not self.requirements[project_name].constraint
|
||||
)
|
||||
|
||||
def get_requirement(self, name: str) -> InstallRequirement:
|
||||
|
|
@ -182,8 +187,8 @@ class RequirementSet:
|
|||
if project_name in self.requirements:
|
||||
return self.requirements[project_name]
|
||||
|
||||
raise KeyError(f"No project with the name {name!r}")
|
||||
raise KeyError(f'No project with the name {name!r}')
|
||||
|
||||
@property
|
||||
def all_requirements(self) -> List[InstallRequirement]:
|
||||
def all_requirements(self) -> list[InstallRequirement]:
|
||||
return self.unnamed_requirements + list(self.requirements.values())
|
||||
|
|
|
|||
|
|
@ -1,9 +1,16 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
from types import TracebackType
|
||||
from typing import Dict, Iterator, Optional, Set, Type, Union
|
||||
from typing import Dict
|
||||
from typing import Iterator
|
||||
from typing import Optional
|
||||
from typing import Set
|
||||
from typing import Type
|
||||
from typing import Union
|
||||
|
||||
from pip._internal.models.link import Link
|
||||
from pip._internal.req.req_install import InstallRequirement
|
||||
|
|
@ -18,7 +25,7 @@ def update_env_context_manager(**changes: str) -> Iterator[None]:
|
|||
|
||||
# Save values from the target and change them.
|
||||
non_existent_marker = object()
|
||||
saved_values: Dict[str, Union[object, str]] = {}
|
||||
saved_values: dict[str, object | str] = {}
|
||||
for name, new_value in changes.items():
|
||||
try:
|
||||
saved_values[name] = target[name]
|
||||
|
|
@ -39,13 +46,13 @@ def update_env_context_manager(**changes: str) -> Iterator[None]:
|
|||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def get_requirement_tracker() -> Iterator["RequirementTracker"]:
|
||||
root = os.environ.get("PIP_REQ_TRACKER")
|
||||
def get_requirement_tracker() -> Iterator[RequirementTracker]:
|
||||
root = os.environ.get('PIP_REQ_TRACKER')
|
||||
with contextlib.ExitStack() as ctx:
|
||||
if root is None:
|
||||
root = ctx.enter_context(TempDirectory(kind="req-tracker")).path
|
||||
root = ctx.enter_context(TempDirectory(kind='req-tracker')).path
|
||||
ctx.enter_context(update_env_context_manager(PIP_REQ_TRACKER=root))
|
||||
logger.debug("Initialized build tracking at %s", root)
|
||||
logger.debug('Initialized build tracking at %s', root)
|
||||
|
||||
with RequirementTracker(root) as tracker:
|
||||
yield tracker
|
||||
|
|
@ -54,18 +61,18 @@ def get_requirement_tracker() -> Iterator["RequirementTracker"]:
|
|||
class RequirementTracker:
|
||||
def __init__(self, root: str) -> None:
|
||||
self._root = root
|
||||
self._entries: Set[InstallRequirement] = set()
|
||||
logger.debug("Created build tracker: %s", self._root)
|
||||
self._entries: set[InstallRequirement] = set()
|
||||
logger.debug('Created build tracker: %s', self._root)
|
||||
|
||||
def __enter__(self) -> "RequirementTracker":
|
||||
logger.debug("Entered build tracker: %s", self._root)
|
||||
def __enter__(self) -> RequirementTracker:
|
||||
logger.debug('Entered build tracker: %s', self._root)
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: Optional[Type[BaseException]],
|
||||
exc_val: Optional[BaseException],
|
||||
exc_tb: Optional[TracebackType],
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> None:
|
||||
self.cleanup()
|
||||
|
||||
|
|
@ -88,18 +95,18 @@ class RequirementTracker:
|
|||
except FileNotFoundError:
|
||||
pass
|
||||
else:
|
||||
message = "{} is already being built: {}".format(req.link, contents)
|
||||
message = f'{req.link} is already being built: {contents}'
|
||||
raise LookupError(message)
|
||||
|
||||
# If we're here, req should really not be building already.
|
||||
assert req not in self._entries
|
||||
|
||||
# Start tracking this requirement.
|
||||
with open(entry_path, "w", encoding="utf-8") as fp:
|
||||
with open(entry_path, 'w', encoding='utf-8') as fp:
|
||||
fp.write(str(req))
|
||||
self._entries.add(req)
|
||||
|
||||
logger.debug("Added %s to build tracker %r", req, self._root)
|
||||
logger.debug('Added %s to build tracker %r', req, self._root)
|
||||
|
||||
def remove(self, req: InstallRequirement) -> None:
|
||||
"""Remove an InstallRequirement from build tracking."""
|
||||
|
|
@ -109,13 +116,13 @@ class RequirementTracker:
|
|||
os.unlink(self._entry_path(req.link))
|
||||
self._entries.remove(req)
|
||||
|
||||
logger.debug("Removed %s from build tracker %r", req, self._root)
|
||||
logger.debug('Removed %s from build tracker %r', req, self._root)
|
||||
|
||||
def cleanup(self) -> None:
|
||||
for req in set(self._entries):
|
||||
self.remove(req)
|
||||
|
||||
logger.debug("Removed build tracker: %r", self._root)
|
||||
logger.debug('Removed build tracker: %r', self._root)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def track(self, req: InstallRequirement) -> Iterator[None]:
|
||||
|
|
|
|||
|
|
@ -1,18 +1,35 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import os
|
||||
import sys
|
||||
import sysconfig
|
||||
from importlib.util import cache_from_source
|
||||
from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Set, Tuple
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Dict
|
||||
from typing import Iterable
|
||||
from typing import Iterator
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Set
|
||||
from typing import Tuple
|
||||
|
||||
from pip._internal.exceptions import UninstallationError
|
||||
from pip._internal.locations import get_bin_prefix, get_bin_user
|
||||
from pip._internal.locations import get_bin_prefix
|
||||
from pip._internal.locations import get_bin_user
|
||||
from pip._internal.metadata import BaseDistribution
|
||||
from pip._internal.utils.compat import WINDOWS
|
||||
from pip._internal.utils.egg_link import egg_link_path_from_location
|
||||
from pip._internal.utils.logging import getLogger, indent_log
|
||||
from pip._internal.utils.misc import ask, is_local, normalize_path, renames, rmtree
|
||||
from pip._internal.utils.temp_dir import AdjacentTempDirectory, TempDirectory
|
||||
from pip._internal.utils.logging import getLogger
|
||||
from pip._internal.utils.logging import indent_log
|
||||
from pip._internal.utils.misc import ask
|
||||
from pip._internal.utils.misc import is_local
|
||||
from pip._internal.utils.misc import normalize_path
|
||||
from pip._internal.utils.misc import renames
|
||||
from pip._internal.utils.misc import rmtree
|
||||
from pip._internal.utils.temp_dir import AdjacentTempDirectory
|
||||
from pip._internal.utils.temp_dir import TempDirectory
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
|
@ -26,18 +43,18 @@ def _script_names(bin_dir: str, script_name: str, is_gui: bool) -> Iterator[str]
|
|||
yield exe_name
|
||||
if not WINDOWS:
|
||||
return
|
||||
yield f"{exe_name}.exe"
|
||||
yield f"{exe_name}.exe.manifest"
|
||||
yield f'{exe_name}.exe'
|
||||
yield f'{exe_name}.exe.manifest'
|
||||
if is_gui:
|
||||
yield f"{exe_name}-script.pyw"
|
||||
yield f'{exe_name}-script.pyw'
|
||||
else:
|
||||
yield f"{exe_name}-script.py"
|
||||
yield f'{exe_name}-script.py'
|
||||
|
||||
|
||||
def _unique(fn: Callable[..., Iterator[Any]]) -> Callable[..., Iterator[Any]]:
|
||||
@functools.wraps(fn)
|
||||
def unique(*args: Any, **kw: Any) -> Iterator[Any]:
|
||||
seen: Set[Any] = set()
|
||||
seen: set[Any] = set()
|
||||
for item in fn(*args, **kw):
|
||||
if item not in seen:
|
||||
seen.add(item)
|
||||
|
|
@ -62,46 +79,46 @@ def uninstallation_paths(dist: BaseDistribution) -> Iterator[str]:
|
|||
https://packaging.python.org/specifications/recording-installed-packages/
|
||||
"""
|
||||
location = dist.location
|
||||
assert location is not None, "not installed"
|
||||
assert location is not None, 'not installed'
|
||||
|
||||
entries = dist.iter_declared_entries()
|
||||
if entries is None:
|
||||
msg = "Cannot uninstall {dist}, RECORD file not found.".format(dist=dist)
|
||||
msg = f'Cannot uninstall {dist}, RECORD file not found.'
|
||||
installer = dist.installer
|
||||
if not installer or installer == "pip":
|
||||
dep = "{}=={}".format(dist.raw_name, dist.version)
|
||||
if not installer or installer == 'pip':
|
||||
dep = f'{dist.raw_name}=={dist.version}'
|
||||
msg += (
|
||||
" You might be able to recover from this via: "
|
||||
' You might be able to recover from this via: '
|
||||
"'pip install --force-reinstall --no-deps {}'.".format(dep)
|
||||
)
|
||||
else:
|
||||
msg += " Hint: The package was installed by {}.".format(installer)
|
||||
msg += f' Hint: The package was installed by {installer}.'
|
||||
raise UninstallationError(msg)
|
||||
|
||||
for entry in entries:
|
||||
path = os.path.join(location, entry)
|
||||
yield path
|
||||
if path.endswith(".py"):
|
||||
if path.endswith('.py'):
|
||||
dn, fn = os.path.split(path)
|
||||
base = fn[:-3]
|
||||
path = os.path.join(dn, base + ".pyc")
|
||||
path = os.path.join(dn, base + '.pyc')
|
||||
yield path
|
||||
path = os.path.join(dn, base + ".pyo")
|
||||
path = os.path.join(dn, base + '.pyo')
|
||||
yield path
|
||||
|
||||
|
||||
def compact(paths: Iterable[str]) -> Set[str]:
|
||||
def compact(paths: Iterable[str]) -> set[str]:
|
||||
"""Compact a path set to contain the minimal number of paths
|
||||
necessary to contain all paths in the set. If /a/path/ and
|
||||
/a/path/to/a/file.txt are both in the set, leave only the
|
||||
shorter path."""
|
||||
|
||||
sep = os.path.sep
|
||||
short_paths: Set[str] = set()
|
||||
short_paths: set[str] = set()
|
||||
for path in sorted(paths, key=len):
|
||||
should_skip = any(
|
||||
path.startswith(shortpath.rstrip("*"))
|
||||
and path[len(shortpath.rstrip("*").rstrip(sep))] == sep
|
||||
path.startswith(shortpath.rstrip('*')) and
|
||||
path[len(shortpath.rstrip('*').rstrip(sep))] == sep
|
||||
for shortpath in short_paths
|
||||
)
|
||||
if not should_skip:
|
||||
|
|
@ -109,7 +126,7 @@ def compact(paths: Iterable[str]) -> Set[str]:
|
|||
return short_paths
|
||||
|
||||
|
||||
def compress_for_rename(paths: Iterable[str]) -> Set[str]:
|
||||
def compress_for_rename(paths: Iterable[str]) -> set[str]:
|
||||
"""Returns a set containing the paths that need to be renamed.
|
||||
|
||||
This set may include directories when the original sequence of paths
|
||||
|
|
@ -118,7 +135,7 @@ def compress_for_rename(paths: Iterable[str]) -> Set[str]:
|
|||
case_map = {os.path.normcase(p): p for p in paths}
|
||||
remaining = set(case_map)
|
||||
unchecked = sorted({os.path.split(p)[0] for p in case_map.values()}, key=len)
|
||||
wildcards: Set[str] = set()
|
||||
wildcards: set[str] = set()
|
||||
|
||||
def norm_join(*a: str) -> str:
|
||||
return os.path.normcase(os.path.join(*a))
|
||||
|
|
@ -128,8 +145,8 @@ def compress_for_rename(paths: Iterable[str]) -> Set[str]:
|
|||
# This directory has already been handled.
|
||||
continue
|
||||
|
||||
all_files: Set[str] = set()
|
||||
all_subdirs: Set[str] = set()
|
||||
all_files: set[str] = set()
|
||||
all_subdirs: set[str] = set()
|
||||
for dirname, subdirs, files in os.walk(root):
|
||||
all_subdirs.update(norm_join(root, dirname, d) for d in subdirs)
|
||||
all_files.update(norm_join(root, dirname, f) for f in files)
|
||||
|
|
@ -143,7 +160,7 @@ def compress_for_rename(paths: Iterable[str]) -> Set[str]:
|
|||
return set(map(case_map.__getitem__, remaining)) | wildcards
|
||||
|
||||
|
||||
def compress_for_output_listing(paths: Iterable[str]) -> Tuple[Set[str], Set[str]]:
|
||||
def compress_for_output_listing(paths: Iterable[str]) -> tuple[set[str], set[str]]:
|
||||
"""Returns a tuple of 2 sets of which paths to display to user
|
||||
|
||||
The first set contains paths that would be deleted. Files of a package
|
||||
|
|
@ -161,9 +178,9 @@ def compress_for_output_listing(paths: Iterable[str]) -> Tuple[Set[str], Set[str
|
|||
folders = set()
|
||||
files = set()
|
||||
for path in will_remove:
|
||||
if path.endswith(".pyc"):
|
||||
if path.endswith('.pyc'):
|
||||
continue
|
||||
if path.endswith("__init__.py") or ".dist-info" in path:
|
||||
if path.endswith('__init__.py') or '.dist-info' in path:
|
||||
folders.add(os.path.dirname(path))
|
||||
files.add(path)
|
||||
|
||||
|
|
@ -177,18 +194,18 @@ def compress_for_output_listing(paths: Iterable[str]) -> Tuple[Set[str], Set[str
|
|||
for folder in folders:
|
||||
for dirpath, _, dirfiles in os.walk(folder):
|
||||
for fname in dirfiles:
|
||||
if fname.endswith(".pyc"):
|
||||
if fname.endswith('.pyc'):
|
||||
continue
|
||||
|
||||
file_ = os.path.join(dirpath, fname)
|
||||
if (
|
||||
os.path.isfile(file_)
|
||||
and os.path.normcase(file_) not in _normcased_files
|
||||
os.path.isfile(file_) and
|
||||
os.path.normcase(file_) not in _normcased_files
|
||||
):
|
||||
# We are skipping this file. Add it to the set.
|
||||
will_skip.add(file_)
|
||||
|
||||
will_remove = files | {os.path.join(folder, "*") for folder in folders}
|
||||
will_remove = files | {os.path.join(folder, '*') for folder in folders}
|
||||
|
||||
return will_remove, will_skip
|
||||
|
||||
|
|
@ -200,10 +217,10 @@ class StashedUninstallPathSet:
|
|||
def __init__(self) -> None:
|
||||
# Mapping from source file root to [Adjacent]TempDirectory
|
||||
# for files under that directory.
|
||||
self._save_dirs: Dict[str, TempDirectory] = {}
|
||||
self._save_dirs: dict[str, TempDirectory] = {}
|
||||
# (old path, new path) tuples for each move that may need
|
||||
# to be undone.
|
||||
self._moves: List[Tuple[str, str]] = []
|
||||
self._moves: list[tuple[str, str]] = []
|
||||
|
||||
def _get_directory_stash(self, path: str) -> str:
|
||||
"""Stashes a directory.
|
||||
|
|
@ -214,7 +231,7 @@ class StashedUninstallPathSet:
|
|||
try:
|
||||
save_dir: TempDirectory = AdjacentTempDirectory(path)
|
||||
except OSError:
|
||||
save_dir = TempDirectory(kind="uninstall")
|
||||
save_dir = TempDirectory(kind='uninstall')
|
||||
self._save_dirs[os.path.normcase(path)] = save_dir
|
||||
|
||||
return save_dir.path
|
||||
|
|
@ -238,7 +255,7 @@ class StashedUninstallPathSet:
|
|||
else:
|
||||
# Did not find any suitable root
|
||||
head = os.path.dirname(path)
|
||||
save_dir = TempDirectory(kind="uninstall")
|
||||
save_dir = TempDirectory(kind='uninstall')
|
||||
self._save_dirs[head] = save_dir
|
||||
|
||||
relpath = os.path.relpath(path, head)
|
||||
|
|
@ -277,19 +294,19 @@ class StashedUninstallPathSet:
|
|||
def rollback(self) -> None:
|
||||
"""Undoes the uninstall by moving stashed files back."""
|
||||
for p in self._moves:
|
||||
logger.info("Moving to %s\n from %s", *p)
|
||||
logger.info('Moving to %s\n from %s', *p)
|
||||
|
||||
for new_path, path in self._moves:
|
||||
try:
|
||||
logger.debug("Replacing %s from %s", new_path, path)
|
||||
logger.debug('Replacing %s from %s', new_path, path)
|
||||
if os.path.isfile(new_path) or os.path.islink(new_path):
|
||||
os.unlink(new_path)
|
||||
elif os.path.isdir(new_path):
|
||||
rmtree(new_path)
|
||||
renames(path, new_path)
|
||||
except OSError as ex:
|
||||
logger.error("Failed to restore %s", new_path)
|
||||
logger.debug("Exception: %s", ex)
|
||||
logger.error('Failed to restore %s', new_path)
|
||||
logger.debug('Exception: %s', ex)
|
||||
|
||||
self.commit()
|
||||
|
||||
|
|
@ -303,9 +320,9 @@ class UninstallPathSet:
|
|||
requirement."""
|
||||
|
||||
def __init__(self, dist: BaseDistribution) -> None:
|
||||
self._paths: Set[str] = set()
|
||||
self._refuse: Set[str] = set()
|
||||
self._pth: Dict[str, UninstallPthEntries] = {}
|
||||
self._paths: set[str] = set()
|
||||
self._refuse: set[str] = set()
|
||||
self._pth: dict[str, UninstallPthEntries] = {}
|
||||
self._dist = dist
|
||||
self._moved_paths = StashedUninstallPathSet()
|
||||
|
||||
|
|
@ -333,7 +350,7 @@ class UninstallPathSet:
|
|||
|
||||
# __pycache__ files can show up after 'installed-files.txt' is created,
|
||||
# due to imports
|
||||
if os.path.splitext(path)[1] == ".py":
|
||||
if os.path.splitext(path)[1] == '.py':
|
||||
self.add(cache_from_source(path))
|
||||
|
||||
def add_pth(self, pth_file: str, entry: str) -> None:
|
||||
|
|
@ -356,8 +373,8 @@ class UninstallPathSet:
|
|||
)
|
||||
return
|
||||
|
||||
dist_name_version = f"{self._dist.raw_name}-{self._dist.version}"
|
||||
logger.info("Uninstalling %s:", dist_name_version)
|
||||
dist_name_version = f'{self._dist.raw_name}-{self._dist.version}'
|
||||
logger.info('Uninstalling %s:', dist_name_version)
|
||||
|
||||
with indent_log():
|
||||
if auto_confirm or self._allowed_to_proceed(verbose):
|
||||
|
|
@ -367,12 +384,12 @@ class UninstallPathSet:
|
|||
|
||||
for path in sorted(compact(for_rename)):
|
||||
moved.stash(path)
|
||||
logger.verbose("Removing file or directory %s", path)
|
||||
logger.verbose('Removing file or directory %s', path)
|
||||
|
||||
for pth in self._pth.values():
|
||||
pth.remove()
|
||||
|
||||
logger.info("Successfully uninstalled %s", dist_name_version)
|
||||
logger.info('Successfully uninstalled %s', dist_name_version)
|
||||
|
||||
def _allowed_to_proceed(self, verbose: bool) -> bool:
|
||||
"""Display which files would be deleted and prompt for confirmation"""
|
||||
|
|
@ -394,13 +411,13 @@ class UninstallPathSet:
|
|||
will_remove = set(self._paths)
|
||||
will_skip = set()
|
||||
|
||||
_display("Would remove:", will_remove)
|
||||
_display("Would not remove (might be manually added):", will_skip)
|
||||
_display("Would not remove (outside of prefix):", self._refuse)
|
||||
_display('Would remove:', will_remove)
|
||||
_display('Would not remove (might be manually added):', will_skip)
|
||||
_display('Would not remove (outside of prefix):', self._refuse)
|
||||
if verbose:
|
||||
_display("Will actually move:", compress_for_rename(self._paths))
|
||||
_display('Will actually move:', compress_for_rename(self._paths))
|
||||
|
||||
return ask("Proceed (Y/n)? ", ("y", "n", "")) != "n"
|
||||
return ask('Proceed (Y/n)? ', ('y', 'n', '')) != 'n'
|
||||
|
||||
def rollback(self) -> None:
|
||||
"""Rollback the changes previously made by remove()."""
|
||||
|
|
@ -410,7 +427,7 @@ class UninstallPathSet:
|
|||
self._dist.raw_name,
|
||||
)
|
||||
return
|
||||
logger.info("Rolling back uninstall of %s", self._dist.raw_name)
|
||||
logger.info('Rolling back uninstall of %s', self._dist.raw_name)
|
||||
self._moved_paths.rollback()
|
||||
for pth in self._pth.values():
|
||||
pth.rollback()
|
||||
|
|
@ -420,12 +437,12 @@ class UninstallPathSet:
|
|||
self._moved_paths.commit()
|
||||
|
||||
@classmethod
|
||||
def from_dist(cls, dist: BaseDistribution) -> "UninstallPathSet":
|
||||
def from_dist(cls, dist: BaseDistribution) -> UninstallPathSet:
|
||||
dist_location = dist.location
|
||||
info_location = dist.info_location
|
||||
if dist_location is None:
|
||||
logger.info(
|
||||
"Not uninstalling %s since it is not installed",
|
||||
'Not uninstalling %s since it is not installed',
|
||||
dist.canonical_name,
|
||||
)
|
||||
return cls(dist)
|
||||
|
|
@ -433,7 +450,7 @@ class UninstallPathSet:
|
|||
normalized_dist_location = normalize_path(dist_location)
|
||||
if not dist.local:
|
||||
logger.info(
|
||||
"Not uninstalling %s at %s, outside environment %s",
|
||||
'Not uninstalling %s at %s, outside environment %s',
|
||||
dist.canonical_name,
|
||||
normalized_dist_location,
|
||||
sys.prefix,
|
||||
|
|
@ -442,11 +459,11 @@ class UninstallPathSet:
|
|||
|
||||
if normalized_dist_location in {
|
||||
p
|
||||
for p in {sysconfig.get_path("stdlib"), sysconfig.get_path("platstdlib")}
|
||||
for p in {sysconfig.get_path('stdlib'), sysconfig.get_path('platstdlib')}
|
||||
if p
|
||||
}:
|
||||
logger.info(
|
||||
"Not uninstalling %s at %s, as it is in the standard library.",
|
||||
'Not uninstalling %s at %s, as it is in the standard library.',
|
||||
dist.canonical_name,
|
||||
normalized_dist_location,
|
||||
)
|
||||
|
|
@ -459,12 +476,12 @@ class UninstallPathSet:
|
|||
# directory. This means it is not a modern .dist-info installation, an
|
||||
# egg, or legacy editable.
|
||||
setuptools_flat_installation = (
|
||||
dist.installed_with_setuptools_egg_info
|
||||
and info_location is not None
|
||||
and os.path.exists(info_location)
|
||||
dist.installed_with_setuptools_egg_info and
|
||||
info_location is not None and
|
||||
os.path.exists(info_location) and
|
||||
# If dist is editable and the location points to a ``.egg-info``,
|
||||
# we are in fact in the legacy editable case.
|
||||
and not info_location.endswith(f"{dist.setuptools_filename}.egg-info")
|
||||
not info_location.endswith(f'{dist.setuptools_filename}.egg-info')
|
||||
)
|
||||
|
||||
# Uninstall cases order do matter as in the case of 2 installs of the
|
||||
|
|
@ -479,31 +496,31 @@ class UninstallPathSet:
|
|||
# FIXME: need a test for this elif block
|
||||
# occurs with --single-version-externally-managed/--record outside
|
||||
# of pip
|
||||
elif dist.is_file("top_level.txt"):
|
||||
elif dist.is_file('top_level.txt'):
|
||||
try:
|
||||
namespace_packages = dist.read_text("namespace_packages.txt")
|
||||
namespace_packages = dist.read_text('namespace_packages.txt')
|
||||
except FileNotFoundError:
|
||||
namespaces = []
|
||||
else:
|
||||
namespaces = namespace_packages.splitlines(keepends=False)
|
||||
for top_level_pkg in [
|
||||
p
|
||||
for p in dist.read_text("top_level.txt").splitlines()
|
||||
for p in dist.read_text('top_level.txt').splitlines()
|
||||
if p and p not in namespaces
|
||||
]:
|
||||
path = os.path.join(dist_location, top_level_pkg)
|
||||
paths_to_remove.add(path)
|
||||
paths_to_remove.add(f"{path}.py")
|
||||
paths_to_remove.add(f"{path}.pyc")
|
||||
paths_to_remove.add(f"{path}.pyo")
|
||||
paths_to_remove.add(f'{path}.py')
|
||||
paths_to_remove.add(f'{path}.pyc')
|
||||
paths_to_remove.add(f'{path}.pyo')
|
||||
|
||||
elif dist.installed_by_distutils:
|
||||
raise UninstallationError(
|
||||
"Cannot uninstall {!r}. It is a distutils installed project "
|
||||
"and thus we cannot accurately determine which files belong "
|
||||
"to it which would lead to only a partial uninstall.".format(
|
||||
'Cannot uninstall {!r}. It is a distutils installed project '
|
||||
'and thus we cannot accurately determine which files belong '
|
||||
'to it which would lead to only a partial uninstall.'.format(
|
||||
dist.raw_name,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
elif dist.installed_as_egg:
|
||||
|
|
@ -514,9 +531,9 @@ class UninstallPathSet:
|
|||
easy_install_egg = os.path.split(dist_location)[1]
|
||||
easy_install_pth = os.path.join(
|
||||
os.path.dirname(dist_location),
|
||||
"easy-install.pth",
|
||||
'easy-install.pth',
|
||||
)
|
||||
paths_to_remove.add_pth(easy_install_pth, "./" + easy_install_egg)
|
||||
paths_to_remove.add_pth(easy_install_pth, './' + easy_install_egg)
|
||||
|
||||
elif dist.installed_with_dist_info:
|
||||
for path in uninstallation_paths(dist):
|
||||
|
|
@ -528,18 +545,18 @@ class UninstallPathSet:
|
|||
with open(develop_egg_link) as fh:
|
||||
link_pointer = os.path.normcase(fh.readline().strip())
|
||||
assert link_pointer == dist_location, (
|
||||
f"Egg-link {link_pointer} does not match installed location of "
|
||||
f"{dist.raw_name} (at {dist_location})"
|
||||
f'Egg-link {link_pointer} does not match installed location of '
|
||||
f'{dist.raw_name} (at {dist_location})'
|
||||
)
|
||||
paths_to_remove.add(develop_egg_link)
|
||||
easy_install_pth = os.path.join(
|
||||
os.path.dirname(develop_egg_link), "easy-install.pth"
|
||||
os.path.dirname(develop_egg_link), 'easy-install.pth',
|
||||
)
|
||||
paths_to_remove.add_pth(easy_install_pth, dist_location)
|
||||
|
||||
else:
|
||||
logger.debug(
|
||||
"Not sure how to uninstall: %s - Check: %s",
|
||||
'Not sure how to uninstall: %s - Check: %s',
|
||||
dist,
|
||||
dist_location,
|
||||
)
|
||||
|
|
@ -551,10 +568,10 @@ class UninstallPathSet:
|
|||
|
||||
# find distutils scripts= scripts
|
||||
try:
|
||||
for script in dist.iterdir("scripts"):
|
||||
for script in dist.iterdir('scripts'):
|
||||
paths_to_remove.add(os.path.join(bin_dir, script.name))
|
||||
if WINDOWS:
|
||||
paths_to_remove.add(os.path.join(bin_dir, f"{script.name}.bat"))
|
||||
paths_to_remove.add(os.path.join(bin_dir, f'{script.name}.bat'))
|
||||
except (FileNotFoundError, NotADirectoryError):
|
||||
pass
|
||||
|
||||
|
|
@ -564,9 +581,9 @@ class UninstallPathSet:
|
|||
bin_dir: str,
|
||||
) -> Iterator[str]:
|
||||
for entry_point in dist.iter_entry_points():
|
||||
if entry_point.group == "console_scripts":
|
||||
if entry_point.group == 'console_scripts':
|
||||
yield from _script_names(bin_dir, entry_point.name, False)
|
||||
elif entry_point.group == "gui_scripts":
|
||||
elif entry_point.group == 'gui_scripts':
|
||||
yield from _script_names(bin_dir, entry_point.name, True)
|
||||
|
||||
for s in iter_scripts_to_remove(dist, bin_dir):
|
||||
|
|
@ -578,8 +595,8 @@ class UninstallPathSet:
|
|||
class UninstallPthEntries:
|
||||
def __init__(self, pth_file: str) -> None:
|
||||
self.file = pth_file
|
||||
self.entries: Set[str] = set()
|
||||
self._saved_lines: Optional[List[bytes]] = None
|
||||
self.entries: set[str] = set()
|
||||
self._saved_lines: list[bytes] | None = None
|
||||
|
||||
def add(self, entry: str) -> None:
|
||||
entry = os.path.normcase(entry)
|
||||
|
|
@ -593,41 +610,41 @@ class UninstallPthEntries:
|
|||
# have more than "\\sever\share". Valid examples: "\\server\share\" or
|
||||
# "\\server\share\folder".
|
||||
if WINDOWS and not os.path.splitdrive(entry)[0]:
|
||||
entry = entry.replace("\\", "/")
|
||||
entry = entry.replace('\\', '/')
|
||||
self.entries.add(entry)
|
||||
|
||||
def remove(self) -> None:
|
||||
logger.verbose("Removing pth entries from %s:", self.file)
|
||||
logger.verbose('Removing pth entries from %s:', self.file)
|
||||
|
||||
# If the file doesn't exist, log a warning and return
|
||||
if not os.path.isfile(self.file):
|
||||
logger.warning("Cannot remove entries from nonexistent file %s", self.file)
|
||||
logger.warning('Cannot remove entries from nonexistent file %s', self.file)
|
||||
return
|
||||
with open(self.file, "rb") as fh:
|
||||
with open(self.file, 'rb') as fh:
|
||||
# windows uses '\r\n' with py3k, but uses '\n' with py2.x
|
||||
lines = fh.readlines()
|
||||
self._saved_lines = lines
|
||||
if any(b"\r\n" in line for line in lines):
|
||||
endline = "\r\n"
|
||||
if any(b'\r\n' in line for line in lines):
|
||||
endline = '\r\n'
|
||||
else:
|
||||
endline = "\n"
|
||||
endline = '\n'
|
||||
# handle missing trailing newline
|
||||
if lines and not lines[-1].endswith(endline.encode("utf-8")):
|
||||
lines[-1] = lines[-1] + endline.encode("utf-8")
|
||||
if lines and not lines[-1].endswith(endline.encode('utf-8')):
|
||||
lines[-1] = lines[-1] + endline.encode('utf-8')
|
||||
for entry in self.entries:
|
||||
try:
|
||||
logger.verbose("Removing entry: %s", entry)
|
||||
lines.remove((entry + endline).encode("utf-8"))
|
||||
logger.verbose('Removing entry: %s', entry)
|
||||
lines.remove((entry + endline).encode('utf-8'))
|
||||
except ValueError:
|
||||
pass
|
||||
with open(self.file, "wb") as fh:
|
||||
with open(self.file, 'wb') as fh:
|
||||
fh.writelines(lines)
|
||||
|
||||
def rollback(self) -> bool:
|
||||
if self._saved_lines is None:
|
||||
logger.error("Cannot roll back changes to %s, none were made", self.file)
|
||||
logger.error('Cannot roll back changes to %s, none were made', self.file)
|
||||
return False
|
||||
logger.debug("Rolling %s back to previous state", self.file)
|
||||
with open(self.file, "wb") as fh:
|
||||
logger.debug('Rolling %s back to previous state', self.file)
|
||||
with open(self.file, 'wb') as fh:
|
||||
fh.writelines(self._saved_lines)
|
||||
return True
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue