Skip to content

Commit b199f90

Browse files
Analyse typeddict decorators (#21267)
Previously, the semantic analyzer's special-case handling for TypedDict (analyze_typeddict_classdef) was returning early without visiting the decorators list in the ClassDef. This caused mypy to silently ignore invalid names, attributes, or subscripted expressions used as decorators on these classes. Fixes #21030 --------- Co-authored-by: hauntsaninja <[email protected]>
1 parent 06e48a0 commit b199f90

2 files changed

Lines changed: 27 additions & 12 deletions

File tree

‎mypy/semanal.py‎

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2110,18 +2110,20 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> bool:
21102110
and defn.info.typeddict_type
21112111
and not has_placeholder(defn.info.typeddict_type)
21122112
):
2113-
# This is a valid TypedDict, and it is fully analyzed.
2114-
return True
2115-
is_typeddict, info = self.typed_dict_analyzer.analyze_typeddict_classdef(defn)
2113+
# Don't reprocess everything
2114+
is_typeddict = True
2115+
info = defn.info
2116+
else:
2117+
is_typeddict, info = self.typed_dict_analyzer.analyze_typeddict_classdef(defn)
21162118
if is_typeddict:
2117-
for decorator in defn.decorators:
2118-
decorator.accept(self)
2119-
if info is not None:
2120-
self.analyze_class_decorator_common(defn, info, decorator)
21212119
if info is None:
21222120
self.mark_incomplete(defn.name, defn)
21232121
else:
21242122
self.prepare_class_def(defn, info, custom_names=True)
2123+
for decorator in defn.decorators:
2124+
decorator.accept(self)
2125+
if defn.info:
2126+
self.analyze_class_decorator_common(defn, decorator)
21252127
return True
21262128
return False
21272129

@@ -2153,7 +2155,7 @@ def analyze_namedtuple_classdef(
21532155
with self.scope.class_scope(defn.info):
21542156
for deco in defn.decorators:
21552157
deco.accept(self)
2156-
self.analyze_class_decorator_common(defn, defn.info, deco)
2158+
self.analyze_class_decorator_common(defn, deco)
21572159
with self.named_tuple_analyzer.save_namedtuple_body(info):
21582160
self.analyze_class_body_common(defn)
21592161
return True
@@ -2235,7 +2237,7 @@ def leave_class(self) -> None:
22352237

22362238
def analyze_class_decorator(self, defn: ClassDef, decorator: Expression) -> None:
22372239
decorator.accept(self)
2238-
self.analyze_class_decorator_common(defn, defn.info, decorator)
2240+
self.analyze_class_decorator_common(defn, decorator)
22392241
if isinstance(decorator, RefExpr):
22402242
if decorator.fullname in RUNTIME_PROTOCOL_DECOS:
22412243
if defn.info.is_protocol:
@@ -2247,13 +2249,12 @@ def analyze_class_decorator(self, defn: ClassDef, decorator: Expression) -> None
22472249
):
22482250
defn.info.dataclass_transform_spec = self.parse_dataclass_transform_spec(decorator)
22492251

2250-
def analyze_class_decorator_common(
2251-
self, defn: ClassDef, info: TypeInfo, decorator: Expression
2252-
) -> None:
2252+
def analyze_class_decorator_common(self, defn: ClassDef, decorator: Expression) -> None:
22532253
"""Common method for applying class decorators.
22542254
22552255
Called on regular classes, typeddicts, and namedtuples.
22562256
"""
2257+
info = defn.info
22572258
if refers_to_fullname(decorator, FINAL_DECORATOR_NAMES):
22582259
info.is_final = True
22592260
elif refers_to_fullname(decorator, DISJOINT_BASE_DECORATOR_NAMES):

‎test-data/unit/check-typeddict.test‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,20 @@ reveal_type(d) # N: Revealed type is "TypedDict('__main__.D', {})"
235235
[builtins fixtures/dict.pyi]
236236
[typing fixtures/typing-typeddict.pyi]
237237

238+
[case testTypedDictDecoratorUndefinedNames]
239+
from typing import TypedDict
240+
241+
@abc # E: Name "abc" is not defined
242+
@efg.hij # E: Name "efg" is not defined
243+
@klm[nop] # E: Name "klm" is not defined \
244+
# E: Name "nop" is not defined
245+
@qrs.tuv[wxy] # E: Name "qrs" is not defined \
246+
# E: Name "wxy" is not defined
247+
class A(TypedDict):
248+
x: int
249+
[builtins fixtures/dict.pyi]
250+
[typing fixtures/typing-typeddict.pyi]
251+
238252
[case testTypedDictWithClassmethodAlternativeConstructorDoesNotCrash]
239253
# https://github.com/python/mypy/issues/5653
240254
from typing import TypedDict

0 commit comments

Comments
 (0)