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.
- woolly/cache.py +21 -6
- woolly/commands/check.py +264 -82
- woolly/http.py +22 -3
- woolly/languages/base.py +187 -10
- woolly/languages/python.py +221 -8
- woolly/languages/rust.py +59 -1
- woolly/reporters/__init__.py +15 -1
- woolly/reporters/base.py +25 -0
- woolly/reporters/json.py +102 -3
- woolly/reporters/markdown.py +83 -9
- woolly/reporters/stdout.py +171 -38
- woolly/reporters/template.py +245 -0
- {woolly-0.3.0.dist-info → woolly-0.5.0.dist-info}/METADATA +3 -1
- woolly-0.5.0.dist-info/RECORD +26 -0
- woolly-0.3.0.dist-info/RECORD +0 -25
- {woolly-0.3.0.dist-info → woolly-0.5.0.dist-info}/WHEEL +0 -0
- {woolly-0.3.0.dist-info → woolly-0.5.0.dist-info}/entry_points.txt +0 -0
- {woolly-0.3.0.dist-info → woolly-0.5.0.dist-info}/licenses/LICENSE +0 -0
woolly/reporters/__init__.py
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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:
|
woolly/reporters/markdown.py
CHANGED
|
@@ -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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
woolly/reporters/stdout.py
CHANGED
|
@@ -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
|
|
26
|
-
"""
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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("", "")
|
|
63
|
+
table.add_row("", "")
|
|
42
64
|
table.add_row(
|
|
43
|
-
"[yellow]Optional dependencies[/yellow]",
|
|
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("[
|
|
46
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
#
|
|
173
|
+
# ── Missing packages: Required | Optional (side by side) ──
|
|
52
174
|
if data.missing_packages:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|