Merge pull request #8594 from pradyunsg/improve-install-conflict-warning

This commit is contained in:
Pradyun Gedam 2020-07-28 18:22:53 +05:30 committed by GitHub
commit a8edffda1c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 92 additions and 45 deletions

View File

@ -21,6 +21,7 @@ from pip._internal.locations import distutils_scheme
from pip._internal.operations.check import check_install_conflicts
from pip._internal.req import install_given_reqs
from pip._internal.req.req_tracker import get_requirement_tracker
from pip._internal.utils.datetime import today_is_later_than
from pip._internal.utils.deprecation import deprecated
from pip._internal.utils.distutils_args import parse_distutils_args
from pip._internal.utils.filesystem import test_writable_dir
@ -444,7 +445,10 @@ class InstallCommand(RequirementCommand):
items.append(item)
if conflicts is not None:
self._warn_about_conflicts(conflicts)
self._warn_about_conflicts(
conflicts,
new_resolver='2020-resolver' in options.features_enabled,
)
installed_desc = ' '.join(items)
if installed_desc:
@ -536,27 +540,68 @@ class InstallCommand(RequirementCommand):
)
return None
def _warn_about_conflicts(self, conflict_details):
# type: (ConflictDetails) -> None
def _warn_about_conflicts(self, conflict_details, new_resolver):
# type: (ConflictDetails, bool) -> None
package_set, (missing, conflicting) = conflict_details
if not missing and not conflicting:
return
parts = [] # type: List[str]
if not new_resolver:
parts.append(
"After October 2020 you may experience errors when installing "
"or updating packages. This is because pip will change the "
"way that it resolves dependency conflicts.\n"
)
parts.append(
"We recommend you use --use-feature=2020-resolver to test "
"your packages with the new resolver before it becomes the "
"default.\n"
)
elif not today_is_later_than(year=2020, month=7, day=31):
# NOTE: trailing newlines here are intentional
parts.append(
"Pip will install or upgrade your package(s) and its "
"dependencies without taking into account other packages you "
"already have installed. This may cause an uncaught "
"dependency conflict.\n"
)
form_link = "https://forms.gle/cWKMoDs8sUVE29hz9"
parts.append(
"If you would like pip to take your other packages into "
"account, please tell us here: {}\n".format(form_link)
)
# NOTE: There is some duplication here, with commands/check.py
for project_name in missing:
version = package_set[project_name][0]
for dependency in missing[project_name]:
logger.critical(
"%s %s requires %s, which is not installed.",
project_name, version, dependency[1],
message = (
"{name} {version} requires {requirement}, "
"which is not installed."
).format(
name=project_name,
version=version,
requirement=dependency[1],
)
parts.append(message)
for project_name in conflicting:
version = package_set[project_name][0]
for dep_name, dep_version, req in conflicting[project_name]:
logger.critical(
"%s %s has requirement %s, but you'll have %s %s which is "
"incompatible.",
project_name, version, req, dep_name, dep_version,
message = (
"{name} {version} requires {requirement}, but you'll have "
"{dep_name} {dep_version} which is incompatible."
).format(
name=project_name,
version=version,
requirement=req,
dep_name=dep_name,
dep_version=dep_version,
)
parts.append(message)
logger.critical("\n".join(parts))
def get_lib_location_guesses(

View File

@ -0,0 +1,14 @@
"""For when pip wants to check the date or time.
"""
from __future__ import absolute_import
import datetime
def today_is_later_than(year, month, day):
# type: (int, int, int) -> bool
today = datetime.date.today()
given = datetime.date(year, month, day)
return today > given

View File

@ -3,9 +3,11 @@ from tests.lib import create_test_package_with_setup
def matches_expected_lines(string, expected_lines):
# Ignore empty lines
output_lines = set(filter(None, string.splitlines()))
# Match regardless of order
return set(output_lines) == set(expected_lines)
output_lines = list(filter(None, string.splitlines()))
# We'll match the last n lines, given n lines to match.
last_few_output_lines = output_lines[-len(expected_lines):]
# And order does not matter
return set(last_few_output_lines) == set(expected_lines)
def test_basic_check_clean(script):

View File

@ -1697,7 +1697,7 @@ def test_install_conflict_results_in_warning(script, data):
result2 = script.pip(
'install', '--no-index', pkgB_path, allow_stderr_error=True,
)
assert "pkga 1.0 has requirement pkgb==1.0" in result2.stderr, str(result2)
assert "pkga 1.0 requires pkgb==1.0" in result2.stderr, str(result2)
assert "Successfully installed pkgB-2.0" in result2.stdout, str(result2)

View File

@ -1,14 +1,11 @@
from tests.lib import create_test_package_with_setup
def matches_expected_lines(string, expected_lines, exact=True):
if exact:
return set(string.splitlines()) == set(expected_lines)
# If not exact, check that all expected lines are present
def contains_expected_lines(string, expected_lines):
return set(expected_lines) <= set(string.splitlines())
def test_check_install_canonicalization(script, deprecated_python):
def test_check_install_canonicalization(script):
pkga_path = create_test_package_with_setup(
script,
name='pkgA',
@ -38,11 +35,10 @@ def test_check_install_canonicalization(script, deprecated_python):
allow_stderr_error=True,
)
expected_lines = [
"ERROR: pkga 1.0 requires SPECIAL.missing, which is not installed.",
"pkga 1.0 requires SPECIAL.missing, which is not installed.",
]
# Deprecated python versions produce an extra warning on stderr
assert matches_expected_lines(
result.stderr, expected_lines, exact=not deprecated_python)
assert contains_expected_lines(result.stderr, expected_lines)
assert result.returncode == 0
# Install the second missing package and expect that there is no warning
@ -51,8 +47,7 @@ def test_check_install_canonicalization(script, deprecated_python):
result = script.pip(
'install', '--no-index', special_path, '--quiet',
)
assert matches_expected_lines(
result.stderr, [], exact=not deprecated_python)
assert "requires" not in result.stderr
assert result.returncode == 0
# Double check that all errors are resolved in the end
@ -60,12 +55,11 @@ def test_check_install_canonicalization(script, deprecated_python):
expected_lines = [
"No broken requirements found.",
]
assert matches_expected_lines(result.stdout, expected_lines)
assert contains_expected_lines(result.stdout, expected_lines)
assert result.returncode == 0
def test_check_install_does_not_warn_for_out_of_graph_issues(
script, deprecated_python):
def test_check_install_does_not_warn_for_out_of_graph_issues(script):
pkg_broken_path = create_test_package_with_setup(
script,
name='broken',
@ -85,33 +79,30 @@ def test_check_install_does_not_warn_for_out_of_graph_issues(
# Install a package without it's dependencies
result = script.pip('install', '--no-index', pkg_broken_path, '--no-deps')
# Deprecated python versions produce an extra warning on stderr
assert matches_expected_lines(
result.stderr, [], exact=not deprecated_python)
assert "requires" not in result.stderr
# Install conflict package
result = script.pip(
'install', '--no-index', pkg_conflict_path, allow_stderr_error=True,
)
assert matches_expected_lines(result.stderr, [
"ERROR: broken 1.0 requires missing, which is not installed.",
assert contains_expected_lines(result.stderr, [
"broken 1.0 requires missing, which is not installed.",
(
"ERROR: broken 1.0 has requirement conflict<1.0, but "
"broken 1.0 requires conflict<1.0, but "
"you'll have conflict 1.0 which is incompatible."
),
], exact=not deprecated_python)
])
# Install unrelated package
result = script.pip(
'install', '--no-index', pkg_unrelated_path, '--quiet',
)
# should not warn about broken's deps when installing unrelated package
assert matches_expected_lines(
result.stderr, [], exact=not deprecated_python)
assert "requires" not in result.stderr
result = script.pip('check', expect_error=True)
expected_lines = [
"broken 1.0 requires missing, which is not installed.",
"broken 1.0 has requirement conflict<1.0, but you have conflict 1.0.",
]
assert matches_expected_lines(result.stdout, expected_lines)
assert contains_expected_lines(result.stdout, expected_lines)

View File

@ -11,12 +11,9 @@ cases:
response:
- error:
code: 0
stderr: ['requirement', 'is\s+incompatible']
stderr: ['incompatible']
skip: old
# -- currently the error message is:
# a 1.0.0 has requirement B==1.0.0, but you'll have b 2.0.0 which is
# incompatible.
# -- better would be:
# -- a good error message would be:
# A 1.0.0 has incompatible requirements B==1.0.0, B==2.0.0
-

View File

@ -14,7 +14,5 @@ cases:
- C 1.0.0
- error:
code: 0
stderr: ['requirement c==1\.0\.0', 'is incompatible']
stderr: ['c==1\.0\.0', 'incompatible']
skip: old
# -- currently the error message is:
# a 1.0.0 has requirement C==1.0.0, but you'll have c 2.0.0 which is incompatible.