Skip to content

Commit 0a03ba0

Browse files
authored
Move final detection for Enum in checker.py (#11984)
Fixes #11850
1 parent 002e309 commit 0a03ba0

File tree

6 files changed

+149
-71
lines changed

6 files changed

+149
-71
lines changed

mypy/checker.py

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@
2424
Context, Decorator, PrintStmt, BreakStmt, PassStmt, ContinueStmt,
2525
ComparisonExpr, StarExpr, EllipsisExpr, RefExpr, PromoteExpr,
2626
Import, ImportFrom, ImportAll, ImportBase, TypeAlias,
27-
ARG_POS, ARG_STAR, LITERAL_TYPE, LDEF, MDEF, GDEF,
27+
ARG_POS, ARG_STAR, ARG_NAMED, LITERAL_TYPE, LDEF, MDEF, GDEF,
2828
CONTRAVARIANT, COVARIANT, INVARIANT, TypeVarExpr, AssignmentExpr,
29-
is_final_node, ARG_NAMED, MatchStmt)
29+
is_final_node, MatchStmt)
3030
from mypy import nodes
3131
from mypy import operators
3232
from mypy.literals import literal, literal_hash, Key
@@ -86,7 +86,7 @@
8686
from mypy import state, errorcodes as codes
8787
from mypy.traverser import has_return_statement, all_return_statements
8888
from mypy.errorcodes import ErrorCode
89-
from mypy.util import is_typeshed_file
89+
from mypy.util import is_typeshed_file, is_dunder, is_sunder
9090

9191
T = TypeVar('T')
9292

@@ -1833,6 +1833,10 @@ def visit_class_def(self, defn: ClassDef) -> None:
18331833
# that completely swap out the type. (e.g. Callable[[Type[A]], Type[B]])
18341834
if typ.is_protocol and typ.defn.type_vars:
18351835
self.check_protocol_variance(defn)
1836+
if not defn.has_incompatible_baseclass and defn.info.is_enum:
1837+
for base in defn.info.mro[1:-1]: # we don't need self and `object`
1838+
if base.is_enum and base.fullname not in ENUM_BASES:
1839+
self.check_final_enum(defn, base)
18361840

18371841
def check_final_deletable(self, typ: TypeInfo) -> None:
18381842
# These checks are only for mypyc. Only perform some checks that are easier
@@ -1890,6 +1894,43 @@ def check_init_subclass(self, defn: ClassDef) -> None:
18901894
# all other bases have already been checked.
18911895
break
18921896

1897+
def check_final_enum(self, defn: ClassDef, base: TypeInfo) -> None:
1898+
for sym in base.names.values():
1899+
if self.is_final_enum_value(sym):
1900+
self.fail(
1901+
'Cannot extend enum with existing members: "{}"'.format(
1902+
base.name,
1903+
),
1904+
defn,
1905+
)
1906+
break
1907+
1908+
def is_final_enum_value(self, sym: SymbolTableNode) -> bool:
1909+
if isinstance(sym.node, (FuncBase, Decorator)):
1910+
return False # A method is fine
1911+
if not isinstance(sym.node, Var):
1912+
return True # Can be a class or anything else
1913+
1914+
# Now, only `Var` is left, we need to check:
1915+
# 1. Private name like in `__prop = 1`
1916+
# 2. Dunder name like `__hash__ = some_hasher`
1917+
# 3. Sunder name like `_order_ = 'a, b, c'`
1918+
# 4. If it is a method / descriptor like in `method = classmethod(func)`
1919+
if (
1920+
is_private(sym.node.name)
1921+
or is_dunder(sym.node.name)
1922+
or is_sunder(sym.node.name)
1923+
# TODO: make sure that `x = @class/staticmethod(func)`
1924+
# and `x = property(prop)` both work correctly.
1925+
# Now they are incorrectly counted as enum members.
1926+
or isinstance(get_proper_type(sym.node.type), FunctionLike)
1927+
):
1928+
return False
1929+
1930+
if self.is_stub or sym.node.has_explicit_value:
1931+
return True
1932+
return False
1933+
18931934
def check_protocol_variance(self, defn: ClassDef) -> None:
18941935
"""Check that protocol definition is compatible with declared
18951936
variances of type variables.

mypy/semanal.py

Lines changed: 4 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,12 @@
7777
REVEAL_LOCALS, is_final_node, TypedDictExpr, type_aliases_source_versions,
7878
typing_extensions_aliases,
7979
EnumCallExpr, RUNTIME_PROTOCOL_DECOS, FakeExpression, Statement, AssignmentExpr,
80-
ParamSpecExpr, EllipsisExpr, TypeVarLikeExpr, FuncBase, implicit_module_attrs,
81-
MatchStmt
80+
ParamSpecExpr, EllipsisExpr, TypeVarLikeExpr, implicit_module_attrs,
81+
MatchStmt,
8282
)
8383
from mypy.patterns import (
8484
AsPattern, OrPattern, ValuePattern, SequencePattern,
85-
StarredPattern, MappingPattern, ClassPattern
85+
StarredPattern, MappingPattern, ClassPattern,
8686
)
8787
from mypy.tvar_scope import TypeVarLikeScope
8888
from mypy.typevars import fill_typevars
@@ -123,7 +123,7 @@
123123
)
124124
from mypy.semanal_namedtuple import NamedTupleAnalyzer
125125
from mypy.semanal_typeddict import TypedDictAnalyzer
126-
from mypy.semanal_enum import EnumCallAnalyzer, ENUM_BASES
126+
from mypy.semanal_enum import EnumCallAnalyzer
127127
from mypy.semanal_newtype import NewTypeAnalyzer
128128
from mypy.reachability import (
129129
infer_reachability_of_if_statement, infer_reachability_of_match_statement,
@@ -1554,13 +1554,6 @@ def configure_base_classes(self,
15541554
elif isinstance(base, Instance):
15551555
if base.type.is_newtype:
15561556
self.fail('Cannot subclass "NewType"', defn)
1557-
if self.enum_has_final_values(base):
1558-
# This means that are trying to subclass a non-default
1559-
# Enum class, with defined members. This is not possible.
1560-
# In runtime, it will raise. We need to mark this type as final.
1561-
# However, methods can be defined on a type: only values can't.
1562-
# We also don't count values with annotations only.
1563-
base.type.is_final = True
15641557
base_types.append(base)
15651558
elif isinstance(base, AnyType):
15661559
if self.options.disallow_subclassing_any:
@@ -1598,25 +1591,6 @@ def configure_base_classes(self,
15981591
return
15991592
self.calculate_class_mro(defn, self.object_type)
16001593

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

mypy/stubtest.py

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from mypy import nodes
2626
from mypy.config_parser import parse_config_file
2727
from mypy.options import Options
28-
from mypy.util import FancyFormatter, bytes_to_human_readable_repr
28+
from mypy.util import FancyFormatter, bytes_to_human_readable_repr, is_dunder, SPECIAL_DUNDERS
2929

3030

3131
class Missing:
@@ -899,20 +899,6 @@ def verify_typealias(
899899
)
900900

901901

902-
SPECIAL_DUNDERS = ("__init__", "__new__", "__call__", "__init_subclass__", "__class_getitem__")
903-
904-
905-
def is_dunder(name: str, exclude_special: bool = False) -> bool:
906-
"""Returns whether name is a dunder name.
907-
908-
:param exclude_special: Whether to return False for a couple special dunder methods.
909-
910-
"""
911-
if exclude_special and name in SPECIAL_DUNDERS:
912-
return False
913-
return name.startswith("__") and name.endswith("__")
914-
915-
916902
def is_probably_a_function(runtime: Any) -> bool:
917903
return (
918904
isinstance(runtime, (types.FunctionType, types.BuiltinFunctionType))

mypy/util.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,26 @@
4646
"C:\\Python27\\python.exe",
4747
]
4848

49+
SPECIAL_DUNDERS: Final = frozenset((
50+
"__init__", "__new__", "__call__", "__init_subclass__", "__class_getitem__",
51+
))
52+
53+
54+
def is_dunder(name: str, exclude_special: bool = False) -> bool:
55+
"""Returns whether name is a dunder name.
56+
57+
Args:
58+
exclude_special: Whether to return False for a couple special dunder methods.
59+
60+
"""
61+
if exclude_special and name in SPECIAL_DUNDERS:
62+
return False
63+
return name.startswith("__") and name.endswith("__")
64+
65+
66+
def is_sunder(name: str) -> bool:
67+
return not is_dunder(name) and name.startswith('_') and name.endswith('_')
68+
4969

5070
def split_module_names(mod_name: str) -> List[str]:
5171
"""Return the module and all parent module names.

test-data/unit/check-enum.test

Lines changed: 78 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1469,22 +1469,22 @@ class NonEmptyFlag(Flag):
14691469
class NonEmptyIntFlag(IntFlag):
14701470
x = 1
14711471

1472-
class ErrorEnumWithValue(NonEmptyEnum): # E: Cannot inherit from final class "NonEmptyEnum"
1472+
class ErrorEnumWithValue(NonEmptyEnum): # E: Cannot extend enum with existing members: "NonEmptyEnum"
14731473
x = 1 # E: Cannot override final attribute "x" (previously declared in base class "NonEmptyEnum")
1474-
class ErrorIntEnumWithValue(NonEmptyIntEnum): # E: Cannot inherit from final class "NonEmptyIntEnum"
1474+
class ErrorIntEnumWithValue(NonEmptyIntEnum): # E: Cannot extend enum with existing members: "NonEmptyIntEnum"
14751475
x = 1 # E: Cannot override final attribute "x" (previously declared in base class "NonEmptyIntEnum")
1476-
class ErrorFlagWithValue(NonEmptyFlag): # E: Cannot inherit from final class "NonEmptyFlag"
1476+
class ErrorFlagWithValue(NonEmptyFlag): # E: Cannot extend enum with existing members: "NonEmptyFlag"
14771477
x = 1 # E: Cannot override final attribute "x" (previously declared in base class "NonEmptyFlag")
1478-
class ErrorIntFlagWithValue(NonEmptyIntFlag): # E: Cannot inherit from final class "NonEmptyIntFlag"
1478+
class ErrorIntFlagWithValue(NonEmptyIntFlag): # E: Cannot extend enum with existing members: "NonEmptyIntFlag"
14791479
x = 1 # E: Cannot override final attribute "x" (previously declared in base class "NonEmptyIntFlag")
14801480

1481-
class ErrorEnumWithoutValue(NonEmptyEnum): # E: Cannot inherit from final class "NonEmptyEnum"
1481+
class ErrorEnumWithoutValue(NonEmptyEnum): # E: Cannot extend enum with existing members: "NonEmptyEnum"
14821482
pass
1483-
class ErrorIntEnumWithoutValue(NonEmptyIntEnum): # E: Cannot inherit from final class "NonEmptyIntEnum"
1483+
class ErrorIntEnumWithoutValue(NonEmptyIntEnum): # E: Cannot extend enum with existing members: "NonEmptyIntEnum"
14841484
pass
1485-
class ErrorFlagWithoutValue(NonEmptyFlag): # E: Cannot inherit from final class "NonEmptyFlag"
1485+
class ErrorFlagWithoutValue(NonEmptyFlag): # E: Cannot extend enum with existing members: "NonEmptyFlag"
14861486
pass
1487-
class ErrorIntFlagWithoutValue(NonEmptyIntFlag): # E: Cannot inherit from final class "NonEmptyIntFlag"
1487+
class ErrorIntFlagWithoutValue(NonEmptyIntFlag): # E: Cannot extend enum with existing members: "NonEmptyIntFlag"
14881488
pass
14891489
[builtins fixtures/bool.pyi]
14901490

@@ -1588,13 +1588,17 @@ class NonEmptyIntFlag(IntFlag):
15881588
class NonEmptyEnumMeta(EnumMeta):
15891589
x = 1
15901590

1591-
class ErrorEnumWithoutValue(NonEmptyEnum): # E: Cannot inherit from final class "NonEmptyEnum"
1591+
class ErrorEnumWithoutValue(NonEmptyEnum): # E: Cannot inherit from final class "NonEmptyEnum" \
1592+
# E: Cannot extend enum with existing members: "NonEmptyEnum"
15921593
pass
1593-
class ErrorIntEnumWithoutValue(NonEmptyIntEnum): # E: Cannot inherit from final class "NonEmptyIntEnum"
1594+
class ErrorIntEnumWithoutValue(NonEmptyIntEnum): # E: Cannot inherit from final class "NonEmptyIntEnum" \
1595+
# E: Cannot extend enum with existing members: "NonEmptyIntEnum"
15941596
pass
1595-
class ErrorFlagWithoutValue(NonEmptyFlag): # E: Cannot inherit from final class "NonEmptyFlag"
1597+
class ErrorFlagWithoutValue(NonEmptyFlag): # E: Cannot inherit from final class "NonEmptyFlag" \
1598+
# E: Cannot extend enum with existing members: "NonEmptyFlag"
15961599
pass
1597-
class ErrorIntFlagWithoutValue(NonEmptyIntFlag): # E: Cannot inherit from final class "NonEmptyIntFlag"
1600+
class ErrorIntFlagWithoutValue(NonEmptyIntFlag): # E: Cannot inherit from final class "NonEmptyIntFlag" \
1601+
# E: Cannot extend enum with existing members: "NonEmptyIntFlag"
15981602
pass
15991603
class ErrorEnumMetaWithoutValue(NonEmptyEnumMeta): # E: Cannot inherit from final class "NonEmptyEnumMeta"
16001604
pass
@@ -1692,13 +1696,13 @@ class NonEmptyIntFlag(IntFlag):
16921696
x = 1
16931697
def method(self) -> None: pass
16941698

1695-
class ErrorEnumWithoutValue(NonEmptyEnum): # E: Cannot inherit from final class "NonEmptyEnum"
1699+
class ErrorEnumWithoutValue(NonEmptyEnum): # E: Cannot extend enum with existing members: "NonEmptyEnum"
16961700
pass
1697-
class ErrorIntEnumWithoutValue(NonEmptyIntEnum): # E: Cannot inherit from final class "NonEmptyIntEnum"
1701+
class ErrorIntEnumWithoutValue(NonEmptyIntEnum): # E: Cannot extend enum with existing members: "NonEmptyIntEnum"
16981702
pass
1699-
class ErrorFlagWithoutValue(NonEmptyFlag): # E: Cannot inherit from final class "NonEmptyFlag"
1703+
class ErrorFlagWithoutValue(NonEmptyFlag): # E: Cannot extend enum with existing members: "NonEmptyFlag"
17001704
pass
1701-
class ErrorIntFlagWithoutValue(NonEmptyIntFlag): # E: Cannot inherit from final class "NonEmptyIntFlag"
1705+
class ErrorIntFlagWithoutValue(NonEmptyIntFlag): # E: Cannot extend enum with existing members: "NonEmptyIntFlag"
17021706
pass
17031707
[builtins fixtures/bool.pyi]
17041708

@@ -1707,7 +1711,7 @@ from enum import Enum
17071711

17081712
class A(Enum):
17091713
class Inner: pass
1710-
class B(A): pass # E: Cannot inherit from final class "A"
1714+
class B(A): pass # E: Cannot extend enum with existing members: "A"
17111715
[builtins fixtures/bool.pyi]
17121716

17131717
[case testEnumFinalSpecialProps]
@@ -1765,12 +1769,12 @@ class B(A):
17651769

17661770
class A1(Enum):
17671771
x: int = 1
1768-
class B1(A1): # E: Cannot inherit from final class "A1"
1772+
class B1(A1): # E: Cannot extend enum with existing members: "A1"
17691773
pass
17701774

17711775
class A2(Enum):
17721776
x = 2
1773-
class B2(A2): # E: Cannot inherit from final class "A2"
1777+
class B2(A2): # E: Cannot extend enum with existing members: "A2"
17741778
pass
17751779

17761780
# We leave this `Final` without a value,
@@ -1788,12 +1792,12 @@ import lib
17881792
from enum import Enum
17891793
class A(Enum):
17901794
x: int
1791-
class B(A): # E: Cannot inherit from final class "A"
1795+
class B(A): # E: Cannot extend enum with existing members: "A"
17921796
x = 1 # E: Cannot override writable attribute "x" with a final one
17931797

17941798
class C(Enum):
17951799
x = 1
1796-
class D(C): # E: Cannot inherit from final class "C"
1800+
class D(C): # E: Cannot extend enum with existing members: "C"
17971801
x: int # E: Cannot assign to final name "x"
17981802
[builtins fixtures/bool.pyi]
17991803

@@ -1811,3 +1815,56 @@ reveal_type(A.int.value) # N: Revealed type is "Literal[1]?"
18111815
reveal_type(A.bool.value) # N: Revealed type is "Literal[False]?"
18121816
reveal_type(A.tuple.value) # N: Revealed type is "Tuple[Literal[1]?]"
18131817
[builtins fixtures/tuple.pyi]
1818+
1819+
[case testFinalWithPrivateAssignment]
1820+
import enum
1821+
class Some(enum.Enum):
1822+
__priv = 1
1823+
1824+
class Other(Some): # Should pass
1825+
pass
1826+
[builtins fixtures/tuple.pyi]
1827+
1828+
[case testFinalWithDunderAssignment]
1829+
import enum
1830+
class Some(enum.Enum):
1831+
__some__ = 1
1832+
1833+
class Other(Some): # Should pass
1834+
pass
1835+
[builtins fixtures/tuple.pyi]
1836+
1837+
[case testFinalWithSunderAssignment]
1838+
import enum
1839+
class Some(enum.Enum):
1840+
_some_ = 1
1841+
1842+
class Other(Some): # Should pass
1843+
pass
1844+
[builtins fixtures/tuple.pyi]
1845+
1846+
[case testFinalWithMethodAssignment]
1847+
import enum
1848+
from typing import overload
1849+
class Some(enum.Enum):
1850+
def lor(self, other) -> bool:
1851+
pass
1852+
1853+
ror = lor
1854+
1855+
class Other(Some): # Should pass
1856+
pass
1857+
1858+
1859+
class WithOverload(enum.IntEnum):
1860+
@overload
1861+
def meth(self, arg: int) -> int: pass
1862+
@overload
1863+
def meth(self, arg: str) -> str: pass
1864+
def meth(self, arg): pass
1865+
1866+
alias = meth
1867+
1868+
class SubWithOverload(WithOverload): # Should pass
1869+
pass
1870+
[builtins fixtures/tuple.pyi]

test-data/unit/check-incremental.test

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5627,9 +5627,9 @@ class FinalEnum(Enum):
56275627
[builtins fixtures/isinstance.pyi]
56285628
[out]
56295629
main:3: error: Cannot override writable attribute "x" with a final one
5630-
main:4: error: Cannot inherit from final class "FinalEnum"
5630+
main:4: error: Cannot extend enum with existing members: "FinalEnum"
56315631
main:5: error: Cannot override final attribute "x" (previously declared in base class "FinalEnum")
56325632
[out2]
56335633
main:3: error: Cannot override writable attribute "x" with a final one
5634-
main:4: error: Cannot inherit from final class "FinalEnum"
5634+
main:4: error: Cannot extend enum with existing members: "FinalEnum"
56355635
main:5: error: Cannot override final attribute "x" (previously declared in base class "FinalEnum")

0 commit comments

Comments
 (0)