From c2cbbed5964459e8b9ee6682cd6371a9c5684fbc Mon Sep 17 00:00:00 2001
From: Vlad Ciobanu <95963142+vl3c@users.noreply.github.com>
Date: Tue, 17 Feb 2026 20:36:57 +0200
Subject: [PATCH 1/3] Add type stubs for Brython browser module imports
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Add .pyi stubs for the browser package (browser/__init__.pyi, browser/ajax.pyi,
browser/aio.pyi) covering all from-browser imports used in client-side Python.
Fix pre-existing mypy issues in canvas2d_primitive_adapter.py exposed by the
new stubs. Update tests in test_browser_typing_stubs.py.
All 1065 tests pass (0 failures). Mypy errors in the changed files are
pre-existing (647 errors on main, 576 on this branch — net improvement).
---
app.py | 42 +-
cli/browser.py | 12 +-
cli/config.py | 1 +
cli/server.py | 7 +-
cli/tests.py | 6 +-
diagrams/scripts/generate_arch.py | 182 +-
diagrams/scripts/generate_brython_diagrams.py | 558 +-
diagrams/scripts/generate_diagrams.py | 339 +-
diagrams/scripts/setup_diagram_tools.py | 54 +-
diagrams/scripts/utils.py | 26 +-
.../metrics/project_metrics_analyzer.py | 353 +-
generate_diagrams_launcher.py | 2 +-
mypy.ini | 3 +-
run_server_tests.py | 6 +-
scripts/canvas_prompt_telemetry_report.py | 2 +-
scripts/linear_algebra_expected_values.py | 33 +-
server_tests/__init__.py | 5 +-
server_tests/client_renderer/__init__.py | 6 +-
.../client_renderer/renderer_fixtures.py | 12 +-
.../test_canvas2d_primitive_adapter.py | 12 +-
.../test_polar_renderer_plan.py | 12 +-
.../test_renderer_factory_plan.py | 4 +-
.../test_webgl_primitive_adapter.py | 16 +-
server_tests/python_path_setup.py | 3 +-
server_tests/test_adaptive_sampler.py | 46 +-
server_tests/test_ai_model.py | 38 +-
server_tests/test_browser_typing_stubs.py | 1412 +++++
server_tests/test_canvas_state_summarizer.py | 3 +-
server_tests/test_cli/test_config.py | 21 +-
server_tests/test_cli/test_server.py | 14 +-
server_tests/test_coordinate_mapper.py | 115 +-
.../test_coordinate_system_manager.py | 4 +-
.../test_function_renderable_paths.py | 9 +-
server_tests/test_image_attachment.py | 241 +-
server_tests/test_local_provider_base.py | 2 +-
server_tests/test_markdown_parser.py | 63 +-
server_tests/test_mocks.py | 120 +-
server_tests/test_ollama_api.py | 1 +
server_tests/test_ollama_integration.py | 98 +-
server_tests/test_openai_api_base.py | 206 +-
server_tests/test_openai_completions_api.py | 134 +-
server_tests/test_openai_responses_api.py | 253 +-
server_tests/test_plot_tool_schemas.py | 18 +-
server_tests/test_polar_conversion.py | 2 +-
server_tests/test_polar_grid.py | 26 +-
server_tests/test_position.py | 2 +-
server_tests/test_provider_connections.py | 23 +-
server_tests/test_regression_pure.py | 5 +-
server_tests/test_routes.py | 378 +-
server_tests/test_search_tool_wiring_smoke.py | 4 +-
server_tests/test_statistics_pure.py | 2 -
server_tests/test_tool_argument_validator.py | 109 +-
server_tests/test_tool_discovery_live.py | 24 +-
server_tests/test_tool_search_service.py | 80 +-
server_tests/test_tts_manager.py | 5 +-
server_tests/test_workspace_management.py | 150 +-
static/ai_model.py | 4 +-
static/app_manager.py | 34 +-
static/client/ai_interface.py | 295 +-
static/client/browser.pyi | 55 -
static/client/canvas.py | 207 +-
static/client/canvas_event_handler.py | 38 +-
static/client/cartesian_system_2axis.py | 14 +-
.../client_tests/ai_result_formatter.py | 6 +-
.../renderer_performance_tests.py | 5 +-
static/client/client_tests/simple_mock.py | 4 +-
.../test_action_trace_collector.py | 74 +-
static/client/client_tests/test_angle.py | 136 +-
.../client/client_tests/test_angle_manager.py | 64 +-
.../client/client_tests/test_arc_manager.py | 18 +-
.../test_area_expression_evaluator.py | 4 +
.../client/client_tests/test_bar_manager.py | 2 -
.../client/client_tests/test_bar_renderer.py | 6 +-
static/client/client_tests/test_canvas.py | 661 +-
static/client/client_tests/test_cartesian.py | 77 +-
.../client_tests/test_chat_message_menu.py | 2 -
static/client/client_tests/test_circle.py | 9 +-
static/client/client_tests/test_circle_arc.py | 1 -
.../client_tests/test_circle_manager.py | 12 +-
.../test_closed_shape_colored_area.py | 2 -
.../client_tests/test_colored_area_helpers.py | 1 -
.../test_coordinate_system_manager.py | 6 +-
.../test_custom_drawable_names.py | 33 +-
static/client/client_tests/test_decagon.py | 1 -
.../test_drawable_dependency_manager.py | 145 +-
.../test_drawable_name_generator.py | 123 +-
.../client_tests/test_drawable_renderers.py | 13 +-
.../client_tests/test_drawables_container.py | 24 +-
static/client/client_tests/test_ellipse.py | 13 +-
.../client_tests/test_ellipse_manager.py | 8 +-
.../client_tests/test_error_recovery.py | 4 +-
.../client/client_tests/test_event_handler.py | 4 +-
.../client_tests/test_expression_validator.py | 44 +-
.../client/client_tests/test_font_helpers.py | 7 +-
static/client/client_tests/test_function.py | 71 +-
...nction_bounded_colored_area_integration.py | 12 +-
.../client_tests/test_function_calling.py | 77 +-
.../client_tests/test_function_manager.py | 4 +-
.../client_tests/test_function_renderables.py | 108 +-
...t_function_segment_bounded_colored_area.py | 57 +-
.../test_functions_bounded_colored_area.py | 46 +-
.../client_tests/test_generic_polygon.py | 1 -
.../client_tests/test_geometry_utils.py | 1 -
.../client_tests/test_graph_analyzer.py | 39 +-
.../client/client_tests/test_graph_layout.py | 431 +-
.../client/client_tests/test_graph_manager.py | 10 +-
.../client/client_tests/test_graph_utils.py | 22 +-
static/client/client_tests/test_heptagon.py | 1 -
static/client/client_tests/test_hexagon.py | 1 -
.../client_tests/test_image_attachment.py | 57 +-
.../client/client_tests/test_intersections.py | 27 +-
static/client/client_tests/test_label.py | 13 +-
.../test_label_overlap_resolver.py | 2 -
.../client_tests/test_linear_algebra_utils.py | 1 -
.../client_tests/test_math_functions.py | 532 +-
static/client/client_tests/test_nonagon.py | 1 -
.../client_tests/test_numeric_solver.py | 11 +-
static/client/client_tests/test_octagon.py | 1 -
.../client_tests/test_optimized_renderers.py | 13 +-
.../client/client_tests/test_path_elements.py | 1 -
static/client/client_tests/test_pentagon.py | 1 -
.../test_periodicity_detection.py | 26 +-
.../client_tests/test_piecewise_function.py | 19 +-
static/client/client_tests/test_point.py | 7 +-
.../client/client_tests/test_point_manager.py | 12 +-
static/client/client_tests/test_polar_grid.py | 4 +-
.../test_polygon_canonicalizer.py | 9 +-
.../client_tests/test_polygon_manager.py | 10 +-
.../client/client_tests/test_quadrilateral.py | 2 +-
static/client/client_tests/test_rectangle.py | 4 +-
static/client/client_tests/test_region.py | 3 -
.../client_tests/test_relation_inspector.py | 85 +-
.../client_tests/test_renderer_edge_cases.py | 41 +-
.../client_tests/test_renderer_logic.py | 2 +-
.../client_tests/test_renderer_primitives.py | 38 +-
.../test_result_processor_traced.py | 63 +-
.../test_screen_offset_label_layout.py | 9 +-
static/client/client_tests/test_segment.py | 14 +-
.../client_tests/test_segment_manager.py | 1 -
.../test_segments_bounded_colored_area.py | 63 +-
.../client_tests/test_slash_commands.py | 49 +-
.../test_statistics_distributions.py | 3 -
.../client_tests/test_statistics_manager.py | 8 +-
.../client_tests/test_tangent_manager.py | 4 +-
static/client/client_tests/test_throttle.py | 3 +-
.../client/client_tests/test_tool_call_log.py | 5 +-
.../test_transformations_manager.py | 8 +-
static/client/client_tests/test_transforms.py | 1 +
static/client/client_tests/test_triangle.py | 5 +-
.../client_tests/test_undo_redo_manager.py | 1 -
static/client/client_tests/test_vector.py | 5 +-
.../client_tests/test_vector_manager.py | 1 -
.../client/client_tests/test_window_mocks.py | 5 +-
.../client_tests/test_workspace_plots.py | 2 -
static/client/client_tests/test_zoom.py | 3 +-
static/client/client_tests/tests.py | 3 +-
static/client/command_autocomplete.py | 21 +-
static/client/coordinate_mapper.py | 63 +-
static/client/drawables/angle.py | 112 +-
static/client/drawables/attached_label.py | 2 -
static/client/drawables/bar.py | 6 +-
static/client/drawables/bars_plot.py | 2 -
static/client/drawables/circle.py | 11 +-
static/client/drawables/circle_arc.py | 7 +-
.../drawables/closed_shape_colored_area.py | 9 +-
static/client/drawables/colored_area.py | 12 +-
static/client/drawables/continuous_plot.py | 3 -
static/client/drawables/decagon.py | 1 -
static/client/drawables/directed_graph.py | 4 +-
static/client/drawables/discrete_plot.py | 2 -
static/client/drawables/drawable.py | 1 +
static/client/drawables/ellipse.py | 27 +-
static/client/drawables/function.py | 62 +-
.../function_segment_bounded_colored_area.py | 393 +-
.../functions_bounded_colored_area.py | 101 +-
static/client/drawables/generic_polygon.py | 1 -
static/client/drawables/heptagon.py | 1 -
static/client/drawables/hexagon.py | 1 -
static/client/drawables/label.py | 4 +-
static/client/drawables/label_render_mode.py | 3 -
static/client/drawables/nonagon.py | 1 -
static/client/drawables/octagon.py | 1 -
.../client/drawables/parametric_function.py | 14 +-
static/client/drawables/pentagon.py | 1 -
static/client/drawables/piecewise_function.py | 57 +-
.../drawables/piecewise_function_interval.py | 3 +-
static/client/drawables/plot.py | 1 -
static/client/drawables/point.py | 9 +-
static/client/drawables/polygon.py | 4 +-
static/client/drawables/position.py | 3 +-
static/client/drawables/quadrilateral.py | 1 -
static/client/drawables/rectangle.py | 39 +-
static/client/drawables/segment.py | 11 +-
.../segments_bounded_colored_area.py | 42 +-
static/client/drawables/triangle.py | 36 +-
static/client/drawables/undirected_graph.py | 4 +-
static/client/drawables/vector.py | 4 +-
static/client/drawables_aggregator.py | 34 +-
static/client/expression_evaluator.py | 16 +-
static/client/expression_validator.py | 376 +-
static/client/function_registry.py | 49 +-
static/client/geometry/__init__.py | 28 +-
static/client/geometry/graph_state.py | 3 -
static/client/geometry/path/__init__.py | 27 +-
static/client/geometry/path/circular_arc.py | 11 +-
static/client/geometry/path/composite_path.py | 7 +-
static/client/geometry/path/elliptical_arc.py | 26 +-
static/client/geometry/path/intersections.py | 70 +-
static/client/geometry/path/line_segment.py | 1 -
static/client/geometry/path/path_element.py | 1 -
static/client/geometry/region.py | 67 +-
static/client/main.py | 32 +-
.../client/managers/action_trace_collector.py | 32 +-
static/client/managers/angle_manager.py | 180 +-
static/client/managers/arc_manager.py | 47 +-
static/client/managers/bar_manager.py | 2 -
static/client/managers/circle_manager.py | 9 +-
.../client/managers/colored_area_manager.py | 25 +-
.../client/managers/construction_manager.py | 73 +-
static/client/managers/dependency_removal.py | 4 +-
.../managers/drawable_dependency_manager.py | 224 +-
static/client/managers/drawable_manager.py | 156 +-
.../client/managers/drawable_manager_proxy.py | 1 +
static/client/managers/drawables_container.py | 67 +-
static/client/managers/edit_policy.py | 2 -
static/client/managers/ellipse_manager.py | 19 +-
static/client/managers/function_manager.py | 9 +-
static/client/managers/graph_manager.py | 8 +-
static/client/managers/label_manager.py | 1 -
.../managers/parametric_function_manager.py | 4 +-
.../managers/piecewise_function_manager.py | 12 +-
static/client/managers/point_manager.py | 53 +-
static/client/managers/polygon_manager.py | 4 +-
static/client/managers/polygon_type.py | 1 -
static/client/managers/segment_manager.py | 72 +-
static/client/managers/statistics_manager.py | 43 +-
static/client/managers/tangent_manager.py | 14 +-
.../managers/transformations_manager.py | 19 +-
static/client/managers/undo_redo_manager.py | 29 +-
static/client/managers/vector_manager.py | 17 +-
static/client/markdown_parser.py | 316 +-
static/client/name_generator/__init__.py | 10 +-
static/client/name_generator/arc.py | 20 +-
static/client/name_generator/base.py | 2 +-
static/client/name_generator/drawable.py | 25 +-
static/client/name_generator/function.py | 30 +-
static/client/name_generator/label.py | 1 -
static/client/name_generator/point.py | 36 +-
static/client/numeric_solver/__init__.py | 2 +-
.../client/numeric_solver/expression_utils.py | 50 +-
static/client/numeric_solver/solver.py | 17 +-
static/client/polar_grid.py | 17 +-
static/client/process_function_calls.py | 13 +-
static/client/rendering/cached_render_plan.py | 69 +-
.../rendering/canvas2d_primitive_adapter.py | 16 +-
static/client/rendering/canvas2d_renderer.py | 42 +-
static/client/rendering/factory.py | 9 +-
static/client/rendering/helpers/__init__.py | 1 -
.../rendering/helpers/angle_renderer.py | 17 +-
.../client/rendering/helpers/area_builders.py | 1 -
.../client/rendering/helpers/bar_renderer.py | 2 -
.../rendering/helpers/cartesian_renderer.py | 163 +-
.../rendering/helpers/circle_arc_renderer.py | 5 +-
.../rendering/helpers/circle_renderer.py | 1 -
.../helpers/colored_area_renderer.py | 1 -
.../rendering/helpers/ellipse_renderer.py | 1 -
.../client/rendering/helpers/font_helpers.py | 1 -
.../rendering/helpers/function_renderer.py | 1 -
.../helpers/label_overlap_resolver.py | 2 -
.../rendering/helpers/label_renderer.py | 1 -
.../helpers/parametric_function_renderer.py | 5 +-
.../rendering/helpers/point_renderer.py | 1 -
.../rendering/helpers/polar_renderer.py | 78 +-
.../helpers/screen_offset_label_helper.py | 2 -
.../helpers/screen_offset_label_layout.py | 15 +-
.../rendering/helpers/segment_renderer.py | 1 -
.../rendering/helpers/shape_decorator.py | 3 +-
.../rendering/helpers/vector_renderer.py | 1 -
.../rendering/helpers/world_label_helper.py | 6 +-
static/client/rendering/interfaces.py | 26 +-
static/client/rendering/primitives.py | 26 +-
.../client/rendering/renderables/__init__.py | 1 -
.../rendering/renderables/adaptive_sampler.py | 21 +-
.../closed_shape_area_renderable.py | 1 -
.../renderables/function_renderable.py | 108 +-
.../function_segment_area_renderable.py | 13 +-
.../renderables/functions_area_renderable.py | 22 +-
.../renderables/segments_area_renderable.py | 5 +-
static/client/rendering/style_manager.py | 14 -
.../client/rendering/svg_primitive_adapter.py | 13 +-
static/client/rendering/svg_renderer.py | 22 +-
.../rendering/webgl_primitive_adapter.py | 1 -
static/client/rendering/webgl_renderer.py | 20 +-
static/client/result_processor.py | 166 +-
static/client/slash_command_handler.py | 3 +
static/client/test_runner.py | 313 +-
static/client/tts_controller.py | 34 +-
static/client/typing/browser/__init__.pyi | 214 +
static/client/typing/browser/_dom.pyi | 70 +
static/client/typing/browser/aio.pyi | 12 +
static/client/typing/browser/ajax.pyi | 46 +
.../client/utils/area_expression_evaluator.py | 47 +-
.../client/utils/canonicalizers/__init__.py | 3 -
static/client/utils/canonicalizers/common.py | 3 -
.../utils/canonicalizers/quadrilateral.py | 26 +-
.../client/utils/canonicalizers/triangle.py | 4 +-
static/client/utils/computation_utils.py | 6 +-
static/client/utils/geometry_utils.py | 87 +-
static/client/utils/graph_analyzer.py | 52 +-
static/client/utils/graph_layout.py | 143 +-
static/client/utils/graph_utils.py | 12 +-
static/client/utils/linear_algebra_utils.py | 4 +-
static/client/utils/math_utils.py | 301 +-
static/client/utils/polygon_canonicalizer.py | 3 -
static/client/utils/polygon_subtypes.py | 3 +-
static/client/utils/relation_inspector.py | 121 +-
.../client/utils/statistics/distributions.py | 7 +-
static/client/utils/statistics/regression.py | 25 +-
static/client/utils/style_utils.py | 169 +-
static/client/workspace_manager.py | 101 +-
static/functions_definitions.py | 5558 ++++++++---------
static/log_manager.py | 24 +-
static/mirror_client_modules.py | 1 -
static/openai_api_base.py | 44 +-
static/openai_completions_api.py | 54 +-
static/openai_responses_api.py | 80 +-
static/providers/__init__.py | 8 +-
static/providers/anthropic_api.py | 99 +-
static/providers/local/__init__.py | 73 +-
static/providers/local/ollama_api.py | 24 +-
static/providers/openrouter_api.py | 1 +
static/routes.py | 477 +-
static/tool_argument_validator.py | 38 +-
static/tool_search_service.py | 42 +-
static/tts_manager.py | 18 +-
static/webdriver_manager.py | 28 +-
static/workspace_manager.py | 17 +-
337 files changed, 12293 insertions(+), 10019 deletions(-)
create mode 100644 server_tests/test_browser_typing_stubs.py
delete mode 100644 static/client/browser.pyi
create mode 100644 static/client/typing/browser/__init__.pyi
create mode 100644 static/client/typing/browser/_dom.pyi
create mode 100644 static/client/typing/browser/aio.pyi
create mode 100644 static/client/typing/browser/ajax.pyi
diff --git a/app.py b/app.py
index 7a4bf742..2fe46a39 100644
--- a/app.py
+++ b/app.py
@@ -17,7 +17,7 @@ def signal_handler(sig: int, frame: FrameType | None) -> None:
Cleans up WebDriver and Ollama resources and exits the application properly.
"""
- print('\nShutting down gracefully...')
+ print("\nShutting down gracefully...")
# Clean up WebDriverManager
if app.webdriver_manager is not None:
try:
@@ -28,6 +28,7 @@ def signal_handler(sig: int, frame: FrameType | None) -> None:
# Clean up Ollama server if we started it
try:
from static.providers.local.ollama_api import OllamaAPI
+
OllamaAPI.stop_server()
except Exception as e:
print(f"Error stopping Ollama: {e}")
@@ -42,54 +43,55 @@ def signal_handler(sig: int, frame: FrameType | None) -> None:
# Register signal handler at module level for both run modes
signal.signal(signal.SIGINT, signal_handler)
-if __name__ == '__main__':
+if __name__ == "__main__":
"""Main execution block.
Starts Flask server in a daemon thread, initializes WebDriver for vision system,
and maintains the main thread for graceful interrupt handling.
"""
# Parse command-line arguments
- parser = argparse.ArgumentParser(description='MatHud Flask Application')
+ parser = argparse.ArgumentParser(description="MatHud Flask Application")
parser.add_argument(
- '-p', '--port',
- type=int,
- default=None,
- help='Port to run the server on (default: 5000, or PORT env var)'
+ "-p", "--port", type=int, default=None, help="Port to run the server on (default: 5000, or PORT env var)"
)
args = parser.parse_args()
try:
# Priority: CLI argument > environment variable > default (5000)
- env_port = os.environ.get('PORT')
+ env_port = os.environ.get("PORT")
port = args.port if args.port is not None else int(env_port or 5000)
# Store port in app config for WebDriverManager to use
- app.config['SERVER_PORT'] = port
+ app.config["SERVER_PORT"] = port
# Check if we're running in a deployment environment
is_deployed = args.port is None and env_port is not None
- force_non_debug = os.environ.get('MATHUD_NON_DEBUG', '').lower() in ('1', 'true', 'yes')
+ force_non_debug = os.environ.get("MATHUD_NON_DEBUG", "").lower() in ("1", "true", "yes")
# Enable debug mode for local development
debug_mode = not (is_deployed or force_non_debug)
if is_deployed:
# For deployment: run Flask directly without threading
- host = '0.0.0.0' # Bind to all interfaces for deployment
+ host = "0.0.0.0" # Bind to all interfaces for deployment
print(f"Starting Flask app on {host}:{port} (deployment mode)")
app.run(host=host, port=port, debug=False)
else:
# For local development: use threading approach with debug capability
- host = '127.0.0.1' # Localhost for development
+ host = "127.0.0.1" # Localhost for development
print(f"Starting Flask app on {host}:{port} (development mode, debug={debug_mode})")
from threading import Thread
- server = Thread(target=app.run, kwargs={
- 'host': host,
- 'port': port,
- 'debug': debug_mode,
- 'use_reloader': False # Disable reloader in thread mode to avoid issues
- })
+
+ server = Thread(
+ target=app.run,
+ kwargs={
+ "host": host,
+ "port": port,
+ "debug": debug_mode,
+ "use_reloader": False, # Disable reloader in thread mode to avoid issues
+ },
+ )
server.daemon = True # Make the server thread a daemon so it exits when main thread exits
server.start()
@@ -99,6 +101,7 @@ def signal_handler(sig: int, frame: FrameType | None) -> None:
# Start Ollama server if installed (only in local development)
try:
from static.providers.local.ollama_api import OllamaAPI
+
if OllamaAPI.is_ollama_installed():
success, message = OllamaAPI.start_server(timeout=10)
print(f"Ollama: {message}")
@@ -110,8 +113,9 @@ def signal_handler(sig: int, frame: FrameType | None) -> None:
# Initialize WebDriver (only in local development)
if app.webdriver_manager is None:
import requests
+
try:
- requests.get(f'http://{host}:{port}/init_webdriver')
+ requests.get(f"http://{host}:{port}/init_webdriver")
print("WebDriver initialized successfully")
except Exception as e:
print(f"Failed to initialize WebDriver: {str(e)}")
diff --git a/cli/browser.py b/cli/browser.py
index f8d1afde..ddc07a6e 100644
--- a/cli/browser.py
+++ b/cli/browser.py
@@ -88,9 +88,7 @@ def setup(self) -> None:
options.add_argument("--remote-debugging-pipe")
profiles_root = runtime_root / "profiles"
profiles_root.mkdir(parents=True, exist_ok=True)
- self._profile_dir = Path(
- tempfile.mkdtemp(prefix="chrome-profile-", dir=str(profiles_root))
- )
+ self._profile_dir = Path(tempfile.mkdtemp(prefix="chrome-profile-", dir=str(profiles_root)))
options.add_argument(f"--user-data-dir={self._profile_dir}")
if platform.machine() in ("aarch64", "arm64"):
@@ -266,9 +264,7 @@ def wait_for_element(
raise RuntimeError("Browser not initialized. Call setup() first.")
try:
- WebDriverWait(self.driver, timeout).until(
- EC.presence_of_element_located((by, selector))
- )
+ WebDriverWait(self.driver, timeout).until(EC.presence_of_element_located((by, selector)))
return True
except Exception:
return False
@@ -313,9 +309,7 @@ def get_canvas_state(self) -> dict[str, Any]:
Returns:
Canvas state dictionary.
"""
- result = self.execute_js(
- "return window._canvas ? JSON.stringify(window._canvas.get_state()) : null"
- )
+ result = self.execute_js("return window._canvas ? JSON.stringify(window._canvas.get_state()) : null")
if result:
parsed: dict[str, Any] = json.loads(result)
return parsed
diff --git a/cli/config.py b/cli/config.py
index e7b9f11d..4e0b57b7 100644
--- a/cli/config.py
+++ b/cli/config.py
@@ -43,6 +43,7 @@
# CLI output directory (for screenshots, etc.)
CLI_OUTPUT_DIR = Path(__file__).parent / "output"
+
# Python interpreter path
def get_python_path() -> Path:
"""Get the path to the Python interpreter in the virtual environment."""
diff --git a/cli/server.py b/cli/server.py
index 86546296..bca952c2 100644
--- a/cli/server.py
+++ b/cli/server.py
@@ -262,8 +262,7 @@ def start(
)
return (
False,
- f"Port {self.port} is already in use. "
- f"Choose another port or stop the existing listener.",
+ f"Port {self.port} is already in use. Choose another port or stop the existing listener.",
)
else:
owner_pid = self._find_listener_pid_on_port()
@@ -275,8 +274,7 @@ def start(
)
return (
False,
- f"Port {self.port} is already in use. "
- f"Choose another port or stop the existing listener.",
+ f"Port {self.port} is already in use. Choose another port or stop the existing listener.",
)
# Set environment to disable auth for CLI operations
@@ -451,6 +449,7 @@ def status(port: int, as_json: bool) -> None:
if as_json:
import json
+
click.echo(json.dumps(info))
else:
if info["running"]:
diff --git a/cli/tests.py b/cli/tests.py
index b9fb96b6..2ca125b4 100644
--- a/cli/tests.py
+++ b/cli/tests.py
@@ -451,6 +451,10 @@ def all_cmd(port: int, with_auth: bool, start_server: bool, skip_lint: bool) ->
raise SystemExit(1)
lint_label = "Lint + " if not skip_lint else ""
- click.echo(click.style(f"\nAll passed! ({lint_label}Server + {results.get('tests_run', 0)} client tests)", fg="green", bold=True))
+ click.echo(
+ click.style(
+ f"\nAll passed! ({lint_label}Server + {results.get('tests_run', 0)} client tests)", fg="green", bold=True
+ )
+ )
if results.get("screenshot"):
click.echo(f"\nScreenshot saved to: {results['screenshot']}")
diff --git a/diagrams/scripts/generate_arch.py b/diagrams/scripts/generate_arch.py
index 8473f23c..d50c18e2 100644
--- a/diagrams/scripts/generate_arch.py
+++ b/diagrams/scripts/generate_arch.py
@@ -54,8 +54,6 @@ def __init__(
self.svg_dir.mkdir(exist_ok=True)
(self.svg_dir / "architecture").mkdir(exist_ok=True)
-
-
def clean_generated_folders(self) -> None:
"""Carefully delete all content from generated_png and generated_svg folders."""
print("Cleaning generated folders before architecture diagram generation...")
@@ -94,6 +92,7 @@ def clean_generated_folders(self) -> None:
elif item.is_dir():
# Recursively delete directory contents
import shutil
+
shutil.rmtree(item)
folder_dirs += 1
total_deleted_dirs += 1
@@ -123,8 +122,6 @@ def get_output_dir(self, fmt: str) -> Path:
else:
return self.png_dir / "architecture"
-
-
def generate_system_overview_diagram(self) -> None:
"""Generate overall MatHud system architecture diagram."""
try:
@@ -142,23 +139,20 @@ def generate_system_overview_diagram(self) -> None:
output_dir = self.get_output_dir(fmt)
diagram_path = output_dir / "system_overview"
- with Diagram("MatHud System Overview",
- filename=str(diagram_path),
- show=False,
- direction="TB",
- outformat=fmt,
- graph_attr={
- "fontname": DIAGRAM_FONT,
- "fontsize": str(DIAGRAM_FONT_SIZE),
- "dpi": "150"
- },
- node_attr={
- "fontname": DIAGRAM_FONT,
- "fontsize": str(DIAGRAM_FONT_SIZE),
- "width": "1.2",
- "height": "1.2"
- }):
-
+ with Diagram(
+ "MatHud System Overview",
+ filename=str(diagram_path),
+ show=False,
+ direction="TB",
+ outformat=fmt,
+ graph_attr={"fontname": DIAGRAM_FONT, "fontsize": str(DIAGRAM_FONT_SIZE), "dpi": "150"},
+ node_attr={
+ "fontname": DIAGRAM_FONT,
+ "fontsize": str(DIAGRAM_FONT_SIZE),
+ "width": "1.2",
+ "height": "1.2",
+ },
+ ):
# User Interface Layer
user = Client("User Browser")
@@ -242,8 +236,8 @@ def generate_system_overview_diagram(self) -> None:
print(f" + System overview diagram: {diagram_path}.{fmt}")
# Post-process SVG files to use configured font
- if fmt == 'svg':
- post_process_svg_fonts(output_dir / f'system_overview.{fmt}')
+ if fmt == "svg":
+ post_process_svg_fonts(output_dir / f"system_overview.{fmt}")
except Exception as e:
print(f"System overview diagram failed: {e}")
@@ -261,23 +255,20 @@ def generate_ai_integration_diagram(self) -> None:
output_dir = self.get_output_dir(fmt)
diagram_path = output_dir / "ai_integration"
- with Diagram("MatHud AI Integration & Function Call Flow",
- filename=str(diagram_path),
- show=False,
- direction="LR",
- outformat=fmt,
- graph_attr={
- "fontname": DIAGRAM_FONT,
- "fontsize": str(DIAGRAM_FONT_SIZE),
- "dpi": "150"
- },
- node_attr={
- "fontname": DIAGRAM_FONT,
- "fontsize": str(DIAGRAM_FONT_SIZE),
- "width": "1.2",
- "height": "1.2"
- }):
-
+ with Diagram(
+ "MatHud AI Integration & Function Call Flow",
+ filename=str(diagram_path),
+ show=False,
+ direction="LR",
+ outformat=fmt,
+ graph_attr={"fontname": DIAGRAM_FONT, "fontsize": str(DIAGRAM_FONT_SIZE), "dpi": "150"},
+ node_attr={
+ "fontname": DIAGRAM_FONT,
+ "fontsize": str(DIAGRAM_FONT_SIZE),
+ "width": "1.2",
+ "height": "1.2",
+ },
+ ):
# Input Sources
with Cluster("User Input"):
user_text = Client("User Message\n(Math Problems)")
@@ -340,8 +331,8 @@ def generate_ai_integration_diagram(self) -> None:
print(f" + AI integration diagram: {diagram_path}.{fmt}")
# Post-process SVG files to use configured font
- if fmt == 'svg':
- post_process_svg_fonts(output_dir / f'ai_integration.{fmt}')
+ if fmt == "svg":
+ post_process_svg_fonts(output_dir / f"ai_integration.{fmt}")
except Exception as e:
print(f"AI integration diagram failed: {e}")
@@ -358,23 +349,20 @@ def generate_webdriver_flow_diagram(self) -> None:
output_dir = self.get_output_dir(fmt)
diagram_path = output_dir / "webdriver_flow"
- with Diagram("MatHud Vision System & WebDriver Flow",
- filename=str(diagram_path),
- show=False,
- direction="TB",
- outformat=fmt,
- graph_attr={
- "fontname": DIAGRAM_FONT,
- "fontsize": str(DIAGRAM_FONT_SIZE),
- "dpi": "150"
- },
- node_attr={
- "fontname": DIAGRAM_FONT,
- "fontsize": str(DIAGRAM_FONT_SIZE),
- "width": "1.2",
- "height": "1.2"
- }):
-
+ with Diagram(
+ "MatHud Vision System & WebDriver Flow",
+ filename=str(diagram_path),
+ show=False,
+ direction="TB",
+ outformat=fmt,
+ graph_attr={"fontname": DIAGRAM_FONT, "fontsize": str(DIAGRAM_FONT_SIZE), "dpi": "150"},
+ node_attr={
+ "fontname": DIAGRAM_FONT,
+ "fontsize": str(DIAGRAM_FONT_SIZE),
+ "width": "1.2",
+ "height": "1.2",
+ },
+ ):
# Trigger
with Cluster("Vision Request Trigger"):
user_request = Client("User Enables Vision\n+ Sends Message")
@@ -432,8 +420,8 @@ def generate_webdriver_flow_diagram(self) -> None:
print(f" + WebDriver flow diagram: {diagram_path}.{fmt}")
# Post-process SVG files to use configured font
- if fmt == 'svg':
- post_process_svg_fonts(output_dir / f'webdriver_flow.{fmt}')
+ if fmt == "svg":
+ post_process_svg_fonts(output_dir / f"webdriver_flow.{fmt}")
except Exception as e:
print(f"WebDriver flow diagram failed: {e}")
@@ -453,23 +441,20 @@ def generate_data_flow_diagram(self) -> None:
output_dir = self.get_output_dir(fmt)
diagram_path = output_dir / "data_flow"
- with Diagram("MatHud Data Flow Pipeline",
- filename=str(diagram_path),
- show=False,
- direction="LR",
- outformat=fmt,
- graph_attr={
- "fontname": DIAGRAM_FONT,
- "fontsize": str(DIAGRAM_FONT_SIZE),
- "dpi": "150"
- },
- node_attr={
- "fontname": DIAGRAM_FONT,
- "fontsize": str(DIAGRAM_FONT_SIZE),
- "width": "1.2",
- "height": "1.2"
- }):
-
+ with Diagram(
+ "MatHud Data Flow Pipeline",
+ filename=str(diagram_path),
+ show=False,
+ direction="LR",
+ outformat=fmt,
+ graph_attr={"fontname": DIAGRAM_FONT, "fontsize": str(DIAGRAM_FONT_SIZE), "dpi": "150"},
+ node_attr={
+ "fontname": DIAGRAM_FONT,
+ "fontsize": str(DIAGRAM_FONT_SIZE),
+ "width": "1.2",
+ "height": "1.2",
+ },
+ ):
# Input Stage
with Cluster("Input Stage"):
user_input = Client("User Input")
@@ -525,8 +510,8 @@ def generate_data_flow_diagram(self) -> None:
print(f" + Data flow diagram: {diagram_path}.{fmt}")
# Post-process SVG files to use configured font
- if fmt == 'svg':
- post_process_svg_fonts(output_dir / f'data_flow.{fmt}')
+ if fmt == "svg":
+ post_process_svg_fonts(output_dir / f"data_flow.{fmt}")
except Exception as e:
print(f"Data flow diagram failed: {e}")
@@ -542,23 +527,20 @@ def generate_manager_architecture_diagram(self) -> None:
output_dir = self.get_output_dir(fmt)
diagram_path = output_dir / "manager_architecture"
- with Diagram("MatHud Manager Pattern Architecture",
- filename=str(diagram_path),
- show=False,
- direction="TB",
- outformat=fmt,
- graph_attr={
- "fontname": DIAGRAM_FONT,
- "fontsize": str(DIAGRAM_FONT_SIZE),
- "dpi": "150"
- },
- node_attr={
- "fontname": DIAGRAM_FONT,
- "fontsize": str(DIAGRAM_FONT_SIZE),
- "width": "1.2",
- "height": "1.2"
- }):
-
+ with Diagram(
+ "MatHud Manager Pattern Architecture",
+ filename=str(diagram_path),
+ show=False,
+ direction="TB",
+ outformat=fmt,
+ graph_attr={"fontname": DIAGRAM_FONT, "fontsize": str(DIAGRAM_FONT_SIZE), "dpi": "150"},
+ node_attr={
+ "fontname": DIAGRAM_FONT,
+ "fontsize": str(DIAGRAM_FONT_SIZE),
+ "width": "1.2",
+ "height": "1.2",
+ },
+ ):
# Central Coordinator
with Cluster("Central Canvas System"):
canvas = Python("Canvas\n(SVG Manipulation)")
@@ -638,8 +620,8 @@ def generate_manager_architecture_diagram(self) -> None:
print(f" + Manager architecture diagram: {diagram_path}.{fmt}")
# Post-process SVG files to use configured font
- if fmt == 'svg':
- post_process_svg_fonts(output_dir / f'manager_architecture.{fmt}')
+ if fmt == "svg":
+ post_process_svg_fonts(output_dir / f"manager_architecture.{fmt}")
except Exception as e:
print(f"Manager architecture diagram failed: {e}")
@@ -703,5 +685,5 @@ def main() -> None:
generator.generate_all_architecture_diagrams(clean_first=True)
-if __name__ == '__main__':
+if __name__ == "__main__":
main()
diff --git a/diagrams/scripts/generate_brython_diagrams.py b/diagrams/scripts/generate_brython_diagrams.py
index ca10c9e7..6536d6a7 100644
--- a/diagrams/scripts/generate_brython_diagrams.py
+++ b/diagrams/scripts/generate_brython_diagrams.py
@@ -27,12 +27,7 @@
from typing import List, Sequence, Tuple
# Import shared utilities
-from utils import (
- setup_graphviz_path,
- setup_font_environment,
- post_process_svg_fonts,
- DIAGRAM_FONT
-)
+from utils import setup_graphviz_path, setup_font_environment, post_process_svg_fonts, DIAGRAM_FONT
class BrythonDiagramGenerator:
@@ -95,29 +90,58 @@ def __init__(
# System component definitions
self.drawable_classes: List[str] = [
- 'point.py', 'segment.py', 'vector.py', 'triangle.py', 'rectangle.py',
- 'circle.py', 'ellipse.py', 'angle.py', 'function.py', 'colored_area.py',
- 'functions_bounded_colored_area.py', 'function_segment_bounded_colored_area.py',
- 'segments_bounded_colored_area.py', 'rotatable_polygon.py', 'drawable.py'
+ "point.py",
+ "segment.py",
+ "vector.py",
+ "triangle.py",
+ "rectangle.py",
+ "circle.py",
+ "ellipse.py",
+ "angle.py",
+ "function.py",
+ "colored_area.py",
+ "functions_bounded_colored_area.py",
+ "function_segment_bounded_colored_area.py",
+ "segments_bounded_colored_area.py",
+ "rotatable_polygon.py",
+ "drawable.py",
]
self.manager_classes: List[str] = [
- 'drawable_manager.py', 'point_manager.py', 'segment_manager.py',
- 'vector_manager.py', 'polygon_manager.py',
- 'circle_manager.py', 'ellipse_manager.py', 'angle_manager.py',
- 'function_manager.py', 'colored_area_manager.py', 'drawable_dependency_manager.py',
- 'drawable_manager_proxy.py', 'transformations_manager.py', 'undo_redo_manager.py',
- 'drawables_container.py'
+ "drawable_manager.py",
+ "point_manager.py",
+ "segment_manager.py",
+ "vector_manager.py",
+ "polygon_manager.py",
+ "circle_manager.py",
+ "ellipse_manager.py",
+ "angle_manager.py",
+ "function_manager.py",
+ "colored_area_manager.py",
+ "drawable_dependency_manager.py",
+ "drawable_manager_proxy.py",
+ "transformations_manager.py",
+ "undo_redo_manager.py",
+ "drawables_container.py",
]
self.core_system_files: List[str] = [
- 'canvas.py', 'ai_interface.py', 'canvas_event_handler.py',
- 'workspace_manager.py', 'result_processor.py', 'process_function_calls.py'
+ "canvas.py",
+ "ai_interface.py",
+ "canvas_event_handler.py",
+ "workspace_manager.py",
+ "result_processor.py",
+ "process_function_calls.py",
]
self.utility_files: List[str] = [
- 'expression_evaluator.py', 'expression_validator.py', 'markdown_parser.py',
- 'function_registry.py', 'result_validator.py', 'constants.py', 'geometry.py'
+ "expression_evaluator.py",
+ "expression_validator.py",
+ "markdown_parser.py",
+ "function_registry.py",
+ "result_validator.py",
+ "constants.py",
+ "geometry.py",
]
def get_brython_output_dir(self, fmt: str, subdir: str = "") -> Path:
@@ -214,32 +238,37 @@ def generate_core_system_diagrams(self) -> None:
for fmt in self.formats:
output_dir = self.get_brython_output_dir(fmt, "core")
cmd = [
- 'pyreverse',
- '-o', fmt,
- '-p', 'brython_core_classes',
- '--output-directory', str(output_dir),
- '--show-associated', '1',
- '--show-ancestors', '1'
+ "pyreverse",
+ "-o",
+ fmt,
+ "-p",
+ "brython_core_classes",
+ "--output-directory",
+ str(output_dir),
+ "--show-associated",
+ "1",
+ "--show-ancestors",
+ "1",
] + core_files
try:
subprocess.run(cmd, check=True, capture_output=True, text=True)
print(f" + Core system diagram generated: {output_dir}/classes_brython_core_classes.{fmt}")
- if fmt == 'svg':
- self._process_svg_font_and_count(output_dir / f'classes_brython_core_classes.{fmt}')
+ if fmt == "svg":
+ self._process_svg_font_and_count(output_dir / f"classes_brython_core_classes.{fmt}")
except subprocess.CalledProcessError as e:
print(f"Error generating core system diagram: {e.stderr}")
# Individual core component diagrams
important_core_modules: List[Tuple[str, str]] = [
- ('canvas.py', 'brython_canvas_system'),
- ('ai_interface.py', 'brython_ai_interface'),
- ('canvas_event_handler.py', 'brython_event_handling'),
- ('workspace_manager.py', 'brython_workspace_client'),
- ('result_processor.py', 'brython_result_processor'),
- ('process_function_calls.py', 'brython_function_execution')
+ ("canvas.py", "brython_canvas_system"),
+ ("ai_interface.py", "brython_ai_interface"),
+ ("canvas_event_handler.py", "brython_event_handling"),
+ ("workspace_manager.py", "brython_workspace_client"),
+ ("result_processor.py", "brython_result_processor"),
+ ("process_function_calls.py", "brython_function_execution"),
]
for module_file, diagram_name in important_core_modules:
@@ -248,20 +277,24 @@ def generate_core_system_diagrams(self) -> None:
for fmt in self.formats:
output_dir = self.get_brython_output_dir(fmt, "core")
cmd = [
- 'pyreverse',
- '-o', fmt,
- '-p', diagram_name,
- '--output-directory', str(output_dir),
- '--show-associated', '1',
- str(module_path)
+ "pyreverse",
+ "-o",
+ fmt,
+ "-p",
+ diagram_name,
+ "--output-directory",
+ str(output_dir),
+ "--show-associated",
+ "1",
+ str(module_path),
]
try:
subprocess.run(cmd, check=True, capture_output=True, text=True)
print(f" + {diagram_name} diagram generated: {output_dir}/classes_{diagram_name}.{fmt}")
- if fmt == 'svg':
- self._process_svg_font_and_count(output_dir / f'classes_{diagram_name}.{fmt}')
+ if fmt == "svg":
+ self._process_svg_font_and_count(output_dir / f"classes_{diagram_name}.{fmt}")
except subprocess.CalledProcessError as e:
print(f"Error generating {diagram_name} diagram: {e.stderr}")
@@ -277,67 +310,96 @@ def generate_drawable_system_diagrams(self) -> None:
# Complete drawable hierarchy diagram
drawable_files: List[str] = [
- str(drawables_dir / module)
- for module in self.drawable_classes
- if (drawables_dir / module).exists()
+ str(drawables_dir / module) for module in self.drawable_classes if (drawables_dir / module).exists()
]
if drawable_files:
for fmt in self.formats:
output_dir = self.get_brython_output_dir(fmt, "drawables")
cmd = [
- 'pyreverse',
- '-o', fmt,
- '-p', 'brython_drawable_hierarchy',
- '--output-directory', str(output_dir),
- '--show-associated', '1',
- '--show-ancestors', '1',
- '--show-builtin', '0'
+ "pyreverse",
+ "-o",
+ fmt,
+ "-p",
+ "brython_drawable_hierarchy",
+ "--output-directory",
+ str(output_dir),
+ "--show-associated",
+ "1",
+ "--show-ancestors",
+ "1",
+ "--show-builtin",
+ "0",
] + drawable_files
try:
subprocess.run(cmd, check=True, capture_output=True, text=True)
- print(f" + Drawable hierarchy diagram generated: {output_dir}/classes_brython_drawable_hierarchy.{fmt}")
+ print(
+ f" + Drawable hierarchy diagram generated: {output_dir}/classes_brython_drawable_hierarchy.{fmt}"
+ )
- if fmt == 'svg':
- self._process_svg_font_and_count(output_dir / f'classes_brython_drawable_hierarchy.{fmt}')
+ if fmt == "svg":
+ self._process_svg_font_and_count(output_dir / f"classes_brython_drawable_hierarchy.{fmt}")
except subprocess.CalledProcessError as e:
print(f" Error generating drawable hierarchy diagram: {e.stderr}")
# Specific drawable type diagrams
drawable_categories: List[Tuple[List[str], str]] = [
- (['point.py', 'segment.py', 'vector.py', 'triangle.py', 'rectangle.py', 'circle.py', 'ellipse.py', 'angle.py'], 'brython_geometric_objects'),
- (['function.py'], 'brython_function_plotting'),
- (['colored_area.py', 'functions_bounded_colored_area.py', 'function_segment_bounded_colored_area.py', 'segments_bounded_colored_area.py'], 'brython_colored_areas'),
- (['rotatable_polygon.py', 'drawable.py', 'position.py'], 'brython_base_drawable_system')
+ (
+ [
+ "point.py",
+ "segment.py",
+ "vector.py",
+ "triangle.py",
+ "rectangle.py",
+ "circle.py",
+ "ellipse.py",
+ "angle.py",
+ ],
+ "brython_geometric_objects",
+ ),
+ (["function.py"], "brython_function_plotting"),
+ (
+ [
+ "colored_area.py",
+ "functions_bounded_colored_area.py",
+ "function_segment_bounded_colored_area.py",
+ "segments_bounded_colored_area.py",
+ ],
+ "brython_colored_areas",
+ ),
+ (["rotatable_polygon.py", "drawable.py", "position.py"], "brython_base_drawable_system"),
]
for category_files, diagram_name in drawable_categories:
category_paths = [
- str(drawables_dir / module)
- for module in category_files
- if (drawables_dir / module).exists()
+ str(drawables_dir / module) for module in category_files if (drawables_dir / module).exists()
]
if category_paths:
for fmt in self.formats:
output_dir = self.get_brython_output_dir(fmt, "drawables")
cmd = [
- 'pyreverse',
- '-o', fmt,
- '-p', diagram_name,
- '--output-directory', str(output_dir),
- '--show-associated', '1',
- '--show-ancestors', '1'
+ "pyreverse",
+ "-o",
+ fmt,
+ "-p",
+ diagram_name,
+ "--output-directory",
+ str(output_dir),
+ "--show-associated",
+ "1",
+ "--show-ancestors",
+ "1",
] + category_paths
try:
subprocess.run(cmd, check=True, capture_output=True, text=True)
print(f" + {diagram_name} diagram generated: {output_dir}/classes_{diagram_name}.{fmt}")
- if fmt == 'svg':
- self._process_svg_font_and_count(output_dir / f'classes_{diagram_name}.{fmt}')
+ if fmt == "svg":
+ self._process_svg_font_and_count(output_dir / f"classes_{diagram_name}.{fmt}")
except subprocess.CalledProcessError as e:
print(f"Error generating {diagram_name} diagram: {e.stderr}")
@@ -353,67 +415,95 @@ def generate_manager_system_diagrams(self) -> None:
# Complete manager orchestration diagram
manager_files: List[str] = [
- str(managers_dir / module)
- for module in self.manager_classes
- if (managers_dir / module).exists()
+ str(managers_dir / module) for module in self.manager_classes if (managers_dir / module).exists()
]
if manager_files:
for fmt in self.formats:
output_dir = self.get_brython_output_dir(fmt, "managers")
cmd = [
- 'pyreverse',
- '-o', fmt,
- '-p', 'brython_manager_orchestration',
- '--output-directory', str(output_dir),
- '--show-associated', '1',
- '--show-ancestors', '1',
- '--show-builtin', '0'
+ "pyreverse",
+ "-o",
+ fmt,
+ "-p",
+ "brython_manager_orchestration",
+ "--output-directory",
+ str(output_dir),
+ "--show-associated",
+ "1",
+ "--show-ancestors",
+ "1",
+ "--show-builtin",
+ "0",
] + manager_files
try:
subprocess.run(cmd, check=True, capture_output=True, text=True)
- print(f" + Manager orchestration diagram generated: {output_dir}/classes_brython_manager_orchestration.{fmt}")
+ print(
+ f" + Manager orchestration diagram generated: {output_dir}/classes_brython_manager_orchestration.{fmt}"
+ )
- if fmt == 'svg':
- self._process_svg_font_and_count(output_dir / f'classes_brython_manager_orchestration.{fmt}')
+ if fmt == "svg":
+ self._process_svg_font_and_count(output_dir / f"classes_brython_manager_orchestration.{fmt}")
except subprocess.CalledProcessError as e:
print(f" Error generating manager orchestration diagram: {e.stderr}")
# Specific manager category diagrams
manager_categories: List[Tuple[List[str], str]] = [
- (['point_manager.py', 'segment_manager.py', 'vector_manager.py', 'polygon_manager.py', 'circle_manager.py', 'ellipse_manager.py', 'angle_manager.py'], 'brython_shape_managers'),
- (['function_manager.py', 'colored_area_manager.py'], 'brython_specialized_managers'),
- (['drawable_manager.py', 'drawable_dependency_manager.py', 'drawable_manager_proxy.py', 'drawables_container.py'], 'brython_core_managers'),
- (['transformations_manager.py', 'undo_redo_manager.py'], 'brython_system_managers')
+ (
+ [
+ "point_manager.py",
+ "segment_manager.py",
+ "vector_manager.py",
+ "polygon_manager.py",
+ "circle_manager.py",
+ "ellipse_manager.py",
+ "angle_manager.py",
+ ],
+ "brython_shape_managers",
+ ),
+ (["function_manager.py", "colored_area_manager.py"], "brython_specialized_managers"),
+ (
+ [
+ "drawable_manager.py",
+ "drawable_dependency_manager.py",
+ "drawable_manager_proxy.py",
+ "drawables_container.py",
+ ],
+ "brython_core_managers",
+ ),
+ (["transformations_manager.py", "undo_redo_manager.py"], "brython_system_managers"),
]
for category_files, diagram_name in manager_categories:
category_paths = [
- str(managers_dir / module)
- for module in category_files
- if (managers_dir / module).exists()
+ str(managers_dir / module) for module in category_files if (managers_dir / module).exists()
]
if category_paths:
for fmt in self.formats:
output_dir = self.get_brython_output_dir(fmt, "managers")
cmd = [
- 'pyreverse',
- '-o', fmt,
- '-p', diagram_name,
- '--output-directory', str(output_dir),
- '--show-associated', '1',
- '--show-ancestors', '1'
+ "pyreverse",
+ "-o",
+ fmt,
+ "-p",
+ diagram_name,
+ "--output-directory",
+ str(output_dir),
+ "--show-associated",
+ "1",
+ "--show-ancestors",
+ "1",
] + category_paths
try:
subprocess.run(cmd, check=True, capture_output=True, text=True)
print(f" + {diagram_name} diagram generated: {output_dir}/classes_{diagram_name}.{fmt}")
- if fmt == 'svg':
- self._process_svg_font_and_count(output_dir / f'classes_{diagram_name}.{fmt}')
+ if fmt == "svg":
+ self._process_svg_font_and_count(output_dir / f"classes_{diagram_name}.{fmt}")
except subprocess.CalledProcessError as e:
print(f"Error generating {diagram_name} diagram: {e.stderr}")
@@ -424,10 +514,10 @@ def generate_integration_diagrams(self) -> None:
# AJAX and AI integration components
integration_files: List[str] = [
- str(self.brython_source_dir / 'ai_interface.py'),
- str(self.brython_source_dir / 'result_processor.py'),
- str(self.brython_source_dir / 'process_function_calls.py'),
- str(self.brython_source_dir / 'workspace_manager.py')
+ str(self.brython_source_dir / "ai_interface.py"),
+ str(self.brython_source_dir / "result_processor.py"),
+ str(self.brython_source_dir / "process_function_calls.py"),
+ str(self.brython_source_dir / "workspace_manager.py"),
]
existing_integration_files: List[str] = [
@@ -438,53 +528,66 @@ def generate_integration_diagrams(self) -> None:
for fmt in self.formats:
output_dir = self.get_brython_output_dir(fmt, "integration")
cmd = [
- 'pyreverse',
- '-o', fmt,
- '-p', 'brython_ajax_communication',
- '--output-directory', str(output_dir),
- '--show-associated', '1',
- '--show-ancestors', '1'
+ "pyreverse",
+ "-o",
+ fmt,
+ "-p",
+ "brython_ajax_communication",
+ "--output-directory",
+ str(output_dir),
+ "--show-associated",
+ "1",
+ "--show-ancestors",
+ "1",
] + existing_integration_files
try:
subprocess.run(cmd, check=True, capture_output=True, text=True)
- print(f" + AJAX communication diagram generated: {output_dir}/classes_brython_ajax_communication.{fmt}")
+ print(
+ f" + AJAX communication diagram generated: {output_dir}/classes_brython_ajax_communication.{fmt}"
+ )
- if fmt == 'svg':
- self._process_svg_font_and_count(output_dir / f'classes_brython_ajax_communication.{fmt}')
+ if fmt == "svg":
+ self._process_svg_font_and_count(output_dir / f"classes_brython_ajax_communication.{fmt}")
except subprocess.CalledProcessError as e:
print(f"Error generating AJAX communication diagram: {e.stderr}")
# Function execution pipeline
execution_files: List[str] = [
- str(self.brython_source_dir / 'process_function_calls.py'),
- str(self.brython_source_dir / 'result_processor.py'),
- str(self.brython_source_dir / 'expression_evaluator.py'),
- str(self.brython_source_dir / 'result_validator.py')
+ str(self.brython_source_dir / "process_function_calls.py"),
+ str(self.brython_source_dir / "result_processor.py"),
+ str(self.brython_source_dir / "expression_evaluator.py"),
+ str(self.brython_source_dir / "result_validator.py"),
]
- existing_execution_files: List[str] = [
- file_path for file_path in execution_files if Path(file_path).exists()
- ]
+ existing_execution_files: List[str] = [file_path for file_path in execution_files if Path(file_path).exists()]
if existing_execution_files:
for fmt in self.formats:
output_dir = self.get_brython_output_dir(fmt, "integration")
cmd = [
- 'pyreverse',
- '-o', fmt,
- '-p', 'brython_function_execution_pipeline',
- '--output-directory', str(output_dir),
- '--show-associated', '1'
+ "pyreverse",
+ "-o",
+ fmt,
+ "-p",
+ "brython_function_execution_pipeline",
+ "--output-directory",
+ str(output_dir),
+ "--show-associated",
+ "1",
] + existing_execution_files
try:
subprocess.run(cmd, check=True, capture_output=True, text=True)
- print(f" + Function execution pipeline diagram generated: {output_dir}/classes_brython_function_execution_pipeline.{fmt}")
+ print(
+ f" + Function execution pipeline diagram generated: {output_dir}/classes_brython_function_execution_pipeline.{fmt}"
+ )
- if fmt == 'svg':
- self._process_svg_font_and_count(output_dir / f'classes_brython_function_execution_pipeline.{fmt}')
+ if fmt == "svg":
+ self._process_svg_font_and_count(
+ output_dir / f"classes_brython_function_execution_pipeline.{fmt}"
+ )
except subprocess.CalledProcessError as e:
print(f"Error generating function execution pipeline diagram: {e.stderr}")
@@ -495,9 +598,9 @@ def generate_utility_system_diagrams(self) -> None:
# Expression and validation utilities
validation_files = [
- str(self.brython_source_dir / 'expression_evaluator.py'),
- str(self.brython_source_dir / 'expression_validator.py'),
- str(self.brython_source_dir / 'result_validator.py')
+ str(self.brython_source_dir / "expression_evaluator.py"),
+ str(self.brython_source_dir / "expression_validator.py"),
+ str(self.brython_source_dir / "result_validator.py"),
]
existing_validation_files = [f for f in validation_files if Path(f).exists()]
@@ -506,27 +609,33 @@ def generate_utility_system_diagrams(self) -> None:
for fmt in self.formats:
output_dir = self.get_brython_output_dir(fmt, "utilities")
cmd = [
- 'pyreverse',
- '-o', fmt,
- '-p', 'brython_expression_system',
- '--output-directory', str(output_dir),
- '--show-associated', '1'
+ "pyreverse",
+ "-o",
+ fmt,
+ "-p",
+ "brython_expression_system",
+ "--output-directory",
+ str(output_dir),
+ "--show-associated",
+ "1",
] + existing_validation_files
try:
subprocess.run(cmd, check=True, capture_output=True, text=True)
- print(f" + Expression system diagram generated: {output_dir}/classes_brython_expression_system.{fmt}")
+ print(
+ f" + Expression system diagram generated: {output_dir}/classes_brython_expression_system.{fmt}"
+ )
- if fmt == 'svg':
- self._process_svg_font_and_count(output_dir / f'classes_brython_expression_system.{fmt}')
+ if fmt == "svg":
+ self._process_svg_font_and_count(output_dir / f"classes_brython_expression_system.{fmt}")
except subprocess.CalledProcessError as e:
print(f"Error generating expression system diagram: {e.stderr}")
# Content processing utilities
content_files = [
- str(self.brython_source_dir / 'markdown_parser.py'),
- str(self.brython_source_dir / 'function_registry.py')
+ str(self.brython_source_dir / "markdown_parser.py"),
+ str(self.brython_source_dir / "function_registry.py"),
]
existing_content_files = [f for f in content_files if Path(f).exists()]
@@ -535,19 +644,25 @@ def generate_utility_system_diagrams(self) -> None:
for fmt in self.formats:
output_dir = self.get_brython_output_dir(fmt, "utilities")
cmd = [
- 'pyreverse',
- '-o', fmt,
- '-p', 'brython_content_processing',
- '--output-directory', str(output_dir),
- '--show-associated', '1'
+ "pyreverse",
+ "-o",
+ fmt,
+ "-p",
+ "brython_content_processing",
+ "--output-directory",
+ str(output_dir),
+ "--show-associated",
+ "1",
] + existing_content_files
try:
subprocess.run(cmd, check=True, capture_output=True, text=True)
- print(f" + Content processing diagram generated: {output_dir}/classes_brython_content_processing.{fmt}")
+ print(
+ f" + Content processing diagram generated: {output_dir}/classes_brython_content_processing.{fmt}"
+ )
- if fmt == 'svg':
- self._process_svg_font_and_count(output_dir / f'classes_brython_content_processing.{fmt}')
+ if fmt == "svg":
+ self._process_svg_font_and_count(output_dir / f"classes_brython_content_processing.{fmt}")
except subprocess.CalledProcessError as e:
print(f"Error generating content processing diagram: {e.stderr}")
@@ -562,19 +677,22 @@ def generate_utility_system_diagrams(self) -> None:
for fmt in self.formats:
output_dir = self.get_brython_output_dir(fmt, "utilities")
cmd = [
- 'pyreverse',
- '-o', fmt,
- '-p', 'brython_utils',
- '--output-directory', str(output_dir),
- str(utils_dir)
+ "pyreverse",
+ "-o",
+ fmt,
+ "-p",
+ "brython_utils",
+ "--output-directory",
+ str(output_dir),
+ str(utils_dir),
]
try:
subprocess.run(cmd, check=True, capture_output=True, text=True)
print(f" + Utils system diagram generated: {output_dir}/classes_brython_utils.{fmt}")
- if fmt == 'svg':
- self._process_svg_font_and_count(output_dir / f'classes_brython_utils.{fmt}')
+ if fmt == "svg":
+ self._process_svg_font_and_count(output_dir / f"classes_brython_utils.{fmt}")
except subprocess.CalledProcessError as e:
print(f"Error generating utils system diagram: {e.stderr}")
@@ -583,19 +701,22 @@ def generate_utility_system_diagrams(self) -> None:
for fmt in self.formats:
output_dir = self.get_brython_output_dir(fmt, "utilities")
cmd = [
- 'pyreverse',
- '-o', fmt,
- '-p', 'brython_name_generator',
- '--output-directory', str(output_dir),
- str(name_gen_dir)
+ "pyreverse",
+ "-o",
+ fmt,
+ "-p",
+ "brython_name_generator",
+ "--output-directory",
+ str(output_dir),
+ str(name_gen_dir),
]
try:
subprocess.run(cmd, check=True, capture_output=True, text=True)
print(f" + Name generator diagram generated: {output_dir}/classes_brython_name_generator.{fmt}")
- if fmt == 'svg':
- self._process_svg_font_and_count(output_dir / f'classes_brython_name_generator.{fmt}')
+ if fmt == "svg":
+ self._process_svg_font_and_count(output_dir / f"classes_brython_name_generator.{fmt}")
except subprocess.CalledProcessError as e:
print(f"Error generating name generator diagram: {e.stderr}")
@@ -612,19 +733,22 @@ def generate_testing_diagrams(self) -> None:
for fmt in self.formats:
output_dir = self.get_brython_output_dir(fmt, "testing")
cmd = [
- 'pyreverse',
- '-o', fmt,
- '-p', 'brython_test_framework',
- '--output-directory', str(output_dir),
- str(tests_dir)
+ "pyreverse",
+ "-o",
+ fmt,
+ "-p",
+ "brython_test_framework",
+ "--output-directory",
+ str(output_dir),
+ str(tests_dir),
]
try:
subprocess.run(cmd, check=True, capture_output=True, text=True)
print(f" + Test framework diagram generated: {output_dir}/classes_brython_test_framework.{fmt}")
- if fmt == 'svg':
- self._process_svg_font_and_count(output_dir / f'classes_brython_test_framework.{fmt}')
+ if fmt == "svg":
+ self._process_svg_font_and_count(output_dir / f"classes_brython_test_framework.{fmt}")
except subprocess.CalledProcessError as e:
print(f"Error generating test framework diagram: {e.stderr}")
@@ -634,19 +758,22 @@ def generate_testing_diagrams(self) -> None:
for fmt in self.formats:
output_dir = self.get_brython_output_dir(fmt, "testing")
cmd = [
- 'pyreverse',
- '-o', fmt,
- '-p', 'brython_test_runner',
- '--output-directory', str(output_dir),
- str(test_runner_file)
+ "pyreverse",
+ "-o",
+ fmt,
+ "-p",
+ "brython_test_runner",
+ "--output-directory",
+ str(output_dir),
+ str(test_runner_file),
]
try:
subprocess.run(cmd, check=True, capture_output=True, text=True)
print(f" + Test runner diagram generated: {output_dir}/classes_brython_test_runner.{fmt}")
- if fmt == 'svg':
- self._process_svg_font_and_count(output_dir / f'classes_brython_test_runner.{fmt}')
+ if fmt == "svg":
+ self._process_svg_font_and_count(output_dir / f"classes_brython_test_runner.{fmt}")
except subprocess.CalledProcessError as e:
print(f"Error generating test runner diagram: {e.stderr}")
@@ -659,33 +786,39 @@ def generate_package_structure_diagrams(self) -> None:
for fmt in self.formats:
output_dir = self.get_brython_output_dir(fmt)
cmd = [
- 'pyreverse',
- '-o', fmt,
- '-p', 'brython_complete_system',
- '--output-directory', str(output_dir),
- '--show-associated', '1',
- '--show-ancestors', '1',
- '-m', 'yes', # Show module names
- str(self.brython_source_dir)
+ "pyreverse",
+ "-o",
+ fmt,
+ "-p",
+ "brython_complete_system",
+ "--output-directory",
+ str(output_dir),
+ "--show-associated",
+ "1",
+ "--show-ancestors",
+ "1",
+ "-m",
+ "yes", # Show module names
+ str(self.brython_source_dir),
]
try:
subprocess.run(cmd, check=True, capture_output=True, text=True)
print(f" + Complete system diagram generated: {output_dir}/packages_brython_complete_system.{fmt}")
- if fmt == 'svg':
- self._process_svg_font_and_count(output_dir / f'packages_brython_complete_system.{fmt}')
+ if fmt == "svg":
+ self._process_svg_font_and_count(output_dir / f"packages_brython_complete_system.{fmt}")
except subprocess.CalledProcessError as e:
print(f"Error generating complete system diagram: {e.stderr}")
# Individual package diagrams
packages: List[Tuple[str, str]] = [
- ('drawables', 'brython_drawables_package'),
- ('managers', 'brython_managers_package'),
- ('utils', 'brython_utils_package'),
- ('name_generator', 'brython_name_generator_package'),
- ('client_tests', 'brython_tests_package')
+ ("drawables", "brython_drawables_package"),
+ ("managers", "brython_managers_package"),
+ ("utils", "brython_utils_package"),
+ ("name_generator", "brython_name_generator_package"),
+ ("client_tests", "brython_tests_package"),
]
for package_name, diagram_name in packages:
@@ -694,20 +827,26 @@ def generate_package_structure_diagrams(self) -> None:
for fmt in self.formats:
output_dir = self.get_brython_output_dir(fmt)
cmd = [
- 'pyreverse',
- '-o', fmt,
- '-p', diagram_name,
- '--output-directory', str(output_dir),
- '-m', 'yes',
- str(package_dir)
+ "pyreverse",
+ "-o",
+ fmt,
+ "-p",
+ diagram_name,
+ "--output-directory",
+ str(output_dir),
+ "-m",
+ "yes",
+ str(package_dir),
]
try:
subprocess.run(cmd, check=True, capture_output=True, text=True)
- print(f" + {package_name} package diagram generated: {output_dir}/packages_{diagram_name}.{fmt}")
+ print(
+ f" + {package_name} package diagram generated: {output_dir}/packages_{diagram_name}.{fmt}"
+ )
- if fmt == 'svg':
- self._process_svg_font_and_count(output_dir / f'packages_{diagram_name}.{fmt}')
+ if fmt == "svg":
+ self._process_svg_font_and_count(output_dir / f"packages_{diagram_name}.{fmt}")
except subprocess.CalledProcessError as e:
print(f"Error generating {package_name} package diagram: {e.stderr}")
@@ -720,24 +859,21 @@ def run(self) -> None:
def main() -> None:
"""Main function with command line argument parsing."""
parser = argparse.ArgumentParser(description="Generate comprehensive Brython client-side diagrams")
- parser.add_argument('--png-dir', default='../generated_png',
- help='Output directory for PNG diagrams (default: ../generated_png)')
- parser.add_argument('--svg-dir', default='../generated_svg',
- help='Output directory for SVG diagrams (default: ../generated_svg)')
- parser.add_argument('--format', default='png,svg',
- help='Output formats (default: png,svg)')
+ parser.add_argument(
+ "--png-dir", default="../generated_png", help="Output directory for PNG diagrams (default: ../generated_png)"
+ )
+ parser.add_argument(
+ "--svg-dir", default="../generated_svg", help="Output directory for SVG diagrams (default: ../generated_svg)"
+ )
+ parser.add_argument("--format", default="png,svg", help="Output formats (default: png,svg)")
args = parser.parse_args()
# Parse formats
- formats: List[str] = [fmt.strip() for fmt in args.format.split(',') if fmt.strip()]
+ formats: List[str] = [fmt.strip() for fmt in args.format.split(",") if fmt.strip()]
# Create and run generator
- generator = BrythonDiagramGenerator(
- png_dir=args.png_dir,
- svg_dir=args.svg_dir,
- formats=formats
- )
+ generator = BrythonDiagramGenerator(png_dir=args.png_dir, svg_dir=args.svg_dir, formats=formats)
generator.run()
diff --git a/diagrams/scripts/generate_diagrams.py b/diagrams/scripts/generate_diagrams.py
index b5c5e997..5f6b7c75 100644
--- a/diagrams/scripts/generate_diagrams.py
+++ b/diagrams/scripts/generate_diagrams.py
@@ -57,13 +57,11 @@ def __init__(
# Setup font configuration for all diagrams
setup_font_environment()
-
-
def get_output_dir(self, fmt: str) -> Path:
"""Get the appropriate output directory for a format."""
- if fmt == 'png':
+ if fmt == "png":
return self.png_dir
- elif fmt == 'svg':
+ elif fmt == "svg":
return self.svg_dir
else:
# For other formats like dot, use svg directory
@@ -72,7 +70,7 @@ def get_output_dir(self, fmt: str) -> Path:
def get_server_output_dir(self, fmt: str) -> Path:
"""Get the server-specific output directory for a format."""
base_dir = self.get_output_dir(fmt)
- server_dir = base_dir / 'server'
+ server_dir = base_dir / "server"
server_dir.mkdir(parents=True, exist_ok=True)
return server_dir
@@ -83,16 +81,12 @@ def _update_fonts_and_count(self, svg_file: Path) -> None:
def check_dependencies(self) -> None:
"""Check if required tools are installed."""
- tools: Dict[str, str] = {
- 'pyreverse': 'pylint',
- 'dot': 'graphviz',
- 'python': 'python'
- }
+ tools: Dict[str, str] = {"pyreverse": "pylint", "dot": "graphviz", "python": "python"}
missing: List[str] = []
for tool, package in tools.items():
# Use 'where' on Windows, 'which' on Unix-like systems
- cmd = 'where' if sys.platform == 'win32' else 'which'
+ cmd = "where" if sys.platform == "win32" else "which"
try:
result = subprocess.run([cmd, tool], capture_output=True, text=True)
if result.returncode != 0:
@@ -113,33 +107,38 @@ def generate_class_diagrams(self) -> None:
# List of all Python files with classes
class_files: List[str] = [
- 'static/app_manager.py',
- 'static/openai_api.py',
- 'static/webdriver_manager.py',
- 'static/workspace_manager.py',
- 'static/ai_model.py',
- 'static/log_manager.py',
- 'static/tool_call_processor.py'
+ "static/app_manager.py",
+ "static/openai_api.py",
+ "static/webdriver_manager.py",
+ "static/workspace_manager.py",
+ "static/ai_model.py",
+ "static/log_manager.py",
+ "static/tool_call_processor.py",
]
for fmt in self.formats:
output_dir = self.get_server_output_dir(fmt)
cmd = [
- 'pyreverse',
- '-o', fmt,
- '-p', 'MatHud_AllClasses',
- '--output-directory', str(output_dir),
- '--show-associated', '1',
- '--show-ancestors', '1'
+ "pyreverse",
+ "-o",
+ fmt,
+ "-p",
+ "MatHud_AllClasses",
+ "--output-directory",
+ str(output_dir),
+ "--show-associated",
+ "1",
+ "--show-ancestors",
+ "1",
] + class_files # Add all class files explicitly
try:
- subprocess.run(cmd, check=True, capture_output=True, text=True, cwd='../..')
+ subprocess.run(cmd, check=True, capture_output=True, text=True, cwd="../..")
print(f" + Main class diagram generated: {output_dir}/classes_MatHud_AllClasses.{fmt}")
# Post-process SVG files to use configured font
- if fmt == 'svg':
- self._update_fonts_and_count(output_dir / f'classes_MatHud_AllClasses.{fmt}')
+ if fmt == "svg":
+ self._update_fonts_and_count(output_dir / f"classes_MatHud_AllClasses.{fmt}")
except subprocess.CalledProcessError as e:
print(f" Error: Error generating main class diagram: {e.stderr}")
@@ -151,25 +150,31 @@ def generate_package_diagrams(self) -> None:
for fmt in self.formats:
output_dir = self.get_server_output_dir(fmt)
cmd = [
- 'pyreverse',
- '-o', fmt,
- '-p', 'MatHud_packages',
- '--output-directory', str(output_dir),
- '--show-associated', '1',
- '--show-ancestors', '1',
- '-m', 'yes', # Show module names
- 'static/', # Main application code
- 'app.py', # Entry point
- 'run_server_tests.py' # Test code
+ "pyreverse",
+ "-o",
+ fmt,
+ "-p",
+ "MatHud_packages",
+ "--output-directory",
+ str(output_dir),
+ "--show-associated",
+ "1",
+ "--show-ancestors",
+ "1",
+ "-m",
+ "yes", # Show module names
+ "static/", # Main application code
+ "app.py", # Entry point
+ "run_server_tests.py", # Test code
]
try:
- subprocess.run(cmd, check=True, capture_output=True, text=True, cwd='../..')
+ subprocess.run(cmd, check=True, capture_output=True, text=True, cwd="../..")
print(f" + Package diagram generated: {output_dir}/packages_MatHud_packages.{fmt}")
# Post-process SVG files to use configured font
- if fmt == 'svg':
- self._update_fonts_and_count(output_dir / f'packages_MatHud_packages.{fmt}')
+ if fmt == "svg":
+ self._update_fonts_and_count(output_dir / f"packages_MatHud_packages.{fmt}")
except subprocess.CalledProcessError as e:
print(f" Error: Error generating package diagram: {e.stderr}")
@@ -179,34 +184,37 @@ def generate_module_specific_diagrams(self) -> None:
print("Generating module-specific diagrams...")
important_modules: List[Tuple[str, str]] = [
- ('static/app_manager.py', 'AppManager'),
- ('static/openai_api.py', 'OpenAI_API'),
- ('static/webdriver_manager.py', 'WebDriver'),
- ('static/workspace_manager.py', 'Workspace')
+ ("static/app_manager.py", "AppManager"),
+ ("static/openai_api.py", "OpenAI_API"),
+ ("static/webdriver_manager.py", "WebDriver"),
+ ("static/workspace_manager.py", "Workspace"),
# Note: routes.py is handled separately in generate_flask_routes_diagram()
]
for module_path, name in important_modules:
# Use absolute path for checking existence
- abs_module_path = Path('../..') / module_path
+ abs_module_path = Path("../..") / module_path
if abs_module_path.exists():
for fmt in self.formats:
output_dir = self.get_server_output_dir(fmt)
cmd = [
- 'pyreverse',
- '-o', fmt,
- '-p', name,
- '--output-directory', str(output_dir),
- module_path # Use relative path for pyreverse
+ "pyreverse",
+ "-o",
+ fmt,
+ "-p",
+ name,
+ "--output-directory",
+ str(output_dir),
+ module_path, # Use relative path for pyreverse
]
try:
- subprocess.run(cmd, check=True, capture_output=True, text=True, cwd='../..')
+ subprocess.run(cmd, check=True, capture_output=True, text=True, cwd="../..")
print(f" + {name} diagram generated: {output_dir}/classes_{name}.{fmt}")
# Post-process SVG files to use configured font
- if fmt == 'svg':
- self._update_fonts_and_count(output_dir / f'classes_{name}.{fmt}')
+ if fmt == "svg":
+ self._update_fonts_and_count(output_dir / f"classes_{name}.{fmt}")
except subprocess.CalledProcessError as e:
print(f" Error: Error generating {name} diagram: {e.stderr}")
@@ -231,7 +239,7 @@ def _generate_custom_routes_diagram(self) -> None:
from pathlib import Path
# Read routes.py content
- routes_file = Path('../../static/routes.py')
+ routes_file = Path("../../static/routes.py")
if not routes_file.exists():
print(" Warning: routes.py not found for custom analysis")
return
@@ -251,25 +259,25 @@ def _generate_custom_routes_diagram(self) -> None:
# Save SVG version
if "svg" in self.formats:
- svg_output_dir = self.get_server_output_dir('svg')
- svg_output_file = svg_output_dir / 'flask_routes_custom.svg'
+ svg_output_dir = self.get_server_output_dir("svg")
+ svg_output_file = svg_output_dir / "flask_routes_custom.svg"
svg_output_file.write_text(svg_content)
print(f" + Custom Flask routes diagram (SVG): {svg_output_file}")
# Convert to PNG if needed
if "png" in self.formats:
- png_output_dir = self.get_server_output_dir('png')
- png_output_file = png_output_dir / 'flask_routes_custom.png'
+ png_output_dir = self.get_server_output_dir("png")
+ png_output_file = png_output_dir / "flask_routes_custom.png"
self._convert_svg_to_png(svg_output_file, png_output_file)
# If only PNG format requested, create SVG first then convert
elif "png" in self.formats:
# Create temporary SVG
- temp_svg = Path('temp_flask_routes.svg')
+ temp_svg = Path("temp_flask_routes.svg")
temp_svg.write_text(svg_content)
- png_output_dir = self.get_server_output_dir('png')
- png_output_file = png_output_dir / 'flask_routes_custom.png'
+ png_output_dir = self.get_server_output_dir("png")
+ png_output_file = png_output_dir / "flask_routes_custom.png"
self._convert_svg_to_png(temp_svg, png_output_file)
# Clean up temporary file
@@ -283,6 +291,7 @@ def _convert_svg_to_png(self, svg_file: Path, png_file: Path) -> None:
"""Convert SVG file to PNG using cairosvg."""
try:
import cairosvg
+
cairosvg.svg2png(url=str(svg_file), write_to=str(png_file))
print(f" + Converted to PNG: {png_file}")
except ImportError:
@@ -296,9 +305,9 @@ def _convert_svg_to_png_fallback(self, svg_file: Path, png_file: Path) -> None:
"""Fallback SVG to PNG conversion using dot command."""
try:
# Use dot (Graphviz) to convert SVG to PNG
- subprocess.run([
- 'dot', '-Tpng', str(svg_file), '-o', str(png_file)
- ], capture_output=True, text=True, check=True)
+ subprocess.run(
+ ["dot", "-Tpng", str(svg_file), "-o", str(png_file)], capture_output=True, text=True, check=True
+ )
print(f" + Converted to PNG (via dot): {png_file}")
except subprocess.CalledProcessError as e:
print(f" Warning: Fallback conversion failed: {e}")
@@ -321,31 +330,31 @@ def _create_routes_svg(
'
+ svg += "\n"
return svg
@@ -374,21 +383,25 @@ def _generate_pyreverse_routes_diagram(self) -> None:
for fmt in self.formats:
output_dir = self.get_server_output_dir(fmt)
cmd = [
- 'pyreverse',
- '-o', fmt,
- '-p', 'FlaskRoutes',
- '--output-directory', str(output_dir),
- '--show-associated', '1',
- 'static/routes.py'
+ "pyreverse",
+ "-o",
+ fmt,
+ "-p",
+ "FlaskRoutes",
+ "--output-directory",
+ str(output_dir),
+ "--show-associated",
+ "1",
+ "static/routes.py",
]
try:
- subprocess.run(cmd, check=True, capture_output=True, text=True, cwd='../..')
+ subprocess.run(cmd, check=True, capture_output=True, text=True, cwd="../..")
print(f" + Pyreverse routes diagram: {output_dir}/classes_FlaskRoutes.{fmt}")
# Post-process SVG files to use configured font
- if fmt == 'svg':
- self._update_fonts_and_count(output_dir / f'classes_FlaskRoutes.{fmt}')
+ if fmt == "svg":
+ self._update_fonts_and_count(output_dir / f"classes_FlaskRoutes.{fmt}")
except subprocess.CalledProcessError:
print(" Warning: Pyreverse routes minimal (functions only, no classes)")
@@ -400,22 +413,26 @@ def _generate_function_call_diagram(self) -> None:
"""Generate function call relationships for routes.py."""
try:
# Use pydeps to analyze function calls in routes.py specifically
- output_dir = self.get_server_output_dir('svg') if "svg" in self.formats else self.get_server_output_dir('png')
+ output_dir = (
+ self.get_server_output_dir("svg") if "svg" in self.formats else self.get_server_output_dir("png")
+ )
cmd = [
- 'pydeps',
- '--show-deps',
- '--max-bacon', '2', # Limited depth for function calls
- '--no-show',
- '-o', str(output_dir / 'routes_functions.svg'),
- 'static/routes.py'
+ "pydeps",
+ "--show-deps",
+ "--max-bacon",
+ "2", # Limited depth for function calls
+ "--no-show",
+ "-o",
+ str(output_dir / "routes_functions.svg"),
+ "static/routes.py",
]
- subprocess.run(cmd, check=True, capture_output=True, text=True, cwd='../..')
+ subprocess.run(cmd, check=True, capture_output=True, text=True, cwd="../..")
print(f" + Routes function calls: {output_dir}/routes_functions.svg")
# Post-process SVG file to use configured font
- self._update_fonts_and_count(output_dir / 'routes_functions.svg')
+ self._update_fonts_and_count(output_dir / "routes_functions.svg")
except subprocess.CalledProcessError:
print(" Warning: Routes function call analysis failed")
@@ -428,14 +445,14 @@ def generate_function_analysis(self) -> None:
# Files that may benefit from function analysis
files_to_analyze: List[Tuple[str, str]] = [
- ('static/functions_definitions.py', 'FunctionDefinitions'),
- ('app.py', 'AppMain'),
- ('run_server_tests.py', 'server_tests')
+ ("static/functions_definitions.py", "FunctionDefinitions"),
+ ("app.py", "AppMain"),
+ ("run_server_tests.py", "server_tests"),
]
for file_path, name in files_to_analyze:
# Check if file exists
- abs_file_path = Path('../..') / file_path
+ abs_file_path = Path("../..") / file_path
if abs_file_path.exists():
self._generate_file_function_analysis(file_path, name)
@@ -443,25 +460,30 @@ def _generate_file_function_analysis(self, file_path: str, name: str) -> None:
"""Generate function analysis for a specific file."""
try:
# Use pydeps for enhanced function call visualization
- output_dir = self.get_server_output_dir('svg') if "svg" in self.formats else self.get_server_output_dir('png')
+ output_dir = (
+ self.get_server_output_dir("svg") if "svg" in self.formats else self.get_server_output_dir("png")
+ )
cmd = [
- 'pydeps',
- '--show-deps',
- '--max-bacon', '2',
- '--cluster',
- '--rankdir', 'TB',
- '--no-show',
- '-o', str(output_dir / f'functions_{name.lower()}.svg'),
- file_path
+ "pydeps",
+ "--show-deps",
+ "--max-bacon",
+ "2",
+ "--cluster",
+ "--rankdir",
+ "TB",
+ "--no-show",
+ "-o",
+ str(output_dir / f"functions_{name.lower()}.svg"),
+ file_path,
]
try:
- subprocess.run(cmd, check=True, capture_output=True, text=True, cwd='../..')
+ subprocess.run(cmd, check=True, capture_output=True, text=True, cwd="../..")
print(f" + Function analysis for {name}: {output_dir}/functions_{name.lower()}.svg")
# Post-process SVG file to use configured font
- self._update_fonts_and_count(output_dir / f'functions_{name.lower()}.svg')
+ self._update_fonts_and_count(output_dir / f"functions_{name.lower()}.svg")
except subprocess.CalledProcessError:
print(f" Warning: Function analysis for {name} failed - file may have no dependencies")
@@ -477,9 +499,7 @@ def generate_architecture_diagram(self) -> None:
# Create architecture diagram generator with same settings
arch_generator = ArchitectureDiagramGenerator(
- png_dir=str(self.png_dir),
- svg_dir=str(self.svg_dir),
- formats=self.formats
+ png_dir=str(self.png_dir), svg_dir=str(self.svg_dir), formats=self.formats
)
# Generate all architecture diagrams (no cleaning in integrated mode)
@@ -498,45 +518,51 @@ def generate_dependency_graph(self) -> None:
print("Generating dependency graph...")
# Dependencies are typically SVG, so use SVG directory if available
- output_dir = self.get_server_output_dir('svg') if "svg" in self.formats else self.get_server_output_dir('png')
+ output_dir = self.get_server_output_dir("svg") if "svg" in self.formats else self.get_server_output_dir("png")
try:
# Generate main project dependency graph
cmd = [
- 'pydeps',
- '--show-deps',
- '--max-bacon', '4', # Increased depth
- '--cluster',
- '--rankdir', 'TB',
- '--no-show', # Prevent automatic opening of the generated file
- '--include-missing', # Show external dependencies
- '-o', str(output_dir / 'dependencies_main.svg'),
- 'app.py' # Start from main entry point
+ "pydeps",
+ "--show-deps",
+ "--max-bacon",
+ "4", # Increased depth
+ "--cluster",
+ "--rankdir",
+ "TB",
+ "--no-show", # Prevent automatic opening of the generated file
+ "--include-missing", # Show external dependencies
+ "-o",
+ str(output_dir / "dependencies_main.svg"),
+ "app.py", # Start from main entry point
]
- subprocess.run(cmd, check=True, capture_output=True, text=True, cwd='../..')
+ subprocess.run(cmd, check=True, capture_output=True, text=True, cwd="../..")
print(f" + Main dependency graph generated: {output_dir}/dependencies_main.svg")
# Post-process SVG file to use configured font
- self._update_fonts_and_count(output_dir / 'dependencies_main.svg')
+ self._update_fonts_and_count(output_dir / "dependencies_main.svg")
# Generate static module dependencies
cmd = [
- 'pydeps',
- '--show-deps',
- '--max-bacon', '3',
- '--cluster',
- '--rankdir', 'LR', # Left-to-right for better readability
- '--no-show',
- '-o', str(output_dir / 'dependencies_static.svg'),
- 'static/'
+ "pydeps",
+ "--show-deps",
+ "--max-bacon",
+ "3",
+ "--cluster",
+ "--rankdir",
+ "LR", # Left-to-right for better readability
+ "--no-show",
+ "-o",
+ str(output_dir / "dependencies_static.svg"),
+ "static/",
]
- subprocess.run(cmd, check=True, capture_output=True, text=True, cwd='../..')
+ subprocess.run(cmd, check=True, capture_output=True, text=True, cwd="../..")
print(f" + Static module dependencies generated: {output_dir}/dependencies_static.svg")
# Post-process SVG file to use configured font
- self._update_fonts_and_count(output_dir / 'dependencies_static.svg')
+ self._update_fonts_and_count(output_dir / "dependencies_static.svg")
except subprocess.CalledProcessError as e:
print(" Error: Error generating dependency graph")
@@ -555,8 +581,8 @@ def generate_call_graph(self) -> None:
from pycallgraph2.output import GraphvizOutput
# Use PNG directory for call graph output
- output_dir = self.get_server_output_dir('png')
- output_file = output_dir / 'call_graph.png'
+ output_dir = self.get_server_output_dir("png")
+ output_file = output_dir / "call_graph.png"
print(" Warning: Call graph generation is experimental")
print(" This will trace app.py execution and may take time...")
@@ -566,7 +592,9 @@ def generate_call_graph(self) -> None:
# For now, just show the command to run manually
print(" Note: To generate call graph manually:")
print(" cd to project root, then run:")
- print(" pycallgraph graphviz --output-file=diagrams/generated_png/server/call_graph.png -- python app.py")
+ print(
+ " pycallgraph graphviz --output-file=diagrams/generated_png/server/call_graph.png -- python app.py"
+ )
except ImportError:
print(" Error: pycallgraph2 not found")
@@ -582,9 +610,7 @@ def generate_brython_diagrams(self) -> None:
# Create Brython diagram generator with same settings
brython_generator = BrythonDiagramGenerator(
- png_dir=str(self.png_dir),
- svg_dir=str(self.svg_dir),
- formats=self.formats
+ png_dir=str(self.png_dir), svg_dir=str(self.svg_dir), formats=self.formats
)
# Generate all Brython diagrams
@@ -632,23 +658,26 @@ def run(self, include_brython: bool = False) -> None:
def main() -> None:
- parser = argparse.ArgumentParser(description='Generate diagrams for MatHud project')
- parser.add_argument('--png-dir', default='../generated_png',
- help='Output directory for PNG diagrams (default: ../generated_png)')
- parser.add_argument('--svg-dir', default='../generated_svg',
- help='Output directory for SVG diagrams (default: ../generated_svg)')
- parser.add_argument('--format', default='png,svg',
- help='Output formats: png,svg,dot (default: png,svg)')
+ parser = argparse.ArgumentParser(description="Generate diagrams for MatHud project")
+ parser.add_argument(
+ "--png-dir", default="../generated_png", help="Output directory for PNG diagrams (default: ../generated_png)"
+ )
+ parser.add_argument(
+ "--svg-dir", default="../generated_svg", help="Output directory for SVG diagrams (default: ../generated_svg)"
+ )
+ parser.add_argument("--format", default="png,svg", help="Output formats: png,svg,dot (default: png,svg)")
# Create mutually exclusive group for Brython options
brython_group = parser.add_mutually_exclusive_group()
- brython_group.add_argument('--include-brython', action='store_true',
- help='Include comprehensive Brython client-side diagrams')
- brython_group.add_argument('--no-brython', action='store_true',
- help='Explicitly disable Brython diagrams (overrides default)')
+ brython_group.add_argument(
+ "--include-brython", action="store_true", help="Include comprehensive Brython client-side diagrams"
+ )
+ brython_group.add_argument(
+ "--no-brython", action="store_true", help="Explicitly disable Brython diagrams (overrides default)"
+ )
args = parser.parse_args()
- formats: List[str] = [f.strip() for f in args.format.split(',') if f.strip()]
+ formats: List[str] = [f.strip() for f in args.format.split(",") if f.strip()]
# Determine if Brython should be included
include_brython = args.include_brython and not args.no_brython
@@ -657,5 +686,5 @@ def main() -> None:
generator.run(include_brython=include_brython)
-if __name__ == '__main__':
+if __name__ == "__main__":
main()
diff --git a/diagrams/scripts/setup_diagram_tools.py b/diagrams/scripts/setup_diagram_tools.py
index cf974e3f..e9d7bc54 100644
--- a/diagrams/scripts/setup_diagram_tools.py
+++ b/diagrams/scripts/setup_diagram_tools.py
@@ -21,11 +21,11 @@ class DiagramToolsSetup:
def __init__(self) -> None:
self.system: str = platform.system().lower()
self.python_packages: List[str] = [
- 'pylint',
- 'graphviz',
- 'diagrams',
- 'pydeps',
- 'pycallgraph2',
+ "pylint",
+ "graphviz",
+ "diagrams",
+ "pydeps",
+ "pycallgraph2",
]
def run_command(self, cmd: str, description: str) -> bool:
@@ -41,24 +41,26 @@ def run_command(self, cmd: str, description: str) -> bool:
def install_graphviz_system(self) -> bool:
"""Install system-level Graphviz based on the operating system."""
- if self.system == 'windows':
+ if self.system == "windows":
print("For Windows, please manually install Graphviz:")
print("1. Download from: https://graphviz.org/download/")
print("2. Install and add to PATH")
print("3. Or use: winget install graphviz")
return True
- elif self.system == 'darwin': # macOS
- return self.run_command('brew install graphviz', 'Installing Graphviz via Homebrew')
- elif self.system == 'linux':
+ elif self.system == "darwin": # macOS
+ return self.run_command("brew install graphviz", "Installing Graphviz via Homebrew")
+ elif self.system == "linux":
# Try different package managers
- if subprocess.run(['which', 'apt'], capture_output=True).returncode == 0:
- return self.run_command('sudo apt update && sudo apt install -y graphviz', 'Installing Graphviz via apt')
- elif subprocess.run(['which', 'yum'], capture_output=True).returncode == 0:
- return self.run_command('sudo yum install -y graphviz', 'Installing Graphviz via yum')
- elif subprocess.run(['which', 'dnf'], capture_output=True).returncode == 0:
- return self.run_command('sudo dnf install -y graphviz', 'Installing Graphviz via dnf')
- elif subprocess.run(['which', 'pacman'], capture_output=True).returncode == 0:
- return self.run_command('sudo pacman -S graphviz', 'Installing Graphviz via pacman')
+ if subprocess.run(["which", "apt"], capture_output=True).returncode == 0:
+ return self.run_command(
+ "sudo apt update && sudo apt install -y graphviz", "Installing Graphviz via apt"
+ )
+ elif subprocess.run(["which", "yum"], capture_output=True).returncode == 0:
+ return self.run_command("sudo yum install -y graphviz", "Installing Graphviz via yum")
+ elif subprocess.run(["which", "dnf"], capture_output=True).returncode == 0:
+ return self.run_command("sudo dnf install -y graphviz", "Installing Graphviz via dnf")
+ elif subprocess.run(["which", "pacman"], capture_output=True).returncode == 0:
+ return self.run_command("sudo pacman -S graphviz", "Installing Graphviz via pacman")
else:
print("Please manually install Graphviz for your Linux distribution")
return False
@@ -69,14 +71,11 @@ def install_python_packages(self) -> None:
print("Installing Python packages...")
# Upgrade pip first
- self.run_command(f'{sys.executable} -m pip install --upgrade pip', 'Upgrading pip')
+ self.run_command(f"{sys.executable} -m pip install --upgrade pip", "Upgrading pip")
# Install packages
for package in self.python_packages:
- success = self.run_command(
- f'{sys.executable} -m pip install {package}',
- f'Installing {package}'
- )
+ success = self.run_command(f"{sys.executable} -m pip install {package}", f"Installing {package}")
if not success:
print(f"Failed to install {package} - you may need to install it manually")
@@ -85,9 +84,9 @@ def verify_installation(self) -> bool:
print("\nVerifying installation...")
tools_to_check = [
- ('pyreverse', 'pyreverse --help'),
- ('dot', 'dot -V'),
- ('python', f'{sys.executable} --version'),
+ ("pyreverse", "pyreverse --help"),
+ ("dot", "dot -V"),
+ ("python", f"{sys.executable} --version"),
]
success_count = 0
@@ -103,8 +102,7 @@ def verify_installation(self) -> bool:
python_success = 0
for package in self.python_packages:
try:
- subprocess.run([sys.executable, '-c', f'import {package}'],
- check=True, capture_output=True)
+ subprocess.run([sys.executable, "-c", f"import {package}"], check=True, capture_output=True)
print(f" Python package {package} is available")
python_success += 1
except subprocess.CalledProcessError:
@@ -164,5 +162,5 @@ def main() -> None:
setup.setup()
-if __name__ == '__main__':
+if __name__ == "__main__":
main()
diff --git a/diagrams/scripts/utils.py b/diagrams/scripts/utils.py
index 4522c115..c6143598 100644
--- a/diagrams/scripts/utils.py
+++ b/diagrams/scripts/utils.py
@@ -20,14 +20,14 @@ def setup_graphviz_path() -> None:
try:
# Check if dot is already available
try:
- subprocess.run(['dot', '-V'], check=True, capture_output=True)
+ subprocess.run(["dot", "-V"], check=True, capture_output=True)
print(" + Graphviz dot command is already available")
return
except (subprocess.CalledProcessError, FileNotFoundError):
pass
# Only setup on Windows
- if sys.platform != 'win32':
+ if sys.platform != "win32":
return
# Common Graphviz installation paths on Windows
@@ -47,14 +47,14 @@ def setup_graphviz_path() -> None:
if graphviz_bin:
# Add to PATH for this session
- current_path = os.environ.get('PATH', '')
+ current_path = os.environ.get("PATH", "")
if graphviz_bin not in current_path:
- os.environ['PATH'] = f"{graphviz_bin};{current_path}"
+ os.environ["PATH"] = f"{graphviz_bin};{current_path}"
print(f" + Added Graphviz to PATH: {graphviz_bin}")
# Verify it works
try:
- subprocess.run(['dot', '-V'], check=True, capture_output=True)
+ subprocess.run(["dot", "-V"], check=True, capture_output=True)
print(" + Graphviz dot command is now available")
except subprocess.CalledProcessError:
print("Warning: Graphviz found but dot command still not working")
@@ -71,11 +71,11 @@ def setup_graphviz_path() -> None:
def setup_font_environment() -> None:
"""Setup environment variables to use configured font in Graphviz."""
# Set Graphviz font preferences
- os.environ['FONTNAME'] = DIAGRAM_FONT
- os.environ['FONTSIZE'] = DIAGRAM_FONT_SIZE_STR
+ os.environ["FONTNAME"] = DIAGRAM_FONT
+ os.environ["FONTSIZE"] = DIAGRAM_FONT_SIZE_STR
# Some systems use different environment variables
- os.environ['GRAPHVIZ_DOT_FONTNAME'] = DIAGRAM_FONT
- os.environ['GRAPHVIZ_DOT_FONTSIZE'] = DIAGRAM_FONT_SIZE_STR
+ os.environ["GRAPHVIZ_DOT_FONTNAME"] = DIAGRAM_FONT
+ os.environ["GRAPHVIZ_DOT_FONTSIZE"] = DIAGRAM_FONT_SIZE_STR
def post_process_svg_fonts(svg_file: Path, diagram_font: str = DIAGRAM_FONT) -> bool:
@@ -84,7 +84,7 @@ def post_process_svg_fonts(svg_file: Path, diagram_font: str = DIAGRAM_FONT) ->
if not svg_file.exists():
return False
- content = svg_file.read_text(encoding='utf-8')
+ content = svg_file.read_text(encoding="utf-8")
# Replace common serif fonts with configured font
font_replacements: Tuple[Tuple[str, str], ...] = (
@@ -104,12 +104,12 @@ def post_process_svg_fonts(svg_file: Path, diagram_font: str = DIAGRAM_FONT) ->
modified = True
# Add default font if no font-family is specified in text elements
- if 'font-family' not in content and ' None:
file_details: DefaultDict[str, List[Dict[str, Any]]] = defaultdict(list)
self.metrics: Dict[str, Any] = {
- 'files': files,
- 'lines': lines,
- 'classes': 0,
- 'methods': 0,
- 'functions': 0,
- 'test_functions': 0,
- 'ai_functions': 0,
- 'drawable_classes': 0,
- 'manager_classes': 0,
- 'imports': 0,
- 'comments': 0,
- 'docstrings': 0,
- 'docstring_lines': 0,
- 'reference_manual_lines': 0,
- 'unique_python_imports': set(),
- 'python_dependencies': 0,
- 'javascript_libraries': 0,
- 'test_files': 0,
- 'file_details': file_details,
+ "files": files,
+ "lines": lines,
+ "classes": 0,
+ "methods": 0,
+ "functions": 0,
+ "test_functions": 0,
+ "ai_functions": 0,
+ "drawable_classes": 0,
+ "manager_classes": 0,
+ "imports": 0,
+ "comments": 0,
+ "docstrings": 0,
+ "docstring_lines": 0,
+ "reference_manual_lines": 0,
+ "unique_python_imports": set(),
+ "python_dependencies": 0,
+ "javascript_libraries": 0,
+ "test_files": 0,
+ "file_details": file_details,
}
# File extensions to analyze
self.extensions: Dict[str, str] = {
- '.py': 'Python',
- '.html': 'HTML',
- '.css': 'CSS',
- '.txt': 'Text',
- '.md': 'Markdown',
+ ".py": "Python",
+ ".html": "HTML",
+ ".css": "CSS",
+ ".txt": "Text",
+ ".md": "Markdown",
# '.js': 'JavaScript',
- '.json': 'JSON'
+ ".json": "JSON",
}
# Directories to exclude
self.exclude_dirs: Set[str] = {
- '__pycache__', '.git', 'venv', '.vscode', '.pytest_cache',
- 'logs', 'workspaces', 'canvas_snapshots', 'generated_svg', 'generated_png'
+ "__pycache__",
+ ".git",
+ "venv",
+ ".vscode",
+ ".pytest_cache",
+ "logs",
+ "workspaces",
+ "canvas_snapshots",
+ "generated_svg",
+ "generated_png",
}
def analyze_project(self) -> None:
@@ -94,13 +102,13 @@ def analyze_file(self, file_path: Path) -> None:
return
file_type = self.extensions[suffix]
- self.metrics['files'][file_type] += 1
+ self.metrics["files"][file_type] += 1
# Read file content
try:
- with open(file_path, 'r', encoding='utf-8') as f:
+ with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
- lines = content.split('\n')
+ lines = content.split("\n")
except UnicodeDecodeError:
# Skip binary files
return
@@ -108,42 +116,42 @@ def analyze_file(self, file_path: Path) -> None:
line_count = len(lines)
# Handle Reference Manual separately (contains duplicated docstrings)
- is_reference_manual = file_path.name == 'Reference Manual.txt'
+ is_reference_manual = file_path.name == "Reference Manual.txt"
if is_reference_manual:
# Track but don't count toward documentation totals
- self.metrics['reference_manual_lines'] = line_count
+ self.metrics["reference_manual_lines"] = line_count
else:
- self.metrics['lines'][file_type] += line_count
+ self.metrics["lines"][file_type] += line_count
# Store file details
relative_path = file_path.relative_to(self.project_root)
file_info: Dict[str, Any] = {
- 'path': str(relative_path),
- 'lines': line_count,
- 'size': file_path.stat().st_size,
- 'is_reference_manual': is_reference_manual
+ "path": str(relative_path),
+ "lines": line_count,
+ "size": file_path.stat().st_size,
+ "is_reference_manual": is_reference_manual,
}
# Analyze Python files in detail
- if suffix == '.py':
+ if suffix == ".py":
py_metrics = self.analyze_python_file(content, file_path)
file_info.update(py_metrics)
# Subtract docstring lines from Python code lines
- docstring_lines = py_metrics.get('docstring_lines', 0)
- self.metrics['lines'][file_type] -= docstring_lines
- self.metrics['docstring_lines'] += docstring_lines
- file_info['lines'] -= docstring_lines
- file_info['docstring_lines'] = docstring_lines
+ docstring_lines = py_metrics.get("docstring_lines", 0)
+ self.metrics["lines"][file_type] -= docstring_lines
+ self.metrics["docstring_lines"] += docstring_lines
+ file_info["lines"] -= docstring_lines
+ file_info["docstring_lines"] = docstring_lines
# Count test files from server_tests and client_tests directories
relative_path = file_path.relative_to(self.project_root)
- if any(part in str(relative_path).lower() for part in ['server_tests', 'client_tests']):
- if file_path.suffix.lower() == '.py': # Only count Python test files
- self.metrics['test_files'] += 1
- file_info['is_test_file'] = True
+ if any(part in str(relative_path).lower() for part in ["server_tests", "client_tests"]):
+ if file_path.suffix.lower() == ".py": # Only count Python test files
+ self.metrics["test_files"] += 1
+ file_info["is_test_file"] = True
- file_details = cast(DefaultDict[str, List[Dict[str, Any]]], self.metrics['file_details'])
+ file_details = cast(DefaultDict[str, List[Dict[str, Any]]], self.metrics["file_details"])
file_details[file_type].append(file_info)
except Exception as e:
@@ -151,19 +159,19 @@ def analyze_file(self, file_path: Path) -> None:
def analyze_python_file(self, content: str, file_path: Path) -> Dict[str, Any]:
"""Detailed analysis of Python files."""
- lines = content.split('\n')
+ lines = content.split("\n")
file_metrics: Dict[str, Any] = {
- 'classes': 0,
- 'methods': 0,
- 'functions': 0,
- 'test_functions': 0,
- 'imports': 0,
- 'comments': 0,
- 'docstrings': 0,
- 'docstring_lines': 0,
- 'is_drawable': False,
- 'is_manager': False,
- 'is_test': False
+ "classes": 0,
+ "methods": 0,
+ "functions": 0,
+ "test_functions": 0,
+ "imports": 0,
+ "comments": 0,
+ "docstrings": 0,
+ "docstring_lines": 0,
+ "is_drawable": False,
+ "is_manager": False,
+ "is_test": False,
}
in_multiline_string = False
@@ -179,18 +187,18 @@ def analyze_python_file(self, content: str, file_path: Path) -> Dict[str, Any]:
# Check if it's a single-line docstring
if stripped.endswith(string_delimiter) and len(stripped) > 6:
# Single-line docstring
- file_metrics['docstrings'] += 1
- self.metrics['docstrings'] += 1
- file_metrics['docstring_lines'] += 1
+ file_metrics["docstrings"] += 1
+ self.metrics["docstrings"] += 1
+ file_metrics["docstring_lines"] += 1
else:
# Multi-line docstring starts
in_multiline_string = True
- file_metrics['docstrings'] += 1
- self.metrics['docstrings'] += 1
- file_metrics['docstring_lines'] += 1
+ file_metrics["docstrings"] += 1
+ self.metrics["docstrings"] += 1
+ file_metrics["docstring_lines"] += 1
else:
# Inside multiline docstring
- file_metrics['docstring_lines'] += 1
+ file_metrics["docstring_lines"] += 1
if string_delimiter and string_delimiter in stripped:
in_multiline_string = False
continue
@@ -199,66 +207,78 @@ def analyze_python_file(self, content: str, file_path: Path) -> Dict[str, Any]:
continue
# Class definitions
- if re.match(r'^class\s+\w+', stripped):
- file_metrics['classes'] += 1
- self.metrics['classes'] += 1
+ if re.match(r"^class\s+\w+", stripped):
+ file_metrics["classes"] += 1
+ self.metrics["classes"] += 1
# Check for specific class types
- if 'drawable' in file_path.name.lower() or any(
- keyword in stripped.lower() for keyword in ['drawable', 'point', 'segment', 'circle', 'triangle', 'rectangle', 'ellipse', 'vector', 'angle', 'function']
+ if "drawable" in file_path.name.lower() or any(
+ keyword in stripped.lower()
+ for keyword in [
+ "drawable",
+ "point",
+ "segment",
+ "circle",
+ "triangle",
+ "rectangle",
+ "ellipse",
+ "vector",
+ "angle",
+ "function",
+ ]
):
- file_metrics['is_drawable'] = True
- if 'drawable' in stripped.lower():
- self.metrics['drawable_classes'] += 1
+ file_metrics["is_drawable"] = True
+ if "drawable" in stripped.lower():
+ self.metrics["drawable_classes"] += 1
- if 'manager' in stripped.lower():
- file_metrics['is_manager'] = True
- self.metrics['manager_classes'] += 1
+ if "manager" in stripped.lower():
+ file_metrics["is_manager"] = True
+ self.metrics["manager_classes"] += 1
# Method definitions (inside classes) - use original line for indentation
- if re.match(r'^\s+def\s+\w+', line):
- file_metrics['methods'] += 1
- self.metrics['methods'] += 1
+ if re.match(r"^\s+def\s+\w+", line):
+ file_metrics["methods"] += 1
+ self.metrics["methods"] += 1
# Test methods
- if 'def test_' in stripped:
- file_metrics['test_functions'] += 1
- self.metrics['test_functions'] += 1
+ if "def test_" in stripped:
+ file_metrics["test_functions"] += 1
+ self.metrics["test_functions"] += 1
# Function definitions (at module level)
- elif re.match(r'^def\s+\w+', stripped):
- file_metrics['functions'] += 1
- self.metrics['functions'] += 1
+ elif re.match(r"^def\s+\w+", stripped):
+ file_metrics["functions"] += 1
+ self.metrics["functions"] += 1
# Test functions
- if 'def test_' in stripped:
- file_metrics['test_functions'] += 1
- self.metrics['test_functions'] += 1
+ if "def test_" in stripped:
+ file_metrics["test_functions"] += 1
+ self.metrics["test_functions"] += 1
# Import statements
- elif stripped.startswith('import ') or stripped.startswith('from '):
- file_metrics['imports'] += 1
- self.metrics['imports'] += 1
+ elif stripped.startswith("import ") or stripped.startswith("from "):
+ file_metrics["imports"] += 1
+ self.metrics["imports"] += 1
# Track unique imports for dependency analysis
import_module = self.extract_import_module(stripped)
if import_module:
- self.metrics['unique_python_imports'].add(import_module)
+ self.metrics["unique_python_imports"].add(import_module)
# Comments
- elif stripped.startswith('#'):
- file_metrics['comments'] += 1
- self.metrics['comments'] += 1
+ elif stripped.startswith("#"):
+ file_metrics["comments"] += 1
+ self.metrics["comments"] += 1
# Check if it's a test file
- if 'test' in file_path.name.lower():
- file_metrics['is_test'] = True
+ if "test" in file_path.name.lower():
+ file_metrics["is_test"] = True
# Special case: analyze functions_definitions.py for AI functions
- if file_path.name == 'functions_definitions.py':
+ if file_path.name == "functions_definitions.py":
ai_functions = self.count_ai_functions(content)
- self.metrics['ai_functions'] = ai_functions
- file_metrics['ai_functions'] = ai_functions
+ self.metrics["ai_functions"] = ai_functions
+ file_metrics["ai_functions"] = ai_functions
return file_metrics
@@ -266,15 +286,15 @@ def extract_import_module(self, import_line: str) -> Optional[str]:
"""Extract the main module name from an import statement."""
try:
# Handle 'import module' and 'from module import ...'
- if import_line.startswith('import '):
- module = import_line[7:].split('.')[0].split(' as ')[0].split(',')[0].strip()
- elif import_line.startswith('from '):
- module = import_line[5:].split('.')[0].split(' import')[0].strip()
+ if import_line.startswith("import "):
+ module = import_line[7:].split(".")[0].split(" as ")[0].split(",")[0].strip()
+ elif import_line.startswith("from "):
+ module = import_line[5:].split(".")[0].split(" import")[0].strip()
else:
return None
# Filter out relative imports and local modules
- if module and not module.startswith('.') and module.isidentifier():
+ if module and not module.startswith(".") and module.isidentifier():
return module
return None
except Exception:
@@ -287,59 +307,66 @@ def analyze_dependencies(self) -> None:
# Main requirements.txt
requirements_files = [
- self.project_root / 'requirements.txt',
- self.project_root / 'diagrams' / 'diagram_requirements.txt'
+ self.project_root / "requirements.txt",
+ self.project_root / "diagrams" / "diagram_requirements.txt",
]
for requirements_file in requirements_files:
if requirements_file.exists():
try:
- with open(requirements_file, 'r', encoding='utf-8') as f:
+ with open(requirements_file, "r", encoding="utf-8") as f:
content = f.read()
- lines = [line.strip() for line in content.split('\n') if line.strip() and not line.strip().startswith('#')]
+ lines = [
+ line.strip()
+ for line in content.split("\n")
+ if line.strip() and not line.strip().startswith("#")
+ ]
# Count dependencies (remove version specs)
for line in lines:
- dep = line.split('==')[0].split('>=')[0].split('<=')[0].split('~=')[0].split('!=')[0].strip()
+ dep = (
+ line.split("==")[0].split(">=")[0].split("<=")[0].split("~=")[0].split("!=")[0].strip()
+ )
if dep:
deps.add(dep)
except Exception:
pass
- self.metrics['python_dependencies'] = len(deps)
+ self.metrics["python_dependencies"] = len(deps)
# Analyze index.html for JavaScript libraries
- index_file = self.project_root / 'templates' / 'index.html'
+ index_file = self.project_root / "templates" / "index.html"
if index_file.exists():
try:
- with open(index_file, 'r', encoding='utf-8') as f:
+ with open(index_file, "r", encoding="utf-8") as f:
content = f.read()
# Count script tags and CDN libraries
js_libs = set()
# Look for script src tags
import re
+
script_pattern = r'