-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Cannot get pylint to register custom checker, is there a missing step in the docs? #7264
Comments
Thank you for opening the issue and the kind words. I don't see what you did wrong at first sight so we'll have to investigate, but I don't have the time right now. Here's a real world example in pylint_django to maybe unblock you without waiting for us: https://github.com/PyCQA/pylint-django/blob/master/pylint_django/checkers/__init__.py |
Hi @Pierre-Sassoulas, thank you for such a quick response! It seems they use a different top-level function name ( |
Update: I added a breakpoint to the dynamic module loader in pylinter.py and... now it does load my checker? I have changed literally nothing else 🤔 Or, at least, I'm almost certain I've changed nothing else. Update Update: It calls register, but actually there is some other error that results in a stack trace. Continuing to investigate.
I suspect I've supplied something wrong with the option spec. Updated update update: Yep, I misunderstood the docs and assumed the type for options would mirror python types, but this is not the case. Have updated the example to accomodate. HOWEVER, adding a breakpoint seems to make it work, and taking the breakpoint away... makes it not work. 🤯 I've not encountered that before, so this is a fun new learning experience for me. |
Ok, right. @Pierre-Sassoulas I don't know if this is a bug, or just a quirk that needs documenting. NOTE: Have tweaked the body below after doing some further research after eating something 😄 TL;DR: Detailed DescriptionWhen supplying custom plugin module names to load via the command line, this list is sent to the importer code before the current directory is added to sys.path. This means that, as in my case, it fails to import the custom checker as it cannot find it in the path. The reason the module is always being imported though, is that later on, pylint loops through the set of plugin names and attempts to load those modules again. This time, it looks for the Putting a breakpoint in makes this work in 2 ways:
Steps to Reproduce
Happy to Help!I'm happy to investigate further, and to contribute documentation, or code, to work around this, but I'd want to make sure I was doing something that was Pylint Approved™️ rather than waste your time with a PR that "solves" the "problem" in a way that the maintainers would never go for. I don't do enough open source, so here is as good a place to start as any 😄 |
Thank you for the very detailed investigation ! It seems it's indeed a bug with the configuration loading vs CLI argument that might be done differently. |
Ok! If I can get the time over the next 3-4 days I'll try to open a PR as per the contribution guidelines, or I'll put a note here if that isn't going to happen. |
EDIT: spoke too soon, please disregard this comment for the moment.
|
In the Run class, it looks like the CLI plugins are explicitly loaded before the config is initialised: lint/run.py#L150-L163. If we raise an error at this stage, this will explicitly break any workflows that were previously silently failing, which is probably not desirable. Update: I moved the plugin load to after the |
@daogilvie Thanks for the thorough investigation. I have one question about this though: why can't you install the plugin like a package? As far as I know, running plugins from the current working directory is not something we support. Plugins should be installed like a package (either from a packaging index like Is there something that is limiting you from installing the plugin as a package? |
Hi @DanielNoord! Nope, there is nothing at all that prevents me from doing that; I just didn't quite realise how much of a strict line that was. The lint is for a specific project, and has little general value outside of it, so was to live in the monorepo for the project. I could adapt the CI toolchain, or install it as another module, or any number of things 😄 Re-reading the "How to Write a Custom Checker" page again, I see that under Debugging a Checker it does briefly mention installing it as a requirement for testing, and there is a note callout that says you can edit PYTHONPATH or put the file in the checkers folder in the pylint source. To be clear, the confusion I have is that my |
Yeah! I think adding this explicitly to the docs is the way to go. Perhaps in the future we should actually support this if somebody finds a use case where installing isn't an option but for now I prefer improving the documentation vs. increasing the complexity of I saw you already made a commit! Feel free to tag me in the PR and I'll make sure to review it quickly 😄 |
@DanielNoord @Pierre-Sassoulas Having submitted the documentation change I think there is a bug here, outside of my specific way of mis-using the configuration. The BugIt is possible to give pylint the instruction to load a custom checker in such a way that
This means that a user could attempt to use a custom lint, see no errors and no messages, which gives the user a false sense that their checker found no issues. I'm fortunate, in that I was doing this to explicitly test a checker that I knew should find at least one thing. If you think it merits it, I'm happy to raise another issue for that? Also fine if you think it's so unlikely to happen in real life that the complexity of catching it isn't worth the effort 😄 |
@DanielNoord I've just seen you commented here, Github UI didn't load it whilst I had the page open so my comment about the bug above was made without seeing this 😂 Sorry, wasn't trying to be rude! Totally happy if you to make the call that there's no bug to be fixed! I'm just grateful pylint exists and has such responsive maintainers. This tool has saved me from myself many, many times... |
@daogilvie That does indeed sound like a bug. To be honest, if I haven't looked at the relevant code myself, but can we reliably determine when this is the case? It seems like the main issue is when |
This is absolutely fair — I think the requirement for it to be installed is completely reasonable, but introducing it now does seem like prime ground for the "broken workflow" problem.
TL;DRYes, I think it would be fairly straightforward to modify the The Long Bit, Read if You WantYes, I think so. The call to load plugins from the command line argument is made just before the config initialisation is done and is relatively simple. The actual function of interest is on the linter class: def load_plugin_modules(self, modnames: list[str]) -> None:
"""Check a list pylint plugins modules, load and register them."""
for modname in modnames:
if modname in self._dynamic_plugins:
continue
self._dynamic_plugins.add(modname)
try:
module = astroid.modutils.load_module_from_name(modname)
module.register(self)
except ModuleNotFoundError:
pass You can see here that it will just fail to register the module if it can't import it, but it will still add it to def load_plugin_configuration(self) -> None:
"""Call the configuration hook for plugins.
This walks through the list of plugins, grabs the "load_configuration"
hook, if exposed, and calls it to allow plugins to configure specific
settings.
"""
for modname in self._dynamic_plugins:
try:
module = astroid.modutils.load_module_from_name(modname)
if hasattr(module, "load_configuration"):
module.load_configuration(self)
except ModuleNotFoundError as e:
self.add_message("bad-plugin-value", args=(modname, e), line=0) This second function is the one that signals any actual issue with importing, and that can still be the case. It would be, I think, pretty simple to add another internal state attribute, perhaps called
I think this would only happen in the case we are describing; where an init-hook has modified the If there is a better mechanism for tracking registered plugins that already exists, it would also work. |
Note that you can use
What about changing
I think it would be good if we could explore this. You can use One thing that could be problematic is if Is this something you would want to create a PR for? |
Thank you for the tip! I don't often discuss code like this on issues or PRs professionally so this is a goldmine 😄
Yes, my only thought is that the reason (I guess) that this doesn't happen, is that It will always call So we could just call
That is fine, but I would want to change the name of the attribute to communicate that, perhaps something like
Yes! Would you like a new bug raised to track this issue specifically, or shall I continue to do it under this one? |
Wouldn't that unnecessarily increase the amount of
👍
Let's continue here. It's still related to the original post. |
Well, that's sort of what I'm saying is the slightly harder bit — we'll need to add some if somewhere, to stop emitting bad-plugin-value twice, even if it's just in I may have misunderstood though; is the intent here for What if, for example, someone has a pylintrc that also specifies the same plugin they supply on the CLI — I accidentally did this myself! It actually doesn't make a difference currently, because the module gets put in the set, but ironically if they supply the argument on the CLI it will actually prevent that same module in config from being loaded. Should we handle this? In the simplest case, is it really just about emitting Does this table accurately express what you would like to happen? ❗ indicates change in behaviour, just so they stand out a bit more. All headings refer to some given plugin I want to load, i.e "Passed on CLI" means I used ``--load-plugins=some_plugin```, etc. Hopefully it's clear 😄, please let me know if not!
I suspect these are very niche cases, as the code has been this way for a long time. I just don't want to make any confusion worse! Will open a PR with what I think might work. The trouble I think would be case 4, which is absolutely definitely a significant change in behaviour, and would mean working against several commits I can see that explicitly put the load after the init hook to allow just this type of thing. Unless you are not worried about it? |
No, I think raising the existing message makes the most sense.
What if we defer the plugin loading to
See suggestion above 😄
Yeah I agree. Case 4 seems to be troublesome. Could we make the changes you suggest here but "catch" case 4 and emit a |
So the code would only ever call There's another case by the way, that might be worth thinking about but also might be tricky: where
I can implement these changes: the problem is that I am about to go on a holiday trip for the rest of the month (we've been planning this for a while) and I think my partner would be rather upset if I took my laptop with me 😄 |
Something like that yeah?
No worries. I don't think anybody is likely to pick up, especially considering the amount of work/research you had to do to understand all of this. |
Hello! Hope you folks are well 😄
I have meddled with the history on my branch a bit; I didn't think it was an issue because it's a draft that hasn't been looked at by anyone, but if you would rather rebasing didn't happen at all once something was on GH, please let me know. PS I completely missed that I'd not enabled auto-signing of my commits. Future ones in the PR will not show as "unverified", sorry for that. |
Thank you for the recap !
Yeah, don't worry about that, I don't 😄 |
UpdateOnce #7284 is merged, cases 3 and 6 will be fixed. The other cases look like they might be a bit more complicated to fix without unintended consequences. I will happily do that, but probably over multiple smaller PRs to make sure everything works well. Current State
|
* Add x-failing test for issue 7264 case 3 This is the case where a plugin can be imported only after the init-hook is run, and the init hook is specified in a pylintrc file We would expect the module to not load in any case, and cause the emission of a bad-plugin-value message. * _dynamic_plugins is a dict not a set This is in preparation for caching the loaded modules, and for storing other information that will help in identifying times loading is dependent on init-hook magic. * Use cached module objects for load_configuration This fixes case 3 in #7264, and is technically a breaking change, in that it now emits a message for what would have previously been a silent failure. * Interim review comment fixes 1. Remove the xfail that snuck back in after the rebases. 2. Now that fake_home yields a str, update the type hint. * Add test for bad plugin with duplicated load This is case 6 in issue #7264, and represents the other silent failure that is being made non-silent by this change. * Apply review feedback - Add an ordering test for CLI arguments - Add clarifying comments on affected functions - Tidy up test assertions to be more pythonic Co-authored-by: Daniël van Noord <[email protected]> Co-authored-by: Pierre Sassoulas <[email protected]>
* Add x-failing test for issue 7264 case 3 This is the case where a plugin can be imported only after the init-hook is run, and the init hook is specified in a pylintrc file We would expect the module to not load in any case, and cause the emission of a bad-plugin-value message. * _dynamic_plugins is a dict not a set This is in preparation for caching the loaded modules, and for storing other information that will help in identifying times loading is dependent on init-hook magic. * Use cached module objects for load_configuration This fixes case 3 in pylint-dev#7264, and is technically a breaking change, in that it now emits a message for what would have previously been a silent failure. * Interim review comment fixes 1. Remove the xfail that snuck back in after the rebases. 2. Now that fake_home yields a str, update the type hint. * Add test for bad plugin with duplicated load This is case 6 in issue pylint-dev#7264, and represents the other silent failure that is being made non-silent by this change. * Apply review feedback - Add an ordering test for CLI arguments - Add clarifying comments on affected functions - Tidy up test assertions to be more pythonic Co-authored-by: Daniël van Noord <[email protected]> Co-authored-by: Pierre Sassoulas <[email protected]>
* Add x-failing test for issue 7264 case 3 This is the case where a plugin can be imported only after the init-hook is run, and the init hook is specified in a pylintrc file We would expect the module to not load in any case, and cause the emission of a bad-plugin-value message. * _dynamic_plugins is a dict not a set This is in preparation for caching the loaded modules, and for storing other information that will help in identifying times loading is dependent on init-hook magic. * Use cached module objects for load_configuration This fixes case 3 in #7264, and is technically a breaking change, in that it now emits a message for what would have previously been a silent failure. * Interim review comment fixes 1. Remove the xfail that snuck back in after the rebases. 2. Now that fake_home yields a str, update the type hint. * Add test for bad plugin with duplicated load This is case 6 in issue #7264, and represents the other silent failure that is being made non-silent by this change. * Apply review feedback - Add an ordering test for CLI arguments - Add clarifying comments on affected functions - Tidy up test assertions to be more pythonic Co-authored-by: Daniël van Noord <[email protected]> Co-authored-by: Pierre Sassoulas <[email protected]>
Hello again! I've been pondering the next stage of this, and I can't help but think that "fixing" 4 and 7 is quite a big change in behaviour. Especially compared to what we did already, which is change a silent failure into a reported failure. In fact, some git/github archaeology tells me that pylint working this way was a deliberate decision: #166. I don't know if anything has changed particularly but this increases the risk that people might actually be using this behaviour; what do you think? |
Thanks for that investigation. I'm not sure I agree with the original decision but I don't think it's worth the trouble of a breaking change here. I think the only thing left to do is make sure we have regression tests against all "sub-cases" within cases 4 and 7. So: |
The bottom two are already present: https://github.com/PyCQA/pylint/blob/fb30fe09d74de3dad2472309fb24396b65b8b81d/tests/lint/unittest_lint.py#L665-L707 I will open a PR with the order-independence tests for the pylintrc case today. EDIT: Also just seen this old CLI-focused test too: https://github.com/PyCQA/pylint/blob/fb30fe09d74de3dad2472309fb24396b65b8b81d/tests/lint/unittest_lint.py#L746-L752 |
Annoyingly, it looks like those 2 tests that I already added are not valid. For reasons I don't understand, and have just discovered by trying to add new tests, the path they are "adding" to the sys path is already in there, before the |
* Add and fix tests for init-hook based plugin load This changes 4 existing tests, due to a misunderstanding of the author (past me) in a couple of the details of those tests. Specifically, we now copy a single python file to use as our plugin, and make sure to correctly check for the name as given in the checker classes. We also make sure not to accidentally load the old copy of the plugin, which apparently sits in a directory that is already on the system path. There is also a single new test, which covers the cases of loading a plugin with ``init-hook`` magic, both specified in an rc file, but in different orders. This should be sufficient to close the issue around #7264. Co-authored-by: Daniël van Noord <[email protected]>
* Improve docs clarity on loading custom checkers Whilst the docs on testing custom checkers do touch on the fact the module has to be in your load path, there are some restrictions that were not made clear before. Specifically, the init-hook configuration item (which is often used explicitly to change the sys.path) is not automatically sufficient. If the init hook is in a file, but the load-plugins argument is passed in the CLI, then the plugins are loaded before the init-hook has run, and so will not be imported. In this case, the failure can also be silent, so the outcome is a lint that will simply not contain any information from the checker, as opposed to a run that will error. This corner case may merit fixing in a separate PR, that is to be confirmed. Closes #7264 Co-authored-by: Daniël van Noord <[email protected]>
Question
Hi! First: Thanks for supporting and maintaining such a great ecosystem!
My question: I'm trying to make a simple checker around some custom formatting in log strings. I have followed every tutorial I can find from within the last few years, and naturally looked at the guide in the documentation.
However, I am finding that the
register
function, described as the way to get your checker actually loaded into pylint, simply is not being called. The module is being loaded, for sure — I can see breakpoints in the top level of the module being hit, but not in theregister
function itself. I've tried setting the--load-plugins
argument, I've tried using theload_plugins
setting in pylintrc.Is there an additional step or constraint missing in the implementation of custom checkers from the docs? Or am I simply making a mistake?
Below is the code for my absolute bare-bones first attempt.
Stored in
testchecker.py
:My file I am linting,
lintme.py
:Both files are in the same directory, in which I execute:
Thanks again!
Documentation for future user
I expect the guide to creating a custom checker to contain everything needed to do so, and to have a section highlighting common and easy mistakes (I'm assuming I have made such a mistake)
Additional context
I have searched stack overflow and pre-existing issues on this repo, and not found anyone reporting this.
There is https://stackoverflow.com/questions/71590677/cant-load-custom-pylint-module, but they report getting an error about not finding the module, then succeeding by placing the module in the pylint source. This is not what I am experiencing.
The text was updated successfully, but these errors were encountered: