Skip to content

Python: applying a decorator creates no edge — affected <decorator> returns "No affected nodes found" for every function it wraps #2154

Closed

Description

@infusini

Summary

Applying a decorator (@my_decorator) creates no edge between the decorated function and the decorator. The decorated function looks independent of the thing that wraps it, so reverse-impact queries miss it.

Reproduction

pkg/deco.py
pkg/consumer.py
# pkg/deco.py
def my_decorator(func):
    def wrapper(*a, **k):
        return func(*a, **k)
    return wrapper
# pkg/consumer.py
from pkg.deco import my_decorator

@my_decorator
def business_logic(x):
    return x * 2

graphify extract . --code-only --force on 0.9.25 — full edge list:

consumer.py --contains-----> business_logic()
consumer.py --imports_from--> deco.py
consumer.py --imports-------> my_decorator()
deco.py     --contains-----> my_decorator()

There is no business_logic() -> my_decorator() edge. The only link is the file-level import.

$ graphify affected "my_decorator" --depth 2
- consumer.py [imports] pkg/consumer.py:L1

business_logic() — the thing whose behaviour actually changes if the decorator changes — is not reported.

Why it matters

This is the standard "framework entrypoint" pattern. In our fleet, a shared package exposes its whole public API as two decorators:

# md_dataset/process.py
def md_py(func): ...      # -> get_file_manager() -> load_data(...)
def md_upload(func): ...  # -> get_file_manager().save_tables(...)

Six downstream repos consume the package only through @md_py / @md_upload. Because decoration creates no edge, reverse traversal from the decorator finds none of the functions it wraps, and:

$ graphify affected md_py --depth 2
No affected nodes found.

That is a false negative on a "what breaks?" question — cheap, fast, confident, and wrong. It is more dangerous than no answer, because an empty result reads as "nothing depends on this."

Suggested fix

When a decorated_definition / decorator list is present, emit an edge from the decorated symbol to the decorator (e.g. decorated_by, or reuse uses / calls). Python is the clearest case; the same applies to TS/JS decorators.

Possibly related: #1566 (extending indirect_call to remaining dispatch patterns) — decoration is arguably another dispatch pattern.

Environment

graphifyy 0.9.25 · macOS 15 (darwin 25.5.0) · Python 3.9 · --code-only

Activity

  1. Rishet11 commented on Jul 25, 2026

    @Rishet11
    Contributor

    Root cause is in the Python walk, and TS/JS already has the behaviour this issue asks for.

    decorated_definition is handled as a transparent wrapper so a decorated method keeps its class-qualified node id (#1050):

    if t == "decorated_definition":
        for child in node.children:
            walk(child, parent_class_nid=parent_class_nid)
        return

    The decorator children are walked, but no branch consumes them, so nothing is emitted. TS/JS instead goes through _ts_emit_decorator_edges, which emits references edges with context="decorator" resolved through ensure_named_node. Python was simply never wired up.

    Verified on 0.9.26 with your repro. Before:

    Affected nodes for my_decorator()
    - consumer.py [imports] pkg/consumer.py:L1
    

    After porting the TS path to Python:

    Affected nodes for my_decorator()
    - consumer.py [imports] pkg/consumer.py:L1
    - business_logic() [references] pkg/consumer.py:L4
    

    Working on this now, PR shortly. It mirrors the TS helper, so there is no new edge type and no schema/exporter/affected change.

  2. safishamsi commented on Jul 26, 2026

    @safishamsi
    Member

    Fixed in v0.9.27 (#2175, thanks @Rishet11). Python decorators now emit reference edges so affected finds what they touch; builtin/stdlib decorators are excluded. https://github.com/Graphify-Labs/graphify/releases/tag/v0.9.27

  3. added a commit that references this issue on Jul 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions