Skip to content

Commit e332ec1

Browse files
committed
feat(mypy): infer Exception error type for unit methods on *E aliases (#407)
1 parent ea76fb5 commit e332ec1

4 files changed

Lines changed: 135 additions & 0 deletions

File tree

‎returns/contrib/mypy/_consts.py‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,57 @@
3434
'returns.future.FutureResult.do',
3535
)
3636
"""Used for :ref:`do-notation`."""
37+
38+
E_ALIAS_METHODS: Final = (
39+
# ``Result`` unit constructors:
40+
'returns.result.Result.from_value',
41+
'returns.result.Result.from_failure',
42+
'returns.result.Result.from_result',
43+
# ``IOResult`` unit constructors:
44+
'returns.io.IOResult.from_typecast',
45+
'returns.io.IOResult.from_failed_io',
46+
'returns.io.IOResult.from_io',
47+
'returns.io.IOResult.from_result',
48+
'returns.io.IOResult.from_ioresult',
49+
'returns.io.IOResult.from_value',
50+
'returns.io.IOResult.from_failure',
51+
# ``FutureResult`` unit constructors:
52+
'returns.future.FutureResult.from_typecast',
53+
'returns.future.FutureResult.from_future',
54+
'returns.future.FutureResult.from_failed_future',
55+
'returns.future.FutureResult.from_future_result',
56+
'returns.future.FutureResult.from_io',
57+
'returns.future.FutureResult.from_failed_io',
58+
'returns.future.FutureResult.from_ioresult',
59+
'returns.future.FutureResult.from_result',
60+
'returns.future.FutureResult.from_value',
61+
'returns.future.FutureResult.from_failure',
62+
# ``RequiresContextResult`` unit constructors:
63+
'returns.context.requires_context_result.RequiresContextResult.from_result',
64+
'returns.context.requires_context_result.RequiresContextResult.from_typecast',
65+
'returns.context.requires_context_result.RequiresContextResult.from_value',
66+
'returns.context.requires_context_result.RequiresContextResult.from_failure',
67+
# ``RequiresContextIOResult`` unit constructors:
68+
'returns.context.requires_context_ioresult.RequiresContextIOResult.from_result',
69+
'returns.context.requires_context_ioresult.RequiresContextIOResult.from_io',
70+
'returns.context.requires_context_ioresult.RequiresContextIOResult.from_failed_io',
71+
'returns.context.requires_context_ioresult.RequiresContextIOResult.from_ioresult',
72+
'returns.context.requires_context_ioresult.RequiresContextIOResult.from_typecast',
73+
'returns.context.requires_context_ioresult.RequiresContextIOResult.from_value',
74+
'returns.context.requires_context_ioresult.RequiresContextIOResult.from_failure',
75+
# ``RequiresContextFutureResult`` unit constructors:
76+
'returns.context.requires_context_future_result.RequiresContextFutureResult.from_result',
77+
'returns.context.requires_context_future_result.RequiresContextFutureResult.from_io',
78+
'returns.context.requires_context_future_result.RequiresContextFutureResult.from_failed_io',
79+
'returns.context.requires_context_future_result.RequiresContextFutureResult.from_ioresult',
80+
'returns.context.requires_context_future_result.RequiresContextFutureResult.from_future',
81+
'returns.context.requires_context_future_result.RequiresContextFutureResult.from_failed_future',
82+
'returns.context.requires_context_future_result.RequiresContextFutureResult.from_future_result',
83+
'returns.context.requires_context_future_result.RequiresContextFutureResult.from_typecast',
84+
'returns.context.requires_context_future_result.RequiresContextFutureResult.from_value',
85+
'returns.context.requires_context_future_result.RequiresContextFutureResult.from_failure',
86+
)
87+
"""Used for ``*E`` aliases unit methods."""
88+
89+
E_ALIAS_ANALYZER: Final = 'returns.contrib.mypy._features.alias_e.analyze'
90+
"""Used for ``*E`` aliases unit methods."""
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"""
2+
Fix for ``*E`` aliases unit methods: ``ResultE``, ``IOResultE``, etc.
3+
4+
Problem: ``IOResultE`` is an alias for ``IOResult[_ValueType_co, Exception]``.
5+
When calling a unit method on the alias, like ``IOResultE.from_value(1)``,
6+
``mypy`` returns ``IOResult[int, Any]`` instead of ``IOResult[int, Exception]``,
7+
because unit constructors use ``Any`` for the type they cannot infer.
8+
9+
This module rewrites the return type of unit constructors
10+
called on ``*E`` aliases: positions where the alias fixes ``Exception``
11+
and the constructor returned ``Any`` are corrected to ``Exception``.
12+
13+
Important: we don't do anything ugly here.
14+
We only solve problems of the current typing implementation.
15+
16+
Related issue: https://github.com/dry-python/returns/issues/407
17+
"""
18+
19+
from mypy.plugin import MethodContext
20+
from mypy.types import (
21+
AnyType,
22+
CallableType,
23+
Instance,
24+
get_proper_type,
25+
)
26+
from mypy.types import Type as MypyType
27+
28+
_EXCEPTION_FULLNAME = 'builtins.Exception'
29+
"""Fullname of the type ``*E`` aliases fix their error type to."""
30+
31+
32+
def analyze(ctx: MethodContext) -> MypyType:
33+
"""
34+
Rewrites return type of a unit constructor called on an ``*E`` alias.
35+
36+
For example, ``IOResultE.from_value(1)`` is rewritten from
37+
``IOResult[int, Any]`` to ``IOResult[int, Exception]``.
38+
"""
39+
self_type = ctx.type
40+
if not isinstance(self_type, CallableType):
41+
return ctx.default_return_type
42+
43+
bound_type = get_proper_type(self_type.ret_type)
44+
if not isinstance(bound_type, Instance):
45+
return ctx.default_return_type
46+
47+
default_ret = get_proper_type(ctx.default_return_type)
48+
assert isinstance(default_ret, Instance)
49+
if default_ret.type != bound_type.type:
50+
# Different containers, nothing to fix.
51+
return ctx.default_return_type
52+
53+
new_args = list(default_ret.args)
54+
changed = False
55+
for index, (bound_arg, ret_arg) in enumerate(zip(bound_type.args, new_args)):
56+
proper_bound_arg = get_proper_type(bound_arg)
57+
proper_ret_arg = get_proper_type(ret_arg)
58+
if (
59+
isinstance(proper_bound_arg, Instance)
60+
and proper_bound_arg.type.fullname == _EXCEPTION_FULLNAME
61+
and isinstance(proper_ret_arg, AnyType)
62+
):
63+
# The alias fixes this position to `Exception`,
64+
# but the constructor could not infer it and used `Any`.
65+
new_args[index] = proper_bound_arg
66+
changed = True
67+
68+
if not changed:
69+
return ctx.default_return_type
70+
return Instance(default_ret.type, new_args, default_ret.line)

‎returns/contrib/mypy/returns_plugin.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
from returns.contrib.mypy import _consts
2828
from returns.contrib.mypy._features import (
29+
alias_e,
2930
curry,
3031
do_notation,
3132
flow,
@@ -75,6 +76,7 @@ class _ReturnsPlugin(Plugin):
7576
_consts.TYPED_KIND_KINDED_CALL: kind.kinded_call,
7677
_consts.TYPED_KIND_KINDED_GET: kind.kinded_get_descriptor,
7778
**dict.fromkeys(_consts.DO_NOTATION_METHODS, do_notation.analyze),
79+
**dict.fromkeys(_consts.E_ALIAS_METHODS, alias_e.analyze),
7880
}
7981

8082
def get_function_hook(

‎typesafety/test_io/test_ioresult_container/test_ioresult_typecast.yml‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,12 @@
133133
134134
result: IOResultE[int] = function(1)
135135
reveal_type(result) # N: Revealed type is "returns.io.IOResult[int, Exception]"
136+
137+
138+
- case: ioresulte_unit_methods
139+
disable_cache: false
140+
main: |
141+
from returns.io import IOResultE
142+
143+
reveal_type(IOResultE.from_value(1)) # N: Revealed type is "returns.io.IOResult[int, Exception]"
144+
reveal_type(IOResultE.from_failure(ValueError('boom'))) # N: Revealed type is "returns.io.IOResult[Any, ValueError]"

0 commit comments

Comments
 (0)