Skip to content

Commit f78a613

Browse files
authored
Merge pull request #710 from rd4398/list-overrides-file-export
Add CSV and JSON export to list-overrides --details
2 parents 8b50a79 + ce8dd7f commit f78a613

2 files changed

Lines changed: 372 additions & 37 deletions

File tree

Lines changed: 119 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1+
import csv
2+
import json
13
import pathlib
4+
import sys
25

36
import click
47
import rich
58
from packaging.version import Version
69
from rich.table import Table
710

8-
from fromager import context
11+
from fromager import clickext, context
912
from fromager.packagesettings import PatchMap
1013

1114

@@ -16,28 +19,50 @@
1619
default=False,
1720
help="Show more details about the overrides.",
1821
)
22+
@click.option(
23+
"--format",
24+
"output_format",
25+
type=click.Choice(["table", "csv", "json"], case_sensitive=False),
26+
default="table",
27+
help="Output format for detailed view (requires --details, default: table)",
28+
)
29+
@click.option(
30+
"-o",
31+
"--output",
32+
type=clickext.ClickPath(),
33+
help="Output file to create (requires --details, default: stdout)",
34+
)
1935
@click.pass_obj
2036
def list_overrides(
2137
wkctx: context.WorkContext,
2238
details: bool,
39+
output_format: str,
40+
output: pathlib.Path | None,
2341
) -> None:
2442
"""List all of the packages with overrides in the current configuration."""
43+
# Warn if format/output options are used without --details
44+
if not details:
45+
if output_format != "table":
46+
click.echo(
47+
"Warning: --format option is ignored when --details is not used",
48+
err=True,
49+
)
50+
if output is not None:
51+
click.echo(
52+
"Warning: --output option is ignored when --details is not used",
53+
err=True,
54+
)
55+
2556
overridden_packages = sorted(wkctx.settings.list_overrides())
2657
if not details:
2758
for name in overridden_packages:
2859
print(name)
2960
return
3061

31-
table = Table(title="Package Overrides")
32-
table.add_column("Package", justify="left", no_wrap=True)
33-
table.add_column("Version", justify="left", no_wrap=True)
34-
table.add_column("Patches", justify="left", no_wrap=True)
35-
62+
# Collect data for export
3663
variants = sorted(wkctx.settings.all_variants())
37-
for v in variants:
38-
table.add_column(v, justify="left", no_wrap=True)
39-
40-
table.add_column("Plugin", justify="left")
64+
variant_names = [str(v) for v in variants]
65+
export_data = []
4166

4267
for name in overridden_packages:
4368
pbi = wkctx.settings.package_build_info(name)
@@ -68,16 +93,16 @@ def list_overrides(
6893
plugin_hooks.append(hook)
6994
plugin_hooks_str = ", ".join(plugin_hooks)
7095

71-
variant_info: list[str] = []
96+
variant_info: dict[str, str] = {}
7297
for v in variants:
7398
v_info = ps.variants.get(v)
7499
if v_info:
75100
if v_info.pre_built:
76-
variant_info.append("pre-built")
101+
variant_info[str(v)] = "pre-built"
77102
else:
78-
variant_info.append("yes")
103+
variant_info[str(v)] = "yes"
79104
else:
80-
variant_info.append("")
105+
variant_info[str(v)] = ""
81106

82107
all_patches: PatchMap = pbi.get_all_patches()
83108
global_patches: list[pathlib.Path] = all_patches.get(None, [])
@@ -87,38 +112,95 @@ def list_overrides(
87112
[v for v in all_patches.keys() if v is not None]
88113
)
89114

90-
row: list[str] = []
91-
patches_str: str = ""
92-
93115
if not all_pkg_versions:
94116
# This package has overrides, but none are version-specific.
95117
patches_str = str(num_global_patches) if num_global_patches else ""
96-
row = (
97-
[
98-
name,
99-
"", # Version
100-
patches_str,
101-
]
102-
+ variant_info
103-
+ [plugin_hooks_str]
104-
)
105-
table.add_row(*row)
118+
row_data = {
119+
"package": name,
120+
"version": "",
121+
"patches": patches_str,
122+
"plugin_hooks": plugin_hooks_str,
123+
}
124+
# Add variant information
125+
row_data.update(variant_info)
126+
export_data.append(row_data)
106127
else:
107128
# This package has version-specific overrides.
108129
for version in all_pkg_versions:
109130
version_patches: list[pathlib.Path] = all_patches.get(version, [])
110131
total_patches: int = num_global_patches + len(version_patches)
111132
patches_str = str(total_patches) if total_patches else ""
112133

113-
row = (
114-
[
115-
name,
116-
str(version),
117-
patches_str,
118-
]
119-
+ variant_info
120-
+ [plugin_hooks_str]
121-
)
122-
table.add_row(*row)
134+
row_data = {
135+
"package": name,
136+
"version": str(version),
137+
"patches": patches_str,
138+
"plugin_hooks": plugin_hooks_str,
139+
}
140+
# Add variant information
141+
row_data.update(variant_info)
142+
export_data.append(row_data)
143+
144+
# Handle different output formats
145+
match output_format:
146+
case "json":
147+
_export_json(export_data, output)
148+
case "csv":
149+
_export_csv(export_data, variant_names, output)
150+
case "table":
151+
_export_table(export_data, variant_names)
152+
case _:
153+
raise ValueError(f"Invalid output format: {output_format}")
154+
155+
156+
def _export_json(data: list[dict], output: pathlib.Path | None) -> None:
157+
"""Export data as JSON."""
158+
if output:
159+
with open(output, "w") as outfile:
160+
json.dump(data, outfile, indent=2)
161+
else:
162+
json.dump(data, sys.stdout, indent=2)
163+
164+
165+
def _export_csv(
166+
data: list[dict], variants: list[str], output: pathlib.Path | None
167+
) -> None:
168+
"""Export data as CSV."""
169+
# Define field names in the order we want them
170+
fieldnames = ["package", "version", "patches"] + variants + ["plugin_hooks"]
171+
172+
if output:
173+
with open(output, "w", newline="") as outfile:
174+
writer = csv.DictWriter(
175+
outfile, fieldnames=fieldnames, quoting=csv.QUOTE_NONNUMERIC
176+
)
177+
writer.writeheader()
178+
writer.writerows(data)
179+
else:
180+
writer = csv.DictWriter(
181+
sys.stdout, fieldnames=fieldnames, quoting=csv.QUOTE_NONNUMERIC
182+
)
183+
writer.writeheader()
184+
writer.writerows(data)
185+
186+
187+
def _export_table(data: list[dict], variants: list[str]) -> None:
188+
"""Export data as Rich table (original behavior)."""
189+
table = Table(title="Package Overrides")
190+
table.add_column("Package", justify="left", no_wrap=True)
191+
table.add_column("Version", justify="left", no_wrap=True)
192+
table.add_column("Patches", justify="left", no_wrap=True)
193+
194+
for v in variants:
195+
table.add_column(v, justify="left", no_wrap=True)
196+
197+
table.add_column("Plugin", justify="left")
198+
199+
# Define column keys in the same order as CSV exporter
200+
column_keys = ["package", "version", "patches"] + variants + ["plugin_hooks"]
201+
202+
for row_data in data:
203+
row = [row_data.get(key, "") for key in column_keys]
204+
table.add_row(*row)
123205

124206
rich.get_console().print(table)

0 commit comments

Comments
 (0)