Closes #136 · dry-python/classes@2568580 · GitHub
Skip to content

Commit 2568580

Browse files
committed
Closes #136
1 parent 0149e8c commit 2568580

5 files changed

Lines changed: 230 additions & 20 deletions

File tree

‎classes/_typeclass.py‎

Lines changed: 35 additions & 1 deletion

‎classes/contrib/mypy/classes_plugin.py‎

Lines changed: 85 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
from typing import Callable, Optional, Type, Union
2121

22+
from mypy.nodes import ARG_POS, Decorator, MemberExpr
2223
from mypy.plugin import FunctionContext, MethodContext, MethodSigContext, Plugin
2324
from mypy.typeops import bind_self
2425
from mypy.types import AnyType, CallableType, Instance
@@ -46,15 +47,16 @@ class _AdjustArguments(object):
4647
"""
4748

4849
def __call__(self, ctx: FunctionContext) -> MypyType:
50+
defn = ctx.arg_types[0][0]
4951
is_defined_by_class = (
50-
isinstance(ctx.arg_types[0][0], CallableType) and
51-
not ctx.arg_types[0][0].arg_types and
52-
isinstance(ctx.arg_types[0][0].ret_type, Instance)
52+
isinstance(defn, CallableType) and
53+
not defn.arg_types and
54+
isinstance(defn.ret_type, Instance)
5355
)
5456

5557
if is_defined_by_class:
5658
return self._adjust_protocol_arguments(ctx)
57-
elif isinstance(ctx.arg_types[0][0], CallableType):
59+
elif isinstance(defn, CallableType):
5860
return self._adjust_function_arguments(ctx)
5961
return ctx.default_return_type
6062

@@ -144,12 +146,87 @@ class _AdjustInstanceSignature(object):
144146
"""
145147

146148
def __call__(self, ctx: MethodContext) -> MypyType:
149+
if not isinstance(ctx.type, Instance):
150+
return ctx.default_return_type
151+
if not isinstance(ctx.default_return_type, CallableType):
152+
return ctx.default_return_type
153+
147154
instance_type = self._adjust_typeclass_callable(ctx)
148155
self._adjust_typeclass_type(ctx, instance_type)
149156
if isinstance(instance_type, Instance):
150157
self._add_supports_metadata(ctx, instance_type)
151158
return ctx.default_return_type
152159

160+
@classmethod
161+
def from_function_decorator(cls, ctx: FunctionContext) -> MypyType:
162+
"""
163+
It is used when ``.instance`` is used without params as a decorator.
164+
165+
Like:
166+
167+
.. code:: python
168+
169+
@some.instance
170+
def _some_str(instance: str) -> str:
171+
...
172+
173+
"""
174+
is_decorator = (
175+
isinstance(ctx.context, Decorator) and
176+
len(ctx.context.decorators) == 1 and
177+
isinstance(ctx.context.decorators[0], MemberExpr) and
178+
ctx.context.decorators[0].name == 'instance'
179+
)
180+
if not is_decorator:
181+
return ctx.default_return_type
182+
183+
passed_function = ctx.arg_types[0][0]
184+
assert isinstance(passed_function, CallableType)
185+
186+
if not passed_function.arg_types:
187+
return ctx.default_return_type
188+
189+
annotation_type = passed_function.arg_types[0]
190+
if isinstance(annotation_type, Instance):
191+
if annotation_type.type and annotation_type.type.is_protocol:
192+
ctx.api.fail(
193+
'Protocols must be passed with `is_protocol=True`',
194+
ctx.context,
195+
)
196+
return ctx.default_return_type
197+
else:
198+
ctx.api.fail(
199+
'Only simple instance types are allowed, got: {0}'.format(
200+
annotation_type,
201+
),
202+
ctx.context,
203+
)
204+
return ctx.default_return_type
205+
206+
ret_type = CallableType(
207+
arg_types=[passed_function],
208+
arg_kinds=[ARG_POS],
209+
arg_names=[None],
210+
ret_type=AnyType(TypeOfAny.implementation_artifact),
211+
fallback=passed_function.fallback,
212+
)
213+
instance_type = ctx.api.expr_checker.accept( # type: ignore
214+
ctx.context.decorators[0].expr, # type: ignore
215+
)
216+
217+
# We need to change the `ctx` type from `Function` to `Method`:
218+
return cls()(MethodContext(
219+
type=instance_type,
220+
arg_types=ctx.arg_types,
221+
arg_kinds=ctx.arg_kinds,
222+
arg_names=ctx.arg_names,
223+
args=ctx.args,
224+
callee_arg_names=ctx.callee_arg_names,
225+
default_return_type=ret_type,
226+
context=ctx.context,
227+
api=ctx.api,
228+
))
229+
153230
def _adjust_typeclass_callable(
154231
self,
155232
ctx: MethodContext,
@@ -302,6 +379,9 @@ def get_function_hook(
302379
"""Here we adjust the typeclass constructor."""
303380
if fullname == 'classes._typeclass.typeclass':
304381
return _AdjustArguments()
382+
if fullname == 'instance of _TypeClass':
383+
# `@some.instance` call without params:
384+
return _AdjustInstanceSignature.from_function_decorator
305385
return None
306386

307387
def get_method_hook(
@@ -310,6 +390,7 @@ def get_method_hook(
310390
) -> Optional[Callable[[MethodContext], MypyType]]:
311391
"""Here we adjust the typeclass with new allowed types."""
312392
if fullname == 'classes._typeclass._TypeClass.instance':
393+
# `@some.instance` call with explicit params:
313394
return _AdjustInstanceSignature()
314395
return None
315396

‎docs/pages/concept.rst‎

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ works differently for ``"string"`` and ``[1, 2]``.
1313
Or ``+`` operator that works for numbers like "add"
1414
and for strings it works like "concatenate".
1515

16+
Existing approaches
17+
-------------------
18+
1619
Classes and interfaces
1720
~~~~~~~~~~~~~~~~~~~~~~
1821

@@ -45,8 +48,17 @@ to add your own types to this process.
4548
So, how does it work?
4649

4750

51+
Typeclasses
52+
-----------
53+
54+
So, typeclasses help us to build new abstractions near the existing types,
55+
not inside them.
56+
57+
Basically, we will learn how to dispatch
58+
different logic based on predefined set of types.
59+
4860
Steps
49-
-----
61+
~~~~~
5062

5163
To use typeclasses you should understand these steps:
5264

@@ -75,7 +87,7 @@ Let's define some instances:
7587

7688
.. code:: python
7789
78-
>>> @json.instance(str)
90+
>>> @json.instance # You can use just the annotation
7991
... def _json_str(instance: str) -> str:
8092
... return '"{0}"'.format(instance)
8193
@@ -87,6 +99,17 @@ Let's define some instances:
8799
That's how we define instances for our typeclass.
88100
These instances will be executed when the corresponding type will be supplied.
89101

102+
.. note::
103+
``.instance`` can use explicit type or just an existing annotation.
104+
It is recommended to use the explicit type, because annotations can be tricky.
105+
For example, sometimes you have to use ``ForwardRef``
106+
or so called string-based-annotations. It is not supported.
107+
Complex type from annotations are also not supported
108+
like: ``Union[str, int]``.
109+
110+
So, use annotations for the simpliest cases only
111+
and use explicit types in all other cases.
112+
90113
And the last step is to call our typeclass
91114
with different value of different types:
92115

‎typesafety/test_typeclass/test_instance.yml‎

Lines changed: 74 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,26 @@
1616
reveal_type(a) # N: Revealed type is 'classes._typeclass._TypeClass[Union[builtins.str*, builtins.int*], builtins.str, def (builtins.str*) -> builtins.str, <nothing>]'
1717
1818
19+
- case: typeclass_instance_mixed_order
20+
disable_cache: False
21+
main: |
22+
from classes import typeclass
23+
24+
@typeclass
25+
def some(instance) -> str:
26+
...
27+
28+
@some.instance(str)
29+
def _some_str(instance: str) -> str:
30+
...
31+
32+
@some.instance(int)
33+
def _some_int(instance: str) -> str:
34+
...
35+
out: |
36+
main:11: error: Argument 1 has incompatible type "Callable[[str], str]"; expected "Callable[[int], str]"
37+
38+
1939
- case: typeclass_instance_any
2040
disable_cache: false
2141
main: |
@@ -46,36 +66,77 @@
4666
...
4767
4868
out: |
49-
main:7: error: No overload variant of "instance" of "_TypeClass" matches argument type "Callable[[], Any]"
69+
main:7: error: Argument 1 to "instance" of "_TypeClass" has incompatible type "Callable[[], Any]"; expected "Callable[[<nothing>], Any]"
70+
71+
72+
- case: typeclass_instance_wrong_param
73+
disable_cache: false
74+
main: |
75+
from classes import typeclass
76+
77+
@typeclass
78+
def a(instance):
79+
...
80+
81+
a.instance(1)
82+
83+
out: |
84+
main:7: error: No overload variant of "instance" of "_TypeClass" matches argument type "int"
5085
main:7: note: <1 more non-matching overload not shown>
86+
main:7: note: def [_InstanceType] instance(self, type_argument: Callable[[_InstanceType], Any]) -> NoReturn
5187
main:7: note: def [_InstanceType] instance(self, type_argument: Type[_InstanceType], *, is_protocol: Literal[False] = ...) -> Callable[[Callable[[_InstanceType], Any]], NoReturn]
52-
main:7: note: Possible overload variant:
88+
main:7: note: Possible overload variants:
5389
5490
55-
- case: typeclass_wrong_param
91+
- case: typeclass_instance_annotation_only
5692
disable_cache: false
5793
main: |
5894
from classes import typeclass
5995
60-
typeclass(1)
96+
@typeclass
97+
def some(instance) -> str:
98+
...
6199
62-
out: |
63-
main:3: error: Value of type variable "_CallbackType" of "typeclass" cannot be "int"
100+
@some.instance
101+
def _some_str(instance: str) -> str:
102+
...
64103
104+
some('abc')
65105
66-
- case: typeclass_instance_wrong_param
106+
107+
- case: typeclass_instance_annotation_only_complex_types
67108
disable_cache: false
68109
main: |
69110
from classes import typeclass
111+
from typing import Union, Sized, Type, Any
70112
71113
@typeclass
72-
def a(instance):
114+
def some(instance) -> str:
73115
...
74116
75-
a.instance(1)
117+
@some.instance
118+
def _some_union(instance: Union[str, int]) -> str:
119+
...
76120
121+
@some.instance
122+
def _some_type_type(instance: Type[str]) -> str:
123+
...
124+
125+
@some.instance
126+
def _some_protocol(instance: Sized) -> str:
127+
...
128+
129+
@some.instance
130+
def _some_annotated(instance) -> str:
131+
...
132+
133+
@some.instance
134+
def _some_explicit_any(instance: Any) -> str:
135+
...
77136
out: |
78-
main:7: error: No overload variant of "instance" of "_TypeClass" matches argument type "int"
79-
main:7: note: <1 more non-matching overload not shown>
80-
main:7: note: def [_InstanceType] instance(self, type_argument: Type[_InstanceType], *, is_protocol: Literal[False] = ...) -> Callable[[Callable[[_InstanceType], Any]], NoReturn]
81-
main:7: note: Possible overload variant:
137+
main:8: error: Only simple instance types are allowed, got: Union[builtins.str, builtins.int]
138+
main:12: error: Only simple instance types are allowed, got: Type[builtins.str]
139+
main:16: error: Protocols must be passed with `is_protocol=True`
140+
main:20: error: Only simple instance types are allowed, got: Any
141+
main:24: error: Only simple instance types are allowed, got: Any
142+
main:25: error: Explicit "Any" is not allowed

‎typesafety/test_typeclass/test_typeclass.yml‎

Lines changed: 11 additions & 0 deletions

0 commit comments

Comments
 (0)