Conversation
|
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"),
) |
|
This looks really nice, I'll try to evaluate it this week. |
6094639 to
407165f
Compare
There was a problem hiding this comment.
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.
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) -> strfunction, 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.coreconsuming fromapplication.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)}[ |
There was a problem hiding this comment.
Ruff already flagged this.
Fix: add from wireup.renderer._consumers import get_consumers
| ) -> GraphNode: | ||
| is_factory = not isinstance(factory.factory, type) | ||
| label_prefix = "🏭" if is_factory else "🐍" | ||
| label = f"{label_prefix} {impl.__name__}" |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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]).
| if qualifier is not None: | ||
| label = f"{label} [{qualifier}]" | ||
|
|
||
| module_name = factory.factory.__module__ |
There was a problem hiding this comment.
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" | |||
There was a problem hiding this comment.
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.
|
Also I should note, my app doesn't use Flask or any other web framework. It's plain old python that manages wireup manually. |
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:

Module view off:
