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
42 changes: 20 additions & 22 deletions src/fromager/commands/list_overrides.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import click
import rich
import rich.console
from packaging.version import Version
from rich.table import Table

Expand All @@ -26,13 +27,13 @@
"output_format",
type=click.Choice(["table", "csv", "json"], case_sensitive=False),
default="table",
help="Output format for detailed view (requires --details, default: table)",
help="Output format for detailed view (used with --details, default: table)",
)
@click.option(
"-o",
"--output",
type=clickext.ClickPath(),
help="Output file to create (requires --details, default: stdout)",
help="Write output to file instead of stdout",
)
@click.pass_obj
def list_overrides(
Expand All @@ -42,23 +43,15 @@ def list_overrides(
output: pathlib.Path | None,
) -> None:
"""List all of the packages with overrides in the current configuration."""
# Warn if format/output options are used without --details
if not details:
if output_format != "table":
click.echo(
"Warning: --format option is ignored when --details is not used",
err=True,
)
if output is not None:
click.echo(
"Warning: --output option is ignored when --details is not used",
err=True,
)

overridden_packages = sorted(wkctx.settings.list_overrides())
if not details:
for name in overridden_packages:
print(name)
if output:
with open(output, "w") as fh:
for name in overridden_packages:
print(name, file=fh)
else:
for name in overridden_packages:
print(name)
return

# Collect data for export
Expand Down Expand Up @@ -140,7 +133,7 @@ def list_overrides(
case "csv":
_export_csv(export_data, variant_names, output)
case "table":
_export_table(export_data, variant_names)
_export_table(export_data, variant_names, output)
case _:
raise ValueError(f"Invalid output format: {output_format}")

Expand Down Expand Up @@ -180,8 +173,10 @@ def _export_csv(
writer.writerows(data)


def _export_table(data: list[dict], variants: list[str]) -> None:
"""Export data as Rich table (original behavior)."""
def _export_table(
data: list[dict], variants: list[str], output: pathlib.Path | None = None
) -> None:
"""Export data as Rich table."""
table = Table(title="Package Overrides")
table.add_column("Package", justify="left", no_wrap=True)
table.add_column("Version", justify="left", no_wrap=True)
Expand All @@ -194,7 +189,6 @@ def _export_table(data: list[dict], variants: list[str]) -> None:

table.add_column("Plugin", justify="left")

# Define column keys in the same order as CSV exporter
column_keys = (
["package", "version", "patches", "min_release_age"]
+ variants
Expand All @@ -205,4 +199,8 @@ def _export_table(data: list[dict], variants: list[str]) -> None:
row = [row_data.get(key, "") for key in column_keys]
table.add_row(*row)

rich.get_console().print(table)
if output:
with open(output, "w") as fh:
rich.console.Console(file=fh, width=120).print(table)
else:
rich.get_console().print(table)
37 changes: 21 additions & 16 deletions src/fromager/commands/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ def package() -> None:
"-o",
"--output",
type=clickext.ClickPath(),
help="Output file (default: stdout)",
help="Write output to file instead of stdout",
)
@click.option(
"--ignore-per-package-overrides",
Expand Down Expand Up @@ -228,18 +228,12 @@ def list_versions(
provider.supports_upload_time,
)

if output is not None and output_format in ("versions", "requirements"):
click.echo(
"Warning: --output option is ignored for 'versions' and 'requirements' formats",
err=True,
)

match output_format:
case "versions":
_export_versions_plain(version_rows, req.name, cooldown)
_export_versions_plain(version_rows, req.name, cooldown, output=output)
case "requirements":
_export_versions_plain(
version_rows, req.name, cooldown, as_requirements=True
version_rows, req.name, cooldown, as_requirements=True, output=output
)
case "table":
_export_versions_table(version_rows, req.name, cooldown, output)
Expand Down Expand Up @@ -350,15 +344,26 @@ def _export_versions_plain(
cooldown: Cooldown | None,
*,
as_requirements: bool = False,
output: pathlib.Path | None = None,
) -> None:
"""Export versions as a plain list, filtering out cooldown-blocked entries."""
for row in data:
if cooldown is not None and row["cooldown"] == "blocked":
continue
if as_requirements:
print(f"{package_name}=={row['version']}")
else:
print(row["version"])
fh: typing.TextIO
if output:
fh = open(output, "w")
else:
fh = sys.stdout

try:
for row in data:
if cooldown is not None and row["cooldown"] == "blocked":
continue
if as_requirements:
print(f"{package_name}=={row['version']}", file=fh)
else:
print(row["version"], file=fh)
finally:
if output:
fh.close()


def _export_versions_json(
Expand Down
46 changes: 13 additions & 33 deletions tests/test_list_overrides.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,38 +203,16 @@ def test_list_overrides_format_option_help(cli_runner: CliRunner) -> None:
)
assert result.exit_code == 0
assert "--format [table|csv|json]" in result.stdout
assert "requires --details" in result.stdout


def test_list_overrides_warnings_without_details(
testdata_path: pathlib.Path, cli_runner: CliRunner
def test_list_overrides_output_file_plain(
testdata_path: pathlib.Path, cli_runner: CliRunner, tmp_path: pathlib.Path
) -> None:
"""Test that warnings are shown when using format/output without details."""
"""--output writes plain name list to a file when --details is not used."""
settings_file = testdata_path / "context" / "overrides" / "settings.yaml"
patches_dir = testdata_path / "context" / "overrides" / "patches"
output_file = tmp_path / "overrides.txt"

# Test format warning
result = cli_runner.invoke(
fromager,
[
"--settings-file",
str(settings_file),
"--patches-dir",
str(patches_dir),
"list-overrides",
"--format",
"json",
],
catch_exceptions=False,
)
assert result.exit_code == 0
assert (
"Warning: --format option is ignored when --details is not used"
in result.output
)
assert "test-other-pkg" in result.output

# Test output warning
result = cli_runner.invoke(
fromager,
[
Expand All @@ -244,16 +222,18 @@ def test_list_overrides_warnings_without_details(
str(patches_dir),
"list-overrides",
"--output",
"test.txt",
str(output_file),
],
catch_exceptions=False,
)
assert result.exit_code == 0
assert (
"Warning: --output option is ignored when --details is not used"
in result.output
)
assert "test-other-pkg" in result.output
assert output_file.exists()
content = output_file.read_text()
assert "test-other-pkg" in content
assert "test-pkg" in content
assert "test-pkg-library" in content
# stdout should be empty (no data printed to console)
assert "test-other-pkg" not in result.stdout
assert "test-pkg-library" not in result.stdout

Comment on lines +234 to 237

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Complete the stdout leak assertion for plain output.

The test misses test-pkg in the “not in stdout” checks, so a partial stdout leak could still pass.

Suggested patch
     # stdout should be empty (no data printed to console)
     assert "test-other-pkg" not in result.stdout
+    assert "test-pkg" not in result.stdout
     assert "test-pkg-library" not in result.stdout
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# stdout should be empty (no data printed to console)
assert "test-other-pkg" not in result.stdout
assert "test-pkg-library" not in result.stdout
# stdout should be empty (no data printed to console)
assert "test-other-pkg" not in result.stdout
assert "test-pkg" not in result.stdout
assert "test-pkg-library" not in result.stdout
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_list_overrides.py` around lines 234 - 237, The test's stdout leak
assertions are incomplete: add an assertion to check that "test-pkg" is not
present in result.stdout alongside the existing checks for "test-other-pkg" and
"test-pkg-library" so that any plain-output leak of "test-pkg" will fail; update
the assertions around result.stdout in tests/test_list_overrides.py to include
assert "test-pkg" not in result.stdout.


def test_list_overrides_min_release_age(
Expand Down
43 changes: 31 additions & 12 deletions tests/test_list_versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,21 +329,40 @@ def test_list_versions_output_file(
assert len(data) == 3


def test_list_versions_output_ignored_for_plain_formats(
def test_list_versions_output_file_versions_format(
cli_runner: CliRunner, tmp_path: pathlib.Path
) -> None:
"""--output is ignored with a warning for 'versions' and 'requirements' formats."""
"""--output writes plain version list to a file for 'versions' format."""
output_file = tmp_path / "versions.txt"
for fmt in ("versions", "requirements"):
result = _invoke_list_versions(
cli_runner,
extra_args=["--format", fmt, "--output", str(output_file)],
)
assert result.exit_code == 0
assert not output_file.exists(), (
f"--output should be ignored for --format {fmt}"
)
assert "Warning: --output option is ignored" in result.output
result = _invoke_list_versions(
cli_runner,
extra_args=["--format", "versions", "--output", str(output_file)],
)
assert result.exit_code == 0
assert output_file.exists()
content = output_file.read_text()
lines = [line for line in content.strip().split("\n") if line.strip()]
assert "1.2.2" in lines
assert "1.3.2" in lines
assert "2.0.0" in lines

Comment on lines +332 to +348

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Assert stdout remains data-free when --output is used for plain formats.

These tests only validate file contents. They should also verify the command does not emit plain version/pin data to stdout when -o is set.

Suggested patch
 def test_list_versions_output_file_versions_format(
@@
     lines = [line for line in content.strip().split("\n") if line.strip()]
     assert "1.2.2" in lines
     assert "1.3.2" in lines
     assert "2.0.0" in lines
+    assert "1.2.2" not in result.stdout
+    assert "1.3.2" not in result.stdout
+    assert "2.0.0" not in result.stdout
@@
 def test_list_versions_output_file_requirements_format(
@@
     lines = [line for line in content.strip().split("\n") if line.strip()]
     assert "test-pkg==1.2.2" in lines
     assert "test-pkg==1.3.2" in lines
     assert "test-pkg==2.0.0" in lines
+    assert "test-pkg==1.2.2" not in result.stdout
+    assert "test-pkg==1.3.2" not in result.stdout
+    assert "test-pkg==2.0.0" not in result.stdout

Also applies to: 350-365

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_list_versions.py` around lines 332 - 348, Update the two tests
that write to an output file (test_list_versions_output_file_versions_format and
the similar test for pins format) to also assert that the CLI produced no plain
version/pin data on stdout when --output/-o is used: after invoking via
_invoke_list_versions (which returns the click CliRunner Result), add an
assertion like assert result.output.strip() == "" (or result.stdout/strip
depending on test helper) to ensure stdout is empty; keep the existing
file-content assertions unchanged.


def test_list_versions_output_file_requirements_format(
cli_runner: CliRunner, tmp_path: pathlib.Path
) -> None:
"""--output writes requirements pins to a file for 'requirements' format."""
output_file = tmp_path / "requirements.txt"
result = _invoke_list_versions(
cli_runner,
extra_args=["--format", "requirements", "--output", str(output_file)],
)
assert result.exit_code == 0
assert output_file.exists()
content = output_file.read_text()
lines = [line for line in content.strip().split("\n") if line.strip()]
assert "test-pkg==1.2.2" in lines
assert "test-pkg==1.3.2" in lines
assert "test-pkg==2.0.0" in lines


# ---------------------------------------------------------------------------
Expand Down
Loading