Change venv

This commit is contained in:
Ambulance Clerc
2023-05-31 08:31:22 +02:00
parent fb6f579089
commit fdbb52c96f
466 changed files with 25899 additions and 64721 deletions

View File

@@ -1,6 +1,6 @@
import collections
import logging
from typing import Iterator, List, Optional, Sequence, Tuple
from typing import Generator, List, Optional, Sequence, Tuple
from pip._internal.utils.logging import indent_log
@@ -9,8 +9,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__)
@@ -26,7 +28,7 @@ class InstallationResult:
def _validate_requirements(
requirements: List[InstallRequirement],
) -> Iterator[Tuple[str, InstallRequirement]]:
) -> Generator[Tuple[str, InstallRequirement], None, None]:
for req in requirements:
assert req.name, f"invalid to-be-installed requirement: {req}"
yield req.name, req
@@ -34,7 +36,6 @@ def _validate_requirements(
def install_given_reqs(
requirements: List[InstallRequirement],
install_options: List[str],
global_options: Sequence[str],
root: Optional[str],
home: Optional[str],
@@ -52,8 +53,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 = []
@@ -61,17 +62,14 @@ 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
)
uninstalled_pathset = requirement.uninstall(auto_confirm=True)
else:
uninstalled_pathset = None
try:
requirement.install(
install_options,
global_options,
root=root,
home=home,

View File

@@ -11,28 +11,28 @@ InstallRequirement.
import logging
import os
import re
from typing import Any, Dict, Optional, Set, Tuple, Union
from typing import Dict, List, 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 pip._vendor.pkg_resources import RequirementParseError, parse_requirements
from pip._internal.exceptions import InstallationError
from pip._internal.models.index import PyPI, TestPyPI
from pip._internal.models.link import Link
from pip._internal.models.wheel import Wheel
from pip._internal.pyproject import make_pyproject_path
from pip._internal.req.req_file import ParsedRequirement
from pip._internal.req.req_install import InstallRequirement
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
__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__)
@@ -40,7 +40,7 @@ operators = Specifier._operators.keys()
def _strip_extras(path: str) -> Tuple[str, Optional[str]]:
m = re.match(r'^(.+)(\[[^\]]+\])$', path)
m = re.match(r"^(.+)(\[[^\]]+\])$", path)
extras = None
if m:
path_no_extras = m.group(1)
@@ -54,7 +54,7 @@ def _strip_extras(path: str) -> Tuple[str, Optional[str]]:
def convert_extras(extras: Optional[str]) -> Set[str]:
if not extras:
return set()
return Requirement("placeholder" + extras.lower()).extras
return get_requirement("placeholder" + extras.lower()).extras
def parse_editable(editable_req: str) -> Tuple[Optional[str], str, Set[str]]:
@@ -74,39 +74,23 @@ def parse_editable(editable_req: str) -> Tuple[Optional[str], str, Set[str]]:
url_no_extras, extras = _strip_extras(url)
if os.path.isdir(url_no_extras):
setup_py = os.path.join(url_no_extras, 'setup.py')
setup_cfg = os.path.join(url_no_extras, 'setup.cfg')
if not os.path.exists(setup_py) and not os.path.exists(setup_cfg):
msg = (
'File "setup.py" or "setup.cfg" not found. Directory cannot be '
'installed in editable mode: {}'
.format(os.path.abspath(url_no_extras))
)
pyproject_path = make_pyproject_path(url_no_extras)
if os.path.isfile(pyproject_path):
msg += (
'\n(A "pyproject.toml" file was found, but editable '
'mode currently requires a setuptools-based build.)'
)
raise InstallationError(msg)
# 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,
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)
@@ -114,9 +98,9 @@ def parse_editable(editable_req: str) -> Tuple[Optional[str], str, Set[str]]:
if not link.is_vcs:
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
@@ -128,43 +112,66 @@ def parse_editable(editable_req: str) -> Tuple[Optional[str], str, Set[str]]:
return package_name, url, set()
def check_first_requirement_in_file(filename: str) -> None:
"""Check if file is parsable as a requirements file.
This is heavily based on ``pkg_resources.parse_requirements``, but
simplified to just check the first meaningful line.
:raises InvalidRequirement: If the first meaningful line cannot be parsed
as an requirement.
"""
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.
)
for line in lines:
# Drop comments -- a hash without a space may be in a URL.
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, "")
Requirement(line)
return
def deduce_helpful_msg(req: str) -> str:
"""Returns helpful msg in case requirements file does not exist,
or cannot be parsed.
:params req: Requirements file path
"""
msg = ""
if os.path.exists(req):
msg = " The path does exist. "
# Try to parse and check if it is a requirements file.
try:
with open(req) as fp:
# parse first line only
next(parse_requirements(fp.read()))
msg += (
"The argument you provided "
"({}) appears to be a"
" requirements file. If that is the"
" case, use the '-r' flag to install"
" the packages specified within it."
).format(req)
except RequirementParseError:
logger.debug(
"Cannot parse '%s' as requirements file", req, exc_info=True
)
if not os.path.exists(req):
return f" File '{req}' does not exist."
msg = " The path does exist. "
# Try to parse and check if it is a requirements file.
try:
check_first_requirement_in_file(req)
except InvalidRequirement:
logger.debug("Cannot parse '%s' as requirements file", req)
else:
msg += f" File '{req}' does not exist."
msg += (
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."
)
return msg
class RequirementParts:
def __init__(
self,
requirement: Optional[Requirement],
link: Optional[Link],
markers: Optional[Marker],
extras: Set[str],
self,
requirement: Optional[Requirement],
link: Optional[Link],
markers: Optional[Marker],
extras: Set[str],
):
self.requirement = requirement
self.link = link
@@ -194,13 +201,16 @@ 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,
isolated: bool = False,
options: Optional[Dict[str, Any]] = None,
global_options: Optional[List[str]] = None,
hash_options: Optional[Dict[str, List[str]]] = None,
constraint: bool = False,
user_supplied: bool = False,
permit_editable_wheels: bool = False,
config_settings: Optional[Dict[str, Union[str, List[str]]]] = None,
) -> InstallRequirement:
parts = parse_req_from_editable(editable_req)
return InstallRequirement(
@@ -208,13 +218,14 @@ def install_req_from_editable(
comes_from=comes_from,
user_supplied=user_supplied,
editable=True,
permit_editable_wheels=permit_editable_wheels,
link=parts.link,
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 {},
global_options=global_options,
hash_options=hash_options,
config_settings=config_settings,
extras=parts.extras,
)
@@ -250,6 +261,8 @@ def _get_url_from_path(path: str, name: str) -> Optional[str]:
if _looks_like_path(name) and os.path.isdir(path):
if is_installable_dir(path):
return path_to_url(path)
# TODO: The is_installable_dir test here might not be necessary
# 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."
@@ -258,24 +271,23 @@ def _get_url_from_path(path: str, name: str) -> Optional[str]:
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',
name
"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:
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()
@@ -302,9 +314,8 @@ 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):
link = Link(
path_to_url(os.path.normpath(os.path.abspath(link.path))))
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
@@ -323,25 +334,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 = Requirement(req_as_string)
req = get_requirement(req_as_string)
except InvalidRequirement:
if os.path.sep in req_as_string:
add_msg = "It looks like a path."
add_msg += deduce_helpful_msg(req_as_string)
elif ('=' in req_as_string and
not any(op in req_as_string for op in operators)):
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 == ?"
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]"
@@ -350,7 +360,7 @@ 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
@@ -366,12 +376,15 @@ 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,
isolated: bool = False,
options: Optional[Dict[str, Any]] = None,
global_options: Optional[List[str]] = None,
hash_options: Optional[Dict[str, List[str]]] = None,
constraint: bool = False,
line_source: Optional[str] = None,
user_supplied: bool = False,
config_settings: Optional[Dict[str, Union[str, List[str]]]] = None,
) -> InstallRequirement:
"""Creates an InstallRequirement from a name, which might be a
requirement, directory containing 'setup.py', filename, or URL.
@@ -382,11 +395,15 @@ def install_req_from_line(
parts = parse_req_from_line(name, line_source)
return InstallRequirement(
parts.requirement, comes_from, link=parts.link, 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 {},
parts.requirement,
comes_from,
link=parts.link,
markers=parts.markers,
use_pep517=use_pep517,
isolated=isolated,
global_options=global_options,
hash_options=hash_options,
config_settings=config_settings,
constraint=constraint,
extras=parts.extras,
user_supplied=user_supplied,
@@ -401,7 +418,7 @@ def install_req_from_req_string(
user_supplied: bool = False,
) -> InstallRequirement:
try:
req = Requirement(req_string)
req = get_requirement(req_string)
except InvalidRequirement:
raise InstallationError(f"Invalid requirement: '{req_string}'")
@@ -409,8 +426,12 @@ def install_req_from_req_string(
PyPI.file_storage_domain,
TestPyPI.file_storage_domain,
]
if (req.url and comes_from and comes_from.link and
comes_from.link.netloc in domains_not_allowed):
if (
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 "
@@ -432,6 +453,7 @@ def install_req_from_parsed_requirement(
isolated: bool = False,
use_pep517: Optional[bool] = None,
user_supplied: bool = False,
config_settings: Optional[Dict[str, Union[str, List[str]]]] = None,
) -> InstallRequirement:
if parsed_req.is_editable:
req = install_req_from_editable(
@@ -441,6 +463,7 @@ def install_req_from_parsed_requirement(
constraint=parsed_req.constraint,
isolated=isolated,
user_supplied=user_supplied,
config_settings=config_settings,
)
else:
@@ -449,10 +472,18 @@ def install_req_from_parsed_requirement(
comes_from=parsed_req.comes_from,
use_pep517=use_pep517,
isolated=isolated,
options=parsed_req.options,
global_options=(
parsed_req.options.get("global_options", [])
if parsed_req.options
else []
),
hash_options=(
parsed_req.options.get("hashes", {}) if parsed_req.options else {}
),
constraint=parsed_req.constraint,
line_source=parsed_req.line_source,
user_supplied=user_supplied,
config_settings=config_settings,
)
return req
@@ -468,7 +499,8 @@ def install_req_from_link_and_ireq(
markers=ireq.markers,
use_pep517=ireq.use_pep517,
isolated=ireq.isolated,
install_options=ireq.install_options,
global_options=ireq.global_options,
hash_options=ireq.hash_options,
config_settings=ireq.config_settings,
user_supplied=ireq.user_supplied,
)

View File

@@ -2,13 +2,24 @@
Requirements file parsing
"""
import logging
import optparse
import os
import re
import shlex
import urllib.parse
from optparse import Values
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Optional, Tuple
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
Generator,
Iterable,
List,
Optional,
Tuple,
)
from pip._internal.cli import cmdoptions
from pip._internal.exceptions import InstallationError, RequirementsFileParseError
@@ -25,20 +36,20 @@ if TYPE_CHECKING:
from pip._internal.index.package_finder import PackageFinder
__all__ = ['parse_requirements']
__all__ = ["parse_requirements"]
ReqFileLines = Iterator[Tuple[int, str]]
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]] = [
cmdoptions.index_url,
@@ -59,14 +70,16 @@ SUPPORTED_OPTIONS: List[Callable[..., optparse.Option]] = [
# options to be passed to requirements
SUPPORTED_OPTIONS_REQ: List[Callable[..., optparse.Option]] = [
cmdoptions.install_options,
cmdoptions.global_options,
cmdoptions.hash,
cmdoptions.config_settings,
]
# the 'dest' string values
SUPPORTED_OPTIONS_REQ_DEST = [str(o().dest) for o in SUPPORTED_OPTIONS_REQ]
logger = logging.getLogger(__name__)
class ParsedRequirement:
def __init__(
@@ -119,7 +132,7 @@ def parse_requirements(
finder: Optional["PackageFinder"] = None,
options: Optional[optparse.Values] = None,
constraint: bool = False,
) -> Iterator[ParsedRequirement]:
) -> Generator[ParsedRequirement, None, None]:
"""Parse a requirements file and yield ParsedRequirement instances.
:param filename: Path or url of requirements file.
@@ -134,10 +147,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
@@ -159,10 +169,11 @@ def handle_requirement_line(
line: ParsedLine,
options: Optional[optparse.Values] = None,
) -> ParsedRequirement:
# preserve for the nested code path
line_comes_from = '{} {} (line {})'.format(
'-c' if line.constraint else '-r', line.filename, line.lineno,
line_comes_from = "{} {} (line {})".format(
"-c" if line.constraint else "-r",
line.filename,
line.lineno,
)
assert line.is_requirement
@@ -177,17 +188,13 @@ def handle_requirement_line(
constraint=line.constraint,
)
else:
if options:
# Disable wheels if the user has specified build options
cmdoptions.check_install_build_global(options, line.opts)
# get the options that apply to requirements
req_options = {}
for dest in SUPPORTED_OPTIONS_REQ_DEST:
if dest in line.opts.__dict__ and line.opts.__dict__[dest]:
req_options[dest] = line.opts.__dict__[dest]
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,
@@ -206,6 +213,12 @@ def handle_option_line(
options: Optional[optparse.Values] = None,
session: Optional[PipSession] = None,
) -> None:
if opts.hashes:
logger.warning(
"%s line %s has --hash but no requirement, and will be ignored.",
filename,
lineno,
)
if options:
# percolate options upward
@@ -213,19 +226,20 @@ def handle_option_line(
options.require_hashes = opts.require_hashes
if opts.features_enabled:
options.features_enabled.extend(
f for f in opts.features_enabled
if f not in options.features_enabled
f for f in opts.features_enabled if f not in options.features_enabled
)
# set finder options
if finder:
find_links = finder.find_links
index_urls = finder.index_urls
if opts.index_url:
index_urls = [opts.index_url]
no_index = finder.search_scope.no_index
if opts.no_index is True:
no_index = True
index_urls = []
if opts.extra_index_urls:
if opts.index_url and not no_index:
index_urls = [opts.index_url]
if opts.extra_index_urls and not no_index:
index_urls.extend(opts.extra_index_urls)
if opts.find_links:
# FIXME: it would be nice to keep track of the source
@@ -245,6 +259,7 @@ def handle_option_line(
search_scope = SearchScope(
find_links=find_links,
index_urls=index_urls,
no_index=no_index,
)
finder.search_scope = search_scope
@@ -256,7 +271,7 @@ 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)
@@ -313,18 +328,18 @@ class RequirementsFileParser:
self._session = session
self._line_parser = line_parser
def parse(self, filename: str, constraint: bool) -> Iterator[ParsedLine]:
"""Parse a given file, yielding parsed lines.
"""
def parse(
self, filename: str, constraint: bool
) -> Generator[ParsedLine, None, None]:
"""Parse a given file, yielding parsed lines."""
yield from self._parse_and_recurse(filename, constraint)
def _parse_and_recurse(
self, filename: str, constraint: bool
) -> Iterator[ParsedLine]:
) -> Generator[ParsedLine, None, None]:
for line in self._parse_file(filename, constraint):
if (
not line.is_requirement and
(line.opts.requirements or line.opts.constraints)
if not line.is_requirement and (
line.opts.requirements or line.opts.constraints
):
# parse a nested requirements file
if line.opts.requirements:
@@ -342,14 +357,17 @@ class RequirementsFileParser:
elif not SCHEME_RE.search(req_path):
# do a join so relative paths work
req_path = os.path.join(
os.path.dirname(filename), req_path,
os.path.dirname(filename),
req_path,
)
yield from self._parse_and_recurse(req_path, nested_constraint)
else:
yield line
def _parse_file(self, filename: str, constraint: bool) -> Iterator[ParsedLine]:
def _parse_file(
self, filename: str, constraint: bool
) -> Generator[ParsedLine, None, None]:
_, content = get_file_content(filename, self._session)
lines_enum = preprocess(content)
@@ -359,7 +377,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(
@@ -383,7 +401,12 @@ def get_line_parser(finder: Optional["PackageFinder"]) -> LineParser:
args_str, options_str = break_args_options(line)
opts, _ = parser.parse_args(shlex.split(options_str), defaults)
try:
options = shlex.split(options_str)
except ValueError as e:
raise OptionParsingError(f"Could not split options: {options_str}") from e
opts, _ = parser.parse_args(options, defaults)
return args_str, opts
@@ -395,16 +418,16 @@ def break_args_options(line: str) -> Tuple[str, str]:
(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):
@@ -427,6 +450,7 @@ def build_parser() -> optparse.OptionParser:
# that in our own exception.
def parser_exit(self: Any, msg: str) -> "NoReturn":
raise OptionParsingError(msg)
# NOTE: mypy disallows assigning to a method
# https://github.com/python/mypy/issues/2427
parser.exit = parser_exit # type: ignore
@@ -441,26 +465,26 @@ def join_lines(lines_enum: ReqFileLines) -> ReqFileLines:
primary_line_number = None
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 '\'.
@@ -470,7 +494,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
@@ -514,15 +538,15 @@ def get_file_content(url: str, session: PipSession) -> Tuple[str, str]:
scheme = get_url_scheme(url)
# Pip has special support for file:// URLs (LocalFSAdapter).
if scheme in ['http', 'https', 'file']:
if scheme in ["http", "https", "file"]:
resp = session.get(url)
raise_for_status(resp)
return resp.url, resp.text
# Assume this is a bare path.
try:
with open(url, 'rb') as f:
with open(url, "rb") as f:
content = auto_decode(f.read())
except OSError as exc:
raise InstallationError(f'Could not open requirements file: {exc}')
raise InstallationError(f"Could not open requirements file: {exc}")
return url, content

View File

@@ -1,55 +1,59 @@
# The following comment should be removed at some point in the future.
# mypy: strict-optional=False
import functools
import logging
import os
import shutil
import sys
import uuid
import zipfile
from typing import Any, Dict, Iterable, List, Optional, Sequence, Union
from optparse import Values
from typing import Any, Collection, Dict, Iterable, List, Optional, Sequence, Union
from pip._vendor import pkg_resources, six
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._vendor.pkg_resources import Distribution
from pip._vendor.pyproject_hooks import BuildBackendHookCaller
from pip._internal.build_env import BuildEnvironment, NoOpBuildEnvironment
from pip._internal.exceptions import InstallationError
from pip._internal.locations import get_scheme
from pip._internal.metadata import (
BaseDistribution,
get_default_environment,
get_directory_distribution,
get_wheel_distribution,
)
from pip._internal.metadata.base import FilesystemWheel
from pip._internal.models.direct_url import DirectUrl
from pip._internal.models.link import Link
from pip._internal.operations.build.metadata import generate_metadata
from pip._internal.operations.build.metadata_editable import generate_editable_metadata
from pip._internal.operations.build.metadata_legacy import (
generate_metadata as generate_metadata_legacy,
)
from pip._internal.operations.install.editable_legacy import (
install_editable as install_editable_legacy,
)
from pip._internal.operations.install.legacy import LegacyInstallFailure
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.req.req_uninstall import UninstallPathSet
from pip._internal.utils.deprecation import deprecated
from pip._internal.utils.direct_url_helpers import direct_url_from_link
from pip._internal.utils.hashes import Hashes
from pip._internal.utils.logging import indent_log
from pip._internal.utils.misc import (
ConfiguredBuildBackendHookCaller,
ask_path_exists,
backup_dir,
display_path,
dist_in_site_packages,
dist_in_usersite,
get_distribution,
hide_url,
redact_auth_from_url,
)
from pip._internal.utils.packaging import get_metadata
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.virtualenv import running_under_virtualenv
from pip._internal.vcs import vcs
@@ -57,32 +61,6 @@ from pip._internal.vcs import vcs
logger = logging.getLogger(__name__)
def _get_dist(metadata_directory: str) -> Distribution:
"""Return a pkg_resources.Distribution for the provided
metadata directory.
"""
dist_dir = metadata_directory.rstrip(os.sep)
# Build a PathMetadata object, from path to metadata. :wink:
base_dir, dist_dir_name = os.path.split(dist_dir)
metadata = pkg_resources.PathMetadata(base_dir, dist_dir)
# Determine the correct Distribution object type.
if dist_dir.endswith(".egg-info"):
dist_cls = pkg_resources.Distribution
dist_name = os.path.splitext(dist_dir_name)[0]
else:
assert dist_dir.endswith(".dist-info")
dist_cls = pkg_resources.DistInfoDistribution
dist_name = os.path.splitext(dist_dir_name)[0].split("-")[0]
return dist_cls(
base_dir,
project_name=dist_name,
metadata=metadata,
)
class InstallRequirement:
"""
Represents something that may be installed later on, may have information
@@ -99,19 +77,21 @@ class InstallRequirement:
markers: Optional[Marker] = None,
use_pep517: Optional[bool] = None,
isolated: bool = False,
install_options: Optional[List[str]] = None,
*,
global_options: Optional[List[str]] = None,
hash_options: Optional[Dict[str, List[str]]] = None,
config_settings: Optional[Dict[str, Union[str, List[str]]]] = None,
constraint: bool = False,
extras: Iterable[str] = (),
extras: Collection[str] = (),
user_supplied: bool = False,
permit_editable_wheels: bool = False,
) -> None:
assert req is None or isinstance(req, Requirement), req
self.req = req
self.comes_from = comes_from
self.constraint = constraint
self.editable = editable
self.legacy_install_reason: Optional[int] = None
self.permit_editable_wheels = permit_editable_wheels
# source_dir is the local directory where the linked requirement is
# located, or unpacked. In case unpacking is needed, creating and
@@ -122,15 +102,21 @@ class InstallRequirement:
if self.editable:
assert link
if link.is_file:
self.source_dir = os.path.normpath(
os.path.abspath(link.file_path)
)
self.source_dir = os.path.normpath(os.path.abspath(link.file_path))
if link is None and req and req.url:
# PEP 508 URL requirement
link = Link(req.url)
self.link = self.original_link = link
self.original_link_is_in_wheel_cache = False
# When this InstallRequirement is a wheel obtained from the cache of locally
# built wheels, this is the source link corresponding to the cache entry, which
# was used to download and build the cached wheel.
self.cached_wheel_source_link: Optional[Link] = None
# Information about the location of the artifact that was downloaded . This
# property is guaranteed to be set in resolver results.
self.download_info: Optional[DirectUrl] = None
# Path to any downloaded or already-existing package.
self.local_file_path: Optional[str] = None
@@ -140,18 +126,15 @@ class InstallRequirement:
if extras:
self.extras = extras
elif req:
self.extras = {
pkg_resources.safe_extra(extra) for extra in req.extras
}
self.extras = {safe_extra(extra) for extra in req.extras}
else:
self.extras = set()
if markers is None and req:
markers = req.marker
self.markers = markers
# This holds the pkg_resources.Distribution object if this requirement
# is already available:
self.satisfied_by: Optional[Distribution] = None
# This holds the Distribution object if this requirement is already installed.
self.satisfied_by: Optional[BaseDistribution] = None
# Whether the installation process should try to uninstall an existing
# distribution before installing this requirement.
self.should_reinstall = False
@@ -160,9 +143,9 @@ class InstallRequirement:
# Set to True after successful installation
self.install_succeeded: Optional[bool] = None
# Supplied options
self.install_options = install_options if install_options else []
self.global_options = global_options if global_options else []
self.hash_options = hash_options if hash_options else {}
self.config_settings = config_settings
# Set to True after successful preparation of this requirement
self.prepared = False
# User supplied requirement are explicitly requested for installation
@@ -186,7 +169,7 @@ class InstallRequirement:
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: Optional[BuildBackendHookCaller] = None
# Are we using PEP 517 for this requirement?
# After pyproject.toml has been loaded, the only valid values are True
@@ -202,36 +185,38 @@ class InstallRequirement:
if self.req:
s = str(self.req)
if self.link:
s += ' from {}'.format(redact_auth_from_url(self.link.url))
s += " from {}".format(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))
if self.satisfied_by.location is not None:
location = display_path(self.satisfied_by.location)
else:
location = "<memory>"
s += f" in {location}"
if self.comes_from:
if isinstance(self.comes_from, str):
comes_from: Optional[str] = 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:
"""An un-tested helper for getting state, for debugging.
"""
"""An un-tested helper for getting state, for debugging."""
attributes = vars(self)
names = sorted(attributes)
state = (
"{}={!r}".format(attr, attributes[attr]) for attr in sorted(names)
)
return '<{name} object: {{{state}}}>'.format(
state = ("{}={!r}".format(attr, attributes[attr]) for attr in sorted(names))
return "<{name} object: {{{state}}}>".format(
name=self.__class__.__name__,
state=", ".join(state),
)
@@ -241,7 +226,19 @@ class InstallRequirement:
def name(self) -> Optional[str]:
if self.req is None:
return None
return pkg_resources.safe_name(self.req.name)
return self.req.name
@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"
)
with self.pep517_backend.subprocess_runner(runner):
return "build_editable" in self.pep517_backend._supported_features()
@property
def specifier(self) -> SpecifierSet:
@@ -254,18 +251,17 @@ 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:
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
@@ -295,14 +291,18 @@ class InstallRequirement:
"""
good_hashes = self.hash_options.copy()
link = self.link if trust_internet else self.original_link
if trust_internet:
link = self.link
elif self.original_link and self.user_supplied:
link = self.original_link
else:
link = None
if link and link.hash:
good_hashes.setdefault(link.hash_name, []).append(link.hash)
return Hashes(good_hashes)
def from_path(self) -> Optional[str]:
"""Format a nice indicator to show where this "comes from"
"""
"""Format a nice indicator to show where this "comes from" """
if self.req is None:
return None
s = str(self.req)
@@ -312,7 +312,7 @@ 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(
@@ -345,7 +345,7 @@ class InstallRequirement:
# 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
@@ -359,8 +359,7 @@ class InstallRequirement:
).path
def _set_requirement(self) -> None:
"""Set requirement after generating metadata.
"""
"""Set requirement after generating metadata."""
assert self.req is None
assert self.metadata is not None
assert self.source_dir is not None
@@ -372,11 +371,13 @@ class InstallRequirement:
op = "==="
self.req = Requirement(
"".join([
self.metadata["Name"],
op,
self.metadata["Version"],
])
"".join(
[
self.metadata["Name"],
op,
self.metadata["Version"],
]
)
)
def warn_on_mismatching_name(self) -> None:
@@ -387,10 +388,12 @@ class InstallRequirement:
# 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.',
self.name, metadata_name, self.name
"Generating metadata for package %s "
"produced metadata for project name %s. Fix your "
"#egg=%s fragments.",
self.name,
metadata_name,
self.name,
)
self.req = Requirement(metadata_name)
@@ -401,30 +404,24 @@ class InstallRequirement:
"""
if self.req is None:
return
existing_dist = get_distribution(self.req.name)
existing_dist = get_default_environment().get_distribution(self.req.name)
if not existing_dist:
return
# pkg_resouces may contain a different copy of packaging.version from
# pip in if the downstream distributor does a poor job debundling pip.
# We avoid existing_dist.parsed_version and let SpecifierSet.contains
# parses the version instead.
existing_version = existing_dist.version
version_compatible = (
existing_version is not None and
self.req.specifier.contains(existing_version, prereleases=True)
version_compatible = self.req.specifier.contains(
existing_dist.version,
prereleases=True,
)
if not version_compatible:
self.satisfied_by = None
if use_user_site:
if dist_in_usersite(existing_dist):
if existing_dist.in_usersite:
self.should_reinstall = True
elif (running_under_virtualenv() and
dist_in_site_packages(existing_dist)):
elif running_under_virtualenv() and existing_dist.in_site_packages:
raise InstallationError(
"Will not install to the user site because it will "
"lack sys.path precedence to {} in {}".format(
existing_dist.project_name, 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
@@ -444,20 +441,33 @@ class InstallRequirement:
return False
return self.link.is_wheel
@property
def is_wheel_from_cache(self) -> bool:
# When True, it means that this InstallRequirement is a local wheel file in the
# cache of locally built wheels.
return self.cached_wheel_source_link is not None
# Things valid for sdists
@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')
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")
return setup_cfg
@property
def pyproject_toml_path(self) -> str:
assert self.source_dir, f"No source dir for {self}"
@@ -472,13 +482,19 @@ 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:
if self.config_settings:
deprecated(
reason=f"Config settings are ignored for project {self}.",
replacement=(
"to use --use-pep517 or add a "
"pyproject.toml file to the project"
),
gone_in="23.3",
)
self.use_pep517 = False
return
@@ -486,46 +502,69 @@ class InstallRequirement:
requires, backend, check, backend_path = pyproject_toml_data
self.requirements_to_check = check
self.pyproject_requires = requires
self.pep517_backend = Pep517HookCaller(
self.unpacked_source_directory, backend, backend_path=backend_path,
self.pep517_backend = ConfiguredBuildBackendHookCaller(
self,
self.unpacked_source_directory,
backend,
backend_path=backend_path,
)
def _generate_metadata(self) -> str:
"""Invokes metadata generator functions, with the required arguments.
def isolated_editable_sanity_check(self) -> None:
"""Check that an editable requirement if valid for use with PEP 517/518.
This verifies that an editable that has a pyproject.toml either supports PEP 660
or as a setup.py or a setup.cfg
"""
if not self.use_pep517:
assert self.unpacked_source_directory
if not os.path.exists(self.setup_py_path):
raise InstallationError(
f'File "setup.py" not found for legacy project {self}.'
)
return generate_metadata_legacy(
build_env=self.build_env,
setup_py_path=self.setup_py_path,
source_dir=self.unpacked_source_directory,
isolated=self.isolated,
details=self.name or f"from {self.link}"
if (
self.editable
and self.use_pep517
and not self.supports_pyproject_editable()
and not os.path.isfile(self.setup_py_path)
and not os.path.isfile(self.setup_cfg_path)
):
raise InstallationError(
f"Project {self} 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."
)
assert self.pep517_backend is not None
return generate_metadata(
build_env=self.build_env,
backend=self.pep517_backend,
)
def prepare_metadata(self) -> None:
"""Ensure that project metadata is available.
Under PEP 517, call the backend hook to prepare the metadata.
Under PEP 517 and PEP 660, call the backend hook to prepare the metadata.
Under legacy processing, call setup.py egg-info.
"""
assert self.source_dir
details = self.name or f"from {self.link}"
with indent_log():
self.metadata_directory = self._generate_metadata()
if self.use_pep517:
assert self.pep517_backend is not None
if (
self.editable
and self.permit_editable_wheels
and self.supports_pyproject_editable()
):
self.metadata_directory = generate_editable_metadata(
build_env=self.build_env,
backend=self.pep517_backend,
details=details,
)
else:
self.metadata_directory = generate_metadata(
build_env=self.build_env,
backend=self.pep517_backend,
details=details,
)
else:
self.metadata_directory = generate_metadata_legacy(
build_env=self.build_env,
setup_py_path=self.setup_py_path,
source_dir=self.unpacked_source_directory,
isolated=self.isolated,
details=details,
)
# Act on the newly generated metadata, based on the name and version.
if not self.name:
@@ -537,26 +576,35 @@ class InstallRequirement:
@property
def metadata(self) -> Any:
if not hasattr(self, '_metadata'):
self._metadata = get_metadata(self.get_dist())
if not hasattr(self, "_metadata"):
self._metadata = self.get_dist().metadata
return self._metadata
def get_dist(self) -> Distribution:
return _get_dist(self.metadata_directory)
def get_dist(self) -> BaseDistribution:
if self.metadata_directory:
return get_directory_distribution(self.metadata_directory)
elif self.local_file_path and self.is_wheel:
return get_wheel_distribution(
FilesystemWheel(self.local_file_path), canonicalize_name(self.name)
)
raise AssertionError(
f"InstallRequirement {self} has no metadata directory and no wheel: "
f"can't make a distribution."
)
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,
@@ -589,14 +637,13 @@ 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)
@@ -604,7 +651,7 @@ class InstallRequirement:
# 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}"
hidden_url = hide_url(self.link.url)
vcs_backend.obtain(self.source_dir, url=hidden_url)
vcs_backend.obtain(self.source_dir, url=hidden_url, verbosity=0)
# Top-level Actions
def uninstall(
@@ -623,29 +670,28 @@ class InstallRequirement:
"""
assert self.req
dist = get_distribution(self.req.name)
dist = get_default_environment().get_distribution(self.req.name)
if not dist:
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)
return uninstalled_pathset
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), (
f"name {name!r} doesn't start with prefix {prefix!r}"
)
name = name[len(prefix) + 1:]
name = name.replace(os.path.sep, '/')
assert name.startswith(
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, "/")
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:
"""Saves archive to provided build_dir.
@@ -657,68 +703,72 @@ 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'))
if response == 'i':
"The file {} exists. (i)gnore, (w)ipe, "
"(b)ackup, (a)bort ".format(display_path(archive_path)),
("i", "w", "b", "a"),
)
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:
return
zip_output = zipfile.ZipFile(
archive_path, 'w', zipfile.ZIP_DEFLATED, allowZip64=True,
archive_path,
"w",
zipfile.ZIP_DEFLATED,
allowZip64=True,
)
with zip_output:
dir = os.path.normcase(
os.path.abspath(self.unpacked_source_directory)
)
dir = os.path.normcase(os.path.abspath(self.unpacked_source_directory))
for dirpath, dirnames, filenames in os.walk(dir):
for dirname in dirnames:
dir_arcname = self._get_archive_name(
dirname, parentdir=dirpath, rootdir=dir,
dirname,
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, parentdir=dirpath, rootdir=dir,
filename,
parentdir=dirpath,
rootdir=dir,
)
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,
warn_script_location: bool = True,
use_user_site: bool = False,
pycompile: bool = True
pycompile: bool = True,
) -> None:
scheme = get_scheme(
self.name,
@@ -729,11 +779,9 @@ class InstallRequirement:
prefix=prefix,
)
global_options = global_options if global_options is not None else []
if self.editable:
if self.editable and not self.is_wheel:
install_editable_legacy(
install_options,
global_options,
global_options=global_options if global_options is not None else [],
prefix=prefix,
home=home,
use_user_site=use_user_site,
@@ -746,79 +794,23 @@ class InstallRequirement:
self.install_succeeded = True
return
if self.is_wheel:
assert self.local_file_path
direct_url = None
if self.original_link:
direct_url = direct_url_from_link(
self.original_link,
self.source_dir,
self.original_link_is_in_wheel_cache,
)
install_wheel(
self.name,
self.local_file_path,
scheme=scheme,
req_description=str(self.req),
pycompile=pycompile,
warn_script_location=warn_script_location,
direct_url=direct_url,
requested=self.user_supplied,
)
self.install_succeeded = True
return
assert self.is_wheel
assert self.local_file_path
# TODO: Why don't we do this for editable installs?
# Extend the list of global and install options passed on to
# the setup.py call with the ones from the requirements file.
# Options specified in requirements file override those
# specified on the command line, since the last option given
# to setup.py is the one that is used.
global_options = list(global_options) + self.global_options
install_options = list(install_options) + self.install_options
try:
success = install_legacy(
install_options=install_options,
global_options=global_options,
root=root,
home=home,
prefix=prefix,
use_user_site=use_user_site,
pycompile=pycompile,
scheme=scheme,
setup_py_path=self.setup_py_path,
isolated=self.isolated,
req_name=self.name,
build_env=self.build_env,
unpacked_source_directory=self.unpacked_source_directory,
req_description=str(self.req),
)
except LegacyInstallFailure as exc:
self.install_succeeded = False
six.reraise(*exc.parent)
except Exception:
self.install_succeeded = True
raise
self.install_succeeded = success
if success and self.legacy_install_reason == 8368:
deprecated(
reason=(
"{} was installed using the legacy 'setup.py install' "
"method, because a wheel could not be built for it.".
format(self.name)
),
replacement="to fix the wheel build issue reported above",
gone_in=None,
issue=8368,
)
install_wheel(
self.name,
self.local_file_path,
scheme=scheme,
req_description=str(self.req),
pycompile=pycompile,
warn_script_location=warn_script_location,
direct_url=self.download_info if self.original_link else None,
requested=self.user_supplied,
)
self.install_succeeded = True
def check_invalid_constraint_type(req: InstallRequirement) -> str:
# Check for unsupported forms
problem = ""
if not req.name:
@@ -844,3 +836,32 @@ def check_invalid_constraint_type(req: InstallRequirement) -> str:
)
return problem
def _has_option(options: Values, reqs: List[InstallRequirement], option: str) -> bool:
if getattr(options, option, None):
return True
for req in reqs:
if getattr(req, option, None):
return True
return False
def check_legacy_setup_py_options(
options: Values,
reqs: List[InstallRequirement],
) -> None:
has_build_options = _has_option(options, reqs, "build_options")
has_global_options = _has_option(options, reqs, "global_options")
if has_build_options or has_global_options:
deprecated(
reason="--build-option and --global-option are deprecated.",
issue=11859,
replacement="to use --config-settings",
gone_in="23.3",
)
logger.warning(
"Implying --no-binary=:all: due to the presence of "
"--build-option / --global-option. "
)
options.format_control.disallow_binaries()

View File

@@ -1,22 +1,17 @@
import logging
from collections import OrderedDict
from typing import Dict, Iterable, List, Optional, Tuple
from typing import Dict, List
from pip._vendor.packaging.utils import canonicalize_name
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
logger = logging.getLogger(__name__)
class RequirementSet:
def __init__(self, check_supported_wheels: bool = True) -> None:
"""Create a RequirementSet.
"""
"""Create a RequirementSet."""
self.requirements: Dict[str, InstallRequirement] = OrderedDict()
self.check_supported_wheels = check_supported_wheels
@@ -28,7 +23,7 @@ class RequirementSet:
(req for req in self.requirements.values() if not req.comes_from),
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(
@@ -36,11 +31,11 @@ class RequirementSet:
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:
@@ -53,128 +48,12 @@ class RequirementSet:
project_name = canonicalize_name(install_req.name)
self.requirements[project_name] = install_req
def add_requirement(
self,
install_req: InstallRequirement,
parent_req_name: Optional[str] = None,
extras_requested: Optional[Iterable[str]] = None
) -> Tuple[List[InstallRequirement], Optional[InstallRequirement]]:
"""Add install_req as a requirement to install.
:param parent_req_name: The name of the requirement that needed this
added. The name is used because when multiple unnamed requirements
resolve to the same name, we could otherwise end up with dependency
links that point outside the Requirements set. parent_req must
already be added. Note that None implies that this is a user
supplied requirement, vs an inferred one.
:param extras_requested: an iterable of extras used to evaluate the
environment markers.
:return: Additional requirements to scan. That is either [] if
the requirement is not applicable, or [install_req] if the
requirement is applicable and has just been added.
"""
# If the markers do not match, ignore this requirement.
if not install_req.match_markers(extras_requested):
logger.info(
"Ignoring %s: markers '%s' don't match your environment",
install_req.name, install_req.markers,
)
return [], None
# If the wheel is not supported, raise an error.
# Should check this after filtering out based on environment markers to
# allow specifying different wheels based on the environment/OS, in a
# single requirements file.
if install_req.link and install_req.link.is_wheel:
wheel = Wheel(install_req.link.filename)
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)
)
# This next bit is really a sanity check.
assert not install_req.user_supplied or parent_req_name is None, (
"a user supplied req shouldn't have a parent"
)
# Unnamed requirements are scanned again and the requirement won't be
# added as a dependency until after scanning.
if not install_req.name:
self.add_unnamed_requirement(install_req)
return [install_req], None
try:
existing_req: Optional[InstallRequirement] = 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
)
if has_conflicting_requirement:
raise InstallationError(
"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
# dependency and it will be scanned again after.
if not existing_req:
self.add_named_requirement(install_req)
# We'd want to rescan this requirement later
return [install_req], install_req
# Assume there's no need to scan, and that we've already
# encountered this for scanning.
if install_req.constraint or not existing_req.constraint:
return [], existing_req
does_not_satisfy_constraint = (
install_req.link and
not (
existing_req.link and
install_req.link.path == existing_req.link.path
)
)
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)
)
# If we're now installing a constraint, mark the existing
# object for real installation.
existing_req.constraint = False
# If we're now installing a user supplied requirement,
# mark the existing object as such.
if install_req.user_supplied:
existing_req.user_supplied = True
existing_req.extras = tuple(sorted(
set(existing_req.extras) | set(install_req.extras)
))
logger.debug(
"Setting %s extras to: %s",
existing_req, existing_req.extras,
)
# Return the existing requirement for addition to the parent and
# scanning again.
return [existing_req], existing_req
def has_requirement(self, name: str) -> bool:
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:
@@ -188,3 +67,16 @@ class RequirementSet:
@property
def all_requirements(self) -> List[InstallRequirement]:
return self.unnamed_requirements + list(self.requirements.values())
@property
def requirements_to_install(self) -> List[InstallRequirement]:
"""Return the list of requirements that need to be installed.
TODO remove this property together with the legacy resolver, since the new
resolver only returns requirements that need to be installed.
"""
return [
install_req
for install_req in self.all_requirements
if not install_req.constraint and not install_req.satisfied_by
]

View File

@@ -1,130 +0,0 @@
import contextlib
import hashlib
import logging
import os
from types import TracebackType
from typing import Dict, Iterator, Optional, Set, Type, Union
from pip._internal.models.link import Link
from pip._internal.req.req_install import InstallRequirement
from pip._internal.utils.temp_dir import TempDirectory
logger = logging.getLogger(__name__)
@contextlib.contextmanager
def update_env_context_manager(**changes: str) -> Iterator[None]:
target = os.environ
# Save values from the target and change them.
non_existent_marker = object()
saved_values: Dict[str, Union[object, str]] = {}
for name, new_value in changes.items():
try:
saved_values[name] = target[name]
except KeyError:
saved_values[name] = non_existent_marker
target[name] = new_value
try:
yield
finally:
# Restore original values in the target.
for name, original_value in saved_values.items():
if original_value is non_existent_marker:
del target[name]
else:
assert isinstance(original_value, str) # for mypy
target[name] = original_value
@contextlib.contextmanager
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
ctx.enter_context(update_env_context_manager(PIP_REQ_TRACKER=root))
logger.debug("Initialized build tracking at %s", root)
with RequirementTracker(root) as tracker:
yield tracker
class RequirementTracker:
def __init__(self, root: str) -> None:
self._root = 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)
return self
def __exit__(
self,
exc_type: Optional[Type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[TracebackType]
) -> None:
self.cleanup()
def _entry_path(self, link: Link) -> str:
hashed = hashlib.sha224(link.url_without_fragment.encode()).hexdigest()
return os.path.join(self._root, hashed)
def add(self, req: InstallRequirement) -> None:
"""Add an InstallRequirement to build tracking.
"""
assert req.link
# Get the file to write information about this requirement.
entry_path = self._entry_path(req.link)
# Try reading from the file. If it exists and can be read from, a build
# is already in progress, so a LookupError is raised.
try:
with open(entry_path) as fp:
contents = fp.read()
except FileNotFoundError:
pass
else:
message = '{} is already being built: {}'.format(
req.link, 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:
fp.write(str(req))
self._entries.add(req)
logger.debug('Added %s to build tracker %r', req, self._root)
def remove(self, req: InstallRequirement) -> None:
"""Remove an InstallRequirement from build tracking.
"""
assert req.link
# Delete the created file and the corresponding entries.
os.unlink(self._entry_path(req.link))
self._entries.remove(req)
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)
@contextlib.contextmanager
def track(self, req: InstallRequirement) -> Iterator[None]:
self.add(req)
yield
self.remove(req)

View File

@@ -1,67 +1,58 @@
import csv
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 pip._vendor import pkg_resources
from pip._vendor.pkg_resources import Distribution
from typing import Any, Callable, Dict, Generator, Iterable, List, Optional, Set, Tuple
from pip._internal.exceptions import UninstallationError
from pip._internal.locations import get_bin_prefix, get_bin_user
from pip._internal.metadata import BaseDistribution
from pip._internal.utils.compat import WINDOWS
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,
dist_in_usersite,
dist_is_local,
egg_link_path,
is_local,
normalize_path,
renames,
rmtree,
)
from pip._internal.utils.misc import ask, normalize_path, renames, rmtree
from pip._internal.utils.temp_dir import AdjacentTempDirectory, TempDirectory
from pip._internal.utils.virtualenv import running_under_virtualenv
logger = getLogger(__name__)
def _script_names(dist: Distribution, script_name: str, is_gui: bool) -> List[str]:
def _script_names(
bin_dir: str, script_name: str, is_gui: bool
) -> Generator[str, None, None]:
"""Create the fully qualified name of the files created by
{console,gui}_scripts for the given ``dist``.
Returns the list of file names
"""
if dist_in_usersite(dist):
bin_dir = get_bin_user()
else:
bin_dir = get_bin_prefix()
exe_name = os.path.join(bin_dir, script_name)
paths_to_remove = [exe_name]
if WINDOWS:
paths_to_remove.append(exe_name + '.exe')
paths_to_remove.append(exe_name + '.exe.manifest')
if is_gui:
paths_to_remove.append(exe_name + '-script.pyw')
else:
paths_to_remove.append(exe_name + '-script.py')
return paths_to_remove
yield exe_name
if not WINDOWS:
return
yield f"{exe_name}.exe"
yield f"{exe_name}.exe.manifest"
if is_gui:
yield f"{exe_name}-script.pyw"
else:
yield f"{exe_name}-script.py"
def _unique(fn: Callable[..., Iterator[Any]]) -> Callable[..., Iterator[Any]]:
def _unique(
fn: Callable[..., Generator[Any, None, None]]
) -> Callable[..., Generator[Any, None, None]]:
@functools.wraps(fn)
def unique(*args: Any, **kw: Any) -> Iterator[Any]:
def unique(*args: Any, **kw: Any) -> Generator[Any, None, None]:
seen: Set[Any] = set()
for item in fn(*args, **kw):
if item not in seen:
seen.add(item)
yield item
return unique
@_unique
def uninstallation_paths(dist: Distribution) -> Iterator[str]:
def uninstallation_paths(dist: BaseDistribution) -> Generator[str, None, None]:
"""
Yield all the uninstallation paths for dist based on RECORD-without-.py[co]
@@ -75,30 +66,32 @@ def uninstallation_paths(dist: Distribution) -> Iterator[str]:
https://packaging.python.org/specifications/recording-installed-packages/
"""
try:
r = csv.reader(dist.get_metadata_lines('RECORD'))
except FileNotFoundError as missing_record_exception:
msg = 'Cannot uninstall {dist}, RECORD file not found.'.format(dist=dist)
try:
installer = next(dist.get_metadata_lines('INSTALLER'))
if not installer or installer == 'pip':
raise ValueError()
except (OSError, StopIteration, ValueError):
dep = '{}=={}'.format(dist.project_name, dist.version)
msg += (" You might be able to recover from this via: "
"'pip install --force-reinstall --no-deps {}'.".format(dep))
location = dist.location
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)
installer = dist.installer
if not installer or installer == "pip":
dep = "{}=={}".format(dist.raw_name, dist.version)
msg += (
" You might be able to recover from this via: "
"'pip install --force-reinstall --no-deps {}'.".format(dep)
)
else:
msg += ' Hint: The package was installed by {}.'.format(installer)
raise UninstallationError(msg) from missing_record_exception
for row in r:
path = os.path.join(dist.location, row[0])
msg += " Hint: The package was installed by {}.".format(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
@@ -112,8 +105,8 @@ def compact(paths: Iterable[str]) -> Set[str]:
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:
@@ -136,18 +129,15 @@ def compress_for_rename(paths: Iterable[str]) -> Set[str]:
return os.path.normcase(os.path.join(*a))
for root in unchecked:
if any(os.path.normcase(root).startswith(w)
for w in wildcards):
if any(os.path.normcase(root).startswith(w) for w in wildcards):
# This directory has already been handled.
continue
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)
all_subdirs.update(norm_join(root, dirname, d) for d in subdirs)
all_files.update(norm_join(root, dirname, f) for f in files)
# If all the files we found are in our remaining set of files to
# remove, then remove them from the latter set and add a wildcard
# for the directory.
@@ -196,14 +186,14 @@ def compress_for_output_listing(paths: Iterable[str]) -> Tuple[Set[str], Set[str
continue
file_ = os.path.join(dirpath, fname)
if (os.path.isfile(file_) and
os.path.normcase(file_) not in _normcased_files):
if (
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
@@ -211,6 +201,7 @@ def compress_for_output_listing(paths: Iterable[str]) -> Tuple[Set[str], Set[str
class StashedUninstallPathSet:
"""A set of file rename operations to stash files while
tentatively uninstalling them."""
def __init__(self) -> None:
# Mapping from source file root to [Adjacent]TempDirectory
# for files under that directory.
@@ -252,7 +243,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)
@@ -271,7 +262,7 @@ class StashedUninstallPathSet:
new_path = self._get_file_stash(path)
self._moves.append((path, new_path))
if (path_is_dir and os.path.isdir(new_path)):
if path_is_dir and os.path.isdir(new_path):
# If we're moving a directory, we need to
# remove the destination first or else it will be
# moved to inside the existing directory.
@@ -295,7 +286,7 @@ class StashedUninstallPathSet:
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):
@@ -315,12 +306,17 @@ class StashedUninstallPathSet:
class UninstallPathSet:
"""A set of file paths to be removed in the uninstallation of a
requirement."""
def __init__(self, dist: Distribution) -> None:
self.paths: Set[str] = set()
def __init__(self, dist: BaseDistribution) -> None:
self._paths: Set[str] = set()
self._refuse: Set[str] = set()
self.pth: Dict[str, UninstallPthEntries] = {}
self.dist = dist
self._pth: Dict[str, UninstallPthEntries] = {}
self._dist = dist
self._moved_paths = StashedUninstallPathSet()
# Create local cache of normalize_path results. Creating an UninstallPathSet
# can result in hundreds/thousands of redundant calls to normalize_path with
# the same args, which hurts performance.
self._normalize_path_cached = functools.lru_cache()(normalize_path)
def _permitted(self, path: str) -> bool:
"""
@@ -328,70 +324,70 @@ class UninstallPathSet:
remove/modify, False otherwise.
"""
return is_local(path)
# aka is_local, but caching normalized sys.prefix
if not running_under_virtualenv():
return True
return path.startswith(self._normalize_path_cached(sys.prefix))
def add(self, path: str) -> None:
head, tail = os.path.split(path)
# we normalize the head to resolve parent directory symlinks, but not
# the tail, since we only want to uninstall symlinks, not their targets
path = os.path.join(normalize_path(head), os.path.normcase(tail))
path = os.path.join(self._normalize_path_cached(head), os.path.normcase(tail))
if not os.path.exists(path):
return
if self._permitted(path):
self.paths.add(path)
self._paths.add(path)
else:
self._refuse.add(path)
# __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:
pth_file = normalize_path(pth_file)
pth_file = self._normalize_path_cached(pth_file)
if self._permitted(pth_file):
if pth_file not in self.pth:
self.pth[pth_file] = UninstallPthEntries(pth_file)
self.pth[pth_file].add(entry)
if pth_file not in self._pth:
self._pth[pth_file] = UninstallPthEntries(pth_file)
self._pth[pth_file].add(entry)
else:
self._refuse.add(pth_file)
def remove(self, auto_confirm: bool = False, verbose: bool = False) -> None:
"""Remove paths in ``self.paths`` with confirmation (unless
"""Remove paths in ``self._paths`` with confirmation (unless
``auto_confirm`` is True)."""
if not self.paths:
if not self._paths:
logger.info(
"Can't uninstall '%s'. No files were found to uninstall.",
self.dist.project_name,
self._dist.raw_name,
)
return
dist_name_version = (
self.dist.project_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):
moved = self._moved_paths
for_rename = compress_for_rename(self.paths)
for_rename = compress_for_rename(self._paths)
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():
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
"""
"""Display which files would be deleted and prompt for confirmation"""
def _display(msg: str, paths: Iterable[str]) -> None:
if not paths:
@@ -403,32 +399,32 @@ class UninstallPathSet:
logger.info(path)
if not verbose:
will_remove, will_skip = compress_for_output_listing(self.paths)
will_remove, will_skip = compress_for_output_listing(self._paths)
else:
# In verbose mode, display all the files that are going to be
# deleted.
will_remove = set(self.paths)
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()."""
if not self._moved_paths.can_rollback:
logger.error(
"Can't roll back %s; was not uninstalled",
self.dist.project_name,
self._dist.raw_name,
)
return
logger.info('Rolling back uninstall of %s', self.dist.project_name)
logger.info("Rolling back uninstall of %s", self._dist.raw_name)
self._moved_paths.rollback()
for pth in self.pth.values():
for pth in self._pth.values():
pth.rollback()
def commit(self) -> None:
@@ -436,132 +432,161 @@ class UninstallPathSet:
self._moved_paths.commit()
@classmethod
def from_dist(cls, dist: Distribution) -> "UninstallPathSet":
dist_path = normalize_path(dist.location)
if not dist_is_local(dist):
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",
dist.canonical_name,
)
return cls(dist)
normalized_dist_location = normalize_path(dist_location)
if not dist.local:
logger.info(
"Not uninstalling %s at %s, outside environment %s",
dist.key,
dist_path,
dist.canonical_name,
normalized_dist_location,
sys.prefix,
)
return cls(dist)
if dist_path in {p for p in {sysconfig.get_path("stdlib"),
sysconfig.get_path("platstdlib")}
if p}:
if normalized_dist_location in {
p
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.",
dist.key,
dist_path,
dist.canonical_name,
normalized_dist_location,
)
return cls(dist)
paths_to_remove = cls(dist)
develop_egg_link = egg_link_path(dist)
develop_egg_link_egg_info = '{}.egg-info'.format(
pkg_resources.to_filename(dist.project_name))
egg_info_exists = dist.egg_info and os.path.exists(dist.egg_info)
# Special case for distutils installed package
distutils_egg_info = getattr(dist._provider, 'path', None)
develop_egg_link = egg_link_path_from_location(dist.raw_name)
# Distribution is installed with metadata in a "flat" .egg-info
# 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)
# 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")
)
# Uninstall cases order do matter as in the case of 2 installs of the
# same package, pip needs to uninstall the currently detected version
if (egg_info_exists and dist.egg_info.endswith('.egg-info') and
not dist.egg_info.endswith(develop_egg_link_egg_info)):
# if dist.egg_info.endswith(develop_egg_link_egg_info), we
# are in fact in the develop_egg_link case
paths_to_remove.add(dist.egg_info)
if dist.has_metadata('installed-files.txt'):
for installed_file in dist.get_metadata(
'installed-files.txt').splitlines():
path = os.path.normpath(
os.path.join(dist.egg_info, installed_file)
)
paths_to_remove.add(path)
if setuptools_flat_installation:
if info_location is not None:
paths_to_remove.add(info_location)
installed_files = dist.iter_declared_entries()
if installed_files is not None:
for installed_file in installed_files:
paths_to_remove.add(os.path.join(dist_location, installed_file))
# FIXME: need a test for this elif block
# occurs with --single-version-externally-managed/--record outside
# of pip
elif dist.has_metadata('top_level.txt'):
if dist.has_metadata('namespace_packages.txt'):
namespaces = dist.get_metadata('namespace_packages.txt')
else:
elif dist.is_file("top_level.txt"):
try:
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.get_metadata('top_level.txt').splitlines()
if p and p not in namespaces]:
path = os.path.join(dist.location, top_level_pkg)
p
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(path + '.py')
paths_to_remove.add(path + '.pyc')
paths_to_remove.add(path + '.pyo')
paths_to_remove.add(f"{path}.py")
paths_to_remove.add(f"{path}.pyc")
paths_to_remove.add(f"{path}.pyo")
elif distutils_egg_info:
elif dist.installed_by_distutils:
raise UninstallationError(
"Cannot uninstall {!r}. It is a distutils installed project "
"and thus we cannot accurately determine which files belong "
"to it which would lead to only a partial uninstall.".format(
dist.project_name,
dist.raw_name,
)
)
elif dist.location.endswith('.egg'):
elif dist.installed_as_egg:
# package installed by easy_install
# We cannot match on dist.egg_name because it can slightly vary
# i.e. setuptools-0.6c11-py2.6.egg vs setuptools-0.6rc11-py2.6.egg
paths_to_remove.add(dist.location)
easy_install_egg = os.path.split(dist.location)[1]
easy_install_pth = os.path.join(os.path.dirname(dist.location),
'easy-install.pth')
paths_to_remove.add_pth(easy_install_pth, './' + easy_install_egg)
paths_to_remove.add(dist_location)
easy_install_egg = os.path.split(dist_location)[1]
easy_install_pth = os.path.join(
os.path.dirname(dist_location),
"easy-install.pth",
)
paths_to_remove.add_pth(easy_install_pth, "./" + easy_install_egg)
elif egg_info_exists and dist.egg_info.endswith('.dist-info'):
elif dist.installed_with_dist_info:
for path in uninstallation_paths(dist):
paths_to_remove.add(path)
elif develop_egg_link:
# develop egg
# PEP 660 modern editable is handled in the ``.dist-info`` case
# above, so this only covers the setuptools-style editable.
with open(develop_egg_link) as fh:
link_pointer = os.path.normcase(fh.readline().strip())
assert (link_pointer == dist.location), (
'Egg-link {} does not match installed location of {} '
'(at {})'.format(
link_pointer, dist.project_name, dist.location)
normalized_link_pointer = paths_to_remove._normalize_path_cached(
link_pointer
)
assert os.path.samefile(
normalized_link_pointer, normalized_dist_location
), (
f"Egg-link {develop_egg_link} (to {link_pointer}) does not match "
f"installed location of {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')
paths_to_remove.add_pth(easy_install_pth, dist.location)
easy_install_pth = os.path.join(
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',
dist, dist.location,
"Not sure how to uninstall: %s - Check: %s",
dist,
dist_location,
)
if dist.in_usersite:
bin_dir = get_bin_user()
else:
bin_dir = get_bin_prefix()
# find distutils scripts= scripts
if dist.has_metadata('scripts') and dist.metadata_isdir('scripts'):
for script in dist.metadata_listdir('scripts'):
if dist_in_usersite(dist):
bin_dir = get_bin_user()
else:
bin_dir = get_bin_prefix()
try:
for script in dist.iter_distutils_script_names():
paths_to_remove.add(os.path.join(bin_dir, script))
if WINDOWS:
paths_to_remove.add(os.path.join(bin_dir, script) + '.bat')
paths_to_remove.add(os.path.join(bin_dir, f"{script}.bat"))
except (FileNotFoundError, NotADirectoryError):
pass
# find console_scripts
_scripts_to_remove = []
console_scripts = dist.get_entry_map(group='console_scripts')
for name in console_scripts.keys():
_scripts_to_remove.extend(_script_names(dist, name, False))
# find gui_scripts
gui_scripts = dist.get_entry_map(group='gui_scripts')
for name in gui_scripts.keys():
_scripts_to_remove.extend(_script_names(dist, name, True))
# find console_scripts and gui_scripts
def iter_scripts_to_remove(
dist: BaseDistribution,
bin_dir: str,
) -> Generator[str, None, None]:
for entry_point in dist.iter_entry_points():
if entry_point.group == "console_scripts":
yield from _script_names(bin_dir, entry_point.name, False)
elif entry_point.group == "gui_scripts":
yield from _script_names(bin_dir, entry_point.name, True)
for s in _scripts_to_remove:
for s in iter_scripts_to_remove(dist, bin_dir):
paths_to_remove.add(s)
return paths_to_remove
@@ -585,45 +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")
for entry in self.entries:
try:
logger.verbose('Removing entry: %s', entry)
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