Skip to content

Commit a35e3c0

Browse files
authored
Prevent crash when enum/typeddict call is stored as a class attribute (python#18861)
Fixes python#18736. Includes same fix for TypedDict (also crashes on master) and NamedTuple (does not crash as it rejects MemberExpr before setting .analyzed, so just for the sake of consistency)
1 parent 4fb187f commit a35e3c0

File tree

4 files changed

+48
-5
lines changed

4 files changed

+48
-5
lines changed

mypy/semanal.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3464,8 +3464,9 @@ def record_special_form_lvalue(self, s: AssignmentStmt) -> None:
34643464
def analyze_enum_assign(self, s: AssignmentStmt) -> bool:
34653465
"""Check if s defines an Enum."""
34663466
if isinstance(s.rvalue, CallExpr) and isinstance(s.rvalue.analyzed, EnumCallExpr):
3467-
# Already analyzed enum -- nothing to do here.
3468-
return True
3467+
# This is an analyzed enum definition.
3468+
# It is valid iff it can be stored correctly, failures were already reported.
3469+
return self._is_single_name_assignment(s)
34693470
return self.enum_call_analyzer.process_enum_call(s, self.is_func_scope())
34703471

34713472
def analyze_namedtuple_assign(self, s: AssignmentStmt) -> bool:
@@ -3474,7 +3475,9 @@ def analyze_namedtuple_assign(self, s: AssignmentStmt) -> bool:
34743475
if s.rvalue.analyzed.info.tuple_type and not has_placeholder(
34753476
s.rvalue.analyzed.info.tuple_type
34763477
):
3477-
return True # This is a valid and analyzed named tuple definition, nothing to do here.
3478+
# This is an analyzed named tuple definition.
3479+
# It is valid iff it can be stored correctly, failures were already reported.
3480+
return self._is_single_name_assignment(s)
34783481
if len(s.lvalues) != 1 or not isinstance(s.lvalues[0], (NameExpr, MemberExpr)):
34793482
return False
34803483
lvalue = s.lvalues[0]
@@ -3515,8 +3518,9 @@ def analyze_typeddict_assign(self, s: AssignmentStmt) -> bool:
35153518
if s.rvalue.analyzed.info.typeddict_type and not has_placeholder(
35163519
s.rvalue.analyzed.info.typeddict_type
35173520
):
3518-
# This is a valid and analyzed typed dict definition, nothing to do here.
3519-
return True
3521+
# This is an analyzed typed dict definition.
3522+
# It is valid iff it can be stored correctly, failures were already reported.
3523+
return self._is_single_name_assignment(s)
35203524
if len(s.lvalues) != 1 or not isinstance(s.lvalues[0], (NameExpr, MemberExpr)):
35213525
return False
35223526
lvalue = s.lvalues[0]
@@ -3540,6 +3544,9 @@ def analyze_typeddict_assign(self, s: AssignmentStmt) -> bool:
35403544
self.setup_alias_type_vars(defn)
35413545
return True
35423546

3547+
def _is_single_name_assignment(self, s: AssignmentStmt) -> bool:
3548+
return len(s.lvalues) == 1 and isinstance(s.lvalues[0], NameExpr)
3549+
35433550
def analyze_lvalues(self, s: AssignmentStmt) -> None:
35443551
# We cannot use s.type, because analyze_simple_literal_type() will set it.
35453552
explicit = s.unanalyzed_type is not None

test-data/unit/check-enum.test

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2512,3 +2512,15 @@ def list_vals(e: Type[T]) -> list[T]:
25122512

25132513
reveal_type(list_vals(Choices)) # N: Revealed type is "builtins.list[__main__.Choices]"
25142514
[builtins fixtures/enum.pyi]
2515+
2516+
[case testEnumAsClassMemberNoCrash]
2517+
# https://github.com/python/mypy/issues/18736
2518+
from enum import Enum
2519+
2520+
class Base:
2521+
def __init__(self, namespace: tuple[str, ...]) -> None:
2522+
# Not a bug: trigger defer
2523+
names = [name for name in namespace if fail] # E: Name "fail" is not defined
2524+
self.o = Enum("o", names) # E: Enum type as attribute is not supported \
2525+
# E: Second argument of Enum() must be string, tuple, list or dict literal for mypy to determine Enum members
2526+
[builtins fixtures/tuple.pyi]

test-data/unit/check-namedtuple.test

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1519,3 +1519,14 @@ class C(T):
15191519
c: Union[C, Any]
15201520
reveal_type(c.f()) # N: Revealed type is "Union[builtins.bool, Any]"
15211521
[builtins fixtures/tuple.pyi]
1522+
1523+
[case testNamedTupleAsClassMemberNoCrash]
1524+
# https://github.com/python/mypy/issues/18736
1525+
from collections import namedtuple
1526+
1527+
class Base:
1528+
def __init__(self, namespace: tuple[str, ...]) -> None:
1529+
# Not a bug: trigger defer
1530+
names = [name for name in namespace if fail] # E: Name "fail" is not defined
1531+
self.n = namedtuple("n", names) # E: NamedTuple type as an attribute is not supported
1532+
[builtins fixtures/tuple.pyi]

test-data/unit/check-typeddict.test

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4138,3 +4138,16 @@ Derived.Params(name="Robert")
41384138
DerivedOverride.Params(name="Robert")
41394139
[builtins fixtures/dict.pyi]
41404140
[typing fixtures/typing-typeddict.pyi]
4141+
4142+
[case testEnumAsClassMemberNoCrash]
4143+
# https://github.com/python/mypy/issues/18736
4144+
from typing import TypedDict
4145+
4146+
class Base:
4147+
def __init__(self, namespace: dict[str, str]) -> None:
4148+
# Not a bug: trigger defer
4149+
names = {n: n for n in namespace if fail} # E: Name "fail" is not defined
4150+
self.d = TypedDict("d", names) # E: TypedDict type as attribute is not supported \
4151+
# E: TypedDict() expects a dictionary literal as the second argument
4152+
[builtins fixtures/dict.pyi]
4153+
[typing fixtures/typing-typeddict.pyi]

0 commit comments

Comments
 (0)