orchestrata_tools.diagram.class_diagram

Reference › orchestrata_tools › diagram › class_diagram

This module generates Mermaid class diagrams from an ASTIndex. MermaidDiagram is a small dataclass holding a title, the diagram body and node and edge counts; its render method prefixes the body with a front-matter title block. It is shared with module_diagram.

ClassDiagramGenerator writes each class as a Mermaid class block listing class variables and methods. Method lines carry visibility (- for names beginning with an underscore, + for public and for dunder names), parameter names with type annotations, and the return type. Inheritance edges are drawn from each base, skipping object, BaseModel, Enum and ABC. generate_for_class draws one class with all its subclasses, generate_for_module draws every class of a module, and generate_full draws up to max_classes (default 50) ranked by method count plus subclass count. An unknown class or an empty module gives an empty diagram. The reverse direction is diagram_to_code.

At a glance

Source src/orchestrata_tools/diagram/class_diagram.py
Lines of code 141
Classes 2
Functions 0
Imports (internal) orchestrata_tools.models.ast_models, orchestrata_tools.parser.indexer
Imported by orchestrata_tools, orchestrata_tools.diagram, orchestrata_tools.diagram.module_diagram
Tests that import it tests/test_diagram_to_code_roundtrip.py

Class diagram

Class diagram orchestrata_tools.diagram.class_diagram

Dependency diagram

Dependency diagram orchestrata_tools.diagram.class_diagram

API

Classes

MermaidDiagram (dataclass)

Decorators: dataclass · Line 14

A generated Mermaid diagram.

Attribute Type Default
title str
body str
node_count int 0
edge_count int 0
Method Signature Summary
render render() -> str

ClassDiagramGenerator

Line 28

Generate Mermaid class diagrams from AST classes.

Method Signature Summary
__init__ __init__(index: ASTIndex)
_escape _escape(name: str) -> str Escape Mermaid special characters.
_visibility _visibility(method: MethodInfo) -> str
_method_signature _method_signature(method: MethodInfo) -> str
_class_node _class_node() -> str
_inheritance_edges _inheritance_edges() -> list[str]
generate_for_class generate_for_class(class_name: str) -> MermaidDiagram Generate a diagram for a single class and its relationships.
generate_for_module generate_for_module(module_path: str) -> MermaidDiagram Generate a diagram for all classes in a module.
generate_full generate_full(max_classes: int = 50) -> MermaidDiagram Generate a full class diagram (limited to top classes).

Source

Permalink: https://gitlab.com/MSamy/orchestrata-ultimate-architecture/-/blob/a4cbc2bdf/src/orchestrata_tools/diagram/class_diagram.py

"""Generate Mermaid class diagrams from AST."""

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from orchestrata_tools.models.ast_models import ClassInfo, MethodInfo
    from orchestrata_tools.parser.indexer import ASTIndex


@dataclass
class MermaidDiagram:
    """A generated Mermaid diagram."""

    title: str
    body: str
    node_count: int = 0
    edge_count: int = 0

    def render(self) -> str:
        lines = ["---", f"title: {self.title}", "---", ""]
        lines.append(self.body)
        return "\n".join(lines)


class ClassDiagramGenerator:
    """Generate Mermaid class diagrams from AST classes."""

    def __init__(self, index: ASTIndex):
        self.index = index

    def _escape(self, name: str) -> str:
        """Escape Mermaid special characters."""
        return name.replace("<", "&lt;").replace(">", "&gt;")

    def _visibility(self, method: MethodInfo) -> str:
        if method.name.startswith("__") and method.name.endswith("__"):
            return "+"  # dunder methods are public API
        if method.name.startswith("_"):
            return "-"
        return "+"

    def _method_signature(self, method: MethodInfo) -> str:
        params = ", ".join(
            p.name + (f": {p.type_annotation}" if p.type_annotation else "")
            for p in method.parameters
        )
        ret = f" -> {method.return_type}" if method.return_type else ""
        return f"{self._visibility(method)}{method.name}({params}){ret}"

    def _class_node(self, cls: ClassInfo) -> str:
        lines = [f"    class {cls.name}{{"]
        for cv in cls.class_vars:
            lines.append(f"        {cv.name}")
        for method in cls.methods:
            lines.append(f"        {self._method_signature(method)}")
        lines.append("    }")
        return "\n".join(lines)

    def _inheritance_edges(self, cls: ClassInfo) -> list[str]:
        edges = []
        for base in cls.bases:
            if base in ("object", "BaseModel", "Enum", "ABC"):
                continue
            edges.append(f"    {base} <|-- {cls.name}")
        return edges

    def generate_for_class(self, class_name: str) -> MermaidDiagram:
        """Generate a diagram for a single class and its relationships."""
        cls = self.index.find_class(class_name)
        if not cls:
            return MermaidDiagram(title=f"Class: {class_name}", body="classDiagram\n")

        lines = ["classDiagram"]
        lines.append(self._class_node(cls))
        lines.extend(self._inheritance_edges(cls))

        # Include subclasses
        for sub in self.index.find_subclasses(class_name):
            lines.append(self._class_node(sub))
            lines.extend(self._inheritance_edges(sub))

        body = "\n".join(lines)
        return MermaidDiagram(
            title=f"Class: {class_name}",
            body=body,
            node_count=1 + len(self.index.find_subclasses(class_name)),
            edge_count=len(lines) - 1,
        )

    def generate_for_module(self, module_path: str) -> MermaidDiagram:
        """Generate a diagram for all classes in a module."""
        classes = self.index.find_classes_in_module(module_path)
        if not classes:
            return MermaidDiagram(title=f"Module: {module_path}", body="classDiagram\n")

        lines = ["classDiagram"]
        edges = []
        for cls in classes:
            lines.append(self._class_node(cls))
            edges.extend(self._inheritance_edges(cls))

        lines.extend(edges)
        body = "\n".join(lines)
        return MermaidDiagram(
            title=f"Module: {module_path}",
            body=body,
            node_count=len(classes),
            edge_count=len(edges),
        )

    def generate_full(self, max_classes: int = 50) -> MermaidDiagram:
        """Generate a full class diagram (limited to top classes)."""
        all_classes = self.index.codebase.all_classes
        # Prioritize classes with most methods and most subclasses
        scored = [
            (
                cls,
                cls.method_count + len(self.index.find_subclasses(cls.name)),
            )
            for cls in all_classes
        ]
        scored.sort(key=lambda x: x[1], reverse=True)
        top_classes = [cls for cls, _ in scored[:max_classes]]

        lines = ["classDiagram"]
        edges = []
        for cls in top_classes:
            lines.append(self._class_node(cls))
            edges.extend(self._inheritance_edges(cls))

        lines.extend(edges)
        body = "\n".join(lines)
        return MermaidDiagram(
            title="Full Class Diagram",
            body=body,
            node_count=len(top_classes),
            edge_count=len(edges),
        )