diff --git a/.deepsource.toml b/.deepsource.toml new file mode 100644 index 00000000..52933f17 --- /dev/null +++ b/.deepsource.toml @@ -0,0 +1,12 @@ +version = 1 + +test_patterns = ["tests/**/test_*.py"] + +exclude_patterns = ["docs/**"] + +[[analyzers]] +name = "python" +enabled = true + + [analyzers.meta] + runtime_version = "3.x.x" diff --git a/CONTRIBUTORS.rst b/CONTRIBUTORS.rst index 54b3d582..23e26315 100644 --- a/CONTRIBUTORS.rst +++ b/CONTRIBUTORS.rst @@ -15,3 +15,4 @@ Dependency Injector Contributors + RĂ¼diger Busche (JarnoRFB) + Dmitry Rassoshenko (rda-dev) + Fotis Koutoupas (kootoopas) ++ Shubhendra Singh Chauhan (withshubh) diff --git a/README.rst b/README.rst index 6eba4ce6..f6f27bbc 100644 --- a/README.rst +++ b/README.rst @@ -155,6 +155,7 @@ Choose one of the following: - `Application example (single container) `_ - `Application example (multiple containers) `_ - `Decoupled packages example (multiple containers) `_ +- `Boto3 example `_ - `Django example `_ - `Flask example `_ - `Aiohttp example `_ diff --git a/docs/examples/boto3.rst b/docs/examples/boto3.rst new file mode 100644 index 00000000..8dc4bb6b --- /dev/null +++ b/docs/examples/boto3.rst @@ -0,0 +1,20 @@ +.. _boto3-example: + +Boto3 example +============= + +.. meta:: + :keywords: Python,Dependency Injection,Boto3,AWS,Amazon Web Services,S3,SQS,Rout53,EC2,Lambda,Example + :description: This example demonstrates a usage of Boto3 AWS client and Dependency Injector. + + +This example shows how to use ``Dependency Injector`` with `Boto3 `_. + +The source code is available on the `Github `_. + +Listing of ``boto3_session_example.py``: + +.. literalinclude:: ../../examples/miniapps/boto3-session/boto3_session_example.py + :language: python + +.. disqus:: diff --git a/docs/examples/index.rst b/docs/examples/index.rst index 6ec55c23..93595380 100644 --- a/docs/examples/index.rst +++ b/docs/examples/index.rst @@ -13,6 +13,7 @@ Explore the examples to see the ``Dependency Injector`` in action. application-single-container application-multiple-containers decoupled-packages + boto3 django flask flask-blueprints diff --git a/docs/introduction/di_in_python.rst b/docs/introduction/di_in_python.rst index 23ba547e..f19455e5 100644 --- a/docs/introduction/di_in_python.rst +++ b/docs/introduction/di_in_python.rst @@ -281,6 +281,7 @@ Choose one of the following as a next step: - :ref:`application-single-container` - :ref:`application-multiple-containers` - :ref:`decoupled-packages` + - :ref:`boto3` - :ref:`django-example` - :ref:`flask-example` - :ref:`flask-blueprints-example` diff --git a/docs/main/changelog.rst b/docs/main/changelog.rst index 3df4dcef..b2143e90 100644 --- a/docs/main/changelog.rst +++ b/docs/main/changelog.rst @@ -7,6 +7,22 @@ that were made in every particular version. From version 0.7.6 *Dependency Injector* framework strictly follows `Semantic versioning`_ +4.27.0 +------ +- Introduce wiring inspect filter to filter out ``flask.request`` and other local proxy objects + from the inspection. + See issue: `#408 `_. + Many thanks to `@bvanfleet `_ for reporting the issue and + help in finding the root cause. +- Add ``boto3`` example. +- Add tests for ``.as_float()`` modifier usage with wiring. +- Make refactoring of wiring module and tests. + See PR # `#406 `_. + Thanks to `@withshubh `_ for the contribution: + - Remove unused imports in tests. + - Use literal syntax to create data structure in tests. +- Add integration with a static analysis tool `DeepSource `_. + 4.26.0 ------ - Add wiring by string id. diff --git a/docs/wiring.rst b/docs/wiring.rst index 195f469e..d4e2d6a6 100644 --- a/docs/wiring.rst +++ b/docs/wiring.rst @@ -405,6 +405,7 @@ Take a look at other application examples: - :ref:`application-single-container` - :ref:`application-multiple-containers` - :ref:`decoupled-packages` +- :ref:`boto3` - :ref:`django-example` - :ref:`flask-example` - :ref:`flask-blueprints-example` diff --git a/examples/miniapps/boto3-session/README.rst b/examples/miniapps/boto3-session/README.rst new file mode 100644 index 00000000..4c66ef92 --- /dev/null +++ b/examples/miniapps/boto3-session/README.rst @@ -0,0 +1,14 @@ +Boto3 Session Example +===================== + +This is a `Boto3 `_ session + +`Dependency Injector `_ example. + +Run +--- + +To run the application do: + +.. code-block:: bash + + python boto3_session_example.py diff --git a/examples/miniapps/boto3-session/boto3_session_example.py b/examples/miniapps/boto3-session/boto3_session_example.py new file mode 100644 index 00000000..33277513 --- /dev/null +++ b/examples/miniapps/boto3-session/boto3_session_example.py @@ -0,0 +1,72 @@ +"""Boto3 session example.""" + +import boto3.session +from dependency_injector import containers, providers + + +class Service: + def __init__(self, s3_client, sqs_client): + self.s3_client = s3_client + self.sqs_client = sqs_client + + +class Container(containers.DeclarativeContainer): + + config = providers.Configuration() + + session = providers.Resource( + boto3.session.Session, + aws_access_key_id=config.aws_access_key_id, + aws_secret_access_key=config.aws_secret_access_key, + aws_session_token=config.aws_session_token, + ) + + s3_client = providers.Resource( + session.provided.client.call(), + service_name='s3', + ) + + sqs_client = providers.Resource( + providers.MethodCaller(session.provided.client), # Alternative syntax + service_name='sqs', + ) + + service1 = providers.Factory( + Service, + s3_client=s3_client, + sqs_client=sqs_client, + ) + + service2 = providers.Factory( + Service, + s3_client=session.provided.client.call(service_name='s3'), # Alternative inline syntax + sqs_client=session.provided.client.call(service_name='sqs'), # Alternative inline syntax + ) + + +def main(): + container = Container() + container.config.aws_access_key_id.from_env('AWS_ACCESS_KEY_ID') + container.config.aws_secret_access_key.from_env('AWS_SECRET_ACCESS_KEY') + container.config.aws_session_token.from_env('AWS_SESSION_TOKEN') + container.init_resources() + + s3_client = container.s3_client() + print(s3_client) + + sqs_client = container.sqs_client() + print(sqs_client) + + service1 = container.service1() + print(service1, service1.s3_client, service1.sqs_client) + assert service1.s3_client is s3_client + assert service1.sqs_client is sqs_client + + service2 = container.service1() + print(service2, service2.s3_client, service2.sqs_client) + assert service2.s3_client is s3_client + assert service2.sqs_client is sqs_client + + +if __name__ == '__main__': + main() diff --git a/src/dependency_injector/__init__.py b/src/dependency_injector/__init__.py index 46c5e9d0..3f784e9b 100644 --- a/src/dependency_injector/__init__.py +++ b/src/dependency_injector/__init__.py @@ -1,6 +1,6 @@ """Top-level package.""" -__version__ = '4.26.0' +__version__ = '4.27.0' """Version number. :type: str diff --git a/src/dependency_injector/wiring.py b/src/dependency_injector/wiring.py index c7debfdf..c5a591da 100644 --- a/src/dependency_injector/wiring.py +++ b/src/dependency_injector/wiring.py @@ -37,10 +37,21 @@ else: try: - from fastapi.params import Depends as FastAPIDepends - fastapi_installed = True + import fastapi.params except ImportError: - fastapi_installed = False + fastapi = None + + +try: + import starlette.requests +except ImportError: + starlette = None + + +try: + import werkzeug.local +except ImportError: + werkzeug = None from . import providers @@ -248,6 +259,28 @@ class ProvidersMap: return providers_map +class InspectFilter: + + def is_excluded(self, instance: object) -> bool: + if self._is_werkzeug_local_proxy(instance): + return True + elif self._is_starlette_request_cls(instance): + return True + else: + return False + + def _is_werkzeug_local_proxy(self, instance: object) -> bool: + return werkzeug and isinstance(instance, werkzeug.local.LocalProxy) + + def _is_starlette_request_cls(self, instance: object) -> bool: + return starlette \ + and isinstance(instance, type) \ + and issubclass(instance, starlette.requests.Request) + + +inspect_filter = InspectFilter() + + def wire( # noqa: C901 container: Container, *, @@ -269,6 +302,8 @@ def wire( # noqa: C901 for module in modules: for name, member in inspect.getmembers(module): + if inspect_filter.is_excluded(member): + continue if inspect.isfunction(member): _patch_fn(module, name, member, providers_map) elif inspect.isclass(member): @@ -531,7 +566,7 @@ def _is_fastapi_default_arg_injection(injection, kwargs): def _is_fastapi_depends(param: Any) -> bool: - return fastapi_installed and isinstance(param, FastAPIDepends) + return fastapi and isinstance(param, fastapi.params.Depends) def _is_patched(fn): diff --git a/tests/unit/providers/test_callables_py2_py3.py b/tests/unit/providers/test_callables_py2_py3.py index 0e0168fa..dcee8478 100644 --- a/tests/unit/providers/test_callables_py2_py3.py +++ b/tests/unit/providers/test_callables_py2_py3.py @@ -69,7 +69,7 @@ class CallableTests(unittest.TestCase): provider = providers.Callable(_example) \ .add_args(1, 2) \ .set_args(3, 4) - self.assertEqual(provider.args, tuple([3, 4])) + self.assertEqual(provider.args, (3, 4)) def test_set_kwargs(self): provider = providers.Callable(_example) \ diff --git a/tests/unit/providers/test_coroutines_py35.py b/tests/unit/providers/test_coroutines_py35.py index a3697044..b0dbf491 100644 --- a/tests/unit/providers/test_coroutines_py35.py +++ b/tests/unit/providers/test_coroutines_py35.py @@ -87,7 +87,7 @@ class CoroutineTests(AsyncTestCase): provider = providers.Coroutine(_example) \ .add_args(1, 2) \ .set_args(3, 4) - self.assertEqual(provider.args, tuple([3, 4])) + self.assertEqual(provider.args, (3, 4)) def test_set_kwargs(self): provider = providers.Coroutine(_example) \ diff --git a/tests/unit/providers/test_factories_py2_py3.py b/tests/unit/providers/test_factories_py2_py3.py index b6587859..edf7de07 100644 --- a/tests/unit/providers/test_factories_py2_py3.py +++ b/tests/unit/providers/test_factories_py2_py3.py @@ -228,7 +228,7 @@ class FactoryTests(unittest.TestCase): provider = providers.Factory(Example) \ .add_args(1, 2) \ .set_args(3, 4) - self.assertEqual(provider.args, tuple([3, 4])) + self.assertEqual(provider.args, (3, 4)) def test_set_kwargs(self): provider = providers.Factory(Example) \ diff --git a/tests/unit/providers/test_list_py2_py3.py b/tests/unit/providers/test_list_py2_py3.py index d11222cf..e303745b 100644 --- a/tests/unit/providers/test_list_py2_py3.py +++ b/tests/unit/providers/test_list_py2_py3.py @@ -42,7 +42,7 @@ class ListTests(unittest.TestCase): provider = providers.List() \ .add_args(1, 2) \ .set_args(3, 4) - self.assertEqual(provider.args, tuple([3, 4])) + self.assertEqual(provider.args, (3, 4)) def test_clear_args(self): provider = providers.List() \ diff --git a/tests/unit/providers/test_resource_py35.py b/tests/unit/providers/test_resource_py35.py index 4b818198..b0943968 100644 --- a/tests/unit/providers/test_resource_py35.py +++ b/tests/unit/providers/test_resource_py35.py @@ -203,7 +203,7 @@ class ResourceTests(unittest.TestCase): provider = providers.Resource(init_fn) \ .add_args(1, 2) \ .set_args(3, 4) - self.assertEqual(provider.args, tuple([3, 4])) + self.assertEqual(provider.args, (3, 4)) def test_clear_args(self): provider = providers.Resource(init_fn) \ diff --git a/tests/unit/providers/test_singletons_py2_py3.py b/tests/unit/providers/test_singletons_py2_py3.py index 6cc4c3c3..4e36f926 100644 --- a/tests/unit/providers/test_singletons_py2_py3.py +++ b/tests/unit/providers/test_singletons_py2_py3.py @@ -190,7 +190,7 @@ class _BaseSingletonTestCase(object): provider = self.singleton_cls(Example) \ .add_args(1, 2) \ .set_args(3, 4) - self.assertEqual(provider.args, tuple([3, 4])) + self.assertEqual(provider.args, (3, 4)) def test_set_kwargs(self): provider = self.singleton_cls(Example) \ diff --git a/tests/unit/samples/wiringflask/web.py b/tests/unit/samples/wiringflask/web.py new file mode 100644 index 00000000..59b5d004 --- /dev/null +++ b/tests/unit/samples/wiringflask/web.py @@ -0,0 +1,34 @@ +import sys + +from flask import Flask, jsonify, request, current_app, session, g +from flask import _request_ctx_stack, _app_ctx_stack +from dependency_injector import containers, providers +from dependency_injector.wiring import inject, Provide + +# This is here for testing wiring bypasses these objects without crashing +request, current_app, session, g # noqa +_request_ctx_stack, _app_ctx_stack # noqa + + +class Service: + def process(self) -> str: + return 'Ok' + + +class Container(containers.DeclarativeContainer): + + service = providers.Factory(Service) + + +app = Flask(__name__) + + +@app.route('/') +@inject +def index(service: Service = Provide[Container.service]): + result = service.process() + return jsonify({'result': result}) + + +container = Container() +container.wire(modules=[sys.modules[__name__]]) diff --git a/tests/unit/samples/wiringsamples/module.py b/tests/unit/samples/wiringsamples/module.py index 01031d2d..4c02b553 100644 --- a/tests/unit/samples/wiringsamples/module.py +++ b/tests/unit/samples/wiringsamples/module.py @@ -44,19 +44,23 @@ def test_function_provider(service_provider: Callable[..., Service] = Provider[C @inject def test_config_value( value_int: int = Provide[Container.config.a.b.c.as_int()], + value_float: float = Provide[Container.config.a.b.c.as_float()], value_str: str = Provide[Container.config.a.b.c.as_(str)], value_decimal: Decimal = Provide[Container.config.a.b.c.as_(Decimal)], value_required: str = Provide[Container.config.a.b.c.required()], value_required_int: int = Provide[Container.config.a.b.c.required().as_int()], + value_required_float: float = Provide[Container.config.a.b.c.required().as_float()], value_required_str: str = Provide[Container.config.a.b.c.required().as_(str)], value_required_decimal: str = Provide[Container.config.a.b.c.required().as_(Decimal)], ): return ( value_int, + value_float, value_str, value_decimal, value_required, value_required_int, + value_required_float, value_required_str, value_required_decimal, ) diff --git a/tests/unit/samples/wiringstringidssamples/module.py b/tests/unit/samples/wiringstringidssamples/module.py index f83a0a27..019e290b 100644 --- a/tests/unit/samples/wiringstringidssamples/module.py +++ b/tests/unit/samples/wiringstringidssamples/module.py @@ -3,7 +3,17 @@ from decimal import Decimal from typing import Callable -from dependency_injector.wiring import inject, Provide, Provider, as_int, as_, required, invariant, provided +from dependency_injector.wiring import ( + inject, + Provide, + Provider, + as_int, + as_float, + as_, + required, + invariant, + provided, +) from .container import Container from .service import Service @@ -44,19 +54,23 @@ def test_function_provider(service_provider: Callable[..., Service] = Provider[' @inject def test_config_value( value_int: int = Provide['config.a.b.c', as_int()], + value_float: float = Provide['config.a.b.c', as_float()], value_str: str = Provide['config.a.b.c', as_(str)], value_decimal: Decimal = Provide['config.a.b.c', as_(Decimal)], value_required: str = Provide['config.a.b.c', required()], value_required_int: int = Provide['config.a.b.c', required().as_int()], + value_required_float: float = Provide['config.a.b.c', required().as_float()], value_required_str: str = Provide['config.a.b.c', required().as_(str)], value_required_decimal: str = Provide['config.a.b.c', required().as_(Decimal)], ): return ( value_int, + value_float, value_str, value_decimal, value_required, value_required_int, + value_required_float, value_required_str, value_required_decimal, ) diff --git a/tests/unit/wiring/test_wiring_py36.py b/tests/unit/wiring/test_wiring_py36.py index 064aed9b..fd08799b 100644 --- a/tests/unit/wiring/test_wiring_py36.py +++ b/tests/unit/wiring/test_wiring_py36.py @@ -120,19 +120,23 @@ class WiringTest(unittest.TestCase): def test_configuration_option(self): ( value_int, + value_float, value_str, value_decimal, value_required, value_required_int, + value_required_float, value_required_str, value_required_decimal, ) = module.test_config_value() self.assertEqual(value_int, 10) + self.assertEqual(value_float, 10.0) self.assertEqual(value_str, '10') self.assertEqual(value_decimal, Decimal(10)) self.assertEqual(value_required, 10) self.assertEqual(value_required_int, 10) + self.assertEqual(value_required_float, 10.0) self.assertEqual(value_required_str, '10') self.assertEqual(value_required_decimal, Decimal(10)) diff --git a/tests/unit/wiring/test_wiring_string_ids_py36.py b/tests/unit/wiring/test_wiring_string_ids_py36.py index b4d41b3b..42002372 100644 --- a/tests/unit/wiring/test_wiring_string_ids_py36.py +++ b/tests/unit/wiring/test_wiring_string_ids_py36.py @@ -1,15 +1,10 @@ -import contextlib from decimal import Decimal -import importlib import unittest from dependency_injector.wiring import ( wire, Provide, - Closing, - register_loader_containers, - unregister_loader_containers, -) + Closing) from dependency_injector import errors # Runtime import to avoid syntax errors in samples on Python < 3.5 @@ -120,19 +115,23 @@ class WiringTest(unittest.TestCase): def test_configuration_option(self): ( value_int, + value_float, value_str, value_decimal, value_required, value_required_int, + value_required_float, value_required_str, value_required_decimal, ) = module.test_config_value() self.assertEqual(value_int, 10) + self.assertEqual(value_float, 10.0) self.assertEqual(value_str, '10') self.assertEqual(value_decimal, Decimal(10)) self.assertEqual(value_required, 10) self.assertEqual(value_required_int, 10) + self.assertEqual(value_required_float, 10.0) self.assertEqual(value_required_str, '10') self.assertEqual(value_required_decimal, Decimal(10)) diff --git a/tests/unit/wiring/test_wiringflask_py36.py b/tests/unit/wiring/test_wiringflask_py36.py new file mode 100644 index 00000000..1eaaa4d8 --- /dev/null +++ b/tests/unit/wiring/test_wiringflask_py36.py @@ -0,0 +1,33 @@ +import unittest + +# Runtime import to avoid syntax errors in samples on Python < 3.5 and reach top-dir +import os +_TOP_DIR = os.path.abspath( + os.path.sep.join(( + os.path.dirname(__file__), + '../', + )), +) +_SAMPLES_DIR = os.path.abspath( + os.path.sep.join(( + os.path.dirname(__file__), + '../samples/', + )), +) +import sys +sys.path.append(_TOP_DIR) +sys.path.append(_SAMPLES_DIR) + +from wiringflask import web + + +class WiringFlaskTest(unittest.TestCase): + + def test(self): + client = web.app.test_client() + + with web.app.app_context(): + response = client.get('/') + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data, b'{"result":"Ok"}\n')