Skip to content
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

add mypy plugin #825

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
2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ ignore_missing_imports = True
warn_unused_configs = True
warn_redundant_casts = True
warn_unused_ignores = True
plugins = idom.mypy
Copy link
Contributor

Choose a reason for hiding this comment

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

We need to document this somehow.

Undocumented features essentially do not exist.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is more in a draft state. I'm not even sure this belongs in idom. It might be better placed in an idom-typing package.

Copy link
Contributor

Choose a reason for hiding this comment

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

A separate package makes a lot of sense. If/when created we should document that it exists within idom docs.


[flake8]
ignore = E203, E266, E501, W503, F811, N802, N806
Expand Down Expand Up @@ -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]
Expand Down
51 changes: 51 additions & 0 deletions src/idom/mypy.py
Original file line number Diff line number Diff line change
@@ -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":
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 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