Skip to content

Adds typeclass definition validation #245

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 3 commits into from
Jul 3, 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
24 changes: 18 additions & 6 deletions classes/contrib/mypy/features/typeclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from classes.contrib.mypy.validation import (
validate_associated_type,
validate_typeclass,
validate_typeclass_def,
)


Expand Down Expand Up @@ -75,11 +76,18 @@ def __call__(self, ctx: FunctionContext) -> MypyType:
assert isinstance(ctx.default_return_type, Instance)
assert isinstance(defn, CallableType)
assert defn.definition

instance_args.mutate_typeclass_def(
ctx.default_return_type,
defn.definition.fullname,
ctx,
typeclass=ctx.default_return_type,
definition_fullname=defn.definition.fullname,
ctx=ctx,
)

validate_typeclass_def.check_type(
typeclass=ctx.default_return_type,
ctx=ctx,
)

return ctx.default_return_type
return AnyType(TypeOfAny.from_error)

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

instance_args.mutate_typeclass_def(
ctx.default_return_type,
ctx.context.func.fullname,
ctx,
typeclass=ctx.default_return_type,
definition_fullname=ctx.context.func.fullname,
ctx=ctx,
)

validate_typeclass_def.check_type(
typeclass=ctx.default_return_type,
ctx=ctx,
)
if isinstance(ctx.default_return_type.args[2], Instance):
validate_associated_type.check_type(
associated_type=ctx.default_return_type.args[2],
Expand Down
71 changes: 71 additions & 0 deletions classes/contrib/mypy/validation/validate_typeclass_def.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from typing import Union

from mypy.nodes import ARG_POS, EllipsisExpr, ExpressionStmt, FuncDef, StrExpr
from mypy.plugin import FunctionContext, MethodContext
from mypy.types import CallableType, Instance
from typing_extensions import Final

_Contexts = Union[MethodContext, FunctionContext]

# Messages:

_AT_LEAST_ONE_ARG_MSG: Final = (
'Typeclass definition must have at least one positional argument'
)
_FIRST_ARG_KIND_MSG: Final = (
'First argument in typeclass definition must be positional'
)
_REDUNDANT_BODY_MSG: Final = 'Typeclass definitions must not have bodies'


def check_type(
typeclass: Instance,
ctx: _Contexts,
) -> bool:
"""Checks typeclass definition."""
return all([
_check_first_arg(typeclass, ctx),
_check_body(typeclass, ctx),
])


def _check_first_arg(
typeclass: Instance,
ctx: _Contexts,
) -> bool:
sig = typeclass.args[1]
assert isinstance(sig, CallableType)

if not len(sig.arg_kinds):
ctx.api.fail(_AT_LEAST_ONE_ARG_MSG, ctx.context)
return False

if sig.arg_kinds[0] != ARG_POS:
ctx.api.fail(_FIRST_ARG_KIND_MSG, ctx.context)
return False
return True


def _check_body(
typeclass: Instance,
ctx: _Contexts,
) -> bool:
sig = typeclass.args[1]
assert isinstance(sig, CallableType)
assert isinstance(sig.definition, FuncDef)

body = sig.definition.body.body
if body:
is_useless_body = (
len(body) == 1 and
isinstance(body[0], ExpressionStmt) and
isinstance(body[0].expr, (EllipsisExpr, StrExpr))
)
if is_useless_body:
# We allow a single ellipsis in function a body.
# We also allow just a docstring.
return True

ctx.api.fail(_REDUNDANT_BODY_MSG, ctx.context)
return False
return True
59 changes: 59 additions & 0 deletions typesafety/test_typeclass/test_validation/test_body.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
- case: typeclass_with_ellipsis
disable_cache: false
main: |
from classes import typeclass

@typeclass
def args(instance) -> str:
...


- case: typeclass_with_docstring
disable_cache: false
main: |
from classes import typeclass

@typeclass
def args(instance) -> str:
"""Some."""


- case: typeclass_with_body
disable_cache: false
main: |
from classes import typeclass

@typeclass
def args(instance) -> str:
return 'a'
out: |
main:3: error: Typeclass definitions must not have bodies


- case: typeclass_with_body_and_associated_type
disable_cache: false
main: |
from classes import typeclass, AssociatedType

class Some(AssociatedType):
...

@typeclass(Some)
def args(instance) -> str:
return 'a'
out: |
main:6: error: Typeclass definitions must not have bodies


- case: typeclass_with_two_ellipsises
disable_cache: false
main: |
from classes import typeclass

@typeclass
def args(instance) -> str:
...
...
out: |
main:3: error: Typeclass definitions must not have bodies
main:4: error: Missing return statement
94 changes: 94 additions & 0 deletions typesafety/test_typeclass/test_validation/test_first_arg.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
- case: typeclass_first_arg_pos
disable_cache: false
main: |
from classes import typeclass

@typeclass
def args(instance) -> str:
...


- case: typeclass_first_arg_pos_only
disable_cache: false
skip: sys.version_info[:2] < (3, 8)
main: |
from classes import typeclass

@typeclass
def args(instance, /) -> str:
...


- case: typeclass_first_arg_opt
disable_cache: false
main: |
from classes import typeclass

@typeclass
def args(instance: int = 1) -> str:
...
out: |
main:3: error: First argument in typeclass definition must be positional


- case: typeclass_first_arg_opt_with_associated
disable_cache: false
main: |
from classes import typeclass, AssociatedType

class Some(AssociatedType):
...

@typeclass(Some)
def args(instance: int = 1) -> str:
...
out: |
main:6: error: First argument in typeclass definition must be positional


- case: typeclass_first_arg_star
disable_cache: false
main: |
from classes import typeclass

@typeclass
def args(*instance: str) -> str:
...
out: |
main:3: error: First argument in typeclass definition must be positional


- case: typeclass_first_arg_star2
disable_cache: false
main: |
from classes import typeclass

@typeclass
def args(**instance) -> str:
...
out: |
main:3: error: First argument in typeclass definition must be positional


- case: typeclass_first_kw
disable_cache: false
main: |
from classes import typeclass

@typeclass
def args(*instance) -> str:
...
out: |
main:3: error: First argument in typeclass definition must be positional


- case: typeclass_first_kw_opt
disable_cache: false
main: |
from classes import typeclass

@typeclass
def args(*, instance: int = 1) -> str:
...
out: |
main:3: error: First argument in typeclass definition must be positional