|
16 | 16 | PyCallableParameter, |
17 | 17 | PyCallArgument, |
18 | 18 | PyCallsite, |
| 19 | + PyDecorator, |
19 | 20 | PyClass, |
20 | 21 | PyClassAttribute, |
21 | 22 | PyComment, |
@@ -295,6 +296,7 @@ def _add_class(self, node: AST, script: Script, source: str, prefix: str = "") - |
295 | 296 | .name(class_name) |
296 | 297 | .signature(signature) |
297 | 298 | .span(span) |
| 299 | + .decorators(self._decorators(child, script, source)) |
298 | 300 | .start_line(start_line) |
299 | 301 | .end_line(end_line) |
300 | 302 | .comments(self._pycomments(child, code)) |
@@ -331,7 +333,7 @@ def _callables(self, node: AST, script: Script, source: str, prefix: str = "") - |
331 | 333 | getattr(child, "end_lineno", child.lineno), |
332 | 334 | getattr(child, "end_col_offset", child.col_offset)), |
333 | 335 | ) |
334 | | - decorators = [ast.unparse(d) for d in child.decorator_list] |
| 336 | + decorators = self._decorators(child, script, source) |
335 | 337 |
|
336 | 338 | if prefix: |
337 | 339 | # We're in a nested context - build signature with prefix |
@@ -590,6 +592,68 @@ def build_param( |
590 | 592 |
|
591 | 593 | return params |
592 | 594 |
|
| 595 | + def _decorators( |
| 596 | + self, node: ast.AST, script: Optional[Script], source: str = "" |
| 597 | + ) -> List[PyDecorator]: |
| 598 | + """Structure each entry of ``node.decorator_list`` (#128). |
| 599 | +
|
| 600 | + ``name`` is the spelling as written and ``qualified_name`` is Jedi's |
| 601 | + resolution of it, inferred at the last identifier of the callee so that |
| 602 | + ``@a.b.c`` resolves ``c`` rather than ``a``. Resolution is best-effort: |
| 603 | + dynamic, conditional and re-exported decorators stay unresolved, and a |
| 604 | + failure here must never abort the symbol table. |
| 605 | + """ |
| 606 | + out: List[PyDecorator] = [] |
| 607 | + for dec in getattr(node, "decorator_list", []) or []: |
| 608 | + callee = dec.func if isinstance(dec, ast.Call) else dec |
| 609 | + positional: List[str] = [] |
| 610 | + keyword: Dict[str, str] = {} |
| 611 | + if isinstance(dec, ast.Call): |
| 612 | + positional = [ast.unparse(a) for a in dec.args] |
| 613 | + for kw in dec.keywords: |
| 614 | + # ``**kwargs`` has no arg name; keep it addressable rather |
| 615 | + # than dropping it. |
| 616 | + key = kw.arg if kw.arg is not None else f"**{ast.unparse(kw.value)}" |
| 617 | + keyword[key] = ast.unparse(kw.value) |
| 618 | + span = Span( |
| 619 | + start=(dec.lineno, dec.col_offset), |
| 620 | + end=(getattr(dec, "end_lineno", dec.lineno), |
| 621 | + getattr(dec, "end_col_offset", dec.col_offset)), |
| 622 | + bytes=byte_offsets(source, dec.lineno, dec.col_offset, |
| 623 | + getattr(dec, "end_lineno", dec.lineno), |
| 624 | + getattr(dec, "end_col_offset", dec.col_offset)), |
| 625 | + ) if source else None |
| 626 | + out.append( |
| 627 | + PyDecorator.builder() |
| 628 | + .name(ast.unparse(callee)) |
| 629 | + .qualified_name(self._decorator_qualified_name(callee, script)) |
| 630 | + .positional_arguments(positional) |
| 631 | + .keyword_arguments(keyword) |
| 632 | + .expression(ast.unparse(dec)) |
| 633 | + .span(span) |
| 634 | + .build() |
| 635 | + ) |
| 636 | + return out |
| 637 | + |
| 638 | + @staticmethod |
| 639 | + def _decorator_qualified_name( |
| 640 | + callee: ast.AST, script: Optional[Script] |
| 641 | + ) -> Optional[str]: |
| 642 | + """Jedi's full name for a decorator's callee, or ``None``.""" |
| 643 | + if script is None: |
| 644 | + return None |
| 645 | + line = getattr(callee, "end_lineno", getattr(callee, "lineno", None)) |
| 646 | + col = getattr(callee, "end_col_offset", None) |
| 647 | + if line is None or col is None: |
| 648 | + return None |
| 649 | + try: |
| 650 | + d = SymbolTableBuilder._first_definition( |
| 651 | + script.infer(line=line, column=max(col - 1, 0)) |
| 652 | + ) |
| 653 | + except Exception: |
| 654 | + return None |
| 655 | + return getattr(d, "full_name", None) if d is not None else None |
| 656 | + |
593 | 657 | def _accessed_symbols( |
594 | 658 | self, fn_node: ast.FunctionDef, script: Script |
595 | 659 | ) -> List[PySymbol]: |
|
0 commit comments