woolly 0.3.0__py3-none-any.whl → 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,6 +6,7 @@ To add a new format, create a module in this directory that defines a class
6
6
  inheriting from Reporter and add it to REPORTERS dict.
7
7
  """
8
8
 
9
+ from pathlib import Path
9
10
  from typing import Optional
10
11
 
11
12
  from pydantic import BaseModel, Field
@@ -15,6 +16,7 @@ from woolly.reporters.base import ReportData, Reporter, strip_markup
15
16
  from woolly.reporters.json import JsonReporter
16
17
  from woolly.reporters.markdown import MarkdownReporter
17
18
  from woolly.reporters.stdout import StdoutReporter
19
+ from woolly.reporters.template import TemplateReporter
18
20
 
19
21
 
20
22
  class ReporterInfo(BaseModel):
@@ -32,6 +34,7 @@ REPORTERS: dict[str, type[Reporter]] = {
32
34
  "stdout": StdoutReporter,
33
35
  "markdown": MarkdownReporter,
34
36
  "json": JsonReporter,
37
+ "template": TemplateReporter,
35
38
  }
36
39
 
37
40
  # Aliases for convenience
@@ -39,11 +42,16 @@ ALIASES: dict[str, str] = {
39
42
  "md": "markdown",
40
43
  "console": "stdout",
41
44
  "terminal": "stdout",
45
+ "tpl": "template",
46
+ "jinja": "template",
47
+ "jinja2": "template",
42
48
  }
43
49
 
44
50
 
45
51
  def get_reporter(
46
- format_name: str, console: Optional[Console] = None
52
+ format_name: str,
53
+ console: Optional[Console] = None,
54
+ template_path: Optional[Path] = None,
47
55
  ) -> Optional[Reporter]:
48
56
  """
49
57
  Get an instantiated reporter for the specified format.
@@ -51,6 +59,7 @@ def get_reporter(
51
59
  Args:
52
60
  format_name: Format identifier or alias (e.g., "json", "markdown", "md")
53
61
  console: Console instance for stdout reporter.
62
+ template_path: Path to template file for template reporter.
54
63
 
55
64
  Returns:
56
65
  Instantiated Reporter, or None if not found.
@@ -68,6 +77,10 @@ def get_reporter(
68
77
  if format_name == "stdout" and console:
69
78
  return StdoutReporter(console=console)
70
79
 
80
+ # TemplateReporter needs a template path
81
+ if format_name == "template":
82
+ return TemplateReporter(template_path=template_path)
83
+
71
84
  return reporter_class()
72
85
 
73
86
 
@@ -104,6 +117,7 @@ __all__ = [
104
117
  "StdoutReporter",
105
118
  "MarkdownReporter",
106
119
  "JsonReporter",
120
+ "TemplateReporter",
107
121
  "get_reporter",
108
122
  "list_reporters",
109
123
  "get_available_formats",
woolly/reporters/base.py CHANGED
@@ -51,6 +51,9 @@ class ReportData(BaseModel):
51
51
  language: str
52
52
  registry: str
53
53
 
54
+ # License
55
+ root_license: Optional[str] = None
56
+
54
57
  # Statistics
55
58
  total_dependencies: int
56
59
  packaged_count: int
@@ -65,6 +68,21 @@ class ReportData(BaseModel):
65
68
  optional_missing: int = 0
66
69
  optional_missing_packages: list[str] = Field(default_factory=list)
67
70
 
71
+ # Dev dependency statistics
72
+ dev_dependencies: list[dict] = Field(default_factory=list)
73
+ dev_total: int = 0
74
+ dev_packaged: int = 0
75
+ dev_missing: int = 0
76
+
77
+ # Build dependency statistics
78
+ build_dependencies: list[dict] = Field(default_factory=list)
79
+ build_total: int = 0
80
+ build_packaged: int = 0
81
+ build_missing: int = 0
82
+
83
+ # Features / extras
84
+ features: list[Any] = Field(default_factory=list)
85
+
68
86
  # Full tree for detailed reports
69
87
  tree: Any # Rich Tree object - not JSON serializable
70
88
 
@@ -73,6 +91,13 @@ class ReportData(BaseModel):
73
91
  max_depth: int = 50
74
92
  version: Optional[str] = None
75
93
 
94
+ # Fedora targeting
95
+ fedora_release: Optional[str] = None
96
+ fedora_repos: Optional[list[str]] = None
97
+
98
+ # Display options
99
+ missing_only: bool = False
100
+
76
101
  @cached_property
77
102
  def required_missing_packages(self) -> set[str]:
78
103
  """Get the set of required (non-optional) missing packages."""
woolly/reporters/json.py CHANGED
@@ -18,6 +18,7 @@ class TreeNodeData(BaseModel):
18
18
  raw: str
19
19
  name: Optional[str] = None
20
20
  version: Optional[str] = None
21
+ license: Optional[str] = None
21
22
  optional: bool = False
22
23
  status: Optional[str] = None
23
24
  is_packaged: Optional[bool] = None
@@ -34,9 +35,28 @@ class ReportMetadata(BaseModel):
34
35
  root_package: str
35
36
  language: str
36
37
  registry: str
38
+ license: Optional[str] = None
37
39
  version: Optional[str] = None
38
40
  max_depth: int
39
41
  include_optional: bool
42
+ missing_only: bool = False
43
+
44
+
45
+ class DevBuildDepData(BaseModel):
46
+ """Structured data for a dev or build dependency."""
47
+
48
+ name: str
49
+ version_requirement: str
50
+ is_packaged: bool
51
+ fedora_versions: list[str] = Field(default_factory=list)
52
+ fedora_packages: list[str] = Field(default_factory=list)
53
+
54
+
55
+ class FeatureData(BaseModel):
56
+ """Structured data for a feature flag or extra."""
57
+
58
+ name: str
59
+ dependencies: list[str] = Field(default_factory=list)
40
60
 
41
61
 
42
62
  class ReportSummary(BaseModel):
@@ -46,6 +66,8 @@ class ReportSummary(BaseModel):
46
66
  packaged_count: int
47
67
  missing_count: int
48
68
  optional: "OptionalSummary"
69
+ dev: "DevBuildSummary"
70
+ build: "DevBuildSummary"
49
71
 
50
72
 
51
73
  class OptionalSummary(BaseModel):
@@ -56,6 +78,14 @@ class OptionalSummary(BaseModel):
56
78
  missing: int
57
79
 
58
80
 
81
+ class DevBuildSummary(BaseModel):
82
+ """Dev or build dependency statistics."""
83
+
84
+ total: int
85
+ packaged: int
86
+ missing: int
87
+
88
+
59
89
  class JsonReport(BaseModel):
60
90
  """Complete JSON report structure."""
61
91
 
@@ -64,6 +94,9 @@ class JsonReport(BaseModel):
64
94
  missing_packages: list[str]
65
95
  missing_optional_packages: list[str]
66
96
  packaged_packages: list[str]
97
+ features: list[FeatureData] = Field(default_factory=list)
98
+ dev_dependencies: list[DevBuildDepData] = Field(default_factory=list)
99
+ build_dependencies: list[DevBuildDepData] = Field(default_factory=list)
67
100
  dependency_tree: TreeNodeData
68
101
 
69
102
 
@@ -77,15 +110,53 @@ class JsonReporter(Reporter):
77
110
 
78
111
  def generate(self, data: ReportData) -> str:
79
112
  """Generate JSON report content."""
113
+ # Convert features to FeatureData
114
+ features_data = []
115
+ for f in data.features:
116
+ if hasattr(f, "name"):
117
+ features_data.append(
118
+ FeatureData(name=f.name, dependencies=f.dependencies)
119
+ )
120
+ else:
121
+ features_data.append(
122
+ FeatureData(
123
+ name=f.get("name", ""), dependencies=f.get("dependencies", [])
124
+ )
125
+ )
126
+
127
+ # Convert dev/build deps to DevBuildDepData
128
+ dev_deps_data = [
129
+ DevBuildDepData(
130
+ name=d["name"],
131
+ version_requirement=d["version_requirement"],
132
+ is_packaged=d["is_packaged"],
133
+ fedora_versions=d.get("fedora_versions", []),
134
+ fedora_packages=d.get("fedora_packages", []),
135
+ )
136
+ for d in data.dev_dependencies
137
+ ]
138
+ build_deps_data = [
139
+ DevBuildDepData(
140
+ name=d["name"],
141
+ version_requirement=d["version_requirement"],
142
+ is_packaged=d["is_packaged"],
143
+ fedora_versions=d.get("fedora_versions", []),
144
+ fedora_packages=d.get("fedora_packages", []),
145
+ )
146
+ for d in data.build_dependencies
147
+ ]
148
+
80
149
  report = JsonReport(
81
150
  metadata=ReportMetadata(
82
151
  generated_at=data.timestamp.isoformat(),
83
152
  root_package=data.root_package,
84
153
  language=data.language,
85
154
  registry=data.registry,
155
+ license=data.root_license,
86
156
  version=data.version,
87
157
  max_depth=data.max_depth,
88
158
  include_optional=data.include_optional,
159
+ missing_only=data.missing_only,
89
160
  ),
90
161
  summary=ReportSummary(
91
162
  total_dependencies=data.total_dependencies,
@@ -96,10 +167,26 @@ class JsonReporter(Reporter):
96
167
  packaged=data.optional_packaged,
97
168
  missing=data.optional_missing,
98
169
  ),
170
+ dev=DevBuildSummary(
171
+ total=data.dev_total,
172
+ packaged=data.dev_packaged,
173
+ missing=data.dev_missing,
174
+ ),
175
+ build=DevBuildSummary(
176
+ total=data.build_total,
177
+ packaged=data.build_packaged,
178
+ missing=data.build_missing,
179
+ ),
99
180
  ),
100
181
  missing_packages=sorted(data.required_missing_packages),
101
182
  missing_optional_packages=sorted(data.optional_missing_set),
102
- packaged_packages=sorted(data.unique_packaged_packages),
183
+ # Skip packaged packages list when missing_only mode is enabled
184
+ packaged_packages=[]
185
+ if data.missing_only
186
+ else sorted(data.unique_packaged_packages),
187
+ features=features_data,
188
+ dev_dependencies=dev_deps_data,
189
+ build_dependencies=build_deps_data,
103
190
  dependency_tree=self._tree_to_model(data.tree),
104
191
  )
105
192
 
@@ -128,10 +215,22 @@ class JsonReporter(Reporter):
128
215
  # Check if this is an optional dependency
129
216
  node.optional = "(optional)" in clean_label
130
217
 
218
+ # Try to extract license from the label (format: "(LICENSE)" before optional/status)
219
+ license_match = re.search(
220
+ r"v[\d.]+\s*\(([^)]+)\)\s*(?:\(optional\))?\s*•", clean_label
221
+ )
222
+ if license_match:
223
+ potential_license = license_match.group(1)
224
+ # Make sure it's not a version number or "optional"
225
+ if potential_license != "optional" and not re.match(
226
+ r"^[\d., ]+$", potential_license
227
+ ):
228
+ node.license = potential_license
229
+
131
230
  # Try to extract package name and version
132
- # Pattern: "package_name vX.Y.Z (optional) • status" or "package_name vX.Y.Z • status"
231
+ # Pattern: "package_name vX.Y.Z (LICENSE) (optional) • status" or "package_name vX.Y.Z • status"
133
232
  match = re.match(
134
- r"^(\S+)\s*(?:v([\d.]+))?\s*(?:\(optional\))?\s*•\s*(.+)$",
233
+ r"^(\S+)\s*(?:v([\d.]+))?\s*(?:\([^)]*\))?\s*(?:\(optional\))?\s*•\s*(.+)$",
135
234
  clean_label.strip(),
136
235
  )
137
236
  if match:
@@ -25,10 +25,14 @@ class MarkdownReporter(Reporter):
25
25
  lines.append(f"**Generated:** {data.timestamp.strftime('%Y-%m-%d %H:%M:%S')}")
26
26
  lines.append(f"**Language:** {data.language}")
27
27
  lines.append(f"**Registry:** {data.registry}")
28
+ if data.root_license:
29
+ lines.append(f"**License:** {data.root_license}")
28
30
  if data.version:
29
31
  lines.append(f"**Version:** {data.version}")
30
32
  if data.include_optional:
31
33
  lines.append("**Include optional:** Yes")
34
+ if data.missing_only:
35
+ lines.append("**Missing only:** Yes")
32
36
  lines.append("")
33
37
 
34
38
  # Summary
@@ -45,8 +49,77 @@ class MarkdownReporter(Reporter):
45
49
  lines.append(f"| Optional dependencies | {data.optional_total} |")
46
50
  lines.append(f"| Optional - Packaged | {data.optional_packaged} |")
47
51
  lines.append(f"| Optional - Missing | {data.optional_missing} |")
52
+
53
+ # Dev dependency stats
54
+ if data.dev_total > 0:
55
+ lines.append(f"| Dev dependencies | {data.dev_total} |")
56
+ lines.append(f"| Dev - Packaged | {data.dev_packaged} |")
57
+ lines.append(f"| Dev - Missing | {data.dev_missing} |")
58
+
59
+ # Build dependency stats
60
+ if data.build_total > 0:
61
+ lines.append(f"| Build dependencies | {data.build_total} |")
62
+ lines.append(f"| Build - Packaged | {data.build_packaged} |")
63
+ lines.append(f"| Build - Missing | {data.build_missing} |")
48
64
  lines.append("")
49
65
 
66
+ # Features / Extras
67
+ if data.features:
68
+ lines.append("## Features / Extras")
69
+ lines.append("")
70
+ for feature in data.features:
71
+ feature_name = (
72
+ feature.name
73
+ if hasattr(feature, "name")
74
+ else feature.get("name", "")
75
+ )
76
+ deps = (
77
+ feature.dependencies
78
+ if hasattr(feature, "dependencies")
79
+ else feature.get("dependencies", [])
80
+ )
81
+ if deps:
82
+ lines.append(f"- **{feature_name}**: {', '.join(deps)}")
83
+ else:
84
+ lines.append(f"- **{feature_name}**")
85
+ lines.append("")
86
+
87
+ # Dev dependencies
88
+ if data.dev_dependencies:
89
+ lines.append("## Dev Dependencies")
90
+ lines.append("")
91
+ lines.append("| Package | Version Req | Fedora Status |")
92
+ lines.append("|---------|-------------|---------------|")
93
+ for dep in data.dev_dependencies:
94
+ status = "Packaged" if dep["is_packaged"] else "Missing"
95
+ ver_info = (
96
+ f" ({', '.join(dep['fedora_versions'])})"
97
+ if dep.get("fedora_versions")
98
+ else ""
99
+ )
100
+ lines.append(
101
+ f"| `{dep['name']}` | {dep['version_requirement']} | {status}{ver_info} |"
102
+ )
103
+ lines.append("")
104
+
105
+ # Build dependencies
106
+ if data.build_dependencies:
107
+ lines.append("## Build Dependencies")
108
+ lines.append("")
109
+ lines.append("| Package | Version Req | Fedora Status |")
110
+ lines.append("|---------|-------------|---------------|")
111
+ for dep in data.build_dependencies:
112
+ status = "Packaged" if dep["is_packaged"] else "Missing"
113
+ ver_info = (
114
+ f" ({', '.join(dep['fedora_versions'])})"
115
+ if dep.get("fedora_versions")
116
+ else ""
117
+ )
118
+ lines.append(
119
+ f"| `{dep['name']}` | {dep['version_requirement']} | {status}{ver_info} |"
120
+ )
121
+ lines.append("")
122
+
50
123
  # Missing packages - use computed properties from ReportData
51
124
  required_missing = data.required_missing_packages
52
125
  optional_missing = data.optional_missing_set
@@ -69,8 +142,8 @@ class MarkdownReporter(Reporter):
69
142
  lines.append(f"- `{name}` *(optional)*")
70
143
  lines.append("")
71
144
 
72
- # Packaged packages - use computed property
73
- if data.unique_packaged_packages:
145
+ # Packaged packages - use computed property (skip if missing_only mode)
146
+ if data.unique_packaged_packages and not data.missing_only:
74
147
  lines.append("## Packaged Packages")
75
148
  lines.append("")
76
149
  lines.append("The following packages are already available in Fedora:")
@@ -79,13 +152,14 @@ class MarkdownReporter(Reporter):
79
152
  lines.append(f"- `{name}`")
80
153
  lines.append("")
81
154
 
82
- # Dependency tree
83
- lines.append("## Dependency Tree")
84
- lines.append("")
85
- lines.append("```")
86
- lines.append(self._tree_to_text(data.tree))
87
- lines.append("```")
88
- lines.append("")
155
+ # Dependency tree (skip if missing_only mode)
156
+ if not data.missing_only:
157
+ lines.append("## Dependency Tree")
158
+ lines.append("")
159
+ lines.append("```")
160
+ lines.append(self._tree_to_text(data.tree))
161
+ lines.append("```")
162
+ lines.append("")
89
163
 
90
164
  return "\n".join(lines)
91
165
 
@@ -2,14 +2,21 @@
2
2
  Standard output reporter using Rich.
3
3
 
4
4
  This is the default reporter that outputs to the console with colors and formatting.
5
+ Uses a grid layout with side-by-side panels on wide terminals (>= 100 cols)
6
+ and falls back to stacked layout on narrow terminals.
5
7
  """
6
8
 
7
9
  from rich import box
8
10
  from rich.console import Console
11
+ from rich.panel import Panel
9
12
  from rich.table import Table
13
+ from rich.text import Text
10
14
 
11
15
  from woolly.reporters.base import ReportData, Reporter
12
16
 
17
+ # Minimum terminal width for side-by-side layout
18
+ _MIN_SIDE_BY_SIDE_WIDTH = 100
19
+
13
20
 
14
21
  class StdoutReporter(Reporter):
15
22
  """Reporter that outputs to stdout using Rich formatting."""
@@ -22,54 +29,180 @@ class StdoutReporter(Reporter):
22
29
  def __init__(self, console: Console | None = None):
23
30
  self.console = console or Console()
24
31
 
25
- def generate(self, data: ReportData) -> str:
26
- """Generate and print the report to stdout."""
27
- # Print summary table
28
- table = Table(
29
- title=f"Dependency Summary for '{data.root_package}' ({data.language})",
30
- box=box.ROUNDED,
31
- )
32
+ def _print_side_by_side(self, left, right) -> None:
33
+ """Print two renderables side by side, or stacked on narrow terminals."""
34
+ if self.console.width >= _MIN_SIDE_BY_SIDE_WIDTH:
35
+ grid = Table.grid(padding=(0, 1), expand=True)
36
+ grid.add_column(ratio=1)
37
+ grid.add_column(ratio=1)
38
+ grid.add_row(left, right)
39
+ self.console.print(grid)
40
+ else:
41
+ self.console.print(left)
42
+ self.console.print(right)
43
+
44
+ def _build_summary_panel(self, data: ReportData) -> Panel:
45
+ """Build the summary panel with license and dependency stats."""
46
+ table = Table(box=box.SIMPLE_HEAVY, expand=True, show_header=True)
32
47
  table.add_column("Metric", style="bold")
33
48
  table.add_column("Value", justify="right")
34
49
 
35
- table.add_row("Total dependencies checked", str(data.total_dependencies))
36
- table.add_row("[green]Packaged in Fedora[/green]", str(data.packaged_count))
37
- table.add_row("[red]Missing from Fedora[/red]", str(data.missing_count))
50
+ # License row at the top if available
51
+ if data.root_license:
52
+ table.add_row("License", f"[magenta]{data.root_license}[/magenta]")
53
+
54
+ table.add_row("Total dependencies", str(data.total_dependencies))
55
+ table.add_row(
56
+ "[green]Packaged in Fedora[/green]", f"[green]{data.packaged_count}[/green]"
57
+ )
58
+ table.add_row(
59
+ "[red]Missing from Fedora[/red]", f"[red]{data.missing_count}[/red]"
60
+ )
38
61
 
39
- # Show optional dependency stats if any were found
40
62
  if data.optional_total > 0:
41
- table.add_row("", "") # Empty row as separator
63
+ table.add_row("", "")
42
64
  table.add_row(
43
- "[yellow]Optional dependencies[/yellow]", str(data.optional_total)
65
+ "[yellow]Optional dependencies[/yellow]",
66
+ str(data.optional_total),
67
+ )
68
+ table.add_row("[yellow] Packaged[/yellow]", str(data.optional_packaged))
69
+ table.add_row("[yellow] Missing[/yellow]", str(data.optional_missing))
70
+
71
+ if data.dev_total > 0:
72
+ table.add_row("", "")
73
+ table.add_row("[cyan]Dev dependencies[/cyan]", str(data.dev_total))
74
+ table.add_row("[cyan] Packaged[/cyan]", str(data.dev_packaged))
75
+ table.add_row("[cyan] Missing[/cyan]", str(data.dev_missing))
76
+
77
+ if data.build_total > 0:
78
+ table.add_row("", "")
79
+ table.add_row("[blue]Build dependencies[/blue]", str(data.build_total))
80
+ table.add_row("[blue] Packaged[/blue]", str(data.build_packaged))
81
+ table.add_row("[blue] Missing[/blue]", str(data.build_missing))
82
+
83
+ title = f"[bold]Summary for [cyan]{data.root_package}[/cyan] ({data.language})[/bold]"
84
+ return Panel(table, title=title, border_style="green", padding=(0, 1))
85
+
86
+ def _build_missing_required_panel(self, data: ReportData) -> Panel:
87
+ """Build the panel for missing required packages."""
88
+ required_missing = data.required_missing_packages
89
+ if required_missing:
90
+ table = Table(box=box.SIMPLE, expand=True, show_header=True)
91
+ table.add_column("Package", style="bold red")
92
+ for name in sorted(required_missing):
93
+ table.add_row(name)
94
+ content = table
95
+ else:
96
+ content = Text("None", style="dim")
97
+
98
+ return Panel(
99
+ content,
100
+ title="[bold red]Missing Required[/bold red]",
101
+ border_style="red",
102
+ padding=(0, 1),
103
+ )
104
+
105
+ def _build_missing_optional_panel(self, data: ReportData) -> Panel:
106
+ """Build the panel for missing optional packages."""
107
+ optional_missing = data.optional_missing_set
108
+ if optional_missing:
109
+ table = Table(box=box.SIMPLE, expand=True, show_header=True)
110
+ table.add_column("Package", style="bold yellow")
111
+ for name in sorted(optional_missing):
112
+ table.add_row(name)
113
+ content = table
114
+ else:
115
+ content = Text("None", style="dim")
116
+
117
+ return Panel(
118
+ content,
119
+ title="[bold yellow]Missing Optional[/bold yellow]",
120
+ border_style="yellow",
121
+ padding=(0, 1),
122
+ )
123
+
124
+ def _build_dep_panel(
125
+ self, deps: list[dict], title: str, border_style: str
126
+ ) -> Panel:
127
+ """Build a panel for dev or build dependencies."""
128
+ if deps:
129
+ table = Table(box=box.SIMPLE, expand=True, show_header=True)
130
+ table.add_column("", width=2) # Status icon
131
+ table.add_column("Package", style="bold")
132
+ table.add_column("Version Req", style="dim")
133
+ table.add_column("Fedora", style="dim")
134
+ for dep in deps:
135
+ icon = "[green]✓[/green]" if dep["is_packaged"] else "[red]✗[/red]"
136
+ fedora_ver = ", ".join(dep.get("fedora_versions", [])) or "-"
137
+ table.add_row(icon, dep["name"], dep["version_requirement"], fedora_ver)
138
+ content = table
139
+ else:
140
+ content = Text("None", style="dim")
141
+
142
+ return Panel(content, title=title, border_style=border_style, padding=(0, 1))
143
+
144
+ def _build_features_panel(self, data: ReportData) -> Panel:
145
+ """Build the features / extras panel."""
146
+ table = Table(box=box.SIMPLE, expand=True, show_header=True)
147
+ table.add_column("Feature", style="bold magenta")
148
+ table.add_column("Dependencies")
149
+
150
+ for feature in data.features:
151
+ deps_str = (
152
+ ", ".join(feature.dependencies)
153
+ if hasattr(feature, "dependencies")
154
+ else ", ".join(feature.get("dependencies", []))
155
+ )
156
+ feature_name = (
157
+ feature.name if hasattr(feature, "name") else feature.get("name", "")
44
158
  )
45
- table.add_row("[yellow] ├─ Packaged[/yellow]", str(data.optional_packaged))
46
- table.add_row("[yellow] └─ Missing[/yellow]", str(data.optional_missing))
159
+ table.add_row(feature_name, deps_str or "[dim]-[/dim]")
160
+
161
+ return Panel(
162
+ table,
163
+ title="[bold magenta]Features / Extras[/bold magenta]",
164
+ border_style="magenta",
165
+ padding=(0, 1),
166
+ )
47
167
 
48
- self.console.print(table)
49
- self.console.print()
168
+ def generate(self, data: ReportData) -> str:
169
+ """Generate and print the report to stdout."""
170
+ # ── Summary (full width) ──
171
+ self.console.print(self._build_summary_panel(data))
50
172
 
51
- # Print missing packages list using computed properties
173
+ # ── Missing packages: Required | Optional (side by side) ──
52
174
  if data.missing_packages:
53
- required_missing = data.required_missing_packages
54
- optional_missing = data.optional_missing_set
55
-
56
- if required_missing:
57
- self.console.print("[bold]Missing packages that need packaging:[/bold]")
58
- for name in sorted(required_missing):
59
- self.console.print(f" • {name}")
60
- self.console.print()
61
-
62
- if optional_missing:
63
- self.console.print(
64
- "[bold yellow]Missing optional packages:[/bold yellow]"
175
+ left = self._build_missing_required_panel(data)
176
+ right = self._build_missing_optional_panel(data)
177
+ self._print_side_by_side(left, right)
178
+
179
+ # ── Dev | Build dependencies (side by side) ──
180
+ if data.dev_dependencies or data.build_dependencies:
181
+ left = self._build_dep_panel(
182
+ data.dev_dependencies,
183
+ "[bold cyan]Dev Dependencies[/bold cyan]",
184
+ "cyan",
185
+ )
186
+ right = self._build_dep_panel(
187
+ data.build_dependencies,
188
+ "[bold blue]Build Dependencies[/bold blue]",
189
+ "blue",
190
+ )
191
+ self._print_side_by_side(left, right)
192
+
193
+ # ── Features (full width) ──
194
+ if data.features:
195
+ self.console.print(self._build_features_panel(data))
196
+
197
+ # ── Dependency Tree (full width, skip if missing_only) ──
198
+ if not data.missing_only:
199
+ self.console.print(
200
+ Panel(
201
+ data.tree,
202
+ title="[bold]Dependency Tree[/bold]",
203
+ border_style="dim",
204
+ padding=(0, 1),
65
205
  )
66
- for name in sorted(optional_missing):
67
- self.console.print(f" • {name} [dim](optional)[/dim]")
68
- self.console.print()
69
-
70
- # Print dependency tree
71
- self.console.print("[bold]Dependency Tree:[/bold]")
72
- self.console.print(data.tree)
73
- self.console.print()
206
+ )
74
207
 
75
208
  return "" # Output is printed directly