woolly 0.2.0__py3-none-any.whl → 0.3.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 +15 -5
- woolly/commands/check.py +107 -43
- woolly/commands/list_formats.py +3 -3
- woolly/commands/list_languages.py +4 -4
- woolly/http.py +34 -0
- woolly/languages/__init__.py +33 -3
- woolly/languages/base.py +11 -7
- woolly/languages/python.py +5 -6
- woolly/languages/rust.py +3 -5
- woolly/reporters/__init__.py +29 -8
- woolly/reporters/base.py +92 -10
- woolly/reporters/json.py +119 -89
- woolly/reporters/markdown.py +30 -53
- woolly/reporters/stdout.py +27 -6
- woolly-0.3.0.dist-info/METADATA +322 -0
- woolly-0.3.0.dist-info/RECORD +25 -0
- woolly-0.2.0.dist-info/METADATA +0 -213
- woolly-0.2.0.dist-info/RECORD +0 -24
- {woolly-0.2.0.dist-info → woolly-0.3.0.dist-info}/WHEEL +0 -0
- {woolly-0.2.0.dist-info → woolly-0.3.0.dist-info}/entry_points.txt +0 -0
- {woolly-0.2.0.dist-info → woolly-0.3.0.dist-info}/licenses/LICENSE +0 -0
woolly/reporters/base.py
CHANGED
|
@@ -16,24 +16,29 @@ Example:
|
|
|
16
16
|
...
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
|
+
import re
|
|
19
20
|
from abc import ABC, abstractmethod
|
|
20
21
|
from datetime import datetime
|
|
22
|
+
from functools import cached_property
|
|
21
23
|
from pathlib import Path
|
|
22
24
|
from typing import Any, Optional
|
|
23
25
|
|
|
24
26
|
from pydantic import BaseModel, ConfigDict, Field
|
|
25
27
|
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
"""
|
|
29
|
+
def strip_markup(text: str) -> str:
|
|
30
|
+
"""
|
|
31
|
+
Strip Rich markup from text.
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
Removes Rich markup tags like [bold], [/bold], [green], etc.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
text: Text containing Rich markup.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Plain text without markup.
|
|
40
|
+
"""
|
|
41
|
+
return re.sub(r"\[/?[^\]]+\]", "", text)
|
|
37
42
|
|
|
38
43
|
|
|
39
44
|
class ReportData(BaseModel):
|
|
@@ -53,15 +58,36 @@ class ReportData(BaseModel):
|
|
|
53
58
|
missing_packages: list[str] = Field(default_factory=list)
|
|
54
59
|
packaged_packages: list[str] = Field(default_factory=list)
|
|
55
60
|
|
|
61
|
+
# Optional dependency statistics
|
|
62
|
+
include_optional: bool = False
|
|
63
|
+
optional_total: int = 0
|
|
64
|
+
optional_packaged: int = 0
|
|
65
|
+
optional_missing: int = 0
|
|
66
|
+
optional_missing_packages: list[str] = Field(default_factory=list)
|
|
67
|
+
|
|
56
68
|
# Full tree for detailed reports
|
|
57
69
|
tree: Any # Rich Tree object - not JSON serializable
|
|
58
|
-
packages: list[PackageStatus] = Field(default_factory=list)
|
|
59
70
|
|
|
60
71
|
# Metadata
|
|
61
72
|
timestamp: datetime = Field(default_factory=datetime.now)
|
|
62
73
|
max_depth: int = 50
|
|
63
74
|
version: Optional[str] = None
|
|
64
75
|
|
|
76
|
+
@cached_property
|
|
77
|
+
def required_missing_packages(self) -> set[str]:
|
|
78
|
+
"""Get the set of required (non-optional) missing packages."""
|
|
79
|
+
return set(self.missing_packages) - set(self.optional_missing_packages)
|
|
80
|
+
|
|
81
|
+
@cached_property
|
|
82
|
+
def optional_missing_set(self) -> set[str]:
|
|
83
|
+
"""Get the set of optional missing packages."""
|
|
84
|
+
return set(self.optional_missing_packages)
|
|
85
|
+
|
|
86
|
+
@cached_property
|
|
87
|
+
def unique_packaged_packages(self) -> set[str]:
|
|
88
|
+
"""Get the unique set of packaged packages."""
|
|
89
|
+
return set(self.packaged_packages)
|
|
90
|
+
|
|
65
91
|
|
|
66
92
|
class Reporter(ABC):
|
|
67
93
|
"""
|
|
@@ -129,3 +155,59 @@ class Reporter(ABC):
|
|
|
129
155
|
output_path = output_dir / self.get_output_filename(data)
|
|
130
156
|
output_path.write_text(content)
|
|
131
157
|
return output_path
|
|
158
|
+
|
|
159
|
+
# ----------------------------------------------------------------
|
|
160
|
+
# Shared tree traversal utilities for subclasses
|
|
161
|
+
# ----------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
def _get_label(self, node) -> str:
|
|
164
|
+
"""
|
|
165
|
+
Extract the label text from a tree node, handling nested Trees.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
node: A Rich Tree node or string.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
The label text as a string.
|
|
172
|
+
"""
|
|
173
|
+
# If it's a string, return it directly
|
|
174
|
+
if isinstance(node, str):
|
|
175
|
+
return node
|
|
176
|
+
|
|
177
|
+
# Try to get label attribute (Rich Tree has this)
|
|
178
|
+
if hasattr(node, "label"):
|
|
179
|
+
label = node.label
|
|
180
|
+
# If label is None, return empty string
|
|
181
|
+
if label is None:
|
|
182
|
+
return ""
|
|
183
|
+
# If label is another Tree-like object (has its own label), recurse
|
|
184
|
+
if hasattr(label, "label"):
|
|
185
|
+
return self._get_label(label)
|
|
186
|
+
# Otherwise convert to string
|
|
187
|
+
return str(label)
|
|
188
|
+
|
|
189
|
+
# Fallback - shouldn't happen
|
|
190
|
+
return str(node)
|
|
191
|
+
|
|
192
|
+
def _get_children(self, node) -> list:
|
|
193
|
+
"""
|
|
194
|
+
Get all children from a tree node, flattening nested Trees.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
node: A Rich Tree node.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
List of child nodes.
|
|
201
|
+
"""
|
|
202
|
+
children = []
|
|
203
|
+
|
|
204
|
+
if hasattr(node, "children"):
|
|
205
|
+
for child in node.children:
|
|
206
|
+
# If the child's label is itself a Tree, use that Tree's children
|
|
207
|
+
if hasattr(child, "label") and hasattr(child.label, "children"):
|
|
208
|
+
# The child is a wrapper around another tree
|
|
209
|
+
children.append(child.label)
|
|
210
|
+
else:
|
|
211
|
+
children.append(child)
|
|
212
|
+
|
|
213
|
+
return children
|
woolly/reporters/json.py
CHANGED
|
@@ -5,10 +5,66 @@ Generates a JSON file with structured data for machine consumption.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import re
|
|
8
|
-
import
|
|
9
|
-
from typing import Any
|
|
8
|
+
from typing import Optional
|
|
10
9
|
|
|
11
|
-
from
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
from woolly.reporters.base import ReportData, Reporter, strip_markup
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TreeNodeData(BaseModel):
|
|
16
|
+
"""Structured data for a single node in the dependency tree."""
|
|
17
|
+
|
|
18
|
+
raw: str
|
|
19
|
+
name: Optional[str] = None
|
|
20
|
+
version: Optional[str] = None
|
|
21
|
+
optional: bool = False
|
|
22
|
+
status: Optional[str] = None
|
|
23
|
+
is_packaged: Optional[bool] = None
|
|
24
|
+
fedora_versions: list[str] = Field(default_factory=list)
|
|
25
|
+
fedora_packages: list[str] = Field(default_factory=list)
|
|
26
|
+
dependencies: list["TreeNodeData"] = Field(default_factory=list)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ReportMetadata(BaseModel):
|
|
30
|
+
"""Metadata for the JSON report."""
|
|
31
|
+
|
|
32
|
+
generated_at: str
|
|
33
|
+
tool: str = "woolly"
|
|
34
|
+
root_package: str
|
|
35
|
+
language: str
|
|
36
|
+
registry: str
|
|
37
|
+
version: Optional[str] = None
|
|
38
|
+
max_depth: int
|
|
39
|
+
include_optional: bool
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ReportSummary(BaseModel):
|
|
43
|
+
"""Summary statistics for the JSON report."""
|
|
44
|
+
|
|
45
|
+
total_dependencies: int
|
|
46
|
+
packaged_count: int
|
|
47
|
+
missing_count: int
|
|
48
|
+
optional: "OptionalSummary"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class OptionalSummary(BaseModel):
|
|
52
|
+
"""Optional dependency statistics."""
|
|
53
|
+
|
|
54
|
+
total: int
|
|
55
|
+
packaged: int
|
|
56
|
+
missing: int
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class JsonReport(BaseModel):
|
|
60
|
+
"""Complete JSON report structure."""
|
|
61
|
+
|
|
62
|
+
metadata: ReportMetadata
|
|
63
|
+
summary: ReportSummary
|
|
64
|
+
missing_packages: list[str]
|
|
65
|
+
missing_optional_packages: list[str]
|
|
66
|
+
packaged_packages: list[str]
|
|
67
|
+
dependency_tree: TreeNodeData
|
|
12
68
|
|
|
13
69
|
|
|
14
70
|
class JsonReporter(Reporter):
|
|
@@ -21,125 +77,99 @@ class JsonReporter(Reporter):
|
|
|
21
77
|
|
|
22
78
|
def generate(self, data: ReportData) -> str:
|
|
23
79
|
"""Generate JSON report content."""
|
|
24
|
-
report =
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
label = node.label
|
|
55
|
-
# If label is None, return empty string
|
|
56
|
-
if label is None:
|
|
57
|
-
return ""
|
|
58
|
-
# If label is another Tree-like object (has its own label), recurse
|
|
59
|
-
if hasattr(label, "label"):
|
|
60
|
-
return self._get_label(label)
|
|
61
|
-
# Otherwise convert to string
|
|
62
|
-
return str(label)
|
|
63
|
-
|
|
64
|
-
# Fallback - shouldn't happen
|
|
65
|
-
return str(node)
|
|
66
|
-
|
|
67
|
-
def _get_children(self, node) -> list:
|
|
68
|
-
"""Get all children from a tree node, flattening nested Trees."""
|
|
69
|
-
children = []
|
|
70
|
-
|
|
71
|
-
if hasattr(node, "children"):
|
|
72
|
-
for child in node.children:
|
|
73
|
-
# If the child's label is itself a Tree, use that Tree's children
|
|
74
|
-
if hasattr(child, "label") and hasattr(child.label, "children"):
|
|
75
|
-
# The child is a wrapper around another tree
|
|
76
|
-
children.append(child.label)
|
|
77
|
-
else:
|
|
78
|
-
children.append(child)
|
|
79
|
-
|
|
80
|
-
return children
|
|
81
|
-
|
|
82
|
-
def _tree_to_dict(self, tree) -> dict[str, Any]:
|
|
83
|
-
"""Convert Rich Tree to dictionary representation."""
|
|
80
|
+
report = JsonReport(
|
|
81
|
+
metadata=ReportMetadata(
|
|
82
|
+
generated_at=data.timestamp.isoformat(),
|
|
83
|
+
root_package=data.root_package,
|
|
84
|
+
language=data.language,
|
|
85
|
+
registry=data.registry,
|
|
86
|
+
version=data.version,
|
|
87
|
+
max_depth=data.max_depth,
|
|
88
|
+
include_optional=data.include_optional,
|
|
89
|
+
),
|
|
90
|
+
summary=ReportSummary(
|
|
91
|
+
total_dependencies=data.total_dependencies,
|
|
92
|
+
packaged_count=data.packaged_count,
|
|
93
|
+
missing_count=data.missing_count,
|
|
94
|
+
optional=OptionalSummary(
|
|
95
|
+
total=data.optional_total,
|
|
96
|
+
packaged=data.optional_packaged,
|
|
97
|
+
missing=data.optional_missing,
|
|
98
|
+
),
|
|
99
|
+
),
|
|
100
|
+
missing_packages=sorted(data.required_missing_packages),
|
|
101
|
+
missing_optional_packages=sorted(data.optional_missing_set),
|
|
102
|
+
packaged_packages=sorted(data.unique_packaged_packages),
|
|
103
|
+
dependency_tree=self._tree_to_model(data.tree),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
return report.model_dump_json(indent=2)
|
|
107
|
+
|
|
108
|
+
def _tree_to_model(self, tree) -> TreeNodeData:
|
|
109
|
+
"""Convert Rich Tree to TreeNodeData model."""
|
|
84
110
|
label = self._get_label(tree)
|
|
85
|
-
|
|
111
|
+
node_data = self._parse_label(label)
|
|
86
112
|
|
|
87
|
-
# Get children
|
|
113
|
+
# Get children using inherited method
|
|
88
114
|
children = self._get_children(tree)
|
|
89
115
|
|
|
90
116
|
if children:
|
|
91
|
-
|
|
117
|
+
node_data.dependencies = [self._tree_to_model(child) for child in children]
|
|
118
|
+
|
|
119
|
+
return node_data
|
|
92
120
|
|
|
93
|
-
|
|
121
|
+
def _parse_label(self, label: str) -> TreeNodeData:
|
|
122
|
+
"""Parse a tree label into structured TreeNodeData."""
|
|
123
|
+
# Strip Rich markup using shared utility
|
|
124
|
+
clean_label = strip_markup(label)
|
|
94
125
|
|
|
95
|
-
|
|
96
|
-
"""Parse a tree label into structured data."""
|
|
97
|
-
# Strip Rich markup
|
|
98
|
-
clean_label = re.sub(r"\[/?[^\]]+\]", "", label)
|
|
126
|
+
node = TreeNodeData(raw=clean_label.strip())
|
|
99
127
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
128
|
+
# Check if this is an optional dependency
|
|
129
|
+
node.optional = "(optional)" in clean_label
|
|
103
130
|
|
|
104
131
|
# Try to extract package name and version
|
|
105
|
-
# Pattern: "package_name vX.Y.Z • status"
|
|
106
|
-
match = re.match(
|
|
132
|
+
# Pattern: "package_name vX.Y.Z (optional) • status" or "package_name vX.Y.Z • status"
|
|
133
|
+
match = re.match(
|
|
134
|
+
r"^(\S+)\s*(?:v([\d.]+))?\s*(?:\(optional\))?\s*•\s*(.+)$",
|
|
135
|
+
clean_label.strip(),
|
|
136
|
+
)
|
|
107
137
|
if match:
|
|
108
|
-
|
|
138
|
+
node.name = match.group(1)
|
|
109
139
|
if match.group(2):
|
|
110
|
-
|
|
140
|
+
node.version = match.group(2)
|
|
111
141
|
|
|
112
142
|
status_text = match.group(3).strip()
|
|
113
143
|
if (
|
|
114
144
|
"packaged" in status_text.lower()
|
|
115
145
|
and "not packaged" not in status_text.lower()
|
|
116
146
|
):
|
|
117
|
-
|
|
147
|
+
node.status = "packaged"
|
|
118
148
|
# Try to extract Fedora versions
|
|
119
149
|
ver_match = re.search(r"\(([\d., ]+)\)", status_text)
|
|
120
150
|
if ver_match:
|
|
121
|
-
|
|
151
|
+
node.fedora_versions = [
|
|
122
152
|
v.strip() for v in ver_match.group(1).split(",")
|
|
123
153
|
]
|
|
124
154
|
# Try to extract package names
|
|
125
155
|
pkg_match = re.search(r"\[([^\]]+)\]", status_text)
|
|
126
156
|
if pkg_match:
|
|
127
|
-
|
|
157
|
+
node.fedora_packages = [
|
|
128
158
|
p.strip() for p in pkg_match.group(1).split(",")
|
|
129
159
|
]
|
|
130
160
|
elif "not packaged" in status_text.lower():
|
|
131
|
-
|
|
161
|
+
node.status = "not_packaged"
|
|
132
162
|
elif "not found" in status_text.lower():
|
|
133
|
-
|
|
163
|
+
node.status = "not_found"
|
|
134
164
|
elif "already visited" in status_text.lower():
|
|
135
|
-
|
|
136
|
-
|
|
165
|
+
node.status = "visited"
|
|
166
|
+
node.is_packaged = "✓" in status_text
|
|
137
167
|
else:
|
|
138
168
|
# Simpler patterns
|
|
139
169
|
if "already visited" in clean_label:
|
|
140
|
-
|
|
141
|
-
|
|
170
|
+
node.status = "visited"
|
|
171
|
+
node.is_packaged = "✓" in clean_label
|
|
142
172
|
elif "max depth" in clean_label:
|
|
143
|
-
|
|
173
|
+
node.status = "max_depth_reached"
|
|
144
174
|
|
|
145
|
-
return
|
|
175
|
+
return node
|
woolly/reporters/markdown.py
CHANGED
|
@@ -4,9 +4,7 @@ Markdown report generator.
|
|
|
4
4
|
Generates a markdown file with the full dependency analysis.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
from woolly.reporters.base import Reporter, ReportData
|
|
7
|
+
from woolly.reporters.base import ReportData, Reporter, strip_markup
|
|
10
8
|
|
|
11
9
|
|
|
12
10
|
class MarkdownReporter(Reporter):
|
|
@@ -29,6 +27,8 @@ class MarkdownReporter(Reporter):
|
|
|
29
27
|
lines.append(f"**Registry:** {data.registry}")
|
|
30
28
|
if data.version:
|
|
31
29
|
lines.append(f"**Version:** {data.version}")
|
|
30
|
+
if data.include_optional:
|
|
31
|
+
lines.append("**Include optional:** Yes")
|
|
32
32
|
lines.append("")
|
|
33
33
|
|
|
34
34
|
# Summary
|
|
@@ -39,25 +39,43 @@ class MarkdownReporter(Reporter):
|
|
|
39
39
|
lines.append(f"| Total dependencies checked | {data.total_dependencies} |")
|
|
40
40
|
lines.append(f"| Packaged in Fedora | {data.packaged_count} |")
|
|
41
41
|
lines.append(f"| Missing from Fedora | {data.missing_count} |")
|
|
42
|
+
|
|
43
|
+
# Optional dependency stats
|
|
44
|
+
if data.optional_total > 0:
|
|
45
|
+
lines.append(f"| Optional dependencies | {data.optional_total} |")
|
|
46
|
+
lines.append(f"| Optional - Packaged | {data.optional_packaged} |")
|
|
47
|
+
lines.append(f"| Optional - Missing | {data.optional_missing} |")
|
|
42
48
|
lines.append("")
|
|
43
49
|
|
|
44
|
-
# Missing packages
|
|
45
|
-
|
|
50
|
+
# Missing packages - use computed properties from ReportData
|
|
51
|
+
required_missing = data.required_missing_packages
|
|
52
|
+
optional_missing = data.optional_missing_set
|
|
53
|
+
|
|
54
|
+
if required_missing:
|
|
46
55
|
lines.append("## Missing Packages")
|
|
47
56
|
lines.append("")
|
|
48
57
|
lines.append("The following packages need to be packaged for Fedora:")
|
|
49
58
|
lines.append("")
|
|
50
|
-
for name in sorted(
|
|
59
|
+
for name in sorted(required_missing):
|
|
51
60
|
lines.append(f"- `{name}`")
|
|
52
61
|
lines.append("")
|
|
53
62
|
|
|
54
|
-
|
|
55
|
-
|
|
63
|
+
if optional_missing:
|
|
64
|
+
lines.append("## Missing Optional Packages")
|
|
65
|
+
lines.append("")
|
|
66
|
+
lines.append("The following optional packages are not available in Fedora:")
|
|
67
|
+
lines.append("")
|
|
68
|
+
for name in sorted(optional_missing):
|
|
69
|
+
lines.append(f"- `{name}` *(optional)*")
|
|
70
|
+
lines.append("")
|
|
71
|
+
|
|
72
|
+
# Packaged packages - use computed property
|
|
73
|
+
if data.unique_packaged_packages:
|
|
56
74
|
lines.append("## Packaged Packages")
|
|
57
75
|
lines.append("")
|
|
58
76
|
lines.append("The following packages are already available in Fedora:")
|
|
59
77
|
lines.append("")
|
|
60
|
-
for name in sorted(
|
|
78
|
+
for name in sorted(data.unique_packaged_packages):
|
|
61
79
|
lines.append(f"- `{name}`")
|
|
62
80
|
lines.append("")
|
|
63
81
|
|
|
@@ -71,53 +89,17 @@ class MarkdownReporter(Reporter):
|
|
|
71
89
|
|
|
72
90
|
return "\n".join(lines)
|
|
73
91
|
|
|
74
|
-
def _get_label(self, node) -> str:
|
|
75
|
-
"""Extract the label text from a tree node, handling nested Trees."""
|
|
76
|
-
# If it's a string, return it directly
|
|
77
|
-
if isinstance(node, str):
|
|
78
|
-
return node
|
|
79
|
-
|
|
80
|
-
# Try to get label attribute (Rich Tree has this)
|
|
81
|
-
if hasattr(node, "label"):
|
|
82
|
-
label = node.label
|
|
83
|
-
# If label is None, return empty string
|
|
84
|
-
if label is None:
|
|
85
|
-
return ""
|
|
86
|
-
# If label is another Tree-like object (has its own label), recurse
|
|
87
|
-
if hasattr(label, "label"):
|
|
88
|
-
return self._get_label(label)
|
|
89
|
-
# Otherwise convert to string
|
|
90
|
-
return str(label)
|
|
91
|
-
|
|
92
|
-
# Fallback - shouldn't happen
|
|
93
|
-
return str(node)
|
|
94
|
-
|
|
95
|
-
def _get_children(self, node) -> list:
|
|
96
|
-
"""Get all children from a tree node, flattening nested Trees."""
|
|
97
|
-
children = []
|
|
98
|
-
|
|
99
|
-
if hasattr(node, "children"):
|
|
100
|
-
for child in node.children:
|
|
101
|
-
# If the child's label is itself a Tree, use that Tree's children
|
|
102
|
-
if hasattr(child, "label") and hasattr(child.label, "children"):
|
|
103
|
-
# The child is a wrapper around another tree
|
|
104
|
-
children.append(child.label)
|
|
105
|
-
else:
|
|
106
|
-
children.append(child)
|
|
107
|
-
|
|
108
|
-
return children
|
|
109
|
-
|
|
110
92
|
def _tree_to_text(self, tree, prefix: str = "") -> str:
|
|
111
93
|
"""Convert Rich Tree to plain text representation."""
|
|
112
94
|
lines = []
|
|
113
95
|
|
|
114
|
-
# Get label text (strip Rich markup)
|
|
96
|
+
# Get label text (strip Rich markup) using inherited method and shared utility
|
|
115
97
|
label = self._get_label(tree)
|
|
116
|
-
label =
|
|
98
|
+
label = strip_markup(label)
|
|
117
99
|
|
|
118
100
|
lines.append(label)
|
|
119
101
|
|
|
120
|
-
# Get children
|
|
102
|
+
# Get children using inherited method
|
|
121
103
|
children = self._get_children(tree)
|
|
122
104
|
|
|
123
105
|
for i, child in enumerate(children):
|
|
@@ -133,8 +115,3 @@ class MarkdownReporter(Reporter):
|
|
|
133
115
|
lines.append(line)
|
|
134
116
|
|
|
135
117
|
return "\n".join(lines)
|
|
136
|
-
|
|
137
|
-
def _strip_markup(self, text: str) -> str:
|
|
138
|
-
"""Strip Rich markup from text."""
|
|
139
|
-
# Remove Rich markup tags like [bold], [/bold], [green], etc.
|
|
140
|
-
return re.sub(r"\[/?[^\]]+\]", "", text)
|
woolly/reporters/stdout.py
CHANGED
|
@@ -8,7 +8,7 @@ from rich import box
|
|
|
8
8
|
from rich.console import Console
|
|
9
9
|
from rich.table import Table
|
|
10
10
|
|
|
11
|
-
from woolly.reporters.base import
|
|
11
|
+
from woolly.reporters.base import ReportData, Reporter
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class StdoutReporter(Reporter):
|
|
@@ -36,15 +36,36 @@ class StdoutReporter(Reporter):
|
|
|
36
36
|
table.add_row("[green]Packaged in Fedora[/green]", str(data.packaged_count))
|
|
37
37
|
table.add_row("[red]Missing from Fedora[/red]", str(data.missing_count))
|
|
38
38
|
|
|
39
|
+
# Show optional dependency stats if any were found
|
|
40
|
+
if data.optional_total > 0:
|
|
41
|
+
table.add_row("", "") # Empty row as separator
|
|
42
|
+
table.add_row(
|
|
43
|
+
"[yellow]Optional dependencies[/yellow]", str(data.optional_total)
|
|
44
|
+
)
|
|
45
|
+
table.add_row("[yellow] ├─ Packaged[/yellow]", str(data.optional_packaged))
|
|
46
|
+
table.add_row("[yellow] └─ Missing[/yellow]", str(data.optional_missing))
|
|
47
|
+
|
|
39
48
|
self.console.print(table)
|
|
40
49
|
self.console.print()
|
|
41
50
|
|
|
42
|
-
# Print missing packages list
|
|
51
|
+
# Print missing packages list using computed properties
|
|
43
52
|
if data.missing_packages:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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]"
|
|
65
|
+
)
|
|
66
|
+
for name in sorted(optional_missing):
|
|
67
|
+
self.console.print(f" • {name} [dim](optional)[/dim]")
|
|
68
|
+
self.console.print()
|
|
48
69
|
|
|
49
70
|
# Print dependency tree
|
|
50
71
|
self.console.print("[bold]Dependency Tree:[/bold]")
|