Skip to content

Commit b31b06b

Browse files
sobolevnJukkaL
authored andcommitted
Enums with annotations and no values are fine to be subclassed (#11579)
Closes #11578. Closes #11702.
1 parent 750b77d commit b31b06b

File tree

4 files changed

+128
-16
lines changed

4 files changed

+128
-16
lines changed

Diff for: mypy/nodes.py

+5
Original file line numberDiff line numberDiff line change
@@ -840,6 +840,7 @@ def deserialize(cls, data: JsonDict) -> 'Decorator':
840840
'is_classmethod', 'is_property', 'is_settable_property', 'is_suppressed_import',
841841
'is_classvar', 'is_abstract_var', 'is_final', 'final_unset_in_class', 'final_set_in_init',
842842
'explicit_self_type', 'is_ready', 'from_module_getattr',
843+
'has_explicit_value',
843844
]
844845

845846

@@ -870,6 +871,7 @@ class Var(SymbolNode):
870871
'is_suppressed_import',
871872
'explicit_self_type',
872873
'from_module_getattr',
874+
'has_explicit_value',
873875
)
874876

875877
def __init__(self, name: str, type: 'Optional[mypy.types.Type]' = None) -> None:
@@ -914,6 +916,9 @@ def __init__(self, name: str, type: 'Optional[mypy.types.Type]' = None) -> None:
914916
self.explicit_self_type = False
915917
# If True, this is an implicit Var created due to module-level __getattr__.
916918
self.from_module_getattr = False
919+
# Var can be created with an explicit value `a = 1` or without one `a: int`,
920+
# we need a way to tell which one is which.
921+
self.has_explicit_value = False
917922

918923
@property
919924
def name(self) -> str:

Diff for: mypy/semanal.py

+55-15
Original file line numberDiff line numberDiff line change
@@ -1552,17 +1552,12 @@ def configure_base_classes(self,
15521552
elif isinstance(base, Instance):
15531553
if base.type.is_newtype:
15541554
self.fail('Cannot subclass "NewType"', defn)
1555-
if (
1556-
base.type.is_enum
1557-
and base.type.fullname not in ENUM_BASES
1558-
and base.type.names
1559-
and any(not isinstance(n.node, (FuncBase, Decorator))
1560-
for n in base.type.names.values())
1561-
):
1555+
if self.enum_has_final_values(base):
15621556
# This means that are trying to subclass a non-default
15631557
# Enum class, with defined members. This is not possible.
15641558
# In runtime, it will raise. We need to mark this type as final.
15651559
# However, methods can be defined on a type: only values can't.
1560+
# We also don't count values with annotations only.
15661561
base.type.is_final = True
15671562
base_types.append(base)
15681563
elif isinstance(base, AnyType):
@@ -1601,6 +1596,25 @@ def configure_base_classes(self,
16011596
return
16021597
self.calculate_class_mro(defn, self.object_type)
16031598

1599+
def enum_has_final_values(self, base: Instance) -> bool:
1600+
if (
1601+
base.type.is_enum
1602+
and base.type.fullname not in ENUM_BASES
1603+
and base.type.names
1604+
and base.type.defn
1605+
):
1606+
for sym in base.type.names.values():
1607+
if isinstance(sym.node, (FuncBase, Decorator)):
1608+
continue # A method
1609+
if not isinstance(sym.node, Var):
1610+
return True # Can be a class
1611+
if self.is_stub_file or sym.node.has_explicit_value:
1612+
# Corner case: assignments like `x: int` are fine in `.py` files.
1613+
# But, not is `.pyi` files, because we don't know
1614+
# if there's aactually a value or not.
1615+
return True
1616+
return False
1617+
16041618
def configure_tuple_base_class(self,
16051619
defn: ClassDef,
16061620
base: TupleType,
@@ -2040,7 +2054,7 @@ def visit_import_all(self, i: ImportAll) -> None:
20402054

20412055
def visit_assignment_expr(self, s: AssignmentExpr) -> None:
20422056
s.value.accept(self)
2043-
self.analyze_lvalue(s.target, escape_comprehensions=True)
2057+
self.analyze_lvalue(s.target, escape_comprehensions=True, has_explicit_value=True)
20442058

20452059
def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
20462060
self.statement = s
@@ -2333,10 +2347,20 @@ def analyze_lvalues(self, s: AssignmentStmt) -> None:
23332347
assert isinstance(s.unanalyzed_type, UnboundType)
23342348
if not s.unanalyzed_type.args:
23352349
explicit = False
2350+
2351+
if s.rvalue:
2352+
if isinstance(s.rvalue, TempNode):
2353+
has_explicit_value = not s.rvalue.no_rhs
2354+
else:
2355+
has_explicit_value = True
2356+
else:
2357+
has_explicit_value = False
2358+
23362359
for lval in s.lvalues:
23372360
self.analyze_lvalue(lval,
23382361
explicit_type=explicit,
2339-
is_final=s.is_final_def)
2362+
is_final=s.is_final_def,
2363+
has_explicit_value=has_explicit_value)
23402364

23412365
def apply_dynamic_class_hook(self, s: AssignmentStmt) -> None:
23422366
if not isinstance(s.rvalue, CallExpr):
@@ -2776,7 +2800,8 @@ def analyze_lvalue(self,
27762800
nested: bool = False,
27772801
explicit_type: bool = False,
27782802
is_final: bool = False,
2779-
escape_comprehensions: bool = False) -> None:
2803+
escape_comprehensions: bool = False,
2804+
has_explicit_value: bool = False) -> None:
27802805
"""Analyze an lvalue or assignment target.
27812806
27822807
Args:
@@ -2790,7 +2815,11 @@ def analyze_lvalue(self,
27902815
if escape_comprehensions:
27912816
assert isinstance(lval, NameExpr), "assignment expression target must be NameExpr"
27922817
if isinstance(lval, NameExpr):
2793-
self.analyze_name_lvalue(lval, explicit_type, is_final, escape_comprehensions)
2818+
self.analyze_name_lvalue(
2819+
lval, explicit_type, is_final,
2820+
escape_comprehensions,
2821+
has_explicit_value=has_explicit_value,
2822+
)
27942823
elif isinstance(lval, MemberExpr):
27952824
self.analyze_member_lvalue(lval, explicit_type, is_final)
27962825
if explicit_type and not self.is_self_member_ref(lval):
@@ -2814,7 +2843,8 @@ def analyze_name_lvalue(self,
28142843
lvalue: NameExpr,
28152844
explicit_type: bool,
28162845
is_final: bool,
2817-
escape_comprehensions: bool) -> None:
2846+
escape_comprehensions: bool,
2847+
has_explicit_value: bool) -> None:
28182848
"""Analyze an lvalue that targets a name expression.
28192849
28202850
Arguments are similar to "analyze_lvalue".
@@ -2844,7 +2874,7 @@ def analyze_name_lvalue(self,
28442874

28452875
if (not existing or isinstance(existing.node, PlaceholderNode)) and not outer:
28462876
# Define new variable.
2847-
var = self.make_name_lvalue_var(lvalue, kind, not explicit_type)
2877+
var = self.make_name_lvalue_var(lvalue, kind, not explicit_type, has_explicit_value)
28482878
added = self.add_symbol(name, var, lvalue, escape_comprehensions=escape_comprehensions)
28492879
# Only bind expression if we successfully added name to symbol table.
28502880
if added:
@@ -2895,7 +2925,9 @@ def is_alias_for_final_name(self, name: str) -> bool:
28952925
existing = self.globals.get(orig_name)
28962926
return existing is not None and is_final_node(existing.node)
28972927

2898-
def make_name_lvalue_var(self, lvalue: NameExpr, kind: int, inferred: bool) -> Var:
2928+
def make_name_lvalue_var(
2929+
self, lvalue: NameExpr, kind: int, inferred: bool, has_explicit_value: bool,
2930+
) -> Var:
28992931
"""Return a Var node for an lvalue that is a name expression."""
29002932
v = Var(lvalue.name)
29012933
v.set_line(lvalue)
@@ -2910,6 +2942,7 @@ def make_name_lvalue_var(self, lvalue: NameExpr, kind: int, inferred: bool) -> V
29102942
# fullanme should never stay None
29112943
v._fullname = lvalue.name
29122944
v.is_ready = False # Type not inferred yet
2945+
v.has_explicit_value = has_explicit_value
29132946
return v
29142947

29152948
def make_name_lvalue_point_to_existing_def(
@@ -2953,7 +2986,14 @@ def analyze_tuple_or_list_lvalue(self, lval: TupleExpr,
29532986
if len(star_exprs) == 1:
29542987
star_exprs[0].valid = True
29552988
for i in items:
2956-
self.analyze_lvalue(i, nested=True, explicit_type=explicit_type)
2989+
self.analyze_lvalue(
2990+
lval=i,
2991+
nested=True,
2992+
explicit_type=explicit_type,
2993+
# Lists and tuples always have explicit values defined:
2994+
# `a, b, c = value`
2995+
has_explicit_value=True,
2996+
)
29572997

29582998
def analyze_member_lvalue(self, lval: MemberExpr, explicit_type: bool, is_final: bool) -> None:
29592999
"""Analyze lvalue that is a member expression.

Diff for: test-data/unit/check-enum.test

+45-1
Original file line numberDiff line numberDiff line change
@@ -1678,7 +1678,6 @@ class A(Enum):
16781678
class B(A): pass # E: Cannot inherit from final class "A"
16791679
[builtins fixtures/bool.pyi]
16801680

1681-
16821681
[case testEnumFinalSpecialProps]
16831682
# https://github.com/python/mypy/issues/11699
16841683
from enum import Enum, IntEnum
@@ -1702,3 +1701,48 @@ class EI(IntEnum):
17021701
E._order_ = 'a' # E: Cannot assign to final attribute "_order_"
17031702
EI.value = 2 # E: Cannot assign to final attribute "value"
17041703
[builtins fixtures/bool.pyi]
1704+
1705+
[case testEnumNotFinalWithMethodsAndUninitializedValues]
1706+
# https://github.com/python/mypy/issues/11578
1707+
from enum import Enum
1708+
from typing import Final
1709+
1710+
class A(Enum):
1711+
x: int
1712+
def method(self) -> int: pass
1713+
class B(A):
1714+
x = 1 # E: Cannot override writable attribute "x" with a final one
1715+
1716+
class A1(Enum):
1717+
x: int = 1
1718+
class B1(A1): # E: Cannot inherit from final class "A1"
1719+
pass
1720+
1721+
class A2(Enum):
1722+
x = 2
1723+
class B2(A2): # E: Cannot inherit from final class "A2"
1724+
pass
1725+
1726+
# We leave this `Final` without a value,
1727+
# because we need to test annotation only mode:
1728+
class A3(Enum):
1729+
x: Final[int] # type: ignore
1730+
class B3(A3):
1731+
x = 1 # E: Cannot override final attribute "x" (previously declared in base class "A3")
1732+
[builtins fixtures/bool.pyi]
1733+
1734+
[case testEnumNotFinalWithMethodsAndUninitializedValuesStub]
1735+
import lib
1736+
1737+
[file lib.pyi]
1738+
from enum import Enum
1739+
class A(Enum):
1740+
x: int
1741+
class B(A): # E: Cannot inherit from final class "A"
1742+
x = 1 # E: Cannot override writable attribute "x" with a final one
1743+
1744+
class C(Enum):
1745+
x = 1
1746+
class D(C): # E: Cannot inherit from final class "C"
1747+
x: int # E: Cannot assign to final name "x"
1748+
[builtins fixtures/bool.pyi]

Diff for: test-data/unit/check-incremental.test

+23
Original file line numberDiff line numberDiff line change
@@ -5610,3 +5610,26 @@ class C:
56105610
pass
56115611
[rechecked]
56125612
[stale]
5613+
5614+
5615+
[case testEnumAreStillFinalAfterCache]
5616+
import a
5617+
class Ok(a.RegularEnum):
5618+
x = 1
5619+
class NotOk(a.FinalEnum):
5620+
x = 1
5621+
[file a.py]
5622+
from enum import Enum
5623+
class RegularEnum(Enum):
5624+
x: int
5625+
class FinalEnum(Enum):
5626+
x = 1
5627+
[builtins fixtures/isinstance.pyi]
5628+
[out]
5629+
main:3: error: Cannot override writable attribute "x" with a final one
5630+
main:4: error: Cannot inherit from final class "FinalEnum"
5631+
main:5: error: Cannot override final attribute "x" (previously declared in base class "FinalEnum")
5632+
[out2]
5633+
main:3: error: Cannot override writable attribute "x" with a final one
5634+
main:4: error: Cannot inherit from final class "FinalEnum"
5635+
main:5: error: Cannot override final attribute "x" (previously declared in base class "FinalEnum")

0 commit comments

Comments
 (0)