Skip to content

Releases: Technologicat/pyan

v2.5.0 — Legend

21 Apr 21:38

Choose a tag to compare

New features

  • Wildcard imports now resolve to actual targets. from pkg import * is desugared at analysis time against the target package's __all__ when declared as a literal list/tuple of strings, and against the public-names rule (every module-scope name not starting with _) otherwise. Names reached via wildcard — including those re-exported through __init__.py — now appear as concrete edges in the call graph instead of as spurious *.* residue at the importer's module level. Non-literal __all__ forms (augmented assignment, dynamic construction) fall back to the public-names rule with a debug log. (#126)

Internal

  • Prescan phase added before the two visitor passes. CallGraphVisitor.process now does a lightweight scope + __all__ walk over every input file up front, so cross-module metadata is fully populated before pass 1. This makes wildcard desugaring order-independent — the consumer of a wildcard import no longer has to appear after the exporting package in the filename list.

v2.4.3

20 Apr 09:03

Choose a tag to compare

Bug fixes

  • Names referenced inside a decorator's arguments are now attributed to the decorated function, not only to the enclosing module. Previously, a function decorated with e.g. @app.get("/x", dependencies=[Depends(Guard())]) showed no uses of Depends or Guard — those edges landed on the module instead. The function now also gets a uses edge to each target referenced in its decorator arguments, mirroring the existing treatment of default values. (#125 — thanks @doctorgu)
  • Class decorators are now analyzed — previously visit_ClassDef ignored decorator_list entirely, so @dataclass or @register(kind="x") on a class produced no uses edges anywhere. Class decorators now behave like function decorators: the decorator expression is visited at module scope, and referenced names are also attributed to the decorated class.

v2.4.2 — Benchmark

15 Apr 22:52

Choose a tag to compare

A surveyor's benchmark is a reference mark — a fixed point of known position, cut into rock, that everything else can be measured against. This release is exactly that: no new user-visible features, but the Sphinx extension is now covered by an end-to-end integration test, so what was previously advertised is now verified.

Thanks to @BlocksecPHD for contributing the test (#124, closes #114).

Internal

  • Build system migrated from hatchling+uv to PDM (pdm-backend). No user-visible changes; pip install pyan3 works as before.
  • Sphinx extension: end-to-end integration test covering sphinx-build, the .. callgraph:: directive, pan/zoom HTML wiring, and directive option propagation. Uses an in-test dot stub, so CI needs no system Graphviz. Closes #114. (#124 — thanks @BlocksecPHD)

See CHANGELOG.md for the full history.

v2.4.1 — Terra Generica

11 Apr 08:03

Choose a tag to compare

Bug fixes

  • Crash on PEP 695 generic syntaxclass C[T], def f[T], and
    type A[T] = ... (Python 3.12+) caused a KeyError in
    visit_FunctionDef because CPython's symtable inserts an implicit
    type-parameter scope that doubled the namespace path. The fix
    preserves the type-parameter scope as a proper lexical closure
    (essentially a let-over-lambda), matching Python's actual scoping
    semantics. Handles all PEP 695 forms: generic classes, generic
    functions, generic methods, nested generics, multiple/bounded type
    parameters, and type parameter shadowing in class bodies.
    (#123 — thanks @uselessscat)

Internal

  • Visitor scope management via context managersvisit_Module,
    visit_ClassDef, visit_FunctionDef, and visit_TypeAlias now use
    contextlib.contextmanager-based helpers (_module_scope,
    _class_scope, _function_scope, _type_params_scope) instead of
    manual push/pop pairs, guaranteeing cleanup on exception.

2.4.0 — Here be dragons

03 Apr 10:10

Choose a tag to compare

New features

  • Node tooltips in DOT output — all defined nodes now carry a tooltip
    attribute containing the fully qualified name plus annotation details
    (filename, line number, flavor). This is always emitted, independent of
    --annotated. Graph viewers that support the tooltip attribute (such
    as raven-xdot-viewer) can
    display this information on hover.

Internal

  • Node.get_annotation_parts() — new method that serves as the single
    source of truth for annotation content, used by both the label methods
    and the tooltip builder.

Full changelog: https://github.com/Technologicat/pyan/blob/master/CHANGELOG.md

2.3.1 — Hotfix

02 Apr 10:37

Choose a tag to compare

Bug fixes

  • Relative imports in __init__.py resolve to wrong parent package
    from . import alpha in a nested package init (e.g. pkg/sub/__init__.py)
    resolved to the grandparent (pkg.alpha) instead of the package itself
    (pkg.sub.alpha). Affected all __init__ modules whose fully qualified
    name contains at least one dot. Fixed in both file-based and sans-IO modes.
    (#121 — thanks @tristanlatr)

Notes

  • from_sources(): __init__ naming convention — to get correct relative
    import resolution for package __init__ modules, pass "pkg.sub.__init__"
    as the module name (not just "pkg.sub"). The previous behaviour silently
    produced wrong or missing edges.
  • resolve_import() — new shared utility in pyan.anutils for resolving
    relative imports. Replaces the inline logic in both the call-graph analyzer
    and module-graph analyzer.
  • __all__ added to anutils, main, and modvis modules.

Full changelog: https://github.com/Technologicat/pyan/blob/master/CHANGELOG.md

2.3.0 — Carta marina edition

01 Apr 23:18

Choose a tag to compare

New features

  • File exclusion (-x / --exclude) — exclude files matching glob
    patterns before analysis. Patterns without a path separator match
    against the basename (e.g. test_*.py); patterns with a separator
    match against the full path (e.g. */tests/*). Available in both
    call-graph and module-level modes, via CLI, Python API (exclude
    parameter in create_callgraph / create_modulegraph), and the
    Sphinx directive (:exclude: option, comma-separated).
    (#119 — thanks @lightswitch05)

  • Class-level constant attribute access — accessing class constants
    (e.g. Color.RED on an Enum, or Config.DEBUG) now creates a uses
    edge to the class itself, so these classes no longer appear
    disconnected in the graph. (#113)

  • Sans-IO analysis via from_sourcesCallGraphVisitor.from_sources()
    and create_callgraph(sources=...) accept (source_text, module_name)
    pairs (or (ast.Module, module_name)) for analysis without any file
    I/O. Useful for embedding pyan in tools that already have source text
    in memory, or for analyzing ASTs from macro expanders.
    (#101 — thanks @tristanlatr)

  • Per-anonymous-scope isolation — multiple lambdas or comprehensions
    in the same function no longer share a single scope. Each instance
    now gets a numbered scope key (e.g. listcomp.0, listcomp.1),
    preventing the second instance's bindings from overwriting the first's.
    Works on both pre-3.12 (symtable-based) and 3.12+ (PEP 709 synthetic)
    scope paths. (#110)

  • Module-graph multi-project coloring — modules are now colored by
    top-level directory relative to the project root, matching the
    call-graph analyzer's approach. Previously, modules from different
    projects could share colors if their immediate parent directories
    had the same name. (#111)

  • Class-prefixed method labels when ungrouped — when grouping is off,
    method labels are now prefixed with the class name (e.g. MyClass.run
    instead of just run), making it possible to tell which class a method
    belongs to without annotations. (#112)


Install: pip install pyan3==2.3.0

Full changelog: CHANGELOG.md

2.2.2 — Hotfix

22 Mar 23:44

Choose a tag to compare

Bug fixes

  • Namespace packages lose cross-module edges — when a regular package
    (with __init__.py) called into a namespace package (without
    __init__.py), the edge was silently lost. The analyzer now auto-infers
    the project root from the input filenames and uses it consistently for
    all module name resolution. (#117 — thanks @doctorgu)

See CHANGELOG.md for full details.

2.2.1 — Hotfix

22 Mar 00:45

Choose a tag to compare

Documentation

  • Recommended options in README — added a section with recommended CLI options for common use cases: clean uses-only graphs, fdp layout for larger projects, and --depth 1 for high-level overviews. Re-rendered the example graph with --no-defines --concentrate.
  • --concentrate precision caveat — noted that GraphViz's edge concentration can produce small gaps at split/merge points.

Bug fixes

  • Missing uses edges for names in default argument values — the #61 fix (2.2.0) correctly moved default-value visiting to the enclosing scope, but lost uses edges from the function to names referenced in its defaults. def f(cb=wrapper(func)) now correctly shows f → wrapper and f → func. (#116)
  • --depth dropped almost all uses edgesfilter_by_depth counted raw dots in the fully qualified name, so modules with dotted names (e.g. pkg.sub.mod) inflated the depth of every node inside them. Ancestor lookup then created phantom nodes with the wrong namespace/name split, which were silently discarded. Depth is now computed relative to each node's containing module, giving consistent behaviour regardless of package depth. The depth scale is: 0 = modules, 1 = classes/top-level functions, 2 = methods, etc.

Full changelog: https://github.com/Technologicat/pyan/blob/master/CHANGELOG.md

Version 2.2.0 — Terra cognita

16 Mar 11:02

Choose a tag to compare

2.2.0 (16 March 2026) — Terra cognita edition

Bug fixes

  • Deterministic edge ordering — all output writers now sort edges by (source, target), making output reproducible across runs. (#77, PR #78 — thanks @aurelg)
  • DOT identifier quoting — node IDs and subgraph names in DOT output are now double-quoted, so directory names containing dashes no longer produce invalid DOT files. (#71)
  • BrokenPipeError on piped outputpyan3 now resets SIGPIPE to the default handler. (#75)
  • Crash on lambda or comprehension as default argument — default value expressions are now visited in the enclosing scope. (#61)
  • Spurious cross-module edges from wildcard expansionexpand_unknowns() now checks import relationships before expanding wildcards. (#88)
  • get_module_name mangled paths with .py in directory names (PR #97 — thanks @CannedFish)
  • resolve_imports KeyError (PR #95, PR #97 — thanks @anetczuk, @CannedFish)

New features

  • --dot-ranksep — control rank separation in GraphViz output. (PR #74 — thanks @maciejczyzewski)
  • --graphviz-layout — select layout algorithm (dot, fdp, neato, sfdp, twopi, circo). (PR #74 — thanks @maciejczyzewski)
  • --direction — filter traversal: down, up, or both. (PR #95 — thanks @anetczuk)
  • __init__ modules omitted by default in modvis — use --init to include them. (#20)
  • Directory input — passing a directory auto-globs **/*.py. (#66)
  • --concentrate — merge bidirectional edges into double-headed arrows. (#21)
  • --paths-from / --paths-to — list call paths between two functions. (#12)
  • --depth — collapse the call graph to a maximum nesting level. (#80)

Housekeeping

  • CI linter migrated from flake8 to ruff.
  • Converted to f-strings throughout.
  • Resolved all ruff lint warnings.