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.

@@ -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
- dir_to_create_files = Path(output_file).parent
69
-
70
- dir_to_create_intermediate_files = Path(dir_to_create_files)
71
-
72
- merge_files(
73
- output_file=dir_to_create_intermediate_files / "dependency_tree.txt",
74
- root_dir=directory,
75
- target_filename=filename,
76
- )
77
-
78
- dependency_tree = create_diagram(
79
- keep_tree=keep_tree,
80
- intermediate_filename="dependency_tree.txt",
81
- )
82
-
83
- if output_format == "html":
84
- create_html_diagram(dependency_tree, output_file, show_versions)
85
- elif output_format == "json":
86
- create_json_output(dependency_tree, output_file, show_versions)
87
-
88
- print(f"Diagram generated and saved to {output_file}")
89
- print("You can open it in your browser to view the dependency tree.")
90
- print("Thank you for using mvn-tree-visualizer!")
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__":
@@ -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
- with open(intermediate_filename, "r") as file:
9
- dependency_tree: str = file.read()
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
- os.remove(intermediate_filename)
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
- with open(output_file, "w", encoding="utf-8") as outfile:
8
- for dirpath, _, filenames in os.walk(root_dir):
9
- for fname in filenames:
10
- if fname == target_filename:
11
- file_path: str = os.path.join(dirpath, fname)
12
- with open(file_path, "r", encoding="utf-8") as infile:
13
- outfile.write(infile.read())
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")