Skip to content

Commit

Permalink
Move final detection for Enum in checker.py (#11984)
Browse files Browse the repository at this point in the history
Fixes #11850
  • Loading branch information
sobolevn committed Jan 18, 2022
1 parent 002e309 commit 0a03ba0
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 71 deletions.
47 changes: 44 additions & 3 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@
Context, Decorator, PrintStmt, BreakStmt, PassStmt, ContinueStmt,
ComparisonExpr, StarExpr, EllipsisExpr, RefExpr, PromoteExpr,
Import, ImportFrom, ImportAll, ImportBase, TypeAlias,
ARG_POS, ARG_STAR, LITERAL_TYPE, LDEF, MDEF, GDEF,
ARG_POS, ARG_STAR, ARG_NAMED, LITERAL_TYPE, LDEF, MDEF, GDEF,
CONTRAVARIANT, COVARIANT, INVARIANT, TypeVarExpr, AssignmentExpr,
is_final_node, ARG_NAMED, MatchStmt)
is_final_node, MatchStmt)
from mypy import nodes
from mypy import operators
from mypy.literals import literal, literal_hash, Key
Expand Down Expand Up @@ -86,7 +86,7 @@
from mypy import state, errorcodes as codes
from mypy.traverser import has_return_statement, all_return_statements
from mypy.errorcodes import ErrorCode
from mypy.util import is_typeshed_file
from mypy.util import is_typeshed_file, is_dunder, is_sunder

T = TypeVar('T')

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

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

def check_final_enum(self, defn: ClassDef, base: TypeInfo) -> None:
for sym in base.names.values():
if self.is_final_enum_value(sym):
self.fail(
'Cannot extend enum with existing members: "{}"'.format(
base.name,
),
defn,
)
break

def is_final_enum_value(self, sym: SymbolTableNode) -> bool:
if isinstance(sym.node, (FuncBase, Decorator)):
return False # A method is fine
if not isinstance(sym.node, Var):
return True # Can be a class or anything else

# Now, only `Var` is left, we need to check:
# 1. Private name like in `__prop = 1`
# 2. Dunder name like `__hash__ = some_hasher`
# 3. Sunder name like `_order_ = 'a, b, c'`
# 4. If it is a method / descriptor like in `method = classmethod(func)`
if (
is_private(sym.node.name)
or is_dunder(sym.node.name)
or is_sunder(sym.node.name)
# TODO: make sure that `x = @class/staticmethod(func)`
# and `x = property(prop)` both work correctly.
# Now they are incorrectly counted as enum members.
or isinstance(get_proper_type(sym.node.type), FunctionLike)
):
return False

if self.is_stub or sym.node.has_explicit_value:
return True
return False

def check_protocol_variance(self, defn: ClassDef) -> None:
"""Check that protocol definition is compatible with declared
variances of type variables.
Expand Down
34 changes: 4 additions & 30 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,12 @@
REVEAL_LOCALS, is_final_node, TypedDictExpr, type_aliases_source_versions,
typing_extensions_aliases,
EnumCallExpr, RUNTIME_PROTOCOL_DECOS, FakeExpression, Statement, AssignmentExpr,
ParamSpecExpr, EllipsisExpr, TypeVarLikeExpr, FuncBase, implicit_module_attrs,
MatchStmt
ParamSpecExpr, EllipsisExpr, TypeVarLikeExpr, implicit_module_attrs,
MatchStmt,
)
from mypy.patterns import (
AsPattern, OrPattern, ValuePattern, SequencePattern,
StarredPattern, MappingPattern, ClassPattern
StarredPattern, MappingPattern, ClassPattern,
)
from mypy.tvar_scope import TypeVarLikeScope
from mypy.typevars import fill_typevars
Expand Down Expand Up @@ -123,7 +123,7 @@
)
from mypy.semanal_namedtuple import NamedTupleAnalyzer
from mypy.semanal_typeddict import TypedDictAnalyzer
from mypy.semanal_enum import EnumCallAnalyzer, ENUM_BASES
from mypy.semanal_enum import EnumCallAnalyzer
from mypy.semanal_newtype import NewTypeAnalyzer
from mypy.reachability import (
infer_reachability_of_if_statement, infer_reachability_of_match_statement,
Expand Down Expand Up @@ -1554,13 +1554,6 @@ def configure_base_classes(self,
elif isinstance(base, Instance):
if base.type.is_newtype:
self.fail('Cannot subclass "NewType"', defn)
if self.enum_has_final_values(base):
# This means that are trying to subclass a non-default
# Enum class, with defined members. This is not possible.
# In runtime, it will raise. We need to mark this type as final.
# However, methods can be defined on a type: only values can't.
# We also don't count values with annotations only.
base.type.is_final = True
base_types.append(base)
elif isinstance(base, AnyType):
if self.options.disallow_subclassing_any:
Expand Down Expand Up @@ -1598,25 +1591,6 @@ def configure_base_classes(self,
return
self.calculate_class_mro(defn, self.object_type)

def enum_has_final_values(self, base: Instance) -> bool:
if (
base.type.is_enum
and base.type.fullname not in ENUM_BASES
and base.type.names
and base.type.defn
):
for sym in base.type.names.values():
if isinstance(sym.node, (FuncBase, Decorator)):
continue # A method
if not isinstance(sym.node, Var):
return True # Can be a class
if self.is_stub_file or sym.node.has_explicit_value:
# Corner case: assignments like `x: int` are fine in `.py` files.
# But, not is `.pyi` files, because we don't know
# if there's aactually a value or not.
return True
return False

def configure_tuple_base_class(self,
defn: ClassDef,
base: TupleType,
Expand Down
16 changes: 1 addition & 15 deletions mypy/stubtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from mypy import nodes
from mypy.config_parser import parse_config_file
from mypy.options import Options
from mypy.util import FancyFormatter, bytes_to_human_readable_repr
from mypy.util import FancyFormatter, bytes_to_human_readable_repr, is_dunder, SPECIAL_DUNDERS


class Missing:
Expand Down Expand Up @@ -899,20 +899,6 @@ def verify_typealias(
)


SPECIAL_DUNDERS = ("__init__", "__new__", "__call__", "__init_subclass__", "__class_getitem__")


def is_dunder(name: str, exclude_special: bool = False) -> bool:
"""Returns whether name is a dunder name.
:param exclude_special: Whether to return False for a couple special dunder methods.
"""
if exclude_special and name in SPECIAL_DUNDERS:
return False
return name.startswith("__") and name.endswith("__")


def is_probably_a_function(runtime: Any) -> bool:
return (
isinstance(runtime, (types.FunctionType, types.BuiltinFunctionType))
Expand Down
20 changes: 20 additions & 0 deletions mypy/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,26 @@
"C:\\Python27\\python.exe",
]

SPECIAL_DUNDERS: Final = frozenset((
"__init__", "__new__", "__call__", "__init_subclass__", "__class_getitem__",
))


def is_dunder(name: str, exclude_special: bool = False) -> bool:
"""Returns whether name is a dunder name.
Args:
exclude_special: Whether to return False for a couple special dunder methods.
"""
if exclude_special and name in SPECIAL_DUNDERS:
return False
return name.startswith("__") and name.endswith("__")


def is_sunder(name: str) -> bool:
return not is_dunder(name) and name.startswith('_') and name.endswith('_')


def split_module_names(mod_name: str) -> List[str]:
"""Return the module and all parent module names.
Expand Down
99 changes: 78 additions & 21 deletions test-data/unit/check-enum.test
Original file line number Diff line number Diff line change
Expand Up @@ -1469,22 +1469,22 @@ class NonEmptyFlag(Flag):
class NonEmptyIntFlag(IntFlag):
x = 1

class ErrorEnumWithValue(NonEmptyEnum): # E: Cannot inherit from final class "NonEmptyEnum"
class ErrorEnumWithValue(NonEmptyEnum): # E: Cannot extend enum with existing members: "NonEmptyEnum"
x = 1 # E: Cannot override final attribute "x" (previously declared in base class "NonEmptyEnum")
class ErrorIntEnumWithValue(NonEmptyIntEnum): # E: Cannot inherit from final class "NonEmptyIntEnum"
class ErrorIntEnumWithValue(NonEmptyIntEnum): # E: Cannot extend enum with existing members: "NonEmptyIntEnum"
x = 1 # E: Cannot override final attribute "x" (previously declared in base class "NonEmptyIntEnum")
class ErrorFlagWithValue(NonEmptyFlag): # E: Cannot inherit from final class "NonEmptyFlag"
class ErrorFlagWithValue(NonEmptyFlag): # E: Cannot extend enum with existing members: "NonEmptyFlag"
x = 1 # E: Cannot override final attribute "x" (previously declared in base class "NonEmptyFlag")
class ErrorIntFlagWithValue(NonEmptyIntFlag): # E: Cannot inherit from final class "NonEmptyIntFlag"
class ErrorIntFlagWithValue(NonEmptyIntFlag): # E: Cannot extend enum with existing members: "NonEmptyIntFlag"
x = 1 # E: Cannot override final attribute "x" (previously declared in base class "NonEmptyIntFlag")

class ErrorEnumWithoutValue(NonEmptyEnum): # E: Cannot inherit from final class "NonEmptyEnum"
class ErrorEnumWithoutValue(NonEmptyEnum): # E: Cannot extend enum with existing members: "NonEmptyEnum"
pass
class ErrorIntEnumWithoutValue(NonEmptyIntEnum): # E: Cannot inherit from final class "NonEmptyIntEnum"
class ErrorIntEnumWithoutValue(NonEmptyIntEnum): # E: Cannot extend enum with existing members: "NonEmptyIntEnum"
pass
class ErrorFlagWithoutValue(NonEmptyFlag): # E: Cannot inherit from final class "NonEmptyFlag"
class ErrorFlagWithoutValue(NonEmptyFlag): # E: Cannot extend enum with existing members: "NonEmptyFlag"
pass
class ErrorIntFlagWithoutValue(NonEmptyIntFlag): # E: Cannot inherit from final class "NonEmptyIntFlag"
class ErrorIntFlagWithoutValue(NonEmptyIntFlag): # E: Cannot extend enum with existing members: "NonEmptyIntFlag"
pass
[builtins fixtures/bool.pyi]

Expand Down Expand Up @@ -1588,13 +1588,17 @@ class NonEmptyIntFlag(IntFlag):
class NonEmptyEnumMeta(EnumMeta):
x = 1

class ErrorEnumWithoutValue(NonEmptyEnum): # E: Cannot inherit from final class "NonEmptyEnum"
class ErrorEnumWithoutValue(NonEmptyEnum): # E: Cannot inherit from final class "NonEmptyEnum" \
# E: Cannot extend enum with existing members: "NonEmptyEnum"
pass
class ErrorIntEnumWithoutValue(NonEmptyIntEnum): # E: Cannot inherit from final class "NonEmptyIntEnum"
class ErrorIntEnumWithoutValue(NonEmptyIntEnum): # E: Cannot inherit from final class "NonEmptyIntEnum" \
# E: Cannot extend enum with existing members: "NonEmptyIntEnum"
pass
class ErrorFlagWithoutValue(NonEmptyFlag): # E: Cannot inherit from final class "NonEmptyFlag"
class ErrorFlagWithoutValue(NonEmptyFlag): # E: Cannot inherit from final class "NonEmptyFlag" \
# E: Cannot extend enum with existing members: "NonEmptyFlag"
pass
class ErrorIntFlagWithoutValue(NonEmptyIntFlag): # E: Cannot inherit from final class "NonEmptyIntFlag"
class ErrorIntFlagWithoutValue(NonEmptyIntFlag): # E: Cannot inherit from final class "NonEmptyIntFlag" \
# E: Cannot extend enum with existing members: "NonEmptyIntFlag"
pass
class ErrorEnumMetaWithoutValue(NonEmptyEnumMeta): # E: Cannot inherit from final class "NonEmptyEnumMeta"
pass
Expand Down Expand Up @@ -1692,13 +1696,13 @@ class NonEmptyIntFlag(IntFlag):
x = 1
def method(self) -> None: pass

class ErrorEnumWithoutValue(NonEmptyEnum): # E: Cannot inherit from final class "NonEmptyEnum"
class ErrorEnumWithoutValue(NonEmptyEnum): # E: Cannot extend enum with existing members: "NonEmptyEnum"
pass
class ErrorIntEnumWithoutValue(NonEmptyIntEnum): # E: Cannot inherit from final class "NonEmptyIntEnum"
class ErrorIntEnumWithoutValue(NonEmptyIntEnum): # E: Cannot extend enum with existing members: "NonEmptyIntEnum"
pass
class ErrorFlagWithoutValue(NonEmptyFlag): # E: Cannot inherit from final class "NonEmptyFlag"
class ErrorFlagWithoutValue(NonEmptyFlag): # E: Cannot extend enum with existing members: "NonEmptyFlag"
pass
class ErrorIntFlagWithoutValue(NonEmptyIntFlag): # E: Cannot inherit from final class "NonEmptyIntFlag"
class ErrorIntFlagWithoutValue(NonEmptyIntFlag): # E: Cannot extend enum with existing members: "NonEmptyIntFlag"
pass
[builtins fixtures/bool.pyi]

Expand All @@ -1707,7 +1711,7 @@ from enum import Enum

class A(Enum):
class Inner: pass
class B(A): pass # E: Cannot inherit from final class "A"
class B(A): pass # E: Cannot extend enum with existing members: "A"
[builtins fixtures/bool.pyi]

[case testEnumFinalSpecialProps]
Expand Down Expand Up @@ -1765,12 +1769,12 @@ class B(A):

class A1(Enum):
x: int = 1
class B1(A1): # E: Cannot inherit from final class "A1"
class B1(A1): # E: Cannot extend enum with existing members: "A1"
pass

class A2(Enum):
x = 2
class B2(A2): # E: Cannot inherit from final class "A2"
class B2(A2): # E: Cannot extend enum with existing members: "A2"
pass

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

class C(Enum):
x = 1
class D(C): # E: Cannot inherit from final class "C"
class D(C): # E: Cannot extend enum with existing members: "C"
x: int # E: Cannot assign to final name "x"
[builtins fixtures/bool.pyi]

Expand All @@ -1811,3 +1815,56 @@ reveal_type(A.int.value) # N: Revealed type is "Literal[1]?"
reveal_type(A.bool.value) # N: Revealed type is "Literal[False]?"
reveal_type(A.tuple.value) # N: Revealed type is "Tuple[Literal[1]?]"
[builtins fixtures/tuple.pyi]

[case testFinalWithPrivateAssignment]
import enum
class Some(enum.Enum):
__priv = 1

class Other(Some): # Should pass
pass
[builtins fixtures/tuple.pyi]

[case testFinalWithDunderAssignment]
import enum
class Some(enum.Enum):
__some__ = 1

class Other(Some): # Should pass
pass
[builtins fixtures/tuple.pyi]

[case testFinalWithSunderAssignment]
import enum
class Some(enum.Enum):
_some_ = 1

class Other(Some): # Should pass
pass
[builtins fixtures/tuple.pyi]

[case testFinalWithMethodAssignment]
import enum
from typing import overload
class Some(enum.Enum):
def lor(self, other) -> bool:
pass

ror = lor

class Other(Some): # Should pass
pass


class WithOverload(enum.IntEnum):
@overload
def meth(self, arg: int) -> int: pass
@overload
def meth(self, arg: str) -> str: pass
def meth(self, arg): pass

alias = meth

class SubWithOverload(WithOverload): # Should pass
pass
[builtins fixtures/tuple.pyi]
4 changes: 2 additions & 2 deletions test-data/unit/check-incremental.test
Original file line number Diff line number Diff line change
Expand Up @@ -5627,9 +5627,9 @@ class FinalEnum(Enum):
[builtins fixtures/isinstance.pyi]
[out]
main:3: error: Cannot override writable attribute "x" with a final one
main:4: error: Cannot inherit from final class "FinalEnum"
main:4: error: Cannot extend enum with existing members: "FinalEnum"
main:5: error: Cannot override final attribute "x" (previously declared in base class "FinalEnum")
[out2]
main:3: error: Cannot override writable attribute "x" with a final one
main:4: error: Cannot inherit from final class "FinalEnum"
main:4: error: Cannot extend enum with existing members: "FinalEnum"
main:5: error: Cannot override final attribute "x" (previously declared in base class "FinalEnum")

0 comments on commit 0a03ba0

Please sign in to comment.