Constraints

Design constraints

Zero external dependencies. The entire tool runs on Python’s standard library (ast, pathlib, json, subprocess, concurrent.futures). Once installed it works in any environment without pulling in a dependency tree.

Framework-agnostic output. The render functions return a string — they know nothing about how it will be served:

from dependaman import dependaman

# FastAPI
@app.get("/graph", response_class=HTMLResponse)
def dependency_graph():
    return dependaman(".", in_memory=True)

Degrade, never abort. A file that cannot be read or parsed is reported as a warning and skipped; the analysis continues and returns an answer for everything else. Malformed files, non-UTF-8 bytes, a missing git and a directory outside a repository are all expected conditions in a real repository.

Deterministic output. The same project analysed twice serializes identically, so the JSON can be diffed between commits or checked in CI. Order is normalized once on the finished payload rather than at every point where a set is materialized.

Parsing in two passes. The first pass collects every definition in the project; the second resolves references against that set, and cannot begin until the set is complete. Each pass reads and AST-parses a module once, so a module is parsed twice per analysis. This one is a constraint of the current design rather than a choice — see Planned.


Current limitations

Known limitations in the current release

Call resolution

obj.method() with local variables. When the root of an attribute chain is a local name, there is nothing to resolve it against. The resolver falls back to matching the trailing attribute against every class imported into or defined in that module, and emits an edge to each one that defines a method with that name.

Reference contexts beyond calls. Only callable-as-argument references are tracked (pool.submit(fn), sorted(xs, key=fn)). Other ways a callable can be referenced without being called are still missed:

  • Assignment: handler = my_func
  • List/dict literals: handlers = [fn_a, fn_b]
  • Return values: return my_func

A callable appearing only in these contexts will be incorrectly flagged as dead.


Libraries are different from applications. A library exists to be called from outside itself. Its public functions and methods are the product, and nothing inside the project calls them. So on a library the dead-code flags fire on exactly the public APIs that the creator shares.

Nothing else in the analysis depends on that flag — import cycles, fan-in and fan-out stay correct, because the graph does not care why a callable has no internal callers.


Naming

from pkg import module lands on the package. When the imported name is itself a module, the import is emitted as pkg::module — a string claiming a callable named module lives in pkg. Nothing matches it, so the edge is lost and the import degrades to the package node. A large project can hold hundreds of imports in this shape.

Every __init__.py reads as an orphan. A package’s __init__.py becomes a module node named pkg.__init__, while imports of the package resolve to pkg. The two never meet, so every __init__.py reports zero fan-in — including DependaMan’s own, whose public API is from dependaman import dependaman.


Planned

Type-aware call resolution

Replace the naive name-matching fallback with annotation-driven type inference:

  • ast.AnnAssign — typed assignments (x: MyClass = ...)
  • Function argument annotations — def foo(x: MyClass) maps x to MyClass
  • Return type annotations — propagate known types through call chains

This gives precise obj.method() resolution for annotated code without a full type checker dependency.

Full reference tracking

Extend edge emission beyond ast.Call to cover assignments, list/dict literals and return statements — closing the false-positive dead-callable cases listed above.

Single-pass parsing

Collapse the two parse passes into one, so each module is read and parsed once. The obstacle is ordering: reference resolution needs the complete definition set, which the first pass is what produces. Either the trees are kept between passes, or resolution is deferred until every file has been visited.