8000 Closes #244, closes #241 · dry-python/classes@9542917 · GitHub
[go: up one dir, main page]

Skip to content

Commit 9542917

Browse files
committed
Closes #244, closes #241
1 parent 7e8ffd1 commit 9542917

File tree

4 files changed

+190
-6
lines changed

4 files changed

+190
-6
lines changed

classes/contrib/mypy/features/typeclass.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from classes.contrib.mypy.validation import (
2525
validate_associated_type,
2626
validate_typeclass,
27+
validate_typeclass_def,
2728
)
2829

2930

@@ -75,11 +76,18 @@ def __call__(self, ctx: FunctionContext) -> MypyType:
7576
assert isinstance(ctx.default_return_type, Instance)
7677
assert isinstance(defn, CallableType)
7778
assert defn.definition
79+
7880
instance_args.mutate_typeclass_def(
79-
ctx.default_return_type,
80-
defn.definition.fullname,
81-
ctx,
81+
typeclass=ctx.default_return_type,
82+
definition_fullname=defn.definition.fullname,
8 8000 3+
ctx=ctx,
84+
)
85+
86+
validate_typeclass_def.check_type(
87+
typeclass=ctx.default_return_type,
88+
ctx=ctx,
8289
)
90+
8391
return ctx.default_return_type
8492
return AnyType(TypeOfAny.from_error)
8593

@@ -107,11 +115,15 @@ def typeclass_def_return_type(ctx: MethodContext) -> MypyType:
107115
assert isinstance(ctx.context, Decorator)
108116

109117
instance_args.mutate_typeclass_def(
110-
ctx.default_return_type,
111-
ctx.context.func.fullname,
112-
ctx,
118+
typeclass=ctx.default_return_type,
119+
definition_fullname=ctx.context.func.fullname,
120+
ctx=ctx,
113121
)
114122

123+
validate_typeclass_def.check_type(
124+
typeclass=ctx.default_return_type,
125+
ctx=ctx,
126+
)
115127
if isinstance(ctx.default_return_type.args[2], Instance):
116128
validate_associated_type.check_type(
117129
associated_type=ctx.default_return_type.args[2],
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
from typing import Union
2+
3+
from mypy.nodes import ARG_POS, EllipsisExpr, ExpressionStmt, FuncDef
4+
from mypy.plugin import FunctionContext, MethodContext
5+
from mypy.types import CallableType, Instance
6+
from typing_extensions import Final
7+
8+
_Contexts = Union[MethodContext, FunctionContext]
9+
10+
# Messages:
11+
12+
_AT_LEAST_ONE_ARG_MSG: Final = (
13+
'Typeclass definition must have at least one positional argument'
14+
)
15+
_FIRST_ARG_KIND_MSG: Final = (
16+
'First argument in typeclass definition must be positional'
17+
)
18+
_REDUNDANT_BODY_MSG: Final = 'Typeclass definitions must not have bodies'
19+
20+
21+
def check_type(
22+
typeclass: Instance,
23+
ctx: _Contexts,
24+
) -> bool:
25+
"""Checks typeclass definition."""
26+
return all([
27+
_check_first_arg(typeclass, ctx),
28+
_check_body(typeclass, ctx),
29+
])
30+
31+
32+
def _check_first_arg(
33+
typeclass: Instance,
34+
ctx: _Contexts,
35+
) -> bool:
36+
sig = typeclass.args[1]
37+
assert isinstance(sig, CallableType)
38+
39+
if not len(sig.arg_kinds):
40+
ctx.api.fail(_AT_LEAST_ONE_ARG_MSG, ctx.context)
41+
return False
42+
43+
if sig.arg_kinds[0] != ARG_POS:
44+
ctx.api.fail(_FIRST_ARG_KIND_MSG, ctx.context)
45+
return False
46+
return True
47+
48+
49+
def _check_body(
50+
typeclass: Instance,
51+
ctx: _Contexts,
52+
) -> bool:
53+
sig = typeclass.args[1]
54+
assert isinstance(sig, CallableType)
55+
assert isinstance(sig.definition, FuncDef)
56+
57+
body = sig.definition.body.body
58+
if body:
59+
is_ellipsis = (
60+
len(body) == 1 and
61+
isinstance(body[0], ExpressionStmt) and
62+
isinstance(body[0].expr, EllipsisExpr)
63+
)
64+
if is_ellipsis: # We allow a single ellipsis in function a body.
65+
return True
66+
67+
ctx.api.fail(_REDUNDANT_BODY_MSG, ctx.context)
68+
return False
69+
return True
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
- case: typeclass_with_body
2+
disable_cache: false
3+
main: |
4+
from classes import typeclass
5+
6+
@typeclass
7+
def args(instance) -> str:
8+
return 'a'
9+
out: |
10+
main:3: error: Typeclass definitions must not have bodies
11+
12+
13+
- case: typeclass_with_two_ellipsises
14+
disable_cache: false
15+
main: |
16+
from classes import typeclass
17+
18+
@typeclass
19+
def args(instance) -> str:
20+
...
21+
...
22+
out: |
23+
main:3: error: Typeclass definitions must not have bodies
24+
main:4: error: Missing return statement
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
- case: typeclass_first_arg_pos
2+
disable_cache: false
3+
main: |
4+
from classes import typeclass
5+
6+
@typeclass
7+
def args(instance) -> str:
8+
...
9+
10+
11+
- case: typeclass_first_arg_pos_only
12+
disable_cache: false
13+
skip: sys.version_info[:2] < (3, 8)
14+
main: |
15+
from classes import typeclass
16+
17+
@typeclass
18+
def args(instance, /) -> str:
19+
...
20+
21+
22+
- case: typeclass_first_arg_opt
23+
disable_cache: false
24+
main: |
25+
from classes import typeclass
26+
27+
@typeclass
28+
def args(instance: int = 1) -> str:
29+
...
30+
out: |
31+
main:3: error: First argument in typeclass definition must be positional
32+
33+
34+
- case: typeclass_first_arg_star
35+
disable_cache: false
36+
main: |
37+
from classes import typeclass
38+
39+
@typeclass
40+
def args(*instance: str) -> str:
41+
...
42+
out: |
43+
main:3: error: First argument in typeclass definition must be positional
44+
45+
46+
- case: typeclass_first_arg_star2
47+
disable_cache: false
48+
main: |
49+
from classes import typeclass
50+
51+
@typeclass
52+
def args(**instance) -> str:
53+
...
54+
out: |
55+
main:3: error: First argument in typeclass definition must be positional
56+
57+
58+
- case: typeclass_first_kw
59+
disable_cache: false
60+
main: |
61+
from classes import typeclass
62+
63+
@typeclass
64+
def args(*instance) -> str:
65+
...
66+
out: |
67+
main:3: error: First argument in typeclass definition must be positional
68+
69+
70+
- case: typeclass_first_kw_opt
71+
disable_cache: false
72+
main: |
73+
from classes import typeclass
74+
75+
@typeclass
76+
def args(*, instance: int = 1) -> str:
77+
...
78+
out: |
79+
main:3: error: First argument in typeclass definition must be positional

0 commit comments

Comments
 (0)
0