Skip to main content

Understand your Python dependencies. Find cycles, dead modules, and architecture problems.

Project description

DependaMan

Understand your Python project's internal structure. Find cycles, dead modules, hotspots, and architecture problems — visualized as an interactive graph.

No external Python dependencies. Pure stdlib only.


Goal

Given a Python project directory, DependaMan produces an interactive HTML graph showing:

  • Which modules import which (directed dependency graph)
  • Which modules are never imported (dead code candidates)
  • Circular import chains
  • Modules with high fan-in (many dependents) or high fan-out (many dependencies)
  • Module size (lines of code, number of functions/classes)
  • Git churn: how often each module changes

Architecture

Phase 1 — File Discovery

Walk the project directory, collect all .py files, and determine the package root. Distinguish internal modules from external ones (stdlib + third-party are ignored).

Phase 2 — Import Parsing

Use ast to parse each file and extract import and from ... import statements. Resolve relative imports. Filter to internal-only imports.

Phase 3 — Graph Construction

Build a directed graph as an adjacency structure:

  • Node = internal module
  • Edge A → B = "module A imports module B"

Attach metadata to each node: file path, line count, function/class count.

Phase 4 — Analysis

Run these passes on the graph:

  • Dead code: nodes with no incoming edges and not an entry point
  • Circular imports: detect cycles (DFS-based)
  • Hotspots: nodes ranked by fan-in (most imported)
  • Coupling: nodes ranked by fan-out (imports the most)

Phase 5 — Git Integration

Use subprocess + git log to compute per-file:

  • Commit frequency (how often it changes)
  • Lines added/removed over time (churn)
  • Last author that changed the file

Attach this data to graph nodes. Optional: skipped if the project is not a git repo.

Phase 6 — HTML Output

Generate a self-contained .html file (or return HTML as a string for web integration).

The HTML template is a static string embedded in Python. Only the data changes between runs. Python serializes the graph to JSON and injects it into the template — no templating library needed:

import json

data = json.dumps({"nodes": [...], "edges": [...]})
html = TEMPLATE.replace("__GRAPH_DATA__", data)

The template contains a <script> block that reads the injected data and renders the graph using the browser's <canvas> or SVG API. No external JS libraries required.

The graph supports:

  • Hover tooltips: quick summary (import count, churn score)
  • Click modals: full detail panel (git log, list of dependents/dependencies, size metrics)

The output function signature is designed to be framework-agnostic:

def render(graph, analysis) -> str:  # returns HTML string
    ...

This makes it trivial to plug into FastAPI, Flask, or any other framework:

# FastAPI example
@app.get("/graph", response_class=HTMLResponse)
def dependency_graph():
    graph = build_graph(".")
    analysis = analyze(graph)
    return render(graph, analysis)

Package Structure

DependaMan is distributed as a Python package (dependaman). The current layout:

dependaman/
    __init__.py        # public API: dependaman()
    __main__.py        # CLI entry point
    core.py            # orchestration
    discovery.py       # Phase 1 — file discovery
    parser.py          # Phase 2 — import parsing
    graph.py           # Phase 3 — graph construction
    analysis.py        # Phase 4 — analysis passes
    git.py             # Phase 5 — git integration
    renderer.py        # Phase 6 — HTML output
    pool.py            # GIL-aware executor selection

Entry point via pyproject.toml:

[project.scripts]
dependaman = "dependaman.__main__:dependaman"

Usage

CLI:

dependaman              # analyzes current directory, opens browser
dependaman /path/to/project

Python API:

from dependaman import dependaman

html = dependaman(".", in_memory=True)   # returns HTML string
dependaman(".")                          # writes output.html + opens browser

Roadmap

  • Phase 1: File discovery
  • Phase 2: Import parsing (ast)
  • Phase 3: Graph construction
  • Phase 4: Analysis (dead code, cycles, hotspots)
  • Phase 5: Git integration
  • Phase 6: HTML renderer
  • Phase 7: Unused symbol detection (functions, classes, methods never imported)
  • Phase 8: Installable package (uv pip install -e .)
  • Phase 9: CLI entry point with auto project root detection and browser open
  • Phase 10: Performance — parallel git stats (ThreadPoolExecutor), GIL-aware pool for parsing and graph construction
  • Phase 11: Published to PyPI (v1.0.2)

Phase 12 — Visualization overhaul

  • Hierarchical circle packing layout — packages contain sub-packages, no circle/node overlap
  • Zoom-to-fit button + F shortcut (fits all package circles in view)
  • Color encoding — node fill: (pastel) blue→red by commit frequency; package border brightness by nesting depth
  • Test filter toggle — hide/show nodes, edges, and packages containing "test"
  • Node spacing fix + minimum arrow length
  • Click interaction — select node + open modal; click package circle to zoom into it; click empty to deselect
  • Modal navigation — click any import/imported-by entry to jump to that node
  • Middle-click panning; Escape closes modal and deselects in one press
  • Search/filter bar — fuzzy search by module name, jump to and highlight it
  • Performance — rAF-throttled panning/zoom, precomputed draw order
  • Publish v1.0.3

Phase 13 — Call graph: just tell me why (v1.1, in progress)

Semantic analysis at the function/method level. The module graph tells you what depends on what — the call graph tells you why.

Architecture

Two graphs, structurally identical, joined implicitly by canonical prefix:

Graph Nodes Edges Question it answers
Module (existing) pkg.mod A imports B What depends on what? Cycles.
Callable (new) pkg.mod::Func or pkg.mod::Class.method A calls B Who calls what? If I change X, what breaks?

Every callable canonical contains its module canonical as a prefix — split on :: to get the parent module. The two graphs are kept independent so all existing analyzers (fanin_analyzer, fanout_analyzer, circular_imports) apply to the callable graph unchanged.

Definitions (definition_collector + a project-wide upfront pass mirroring module_level_node_generator): walk the top-level AST nodes of each module and collect all FunctionDef, AsyncFunctionDef, and ClassDef names in canonical form:

module.path::function_name
module.path::ClassName
module.path::ClassName.method_name

The project-wide set project_definitions is built once upfront so call resolution is a deterministic lookup against known callables, not a heuristic on the :: separator.

Alias mapper: a single local_name → canonical dict capturing all imports, aliased or not. Non-aliased forms like from . import discovery or import dependaman.parser bind a local name (discovery, dependaman) that the chain walker needs to resolve.

Import statement local_name canonical
import a a a
import a as b b a
import a.b a a
import a.b as ab ab a.b
from a import b b a::b if in project_definitions, else a.b if in project_nodes
from a import b as c c same lookup as above
from .x import y y resolved relatively, then same lookup

Lookup priority: callable (::) before module (.) — matches Python's __init__.py-name-wins-over-submodule rule.

Call collection (parse_calls, caller-scoped): for each top-level FunctionDef / AsyncFunctionDef / ClassDef (and methods inside classes), walk the body and emit edges source_canonical → target_canonical. Module-scope calls (outside any function) attribute to a synthetic source mod::<module> so entry-point invocations like cli() in __main__.py aren't lost. Nested function defs are not separate nodes; calls inside them attribute to the outermost enclosing callable.

References tracked as edges:

  • ast.Call direct: helper() → look up helper in alias_mapper.
  • ast.Call attribute: walk the chain (np.linalg.norm["np", "linalg", "norm"]), resolve the root via alias_mapper, then reconstruct candidates against project_definitions and project_nodes — first match wins. If the root is not in alias_mapper (local variable), fall back to scoped naive name-matching (see below).
  • ClassDef.bases — inheritance: class B(A) → edge mod::B → mod::A.
  • decorator_list — decorators (bare @foo is ast.Name, parameterized @foo(arg) is ast.Call; same target either way — unwrap and resolve).

Returns call_edges: dict[str, set[str]] (source → targets). Externals are filtered at collection time (target not in project_definitions ∪ project_nodes → drop).

Scoped naive name-matching (fallback for unresolved attribute calls): when obj.method() can't be resolved because obj is a local variable, match the method name against canonicals in scope for this module — its imports (alias_mapper values) and its own class definitions. Imprecise but bounded:

in_scope = set(alias_mapper.values()) | local_class_canonicals
matches = {
    f"{c}.{method_name}"
    for c in in_scope
    if f"{c}.{method_name}" in project_definitions
}

This avoids the false-dead case where a method is only ever called on local instances. Trade: a method named process defined in two in-scope classes produces edges to both for a single call site. Acceptable for v1.1.

Callable graph construction: aggregate per-module call_edges into a project-wide dict[str, set[str]], mirroring speed_grapher. Fan-in / fan-out / cycle analyzers apply unchanged.

Dead callable detection: a callable is dead when nothing references it — zero fan-in in the callable graph and its canonical not present in the project import set:

dead = {c for c in project_definitions
        if callable_fanin[c] == 0 and c not in project_imports}

Known v1.1 limitation: obj.method() where obj is a local variable without type information is resolved via scoped naive name-matching only. True precision requires type inference; deferred to v1.2 (see below).


v1.1 checklist

  • Extract all function, method, and class definitions per module (AST)
  • Alias mapper — basic aliased-import handling
  • ast.Name direct call resolution
  • Extend alias mapper to non-aliased imports (import x, from . import x)
  • Project-wide definition collection (upfront pass — callable_level_node_generator)
  • ast.Attribute chain walker with lookup-driven canonical reconstruction
  • Caller-scoped call collection — emit edges (source → set of targets)
  • Synthetic mod::<module> source for module-scope calls
  • Inheritance edges from ClassDef.bases
  • Decorator edges from decorator_list (bare + Call-wrapped)
  • Scoped naive name-matching fallback for unresolved ast.Attribute calls
  • Callable graph construction (mirror of module-graph builder)
  • Reuse fan-in / fan-out / cycles analyzers on the callable graph
  • Call-aware dead-callable detection
  • Expose callable graph in JSON output alongside the module graph
  • Visualize callable edges in the module modal (expandable function-level canvas view deferred to v1.2)
  • Function references passed as Call arguments / keyword arguments tracked as edges — catches pool.submit(fn), sorted(xs, key=fn) patterns; closes the false-positive where pool.submit(_get_git_stats, ...) marked _get_git_stats as dead
  • Single-pass file parsing — speed_grapher reads and AST-parses each module once, fanning the result into project imports, module graph, and callable graph; replaces three separate pool rounds (~30% wall-clock speedup on a 1 354-file project: 3.48s → 2.44s)
  • CHANGELOG + bump to v1.1.0 (publish step remains for the user)

Deferred to v1.2

  • Instance method resolution via type annotations (ast.AnnAssign + function-arg + return annotations → var_types map → precise obj.method() resolution, replacing the naive name-match fallback)
  • THIS IS BEING EVALUATED Expandable function-level view drawn on the canvas — call edges as visual arrows between callables inside their parent module circle. v1.1 ships the callable graph as a modal-only view; the in-canvas rendering is the next visualization step.

Future ideas

  • Documentation site
  • --format json output option for external tooling

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

dependaman-1.1.0.tar.gz (34.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

dependaman-1.1.0-py3-none-any.whl (32.2 kB view details)

Uploaded Python 3

File details

Details for the file dependaman-1.1.0.tar.gz.

File metadata

  • Download URL: dependaman-1.1.0.tar.gz
  • Upload date:
  • Size: 34.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for dependaman-1.1.0.tar.gz
Algorithm Hash digest
SHA256 2723f4087704391c55206a763c54f1c23d3521200e6b6f5aa152cfcb83a91aef
MD5 b272ad611597979974d002853aa62d3e
BLAKE2b-256 c00186e7a1d92a51c0a9a774bc41d6544bc9899e150dbb01b79e1a5c9be061de

See more details on using hashes here.

File details

Details for the file dependaman-1.1.0-py3-none-any.whl.

File metadata

  • Download URL: dependaman-1.1.0-py3-none-any.whl
  • Upload date:
  • Size: 32.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for dependaman-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7f22ff413b5210daabf011bafa504518d7271c54bb00654aee20c1ec0d8e40c6
MD5 dda14cfbb696d02b944f9989c69b79d1
BLAKE2b-256 48a8866fb093f85156a306f82d348ba908fbdb72cd0164a655a46c5cba3b4dc4

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page