diff --git a/.travis.yml b/.travis.yml index b21de242..bf64b222 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ dist: xenial language: python jobs: include: - - python: 3.6 + - python: 3.8 env: TOXENV=coveralls DEPENDENCY_INJECTOR_DEBUG_MODE=1 install: - pip install tox diff --git a/docs/api/aiohttpext.rst b/docs/api/aiohttpext.rst new file mode 100644 index 00000000..fa51411f --- /dev/null +++ b/docs/api/aiohttpext.rst @@ -0,0 +1,9 @@ +dependency_injector.ext.aiohttp +=============================== + +.. automodule:: dependency_injector.ext.aiohttp + :members: + :special-members: + + +.. disqus:: diff --git a/docs/api/index.rst b/docs/api/index.rst index 738ec7a0..8784d94b 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -8,4 +8,5 @@ API Documentation providers containers errors + aiohttpext flaskext diff --git a/docs/main/changelog.rst b/docs/main/changelog.rst index dd6f7fe8..79771859 100644 --- a/docs/main/changelog.rst +++ b/docs/main/changelog.rst @@ -7,6 +7,11 @@ that were made in every particular version. From version 0.7.6 *Dependency Injector* framework strictly follows `Semantic versioning`_ +3.24.0 +------ +- Add ``Aiohttp`` integration module ``dependency_injector.ext.aiohttp``. +- Add ``Aiohttp`` + ``Dependency Injector`` example ``giphynav-aiohttp``. + 3.23.2 ------ - Fix ``Flask`` tutorial code issues, typos and change some wording. @@ -48,7 +53,7 @@ follows `Semantic versioning`_ 3.20.0 ------ -- Add ``Flask`` integration module ``dependency_injector.flask.ext``. +- Add ``Flask`` integration module ``dependency_injector.ext.flask``. - Add ``Flask`` + ``Dependency Injector`` example ``ghnav-flask``. - Add ``Factory.provides`` attribute. It is an alias to the ``Factory.cls``. - New README. diff --git a/examples/miniapps/ghnav-flask/README.rst b/examples/miniapps/ghnav-flask/README.rst index ca6b3a2c..70d75185 100644 --- a/examples/miniapps/ghnav-flask/README.rst +++ b/examples/miniapps/ghnav-flask/README.rst @@ -68,7 +68,7 @@ After that visit http://127.0.0.1:5000/ in your browser. Test ---- -This application comes with unit tests. +This application comes with the unit tests. To run the tests do: diff --git a/examples/miniapps/giphynav-aiohttp/README.rst b/examples/miniapps/giphynav-aiohttp/README.rst new file mode 100644 index 00000000..d9dddaf6 --- /dev/null +++ b/examples/miniapps/giphynav-aiohttp/README.rst @@ -0,0 +1,117 @@ +Aiohttp Dependency Injection Example +==================================== + +Application ``giphynavigator`` is a `Aiohttp `_ + +`Dependency Injector `_ application. + +Run +--- + +Create virtual environment: + +.. code-block:: bash + + virtualenv venv + . venv/bin/activate + +Install requirements: + +.. code-block:: bash + + pip install -r requirements.txt + +To run the application do: + +.. code-block:: bash + + export GIPHY_API_KEY=wBJ2wZG7SRqfrU9nPgPiWvORmloDyuL0 + adev runserver giphynavigator/application.py --livereload + +The output should be something like: + +.. code-block:: + + [18:52:59] Starting aux server at http://localhost:8001 ◆ + [18:52:59] Starting dev server at http://localhost:8000 ● + +After that visit http://127.0.0.1:8000/ in your browser or use CLI command (``curl``, ``httpie``, +etc). You should see something like: + +.. code-block:: json + + { + "query": "Dependency Injector", + "limit": 10, + "gifs": [ + { + "url": "https://giphy.com/gifs/boxes-dependent-swbf2-6Eo7KzABxgJMY" + }, + { + "url": "https://giphy.com/gifs/depends-J56qCcOhk6hKE" + }, + { + "url": "https://giphy.com/gifs/web-series-ccstudios-bro-dependent-1lhU8KAVwmVVu" + }, + { + "url": "https://giphy.com/gifs/TheBoysTV-friends-friend-weneedeachother-XxR9qcIwcf5Jq404Sx" + }, + { + "url": "https://giphy.com/gifs/netflix-a-series-of-unfortunate-events-asoue-9rgeQXbwoK53pcxn7f" + }, + { + "url": "https://giphy.com/gifs/black-and-white-sad-skins-Hs4YzLs2zJuLu" + }, + { + "url": "https://giphy.com/gifs/always-there-for-you-i-am-here-PlayjhCco9jHBYrd9w" + }, + { + "url": "https://giphy.com/gifs/stream-famous-dollar-YT2dvOByEwXCdoYiA1" + }, + { + "url": "https://giphy.com/gifs/i-love-you-there-for-am-1BhGzgpZXYWwWMAGB1" + }, + { + "url": "https://giphy.com/gifs/life-like-twerk-9hlnWxjHqmH28" + } + ] + } + +.. note:: + + To create your own Giphy API key follow this + `guide `_. + +Test +---- + +This application comes with the unit tests. + +To run the tests do: + +.. code-block:: bash + + py.test giphynavigator/tests.py --cov=giphynavigator + +The output should be something like: + +.. code-block:: + + platform darwin -- Python 3.8.3, pytest-5.4.3, py-1.9.0, pluggy-0.13.1 + plugins: cov-2.10.0, aiohttp-0.3.0, asyncio-0.14.0 + collected 3 items + + giphynavigator/tests.py ... [100%] + + ---------- coverage: platform darwin, python 3.8.3-final-0 ----------- + Name Stmts Miss Cover + --------------------------------------------------- + giphynavigator/__init__.py 0 0 100% + giphynavigator/__main__.py 5 5 0% + giphynavigator/application.py 10 0 100% + giphynavigator/containers.py 10 0 100% + giphynavigator/giphy.py 16 11 31% + giphynavigator/services.py 9 1 89% + giphynavigator/tests.py 35 0 100% + giphynavigator/views.py 7 0 100% + --------------------------------------------------- + TOTAL 92 17 82% diff --git a/examples/miniapps/giphynav-aiohttp/config.yml b/examples/miniapps/giphynav-aiohttp/config.yml new file mode 100644 index 00000000..e1a0a14b --- /dev/null +++ b/examples/miniapps/giphynav-aiohttp/config.yml @@ -0,0 +1,5 @@ +giphy: + request_timeout: 10 +search: + default_query: "Dependency Injector" + default_limit: 10 diff --git a/examples/miniapps/giphynav-aiohttp/giphynavigator/__init__.py b/examples/miniapps/giphynav-aiohttp/giphynavigator/__init__.py new file mode 100644 index 00000000..1c744ca5 --- /dev/null +++ b/examples/miniapps/giphynav-aiohttp/giphynavigator/__init__.py @@ -0,0 +1 @@ +"""Top-level package.""" diff --git a/examples/miniapps/giphynav-aiohttp/giphynavigator/__main__.py b/examples/miniapps/giphynav-aiohttp/giphynavigator/__main__.py new file mode 100644 index 00000000..3ef0e377 --- /dev/null +++ b/examples/miniapps/giphynav-aiohttp/giphynavigator/__main__.py @@ -0,0 +1,10 @@ +"""Main module.""" + +from aiohttp import web + +from .application import create_app + + +if __name__ == '__main__': + app = create_app() + web.run_app(app) diff --git a/examples/miniapps/giphynav-aiohttp/giphynavigator/application.py b/examples/miniapps/giphynav-aiohttp/giphynavigator/application.py new file mode 100644 index 00000000..0a9b0e70 --- /dev/null +++ b/examples/miniapps/giphynav-aiohttp/giphynavigator/application.py @@ -0,0 +1,21 @@ +"""Application module.""" + +from aiohttp import web + +from .containers import ApplicationContainer + + +def create_app(): + """Create and return Flask application.""" + container = ApplicationContainer() + container.config.from_yaml('config.yml') + container.config.giphy.api_key.from_env('GIPHY_API_KEY') + + app: web.Application = container.app() + app.container = container + + app.add_routes([ + web.get('/', container.index_view.as_view()), + ]) + + return app diff --git a/examples/miniapps/giphynav-aiohttp/giphynavigator/containers.py b/examples/miniapps/giphynav-aiohttp/giphynavigator/containers.py new file mode 100644 index 00000000..c7ee8af3 --- /dev/null +++ b/examples/miniapps/giphynav-aiohttp/giphynavigator/containers.py @@ -0,0 +1,33 @@ +"""Application containers module.""" + +from dependency_injector import containers, providers +from dependency_injector.ext import aiohttp + +from aiohttp import web +from . import giphy, services, views + + +class ApplicationContainer(containers.DeclarativeContainer): + """Application container.""" + + app = aiohttp.Application(web.Application) + + config = providers.Configuration() + + giphy_client = providers.Factory( + giphy.GiphyClient, + api_key=config.giphy.api_key, + timeout=config.giphy.request_timeout, + ) + + search_service = providers.Factory( + services.SearchService, + giphy_client=giphy_client, + ) + + index_view = aiohttp.View( + views.index, + search_service=search_service, + default_query=config.search.default_query, + default_limit=config.search.default_limit, + ) diff --git a/examples/miniapps/giphynav-aiohttp/giphynavigator/giphy.py b/examples/miniapps/giphynav-aiohttp/giphynavigator/giphy.py new file mode 100644 index 00000000..6fdd8766 --- /dev/null +++ b/examples/miniapps/giphynav-aiohttp/giphynavigator/giphy.py @@ -0,0 +1,29 @@ +"""Giphy client module.""" + +from aiohttp import ClientSession, ClientTimeout + + +class GiphyClient: + + API_URL = 'http://api.giphy.com/v1' + + def __init__(self, api_key, timeout): + self._api_key = api_key + self._timeout = ClientTimeout(timeout) + + async def search(self, query, limit): + """Make search API call and return result.""" + if not query: + return [] + + url = f'{self.API_URL}/gifs/search' + params = { + 'q': query, + 'api_key': self._api_key, + 'limit': limit, + } + async with ClientSession(timeout=self._timeout) as session: + async with session.get(url, params=params) as response: + if response.status != 200: + response.raise_for_status() + return await response.json() diff --git a/examples/miniapps/giphynav-aiohttp/giphynavigator/services.py b/examples/miniapps/giphynav-aiohttp/giphynavigator/services.py new file mode 100644 index 00000000..1c86e0d7 --- /dev/null +++ b/examples/miniapps/giphynav-aiohttp/giphynavigator/services.py @@ -0,0 +1,18 @@ +"""Services module.""" + +from .giphy import GiphyClient + + +class SearchService: + + def __init__(self, giphy_client: GiphyClient): + self._giphy_client = giphy_client + + async def search(self, query, limit): + """Search for gifs and return formatted data.""" + if not query: + return [] + + result = await self._giphy_client.search(query, limit) + + return [{'url': gif['url']} for gif in result['data']] diff --git a/examples/miniapps/giphynav-aiohttp/giphynavigator/tests.py b/examples/miniapps/giphynav-aiohttp/giphynavigator/tests.py new file mode 100644 index 00000000..c34dfc10 --- /dev/null +++ b/examples/miniapps/giphynav-aiohttp/giphynavigator/tests.py @@ -0,0 +1,77 @@ +"""Tests module.""" + +from unittest import mock + +import pytest + +from giphynavigator.application import create_app +from giphynavigator.giphy import GiphyClient + + +@pytest.fixture +def app(): + return create_app() + + +@pytest.fixture +def client(app, aiohttp_client, loop): + return loop.run_until_complete(aiohttp_client(app)) + + +async def test_index(client, app): + giphy_client_mock = mock.AsyncMock(spec=GiphyClient) + giphy_client_mock.search.return_value = { + 'data': [ + {'url': 'https://giphy/gif1.gif'}, + {'url': 'https://giphy/gif2.gif'}, + ], + } + + with app.container.giphy_client.override(giphy_client_mock): + response = await client.get( + '/', + params={ + 'query': 'test', + 'limit': 10, + }, + ) + + assert response.status == 200 + data = await response.json() + assert data == { + 'query': 'test', + 'limit': 10, + 'gifs': [ + {'url': 'https://giphy/gif1.gif'}, + {'url': 'https://giphy/gif2.gif'}, + ], + } + + +async def test_index_no_data(client, app): + giphy_client_mock = mock.AsyncMock(spec=GiphyClient) + giphy_client_mock.search.return_value = { + 'data': [], + } + + with app.container.giphy_client.override(giphy_client_mock): + response = await client.get('/') + + assert response.status == 200 + data = await response.json() + assert data['gifs'] == [] + + +async def test_index_default_params(client, app): + giphy_client_mock = mock.AsyncMock(spec=GiphyClient) + giphy_client_mock.search.return_value = { + 'data': [], + } + + with app.container.giphy_client.override(giphy_client_mock): + response = await client.get('/') + + assert response.status == 200 + data = await response.json() + assert data['query'] == app.container.config.search.default_query() + assert data['limit'] == app.container.config.search.default_limit() diff --git a/examples/miniapps/giphynav-aiohttp/giphynavigator/views.py b/examples/miniapps/giphynav-aiohttp/giphynavigator/views.py new file mode 100644 index 00000000..7af5a0f5 --- /dev/null +++ b/examples/miniapps/giphynav-aiohttp/giphynavigator/views.py @@ -0,0 +1,25 @@ +"""Views module.""" + +from aiohttp import web + +from .services import SearchService + + +async def index( + request: web.Request, + search_service: SearchService, + default_query: str, + default_limit: int, +) -> web.Response: + query = request.query.get('query', default_query) + limit = int(request.query.get('limit', default_limit)) + + gifs = await search_service.search(query, limit) + + return web.json_response( + { + 'query': query, + 'limit': limit, + 'gifs': gifs, + }, + ) diff --git a/examples/miniapps/giphynav-aiohttp/requirements.txt b/examples/miniapps/giphynav-aiohttp/requirements.txt new file mode 100644 index 00000000..e849f0dc --- /dev/null +++ b/examples/miniapps/giphynav-aiohttp/requirements.txt @@ -0,0 +1,6 @@ +dependency-injector +aiohttp +aiohttp-devtools +pyyaml +pytest-aiohttp +pytest-cov diff --git a/requirements-ext.txt b/requirements-ext.txt index 7e106024..ec1b3858 100644 --- a/requirements-ext.txt +++ b/requirements-ext.txt @@ -1 +1,2 @@ flask +aiohttp diff --git a/setup.py b/setup.py index 2aa43a52..10e2653d 100644 --- a/setup.py +++ b/setup.py @@ -67,6 +67,9 @@ setup(name='dependency-injector', 'flask': [ 'flask', ], + 'aiohttp': [ + 'aiohttp', + ], }, zip_safe=True, license='BSD New', diff --git a/src/dependency_injector/__init__.py b/src/dependency_injector/__init__.py index 13e92e6e..4573132c 100644 --- a/src/dependency_injector/__init__.py +++ b/src/dependency_injector/__init__.py @@ -1,6 +1,6 @@ """Dependency injector top-level package.""" -__version__ = '3.23.2' +__version__ = '3.24.0' """Version number that follows semantic versioning. :type: str diff --git a/src/dependency_injector/ext/aiohttp.py b/src/dependency_injector/ext/aiohttp.py new file mode 100644 index 00000000..f42d95e4 --- /dev/null +++ b/src/dependency_injector/ext/aiohttp.py @@ -0,0 +1,46 @@ +"""Aiohttp extension module.""" + +from __future__ import absolute_import + +import functools + +from dependency_injector import providers + + +class Application(providers.Singleton): + """Aiohttp application provider.""" + + +class Extension(providers.Singleton): + """Aiohttp extension provider.""" + + +class Middleware(providers.DelegatedCallable): + """Aiohttp middleware provider.""" + + __middleware_version__ = 1 + + +class MiddlewareFactory(providers.Factory): + """Aiohttp middleware factory provider.""" + + +class View(providers.Callable): + """Aiohttp view provider.""" + + def as_view(self): + """Return aiohttp view function.""" + @functools.wraps(self.provides) + async def _view(request, *args, **kwargs): + return await self.__call__(request, *args, **kwargs) + return _view + + +class ClassBasedView(providers.Factory): + """Aiohttp class-based view provider.""" + + def as_view(self): + """Return aiohttp view function.""" + async def _view(request, *args, **kwargs): + return await self.__call__(request, *args, **kwargs) + return _view diff --git a/tests/unit/ext/test_aiohttp_py35.py b/tests/unit/ext/test_aiohttp_py35.py new file mode 100644 index 00000000..6f7b0205 --- /dev/null +++ b/tests/unit/ext/test_aiohttp_py35.py @@ -0,0 +1,93 @@ +"""Dependency injector Aiohttp extension unit tests.""" + +from aiohttp import web +from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop + +from dependency_injector import containers, providers +from dependency_injector.ext import aiohttp + + +async def index(_): + return web.Response(text='Hello World!') + + +async def test(_): + return web.Response(text='Test!') + + +class Test(web.View): + async def get(self): + return web.Response(text='Test class-based!') + + +@web.middleware +async def middleware(request, handler): + resp = await handler(request) + resp.text = resp.text + ' wink1' + return resp + + +def middleware_factory(text): + @web.middleware + async def sample_middleware(request, handler): + resp = await handler(request) + resp.text = resp.text + text + return resp + return sample_middleware + + +class ApplicationContainer(containers.DeclarativeContainer): + + app = aiohttp.Application( + web.Application, + middlewares=providers.List( + aiohttp.Middleware(middleware), + aiohttp.MiddlewareFactory(middleware_factory, text=' wink2'), + ), + ) + + index_view = aiohttp.View(index) + test_view = aiohttp.View(test) + test_class_view = aiohttp.ClassBasedView(Test) + + +class ApplicationTests(AioHTTPTestCase): + + async def get_application(self): + """ + Override the get_app method to return your application. + """ + container = ApplicationContainer() + app = container.app() + app.container = container + app.add_routes([ + web.get('/', container.index_view.as_view()), + web.get('/test', container.test_view.as_view(), name='test'), + web.get('/test-class', container.test_class_view.as_view()), + ]) + return app + + @unittest_run_loop + async def test_index(self): + response = await self.client.get('/') + + self.assertEqual(response.status, 200) + self.assertEqual(await response.text(), 'Hello World! wink2 wink1') + + @unittest_run_loop + async def test_test(self): + response = await self.client.get('/test') + + self.assertEqual(response.status, 200) + self.assertEqual(await response.text(), 'Test! wink2 wink1') + + @unittest_run_loop + async def test_test_class_based(self): + response = await self.client.get('/test-class') + + self.assertEqual(response.status, 200) + self.assertEqual(await response.text(), 'Test class-based! wink2 wink1') + + @unittest_run_loop + async def test_endpoints(self): + self.assertEqual(str(self.app.router['test'].url_for()), '/test') diff --git a/tests/unit/ext/test_flask_py2_py3.py b/tests/unit/ext/test_flask_py2_py3.py index 7b9695a4..49cf3d5f 100644 --- a/tests/unit/ext/test_flask_py2_py3.py +++ b/tests/unit/ext/test_flask_py2_py3.py @@ -4,7 +4,7 @@ import unittest2 as unittest from flask import Flask, url_for from flask.views import MethodView -from dependency_injector import containers, providers +from dependency_injector import containers from dependency_injector.ext import flask diff --git a/tests/unit/providers/test_coroutines_py3.py b/tests/unit/providers/test_coroutines_py35.py similarity index 81% rename from tests/unit/providers/test_coroutines_py3.py rename to tests/unit/providers/test_coroutines_py35.py index e970b2ee..c9109f26 100644 --- a/tests/unit/providers/test_coroutines_py3.py +++ b/tests/unit/providers/test_coroutines_py35.py @@ -1,6 +1,9 @@ """Dependency injector coroutine providers unit tests.""" import asyncio +import contextlib +import sys +import gc import unittest2 as unittest @@ -10,20 +13,65 @@ from dependency_injector import ( ) -@asyncio.coroutine -def _example(arg1, arg2, arg3, arg4): +async def _example(arg1, arg2, arg3, arg4): future = asyncio.Future() future.set_result(None) - yield from future + await future return arg1, arg2, arg3, arg4 -def _run(coro): +def run(main): loop = asyncio.get_event_loop() - return loop.run_until_complete(coro) + return loop.run_until_complete(main) -class CoroutineTests(unittest.TestCase): +def setup_test_loop( + loop_factory=asyncio.new_event_loop +) -> asyncio.AbstractEventLoop: + loop = loop_factory() + try: + module = loop.__class__.__module__ + skip_watcher = 'uvloop' in module + except AttributeError: # pragma: no cover + # Just in case + skip_watcher = True + asyncio.set_event_loop(loop) + if sys.platform != "win32" and not skip_watcher: + policy = asyncio.get_event_loop_policy() + watcher = asyncio.SafeChildWatcher() # type: ignore + watcher.attach_loop(loop) + with contextlib.suppress(NotImplementedError): + policy.set_child_watcher(watcher) + return loop + + +def teardown_test_loop(loop: asyncio.AbstractEventLoop, + fast: bool=False) -> None: + closed = loop.is_closed() + if not closed: + loop.call_soon(loop.stop) + loop.run_forever() + loop.close() + + if not fast: + gc.collect() + + asyncio.set_event_loop(None) + + +class AsyncTestCase(unittest.TestCase): + + def setUp(self): + self.loop = setup_test_loop() + + def tearDown(self): + teardown_test_loop(self.loop) + + def _run(self, f): + return self.loop.run_until_complete(f) + + +class CoroutineTests(AsyncTestCase): def test_init_with_coroutine(self): self.assertTrue(providers.Coroutine(_example)) @@ -33,34 +81,34 @@ class CoroutineTests(unittest.TestCase): def test_call_with_positional_args(self): provider = providers.Coroutine(_example, 1, 2, 3, 4) - self.assertTupleEqual(_run(provider()), (1, 2, 3, 4)) + self.assertTupleEqual(self._run(provider()), (1, 2, 3, 4)) def test_call_with_keyword_args(self): provider = providers.Coroutine(_example, arg1=1, arg2=2, arg3=3, arg4=4) - self.assertTupleEqual(_run(provider()), (1, 2, 3, 4)) + self.assertTupleEqual(self._run(provider()), (1, 2, 3, 4)) def test_call_with_positional_and_keyword_args(self): provider = providers.Coroutine(_example, 1, 2, arg3=3, arg4=4) - self.assertTupleEqual(_run(provider()), (1, 2, 3, 4)) + self.assertTupleEqual(run(provider()), (1, 2, 3, 4)) def test_call_with_context_args(self): provider = providers.Coroutine(_example, 1, 2) - self.assertTupleEqual(_run(provider(3, 4)), (1, 2, 3, 4)) + self.assertTupleEqual(self._run(provider(3, 4)), (1, 2, 3, 4)) def test_call_with_context_kwargs(self): provider = providers.Coroutine(_example, arg1=1) self.assertTupleEqual( - _run(provider(arg2=2, arg3=3, arg4=4)), + self._run(provider(arg2=2, arg3=3, arg4=4)), (1, 2, 3, 4), ) def test_call_with_context_args_and_kwargs(self): provider = providers.Coroutine(_example, 1) self.assertTupleEqual( - _run(provider(2, arg3=3, arg4=4)), + self._run(provider(2, arg3=3, arg4=4)), (1, 2, 3, 4), ) @@ -69,7 +117,7 @@ class CoroutineTests(unittest.TestCase): .add_args(1, 2) \ .add_kwargs(arg3=3, arg4=4) - self.assertTupleEqual(_run(provider()), (1, 2, 3, 4)) + self.assertTupleEqual(self._run(provider()), (1, 2, 3, 4)) def test_set_args(self): provider = providers.Coroutine(_example) \ @@ -213,7 +261,7 @@ class DelegatedCoroutineTests(unittest.TestCase): hex(id(provider)))) -class AbstractCoroutineTests(unittest.TestCase): +class AbstractCoroutineTests(AsyncTestCase): def test_inheritance(self): self.assertIsInstance(providers.AbstractCoroutine(_example), @@ -227,7 +275,7 @@ class AbstractCoroutineTests(unittest.TestCase): provider = providers.AbstractCoroutine(_abstract_example) provider.override(providers.Coroutine(_example)) - self.assertTrue(_run(provider(1, 2, 3, 4)), (1, 2, 3, 4)) + self.assertTrue(self._run(provider(1, 2, 3, 4)), (1, 2, 3, 4)) def test_call_overridden_by_delegated_coroutine(self): @asyncio.coroutine @@ -237,7 +285,7 @@ class AbstractCoroutineTests(unittest.TestCase): provider = providers.AbstractCoroutine(_abstract_example) provider.override(providers.DelegatedCoroutine(_example)) - self.assertTrue(_run(provider(1, 2, 3, 4)), (1, 2, 3, 4)) + self.assertTrue(self._run(provider(1, 2, 3, 4)), (1, 2, 3, 4)) def test_call_not_overridden(self): provider = providers.AbstractCoroutine(_example) diff --git a/tox.ini b/tox.ini index 61d6a098..e494554a 100644 --- a/tox.ini +++ b/tox.ini @@ -8,12 +8,13 @@ deps= extras= yaml flask + aiohttp commands= - unit2 discover -s tests/unit -p test_*_py3.py + unit2 discover -s tests/unit -p test_*_py3*.py [testenv:coveralls] passenv=TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH DEPENDENCY_INJECTOR_DEBUG_MODE -basepython=python3.6 +basepython=python3.8 usedevelop=True deps= {[testenv]deps} @@ -22,19 +23,27 @@ deps= coveralls commands= coverage erase - coverage run --rcfile=./.coveragerc -m unittest2 discover -s tests/unit/ -p test_*_py3.py + coverage run --rcfile=./.coveragerc -m unittest2 discover -s tests/unit/ -p test_*_py3*.py coverage report --rcfile=./.coveragerc coveralls [testenv:py27] +extras= + yaml + flask commands= unit2 discover -s tests/unit -p test_*_py2_py3.py [testenv:py34] +commands= + unit2 discover -s tests/unit -p test_*_py3.py extras= flask [testenv:pypy] +extras= + yaml + flask commands= unit2 discover -s tests/unit -p test_*_py2_py3.py