Skip to content

Commit c59c4a5

Browse files
committed
Adds concrete generics support
1 parent a3c9b82 commit c59c4a5

File tree

13 files changed

+430
-104
lines changed

13 files changed

+430
-104
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ We follow Semantic Versions since the `0.1.0` release.
77

88
### Features
99

10+
- Adds support for concrete generic types like `List[str]` and `Set[int]` #24
1011
- Adds support for multiple type arguments in `Supports` type #244
1112
- Adds support for types that have `__instancecheck__` defined #248
1213

classes/_registry.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from types import MethodType
2+
from typing import Callable, Dict, NoReturn, Optional
3+
4+
TypeRegistry = Dict[type, Callable]
5+
6+
7+
def choose_registry( # noqa: WPS211
8+
# It has multiple argumnets, but I don't see an easy and performant way
9+
# to refactor it: I don't want to create extra structures
10+
# and I don't want to create a class with methods.
11+
typ: type,
12+
is_protocol: bool,
13+
delegate: Optional[type],
14+
concretes: TypeRegistry,
15+
instances: TypeRegistry,
16+
protocols: TypeRegistry,
17+
) -> TypeRegistry:
18+
"""
19+
Returns the appropriate registry to store the passed type.
20+
21+
It depends on how ``instance`` method is used and also on the type itself.
22+
"""
23+
if is_protocol:
24+
return protocols
25+
26+
is_concrete = (
27+
delegate is not None or
28+
isinstance(getattr(typ, '__instancecheck__', None), MethodType)
29+
)
30+
if is_concrete:
31+
# This means that this type has `__instancecheck__` defined,
32+
# which allows dynamic checks of what `isinstance` of this type.
33+
# That's why we also treat this type as a conrete.
34+
return concretes
35+
return instances
36+
37+
38+
def default_implementation(instance, *args, **kwargs) -> NoReturn:
39+
"""By default raises an exception."""
40+
raise NotImplementedError(
41+
'Missing matched typeclass instance for type: {0}'.format(
42+
type(instance).__qualname__,
43+
),
44+
)

classes/_typeclass.py

Lines changed: 53 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -114,16 +114,12 @@
114114
115115
See our `official docs <https://classes.readthedocs.io>`_ to learn more!
116116
"""
117-
118-
from abc import get_cache_token
119117
from functools import _find_impl # type: ignore # noqa: WPS450
120-
from types import MethodType
121118
from typing import ( # noqa: WPS235
122119
TYPE_CHECKING,
123120
Callable,
124121
Dict,
125122
Generic,
126-
NoReturn,
127123
Optional,
128124
Type,
129125
TypeVar,
@@ -134,6 +130,12 @@
134130

135131
from typing_extensions import TypeGuard, final
136132

133+
from classes._registry import (
134+
TypeRegistry,
135+
choose_registry,
136+
default_implementation,
137+
)
138+
137139
_InstanceType = TypeVar('_InstanceType')
138140
_SignatureType = TypeVar('_SignatureType', bound=Callable)
139141
_AssociatedType = TypeVar('_AssociatedType')
@@ -305,12 +307,17 @@ class _TypeClass( # noqa: WPS214
305307
"""
306308

307309
__slots__ = (
310+
# Str:
308311
'_signature',
309312
'_associated_type',
313+
314+
# Registry:
315+
'_concretes',
310316
'_instances',
311317
'_protocols',
318+
319+
# Cache:
312320
'_dispatch_cache',
313-
'_cache_token',
314321
)
315322

316323
_dispatch_cache: Dict[type, Callable]
@@ -349,16 +356,17 @@ def __init__(
349356
The only exception is the first argument: it is polymorfic.
350357
351358
"""
352-
self._instances: Dict[type, Callable] = {}
353-
self._protocols: Dict[type, Callable] = {}
354-
355359
# We need this for `repr`:
356360
self._signature = signature
357361
self._associated_type = associated_type
358362

363+
# Registries:
364+
self._concretes: TypeRegistry = {}
365+
self._instances: TypeRegistry = {}
366+
self._protocols: TypeRegistry = {}
367+
359368
# Cache parts:
360369
self._dispatch_cache = WeakKeyDictionary() # type: ignore
361-
self._cache_token = None
362370

363371
def __call__(
364372
self,
@@ -410,7 +418,16 @@ def __call__(
410418
And all typeclasses that match ``Callable[[int, int], int]`` signature
411419
will typecheck.
412420
"""
413-
self._control_abc_cache()
421+
# At first, we try all our conrete types,
422+
# we don't cache it, because we cannot.
423+
# We only have runtime type info: `type([1]) == type(['a'])`.
424+
# It might be slow!
425+
# Don't add concrete types unless
426+
# you are absolutely know what you are doing.
427+
impl = self._dispatch_concrete(instance)
428+
if impl is not None:
429+
return impl(instance, *args, **kwargs)
430+
414431
instance_type = type(instance)
415432

416433
try:
@@ -419,7 +436,7 @@ def __call__(
419436
impl = self._dispatch(
420437
instance,
421438
instance_type,
422-
) or self._default_implementation
439+
) or default_implementation
423440
self._dispatch_cache[instance_type] = impl
424441
return impl(instance, *args, **kwargs)
425442

@@ -481,16 +498,24 @@ def supports(
481498
482499
See also: https://www.python.org/dev/peps/pep-0647
483500
"""
484-
self._control_abc_cache()
485-
501+
# Here we first check that instance is already in the cache
502+
# and only then we check concrete types.
503+
# Why?
504+
# Because if some type is already in the cache,
505+
# it means that it is not concrete.
506+
# So, this is simply faster.
486507
instance_type = type(instance)
487508
if instance_type in self._dispatch_cache:
488509
return True
489510

490-
# This only happens when we don't have a cache in place:
511+
# We never cache concrete types.
512+
if self._dispatch_concrete(instance) is not None:
513+
return True
514+
515+
# This only happens when we don't have a cache in place
516+
# and this is not a concrete generic:
491517
impl = self._dispatch(instance, instance_type)
492518
if impl is None:
493-
self._dispatch_cache[instance_type] = self._default_implementation
494519
return False
495520

496521
self._dispatch_cache[instance_type] = impl
@@ -541,35 +566,21 @@ def instance(
541566
isinstance(object(), typ)
542567

543568
def decorator(implementation):
544-
container = self._protocols if is_protocol else self._instances
569+
container = choose_registry(
570+
typ=typ,
571+
is_protocol=is_protocol,
572+
delegate=delegate,
573+
concretes=self._concretes,
574+
instances=self._instances,
575+
protocols=self._protocols,
576+
)
545577
container[typ] = implementation
546578

547-
if isinstance(getattr(typ, '__instancecheck__', None), MethodType):
548-
# This means that this type has `__instancecheck__` defined,
549-
# which allows dynamic checks of what `isinstance` of this type.
550-
# That's why we also treat this type as a protocol.
551-
self._protocols[typ] = implementation
552-
553-
if self._cache_token is None: # pragma: no cover
554-
if getattr(typ, '__abstractmethods__', None):
555-
self._cache_token = get_cache_token()
556579
self._dispatch_cache.clear()
557580
return implementation
558581

559582
return decorator
560583

561-
def _control_abc_cache(self) -> None:
562-
"""
563-
Required to drop cache if ``abc`` type got new subtypes in runtime.
564-
565-
Copied from ``cpython``.
566-
"""
567-
if self._cache_token is not None:
568-
current_token = get_cache_token()
569-
if self._cache_token != current_token:
570-
self._dispatch_cache.clear()
571-
self._cache_token = current_token
572-
573584
def _dispatch(self, instance, instance_type: type) -> Optional[Callable]:
574585
"""
575586
Dispatches a function by its type.
@@ -589,13 +600,11 @@ def _dispatch(self, instance, instance_type: type) -> Optional[Callable]:
589600

590601
return _find_impl(instance_type, self._instances)
591602

592-
def _default_implementation(self, instance, *args, **kwargs) -> NoReturn:
593-
"""By default raises an exception."""
594-
raise NotImplementedError(
595-
'Missing matched typeclass instance for type: {0}'.format(
596-
type(instance).__qualname__,
597-
),
598-
)
603+
def _dispatch_concrete(self, instance) -> Optional[Callable]:
604+
for concrete, callback in self._concretes.items():
605+
if isinstance(instance, concrete):
606+
return callback
607+
return None
599608

600609

601610
if TYPE_CHECKING:

classes/contrib/mypy/validation/validate_instance_args.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
@final
2121
class _ArgValidationContext(NamedTuple):
22-
""""""
22+
"""Context for instance arg validation."""
2323

2424
is_protocol: bool
2525
delegate: Optional[MypyType]
@@ -30,6 +30,14 @@ def check_type(
3030
passed_types: TupleType,
3131
ctx: MethodContext,
3232
) -> _ArgValidationContext:
33+
"""
34+
Checks that args to ``.instance`` method are correct.
35+
36+
We cannot use ``@overload`` on ``.instance`` because ``mypy``
37+
does not correctly handle ``ctx.api.fail`` on ``@overload`` items:
38+
it then tries new ones, which produce incorrect results.
39+
So, that's why we need this custom checker.
40+
"""
3341
passed_args = passed_types.items
3442

3543
is_protocol, protocol_check = _check_protocol_arg(passed_args[1], ctx)
@@ -72,10 +80,6 @@ def _check_delegate_arg(
7280
delegate: MypyType,
7381
ctx: MethodContext,
7482
) -> Tuple[Optional[MypyType], bool]:
75-
# TODO: maybe we need to inforce that `delegate` should be
76-
# similar to `runtime_type`?
77-
# For example, we can ask for subtypes of `runtime_type`.
78-
# However, motivation is not clear for now.
7983
if isinstance(delegate, FunctionLike) and delegate.is_type_obj():
8084
return delegate.items()[-1].ret_type, True
8185
return None, True

classes/contrib/mypy/validation/validate_runtime.py

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -85,22 +85,18 @@ def check_instance_definition(
8585
ctx.context,
8686
)
8787

88-
return _RuntimeValidationContext(
89-
runtime_type=runtime_type,
90-
is_protocol=args_check.is_protocol,
91-
check_result=all([
92-
args_check.check_result,
93-
instance_check,
94-
95-
_check_runtime_protocol(
96-
runtime_type, ctx, is_protocol=args_check.is_protocol,
97-
),
98-
_check_concrete_generics(
99-
runtime_type, instance_type, args_check.delegate, ctx,
100-
),
101-
_check_tuple_size(instance_type, ctx),
102-
],
103-
))
88+
return _RuntimeValidationContext(runtime_type, args_check.is_protocol, all([
89+
args_check.check_result,
90+
instance_check,
91+
92+
_check_runtime_protocol(
93+
runtime_type, ctx, is_protocol=args_check.is_protocol,
94+
),
95+
_check_concrete_generics(
96+
runtime_type, instance_type, args_check.delegate, ctx,
97+
),
98+
_check_tuple_size(instance_type, ctx),
99+
]))
104100

105101

106102
def _check_runtime_protocol(

docs/pages/api-docs.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
Typeclass
22
=========
33

4+
Caching
5+
-------
6+
7+
API
8+
---
9+
410
Here are the technical docs about ``typeclass`` and how to use it.
511

612
.. automodule:: classes._typeclass

0 commit comments

Comments
 (0)