|
21 | 21 | ATTRS_DECORATORS, |
22 | 22 | ATTRS_IMPORTS, |
23 | 23 | BINOP_OPERAND_PROPERTY, |
| 24 | + MISSING, |
24 | 25 | TC001, |
25 | 26 | TC002, |
26 | 27 | TC003, |
@@ -1105,6 +1106,8 @@ def __init__( |
1105 | 1106 | # E.g. the type expression of `typing.Annotated[type, value]` |
1106 | 1107 | self.in_soft_use_context: bool = False |
1107 | 1108 |
|
| 1109 | + self._lookup_cache: dict[ast.AST, str | None] = {} |
| 1110 | + |
1108 | 1111 | @contextmanager |
1109 | 1112 | def create_scope(self, node: ast.ClassDef | Function, is_head: bool = True) -> Iterator[Scope]: |
1110 | 1113 | """Create a new scope.""" |
@@ -1158,36 +1161,46 @@ def type_checking_symbols(self) -> Iterator[Symbol]: |
1158 | 1161 |
|
1159 | 1162 | def lookup_full_name(self, node: ast.AST) -> str | None: |
1160 | 1163 | """Lookup the fully qualified name of the given node.""" |
| 1164 | + if (name := self._lookup_cache.get(node, MISSING)) is not MISSING: |
| 1165 | + return name |
| 1166 | + |
1161 | 1167 | if isinstance(node, ast.Name): |
1162 | 1168 | imp = self.imports.get(node.id) |
1163 | | - return imp.full_name if imp is not None else node.id |
| 1169 | + name = imp.full_name if imp is not None else node.id |
1164 | 1170 |
|
1165 | | - if not isinstance(node, ast.Attribute): |
1166 | | - return None |
| 1171 | + elif not isinstance(node, ast.Attribute): |
| 1172 | + name = None |
1167 | 1173 |
|
1168 | | - parts: list[str] = [] |
1169 | | - while isinstance(node, ast.Attribute): |
1170 | | - # left append to the list so the names are in the |
1171 | | - # natural reading order i.e. `a.b.c` becomes `['a', 'b', 'c']` |
1172 | | - parts.insert(0, node.attr) |
1173 | | - node = node.value |
1174 | | - |
1175 | | - if not isinstance(node, ast.Name): |
1176 | | - return None |
1177 | | - |
1178 | | - parts.insert(0, node.id) |
| 1174 | + else: |
| 1175 | + current_node: ast.AST = node |
| 1176 | + parts: list[str] = [] |
| 1177 | + while isinstance(current_node, ast.Attribute): |
| 1178 | + # left append to the list so the names are in the |
| 1179 | + # natural reading order i.e. `a.b.c` becomes `['a', 'b', 'c']` |
| 1180 | + parts.insert(0, current_node.attr) |
| 1181 | + current_node = current_node.value |
| 1182 | + |
| 1183 | + if not isinstance(current_node, ast.Name): |
| 1184 | + self._lookup_cache[node] = None |
| 1185 | + return None |
1179 | 1186 |
|
1180 | | - # lookup all variations of `a` `a.b` `a.b.c` in that order |
1181 | | - for num_parts in range(1, len(parts) + 1): |
1182 | | - name = '.'.join(parts[:num_parts]) |
1183 | | - imp = self.imports.get(name) |
1184 | | - if imp is not None: |
1185 | | - prefix = imp.full_name |
1186 | | - remainder = '.'.join(parts[num_parts:]) |
1187 | | - return f'{prefix}.{remainder}' if remainder else prefix |
| 1187 | + parts.insert(0, current_node.id) |
| 1188 | + |
| 1189 | + # lookup all variations of `a` `a.b` `a.b.c` in that order |
| 1190 | + for num_parts in range(1, len(parts) + 1): |
| 1191 | + name = '.'.join(parts[:num_parts]) |
| 1192 | + imp = self.imports.get(name) |
| 1193 | + if imp is not None: |
| 1194 | + prefix = imp.full_name |
| 1195 | + remainder = '.'.join(parts[num_parts:]) |
| 1196 | + name = f'{prefix}.{remainder}' if remainder else prefix |
| 1197 | + break |
| 1198 | + else: |
| 1199 | + # fallback to returning the name as-is |
| 1200 | + name = '.'.join(parts) |
1188 | 1201 |
|
1189 | | - # fallback to returning the name as-is |
1190 | | - return '.'.join(parts) |
| 1202 | + self._lookup_cache[node] = name |
| 1203 | + return name |
1191 | 1204 |
|
1192 | 1205 | def is_typing(self, node: ast.AST, symbol: str) -> bool: |
1193 | 1206 | """Check if the given node matches the given typing symbol.""" |
|
0 commit comments