diff --git a/dependency_injector/containers.py b/dependency_injector/containers.py index 858d8e1a..6c6d690d 100644 --- a/dependency_injector/containers.py +++ b/dependency_injector/containers.py @@ -2,7 +2,10 @@ import six -from dependency_injector import utils +from dependency_injector import ( + utils, + errors, +) class DeclarativeContainerMetaClass(type): @@ -21,6 +24,7 @@ class DeclarativeContainerMetaClass(type): attributes['cls_providers'] = dict(cls_providers) attributes['inherited_providers'] = dict(inherited_providers) + attributes['providers'] = dict(cls_providers + inherited_providers) return type.__new__(mcs, class_name, bases, attributes) @@ -32,62 +36,69 @@ class DeclarativeContainerMetaClass(type): """ if utils.is_provider(value): cls.providers[name] = value + cls.cls_providers[name] = value super(DeclarativeContainerMetaClass, cls).__setattr__(name, value) + def __delattr__(cls, name): + """Delete class attribute. -class Container(object): - """Inversion of control container.""" - - __IS_CATALOG__ = True - - def __init__(self): - """Initializer.""" - self.providers = dict() - - def bind_providers(self, **providers): - """Bind providers to the container.""" - for name, provider in six.iteritems(providers): - setattr(self, name, utils.ensure_is_provider(provider)) - return self - - def __setattr__(self, name, value): - """Set instance attribute. - - If value of attribute is provider, it will be added into providers + If value of attribute is provider, it will be deleted from providers dictionary. """ - if utils.is_provider(value): - self.providers[name] = value - super(Container, self).__setattr__(name, value) + if name in cls.providers and name in cls.cls_providers: + del cls.providers[name] + del cls.cls_providers[name] + super(DeclarativeContainerMetaClass, cls).__delattr__(name) @six.add_metaclass(DeclarativeContainerMetaClass) class DeclarativeContainer(object): """Declarative inversion of control container.""" + __IS_CATALOG__ = True + + providers = dict() cls_providers = dict() inherited_providers = dict() - def __init__(self): - """Initializer.""" - self.providers = dict() - self.providers.update(self.__class__.inherited_providers) - self.providers.update(self.__class__.cls_providers) - super(DeclarativeContainer, self).__init__() + overridden_by = tuple() + + @classmethod + def override(cls, overriding): + """Override current container by overriding container. + + :param overriding: Overriding container. + :type overriding: :py:class:`DeclarativeContainer` + + :raise: :py:exc:`dependency_injector.errors.Error` if trying to + override container by itself or its subclasses + + :rtype: None + """ + if issubclass(cls, overriding): + raise errors.Error('Catalog {0} could not be overridden ' + 'with itself or its subclasses'.format(cls)) + + cls.overridden_by += (overriding,) + + for name, provider in six.iteritems(overriding.cls_providers): + try: + getattr(cls, name).override(provider) + except AttributeError: + pass -def override(declarative_container): +def override(container): """:py:class:`DeclarativeContainer` overriding decorator. - :param declarative_container: Container that should be overridden by - decorated container. - :type declarative_container: :py:class:`DeclarativeContainer` + :param catalog: Container that should be overridden by decorated container. + :type catalog: :py:class:`DeclarativeContainer` :return: Declarative container's overriding decorator. :rtype: callable(:py:class:`DeclarativeContainer`) """ def decorator(overriding_container): """Overriding decorator.""" - declarative_container.override(overriding_container) + container.override(overriding_container) return overriding_container return decorator diff --git a/examples/miniapps/movie_lister/app_csv.py b/examples/miniapps/movie_lister/app_csv.py index f56dec68..64a3866f 100644 --- a/examples/miniapps/movie_lister/app_csv.py +++ b/examples/miniapps/movie_lister/app_csv.py @@ -9,7 +9,7 @@ This mini application uses ``movies`` library, that is configured to work with csv file movies database. """ -from dependency_injector import catalogs +from dependency_injector import containers from dependency_injector import providers from dependency_injector import injections @@ -19,12 +19,12 @@ from movies import finders from settings import MOVIES_CSV_PATH -@catalogs.override(MoviesModule) -class MyMoviesModule(catalogs.DeclarativeCatalog): - """Customized catalog of movies module component providers.""" +@containers.override(MoviesModule) +class MyMoviesModule(containers.DeclarativeContainer): + """IoC container for overriding movies module component providers.""" movie_finder = providers.Factory(finders.CsvMovieFinder, - *MoviesModule.movie_finder.injections, + movie_model=MoviesModule.movie_model, csv_file=MOVIES_CSV_PATH, delimeter=',') diff --git a/examples/miniapps/movie_lister/app_db.py b/examples/miniapps/movie_lister/app_db.py index f9e614fc..5e62676b 100644 --- a/examples/miniapps/movie_lister/app_db.py +++ b/examples/miniapps/movie_lister/app_db.py @@ -11,7 +11,7 @@ sqlite movies database. import sqlite3 -from dependency_injector import catalogs +from dependency_injector import containers from dependency_injector import providers from dependency_injector import injections @@ -21,18 +21,18 @@ from movies import finders from settings import MOVIES_DB_PATH -class ApplicationModule(catalogs.DeclarativeCatalog): - """Catalog of application component providers.""" +class ApplicationModule(containers.DeclarativeContainer): + """IoC container of application component providers.""" database = providers.Singleton(sqlite3.connect, MOVIES_DB_PATH) -@catalogs.override(MoviesModule) -class MyMoviesModule(catalogs.DeclarativeCatalog): - """Customized catalog of movies module component providers.""" +@containers.override(MoviesModule) +class MyMoviesModule(containers.DeclarativeContainer): + """IoC container for overriding movies module component providers.""" movie_finder = providers.Factory(finders.SqliteMovieFinder, - *MoviesModule.movie_finder.injections, + movie_model=MoviesModule.movie_model, database=ApplicationModule.database) diff --git a/examples/miniapps/movie_lister/app_db_csv.py b/examples/miniapps/movie_lister/app_db_csv.py index 3608b97e..83d2739f 100644 --- a/examples/miniapps/movie_lister/app_db_csv.py +++ b/examples/miniapps/movie_lister/app_db_csv.py @@ -11,7 +11,7 @@ sqlite movies database and csv file movies database. import sqlite3 -from dependency_injector import catalogs +from dependency_injector import containers from dependency_injector import providers from dependency_injector import injections @@ -22,27 +22,27 @@ from settings import MOVIES_CSV_PATH from settings import MOVIES_DB_PATH -class ApplicationModule(catalogs.DeclarativeCatalog): - """Catalog of application component providers.""" +class ApplicationModule(containers.DeclarativeContainer): + """IoC container of application component providers.""" database = providers.Singleton(sqlite3.connect, MOVIES_DB_PATH) -@catalogs.copy(MoviesModule) +@containers.copy(MoviesModule) class DbMoviesModule(MoviesModule): - """Customized catalog of movies module component providers.""" + """IoC container for overriding movies module component providers.""" movie_finder = providers.Factory(finders.SqliteMovieFinder, - *MoviesModule.movie_finder.injections, + movie_model=MoviesModule.movie_model, database=ApplicationModule.database) -@catalogs.copy(MoviesModule) +@containers.copy(MoviesModule) class CsvMoviesModule(MoviesModule): - """Customized catalog of movies module component providers.""" + """IoC container for overriding movies module component providers.""" movie_finder = providers.Factory(finders.CsvMovieFinder, - *MoviesModule.movie_finder.injections, + movie_model=MoviesModule.movie_model, csv_file=MOVIES_CSV_PATH, delimeter=',') diff --git a/examples/miniapps/movie_lister/movies/__init__.py b/examples/miniapps/movie_lister/movies/__init__.py index 360c3979..38d11157 100644 --- a/examples/miniapps/movie_lister/movies/__init__.py +++ b/examples/miniapps/movie_lister/movies/__init__.py @@ -1,9 +1,9 @@ """Movies package. -Top-level package of movies library. This package contains catalog of movies -module component providers - ``MoviesModule``. It is recommended to use movies -library functionality by fetching required instances from ``MoviesModule`` -providers. +Top-level package of movies library. This package contains IoC container of +movies module component providers - ``MoviesModule``. It is recommended to use +movies library functionality by fetching required instances from +``MoviesModule`` providers. ``MoviesModule.movie_finder`` is a factory that provides abstract component ``finders.MovieFinder``. This provider should be overridden by provider of @@ -12,7 +12,7 @@ concrete finder implementation in terms of library configuration. Each of ``MoviesModule`` providers could be overridden. """ -from dependency_injector import catalogs +from dependency_injector import containers from dependency_injector import providers from . import finders @@ -20,8 +20,8 @@ from . import listers from . import models -class MoviesModule(catalogs.DeclarativeCatalog): - """Catalog of movies module component providers.""" +class MoviesModule(containers.DeclarativeContainer): + """IoC container of movies module component providers.""" movie_model = providers.DelegatedFactory(models.Movie) diff --git a/tests/test_containers.py b/tests/test_containers.py new file mode 100644 index 00000000..38e6e667 --- /dev/null +++ b/tests/test_containers.py @@ -0,0 +1,95 @@ +"""Dependency injector container unit tests.""" + +import unittest2 as unittest + +from dependency_injector import ( + containers, + providers, +) + + +class ContainerA(containers.DeclarativeContainer): + """Declarative IoC container A.""" + + p11 = providers.Provider() + p12 = providers.Provider() + + +class ContainerB(ContainerA): + """Declarative IoC container B. + + Extends container A. + """ + + p21 = providers.Provider() + p22 = providers.Provider() + + +class DeclarativeContainerTests(unittest.TestCase): + """Declarative container tests.""" + + def test_providers_attribute_with(self): + """Test providers attribute.""" + self.assertEqual(ContainerA.providers, dict(p11=ContainerA.p11, + p12=ContainerA.p12)) + self.assertEqual(ContainerB.providers, dict(p11=ContainerA.p11, + p12=ContainerA.p12, + p21=ContainerB.p21, + p22=ContainerB.p22)) + + def test_cls_providers_attribute_with(self): + """Test cls_providers attribute.""" + self.assertEqual(ContainerA.cls_providers, dict(p11=ContainerA.p11, + p12=ContainerA.p12)) + self.assertEqual(ContainerB.cls_providers, dict(p21=ContainerB.p21, + p22=ContainerB.p22)) + + def test_inherited_providers_attribute(self): + """Test inherited_providers attribute.""" + self.assertEqual(ContainerA.inherited_providers, dict()) + self.assertEqual(ContainerB.inherited_providers, + dict(p11=ContainerA.p11, + p12=ContainerA.p12)) + + def test_set_get_del_provider_attribute(self): + """Test set/get/del provider attributes.""" + a_p13 = providers.Provider() + b_p23 = providers.Provider() + + ContainerA.p13 = a_p13 + ContainerB.p23 = b_p23 + + self.assertEqual(ContainerA.providers, dict(p11=ContainerA.p11, + p12=ContainerA.p12, + p13=a_p13)) + self.assertEqual(ContainerB.providers, dict(p11=ContainerA.p11, + p12=ContainerA.p12, + p21=ContainerB.p21, + p22=ContainerB.p22, + p23=b_p23)) + + self.assertEqual(ContainerA.cls_providers, dict(p11=ContainerA.p11, + p12=ContainerA.p12, + p13=a_p13)) + self.assertEqual(ContainerB.cls_providers, dict(p21=ContainerB.p21, + p22=ContainerB.p22, + p23=b_p23)) + + del ContainerA.p13 + del ContainerB.p23 + + self.assertEqual(ContainerA.providers, dict(p11=ContainerA.p11, + p12=ContainerA.p12)) + self.assertEqual(ContainerB.providers, dict(p11=ContainerA.p11, + p12=ContainerA.p12, + p21=ContainerB.p21, + p22=ContainerB.p22)) + + self.assertEqual(ContainerA.cls_providers, dict(p11=ContainerA.p11, + p12=ContainerA.p12)) + self.assertEqual(ContainerB.cls_providers, dict(p21=ContainerB.p21, + p22=ContainerB.p22)) + + +if __name__ == '__main__': + unittest.main()