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

Fix enum .value with tuple #15637

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
11 changes: 10 additions & 1 deletion mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -2468,6 +2468,10 @@ def format_literal_value(typ: LiteralType) -> str:
if itype.type.fullname == "typing._SpecialForm":
# This is not a real type but used for some typing-related constructs.
return "<typing special form>"
if itype.last_known_value and (
fullnames and f"typing.Literal[{itype.last_known_value.value}]" in fullnames
):
return format(itype.last_known_value)
if itype.type.fullname in reverse_builtin_aliases and not options.use_lowercase_names():
alias = reverse_builtin_aliases[itype.type.fullname]
base_str = alias.split(".")[-1]
Expand Down Expand Up @@ -2652,7 +2656,12 @@ def find_type_overlaps(*types: Type) -> set[str]:
d: dict[str, set[str]] = {}
A5rocks marked this conversation as resolved.
Show resolved Hide resolved
for type in types:
for inst in collect_all_instances(type):
d.setdefault(inst.type.name, set()).add(inst.type.fullname)
if inst.last_known_value:
d.setdefault(inst.type.name, set()).add(
f"typing.Literal[{inst.last_known_value.value}]"
)
else:
d.setdefault(inst.type.name, set()).add(inst.type.fullname)
for shortname in d.keys():
if f"typing.{shortname}" in TYPES_FOR_UNIMPORTED_HINTS:
d[shortname].add(f"typing.{shortname}")
Expand Down
5 changes: 4 additions & 1 deletion mypy/plugins/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,10 @@ def int_neg_callback(ctx: MethodContext) -> Type:
else:
return ctx.type.copy_modified(
last_known_value=LiteralType(
value=-value, fallback=ctx.type, line=ctx.type.line, column=ctx.type.column
value=-value,
fallback=ctx.type.copy_modified(last_known_value=None),
line=ctx.type.line,
column=ctx.type.column,
)
)
elif isinstance(ctx.type, LiteralType):
Expand Down
15 changes: 11 additions & 4 deletions mypy/plugins/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"""
from __future__ import annotations

from typing import Final, Iterable, Sequence, TypeVar, cast
from typing import Final, Iterable, Optional, Sequence, TypeVar, cast

import mypy.plugin # To avoid circular imports.
from mypy.nodes import TypeInfo
Expand Down Expand Up @@ -167,11 +167,11 @@ class SomeEnum:
if n is None or not n.implicit
)
proper_types = list(
_infer_value_type_with_auto_fallback(ctx, t)
get_proper_type(_infer_value_type_with_auto_fallback(ctx, t))
for t in node_types
if t is None or not isinstance(t, CallableType)
)
underlying_type = _first(proper_types)
underlying_type: Optional[Type] = _first(proper_types)
if underlying_type is None:
return ctx.default_attr_type

Expand Down Expand Up @@ -202,7 +202,14 @@ class SomeEnum:
# So, we unify them to make sure `.value` prediction still works.
# Result will be `Literal[1] | Literal[2] | Literal[3]` for this case.
all_equivalent_types = all(
proper_type is not None and is_equivalent(proper_type, underlying_type)
proper_type is not None
and is_equivalent(
proper_type.copy_modified(last_known_value=None)
if isinstance(proper_type, Instance)
# I'm not sure when this is the case:
else proper_type,
underlying_type,
)
for proper_type in proper_types
)
if all_equivalent_types:
Expand Down
2 changes: 2 additions & 0 deletions mypy/subtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,8 @@ def visit_instance(self, left: Instance) -> bool:
# and we can't have circular promotions.
if left.type.alt_promote and left.type.alt_promote.type is right.type:
return True
if left.last_known_value and right.last_known_value:
return self._is_subtype(left.last_known_value, right.last_known_value)
rname = right.type.fullname
# Always try a nominal check if possible,
# there might be errors that a user wants to silence *once*.
Expand Down
2 changes: 1 addition & 1 deletion test-data/unit/check-basic.test
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,7 @@ def bar() -> List[A]:

def baz() -> Union[A, int]:
b = True
return a.A() if b else 10 # E: Incompatible return value type (got "Union[a.A, int]", expected "Union[b.A, int]")
return a.A() if b else 10 # E: Incompatible return value type (got "Union[a.A, Literal[10]]", expected "Union[b.A, builtins.int]")

def spam() -> Optional[A]:
return a.A() # E: Incompatible return value type (got "a.A", expected "Optional[b.A]")
Expand Down
4 changes: 2 additions & 2 deletions test-data/unit/check-class-namedtuple.test
Original file line number Diff line number Diff line change
Expand Up @@ -234,8 +234,8 @@ a = l[0]
(i,) = l[0]
i, i = l[0] # E: Need more than 1 value to unpack (2 expected)
l = [A(1)]
a = (1,) # E: Incompatible types in assignment (expression has type "Tuple[int]", \
variable has type "A")
a = (1,) # E: Incompatible types in assignment (expression has type "Tuple[Literal[1]]", variable has type "A")

[builtins fixtures/list.pyi]

[case testNewNamedTupleMissingClassAttribute]
Expand Down
2 changes: 1 addition & 1 deletion test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ class Base:
pass

class Derived(Base):
__hash__ = 1 # E: Incompatible types in assignment (expression has type "int", base class "Base" defined the type as "Callable[[Base], int]")
__hash__ = 1 # E: Incompatible types in assignment (expression has type "Literal[1]", base class "Base" defined the type as "Callable[[Base], builtins.int]")


[case testOverridePartialAttributeWithMethod]
Expand Down
6 changes: 4 additions & 2 deletions test-data/unit/check-dataclasses.test
Original file line number Diff line number Diff line change
Expand Up @@ -1897,9 +1897,11 @@ class SecondClass:
SECOND_CONST: Final = FirstClass.FIRST_CONST # E: Need type argument for Final[...] with non-literal default in dataclass

reveal_type(FirstClass().FIRST_CONST) # N: Revealed type is "Literal[3]?"
FirstClass().FIRST_CONST = 42 # E: Cannot assign to final attribute "FIRST_CONST"
FirstClass().FIRST_CONST = 42 # E: Cannot assign to final attribute "FIRST_CONST" \
# E: Incompatible types in assignment (expression has type "Literal[42]", variable has type "Literal[3]")
reveal_type(SecondClass().SECOND_CONST) # N: Revealed type is "Literal[3]?"
SecondClass().SECOND_CONST = 42 # E: Cannot assign to final attribute "SECOND_CONST"
SecondClass().SECOND_CONST = 42 # E: Cannot assign to final attribute "SECOND_CONST" \
# E: Incompatible types in assignment (expression has type "Literal[42]", variable has type "Literal[3]")
[builtins fixtures/dataclasses.pyi]

[case testDataclassFieldsProtocol]
Expand Down
28 changes: 23 additions & 5 deletions test-data/unit/check-enum.test
Original file line number Diff line number Diff line change
Expand Up @@ -1427,7 +1427,8 @@ class Medal(Enum):
silver = 2

# Another value:
Medal.gold = 0 # E: Cannot assign to final attribute "gold"
Medal.gold = 0 # E: Cannot assign to final attribute "gold" \
# E: Incompatible types in assignment (expression has type "Literal[0]", variable has type "Literal[1]")
# Same value:
Medal.silver = 2 # E: Cannot assign to final attribute "silver"

Expand All @@ -1453,10 +1454,13 @@ reveal_type(Foo.A.value) # N: Revealed type is "Literal[1]?"

class Bar(Enum):
A = 1
B = A = 2 # E: Attempted to reuse member name "A" in Enum definition "Bar"
B = A = 2 # E: Attempted to reuse member name "A" in Enum definition "Bar" \
# E: Incompatible types in assignment (expression has type "Literal[2]", variable has type "Literal[1]")
class Baz(Enum):
A = 1
B, A = (1, 2) # E: Attempted to reuse member name "A" in Enum definition "Baz"
B, A = (1, 2) # E: Attempted to reuse member name "A" in Enum definition "Baz" \
# E: Incompatible types in assignment (expression has type "Literal[2]", variable has type "Literal[1]")

[builtins fixtures/tuple.pyi]

[case testEnumReusedKeysOverlapWithLocalVar]
Expand Down Expand Up @@ -1764,8 +1768,10 @@ class EI(IntEnum):
__annotations__ = {'a': int}
__dict__ = {'a': 1}

E._order_ = 'a' # E: Cannot assign to final attribute "_order_"
EI.value = 2 # E: Cannot assign to final attribute "value"
E._order_ = 'a' # E: Cannot assign to final attribute "_order_" \
# E: Incompatible types in assignment (expression has type "Literal['a']", variable has type "Literal['X Y']")
EI.value = 2 # E: Cannot assign to final attribute "value" \
# E: Incompatible types in assignment (expression has type "Literal[2]", variable has type "Literal[1]")
[builtins fixtures/dict.pyi]

[case testEnumNotFinalWithMethodsAndUninitializedValues]
Expand Down Expand Up @@ -2125,3 +2131,15 @@ class AllPartialList(Enum):

def check(self) -> None:
reveal_type(self.value) # N: Revealed type is "builtins.list[Any]"

[case testEnumValueWithTupleType]
import enum
from typing import Tuple

class Color(enum.Enum):
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)

x: Color
reveal_type(x.value) # N: Revealed type is "Any"
[builtins fixtures/tuple.pyi]
2 changes: 1 addition & 1 deletion test-data/unit/check-expressions.test
Original file line number Diff line number Diff line change
Expand Up @@ -1659,7 +1659,7 @@ dict(undefined) # E: Name "undefined" is not defined
from typing import Dict
d = dict([(1, 'x'), (2, 'y')])
d() # E: "Dict[int, str]" not callable
d2 = dict([(1, 'x')]) # type: Dict[str, str] # E: List item 0 has incompatible type "Tuple[int, str]"; expected "Tuple[str, str]"
d2 = dict([(1, 'x')]) # type: Dict[str, str] # E: List item 0 has incompatible type "Tuple[int, Literal['x']]"; expected "Tuple[builtins.str, builtins.str]"
[builtins fixtures/dict.pyi]

[case testDictFromIterableAndKeywordArg]
Expand Down
Loading