Skip to content

Commit a8edffd

Browse files
authored
Merge pull request #8594 from pradyunsg/improve-install-conflict-warning
2 parents b6c99af + 9033824 commit a8edffd

File tree

7 files changed

+92
-45
lines changed

7 files changed

+92
-45
lines changed

src/pip/_internal/commands/install.py

+55-10
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from pip._internal.operations.check import check_install_conflicts
2222
from pip._internal.req import install_given_reqs
2323
from pip._internal.req.req_tracker import get_requirement_tracker
24+
from pip._internal.utils.datetime import today_is_later_than
2425
from pip._internal.utils.deprecation import deprecated
2526
from pip._internal.utils.distutils_args import parse_distutils_args
2627
from pip._internal.utils.filesystem import test_writable_dir
@@ -444,7 +445,10 @@ def run(self, options, args):
444445
items.append(item)
445446

446447
if conflicts is not None:
447-
self._warn_about_conflicts(conflicts)
448+
self._warn_about_conflicts(
449+
conflicts,
450+
new_resolver='2020-resolver' in options.features_enabled,
451+
)
448452

449453
installed_desc = ' '.join(items)
450454
if installed_desc:
@@ -536,27 +540,68 @@ def _determine_conflicts(self, to_install):
536540
)
537541
return None
538542

539-
def _warn_about_conflicts(self, conflict_details):
540-
# type: (ConflictDetails) -> None
543+
def _warn_about_conflicts(self, conflict_details, new_resolver):
544+
# type: (ConflictDetails, bool) -> None
541545
package_set, (missing, conflicting) = conflict_details
546+
if not missing and not conflicting:
547+
return
548+
549+
parts = [] # type: List[str]
550+
if not new_resolver:
551+
parts.append(
552+
"After October 2020 you may experience errors when installing "
553+
"or updating packages. This is because pip will change the "
554+
"way that it resolves dependency conflicts.\n"
555+
)
556+
parts.append(
557+
"We recommend you use --use-feature=2020-resolver to test "
558+
"your packages with the new resolver before it becomes the "
559+
"default.\n"
560+
)
561+
elif not today_is_later_than(year=2020, month=7, day=31):
562+
# NOTE: trailing newlines here are intentional
563+
parts.append(
564+
"Pip will install or upgrade your package(s) and its "
565+
"dependencies without taking into account other packages you "
566+
"already have installed. This may cause an uncaught "
567+
"dependency conflict.\n"
568+
)
569+
form_link = "https://forms.gle/cWKMoDs8sUVE29hz9"
570+
parts.append(
571+
"If you would like pip to take your other packages into "
572+
"account, please tell us here: {}\n".format(form_link)
573+
)
542574

543575
# NOTE: There is some duplication here, with commands/check.py
544576
for project_name in missing:
545577
version = package_set[project_name][0]
546578
for dependency in missing[project_name]:
547-
logger.critical(
548-
"%s %s requires %s, which is not installed.",
549-
project_name, version, dependency[1],
579+
message = (
580+
"{name} {version} requires {requirement}, "
581+
"which is not installed."
582+
).format(
583+
name=project_name,
584+
version=version,
585+
requirement=dependency[1],
550586
)
587+
parts.append(message)
551588

552589
for project_name in conflicting:
553590
version = package_set[project_name][0]
554591
for dep_name, dep_version, req in conflicting[project_name]:
555-
logger.critical(
556-
"%s %s has requirement %s, but you'll have %s %s which is "
557-
"incompatible.",
558-
project_name, version, req, dep_name, dep_version,
592+
message = (
593+
"{name} {version} requires {requirement}, but you'll have "
594+
"{dep_name} {dep_version} which is incompatible."
595+
).format(
596+
name=project_name,
597+
version=version,
598+
requirement=req,
599+
dep_name=dep_name,
600+
dep_version=dep_version,
559601
)
602+
parts.append(message)
603+
604+
logger.critical("\n".join(parts))
560605

561606

562607
def get_lib_location_guesses(

src/pip/_internal/utils/datetime.py

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""For when pip wants to check the date or time.
2+
"""
3+
4+
from __future__ import absolute_import
5+
6+
import datetime
7+
8+
9+
def today_is_later_than(year, month, day):
10+
# type: (int, int, int) -> bool
11+
today = datetime.date.today()
12+
given = datetime.date(year, month, day)
13+
14+
return today > given

tests/functional/test_check.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33

44
def matches_expected_lines(string, expected_lines):
55
# Ignore empty lines
6-
output_lines = set(filter(None, string.splitlines()))
7-
# Match regardless of order
8-
return set(output_lines) == set(expected_lines)
6+
output_lines = list(filter(None, string.splitlines()))
7+
# We'll match the last n lines, given n lines to match.
8+
last_few_output_lines = output_lines[-len(expected_lines):]
9+
# And order does not matter
10+
return set(last_few_output_lines) == set(expected_lines)
911

1012

1113
def test_basic_check_clean(script):

tests/functional/test_install.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1697,7 +1697,7 @@ def test_install_conflict_results_in_warning(script, data):
16971697
result2 = script.pip(
16981698
'install', '--no-index', pkgB_path, allow_stderr_error=True,
16991699
)
1700-
assert "pkga 1.0 has requirement pkgb==1.0" in result2.stderr, str(result2)
1700+
assert "pkga 1.0 requires pkgb==1.0" in result2.stderr, str(result2)
17011701
assert "Successfully installed pkgB-2.0" in result2.stdout, str(result2)
17021702

17031703

tests/functional/test_install_check.py

+14-23
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
from tests.lib import create_test_package_with_setup
22

33

4-
def matches_expected_lines(string, expected_lines, exact=True):
5-
if exact:
6-
return set(string.splitlines()) == set(expected_lines)
7-
# If not exact, check that all expected lines are present
4+
def contains_expected_lines(string, expected_lines):
85
return set(expected_lines) <= set(string.splitlines())
96

107

11-
def test_check_install_canonicalization(script, deprecated_python):
8+
def test_check_install_canonicalization(script):
129
pkga_path = create_test_package_with_setup(
1310
script,
1411
name='pkgA',
@@ -38,11 +35,10 @@ def test_check_install_canonicalization(script, deprecated_python):
3835
allow_stderr_error=True,
3936
)
4037
expected_lines = [
41-
"ERROR: pkga 1.0 requires SPECIAL.missing, which is not installed.",
38+
"pkga 1.0 requires SPECIAL.missing, which is not installed.",
4239
]
4340
# Deprecated python versions produce an extra warning on stderr
44-
assert matches_expected_lines(
45-
result.stderr, expected_lines, exact=not deprecated_python)
41+
assert contains_expected_lines(result.stderr, expected_lines)
4642
assert result.returncode == 0
4743

4844
# Install the second missing package and expect that there is no warning
@@ -51,21 +47,19 @@ def test_check_install_canonicalization(script, deprecated_python):
5147
result = script.pip(
5248
'install', '--no-index', special_path, '--quiet',
5349
)
54-
assert matches_expected_lines(
55-
result.stderr, [], exact=not deprecated_python)
50+
assert "requires" not in result.stderr
5651
assert result.returncode == 0
5752

5853
# Double check that all errors are resolved in the end
5954
result = script.pip('check')
6055
expected_lines = [
6156
"No broken requirements found.",
6257
]
63-
assert matches_expected_lines(result.stdout, expected_lines)
58+
assert contains_expected_lines(result.stdout, expected_lines)
6459
assert result.returncode == 0
6560

6661

67-
def test_check_install_does_not_warn_for_out_of_graph_issues(
68-
script, deprecated_python):
62+
def test_check_install_does_not_warn_for_out_of_graph_issues(script):
6963
pkg_broken_path = create_test_package_with_setup(
7064
script,
7165
name='broken',
@@ -85,33 +79,30 @@ def test_check_install_does_not_warn_for_out_of_graph_issues(
8579

8680
# Install a package without it's dependencies
8781
result = script.pip('install', '--no-index', pkg_broken_path, '--no-deps')
88-
# Deprecated python versions produce an extra warning on stderr
89-
assert matches_expected_lines(
90-
result.stderr, [], exact=not deprecated_python)
82+
assert "requires" not in result.stderr
9183

9284
# Install conflict package
9385
result = script.pip(
9486
'install', '--no-index', pkg_conflict_path, allow_stderr_error=True,
9587
)
96-
assert matches_expected_lines(result.stderr, [
97-
"ERROR: broken 1.0 requires missing, which is not installed.",
88+
assert contains_expected_lines(result.stderr, [
89+
"broken 1.0 requires missing, which is not installed.",
9890
(
99-
"ERROR: broken 1.0 has requirement conflict<1.0, but "
91+
"broken 1.0 requires conflict<1.0, but "
10092
"you'll have conflict 1.0 which is incompatible."
10193
),
102-
], exact=not deprecated_python)
94+
])
10395

10496
# Install unrelated package
10597
result = script.pip(
10698
'install', '--no-index', pkg_unrelated_path, '--quiet',
10799
)
108100
# should not warn about broken's deps when installing unrelated package
109-
assert matches_expected_lines(
110-
result.stderr, [], exact=not deprecated_python)
101+
assert "requires" not in result.stderr
111102

112103
result = script.pip('check', expect_error=True)
113104
expected_lines = [
114105
"broken 1.0 requires missing, which is not installed.",
115106
"broken 1.0 has requirement conflict<1.0, but you have conflict 1.0.",
116107
]
117-
assert matches_expected_lines(result.stdout, expected_lines)
108+
assert contains_expected_lines(result.stdout, expected_lines)

tests/yaml/conflict_1.yml

+2-5
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,9 @@ cases:
1111
response:
1212
- error:
1313
code: 0
14-
stderr: ['requirement', 'is\s+incompatible']
14+
stderr: ['incompatible']
1515
skip: old
16-
# -- currently the error message is:
17-
# a 1.0.0 has requirement B==1.0.0, but you'll have b 2.0.0 which is
18-
# incompatible.
19-
# -- better would be:
16+
# -- a good error message would be:
2017
# A 1.0.0 has incompatible requirements B==1.0.0, B==2.0.0
2118

2219
-

tests/yaml/conflicting_triangle.yml

+1-3
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,5 @@ cases:
1414
- C 1.0.0
1515
- error:
1616
code: 0
17-
stderr: ['requirement c==1\.0\.0', 'is incompatible']
17+
stderr: ['c==1\.0\.0', 'incompatible']
1818
skip: old
19-
# -- currently the error message is:
20-
# a 1.0.0 has requirement C==1.0.0, but you'll have c 2.0.0 which is incompatible.

0 commit comments

Comments
 (0)