diff --git a/sarah/bot/base.py b/sarah/bot/base.py index 159e77f..66e1da3 100644 --- a/sarah/bot/base.py +++ b/sarah/bot/base.py @@ -1,17 +1,17 @@ # -*- coding: utf-8 -*- import abc -from collections import OrderedDict -from concurrent.futures import ThreadPoolExecutor, Future -from functools import wraps import imp import importlib +import inspect import logging import re import sys +from collections import OrderedDict +from concurrent.futures import ThreadPoolExecutor, Future +from functools import wraps from apscheduler.schedulers.background import BackgroundScheduler - -from typing import Sequence, Optional, Callable, Union +from typing import Optional, Callable, Union, Iterable from sarah.bot.types import PluginConfig, AnyFunction, CommandFunction from sarah.bot.values import Command, CommandMessage, UserContext, \ @@ -25,7 +25,7 @@ class Base(object, metaclass=abc.ABCMeta): __instances = {} def __init__(self, - plugins: Sequence[PluginConfig] = None, + plugins: Iterable[PluginConfig] = None, max_workers: Optional[int] = None) -> None: if not plugins: plugins = () @@ -214,17 +214,19 @@ def wrapper(func: CommandFunction) -> None: def wrapped_function(*args, **kwargs) -> str: return func(*args, **kwargs) - # Register only if bot is instantiated. + module = inspect.getmodule(func) self = cls.__instances.get(cls.__name__, None) - if self: - config = self.plugin_config.get(func.__module__, {}) + # Register only if bot is instantiated. + if self and module: + module_name = module.__name__ + config = self.plugin_config.get(module_name, {}) schedule_config = config.get('schedule', None) if schedule_config: # If command name duplicates, update with the later one. # The order stays. command = ScheduledCommand(name, wrapped_function, - func.__module__, + module_name, config, schedule_config) try: @@ -238,14 +240,14 @@ def wrapped_function(*args, **kwargs) -> str: else: logging.warning( 'Missing configuration for schedule job. %s. ' - 'Skipping.' % func.__module__) + 'Skipping.' % module_name) # To ease plugin's unit test return wrapped_function return wrapper - def add_schedule_jobs(self, commands: Sequence[ScheduledCommand]) -> None: + def add_schedule_jobs(self, commands: Iterable[ScheduledCommand]) -> None: for command in commands: # self.add_schedule_job(command) job_function = self.generate_schedule_job(command) @@ -273,14 +275,16 @@ def wrapper(func: CommandFunction) -> CommandFunction: def wrapped_function(*args, **kwargs) -> Union[str, UserContext]: return func(*args, **kwargs) - # Register only if bot is instantiated. + module = inspect.getmodule(func) self = cls.__instances.get(cls.__name__, None) - if self: - config = self.plugin_config.get(func.__module__, {}) + # Register only if bot is instantiated. + if self and module: + module_name = module.__name__ + config = self.plugin_config.get(module_name, {}) # If command name duplicates, update with the later one. # The order stays. - command = Command(name, func, func.__module__, config) + command = Command(name, func, module_name, config) try: # If command is already registered, updated it. idx = [c.name for c in cls.__commands[cls.__name__]] \ diff --git a/sarah/bot/hipchat.py b/sarah/bot/hipchat.py index 7a932dc..07355cc 100644 --- a/sarah/bot/hipchat.py +++ b/sarah/bot/hipchat.py @@ -1,32 +1,30 @@ # -*- coding: utf-8 -*- -from concurrent.futures import Future import logging +from concurrent.futures import Future from sleekxmpp import ClientXMPP, Message from sleekxmpp.exceptions import IqTimeout, IqError -from typing import Dict, Optional, Sequence, Callable +from typing import Dict, Optional, Callable, Iterable -from sarah.exceptions import SarahException from sarah.bot import Base, concurrent -from sarah.bot.values import ScheduledCommand from sarah.bot.types import PluginConfig +from sarah.bot.values import ScheduledCommand +from sarah.exceptions import SarahException class HipChat(Base): def __init__(self, - plugins: Sequence[PluginConfig]=None, - jid: str='', - password: str='', - rooms: Sequence[str]=None, - nick: str='', - proxy: Dict=None, - max_workers: int=None) -> None: + plugins: Iterable[PluginConfig] = None, + jid: str = '', + password: str = '', + rooms: Iterable[str] = None, + nick: str = '', + proxy: Dict = None, + max_workers: int = None) -> None: super().__init__(plugins=plugins, max_workers=max_workers) - if not rooms: - rooms = [] - self.rooms = rooms + self.rooms = rooms if rooms else [] # type: Iterable[str] self.nick = nick self.client = self.setup_xmpp_client(jid, password, proxy) @@ -34,7 +32,7 @@ def generate_schedule_job(self, command: ScheduledCommand) -> Optional[Callable]: # pop room configuration to leave minimal information for command # argument - rooms = command.schedule_config.pop('rooms', None) + rooms = command.schedule_config.pop('rooms', []) if not rooms: logging.warning( 'Missing rooms configuration for schedule job. %s. ' @@ -61,7 +59,7 @@ def connect(self) -> None: def setup_xmpp_client(self, jid: str, password: str, - proxy: Dict=None) -> ClientXMPP: + proxy: Dict = None) -> ClientXMPP: client = ClientXMPP(jid, password) if proxy: diff --git a/sarah/bot/plugins/bmw_quotes.py b/sarah/bot/plugins/bmw_quotes.py index 17267ac..5ffa547 100644 --- a/sarah/bot/plugins/bmw_quotes.py +++ b/sarah/bot/plugins/bmw_quotes.py @@ -2,10 +2,10 @@ import random from typing import Dict -from sarah.bot.values import CommandMessage + from sarah.bot.hipchat import HipChat from sarah.bot.slack import Slack, SlackMessage, MessageAttachment - +from sarah.bot.values import CommandMessage # http://www.imdb.com/title/tt0105958/quotes quotes = ([('Eric', "So i said to myself, 'Kyle'"), diff --git a/sarah/bot/plugins/echo.py b/sarah/bot/plugins/echo.py index 598f620..9376958 100644 --- a/sarah/bot/plugins/echo.py +++ b/sarah/bot/plugins/echo.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- from typing import Dict -from sarah.bot.values import CommandMessage + from sarah.bot.hipchat import HipChat from sarah.bot.slack import Slack +from sarah.bot.values import CommandMessage # noinspection PyUnusedLocal diff --git a/sarah/bot/plugins/hello.py b/sarah/bot/plugins/hello.py index f4549eb..e1a0fd8 100644 --- a/sarah/bot/plugins/hello.py +++ b/sarah/bot/plugins/hello.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- from typing import Dict -from sarah.bot.values import CommandMessage, UserContext, InputOption from sarah.bot.hipchat import HipChat +from sarah.bot.values import CommandMessage, UserContext, InputOption # noinspection PyUnusedLocal diff --git a/sarah/bot/plugins/simple_counter.py b/sarah/bot/plugins/simple_counter.py index 1e89bb5..13240f9 100644 --- a/sarah/bot/plugins/simple_counter.py +++ b/sarah/bot/plugins/simple_counter.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- from typing import Dict -from sarah.bot.values import CommandMessage from sarah.bot.hipchat import HipChat from sarah.bot.slack import Slack +from sarah.bot.values import CommandMessage __stash = {'hipchat': {}, 'slack': {}} diff --git a/sarah/bot/slack.py b/sarah/bot/slack.py index 1ce60ee..63e8ead 100644 --- a/sarah/bot/slack.py +++ b/sarah/bot/slack.py @@ -1,24 +1,24 @@ # -*- coding: utf-8 -*- # https://api.slack.com/rtm -from concurrent.futures import Future import json import logging +from concurrent.futures import Future -from typing import Optional, Dict, Sequence, Callable import requests +from typing import Optional, Dict, Callable, Iterable from websocket import WebSocketApp -from sarah import ValueObject -from sarah.exceptions import SarahException +from sarah import ValueObject from sarah.bot import Base, concurrent -from sarah.bot.values import RichMessage, ScheduledCommand from sarah.bot.types import PluginConfig +from sarah.bot.values import RichMessage, ScheduledCommand +from sarah.exceptions import SarahException class SlackClient(object): def __init__(self, token: str, - base_url: str='https://slack.com/api/') -> None: + base_url: str = 'https://slack.com/api/') -> None: self.base_url = base_url self.token = token @@ -36,8 +36,8 @@ def post(self, method, params=None, data=None) -> Dict: def request(self, http_method: str, method: str, - params: Dict=None, - data: Dict=None) -> Dict: + params: Dict = None, + data: Dict = None) -> Dict: http_method = http_method.upper() endpoint = self.generate_endpoint(method) @@ -61,7 +61,7 @@ def request(self, class AttachmentField(ValueObject): - def __init__(self, title: str, value: str, short: bool=None): + def __init__(self, title: str, value: str, short: bool = None): pass def to_dict(self): @@ -81,15 +81,15 @@ class MessageAttachment(ValueObject): def __init__(self, fallback: str, title: str, - title_link: str=None, - author_name: str=None, - author_link: str=None, - author_icon: str=None, - fields: Sequence[AttachmentField]=None, - image_url: str=None, - thumb_url: str=None, - pretext: str=None, - color: str=None): + title_link: str = None, + author_name: str = None, + author_link: str = None, + author_icon: str = None, + fields: Iterable[AttachmentField] = None, + image_url: str = None, + thumb_url: str = None, + pretext: str = None, + color: str = None): pass def to_dict(self): @@ -109,16 +109,16 @@ def to_dict(self): class SlackMessage(RichMessage): def __init__(self, - text: str=None, - as_user: bool=True, - username: str=None, - parse: str="full", - link_names: int=1, - unfurl_links: bool=True, - unfurl_media: bool=False, - icon_url: str=None, - icon_emoji: str=None, - attachments: Sequence[MessageAttachment]=None): + text: str = None, + as_user: bool = True, + username: str = None, + parse: str = "full", + link_names: int = 1, + unfurl_links: bool = True, + unfurl_media: bool = False, + icon_url: str = None, + icon_emoji: str = None, + attachments: Iterable[MessageAttachment] = None): pass def __str__(self) -> str: @@ -135,7 +135,7 @@ def to_dict(self): return params - def to_request_params(self): + def to_request_params(self) -> Dict: params = self.to_dict() if 'attachments' in params: @@ -147,9 +147,9 @@ def to_request_params(self): class Slack(Base): def __init__(self, - token: str='', - plugins: Sequence[PluginConfig]=None, - max_workers: int=None) -> None: + token: str = '', + plugins: Iterable[PluginConfig] = None, + max_workers: int = None) -> None: super().__init__(plugins=plugins, max_workers=max_workers) @@ -181,7 +181,7 @@ def connect(self) -> None: def generate_schedule_job(self, command: ScheduledCommand) -> Optional[Callable]: - channels = command.schedule_config.pop('channels', None) + channels = command.schedule_config.pop('channels', []) if not channels: logging.warning( 'Missing channels configuration for schedule job. %s. ' @@ -193,7 +193,7 @@ def job_function() -> None: if isinstance(ret, SlackMessage): for channel in channels: # TODO Error handling - data = dict({'channel': channel}) + data = {'channel': channel} data.update(ret.to_request_params()) self.client.post('chat.postMessage', data=data) else: @@ -280,7 +280,7 @@ def handle_message(self, content: Dict) -> Optional[Future]: ret = self.respond(content['user'], content['text']) if isinstance(ret, SlackMessage): # TODO Error handling - data = dict({'channel': content["channel"]}) + data = {'channel': content["channel"]} data.update(ret.to_request_params()) self.client.post('chat.postMessage', data=data) elif isinstance(ret, str): @@ -300,7 +300,7 @@ def on_close(self, _: WebSocketApp) -> None: def send_message(self, channel: str, text: str, - message_type: str='message') -> None: + message_type: str = 'message') -> None: params = {'channel': channel, 'text': text, 'type': message_type, diff --git a/sarah/bot/values/__init__.py b/sarah/bot/values/__init__.py index d8a5002..337c8d0 100644 --- a/sarah/bot/values/__init__.py +++ b/sarah/bot/values/__init__.py @@ -1,7 +1,9 @@ # -*- coding: utf-8 -*- import abc import re -from typing import Union, Pattern, AnyStr, Callable, Sequence, Dict + +from typing import Union, Pattern, AnyStr, Callable, Dict, Iterable + from sarah import ValueObject from sarah.bot.types import CommandFunction, CommandConfig @@ -16,7 +18,6 @@ class InputOption(ValueObject): def __init__(self, pattern: Union[Pattern, AnyStr], next_step: Callable) -> None: - if isinstance(pattern, str): self['pattern'] = re.compile(pattern) @@ -36,7 +37,7 @@ class UserContext(ValueObject): def __init__(self, message: Union[str, RichMessage], help_message: str, - input_options: Sequence[InputOption]) -> None: + input_options: Iterable[InputOption]) -> None: pass def __str__(self): @@ -51,7 +52,7 @@ def help_message(self) -> str: return self['help_message'] @property - def input_options(self) -> Sequence[InputOption]: + def input_options(self) -> Iterable[InputOption]: return self['input_options'] @@ -103,6 +104,7 @@ def execute(self, *args) -> Union[UserContext, str]: class ScheduledCommand(Command): + # noinspection PyMissingConstructor def __init__(self, name: str, function: CommandFunction, diff --git a/sarah/main.py b/sarah/main.py index 3d441d0..c314980 100644 --- a/sarah/main.py +++ b/sarah/main.py @@ -1,21 +1,21 @@ # -*- coding: utf-8 -*- import logging -from multiprocessing import Process import os +from multiprocessing import Process -from typing import Dict, Sequence import yaml +from typing import Dict, Iterable from sarah.bot.hipchat import HipChat from sarah.bot.slack import Slack -from sarah.exceptions import SarahException from sarah.bot.types import Path +from sarah.exceptions import SarahException class Sarah(object): def __init__(self, - config_paths: Sequence[Path]) -> None: + config_paths: Iterable[Path]) -> None: self.config = self.load_config(config_paths) @@ -33,7 +33,7 @@ def start(self) -> None: slack_process.start() @staticmethod - def load_config(paths: Sequence[Path]) -> Dict: + def load_config(paths: Iterable[Path]) -> Dict: config = {} if not paths: diff --git a/sarah/thread.py b/sarah/thread.py index 07d9fa2..01283b7 100644 --- a/sarah/thread.py +++ b/sarah/thread.py @@ -1,12 +1,12 @@ # -*- coding: utf-8 -*- # noinspection PyProtectedMember -from concurrent.futures.thread import _WorkItem as WorkItem -from concurrent.futures import Executor, Future +import atexit import logging -from queue import Queue import threading import weakref -import atexit +from concurrent.futures import Executor, Future +from concurrent.futures.thread import _WorkItem as WorkItem +from queue import Queue # Provide the same interface as ThreadPoolExecutor, but create only on thread. # Worker is created as daemon thread. This is done to allow the interpreter to diff --git a/sarah/value_object.py b/sarah/value_object.py index e147ce0..ec4c9ae 100644 --- a/sarah/value_object.py +++ b/sarah/value_object.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- from inspect import getfullargspec + from typing import Any diff --git a/setup.py b/setup.py index ea2a984..f1ef85c 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,6 @@ import os from setuptools import setup, find_packages - # noinspection PyPep8Naming from sarah import __name__ as PACKAGE_NAME from sarah import VERSION diff --git a/tests/test_hipchat.py b/tests/test_hipchat.py index 35e9623..8d3eb99 100644 --- a/tests/test_hipchat.py +++ b/tests/test_hipchat.py @@ -1,22 +1,22 @@ # -*- coding: utf-8 -*- import concurrent -from concurrent.futures import ALL_COMPLETED -from time import sleep import logging import types +from concurrent.futures import ALL_COMPLETED +from time import sleep import pytest +from assertpy import assert_that +from mock import MagicMock, call, patch from sleekxmpp import ClientXMPP -from sleekxmpp.test import TestSocket -from sleekxmpp.stanza import Message from sleekxmpp.exceptions import IqTimeout, IqError +from sleekxmpp.stanza import Message +from sleekxmpp.test import TestSocket from sleekxmpp.xmlstream import JID -from mock import MagicMock, call, patch -from assertpy import assert_that -from sarah.bot.values import UserContext, CommandMessage, Command -from sarah.bot.hipchat import HipChat, SarahHipChatException import sarah.bot.plugins.simple_counter +from sarah.bot.hipchat import HipChat, SarahHipChatException +from sarah.bot.values import UserContext, CommandMessage, Command # noinspection PyProtectedMember diff --git a/tests/test_hipchat_plugin.py b/tests/test_hipchat_plugin.py index 958800b..91025ca 100644 --- a/tests/test_hipchat_plugin.py +++ b/tests/test_hipchat_plugin.py @@ -1,12 +1,13 @@ # -*- coding: utf-8 -*- from assertpy import assert_that -from sarah.bot.values import UserContext, CommandMessage + +from sarah.bot.plugins.bmw_quotes import hipchat_quote, hipchat_scheduled_quote from sarah.bot.plugins.echo import hipchat_echo from sarah.bot.plugins.hello import hipchat_hello, hipchat_user_feeling_good, \ hipchat_user_feeling_bad from sarah.bot.plugins.simple_counter import hipchat_count, \ hipchat_reset_count, reset_count -from sarah.bot.plugins.bmw_quotes import hipchat_quote, hipchat_scheduled_quote +from sarah.bot.values import UserContext, CommandMessage class TestEcho(object): diff --git a/tests/test_main.py b/tests/test_main.py index 8686483..338dfd0 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- import os -from assertpy import assert_that import pytest +from assertpy import assert_that from sarah.main import Sarah, SarahException @@ -24,6 +24,6 @@ def test_non_existing_paths(self): with pytest.raises(SarahException) as e: Sarah(config_paths=non_existing_paths) - assert_that(str(e))\ - .contains('Configuration file does not exist')\ + assert_that(str(e)) \ + .contains('Configuration file does not exist') \ .contains(non_existing_paths[0]) diff --git a/tests/test_slack.py b/tests/test_slack.py index d447a72..087eae2 100644 --- a/tests/test_slack.py +++ b/tests/test_slack.py @@ -2,9 +2,8 @@ import logging import types -from assertpy import assert_that - import pytest +from assertpy import assert_that from mock import patch, MagicMock, call import sarah diff --git a/tests/test_slack_plugin.py b/tests/test_slack_plugin.py index 2e175ba..f7821da 100644 --- a/tests/test_slack_plugin.py +++ b/tests/test_slack_plugin.py @@ -1,11 +1,12 @@ # -*- coding: utf-8 -*- from assertpy import assert_that -from sarah.bot.slack import SlackMessage -from sarah.bot.values import CommandMessage + from sarah.bot.plugins.bmw_quotes import slack_quote from sarah.bot.plugins.echo import slack_echo from sarah.bot.plugins.simple_counter import reset_count, slack_count, \ slack_reset_count +from sarah.bot.slack import SlackMessage +from sarah.bot.values import CommandMessage class TestEcho(object): diff --git a/tests/test_value_object.py b/tests/test_value_object.py index d7ef5a1..ab2784e 100644 --- a/tests/test_value_object.py +++ b/tests/test_value_object.py @@ -1,9 +1,11 @@ # -*- coding: utf-8 -*- -from assertpy import assert_that +import re + import pytest +from assertpy import assert_that from typing import Union, AnyStr, Pattern + from sarah import ValueObject -import re class TestInit(object): @@ -28,7 +30,9 @@ def __init__(self, key1="spam", key2="ham", key3=None): class TestOverride(object): class MyValueWithInit(ValueObject): - def __init__(self, pattern: Union[Pattern, AnyStr]=None, key1="spam"): + def __init__(self, + pattern: Union[Pattern, AnyStr, None], + key1="spam") -> None: if isinstance(pattern, str): self['pattern'] = re.compile(pattern)