From 8fc8c3e5935f97ed997dedbad1dc5ffdf94a9de1 Mon Sep 17 00:00:00 2001 From: rmorshea Date: Wed, 19 Oct 2022 23:32:20 -0700 Subject: [PATCH 1/2] add mypy plugin --- setup.cfg | 2 ++ src/idom/mypy.py | 51 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 src/idom/mypy.py diff --git a/setup.cfg b/setup.cfg index 293907987..f579d16a6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,6 +6,7 @@ ignore_missing_imports = True warn_unused_configs = True warn_redundant_casts = True warn_unused_ignores = True +plugins = idom.mypy [flake8] ignore = E203, E266, E501, W503, F811, N802, N806 @@ -41,6 +42,7 @@ exclude_lines = raise NotImplementedError omit = src/idom/__main__.py + src/idom/mypy.py src/idom/core/_fixed_jsonpatch.py [build_sphinx] diff --git a/src/idom/mypy.py b/src/idom/mypy.py new file mode 100644 index 000000000..333ddfb4c --- /dev/null +++ b/src/idom/mypy.py @@ -0,0 +1,51 @@ +from typing import Callable, Final + +from mypy.errorcodes import ErrorCode +from mypy.nodes import ArgKind +from mypy.plugin import FunctionContext, Plugin +from mypy.types import CallableType, Instance, Type + + +KEY_IN_RENDER_FUNC: Final = ErrorCode( + "idom-key-in-render-func", + "Component render function has reserved 'key' parameter", + "IDOM", +) + + +class MypyPlugin(Plugin): + """MyPy plugin for IDOM""" + + def get_function_hook(self, fullname: str) -> Callable[[FunctionContext], Type]: + if fullname == "idom.core.component.component": + return self.component_decorator_hook + return super().get_function_hook(fullname) + + def component_decorator_hook(self, ctx: FunctionContext) -> CallableType: + if not ctx.arg_types or not ctx.arg_types[0]: + return ctx.default_return_type + render_func: CallableType = ctx.arg_types[0][0] + + if render_func.argument_by_name("key") is not None: + ctx.api.msg.fail( + "Component render function has reserved 'key' parameter", + ctx.context, + code=KEY_IN_RENDER_FUNC, + ) + return ctx.default_return_type + + component_symbol = self.lookup_fully_qualified("idom.core.component.Component") + assert component_symbol is not None + assert component_symbol.node is not None + + return render_func.copy_modified( + arg_kinds=render_func.arg_kinds + [ArgKind.ARG_NAMED_OPT], + arg_names=render_func.arg_names + ["key"], + arg_types=render_func.arg_types + [ctx.api.named_generic_type("str", [])], + ret_type=Instance(component_symbol.node, []), + ) + + +def plugin(version: str): + # ignore version argument if the plugin works with all mypy versions. + return MypyPlugin From edf9e420d041001130624ee7adf1e85a6a7e9c15 Mon Sep 17 00:00:00 2001 From: rmorshea Date: Thu, 1 Dec 2022 15:47:01 -0800 Subject: [PATCH 2/2] protect against *args usage too --- src/idom/mypy.py | 45 ++++++++++++++++++++++++++++++++++++++++----- temp.py | 9 +++++++++ 2 files changed, 49 insertions(+), 5 deletions(-) create mode 100644 temp.py diff --git a/src/idom/mypy.py b/src/idom/mypy.py index 333ddfb4c..7c2203d09 100644 --- a/src/idom/mypy.py +++ b/src/idom/mypy.py @@ -1,4 +1,6 @@ -from typing import Callable, Final +from __future__ import annotations + +from typing import Any, Callable, Final from mypy.errorcodes import ErrorCode from mypy.nodes import ArgKind @@ -6,31 +8,64 @@ from mypy.types import CallableType, Instance, Type -KEY_IN_RENDER_FUNC: Final = ErrorCode( +KEY_IN_RENDER_FUNC_ERROR: Final = ErrorCode( "idom-key-in-render-func", "Component render function has reserved 'key' parameter", "IDOM", ) +NO_STAR_ARGS_ERROR: Final = ErrorCode( + "idom-no-star-args", + "Children were passed using *args instead of as a list or tuple", + "IDOM", +) + class MypyPlugin(Plugin): """MyPy plugin for IDOM""" + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self._idom_component_function_names: set[str] = set() + def get_function_hook(self, fullname: str) -> Callable[[FunctionContext], Type]: if fullname == "idom.core.component.component": - return self.component_decorator_hook + return self._idom_component_decorator_hook + elif fullname in self._idom_component_function_names: + return self._idom_component_function_hook return super().get_function_hook(fullname) - def component_decorator_hook(self, ctx: FunctionContext) -> CallableType: + def _idom_component_function_hook(self, ctx: FunctionContext) -> CallableType: + if any(ArgKind.ARG_STAR in arg_kinds for arg_kinds in ctx.arg_kinds): + ctx.api.msg.fail( + "Cannot pass variable number of children to component using *args", + ctx.context, + code=NO_STAR_ARGS_ERROR, + ) + ctx.api.msg.note( + "You can pass a sequence of children directly to a component. That is, " + "`example(*children)` will be treated the same as `example(children)`. " + 'Bear in mind that all children in such a sequence must have a "key" ' + "that uniquely identifies each child amongst its siblings.", + ctx.context, + code=NO_STAR_ARGS_ERROR, + ) + return ctx.default_return_type + + def _idom_component_decorator_hook(self, ctx: FunctionContext) -> CallableType: if not ctx.arg_types or not ctx.arg_types[0]: return ctx.default_return_type + render_func: CallableType = ctx.arg_types[0][0] + if render_func.definition: + self._idom_component_function_names.add(render_func.definition.fullname) + if render_func.argument_by_name("key") is not None: ctx.api.msg.fail( "Component render function has reserved 'key' parameter", ctx.context, - code=KEY_IN_RENDER_FUNC, + code=KEY_IN_RENDER_FUNC_ERROR, ) return ctx.default_return_type diff --git a/temp.py b/temp.py new file mode 100644 index 000000000..6fb02ccf4 --- /dev/null +++ b/temp.py @@ -0,0 +1,9 @@ +from idom import component + + +@component +def temp(x: int, y: int) -> None: + return None + + +temp(*[1, 2])