diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..0157630 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- + +[run] +branch = True +source = arq + +[paths] +source = + src/arq + .tox/*/lib/python*/site-packages/arq + .tox/pypy*/site-packages/arq + .tox/*/site-packages/arq diff --git a/.gitignore b/.gitignore index 2f836aa..ad79e0f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,7 @@ *~ *.pyc +**/*.egg-info/ +/.cache/ +/.coverage +/.tox/ +/htmlcov/ diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..b71bf81 --- /dev/null +++ b/setup.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- + + +from setuptools import ( + find_packages, + setup, +) + + +def readfile(path): + with open(path, 'rb') as stream: + return stream.read().decode('utf-8') + + +readme = readfile('README.rst') + + +setup( + name='arq', + url='https://github.com/AndreLouisCaron/arq.py', + license='MIT', + author='Andre Caron', + author_email='andre.l.caron@gmail.com', + description='ARQ implementations in Python, using Gevent', + long_description=readme, + keywords='arq udp gevent', + version='0.0.0', + packages=find_packages(where='src'), + package_dir={ + '': 'src', + }, +) diff --git a/src/arq/__init__.py b/src/arq/__init__.py new file mode 100644 index 0000000..9adb42d --- /dev/null +++ b/src/arq/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + + +__all__ = ( +) diff --git a/src/arq/_utils.py b/src/arq/_utils.py new file mode 100644 index 0000000..b68e5d7 --- /dev/null +++ b/src/arq/_utils.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- + + +import gevent +import gevent.socket +import socket + +from contextlib import ( + closing, + contextmanager, +) + + +@contextmanager +def udpsocket(host='127.0.0.1', port=0): + """Create, bind and automtically close a UDP socket.""" + s = gevent.socket.socket( + socket.AF_INET, + socket.SOCK_DGRAM, + socket.IPPROTO_UDP, + ) + with closing(s): + s.bind((host, port)) + yield s + + +@contextmanager +def concurrently(function, *args, **kwds): + """Create, schedule and automatically cancel and join a task.""" + task = gevent.spawn(function, *args, **kwds) + try: + yield task + finally: + task.kill() + + +__all__ = ( + 'concurrently', + 'udpsocket', +) diff --git a/src/arq/testing.py b/src/arq/testing.py new file mode 100644 index 0000000..a9490f8 --- /dev/null +++ b/src/arq/testing.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- + + +from ._utils import ( + concurrently, + udpsocket, +) + + +__all__ = ( + 'concurrently', + 'udpsocket', +) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..e24867b --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- + + +import pytest + +from contextlib2 import ExitStack + + +@pytest.fixture(scope='function') +def stack(): + """Provide a cleanup stack to use in the test (without indentation).""" + with ExitStack() as stack: + yield stack diff --git a/tests/requirements.in b/tests/requirements.in new file mode 100644 index 0000000..e6ae260 --- /dev/null +++ b/tests/requirements.in @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- + +contextlib2==0.5.5 +coverage==4.4.2 +flake8==3.5.0 +gevent==1.2.2 +pytest==3.3.1 \ No newline at end of file diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 0000000..bf42545 --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,19 @@ +# +# This file is autogenerated by pip-compile +# To update, run: +# +# pip-compile --output-file tests/requirements.txt tests/requirements.in +# +attrs==17.3.0 # via pytest +contextlib2==0.5.5 +coverage==4.4.2 +flake8==3.5.0 +gevent==1.2.2 +greenlet==0.4.12 # via gevent +mccabe==0.6.1 # via flake8 +pluggy==0.6.0 # via pytest +py==1.5.2 # via pytest +pycodestyle==2.3.1 # via flake8 +pyflakes==1.6.0 # via flake8 +pytest==3.3.1 +six==1.11.0 # via pytest diff --git a/tests/test_testing.py b/tests/test_testing.py new file mode 100644 index 0000000..fdc6348 --- /dev/null +++ b/tests/test_testing.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- + + +import os + +from arq.testing import ( + concurrently, + udpsocket, +) + + +def test_concurrent_udp_peers(stack): + """Tests can run a UDP client-server pair in parallel.""" + + def echo(socket): + """UDP echo service.""" + while True: + data, peer = socket.recvfrom(1024) + socket.sendto(data, peer) + + server = stack.enter_context(udpsocket()) + client = stack.enter_context(udpsocket()) + with concurrently(echo, server): + peer = server.getsockname() + for i in range(100): + data = os.urandom(3) + client.sendto(data, peer) + d, p = client.recvfrom(1024) + assert p == peer + assert d == data diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..596f5b7 --- /dev/null +++ b/tox.ini @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- + +[tox] +envlist = + py36 + py35 + py27 + +[testenv] +deps = + -rtests/requirements.txt +passenv = + * +commands = + python setup.py check + flake8 src/arq/ tests/ + coverage erase + coverage run -m pytest -s -vv tests/ + coverage html + coverage report --fail-under=100 + + +[testenv:deps] +deps = + pip-tools==1.11.0 +commands = + pip-compile -o tests/requirements.txt tests/requirements.in