-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
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 basic support for user-defined mypy plugins #3517
Conversation
Configure them through "plugins=path/plugin.py, ..." in the ini file. The paths are relative to the configuration file. This is an almost minimal implementation and some features are missing: * Plugins installed through pip aren't properly supported. * Plugins within packages aren't properly supported. * Incremental mode doesn't invalidate cache files when plugins change.
Previously we sometimes normalized to Windows paths and sometimes to Linux paths. Now switching to always use Linux paths.
def get_method_hook(self, fullname: str) -> Optional[MethodHook]: | ||
return self._find_hook(lambda plugin: plugin.get_method_hook(fullname)) | ||
|
||
def _find_hook(self, lookup: Callable[[Plugin], T]) -> Optional[T]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The result of this should probably be cached based on hook-type and fullname
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Created an issue about caching (#3533). This may need some analysis or experimentation to decide a caching strategy (e.g. unlimited cache size vs bounded cache size; maximum size of the cache) so I feel that it's better to do it separately.
mypy/test/testgraph.py
Outdated
@@ -42,6 +43,7 @@ def _make_manager(self) -> BuildManager: | |||
reports=Reports('', {}), | |||
options=Options(), | |||
version_id=__version__, | |||
plugin=Plugin((3, 6)), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should this use defaults.PYTHON3_VERSION
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated
Looks really similar to what I have except it includes tests, so you win! There are two other functional differences that I want to discuss: Support for plugin optionsSome plugins may want to expose user-configurable options. For example, with my docstring parser I want users to be able to specify which style of docstrings to expect (the default behavior of automatic discovery is a bit slower). Here are three proposals for how to link plugin registration with per-plugin configuration options: Option A The correlation here is bit fragile and the per-plugin section headers may be difficult to grok for longer (i.e. absolute) paths: [mypy]
fast_parser = true
plugins = /path/to/typeddict.py, /path/to/mypydoc.py
[mypy.plugins-/path/to/mypydoc.py]
docstring_style = 'google' Option B The following is visually clean, but can't as easily piggy-back on the current options-parsing code. (Note: I believe that [mypy]
fast_parser = true
[mypy.plugins]
typeddict = /path/to/typeddict.py
mypydoc = /path/to/mypydoc.py
[mypy.plugins-mypydoc]
docstring_style = 'google' Option C The following dotted registration style is used heavily by mercurial, and this is what I decided on in my implementation. It piggy-backs existing options parsing code, so it could easily be extended to per-module options in the future, if we found a need for that: [mypy]
fast_parser = true
plugins.typeddict = /path/to/typeddict.py
plugins.mypydoc = /path/to/mypydoc.py
[mypy.plugins-mypydoc]
docstring_style = 'google' Support for module pluginsYou already mentioned this omission, but it should be fairly trivial to detect a plugin specifier that is path-like ( |
Regarding plugin options, I recall the options to pytest plugins driving me mad, because they appear to be pytest options but aren't in the pytest docs (and the usage message goes on and on and on...). A more minimal alternative would be to just have different names for the plugin, one for each variation. |
@chadrik |
@miedzinski good point. I updated my examples above to include proper namespaces. I tried to come up with some logic for the separators:
I'm completely open to other suggestions. Underscore could work in place of periods, but I found it less visually appealing. |
@chadrik I agree that specifying options for plugins would be useful. They can be implemented in a separate PR -- it would also be good to first discuss the best way to provide them (outside this PR). Support for referring to plugins through a module name can also be added with another PR. I usually prefer keeping PRs small to make them easy to review and to keep discussions focused. Feel free to create issues for the new features. |
@JukkaL that's fine as long as you don't do a release between this PR and the next, since the signature to the This PR looks good to me. |
Actually, I forgot I had two review notes above. What do you think about caching the lookup on the |
I created #3533 about caching. |
-- Test cases for user-defined plugins | ||
-- | ||
-- Note: Plugins used by tests live under test-data/unit/plugins. Defining | ||
-- plugin files in test cases does not work reliably. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure exactly. I spent some time trying to get it to work and then gave up. A couple of hypotheses:
- If two test cases use the same plugin name, we need to reliably free the plugin between test cases as otherwise the plugin module will be visible between tests. It may be hard to ensure that the plugin is always deleted at the end of the test case.
- Python may do some caching / time stamp magic that may cause the import machinery to sometimes not see a recently created module.
[[mypy] | ||
plugins=missing.py | ||
[out] | ||
tmp/missing.py:0: error: Can't find plugin |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This error is confusing. Maybe it would be less confusing if the plugin name as specified in the config file (here "missing.py") was repeated in the error message, and the error mentioned that this was a plugin specified by the config file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will update the error message (in a separate PR)
plugins=badext.pyi | ||
[file badext.pyi] | ||
[out] | ||
tmp/badext.pyi:0: error: Plugin must have .py extension |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should similarly have a better error message.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok will update the error message
[[mypy] | ||
plugins=<ROOT>/test-data/unit/plugins/noentry.py | ||
[out] | ||
<ROOT>/test-data/unit/plugins/noentry.py:0: error: Plugin does not define entry point function "plugin" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if line 1 would be less jarring?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could be -- I'll give it a try
[[mypy] | ||
plugins=<ROOT>/test-data/unit/plugins/badreturn.py | ||
[out] | ||
<ROOT>/test-data/unit/plugins/badreturn.py:0: error: Type object expected as the return value of "plugin" (got None) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there no way to make this list the line number of the plugin() function? Ditto below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It may be possible, but giving the line number of the return statement doesn't seem feasible. I'll look at this.
Address feedback by @gvanrossum in #3517.
I know this has already been merged, but would it be possible to also add a command-line parameter for this, to make it easier to be used by IDEs and such? |
Address feedback by @gvanrossum in #3517.
Configure them through
plugins=path/plugin.py, ...
in the ini file.The paths are relative to the configuration file.
This is an almost minimal implementation and some features
are missing:
plugins change.
@chadrik We've been working on the same issue. This seems fairly
similar to your #3512. Not sure what's the best way forward.
Here are some thoughts about the design of this PR:
(or currently path) of the plugin. Every plugin has a single,
fixed entry point
plugin(mypy_version)
. The idea is to makeconfiguring plugins as easy as possible, as this is the main
explicit user interface to the plugin system.
should be possible to write plugins that are compatible across
multiple mypy versions even if the internal API changes. This
won't be particularly convenient, though.
display a normal Python stack trace (along with a note about what
was going on). Plugin errors aren't really mypy internal errors but
programmer errors (in this case of the plugin developer) so
I want to make them very visible.