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

Make it possible to create class-based skills #734

Merged
merged 24 commits into from Jan 12, 2019

Conversation

gergelypolonkai
Copy link
Contributor

@gergelypolonkai gergelypolonkai commented Nov 13, 2018

Description

Make it possible to create class-based skills. This can be very useful when the skill needs a helper object, like an SDK for an external API.

Status

UNDER DEVELOPMENT

Type of change

  • New feature (non-breaking change which adds functionality)
  • This change requires a documentation update

How Has This Been Tested?

I created a very basic skill class for initial testing, but generic tests are still yet to come.

from opsdroid.matchers import match_regex
from opsdroid.skill import Skill


class TestSkill(Skill):
    @match_regex(r'number [0-9]')
    async def describe_project(self, opsdroid, config, message):
        await message.respond('Yes, that’s a number.')

Checklist:

  • I have performed a self-review of my own code
  • I have made corresponding changes to the documentation (if applicable)
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

Copy link
Member

@jacobtomlinson jacobtomlinson left a comment

Choose a reason for hiding this comment

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

Thanks so much for raising this. Looks interesting!

I have a few questions about your design decisions. Could you please respond to each one.

opsdroid/parsers/regex.py Outdated Show resolved Hide resolved
opsdroid/skill.py Outdated Show resolved Hide resolved
opsdroid/skill.py Outdated Show resolved Hide resolved
Copy link
Member

@jacobtomlinson jacobtomlinson left a comment

Choose a reason for hiding this comment

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

I feel like I am understanding more the approach you are taking. However I'm still not happy with it. We should be able to use the same matcher decorators as we do normally.

Let me try to describe the problem you experienced which caused you to fork the API:

  • When you create a skill class and import it the matchers are run against the unbound methods and update the method attributes with the matcher info.
  • Then you are creating an instance of the class and creating pointers to the bound methods of the instance.
  • The existing code then tries to set the config attribute on the bound method which fails because you cannot do that.
  • Your solution is to store the config in __skill_attrs__ which you are pre-creating in the __init__ method.

Perhaps as an alternative approach you could try this:

  • Catch the exception raised when trying to set the config on the method and ignore it.
  • Access the config at skill.__self__.config in the parser.

@codecov
Copy link

codecov bot commented Nov 13, 2018

Codecov Report

Merging #734 into master will not change coverage.
The diff coverage is 100%.

Impacted file tree graph

@@          Coverage Diff          @@
##           master   #734   +/-   ##
=====================================
  Coverage     100%   100%           
=====================================
  Files          29     30    +1     
  Lines        1901   1934   +33     
=====================================
+ Hits         1901   1934   +33
Impacted Files Coverage Δ
opsdroid/web.py 100% <100%> (ø) ⬆️
opsdroid/core.py 100% <100%> (ø) ⬆️
opsdroid/loader.py 100% <100%> (ø) ⬆️
opsdroid/skill/__init__.py 100% <100%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update dc70f4a...d6e5d7f. Read the comment docs.

@gergelypolonkai
Copy link
Contributor Author

I have reworked the whole thing (and learned a bit about Python internals while there). Now it works fine with the original decorators, without modifying the decorators themselves; it was much easier than i initially thought.

Copy link
Member

@jacobtomlinson jacobtomlinson left a comment

Choose a reason for hiding this comment

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

This looks even better than I imagined! This is a very nice way to add this functionality.

Could you now add tests and documentation for this.

Also we currently run a setup function for each skill if it exists. However after this change I think we should encourage people to use the class version going forwards and put their setup code in the __init__ method. So could you please add a deprecation warning message when the setup is called telling people that this will be removed in a future version.

for name in skill_obj.__dir__():
try:
method = getattr(skill_obj, name)
except Exception:
Copy link
Member

Choose a reason for hiding this comment

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

Can you make this less broad?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually, no. Imagine this code:

class MySkill(Skill):
    @property
    def erroneous_property(self):
        return self.sdk_this_skill_uses.get_erroneous_property()

Where get_erroneous_property() can raise any type of exception. Been there, done that 😞

We can either remove the try/except altogether and ask skill writers to look out for this corner case, or leave the except block this broad.

Copy link
Member

Choose a reason for hiding this comment

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

Fair enough! Can you please add a pylint disable with this justification in a commend. See here for an example of this.

@gergelypolonkai
Copy link
Contributor Author

Should the deprecation warning be an actual DeprecationWarning or just a log message like the one for the Api.ai parser?

@gergelypolonkai
Copy link
Contributor Author

Also, should the deprecation message appear if a function based skill is used, even if there is no setup function?

@jacobtomlinson
Copy link
Member

We should probably be raising deprecation warnings. No we should only show it if the skill uses it.

@jacobtomlinson
Copy link
Member

@gergelypolonkai I'm really keen to get this merged. Any chance you can pick it up again?

@gergelypolonkai
Copy link
Contributor Author

I’m a bit lost in the tests, but expect to finish soon.

@gergelypolonkai gergelypolonkai force-pushed the class-based-skills branch 3 times, most recently from e950db5 to 528e6f0 Compare November 28, 2018 14:28
@stale
Copy link

stale bot commented Dec 12, 2018

This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the stale label Dec 12, 2018
@jacobtomlinson
Copy link
Member

Hi @gergelypolonkai. Are you able to pick this up again?

@stale stale bot removed the stale label Dec 12, 2018
@gergelypolonkai
Copy link
Contributor Author

Most definitely! I really want to finish this, it’s just my $job that intervened.

@stale
Copy link

stale bot commented Dec 26, 2018

This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the stale label Dec 26, 2018
@stale stale bot removed the stale label Dec 28, 2018
@gergelypolonkai
Copy link
Contributor Author

Given i just got my second “stale” message, i hereby ask for some help. I tried to create a test for the class based skills, and for that i have created a mockedmodule with a skill in it. However, i can’t seem to figure out how to load it. Is there anyone who can give me some pointers?

@jacobtomlinson
Copy link
Member

You should be able to create an opsdroid instance, preferably using the context manager. Then you have a few options. You could either mock out opsdroid.loader.load_modules_from_config to return a list containing your module, then call opsdroid.load. Or you could add your module to opsdroid.modules and call the pieces of opsdroid.load that you wish to test.

@jacobtomlinson
Copy link
Member

Well it has taken a couple of months and some back and forth but it looks like this is ready to merge. Thanks for all the hard work @gergelypolonkai. I've managed to find some time today to sit down and sort out the tests and documentation, I understand how hard it can be to find time to pick these things back up again.

Ping me your address via DM on Twitter or Gitter and I'll post you some opsdroid stickers!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants