Skip to content

Commit

Permalink
Use dispatch registration for custom instance checks.
Browse files Browse the repository at this point in the history
`typing.Sized` becomes `Generic` when subclassed, despite not supporting subscripts. Rather than relying on attribute inspection, allowing custom types to register with a dispatch function should - fittingly - avoid these metaclass idiosyncrasies.
  • Loading branch information
coady committed Feb 25, 2024
1 parent 84fe832 commit 4e22af5
Show file tree
Hide file tree
Showing 3 changed files with 14 additions and 9 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ method[type, ...] # get registered function
method[type, ...] = func # register function by explicit types
```

Multimethods support any types that satisfy the `issubclass` relation, including abstract base classes in `collections.abc` and `typing`. Subscripted generics are supported:
Multimethods support any types that satisfy the `issubclass` relation, including abstract base classes in `collections.abc`. Note `typing` aliases do not support `issubclass` consistently, and are no longer needed for subscripts. Using ABCs instead is recommended. Subscripted generics are supported:
* `Union[...]` or `... | ...`
* `Mapping[...]` - the first key-value pair is checked
* `tuple[...]` - all args are checked
Expand Down Expand Up @@ -117,7 +117,7 @@ for subclass in (float, list, list[float], tuple[int]):
assert not issubclass(subclass, cls)
```

If a type implements a custom `__instancecheck__`, it can opt-in to dispatch (without caching) by specifying `__orig_bases__` . `parametric` provides a convenient constructor, with support for predicate functions and checking attributes.
If a type implements a custom `__instancecheck__`, it can opt-in to dispatch (without caching) by registering its metaclass and bases with `subtype.origins`. `parametric` provides a convenient constructor, with support for predicate functions and checking attributes.

```python
from multimethod import parametric
Expand Down
16 changes: 10 additions & 6 deletions multimethod/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,12 @@ def __instancecheck__(self, instance):
instance = itertools.islice(instance, 1)
return all(map(isinstance, instance, self.__args__))

def origins(self) -> Iterator[type]:
"""Generate origins which would need subscript checking."""
@functools.singledispatch
def origins(self) -> Iterable[type]:
"""Return origin types which would require instance checks.
Provisional custom usage: `subtype.origins.register(<metaclass>, lambda cls: ...)
"""
origin = get_origin(self)
if origin is Literal:
yield from set(map(type, self.__args__))
Expand All @@ -135,8 +139,6 @@ def origins(self) -> Iterator[type]:
yield from subtype.origins(arg)
elif origin is not None:
yield origin
elif isinstance(self.__instancecheck__, types.MethodType):
yield from getattr(self, '__orig_bases__', ())


class parametric(abc.ABCMeta):
Expand All @@ -151,8 +153,7 @@ class parametric(abc.ABCMeta):
def __new__(cls, base: type, *funcs: Callable, **attrs):
return super().__new__(cls, base.__name__, (base,), {'funcs': funcs, 'attrs': attrs})

def __init__(self, *_, **__):
self.__orig_bases__ = self.__bases__
def __init__(self, *_, **__): ...

def __subclasscheck__(self, subclass):
missing = object()
Expand All @@ -176,6 +177,9 @@ def __and__(self, other):
return type(self)(base, *set(self.funcs + other.funcs), **(self.attrs | other.attrs))


subtype.origins.register(parametric, lambda cls: cls.__bases__)


def distance(cls, subclass: type) -> int:
"""Return estimated distance between classes for tie-breaking."""
if get_origin(cls) is Union:
Expand Down
3 changes: 2 additions & 1 deletion tests/test_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import types
import pytest
from collections.abc import Collection, Iterable, Mapping, Set
from typing import Any, AnyStr, NewType, Protocol, TypeVar, Union
from typing import Any, AnyStr, NewType, Protocol, Sized, TypeVar, Union
from multimethod import DispatchError, multimeta, multimethod, signature, subtype


Expand Down Expand Up @@ -72,6 +72,7 @@ def test_subtype():
base = subclass(metaclass=subclass(type))
assert subtype(Union[base, subclass(base)])
assert not list(subtype.origins(subclass(subclass(Protocol))))
assert not list(subtype.origins(subclass(Sized)))


def test_signature():
Expand Down

0 comments on commit 4e22af5

Please sign in to comment.