Skip to content

Allows to annotate instance as delegate #266

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ We follow Semantic Versions since the `0.1.0` release.
### Features

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

### Bugfixes

Expand Down
17 changes: 4 additions & 13 deletions classes/_registry.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from types import MethodType
from typing import Callable, Dict, NoReturn, Optional

TypeRegistry = Dict[type, Callable]
Expand All @@ -11,7 +10,7 @@ def choose_registry( # noqa: WPS211
typ: type,
is_protocol: bool,
delegate: Optional[type],
concretes: TypeRegistry,
delegates: TypeRegistry,
instances: TypeRegistry,
protocols: TypeRegistry,
) -> TypeRegistry:
Expand All @@ -21,20 +20,12 @@ def choose_registry( # noqa: WPS211
It depends on how ``instance`` method is used and also on the type itself.
"""
if is_protocol and delegate is not None:
raise ValueError('Both `is_protocol` and `delegated` are passed')
raise ValueError('Both `is_protocol` and `delegate` are passed')

if is_protocol:
return protocols

is_concrete = (
delegate is not None or
isinstance(getattr(typ, '__instancecheck__', None), MethodType)
)
if is_concrete:
# This means that this type has `__instancecheck__` defined,
# which allows dynamic checks of what `isinstance` of this type.
# That's why we also treat this type as a concrete.
return concretes
elif delegate is not None:
return delegates
return instances


Expand Down
35 changes: 18 additions & 17 deletions classes/_typeclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ class _TypeClass( # noqa: WPS214
'_associated_type',

# Registry:
'_concretes',
'_delegates',
'_instances',
'_protocols',

Expand Down Expand Up @@ -361,7 +361,7 @@ def __init__(
self._associated_type = associated_type

# Registries:
self._concretes: TypeRegistry = {}
self._delegates: TypeRegistry = {}
self._instances: TypeRegistry = {}
self._protocols: TypeRegistry = {}

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

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

# We never cache concrete types.
if self._dispatch_concrete(instance) is not None:
# We never cache delegate types.
if self._dispatch_delegate(instance) is not None:
return True

# This only happens when we don't have a cache in place
# and this is not a concrete generic:
# and this is not a delegate type:
impl = self._dispatch(instance, instance_type)
if impl is None:
return False
Expand All @@ -534,8 +534,9 @@ def instance(
We use this method to store implementation for each specific type.

Args:
is_protocol - required when passing protocols.
delegate - required when using concrete generics like ``List[str]``.
is_protocol: required when passing protocols.
delegate: required when using delegate types, for example,
when working with concrete generics like ``List[str]``.

Returns:
Decorator for instance handler.
Expand Down Expand Up @@ -570,7 +571,7 @@ def decorator(implementation):
typ=typ,
is_protocol=is_protocol,
delegate=delegate,
concretes=self._concretes,
delegates=self._delegates,
instances=self._instances,
protocols=self._protocols,
)
Expand Down Expand Up @@ -600,9 +601,9 @@ def _dispatch(self, instance, instance_type: type) -> Optional[Callable]:

return _find_impl(instance_type, self._instances)

def _dispatch_concrete(self, instance) -> Optional[Callable]:
for concrete, callback in self._concretes.items():
if isinstance(instance, concrete):
def _dispatch_delegate(self, instance) -> Optional[Callable]:
for delegate, callback in self._delegates.items():
if isinstance(instance, delegate):
return callback
return None

Expand Down
44 changes: 26 additions & 18 deletions classes/contrib/mypy/features/typeclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@
mro,
type_loader,
)
from classes.contrib.mypy.typeops.instance_context import InstanceContext
from classes.contrib.mypy.validation import (
validate_associated_type,
validate_typeclass,
validate_instance,
validate_typeclass_def,
)

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

# We need to add `Supports` metadata before typechecking,
# because it will affect type hierarchies.
metadata = mro.MetadataInjector(
typeclass.args[2],
instance_signature.arg_types[0],
ctx,
)
metadata.add_supports_metadata()

is_proper_typeclass = validate_typeclass.check_typeclass(
instance_context = InstanceContext.build(
typeclass_signature=typeclass.args[1],
instance_signature=instance_signature,
passed_args=ctx.type.args[0],
associated_type=typeclass.args[2],
fullname=fullname,
passed_types=ctx.type.args[0],
ctx=ctx,
)
if not is_proper_typeclass:
# Since the typeclass is not valid,
# we undo the metadata manipulation,
# otherwise we would spam with invalid `Supports[]` base types:
metadata.remove_supports_metadata()
if not self._run_validation(instance_context):
return AnyType(TypeOfAny.from_error)

# If typeclass is checked, than it is safe to add new instance types:
self._add_new_instance_type(
typeclass=typeclass,
new_type=instance_signature.arg_types[0],
new_type=instance_context.instance_type,
ctx=ctx,
)
return ctx.default_return_type
Expand All @@ -247,6 +236,25 @@ def _load_typeclass(
assert isinstance(typeclass, Instance)
return typeclass, typeclass_ref.args[3].value

def _run_validation(self, instance_context: InstanceContext) -> bool:
# We need to add `Supports` metadata before typechecking,
# because it will affect type hierarchies.
metadata = mro.MetadataInjector(
associated_type=instance_context.associated_type,
instance_type=instance_context.instance_type,
delegate=instance_context.delegate,
ctx=instance_context.ctx,
)
metadata.add_supports_metadata()

is_proper_instance = validate_instance.check_type(instance_context)
if not is_proper_instance:
# Since the typeclass is not valid,
# we undo the metadata manipulation,
# otherwise we would spam with invalid `Supports[]` base types:
metadata.remove_supports_metadata()
return is_proper_instance

def _add_new_instance_type(
self,
typeclass: Instance,
Expand Down
Loading