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

Allow iterable class objects to be unpacked (including enums) #14827

Merged
merged 3 commits into from
Mar 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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): ...
AlexWaygood marked this conversation as resolved.
Show resolved Hide resolved
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