Releases: Technologicat/pyan
v2.6.0 — Cadastre
New features
- Module-level name bindings now produce graph Nodes. Every module-level assignment (e.g.
CONSTANT = 42,LOGGER = logging.getLogger(__name__),store = _NS()) creates a definedFlavor.NAMENode at the bound dotted path.from mymod import xnow resolves to the actual binding instead of contracting to a wildcard, and the #127 attribute-fallback lands on the specific binding rather than climbing all the way to the enclosing module. Function-locals are unchanged — they stay as scope-only bindings to keep the graph readable. Edgeless NAME Nodes (module constants nobody imports) are suppressed from the rendered output by default; they remain in the analyzer's graph for cross-module resolution. - Namespace-style modules now resolve attribute access to the kwarg's target. When the rhs of a binding is
Call(func=...)whose resolved import origin is in the built-in registry (unpythonic.env.env, top-level re-exportunpythonic.env,types.SimpleNamespace,argparse.Namespace), the LHS is upgraded toFlavor.NAMESPACE_OBJECTand its scope is populated with the call's keyword arguments. Externalconfig.thingyresolves directly to whatever was passed asthingy=, bypassing the #127 module-level fallback. Recognised at all four binding sites:config = env(...),config: Env = env(...), walrus(config := env(...)), andwith env(...) as config:. Staged formconfig = env(); config.a = baais also covered (later attribute writes populate the namespace's scope through the existingset_attributemachinery).setattr(config, name, value)writes are recognised when name is a string literal, a name bound to a string literal in any scope reachable from the call site, or an imported name resolving to a string literal in another module. (#129) --namespace-constructor FQNregisters an extra namespace-constructor beyond the built-in registry (e.g.--namespace-constructor mylib.MyNamespace). Repeatable, or comma-separated. Threaded through to thecreate_callgraphAPI asnamespace_constructors=[...]. Supplying the option emits a one-shot stderr nudge inviting the user to file an issue if their constructor is reasonably common, so the built-in registry can grow from observed real-world use. (#129)
Bug fixes
- Cross-module attribute reads on namespace-style modules now produce uses edges. A module whose public surface is a runtime-built object (
SimpleNamespace,unpythonic.env.env, a smallclass _NS: pass; store = _NS()shim) used to appear as an isolated node even when it was central to the subsystem — everystore.datasetaccess landed on a synthetic ATTRIBUTE node that visgraph dropped as undefined. The analyzer now also emits an edge to the immediate defined parent of the obj (typically the exporting module) when the attribute itself can't be resolved. Symmetric for attribute writes (store.flag = value). One-level only — does not climb through unanalyzed packages, and within-scope self-references are suppressed (a method reading an undefined attribute on its own class, or a function reading module-level state in its own module, is just normal scoping). Generalizes the existing class-fallback (Enum members, class constants) introduced in 2.4.0. (#127) - Advisory when
infer_rootmay have misidentified the package root. Two ambiguous situations now emit a warning suggesting--root: (1) inference walked up at least one package level and stopped at a directory that has neither__init__.pynor a project-root marker (pyproject.toml,setup.py,setup.cfg) — consistent with a top-level PEP 420 namespace package; (2) inference didn't walk up at all but the input directory's parent has__init__.py— consistent with the user feeding pyan the contents of a namespace subpackage (e.g.pyan3 pkg/sub_ns/*.pywheresub_ns/has no__init__.py), which would otherwise silently produce bare module names and broken relative imports. Auto-walking further is unsafe — the same filesystem shapes also occur for workspace directories liketests/orexamples/— so the choice is left to the user. The--roothelp text now also mentions the namespace-package case explicitly. (#128)
Internal
analyzer.pydecomposed. Graph state and post-analysis query API extracted topyan.callgraph; postprocessing pipeline topyan.postprocessor; NAMESPACE_OBJECT pattern recognizers topyan.recognizers. Public API (create_callgraph,create_modulegraph,main) unchanged.- Test suite reorganised.
test_features.pysplit into per-concern files (test_classes.py,test_functions.py,test_iteration.py,test_async_context.py,test_match.py,test_assignments.py,test_imports.py,test_type_params.py,test_misc.py). - Flavor rename:
Flavor.NAMESPACE(synthetic structural marker for module/class/function scope bookkeeping) is nowFlavor.SCOPE. The newFlavor.NAMESPACE_OBJECTrepresents a runtime namespace value (anenvinstance, aSimpleNamespaceinstance) — aNAMEwith a populated scope listing its statically-visible attributes. The Node represents the scope; theScopeclass implements one — same concept at two layers.
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