Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move final detection for Enum in checker.py #11984

Merged
merged 6 commits into from
Jan 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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]
sobolevn marked this conversation as resolved.
Show resolved Hide resolved
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")