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]
|