-
Notifications
You must be signed in to change notification settings - Fork 104
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
Implement PluginManager (3 of 3) #226
Conversation
Some significant changes from #212 - Tests are not yet in sync. More on that in the next push:
The current help output with plugins |
f601607
to
7d4d2af
Compare
I think this is ready for a final of review. |
This PR will also close #165 |
Again @unode thanks for putting in all this work. I have a quick request, can you elaborate on what you mean by the bullet below and what exactly does it mean for users who use their own plugins:
|
I mean this Line 37 in a584bf2
If you run the bot without adding any custom plugins, it will be included among the default plugins. Initially I thought of having it always enabled (i.e. |
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.
Nice, thanks for all the work 🙌🏼! I think having a separate PluginManager
is great, I just have a bunch of questions and suggestions here and there. It's a huge PR, so there are loads of comments 🙃 In the future it'd be nicer to split a big one like this into maybe ~4 PRs, that makes it much easier to see what's going on. I haven't look at all the details yet, so will probably have to do another round of reviewing later.
) | ||
|
||
# Preserve docstring | ||
new_func.__doc__ = func.__doc__ |
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 is this necessary? We can definitely explicitly store the __doc__
of the wrapped function somewhere, but I'd prefer not to modify the __doc__
of the wrapping Function
since __doc__
is normally class-specific, not instance-specific.
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 one was tricky. We now have PluginHelpInfo.function_doc[header|full]
as well as PluginHelpInfo.plugin_doc[header|full]
which contain the method and class level Plugin
docstring, respectively.
With the way decorated functions are transformed into MessageFunction
or WebHookFunction
and the fact that methods can be decorated multiple times (multiple listen patterns, click decorators, ...), this was the simplest way to propagate the docstring without having to do some kind of conditional code using isinstance()
.
The test tests/unit_tests/plugin_manager_test.py::TestPluginManager::test_get_help
verifies if the docstring is propagated correctly. Commenting these lines breaks it.
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 still don't understand this. The base Function
class has self.docstring = self.function.__doc__ or ""
, where self.function
is always the absolute innermost function (i.e. if you pass it a Function, it will keep unwrapping it until it reaches the callable that this Function wraps). Why then would we need to change self.__doc__
as well, and can't we just use self.docstring
?
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.
There might be some confusion on my part. When I tried to capture this in a test, the test fails unless I included the line above.
Can you perhaps have a look at the test tests/unit_tests/plugin_manager_test.py::TestPluginManager::test_get_help
and confirm that the test and its assumptions are correct?
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 can't judge what's going on just from looking at the code, so will have to try running it later
See attzonko#226 (comment) (cherry picked from commit c863a9e1d03dfbe1c3d72dcd0ae6f22403e0fde6)
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.
Nice, thanks for all the work 🙌🏼! I think having a separate
PluginManager
is great, I just have a bunch of questions and suggestions here and there. It's a huge PR, so there are loads of comments upside_down_face In the future it'd be nicer to split a big one like this into maybe ~4 PRs, that makes it much easier to see what's going on. I haven't look at all the details yet, so will probably have to do another round of reviewing later.
Thanks a lot Jelmer. Your detailed review is super appreciated.
Indeed this grew more than expected so, take your time. I also took mine 😅 .
I tried to get different PRs at some point but wasn't clear how to go about dependencies between PRs. I can pull some things out if you think it would make your life easier.
mmpy_bot/plugins/base.py
Outdated
def __iter__(self): | ||
return iter(self.plugins) | ||
|
||
def initialize_manager(self, driver: Driver, settings: Settings): |
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 one I did intentionally to avoid mixing up with Plugin.initialize()
. This was quite confusing when PluginManager
was also behaving as a Plugin
. It's no longer but I still kept it to highlight the distinction. Particularly relevant since they now receive different sets of arguments (Plugin.initialize()
also receives a reference to PluginManager
).
msg_listeners = { | ||
re.compile("pattern"): expand_func_names(FakePlugin.my_function), | ||
re.compile("direct_pattern"): expand_func_names(FakePlugin.direct_function), | ||
re.compile("another_async_pattern"): expand_func_names( | ||
FakePlugin.my_async_function | ||
), | ||
re.compile("async_pattern"): expand_func_names(FakePlugin.my_async_function), | ||
re.compile("hi_custom"): expand_func_names(FakePlugin.hi_custom), | ||
# Click commands construct a regex pattern from the listen_to pattern | ||
re.compile("^click_command (.*)?"): expand_func_names(FakePlugin.click_commmand), | ||
} |
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 is how it was before. That part of the code wasn't touched. See
Line 216 in 9d32fdd
reg = f"^{reg.strip('^')} (.*)?" # noqa |
Pulled #233 out of this one. That's a pretty self contained feature. |
See attzonko#226 (comment) (cherry picked from commit c863a9e1d03dfbe1c3d72dcd0ae6f22403e0fde6)
After trying to customize a little the help info I noticed the current approach actually forces you to modify To simplify the use-case of filtering listeners, I now introduced a |
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.
Round two 😅 These reviews are taking me a long time, so I would be grateful if you could either make PRs into this branch and/or not force-push to it, so I can look only at the latest changes
self.docstring = self.function.get_help(ctx).replace( | ||
"\n", f"\n{spaces(8)}" | ||
) | ||
self.docstring += f"\n\n{self.function.get_help(ctx)}" |
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 think this is what causes those duplicate docstrings you showed earlier in Discord. click
already includes the docstring in get_help
, so this shouldn't be +=
.
self.docstring += f"\n\n{self.function.get_help(ctx)}" | |
self.docstring = f"\n\n{self.function.get_help(ctx)}" |
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 tried =
before but then we lose all of the help from click
.
I'm not happy with either of the solutions but I think we'll need to address click
UI/UX in another PR.
I agree that click
is convenient for argument parsing but the help information is (IMO) too verbose for a chat-bot.
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 tried = before but then we lose all of the help from click.
What do you mean? The "help from click" is what is returned by self.function.get_help(ctx)
right?
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.
Yes to self.function.get_help(ctx)
. If memory serves the issue was functions decorated with both @listen_to
and @click...
. The inner click decorated is processed and function.get_help(ctx)
is called but as the outer decorator (listen_to
) is processed later, this was overwriting what was done before, hence the +=
.
) | ||
|
||
# Preserve docstring | ||
new_func.__doc__ = func.__doc__ |
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 still don't understand this. The base Function
class has self.docstring = self.function.__doc__ or ""
, where self.function
is always the absolute innermost function (i.e. if you pass it a Function, it will keep unwrapping it until it reaches the callable that this Function wraps). Why then would we need to change self.__doc__
as well, and can't we just use self.docstring
?
msg_listeners = { | ||
re.compile("pattern"): expand_func_names(FakePlugin.my_function), | ||
re.compile("direct_pattern"): expand_func_names(FakePlugin.direct_function), | ||
re.compile("another_async_pattern"): expand_func_names( | ||
FakePlugin.my_async_function | ||
), | ||
re.compile("async_pattern"): expand_func_names(FakePlugin.my_async_function), | ||
re.compile("hi_custom"): expand_func_names(FakePlugin.hi_custom), | ||
# Click commands construct a regex pattern from the listen_to pattern | ||
re.compile("^click_command (.*)?"): expand_func_names(FakePlugin.click_commmand), | ||
} |
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.
Did you accidentally link the wrong file? I don't see the relevance of that line here. I think the relevant test was this one, and it did not require duplicating the listeners like this
Sorry, I will stop pulling things in/out until the review is complete. |
# Prior to initialization there is no help | ||
assert self.plugin_manager.get_help() == [] |
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 seems to rely on self.plugin_manager
not having been initialized, but it's initialized first thing in test_init
?
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.
Doesn't pytest run the two tests independently? i.e.
t = TestPluginManager()
t.setup_method()
t.test_get_help()
t = TestPluginManager()
t.setup_method()
t.test_get_init()
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.
Your recent changes look good to me and I've resolved some comments. Some comments I left earlier are still relevant though, could you have another look at those?
I think we're nearing convergence here, so I propose the following:
- Let's try to first resolve all comments open in this PR. I won't add any new ones, but we can continue discussing the ones that already have some discussion going on.
- Once everything is resolved, I'll need to have a fresh look at all the changes here, and since this is an enormous PR and there have been months in between each time I looked at it, it would be best to split this into several easily reviewable PRs so it becomes very clear what changes are being made in what context, and maybe @attzonko can then have a look at it as well if he wants. I think there are easily several PRs worth of changes here, such as the introduction of metadata, the introduction of the FunctionInfo, etc. If the interdependence between all these things is an issue, feel free to open one initial PR of very limited scope, and then make the others PR merge into that one rather than directly into
main
. We can then wait with merging that central PR until all the individual changes have been reviewed and merged into it without needing to look at 17 files at a time 😅
Thanks @jneeven! I'll give it another try at pulling out different PRs. Will likely go with your suggestion of making the PRs against the previous. |
Im eagerly awaiting this PR! Currently have a .help command to show what commands are available and what they do |
Allows passing custom function annotations that can be used by the help plugin to format or group functions (cherry picked from commit 66bb750)
(cherry picked from commit 8500e031ab34de5ead988e68fb41d4de6a4968a0)
Defines an interface for listeners to allow easier collection and formatting of help information
This interface splits up code from Plugin and separates the role of EventHandler from Plugins.
Code Climate has analyzed commit ef7180a and detected 3 issues on this pull request. Here's the issue category breakdown:
View more on Code Climate. |
Reopened #212 targeting
main
instead ofmaster
.