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.operations.check import check_install_conflicts
from pip._internal.req import install_given_reqs from pip._internal.req import install_given_reqs
from pip._internal.req.req_tracker import get_requirement_tracker 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.deprecation import deprecated
from pip._internal.utils.distutils_args import parse_distutils_args from pip._internal.utils.distutils_args import parse_distutils_args
from pip._internal.utils.filesystem import test_writable_dir from pip._internal.utils.filesystem import test_writable_dir
@ -444,7 +445,10 @@ class InstallCommand(RequirementCommand):
items.append(item) items.append(item)
if conflicts is not None: 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) installed_desc = ' '.join(items)
if installed_desc: if installed_desc:
@ -536,27 +540,68 @@ class InstallCommand(RequirementCommand):
) )
return None return None
def _warn_about_conflicts(self, conflict_details): def _warn_about_conflicts(self, conflict_details, new_resolver):
# type: (ConflictDetails) -> None # type: (ConflictDetails, bool) -> None
package_set, (missing, conflicting) = conflict_details 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 # NOTE: There is some duplication here, with commands/check.py
for project_name in missing: for project_name in missing:
version = package_set[project_name][0] version = package_set[project_name][0]
for dependency in missing[project_name]: for dependency in missing[project_name]:
logger.critical( message = (
"%s %s requires %s, which is not installed.", "{name} {version} requires {requirement}, "
project_name, version, dependency[1], "which is not installed."
).format(
name=project_name,
version=version,
requirement=dependency[1],
) )
parts.append(message)
for project_name in conflicting: for project_name in conflicting:
version = package_set[project_name][0] version = package_set[project_name][0]
for dep_name, dep_version, req in conflicting[project_name]: for dep_name, dep_version, req in conflicting[project_name]:
logger.critical( message = (
"%s %s has requirement %s, but you'll have %s %s which is " "{name} {version} requires {requirement}, but you'll have "
"incompatible.", "{dep_name} {dep_version} which is incompatible."
project_name, version, req, dep_name, dep_version, ).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( 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): def matches_expected_lines(string, expected_lines):
# Ignore empty lines # Ignore empty lines
output_lines = set(filter(None, string.splitlines())) output_lines = list(filter(None, string.splitlines()))
# Match regardless of order # We'll match the last n lines, given n lines to match.
return set(output_lines) == set(expected_lines) 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): def test_basic_check_clean(script):

View File

@ -1697,7 +1697,7 @@ def test_install_conflict_results_in_warning(script, data):
result2 = script.pip( result2 = script.pip(
'install', '--no-index', pkgB_path, allow_stderr_error=True, '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) 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 from tests.lib import create_test_package_with_setup
def matches_expected_lines(string, expected_lines, exact=True): def contains_expected_lines(string, expected_lines):
if exact:
return set(string.splitlines()) == set(expected_lines)
# If not exact, check that all expected lines are present
return set(expected_lines) <= set(string.splitlines()) 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( pkga_path = create_test_package_with_setup(
script, script,
name='pkgA', name='pkgA',
@ -38,11 +35,10 @@ def test_check_install_canonicalization(script, deprecated_python):
allow_stderr_error=True, allow_stderr_error=True,
) )
expected_lines = [ 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 # Deprecated python versions produce an extra warning on stderr
assert matches_expected_lines( assert contains_expected_lines(result.stderr, expected_lines)
result.stderr, expected_lines, exact=not deprecated_python)
assert result.returncode == 0 assert result.returncode == 0
# Install the second missing package and expect that there is no warning # 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( result = script.pip(
'install', '--no-index', special_path, '--quiet', 'install', '--no-index', special_path, '--quiet',
) )
assert matches_expected_lines( assert "requires" not in result.stderr
result.stderr, [], exact=not deprecated_python)
assert result.returncode == 0 assert result.returncode == 0
# Double check that all errors are resolved in the end # Double check that all errors are resolved in the end
@ -60,12 +55,11 @@ def test_check_install_canonicalization(script, deprecated_python):
expected_lines = [ expected_lines = [
"No broken requirements found.", "No broken requirements found.",
] ]
assert matches_expected_lines(result.stdout, expected_lines) assert contains_expected_lines(result.stdout, expected_lines)
assert result.returncode == 0 assert result.returncode == 0
def test_check_install_does_not_warn_for_out_of_graph_issues( def test_check_install_does_not_warn_for_out_of_graph_issues(script):
script, deprecated_python):
pkg_broken_path = create_test_package_with_setup( pkg_broken_path = create_test_package_with_setup(
script, script,
name='broken', 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 # Install a package without it's dependencies
result = script.pip('install', '--no-index', pkg_broken_path, '--no-deps') result = script.pip('install', '--no-index', pkg_broken_path, '--no-deps')
# Deprecated python versions produce an extra warning on stderr assert "requires" not in result.stderr
assert matches_expected_lines(
result.stderr, [], exact=not deprecated_python)
# Install conflict package # Install conflict package
result = script.pip( result = script.pip(
'install', '--no-index', pkg_conflict_path, allow_stderr_error=True, 'install', '--no-index', pkg_conflict_path, allow_stderr_error=True,
) )
assert matches_expected_lines(result.stderr, [ assert contains_expected_lines(result.stderr, [
"ERROR: broken 1.0 requires missing, which is not installed.", "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." "you'll have conflict 1.0 which is incompatible."
), ),
], exact=not deprecated_python) ])
# Install unrelated package # Install unrelated package
result = script.pip( result = script.pip(
'install', '--no-index', pkg_unrelated_path, '--quiet', 'install', '--no-index', pkg_unrelated_path, '--quiet',
) )
# should not warn about broken's deps when installing unrelated package # should not warn about broken's deps when installing unrelated package
assert matches_expected_lines( assert "requires" not in result.stderr
result.stderr, [], exact=not deprecated_python)
result = script.pip('check', expect_error=True) result = script.pip('check', expect_error=True)
expected_lines = [ expected_lines = [
"broken 1.0 requires missing, which is not installed.", "broken 1.0 requires missing, which is not installed.",
"broken 1.0 has requirement conflict<1.0, but you have conflict 1.0.", "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: response:
- error: - error:
code: 0 code: 0
stderr: ['requirement', 'is\s+incompatible'] stderr: ['incompatible']
skip: old skip: old
# -- currently the error message is: # -- a good error message would be:
# 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 1.0.0 has incompatible requirements B==1.0.0, B==2.0.0 # 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 - C 1.0.0
- error: - error:
code: 0 code: 0
stderr: ['requirement c==1\.0\.0', 'is incompatible'] stderr: ['c==1\.0\.0', 'incompatible']
skip: old 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.