Skip to content

Commit 8842dff

Browse files
authored
Closes #248 (#251)
* Closes #248 * Better example * Fixes CI * Fixes CI * Fixes CI * Fixes CI * Fixes CI
1 parent ecd6533 commit 8842dff

File tree

3 files changed

+56
-6
lines changed

3 files changed

+56
-6
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ We follow Semantic Versions since the `0.1.0` release.
77

88
### Features
99

10-
- Adds support for multiple type arguments in `Supports` type
10+
- Adds support for multiple type arguments in `Supports` type #244
11+
- Adds support for types that have `__instancecheck__` defined #248
1112

1213
### Bugfixes
1314

classes/_typeclass.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@
117117

118118
from abc import get_cache_token
119119
from functools import _find_impl # type: ignore # noqa: WPS450
120+
from types import MethodType
120121
from typing import ( # noqa: WPS235
121122
TYPE_CHECKING,
122123
Callable,
@@ -499,6 +500,8 @@ def instance(
499500
self,
500501
type_argument: Optional[_NewInstanceType],
501502
*,
503+
# TODO: at one point I would like to remove `is_protocol`
504+
# and make this function decide whether this type is protocol or not.
502505
is_protocol: bool = False,
503506
) -> '_TypeClassInstanceDef[_NewInstanceType, _TypeClassType]':
504507
"""
@@ -507,21 +510,26 @@ def instance(
507510
The only setting we provide is ``is_protocol`` which is required
508511
when passing protocols. See our ``mypy`` plugin for that.
509512
"""
510-
if type_argument is None: # `None` is a special case
511-
type_argument = type(None) # type: ignore
513+
typ = type_argument or type(None) # `None` is a special case
512514

513515
# That's how we check for generics,
514516
# generics that look like `List[int]` or `set[T]` will fail this check,
515517
# because they are `_GenericAlias` instance,
516518
# which raises an exception for `__isinstancecheck__`
517-
isinstance(object(), type_argument) # type: ignore
519+
isinstance(object(), typ)
518520

519521
def decorator(implementation):
520522
container = self._protocols if is_protocol else self._instances
521-
container[type_argument] = implementation # type: ignore
523+
container[typ] = implementation
524+
525+
if isinstance(getattr(typ, '__instancecheck__', None), MethodType):
526+
# This means that this type has `__instancecheck__` defined,
527+
# which allows dynamic checks of what `isinstance` of this type.
528+
# That's why we also treat this type as a protocol.
529+
self._protocols[typ] = implementation
522530

523531
if self._cache_token is None: # pragma: no cover
524-
if getattr(type_argument, '__abstractmethods__', None):
532+
if getattr(typ, '__abstractmethods__', None):
525533
self._cache_token = get_cache_token()
526534

527535
self._dispatch_cache.clear()

docs/pages/concept.rst

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,47 @@ 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+
.. note::
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+
98139
Type resolution order
99140
---------------------
100141

0 commit comments

Comments
 (0)