Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -1173,6 +1173,7 @@ required by
* `--all (-a)`: Show all packages (even those not compatible with current system).
* `--top-level (-T)`: Only show explicitly defined packages.
* `--no-truncate`: Do not truncate the output based on the terminal width.
* `--source`: Show the source of each package (`PyPI`, a configured source name, or a direct source URL).
* `--format (-f)`: Specify the output format (`json` or `text`). Default is `text`. `json` cannot be combined with the `--tree` option.

{{% note %}}
Expand Down
68 changes: 62 additions & 6 deletions src/poetry/console/commands/show.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@ def reverse_deps(pkg: Package, repo: Repository) -> dict[str, str]:
return required_by


def get_package_source(package: Package) -> str:
if package.source_type is None:
return "PyPI"

if package.source_type == "legacy":
return package.source_reference or package.source_url or "PyPI"

return package.source_url or package.source_reference or package.source_type


class OutputFormats(str, Enum):
JSON = "json"
TEXT = "text"
Expand Down Expand Up @@ -79,6 +89,7 @@ class ShowCommand(GroupCommand, EnvCommand):
None,
"Do not truncate the output based on the terminal width.",
),
option("source", None, "Show the source of each package."),
option(
"format",
"f",
Expand Down Expand Up @@ -143,6 +154,12 @@ def handle(self) -> int:
)
return 1

if self.option("source") and self.option("tree"):
self.line_error(
"<error>Error: --source cannot be used with --tree.</error>"
)
return 1

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

Expand Down Expand Up @@ -229,6 +246,8 @@ def _display_single_package_information(
"version": pkg.pretty_version,
"description": pkg.description,
}
if self.option("source"):
package_info["source"] = get_package_source(pkg)
if pkg.requires:
package_info["dependencies"] = {
dependency.pretty_name: dependency.pretty_constraint
Expand All @@ -246,6 +265,10 @@ def _display_single_package_information(
["<info>version</>", f" : <b>{pkg.pretty_version}</b>"],
["<info>description</>", f" : {pkg.description}"],
]
if self.option("source"):
rows.insert(
2, ["<info>source</>", f" : <c1>{get_package_source(pkg)}</c1>"]
)

self.table(rows=rows, style="compact").render()

Expand Down Expand Up @@ -304,12 +327,14 @@ def _display_packages_information(
show_all = self.option("all")
show_top_level = self.option("top-level")
show_why = self.option("why")
show_source = self.option("source")
width = (
sys.maxsize
if self.option("no-truncate")
else shutil.get_terminal_size().columns
)
name_length = version_length = latest_length = required_by_length = 0
name_length = version_length = latest_length = source_length = 0
required_by_length = 0
latest_packages = {}
latest_statuses = {}
installed_repo = InstalledRepository.load(self.env)
Expand Down Expand Up @@ -344,7 +369,9 @@ def _display_packages_information(
version_length = max(
version_length,
len(
get_package_version_display_string(
locked.pretty_version
if show_source and locked.is_direct_origin()
else get_package_version_display_string(
locked, root=self.poetry.file.path.parent
)
),
Expand All @@ -364,12 +391,19 @@ def _display_packages_information(
required_by_length,
len(" from " + ",".join(required_by.keys())),
)

if show_source:
source_length = max(
source_length, len(get_package_source(locked))
)
else:
name_length = max(name_length, current_length)
version_length = max(
version_length,
len(
get_package_version_display_string(
locked.pretty_version
if show_source and locked.is_direct_origin()
else get_package_version_display_string(
locked, root=self.poetry.file.path.parent
)
),
Expand All @@ -381,6 +415,9 @@ def _display_packages_information(
required_by_length, len(" from " + ",".join(required_by.keys()))
)

if show_source:
source_length = max(source_length, len(get_package_source(locked)))

if self.option("format") == OutputFormats.JSON:
packages = []

Expand Down Expand Up @@ -413,6 +450,9 @@ def _display_packages_information(
latest, root=self.poetry.file.path.parent
)

if show_source:
package["source"] = get_package_source(locked)

if show_why:
required_by = reverse_deps(locked, locked_repository)
if required_by:
Expand All @@ -429,8 +469,14 @@ def _display_packages_information(
write_version = name_length + version_length + 3 <= width
write_latest = name_length + version_length + latest_length + 3 <= width

source_end_column = name_length + version_length + latest_length + source_length
write_source = show_source and (source_end_column + 3) <= width
why_end_column = (
name_length + version_length + latest_length + required_by_length
name_length
+ version_length
+ latest_length
+ (source_length if write_source else 0)
+ required_by_length
)
write_why = show_why and (why_end_column + 3) <= width
write_description = (why_end_column + 24) <= width
Expand Down Expand Up @@ -471,8 +517,12 @@ def _display_packages_information(
f"{name:{name_length - len(install_marker)}}{install_marker}</>"
)
if write_version:
version = get_package_version_display_string(
locked, root=self.poetry.file.path.parent
version = (
locked.pretty_version
if show_source and locked.is_direct_origin()
else get_package_version_display_string(
locked, root=self.poetry.file.path.parent
)
)
line += f" <b>{version:{version_length}}</b>"
if show_latest:
Expand All @@ -491,6 +541,9 @@ def _display_packages_information(
)
line += f" <fg={color}>{version:{latest_length}}</>"

if write_source:
line += f" {get_package_source(locked):{source_length}}"

if write_why:
required_by = reverse_deps(locked, locked_repository)
if required_by:
Expand All @@ -509,6 +562,9 @@ def _display_packages_information(
if show_latest:
remaining -= latest_length

if write_source:
remaining -= source_length + 1

if len(locked.description) > remaining:
description = description[: remaining - 3] + "..."

Expand Down
165 changes: 165 additions & 0 deletions tests/console/commands/test_show.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,171 @@ def test_show_basic_with_installed_packages(
assert tester.io.fetch_output() == expected


@output_format_parametrize
def test_show_with_source(
output_format: str,
tester: CommandTester,
poetry: Poetry,
installed: Repository,
) -> None:
poetry.package.add_dependency(Factory.create_dependency("cachy", "^0.1.0"))
poetry.package.add_dependency(
Factory.create_dependency(
"internal-package",
{"version": "^1.0.0", "source": "internal"},
)
)

cachy_010 = get_package("cachy", "0.1.0")
cachy_010.description = "Cachy package"
internal_package_100 = get_package("internal-package", "1.0.0")
internal_package_100.description = "Internal package"

installed.add_package(cachy_010)
installed.add_package(internal_package_100)

assert isinstance(poetry.locker, DummyLocker)
poetry.locker.mock_lock_data(
{
"package": [
{
"name": "cachy",
"version": "0.1.0",
"description": "Cachy package",
"optional": False,
"platform": "*",
"python-versions": "*",
"checksum": [],
},
{
"name": "internal-package",
"version": "1.0.0",
"description": "Internal package",
"optional": False,
"platform": "*",
"python-versions": "*",
"checksum": [],
"source": {
"type": "legacy",
"url": "https://example.com/simple/",
"reference": "internal",
},
},
],
"metadata": {
"python-versions": "*",
"platform": "*",
"content-hash": "123456789",
"files": {"cachy": [], "internal-package": []},
},
}
)

tester.execute(f"--source {output_format}")

expected: str | list[dict[str, str]] = ""
if "json" in output_format:
expected = [
{
"name": "cachy",
"installed_status": "installed",
"version": "0.1.0",
"source": "PyPI",
"description": "Cachy package",
},
{
"name": "internal-package",
"installed_status": "installed",
"version": "1.0.0",
"source": "internal",
"description": "Internal package",
},
]
assert json.loads(tester.io.fetch_output()) == expected
else:
expected = """\
cachy 0.1.0 PyPI Cachy package
internal-package 1.0.0 internal Internal package
"""
assert tester.io.fetch_output() == expected


def test_show_with_source_long_direct_url_keeps_descriptions(
tester: CommandTester,
poetry: Poetry,
installed: Repository,
) -> None:
direct_url = "https://example.com/aaaaaaaaaaa.whl"
poetry.package.add_dependency(
Factory.create_dependency("regular-package", "^1.0.0")
)
poetry.package.add_dependency(
Factory.create_dependency("direct-package", {"url": direct_url})
)

regular_package = get_package("regular-package", "1.0.0")
regular_package.description = "Regular package"
direct_package = get_package("direct-package", "1.0.0")
direct_package.description = "Direct package"
direct_package._source_type = "url"
direct_package._source_url = direct_url

installed.add_package(regular_package)
installed.add_package(direct_package)

assert isinstance(poetry.locker, DummyLocker)
poetry.locker.mock_lock_data(
{
"package": [
{
"name": "regular-package",
"version": "1.0.0",
"description": "Regular package",
"optional": False,
"platform": "*",
"python-versions": "*",
"checksum": [],
},
{
"name": "direct-package",
"version": "1.0.0",
"description": "Direct package",
"optional": False,
"platform": "*",
"python-versions": "*",
"checksum": [],
"source": {
"type": "url",
"url": direct_url,
},
},
],
"metadata": {
"python-versions": "*",
"platform": "*",
"content-hash": "123456789",
"files": {"regular-package": [], "direct-package": []},
},
}
)

tester.execute("--source --no-truncate")

source_width = len(direct_url)
expected = (
f"{'regular-package':<15} {'1.0.0':<5} {'PyPI':<{source_width}} Regular package\n"
f"{'direct-package':<15} {'1.0.0':<5} {direct_url:<{source_width}} Direct package\n"
)
assert tester.io.fetch_output() == expected


def test_show_with_source_rejects_tree(tester: CommandTester) -> None:
tester.execute("--source --tree")

assert tester.status_code == 1
assert tester.io.fetch_error() == "Error: --source cannot be used with --tree.\n"


def _configure_project_with_groups(poetry: Poetry, installed: Repository) -> None:
poetry.package.add_dependency(Factory.create_dependency("cachy", "^0.1.0"))

Expand Down
Loading