Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions returns/contrib/mypy/_consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
80 changes: 80 additions & 0 deletions returns/contrib/mypy/_features/alias_e.py
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions returns/contrib/mypy/returns_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

from returns.contrib.mypy import _consts
from returns.contrib.mypy._features import (
alias_e,
curry,
do_notation,
flow,
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]"
Loading