Skip to content

Commit

Permalink
Merge db56302 into bc2baf0
Browse files Browse the repository at this point in the history
  • Loading branch information
jacobtomlinson committed Nov 11, 2016
2 parents bc2baf0 + db56302 commit 108aad3
Show file tree
Hide file tree
Showing 15 changed files with 166 additions and 119 deletions.
2 changes: 0 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ sudo: false
matrix:
fast_finish: true
include:
- python: "3.4"
env: TOXENV=py34
- python: "3.5"
env: TOXENV=lint
- python: "3.5"
Expand Down
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3-alpine
FROM python:3.5-alpine
MAINTAINER Jacob Tomlinson <jacob@tom.linson.uk>

RUN mkdir -p /usr/src/app
Expand All @@ -8,6 +8,7 @@ WORKDIR /usr/src/app
COPY . .

RUN apk update && apk add git
RUN pip3 install --upgrade pip
RUN pip3 install --no-cache-dir -r requirements.txt
RUN pip3 install -U tox

Expand Down
19 changes: 11 additions & 8 deletions opsdroid/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import logging
import argparse

from opsdroid.loader import Loader
from opsdroid.core import OpsDroid
from opsdroid.helper import set_logging_level
from opsdroid.const import LOG_FILENAME
Expand All @@ -19,6 +18,13 @@ def parse_args(args):
return parser.parse_args(args)


def check_dependencies():
"""Check for system dependencies required by opsdroid."""
if sys.version_info[0] < 3 or sys.version_info[1] < 5:
logging.critical("Whoops! opsdroid requires python 3.5 or above.")
sys.exit(1)


def main():
"""The main function."""
logging.basicConfig(filename=LOG_FILENAME, level=logging.INFO)
Expand All @@ -35,16 +41,13 @@ def main():
print(conf.read())
sys.exit(0)

check_dependencies()

with OpsDroid() as opsdroid:
loader = Loader(opsdroid)
opsdroid.config = loader.load_config_file([
"./configuration.yaml",
"~/.opsdroid/configuration.yaml",
"/etc/opsdroid/configuration.yaml"
])
opsdroid.load()
if "logging" in opsdroid.config:
set_logging_level(opsdroid.config['logging'])
loader.load_config(opsdroid.config)
opsdroid.start_loop()
opsdroid.exit()

if __name__ == "__main__":
Expand Down
27 changes: 20 additions & 7 deletions opsdroid/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,37 @@ def __init__(self, config):
self.name = ""
self.config = config

def connect(self, opsdroid):
"""Connect to chat service and parse all messages.
async def connect(self, opsdroid):
"""Connect to chat service.
This method should create a connection to the desired chat service.
It should also be possible to call it multiple times in the event of
being disconnected.
Args:
opsdroid (OpsDroid): An instance of the opsdroid core.
"""
raise NotImplementedError

async def listen(self, opsdroid):
"""Listen to chat service and parse all messages.
This method should block the thread with an infinite loop and create
Message objects for chat messages coming from the service. It should
then call `opsdroid.parse(message)` on those messages.
then call `await opsdroid.parse(message)` on those messages.
Due to this method blocking, if multiple connectors are configured in
opsdroid they will be run in parallel using the multiprocessing
library.
As the method should include some kind of `while True` all messages
from the chat service should be "awaited" asyncronously to avoid
blocking the thread.
Args:
opsdroid (OpsDroid): An instance of the opsdroid core.
"""
raise NotImplementedError

def respond(self, message):
async def respond(self, message):
"""Send a message back to the chat service.
The message object will have a `text` property which should be sent
Expand Down
84 changes: 58 additions & 26 deletions opsdroid/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,34 @@
import logging
import sys
import weakref
from multiprocessing import Process
import asyncio

from opsdroid.helper import match
from opsdroid.memory import Memory
from opsdroid.connector import Connector
from opsdroid.database import Database
from opsdroid.loader import Loader


class OpsDroid():
"""Root object for opsdroid."""

# pylint: disable=too-many-instance-attributes
# All are reasonable in this case.

instances = []

def __init__(self):
"""Start opsdroid."""
self.bot_name = 'opsdroid'
self.sys_status = 0
self.connectors = []
self.connector_jobs = []
self.connector_tasks = []
self.eventloop = asyncio.get_event_loop()
self.skills = []
self.memory = Memory()
self.loader = {}
self.config = {}
logging.info("Created main opsdroid object")

def __enter__(self):
Expand All @@ -41,6 +49,8 @@ def exit(self):
"""Exit application."""
logging.info("Exiting application with return code " +
str(self.sys_status))
if self.eventloop.is_running():
self.eventloop.stop()
sys.exit(self.sys_status)

def critical(self, error, code):
Expand All @@ -50,34 +60,56 @@ def critical(self, error, code):
print("Error: " + error)
self.exit()

def start_connectors(self, connectors):
def load(self):
"""Load configuration."""
self.loader = Loader(self)
self.config = self.loader.load_config_file([
"./configuration.yaml",
"~/.opsdroid/configuration.yaml",
"/etc/opsdroid/configuration.yaml"
])

def start_loop(self):
"""Start the event loop."""
connectors, databases, skills = self.loader.load_config(self.config)
if databases is not None:
self.start_databases(databases)
self.setup_skills(skills)
self.start_connector_tasks(connectors)
try:
self.eventloop.run_forever()
except (KeyboardInterrupt, EOFError):
print('') # Prints a character return for return to shell
logging.info("Keyboard interrupt, exiting.")
self.exit()

def setup_skills(self, skills):
"""Call the setup function on the passed in skills."""
for skill in skills:
try:
skill["module"].setup(self)
except AttributeError:
pass

def start_connector_tasks(self, connectors):
"""Start the connectors."""
if len(connectors) == 0:
self.critical("All connectors failed to load", 1)
elif len(connectors) == 1:
for name, cls in connectors[0]["module"].__dict__.items():
for connector_module in connectors:
for _, cls in connector_module["module"].__dict__.items():
if isinstance(cls, type) and \
issubclass(cls, Connector) and\
cls is not Connector:
logging.debug("Adding connector: " + name)
connectors[0]["config"]["bot-name"] = self.bot_name
connector = cls(connectors[0]["config"])
connector_module["config"]["bot-name"] = self.bot_name
connector = cls(connector_module["config"])
self.connectors.append(connector)
connector.connect(self)

if len(connectors) > 0:
for connector in self.connectors:
self.eventloop.run_until_complete(connector.connect(self))
for connector in self.connectors:
task = self.eventloop.create_task(connector.listen(self))
self.connector_tasks.append(task)
else:
for connector_module in connectors:
for name, cls in connector_module["module"].__dict__.items():
if isinstance(cls, type) and \
issubclass(cls, Connector) and\
cls is not Connector:
connector_module["config"]["bot-name"] = self.bot_name
connector = cls(connector_module["config"])
self.connectors.append(connector)
job = Process(target=connector.connect, args=(self,))
job.start()
self.connector_jobs.append(job)
for job in self.connector_jobs:
job.join()
self.critical("All connectors failed to load", 1)

def start_databases(self, databases):
"""Start the databases."""
Expand All @@ -97,7 +129,7 @@ def load_regex_skill(self, regex, skill):
"""Load skills."""
self.skills.append({"regex": regex, "skill": skill})

def parse(self, message):
async def parse(self, message):
"""Parse a string against all skills."""
if message.text.strip() != "":
logging.debug("Parsing input: " + message.text)
Expand All @@ -106,4 +138,4 @@ def parse(self, message):
regex = match(skill["regex"], message.text)
if regex:
message.regex = regex
skill["skill"](self, message)
await skill["skill"](self, message)
22 changes: 7 additions & 15 deletions opsdroid/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,27 +99,27 @@ def load_config(self, config):
"""Load all module types based on config."""
logging.debug("Loading modules from config")

connectors, databases, skills = None, None, None

if 'databases' in config.keys():
self.opsdroid.start_databases(
self._load_modules('database', config['databases']))
databases = self._load_modules('database', config['databases'])
else:
logging.warning("No databases in configuration")

if 'skills' in config.keys():
self._setup_modules(
self._load_modules('skill', config['skills'])
)
skills = self._load_modules('skill', config['skills'])
else:
self.opsdroid.critical(
"No skills in configuration, at least 1 required", 1)

if 'connectors' in config.keys():
self.opsdroid.start_connectors(
self._load_modules('connector', config['connectors']))
connectors = self._load_modules('connector', config['connectors'])
else:
self.opsdroid.critical(
"No connectors in configuration, at least 1 required", 1)

return connectors, databases, skills

def _load_modules(self, modules_type, modules):
"""Install and load modules."""
logging.debug("Loading " + modules_type + " modules")
Expand Down Expand Up @@ -156,14 +156,6 @@ def _load_modules(self, modules_type, modules):

return loaded_modules

def _setup_modules(self, modules):
"""Call the setup function on the passed in modules."""
for module in modules:
try:
module["module"].setup(self.opsdroid)
except AttributeError:
pass

def _install_module(self, config):
# pylint: disable=R0201
"""Install a module."""
Expand Down
4 changes: 2 additions & 2 deletions opsdroid/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ def __init__(self, text, user, room, connector):
self.connector = connector
self.regex = None

def respond(self, text):
async def respond(self, text):
"""Respond to this message using the connector it was created by."""
response = copy(self)
response.text = text
self.connector.respond(response)
await self.connector.respond(response)
3 changes: 2 additions & 1 deletion requirements_test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ coveralls>=1.1
pytest>=2.9.2
pytest-cov>=2.2.1
pytest-timeout>=1.0.0
pytest-capturelog>=0.7
pytest-catchlog>=1.2.2
asynctest
pydocstyle>=1.0.0
requests_mock>=1.0
mypy-lang>=0.4
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[wheel]
universal = 1

[pytest]
[tool:pytest]
testpaths = tests
norecursedirs = .git testing_config

Expand Down
13 changes: 11 additions & 2 deletions tests/test_connector.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@

import unittest
import asyncio

from opsdroid.connector import Connector


class TestConnectorBaseClass(unittest.TestCase):
"""Test the opsdroid connector base class."""

def setUp(self):
self.loop = asyncio.new_event_loop()

def test_init(self):
config = {"example_item": "test"}
connector = Connector(config)
Expand All @@ -16,9 +20,14 @@ def test_init(self):
def test_connect(self):
connector = Connector({})
with self.assertRaises(NotImplementedError):
connector.connect({})
self.loop.run_until_complete(connector.connect({}))

def test_listen(self):
connector = Connector({})
with self.assertRaises(NotImplementedError):
self.loop.run_until_complete(connector.listen({}))

def test_respond(self):
connector = Connector({})
with self.assertRaises(NotImplementedError):
connector.respond({})
self.loop.run_until_complete(connector.respond({}))

0 comments on commit 108aad3

Please sign in to comment.