diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 454c0b70c..8c2c32fd4 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -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( diff --git a/src/pip/_internal/utils/datetime.py b/src/pip/_internal/utils/datetime.py new file mode 100644 index 000000000..4d0503c2f --- /dev/null +++ b/src/pip/_internal/utils/datetime.py @@ -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 diff --git a/tests/functional/test_check.py b/tests/functional/test_check.py index 4e9a144bf..5cb41a97e 100644 --- a/tests/functional/test_check.py +++ b/tests/functional/test_check.py @@ -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): diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 2657a8aee..2185251d2 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -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) diff --git a/tests/functional/test_install_check.py b/tests/functional/test_install_check.py index 017d6256b..88609422b 100644 --- a/tests/functional/test_install_check.py +++ b/tests/functional/test_install_check.py @@ -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) diff --git a/tests/yaml/conflict_1.yml b/tests/yaml/conflict_1.yml index 847bb2b6a..c0a561995 100644 --- a/tests/yaml/conflict_1.yml +++ b/tests/yaml/conflict_1.yml @@ -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 - diff --git a/tests/yaml/conflicting_triangle.yml b/tests/yaml/conflicting_triangle.yml index 666c37363..e8e88b347 100644 --- a/tests/yaml/conflicting_triangle.yml +++ b/tests/yaml/conflicting_triangle.yml @@ -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.