Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Add support for mapping to supertype
  • Loading branch information
ilevkivskyi committed Aug 12, 2022
commit 83f78084197d038a466031b73a50bc228c43def9
27 changes: 26 additions & 1 deletion mypy/maptype.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
from typing import Dict, List

import mypy.typeops

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This causes a circular import:

  File "venv/lib/python3.10/site-packages/mypy/report.py", line 20, in <module>
    from mypy import stats
  File "venv/lib/python3.10/site-packages/mypy/stats.py", line 13, in <module>
    from mypy.argmap import map_formals_to_actuals
  File "venv/lib/python3.10/site-packages/mypy/argmap.py", line 8, in <module>
    from mypy.maptype import map_instance_to_supertype
  File "venv/lib/python3.10/site-packages/mypy/maptype.py", line 3, in <module>
    import mypy.typeops
  File "venv/lib/python3.10/site-packages/mypy/typeops.py", line 15, in <module>
    from mypy.maptype import map_instance_to_supertype
ImportError: cannot import name 'map_instance_to_supertype' from partially initialized module 'mypy.maptype' (most likely due to a circular import) (venv/lib/python3.10/site-packages/mypy/maptype.py)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks for reporting! Do you have a (preferably simple) repro for this?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Simply importing mypy.report without mypyc enabled should trigger this issue.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

See #13809 Btw just curious, why do you need to import mypy.report directly?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

It's one of sanity checks in nixpkgs, to make sure mypy module is not broken.

from mypy.expandtype import expand_type
from mypy.nodes import TypeInfo
from mypy.types import AnyType, Instance, ProperType, Type, TypeOfAny, TypeVarId
from mypy.types import (
AnyType,
Instance,
ProperType,
TupleType,
Type,
TypeOfAny,
TypeVarId,
get_proper_type,
has_type_vars,
)


def map_instance_to_supertype(instance: Instance, superclass: TypeInfo) -> Instance:
Expand All @@ -16,6 +27,20 @@ def map_instance_to_supertype(instance: Instance, superclass: TypeInfo) -> Insta
# Fast path: `instance` already belongs to `superclass`.
return instance

if superclass.fullname == "builtins.tuple" and instance.type.tuple_type:
if has_type_vars(instance.type.tuple_type):
# We special case mapping generic tuple types to tuple base, because for
# such tuples fallback can't be calculated before applying type arguments.
alias = instance.type.special_alias
assert alias is not None
if not alias._is_recursive:
# Unfortunately we can't support this for generic recursive tuples.
# If we skip this special casing we will fall back to tuple[Any, ...].
env = instance_to_type_environment(instance)
tuple_type = get_proper_type(expand_type(instance.type.tuple_type, env))
if isinstance(tuple_type, TupleType):
return mypy.typeops.tuple_fallback(tuple_type)

if not superclass.type_vars:
# Fast path: `superclass` has no type variables to map to.
return Instance(superclass, [])
Expand Down
3 changes: 2 additions & 1 deletion mypy/semanal_namedtuple.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
TypeType,
TypeVarType,
UnboundType,
has_type_vars,
)
from mypy.util import get_unique_redefinition_name

Expand Down Expand Up @@ -487,7 +488,7 @@ def build_namedtuple_typeinfo(
# We can't calculate the complete fallback type until after semantic
# analysis, since otherwise base classes might be incomplete. Postpone a
# callback function that patches the fallback.
if not has_placeholder(tuple_base):
if not has_placeholder(tuple_base) and not has_type_vars(tuple_base):
self.api.schedule_patch(
PRIORITY_FALLBACKS, lambda: calculate_tuple_fallback(tuple_base)
)
Expand Down
60 changes: 58 additions & 2 deletions test-data/unit/check-namedtuple.test
Original file line number Diff line number Diff line change
Expand Up @@ -1186,23 +1186,79 @@ NT[str](key=0, value=0) # E: Argument "value" to "NT" has incompatible type "in
[case testGenericNamedTupleMethods]
from typing import Generic, NamedTuple, TypeVar

T = TypeVar("T")
class NT(NamedTuple, Generic[T]):
key: int
value: T
x: int

nti: NT[int]
reveal_type(nti * x) # N: Revealed type is "builtins.tuple[builtins.int, ...]"

nts: NT[str]
reveal_type(nts * x) # N: Revealed type is "builtins.tuple[builtins.object, ...]"
[builtins fixtures/tuple.pyi]
[typing fixtures/typing-namedtuple.pyi]

[case testGenericNamedTupleCustomMethods]
from typing import Generic, NamedTuple, TypeVar

T = TypeVar("T")
class NT(NamedTuple, Generic[T]):
key: int
value: T
def foo(self) -> T: ...
@classmethod
def from_value(cls, value: T) -> NT[T]: ...

nts: NT[str]
reveal_type(nts.foo()) # N: Revealed type is "builtins.str"

nti = NT.from_value(1)
reveal_type(nti) # N: Revealed type is "Tuple[builtins.int, builtins.int, fallback=__main__.NT[builtins.int]]"
NT[str].from_value(1) # E: Argument 1 to "from_value" of "NT" has incompatible type "int"; expected "str"
[builtins fixtures/tuple.pyi]
[typing fixtures/typing-namedtuple.pyi]

[case testGenericNamedTupleSubtyping]
from typing import Generic, NamedTuple, TypeVar
from typing import Generic, NamedTuple, TypeVar, Tuple

T = TypeVar("T")
class NT(NamedTuple, Generic[T]):
key: int
value: T

nts: NT[str]
nti: NT[int]

def foo(x: Tuple[int, ...]) -> None: ...
foo(nti)
foo(nts) # E: Argument 1 to "foo" has incompatible type "NT[str]"; expected "Tuple[int, ...]"
[builtins fixtures/tuple.pyi]
[typing fixtures/typing-namedtuple.pyi]

[case testGenericNamedTupleJoin]
from typing import Generic, NamedTuple, TypeVar
from typing import Generic, NamedTuple, TypeVar, Tuple

T = TypeVar("T")
class NT(NamedTuple, Generic[T]):
key: int
value: T

nts: NT[str]
nti: NT[int]
x: Tuple[int, ...]

def foo(x: T, y: T) -> T: ...
reveal_type(foo(nti, nti)) # N: Revealed type is "Tuple[builtins.int, builtins.int, fallback=__main__.NT[builtins.int]]"

# Here fallbacks are object because named tuples are invariant.
reveal_type(foo(nti, nts)) # N: Revealed type is "Tuple[builtins.int, builtins.object, fallback=builtins.object]"
reveal_type(foo(nts, nti)) # N: Revealed type is "Tuple[builtins.int, builtins.object, fallback=builtins.object]"

reveal_type(foo(nti, x)) # N: Revealed type is "builtins.tuple[builtins.int, ...]"
Comment on lines +1273 to +1275

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The difference here (Tuple for fixed-length and builtins.tuple for variable-length) is unfortunate but that seems like a problem for a different PR.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

IIRC in error messages we format them nicely, it is only reveal_type() that reflects the internal representation.

reveal_type(foo(nts, x)) # N: Revealed type is "builtins.tuple[builtins.object, ...]"
reveal_type(foo(x, nti)) # N: Revealed type is "builtins.tuple[builtins.int, ...]"
reveal_type(foo(x, nts)) # N: Revealed type is "builtins.tuple[builtins.object, ...]"
[builtins fixtures/tuple.pyi]
[typing fixtures/typing-namedtuple.pyi]
1 change: 1 addition & 0 deletions test-data/unit/fixtures/tuple.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class tuple(Sequence[Tco], Generic[Tco]):
def count(self, obj: object) -> int: pass
class function: pass
class ellipsis: pass
class classmethod: pass

# We need int and slice for indexing tuples.
class int:
Expand Down