diff --git a/CHANGELOG.md b/CHANGELOG.md index 8998b2f..eecdf62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ We follow Semantic Versions since the `0.1.0` release. +## Version 0.3.0 WIP + +### Features + + + ## Version 0.2.0 ### Features diff --git a/classes/__init__.py b/classes/__init__.py index bb402d0..98bc4a1 100644 --- a/classes/__init__.py +++ b/classes/__init__.py @@ -5,5 +5,6 @@ so mypy's ``implicit_reexport`` rule will be happy. """ +from classes._typeclass import AssociatedType as AssociatedType from classes._typeclass import Supports as Supports from classes._typeclass import typeclass as typeclass diff --git a/classes/_typeclass.py b/classes/_typeclass.py index 99fb1f6..6e01121 100644 --- a/classes/_typeclass.py +++ b/classes/_typeclass.py @@ -1,194 +1,192 @@ -from typing import ( - Callable, - Dict, - Generic, - NoReturn, - Type, - TypeVar, - Union, - overload, -) +""" +Typeclasses for Python. -from typing_extensions import Literal, final +.. rubric:: Basic usage -_TypeClassType = TypeVar('_TypeClassType') -_ReturnType = TypeVar('_ReturnType') -_CallbackType = TypeVar('_CallbackType', bound=Callable) -_InstanceType = TypeVar('_InstanceType') -_DefinitionType = TypeVar('_DefinitionType', bound=Type) +The first and the simplest example of a typeclass is just its definition: +.. code:: python -def typeclass( - signature: _CallbackType, - # By default `_TypeClassType` and `_ReturnType` are `nothing`, - # but we enhance them via mypy plugin later: -) -> '_TypeClass[_TypeClassType, _ReturnType, _CallbackType, _DefinitionType]': - """ - Function to define typeclasses. + >>> from classes import typeclass - .. rubric:: Basic usage + >>> @typeclass + ... def example(instance) -> str: + ... '''Example typeclass.''' - The first and the simplest example of a typeclass is just its definition: + >>> example(1) + Traceback (most recent call last): + ... + NotImplementedError: Missing matched typeclass instance for type: int - .. code:: python +In this example we work with the default implementation of a typeclass. +It raises a ``NotImplementedError`` when no instances match. +And we don't yet have a special case for ``int``, +that why we fallback to the default implementation. - >>> from classes import typeclass - - >>> @typeclass - ... def example(instance) -> str: - ... '''Example typeclass.''' - - >>> example(1) - Traceback (most recent call last): - ... - NotImplementedError: Missing matched typeclass instance for type: int +It works almost like a regular function right now. +Let's do the next step and introduce +the ``int`` instance for our typeclass: - In this example we work with the default implementation of a typeclass. - It raise a ``NotImplementedError`` when no instances match. - And we don't yet have a special case for ``int``, - that why we fallback to the default implementation. +.. code:: python - It works like a regular function right now. - Let's do the next step and introduce - the ``int`` instance for the typeclass: + >>> @example.instance(int) + ... def _example_int(instance: int) -> str: + ... return 'int case' - .. code:: python + >>> assert example(1) == 'int case' - >>> @example.instance(int) - ... def _example_int(instance: int) -> str: - ... return 'int case' +Now we have a specific instance for ``int`` +which does something different from the default implementation. - >>> assert example(1) == 'int case' +What will happen if we pass something new, like ``str``? - Now we have a specific instance for ``int`` - which does something different from the default implementation. +.. code:: python - What will happen if we pass something new, like ``str``? + >>> example('a') + Traceback (most recent call last): + ... + NotImplementedError: Missing matched typeclass instance for type: str - .. code:: python +Because again, we don't yet have +an instance of this typeclass for ``str`` type. +Let's fix that. - >>> example('a') - Traceback (most recent call last): - ... - NotImplementedError: Missing matched typeclass instance for type: str +.. code:: python - Because again, we don't yet have - an instance of this typeclass for ``str`` type. - Let's fix that. + >>> @example.instance(str) + ... def _example_str(instance: str) -> str: + ... return instance - .. code:: python + >>> assert example('a') == 'a' - >>> @example.instance(str) - ... def _example_str(instance: str) -> str: - ... return instance +Now it works with ``str`` as well. But differently. +This allows developer to base the implementation on type information. - >>> assert example('a') == 'a' +So, the rule is clear: +if we have a typeclass instance for a specific type, +then it will be called, +otherwise the default implementation will be called instead. - Now it works with ``str`` as well. But differently. - This allows developer to base the implementation on type information. +.. rubric:: Protocols - So, the rule is clear: - if we have a typeclass instance for a specific type, - then it will be called, - otherwise the default implementation will be called instead. +We also support protocols. It has the same limitation as ``Generic`` types. +It is also dispatched after all regular instances are checked. - You can also use ``.instance`` with just annotation for better readability: +To work with protocols, one needs to pass ``is_protocol`` flag to instance: - .. code:: python +.. code:: python - >>> @example.instance - ... def _example_float(instance: float) -> str: - ... return 0.5 + >>> from typing import Sequence - >>> assert example(5.1) == 0.5 + >>> @example.instance(Sequence, is_protocol=True) + ... def _sequence_example(instance: Sequence) -> str: + ... return ','.join(str(item) for item in instance) - .. rubric:: Generics + >>> assert example([1, 2, 3]) == '1,2,3' - We also support generic, but the support is limited. - We cannot rely on type parameters of the generic type, - only on the base generic class: +But, ``str`` will still have higher priority over ``Sequence``: - .. code:: python +.. code:: python - >>> from typing import Generic, TypeVar + >>> assert example('abc') == 'abc' - >>> T = TypeVar('T') +We also support user-defined protocols: - >>> class MyGeneric(Generic[T]): - ... def __init__(self, arg: T) -> None: - ... self.arg = arg +.. code:: python - Now, let's define the typeclass instance for this type: + >>> from typing_extensions import Protocol - .. code:: python + >>> class CustomProtocol(Protocol): + ... field: str - >>> @example.instance(MyGeneric) - ... def _my_generic_example(instance: MyGeneric) -> str: - ... return 'generi' + str(instance.arg) + >>> @example.instance(CustomProtocol, is_protocol=True) + ... def _custom_protocol_example(instance: CustomProtocol) -> str: + ... return instance.field - >>> assert example(MyGeneric('c')) == 'generic' +Now, let's build a class that match this protocol and test it: - This case will work for all type parameters of ``MyGeneric``, - or in other words it can be assumed as ``MyGeneric[Any]``: +.. code:: python - .. code:: python + >>> class WithField(object): + ... field: str = 'with field' - >>> assert example(MyGeneric(1)) == 'generi1' + >>> assert example(WithField()) == 'with field' - In the future, when Python will have new type mechanisms, - we would like to improve our support for specific generic instances - like ``MyGeneric[int]`` only. But, that's the best we can do for now. +""" - .. rubric:: Protocols +from typing import ( # noqa: WPS235 + TYPE_CHECKING, + Callable, + Dict, + Generic, + Type, + TypeVar, + Union, + overload, +) - We also support protocols. It has the same limitation as ``Generic`` types. - It is also dispatched after all regular instances are checked. +from typing_extensions import final - To work with protocols, one needs to pass ``is_protocol`` flag to instance: +_InstanceType = TypeVar('_InstanceType') +_SignatureType = TypeVar('_SignatureType', bound=Callable) +_AssociatedType = TypeVar('_AssociatedType') +_Fullname = TypeVar('_Fullname', bound=str) # Literal value - .. code:: python +_NewInstanceType = TypeVar('_NewInstanceType', bound=Type) - >>> from typing import Sequence - >>> @example.instance(Sequence, is_protocol=True) - ... def _sequence_example(instance: Sequence) -> str: - ... return ','.join(str(item) for item in instance) +_StrictAssociatedType = TypeVar('_StrictAssociatedType', bound='AssociatedType') +_TypeClassType = TypeVar('_TypeClassType', bound='_TypeClass') +_ReturnType = TypeVar('_ReturnType') - >>> assert example([1, 2, 3]) == '1,2,3' - But, ``str`` will still have higher priority over ``Sequence``: +@overload +def typeclass( + definition: Type[_AssociatedType], +) -> '_TypeClassDef[_AssociatedType]': + """Function to created typeclasses with associated types.""" - .. code:: python - >>> assert example('abc') == 'abc' +@overload +def typeclass( + signature: _SignatureType, + # By default almost all variables are `nothing`, + # but we enhance them via mypy plugin later: +) -> '_TypeClass[_InstanceType, _SignatureType, _AssociatedType, _Fullname]': + """Function to define typeclasses with just functions.""" - We also support user-defined protocols: - .. code:: python +def typeclass(signature): + """General case function to create typeclasses.""" + if isinstance(signature, type): + return _TypeClass # It means, that it has a associated type with it + return _TypeClass(signature) # In this case it is a regular function - >>> from typing_extensions import Protocol - >>> class CustomProtocol(Protocol): - ... field: str - >>> @example.instance(CustomProtocol, is_protocol=True) - ... def _custom_protocol_example(instance: CustomProtocol) -> str: - ... return instance.field +class AssociatedType(object): + """ + Base class for all associated types. - Now, let's build a class that match this protocol and test it: + How to use? Just import and subclass it: .. code:: python - >>> class WithField(object): - ... field: str = 'with field' + >>> from classes import AssociatedType, typeclass - >>> assert example(WithField()) == 'with field' + >>> class Example(AssociatedType): + ... ... - Remember, that generic protocols have the same limitation as generic types. + >>> @typeclass(Example) + ... def example(instance) -> str: + ... ... + Right now it does nothing in runtime, but this can change in the future. """ - return _TypeClass(signature) + + __slots__ = () -class Supports(Generic[_CallbackType]): +@final +class Supports(Generic[_StrictAssociatedType]): """ Used to specify that some value is a part of a typeclass. @@ -199,10 +197,11 @@ class Supports(Generic[_CallbackType]): >>> from classes import typeclass, Supports >>> class ToJson(object): - ... def __call__(self, instance) -> str: - ... ... + ... ... - >>> to_json = typeclass(ToJson) + >>> @typeclass(ToJson) + ... def to_json(instance) -> str: + ... ... >>> @to_json.instance(int) ... def _to_json_int(instance: int) -> str: @@ -232,14 +231,16 @@ class Supports(Generic[_CallbackType]): # (expression has type "str", variable has type "Supports[ToJson]") .. warning:: - ``Supports`` only works with typeclasses defined as types. + ``Supports`` only works with typeclasses defined with associated types. """ + __slots__ = () + @final class _TypeClass( - Generic[_TypeClassType, _ReturnType, _CallbackType, _DefinitionType], + Generic[_InstanceType, _SignatureType, _AssociatedType, _Fullname], ): """ That's how we represent typeclasses. @@ -275,7 +276,7 @@ class _TypeClass( __slots__ = ('_instances', '_protocols') - def __init__(self, signature: _CallbackType) -> None: + def __init__(self, signature: _SignatureType) -> None: """ Protected constructor of the typeclass. @@ -298,7 +299,7 @@ def __init__(self, signature: _CallbackType) -> None: the same return type for all cases. Only modified once during ``@typeclass`` creation - - ``_CallbackType`` is used to ensure that all parameters + - ``_SignatureType`` is used to ensure that all parameters for all type cases are the same. That's how we enforce consistency in all function signatures. The only exception is the first argument: it is polymorfic. @@ -309,7 +310,10 @@ def __init__(self, signature: _CallbackType) -> None: def __call__( self, - instance: Union[_TypeClassType, Supports[_DefinitionType]], + instance: Union[ # type: ignore + _InstanceType, + Supports[_AssociatedType], + ], *args, **kwargs, ) -> _ReturnType: @@ -393,63 +397,18 @@ def supports( instance_type in self._protocols ) - @overload - def instance( - self, - type_argument: Type[_InstanceType], - *, - is_protocol: Literal[False] = ..., - ) -> Callable[ - [Callable[[_InstanceType], _ReturnType]], - NoReturn, # We need this type to disallow direct instance calls - ]: - """Case for regular typeclasses.""" - - @overload - def instance( - self, - type_argument: Callable[[_InstanceType], _ReturnType], - ) -> NoReturn: - """Case for typeclasses that are defined by annotation only.""" - - @overload - def instance( - self, - type_argument, - *, - is_protocol: Literal[True], - ) -> Callable[ - [Callable[[_InstanceType], _ReturnType]], - NoReturn, # We need this type to disallow direct instance calls - ]: - """Case for protocol based typeclasses.""" - def instance( self, - type_argument, + type_argument: _NewInstanceType, *, is_protocol: bool = False, - ): + ) -> '_TypeClassInstanceDef[_NewInstanceType, _TypeClassType]': """ We use this method to store implementation for each specific type. The only setting we provide is ``is_protocol`` which is required - when passing protocols. - That's why we also have this ugly ``@overload`` cases. - Otherwise, ``Protocol`` instances - would not match ``Type[_InstanceType]`` type due to ``mypy`` rules. - + when passing protocols. See our ``mypy`` plugin for that. """ - original_handler = None - if not is_protocol: - # If it is not a protocol, we can try to get an annotation from - annotations = getattr(type_argument, '__annotations__', None) - if annotations: - original_handler = type_argument - type_argument = annotations[ - type_argument.__code__.co_varnames[0] # noqa: WPS609 - ] - # 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, @@ -460,7 +419,51 @@ def decorator(implementation): container = self._protocols if is_protocol else self._instances container[type_argument] = implementation return implementation - - if original_handler is not None: - return decorator(original_handler) # type: ignore return decorator + + +if TYPE_CHECKING: + from typing_extensions import Protocol + + class _TypeClassDef(Protocol[_AssociatedType]): + """ + Callable protocol to help us with typeclass definition. + + This protocol does not exist in real life, + we just need it because we use it in ``mypy`` plugin. + That's why we define it under ``if TYPE_CHECKING:``. + It should not be used directly. + + See ``TypeClassDefReturnType`` for more information. + """ + + def __call__( + self, + signature: _SignatureType, + ) -> _TypeClass[ + _InstanceType, + _SignatureType, + _AssociatedType, + _Fullname, + ]: + """It can be called, because in real life it is a function.""" + + class _TypeClassInstanceDef( # type: ignore + Protocol[_InstanceType, _TypeClassType], + ): + """ + Callable protocol to help us with typeclass instance callbacks. + + This protocol does not exist in real life, + we just need it because we use it in ``mypy`` plugin. + That's why we define it under ``if TYPE_CHECKING:``. + It should not be used directly. + + See ``InstanceDefReturnType`` for more information. + + One more important thing here: we fill its type vars inside our plugin, + so, don't even care about its definition. + """ + + def __call__(self, callback: _SignatureType) -> _SignatureType: + """It can be called, because in real life it is a function.""" diff --git a/classes/contrib/mypy/classes_plugin.py b/classes/contrib/mypy/classes_plugin.py index 0d4e31d..f52b161 100644 --- a/classes/contrib/mypy/classes_plugin.py +++ b/classes/contrib/mypy/classes_plugin.py @@ -17,356 +17,31 @@ """ -from typing import Callable, Optional, Type, Union +from typing import Callable, Optional, Type -from mypy.nodes import ARG_POS, Decorator, MemberExpr from mypy.plugin import FunctionContext, MethodContext, MethodSigContext, Plugin -from mypy.typeops import bind_self -from mypy.types import AnyType, CallableType, Instance +from mypy.types import CallableType from mypy.types import Type as MypyType -from mypy.types import ( - TypeOfAny, - TypeVarType, - UninhabitedType, - UnionType, - get_proper_type, -) -from typing_extensions import final - - -@final -class _AdjustArguments(object): - """ - Adjust argument types when we define typeclasses via ``typeclass`` function. - - It has two modes: - 1. As a decorator ``@typeclass`` - 2. As a regular call with a class definition: ``typeclass(SomeProtocol)`` - - It also checks how typeclasses are defined. - """ - - def __call__(self, ctx: FunctionContext) -> MypyType: - defn = ctx.arg_types[0][0] - is_defined_by_class = ( - isinstance(defn, CallableType) and - not defn.arg_types and - isinstance(defn.ret_type, Instance) - ) - - if is_defined_by_class: - return self._adjust_protocol_arguments(ctx) - elif isinstance(defn, CallableType): - return self._adjust_function_arguments(ctx) - return ctx.default_return_type - - def _adjust_protocol_arguments(self, ctx: FunctionContext) -> MypyType: - assert isinstance(ctx.arg_types[0][0], CallableType) - assert isinstance(ctx.arg_types[0][0].ret_type, Instance) - - instance = ctx.arg_types[0][0].ret_type - type_info = instance.type - signature = type_info.get_method('__call__') - if not signature: - ctx.api.fail( - 'Typeclass definition must have `__call__` method', - ctx.context, - ) - return AnyType(TypeOfAny.from_error) - - signature_type = get_proper_type(signature.type) - assert isinstance(signature_type, CallableType) - return self._adjust_typeclass( - bind_self(signature_type), - ctx, - class_definition=instance, - ) - - def _adjust_function_arguments(self, ctx: FunctionContext) -> MypyType: - assert isinstance(ctx.default_return_type, Instance) - - typeclass_def = ctx.default_return_type.args[2] - assert isinstance(typeclass_def, CallableType) - return self._adjust_typeclass(typeclass_def, ctx) - - def _adjust_typeclass( - self, - typeclass_def: MypyType, - ctx: FunctionContext, - class_definition: Optional[Instance] = None, - ) -> MypyType: - assert isinstance(typeclass_def, CallableType) - assert isinstance(ctx.default_return_type, Instance) - - typeclass_def.arg_types[0] = UninhabitedType() - - args = [ - typeclass_def.arg_types[0], - typeclass_def.ret_type, - ] - - definition_type = ( - class_definition - if class_definition - else UninhabitedType() - ) +from typing_extensions import Final, final - ctx.default_return_type.args = (*args, typeclass_def, definition_type) - return ctx.default_return_type +from classes.contrib.mypy.features import typeclass - -def _adjust_call_signature(ctx: MethodSigContext) -> CallableType: - """Returns proper ``__call__`` signature of a typeclass.""" - assert isinstance(ctx.type, Instance) - - real_signature = ctx.type.args[2] - if not isinstance(real_signature, CallableType): - return ctx.default_signature - - real_signature.arg_types[0] = ctx.type.args[0] - - if isinstance(ctx.type.args[3], Instance): - supports_spec = _load_supports_type(ctx.type.args[3], ctx) - real_signature.arg_types[0] = UnionType.make_union([ - real_signature.arg_types[0], - supports_spec, - ]) - - return real_signature +_TYPECLASS_FULLNAME: Final = 'classes._typeclass._TypeClass' +_TYPECLASS_DEF_FULLNAME: Final = 'classes._typeclass._TypeClassDef' +_TYPECLASS_INSTANCE_DEF_FULLNAME: Final = ( + 'classes._typeclass._TypeClassInstanceDef' +) @final -class _AdjustInstanceSignature(object): - """ - Adjusts the typing signature after ``.instance(type)`` call. - - We need this to get typing match working: - so the type mentioned in ``.instance()`` call - will be the same as the one in a function later on. - """ - - def __call__(self, ctx: MethodContext) -> MypyType: - if not isinstance(ctx.type, Instance): - return ctx.default_return_type - if not isinstance(ctx.default_return_type, CallableType): - return ctx.default_return_type - - instance_type = self._adjust_typeclass_callable(ctx) - self._adjust_typeclass_type(ctx, instance_type) - if isinstance(instance_type, Instance): - self._add_supports_metadata(ctx, instance_type) - return ctx.default_return_type - - @classmethod - def from_function_decorator(cls, ctx: FunctionContext) -> MypyType: - """ - It is used when ``.instance`` is used without params as a decorator. - - Like: - - .. code:: python - - @some.instance - def _some_str(instance: str) -> str: - ... - - """ - is_decorator = ( - isinstance(ctx.context, Decorator) and - len(ctx.context.decorators) == 1 and - isinstance(ctx.context.decorators[0], MemberExpr) and - ctx.context.decorators[0].name == 'instance' - ) - if not is_decorator: - return ctx.default_return_type - - passed_function = ctx.arg_types[0][0] - assert isinstance(passed_function, CallableType) - - if not passed_function.arg_types: - return ctx.default_return_type - - annotation_type = passed_function.arg_types[0] - if isinstance(annotation_type, Instance): - if annotation_type.type and annotation_type.type.is_protocol: - ctx.api.fail( - 'Protocols must be passed with `is_protocol=True`', - ctx.context, - ) - return ctx.default_return_type - else: - ctx.api.fail( - 'Only simple instance types are allowed, got: {0}'.format( - annotation_type, - ), - ctx.context, - ) - return ctx.default_return_type - - ret_type = CallableType( - arg_types=[passed_function], - arg_kinds=[ARG_POS], - arg_names=[None], - ret_type=AnyType(TypeOfAny.implementation_artifact), - fallback=passed_function.fallback, - ) - instance_type = ctx.api.expr_checker.accept( # type: ignore - ctx.context.decorators[0].expr, # type: ignore - ) - - # We need to change the `ctx` type from `Function` to `Method`: - return cls()(MethodContext( - type=instance_type, - arg_types=ctx.arg_types, - arg_kinds=ctx.arg_kinds, - arg_names=ctx.arg_names, - args=ctx.args, - callee_arg_names=ctx.callee_arg_names, - default_return_type=ret_type, - context=ctx.context, - api=ctx.api, - )) - - def _adjust_typeclass_callable( - self, - ctx: MethodContext, - ) -> MypyType: - assert isinstance(ctx.type, Instance) - assert isinstance(ctx.default_return_type, CallableType) - - real_signature = ctx.type.args[2] - to_adjust = ctx.default_return_type.arg_types[0] - - assert isinstance(real_signature, CallableType) - assert isinstance(to_adjust, CallableType) - - instance_type = to_adjust.arg_types[0] - instance_kind = to_adjust.arg_kinds[0] - instance_name = to_adjust.arg_names[0] - - to_adjust.arg_types = real_signature.arg_types - to_adjust.arg_kinds = real_signature.arg_kinds - to_adjust.arg_names = real_signature.arg_names - to_adjust.variables = real_signature.variables - to_adjust.is_ellipsis_args = real_signature.is_ellipsis_args - - to_adjust.arg_types[0] = instance_type - to_adjust.arg_kinds[0] = instance_kind - to_adjust.arg_names[0] = instance_name - - return instance_type - - def _adjust_typeclass_type( - self, - ctx: MethodContext, - instance_type: MypyType, - ) -> None: - assert isinstance(ctx.type, Instance) - - unified = list(filter( - # It means that function was defined without annotation - # or with explicit `Any`, we prevent our Union from pollution. - # Because `Union[Any, int]` is just `Any`. - # We also clear accidental type vars. - self._filter_out_unified_types, - [instance_type, ctx.type.args[0]], - )) - - ctx.type.args = ( - UnionType.make_union(unified), - *ctx.type.args[1:], - ) - - def _add_supports_metadata( - self, - ctx: MethodContext, - instance_type: Instance, - ) -> None: - """ - Injects fake ``Supports[TypeClass]`` parent classes into ``mro``. - - Ok, this is wild. Why do we need this? - Because, otherwise expressing ``Supports`` is not possible, - here's an example: - - .. code:: python - - >>> from classes import Supports, typeclass - >>> from typing_extensions import Protocol - - >>> class ToStr(Protocol): - ... def __call__(self, instance) -> str: - ... ... - - >>> to_str = typeclass(ToStr) - >>> @to_str.instance(int) - ... def _to_str_int(instance: int) -> str: - ... return 'Number: {0}'.format(instance) - - >>> assert to_str(1) == 'Number: 1' - - Now, let's use ``Supports`` to only pass specific - typeclass instances in a function: - - .. code:: python - - >>> def convert_to_string(arg: Supports[ToStr]) -> str: - ... return to_str(arg) - - This is possible, due to a fact that we insert ``Supports[ToStr]`` - into all classes that are mentioned as ``.instance()`` for ``ToStr`` - typeclass. - - So, we can call: - - .. code:: python - - >>> assert convert_to_string(1) == 'Number: 1' - - But, ``convert_to_string(None)`` will raise a type error. - """ - assert isinstance(ctx.type, Instance) - - supports_spec = _load_supports_type(ctx.type.args[3], ctx) - - if supports_spec not in instance_type.type.bases: - instance_type.type.bases.append(supports_spec) - if supports_spec.type not in instance_type.type.mro: - instance_type.type.mro.insert(0, supports_spec.type) - - def _filter_out_unified_types(self, type_: MypyType) -> bool: - return not isinstance(type_, (TypeVarType, UninhabitedType)) - - -def _load_supports_type( - arg_type: MypyType, - ctx: Union[MethodContext, MethodSigContext], -) -> Instance: - """ - Loads ``Supports[]`` type with proper generic type. - - It uses the short name, - because for some reason full name is not always loaded. - """ - assert isinstance(ctx.type, Instance) - class_definition = ctx.type.args[3] - - supports_spec = ctx.api.named_generic_type( - 'classes.Supports', - [class_definition], - ) - assert supports_spec - supports_spec.type._promote = None # noqa: WPS437 - return supports_spec - - -class _TypedDecoratorPlugin(Plugin): +class _TypeClassPlugin(Plugin): """ Our plugin for typeclasses. - It has three steps: + It has four steps: - Creating typeclasses via ``typeclass`` function - - Adding cases for typeclasses via ``.instance()`` calls + - Adding cases for typeclasses via ``.instance()`` calls with explicit types + - Adding callbacks functions after the ``.instance()`` decorator - Converting typeclasses to simple callable via ``__call__`` method Hooks are in the logical order. @@ -378,10 +53,10 @@ def get_function_hook( ) -> Optional[Callable[[FunctionContext], MypyType]]: """Here we adjust the typeclass constructor.""" if fullname == 'classes._typeclass.typeclass': - return _AdjustArguments() - if fullname == 'instance of _TypeClass': - # `@some.instance` call without params: - return _AdjustInstanceSignature.from_function_decorator + return typeclass.TypeClassReturnType( + typeclass=_TYPECLASS_FULLNAME, + typeclass_def=_TYPECLASS_DEF_FULLNAME, + ) return None def get_method_hook( @@ -389,9 +64,12 @@ def get_method_hook( fullname: str, ) -> Optional[Callable[[MethodContext], MypyType]]: """Here we adjust the typeclass with new allowed types.""" - if fullname == 'classes._typeclass._TypeClass.instance': - # `@some.instance` call with explicit params: - return _AdjustInstanceSignature() + if fullname == '{0}.__call__'.format(_TYPECLASS_DEF_FULLNAME): + return typeclass.typeclass_def_return_type + if fullname == '{0}.__call__'.format(_TYPECLASS_INSTANCE_DEF_FULLNAME): + return typeclass.InstanceDefReturnType() + if fullname == '{0}.instance'.format(_TYPECLASS_FULLNAME): + return typeclass.instance_return_type return None def get_method_signature_hook( @@ -399,11 +77,11 @@ def get_method_signature_hook( fullname: str, ) -> Optional[Callable[[MethodSigContext], CallableType]]: """Here we fix the calling method types to accept only valid types.""" - if fullname == 'classes._typeclass._TypeClass.__call__': - return _adjust_call_signature + if fullname == '{0}.__call__'.format(_TYPECLASS_FULLNAME): + return typeclass.call_signature return None def plugin(version: str) -> Type[Plugin]: """Plugin's public API and entrypoint.""" - return _TypedDecoratorPlugin + return _TypeClassPlugin diff --git a/classes/contrib/mypy/features/__init__.py b/classes/contrib/mypy/features/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/classes/contrib/mypy/features/typeclass.py b/classes/contrib/mypy/features/typeclass.py new file mode 100644 index 0000000..de68a03 --- /dev/null +++ b/classes/contrib/mypy/features/typeclass.py @@ -0,0 +1,313 @@ + +from mypy.nodes import Decorator +from mypy.plugin import FunctionContext, MethodContext, MethodSigContext +from mypy.types import ( + AnyType, + CallableType, + FunctionLike, + Instance, + LiteralType, + TupleType, +) +from mypy.types import Type as MypyType +from mypy.types import TypeOfAny, UnionType +from typing_extensions import final + +from classes.contrib.mypy.typeops import ( + associated_types, + inference, + instance_args, + type_loader, + typecheck, +) + + +@final +class TypeClassReturnType(object): + """ + Adjust argument types when we define typeclasses via ``typeclass`` function. + + It has two modes: + 1. As a decorator ``@typeclass`` + 2. As a regular call with a class definition: ``typeclass(SomeProtocol)`` + + It also checks how typeclasses are defined. + """ + + __slots__ = ('_typeclass', '_typeclass_def') + + def __init__(self, typeclass: str, typeclass_def: str) -> None: + """We pass exact type names as the context.""" + self._typeclass = typeclass + self._typeclass_def = typeclass_def + + def __call__(self, ctx: FunctionContext) -> MypyType: + """Main entry point.""" + defn = ctx.arg_types[0][0] + + is_typeclass_def = ( + isinstance(ctx.default_return_type, Instance) and + ctx.default_return_type.type.fullname == self._typeclass_def and + isinstance(defn, FunctionLike) and + defn.is_type_obj() + ) + is_typeclass = ( + isinstance(ctx.default_return_type, Instance) and + ctx.default_return_type.type.fullname == self._typeclass and + isinstance(defn, CallableType) and + defn.definition + ) + + if is_typeclass_def: + assert isinstance(ctx.default_return_type, Instance) + assert isinstance(defn, FunctionLike) + return self._process_typeclass_def_return_type( + ctx.default_return_type, + defn, + ctx, + ) + elif is_typeclass: + assert isinstance(ctx.default_return_type, Instance) + assert isinstance(defn, CallableType) + assert defn.definition + instance_args.mutate_typeclass_def( + ctx.default_return_type, + defn.definition.fullname, + ctx, + ) + return ctx.default_return_type + return AnyType(TypeOfAny.from_error) + + def _process_typeclass_def_return_type( + self, + typeclass_intermediate_def: Instance, + defn: FunctionLike, + ctx: FunctionContext, + ) -> MypyType: + type_info = defn.type_object() + instance = Instance(type_info, []) + typeclass_intermediate_def.args = (instance,) + return typeclass_intermediate_def + + +def typeclass_def_return_type(ctx: MethodContext) -> MypyType: + """ + Callback for cases like ``@typeclass(SomeType)``. + + What it does? It works with the associated types. + It checks that ``SomeType`` is correct, modifies the current typeclass. + And returns it back. + """ + assert isinstance(ctx.default_return_type, Instance) + # TODO: change to condition + # This will allow us to warn users on `x = typeclass(T)(func)`, + # instead of falling with exception. + assert isinstance(ctx.context, Decorator) + + if isinstance(ctx.default_return_type.args[2], Instance): + associated_types.check_type(ctx.default_return_type.args[2], ctx) + + instance_args.mutate_typeclass_def( + ctx.default_return_type, + ctx.context.func.fullname, + ctx, + ) + return ctx.default_return_type + + +def instance_return_type(ctx: MethodContext) -> MypyType: + """Adjusts the typing signature on ``.instance(type)`` call.""" + assert isinstance(ctx.default_return_type, Instance) + assert isinstance(ctx.type, Instance) + + instance_args.mutate_typeclass_instance_def( + ctx.default_return_type, + ctx=ctx, + typeclass=ctx.type, + passed_types=[ + type_ + for args in ctx.arg_types + for type_ in args + ], + ) + return ctx.default_return_type + + +@final +class InstanceDefReturnType(object): + """ + Class to check how instance definition is created. + + When it is called? + It is called on the second call of ``.instance(str)(callback)``. + + We do a lot of stuff here: + 1. Typecheck usage correctness + 2. Adding new instance types to typeclass definition + 3. Adding ``Supports[]`` metadata + + """ + + def __call__(self, ctx: MethodContext) -> MypyType: # noqa: WPS218 + """Main entry point.""" + assert isinstance(ctx.type, Instance) + assert isinstance(ctx.type.args[0], TupleType) + assert isinstance(ctx.type.args[1], Instance) + + typeclass_ref = ctx.type.args[1] + assert isinstance(typeclass_ref.args[3], LiteralType) + assert isinstance(typeclass_ref.args[3].value, str) + + typeclass = type_loader.load_typeclass( + fullname=typeclass_ref.args[3].value, + ctx=ctx, + ) + assert isinstance(typeclass.args[1], CallableType) + + instance_signature = ctx.arg_types[0][0] + assert isinstance(instance_signature, CallableType) + instance_type = instance_signature.arg_types[0] + + # We need to add `Supports` metadata before typechecking, + # because it will affect type hierarchies. + self._add_supports_metadata(typeclass, instance_type, ctx) + + typecheck.check_typeclass( + typeclass_signature=typeclass.args[1], + instance_signature=instance_signature, + passed_types=ctx.type.args[0], + ctx=ctx, + ) + self._add_new_instance_type( + typeclass=typeclass, + new_type=instance_type, + fullname=typeclass_ref.args[3].value, + ctx=ctx, + ) + + # Without this line we won't mutate args of a class-defined typeclass: + ctx.type.args[1].args = typeclass.args + return ctx.default_return_type + + def _add_new_instance_type( + self, + typeclass: Instance, + new_type: MypyType, + fullname: str, + ctx: MethodContext, + ) -> None: + has_multiple_decorators = ( + isinstance(ctx.context, Decorator) and + len(ctx.context.decorators) > 1 + ) + if has_multiple_decorators: + # If we have multiple decorators on a function, + # it is not safe to assume + # that all the regular instance type is fine. Here's an example: + # + # @some.instance(str) + # @other.instance(int) + # (instance: Union[str, int]) -> ... + # + # So, if we just copy copy `instance`, + # both typeclasses will have both `int` and `str` + # as their instance types. This is not what we want. + # We want: `some` to have `str` and `other` to have `int` + new_type = inference.infer_runtime_type_from_context( + fallback=new_type, + fullname=fullname, + ctx=ctx, + ) + + typeclass.args = ( + instance_args.add_unique(new_type, typeclass.args[0]), + *typeclass.args[1:], + ) + + def _add_supports_metadata( + self, + typeclass: Instance, + instance_type: MypyType, + ctx: MethodContext, + ) -> None: + """ + Injects fake ``Supports[TypeClass]`` parent classes into ``mro``. + + Ok, this is wild. Why do we need this? + Because, otherwise expressing ``Supports`` is not possible, + here's an example: + + .. code:: python + + >>> from classes import Supports, typeclass + + >>> class ToStr(object): + ... ... + + >>> @typeclass(ToStr) + ... def to_str(instance) -> str: + ... ... + + >>> @to_str.instance(int) + ... def _to_str_int(instance: int) -> str: + ... return 'Number: {0}'.format(instance) + + >>> assert to_str(1) == 'Number: 1' + + Now, let's use ``Supports`` to only pass specific + typeclass instances in a function: + + .. code:: python + + >>> def convert_to_string(arg: Supports[ToStr]) -> str: + ... return to_str(arg) + + This is possible, due to a fact that we insert ``Supports[ToStr]`` + into all classes that are mentioned as ``.instance()`` for ``ToStr`` + typeclass. + + So, we can call: + + .. code:: python + + >>> assert convert_to_string(1) == 'Number: 1' + + But, ``convert_to_string(None)`` will raise a type error. + """ + if not isinstance(instance_type, Instance): + return + if not isinstance(typeclass.args[2], Instance): + return + + assert isinstance(ctx.type, Instance) + + supports_spec = type_loader.load_supports_type(typeclass.args[2], ctx) + if supports_spec not in instance_type.type.bases: + instance_type.type.bases.append(supports_spec) + if supports_spec.type not in instance_type.type.mro: + instance_type.type.mro.insert(0, supports_spec.type) + + +def call_signature(ctx: MethodSigContext) -> CallableType: + """Returns proper ``__call__`` signature of a typeclass.""" + assert isinstance(ctx.type, Instance) + + real_signature = ctx.type.args[1] + if not isinstance(real_signature, CallableType): + return ctx.default_signature + + real_signature.arg_types[0] = ctx.type.args[0] + + if isinstance(ctx.type.args[2], Instance): + # Why do we need this check? + # Let's see what will happen without it: + # For example, typeclass `ToJson` with `int` and `str` have will have + # `Union[str, int]` as the first argument type. + # But, we need `Union[str, int, Supports[ToJson]]` + # That's why we are loading this type if the definition is there. + supports_spec = type_loader.load_supports_type(ctx.type.args[2], ctx) + real_signature.arg_types[0] = UnionType.make_union([ + real_signature.arg_types[0], + supports_spec, + ]) + return real_signature diff --git a/classes/contrib/mypy/typeops/__init__.py b/classes/contrib/mypy/typeops/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/classes/contrib/mypy/typeops/associated_types.py b/classes/contrib/mypy/typeops/associated_types.py new file mode 100644 index 0000000..2c0fb7a --- /dev/null +++ b/classes/contrib/mypy/typeops/associated_types.py @@ -0,0 +1,49 @@ +from mypy.plugin import MethodContext +from mypy.types import Instance +from typing_extensions import Final + +#: Fullname of the `AssociatedType` class. +_ASSOCIATED_TYPE_FULLNAME: Final = 'classes._typeclass.AssociatedType' + +# Messages: +_WRONG_SUBCLASS_MSG: Final = ( + 'Single direct subclass of "{0}" required; got "{1}"' +) + + +def check_type( + associated_type: Instance, + ctx: MethodContext, +) -> bool: + """ + Checks passed ``AssociatedType`` instance. + + Right now, we only check that + it is a subtype of our ``AssociatedType`` instance. + In the future, it will do way more. + """ + return all([ + _check_base_class(associated_type, ctx), + # TODO: check_type_reuse + # TODO: check_body + ]) + + +def _check_base_class( + associated_type: Instance, + ctx: MethodContext, +) -> bool: + bases = associated_type.type.bases + has_correct_base = ( + len(bases) == 1 and + _ASSOCIATED_TYPE_FULLNAME == bases[0].type.fullname + ) + if not has_correct_base: + ctx.api.fail( + _WRONG_SUBCLASS_MSG.format( + _ASSOCIATED_TYPE_FULLNAME, + associated_type, + ), + ctx.context, + ) + return has_correct_base diff --git a/classes/contrib/mypy/typeops/inference.py b/classes/contrib/mypy/typeops/inference.py new file mode 100644 index 0000000..1721d29 --- /dev/null +++ b/classes/contrib/mypy/typeops/inference.py @@ -0,0 +1,108 @@ + +from typing import Optional + +from mypy.nodes import Decorator, Expression +from mypy.plugin import MethodContext +from mypy.typeops import make_simplified_union +from mypy.types import FunctionLike, Instance, LiteralType +from mypy.types import Type as MypyType +from typing_extensions import Final + +_TYPECLASS_DEF_FULLNAMES: Final = frozenset(( + 'classes._typeclass._TypeClassInstanceDef', +)) + + +def infer_runtime_type_from_context( + fallback: MypyType, + ctx: MethodContext, + fullname: Optional[str] = None, +) -> MypyType: + """ + Infers instance type from several ``@some.instance()`` decorators. + + We have a problem: when user has two ``.instance()`` decorators + on a single function, inference will work only + for a single one of them at the time. + + So, let's say you have this: + + .. code:: python + + @some.instance(str) + @some.instance(int) + def _some_int_str(instance: Union[str, int]): ... + + Your instance has ``Union[str, int]`` annotation as it should have. + But, our ``fallback`` type would be just ``int`` on the first call + and just ``str`` on the second call. + + And this will break our ``is_same_type`` check, + because ``Union[str, int]`` is not the same as ``int`` or ``str``. + + In this case we need to fetch all typeclass decorators and infer + the resulting type manually. + """ + if isinstance(ctx.context, Decorator) and len(ctx.context.decorators) > 1: + # Why do we only care for this case? + # Because if it is a call / or just a single decorator, + # then we are fine with regular type inference. + # Inferred type from `mypy` is good enough, just return `fallback`. + instance_types = [] + for decorator in ctx.context.decorators: + instance_type = _get_typeclass_instance_type( + decorator, + fullname, + ctx, + ) + if instance_type is not None: + instance_types.append(_post_process_type(instance_type)) + + # Inferred resulting type: + return make_simplified_union(instance_types) + return _post_process_type(fallback) + + +def _get_typeclass_instance_type( + expr: Expression, + fullname: Optional[str], + ctx: MethodContext, +) -> Optional[MypyType]: + expr_type = ctx.api.expr_checker.accept(expr) # type: ignore + is_typeclass_instance_def = ( + isinstance(expr_type, Instance) and + bool(expr_type.type) and + expr_type.type.fullname in _TYPECLASS_DEF_FULLNAMES and + isinstance(expr_type.args[1], Instance) + ) + if is_typeclass_instance_def: + inst = expr_type.args[1] + is_same_typeclass = ( + isinstance(inst.args[3], LiteralType) and + inst.args[3].value == fullname or + fullname is None + ) + if is_same_typeclass: + return expr_type.args[0].items[0] + return None + + +def _post_process_type(type_: MypyType) -> MypyType: + if isinstance(type_, FunctionLike) and type_.is_type_obj(): + # What happens here? + # Let's say you define a function like this: + # + # @some.instance(Sized) + # (instance: Sized, b: int) -> str: ... + # + # So, you will receive callable type + # `def () -> Sized` as `runtime_type` in this case. + # We need to convert it back to regular `Instance`. + # + # It can also be `Overloaded` type, + # but they are safe to return the same `type_object`, + # however we still use `ret_type`, + # because it is practically the same thing, + # but with proper type arguments. + return type_.items()[0].ret_type + return type_ diff --git a/classes/contrib/mypy/typeops/instance_args.py b/classes/contrib/mypy/typeops/instance_args.py new file mode 100644 index 0000000..f65ff39 --- /dev/null +++ b/classes/contrib/mypy/typeops/instance_args.py @@ -0,0 +1,73 @@ +from typing import List, Union + +from mypy.plugin import FunctionContext, MethodContext +from mypy.typeops import make_simplified_union +from mypy.types import AnyType, Instance, LiteralType, TupleType +from mypy.types import Type as MypyType +from mypy.types import TypeVarType, UnboundType, UninhabitedType +from typing_extensions import Final + +#: Types that pollute instance args. +_TYPES_TO_FILTER_OUT: Final = ( + TypeVarType, + UninhabitedType, + UnboundType, + AnyType, +) + + +def add_unique( + new_instance_type: MypyType, + existing_instance_type: MypyType, +) -> MypyType: + """ + Adds new instance type to existing ones. + + It is smart: filters our junk and uses unique and flat ``Union`` types. + """ + unified = list(filter( + # We filter our `NoReturn` and + lambda type_: not isinstance(type_, _TYPES_TO_FILTER_OUT), + [new_instance_type, existing_instance_type], + )) + return make_simplified_union(unified) + + +def mutate_typeclass_def( + typeclass: Instance, + definition_fullname: str, + ctx: Union[FunctionContext, MethodContext], +) -> None: + """Adds definition fullname to the typeclass type.""" + str_fallback = ctx.api.str_type() # type: ignore + + typeclass.args = ( + *typeclass.args[:3], + LiteralType(definition_fullname, str_fallback), + ) + + +def mutate_typeclass_instance_def( + instance: Instance, + *, + passed_types: List[MypyType], + typeclass: Instance, + ctx: Union[MethodContext, FunctionContext], +) -> None: + """ + Mutates ``TypeClassInstanceDef`` args. + + That's where we fill their values. + Why? Because we need all types from ``some.instance()`` call. + Including ``is_protocol`` for later checks. + """ + tuple_type = TupleType( + # We now store passed arg types in a single tuple: + passed_types, + fallback=ctx.api.named_type('builtins.tuple'), # type: ignore + ) + + instance.args = ( + tuple_type, # Passed runtime types, like str in `@some.instance(str)` + typeclass, # `_TypeClass` instance itself + ) diff --git a/classes/contrib/mypy/typeops/type_loader.py b/classes/contrib/mypy/typeops/type_loader.py new file mode 100644 index 0000000..1b8edae --- /dev/null +++ b/classes/contrib/mypy/typeops/type_loader.py @@ -0,0 +1,37 @@ +from typing import Union + +from mypy.plugin import MethodContext, MethodSigContext +from mypy.types import Instance +from mypy.types import Type as MypyType +from typing_extensions import Final + +_SUPPORTS_QUALIFIED_NAME: Final = 'classes.Supports' + + +def load_supports_type( + arg_type: MypyType, + ctx: Union[MethodContext, MethodSigContext], +) -> Instance: + """ + Loads ``Supports[]`` type with proper generic type. + + It uses the short name, + because for some reason full name is not always loaded. + """ + supports_spec = ctx.api.named_generic_type( + _SUPPORTS_QUALIFIED_NAME, + [arg_type], + ) + assert supports_spec + supports_spec.type._promote = None # noqa: WPS437 + return supports_spec + + +def load_typeclass( + fullname: str, + ctx: MethodContext, +) -> Instance: + """Loads given typeclass from a symboltable by a fullname.""" + typeclass_info = ctx.api.lookup_qualified(fullname) # type: ignore + assert isinstance(typeclass_info.type, Instance) + return typeclass_info.type diff --git a/classes/contrib/mypy/typeops/type_queries.py b/classes/contrib/mypy/typeops/type_queries.py new file mode 100644 index 0000000..8324ff4 --- /dev/null +++ b/classes/contrib/mypy/typeops/type_queries.py @@ -0,0 +1,106 @@ +from typing import Callable, Iterable + +from mypy.plugin import MethodContext +from mypy.type_visitor import TypeQuery +from mypy.types import AnyType, Instance +from mypy.types import Type as MypyType +from mypy.types import TypeVarType, UnboundType, get_proper_type + + +def has_concrete_type( + instance_type: MypyType, + ctx: MethodContext, + *, + forbid_explicit_any: bool, +) -> bool: + """ + Queries if your instance has any concrete types. + + What do we call "concrete types"? Some examples: + + ``List[X]`` is generic, ``List[int]`` is concrete. + ``List[Union[int, str]]`` is also concrete. + ``Dict[str, X]`` is also concrete. + + So, this helps to write code like this: + + .. code:: python + + @some.instance(list) + def _some_list(instance: List[X]): ... + + And not like: + + .. code:: python + + @some.instance(list) + def _some_list(instance: List[int]): ... + + """ + instance_type = get_proper_type(instance_type) + if isinstance(instance_type, Instance): + return any( + type_arg.accept(_HasNoConcreteTypes( + lambda _: True, + forbid_explicit_any=forbid_explicit_any, + )) + for type_arg in instance_type.args + ) + return False + + +def has_unbound_type(runtime_type: MypyType, ctx: MethodContext) -> bool: + """ + Queries if your instance has any unbound types. + + Note, that you need to understand + how semantic and type analyzers work in ``mypy`` + to understand what "unbound type" is. + + Long story short, this helps to write code like this: + + .. code:: python + + @some.instance(list) + def _some_list(instance: List[X]): ... + + And not like: + + .. code:: python + + @some.instance(List[X]) + def _some_list(instance: List[X]): ... + + """ + runtime_type = get_proper_type(runtime_type) + if isinstance(runtime_type, Instance): + return any( + type_arg.accept(_HasUnboundTypes(lambda _: False)) + for type_arg in runtime_type.args + ) + return False + + +class _HasNoConcreteTypes(TypeQuery[bool]): + def __init__( + self, + strategy: Callable[[Iterable[bool]], bool], + *, + forbid_explicit_any: bool, + ) -> None: + super().__init__(strategy) + self._forbid_explicit_any = forbid_explicit_any + + def visit_type_var(self, type_: TypeVarType) -> bool: + return False + + def visit_unbound_type(self, type_: UnboundType) -> bool: + return False + + def visit_any(self, type_: AnyType) -> bool: + return self._forbid_explicit_any + + +class _HasUnboundTypes(TypeQuery[bool]): + def visit_unbound_type(self, type_: UnboundType) -> bool: + return True diff --git a/classes/contrib/mypy/typeops/typecheck.py b/classes/contrib/mypy/typeops/typecheck.py new file mode 100644 index 0000000..630b419 --- /dev/null +++ b/classes/contrib/mypy/typeops/typecheck.py @@ -0,0 +1,296 @@ +from typing import Tuple + +from mypy.erasetype import erase_type +from mypy.plugin import MethodContext +from mypy.sametypes import is_same_type +from mypy.subtypes import is_subtype +from mypy.types import AnyType, CallableType, Instance, LiteralType, TupleType +from mypy.types import Type as MypyType +from mypy.types import TypeOfAny +from typing_extensions import Final + +from classes.contrib.mypy.typeops import inference, type_queries + +_INCOMPATIBLE_INSTANCE_MSG: Final = ( + 'Instance callback is incompatible "{0}"; expected "{1}"' +) + +_INSTANCE_RESTRICTION_MSG: Final = ( + 'Instance "{0}" does not match original type "{1}"' +) + +_INSTANCE_RUNTIME_MISMATCH_MSG: Final = ( + 'Instance "{0}" does not match runtime type "{1}"' +) + +_IS_PROTOCOL_LITERAL_BOOL_MSG: Final = ( + 'Use literal bool for "is_protocol" argument, got: "{0}"' +) + +_IS_PROTOCOL_MISSING_MSG: Final = ( + 'Protocols must be passed with "is_protocol=True"' +) + +_IS_PROTOCOL_UNWANTED_MSG: Final = ( + 'Regular types must be passed with "is_protocol=False"' +) + +_CONCRETE_GENERIC_MSG: Final = ( + 'Instance "{0}" has concrete generic type, ' + + 'it is not supported during runtime' +) + +_UNBOUND_TYPE_MSG: Final = ( + 'Runtime type "{0}" has unbound type, use implicit any' +) + + +def check_typeclass( + typeclass_signature: CallableType, + instance_signature: CallableType, + passed_types: TupleType, + ctx: MethodContext, +) -> bool: + """ + We need to typecheck passed functions in order to build correct typeclasses. + + Please, see docs on each step. + """ + signature_check = _check_typeclass_signature( + typeclass_signature, + instance_signature, + ctx, + ) + instance_check = _check_instance_type( + typeclass_signature, + instance_signature, + ctx, + ) + runtime_check = _check_runtime_type( + passed_types, + instance_signature, + ctx, + ) + return signature_check and instance_check and runtime_check + + +def _check_typeclass_signature( + typeclass_signature: CallableType, + instance_signature: CallableType, + ctx: MethodContext, +) -> bool: + """ + Checks that instance signature is compatible with. + + We use contravariant on arguments and covariant on return type logic here. + What does this mean? + + Let's say that you have this typeclass signature: + + .. code:: python + + class A: ... + class B(A): ... + class C(B): ... + + @typeclass + def some(instance, arg: B) -> B: ... + + What instance signatures will be compatible? + + .. code:: python + + (instance: ..., arg: B) -> B: ... + (instance: ..., arg: A) -> C: ... + + But, any other cases will raise an error. + + .. note:: + We don't check instance types here at all, + we replace it with ``Any``. + See special function, where we check instance type. + + """ + simplified_typeclass_signature = typeclass_signature.copy_modified( + arg_types=[ + AnyType(TypeOfAny.implementation_artifact), + *typeclass_signature.arg_types[1:], + ], + ) + simplified_instance_signature = instance_signature.copy_modified( + arg_types=[ + AnyType(TypeOfAny.implementation_artifact), + *instance_signature.arg_types[1:], + ], + ) + signature_check = is_subtype( + simplified_instance_signature, + simplified_typeclass_signature, + ) + if not signature_check: + ctx.api.fail( + _INCOMPATIBLE_INSTANCE_MSG.format( + instance_signature, + typeclass_signature.copy_modified(arg_types=[ + instance_signature.arg_types[0], # Better error message + *typeclass_signature.arg_types[1:], + ]), + ), + ctx.context, + ) + return signature_check + + +def _check_instance_type( + typeclass_signature: CallableType, + instance_signature: CallableType, + ctx: MethodContext, +) -> bool: + """ + Checks instance type, helpful when typeclass has type restrictions. + + We use covariant logic on instance type. + What does this mean? + + .. code:: python + + class A: ... + class B(A): ... + class C(B): ... + + @typeclass + def some(instance: B): ... + + What can we use on instance callbacks? + + .. code:: python + + (instance: B) + (instance: C) + + Any other cases will raise an error. + """ + instance_check = is_subtype( + instance_signature.arg_types[0], + typeclass_signature.arg_types[0], + ) + if not instance_check: + ctx.api.fail( + _INSTANCE_RESTRICTION_MSG.format( + instance_signature.arg_types[0], + typeclass_signature.arg_types[0], + ), + ctx.context, + ) + return instance_check + + +def _check_runtime_type( + passed_types: TupleType, + instance_signature: CallableType, + ctx: MethodContext, +) -> bool: + """ + Checks runtime type. + + We call "runtime types" things that we use at runtime to dispatch our calls. + For example: + + 1. We check that type passed in ``some.instance(...)`` matches + one defined in a type annotation + 2. We check that types don't have any concrete types + 3. We check that types don't have any unbound type variables + 4. We check that ``is_protocol`` is passed correctly + + """ + runtime_type = inference.infer_runtime_type_from_context( + passed_types.items[0], + ctx, + ) + + if len(passed_types.items) == 2: + is_protocol, protocol_arg_check = _check_protocol_arg(passed_types, ctx) + else: + is_protocol = False + protocol_arg_check = True + + instance_type = instance_signature.arg_types[0] + instance_check = is_same_type( + erase_type(instance_type), + erase_type(runtime_type), + ) + if not instance_check: + ctx.api.fail( + _INSTANCE_RUNTIME_MISMATCH_MSG.format(instance_type, runtime_type), + ctx.context, + ) + + return ( + _check_runtime_protocol(runtime_type, ctx, is_protocol=is_protocol) and + _check_concrete_generics(instance_type, runtime_type, ctx) and + protocol_arg_check and + instance_check + ) + + +def _check_protocol_arg( + passed_types: TupleType, + ctx: MethodContext, +) -> Tuple[bool, bool]: + passed_arg = passed_types.items[1] + is_literal_bool = ( + isinstance(passed_arg, Instance) and + isinstance(passed_arg.last_known_value, LiteralType) and + isinstance(passed_arg.last_known_value.value, bool) + ) + if is_literal_bool: + return passed_arg.last_known_value.value, True # type: ignore + + ctx.api.fail( + _IS_PROTOCOL_LITERAL_BOOL_MSG.format(passed_types.items[1]), + ctx.context, + ) + return False, False + + +def _check_runtime_protocol( + runtime_type: MypyType, + ctx: MethodContext, + *, + is_protocol: bool, +) -> bool: + if isinstance(runtime_type, Instance) and runtime_type.type: + if not is_protocol and runtime_type.type.is_protocol: + ctx.api.fail(_IS_PROTOCOL_MISSING_MSG, ctx.context) + return False + elif is_protocol and not runtime_type.type.is_protocol: + ctx.api.fail(_IS_PROTOCOL_UNWANTED_MSG, ctx.context) + return False + return True + + +def _check_concrete_generics( + instance_type: MypyType, + runtime_type: MypyType, + ctx: MethodContext, +) -> bool: + has_concrete_type = False + type_settings = ( # Not a dict, because of `hash` problems + (instance_type, False), + (runtime_type, True), + ) + + for type_, forbid_explicit_any in type_settings: + local_check = type_queries.has_concrete_type( + type_, + ctx, + forbid_explicit_any=forbid_explicit_any, + ) + if local_check: + ctx.api.fail(_CONCRETE_GENERIC_MSG.format(type_), ctx.context) + has_concrete_type = has_concrete_type or local_check + + if type_queries.has_unbound_type(runtime_type, ctx): + ctx.api.fail(_UNBOUND_TYPE_MSG.format(runtime_type), ctx.context) + return False + return has_concrete_type diff --git a/docs/conf.py b/docs/conf.py index d17369f..1897504 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -26,6 +26,7 @@ # we need to mokey patch that constant. if sphinx.version_info[0] >= 4: import errno # noqa: WPS433 + import sphinx.util.osutil # noqa: I003, WPS301, WPS433 sphinx.util.osutil.ENOENT = errno.ENOENT diff --git a/docs/pages/concept.rst b/docs/pages/concept.rst index ebc557c..886deb1 100644 --- a/docs/pages/concept.rst +++ b/docs/pages/concept.rst @@ -87,7 +87,7 @@ Let's define some instances: .. code:: python - >>> @json.instance # You can use just the annotation + >>> @json.instance(str) ... def _json_str(instance: str) -> str: ... return '"{0}"'.format(instance) @@ -99,17 +99,6 @@ Let's define some instances: That's how we define instances for our typeclass. These instances will be executed when the corresponding type will be supplied. -.. note:: - ``.instance`` can use explicit type or just an existing annotation. - It is recommended to use the explicit type, because annotations can be tricky. - For example, sometimes you have to use ``ForwardRef`` - or so called string-based-annotations. It is not supported. - Complex type from annotations are also not supported - like: ``Union[str, int]``. - - So, use annotations for the simplest cases only - and use explicit types in all other cases. - And the last step is to call our typeclass with different value of different types: @@ -136,29 +125,24 @@ Example: >>> assert json.supports(int) is True >>> assert json.supports(dict) is False -Class-based definition -~~~~~~~~~~~~~~~~~~~~~~ +Typeclasses with associated types +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -You can also define typeclasses not as functions, but as classes. -It won't affect anything, except some advanced ``mypy`` usage. -For example, functions in ``mypy`` cannot be used as type arguments. +You can also define typeclasses with associated types. +It will allow you to use ``Supports`` type later on. -Instead of regular functions, you can define classes with ``__call__`` method. The syntax looks like this: .. code:: python - >>> from classes import typeclass - - >>> class CanBeTrimmed(object): - ... def __call__(self, instance, length: int) -> str: - ... ... + >>> from classes import AssociatedType, typeclass - >>> can_be_trimmed = typeclass(CanBeTrimmed) + >>> class CanBeTrimmed(AssociatedType): # Associated type definition + ... ... -.. note:: - Note that you have to use ``typeclass`` as a function call here, - class decorator won't work. Because ``mypy`` does not type-check them yet. + >>> @typeclass(CanBeTrimmed) + ... def can_be_trimmed(instance, length: int) -> str: + ... ... The instance definition syntax is the same: @@ -185,13 +169,14 @@ that are able to be converted to JSON: .. code:: python - >>> from classes import Supports, typeclass + >>> from classes import AssociatedType, Supports, typeclass - >>> class ToJson(object): - ... def __call__(self, instance) -> str: - ... ... + >>> class ToJson(AssociatedType): + ... ... - >>> to_json = typeclass(ToJson) + >>> @typeclass(ToJson) + ... def to_json(instance) -> str: + ... ... >>> @to_json.instance(int) ... def _to_json_int(instance: int) -> str: @@ -220,32 +205,31 @@ And this will fail (both in runtime and during type checking): ... NotImplementedError: Missing matched typeclass instance for type: NoneType - -One more tip: you can use ``Protocol`` as the base class for this. -It might be a good indicator that this is not a real Python class, -but is just an abstraction. Our team would recommend this style: +You can also use ``Supports`` as a type annotation for defining typeclasses: .. code:: python - >>> from typing_extensions import Protocol, final + >>> class MyFeature(AssociatedType): + ... ... - >>> @final # This type cannot have sub-types - ... class MyTypeclass(Protocol): # It cannot have instances - ... def __call__(self, instance) -> str: # Protocols don't have bodies - ... """Tell us, what this typeclass is about.""" + >>> @typeclass(MyFeature) + ... def my_feature(instance: 'Supports[MyFeature]') -> str: + ... ... -You can also use ``Supports`` as a type annotation for defining typeclasses: +It might be helpful, when you have ``no-untyped-def`` rule enabled. + +One more tip: our team would recommend this style: .. code:: python - >>> class MyFeature(object): - ... def __call__(self, instance: 'Supports[MyFeature]') -> str: - ... ... + >>> from typing_extensions import Protocol, final -It might be helpful, when you have ``no-untyped-def`` rule enabled. + >>> @final # This type cannot have sub-types + ... class MyTypeclass(AssociatedType): + ... """Tell us, what this typeclass is about.""" .. warning:: - ``Supports`` only works with typeclasses defined as Python classes. + ``Supports`` only works with typeclasses defined with associated types. Related concepts diff --git a/poetry.lock b/poetry.lock index 227feef..eb603e6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -96,7 +96,7 @@ msgpack = ["msgpack-python (>=0.5,<0.6)"] [[package]] name = "certifi" -version = "2020.12.5" +version = "2021.5.30" description = "Python package for providing Mozilla's CA Bundle." category = "dev" optional = false @@ -538,7 +538,7 @@ test = ["flake8 (>=3.8.4,<3.9.0)", "pycodestyle (>=2.6.0,<2.7.0)"] [[package]] name = "importlib-metadata" -version = "4.3.0" +version = "4.3.1" description = "Read metadata from Python packages" category = "dev" optional = false @@ -987,7 +987,7 @@ docutils = ">=0.11,<1.0" [[package]] name = "ruamel.yaml" -version = "0.17.4" +version = "0.17.6" description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" category = "dev" optional = false @@ -1365,8 +1365,8 @@ cachy = [ {file = "cachy-0.3.0.tar.gz", hash = "sha256:186581f4ceb42a0bbe040c407da73c14092379b1e4c0e327fdb72ae4a9b269b1"}, ] certifi = [ - {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, - {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"}, + {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"}, + {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"}, ] chardet = [ {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, @@ -1579,8 +1579,8 @@ immutables = [ {file = "immutables-0.15.tar.gz", hash = "sha256:3713ab1ebbb6946b7ce1387bb9d1d7f5e09c45add58c2a2ee65f963c171e746b"}, ] importlib-metadata = [ - {file = "importlib_metadata-4.3.0-py3-none-any.whl", hash = "sha256:c8b9a7c6000baa7adf7abcd2c41db11f172bcb5d6e448c73c2407dbc7e7e2af3"}, - {file = "importlib_metadata-4.3.0.tar.gz", hash = "sha256:c4646abbce80191bb548636f846e353ff1edc46a06bc536ea0a60d53211dc690"}, + {file = "importlib_metadata-4.3.1-py3-none-any.whl", hash = "sha256:c2e27fa8b6c8b34ebfcd4056ae2ca290e36250d1fbeceec85c1c67c711449fac"}, + {file = "importlib_metadata-4.3.1.tar.gz", hash = "sha256:2d932ea08814f745863fd20172fe7de4794ad74567db78f2377343e24520a5b6"}, ] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, @@ -1824,8 +1824,8 @@ restructuredtext-lint = [ {file = "restructuredtext_lint-1.3.2.tar.gz", hash = "sha256:d3b10a1fe2ecac537e51ae6d151b223b78de9fafdd50e5eb6b08c243df173c80"}, ] "ruamel.yaml" = [ - {file = "ruamel.yaml-0.17.4-py3-none-any.whl", hash = "sha256:ac79fb25f5476e8e9ed1c53b8a2286d2c3f5dde49eb37dbcee5c7eb6a8415a22"}, - {file = "ruamel.yaml-0.17.4.tar.gz", hash = "sha256:44bc6b54fddd45e4bc0619059196679f9e8b79c027f4131bb072e6a22f4d5e28"}, + {file = "ruamel.yaml-0.17.6-py3-none-any.whl", hash = "sha256:748bbdddf9e7f6e1aad9481dfdd93a42f9c39c45821a0f09a31309cd0086e803"}, + {file = "ruamel.yaml-0.17.6.tar.gz", hash = "sha256:5605cb8ceeebaeed85ae4e97fc80547eca1b3537c163404ffe83f26adf5c9ce7"}, ] "ruamel.yaml.clib" = [ {file = "ruamel.yaml.clib-0.2.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:28116f204103cb3a108dfd37668f20abe6e3cafd0d3fd40dba126c732457b3cc"}, diff --git a/setup.cfg b/setup.cfg index 2a2e8f0..1307bdf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -37,7 +37,7 @@ per-file-ignores = classes/__init__.py: F401, WPS113, WPS436 classes/_typeclass.py: WPS320 # We need `assert`s to please mypy: - classes/contrib/mypy/classes_plugin.py: S101 + classes/contrib/mypy/*.py: S101 # There are multiple assert's in tests: tests/*.py: S101, WPS226, WPS432, WPS436 @@ -80,6 +80,13 @@ omit = # which does not work with coverage: classes/contrib/mypy/* +[coverage:report] +exclude_lines = + # a more strict default pragma + \# pragma: no cover\b + + ^if TYPE_CHECKING: + [mypy] # mypy configurations: http://bit.ly/2zEl9WI diff --git a/typesafety/test_supports_type.yml b/typesafety/test_supports_type.yml index 94fa07e..809cb38 100644 --- a/typesafety/test_supports_type.yml +++ b/typesafety/test_supports_type.yml @@ -1,13 +1,14 @@ - case: typeclass_supports_type disable_cache: false main: | - from classes import typeclass, Supports + from classes import typeclass, Supports, AssociatedType - class ToJson(object): - def __call__(self, instance) -> str: - ... + class ToJson(AssociatedType): + ... - to_json = typeclass(ToJson) + @typeclass(ToJson) + def to_json(instance) -> str: + ... @to_json.instance(int) def _to_json_int(instance: int) -> str: @@ -24,20 +25,38 @@ convert_to_json('a') convert_to_json(None) out: | - main:22: error: Argument 1 to "convert_to_json" has incompatible type "None"; expected "Supports[ToJson]" + main:23: error: Argument 1 to "convert_to_json" has incompatible type "None"; expected "Supports[ToJson]" + + +- case: typeclass_supports_type_restriction + disable_cache: false + main: | + from classes import typeclass, Supports, AssociatedType + + class ToJson(AssociatedType): + ... + + @typeclass(ToJson) + def to_json(instance: Supports[ToJson]) -> str: + ... + + @to_json.instance(int) + def _to_json_int(instance: int) -> str: + return str(instance) - case: typeclass_supports_callback disable_cache: false main: | - from classes import typeclass, Supports + from classes import typeclass, Supports, AssociatedType from typing import Callable - class ToJson(object): - def __call__(self, instance) -> str: - ... + class ToJson(AssociatedType): + ... - to_json = typeclass(ToJson) + @typeclass(ToJson) + def to_json(instance) -> str: + ... @to_json.instance(int) def _to_json_int(instance: int) -> str: @@ -57,7 +76,7 @@ convert_to_json(to_json, 'a') convert_to_json(to_json, None) out: | - main:26: error: Argument 2 to "convert_to_json" has incompatible type "None"; expected "Supports[ToJson]" + main:27: error: Argument 2 to "convert_to_json" has incompatible type "None"; expected "Supports[ToJson]" - case: typeclass_supports_with_function @@ -87,34 +106,35 @@ - case: typeclass_supports_other disable_cache: false main: | - from classes import typeclass, Supports + from classes import typeclass, Supports, AssociatedType - class ToJson(object): - def __call__(self, instance) -> str: - ... + class ToJson(AssociatedType): + ... - to_json = typeclass(ToJson) + @typeclass(ToJson) + def to_json(instance) -> str: + ... - class Other(object): - def __call__(self, instance) -> str: - ... + class Other(AssociatedType): + ... def convert_to_json(instance: Supports[Other]) -> str: return to_json(instance) out: | - main:14: error: Argument 1 to "__call__" of "ToJson" has incompatible type "Supports[Other]"; expected "Union[, Supports[ToJson]]" + main:14: error: Argument 1 to "to_json" has incompatible type "Supports[Other]"; expected "Union[, Supports[ToJson]]" - case: supports_annotation disable_cache: false main: | - from classes import typeclass, Supports + from classes import typeclass, Supports, AssociatedType - class ToJson(object): - def __call__(self, instance) -> str: - ... + class ToJson(AssociatedType): + ... - to_json = typeclass(ToJson) + @typeclass(ToJson) + def to_json(instance) -> str: + ... @to_json.instance(int) def _to_json_int(instance: int) -> str: @@ -129,4 +149,4 @@ main: | from classes import Supports - Supports[int] # E: Value of type variable "_CallbackType" of "Supports" cannot be "int" + Supports[int] # E: Value of type variable "_StrictAssociatedType" of "Supports" cannot be "int" diff --git a/typesafety/test_typeclass/test_callback.yml b/typesafety/test_typeclass/test_callback.yml index 86f05ce..ec93836 100644 --- a/typesafety/test_typeclass/test_callback.yml +++ b/typesafety/test_typeclass/test_callback.yml @@ -1,7 +1,7 @@ - case: typeclass_callback_correct disable_cache: false main: | - from typing import Callable + from typing import Callable, Union from classes import typeclass @typeclass @@ -10,7 +10,7 @@ @example.instance(int) @example.instance(float) - def _example_int_float(instance, attr: bool) -> bool: + def _example_int_float(instance: Union[int, float], attr: bool) -> bool: ... @@ -23,7 +23,7 @@ - case: typeclass_callback_wrong disable_cache: false main: | - from typing import Callable + from typing import Callable, Union from classes import typeclass @typeclass @@ -32,7 +32,7 @@ @example.instance(int) @example.instance(float) - def _example_int_float(instance, attr: bool) -> bool: + def _example_int_float(instance: Union[int, float], attr: bool) -> bool: ... def accepts_typeclass(callback: Callable[[str, bool], bool]) -> bool: @@ -40,6 +40,6 @@ reveal_type(accepts_typeclass(example)) out: | - main:16: error: Argument 1 to "accepts_typeclass" has incompatible type "_TypeClass[Union[int, float], bool, Callable[[int, bool], bool], ]"; expected "Callable[[str, bool], bool]" - main:16: note: "_TypeClass[Union[int, float], bool, Callable[[int, bool], bool], ].__call__" has type "Callable[[Arg(Supports[], 'instance'), VarArg(Any), KwArg(Any)], bool]" + main:16: error: Argument 1 to "accepts_typeclass" has incompatible type "_TypeClass[float, Callable[[Any, bool], bool], , Literal['main.example']]"; expected "Callable[[str, bool], bool]" + main:16: note: "_TypeClass[float, Callable[[Any, bool], bool], , Literal['main.example']].__call__" has type "Callable[[Arg(Union[float, Supports[]], 'instance'), VarArg(Any), KwArg(Any)], _ReturnType]" main:16: note: Revealed type is 'builtins.bool' diff --git a/typesafety/test_typeclass/test_definition_by_class.yml b/typesafety/test_typeclass/test_definition_by_class.yml deleted file mode 100644 index cd0d50c..0000000 --- a/typesafety/test_typeclass/test_definition_by_class.yml +++ /dev/null @@ -1,104 +0,0 @@ -- case: typeclass_definied_by_type - disable_cache: false - main: | - from classes import typeclass - - class ToJson(object): - def __call__(self, instance, verbose: bool = False) -> str: - ... - - to_json = typeclass(ToJson) - - @to_json.instance(int) - def _to_json_int(instance: int, verbose: bool = False) -> str: - return str(instance) - - @to_json.instance(str) - def _to_json_str(instance: str, verbose: bool = False) -> str: - return instance - - to_json(1) - to_json('a') - to_json(None) - reveal_type(to_json) - out: | - main:19: error: Argument 1 to "__call__" of "ToJson" has incompatible type "None"; expected "Union[str, int, Supports[ToJson]]" - main:20: note: Revealed type is 'classes._typeclass._TypeClass[Union[builtins.str*, builtins.int*], builtins.str, def (Union[builtins.str*, builtins.int*, classes._typeclass.Supports[main.ToJson]], verbose: builtins.bool =) -> builtins.str, main.ToJson]' - - -- case: typeclass_class_wrong_sig - disable_cache: false - main: | - from classes import typeclass - - class ToJson(object): - def __call__(self, instance, verbose: bool = False) -> str: - ... - - to_json = typeclass(ToJson) - - @to_json.instance(int) - def _to_json_int(instance: str) -> int: - ... - out: | - main:9: error: Argument 1 has incompatible type "Callable[[str], int]"; expected "Callable[[int, bool], str]" - - -- case: typeclass_definied_by_protocol - disable_cache: false - main: | - from classes import typeclass - from typing_extensions import Protocol - - class ToJson(Protocol): - def __call__(self, instance, verbose: bool = False) -> str: - ... - - to_json = typeclass(ToJson) - - @to_json.instance(int) - def _to_json_int(instance: int, verbose: bool = False) -> str: - return str(instance) - - @to_json.instance(str) - def _to_json_str(instance: str, verbose: bool = False) -> str: - return instance - - to_json(1) - to_json('a') - to_json(None) - reveal_type(to_json) - out: | - main:20: error: Argument 1 to "__call__" of "ToJson" has incompatible type "None"; expected "Union[str, int, Supports[ToJson]]" - main:21: note: Revealed type is 'classes._typeclass._TypeClass[Union[builtins.str*, builtins.int*], builtins.str, def (Union[builtins.str*, builtins.int*, classes._typeclass.Supports[main.ToJson]], verbose: builtins.bool =) -> builtins.str, main.ToJson]' - - -- case: typeclass_protocol_wrong_sig - disable_cache: false - main: | - from classes import typeclass - from typing_extensions import Protocol - - class ToJson(Protocol): - def __call__(self, instance, verbose: bool = False) -> str: - ... - - to_json = typeclass(ToJson) - - @to_json.instance(int) - def _to_json_int(instance: str) -> int: - ... - out: | - main:10: error: Argument 1 has incompatible type "Callable[[str], int]"; expected "Callable[[int, bool], str]" - - -- case: typeclass_protocol_wrong_method - disable_cache: false - main: | - from classes import typeclass - - class ToJson(object): - def __init__(self, instance, verbose: bool = False) -> str: # E: The return type of "__init__" must be None - ... - - to_json = typeclass(ToJson) diff --git a/typesafety/test_typeclass/test_generics.yml b/typesafety/test_typeclass/test_generics.yml new file mode 100644 index 0000000..e20608e --- /dev/null +++ b/typesafety/test_typeclass/test_generics.yml @@ -0,0 +1,185 @@ +- case: typeclass_generic_definition + disable_cache: false + main: | + from typing import List, TypeVar + from classes import typeclass + + X = TypeVar('X') + + @typeclass + def some(instance, b: int) -> X: + ... + + @some.instance(list) + def _some_ex(instance: List[X], b: int) -> X: + return instance[b] # We need this line to test inner inference + + reveal_type(some(['a', 'b'], 0)) # N: Revealed type is 'builtins.str*' + reveal_type(some([1, 2, 3], 0)) # N: Revealed type is 'builtins.int*' + + +# TODO: enable after fixing how `Supports[X]` is inserted into a metadata +- case: typeclass_generic_definition_associated_type + disable_cache: false + skip: true + main: | + from typing import List, TypeVar + from classes import typeclass, AssociatedType + + X = TypeVar('X') + + class Some(AssociatedType): + ... + + @typeclass(Some) + def some(instance, b: int) -> X: + ... + + @some.instance(list) + def _some_ex(instance: List[X], b: int) -> X: + return instance[b] # We need this line to test inner inference + + reveal_type(some(['a', 'b'], 0)) # N: Revealed type is 'builtins.str*' + reveal_type(some([1, 2, 3], 0)) # N: Revealed type is 'builtins.int*' + + +- case: typeclass_generic_definition_restriction_correct + disable_cache: false + main: | + from typing import Iterable, List, TypeVar + from classes import typeclass + + X = TypeVar('X') + + @typeclass + def some(instance: Iterable[X], b: int) -> X: + ... + + @some.instance(list) + def _some_ex(instance: List[X], b: int) -> X: + return instance[b] # We need this line to test inner inference + + reveal_type(some(['a', 'b'], 0)) # N: Revealed type is 'builtins.str*' + reveal_type(some([1, 2, 3], 0)) # N: Revealed type is 'builtins.int*' + + +- case: typeclass_generic_definition_restriction_wrong + disable_cache: false + main: | + from typing import Generic, List, TypeVar + from classes import typeclass + + X = TypeVar('X') + + class Some(Generic[X]): + ... + + @typeclass + def some(instance: Some[X], b: int) -> X: + ... + + @some.instance(list) + def _some_ex(instance: List[X], b: int) -> X: + return instance[b] # We need this line to test inner inference + out: | + main:13: error: Instance "builtins.list[X`-1]" does not match original type "main.Some[X`-1]" + + +- case: typeclass_generic_definition_any1 + disable_cache: false + mypy_config: | + disallow_any_explicit = false + disallow_any_generics = false + main: | + from typing import Iterable, List, TypeVar, Any + from classes import typeclass + + X = TypeVar('X') + + @typeclass + def some(instance, b: int) -> int: + ... + + @some.instance(list) + def _some_ex(instance: List[Any], b: int) -> int: + ... + + +- case: typeclass_generic_definition_any2 + disable_cache: false + mypy_config: | + disallow_any_explicit = false + disallow_any_generics = false + main: | + from typing import Iterable, List, TypeVar, Any + from classes import typeclass + + X = TypeVar('X') + + @typeclass + def some(instance, b: int) -> int: + ... + + @some.instance(List[Any]) + def _some_ex(instance: list, b: int) -> int: + ... + out: | + main:10: error: Instance "builtins.list[Any]" has concrete generic type, it is not supported during runtime + + +- case: typeclass_generic_definition_unbound + disable_cache: false + main: | + from typing import Iterable, List, TypeVar + from classes import typeclass + + X = TypeVar('X') + + @typeclass + def some(instance, b: int) -> X: + ... + + @some.instance(List[X]) + def _some_ex(instance: List[X], b: int) -> X: + return instance[b] # We need this line to test inner inference + out: | + main:10: error: Runtime type "builtins.list[X?]" has unbound type, use implicit any + + +- case: typeclass_generic_definition_concrete1 + disable_cache: false + main: | + from typing import Iterable, List, TypeVar + from classes import typeclass + + X = TypeVar('X') + + @typeclass + def some(instance, b: int) -> X: + ... + + @some.instance(list) + def _some_ex(instance: List[int], b: int) -> X: + return instance[b] # We need this line to test inner inference + out: | + main:10: error: Instance "builtins.list[builtins.int]" has concrete generic type, it is not supported during runtime + main:12: error: Incompatible return value type (got "int", expected "X") + + +- case: typeclass_generic_definition_concrete2 + disable_cache: false + main: | + from typing import Iterable, List, TypeVar + from classes import typeclass + + X = TypeVar('X') + + @typeclass + def some(instance, b: int) -> X: + ... + + @some.instance(List[int]) + def _some_ex(instance: List[X], b: int) -> X: + return instance[b] # We need this line to test inner inference + out: | + main:10: error: Instance "builtins.list[builtins.int*]" has concrete generic type, it is not supported during runtime diff --git a/typesafety/test_typeclass/test_instance.yml b/typesafety/test_typeclass/test_instance.yml index 44b503c..8853fdf 100644 --- a/typesafety/test_typeclass/test_instance.yml +++ b/typesafety/test_typeclass/test_instance.yml @@ -1,4 +1,4 @@ -- case: typeclass_instances_union +- case: typeclass_two_typeclasses_two_instances disable_cache: false main: | from typing import Union @@ -8,88 +8,131 @@ def a(instance) -> str: ... + @typeclass + def b(instance) -> str: + ... + @a.instance(str) - @a.instance(int) - def _a_int_str(instance: Union[str, int]) -> str: - return str(instance) + def _a_str(instance: str) -> str: + ... - reveal_type(a) # N: Revealed type is 'classes._typeclass._TypeClass[Union[builtins.str*, builtins.int*], builtins.str, def (builtins.str*) -> builtins.str, ]' + @b.instance(int) + def _b_int(instance: int) -> str: + ... + a('a') + b(2) -- case: typeclass_instance_mixed_order - disable_cache: False + a(1) + b('b') + out: | + main:23: error: Argument 1 to "a" has incompatible type "int"; expected "str" + main:24: error: Argument 1 to "b" has incompatible type "str"; expected "int" + + +- case: typeclass_two_typeclasses_one_instance + disable_cache: false main: | + from typing import Union from classes import typeclass @typeclass - def some(instance) -> str: + def a(instance) -> str: ... - @some.instance(str) - def _some_str(instance: str) -> str: - ... + @typeclass + def b(instance) -> str: + ... - @some.instance(int) - def _some_int(instance: str) -> str: - ... + @a.instance(str) + @b.instance(int) + def _a_int_str(instance: Union[str, int]) -> str: + return str(instance) + + a('a') + b(2) + + a(1) + b('b') out: | - main:11: error: Argument 1 has incompatible type "Callable[[str], str]"; expected "Callable[[int], str]" + main:20: error: Argument 1 to "a" has incompatible type "int"; expected "str" + main:21: error: Argument 1 to "b" has incompatible type "str"; expected "int" -- case: typeclass_instance_any +- case: typeclass_instances_union1 disable_cache: false main: | + from typing import Union from classes import typeclass @typeclass - def a(instance): + def a(instance) -> str: ... @a.instance(str) - def _a_int_str(instance: str) -> str: + @a.instance(int) + def _a_int_str(instance: Union[str, int]) -> str: return str(instance) - reveal_type(a) # N: Revealed type is 'classes._typeclass._TypeClass[builtins.str*, Any, def (builtins.str*) -> Any, ]' + a(1) + a('a') + a(None) # E: Argument 1 to "a" has incompatible type "None"; expected "Union[str, int]" -- case: typeclass_instance_missing_first_arg +- case: typeclass_instances_union2 disable_cache: false main: | from classes import typeclass @typeclass - def a(instance): + def a(instance) -> str: ... - @a.instance - def some(): + @a.instance(str) + @a.instance(int) + def _a_int_str(instance: int) -> str: ... - out: | - main:7: error: Argument 1 to "instance" of "_TypeClass" has incompatible type "Callable[[], Any]"; expected "Callable[[], Any]" + main:7: error: Instance "builtins.int" does not match runtime type "Union[builtins.str*, builtins.int*]" -- case: typeclass_instance_wrong_param +- case: typeclass_instances_union3 disable_cache: false main: | from classes import typeclass @typeclass - def a(instance): + def a(instance) -> str: ... - a.instance(1) - + @a.instance(str) + @a.instance(int) + def _a_int_str(instance: str) -> str: + ... out: | - main:7: error: No overload variant of "instance" of "_TypeClass" matches argument type "int" - main:7: note: <1 more non-matching overload not shown> - main:7: note: def [_InstanceType] instance(self, type_argument: Callable[[_InstanceType], Any]) -> NoReturn - main:7: note: def [_InstanceType] instance(self, type_argument: Type[_InstanceType], *, is_protocol: Literal[False] = ...) -> Callable[[Callable[[_InstanceType], Any]], NoReturn] - main:7: note: Possible overload variants: + main:7: error: Instance "builtins.str" does not match runtime type "Union[builtins.str*, builtins.int*]" -- case: typeclass_instance_annotation_only +- case: typeclass_instances_union4 disable_cache: false + main: | + from typing import Union + from classes import typeclass + + @typeclass + def a(instance) -> str: + ... + + @a.instance(str) + @a.instance(int) + def _a_int_str(instance: Union[str, int, None]) -> str: + ... + out: | + main:8: error: Instance "Union[builtins.str, builtins.int, None]" does not match runtime type "Union[builtins.str*, builtins.int*]" + + +- case: typeclass_instance_mixed_order + disable_cache: False main: | from classes import typeclass @@ -97,46 +140,95 @@ def some(instance) -> str: ... - @some.instance + @some.instance(int) def _some_str(instance: str) -> str: ... - some('abc') + @some.instance(int) + def _some_int(instance: str) -> str: + ... + out: | + main:7: error: Instance "builtins.str" does not match runtime type "builtins.int*" + main:11: error: Instance "builtins.str" does not match runtime type "builtins.int*" -- case: typeclass_instance_annotation_only_complex_types +- case: typeclass_instance_any1 disable_cache: false + mypy_config: | + disallow_any_explicit = false + disallow_any_generics = false main: | + from typing import Any from classes import typeclass - from typing import Union, Sized, Type, Any @typeclass - def some(instance) -> str: + def a(instance) -> str: ... - @some.instance - def _some_union(instance: Union[str, int]) -> str: + @a.instance(int) + def _a_int(instance: Any) -> str: ... + out: | + main:8: error: Instance "Any" does not match runtime type "builtins.int*" + - @some.instance - def _some_type_type(instance: Type[str]) -> str: +- case: typeclass_instance_any2 + disable_cache: false + mypy_config: | + disallow_any_explicit = false + disallow_any_generics = false + main: | + from typing import Any + from classes import typeclass + + @typeclass + def a(instance) -> str: ... - @some.instance - def _some_protocol(instance: Sized) -> str: + @a.instance(Any) + def _a_any(instance: Any) -> str: ... + out: | + main:8: error: Instance "Any" does not match runtime type "builtins.object" + main:8: error: Value of type variable "_NewInstanceType" of "instance" of "_TypeClass" cannot be "object" - @some.instance - def _some_annotated(instance) -> str: - ... - @some.instance - def _some_explicit_any(instance: Any) -> str: - ... - out: | - main:8: error: Only simple instance types are allowed, got: Union[builtins.str, builtins.int] - main:12: error: Only simple instance types are allowed, got: Type[builtins.str] - main:16: error: Protocols must be passed with `is_protocol=True` - main:20: error: Only simple instance types are allowed, got: Any - main:24: error: Only simple instance types are allowed, got: Any - main:25: error: Explicit "Any" is not allowed +- case: typeclass_instance_wrong_param + disable_cache: false + main: | + from classes import typeclass + + @typeclass + def a(instance) -> str: + ... + + a.instance(1) # E: Value of type variable "_NewInstanceType" of "instance" of "_TypeClass" cannot be "int" + + +- case: typeclass_instance_callback_def + disable_cache: false + main: | + from classes import typeclass + + @typeclass + def some(instance, b: int) -> int: + ... + + def _some_str(instance: str, b: int) -> int: + ... + some.instance(str)(_some_str) + + some('a', 1) + some(None, 1) # E: Argument 1 to "some" has incompatible type "None"; expected "str" + + +- case: typeclass_instance_named_alias + disable_cache: false + main: | + from classes import typeclass + + @typeclass + def some(instance, b: int) -> int: + ... + + alias = some.instance(str) # E: Need type annotation for 'alias' diff --git a/typesafety/test_typeclass/test_instance_variance.yml b/typesafety/test_typeclass/test_instance_variance.yml new file mode 100644 index 0000000..4d00cb9 --- /dev/null +++ b/typesafety/test_typeclass/test_instance_variance.yml @@ -0,0 +1,131 @@ +- case: typeclass_instance_arg_variance + disable_cache: False + main: | + from classes import typeclass + + class A(object): + ... + + class B(A): + ... + + class C(B): + ... + + @typeclass + def some(instance, arg: B) -> str: + ... + + @some.instance(str) + def _some_str(instance: str, arg: A) -> str: + ... + + @some.instance(bool) + def _some_bool(instance: bool, arg: B) -> str: + ... + + @some.instance(int) + def _some_int(instance: int, arg: C) -> str: + ... + out: | + main:24: error: Instance callback is incompatible "def (instance: builtins.int, arg: main.C) -> builtins.str"; expected "def (instance: builtins.int, arg: main.B) -> builtins.str" + + +- case: typeclass_instance_ret_type_variance + disable_cache: False + main: | + from classes import typeclass + + class A(object): + ... + + class B(A): + ... + + class C(B): + ... + + @typeclass + def some(instance) -> B: + ... + + @some.instance(str) + def _some_str(instance: str) -> A: + ... + + @some.instance(bool) + def _some_bool(instance: bool) -> B: + ... + + @some.instance(int) + def _some_int(instance: int) -> C: + ... + out: | + main:16: error: Instance callback is incompatible "def (instance: builtins.str) -> main.A"; expected "def (instance: builtins.str) -> main.B" + + +- case: typeclass_instance_self_variance + disable_cache: False + main: | + from classes import typeclass + + class A(object): + ... + + class B(A): + ... + + class C(B): + ... + + @typeclass + def some(instance: B): + ... + + @some.instance(A) + def _some_a(instance: A): + ... + + @some.instance(B) + def _some_b(instance: B): + ... + + @some.instance(C) + def _some_c(instance: C): + ... + out: | + main:16: error: Instance "main.A" does not match original type "main.B" + + +- case: typeclass_instance_runtime_variance + disable_cache: False + main: | + from classes import typeclass + + class A(object): + ... + + class B(A): + ... + + class C(B): + ... + + @typeclass + def some(instance) -> str: + ... + + @some.instance(A) + def _some_a(instance: B) -> str: + ... + + @some.instance(B) + def _some_b(instance: B) -> str: + ... + + @some.instance(C) + def _some_c(instance: B) -> str: + ... + out: | + main:16: error: Instance "main.B" does not match runtime type "main.A" + main:24: error: Instance "main.B" does not match runtime type "main.C" diff --git a/typesafety/test_typeclass/test_protocols.yml b/typesafety/test_typeclass/test_protocols.yml index 315fdc9..fe2af53 100644 --- a/typesafety/test_typeclass/test_protocols.yml +++ b/typesafety/test_typeclass/test_protocols.yml @@ -17,7 +17,7 @@ protocols(None, 'xyz') # E: Argument 1 to "protocols" has incompatible type "None"; expected "Sized" -- case: typeclass_protocol_wrong_usage +- case: typeclass_protocol_wrong_usage0 disable_cache: false main: | from typing import Sized @@ -27,6 +27,97 @@ def protocols(instance, other: str) -> str: ... - @protocols.instance(Sized) # E: Only concrete class can be given where "Type[Sized]" is expected + @protocols.instance(Sized) def _sized_protocols(instance: Sized, other: str) -> str: ... + out: | + main:8: error: Protocols must be passed with "is_protocol=True" + + +- case: typeclass_protocol_wrong_usage1 + disable_cache: false + main: | + from typing import Sized + from classes import typeclass + + @typeclass + def protocols(instance, other: str) -> str: + ... + + @protocols.instance(Sized, is_protocol=False) + def _sized_protocols(instance: Sized, other: str) -> str: + ... + out: | + main:8: error: Protocols must be passed with "is_protocol=True" + + +- case: typeclass_protocol_wrong_usage2 + disable_cache: false + main: | + from typing import Sized + from classes import typeclass + + @typeclass + def protocols(instance, other: str) -> str: + ... + + @protocols.instance(int, is_protocol=True) + def _sized_protocols(instance: int, other: str) -> str: + ... + out: | + main:8: error: Regular types must be passed with "is_protocol=False" + + +- case: typeclass_protocol_wrong_usage3 + disable_cache: false + main: | + from typing import Sized + from classes import typeclass + + @typeclass + def protocols(instance, other: str) -> str: + ... + + p: bool + + @protocols.instance(int, is_protocol=p) + def _sized_protocols(instance: int, other: str) -> str: + ... + out: | + main:10: error: Use literal bool for "is_protocol" argument, got: "builtins.bool" + + +- case: typeclass_protocol_wrong_usage4 + disable_cache: false + main: | + from typing import Sized + from classes import typeclass + + @typeclass + def protocols(instance, other: str) -> str: + ... + + @protocols.instance(int, is_protocol='abc') + def _sized_protocols(instance: int, other: str) -> str: + ... + out: | + main:8: error: Argument "is_protocol" to "instance" of "_TypeClass" has incompatible type "str"; expected "bool" + main:8: error: Use literal bool for "is_protocol" argument, got: "Literal['abc']?" + + +- case: typeclass_protocol_wrong_usage5 + disable_cache: false + main: | + from typing import Sized + from classes import typeclass + + @typeclass + def protocols(instance, other: str) -> str: + ... + + @protocols.instance(int, is_protocol=bool) + def _sized_protocols(instance: int, other: str) -> str: + ... + out: | + main:8: error: Argument "is_protocol" to "instance" of "_TypeClass" has incompatible type "Type[bool]"; expected "bool" + main:8: error: Use literal bool for "is_protocol" argument, got: "def (builtins.object =) -> builtins.bool*" diff --git a/typesafety/test_typeclass/test_supports.yml b/typesafety/test_typeclass/test_supports.yml index 432afcc..9dc3f47 100644 --- a/typesafety/test_typeclass/test_supports.yml +++ b/typesafety/test_typeclass/test_supports.yml @@ -1,14 +1,14 @@ -- case: typeclass_protocol_supports +- case: typeclass_object_supports disable_cache: false main: | - from classes import typeclass - from typing_extensions import Protocol + from classes import typeclass, AssociatedType - class ToJson(Protocol): - def __call__(self, instance) -> str: - ... + class ToJson(AssociatedType): + ... - to_json = typeclass(ToJson) + @typeclass(ToJson) + def to_json(instance) -> str: + ... @to_json.instance(int) def _to_json_int(instance: int) -> str: diff --git a/typesafety/test_typeclass/test_typeclass.yml b/typesafety/test_typeclass/test_typeclass.yml index 8e58d44..1331125 100644 --- a/typesafety/test_typeclass/test_typeclass.yml +++ b/typesafety/test_typeclass/test_typeclass.yml @@ -1,115 +1,101 @@ -- case: typeclass_definition +- case: typeclass_definition_any disable_cache: false main: | from classes import typeclass @typeclass - def example(instance, arg: str, other: int, *, attr: bool) -> bool: + def example(instance): ... - reveal_type(example) # N: Revealed type is 'classes._typeclass._TypeClass[, builtins.bool, def (instance: , arg: builtins.str, other: builtins.int, *, attr: builtins.bool) -> builtins.bool, ]' - -- case: typeclass_wrong_param +- case: typeclass_definied_by_type disable_cache: false main: | - from classes import typeclass - - typeclass(1) + from classes import typeclass, AssociatedType - out: | - main:3: error: Value of type variable "_CallbackType" of "typeclass" cannot be "int" - - -- case: typeclass_instance - disable_cache: false - main: | - from classes import typeclass - - @typeclass - def example(instance, arg: str) -> bool: + class ToJson(AssociatedType): ... - @example.instance(str) - def _example_str(instance: str, arg: str) -> bool: + @typeclass(ToJson) + def to_json(instance, verbose: bool = False) -> str: ... - reveal_type(example) # N: Revealed type is 'classes._typeclass._TypeClass[builtins.str*, builtins.bool, def (builtins.str*, arg: builtins.str) -> builtins.bool, ]' + @to_json.instance(int) + def _to_json_int(instance: int, verbose: bool = False) -> str: + return str(instance) + + @to_json.instance(str) + def _to_json_str(instance: str, verbose: bool = False) -> str: + return instance + + to_json(1, verbose=True) + to_json('a') + to_json(None) + out: | + main:20: error: Argument 1 to "to_json" has incompatible type "None"; expected "Union[str, int, Supports[ToJson]]" -- case: typeclass_instance_union +- case: typeclass_class_wrong_sig disable_cache: false main: | - from classes import typeclass + from classes import typeclass, AssociatedType - @typeclass - def example(instance, arg: str) -> bool: + class ToJson(AssociatedType): ... - @example.instance(int) - @example.instance(float) - def _example_int_float(instance, arg: str) -> bool: + @typeclass(ToJson) + def to_json(instance, verbose: bool = False) -> str: ... - @example.instance(str) - def _example_str(instance: str, arg: str) -> bool: + @to_json.instance(int) + def _to_json_int(instance: str) -> int: ... - - reveal_type(example) # N: Revealed type is 'classes._typeclass._TypeClass[Union[builtins.str*, builtins.int*, builtins.float*], builtins.bool, def (builtins.str*, arg: builtins.str) -> builtins.bool, ]' + out: | + main:10: error: Instance "builtins.str" does not match runtime type "builtins.int*" + main:10: error: Instance callback is incompatible "def (instance: builtins.str) -> builtins.int"; expected "def (instance: builtins.str, verbose: builtins.bool =) -> builtins.str" -- case: typeclass_incorrect_instance_callback1 +- case: typeclass_definied_by_wrong_type disable_cache: false main: | from classes import typeclass - @typeclass - def example(instance, arg: str) -> bool: + class ToJson(object): ... - @example.instance(int) - def _example_int(instance: str, arg: str) -> bool: + @typeclass(ToJson) + def to_json(instance, verbose: bool = False) -> str: ... - - reveal_type(example) out: | - main:7: error: Argument 1 has incompatible type "Callable[[str, str], bool]"; expected "Callable[[int, str], bool]" - main:11: note: Revealed type is 'classes._typeclass._TypeClass[builtins.int*, builtins.bool, def (builtins.int*, arg: builtins.str) -> builtins.bool, ]' + main:6: error: Single direct subclass of "classes._typeclass.AssociatedType" required; got "main.ToJson" -- case: typeclass_incorrect_instance_callback2 +- case: typeclass_definied_by_multiple_parents disable_cache: false main: | - from classes import typeclass + from classes import typeclass, AssociatedType - @typeclass - def example(instance, arg: str) -> bool: + class A(object): ... - @example.instance(int) - def _example_int(instance: int) -> bool: + class ToJson(AssociatedType, A): ... - reveal_type(example) + @typeclass(ToJson) + def to_json(instance, verbose: bool = False) -> str: + ... out: | - main:7: error: Argument 1 has incompatible type "Callable[[int], bool]"; expected "Callable[[int, str], bool]" - main:11: note: Revealed type is 'classes._typeclass._TypeClass[builtins.int*, builtins.bool, def (builtins.int*, arg: builtins.str) -> builtins.bool, ]' + main:9: error: Single direct subclass of "classes._typeclass.AssociatedType" required; got "main.ToJson" -- case: typeclass_incorrect_instance_callback3 +- case: typeclass_definied_by_literal disable_cache: false main: | from classes import typeclass - @typeclass - def example(instance, arg: str) -> bool: - ... - - @example.instance(int) - def _example_int(instance, arg: int) -> bool: - ... - - reveal_type(example) + typeclass(1) out: | - main:7: error: Argument 1 has incompatible type "Callable[[Any, int], bool]"; expected "Callable[[int, str], bool]" - main:11: note: Revealed type is 'classes._typeclass._TypeClass[builtins.int*, builtins.bool, def (builtins.int*, arg: builtins.str) -> builtins.bool, ]' + main:3: error: No overload variant of "typeclass" matches argument type "int" + main:3: note: def [_AssociatedType] typeclass(definition: Type[_AssociatedType]) -> _TypeClassDef[_AssociatedType] + main:3: note: def [_SignatureType, _InstanceType, _AssociatedType, _Fullname <: str] typeclass(signature: _SignatureType) -> _TypeClass[_InstanceType, _SignatureType, _AssociatedType, _Fullname] + main:3: note: Possible overload variants: