Skip to content

Releases: Technologicat/pyan

v2.6.0 — Cadastre

30 Apr 13:55

Choose a tag to compare

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 defined Flavor.NAME Node at the bound dotted path. from mymod import x now 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-export unpythonic.env, types.SimpleNamespace, argparse.Namespace), the LHS is upgraded to Flavor.NAMESPACE_OBJECT and its scope is populated with the call's keyword arguments. External config.thingy resolves directly to whatever was passed as thingy=, bypassing the #127 module-level fallback. Recognised at all four binding sites: config = env(...), config: Env = env(...), walrus (config := env(...)), and with env(...) as config:. Staged form config = env(); config.a = baa is also covered (later attribute writes populate the namespace's scope through the existing set_attribute machinery). 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 FQN registers an extra namespace-constructor beyond the built-in registry (e.g. --namespace-constructor mylib.MyNamespace). Repeatable, or comma-separated. Threaded through to the create_callgraph API as namespace_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 small class _NS: pass; store = _NS() shim) used to appear as an isolated node even when it was central to the subsystem — every store.dataset access 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_root may 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__.py nor 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/*.py where sub_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 like tests/ or examples/ — so the choice is left to the user. The --root help text now also mentions the namespace-package case explicitly. (#128)

Internal

  • analyzer.py decomposed. Graph state and post-analysis query API extracted to pyan.callgraph; postprocessing pipeline to pyan.postprocessor; NAMESPACE_OBJECT pattern recognizers to pyan.recognizers. Public API (create_callgraph, create_modulegraph, main) unchanged.
  • Test suite reorganised. test_features.py split 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 now Flavor.SCOPE. The new Flavor.NAMESPACE_OBJECT represents a runtime namespace value (an env instance, a SimpleNamespace instance) — a NAME with a populated scope listing its statically-visible attributes. The Node represents the scope; the Scope class implements one — same concept at two layers.

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