diff --git a/.circleci/config.yml b/.circleci/config.yml
index ee761dd..72bf74d 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -15,9 +15,9 @@ jobs:
- run:
name: install dependencies
command: |
- pip install pipenv;
- pipenv install;
- pipenv install --dev;
+ pip install pipenv
+ pipenv install --python `which pypy3`
+ pipenv install --python `which pypy3` --dev
- save_cache:
paths:
- ./repo
@@ -38,9 +38,9 @@ jobs:
- run:
name: install dependencies
command: |
- pip install pipenv;
- pipenv install;
- pipenv install --dev;
+ pip install pipenv
+ pipenv install --three
+ pipenv install --three --dev
- save_cache:
paths:
- ./repo
@@ -61,9 +61,9 @@ jobs:
- run:
name: install dependencies
command: |
- pip install pipenv;
- pipenv install;
- pipenv install --dev;
+ pip install pipenv
+ pipenv install --three
+ pipenv install --three --dev
- save_cache:
paths:
- ./repo
@@ -84,16 +84,16 @@ jobs:
- run:
name: install dependencies
command: |
- pip install pipenv;
- pipenv install;
- pipenv install --dev;
+ pip install pipenv
+ pipenv install --three
+ pipenv install --three --dev
- save_cache:
paths:
- ./repo
key: v1-dependencies-{{ checksum "Pipfile.lock" }}
- run:
name: run tests
- command: pipenv run flake8 eeee tests
+ command: pipenv run flake8 cl tests
workflows:
version: 2
test_all:
diff --git a/Makefile b/Makefile
index 9911653..1408b53 100644
--- a/Makefile
+++ b/Makefile
@@ -10,7 +10,7 @@ install: ## install dependencies
install-dev: install ## install dev dependencies
pipenv install --dev
-clean: clean-build clean-pyc
+clean: clean-build clean-pyc clean-cache
clean-build: ## remove build artifacts
rm -fr build/
@@ -22,18 +22,24 @@ clean-pyc: ## remove Python file artifacts
find . -name '*.pyo' -exec rm -f {} +
find . -name '*~' -exec rm -f {} +
+clean-cache: ## remove .cache and .pytest_cache
+ rm -rf .cache
+ rm -rf .pytest_cache
+
lint: ## check style with flake8
- pipenv run flake8 eeee tests
+ pipenv run flake8
test: ## run tests quickly with the default Python
pipenv run pytest
test-all: ## run tests on every Python version with tox
+ rm -rf .tox
pipenv run tox --skip-missing-interpreters
coverage: ## check code coverage quickly with the default Python
rm -rf htmlcov
- pipenv run coverage run --source eeee -m pytest
+ pipenv run coverage erase
+ pipenv run coverage run -m pytest
pipenv run coverage report -m
pipenv run coverage html
diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html
index 5b45a2c..cd7dd7c 100644
--- a/docs/_templates/layout.html
+++ b/docs/_templates/layout.html
@@ -3,12 +3,12 @@
{% block extrabody %}
{{ super() }}
-
+
{% endblock %}
diff --git a/eeee/event.py b/eeee/event.py
index ad9d3be..7035cc7 100644
--- a/eeee/event.py
+++ b/eeee/event.py
@@ -62,6 +62,7 @@ class Event:
:param name: Optional Event name. If empty will be assigned to name of Class.
:type name: eeee.event.Event, str
+ :raises: eeee.exceptions.NamingError
"""
RETURN_EXCEPTIONS = False
@@ -119,16 +120,16 @@ async def publish(self, message: Any, publisher: Union["Publisher", str] = None)
:type publisher: eeee.event.Publisher, str
:return: List of results from subscribed handlers or None if event is disabled.
"""
- if self.is_enable:
- if publisher is not None:
- publisher = Publisher(publisher)
- coros = []
- for ps in self.pub_sub:
- if ps.publisher is None or (publisher is not None and ps.publisher == publisher):
- coros.append(ps.subscriber(message, publisher, event=self.name))
- if coros:
- return await asyncio.gather(*coros, return_exceptions=self.RETURN_EXCEPTIONS)
- return None
+ if not self.is_enable:
+ return None
+
+ publisher = Publisher(publisher) if publisher else publisher
+
+ coros = []
+ for ps in self.pub_sub:
+ if ps.publisher is None or (publisher is not None and ps.publisher == publisher):
+ coros.append(ps.subscriber(message, publisher, event=self.name))
+ return await asyncio.gather(*coros, return_exceptions=self.RETURN_EXCEPTIONS)
def subscribe(self, publisher: Union["Publisher", str] = None):
"""Subscribe decorator integrated within Event object.
@@ -279,10 +280,11 @@ def __eq__(self, other):
return False
@property
- def id(self):
+ def id(self) -> str:
"""Publisher identification.
:return: string ID
+ :rtype: str
"""
return self.__id
@@ -323,24 +325,11 @@ class Subscriber:
"""
def __init__(self, handler: Union["Subscriber", callable]):
- if isinstance(handler, self.__class__):
- self.handler = handler.handler
- self.name = handler.name
- elif callable(handler):
- try:
- self.name = handler.__name__
- self.handler = handler
- is_coro = iscoroutinefunction(handler)
- except AttributeError:
- # assume instance of callable class
- self.name = handler.__class__.__name__
- self.handler = handler
- is_coro = iscoroutinefunction(handler.__call__)
-
- if not is_coro:
- raise exceptions.NotCoroutineError
- else:
- raise exceptions.NotCallableError
+ self.name, self.handler = _parse_handler(handler)
+
+ # handler validation
+ _is_callable(self.handler)
+ _is_coro(self.handler)
self.__template = str(self.__class__) + '{name}'
self.__id = self.__template.format(name=self.name)
@@ -352,13 +341,66 @@ def __eq__(self, other):
return self.id == self.__template.format(name=other)
return False
- async def __call__(self, *args, **kwargs):
- return await self.handler(*args, **kwargs)
+ async def __call__(self, message, publisher, event):
+ return await self.handler(message=message, publisher=publisher, event=event)
@property
def id(self):
"""Subscriber identification.
:return: string ID
+ :rtype: str
"""
return self.__id
+
+
+def _parse_handler(handler: Union[callable, object, Subscriber]):
+ """Parse handler name and body.
+
+ Function accept functions, callable object and instance of Subscriber.
+ If Subscriber has been given, parser will extract its name and handler.
+
+ :param handler:
+ :return:
+ """
+ if isinstance(handler, Subscriber):
+ return handler.name, handler.handler
+
+ try:
+ name = handler.__name__
+ except AttributeError:
+ # assume instance of callable object
+ name = handler.__class__.__name__
+
+ return name, handler
+
+
+def _is_callable(handler: Union[callable, object]):
+ """Check if handler is callable.
+
+ :param handler: Function or callable object.
+ :type handler: callable, object
+ :raises: eeee.exception.NotCallableError
+ :return: None
+ """
+ if not callable(handler):
+ raise exceptions.NotCallableError
+
+
+def _is_coro(handler: Union[callable, object]):
+ """Check if handler is coroutine.
+
+ Class with async __call__ method is considered a coroutine.
+
+ :param handler: Function or callable object.
+ :type handler: callable, object
+ :raises: eeee.exceptions.NotCoroutineError
+ :return: None
+ """
+ try:
+ is_coro = iscoroutinefunction(handler) or iscoroutinefunction(handler.__call__)
+ except AttributeError:
+ is_coro = False
+
+ if not is_coro:
+ raise exceptions.NotCoroutineError
diff --git a/setup.cfg b/setup.cfg
index 69d3e1e..6fd7975 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -9,19 +9,23 @@ tag = True
[flake8]
max-line-length = 100
-max-complexity = 10
+max-complexity = 4
exclude =
- .git,
- .idea,
- .tox,
- __pycache__,
- docs,
- build,
- dist,
- node_modules,
+ .cache
.circleci
- *_backup.*,
- setup.py,
+ .git
+ .idea
+ .pytest_cache
+ .tox
+ __pycache__
+ docs
+ build
+ dist
+ htmlcov
+ node_modules
+ *.egg-info
+ *_backup.*
+ setup.py
[tool:pytest]
addopts = --verbose
diff --git a/tests/test_event.py b/tests/test_event.py
index b6d4016..b19725b 100644
--- a/tests/test_event.py
+++ b/tests/test_event.py
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
import unittest
-from eeee import Event
+from eeee import Event, exceptions
__author__ = 'Paweł Zadrożny'
__copyright__ = 'Copyright (c) 2018, Pawelzny'
@@ -41,3 +41,18 @@ def test_toggle(self):
def test_new_event_without_name(self):
event = Event()
self.assertEqual(event.name, 'Event')
+
+ def test_get_name_from_other_event(self):
+ optimus = Event('Optimus Prime')
+ clone = Event(optimus)
+
+ # name has been cloned
+ self.assertEqual(optimus.name, clone.name)
+
+ # but are not the same
+ self.assertNotEqual(optimus, clone)
+ self.assertIsNot(optimus, clone)
+
+ def test_raise_on_bad_naming(self):
+ with self.assertRaises(exceptions.NamingError):
+ Event({'name': 'test'})
diff --git a/tests/test_pub_sub.py b/tests/test_pub_sub.py
index 5cd1f95..718e962 100644
--- a/tests/test_pub_sub.py
+++ b/tests/test_pub_sub.py
@@ -150,7 +150,7 @@ def test_publish_to_empty_event(self):
with Loop(event.publish('enter the void')) as loop:
result = loop.run_until_complete()
- self.assertIsNone(result)
+ self.assertListEqual(result, [])
def test_publish_on_disabled_event(self):
event = Event('disabled')
diff --git a/tests/test_subscriber.py b/tests/test_subscriber.py
index dd95760..2f00218 100644
--- a/tests/test_subscriber.py
+++ b/tests/test_subscriber.py
@@ -47,7 +47,18 @@ def handler():
with self.assertRaises(exceptions.NotCoroutineError):
Subscriber(handler)
- def test_raise_handler_error(self):
+ def test_handler_of_wrong_type(self):
+ with self.assertRaises(exceptions.HandlerError):
+ Subscriber(tuple)
+
+ def test_not_callable_object_handler(self):
+ class NotCallable:
+ name = 'test'
+
+ with self.assertRaises(exceptions.HandlerError):
+ Subscriber(NotCallable())
+
+ def test_raise_not_error(self):
with self.assertRaises(exceptions.NotCallableError):
Subscriber('foo')
diff --git a/tests/test_utils.py b/tests/test_utils.py
new file mode 100644
index 0000000..55dee16
--- /dev/null
+++ b/tests/test_utils.py
@@ -0,0 +1,75 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+import unittest
+
+from eeee import exceptions
+from eeee.event import _is_coro, _is_callable, Subscriber, _parse_handler
+
+__author__ = 'Paweł Zadrożny'
+__copyright__ = 'Copyright (c) 2018, Pawelzny'
+
+
+class TestUtils(unittest.TestCase):
+ def test_is_coro(self):
+ async def i_am_coro():
+ pass
+
+ self.assertIsNone(_is_coro(i_am_coro))
+
+ def test_raises_is_not_coro(self):
+ def i_am_not_coro():
+ pass
+
+ with self.assertRaises(exceptions.NotCoroutineError):
+ _is_coro(i_am_not_coro)
+
+ def test_raises_on_not_callable_obj(self):
+ class NotCallable:
+ pass
+
+ with self.assertRaises(exceptions.NotCoroutineError):
+ _is_coro(NotCallable())
+
+ def test_is_callable(self):
+ async def i_am_callable():
+ pass
+
+ def i_am_callable_too():
+ pass
+
+ class IamCallable:
+ def __call__(self):
+ pass
+
+ self.assertIsNone(_is_callable(i_am_callable))
+ self.assertIsNone(_is_callable(i_am_callable_too))
+ self.assertIsNone(_is_callable(IamCallable()))
+
+ def test_parse_handler_sub(self):
+ async def some_coro():
+ pass
+
+ sub = Subscriber(some_coro)
+
+ name, handler = _parse_handler(sub)
+ self.assertEqual(name, 'some_coro')
+ self.assertIs(handler, some_coro)
+
+ def test_parse_handler_coro(self):
+ async def other_coro():
+ pass
+
+ name, handler = _parse_handler(other_coro)
+ self.assertEqual(name, 'other_coro')
+ self.assertIs(handler, other_coro)
+
+ def test_parse_handler_callable_obj(self):
+ class Coro:
+ def __call__(self):
+ pass
+
+ coro = Coro()
+
+ name, handler = _parse_handler(coro)
+ self.assertEqual(name, 'Coro')
+ self.assertIs(handler, coro)
diff --git a/tox.ini b/tox.ini
index eafd081..f8205d7 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,17 +1,13 @@
[tox]
-envlist =
- py35
- py36
- pypy35
- flake8
+envlist = py35,py36,pypy35,flake8
[testenv:flake8]
basepython = python3.6
commands =
pip install pipenv
- pipenv install
- pipenv install --dev
- pipenv run flake8 eeee tests
+ pipenv install --three
+ pipenv install --three --dev
+ pipenv run flake8
[testenv:py35]
basepython = python3.5
@@ -20,34 +16,20 @@ basepython = python3.5
basepython = python3.6
[testenv:pypy35]
-basepython = python
+basepython = pypy3
+commands =
+ pip install pipenv
+ pipenv install --python pypy3
+ pipenv install --python pypy3 --dev
+ pipenv run pytest
[testenv]
changedir = {toxinidir}
setenv =
- PYTHONPATH = {toxinidir}:{toxinidir}/eeee
+ PYTHONPATH = {toxinidir}:{toxinidir}
passenv = PYTHONPATH
commands =
pip install pipenv
- pipenv install
- pipenv install --dev
+ pipenv install --three
+ pipenv install --three --dev
pipenv run pytest
-
-[flake8]
-max-line-length = 100
-max-complexity = 8
-exclude =
- .git
- .cache
- .pytest_cache
- .circleci
- .idea
- .tox
- __pycache__
- docs
- build
- dist
- htmlcov
- node_modules,
- *.egg-info
- setup.py