Skip to content

Commit

Permalink
Allow iterable class objects to be unpacked (including enums) (#14827)
Browse files Browse the repository at this point in the history
Fixes #14782

Currently, mypy issues a false positive if you try to unpack an enum
class:

```python
from enum import Enum

class E(Enum):
    A = 1
    B = 2

A, B = E  #  error: "Type[E]" object is not iterable  [misc]
```

This is because of a more general problem with class objects that have
`__iter__` defined on their metaclass. Mypy issues a false positive on
this code, where `Foo` is iterable by virtue of having `Meta` as its
metaclass:

```python
from typing import Iterator
class Meta(type):
    def __iter__(cls) -> Iterator[int]:
        yield from [1, 2, 3]

class Foo(metaclass=Meta): ...

a, b, c = Foo  # error: "Type[Foo]" object is not iterable  [misc]
reveal_type(a)  # error: Cannot determine type of "a"  [has-type]  # note: Revealed type is "Any"
```

This PR fixes the false positive with enums, and the more general false
positive with iterable class objects.
  • Loading branch information
AlexWaygood authored Mar 6, 2023
1 parent 31f70d7 commit b6cb0ed
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 9 deletions.
21 changes: 12 additions & 9 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -3632,7 +3632,9 @@ def check_multi_assignment_from_iterable(
infer_lvalue_type: bool = True,
) -> None:
rvalue_type = get_proper_type(rvalue_type)
if self.type_is_iterable(rvalue_type) and isinstance(rvalue_type, Instance):
if self.type_is_iterable(rvalue_type) and isinstance(
rvalue_type, (Instance, CallableType, TypeType, Overloaded)
):
item_type = self.iterable_item_type(rvalue_type)
for lv in lvalues:
if isinstance(lv, StarExpr):
Expand Down Expand Up @@ -6387,15 +6389,16 @@ def note(
return
self.msg.note(msg, context, offset=offset, code=code)

def iterable_item_type(self, instance: Instance) -> Type:
iterable = map_instance_to_supertype(instance, self.lookup_typeinfo("typing.Iterable"))
item_type = iterable.args[0]
if not isinstance(get_proper_type(item_type), AnyType):
# This relies on 'map_instance_to_supertype' returning 'Iterable[Any]'
# in case there is no explicit base class.
return item_type
def iterable_item_type(self, it: Instance | CallableType | TypeType | Overloaded) -> Type:
if isinstance(it, Instance):
iterable = map_instance_to_supertype(it, self.lookup_typeinfo("typing.Iterable"))
item_type = iterable.args[0]
if not isinstance(get_proper_type(item_type), AnyType):
# This relies on 'map_instance_to_supertype' returning 'Iterable[Any]'
# in case there is no explicit base class.
return item_type
# Try also structural typing.
return self.analyze_iterable_item_type_without_expression(instance, instance)[1]
return self.analyze_iterable_item_type_without_expression(it, it)[1]

def function_type(self, func: FuncBase) -> FunctionLike:
return function_type(func, self.named_type("builtins.function"))
Expand Down
114 changes: 114 additions & 0 deletions test-data/unit/check-inference.test
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,120 @@ def f() -> None:
class A: pass
[out]

[case testClassObjectsNotUnpackableWithoutIterableMetaclass]
from typing import Type

class Foo: ...
A: Type[Foo] = Foo
a, b = Foo # E: "Type[Foo]" object is not iterable
c, d = A # E: "Type[Foo]" object is not iterable

class Meta(type): ...
class Bar(metaclass=Meta): ...
B: Type[Bar] = Bar
e, f = Bar # E: "Type[Bar]" object is not iterable
g, h = B # E: "Type[Bar]" object is not iterable

reveal_type(a) # E: Cannot determine type of "a" # N: Revealed type is "Any"
reveal_type(b) # E: Cannot determine type of "b" # N: Revealed type is "Any"
reveal_type(c) # E: Cannot determine type of "c" # N: Revealed type is "Any"
reveal_type(d) # E: Cannot determine type of "d" # N: Revealed type is "Any"
reveal_type(e) # E: Cannot determine type of "e" # N: Revealed type is "Any"
reveal_type(f) # E: Cannot determine type of "f" # N: Revealed type is "Any"
reveal_type(g) # E: Cannot determine type of "g" # N: Revealed type is "Any"
reveal_type(h) # E: Cannot determine type of "h" # N: Revealed type is "Any"
[out]

[case testInferringLvarTypesUnpackedFromIterableClassObject]
from typing import Iterator, Type, TypeVar, Union, overload
class Meta(type):
def __iter__(cls) -> Iterator[int]:
yield from [1, 2, 3]

class Meta2(type):
def __iter__(cls) -> Iterator[str]:
yield from ["foo", "bar", "baz"]

class Meta3(type): ...

class Foo(metaclass=Meta): ...
class Bar(metaclass=Meta2): ...
class Baz(metaclass=Meta3): ...
class Spam: ...

class Eggs(metaclass=Meta):
@overload
def __init__(self, x: int) -> None: ...
@overload
def __init__(self, x: int, y: int, z: int) -> None: ...
def __init__(self, x: int, y: int = ..., z: int = ...) -> None: ...

A: Type[Foo] = Foo
B: Type[Union[Foo, Bar]] = Foo
C: Union[Type[Foo], Type[Bar]] = Foo
D: Type[Union[Foo, Baz]] = Foo
E: Type[Union[Foo, Spam]] = Foo
F: Type[Eggs] = Eggs
G: Type[Union[Foo, Eggs]] = Foo

a, b, c = Foo
d, e, f = A
g, h, i = B
j, k, l = C
m, n, o = D # E: "Type[Baz]" object is not iterable
p, q, r = E # E: "Type[Spam]" object is not iterable
s, t, u = Eggs
v, w, x = F
y, z, aa = G

for var in [a, b, c, d, e, f, s, t, u, v, w, x, y, z, aa]:
reveal_type(var) # N: Revealed type is "builtins.int"

for var2 in [g, h, i, j, k, l]:
reveal_type(var2) # N: Revealed type is "Union[builtins.int, builtins.str]"

for var3 in [m, n, o, p, q, r]:
reveal_type(var3) # N: Revealed type is "Union[builtins.int, Any]"

T = TypeVar("T", bound=Type[Foo])

def check(x: T) -> T:
a, b, c = x
for var in [a, b, c]:
reveal_type(var) # N: Revealed type is "builtins.int"
return x

T2 = TypeVar("T2", bound=Type[Union[Foo, Bar]])

def check2(x: T2) -> T2:
a, b, c = x
for var in [a, b, c]:
reveal_type(var) # N: Revealed type is "Union[builtins.int, builtins.str]"
return x

T3 = TypeVar("T3", bound=Union[Type[Foo], Type[Bar]])

def check3(x: T3) -> T3:
a, b, c = x
for var in [a, b, c]:
reveal_type(var) # N: Revealed type is "Union[builtins.int, builtins.str]"
return x
[out]

[case testInferringLvarTypesUnpackedFromIterableClassObjectWithGenericIter]
from typing import Iterator, Type, TypeVar

T = TypeVar("T")
class Meta(type):
def __iter__(self: Type[T]) -> Iterator[T]: ...
class Foo(metaclass=Meta): ...

A, B, C = Foo
reveal_type(A) # N: Revealed type is "__main__.Foo"
reveal_type(B) # N: Revealed type is "__main__.Foo"
reveal_type(C) # N: Revealed type is "__main__.Foo"
[out]

[case testInferringLvarTypesInMultiDefWithInvalidTuple]
from typing import Tuple
t = None # type: Tuple[object, object, object]
Expand Down
17 changes: 17 additions & 0 deletions test-data/unit/pythoneval.test
Original file line number Diff line number Diff line change
Expand Up @@ -1878,6 +1878,23 @@ _testEnumIterMetaInference.py:8: note: Revealed type is "typing.Iterator[_E`-1]"
_testEnumIterMetaInference.py:9: note: Revealed type is "_E`-1"
_testEnumIterMetaInference.py:13: note: Revealed type is "socket.SocketKind"

[case testEnumUnpackedViaMetaclass]
from enum import Enum

class FooEnum(Enum):
A = 1
B = 2
C = 3

a, b, c = FooEnum
reveal_type(a)
reveal_type(b)
reveal_type(c)
[out]
_testEnumUnpackedViaMetaclass.py:9: note: Revealed type is "_testEnumUnpackedViaMetaclass.FooEnum"
_testEnumUnpackedViaMetaclass.py:10: note: Revealed type is "_testEnumUnpackedViaMetaclass.FooEnum"
_testEnumUnpackedViaMetaclass.py:11: note: Revealed type is "_testEnumUnpackedViaMetaclass.FooEnum"

[case testNativeIntTypes]
# Spot check various native int operations with full stubs.
from mypy_extensions import i64, i32
Expand Down

0 comments on commit b6cb0ed

Please sign in to comment.