Releases: Technologicat/pyan
v2.5.0 — Legend
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.processnow 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
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 ofDependsorGuard— 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_ClassDefignoreddecorator_listentirely, so@dataclassor@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
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 pyan3works 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-testdotstub, so CI needs no system Graphviz. Closes #114. (#124 — thanks @BlocksecPHD)
See CHANGELOG.md for the full history.
v2.4.1 — Terra Generica
Bug fixes
- Crash on PEP 695 generic syntax —
class C[T],def f[T], and
type A[T] = ...(Python 3.12+) caused aKeyErrorin
visit_FunctionDefbecause CPython'ssymtableinserts 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 managers —
visit_Module,
visit_ClassDef,visit_FunctionDef, andvisit_TypeAliasnow 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
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 thetooltipattribute (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
Bug fixes
- Relative imports in
__init__.pyresolve to wrong parent package —
from . import alphain 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 inpyan.anutilsfor resolving
relative imports. Replaces the inline logic in both the call-graph analyzer
and module-graph analyzer.__all__added toanutils,main, andmodvismodules.
Full changelog: https://github.com/Technologicat/pyan/blob/master/CHANGELOG.md
2.3.0 — Carta marina edition
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 increate_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.REDon an Enum, orConfig.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_sources—CallGraphVisitor.from_sources()
andcreate_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 justrun), 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
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
Documentation
- Recommended options in README — added a section with recommended CLI options for common use cases: clean uses-only graphs,
fdplayout for larger projects, and--depth 1for high-level overviews. Re-rendered the example graph with--no-defines --concentrate. --concentrateprecision 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 showsf → wrapperandf → func. (#116) --depthdropped almost all uses edges —filter_by_depthcounted 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
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 output —
pyan3now 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 expansion —
expand_unknowns()now checks import relationships before expanding wildcards. (#88) get_module_namemangled paths with.pyin directory names (PR #97 — thanks @CannedFish)resolve_importsKeyError (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, orboth. (PR #95 — thanks @anetczuk)__init__modules omitted by default in modvis — use--initto 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.