woolly 0.2.0__py3-none-any.whl → 0.4.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 +176 -44
- 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 +43 -8
- woolly/reporters/base.py +95 -10
- woolly/reporters/json.py +124 -89
- woolly/reporters/markdown.py +40 -60
- woolly/reporters/stdout.py +32 -10
- woolly/reporters/template.py +215 -0
- woolly-0.4.0.dist-info/METADATA +324 -0
- woolly-0.4.0.dist-info/RECORD +26 -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.4.0.dist-info}/WHEEL +0 -0
- {woolly-0.2.0.dist-info → woolly-0.4.0.dist-info}/entry_points.txt +0 -0
- {woolly-0.2.0.dist-info → woolly-0.4.0.dist-info}/licenses/LICENSE +0 -0
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,10 @@ 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
|
+
if data.missing_only:
|
|
33
|
+
lines.append("**Missing only:** Yes")
|
|
32
34
|
lines.append("")
|
|
33
35
|
|
|
34
36
|
# Summary
|
|
@@ -39,85 +41,68 @@ class MarkdownReporter(Reporter):
|
|
|
39
41
|
lines.append(f"| Total dependencies checked | {data.total_dependencies} |")
|
|
40
42
|
lines.append(f"| Packaged in Fedora | {data.packaged_count} |")
|
|
41
43
|
lines.append(f"| Missing from Fedora | {data.missing_count} |")
|
|
44
|
+
|
|
45
|
+
# Optional dependency stats
|
|
46
|
+
if data.optional_total > 0:
|
|
47
|
+
lines.append(f"| Optional dependencies | {data.optional_total} |")
|
|
48
|
+
lines.append(f"| Optional - Packaged | {data.optional_packaged} |")
|
|
49
|
+
lines.append(f"| Optional - Missing | {data.optional_missing} |")
|
|
42
50
|
lines.append("")
|
|
43
51
|
|
|
44
|
-
# Missing packages
|
|
45
|
-
|
|
52
|
+
# Missing packages - use computed properties from ReportData
|
|
53
|
+
required_missing = data.required_missing_packages
|
|
54
|
+
optional_missing = data.optional_missing_set
|
|
55
|
+
|
|
56
|
+
if required_missing:
|
|
46
57
|
lines.append("## Missing Packages")
|
|
47
58
|
lines.append("")
|
|
48
59
|
lines.append("The following packages need to be packaged for Fedora:")
|
|
49
60
|
lines.append("")
|
|
50
|
-
for name in sorted(
|
|
61
|
+
for name in sorted(required_missing):
|
|
51
62
|
lines.append(f"- `{name}`")
|
|
52
63
|
lines.append("")
|
|
53
64
|
|
|
54
|
-
|
|
55
|
-
|
|
65
|
+
if optional_missing:
|
|
66
|
+
lines.append("## Missing Optional Packages")
|
|
67
|
+
lines.append("")
|
|
68
|
+
lines.append("The following optional packages are not available in Fedora:")
|
|
69
|
+
lines.append("")
|
|
70
|
+
for name in sorted(optional_missing):
|
|
71
|
+
lines.append(f"- `{name}` *(optional)*")
|
|
72
|
+
lines.append("")
|
|
73
|
+
|
|
74
|
+
# Packaged packages - use computed property (skip if missing_only mode)
|
|
75
|
+
if data.unique_packaged_packages and not data.missing_only:
|
|
56
76
|
lines.append("## Packaged Packages")
|
|
57
77
|
lines.append("")
|
|
58
78
|
lines.append("The following packages are already available in Fedora:")
|
|
59
79
|
lines.append("")
|
|
60
|
-
for name in sorted(
|
|
80
|
+
for name in sorted(data.unique_packaged_packages):
|
|
61
81
|
lines.append(f"- `{name}`")
|
|
62
82
|
lines.append("")
|
|
63
83
|
|
|
64
|
-
# Dependency tree
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
84
|
+
# Dependency tree (skip if missing_only mode)
|
|
85
|
+
if not data.missing_only:
|
|
86
|
+
lines.append("## Dependency Tree")
|
|
87
|
+
lines.append("")
|
|
88
|
+
lines.append("```")
|
|
89
|
+
lines.append(self._tree_to_text(data.tree))
|
|
90
|
+
lines.append("```")
|
|
91
|
+
lines.append("")
|
|
71
92
|
|
|
72
93
|
return "\n".join(lines)
|
|
73
94
|
|
|
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
95
|
def _tree_to_text(self, tree, prefix: str = "") -> str:
|
|
111
96
|
"""Convert Rich Tree to plain text representation."""
|
|
112
97
|
lines = []
|
|
113
98
|
|
|
114
|
-
# Get label text (strip Rich markup)
|
|
99
|
+
# Get label text (strip Rich markup) using inherited method and shared utility
|
|
115
100
|
label = self._get_label(tree)
|
|
116
|
-
label =
|
|
101
|
+
label = strip_markup(label)
|
|
117
102
|
|
|
118
103
|
lines.append(label)
|
|
119
104
|
|
|
120
|
-
# Get children
|
|
105
|
+
# Get children using inherited method
|
|
121
106
|
children = self._get_children(tree)
|
|
122
107
|
|
|
123
108
|
for i, child in enumerate(children):
|
|
@@ -133,8 +118,3 @@ class MarkdownReporter(Reporter):
|
|
|
133
118
|
lines.append(line)
|
|
134
119
|
|
|
135
120
|
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,19 +36,41 @@ 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
|
-
self.console.print(f" • {name}")
|
|
47
|
-
self.console.print()
|
|
53
|
+
required_missing = data.required_missing_packages
|
|
54
|
+
optional_missing = data.optional_missing_set
|
|
48
55
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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()
|
|
69
|
+
|
|
70
|
+
# Print dependency tree (skip if missing_only mode is enabled)
|
|
71
|
+
if not data.missing_only:
|
|
72
|
+
self.console.print("[bold]Dependency Tree:[/bold]")
|
|
73
|
+
self.console.print(data.tree)
|
|
74
|
+
self.console.print()
|
|
53
75
|
|
|
54
76
|
return "" # Output is printed directly
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Template-based report generator using Jinja2.
|
|
3
|
+
|
|
4
|
+
Allows users to provide a custom markdown template file for report generation.
|
|
5
|
+
Only a limited set of variables are exposed for security and simplicity.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from woolly.reporters.base import ReportData, Reporter, strip_markup
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TemplateReporter(Reporter):
|
|
15
|
+
"""Reporter that generates reports from user-provided Jinja2 templates.
|
|
16
|
+
|
|
17
|
+
This reporter allows users to customize the output format by providing
|
|
18
|
+
a markdown template file. Only a limited, safe set of variables are
|
|
19
|
+
exposed to the template.
|
|
20
|
+
|
|
21
|
+
Available template variables:
|
|
22
|
+
Metadata:
|
|
23
|
+
- root_package: Name of the analyzed package
|
|
24
|
+
- language: Language/ecosystem name (e.g., "Rust", "Python")
|
|
25
|
+
- registry: Registry name (e.g., "crates.io", "PyPI")
|
|
26
|
+
- version: Package version (if specified)
|
|
27
|
+
- timestamp: Formatted timestamp string (YYYY-MM-DD HH:MM:SS)
|
|
28
|
+
- max_depth: Maximum recursion depth used
|
|
29
|
+
|
|
30
|
+
Statistics:
|
|
31
|
+
- total_dependencies: Total number of dependencies analyzed
|
|
32
|
+
- packaged_count: Number of packages available in Fedora
|
|
33
|
+
- missing_count: Number of packages missing from Fedora
|
|
34
|
+
|
|
35
|
+
Optional dependency statistics:
|
|
36
|
+
- include_optional: Whether optional deps were included
|
|
37
|
+
- optional_total: Total optional dependencies
|
|
38
|
+
- optional_packaged: Optional deps available in Fedora
|
|
39
|
+
- optional_missing: Optional deps missing from Fedora
|
|
40
|
+
|
|
41
|
+
Package lists (sorted):
|
|
42
|
+
- missing_packages: List of missing required package names
|
|
43
|
+
- packaged_packages: List of packaged package names
|
|
44
|
+
- optional_missing_packages: List of missing optional package names
|
|
45
|
+
|
|
46
|
+
Flags:
|
|
47
|
+
- missing_only: Whether missing-only mode was enabled
|
|
48
|
+
|
|
49
|
+
Example template:
|
|
50
|
+
# Report for {{ root_package }}
|
|
51
|
+
|
|
52
|
+
Generated: {{ timestamp }}
|
|
53
|
+
Language: {{ language }}
|
|
54
|
+
|
|
55
|
+
## Summary
|
|
56
|
+
- Total: {{ total_dependencies }}
|
|
57
|
+
- Packaged: {{ packaged_count }}
|
|
58
|
+
- Missing: {{ missing_count }}
|
|
59
|
+
|
|
60
|
+
{% if missing_packages %}
|
|
61
|
+
## Missing Packages
|
|
62
|
+
{% for pkg in missing_packages %}
|
|
63
|
+
- {{ pkg }}
|
|
64
|
+
{% endfor %}
|
|
65
|
+
{% endif %}
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
name = "template"
|
|
69
|
+
description = "Custom template-based report (requires --template)"
|
|
70
|
+
file_extension = "md"
|
|
71
|
+
writes_to_file = True
|
|
72
|
+
|
|
73
|
+
def __init__(self, template_path: Optional[Path] = None):
|
|
74
|
+
"""Initialize the template reporter.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
template_path: Path to the Jinja2 template file.
|
|
78
|
+
"""
|
|
79
|
+
self._template_path = template_path
|
|
80
|
+
self._jinja2_available: Optional[bool] = None
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def template_path(self) -> Optional[Path]:
|
|
84
|
+
"""Get the template path."""
|
|
85
|
+
return self._template_path
|
|
86
|
+
|
|
87
|
+
@template_path.setter
|
|
88
|
+
def template_path(self, value: Optional[Path]) -> None:
|
|
89
|
+
"""Set the template path."""
|
|
90
|
+
self._template_path = value
|
|
91
|
+
|
|
92
|
+
def _check_jinja2(self) -> bool:
|
|
93
|
+
"""Check if Jinja2 is available.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
True if Jinja2 is available, False otherwise.
|
|
97
|
+
"""
|
|
98
|
+
if self._jinja2_available is None:
|
|
99
|
+
try:
|
|
100
|
+
import jinja2 # type: ignore[import-not-found] # noqa: F401
|
|
101
|
+
|
|
102
|
+
self._jinja2_available = True
|
|
103
|
+
except ImportError:
|
|
104
|
+
self._jinja2_available = False
|
|
105
|
+
return self._jinja2_available
|
|
106
|
+
|
|
107
|
+
def _get_template_context(self, data: ReportData) -> dict:
|
|
108
|
+
"""Build the template context with allowed variables only.
|
|
109
|
+
|
|
110
|
+
This method creates a safe, limited context for template rendering.
|
|
111
|
+
Only specific variables from ReportData are exposed.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
data: Report data containing all information.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Dictionary of template variables.
|
|
118
|
+
"""
|
|
119
|
+
return {
|
|
120
|
+
# Metadata
|
|
121
|
+
"root_package": data.root_package,
|
|
122
|
+
"language": data.language,
|
|
123
|
+
"registry": data.registry,
|
|
124
|
+
"version": data.version,
|
|
125
|
+
"timestamp": data.timestamp.strftime("%Y-%m-%d %H:%M:%S"),
|
|
126
|
+
"max_depth": data.max_depth,
|
|
127
|
+
# Statistics
|
|
128
|
+
"total_dependencies": data.total_dependencies,
|
|
129
|
+
"packaged_count": data.packaged_count,
|
|
130
|
+
"missing_count": data.missing_count,
|
|
131
|
+
# Optional dependency statistics
|
|
132
|
+
"include_optional": data.include_optional,
|
|
133
|
+
"optional_total": data.optional_total,
|
|
134
|
+
"optional_packaged": data.optional_packaged,
|
|
135
|
+
"optional_missing": data.optional_missing,
|
|
136
|
+
# Package lists (sorted for consistent output)
|
|
137
|
+
"missing_packages": sorted(data.required_missing_packages),
|
|
138
|
+
"packaged_packages": sorted(data.unique_packaged_packages),
|
|
139
|
+
"optional_missing_packages": sorted(data.optional_missing_set),
|
|
140
|
+
# Flags
|
|
141
|
+
"missing_only": data.missing_only,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
def generate(self, data: ReportData) -> str:
|
|
145
|
+
"""Generate report content from the template.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
data: Report data containing all information.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Rendered template content as a string.
|
|
152
|
+
|
|
153
|
+
Raises:
|
|
154
|
+
RuntimeError: If Jinja2 is not installed or template is not set.
|
|
155
|
+
FileNotFoundError: If the template file doesn't exist.
|
|
156
|
+
jinja2.TemplateError: If there's a template syntax error.
|
|
157
|
+
"""
|
|
158
|
+
if not self._check_jinja2():
|
|
159
|
+
raise RuntimeError(
|
|
160
|
+
"Jinja2 is required for template reports. "
|
|
161
|
+
"Install it with: pip install jinja2"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
if self._template_path is None:
|
|
165
|
+
raise RuntimeError(
|
|
166
|
+
"No template path specified. Use --template to provide a template file."
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
if not self._template_path.exists():
|
|
170
|
+
raise FileNotFoundError(f"Template file not found: {self._template_path}")
|
|
171
|
+
|
|
172
|
+
# Import Jinja2 here (we've already checked it's available)
|
|
173
|
+
from jinja2 import ( # type: ignore[import-not-found]
|
|
174
|
+
Environment,
|
|
175
|
+
FileSystemLoader,
|
|
176
|
+
StrictUndefined,
|
|
177
|
+
select_autoescape,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# Create Jinja2 environment with security settings
|
|
181
|
+
env = Environment(
|
|
182
|
+
loader=FileSystemLoader(self._template_path.parent),
|
|
183
|
+
autoescape=select_autoescape(default=False),
|
|
184
|
+
undefined=StrictUndefined, # Raise error on undefined variables
|
|
185
|
+
# Disable dangerous features
|
|
186
|
+
extensions=[],
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Add custom filter to strip Rich markup
|
|
190
|
+
env.filters["strip_markup"] = strip_markup
|
|
191
|
+
|
|
192
|
+
# Load and render template
|
|
193
|
+
template = env.get_template(self._template_path.name)
|
|
194
|
+
context = self._get_template_context(data)
|
|
195
|
+
|
|
196
|
+
return template.render(**context)
|
|
197
|
+
|
|
198
|
+
def get_output_filename(self, data: ReportData) -> str:
|
|
199
|
+
"""Get the output filename for the template report.
|
|
200
|
+
|
|
201
|
+
Uses the template filename as a base if available, otherwise
|
|
202
|
+
falls back to the default naming convention.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
data: Report data.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Filename string.
|
|
209
|
+
"""
|
|
210
|
+
timestamp = data.timestamp.strftime("%Y%m%d_%H%M%S")
|
|
211
|
+
if self._template_path:
|
|
212
|
+
# Use template name as base (without extension)
|
|
213
|
+
base_name = self._template_path.stem
|
|
214
|
+
return f"woolly_{data.root_package}_{base_name}_{timestamp}.md"
|
|
215
|
+
return f"woolly_{data.root_package}_template_{timestamp}.md"
|