diff --git a/docs/cli.md b/docs/cli.md index e57b1548e2e..ada0a8764ac 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -510,6 +510,7 @@ required by * `--latest (-l)`: Show the latest version. * `--outdated (-o)`: Show the latest version but only for packages that are outdated. * `--all (-a)`: Show all packages (even those not compatible with current system). +* `--top-level (-T)`: Only show explicitly defined packages. {{% note %}} When `--only` is specified, `--with` and `--without` options are ignored. diff --git a/src/poetry/console/commands/show.py b/src/poetry/console/commands/show.py index a97c0dc34bc..1b8b03e718b 100644 --- a/src/poetry/console/commands/show.py +++ b/src/poetry/console/commands/show.py @@ -64,6 +64,7 @@ class ShowCommand(GroupCommand, EnvCommand): "a", "Show all packages (even those not compatible with current system).", ), + option("top-level", "T", "Show only top-level dependencies."), ] help = """The show command displays detailed information about a package, or @@ -77,6 +78,20 @@ def handle(self) -> int: if self.option("tree"): self.init_styles(self.io) + if self.option("top-level"): + if self.option("tree"): + self.line_error( + "Error: Cannot use --tree and --top-level at the same" + " time." + ) + return 1 + if package is not None: + self.line_error( + "Error: Cannot use --top-level when displaying a single" + " package." + ) + return 1 + if self.option("why"): if self.option("tree") and package is None: self.line_error( @@ -215,6 +230,7 @@ def _display_packages_information( show_latest = self.option("latest") show_all = self.option("all") + show_top_level = self.option("top-level") width = shutil.get_terminal_size().columns name_length = version_length = latest_length = required_by_length = 0 latest_packages = {} @@ -296,10 +312,18 @@ def _display_packages_information( write_why = self.option("why") and (why_end_column + 3) <= width write_description = (why_end_column + 24) <= width + requires = root.all_requires + for locked in locked_packages: color = "cyan" name = locked.pretty_name install_marker = "" + + if show_top_level and not any( + locked.is_same_package_as(r) for r in requires + ): + continue + if locked not in required_locked_packages: if not show_all: continue diff --git a/tests/console/commands/test_show.py b/tests/console/commands/test_show.py index c7d15f7ce6c..821e5b87c85 100644 --- a/tests/console/commands/test_show.py +++ b/tests/console/commands/test_show.py @@ -2082,6 +2082,125 @@ def test_url_dependency_is_not_outdated_by_repository_package( assert tester.io.fetch_output() == "" +def test_show_top_level( + tester: CommandTester, poetry: Poetry, installed: Repository +) -> None: + poetry.package.add_dependency(Factory.create_dependency("cachy", "^0.2.0")) + + cachy2 = get_package("cachy", "0.2.0") + cachy2.add_dependency(Factory.create_dependency("msgpack-python", ">=0.5 <0.6")) + + installed.add_package(cachy2) + + poetry.locker.mock_lock_data( + { + "package": [ + { + "name": "cachy", + "version": "0.2.0", + "description": "", + "category": "main", + "optional": False, + "platform": "*", + "python-versions": "*", + "checksum": [], + "dependencies": {"msgpack-python": ">=0.5 <0.6"}, + }, + { + "name": "msgpack-python", + "version": "0.5.1", + "description": "", + "category": "main", + "optional": False, + "platform": "*", + "python-versions": "*", + "checksum": [], + }, + ], + "metadata": { + "python-versions": "*", + "platform": "*", + "content-hash": "123456789", + "files": {"cachy": [], "msgpack-python": []}, + }, + } + ) + + tester.execute("--top-level") + + expected = """cachy 0.2.0 \n""" + + assert tester.io.fetch_output() == expected + + +def test_show_top_level_with_explicitly_defined_depenancy( + tester: CommandTester, poetry: Poetry, installed: Repository +) -> None: + poetry.package.add_dependency(Factory.create_dependency("a", "^0.1.0")) + poetry.package.add_dependency(Factory.create_dependency("b", "^0.2.0")) + + a = get_package("a", "0.1.0") + a.add_dependency(Factory.create_dependency("b", "0.2.0")) + b = get_package("b", "0.2.0") + + installed.add_package(a) + installed.add_package(b) + + poetry.locker.mock_lock_data( + { + "package": [ + { + "name": "a", + "version": "0.1.0", + "description": "", + "category": "main", + "optional": False, + "platform": "*", + "python-versions": "*", + "checksum": [], + "dependencies": {"b": "0.2.0"}, + }, + { + "name": "b", + "version": "0.2.0", + "description": "", + "category": "main", + "optional": False, + "platform": "*", + "python-versions": "*", + "checksum": [], + }, + ], + "metadata": { + "python-versions": "*", + "platform": "*", + "content-hash": "123456789", + "files": {"a": [], "b": []}, + }, + } + ) + + tester.execute("--top-level") + + expected = """a 0.1.0 \nb 0.2.0 \n""" + + assert tester.io.fetch_output() == expected + + +def test_show_error_top_level_with_tree(tester: CommandTester) -> None: + expected = "Error: Cannot use --tree and --top-level at the same time.\n" + tester.execute("--top-level --tree") + assert tester.io.fetch_error() == expected + assert tester.status_code == 1 + + +def test_show_error_top_level_with_single_package(tester: CommandTester) -> None: + expected = "Error: Cannot use --top-level when displaying a single package.\n" + tester.execute("--top-level some_package_name") + assert tester.io.fetch_error() == expected + assert tester.status_code == 1 + + @pytest.mark.parametrize( ("project_directory", "required_fixtures"), [