From a9d9aac6543ffacc0a1a19ba1fb3e643ab40e319 Mon Sep 17 00:00:00 2001 From: linuxdaemon Date: Wed, 20 Feb 2019 21:54:24 -0600 Subject: [PATCH 1/4] Switch client loading to use venusian Allows for easier dyanmic loading of decorated classes for registration --- cloudbot/bot.py | 21 ++++++++++++--------- cloudbot/client.py | 15 ++++++++------- cloudbot/clients/irc.py | 4 ++-- requirements.txt | 1 + 4 files changed, 23 insertions(+), 18 deletions(-) diff --git a/cloudbot/bot.py b/cloudbot/bot.py index 1f20ed2b5..d74e58cac 100644 --- a/cloudbot/bot.py +++ b/cloudbot/bot.py @@ -1,7 +1,6 @@ import asyncio import collections import gc -import importlib import logging import os import re @@ -13,9 +12,11 @@ from sqlalchemy import create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import scoped_session, sessionmaker +from venusian import Scanner from watchdog.observers import Observer -from cloudbot.client import Client, CLIENTS +from cloudbot import clients +from cloudbot.client import Client from cloudbot.config import Config from cloudbot.event import Event, CommandEvent, RegexEvent, EventType from cloudbot.hook import Action @@ -110,6 +111,7 @@ def __init__(self, loop=asyncio.get_event_loop()): self.loop = loop self.start_time = time.time() self.running = True + self.clients = {} # future which will be called when the bot stopsIf you self.stopped_future = async_util.create_future(self.loop) @@ -191,7 +193,10 @@ def run(self): return restart def get_client(self, name: str) -> Type[Client]: - return CLIENTS[name] + return self.clients[name] + + def register_client(self, name, cls): + self.clients[name] = cls def create_connections(self): """ Create a BotConnection for all the networks defined in the config """ @@ -202,7 +207,8 @@ def create_connections(self): _type = config.get("type", "irc") self.connections[name] = self.get_client(_type)( - self, name, nick, config=config, channels=config['channels'] + self, _type, name, nick, config=config, + channels=config['channels'] ) logger.debug("[%s] Created connection.", name) @@ -286,11 +292,8 @@ def load_clients(self): """ Load all clients from the "clients" directory """ - client_dir = self.base_dir / "cloudbot" / "clients" - for path in client_dir.rglob('*.py'): - rel_path = path.relative_to(self.base_dir) - mod_path = '.'.join(rel_path.parts).rsplit('.', 1)[0] - importlib.import_module(mod_path) + scanner = Scanner(bot=self) + scanner.scan(clients, categories=['cloudbot.client']) async def process(self, event): """ diff --git a/cloudbot/client.py b/cloudbot/client.py index bf20ad78c..817a2f091 100644 --- a/cloudbot/client.py +++ b/cloudbot/client.py @@ -3,18 +3,20 @@ import logging import random +import venusian + from cloudbot.permissions import PermissionManager from cloudbot.util import async_util logger = logging.getLogger("cloudbot") -CLIENTS = {} - def client(_type): def _decorate(cls): - CLIENTS[_type] = cls - cls._type = _type + def callback_cb(context, name, obj): + context.bot.register_client(_type, cls) + + venusian.attach(cls, callback_cb, category='cloudbot.client') return cls return _decorate @@ -41,9 +43,7 @@ class Client: :type permissions: PermissionManager """ - _type = None - - def __init__(self, bot, name, nick, *, channels=None, config=None): + def __init__(self, bot, _type, name, nick, *, channels=None, config=None): """ :type bot: cloudbot.bot.CloudBot :type name: str @@ -55,6 +55,7 @@ def __init__(self, bot, name, nick, *, channels=None, config=None): self.loop = bot.loop self.name = name self.nick = nick + self._type = _type if channels is None: self.channels = [] diff --git a/cloudbot/clients/irc.py b/cloudbot/clients/irc.py index b0aca8056..d1dc36274 100644 --- a/cloudbot/clients/irc.py +++ b/cloudbot/clients/irc.py @@ -56,7 +56,7 @@ class IrcClient(Client): :type _ignore_cert_errors: bool """ - def __init__(self, bot, name, nick, *, channels=None, config=None): + def __init__(self, bot, _type, name, nick, *, channels=None, config=None): """ :type bot: cloudbot.bot.CloudBot :type name: str @@ -64,7 +64,7 @@ def __init__(self, bot, name, nick, *, channels=None, config=None): :type channels: list[str] :type config: dict[str, unknown] """ - super().__init__(bot, name, nick, channels=channels, config=config) + super().__init__(bot, _type, name, nick, channels=channels, config=config) self.use_ssl = config['connection'].get('ssl', False) self._ignore_cert_errors = config['connection'].get('ignore_cert', False) diff --git a/requirements.txt b/requirements.txt index e99b3548a..c7c29c75f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,5 +16,6 @@ pyenchant==2.0.0 requests==2.22.0 SQLAlchemy==1.3.5 tweepy==3.7.0 +venusian==1.2.0 watchdog==0.9.0 yarl==1.3.0 From 3b2279e5a8efd43cdb2ed798717a73b97796b93f Mon Sep 17 00:00:00 2001 From: linuxdaemon Date: Wed, 20 Feb 2019 22:24:19 -0600 Subject: [PATCH 2/4] Update core_connect test for new client format --- tests/plugin_tests/test_core_connect.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/plugin_tests/test_core_connect.py b/tests/plugin_tests/test_core_connect.py index 7dccf423b..5bcacce57 100644 --- a/tests/plugin_tests/test_core_connect.py +++ b/tests/plugin_tests/test_core_connect.py @@ -18,12 +18,13 @@ class MockBot: def test_core_connects(): bot = MockBot() - client = MockClient(bot, 'foo', 'FooBot', config={ + client = MockClient(bot, 'mock', 'foo', 'FooBot', config={ 'connection': { 'server': 'example.com', 'password': 'foobar123' } }) + assert client.type == 'mock' client.connect() From 6c1184ab205439cf2d136f53776846e3924e65b7 Mon Sep 17 00:00:00 2001 From: linuxdaemon Date: Thu, 21 Feb 2019 09:26:22 -0600 Subject: [PATCH 3/4] Add tests for client loading --- tests/core_tests/test_bot.py | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/tests/core_tests/test_bot.py b/tests/core_tests/test_bot.py index c82be4108..8cbfc6e7b 100644 --- a/tests/core_tests/test_bot.py +++ b/tests/core_tests/test_bot.py @@ -1,12 +1,15 @@ import textwrap import pytest +from mock import patch + +from cloudbot import config @pytest.mark.parametrize('text,result', ( - ('connection', 'connection'), - ('c onn ection', 'c_onn_ection'), - ('c+onn ection', 'conn_ection'), + ('connection', 'connection'), + ('c onn ection', 'c_onn_ection'), + ('c+onn ection', 'conn_ection'), )) def test_clean_name(text, result): from cloudbot.bot import clean_name @@ -36,3 +39,28 @@ def test_get_cmd_regex(): (?:$|\s+) (?P.*) # Text """) + + +class MockConfig(config.Config): + def load_config(self): + self.update({ + 'connections': [ + { + 'type': 'irc', + 'name': 'foobar', + 'nick': 'TestBot', + 'channels': [], + 'connection': { + 'server': 'irc.example.com' + } + } + ] + }) + + +def test_load_clients(): + with patch('cloudbot.bot.Config', new=MockConfig): + from cloudbot.bot import CloudBot + bot = CloudBot() + assert bot.connections['foobar'].nick == 'TestBot' + assert bot.connections['foobar'].type == 'irc' From 52566100d96e90bf7db46440495a49ac5382b453 Mon Sep 17 00:00:00 2001 From: linuxdaemon Date: Fri, 28 Jun 2019 22:16:24 -0500 Subject: [PATCH 4/4] tests: Update test_client to new client constructor --- tests/core_tests/test_client.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/core_tests/test_client.py b/tests/core_tests/test_client.py index 1aa17efb7..f566fd00d 100644 --- a/tests/core_tests/test_client.py +++ b/tests/core_tests/test_client.py @@ -10,11 +10,10 @@ class Bot(MagicMock): class TestClient(Client): # pylint: disable=abstract-method - _type = 'TestClient' _connected = False - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, bot, *args, **kwargs): + super().__init__(bot, 'TestClient', *args, **kwargs) self.active = True @property