Skip to content

PyCG drops the call edge for decorators with arguments (functools.lru_cache) #131

Description

@rahlk

Problem

At -a 2, one decorator on a function produces a call-graph edge and another on the same
function produces none. Verified on main (6f02581) with a fixture whose module-level
plain_function carries @trace('hot') and @functools.lru_cache(maxsize=128):

app -> trace(tag) ['pycg']
Account -> trace(tag) ['pycg']
deco(fn) -> wraps ['jedi', 'pycg']

functools.wraps gets an edge. functools.lru_cache — same module, same file, same decorator
position — gets none, and never appears in external_symbols either.

This was found while investigating #129. That issue closed because decoration is being made
recoverable through qualified_name rather than through edges (see
the spec, D5),
but the missing edge is a separate defect that survives that decision: it is a hole in the call
graph, not a question about how decoration is modelled.

Scope boundary

Diagnoses and fixes why this call edge is missing. Does not change how decoration is
represented (that is #128), does not add a decoration edge family, and does not re-attribute the
existing app -> trace(tag) edges — those are correct for evaluation order.

Goals

  • Root-cause why lru_cache is dropped where wraps is not — written down, not just patched
  • Determine whether the cause is specific to decorators-with-arguments, or affects ordinary
    calls too; if the latter, the fix belongs at the shared site, not the decorator path
  • Missing edge emitted, with its target homed in external_symbols
  • Regression test with a hand-written expected edge set

Caveats and known risks

  • Fix the cause, not the symptom. If PyCG mishandles a broader class of calls, patching only
    the decorator path leaves every sibling caller still broken.
  • PyCG is archived upstream (Nov 2023) and already carries local patches in
    semantic_analysis/pycg/pycg_analysis.py. A fix likely lands as another local patch, which is
    maintenance debt worth naming rather than absorbing silently.
  • L2 contract. New edges at L2 are additive and safe for L1 ⊆ L2. Changing existing edge
    endpoints is not, and is out of scope.
  • The Jedi backend may already resolve this correctly — worth checking whether the gap is
    PyCG-only before patching PyCG.

Definition of done

  • The root cause is stated in the PR description, not just fixed
  • @functools.lru_cache(maxsize=128) produces a call edge whose target is homed in
    external_symbols
  • The expected edge set for the fixture is written by hand first, then matched exactly
  • analysis.json(-a 1) ⊆ analysis.json(-a 2) still holds

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