File: kind.py

package info (click to toggle)
python-returns 0.26.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,652 kB
  • sloc: python: 11,000; makefile: 18
file content (184 lines) | stat: -rw-r--r-- 5,570 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
from collections.abc import Sequence
from enum import Enum, unique
from importlib.metadata import version
from typing import Any

from mypy.checkmember import analyze_member_access
from mypy.plugin import (
    AttributeContext,
    FunctionContext,
    MethodContext,
    MethodSigContext,
)
from mypy.typeops import bind_self
from mypy.types import (
    AnyType,
    CallableType,
    FunctionLike,
    Instance,
    Overloaded,
    TypeOfAny,
    TypeType,
    TypeVarType,
    get_proper_type,
)
from mypy.types import Type as MypyType

from returns.contrib.mypy._typeops.fallback import asserts_fallback_to_any
from returns.contrib.mypy._typeops.visitor import translate_kind_instance

# TODO: probably we can validate `KindN[]` creation during `get_analtype`


@asserts_fallback_to_any
def attribute_access(ctx: AttributeContext) -> MypyType:
    """
    Ensures that attribute access to ``KindN`` is correct.

    In other words:

    .. code:: python

        from typing import TypeVar
        from returns.primitives.hkt import KindN
        from returns.interfaces.mappable import MappableN

        _MappableType = TypeVar('_MappableType', bound=MappableN)

        kind: KindN[_MappableType, int, int, int]
        reveal_type(kind.map)  # will work correctly!

    """
    assert isinstance(ctx.type, Instance)
    instance = get_proper_type(ctx.type.args[0])

    if isinstance(instance, TypeVarType):
        bound = get_proper_type(instance.upper_bound)
        assert isinstance(bound, Instance)
        accessed = bound.copy_modified(
            args=_crop_kind_args(ctx.type, bound.args),
        )
    elif isinstance(instance, Instance):
        accessed = instance.copy_modified(args=_crop_kind_args(ctx.type))
    else:
        return ctx.default_attr_type

    mypy_version_tuple = tuple(
        map(int, version('mypy').partition('+')[0].split('.'))
    )

    extra_kwargs: dict[str, Any] = {}
    if mypy_version_tuple < (1, 16):
        extra_kwargs['msg'] = ctx.api.msg
    return analyze_member_access(
        ctx.context.name,  # type: ignore
        accessed,
        ctx.context,
        is_lvalue=False,
        is_super=False,
        is_operator=False,
        original_type=instance,
        chk=ctx.api,  # type: ignore
        in_literal_context=ctx.api.expr_checker.is_literal_context(),  # type: ignore
        **extra_kwargs,
    )


def dekind(ctx: FunctionContext) -> MypyType:
    """
    Infers real type behind ``Kind`` form.

    Basically, it turns ``Kind[IO, int]`` into ``IO[int]``.
    The only limitation is that it works with
    only ``Instance`` type in the first type argument position.

    So, ``dekind(KindN[T, int])`` will fail.
    """
    kind = get_proper_type(ctx.arg_types[0][0])
    assert isinstance(kind, Instance)  # mypy requires these lines

    kind_inst = get_proper_type(kind.args[0])

    if not isinstance(kind_inst, Instance):
        ctx.api.fail(_KindErrors.dekind_not_instance, ctx.context)
        return AnyType(TypeOfAny.from_error)

    return kind_inst.copy_modified(args=_crop_kind_args(kind))


@asserts_fallback_to_any
def kinded_signature(ctx: MethodSigContext) -> CallableType:
    """
    Returns the internal function wrapped as ``Kinded[def]``.

    Works for ``Kinded`` class when ``__call__`` magic method is used.
    See :class:`returns.primitives.hkt.Kinded` for more information.
    """
    assert isinstance(ctx.type, Instance)

    wrapped_method = get_proper_type(ctx.type.args[0])
    assert isinstance(wrapped_method, FunctionLike)

    if isinstance(wrapped_method, Overloaded):
        return ctx.default_signature

    assert isinstance(wrapped_method, CallableType)
    return wrapped_method


# TODO: we should raise an error if bound type does not have any `KindN`
# instances, because that's not how `@kinded` and `Kinded[]` should be used.
def kinded_call(ctx: MethodContext) -> MypyType:
    """
    Reveals the correct return type of ``Kinded.__call__`` method.

    Turns ``-> KindN[I, t1, t2, t3]`` into ``-> I[t1, t2, t3]``.

    Also strips unused type arguments for ``KindN``, so:
    - ``KindN[IO, int, Never, Never]`` will be ``IO[int]``
    - ``KindN[Result, int, str, Never]`` will be ``Result[int, str]``

    It also processes nested ``KindN`` with recursive strategy.

    See :class:`returns.primitives.hkt.Kinded` for more information.
    """
    return translate_kind_instance(ctx.default_return_type)


@asserts_fallback_to_any
def kinded_get_descriptor(ctx: MethodContext) -> MypyType:
    """
    Used to analyze ``@kinded`` method calls.

    We do this due to ``__get__`` descriptor magic.
    """
    assert isinstance(ctx.type, Instance)

    wrapped_method = get_proper_type(ctx.type.args[0])
    assert isinstance(wrapped_method, CallableType)

    self_type = get_proper_type(wrapped_method.arg_types[0])
    signature = bind_self(
        wrapped_method,
        is_classmethod=isinstance(self_type, TypeType),
    )
    return ctx.type.copy_modified(args=[signature])


@unique  # noqa: WPS600
class _KindErrors(str, Enum):  # noqa: WPS600
    """Represents a set of possible errors we can throw during typechecking."""

    dekind_not_instance = (
        'dekind must be used with Instance as the first type argument'
    )


def _crop_kind_args(
    kind: Instance,
    limit: Sequence[MypyType] | None = None,
) -> tuple[MypyType, ...]:
    """Returns the correct amount of type arguments for a kind."""
    if limit is None:
        limit = kind.args[0].args  # type: ignore
    return kind.args[1 : len(limit) + 1]