Skip to content

add mypy plugin #825

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

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
45 changes: 40 additions & 5 deletions src/idom/mypy.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,71 @@
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
from mypy.plugin import FunctionContext, Plugin
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":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we allowed to do an from idom import component and then have this check be fullname == f"{component.__module}.{component.__name__}"?

Would improve refactorability.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MyPy automatically resolves the full name of the function in question. Even if component is imported under an alias or from a different location the name is still idom.core.component.component.

Copy link
Contributor

@Archmonger Archmonger Oct 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, however, if we decide to change the module's path from idom.core.component.component in the future, this plugin will break silently.

I would recommend using python imports to generate that path, that way refactoring libraries such as rope can automatically update these paths if they change in the future. Or at the very least, this plugin would create a visible exception to demonstrate that things are broken.

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

Expand Down
9 changes: 9 additions & 0 deletions temp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from idom import component


@component
def temp(x: int, y: int) -> None:
return None


temp(*[1, 2])
Comment on lines +1 to +9
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want to put temp.py into the gitignore if you frequently use it for testing