Skip to content

Commit b608fbd

Browse files
committed
Finishes with delegates
1 parent 2646cfd commit b608fbd

File tree

7 files changed

+152
-112
lines changed

7 files changed

+152
-112
lines changed

classes/_typeclass.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -534,9 +534,9 @@ def instance(
534534
We use this method to store implementation for each specific type.
535535
536536
Args:
537-
is_protocol - required when passing protocols.
538-
delegate - required when using delegate types, for example,
539-
when working with concrete generics like ``List[str]``.
537+
is_protocol: required when passing protocols.
538+
delegate: required when using delegate types, for example,
539+
when working with concrete generics like ``List[str]``.
540540
541541
Returns:
542542
Decorator for instance handler.

classes/contrib/mypy/typeops/instance_context.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ def _infer_instance_type(
156156
def _some_list(instance: list) -> int:
157157
...
158158
159-
Then, infered instance type is just ``list``.
159+
Then, inferred instance type is just ``list``.
160160
161161
Second, we have a delegate of its own:
162162
@@ -166,7 +166,7 @@ def _some_list(instance: list) -> int:
166166
def _some_list(instance: list) -> int:
167167
...
168168
169-
Then, infered instance type is ``list`` as well.
169+
Then, inferred instance type is ``list`` as well.
170170
171171
Lastly, we can have this case,
172172
when ``delegate`` type is used for instance annotation:

docs/conf.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@
1515
import os
1616
import sys
1717

18-
import sphinx
19-
2018
sys.path.insert(0, os.path.abspath('..'))
2119

2220

docs/pages/concept.rst

Lines changed: 32 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -95,67 +95,6 @@ to be specified on ``.instance()`` call:
9595
>>> assert to_json([1, 'a', None]) == '[1, "a", null]'
9696
9797
98-
``__instancecheck__`` magic method
99-
----------------------------------
100-
101-
We also support types that have ``__instancecheck__`` magic method defined,
102-
like `phantom-types <https://github.com/antonagestam/phantom-types>`_.
103-
104-
We treat them similar to ``Protocol`` types, by checking passed values
105-
with ``isinstance`` for each type with ``__instancecheck__`` defined.
106-
First match wins.
107-
108-
Example:
109-
110-
.. code:: python
111-
112-
>>> from classes import typeclass
113-
114-
>>> class Meta(type):
115-
... def __instancecheck__(self, other) -> bool:
116-
... return other == 1
117-
118-
>>> class Some(object, metaclass=Meta):
119-
... ...
120-
121-
>>> @typeclass
122-
... def some(instance) -> int:
123-
... ...
124-
125-
>>> @some.instance(Some)
126-
... def _some_some(instance: Some) -> int:
127-
... return 2
128-
129-
>>> argument = 1
130-
>>> assert isinstance(argument, Some)
131-
>>> assert some(argument) == 2
132-
133-
.. warning::
134-
135-
It is impossible for ``mypy`` to understand that ``1`` has ``Some``
136-
type in this example. Be careful, it might break your code!
137-
138-
This example is not really useful on its own,
139-
because as it was said, it can break things.
140-
141-
Instead, we are going to learn about
142-
how this feature can be used to model
143-
your domain model precisely with delegates.
144-
145-
Performance considerations
146-
~~~~~~~~~~~~~~~~~~~~~~~~~~
147-
148-
Types that are matched via ``__instancecheck__`` are the first one we try.
149-
So, the worst case complexity of this is ``O(n)``
150-
where ``n`` is the number of types to try.
151-
152-
We also always try them first and do not cache the result.
153-
This feature is here because we need to handle concrete generics.
154-
But, we recommend to think at least
155-
twice about the performance side of this feature.
156-
Maybe you can just write a function?
157-
158-
15998
Delegates
16099
---------
161100

@@ -172,6 +111,9 @@ that some ``list`` is ``List[int]`` or ``List[str]``:
172111
...
173112
TypeError: Subscripted generics cannot be used with class and instance checks
174113
114+
``__instancecheck__`` magic method
115+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
116+
175117
We need some custom type inference mechanism:
176118

177119
.. code:: python
@@ -197,40 +139,10 @@ Now we can be sure that our ``List[int]`` can be checked in runtime:
197139
>>> assert isinstance([1, 'a'], ListOfInt) is False
198140
>>> assert isinstance([], ListOfInt) is False # empty
199141
200-
And now we can use it with ``classes``:
142+
``delegate`` argument
143+
~~~~~~~~~~~~~~~~~~~~~
201144

202-
.. code:: python
203-
204-
>>> from classes import typeclass
205-
206-
>>> @typeclass
207-
... def sum_all(instance) -> int:
208-
... ...
209-
210-
>>> @sum_all.instance(ListOfInt)
211-
... def _sum_all_list_int(instance: ListOfInt) -> int:
212-
... return sum(instance)
213-
214-
>>> your_list = [1, 2, 3]
215-
>>> if isinstance(your_list, ListOfInt):
216-
... assert sum_all(your_list) == 6
217-
218-
This solution still has several problems:
219-
220-
1. Notice, that you have to use ``if isinstance`` or ``assert isinstance`` here.
221-
Because otherwise ``mypy`` won't be happy without it,
222-
type won't be narrowed to ``ListOfInt`` from ``List[int]``.
223-
This does not feel right.
224-
2. ``ListOfInt`` is very verbose, it even has a metaclass!
225-
3. There's a typing mismatch: in runtime ``your_list`` would be ``List[int]``
226-
and ``mypy`` thinks that it is ``ListOfInt``
227-
(a fake type that we are not ever using directly)
228-
229-
delegate argument
230-
~~~~~~~~~~~~~~~~~
231-
232-
To solve the first problem,
233-
we can use ``delegate=`` argument to ``.instance`` call:
145+
And now we can use it with ``classes``:
234146

235147
.. code:: python
236148
@@ -252,8 +164,8 @@ What happens here? When defining an instance with ``delegate`` argument,
252164
what we really do is: we add our ``delegate``
253165
into a special registry inside ``sum_all`` typeclass.
254166

255-
This registry is using ``isinstance``
256-
to find handler that fit the defined predicate.
167+
This registry is using ``isinstance`` function
168+
to find handler that fits the defined predicate.
257169
It has the highest priority among other dispatch methods.
258170

259171
This allows to sync both runtime and ``mypy`` behavior:
@@ -271,9 +183,9 @@ This allows to sync both runtime and ``mypy`` behavior:
271183
Phantom types
272184
~~~~~~~~~~~~~
273185
274-
To solve problems ``2`` and ``3`` we recommend to use ``phantom-types`` package.
186+
Notice, that ``ListOfInt`` is very verbose, it even has an explicit metaclass!
275187
276-
First, you need to define a "phantom" type
188+
There's a better way, you need to define a "phantom" type
277189
(it is called "phantom" because it does not exist in runtime):
278190
279191
.. code:: python
@@ -306,6 +218,19 @@ Now, we can define our typeclass with ``phantom`` type support:
306218
307219
.. code:: python
308220
221+
>>> from phantom import Phantom
222+
>>> from phantom.predicates import boolean, collection, generic, numeric
223+
224+
>>> class ListOfInt(
225+
... List[int],
226+
... Phantom,
227+
... predicate=boolean.both(
228+
... collection.count(numeric.greater(0)),
229+
... collection.every(generic.of_type(int)),
230+
... ),
231+
... ):
232+
... ...
233+
309234
>>> from classes import typeclass
310235
311236
>>> @typeclass
@@ -325,8 +250,12 @@ we delegate all the runtime type checking to ``ListOfInt`` phantom type.
325250
Performance considerations
326251
~~~~~~~~~~~~~~~~~~~~~~~~~~
327252
253+
Types that are matched via ``__instancecheck__`` are the first one we try.
328254
Traversing the whole list to check that all elements
329255
are of the given type can be really slow.
256+
The worst case complexity of this is ``O(n)``
257+
where ``n`` is the number of types to try.
258+
We also always try them first and do not cache the result.
330259
331260
You might need a different algorithm.
332261
Take a look at `beartype <https://github.com/beartype/beartype>`_.
@@ -335,13 +264,17 @@ with negligible constant factors.
335264
336265
Take a look at their docs to learn more.
337266
267+
We recommend to think at least
268+
twice about the performance side of this feature.
269+
Maybe you can just write a function?
270+
338271
339272
Type resolution order
340273
---------------------
341274
342275
Here's how typeclass resolve types:
343276
344-
1. At first we try to resolve types via delegates and ``isinstance`` checks
277+
1. At first we try to resolve types via delegates and ``isinstance`` check
345278
2. We try to resolve exact match by a passed type
346279
3. Then we try to match passed type with ``isinstance``
347280
against protocol types,

docs/pages/supports.rst

Lines changed: 111 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ Supports
66
We also have a special type to help you specifying
77
that you want to work with only types that are a part of a specific typeclass.
88

9+
.. warning::
10+
``Supports`` only works with typeclasses defined with associated types.
11+
12+
13+
Regular types
14+
-------------
15+
916
For example, you might want to work with only types
1017
that are able to be converted to JSON.
1118

@@ -51,6 +58,10 @@ And this will fail (both in runtime and during type checking):
5158
...
5259
NotImplementedError: Missing matched typeclass instance for type: NoneType
5360

61+
62+
Supports for instance annotations
63+
---------------------------------
64+
5465
You can also use ``Supports`` as a type annotation for defining typeclasses:
5566

5667
.. code:: python
@@ -74,5 +85,103 @@ One more tip, our team would recommend this style:
7485
... class MyFeature(AssociatedType):
7586
... """Tell us, what this typeclass is about."""
7687
77-
.. warning::
78-
``Supports`` only works with typeclasses defined with associated types.
88+
89+
Supports and delegates
90+
----------------------
91+
92+
``Supports`` type has a special handling of ``delegate`` types.
93+
Let's see an example. We would start with defining a ``delegate`` type:
94+
95+
.. code:: python
96+
97+
>>> from typing import List
98+
>>> from classes import AssociatedType, Supports, typeclass
99+
100+
>>> class ListOfIntMeta(type):
101+
... def __instancecheck__(cls, arg) -> bool:
102+
... return (
103+
... isinstance(arg, list) and
104+
... bool(arg) and
105+
... all(isinstance(list_item, int) for list_item in arg)
106+
... )
107+
108+
>>> class ListOfInt(List[int], metaclass=ListOfIntMeta):
109+
... ...
110+
111+
Now, let's define a typeclass:
112+
113+
.. code:: python
114+
115+
>>> class SumAll(AssociatedType):
116+
... ...
117+
118+
>>> @typeclass(SumAll)
119+
... def sum_all(instance) -> int:
120+
... ...
121+
122+
>>> @sum_all.instance(List[int], delegate=ListOfInt)
123+
... def _sum_all_list_int(
124+
... # It can be either `List[int]` or `ListOfInt`
125+
... instance: List[int],
126+
... ) -> int:
127+
... return sum(instance)
128+
129+
And a function with ``Supports`` type:
130+
131+
.. code:: python
132+
133+
>>> def test(to_sum: Supports[SumAll]) -> int:
134+
... return sum_all(to_sum)
135+
136+
This will not make ``mypy`` happy:
137+
138+
.. code:: python
139+
140+
>>> list1 = [1, 2, 3]
141+
>>> assert test(list1) == 6 # Argument 1 to "test" has incompatible type "List[int]"; expected "Supports[SumAll]"
142+
143+
It will be treated the same as unsupported cases, like ``List[str]``:
144+
145+
.. code:: python
146+
147+
list2: List[str]
148+
test(list2) # Argument 1 to "test" has incompatible type "List[int]"; expected "Supports[SumAll]"
149+
150+
But, this will work correctly:
151+
152+
.. code:: python
153+
154+
>>> list_of_int = ListOfInt([1, 2, 3])
155+
>>> assert test(list_of_int) == 6 # ok
156+
157+
>>> list1 = [1, 2, 3]
158+
>>> if isinstance(list1, ListOfInt):
159+
... assert test(list1) == 6 # ok
160+
161+
This happens because we don't treat ``List[int]`` as ``Supports[SumAll]``.
162+
This is by design.
163+
164+
But, we treat ``ListOfInt`` as ``Supports[SumAll]``.
165+
So, you would need to narrow ``List[int]`` to ``ListOfInt`` to make it work.
166+
167+
General cases
168+
~~~~~~~~~~~~~
169+
170+
One way to make ``List[int]`` to work without explicit type narrowing
171+
is to define a generic case for all ``list`` subtypes:
172+
173+
.. code:: python
174+
175+
>>> @sum_all.instance(list)
176+
... def _sum_all_list(instance: list) -> int:
177+
... return 0
178+
179+
Now, this will work:
180+
181+
.. code:: python
182+
183+
>>> list1 = [1, 2, 3]
184+
>>> assert test(list1) == 6 # ok
185+
186+
>>> list2 = ['a', 'b']
187+
>>> assert test(list2) == 0 # ok

tests/test_typeclass/test_cache.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,11 @@ def test_cache_concrete(clear_cache) -> None: # noqa: WPS218
4343
assert not my_typeclass._dispatch_cache # noqa: WPS437
4444

4545
assert my_typeclass(_MyConcrete()) == 1
46-
assert not my_typeclass._dispatch_cache # noqa: WPS437
46+
assert _MyConcrete in my_typeclass._dispatch_cache # noqa: WPS437
4747

4848
_MyABC.register(_MyRegistered)
4949
assert my_typeclass(_MyRegistered()) == 2 # type: ignore
50-
assert not my_typeclass._dispatch_cache # noqa: WPS437
50+
assert _MyRegistered in my_typeclass._dispatch_cache # noqa: WPS437
5151

5252

5353
def test_cached_calls(clear_cache) -> None:

typesafety/test_typeclass/test_generics/test_generics_concrete.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@
8989
class ListOfIntMeta(type):
9090
def __instancecheck__(cls, arg) -> bool:
9191
return (
92-
isinstance(cls, list) and
92+
isinstance(arg, list) and
9393
bool(arg) and
9494
all(isinstance(list_item, int) for list_item in arg)
9595
)
@@ -131,7 +131,7 @@
131131
class ListOfIntMeta(type):
132132
def __instancecheck__(cls, arg) -> bool:
133133
return (
134-
isinstance(cls, list) and
134+
isinstance(arg, list) and
135135
bool(arg) and
136136
all(isinstance(list_item, int) for list_item in arg)
137137
)

0 commit comments

Comments
 (0)