From a86c68dcf9a5a9e22f71c2ca7ca4d79fb9d3dbf8 Mon Sep 17 00:00:00 2001 From: Erez Shinan Date: Tue, 5 Sep 2023 09:48:29 +0300 Subject: [PATCH 1/3] refactor --- runtype/pytypes.py | 150 ++++++++++++++++++++++----------------------- 1 file changed, 74 insertions(+), 76 deletions(-) diff --git a/runtype/pytypes.py b/runtype/pytypes.py index dab4dd8..064e12a 100644 --- a/runtype/pytypes.py +++ b/runtype/pytypes.py @@ -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): @@ -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: From 1a87aa91458a6fa10fb7320d51d17d40d907435e Mon Sep 17 00:00:00 2001 From: Erez Shinan Date: Tue, 5 Sep 2023 09:49:07 +0300 Subject: [PATCH 2/3] Docs: added documentation for mypy support --- README.md | 2 ++ docs/dispatch.rst | 51 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/README.md b/README.md index 8cb2339..aceba3d 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docs/dispatch.rst b/docs/dispatch.rst index 89d6096..4e517b6 100644 --- a/docs/dispatch.rst +++ b/docs/dispatch.rst @@ -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) @@ -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 ----------- From 15bcc22dc03a5004c6010cac1e08fd20e241fa7e Mon Sep 17 00:00:00 2001 From: Erez Shinan Date: Wed, 6 Sep 2023 23:07:39 +0300 Subject: [PATCH 3/3] Small fix for 3.6 --- runtype/pytypes.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/runtype/pytypes.py b/runtype/pytypes.py index 064e12a..d372810 100644 --- a/runtype/pytypes.py +++ b/runtype/pytypes.py @@ -482,6 +482,21 @@ def _to_canon(self, t): # Python 3.6 return to_canon(t.__args__[0]) + 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 None: if isinstance(t, typing.TypeVar): return Any # XXX is this correct? @@ -489,21 +504,6 @@ def _to_canon(self, t): 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