Skip to content

Commit b6cb0ed

Browse files
authored
Allow iterable class objects to be unpacked (including enums) (#14827)
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.
1 parent 31f70d7 commit b6cb0ed

File tree

3 files changed

+143
-9
lines changed

3 files changed

+143
-9
lines changed

mypy/checker.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3632,7 +3632,9 @@ def check_multi_assignment_from_iterable(
36323632
infer_lvalue_type: bool = True,
36333633
) -> None:
36343634
rvalue_type = get_proper_type(rvalue_type)
3635-
if self.type_is_iterable(rvalue_type) and isinstance(rvalue_type, Instance):
3635+
if self.type_is_iterable(rvalue_type) and isinstance(
3636+
rvalue_type, (Instance, CallableType, TypeType, Overloaded)
3637+
):
36363638
item_type = self.iterable_item_type(rvalue_type)
36373639
for lv in lvalues:
36383640
if isinstance(lv, StarExpr):
@@ -6387,15 +6389,16 @@ def note(
63876389
return
63886390
self.msg.note(msg, context, offset=offset, code=code)
63896391

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

64006403
def function_type(self, func: FuncBase) -> FunctionLike:
64016404
return function_type(func, self.named_type("builtins.function"))

test-data/unit/check-inference.test

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,120 @@ def f() -> None:
270270
class A: pass
271271
[out]
272272

273+
[case testClassObjectsNotUnpackableWithoutIterableMetaclass]
274+
from typing import Type
275+
276+
class Foo: ...
277+
A: Type[Foo] = Foo
278+
a, b = Foo # E: "Type[Foo]" object is not iterable
279+
c, d = A # E: "Type[Foo]" object is not iterable
280+
281+
class Meta(type): ...
282+
class Bar(metaclass=Meta): ...
283+
B: Type[Bar] = Bar
284+
e, f = Bar # E: "Type[Bar]" object is not iterable
285+
g, h = B # E: "Type[Bar]" object is not iterable
286+
287+
reveal_type(a) # E: Cannot determine type of "a" # N: Revealed type is "Any"
288+
reveal_type(b) # E: Cannot determine type of "b" # N: Revealed type is "Any"
289+
reveal_type(c) # E: Cannot determine type of "c" # N: Revealed type is "Any"
290+
reveal_type(d) # E: Cannot determine type of "d" # N: Revealed type is "Any"
291+
reveal_type(e) # E: Cannot determine type of "e" # N: Revealed type is "Any"
292+
reveal_type(f) # E: Cannot determine type of "f" # N: Revealed type is "Any"
293+
reveal_type(g) # E: Cannot determine type of "g" # N: Revealed type is "Any"
294+
reveal_type(h) # E: Cannot determine type of "h" # N: Revealed type is "Any"
295+
[out]
296+
297+
[case testInferringLvarTypesUnpackedFromIterableClassObject]
298+
from typing import Iterator, Type, TypeVar, Union, overload
299+
class Meta(type):
300+
def __iter__(cls) -> Iterator[int]:
301+
yield from [1, 2, 3]
302+
303+
class Meta2(type):
304+
def __iter__(cls) -> Iterator[str]:
305+
yield from ["foo", "bar", "baz"]
306+
307+
class Meta3(type): ...
308+
309+
class Foo(metaclass=Meta): ...
310+
class Bar(metaclass=Meta2): ...
311+
class Baz(metaclass=Meta3): ...
312+
class Spam: ...
313+
314+
class Eggs(metaclass=Meta):
315+
@overload
316+
def __init__(self, x: int) -> None: ...
317+
@overload
318+
def __init__(self, x: int, y: int, z: int) -> None: ...
319+
def __init__(self, x: int, y: int = ..., z: int = ...) -> None: ...
320+
321+
A: Type[Foo] = Foo
322+
B: Type[Union[Foo, Bar]] = Foo
323+
C: Union[Type[Foo], Type[Bar]] = Foo
324+
D: Type[Union[Foo, Baz]] = Foo
325+
E: Type[Union[Foo, Spam]] = Foo
326+
F: Type[Eggs] = Eggs
327+
G: Type[Union[Foo, Eggs]] = Foo
328+
329+
a, b, c = Foo
330+
d, e, f = A
331+
g, h, i = B
332+
j, k, l = C
333+
m, n, o = D # E: "Type[Baz]" object is not iterable
334+
p, q, r = E # E: "Type[Spam]" object is not iterable
335+
s, t, u = Eggs
336+
v, w, x = F
337+
y, z, aa = G
338+
339+
for var in [a, b, c, d, e, f, s, t, u, v, w, x, y, z, aa]:
340+
reveal_type(var) # N: Revealed type is "builtins.int"
341+
342+
for var2 in [g, h, i, j, k, l]:
343+
reveal_type(var2) # N: Revealed type is "Union[builtins.int, builtins.str]"
344+
345+
for var3 in [m, n, o, p, q, r]:
346+
reveal_type(var3) # N: Revealed type is "Union[builtins.int, Any]"
347+
348+
T = TypeVar("T", bound=Type[Foo])
349+
350+
def check(x: T) -> T:
351+
a, b, c = x
352+
for var in [a, b, c]:
353+
reveal_type(var) # N: Revealed type is "builtins.int"
354+
return x
355+
356+
T2 = TypeVar("T2", bound=Type[Union[Foo, Bar]])
357+
358+
def check2(x: T2) -> T2:
359+
a, b, c = x
360+
for var in [a, b, c]:
361+
reveal_type(var) # N: Revealed type is "Union[builtins.int, builtins.str]"
362+
return x
363+
364+
T3 = TypeVar("T3", bound=Union[Type[Foo], Type[Bar]])
365+
366+
def check3(x: T3) -> T3:
367+
a, b, c = x
368+
for var in [a, b, c]:
369+
reveal_type(var) # N: Revealed type is "Union[builtins.int, builtins.str]"
370+
return x
371+
[out]
372+
373+
[case testInferringLvarTypesUnpackedFromIterableClassObjectWithGenericIter]
374+
from typing import Iterator, Type, TypeVar
375+
376+
T = TypeVar("T")
377+
class Meta(type):
378+
def __iter__(self: Type[T]) -> Iterator[T]: ...
379+
class Foo(metaclass=Meta): ...
380+
381+
A, B, C = Foo
382+
reveal_type(A) # N: Revealed type is "__main__.Foo"
383+
reveal_type(B) # N: Revealed type is "__main__.Foo"
384+
reveal_type(C) # N: Revealed type is "__main__.Foo"
385+
[out]
386+
273387
[case testInferringLvarTypesInMultiDefWithInvalidTuple]
274388
from typing import Tuple
275389
t = None # type: Tuple[object, object, object]

test-data/unit/pythoneval.test

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1878,6 +1878,23 @@ _testEnumIterMetaInference.py:8: note: Revealed type is "typing.Iterator[_E`-1]"
18781878
_testEnumIterMetaInference.py:9: note: Revealed type is "_E`-1"
18791879
_testEnumIterMetaInference.py:13: note: Revealed type is "socket.SocketKind"
18801880

1881+
[case testEnumUnpackedViaMetaclass]
1882+
from enum import Enum
1883+
1884+
class FooEnum(Enum):
1885+
A = 1
1886+
B = 2
1887+
C = 3
1888+
1889+
a, b, c = FooEnum
1890+
reveal_type(a)
1891+
reveal_type(b)
1892+
reveal_type(c)
1893+
[out]
1894+
_testEnumUnpackedViaMetaclass.py:9: note: Revealed type is "_testEnumUnpackedViaMetaclass.FooEnum"
1895+
_testEnumUnpackedViaMetaclass.py:10: note: Revealed type is "_testEnumUnpackedViaMetaclass.FooEnum"
1896+
_testEnumUnpackedViaMetaclass.py:11: note: Revealed type is "_testEnumUnpackedViaMetaclass.FooEnum"
1897+
18811898
[case testNativeIntTypes]
18821899
# Spot check various native int operations with full stubs.
18831900
from mypy_extensions import i64, i32

0 commit comments

Comments
 (0)