mvn-tree-visualizer 1.2.0__py3-none-any.whl → 1.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.
Potentially problematic release.
This version of mvn-tree-visualizer might be problematic. Click here for more details.
- mvn_tree_visualizer/cli.py +155 -24
- mvn_tree_visualizer/diagram.py +15 -3
- mvn_tree_visualizer/enhanced_template.py +218 -0
- mvn_tree_visualizer/exceptions.py +25 -0
- mvn_tree_visualizer/file_watcher.py +71 -0
- mvn_tree_visualizer/get_dependencies_in_one_file.py +34 -7
- mvn_tree_visualizer/outputs/html_output.py +81 -14
- mvn_tree_visualizer/outputs/json_output.py +2 -2
- mvn_tree_visualizer/themes.py +189 -0
- mvn_tree_visualizer/validation.py +86 -0
- mvn_tree_visualizer-1.4.0.dist-info/METADATA +209 -0
- mvn_tree_visualizer-1.4.0.dist-info/RECORD +17 -0
- mvn_tree_visualizer/TEMPLATE.py +0 -61
- mvn_tree_visualizer-1.2.0.dist-info/METADATA +0 -94
- mvn_tree_visualizer-1.2.0.dist-info/RECORD +0 -13
- {mvn_tree_visualizer-1.2.0.dist-info → mvn_tree_visualizer-1.4.0.dist-info}/WHEEL +0 -0
- {mvn_tree_visualizer-1.2.0.dist-info → mvn_tree_visualizer-1.4.0.dist-info}/entry_points.txt +0 -0
- {mvn_tree_visualizer-1.2.0.dist-info → mvn_tree_visualizer-1.4.0.dist-info}/licenses/LICENSE +0 -0
mvn_tree_visualizer/cli.py
CHANGED
|
@@ -1,11 +1,128 @@
|
|
|
1
1
|
import argparse
|
|
2
|
+
import time
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
from typing import NoReturn
|
|
4
5
|
|
|
5
6
|
from .diagram import create_diagram
|
|
7
|
+
from .exceptions import DependencyFileNotFoundError, DependencyParsingError, MvnTreeVisualizerError, OutputGenerationError
|
|
8
|
+
from .file_watcher import FileWatcher
|
|
6
9
|
from .get_dependencies_in_one_file import merge_files
|
|
7
10
|
from .outputs.html_output import create_html_diagram
|
|
8
11
|
from .outputs.json_output import create_json_output
|
|
12
|
+
from .validation import find_dependency_files, print_maven_help, validate_dependency_files, validate_output_directory
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def generate_diagram(
|
|
16
|
+
directory: str,
|
|
17
|
+
output_file: str,
|
|
18
|
+
filename: str,
|
|
19
|
+
keep_tree: bool,
|
|
20
|
+
output_format: str,
|
|
21
|
+
show_versions: bool,
|
|
22
|
+
theme: str = "minimal",
|
|
23
|
+
) -> None:
|
|
24
|
+
"""Generate the dependency diagram with comprehensive error handling."""
|
|
25
|
+
timestamp = time.strftime("%H:%M:%S")
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
# Validate inputs
|
|
29
|
+
validate_dependency_files(directory, filename)
|
|
30
|
+
validate_output_directory(output_file)
|
|
31
|
+
|
|
32
|
+
# Show what files we found
|
|
33
|
+
dependency_files = find_dependency_files(directory, filename)
|
|
34
|
+
if len(dependency_files) > 1:
|
|
35
|
+
print(f"[{timestamp}] Found {len(dependency_files)} dependency files")
|
|
36
|
+
|
|
37
|
+
# Setup paths
|
|
38
|
+
dir_to_create_files = Path(output_file).parent
|
|
39
|
+
dir_to_create_intermediate_files = Path(dir_to_create_files)
|
|
40
|
+
intermediate_file_path: Path = dir_to_create_intermediate_files / "dependency_tree.txt"
|
|
41
|
+
|
|
42
|
+
# Merge dependency files
|
|
43
|
+
try:
|
|
44
|
+
merge_files(
|
|
45
|
+
output_file=intermediate_file_path,
|
|
46
|
+
root_dir=directory,
|
|
47
|
+
target_filename=filename,
|
|
48
|
+
)
|
|
49
|
+
except FileNotFoundError as e:
|
|
50
|
+
raise DependencyParsingError(f"Error reading dependency file: {e}\nThe file may have been moved or deleted during processing.")
|
|
51
|
+
except PermissionError as e:
|
|
52
|
+
raise DependencyParsingError(f"Permission denied while reading dependency files: {e}\nPlease check file permissions and try again.")
|
|
53
|
+
except UnicodeDecodeError as e:
|
|
54
|
+
raise DependencyParsingError(
|
|
55
|
+
f"Error decoding dependency file content: {e}\n"
|
|
56
|
+
f"The file may contain invalid characters or use an unsupported encoding.\n"
|
|
57
|
+
f"Please ensure the file is in UTF-8 format."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Validate merged content
|
|
61
|
+
if not intermediate_file_path.exists() or intermediate_file_path.stat().st_size == 0:
|
|
62
|
+
raise DependencyParsingError(
|
|
63
|
+
"Generated dependency tree file is empty.\n"
|
|
64
|
+
"This usually means the Maven dependency files contain no valid dependency information.\n"
|
|
65
|
+
"Please check that your Maven dependency files were generated correctly."
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Create diagram from merged content
|
|
69
|
+
try:
|
|
70
|
+
dependency_tree = create_diagram(
|
|
71
|
+
keep_tree=keep_tree,
|
|
72
|
+
intermediate_filename=str(intermediate_file_path),
|
|
73
|
+
)
|
|
74
|
+
except FileNotFoundError:
|
|
75
|
+
raise DependencyParsingError("Intermediate dependency tree file was not found.\nThis is an internal error - please report this issue.")
|
|
76
|
+
except Exception as e:
|
|
77
|
+
raise DependencyParsingError(f"Error processing dependency tree: {e}\nThe dependency file format may be invalid or corrupted.")
|
|
78
|
+
|
|
79
|
+
# Validate that we have content to work with
|
|
80
|
+
if not dependency_tree.strip():
|
|
81
|
+
raise DependencyParsingError(
|
|
82
|
+
"Dependency tree is empty after processing.\n"
|
|
83
|
+
"Please check that your Maven dependency files contain valid dependency information.\n"
|
|
84
|
+
"You can verify this by opening the files and checking their content."
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Generate output
|
|
88
|
+
try:
|
|
89
|
+
if output_format == "html":
|
|
90
|
+
create_html_diagram(dependency_tree, output_file, show_versions, theme)
|
|
91
|
+
elif output_format == "json":
|
|
92
|
+
create_json_output(dependency_tree, output_file, show_versions)
|
|
93
|
+
else:
|
|
94
|
+
raise OutputGenerationError(f"Unsupported output format: {output_format}")
|
|
95
|
+
except PermissionError:
|
|
96
|
+
raise OutputGenerationError(
|
|
97
|
+
f"Permission denied writing to '{output_file}'.\nPlease check that you have write permissions to this location."
|
|
98
|
+
)
|
|
99
|
+
except OSError as e:
|
|
100
|
+
raise OutputGenerationError(
|
|
101
|
+
f"Error writing output file '{output_file}': {e}\nPlease check that you have enough disk space and write permissions."
|
|
102
|
+
)
|
|
103
|
+
except Exception as e:
|
|
104
|
+
raise OutputGenerationError(f"Error generating {output_format.upper()} output: {e}")
|
|
105
|
+
|
|
106
|
+
print(f"[{timestamp}] ✅ Diagram generated and saved to {output_file}")
|
|
107
|
+
|
|
108
|
+
except MvnTreeVisualizerError as e:
|
|
109
|
+
# Our custom errors already have helpful messages
|
|
110
|
+
print(f"[{timestamp}] ❌ Error: {e}")
|
|
111
|
+
if isinstance(e, DependencyFileNotFoundError):
|
|
112
|
+
print_maven_help()
|
|
113
|
+
except KeyboardInterrupt:
|
|
114
|
+
print(f"\n[{timestamp}] ⏹️ Operation cancelled by user")
|
|
115
|
+
except Exception as e:
|
|
116
|
+
# Unexpected errors
|
|
117
|
+
print(f"[{timestamp}] ❌ Unexpected error: {e}")
|
|
118
|
+
print("This is an internal error. Please report this issue with the following details:")
|
|
119
|
+
print(f" - Directory: {directory}")
|
|
120
|
+
print(f" - Filename: {filename}")
|
|
121
|
+
print(f" - Output: {output_file}")
|
|
122
|
+
print(f" - Format: {output_format}")
|
|
123
|
+
import traceback
|
|
124
|
+
|
|
125
|
+
traceback.print_exc()
|
|
9
126
|
|
|
10
127
|
|
|
11
128
|
def cli() -> NoReturn:
|
|
@@ -57,6 +174,20 @@ def cli() -> NoReturn:
|
|
|
57
174
|
help="Show dependency versions in the diagram. Applicable to both HTML and JSON output formats.",
|
|
58
175
|
)
|
|
59
176
|
|
|
177
|
+
parser.add_argument(
|
|
178
|
+
"--watch",
|
|
179
|
+
action="store_true",
|
|
180
|
+
help="Watch for changes in Maven dependency files and automatically regenerate the diagram.",
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
parser.add_argument(
|
|
184
|
+
"--theme",
|
|
185
|
+
type=str,
|
|
186
|
+
default="minimal",
|
|
187
|
+
choices=["minimal", "dark"],
|
|
188
|
+
help="Theme for the diagram visualization. Default is 'minimal'.",
|
|
189
|
+
)
|
|
190
|
+
|
|
60
191
|
args = parser.parse_args()
|
|
61
192
|
directory: str = args.directory
|
|
62
193
|
output_file: str = args.output
|
|
@@ -64,30 +195,30 @@ def cli() -> NoReturn:
|
|
|
64
195
|
keep_tree: bool = args.keep_tree
|
|
65
196
|
output_format: str = args.format
|
|
66
197
|
show_versions: bool = args.show_versions
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
198
|
+
watch_mode: bool = args.watch
|
|
199
|
+
theme: str = args.theme
|
|
200
|
+
|
|
201
|
+
# Generate initial diagram
|
|
202
|
+
print("Generating initial diagram...")
|
|
203
|
+
generate_diagram(directory, output_file, filename, keep_tree, output_format, show_versions, theme)
|
|
204
|
+
|
|
205
|
+
if not watch_mode:
|
|
206
|
+
print("You can open it in your browser to view the dependency tree.")
|
|
207
|
+
print("Thank you for using mvn-tree-visualizer!")
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
# Watch mode
|
|
211
|
+
def regenerate_callback():
|
|
212
|
+
"""Callback function for file watcher."""
|
|
213
|
+
generate_diagram(directory, output_file, filename, keep_tree, output_format, show_versions, theme)
|
|
214
|
+
|
|
215
|
+
watcher = FileWatcher(directory, filename, regenerate_callback)
|
|
216
|
+
watcher.start()
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
watcher.wait()
|
|
220
|
+
finally:
|
|
221
|
+
print("Thank you for using mvn-tree-visualizer!")
|
|
91
222
|
|
|
92
223
|
|
|
93
224
|
if __name__ == "__main__":
|
mvn_tree_visualizer/diagram.py
CHANGED
|
@@ -5,10 +5,22 @@ def create_diagram(
|
|
|
5
5
|
keep_tree: bool = False,
|
|
6
6
|
intermediate_filename: str = "dependency_tree.txt",
|
|
7
7
|
) -> str:
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
"""Create diagram from dependency tree file."""
|
|
9
|
+
try:
|
|
10
|
+
with open(intermediate_filename, "r", encoding="utf-8") as file:
|
|
11
|
+
dependency_tree: str = file.read()
|
|
12
|
+
except FileNotFoundError:
|
|
13
|
+
raise FileNotFoundError(f"Dependency tree file '{intermediate_filename}' not found")
|
|
14
|
+
except PermissionError:
|
|
15
|
+
raise PermissionError(f"Permission denied reading '{intermediate_filename}'")
|
|
16
|
+
except UnicodeDecodeError as e:
|
|
17
|
+
raise UnicodeDecodeError(e.encoding, e.object, e.start, e.end, f"Error decoding '{intermediate_filename}': {e.reason}")
|
|
10
18
|
|
|
11
19
|
if not keep_tree:
|
|
12
|
-
|
|
20
|
+
try:
|
|
21
|
+
os.remove(intermediate_filename)
|
|
22
|
+
except OSError:
|
|
23
|
+
# If we can't remove the intermediate file, it's not critical
|
|
24
|
+
pass
|
|
13
25
|
|
|
14
26
|
return dependency_tree
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Enhanced HTML templates with the interactive features."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict
|
|
4
|
+
|
|
5
|
+
from .themes import STANDARD_COLORS, Theme
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_html_template(theme: Theme) -> str:
|
|
9
|
+
"""Generate HTML template with theme-specific styling and interactive features."""
|
|
10
|
+
|
|
11
|
+
# Build Mermaid configuration
|
|
12
|
+
mermaid_config = {
|
|
13
|
+
"startOnLoad": True,
|
|
14
|
+
"sequence": {"useMaxWidth": False},
|
|
15
|
+
"theme": theme.mermaid_theme,
|
|
16
|
+
**theme.mermaid_config,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# Convert config to JavaScript object
|
|
20
|
+
mermaid_config_js = _dict_to_js_object(mermaid_config)
|
|
21
|
+
|
|
22
|
+
return f"""<!DOCTYPE html>
|
|
23
|
+
<html lang="en">
|
|
24
|
+
<head>
|
|
25
|
+
<meta charset="UTF-8">
|
|
26
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
27
|
+
<title>Maven Dependency Diagram - {theme.name.title()} Theme</title>
|
|
28
|
+
<style>
|
|
29
|
+
#mySvgId {{
|
|
30
|
+
height: 100%;
|
|
31
|
+
width: 100%;
|
|
32
|
+
}}
|
|
33
|
+
|
|
34
|
+
/* Theme-specific styles */
|
|
35
|
+
{theme.custom_css}
|
|
36
|
+
|
|
37
|
+
/* Dark theme text visibility fixes */
|
|
38
|
+
{
|
|
39
|
+
""
|
|
40
|
+
if theme.name != "dark"
|
|
41
|
+
else '''
|
|
42
|
+
/* Force white text for all mermaid elements in dark theme */
|
|
43
|
+
.node text, .edgeLabel text, text, .label text {
|
|
44
|
+
fill: #ffffff !important;
|
|
45
|
+
color: #ffffff !important;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* Ensure node backgrounds are visible */
|
|
49
|
+
.node rect, .node circle, .node ellipse, .node polygon {
|
|
50
|
+
fill: #4a5568 !important;
|
|
51
|
+
stroke: #e2e8f0 !important;
|
|
52
|
+
stroke-width: 1px !important;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* Edge styling for dark theme */
|
|
56
|
+
.edge path, .flowchart-link {
|
|
57
|
+
stroke: #a0aec0 !important;
|
|
58
|
+
stroke-width: 2px !important;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* Arrow styling */
|
|
62
|
+
.arrowheadPath {
|
|
63
|
+
fill: #a0aec0 !important;
|
|
64
|
+
stroke: #a0aec0 !important;
|
|
65
|
+
}
|
|
66
|
+
'''
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* Improved node styling */
|
|
70
|
+
.node {{
|
|
71
|
+
cursor: pointer;
|
|
72
|
+
transition: opacity 0.2s ease;
|
|
73
|
+
}}
|
|
74
|
+
|
|
75
|
+
.node:hover {{
|
|
76
|
+
opacity: 0.8;
|
|
77
|
+
}}
|
|
78
|
+
|
|
79
|
+
/* Highlighting styles */
|
|
80
|
+
.highlighted {{
|
|
81
|
+
opacity: 1 !important;
|
|
82
|
+
filter: drop-shadow(0 0 8px {STANDARD_COLORS["root_node"]});
|
|
83
|
+
}}
|
|
84
|
+
|
|
85
|
+
.dimmed {{
|
|
86
|
+
opacity: 0.3;
|
|
87
|
+
}}
|
|
88
|
+
</style>
|
|
89
|
+
</head>
|
|
90
|
+
<body>
|
|
91
|
+
<div class="controls">
|
|
92
|
+
<div class="control-group">
|
|
93
|
+
<button id="downloadButton" class="toggle-btn">Download SVG</button>
|
|
94
|
+
<!-- Note: PNG download feature to be implemented in future version -->
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div id="graphDiv"></div>
|
|
99
|
+
|
|
100
|
+
<script src="https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.5.0/dist/svg-pan-zoom.min.js"></script>
|
|
101
|
+
<script type="module">
|
|
102
|
+
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11.9.0/dist/mermaid.esm.min.mjs';
|
|
103
|
+
|
|
104
|
+
// Initialize mermaid with theme configuration
|
|
105
|
+
mermaid.initialize({mermaid_config_js});
|
|
106
|
+
|
|
107
|
+
// Global variables
|
|
108
|
+
let panZoomInstance = null;
|
|
109
|
+
|
|
110
|
+
const drawDiagram = async function () {{
|
|
111
|
+
const element = document.querySelector('#graphDiv');
|
|
112
|
+
const graphDefinition = `{{{{diagram_definition}}}}`;
|
|
113
|
+
|
|
114
|
+
try {{
|
|
115
|
+
const {{ svg }} = await mermaid.render('mySvgId', graphDefinition);
|
|
116
|
+
element.innerHTML = svg.replace(/[ ]*max-width:[ 0-9\\.]*px;/i , '');
|
|
117
|
+
|
|
118
|
+
// Initialize pan & zoom
|
|
119
|
+
panZoomInstance = svgPanZoom('#mySvgId', {{
|
|
120
|
+
zoomEnabled: true,
|
|
121
|
+
controlIconsEnabled: true,
|
|
122
|
+
fit: true,
|
|
123
|
+
center: true,
|
|
124
|
+
minZoom: 0.1,
|
|
125
|
+
maxZoom: 10
|
|
126
|
+
}});
|
|
127
|
+
|
|
128
|
+
// Setup node interactions
|
|
129
|
+
setupNodeInteractions();
|
|
130
|
+
|
|
131
|
+
}} catch (error) {{
|
|
132
|
+
console.error('Error rendering diagram:', error);
|
|
133
|
+
element.innerHTML = `<p style="color: red; padding: 20px;">Error rendering diagram: ${{error.message}}</p>`;
|
|
134
|
+
}}
|
|
135
|
+
}};
|
|
136
|
+
|
|
137
|
+
const setupNodeInteractions = function() {{
|
|
138
|
+
const nodes = document.querySelectorAll('#mySvgId .node');
|
|
139
|
+
|
|
140
|
+
nodes.forEach(node => {{
|
|
141
|
+
node.style.cursor = 'pointer';
|
|
142
|
+
}});
|
|
143
|
+
}};
|
|
144
|
+
|
|
145
|
+
// Download functionality
|
|
146
|
+
document.getElementById('downloadButton').addEventListener('click', function() {{
|
|
147
|
+
downloadSVG();
|
|
148
|
+
}});
|
|
149
|
+
|
|
150
|
+
const downloadSVG = function() {{
|
|
151
|
+
const svg = document.querySelector('#mySvgId');
|
|
152
|
+
let svgData = new XMLSerializer().serializeToString(svg);
|
|
153
|
+
|
|
154
|
+
// Clean up pan & zoom controls
|
|
155
|
+
svgData = svgData.replace(/<g\\b[^>]*\\bclass="svg-pan-zoom-.*?".*?>.*?<\\/g>/g, '');
|
|
156
|
+
svgData = svgData.replace(/<\\/g><\\/svg>/, '</svg>');
|
|
157
|
+
|
|
158
|
+
const svgBlob = new Blob([svgData], {{type: 'image/svg+xml;charset=utf-8'}});
|
|
159
|
+
const svgUrl = URL.createObjectURL(svgBlob);
|
|
160
|
+
const downloadLink = document.createElement('a');
|
|
161
|
+
downloadLink.href = svgUrl;
|
|
162
|
+
downloadLink.download = 'dependency-diagram.svg';
|
|
163
|
+
document.body.appendChild(downloadLink);
|
|
164
|
+
downloadLink.click();
|
|
165
|
+
document.body.removeChild(downloadLink);
|
|
166
|
+
URL.revokeObjectURL(svgUrl);
|
|
167
|
+
}};
|
|
168
|
+
|
|
169
|
+
// Initialize the diagram
|
|
170
|
+
await drawDiagram();
|
|
171
|
+
|
|
172
|
+
// Keyboard shortcuts
|
|
173
|
+
document.addEventListener('keydown', (e) => {{
|
|
174
|
+
if (e.ctrlKey || e.metaKey) {{
|
|
175
|
+
switch(e.key) {{
|
|
176
|
+
case 's':
|
|
177
|
+
e.preventDefault();
|
|
178
|
+
downloadSVG();
|
|
179
|
+
break;
|
|
180
|
+
case 'r':
|
|
181
|
+
e.preventDefault();
|
|
182
|
+
if (panZoomInstance) {{
|
|
183
|
+
panZoomInstance.reset();
|
|
184
|
+
}}
|
|
185
|
+
break;
|
|
186
|
+
}}
|
|
187
|
+
}}
|
|
188
|
+
}});
|
|
189
|
+
|
|
190
|
+
</script>
|
|
191
|
+
</body>
|
|
192
|
+
</html>"""
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _dict_to_js_object(d: Dict[str, Any], indent: int = 0) -> str:
|
|
196
|
+
"""Convert Python dict to JavaScript object string."""
|
|
197
|
+
if not isinstance(d, dict):
|
|
198
|
+
if isinstance(d, str):
|
|
199
|
+
return f'"{d}"'
|
|
200
|
+
elif isinstance(d, bool):
|
|
201
|
+
return str(d).lower()
|
|
202
|
+
else:
|
|
203
|
+
return str(d)
|
|
204
|
+
|
|
205
|
+
items = []
|
|
206
|
+
for key, value in d.items():
|
|
207
|
+
if isinstance(value, dict):
|
|
208
|
+
value_str = _dict_to_js_object(value, indent + 1)
|
|
209
|
+
elif isinstance(value, str):
|
|
210
|
+
value_str = f'"{value}"'
|
|
211
|
+
elif isinstance(value, bool):
|
|
212
|
+
value_str = str(value).lower()
|
|
213
|
+
else:
|
|
214
|
+
value_str = str(value)
|
|
215
|
+
|
|
216
|
+
items.append(f'"{key}": {value_str}')
|
|
217
|
+
|
|
218
|
+
return "{" + ", ".join(items) + "}"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Custom exceptions for mvn-tree-visualizer."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class MvnTreeVisualizerError(Exception):
|
|
5
|
+
"""Base exception for mvn-tree-visualizer errors."""
|
|
6
|
+
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DependencyFileNotFoundError(MvnTreeVisualizerError):
|
|
11
|
+
"""Raised when no dependency files are found."""
|
|
12
|
+
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DependencyParsingError(MvnTreeVisualizerError):
|
|
17
|
+
"""Raised when there's an error parsing dependency files."""
|
|
18
|
+
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class OutputGenerationError(MvnTreeVisualizerError):
|
|
23
|
+
"""Raised when there's an error generating output files."""
|
|
24
|
+
|
|
25
|
+
pass
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""File watcher functionality for monitoring Maven dependency files."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import Callable
|
|
5
|
+
|
|
6
|
+
from watchdog.events import FileSystemEventHandler
|
|
7
|
+
from watchdog.observers import Observer
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DependencyFileHandler(FileSystemEventHandler):
|
|
11
|
+
"""Handler for file system events to trigger diagram regeneration."""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
filename: str,
|
|
16
|
+
callback: Callable[[], None],
|
|
17
|
+
):
|
|
18
|
+
"""Initialize the file handler.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
filename: Name of the file to monitor for changes
|
|
22
|
+
callback: Function to call when file changes are detected
|
|
23
|
+
"""
|
|
24
|
+
self.filename = filename
|
|
25
|
+
self.callback = callback
|
|
26
|
+
|
|
27
|
+
def on_modified(self, event):
|
|
28
|
+
"""Handle file modification events."""
|
|
29
|
+
if not event.is_directory and event.src_path.endswith(self.filename):
|
|
30
|
+
print(f"Detected change in {event.src_path}")
|
|
31
|
+
self.callback()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class FileWatcher:
|
|
35
|
+
"""File system watcher for monitoring Maven dependency files."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, directory: str, filename: str, callback: Callable[[], None]):
|
|
38
|
+
"""Initialize the file watcher.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
directory: Directory to watch for file changes
|
|
42
|
+
filename: Name of the file to monitor
|
|
43
|
+
callback: Function to call when file changes are detected
|
|
44
|
+
"""
|
|
45
|
+
self.directory = directory
|
|
46
|
+
self.filename = filename
|
|
47
|
+
self.callback = callback
|
|
48
|
+
self.observer = Observer()
|
|
49
|
+
self.event_handler = DependencyFileHandler(filename, callback)
|
|
50
|
+
|
|
51
|
+
def start(self) -> None:
|
|
52
|
+
"""Start watching for file changes."""
|
|
53
|
+
print(f"Watching for changes in '{self.filename}' files in '{self.directory}'...")
|
|
54
|
+
print("Press Ctrl+C to stop watching.")
|
|
55
|
+
|
|
56
|
+
self.observer.schedule(self.event_handler, self.directory, recursive=True)
|
|
57
|
+
self.observer.start()
|
|
58
|
+
|
|
59
|
+
def stop(self) -> None:
|
|
60
|
+
"""Stop watching for file changes."""
|
|
61
|
+
print("\nStopping file watcher...")
|
|
62
|
+
self.observer.stop()
|
|
63
|
+
self.observer.join()
|
|
64
|
+
|
|
65
|
+
def wait(self) -> None:
|
|
66
|
+
"""Wait for file changes (blocking)."""
|
|
67
|
+
try:
|
|
68
|
+
while True:
|
|
69
|
+
time.sleep(1)
|
|
70
|
+
except KeyboardInterrupt:
|
|
71
|
+
self.stop()
|
|
@@ -4,10 +4,37 @@ from typing import Union
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
def merge_files(output_file: Union[str, Path], root_dir: str = ".", target_filename: str = "maven_dependency_file") -> None:
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
"""Merge all dependency files from the directory tree into a single file."""
|
|
8
|
+
files_found = 0
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
with open(output_file, "w", encoding="utf-8") as outfile:
|
|
12
|
+
for dirpath, _, filenames in os.walk(root_dir):
|
|
13
|
+
for fname in filenames:
|
|
14
|
+
if fname == target_filename:
|
|
15
|
+
file_path: str = os.path.join(dirpath, fname)
|
|
16
|
+
try:
|
|
17
|
+
with open(file_path, "r", encoding="utf-8") as infile:
|
|
18
|
+
content = infile.read()
|
|
19
|
+
if content.strip(): # Only write non-empty content
|
|
20
|
+
outfile.write(content)
|
|
21
|
+
if not content.endswith("\n"):
|
|
22
|
+
outfile.write("\n")
|
|
23
|
+
files_found += 1
|
|
24
|
+
except UnicodeDecodeError as e:
|
|
25
|
+
raise UnicodeDecodeError(
|
|
26
|
+
e.encoding,
|
|
27
|
+
e.object,
|
|
28
|
+
e.start,
|
|
29
|
+
e.end,
|
|
30
|
+
f"Error reading '{file_path}': {e.reason}. Please ensure the file is in UTF-8 format.",
|
|
31
|
+
)
|
|
32
|
+
except PermissionError:
|
|
33
|
+
raise PermissionError(f"Permission denied reading '{file_path}'")
|
|
34
|
+
except PermissionError:
|
|
35
|
+
raise PermissionError(f"Permission denied writing to '{output_file}'")
|
|
36
|
+
except OSError as e:
|
|
37
|
+
raise OSError(f"Error writing to '{output_file}': {e}")
|
|
38
|
+
|
|
39
|
+
if files_found == 0:
|
|
40
|
+
raise FileNotFoundError(f"No '{target_filename}' files found in '{root_dir}' or its subdirectories")
|