Skip to content

Commit 91905d0

Browse files
committed
Adds Supports[] type with multiple type args
1 parent fdd5c3d commit 91905d0

File tree

13 files changed

+458
-72
lines changed

13 files changed

+458
-72
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@
33
We follow Semantic Versions since the `0.1.0` release.
44

55

6+
## Version 0.4.0 WIP
7+
8+
### Features
9+
10+
- Adds support for multiple type arguments in `Supports` type
11+
12+
### Bugfixes
13+
14+
- Fixes that types referenced in multiple typeclasses
15+
were not handling `Supports` properly #249
16+
17+
618
## Version 0.3.0
719

820
### Features

classes/_typeclass.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@
140140

141141
_NewInstanceType = TypeVar('_NewInstanceType', bound=Type)
142142

143-
_StrictAssociatedType = TypeVar('_StrictAssociatedType', bound='AssociatedType')
143+
_AssociatedTypeDef = TypeVar('_AssociatedTypeDef', contravariant=True)
144144
_TypeClassType = TypeVar('_TypeClassType', bound='_TypeClass')
145145
_ReturnType = TypeVar('_ReturnType')
146146

@@ -240,7 +240,7 @@ def __class_getitem__(cls, type_params) -> type:
240240

241241

242242
@final
243-
class Supports(Generic[_StrictAssociatedType]):
243+
class Supports(Generic[_AssociatedTypeDef]):
244244
"""
245245
Used to specify that some value is a part of a typeclass.
246246

classes/contrib/mypy/classes_plugin.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@
3030
from mypy.types import Type as MypyType
3131
from typing_extensions import Final, final
3232

33-
from classes.contrib.mypy.features import associated_type, typeclass
33+
from classes.contrib.mypy.features import associated_type, supports, typeclass
3434

35+
_ASSOCIATED_TYPE_FULLNAME: Final = 'classes._typeclass.AssociatedType'
3536
_TYPECLASS_FULLNAME: Final = 'classes._typeclass._TypeClass'
3637
_TYPECLASS_DEF_FULLNAME: Final = 'classes._typeclass._TypeClassDef'
3738
_TYPECLASS_INSTANCE_DEF_FULLNAME: Final = (
@@ -58,8 +59,14 @@ def get_type_analyze_hook(
5859
fullname: str,
5960
) -> Optional[Callable[[AnalyzeTypeContext], MypyType]]:
6061
"""Hook that works on type analyzer phase."""
61-
if fullname == 'classes._typeclass.AssociatedType':
62+
if fullname == _ASSOCIATED_TYPE_FULLNAME:
6263
return associated_type.variadic_generic
64+
if fullname == 'classes._typeclass.Supports':
65+
associated_type_node = self.lookup_fully_qualified(
66+
_ASSOCIATED_TYPE_FULLNAME,
67+
)
68+
assert associated_type_node
69+
return supports.VariadicGeneric(associated_type_node)
6370
return None
6471

6572
def get_function_hook(
@@ -80,7 +87,7 @@ def get_method_hook(
8087
) -> Optional[Callable[[MethodContext], MypyType]]:
8188
"""Here we adjust the typeclass with new allowed types."""
8289
if fullname == '{0}.__call__'.format(_TYPECLASS_DEF_FULLNAME):
83-
return typeclass.typeclass_def_return_type
90+
return typeclass.TypeClassDefReturnType(_ASSOCIATED_TYPE_FULLNAME)
8491
if fullname == '{0}.__call__'.format(_TYPECLASS_INSTANCE_DEF_FULLNAME):
8592
return typeclass.InstanceDefReturnType()
8693
if fullname == '{0}.instance'.format(_TYPECLASS_FULLNAME):
Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,11 @@
11
from mypy.plugin import AnalyzeTypeContext
2-
from mypy.types import Instance
32
from mypy.types import Type as MypyType
43

4+
from classes.contrib.mypy.semanal.variadic_generic import (
5+
analize_variadic_generic,
6+
)
57

6-
def variadic_generic(ctx: AnalyzeTypeContext) -> MypyType:
7-
"""
8-
Variadic generic support.
9-
10-
What is "variadic generic"?
11-
It is a generic type with any amount of type variables.
12-
Starting with 0 up to infinity.
138

14-
We primarily use it for our ``AssociatedType`` implementation.
15-
"""
16-
sym = ctx.api.lookup_qualified(ctx.type.name, ctx.context) # type: ignore
17-
if not sym or not sym.node:
18-
# This will happen if `Supports[IsNotDefined]` will be called.
19-
return ctx.type
20-
return Instance(
21-
sym.node,
22-
ctx.api.anal_array(ctx.type.args), # type: ignore
23-
)
9+
def variadic_generic(ctx: AnalyzeTypeContext) -> MypyType:
10+
"""Variadic generic support for ``AssociatedType`` type."""
11+
return analize_variadic_generic(ctx)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from mypy.nodes import SymbolTableNode
2+
from mypy.plugin import AnalyzeTypeContext
3+
from mypy.types import Instance
4+
from mypy.types import Type as MypyType
5+
from mypy.types import UnionType
6+
from typing_extensions import final
7+
8+
from classes.contrib.mypy.semanal.variadic_generic import (
9+
analize_variadic_generic,
10+
)
11+
from classes.contrib.mypy.validation import validate_supports
12+
13+
14+
@final
15+
class VariadicGeneric(object):
16+
"""
17+
Variadic generic support for ``Supports`` type.
18+
19+
We also need to validate that
20+
all type args of ``Supports`` are subtypes of ``AssociatedType``.
21+
"""
22+
23+
__slots__ = ('_associated_type_node',)
24+
25+
def __init__(self, associated_type_node: SymbolTableNode) -> None:
26+
"""We need ``AssociatedType`` fullname here."""
27+
self._associated_type_node = associated_type_node
28+
29+
def __call__(self, ctx: AnalyzeTypeContext) -> MypyType:
30+
"""Main entry point."""
31+
analyzed_type = analize_variadic_generic(
32+
validate_callback=self._validate,
33+
ctx=ctx,
34+
)
35+
if isinstance(analyzed_type, Instance):
36+
return analyzed_type.copy_modified(
37+
args=[UnionType.make_union(analyzed_type.args)],
38+
)
39+
return analyzed_type
40+
41+
def _validate(self, instance: Instance, ctx: AnalyzeTypeContext) -> bool:
42+
return validate_supports.check_type(
43+
instance,
44+
self._associated_type_node,
45+
ctx,
46+
)

classes/contrib/mypy/features/typeclass.py

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -103,35 +103,46 @@ def _process_typeclass_def_return_type(
103103
return typeclass_intermediate_def
104104

105105

106-
def typeclass_def_return_type(ctx: MethodContext) -> MypyType:
106+
@final
107+
class TypeClassDefReturnType(object):
107108
"""
108109
Callback for cases like ``@typeclass(SomeType)``.
109110
110111
What it does? It works with the associated types.
111112
It checks that ``SomeType`` is correct, modifies the current typeclass.
112113
And returns it back.
113114
"""
114-
assert isinstance(ctx.default_return_type, Instance)
115-
assert isinstance(ctx.context, Decorator)
116115

117-
instance_args.mutate_typeclass_def(
118-
typeclass=ctx.default_return_type,
119-
definition_fullname=ctx.context.func.fullname,
120-
ctx=ctx,
121-
)
116+
__slots__ = ('_associated_type',)
122117

123-
validate_typeclass_def.check_type(
124-
typeclass=ctx.default_return_type,
125-
ctx=ctx,
126-
)
127-
if isinstance(ctx.default_return_type.args[2], Instance):
128-
validate_associated_type.check_type(
129-
associated_type=ctx.default_return_type.args[2],
118+
def __init__(self, associated_type: str) -> None:
119+
"""We need ``AssociatedType`` fullname here."""
120+
self._associated_type = associated_type
121+
122+
def __call__(self, ctx: MethodContext) -> MypyType:
123+
"""Main entry point."""
124+
assert isinstance(ctx.default_return_type, Instance)
125+
assert isinstance(ctx.context, Decorator)
126+
127+
instance_args.mutate_typeclass_def(
130128
typeclass=ctx.default_return_type,
129+
definition_fullname=ctx.context.func.fullname,
131130
ctx=ctx,
132131
)
133132

134-
return ctx.default_return_type
133+
validate_typeclass_def.check_type(
134+
typeclass=ctx.default_return_type,
135+
ctx=ctx,
136+
)
137+
if isinstance(ctx.default_return_type.args[2], Instance):
138+
validate_associated_type.check_type(
139+
associated_type=ctx.default_return_type.args[2],
140+
associated_type_fullname=self._associated_type,
141+
typeclass=ctx.default_return_type,
142+
ctx=ctx,
143+
)
144+
145+
return ctx.default_return_type
135146

136147

137148
def instance_return_type(ctx: MethodContext) -> MypyType:

classes/contrib/mypy/semanal/__init__.py

Whitespace-only changes.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from typing import Callable, Optional
2+
3+
from mypy.plugin import AnalyzeTypeContext
4+
from mypy.types import Instance
5+
from mypy.types import Type as MypyType
6+
7+
_ValidateCallback = Callable[[Instance, AnalyzeTypeContext], bool]
8+
9+
10+
def analize_variadic_generic(
11+
ctx: AnalyzeTypeContext,
12+
validate_callback: Optional[_ValidateCallback] = None,
13+
) -> MypyType:
14+
"""
15+
Variadic generic support.
16+
17+
What is "variadic generic"?
18+
It is a generic type with any amount of type variables.
19+
Starting with 0 up to infinity.
20+
21+
We also conditionally validate types of passed arguments.
22+
"""
23+
sym = ctx.api.lookup_qualified(ctx.type.name, ctx.context) # type: ignore
24+
if not sym or not sym.node:
25+
# This will happen if `Supports[IsNotDefined]` will be called.
26+
return ctx.type
27+
28+
instance = Instance(
29+
sym.node,
30+
ctx.api.anal_array(ctx.type.args), # type: ignore
31+
)
32+
33+
if validate_callback is not None:
34+
validate_callback(instance, ctx)
35+
return instance

classes/contrib/mypy/typeops/mro.py

Lines changed: 78 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
from typing import List
1+
from typing import List, Optional
22

33
from mypy.plugin import MethodContext
4+
from mypy.subtypes import is_equivalent
45
from mypy.types import Instance
56
from mypy.types import Type as MypyType
6-
from mypy.types import TypeVarType, union_items
7+
from mypy.types import TypeVarType, UnionType, union_items
78
from typing_extensions import final
89

910
from classes.contrib.mypy.typeops import type_loader
@@ -96,10 +97,21 @@ def add_supports_metadata(self) -> None:
9697
self._ctx,
9798
)
9899

99-
if supports_spec not in instance_type.type.bases:
100+
index = self._find_supports_index(instance_type, supports_spec)
101+
if index is not None:
102+
# We already have `Supports` base class inserted,
103+
# it means that we need to unify them:
104+
# `Supports[A] + Supports[B] == Supports[Union[A, B]]`
105+
self._add_unified_type(instance_type, supports_spec, index)
106+
else:
107+
# This is the first time this type is referenced in
108+
# a typeclass'es instance defintinion.
109+
# Just inject `Supports` with no extra steps:
100110
instance_type.type.bases.append(supports_spec)
111+
101112
if supports_spec.type not in instance_type.type.mro:
102-
instance_type.type.mro.insert(0, supports_spec.type)
113+
# We only need to add `Supports` type to `mro` once:
114+
instance_type.type.mro.append(supports_spec.type)
103115

104116
self._added_types.append(supports_spec)
105117

@@ -110,11 +122,66 @@ def remove_supports_metadata(self) -> None:
110122

111123
for instance_type in self._instance_types:
112124
assert isinstance(instance_type, Instance)
113-
114-
for added_type in self._added_types:
115-
if added_type in instance_type.type.bases:
116-
instance_type.type.bases.remove(added_type)
117-
if added_type.type in instance_type.type.mro:
118-
instance_type.type.mro.remove(added_type.type)
119-
125+
self._clean_instance_type(instance_type)
120126
self._added_types = []
127+
128+
def _clean_instance_type(self, instance_type: Instance) -> None:
129+
remove_mro = True
130+
for added_type in self._added_types:
131+
index = self._find_supports_index(instance_type, added_type)
132+
if index is not None:
133+
remove_mro = self._remove_unified_type(
134+
instance_type,
135+
added_type,
136+
index,
137+
)
138+
139+
if remove_mro and added_type.type in instance_type.type.mro:
140+
# We remove `Supports` type from `mro` only if
141+
# there are not associated types left.
142+
# For example, `Supports[A, B] - Supports[B] == Supports[A]`
143+
# then `Supports[A]` stays.
144+
# `Supports[A] - Supports[A] == None`
145+
# then `Supports` is removed from `mro` as well.
146+
instance_type.type.mro.remove(added_type.type)
147+
148+
def _find_supports_index(
149+
self,
150+
instance_type: Instance,
151+
supports_spec: Instance,
152+
) -> Optional[int]:
153+
for index, base in enumerate(instance_type.type.bases):
154+
if is_equivalent(base, supports_spec, ignore_type_params=True):
155+
return index
156+
return None
157+
158+
def _add_unified_type(
159+
self,
160+
instance_type: Instance,
161+
supports_spec: Instance,
162+
index: int,
163+
) -> None:
164+
unified_arg = UnionType.make_union([
165+
*supports_spec.args,
166+
*instance_type.type.bases[index].args,
167+
])
168+
instance_type.type.bases[index] = supports_spec.copy_modified(
169+
args=[unified_arg],
170+
)
171+
172+
def _remove_unified_type(
173+
self,
174+
instance_type: Instance,
175+
supports_spec: Instance,
176+
index: int,
177+
) -> bool:
178+
base = instance_type.type.bases[index]
179+
union_types = [
180+
type_arg
181+
for type_arg in union_items(base.args[0])
182+
if type_arg not in supports_spec.args
183+
]
184+
instance_type.type.bases[index] = supports_spec.copy_modified(
185+
args=[UnionType.make_union(union_types)],
186+
)
187+
return not bool(union_types)

0 commit comments

Comments
 (0)