Skip to content

Commit be13224

Browse files
authored
Closes #136 (#189)
* Closes #136 * Fixes github actions * Fixes CI * Fixes CI * Fixes CI
1 parent 0149e8c commit be13224

File tree

8 files changed

+385
-180
lines changed

8 files changed

+385
-180
lines changed

.github/workflows/test.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
name: test
22

3-
on: [push, pull_request, workflow_dispatch]
3+
on:
4+
push:
5+
branches:
6+
- master
7+
pull_request:
8+
workflow_dispatch:
49

510
jobs:
611
build:

classes/_typeclass.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,16 @@ def typeclass(
9292
then it will be called,
9393
otherwise the default implementation will be called instead.
9494
95+
You can also use ``.instance`` with just annotation for better readability:
96+
97+
.. code:: python
98+
99+
>>> @example.instance
100+
... def _example_float(instance: float) -> str:
101+
... return 0.5
102+
103+
>>> assert example(5.1) == 0.5
104+
95105
.. rubric:: Generics
96106
97107
We also support generic, but the support is limited.
@@ -395,6 +405,13 @@ def instance(
395405
]:
396406
"""Case for regular typeclasses."""
397407

408+
@overload
409+
def instance(
410+
self,
411+
type_argument: Callable[[_InstanceType], _ReturnType],
412+
) -> NoReturn:
413+
"""Case for typeclasses that are defined by annotation only."""
414+
398415
@overload
399416
def instance(
400417
self,
@@ -423,10 +440,27 @@ def instance(
423440
would not match ``Type[_InstanceType]`` type due to ``mypy`` rules.
424441
425442
"""
426-
isinstance(object(), type_argument) # That's how we check for generics
443+
original_handler = None
444+
if not is_protocol:
445+
# If it is not a protocol, we can try to get an annotation from
446+
annotations = getattr(type_argument, '__annotations__', None)
447+
if annotations:
448+
original_handler = type_argument
449+
type_argument = annotations[
450+
type_argument.__code__.co_varnames[0] # noqa: WPS609
451+
]
452+
453+
# That's how we check for generics,
454+
# generics that look like `List[int]` or `set[T]` will fail this check,
455+
# because they are `_GenericAlias` instance,
456+
# which raises an exception for `__isinstancecheck__`
457+
isinstance(object(), type_argument)
427458

428459
def decorator(implementation):
429460
container = self._protocols if is_protocol else self._instances
430461
container[type_argument] = implementation
431462
return implementation
463+
464+
if original_handler is not None:
465+
return decorator(original_handler) # type: ignore
432466
return decorator

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 simplest 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

0 commit comments

Comments
 (0)