Skip to content

Commit 1d4f722

Browse files
committed
WIP
1 parent 9e300d7 commit 1d4f722

File tree

11 files changed

+503
-227
lines changed

11 files changed

+503
-227
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ We follow Semantic Versions since the `0.1.0` release.
88
### Features
99

1010
- Adds support for concrete generic types like `List[str]` and `Set[int]` #24
11+
- Adds support for types that have `__instancecheck__` defined
12+
via `delegate` argument #248
1113
- Adds support for multiple type arguments in `Supports` type #244
12-
- Adds support for types that have `__instancecheck__` defined #248
1314

1415
### Bugfixes
1516

classes/_registry.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,7 @@ def choose_registry( # noqa: WPS211
2525

2626
if is_protocol:
2727
return protocols
28-
29-
is_concrete = (
30-
delegate is not None or
31-
isinstance(getattr(typ, '__instancecheck__', None), MethodType)
32-
)
33-
if is_concrete:
34-
# This means that this type has `__instancecheck__` defined,
35-
# which allows dynamic checks of what `isinstance` of this type.
36-
# That's why we also treat this type as a concrete.
28+
elif delegate is not None:
3729
return concretes
3830
return instances
3931

classes/contrib/mypy/features/typeclass.py

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@
1616

1717
from classes.contrib.mypy.typeops import (
1818
call_signatures,
19-
fallback,
2019
instance_args,
2120
mro,
2221
type_loader,
2322
)
23+
from classes.contrib.mypy.typeops.instance_context import InstanceContext
2424
from classes.contrib.mypy.validation import (
2525
validate_associated_type,
26-
validate_typeclass,
26+
validate_instance,
2727
validate_typeclass_def,
2828
)
2929

@@ -184,10 +184,10 @@ class InstanceDefReturnType(object):
184184
185185
"""
186186

187-
@fallback.error_to_any({
188-
# TODO: later we can use a custom exception type for this:
189-
KeyError: 'Typeclass cannot be loaded, it must be a global declaration',
190-
})
187+
# @fallback.error_to_any({
188+
# # TODO: later we can use a custom exception type for this:
189+
# KeyError: 'Typeclass cannot be loaded, it must be a global declaration',
190+
# })
191191
def __call__(self, ctx: MethodContext) -> MypyType:
192192
"""Main entry point."""
193193
assert isinstance(ctx.type, Instance)
@@ -201,33 +201,21 @@ def __call__(self, ctx: MethodContext) -> MypyType:
201201
if not isinstance(instance_signature, CallableType):
202202
return ctx.default_return_type
203203

204-
# We need to add `Supports` metadata before typechecking,
205-
# because it will affect type hierarchies.
206-
metadata = mro.MetadataInjector(
207-
typeclass.args[2],
208-
instance_signature.arg_types[0],
209-
ctx,
210-
)
211-
metadata.add_supports_metadata()
212-
213-
is_proper_typeclass = validate_typeclass.check_typeclass(
204+
instance_context = InstanceContext.build(
214205
typeclass_signature=typeclass.args[1],
215206
instance_signature=instance_signature,
207+
passed_args=ctx.type.args[0],
208+
associated_type=typeclass.args[2],
216209
fullname=fullname,
217-
passed_types=ctx.type.args[0],
218210
ctx=ctx,
219211
)
220-
if not is_proper_typeclass:
221-
# Since the typeclass is not valid,
222-
# we undo the metadata manipulation,
223-
# otherwise we would spam with invalid `Supports[]` base types:
224-
metadata.remove_supports_metadata()
212+
if not self._run_validation(instance_context):
225213
return AnyType(TypeOfAny.from_error)
226214

227215
# If typeclass is checked, than it is safe to add new instance types:
228216
self._add_new_instance_type(
229217
typeclass=typeclass,
230-
new_type=instance_signature.arg_types[0],
218+
new_type=instance_context.instance_type,
231219
ctx=ctx,
232220
)
233221
return ctx.default_return_type
@@ -247,6 +235,25 @@ def _load_typeclass(
247235
assert isinstance(typeclass, Instance)
248236
return typeclass, typeclass_ref.args[3].value
249237

238+
def _run_validation(self, instance_context: InstanceContext) -> bool:
239+
# We need to add `Supports` metadata before typechecking,
240+
# because it will affect type hierarchies.
241+
metadata = mro.MetadataInjector(
242+
associated_type=instance_context.associated_type,
243+
instance_type=instance_context.instance_type,
244+
delegate=instance_context.delegate,
245+
ctx=instance_context.ctx,
246+
)
247+
metadata.add_supports_metadata()
248+
249+
is_proper_instance = validate_instance.check_type(instance_context)
250+
if not is_proper_instance:
251+
# Since the typeclass is not valid,
252+
# we undo the metadata manipulation,
253+
# otherwise we would spam with invalid `Supports[]` base types:
254+
metadata.remove_supports_metadata()
255+
return is_proper_instance
256+
250257
def _add_new_instance_type(
251258
self,
252259
typeclass: Instance,
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
from typing import NamedTuple, Optional, Tuple
2+
3+
from mypy.plugin import MethodContext
4+
from mypy.sametypes import is_same_type
5+
from mypy.types import (
6+
CallableType,
7+
FunctionLike,
8+
Instance,
9+
LiteralType,
10+
TupleType,
11+
)
12+
from mypy.types import Type as MypyType
13+
from mypy.types import UninhabitedType
14+
from typing_extensions import final
15+
16+
from classes.contrib.mypy.typeops import inference
17+
18+
19+
@final
20+
class InstanceContext(NamedTuple):
21+
""""""
22+
23+
# Signatures:
24+
typeclass_signature: CallableType
25+
instance_signature: CallableType
26+
infered_signature: CallableType
27+
28+
# Instance / runtime types:
29+
instance_type: MypyType
30+
runtime_type: MypyType
31+
32+
# Passed arguments:
33+
passed_args: TupleType
34+
is_protocol: Optional[bool]
35+
delegate: Optional[MypyType]
36+
37+
# Meta:
38+
fullname: str
39+
associated_type: MypyType
40+
41+
# Mypy context:
42+
ctx: MethodContext
43+
44+
@classmethod
45+
def build(
46+
cls,
47+
typeclass_signature: CallableType,
48+
instance_signature: CallableType,
49+
passed_args: TupleType,
50+
associated_type: MypyType,
51+
fullname: str,
52+
ctx: MethodContext,
53+
) -> 'InstanceContext':
54+
runtime_type = inference.infer_runtime_type_from_context(
55+
fallback=passed_args.items[0],
56+
fullname=fullname,
57+
ctx=ctx,
58+
)
59+
60+
infered_signature = inference.try_to_apply_generics(
61+
signature=typeclass_signature,
62+
runtime_type=runtime_type,
63+
ctx=ctx,
64+
)
65+
66+
is_protocol, delegate = _ArgumentInference(passed_args)()
67+
instance_type = _infer_instance_type(
68+
instance_type=instance_signature.arg_types[0],
69+
runtime_type=runtime_type,
70+
delegate=delegate,
71+
)
72+
73+
return InstanceContext(
74+
typeclass_signature=typeclass_signature,
75+
instance_signature=instance_signature,
76+
infered_signature=infered_signature,
77+
instance_type=instance_type,
78+
runtime_type=runtime_type,
79+
passed_args=passed_args,
80+
is_protocol=is_protocol,
81+
delegate=delegate,
82+
associated_type=associated_type,
83+
fullname=fullname,
84+
ctx=ctx,
85+
)
86+
87+
88+
@final
89+
class _ArgumentInference(object):
90+
__slots__ = ('_passed_args',)
91+
92+
def __init__(self, passed_args: TupleType) -> None:
93+
self._passed_args = passed_args
94+
95+
def __call__(self) -> Tuple[Optional[bool], Optional[MypyType]]:
96+
_, is_protocol, delegate = self._passed_args.items
97+
return (
98+
self._infer_protocol_arg(is_protocol),
99+
self._infer_delegate_arg(delegate),
100+
)
101+
102+
def _infer_protocol_arg(
103+
self,
104+
is_protocol: MypyType,
105+
) -> Optional[bool]:
106+
if isinstance(is_protocol, UninhabitedType):
107+
return False
108+
109+
is_protocol_bool = (
110+
isinstance(is_protocol, Instance) and
111+
isinstance(is_protocol.last_known_value, LiteralType) and
112+
isinstance(is_protocol.last_known_value.value, bool)
113+
)
114+
if is_protocol_bool:
115+
return is_protocol.last_known_value.value # type: ignore
116+
return None
117+
118+
def _infer_delegate_arg(
119+
self,
120+
delegate: MypyType,
121+
) -> Optional[MypyType]:
122+
if isinstance(delegate, FunctionLike) and delegate.is_type_obj():
123+
return delegate.items()[-1].ret_type
124+
return None
125+
126+
127+
def _infer_instance_type(
128+
instance_type: MypyType,
129+
runtime_type: MypyType,
130+
delegate: Optional[MypyType],
131+
) -> MypyType:
132+
# TODO: document
133+
if delegate is not None and is_same_type(instance_type, delegate):
134+
return runtime_type
135+
return instance_type

0 commit comments

Comments
 (0)