Skip to content

Commit ca5b8f6

Browse files
authored
Adds support for nested KindN (#688)
* Adds support for nested KindN * Fixes CI * Fixes CI * Adds type tests * Fixes part of type tests * Fixes type tests * Fixes type tests
1 parent 7e1ba2c commit ca5b8f6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+486
-267
lines changed

returns/contrib/mypy/_features/kind.py

Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@
88
MethodContext,
99
MethodSigContext,
1010
)
11-
from mypy.typeops import bind_self, erase_to_bound
11+
from mypy.typeops import bind_self
1212
from mypy.types import AnyType, CallableType, FunctionLike, Instance, Overloaded
1313
from mypy.types import Type as MypyType
1414
from mypy.types import TypeOfAny, TypeType, TypeVarType, get_proper_type
1515

16-
from returns.contrib.mypy._consts import TYPED_KINDN
1716
from returns.contrib.mypy._typeops.fallback import asserts_fallback_to_any
17+
from returns.contrib.mypy._typeops.visitor import translate_kind_instance
1818

1919
# TODO: probably we can validate `KindN[]` creation during `get_analtype`
2020

@@ -111,6 +111,8 @@ def kinded_signature(ctx: MethodSigContext) -> CallableType:
111111
return wrapped_method
112112

113113

114+
# TODO: we should raise an error if bound type does not have any `KindN`
115+
# instances, because that's not how `@kinded` and `Kinded[]` should be used.
114116
def kinded_call(ctx: MethodContext) -> MypyType:
115117
"""
116118
Reveals the correct return type of ``Kinded.__call__`` method.
@@ -125,7 +127,7 @@ def kinded_call(ctx: MethodContext) -> MypyType:
125127
126128
See :class:`returns.primitives.hkt.Kinded` for more information.
127129
"""
128-
return _process_kinded_type(ctx.default_return_type)
130+
return translate_kind_instance(ctx.default_return_type)
129131

130132

131133
@asserts_fallback_to_any
@@ -164,27 +166,3 @@ def _crop_kind_args(
164166
if limit is None:
165167
limit = kind.args[0].args # type: ignore
166168
return kind.args[1:len(limit) + 1]
167-
168-
169-
def _process_kinded_type(instance: MypyType) -> MypyType:
170-
"""Recursively process all type arguments in a kind."""
171-
kind = get_proper_type(instance)
172-
if not isinstance(kind, Instance) or not kind.args:
173-
return instance
174-
175-
if kind.type.fullname != TYPED_KINDN: # this is some other instance
176-
return instance
177-
178-
real_type = get_proper_type(kind.args[0])
179-
if isinstance(real_type, TypeVarType):
180-
return erase_to_bound(real_type)
181-
elif isinstance(real_type, Instance):
182-
return real_type.copy_modified(args=[
183-
# Let's check if there are any nested `KindN[]` instance,
184-
# if so, it would be dekinded into a regular type following
185-
# the same rules:
186-
_process_kinded_type(type_arg)
187-
for type_arg in kind.args[1:len(real_type.args) + 1]
188-
])
189-
# This should never happen, probably can be an exception:
190-
return AnyType(TypeOfAny.implementation_artifact)
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
from typing import Iterable, List, Optional
2+
3+
from mypy.typeops import erase_to_bound
4+
from mypy.types import (
5+
AnyType,
6+
CallableType,
7+
DeletedType,
8+
ErasedType,
9+
Instance,
10+
LiteralType,
11+
NoneType,
12+
Overloaded,
13+
PartialType,
14+
TupleType,
15+
Type,
16+
TypedDictType,
17+
TypeOfAny,
18+
TypeType,
19+
TypeVarType,
20+
UnboundType,
21+
UninhabitedType,
22+
UnionType,
23+
get_proper_type,
24+
)
25+
26+
from returns.contrib.mypy._consts import TYPED_KINDN
27+
28+
# TODO: replace with real `TypeTranslator` in the next mypy release.
29+
_LEAF_TYPES = (
30+
UnboundType,
31+
AnyType,
32+
NoneType,
33+
UninhabitedType,
34+
ErasedType,
35+
DeletedType,
36+
TypeVarType,
37+
PartialType,
38+
)
39+
40+
41+
def translate_kind_instance(typ: Type) -> Type: # noqa: WPS, C901
42+
"""
43+
We use this ugly hack to translate ``KindN[x, y]`` into ``x[y]``.
44+
45+
This is required due to the fact that ``KindN``
46+
can be nested in other types, like: ``List[KindN[...]]``.
47+
48+
We will refactor this code after ``TypeTranslator``
49+
is released in ``mypy@0.800`` version.
50+
"""
51+
typ = get_proper_type(typ)
52+
53+
if isinstance(typ, _LEAF_TYPES): # noqa: WPS223
54+
return typ
55+
elif isinstance(typ, Instance):
56+
last_known_value: Optional[LiteralType] = None
57+
if typ.last_known_value is not None:
58+
raw_last_known_value = translate_kind_instance(typ.last_known_value)
59+
assert isinstance(raw_last_known_value, LiteralType)
60+
last_known_value = raw_last_known_value
61+
instance = Instance(
62+
typ=typ.type,
63+
args=_translate_types(typ.args),
64+
line=typ.line,
65+
column=typ.column,
66+
last_known_value=last_known_value,
67+
)
68+
if typ.type.fullname == TYPED_KINDN: # That's where we do the change
69+
return _process_kinded_type(instance)
70+
return instance
71+
72+
elif isinstance(typ, CallableType):
73+
return typ.copy_modified(
74+
arg_types=_translate_types(typ.arg_types),
75+
ret_type=translate_kind_instance(typ.ret_type),
76+
)
77+
elif isinstance(typ, TupleType):
78+
return TupleType(
79+
_translate_types(typ.items),
80+
translate_kind_instance(typ.partial_fallback), # type: ignore
81+
typ.line,
82+
typ.column,
83+
)
84+
elif isinstance(typ, TypedDictType):
85+
dict_items = {
86+
item_name: translate_kind_instance(item_type)
87+
for item_name, item_type in typ.items.items()
88+
}
89+
return TypedDictType(
90+
dict_items,
91+
typ.required_keys,
92+
translate_kind_instance(typ.fallback), # type: ignore
93+
typ.line,
94+
typ.column,
95+
)
96+
elif isinstance(typ, LiteralType):
97+
fallback = translate_kind_instance(typ.fallback)
98+
assert isinstance(fallback, Instance)
99+
return LiteralType(
100+
value=typ.value,
101+
fallback=fallback,
102+
line=typ.line,
103+
column=typ.column,
104+
)
105+
elif isinstance(typ, UnionType):
106+
return UnionType(_translate_types(typ.items), typ.line, typ.column)
107+
elif isinstance(typ, Overloaded):
108+
functions: List[CallableType] = []
109+
for func in typ.items():
110+
new = translate_kind_instance(func)
111+
assert isinstance(new, CallableType)
112+
functions.append(new)
113+
return Overloaded(items=functions)
114+
elif isinstance(typ, TypeType):
115+
return TypeType.make_normalized(
116+
translate_kind_instance(typ.item),
117+
line=typ.line,
118+
column=typ.column,
119+
)
120+
return typ
121+
122+
123+
def _translate_types(types: Iterable[Type]) -> List[Type]:
124+
return [translate_kind_instance(typ) for typ in types]
125+
126+
127+
def _process_kinded_type(kind: Instance) -> Type:
128+
"""Recursively process all type arguments in a kind."""
129+
if not kind.args:
130+
return kind
131+
132+
real_type = get_proper_type(kind.args[0])
133+
if isinstance(real_type, TypeVarType):
134+
return erase_to_bound(real_type)
135+
elif isinstance(real_type, Instance):
136+
return real_type.copy_modified(
137+
args=kind.args[1:len(real_type.args) + 1],
138+
)
139+
140+
# This should never happen, probably can be an exception:
141+
return AnyType(TypeOfAny.implementation_artifact)

returns/primitives/hkt.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@
99

1010
_FunctionDefType = TypeVar(
1111
'_FunctionDefType',
12-
bound=Callable[..., 'KindN'],
12+
bound=Callable,
1313
covariant=True, # This is a must! Otherwise it would not work.
1414
)
1515
_FunctionType = TypeVar(
1616
'_FunctionType',
17-
bound=Callable[..., 'KindN'],
17+
bound=Callable,
1818
)
1919

2020
_UpdatedType = TypeVar('_UpdatedType')

setup.cfg

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ per-file-ignores =
7171
returns/contrib/mypy/*.py: S101, WPS201
7272
returns/contrib/pytest/plugin.py: WPS201, WPS430, WPS437, WPS609
7373
returns/contrib/hypothesis/*.py: WPS437, WPS609
74+
# TODO: remove after mypy@0.800
75+
returns/contrib/mypy/_typeops/visitor.py: S101, WPS232
7476
# There are multiple assert's in tests:
7577
tests/*.py: S101, WPS204, WPS218, WPS226, WPS432, WPS436
7678
# Some examples don't have any docs on purpose:

typesafety/test_converters/test_flatten.yml

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
2727
x: Result[Result[int, str], float]
2828
# TODO: I am pretty sure we can later fix this to be an error:
29-
reveal_type(flatten(x)) # N: Revealed type is 'returns.result.Result*[builtins.int*, builtins.object*]'
29+
reveal_type(flatten(x)) # N: Revealed type is 'returns.result.Result[builtins.int, builtins.object]'
3030
3131
3232
- case: flatten_custom_type
@@ -43,7 +43,7 @@
4343
...
4444
4545
x: MyClass[MyClass[int]]
46-
reveal_type(flatten(x)) # N: Revealed type is 'main.MyClass*[builtins.int*]'
46+
reveal_type(flatten(x)) # N: Revealed type is 'main.MyClass[builtins.int]'
4747
4848
4949
- case: flatten_wrong_flatten_error_type
@@ -66,7 +66,7 @@
6666
from returns.converters import flatten
6767
from returns.io import IO
6868
69-
reveal_type(flatten(IO(IO(1)))) # N: Revealed type is 'returns.io.IO*[builtins.int*]'
69+
reveal_type(flatten(IO(IO(1)))) # N: Revealed type is 'returns.io.IO[builtins.int]'
7070
7171
7272
- case: flatten_maybe
@@ -75,7 +75,7 @@
7575
from returns.converters import flatten
7676
from returns.maybe import Some
7777
78-
reveal_type(flatten(Some(Some(1)))) # N: Revealed type is 'returns.maybe.Maybe*[builtins.int*]'
78+
reveal_type(flatten(Some(Some(1)))) # N: Revealed type is 'returns.maybe.Maybe[builtins.int]'
7979
8080
8181
- case: flatten_result
@@ -87,7 +87,7 @@
8787
def returns_result() -> Result[Result[int, str], str]:
8888
...
8989
90-
reveal_type(flatten(returns_result())) # N: Revealed type is 'returns.result.Result*[builtins.int*, builtins.str*]'
90+
reveal_type(flatten(returns_result())) # N: Revealed type is 'returns.result.Result[builtins.int, builtins.str]'
9191
9292
9393
- case: flatten_ioresult
@@ -99,7 +99,7 @@
9999
def returns_ioresult() -> IOResult[IOResult[int, str], str]:
100100
...
101101
102-
reveal_type(flatten(returns_ioresult())) # N: Revealed type is 'returns.io.IOResult*[builtins.int*, builtins.str*]'
102+
reveal_type(flatten(returns_ioresult())) # N: Revealed type is 'returns.io.IOResult[builtins.int, builtins.str]'
103103
104104
105105
- case: flatten_context
@@ -110,7 +110,7 @@
110110
111111
x: RequiresContext[RequiresContext[str, int], int]
112112
113-
reveal_type(flatten(x)) # N: Revealed type is 'returns.context.requires_context.RequiresContext*[builtins.str*, builtins.int*]'
113+
reveal_type(flatten(x)) # N: Revealed type is 'returns.context.requires_context.RequiresContext[builtins.str, builtins.int]'
114114
115115
116116
- case: flatten_context_result
@@ -121,7 +121,7 @@
121121
122122
x: RequiresContextResult[RequiresContextResult[str, int, float], int, float]
123123
124-
reveal_type(flatten(x)) # N: Revealed type is 'returns.context.requires_context_result.RequiresContextResult*[builtins.str*, builtins.int*, builtins.float*]'
124+
reveal_type(flatten(x)) # N: Revealed type is 'returns.context.requires_context_result.RequiresContextResult[builtins.str, builtins.int, builtins.float]'
125125
126126
127127
- case: flatten_context_ioresult
@@ -132,7 +132,7 @@
132132
133133
x: RequiresContextIOResult[RequiresContextIOResult[str, int, float], int, float]
134134
135-
reveal_type(flatten(x)) # N: Revealed type is 'returns.context.requires_context_ioresult.RequiresContextIOResult*[builtins.str*, builtins.int*, builtins.float*]'
135+
reveal_type(flatten(x)) # N: Revealed type is 'returns.context.requires_context_ioresult.RequiresContextIOResult[builtins.str, builtins.int, builtins.float]'
136136
137137
138138
- case: flatten_context_ioresult
@@ -143,7 +143,7 @@
143143
144144
x: RequiresContextIOResult[RequiresContextIOResult[str, int, float], int, float]
145145
146-
reveal_type(flatten(x)) # N: Revealed type is 'returns.context.requires_context_ioresult.RequiresContextIOResult*[builtins.str*, builtins.int*, builtins.float*]'
146+
reveal_type(flatten(x)) # N: Revealed type is 'returns.context.requires_context_ioresult.RequiresContextIOResult[builtins.str, builtins.int, builtins.float]'
147147
148148
149149
- case: flatten_future_result
@@ -154,7 +154,7 @@
154154
155155
x: ReaderFutureResult[ReaderFutureResult[int, bool, str], bool, str]
156156
157-
reveal_type(flatten(x)) # N: Revealed type is 'returns.context.requires_context_future_result.RequiresContextFutureResult*[builtins.int*, builtins.bool*, builtins.str*]'
157+
reveal_type(flatten(x)) # N: Revealed type is 'returns.context.requires_context_future_result.RequiresContextFutureResult[builtins.int, builtins.bool, builtins.str]'
158158
159159
160160
- case: flatten_future_result
@@ -165,4 +165,4 @@
165165
166166
x: FutureResult[FutureResult[int, str], str]
167167
168-
reveal_type(flatten(x)) # N: Revealed type is 'returns.future.FutureResult*[builtins.int*, builtins.str*]'
168+
reveal_type(flatten(x)) # N: Revealed type is 'returns.future.FutureResult[builtins.int, builtins.str]'

typesafety/test_examples/test_pair4_reuse.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@
1313
reveal_type(map_(str)(my_pair))
1414
out: |
1515
main:5: note: Revealed type is 'test_pair4.Pair[builtins.str*, builtins.int]'
16-
main:6: note: Revealed type is 'test_pair4.Pair*[builtins.str, builtins.int*]'
16+
main:6: note: Revealed type is 'test_pair4.Pair[builtins.str, builtins.int]'

0 commit comments

Comments
 (0)