Skip to content

Graph renderer#116

Open
maldoinc wants to merge 4 commits intomasterfrom
graph-renderer
Open

Graph renderer#116
maldoinc wants to merge 4 commits intomasterfrom
graph-renderer

Conversation

@maldoinc
Copy link
Copy Markdown
Owner

@maldoinc maldoinc commented Mar 21, 2026

This addresses #59 but in a different take. I found mermaid to be lacking for any sort of graph produced by this so this is a dynamic renderer utility that returns html with an interacive page for users to decide how to render and a fastapi-specific integration which adds a /_wireup page to render the page if opted-in and also tracks usage of Wireup in fastapi endpoints for better visibility.

Screenshots:

Module view on:
image

Module view off:
image

@maldoinc
Copy link
Copy Markdown
Owner Author

maldoinc commented Mar 21, 2026

Tagging a few fastapi users: @iverberk, @severinh, @anon-dev-gh, @brnosouza, @a9a4k, @Pravez


Happty to hear feedback if you get the change to try this out locally

Install

pip install "wireup @ git+https://github.com/maldoinc/wireup.git@graph-renderer"
wireup.integration.fastapi.setup(
    container,
    app,
    class_based_handlers=[DemoClassBasedHandler],
    graph_endpoint=GraphEndpointOptions(enabled=True, base_module="myapp"),
)

@iverberk
Copy link
Copy Markdown
Contributor

This looks really nice, I'll try to evaluate it this week.

Copy link
Copy Markdown

@Stuckya Stuckya left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Outside my high-level code review, I've split my feedback into 2 categories.

1. User experience

1a. Graph readability at scale. The interactive graph works well for small applications. Past roughly 50-100 nodes, the layout degrades regardless of how much search and filtering is available. My fundamental opinion is that humans don't think in "graphs".

1b. Non-web applications produce an empty consumers view. Applications without FastAPI or Flask (background workers, applications with manual wireup insturmentation, CLI tools) generate no consumers entries because routes are the only tracked consumer type. Users currently have no indication that the empty view is expected rather than a mistake.

Not sure if the desire is for this to automatically also fit pub/sub like patterns too?

1c. Node icons. The emoji prefixes (🏭, 🐍, ⚙️) feel informal. Replacing them with a consistent icon set would match the technical presentation of the graph. Recommended: Lucide (ISC license, approximately 200 bytes per icon as inline SVG). FWIW, I think this would also level up README.md.

Image

1d. Optional export formats. Two formats could make the graph more useful. I'm thinking about use in documentation and version-control workflows:

  • PlantUML component diagram export. A to_plantuml(graph_data) -> str function, for teams that embed architecture diagrams in static documentation.
  • Text dependency report. A to_text_report(container) function producing an indented dependency tree. Useful for diffing in code review.

2. Use by agents

I have a fundamental opinion that graphs are great for machines, but not humans.

The graph data is well suited for programmatic consumption by Cursor, Claude Code, Codex-style programming tools. A skill operating on to_graph_data() output answers structural questions about a codebase without reading source files. Providing a significantly faster and more accurate experience than grep-based approaches. Plus, it's self-updating as the wiring changes.

2a. Add a skill.md to graph-renderer: wireup-graph-explorer

The skill generates a one-time snapshot of the graph via to_graph_data() and answers questions by walking the JSON rather than re-reading source. It triggers on repositories with wireup in their dependency tree when the user asks about dependencies, refactor impact, unresolved injectables, or architectural layer questions.

I suppose this could even be checked into git history and updated by CI/CD or the skill itself.

Supported query types

Query shape Implementation
"What depends on X?" Traverse edges with source X; list consumers by module.
"What does X depend on?" Traverse edges with target X; list providers with injection parameter names.
"Who reads config Y?" Filter edges by config node; deduplicate by consumer.
"Impact radius of changing Z" BFS outward from Z, capped at two hops, grouped by layer.
"Any circular dependencies?" Tarjan's strongly-connected-components.
"Does module A depend on module B?" Filter edges by module prefix on source and target.

Architectural quality analysis the skill can produce without additional input

Running a draft skill against a real application revealed concrete, actionable signals:

  • Layer-discipline verification. Counts edges that cross layer boundaries in the wrong direction (for example application.core consuming from application.services).
  • Coupling hotspots. Identifies nodes with high fan-in or fan-out that warrant attention during refactoring.
  • Graceful-degradation inventory. Lists Optional/Union factories and their consumers.
  • Orphan detection. Identifies services registered with the container but not consumed by any other registered service.
  • Longest dependency chain. A depth metric; chains exceeding roughly 10 nodes often indicate over-wrapping.
  • Configuration coupling. Surfaces services reading many individual config leafs when a single config object would serve better.

To make this a reality, we'd need to:

Stabalize the to_graph_data() return shape as a public interface.

Downstream tooling needs to depend on the schema. A documented or versioned schema for GraphGroup / GraphNode / GraphEdge dataclasses would enable this.

def target(foo: Injected[Foo]) -> Foo:
return foo

consumer = {consumer.id: consumer for consumer in get_consumers(container)}[
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ruff already flagged this.

Fix: add from wireup.renderer._consumers import get_consumers

Comment thread wireup/renderer/core.py
) -> GraphNode:
is_factory = not isinstance(factory.factory, type)
label_prefix = "🏭" if is_factory else "🐍"
label = f"{label_prefix} {impl.__name__}"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

impl.__name__ and impl.__qualname__ strip type arguments from parameterized generics. For a Mapping[str, T] factory, both the label and the node id render as plain Mapping.

Two different collections of the same shape (for example Mapping[str, Device] and Mapping[str, Plugin]) would collide under the same node id.

Fix: When impl is a parameterized generic, include the type arguments in both the label and the id.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, a factory that returns something like T | None shows up in the graph as just Union. Every Optional factory in the app ends up with the same label, with no way to tell which type it actually wraps.

Fix: Detect Union/Optional via typing.get_origin()/get_args() and render with the wrapped types visible (for example Optional[T]).

Comment thread wireup/renderer/core.py
if qualifier is not None:
label = f"{label} [{qualifier}]"

module_name = factory.factory.__module__
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For synthetic factories (is_synthetic=True on the InjectableFactory), factory.factory.__module__ resolves to wireup.ioc.registry, the module that builds the synthetic factory, not the module of the wrapped type.

The result is that Mapping[str, Device] appears in the graph's wireup.ioc.registry group next to AsyncContainer, instead of under the developer's own module.

Fix: For synthetic factories, maybe consider impl.__module__ (the wrapped type's module).

@@ -0,0 +1,93 @@
# Interactive Graph

!!! interactive-graph "Interactive Graph"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edge-direction convention should be documented. The renderer emits source = provider, target = consumer, which is the opposite of UML's dependency-arrow convention (where the arrow points from the dependent to the depended-upon). Tools and readers built on the graph JSON will silently make the wrong inference without this.

Fix: Consider reversing the direction to match UML, or adding a note to this page clarifying the current convention.

@Stuckya
Copy link
Copy Markdown

Stuckya commented Apr 19, 2026

Also I should note, my app doesn't use Flask or any other web framework. It's plain old python that manages wireup manually.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants