Skip to content

Make classes with __getitem__ work in for context #10386

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
37 changes: 29 additions & 8 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
from mypy.sametypes import is_same_type
from mypy.messages import (
MessageBuilder, make_inferred_type_note, append_invariance_notes, pretty_seq,
format_type, format_type_bare, format_type_distinctly, SUGGESTED_TEST_FIXTURES
format_type, format_type_bare, format_type_distinctly, SUGGESTED_TEST_FIXTURES,
temp_message_builder
)
import mypy.checkexpr
from mypy.checkmember import (
Expand Down Expand Up @@ -3499,20 +3500,40 @@ def analyze_iterable_item_type(self, expr: Expression) -> Tuple[Type, Type]:
"""Analyse iterable expression and return iterator and iterator item types."""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's explain how iterables are handled in CPython. Priorities and possibilities.

echk = self.expr_checker
iterable = get_proper_type(echk.accept(expr))
iterator = echk.check_method_call_by_name('__iter__', iterable, [], [], expr)[0]
iter_msg_builder = self.msg.clean_copy()
iterator = echk.check_method_call_by_name(
'__iter__', iterable, [], [], expr, iter_msg_builder)[0]

if isinstance(iterable, TupleType):
joined = UninhabitedType() # type: Type
for item in iterable.items:
joined = join_types(joined, item)
return iterator, joined

if iter_msg_builder.is_errors():
# We couldn't find __iter__ so let's try __getitem__
getitem_msg_builder = temp_message_builder()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can try to use .clean_copy() here.

arg = self.temp_node(self.named_type("builtins.int"), expr)
getitem_type = echk.check_method_call_by_name(
'__getitem__',
iterable,
[arg],
[nodes.ARG_POS],
expr,
getitem_msg_builder
)[0]

if not getitem_msg_builder.is_errors():
# We found __getitem__
return self.named_generic_type("typing.Iterator", [getitem_type]), getitem_type
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like it should be typing.Iterable and not Iterator.


self.msg.add_errors(iter_msg_builder)
# Non-tuple iterable.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment does not really explain what is going on anymore.

if self.options.python_version[0] >= 3:
nextmethod = '__next__'
else:
# Non-tuple iterable.
if self.options.python_version[0] >= 3:
nextmethod = '__next__'
else:
nextmethod = 'next'
return iterator, echk.check_method_call_by_name(nextmethod, iterator, [], [], expr)[0]
nextmethod = 'next'
return iterator, echk.check_method_call_by_name(nextmethod, iterator, [], [], expr)[0]

def analyze_container_item_type(self, typ: Type) -> Optional[Type]:
"""Check if a type is a nominal container of a union of such.
Expand Down
11 changes: 11 additions & 0 deletions test-data/unit/check-protocols.test
Original file line number Diff line number Diff line change
Expand Up @@ -1940,6 +1940,17 @@ reveal_type(list(c for c in C)) # N: Revealed type is "builtins.list[__main__.C
reveal_type(list(C)) # N: Revealed type is "builtins.list[__main__.C*]"
[builtins fixtures/list.pyi]

[case testIterableGetItemOnClass]
class A:
Copy link
Member

@sobolevn sobolevn Dec 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's also test:

  • Metaclasses with __getitem__ and __class_getitem__
  • for statements
  • list(iter(A())) and list(iter(B()))

def __getitem__(self, x: int) -> int: pass

class B:
def __getitem__(self, x: str) -> str: pass

reveal_type(list(a for a in A())) # N: Revealed type is "builtins.list[builtins.int*]"
list(b for b in B()) # E: "B" has no attribute "__iter__" (not iterable)
[builtins fixtures/list.pyi]

[case testClassesGetattrWithProtocols]
from typing import Protocol

Expand Down