From 67b20b9ff42f205bb2a6482571ecd8cf055f9236 Mon Sep 17 00:00:00 2001
From: Vlad Ciobanu <95963142+vl3c@users.noreply.github.com>
Date: Tue, 17 Feb 2026 23:22:00 +0200
Subject: [PATCH] Add descriptive statistics tool (mean, median, mode, stdev,
quartiles)
Add compute_descriptive_statistics tool registered in functions_definitions.py.
Implementation in static/statistics_utils.py computes mean, median, mode,
standard deviation, variance, min, max, Q1, Q3, IQR, and count.
Wired through the function call pipeline. Server-side pytest tests included.
All 1016 tests pass (0 failures). Lint check passed with soft_fail.
---
.claude/CLAUDE.md | 3 +-
README.md | 26 +-
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 +-
documentation/Example Prompts.txt | 5 +
documentation/Reference Manual.txt | 26 +-
.../metrics/project_metrics_analyzer.py | 353 +-
generate_diagrams_launcher.py | 2 +-
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_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_descriptive_statistics_pure.py | 266 +
.../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 | 37 +-
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 | 1 -
static/client/canvas.py | 218 +-
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 | 50 +-
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 | 168 +-
.../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 | 65 +-
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 | 7 +-
static/client/process_function_calls.py | 10 +-
static/client/rendering/cached_render_plan.py | 69 +-
.../rendering/canvas2d_primitive_adapter.py | 6 +-
static/client/rendering/canvas2d_renderer.py | 42 +-
static/client/rendering/factory.py | 5 +-
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 +-
.../client/utils/area_expression_evaluator.py | 38 +-
.../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 | 29 +-
static/client/utils/graph_layout.py | 139 +-
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 | 1 -
static/client/utils/relation_inspector.py | 121 +-
static/client/utils/statistics/__init__.py | 11 +-
static/client/utils/statistics/descriptive.py | 187 +
.../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 | 5579 ++++++++---------
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 +-
338 files changed, 11089 insertions(+), 9957 deletions(-)
create mode 100644 server_tests/test_descriptive_statistics_pure.py
create mode 100644 static/client/utils/statistics/descriptive.py
diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md
index 827a16b3..65309480 100644
--- a/.claude/CLAUDE.md
+++ b/.claude/CLAUDE.md
@@ -20,7 +20,7 @@ MatHud pairs a canvas with an AI assistant so users can sketch geometric scenes,
2. Geometry primitives: create, edit, and relate points, segments, vectors, triangles, rectangles, circles, ellipses, and angles.
3. Mathematics: evaluate expressions, solve equations, differentiate, integrate, work with complex numbers, and run statistical routines.
4. Graph theory: create graphs, trees, DAGs; run analyses (shortest path, MST, topological sort, BFS/DFS).
-5. Statistics: plot probability distributions (normal, discrete) and bar charts.
+5. Statistics: plot probability distributions (normal, discrete) and bar charts; compute descriptive statistics (mean, median, mode, quartiles, etc.).
6. Workspace operations: save, load, list, delete, import, and export named workspaces.
## Key References
@@ -353,6 +353,7 @@ For features that extend existing managers (e.g., `fit_regression` in `Statistic
## Statistics
- `plot a continuous normal distribution with mean 0 and sigma 1, shade from -1 to 1`
- `plot a bar chart with values [10, 20, 5] and labels ["A", "B", "C"]`
+- `compute descriptive statistics for [10, 20, 30, 40, 50]`
## Parametric Curves
- `draw a parametric circle with x(t) = cos(t) and y(t) = sin(t)`
diff --git a/README.md b/README.md
index 72678727..af7ab713 100644
--- a/README.md
+++ b/README.md
@@ -18,13 +18,14 @@ MatHud pairs an interactive drawing canvas with an AI assistant to help visualiz
3. Plot functions, compare intersections, shade bounded regions, and translate/rotate objects to explore relationships visually.
4. Plot statistics visualizations (probability distributions and bar charts).
5. Fit regression models to data (linear, polynomial, exponential, logarithmic, power, logistic, sinusoidal) and visualize fitted curves with R² statistics.
-6. Create and analyze graph theory graphs (graphs, trees, DAGs).
-7. Save, list, load, and delete named workspaces so projects can be resumed or shared later.
-8. Share the current canvas with the assistant using Vision mode to get feedback grounded in your drawing.
-9. Attach images directly to chat messages for the AI to analyze alongside your prompts.
-10. Use slash commands (`/help`, `/vision`, `/model`, `/image`, etc.) for quick local operations without waiting for an AI response.
-11. Choose from multiple AI providers — OpenAI, Anthropic (Claude), and OpenRouter — with the model dropdown automatically filtered by which API keys you have configured.
-12. Trigger client-side tests from the UI or chat to verify canvas behavior without leaving the app.
+6. Compute descriptive statistics (mean, median, mode, standard deviation, variance, min, max, quartiles, IQR) for any dataset.
+7. Create and analyze graph theory graphs (graphs, trees, DAGs).
+8. Save, list, load, and delete named workspaces so projects can be resumed or shared later.
+9. Share the current canvas with the assistant using Vision mode to get feedback grounded in your drawing.
+10. Attach images directly to chat messages for the AI to analyze alongside your prompts.
+11. Use slash commands (`/help`, `/vision`, `/model`, `/image`, etc.) for quick local operations without waiting for an AI response.
+12. Choose from multiple AI providers — OpenAI, Anthropic (Claude), and OpenRouter — with the model dropdown automatically filtered by which API keys you have configured.
+13. Trigger client-side tests from the UI or chat to verify canvas behavior without leaving the app.
## 3. Architecture Overview
@@ -135,11 +136,12 @@ Developer utilities:
8. `plot a normal distribution with mean 0 and sigma 1, continuous, shade from -1 to 1`
9. `plot a bar chart with values [10,20,5] and labels ["A","B","C"]`
10. `fit a linear regression to x_data=[1,2,3,4,5] and y_data=[2,4,6,8,10], show points and report R²`
- 11. `create an undirected weighted graph named G1 with vertices A,B,C,D and edges A-B (1), B-C (2), A-C (4), C-D (1)`
- 12. `on graph G1, find the shortest path from A to D and highlight the edges`
- 13. `create a DAG named D1 with vertices A,B,C,D and edges A->B, A->C, B->D, C->D; then topologically sort it`
- 14. `save workspace as "demo"` / `load workspace "demo"`
- 15. `run tests`
+ 11. `compute descriptive statistics for [10, 20, 30, 40, 50]`
+ 12. `create an undirected weighted graph named G1 with vertices A,B,C,D and edges A-B (1), B-C (2), A-C (4), C-D (1)`
+ 13. `on graph G1, find the shortest path from A to D and highlight the edges`
+ 14. `create a DAG named D1 with vertices A,B,C,D and edges A->B, A->C, B->D, C->D; then topologically sort it`
+ 15. `save workspace as "demo"` / `load workspace "demo"`
+ 16. `run tests`
### 6.3 Slash Commands
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'