From 66fdb5d606902d0eaed5bd1350ffd436d94dddbe Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sat, 3 Jul 2021 11:38:46 +0300 Subject: [PATCH 1/3] Closes #244, closes #241 --- classes/contrib/mypy/features/typeclass.py | 24 ++++-- .../mypy/validation/validate_typeclass_def.py | 69 ++++++++++++++++ .../test_validation/test_body.yml | 24 ++++++ .../test_validation/test_first_arg.yml | 79 +++++++++++++++++++ 4 files changed, 190 insertions(+), 6 deletions(-) create mode 100644 classes/contrib/mypy/validation/validate_typeclass_def.py create mode 100644 typesafety/test_typeclass/test_validation/test_body.yml create mode 100644 typesafety/test_typeclass/test_validation/test_first_arg.yml diff --git a/classes/contrib/mypy/features/typeclass.py b/classes/contrib/mypy/features/typeclass.py index ef58ac4..3a64d56 100644 --- a/classes/contrib/mypy/features/typeclass.py +++ b/classes/contrib/mypy/features/typeclass.py @@ -24,6 +24,7 @@ from classes.contrib.mypy.validation import ( validate_associated_type, validate_typeclass, + validate_typeclass_def, ) @@ -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) @@ -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], diff --git a/classes/contrib/mypy/validation/validate_typeclass_def.py b/classes/contrib/mypy/validation/validate_typeclass_def.py new file mode 100644 index 0000000..d215e0c --- /dev/null +++ b/classes/contrib/mypy/validation/validate_typeclass_def.py @@ -0,0 +1,69 @@ +from typing import Union + +from mypy.nodes import ARG_POS, EllipsisExpr, ExpressionStmt, FuncDef +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_ellipsis = ( + len(body) == 1 and + isinstance(body[0], ExpressionStmt) and + isinstance(body[0].expr, EllipsisExpr) + ) + if is_ellipsis: # We allow a single ellipsis in function a body. + return True + + ctx.api.fail(_REDUNDANT_BODY_MSG, ctx.context) + return False + return True diff --git a/typesafety/test_typeclass/test_validation/test_body.yml b/typesafety/test_typeclass/test_validation/test_body.yml new file mode 100644 index 0000000..62c83a9 --- /dev/null +++ b/typesafety/test_typeclass/test_validation/test_body.yml @@ -0,0 +1,24 @@ +- 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_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 diff --git a/typesafety/test_typeclass/test_validation/test_first_arg.yml b/typesafety/test_typeclass/test_validation/test_first_arg.yml new file mode 100644 index 0000000..66bda15 --- /dev/null +++ b/typesafety/test_typeclass/test_validation/test_first_arg.yml @@ -0,0 +1,79 @@ +- 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_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 From f688197cb3976344544021c072e2e34de88613d4 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sat, 3 Jul 2021 11:41:02 +0300 Subject: [PATCH 2/3] Adds more test for associated types --- .../test_typeclass/test_validation/test_body.yml | 15 +++++++++++++++ .../test_validation/test_first_arg.yml | 15 +++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/typesafety/test_typeclass/test_validation/test_body.yml b/typesafety/test_typeclass/test_validation/test_body.yml index 62c83a9..1e1246c 100644 --- a/typesafety/test_typeclass/test_validation/test_body.yml +++ b/typesafety/test_typeclass/test_validation/test_body.yml @@ -10,6 +10,21 @@ 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: | diff --git a/typesafety/test_typeclass/test_validation/test_first_arg.yml b/typesafety/test_typeclass/test_validation/test_first_arg.yml index 66bda15..f7a08d3 100644 --- a/typesafety/test_typeclass/test_validation/test_first_arg.yml +++ b/typesafety/test_typeclass/test_validation/test_first_arg.yml @@ -31,6 +31,21 @@ 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 fdd5c3d0126a9456859bc96119d4c3701e289477 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sat, 3 Jul 2021 11:46:15 +0300 Subject: [PATCH 3/3] Also ignores docstrings --- .../mypy/validation/validate_typeclass_def.py | 10 ++++++---- .../test_validation/test_body.yml | 20 +++++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/classes/contrib/mypy/validation/validate_typeclass_def.py b/classes/contrib/mypy/validation/validate_typeclass_def.py index d215e0c..259be16 100644 --- a/classes/contrib/mypy/validation/validate_typeclass_def.py +++ b/classes/contrib/mypy/validation/validate_typeclass_def.py @@ -1,6 +1,6 @@ from typing import Union -from mypy.nodes import ARG_POS, EllipsisExpr, ExpressionStmt, FuncDef +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 @@ -56,12 +56,14 @@ def _check_body( body = sig.definition.body.body if body: - is_ellipsis = ( + is_useless_body = ( len(body) == 1 and isinstance(body[0], ExpressionStmt) and - isinstance(body[0].expr, EllipsisExpr) + isinstance(body[0].expr, (EllipsisExpr, StrExpr)) ) - if is_ellipsis: # We allow a single ellipsis in function a body. + 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) diff --git a/typesafety/test_typeclass/test_validation/test_body.yml b/typesafety/test_typeclass/test_validation/test_body.yml index 1e1246c..95599c5 100644 --- a/typesafety/test_typeclass/test_validation/test_body.yml +++ b/typesafety/test_typeclass/test_validation/test_body.yml @@ -1,3 +1,23 @@ +- 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: |