diff --git a/returns/contrib/mypy/_consts.py b/returns/contrib/mypy/_consts.py index 69313b8d..1eef2d7f 100644 --- a/returns/contrib/mypy/_consts.py +++ b/returns/contrib/mypy/_consts.py @@ -34,3 +34,57 @@ 'returns.future.FutureResult.do', ) """Used for :ref:`do-notation`.""" + +E_ALIAS_METHODS: Final = ( + # ``Result`` unit constructors: + 'returns.result.Result.from_value', + 'returns.result.Result.from_failure', + 'returns.result.Result.from_result', + # ``IOResult`` unit constructors: + 'returns.io.IOResult.from_typecast', + 'returns.io.IOResult.from_failed_io', + 'returns.io.IOResult.from_io', + 'returns.io.IOResult.from_result', + 'returns.io.IOResult.from_ioresult', + 'returns.io.IOResult.from_value', + 'returns.io.IOResult.from_failure', + # ``FutureResult`` unit constructors: + 'returns.future.FutureResult.from_typecast', + 'returns.future.FutureResult.from_future', + 'returns.future.FutureResult.from_failed_future', + 'returns.future.FutureResult.from_future_result', + 'returns.future.FutureResult.from_io', + 'returns.future.FutureResult.from_failed_io', + 'returns.future.FutureResult.from_ioresult', + 'returns.future.FutureResult.from_result', + 'returns.future.FutureResult.from_value', + 'returns.future.FutureResult.from_failure', + # ``RequiresContextResult`` unit constructors: + 'returns.context.requires_context_result.RequiresContextResult.from_result', + 'returns.context.requires_context_result.RequiresContextResult.from_typecast', + 'returns.context.requires_context_result.RequiresContextResult.from_value', + 'returns.context.requires_context_result.RequiresContextResult.from_failure', + # ``RequiresContextIOResult`` unit constructors: + 'returns.context.requires_context_ioresult.RequiresContextIOResult.from_result', + 'returns.context.requires_context_ioresult.RequiresContextIOResult.from_io', + 'returns.context.requires_context_ioresult.RequiresContextIOResult.from_failed_io', + 'returns.context.requires_context_ioresult.RequiresContextIOResult.from_ioresult', + 'returns.context.requires_context_ioresult.RequiresContextIOResult.from_typecast', + 'returns.context.requires_context_ioresult.RequiresContextIOResult.from_value', + 'returns.context.requires_context_ioresult.RequiresContextIOResult.from_failure', + # ``RequiresContextFutureResult`` unit constructors: + 'returns.context.requires_context_future_result.RequiresContextFutureResult.from_result', + 'returns.context.requires_context_future_result.RequiresContextFutureResult.from_io', + 'returns.context.requires_context_future_result.RequiresContextFutureResult.from_failed_io', + 'returns.context.requires_context_future_result.RequiresContextFutureResult.from_ioresult', + 'returns.context.requires_context_future_result.RequiresContextFutureResult.from_future', + 'returns.context.requires_context_future_result.RequiresContextFutureResult.from_failed_future', + 'returns.context.requires_context_future_result.RequiresContextFutureResult.from_future_result', + 'returns.context.requires_context_future_result.RequiresContextFutureResult.from_typecast', + 'returns.context.requires_context_future_result.RequiresContextFutureResult.from_value', + 'returns.context.requires_context_future_result.RequiresContextFutureResult.from_failure', +) +"""Used for ``*E`` aliases unit methods.""" + +E_ALIAS_ANALYZER: Final = 'returns.contrib.mypy._features.alias_e.analyze' +"""Used for ``*E`` aliases unit methods.""" diff --git a/returns/contrib/mypy/_features/alias_e.py b/returns/contrib/mypy/_features/alias_e.py new file mode 100644 index 00000000..e72dc063 --- /dev/null +++ b/returns/contrib/mypy/_features/alias_e.py @@ -0,0 +1,80 @@ +""" +Fix for ``*E`` aliases unit methods: ``ResultE``, ``IOResultE``, etc. + +Problem: ``IOResultE`` is an alias for ``IOResult[_ValueType_co, Exception]``. +When calling a unit method on the alias, like ``IOResultE.from_value(1)``, +``mypy`` returns ``IOResult[int, Any]`` instead of ``IOResult[int, Exception]``, +because unit constructors use ``Any`` for the type they cannot infer. + +This module rewrites the return type of unit constructors +called on ``*E`` aliases: positions where the alias fixes ``Exception`` +and the constructor returned ``Any`` are corrected to ``Exception``. + +Important: we don't do anything ugly here. +We only solve problems of the current typing implementation. + +Related issue: https://github.com/dry-python/returns/issues/407 +""" + +from collections.abc import Sequence + +from mypy.plugin import MethodContext +from mypy.types import ( + AnyType, + CallableType, + Instance, + get_proper_type, +) +from mypy.types import Type as MypyType + +_EXCEPTION_FULLNAME = 'builtins.Exception' +"""Fullname of the type ``*E`` aliases fix their error type to.""" + + +def analyze(ctx: MethodContext) -> MypyType: + """ + Rewrites return type of a unit constructor called on an ``*E`` alias. + + For example, ``IOResultE.from_value(1)`` is rewritten from + ``IOResult[int, Any]`` to ``IOResult[int, Exception]``. + """ + self_type = ctx.type + if not isinstance(self_type, CallableType): + return ctx.default_return_type + + bound_type = get_proper_type(self_type.ret_type) + if not isinstance(bound_type, Instance): + return ctx.default_return_type + + default_ret = get_proper_type(ctx.default_return_type) + assert isinstance(default_ret, Instance) + if default_ret.type != bound_type.type: + # Different containers, nothing to fix. + return ctx.default_return_type + + new_args, changed = _rewrite_args(bound_type.args, list(default_ret.args)) + if not changed: + return ctx.default_return_type + return Instance(default_ret.type, new_args, default_ret.line) + + +def _rewrite_args( + bound_args: Sequence[MypyType], + default_args: list[MypyType], +) -> tuple[list[MypyType], bool]: + """Replaces uninferable Any types with Exception when fixed by alias.""" + new_args = list(default_args) + changed = False + for index, (bound_arg, ret_arg) in enumerate( + zip(bound_args, new_args, strict=True), + ): + proper_bound_arg = get_proper_type(bound_arg) + proper_ret_arg = get_proper_type(ret_arg) + if ( + isinstance(proper_bound_arg, Instance) + and proper_bound_arg.type.fullname == _EXCEPTION_FULLNAME + and isinstance(proper_ret_arg, AnyType) + ): + new_args[index] = proper_bound_arg + changed = True + return new_args, changed diff --git a/returns/contrib/mypy/returns_plugin.py b/returns/contrib/mypy/returns_plugin.py index 0d7772eb..80e688c2 100644 --- a/returns/contrib/mypy/returns_plugin.py +++ b/returns/contrib/mypy/returns_plugin.py @@ -26,6 +26,7 @@ from returns.contrib.mypy import _consts from returns.contrib.mypy._features import ( + alias_e, curry, do_notation, flow, @@ -75,6 +76,7 @@ class _ReturnsPlugin(Plugin): _consts.TYPED_KIND_KINDED_CALL: kind.kinded_call, _consts.TYPED_KIND_KINDED_GET: kind.kinded_get_descriptor, **dict.fromkeys(_consts.DO_NOTATION_METHODS, do_notation.analyze), + **dict.fromkeys(_consts.E_ALIAS_METHODS, alias_e.analyze), } def get_function_hook( diff --git a/typesafety/test_io/test_ioresult_container/test_ioresult_typecast.yml b/typesafety/test_io/test_ioresult_container/test_ioresult_typecast.yml index 730b74b8..6daca240 100644 --- a/typesafety/test_io/test_ioresult_container/test_ioresult_typecast.yml +++ b/typesafety/test_io/test_ioresult_container/test_ioresult_typecast.yml @@ -133,3 +133,12 @@ result: IOResultE[int] = function(1) reveal_type(result) # N: Revealed type is "returns.io.IOResult[int, Exception]" + + +- case: ioresulte_unit_methods + disable_cache: false + main: | + from returns.io import IOResultE + + reveal_type(IOResultE.from_value(1)) # N: Revealed type is "returns.io.IOResult[int, Exception]" + reveal_type(IOResultE.from_failure(ValueError('boom'))) # N: Revealed type is "returns.io.IOResult[Any, ValueError]"