Skip to content

Commit ac2a682

Browse files
authored
Allows to annotate instance as delegate (#266)
* WIP * WIP * Finishes with delegates * Fixes CI
1 parent 2fc8b6b commit ac2a682

File tree

17 files changed

+780
-368
lines changed

17 files changed

+780
-368
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: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from types import MethodType
21
from typing import Callable, Dict, NoReturn, Optional
32

43
TypeRegistry = Dict[type, Callable]
@@ -11,7 +10,7 @@ def choose_registry( # noqa: WPS211
1110
typ: type,
1211
is_protocol: bool,
1312
delegate: Optional[type],
14-
concretes: TypeRegistry,
13+
delegates: TypeRegistry,
1514
instances: TypeRegistry,
1615
protocols: TypeRegistry,
1716
) -> TypeRegistry:
@@ -21,20 +20,12 @@ def choose_registry( # noqa: WPS211
2120
It depends on how ``instance`` method is used and also on the type itself.
2221
"""
2322
if is_protocol and delegate is not None:
24-
raise ValueError('Both `is_protocol` and `delegated` are passed')
23+
raise ValueError('Both `is_protocol` and `delegate` are passed')
2524

2625
if is_protocol:
2726
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.
37-
return concretes
27+
elif delegate is not None:
28+
return delegates
3829
return instances
3930

4031

classes/_typeclass.py

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ class _TypeClass( # noqa: WPS214
312312
'_associated_type',
313313

314314
# Registry:
315-
'_concretes',
315+
'_delegates',
316316
'_instances',
317317
'_protocols',
318318

@@ -361,7 +361,7 @@ def __init__(
361361
self._associated_type = associated_type
362362

363363
# Registries:
364-
self._concretes: TypeRegistry = {}
364+
self._delegates: TypeRegistry = {}
365365
self._instances: TypeRegistry = {}
366366
self._protocols: TypeRegistry = {}
367367

@@ -418,13 +418,13 @@ def __call__(
418418
And all typeclasses that match ``Callable[[int, int], int]`` signature
419419
will typecheck.
420420
"""
421-
# At first, we try all our concrete types,
422-
# we don't cache it, because we cannot.
421+
# At first, we try all our delegate types,
422+
# we don't cache it, because it is impossible.
423423
# We only have runtime type info: `type([1]) == type(['a'])`.
424424
# It might be slow!
425-
# Don't add concrete types unless
425+
# Don't add any delegate types unless
426426
# you are absolutely know what you are doing.
427-
impl = self._dispatch_concrete(instance)
427+
impl = self._dispatch_delegate(instance)
428428
if impl is not None:
429429
return impl(instance, *args, **kwargs)
430430

@@ -499,21 +499,21 @@ def supports(
499499
See also: https://www.python.org/dev/peps/pep-0647
500500
"""
501501
# Here we first check that instance is already in the cache
502-
# and only then we check concrete types.
502+
# and only then we check delegate types.
503503
# Why?
504504
# Because if some type is already in the cache,
505-
# it means that it is not concrete.
505+
# it means that it is not a delegate.
506506
# So, this is simply faster.
507507
instance_type = type(instance)
508508
if instance_type in self._dispatch_cache:
509509
return True
510510

511-
# We never cache concrete types.
512-
if self._dispatch_concrete(instance) is not None:
511+
# We never cache delegate types.
512+
if self._dispatch_delegate(instance) is not None:
513513
return True
514514

515515
# This only happens when we don't have a cache in place
516-
# and this is not a concrete generic:
516+
# and this is not a delegate type:
517517
impl = self._dispatch(instance, instance_type)
518518
if impl is None:
519519
return False
@@ -534,8 +534,9 @@ def instance(
534534
We use this method to store implementation for each specific type.
535535
536536
Args:
537-
is_protocol - required when passing protocols.
538-
delegate - required when using concrete generics like ``List[str]``.
537+
is_protocol: required when passing protocols.
538+
delegate: required when using delegate types, for example,
539+
when working with concrete generics like ``List[str]``.
539540
540541
Returns:
541542
Decorator for instance handler.
@@ -570,7 +571,7 @@ def decorator(implementation):
570571
typ=typ,
571572
is_protocol=is_protocol,
572573
delegate=delegate,
573-
concretes=self._concretes,
574+
delegates=self._delegates,
574575
instances=self._instances,
575576
protocols=self._protocols,
576577
)
@@ -600,9 +601,9 @@ def _dispatch(self, instance, instance_type: type) -> Optional[Callable]:
600601

601602
return _find_impl(instance_type, self._instances)
602603

603-
def _dispatch_concrete(self, instance) -> Optional[Callable]:
604-
for concrete, callback in self._concretes.items():
605-
if isinstance(instance, concrete):
604+
def _dispatch_delegate(self, instance) -> Optional[Callable]:
605+
for delegate, callback in self._delegates.items():
606+
if isinstance(instance, delegate):
606607
return callback
607608
return None
608609

classes/contrib/mypy/features/typeclass.py

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@
2121
mro,
2222
type_loader,
2323
)
24+
from classes.contrib.mypy.typeops.instance_context import InstanceContext
2425
from classes.contrib.mypy.validation import (
2526
validate_associated_type,
26-
validate_typeclass,
27+
validate_instance,
2728
validate_typeclass_def,
2829
)
2930

@@ -201,33 +202,21 @@ def __call__(self, ctx: MethodContext) -> MypyType:
201202
if not isinstance(instance_signature, CallableType):
202203
return ctx.default_return_type
203204

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(
205+
instance_context = InstanceContext.build(
214206
typeclass_signature=typeclass.args[1],
215207
instance_signature=instance_signature,
208+
passed_args=ctx.type.args[0],
209+
associated_type=typeclass.args[2],
216210
fullname=fullname,
217-
passed_types=ctx.type.args[0],
218211
ctx=ctx,
219212
)
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()
213+
if not self._run_validation(instance_context):
225214
return AnyType(TypeOfAny.from_error)
226215

227216
# If typeclass is checked, than it is safe to add new instance types:
228217
self._add_new_instance_type(
229218
typeclass=typeclass,
230-
new_type=instance_signature.arg_types[0],
219+
new_type=instance_context.instance_type,
231220
ctx=ctx,
232221
)
233222
return ctx.default_return_type
@@ -247,6 +236,25 @@ def _load_typeclass(
247236
assert isinstance(typeclass, Instance)
248237
return typeclass, typeclass_ref.args[3].value
249238

239+
def _run_validation(self, instance_context: InstanceContext) -> bool:
240+
# We need to add `Supports` metadata before typechecking,
241+
# because it will affect type hierarchies.
242+
metadata = mro.MetadataInjector(
243+
associated_type=instance_context.associated_type,
244+
instance_type=instance_context.instance_type,
245+
delegate=instance_context.delegate,
246+
ctx=instance_context.ctx,
247+
)
248+
metadata.add_supports_metadata()
249+
250+
is_proper_instance = validate_instance.check_type(instance_context)
251+
if not is_proper_instance:
252+
# Since the typeclass is not valid,
253+
# we undo the metadata manipulation,
254+
# otherwise we would spam with invalid `Supports[]` base types:
255+
metadata.remove_supports_metadata()
256+
return is_proper_instance
257+
250258
def _add_new_instance_type(
251259
self,
252260
typeclass: Instance,

0 commit comments

Comments
 (0)