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..9bb99cdf2a5 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,18 @@ def run(self, options: Values, args: List[str]) -> int: reqs, check_supported_wheels=not options.target_dir ) + if options.dry_run: + would_install_items = sorted( + (r.metadata["name"], r.metadata["version"]) + for r in requirement_set.requirements_to_install + ) + if would_install_items: + write_output( + "Would install %s", + " ".join("-".join(item) for item in would_install_items), + ) + return SUCCESS + try: pip_req = requirement_set.get_requirement("pip") except KeyError: 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 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 + ] 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