mirror of https://github.com/pypa/pip
Merge pull request #8594 from pradyunsg/improve-install-conflict-warning
This commit is contained in:
commit
a8edffda1c
|
@ -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(
|
||||
|
|
|
@ -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
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
-
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue