Skip to content

Commit

Permalink
documentation, docstrings, frew tests
Browse files Browse the repository at this point in the history
  • Loading branch information
pawelzny committed Mar 17, 2018
1 parent e39b6c9 commit 42c73b9
Show file tree
Hide file tree
Showing 9 changed files with 246 additions and 10 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -184,3 +184,4 @@ fabric.properties

# custon
*_backup.*
/.pytest_cache/
2 changes: 1 addition & 1 deletion Makefile
Expand Up @@ -33,7 +33,7 @@ test-all: ## run tests on every Python version with tox

coverage: ## check code coverage quickly with the default Python
rm -rf htmlcov
pipenv run coverage run --source eeee -m pytest
pipenv run coverage run --source cl -m pytest
pipenv run coverage report -m
pipenv run coverage html

Expand Down
6 changes: 2 additions & 4 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 21 additions & 3 deletions README.rst
Expand Up @@ -9,7 +9,12 @@ Context loop
Features
========

*
* Work with sync and async frameworks
* Schedule tasks to existing loop or create new one
* No need to understand how async works
* No callbacks required
* Run async tasks whenever and wherever you want


Installation
============
Expand All @@ -22,10 +27,23 @@ Installation
**Package**: https://pypi.org/project/context-loop/


Documentation
=============

Read full documentation at http://context-loop.readthedocs.io/en/stable/


Quick Example
=============

.. code:: python
import cl.Loop
>>> async def coro():
... return await something_from_future()
...
>>> import cl.Loop
>>> with cl.Loop(coro(), coro(), coro()) as loop:
... result = loop.run_until_complete()
...
>>> result
['success', 'success', 'success']
3 changes: 2 additions & 1 deletion cl/__init__.py
@@ -1,7 +1,8 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from cl.loop import Loop

__author__ = 'Paweł Zadrożny'
__copyright__ = 'Copyright (c) 2018, Pawelzny'
__version__ = '0.0.0'
__all__ = []
__all__ = ['Loop']
166 changes: 166 additions & 0 deletions cl/loop.py
@@ -0,0 +1,166 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import asyncio
from typing import Union

__author__ = 'Paweł Zadrożny'
__copyright__ = 'Copyright (c) 2018, Pawelzny'


class Loop:
"""Asyncio Event loop context manager.
Context manager which get existing event loop or if none exist
will create new one.
All coroutines are converted to task and scheduled to execute in near future.
Scheduling is safe for long running tasks.
:Example:
Create coroutine using `@asyncio.coroutine` decorator or
with async/await syntax
.. code-block:: python
>>> async def wait_for_it(timeout):
... await asyncio.sleep(timeout)
... return 'success sleep for {} seconds'.format(timeout)
...
Use context manager to get result from one or more coroutines
.. code-block:: python
>>> with Loop(wait_for_it(5), wait_for_it(3), return_exceptions=True) as loop:
... result = loop.run_until_complete()
...
>>> result
['success sleep for 3 seconds', 'success sleep for 5 seconds']
:param futures: One or more coroutine or future.
:type futures: asyncio.Future, asyncio.coroutine
:param loop: Optional existing loop.
:type loop: asyncio.AbstractEventLoop
:param return_exceptions: If True will return exceptions as result.
:type return_exceptions: Boolean
:param stop_when_done: If True will close the loop on context exit.
:type stop_when_done: Boolean
"""

futures = None
"""Gathered futures."""

def __init__(self, *futures, loop=None, return_exceptions=False, stop_when_done=False):
self.loop = self.get_event_loop(loop)
self.return_exceptions = return_exceptions
self.stop_when_done = stop_when_done
if futures:
self.gather(futures)

def __enter__(self):
return self

def __exit__(self, exc_type, exc_val, exc_tb):
if self.stop_when_done:
self.stop()

@staticmethod
def get_event_loop(loop: asyncio.AbstractEventLoop = None) -> asyncio.AbstractEventLoop:
"""Get existing loop or create new one.
:param loop: Optional, already existing loop.
:type loop: asyncio.AbstractEventLoop
:return: Asyncio loop
:rtype: asyncio.AbstractEventLoop
"""

return loop if isinstance(loop, asyncio.AbstractEventLoop) else asyncio.get_event_loop()

def gather(self, *futures: Union[asyncio.Future, asyncio.coroutine]):
"""Gather list of futures / coros into group of asyncio.Task.
:Example:
Prepare all futures to execution
.. code-block:: python
>>> async def do_something():
... return 'something'
...
>>> async def do_something_else():
... return 'something_else'
...
Gather all tasks and then pass to context loop
.. code-block:: python
>>> loop = Loop(return_exceptions=True)
>>> loop.gather(do_something(), do_something_else())
>>> with loop as l:
... result = l.run_until_complete()
...
>>> result
['something', 'something_else']
:param futures: One or more coroutine or future.
:type futures: asyncio.Future, asyncio.coroutine
:return: Futures grouped into single future
:rtype: asyncio.Task, asyncio.Future
"""

if len(futures) > 1:
self.futures = asyncio.gather(*futures, loop=self.loop,
return_exceptions=self.return_exceptions)
else:
self.futures = asyncio.ensure_future(futures[0], loop=self.loop)

def run_until_complete(self):
"""Run loop until all futures are done.
:return: Single or list of results from all scheduled futures.
:rtype: list, Any
"""

return self.loop.run_until_complete(self.futures)

def stop(self):
"""Close loop when no other tasks are scheduled."""

self.loop.stop()

def is_running(self):
"""Check if loop is still running.
:return: Boolean
"""

return self.loop.is_running()

def is_closed(self):
"""Check if loop has been closed.
:return: Boolean
"""

return self.loop.is_closed()

def close(self):
"""Cancel all scheduled tasks and close loop immediately."""

self.loop.close()

def cancel(self):
"""Cancel futures execution.
If futures are already done will return False, otherwise will return True
:return: Cancellation status.
:rtype: Boolean
"""

return self.futures.cancel()
9 changes: 9 additions & 0 deletions docs/api.rst
@@ -0,0 +1,9 @@
==========
Public API
==========


.. py:module:: cl.loop
.. autoclass:: Loop
:member-order: bysource
:members:
3 changes: 2 additions & 1 deletion docs/index.rst
Expand Up @@ -8,7 +8,7 @@ Context loop (cl)
=================

Simple context manager utility for asyncio event loop.
CL Helps with async pieces of code to be scheduled and run within synchronous code.
Context loop helps with async pieces of code to be scheduled and run within synchronous code.

Can be used with synchronous and asynchronous frameworks like Django, Flask
or Tornado and Twisted.
Expand All @@ -18,6 +18,7 @@ or Tornado and Twisted.
:caption: Contents:

context-loop
api
authors
contributing

Expand Down
42 changes: 42 additions & 0 deletions tests/test_loop.py
@@ -0,0 +1,42 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import unittest
import asyncio
from cl import Loop

__author__ = 'Paweł Zadrożny'
__copyright__ = 'Copyright (c) 2018, Pawelzny'


async def wait_for_it(timeout):
await asyncio.sleep(timeout)
return 'success with timeout {}'.format(timeout)


class TestLoop(unittest.TestCase):
def tearDown(self):
asyncio.get_event_loop().stop()

def test_get_event_loop(self):
loop = Loop.get_event_loop()
self.assertIsInstance(loop, asyncio.AbstractEventLoop)

def test_get_existing_event_loop(self):
existing_loop = asyncio.get_event_loop()
loop = Loop.get_event_loop(existing_loop)

self.assertIs(loop, existing_loop)

def test_gather_futures(self):
loop = Loop()
self.assertIsNone(loop.futures)

loop.gather(wait_for_it(0.1), wait_for_it(0.1))
self.assertIsInstance(loop.futures, asyncio.Future)

def test_gather_single_future(self):
loop = Loop()
self.assertIsNone(loop.futures)

loop.gather(wait_for_it(0.1))
self.assertIsInstance(loop.futures, asyncio.Task)

0 comments on commit 42c73b9

Please sign in to comment.