Uh oh!
There was an error while loading. Please reload this page.
feat(schema)!: structured decorators on callables and classes - #136
Merged
Conversation
`decorators` was a list of raw `ast.unparse` output on callables only. The callee, its arguments and its location were fused into one opaque string, and `PyClass` had no field at all -- so `@dataclass` was dropped outright. It was not recoverable from anything else either: `ClassDef.lineno` points at the `class` keyword, so decorator lines fall outside `PyClass.span` and `module.source[span.bytes]` cannot reach them. Adds `PyDecorator` -- `name`, `qualified_name`, `positional_arguments`, `keyword_arguments`, `expression`, `span` -- and carries it on `PyCallable` and `PyClass`. `expression` keeps the full unparsed source so decorators too complex to decompose lose nothing. `qualified_name` is plumbing, not new analysis: Jedi already resolved these and the result was being discarded (`accessed_symbols` on a decorated callable already carried `functools.lru_cache`). Resolution infers at the LAST identifier of the callee so `@a.b.c` resolves `c` rather than `a`, and is best-effort -- dynamic and conditional decorators stay `None`, and a failure never aborts the symbol table. Neo4j: `:PyDecorator` merges on the resolved `qualified_name` where there is one, so `@lru_cache` and `@lru_cache(maxsize=128)` stop being two unrelated nodes. Per-application facts (arguments, expression) move onto `PY_DECORATED_BY`, since `:PyDecorator` is project-shared and never pruned -- anything application- specific on the node would accumulate across every project in the database. `PY_DECORATED_BY` now accepts `PyClass` as a start label. `PyClassAttribute` and `PyCallableParameter` get the field for cross-language parity but no plumbing: Python has no decorator syntax for either, so there is nothing to populate and the `[]` default is the whole implementation. No cache-guard change is needed, contrary to what the issue assumed: an old-shape cache fails Pydantic validation (`2 validation errors for PyCallable`) and core.py already catches that and rebuilds. The shape change is self-invalidating. BREAKING CHANGE: `decorators` elements change from `str` to an object. Consumers reading `decorators[0]` as a string must read `.name` or `.qualified_name`. `docs/handoff/` is deliberately untouched -- it is a frozen bundle pinned to 1.0.1.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes#128.
Problem
decoratorswasList[str]of rawast.unparseoutput, on callables only(
codeanalyzer/schema/py_schema.py:274, written atcodeanalyzer/syntactic_analysis/symbol_table_builder.py:334). Callee, arguments and locationwere fused into one opaque string, so
@app.routeand@blueprint.routebound to the samefunction were unequal, and
@lru_cacheand@lru_cache(maxsize=128)were unrelated.PyClasshad no field at all, and it was not recoverable elsewhere:ClassDef.linenopoints atthe
classkeyword, so decorator lines fall outsidePyClass.spanandmodule.source[span.bytes]cannot reach them.
@dataclasswas simply gone.Change
PyDecorator—name,qualified_name,positional_arguments,keyword_arguments,expression,span— carried onPyCallableandPyClass.expressionkeeps the full unparsedsource, so decorators too complex to decompose lose nothing.
qualified_nameis plumbing, not new analysis. Jedi already resolved these and the result wasdiscarded —
accessed_symbolson a decorated callable already carriedfunctools.lru_cache.Resolution infers at the last identifier of the callee, so
@a.b.cresolvescrather thana, and is best-effort: dynamic and conditional decorators stayNoneand never abort the build.Neo4j:
:PyDecoratormerges on the resolvedqualified_namewhere there is one, so the twolru_cachespellings collapse to one node. Per-application facts move ontoPY_DECORATED_BY—:PyDecoratoris project-shared and never pruned (neo4j/bolt.py:32), so anythingapplication-specific on the node accumulates across every project in the database.
PY_DECORATED_BYnow acceptsPyClassas a start label.Verification
7 tests in
test/test_decorators_structured.py: class decorator with arguments, callee/argumentseparation, local and library resolution, dotted spelling, span byte-slicing,
[]rather than amissing key, and unresolvable decorators yielding
Nonewithout raising.Three deviations from the issue
Pydantic validation (
2 validation errors for PyCallable) andcore.pyalready catches that andrebuilds. The shape change is self-invalidating.
PyClassAttribute/PyCallableParameter. They get the field forcross-language parity, but Python has no decorator syntax for either, so the
[]default is theentire implementation.
qualified_nameresolved at L1, not L2. This avoids introducing a second sanctionedmonotonicity refinement — nothing goes
null → idacross a level boundary.docs/handoff/is deliberately untouched: its README pins it as a frozen bundle forcodeanalyzer-python==1.0.1.Caveats
decoratorselements changestr→ object; consumers readingdecorators[0]as astring must read
.nameor.qualified_name.python-sdkmodels this field and needs alockstep update. No
schema_versionbump — v2 is a moving target until Java/Python/TypeScriptconverge, so there is no stable number to move.
qualified_name == "builtins.staticmethod"insteadof string-matching the spelling.
:PyDecoratorstrands nodes in an existing database; a load against a pre-existinggraph leaves the old string-keyed nodes behind.
progress and will be posted.