diff --git a/mypy/checker.py b/mypy/checker.py index 6f8ab758cc94..2f99b9b4fece 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -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 @@ -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') @@ -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 @@ -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. diff --git a/mypy/semanal.py b/mypy/semanal.py index 7b9348eb21e8..a7c5eda732af 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -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 @@ -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, @@ -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: @@ -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, diff --git a/mypy/stubtest.py b/mypy/stubtest.py index cb005c4bd52c..d0f62dfc3ed2 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -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: @@ -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)) diff --git a/mypy/util.py b/mypy/util.py index 9f620e823e91..cd1e15afb4cf 100644 --- a/mypy/util.py +++ b/mypy/util.py @@ -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. diff --git a/test-data/unit/check-enum.test b/test-data/unit/check-enum.test index aac97c6c69af..2af57da9cbdc 100644 --- a/test-data/unit/check-enum.test +++ b/test-data/unit/check-enum.test @@ -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] @@ -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 @@ -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] @@ -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] @@ -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, @@ -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] @@ -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] diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index ec1dff4977d4..1a73f9f33274 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -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")