diff --git a/CHANGELOG.md b/CHANGELOG.md index a38ae3d..78f1e64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,8 @@ We follow Semantic Versions since the `0.1.0` release. ### Features -- Adds support for multiple type arguments in `Supports` type +- Adds support for multiple type arguments in `Supports` type #244 +- Adds support for types that have `__instancecheck__` defined #248 ### Bugfixes diff --git a/classes/_typeclass.py b/classes/_typeclass.py index cff27fa..b48273a 100644 --- a/classes/_typeclass.py +++ b/classes/_typeclass.py @@ -117,6 +117,7 @@ from abc import get_cache_token from functools import _find_impl # type: ignore # noqa: WPS450 +from types import MethodType from typing import ( # noqa: WPS235 TYPE_CHECKING, Callable, @@ -499,6 +500,8 @@ def instance( self, type_argument: Optional[_NewInstanceType], *, + # TODO: at one point I would like to remove `is_protocol` + # and make this function decide whether this type is protocol or not. is_protocol: bool = False, ) -> '_TypeClassInstanceDef[_NewInstanceType, _TypeClassType]': """ @@ -507,21 +510,26 @@ def instance( The only setting we provide is ``is_protocol`` which is required when passing protocols. See our ``mypy`` plugin for that. """ - if type_argument is None: # `None` is a special case - type_argument = type(None) # type: ignore + typ = type_argument or type(None) # `None` is a special case # That's how we check for generics, # generics that look like `List[int]` or `set[T]` will fail this check, # because they are `_GenericAlias` instance, # which raises an exception for `__isinstancecheck__` - isinstance(object(), type_argument) # type: ignore + isinstance(object(), typ) def decorator(implementation): container = self._protocols if is_protocol else self._instances - container[type_argument] = implementation # type: ignore + container[typ] = implementation + + if isinstance(getattr(typ, '__instancecheck__', None), MethodType): + # This means that this type has `__instancecheck__` defined, + # which allows dynamic checks of what `isinstance` of this type. + # That's why we also treat this type as a protocol. + self._protocols[typ] = implementation if self._cache_token is None: # pragma: no cover - if getattr(type_argument, '__abstractmethods__', None): + if getattr(typ, '__abstractmethods__', None): self._cache_token = get_cache_token() self._dispatch_cache.clear() diff --git a/docs/pages/concept.rst b/docs/pages/concept.rst index ffeb7c4..9a60757 100644 --- a/docs/pages/concept.rst +++ b/docs/pages/concept.rst @@ -95,6 +95,47 @@ to be specified on ``.instance()`` call: >>> assert to_json([1, 'a', None]) == '[1, "a", null]' +``__instancecheck__`` magic method +---------------------------------- + +We also support types that have ``__instancecheck__`` magic method defined, +like `phantom-types `_. + +We treat them similar to ``Protocol`` types, by checking passed values +with ``isinstance`` for each type with ``__instancecheck__`` defined. +First match wins. + +Example: + +.. code:: python + + >>> from classes import typeclass + + >>> class Meta(type): + ... def __instancecheck__(self, other) -> bool: + ... return other == 1 + + >>> class Some(object, metaclass=Meta): + ... ... + + >>> @typeclass + ... def some(instance) -> int: + ... ... + + >>> @some.instance(Some) + ... def _some_some(instance: Some) -> int: + ... return 2 + + >>> argument = 1 + >>> assert isinstance(argument, Some) + >>> assert some(argument) == 2 + +.. note:: + + It is impossible for ``mypy`` to understand that ``1`` has ``Some`` + type in this example. Be careful, it might break your code! + + Type resolution order ---------------------