Skip to content

Commit

Permalink
cmd/show: add --why option (#5444)
Browse files Browse the repository at this point in the history
  • Loading branch information
tgolsson authored May 10, 2022
1 parent 5840ac5 commit d22c5a7
Show file tree
Hide file tree
Showing 3 changed files with 226 additions and 17 deletions.
1 change: 1 addition & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,7 @@ required by
### Options

* `--without`: The dependency groups to ignore.
* `--why`: Include reverse dependencies where applicable.
* `--with`: The optional dependency groups to include.
* `--only`: The only dependency groups to include.
* `--default`: Only include the main dependencies. (**Deprecated**)
Expand Down
127 changes: 110 additions & 17 deletions src/poetry/console/commands/show.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@
from poetry.repositories.repository import Repository


def reverse_deps(pkg: Package, repo: Repository) -> dict[str, str]:
required_by = {}
for locked in repo.packages:
dependencies = {d.name: d.pretty_constraint for d in locked.requires}

if pkg.name in dependencies:
required_by[locked.pretty_name] = dependencies[pkg.name]

return required_by


class ShowCommand(GroupCommand):

name = "show"
Expand All @@ -36,6 +47,11 @@ class ShowCommand(GroupCommand):
"Do not list the development dependencies. (<warning>Deprecated</warning>)",
),
option("tree", "t", "List the dependencies as a tree."),
option(
"why",
None,
"When listing the tree for a single package, start from parents.",
),
option("latest", "l", "Show the latest version."),
option(
"outdated",
Expand Down Expand Up @@ -69,6 +85,23 @@ def handle(self) -> int | None:
if self.option("tree"):
self.init_styles(self.io)

if self.option("why"):
if self.option("tree") and package is None:
self.line_error(
"<error>Error: --why requires a package when combined with"
" --tree.</error>"
)

return 1

if not self.option("tree") and package:
self.line_error(
"<error>Error: --why cannot be used without --tree when displaying"
" a single package.</error>"
)

return 1

if self.option("outdated"):
self._io.input.set_option("latest", True)

Expand All @@ -83,7 +116,7 @@ def handle(self) -> int | None:
root = self.project_with_activated_groups_only()

# Show tree view if requested
if self.option("tree") and not package:
if self.option("tree") and package is None:
requires = root.all_requires
packages = locked_repo.packages
for p in packages:
Expand Down Expand Up @@ -121,17 +154,38 @@ def handle(self) -> int | None:
if not pkg:
raise ValueError(f"Package {package} not found")

required_by = reverse_deps(pkg, locked_repo)

if self.option("tree"):
self.display_package_tree(self.io, pkg, locked_repo)
if self.option("why"):
# The default case if there's no reverse dependencies is to query
# the subtree for pkg but if any rev-deps exist we'll query for each
# of them in turn
packages = [pkg]
if required_by:
packages = [
p
for p in locked_packages
for r in required_by.keys()
if p.name == r
]
else:
# if no rev-deps exist we'll make this clear as it can otherwise
# look very odd for packages that also have no or few direct
# dependencies
self._io.write_line(
f"Package {package} is a direct dependency."
)

return 0
for p in packages:
self.display_package_tree(
self._io, p, locked_repo, why_package=pkg
)

required_by = {}
for locked in locked_packages:
dependencies = {d.name: d.pretty_constraint for d in locked.requires}
else:
self.display_package_tree(self._io, pkg, locked_repo)

if pkg.name in dependencies:
required_by[locked.pretty_name] = dependencies[pkg.name]
return 0

rows = [
["<info>name</>", f" : <c1>{pkg.pretty_name}</>"],
Expand Down Expand Up @@ -163,7 +217,7 @@ def handle(self) -> int | None:
show_all = self.option("all")
terminal = Terminal()
width = terminal.width
name_length = version_length = latest_length = 0
name_length = version_length = latest_length = required_by_length = 0
latest_packages = {}
latest_statuses = {}
installed_repo = InstalledRepository.load(self.env)
Expand Down Expand Up @@ -208,6 +262,13 @@ def handle(self) -> int | None:
)
),
)

if self.option("why"):
required_by = reverse_deps(locked, locked_repo)
required_by_length = max(
required_by_length,
len(" from " + ",".join(required_by.keys())),
)
else:
name_length = max(name_length, current_length)
version_length = max(
Expand All @@ -219,9 +280,20 @@ def handle(self) -> int | None:
),
)

if self.option("why"):
required_by = reverse_deps(locked, locked_repo)
required_by_length = max(
required_by_length, len(" from " + ",".join(required_by.keys()))
)

write_version = name_length + version_length + 3 <= width
write_latest = name_length + version_length + latest_length + 3 <= width
write_description = name_length + version_length + latest_length + 24 <= width

why_end_column = (
name_length + version_length + latest_length + required_by_length
)
write_why = self.option("why") and (why_end_column + 3) <= width
write_description = (why_end_column + 24) <= width

for locked in locked_packages:
color = "cyan"
Expand Down Expand Up @@ -273,9 +345,21 @@ def handle(self) -> int | None:
)
line += f" <fg={color}>{version:{latest_length}}</>"

if write_why:
required_by = reverse_deps(locked, locked_repo)
if required_by:
content = ",".join(required_by.keys())
# subtract 6 for ' from '
line += f" from {content:{required_by_length - 6}}"
else:
line += " " * required_by_length

if write_description:
description = locked.description
remaining = width - name_length - version_length - 4
remaining = (
width - name_length - version_length - required_by_length - 4
)

if show_latest:
remaining -= latest_length

Expand All @@ -285,10 +369,15 @@ def handle(self) -> int | None:
line += " " + description

self.line(line)

return None

def display_package_tree(
self, io: IO, package: Package, installed_repo: Repository
self,
io: IO,
package: Package,
installed_repo: Repository,
why_package: Package | None = None,
) -> None:
io.write(f"<c1>{package.pretty_name}</c1>")
description = ""
Expand All @@ -297,11 +386,15 @@ def display_package_tree(

io.write_line(f" <b>{package.pretty_version}</b>{description}")

dependencies = package.requires
dependencies = sorted(
dependencies,
key=lambda x: x.name, # type: ignore[no-any-return]
)
if why_package is not None:
dependencies = [p for p in package.requires if p.name == why_package.name]
else:
dependencies = package.requires
dependencies = sorted(
dependencies,
key=lambda x: x.name, # type: ignore[no-any-return]
)

tree_bar = "├"
total = len(dependencies)
for i, dependency in enumerate(dependencies, 1):
Expand Down
115 changes: 115 additions & 0 deletions tests/console/commands/test_show.py
Original file line number Diff line number Diff line change
Expand Up @@ -1686,6 +1686,121 @@ def test_show_tree_no_dev(tester: CommandTester, poetry: Poetry, installed: Repo
assert tester.io.fetch_output() == expected


def test_show_tree_why_package(
tester: CommandTester, poetry: Poetry, installed: Repository
):
poetry.package.add_dependency(Factory.create_dependency("a", "=0.0.1"))

a = get_package("a", "0.0.1")
installed.add_package(a)
a.add_dependency(Factory.create_dependency("b", "=0.0.1"))

b = get_package("b", "0.0.1")
a.add_dependency(Factory.create_dependency("c", "=0.0.1"))
installed.add_package(b)

c = get_package("c", "0.0.1")
installed.add_package(c)

poetry.locker.mock_lock_data(
{
"package": [
{
"name": "a",
"version": "0.0.1",
"dependencies": {"b": "=0.0.1"},
"python-versions": "*",
"optional": False,
},
{
"name": "b",
"version": "0.0.1",
"dependencies": {"c": "=0.0.1"},
"python-versions": "*",
"optional": False,
},
{
"name": "c",
"version": "0.0.1",
"python-versions": "*",
"optional": False,
},
],
"metadata": {
"python-versions": "*",
"platform": "*",
"content-hash": "123456789",
"hashes": {"a": [], "b": [], "c": []},
},
}
)

tester.execute("--tree --why b")

expected = """\
a 0.0.1
└── b =0.0.1
└── c =0.0.1 \n"""

assert tester.io.fetch_output() == expected


def test_show_tree_why(tester: CommandTester, poetry: Poetry, installed: Repository):
poetry.package.add_dependency(Factory.create_dependency("a", "=0.0.1"))

a = get_package("a", "0.0.1")
installed.add_package(a)
a.add_dependency(Factory.create_dependency("b", "=0.0.1"))

b = get_package("b", "0.0.1")
a.add_dependency(Factory.create_dependency("c", "=0.0.1"))
installed.add_package(b)

c = get_package("c", "0.0.1")
installed.add_package(c)

poetry.locker.mock_lock_data(
{
"package": [
{
"name": "a",
"version": "0.0.1",
"dependencies": {"b": "=0.0.1"},
"python-versions": "*",
"optional": False,
},
{
"name": "b",
"version": "0.0.1",
"dependencies": {"c": "=0.0.1"},
"python-versions": "*",
"optional": False,
},
{
"name": "c",
"version": "0.0.1",
"python-versions": "*",
"optional": False,
},
],
"metadata": {
"python-versions": "*",
"platform": "*",
"content-hash": "123456789",
"hashes": {"a": [], "b": [], "c": []},
},
}
)

tester.execute("--why")

# this has to be on a single line due to the padding whitespace, which gets stripped
# by pre-commit.
expected = """a 0.0.1 \nb 0.0.1 from a \nc 0.0.1 from b \n"""

assert tester.io.fetch_output() == expected


def test_show_required_by_deps(
tester: CommandTester, poetry: Poetry, installed: Repository
):
Expand Down

0 comments on commit d22c5a7

Please sign in to comment.