Skip to content

Commit

Permalink
Special cases Enum and some special props to be non-final, refs #11699
Browse files Browse the repository at this point in the history
 (#11713)

Closes #11699
  • Loading branch information
sobolevn authored Dec 13, 2021
1 parent 5f7a3f1 commit 1970029
Show file tree
Hide file tree
Showing 6 changed files with 50 additions and 17 deletions.
6 changes: 4 additions & 2 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
type_object_type,
analyze_decorator_or_funcbase_access,
)
from mypy.semanal_enum import ENUM_BASES, ENUM_SPECIAL_PROPS
from mypy.typeops import (
map_type_from_supertype, bind_self, erase_to_bound, make_simplified_union,
erase_def_to_union_or_bound, erase_to_union_or_bound, coerce_to_literal,
Expand Down Expand Up @@ -2522,13 +2523,14 @@ def check_compatibility_final_super(self, node: Var,
self.msg.cant_override_final(node.name, base.name, node)
return False
if node.is_final:
if base.fullname in ENUM_BASES and node.name in ENUM_SPECIAL_PROPS:
return True
self.check_if_final_var_override_writable(node.name, base_node, node)
return True

def check_if_final_var_override_writable(self,
name: str,
base_node:
Optional[Node],
base_node: Optional[Node],
ctx: Context) -> None:
"""Check that a final variable doesn't override writeable attribute.
Expand Down
5 changes: 2 additions & 3 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
has_any_from_unimported_type, check_for_explicit_any, set_any_tvars, expand_type_alias,
make_optional_type,
)
from mypy.semanal_enum import ENUM_BASES
from mypy.types import (
Type, AnyType, CallableType, Overloaded, NoneType, TypeVarType,
TupleType, TypedDictType, Instance, ErasedType, UnionType,
Expand Down Expand Up @@ -1000,9 +1001,7 @@ def check_callable_call(self,
ret_type = get_proper_type(callee.ret_type)
if callee.is_type_obj() and isinstance(ret_type, Instance):
callable_name = ret_type.type.fullname
if (isinstance(callable_node, RefExpr)
and callable_node.fullname in ('enum.Enum', 'enum.IntEnum',
'enum.Flag', 'enum.IntFlag')):
if isinstance(callable_node, RefExpr) and callable_node.fullname in ENUM_BASES:
# An Enum() call that failed SemanticAnalyzerPass2.check_enum_call().
return callee.ret_type, callee

Expand Down
12 changes: 5 additions & 7 deletions mypy/plugins/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,13 @@
from mypy.typeops import make_simplified_union
from mypy.nodes import TypeInfo
from mypy.subtypes import is_equivalent
from mypy.semanal_enum import ENUM_BASES

# Note: 'enum.EnumMeta' is deliberately excluded from this list. Classes that directly use
# enum.EnumMeta do not necessarily automatically have the 'name' and 'value' attributes.
ENUM_PREFIXES: Final = {"enum.Enum", "enum.IntEnum", "enum.Flag", "enum.IntFlag"}
ENUM_NAME_ACCESS: Final = {"{}.name".format(prefix) for prefix in ENUM_PREFIXES} | {
"{}._name_".format(prefix) for prefix in ENUM_PREFIXES
ENUM_NAME_ACCESS: Final = {"{}.name".format(prefix) for prefix in ENUM_BASES} | {
"{}._name_".format(prefix) for prefix in ENUM_BASES
}
ENUM_VALUE_ACCESS: Final = {"{}.value".format(prefix) for prefix in ENUM_PREFIXES} | {
"{}._value_".format(prefix) for prefix in ENUM_PREFIXES
ENUM_VALUE_ACCESS: Final = {"{}.value".format(prefix) for prefix in ENUM_BASES} | {
"{}._value_".format(prefix) for prefix in ENUM_BASES
}


Expand Down
5 changes: 2 additions & 3 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@
)
from mypy.semanal_namedtuple import NamedTupleAnalyzer
from mypy.semanal_typeddict import TypedDictAnalyzer
from mypy.semanal_enum import EnumCallAnalyzer
from mypy.semanal_enum import EnumCallAnalyzer, ENUM_BASES
from mypy.semanal_newtype import NewTypeAnalyzer
from mypy.reachability import (
infer_reachability_of_if_statement, infer_condition_value, ALWAYS_FALSE, ALWAYS_TRUE,
Expand Down Expand Up @@ -1554,8 +1554,7 @@ def configure_base_classes(self,
self.fail('Cannot subclass "NewType"', defn)
if (
base.type.is_enum
and base.type.fullname not in (
'enum.Enum', 'enum.IntEnum', 'enum.Flag', 'enum.IntFlag')
and base.type.fullname not in ENUM_BASES
and base.type.names
and any(not isinstance(n.node, (FuncBase, Decorator))
for n in base.type.names.values())
Expand Down
12 changes: 11 additions & 1 deletion mypy/semanal_enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"""

from typing import List, Tuple, Optional, Union, cast
from typing_extensions import Final

from mypy.nodes import (
Expression, Context, TypeInfo, AssignmentStmt, NameExpr, CallExpr, RefExpr, StrExpr,
Expand All @@ -13,6 +14,15 @@
from mypy.semanal_shared import SemanticAnalyzerInterface
from mypy.options import Options

# Note: 'enum.EnumMeta' is deliberately excluded from this list. Classes that directly use
# enum.EnumMeta do not necessarily automatically have the 'name' and 'value' attributes.
ENUM_BASES: Final = frozenset((
'enum.Enum', 'enum.IntEnum', 'enum.Flag', 'enum.IntFlag',
))
ENUM_SPECIAL_PROPS: Final = frozenset((
'name', 'value', '_name_', '_value_', '_order_', '__order__',
))


class EnumCallAnalyzer:
def __init__(self, options: Options, api: SemanticAnalyzerInterface) -> None:
Expand Down Expand Up @@ -62,7 +72,7 @@ class A(enum.Enum):
if not isinstance(callee, RefExpr):
return None
fullname = callee.fullname
if fullname not in ('enum.Enum', 'enum.IntEnum', 'enum.Flag', 'enum.IntFlag'):
if fullname not in ENUM_BASES:
return None
items, values, ok = self.parse_enum_call_args(call, fullname.split('.')[-1])
if not ok:
Expand Down
27 changes: 26 additions & 1 deletion test-data/unit/check-enum.test
Original file line number Diff line number Diff line change
Expand Up @@ -1392,7 +1392,7 @@ Medal.silver = 2 # E: Cannot assign to final attribute "silver"
from enum import Enum
class Types(Enum):
key = 0
value = 1 # E: Cannot override writable attribute "value" with a final one
value = 1


[case testEnumReusedKeys]
Expand Down Expand Up @@ -1677,3 +1677,28 @@ class A(Enum):
class Inner: pass
class B(A): pass # E: Cannot inherit from final class "A"
[builtins fixtures/bool.pyi]


[case testEnumFinalSpecialProps]
# https://github.com/python/mypy/issues/11699
from enum import Enum, IntEnum

class E(Enum):
name = 'a'
value = 'b'
_name_ = 'a1'
_value_ = 'b2'
_order_ = 'X Y'
__order__ = 'X Y'

class EI(IntEnum):
name = 'a'
value = 1
_name_ = 'a1'
_value_ = 2
_order_ = 'X Y'
__order__ = 'X Y'

E._order_ = 'a' # E: Cannot assign to final attribute "_order_"
EI.value = 2 # E: Cannot assign to final attribute "value"
[builtins fixtures/bool.pyi]

0 comments on commit 1970029

Please sign in to comment.