Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor + added documentation for mypy support to dispatch #37

Merged
merged 3 commits into from
Sep 6, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ It is:

- :star: [**dispatch**](https://runtype.readthedocs.io/en/latest/dispatch.html) - Provides fast multiple-dispatch for functions and methods, via a decorator.

- Full specificity resolution
- Mypy support: Can be used with @overload decorator
- Inspired by Julia.

- :star: [**type utilities**](https://runtype.readthedocs.io/en/latest/types.html) - Provides a set of classes to implement your own type-system.
Expand Down
51 changes: 51 additions & 0 deletions docs/dispatch.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ Dispatch

Provides a decorator that enables multiple-dispatch for functions.

Features:

- Full specificity resolution

- Mypy support: Can be used with @overload decorator

(Inspired by Julia)


Expand Down Expand Up @@ -185,6 +191,51 @@ Another example:

Dispatch chooses the right function based on the idea specificity, which means that `class MyStr(str)` is more specific than `str`, and so on: `MyStr(str) < str < Union[int, str] < object`.

MyPy support (@overload)
------------------------

Dispatch can be made to work with the overload decorator, aiding in granular type resolution.

However, due to the limited design of the overload decorator, there are several rules that need to be followed, and limitations that should be considered.

1. The overload decorator must be placed above the dispatch decorator.

1. The last dispatched function of each function group, must be written without type declarations, and without the overload decorator. It is recommended to use this function for error handling.

3. Mypy doesn't support all of the functionality of Runtype's dispatch, such as full specificity resolution. Therefore, some valid dispatch constructs will produce an error in mypy.


Example usage:

::

from runtype import Dispatch
from typing import overload
dp = Dispatch()

@overload
@dp
def join(seq, sep: str = ''):
return sep.join(str(s) for s in seq)

@overload
@dp
def join(seq, sep: list):
return join(join(sep, str(s)) for s in seq)

@dp
def join(seq, sep):
raise NotImplementedError()

# Calling join() with the wrong types -
join(1,2) # At runtime, raises NotImplementedError

# Mypy generates the following report:
# error: No overload variant of "join" matches argument types "int", "int" [call-overload]
# note: Possible overload variants:
# note: def join(seq: Any, sep: str = ...) -> Any
# note: def join(seq: Any, sep: list[Any]) -> Any


Performance
-----------
Expand Down
150 changes: 74 additions & 76 deletions runtype/pytypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ class CastFailed(TypeMismatchError):


class PythonType(base_types.Type, Validator):
pass

def cast_from(self, obj):
raise NotImplementedError()


class Constraint(base_types.Constraint):
Expand Down Expand Up @@ -467,89 +467,87 @@ def _to_canon(self, t):
if isinstance(t, tuple):
return SumType([to_canon(x) for x in t])

if hasattr(types, 'UnionType') and isinstance(t, types.UnionType):
res = [to_canon(x) for x in t.__args__]
return SumType(res)

origin = getattr(t, '__origin__', None)
if hasattr(typing, '_AnnotatedAlias') and isinstance(t, typing._AnnotatedAlias):
return to_canon(t.__origin__)
return to_canon(origin)

if typing_extensions:
if hasattr(typing_extensions, '_AnnotatedAlias') and isinstance(t, typing_extensions._AnnotatedAlias):
return to_canon(t.__origin__)
return to_canon(origin)
elif hasattr(typing_extensions, 'AnnotatedMeta') and isinstance(t, typing_extensions.AnnotatedMeta):
# Python 3.6
return to_canon(t.__args__[0])

if hasattr(types, 'UnionType') and isinstance(t, types.UnionType):
res = [to_canon(x) for x in t.__args__]
if origin is None:
if isinstance(t, typing.TypeVar):
return Any # XXX is this correct?

return PythonDataType(t)

args = getattr(t, '__args__', None)
if args is None:
if t is typing.List:
return List
elif t is typing.Dict:
return Dict
elif t is typing.Set:
return Set
elif t is typing.FrozenSet:
return FrozenSet
elif t is typing.Tuple:
return Tuple
elif t is typing.Mapping: # 3.6
return Mapping
elif t is typing.Sequence:
return Sequence

if origin is origin_list:
x ,= args
return List[to_canon(x)]
elif origin is origin_set:
x ,= args
return Set[to_canon(x)]
elif origin is origin_frozenset:
x ,= args
return FrozenSet[to_canon(x)]
elif origin is origin_dict:
k, v = args
return Dict[to_canon(k), to_canon(v)]
elif origin is origin_tuple:
if not args:
return Tuple
if Ellipsis in args:
if len(args) != 2 or args[0] == Ellipsis:
raise ValueError("Tuple with '...'' expected to be of the exact form: tuple[t, ...].")
return TupleEllipsis[to_canon(args[0])]

return ProductType([to_canon(x) for x in args])

elif origin is typing.Union:
res = [to_canon(x) for x in args]
return SumType(res)

try:
t.__origin__
except AttributeError:
pass
else:
if getattr(t, '__args__', None) is None:
if t is typing.List:
return List
elif t is typing.Dict:
return Dict
elif t is typing.Set:
return Set
elif t is typing.FrozenSet:
return FrozenSet
elif t is typing.Tuple:
return Tuple
elif t is typing.Mapping: # 3.6
return Mapping
elif t is typing.Sequence:
return Sequence

if t.__origin__ is origin_list:
x ,= t.__args__
return List[to_canon(x)]
elif t.__origin__ is origin_set:
x ,= t.__args__
return Set[to_canon(x)]
elif t.__origin__ is origin_frozenset:
x ,= t.__args__
return FrozenSet[to_canon(x)]
elif t.__origin__ is origin_dict:
k, v = t.__args__
return Dict[to_canon(k), to_canon(v)]
elif t.__origin__ is origin_tuple:
if not t.__args__:
return Tuple
if Ellipsis in t.__args__:
if len(t.__args__) != 2 or t.__args__[0] == Ellipsis:
raise ValueError("Tuple with '...'' expected to be of the exact form: tuple[t, ...].")
return TupleEllipsis[to_canon(t.__args__[0])]

return ProductType([to_canon(x) for x in t.__args__])

elif t.__origin__ is typing.Union:
res = [to_canon(x) for x in t.__args__]
return SumType(res)
elif t.__origin__ is abc.Callable or t is typing.Callable:
# return Callable[ProductType(to_canon(x) for x in t.__args__)]
return Callable # TODO
elif py38 and t.__origin__ is typing.Literal:
return OneOf(t.__args__)
elif t.__origin__ is abc.Mapping or t.__origin__ is typing.Mapping:
k, v = t.__args__
return Mapping[to_canon(k), to_canon(v)]
elif t.__origin__ is abc.Sequence or t.__origin__ is typing.Sequence:
x ,= t.__args__
return Sequence[to_canon(x)]
elif t.__origin__ is type or t.__origin__ is typing.Type:
# TODO test issubclass on t.__args__
return PythonDataType(type)

raise NotImplementedError("No support for type:", t)

if isinstance(t, typing.TypeVar):
return Any # XXX is this correct?

return PythonDataType(t)

def to_canon(self, t):
elif origin is abc.Callable or t is typing.Callable:
# return Callable[ProductType(to_canon(x) for x in t.__args__)]
return Callable # TODO
elif py38 and origin is typing.Literal:
return OneOf(args)
elif origin is abc.Mapping or origin is typing.Mapping:
k, v = args
return Mapping[to_canon(k), to_canon(v)]
elif origin is abc.Sequence or origin is typing.Sequence:
x ,= args
return Sequence[to_canon(x)]
elif origin is type or origin is typing.Type:
# TODO test issubclass on t.__args__
return PythonDataType(type)

raise NotImplementedError("No support for type:", t)

def to_canon(self, t) -> PythonType:
try:
return self.cache[t]
except KeyError:
Expand Down
Loading