From c546c99480875cfe4cdeaefa6d16bad9998d0f70 Mon Sep 17 00:00:00 2001 From: Guillaume Seguin Date: Mon, 6 Jun 2022 11:39:20 -0700 Subject: [PATCH] Display dependency chain on each Collecting line This tremendously helps understand why a package is being fetched and can help investigate and fix dependency resolver backtracking issues when incoherent constraints/package sets are provided or when new versions of a package trigger a completely different backtracking strategy, leading to very hard to debug situations. --- news/11169.feature.rst | 1 + src/pip/_internal/operations/prepare.py | 10 +++++ tests/functional/test_install.py | 58 +++++++++++++++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 news/11169.feature.rst diff --git a/news/11169.feature.rst b/news/11169.feature.rst new file mode 100644 index 00000000000..54cc6637bc6 --- /dev/null +++ b/news/11169.feature.rst @@ -0,0 +1 @@ +Display dependency chain on each Collecting/Processing log line. diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 4bf414cb005..343a01bef4b 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -270,6 +270,16 @@ def _log_preparing_link(self, req: InstallRequirement) -> None: message = "Collecting %s" information = str(req.req or req) + # If we used req.req, inject requirement source if available (this + # would already be included if we used req directly) + if req.req and req.comes_from: + if isinstance(req.comes_from, str): + comes_from: Optional[str] = req.comes_from + else: + comes_from = req.comes_from.from_path() + if comes_from: + information += f" (from {comes_from})" + if (message, information) != self._previous_requirement_header: self._previous_requirement_header = (message, information) logger.info(message, information) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 3fd9329bc6e..65ba1528860 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -2393,3 +2393,61 @@ def find_distributions(self, context=None): result = script.pip("install", "example") assert "Requirement already satisfied: example in " in result.stdout + + +def test_install_pip_prints_req_chain_local(script: PipTestEnvironment) -> None: + """ + Test installing a local package with a dependency and check that the + dependency chain is reported. + """ + + req_path = script.scratch_path.joinpath("requirements.txt") + req_path.write_text("base==0.1.0") + + create_basic_wheel_for_package( + script, + "base", + "0.1.0", + depends=["dep"], + ) + dep_path = create_basic_wheel_for_package( + script, + "dep", + "0.1.0", + ) + + result = script.pip( + "install", + "--no-cache-dir", + "--no-index", + "--find-links", + script.scratch_path, + "-r", + req_path, + ) + assert_re_match( + rf"Processing .*{re.escape(os.path.basename(dep_path))} " + rf"\(from base==0.1.0->-r {re.escape(str(req_path))} \(line 1\)\)", + result.stdout, + ) + + +@pytest.mark.network +def test_install_pip_prints_req_chain_pypi(script: PipTestEnvironment) -> None: + """ + Test installing a package with a dependency from PyPI and check that the + dependency chain is reported. + """ + req_path = script.scratch_path.joinpath("requirements.txt") + req_path.write_text("Paste[openid]==1.7.5.1") + + result = script.pip( + "install", + "-r", + req_path, + ) + + assert ( + f"Collecting python-openid " + f"(from Paste[openid]==1.7.5.1->-r {req_path} (line 1))" in result.stdout + )