@@ -57,6 +57,26 @@ def location(self) -> Optional[str]:
A string value is not necessarily a filesystem path, since distributions
can be loaded from other sources, e.g. arbitrary zip archives. ``None``
means the distribution is created in-memory.
Do not canonicalize this value with e.g. ``pathlib.Path.resolve()``. If
this is a symbolic link, we want to preserve the relative path between
it and files in the distribution.
"""
raise NotImplementedError()

@property
def info_directory(self) -> Optional[str]:
"""Location of the .[egg|dist]-info directory.
Similarly to ``location``, a string value is not necessarily a
filesystem path. ``None`` means the distribution is created in-memory.
For a modern .dist-info installation on disk, this should be something
like ``{location}/{raw_name}-{version}.dist-info``.
Do not canonicalize this value with e.g. ``pathlib.Path.resolve()``. If
this is a symbolic link, we want to preserve the relative path between
it and other files in the distribution.
"""
raise NotImplementedError()

@@ -48,6 +48,10 @@ def from_wheel(cls, path: str, name: str) -> "Distribution":
def location(self) -> Optional[str]:
return self._dist.location

@property
def info_directory(self) -> Optional[str]:
return self._dist.egg_info

@property
def canonical_name(self) -> "NormalizedName":
return canonicalize_name(self._dist.project_name)
@@ -25,6 +25,46 @@ def __init__(self):
self.parent = sys.exc_info()


def write_installed_files_from_setuptools_record(
record_lines: List[str],
root: Optional[str],
req_description: str,
) -> None:
def prepend_root(path):
# type: (str) -> str
if root is None or not os.path.isabs(path):
return path
else:
return change_root(root, path)

for line in record_lines:
directory = os.path.dirname(line)
if directory.endswith('.egg-info'):
egg_info_dir = prepend_root(directory)
break
else:
message = (
"{} did not indicate that it installed an "
".egg-info directory. Only setup.py projects "
"generating .egg-info directories are supported."
).format(req_description)
raise InstallationError(message)

new_lines = []
for line in record_lines:
filename = line.strip()
if os.path.isdir(filename):
filename += os.path.sep
new_lines.append(
os.path.relpath(prepend_root(filename), egg_info_dir)
)
new_lines.sort()
ensure_dir(egg_info_dir)
inst_files_path = os.path.join(egg_info_dir, 'installed-files.txt')
with open(inst_files_path, 'w') as f:
f.write('\n'.join(new_lines) + '\n')


def install(
install_options, # type: List[str]
global_options, # type: Sequence[str]
@@ -88,38 +128,5 @@ def install(
with open(record_filename) as f:
record_lines = f.read().splitlines()

def prepend_root(path):
# type: (str) -> str
if root is None or not os.path.isabs(path):
return path
else:
return change_root(root, path)

for line in record_lines:
directory = os.path.dirname(line)
if directory.endswith('.egg-info'):
egg_info_dir = prepend_root(directory)
break
else:
message = (
"{} did not indicate that it installed an "
".egg-info directory. Only setup.py projects "
"generating .egg-info directories are supported."
).format(req_description)
raise InstallationError(message)

new_lines = []
for line in record_lines:
filename = line.strip()
if os.path.isdir(filename):
filename += os.path.sep
new_lines.append(
os.path.relpath(prepend_root(filename), egg_info_dir)
)
new_lines.sort()
ensure_dir(egg_info_dir)
inst_files_path = os.path.join(egg_info_dir, 'installed-files.txt')
with open(inst_files_path, 'w') as f:
f.write('\n'.join(new_lines) + '\n')

write_installed_files_from_setuptools_record(record_lines, root, req_description)
return True
@@ -143,6 +143,21 @@ def get_preference(
identifier,
)

def _get_constraint(self, identifier: str) -> Constraint:
if identifier in self._constraints:
return self._constraints[identifier]

# HACK: Theoratically we should check whether this identifier is a valid
# "NAME[EXTRAS]" format, and parse out the name part with packaging or
# some regular expression. But since pip's resolver only spits out
# three kinds of identifiers: normalized PEP 503 names, normalized names
# plus extras, and Requires-Python, we can cheat a bit here.
name, open_bracket, _ = identifier.partition("[")
if open_bracket and name in self._constraints:
return self._constraints[name]

return Constraint.empty()

def find_matches(
self,
identifier: str,
@@ -169,7 +184,7 @@ def _eligible_for_upgrade(name: str) -> bool:
return self._factory.find_candidates(
identifier=identifier,
requirements=requirements,
constraint=self._constraints.get(identifier, Constraint.empty()),
constraint=self._get_constraint(identifier),
prefers_installed=(not _eligible_for_upgrade(identifier)),
incompatibilities=incompatibilities,
)
@@ -190,7 +190,7 @@ def __init__(self, last_attempt: "Future") -> None:
self.last_attempt = last_attempt
super().__init__(last_attempt)

def reraise(self) -> t.NoReturn:
def reraise(self) -> "t.NoReturn":
if self.last_attempt.failed:
raise self.last_attempt.result()
raise self
@@ -2009,3 +2009,21 @@ def test_new_resolver_file_url_normalize(script, format_dep, format_input):
format_input(lib_a), lib_b,
)
script.assert_installed(lib_a="1", lib_b="1")


def test_new_resolver_dont_backtrack_on_extra_if_base_constrained(script):
create_basic_wheel_for_package(script, "dep", "1.0")
create_basic_wheel_for_package(script, "pkg", "1.0", extras={"ext": ["dep"]})
create_basic_wheel_for_package(script, "pkg", "2.0", extras={"ext": ["dep"]})
constraints_file = script.scratch_path / "constraints.txt"
constraints_file.write_text("pkg==1.0")

result = script.pip(
"install",
"--no-cache-dir", "--no-index",
"--find-links", script.scratch_path,
"--constraint", constraints_file,
"pkg[ext]",
)
assert "pkg-2.0" not in result.stdout, "Should not try 2.0 due to constraint"
script.assert_installed(pkg="1.0", dep="1.0")
@@ -1,10 +1,12 @@
import os
import re

import pytest

from pip import __version__
from pip._internal.commands.show import search_packages_info
from pip._internal.operations.install.legacy import (
write_installed_files_from_setuptools_record,
)
from pip._internal.utils.unpacking import untar_file
from tests.lib import create_test_package_with_setup


@@ -41,7 +43,7 @@ def test_show_with_files_not_found(script, data):

def test_show_with_files_from_wheel(script, data):
"""
Test that a wheel's files can be listed
Test that a wheel's files can be listed.
"""
wheel_file = data.packages.joinpath('simple.dist-0.1-py2.py3-none-any.whl')
script.pip('install', '--no-index', wheel_file)
@@ -50,18 +52,36 @@ def test_show_with_files_from_wheel(script, data):
assert 'Name: simple.dist' in lines
assert 'Cannot locate RECORD or installed-files.txt' not in lines[6], lines[6]
assert re.search(r"Files:\n( .+\n)+", result.stdout)
assert f" simpledist{os.sep}__init__.py" in lines[6:]


@pytest.mark.network
def test_show_with_all_files(script):
def test_show_with_files_from_legacy(tmp_path, script, data):
"""
Test listing all files in the show command.
Test listing files in the show command (legacy installed-files.txt).
"""
script.pip('install', 'initools==0.2')
result = script.pip('show', '--files', 'initools')
# Since 'pip install' now always tries to build a wheel from sdist, it
# cannot properly generate a setup. The legacy code path is basically
# 'setup.py install' plus installed-files.txt, which we manually generate.
source_dir = tmp_path.joinpath("unpacked-sdist")
setuptools_record = tmp_path.joinpath("installed-record.txt")
untar_file(data.packages.joinpath("simple-1.0.tar.gz"), str(source_dir))
script.run(
"python", "setup.py", "install",
"--single-version-externally-managed",
"--record", str(setuptools_record),
cwd=source_dir,
)
write_installed_files_from_setuptools_record(
setuptools_record.read_text().splitlines(),
root=None,
req_description="simple==1.0",
)

result = script.pip('show', '--files', 'simple')
lines = result.stdout.splitlines()
assert 'Cannot locate RECORD or installed-files.txt' not in lines[6], lines[6]
assert re.search(r"Files:\n( .+\n)+", result.stdout)
assert f" simple{os.sep}__init__.py" in lines[6:]


def test_missing_argument(script):
@@ -1,5 +1,5 @@
diff --git a/src/pip/_vendor/tenacity/__init__.py b/src/pip/_vendor/tenacity/__init__.py
index 88c28d2d6..f984eec4e 100644
index 88c28d2d6..086ad46e1 100644
--- a/src/pip/_vendor/tenacity/__init__.py
+++ b/src/pip/_vendor/tenacity/__init__.py
@@ -76,10 +76,12 @@ from .after import after_nothing # noqa
@@ -19,3 +19,16 @@ index 88c28d2d6..f984eec4e 100644

if t.TYPE_CHECKING:
import types

--- a/src/pip/_vendor/tenacity/__init__.py
+++ b/src/pip/_vendor/tenacity/__init__.py
@@ -190,7 +190,7 @@ class RetryError(Exception):
self.last_attempt = last_attempt
super().__init__(last_attempt)

- def reraise(self) -> t.NoReturn:
+ def reraise(self) -> "t.NoReturn":
if self.last_attempt.failed:
raise self.last_attempt.result()
raise self