From e15e5191fa84fa51bfbe3c1661bd78767dd2743d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sat, 7 May 2022 17:22:41 +0200 Subject: [PATCH 1/5] Add wheel support to InstallRequirement.get_dist() Before it did support only requirements that had their metadata prepared to a local directory. WIth wheels that does not happen so we need to handle that case too. get_dist() is used by the metadata property of InstallRequirement, which in turn is useful to obtain metadata of the RequirementSet returned by the resolver. --- src/pip/_internal/req/req_install.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index e01da2d69ef..a1e376c893a 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -25,7 +25,9 @@ BaseDistribution, get_default_environment, get_directory_distribution, + get_wheel_distribution, ) +from pip._internal.metadata.base import FilesystemWheel from pip._internal.models.direct_url import DirectUrl from pip._internal.models.link import Link from pip._internal.operations.build.metadata import generate_metadata @@ -558,7 +560,16 @@ def metadata(self) -> Any: return self._metadata def get_dist(self) -> BaseDistribution: - return get_directory_distribution(self.metadata_directory) + if self.metadata_directory: + return get_directory_distribution(self.metadata_directory) + elif self.local_file_path and self.is_wheel: + return get_wheel_distribution( + FilesystemWheel(self.local_file_path), canonicalize_name(self.name) + ) + raise AssertionError( + f"InstallRequirement {self} has no metadata directory and no wheel: " + f"can't make a distribution." + ) def assert_source_matches_version(self) -> None: assert self.source_dir From 3726f71720889e9d56f7caa024b2c946b0f5e935 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sat, 7 May 2022 17:23:43 +0200 Subject: [PATCH 2/5] Add a --dry-run option to pip install --- news/11096.feature.rst | 2 ++ src/pip/_internal/commands/install.py | 22 ++++++++++++++++++++++ tests/functional/test_install.py | 9 +++++++++ 3 files changed, 33 insertions(+) create mode 100644 news/11096.feature.rst diff --git a/news/11096.feature.rst b/news/11096.feature.rst new file mode 100644 index 00000000000..c7b2346a295 --- /dev/null +++ b/news/11096.feature.rst @@ -0,0 +1,2 @@ +Add ``--dry-run`` option to ``pip install``, to let it print what it would install but +not actually change anything in the target environment. diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 3634ea04c1e..4e63df1bb4f 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -85,6 +85,17 @@ def add_options(self) -> None: self.cmd_opts.add_option(cmdoptions.pre()) self.cmd_opts.add_option(cmdoptions.editable()) + self.cmd_opts.add_option( + "--dry-run", + action="store_true", + dest="dry_run", + default=False, + help=( + "Don't actually install anything, just print what would be. " + "Can be used in combination with --ignore-installed " + "to 'resolve' the requirements." + ), + ) self.cmd_opts.add_option( "-t", "--target", @@ -342,6 +353,17 @@ def run(self, options: Values, args: List[str]) -> int: reqs, check_supported_wheels=not options.target_dir ) + if options.dry_run: + items = [ + f"{item.name}-{item.metadata['version']}" + for item in sorted( + requirement_set.all_requirements, key=lambda x: str(x.name) + ) + ] + if items: + write_output("Would install %s", " ".join(items)) + return SUCCESS + try: pip_req = requirement_set.get_requirement("pip") except KeyError: diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 883dd2424e1..ad2b4f5e9b3 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -2238,3 +2238,12 @@ def test_install_logs_pip_version_in_debug( result = script.pip("install", "-v", fake_package) pattern = "Using pip .* from .*" assert_re_match(pattern, result.stdout) + + +def test_install_dry_run(script: PipTestEnvironment, data: TestData) -> None: + """Test that pip install --dry-run logs what it would install.""" + result = script.pip( + "install", "--dry-run", "--find-links", data.find_links, "simple" + ) + assert "Would install simple-3.0" in result.stdout + assert "Successfully installed" not in result.stdout From d378007a9ae2c4856cb884a1621b8c7a8ee330cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Mon, 30 May 2022 10:22:39 +0200 Subject: [PATCH 3/5] Use metadata['name'] to get requirement name Co-authored-by: Tzu-ping Chung --- src/pip/_internal/commands/install.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 4e63df1bb4f..191239750eb 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -354,14 +354,15 @@ def run(self, options: Values, args: List[str]) -> int: ) if options.dry_run: - items = [ - f"{item.name}-{item.metadata['version']}" - for item in sorted( - requirement_set.all_requirements, key=lambda x: str(x.name) + would_install_items = sorted( + (r.metadata["name"], r.metadata["version"]) + for r in requirement_set.all_requirements + ) + if would_install_items: + write_output( + "Would install %s", + " ".join("-".join(item) for item in would_install_items), ) - ] - if items: - write_output("Would install %s", " ".join(items)) return SUCCESS try: From f8a54abb3e4039f5c9deef99da13c4d3cedd447f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Mon, 30 May 2022 13:05:28 +0200 Subject: [PATCH 4/5] Improve --dry-run with legacy resolver --- src/pip/_internal/commands/install.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 191239750eb..02bea04501f 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -356,7 +356,9 @@ def run(self, options: Values, args: List[str]) -> int: if options.dry_run: would_install_items = sorted( (r.metadata["name"], r.metadata["version"]) - for r in requirement_set.all_requirements + # Use get_installation_order because it does some important + # filtering with the legacy resolver. + for r in resolver.get_installation_order(requirement_set) ) if would_install_items: write_output( From 701a5d6f50bca0b25290812b42f04e78833e23b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Fri, 3 Jun 2022 19:35:58 +0200 Subject: [PATCH 5/5] Add requirements_to_install to RequirementSet This property is necessary because the legacy resolver returns requirements that need not be installed. --- src/pip/_internal/commands/install.py | 4 +--- src/pip/_internal/req/req_set.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 02bea04501f..9bb99cdf2a5 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -356,9 +356,7 @@ def run(self, options: Values, args: List[str]) -> int: if options.dry_run: would_install_items = sorted( (r.metadata["name"], r.metadata["version"]) - # Use get_installation_order because it does some important - # filtering with the legacy resolver. - for r in resolver.get_installation_order(requirement_set) + for r in requirement_set.requirements_to_install ) if would_install_items: write_output( diff --git a/src/pip/_internal/req/req_set.py b/src/pip/_internal/req/req_set.py index 0f550bf1ea5..ec7a6e07a25 100644 --- a/src/pip/_internal/req/req_set.py +++ b/src/pip/_internal/req/req_set.py @@ -67,3 +67,16 @@ def get_requirement(self, name: str) -> InstallRequirement: @property def all_requirements(self) -> List[InstallRequirement]: return self.unnamed_requirements + list(self.requirements.values()) + + @property + def requirements_to_install(self) -> List[InstallRequirement]: + """Return the list of requirements that need to be installed. + + TODO remove this property together with the legacy resolver, since the new + resolver only returns requirements that need to be installed. + """ + return [ + install_req + for install_req in self.all_requirements + if not install_req.constraint and not install_req.satisfied_by + ]