diff --git a/docs/index.rst b/docs/index.rst index d1f9d13f..c58ae813 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -38,6 +38,7 @@ Contents introduction installation + providers entities advanced_usage examples diff --git a/docs/providers.rst b/docs/providers.rst new file mode 100644 index 00000000..94ee9066 --- /dev/null +++ b/docs/providers.rst @@ -0,0 +1,242 @@ +Providers +========= + +Providers are strategies of accessing objects. + +All providers are callable. They describe how particular objects will be +provided. + + +Instance providers & Injections +------------------------------- + +*Instance* providers are providers that deal with object's creation and +initialization. + +There are few *Instance* providers: + + - ``NewInstance`` provider creates new instance of specified class on every + call. + - ``Singleton`` provider creates new instance of specified class on first + call and returns same instance on every next call. + +.. code-block:: python + + """`NewInstance` and `Singleton` providers example.""" + + from objects.providers import NewInstance + from objects.providers import Singleton + + + # NewInstance provider creates new instance of specified class on every call. + new_object = NewInstance(object) + + object_1 = new_object() + object_2 = new_object() + + assert object_1 is not object_2 + assert isinstance(object_1, object) and isinstance(object_2, object) + + # Singleton provider creates new instance of specified class on first call + # and returns same instance on every next call. + single_object = Singleton(object) + + single_object_1 = single_object() + single_object_2 = single_object() + + assert single_object_1 is single_object_2 + assert isinstance(object_1, object) and isinstance(object_2, object) + + +Objects can take dependencies in various forms. Some objects take init +arguments, other are using attributes or methods to be initialized. It affects +how such objects need to be created and initialized, and that is the place +where **Injections** need to be used. + +In terms of computer science, **Injection** of dependency is a way how +dependency can be coupled with dependent object. + +In terms of **Objects**, **Injection** is an instruction how to provide +dependency for the particular object. + +Every Python object could be an injection's value. Special case is an **Objects** +provider as an injection's value. In such case, injection value is a result of +injectable provider call (every time injection is done). + +There are several types of injections. Below is a description of how they are +used by instance providers: + + - ``KwArg`` - is injected in object's ``__init__()`` method in time of + object's initialization via keyword argument. + - ``Attribute`` - is injected into object's attribute (not class attribute) + after object's initialization. + - ``Method`` - is injected into object method's call after objects + initialization. + +Example: + +.. code-block:: python + + """`NewInstance` and `Singleton` providers with injections example.""" + + import sqlite3 + + from objects.providers import Singleton + from objects.providers import NewInstance + + from objects.injections import KwArg + from objects.injections import Attribute + + + class ObjectA(object): + + """ObjectA has dependency on database.""" + + def __init__(self, database): + """Initializer. + + Database dependency need to be injected via init arg.""" + self.database = database + + def get_one(self): + """Select one from database and return it.""" + return self.database.execute('SELECT 1').fetchone()[0] + + + # Database and `ObjectA` providers. + database = Singleton(sqlite3.Connection, + KwArg('database', ':memory:'), + KwArg('timeout', 30), + KwArg('detect_types', True), + KwArg('isolation_level', 'EXCLUSIVE'), + Attribute('row_factory', sqlite3.Row)) + + object_a = NewInstance(ObjectA, + KwArg('database', database)) + + # Creating several `ObjectA` instances. + object_a_1 = object_a() + object_a_2 = object_a() + + # Making some asserts. + assert object_a_1 is not object_a_2 + assert object_a_1.database is object_a_2.database is database() + assert object_a_1.get_one() == object_a_2.get_one() == 1 + +Static providers +---------------- + +Static providers are family of providers that returns its values "as is". +There are four of static providers: ``Class``, ``Object``, ``Function`` and +``Value``. All of them has the same behaviour, but usage of anyone is +predicted by readability and providable object's type. + +.. code-block:: python + + """Static providers example.""" + + from objects.providers import Class + from objects.providers import Object + from objects.providers import Function + from objects.providers import Value + + + cls_provider = Class(object) + assert cls_provider() is object + + object_provider = Object(object()) + assert isinstance(object_provider(), object) + + function_provider = Function(len) + assert function_provider() is len + + value_provider = Value(123) + assert value_provider() == 123 + +Callable provider +----------------- + +External dependency provider +---------------------------- + +Config provider +--------------- + +Providers delegation +-------------------- + +Overriding of providers +----------------------- + +Any provider can be overridden by another provider. + +Example: + +.. code-block:: python + + """Provider overriding example.""" + + import sqlite3 + + from objects.providers import Singleton + from objects.providers import NewInstance + + from objects.injections import KwArg + from objects.injections import Attribute + + + class ObjectA(object): + + """ObjectA has dependency on database.""" + + def __init__(self, database): + """Initializer. + + Database dependency need to be injected via init arg.""" + self.database = database + + def get_one(self): + """Select one from database and return it.""" + return self.database.execute('SELECT 1') + + + class ObjectAMock(ObjectA): + + """Mock of ObjectA. + + Has no dependency on database. + """ + + def __init__(self): + """Initializer.""" + + def get_one(self): + """Select one from database and return it. + + Mock makes no database queries and always returns two instead of one. + """ + return 2 + + + # Database and `ObjectA` providers. + database = Singleton(sqlite3.Connection, + KwArg('database', ':memory:'), + KwArg('timeout', 30), + KwArg('detect_types', True), + KwArg('isolation_level', 'EXCLUSIVE'), + Attribute('row_factory', sqlite3.Row)) + + object_a = NewInstance(ObjectA, + KwArg('database', database)) + + + # Overriding `ObjectA` provider with `ObjectAMock` provider. + object_a.override(NewInstance(ObjectAMock)) + + # Creating several `ObjectA` instances. + object_a_1 = object_a() + object_a_2 = object_a() + + # Making some asserts. + assert object_a_1 is not object_a_2 + assert object_a_1.get_one() == object_a_2.get_one() == 2 diff --git a/examples/concept.py b/examples/concept.py index 6d934b48..75fe2c98 100644 --- a/examples/concept.py +++ b/examples/concept.py @@ -7,7 +7,8 @@ from objects.providers import NewInstance from objects.injections import KwArg from objects.injections import Attribute -from objects.injections import inject + +from objects.decorators import inject import sqlite3 diff --git a/examples/override.py b/examples/override.py index 6cffbab8..98f1c91b 100644 --- a/examples/override.py +++ b/examples/override.py @@ -1,7 +1,6 @@ """Override example.""" from objects.catalog import AbstractCatalog -from objects.catalog import override from objects.providers import Singleton from objects.providers import NewInstance @@ -9,6 +8,8 @@ from objects.providers import NewInstance from objects.injections import KwArg from objects.injections import Attribute +from objects.decorators import override + import sqlite3 diff --git a/examples/readme/inject_decorator.py b/examples/readme/inject_decorator.py index 702f2727..6bfd3c7f 100644 --- a/examples/readme/inject_decorator.py +++ b/examples/readme/inject_decorator.py @@ -1,9 +1,8 @@ """`@inject` decorator example.""" from objects.providers import NewInstance - from objects.injections import KwArg -from objects.injections import inject +from objects.decorators import inject new_object = NewInstance(object) diff --git a/examples/readme/overriding_catalog.py b/examples/readme/overriding_catalog.py index 657def1a..d05e4ac0 100644 --- a/examples/readme/overriding_catalog.py +++ b/examples/readme/overriding_catalog.py @@ -3,7 +3,6 @@ import sqlite3 from objects.catalog import AbstractCatalog -from objects.catalog import override from objects.providers import Singleton from objects.providers import NewInstance @@ -11,6 +10,8 @@ from objects.providers import NewInstance from objects.injections import KwArg from objects.injections import Attribute +from objects.decorators import override + class ObjectA(object): diff --git a/examples/readme/providers.py b/examples/readme/providers.py index ce662c38..1d0dff6f 100644 --- a/examples/readme/providers.py +++ b/examples/readme/providers.py @@ -4,8 +4,7 @@ from objects.providers import NewInstance from objects.providers import Singleton -# NewInstance provider will create new instance of specified class -# on every call. +# NewInstance provider creates new instance of specified class on every call. new_object = NewInstance(object) object_1 = new_object() @@ -13,8 +12,8 @@ object_2 = new_object() assert object_1 is not object_2 -# Singleton provider will create new instance of specified class on first call, -# and return same instance on every next call. +# Singleton provider creates new instance of specified class on first call +# and returns same instance on every next call. single_object = Singleton(object) single_object_1 = single_object() diff --git a/examples/readme2/instance_providers.py b/examples/readme2/instance_providers.py new file mode 100644 index 00000000..d18783e6 --- /dev/null +++ b/examples/readme2/instance_providers.py @@ -0,0 +1,24 @@ +"""`NewInstance` and `Singleton` providers example.""" + +from objects.providers import NewInstance +from objects.providers import Singleton + + +# NewInstance provider creates new instance of specified class on every call. +new_object = NewInstance(object) + +object_1 = new_object() +object_2 = new_object() + +assert object_1 is not object_2 +assert isinstance(object_1, object) and isinstance(object_2, object) + +# Singleton provider creates new instance of specified class on first call +# and returns same instance on every next call. +single_object = Singleton(object) + +single_object_1 = single_object() +single_object_2 = single_object() + +assert single_object_1 is single_object_2 +assert isinstance(object_1, object) and isinstance(object_2, object) diff --git a/examples/readme/injections.py b/examples/readme2/instance_providers_with_injections.py similarity index 89% rename from examples/readme/injections.py rename to examples/readme2/instance_providers_with_injections.py index 52590a7e..ba20793f 100644 --- a/examples/readme/injections.py +++ b/examples/readme2/instance_providers_with_injections.py @@ -1,4 +1,4 @@ -"""`KwArg` and `Attribute` injections example.""" +"""`NewInstance` and `Singleton` providers with injections example.""" import sqlite3 @@ -41,5 +41,5 @@ object_a_2 = object_a() # Making some asserts. assert object_a_1 is not object_a_2 -assert object_a_1.database is object_a_2.database +assert object_a_1.database is object_a_2.database is database() assert object_a_1.get_one() == object_a_2.get_one() == 1 diff --git a/examples/readme/overriding_provider.py b/examples/readme2/overriding_providers.py similarity index 100% rename from examples/readme/overriding_provider.py rename to examples/readme2/overriding_providers.py diff --git a/examples/readme2/static_providers.py b/examples/readme2/static_providers.py new file mode 100644 index 00000000..7664d59b --- /dev/null +++ b/examples/readme2/static_providers.py @@ -0,0 +1,19 @@ +"""Static providers example.""" + +from objects.providers import Class +from objects.providers import Object +from objects.providers import Function +from objects.providers import Value + + +cls_provider = Class(object) +assert cls_provider() is object + +object_provider = Object(object()) +assert isinstance(object_provider(), object) + +function_provider = Function(len) +assert function_provider() is len + +value_provider = Value(123) +assert value_provider() == 123 diff --git a/objects/__init__.py b/objects/__init__.py index b6d6d6bb..f5921f20 100644 --- a/objects/__init__.py +++ b/objects/__init__.py @@ -4,7 +4,6 @@ Dependency management tool for Python projects. """ from .catalog import AbstractCatalog -from .catalog import override from .providers import Provider from .providers import Delegate @@ -22,11 +21,13 @@ from .injections import KwArg from .injections import Attribute from .injections import Method +from .decorators import override +from .decorators import inject + from .errors import Error __all__ = ('AbstractCatalog', - 'override', # Providers 'Provider', @@ -46,5 +47,9 @@ __all__ = ('AbstractCatalog', 'Attribute', 'Method', + # Decorators + 'override', + 'inject', + # Errors 'Error') diff --git a/objects/catalog.py b/objects/catalog.py index 81d12bcf..c69346d0 100644 --- a/objects/catalog.py +++ b/objects/catalog.py @@ -47,12 +47,3 @@ class AbstractCatalog(object): for name, provider in overridden: overridden_provider = getattr(cls, name) overridden_provider.override(provider) - - -def override(catalog): - """Catalog overriding decorator.""" - def decorator(overriding_catalog): - """Overriding decorator.""" - catalog.override(overriding_catalog) - return overriding_catalog - return decorator diff --git a/objects/decorators.py b/objects/decorators.py new file mode 100644 index 00000000..0113ae05 --- /dev/null +++ b/objects/decorators.py @@ -0,0 +1,34 @@ +"""Decorators module.""" + +from six import wraps + +from .utils import ensure_is_injection + + +def override(catalog): + """Catalog overriding decorator.""" + def decorator(overriding_catalog): + """Overriding decorator.""" + catalog.override(overriding_catalog) + return overriding_catalog + return decorator + + +def inject(injection): + """Inject decorator. + + :type injection: Injection + :return: (callable) -> (callable) + """ + injection = ensure_is_injection(injection) + + def decorator(callback): + """Decorator.""" + @wraps(callback) + def decorated(*args, **kwargs): + """Decorated.""" + if injection.name not in kwargs: + kwargs[injection.name] = injection.value + return callback(*args, **kwargs) + return decorated + return decorator diff --git a/objects/injections.py b/objects/injections.py index 99245e88..c574071b 100644 --- a/objects/injections.py +++ b/objects/injections.py @@ -1,9 +1,6 @@ """Injections module.""" -from six import wraps - from .utils import is_provider -from .utils import ensure_is_injection class Injection(object): @@ -45,23 +42,3 @@ class Method(Injection): """Method injection.""" __IS_OBJECTS_METHOD_INJECTION__ = True - - -def inject(injection): - """Inject decorator. - - :type injection: Injection - :return: (callable) -> (callable) - """ - injection = ensure_is_injection(injection) - - def decorator(callback): - """Decorator.""" - @wraps(callback) - def decorated(*args, **kwargs): - """Decorated.""" - if injection.name not in kwargs: - kwargs[injection.name] = injection.value - return callback(*args, **kwargs) - return decorated - return decorator diff --git a/tests/test_catalog.py b/tests/test_catalog.py index 065e56f5..cc0baf8b 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -3,7 +3,6 @@ import unittest2 as unittest from objects.catalog import AbstractCatalog -from objects.catalog import override from objects.providers import Object from objects.providers import Value @@ -50,16 +49,3 @@ class CatalogTests(unittest.TestCase): """Test getting of all catalog providers of specific type.""" self.assertTrue(len(self.Catalog.all_providers(Object)) == 2) self.assertTrue(len(self.Catalog.all_providers(Value)) == 0) - - def test_overriding(self): - """Test catalog overriding with another catalog.""" - @override(self.Catalog) - class OverridingCatalog(self.Catalog): - - """Overriding catalog.""" - - obj = Value(1) - another_obj = Value(2) - - self.assertEqual(self.Catalog.obj(), 1) - self.assertEqual(self.Catalog.another_obj(), 2) diff --git a/tests/test_decorators.py b/tests/test_decorators.py new file mode 100644 index 00000000..c6ce5f42 --- /dev/null +++ b/tests/test_decorators.py @@ -0,0 +1,114 @@ +"""Objects decorators unittests.""" + +import unittest2 as unittest + +from objects.decorators import override +from objects.decorators import inject + +from objects.catalog import AbstractCatalog + +from objects.providers import NewInstance +from objects.providers import Object +from objects.providers import Value + +from objects.injections import KwArg + +from objects.errors import Error + + +class OverrideTests(unittest.TestCase): + + """Override decorator test cases.""" + + class Catalog(AbstractCatalog): + + """Test catalog.""" + + obj = Object(object()) + another_obj = Object(object()) + + def test_overriding(self): + """Test catalog overriding with another catalog.""" + @override(self.Catalog) + class OverridingCatalog(self.Catalog): + + """Overriding catalog.""" + + obj = Value(1) + another_obj = Value(2) + + self.assertEqual(self.Catalog.obj(), 1) + self.assertEqual(self.Catalog.another_obj(), 2) + +class InjectTests(unittest.TestCase): + + """Inject decorator test cases.""" + + def test_decorated(self): + """Test `inject()` decorated callback.""" + provider1 = NewInstance(object) + provider2 = NewInstance(list) + + @inject(KwArg('a', provider1)) + @inject(KwArg('b', provider2)) + def test(a, b): + return a, b + + a1, b1 = test() + a2, b2 = test() + + self.assertIsInstance(a1, object) + self.assertIsInstance(a2, object) + self.assertIsNot(a1, a2) + + self.assertIsInstance(b1, list) + self.assertIsInstance(b2, list) + self.assertIsNot(b1, b2) + + def test_decorated_kwargs_priority(self): + """Test `inject()` decorated callback kwargs priority.""" + provider1 = NewInstance(object) + provider2 = NewInstance(list) + object_a = object() + + @inject(KwArg('a', provider1)) + @inject(KwArg('b', provider2)) + def test(a, b): + return a, b + + a1, b1 = test(a=object_a) + a2, b2 = test(a=object_a) + + self.assertIsInstance(a1, object) + self.assertIsInstance(a2, object) + self.assertIs(a1, object_a) + self.assertIs(a2, object_a) + + self.assertIsInstance(b1, list) + self.assertIsInstance(b2, list) + self.assertIsNot(b1, b2) + + def test_decorated_with_args(self): + """Test `inject()` decorated callback with args.""" + provider = NewInstance(list) + object_a = object() + + @inject(KwArg('b', provider)) + def test(a, b): + return a, b + + a1, b1 = test(object_a) + a2, b2 = test(object_a) + + self.assertIsInstance(a1, object) + self.assertIsInstance(a2, object) + self.assertIs(a1, object_a) + self.assertIs(a2, object_a) + + self.assertIsInstance(b1, list) + self.assertIsInstance(b2, list) + self.assertIsNot(b1, b2) + + def test_decorate_with_not_injection(self): + """Test `inject()` decorator with not an injection instance.""" + self.assertRaises(Error, inject, object) diff --git a/tests/test_injections.py b/tests/test_injections.py index b4d5ff3c..b9612e1a 100644 --- a/tests/test_injections.py +++ b/tests/test_injections.py @@ -6,12 +6,9 @@ from objects.injections import Injection from objects.injections import KwArg from objects.injections import Attribute from objects.injections import Method -from objects.injections import inject from objects.providers import NewInstance -from objects.providers import Error - class InjectionTests(unittest.TestCase): @@ -65,77 +62,3 @@ class MethodTests(unittest.TestCase): injection = Method('some_arg_name', 'some_value') self.assertEqual(injection.name, 'some_arg_name') self.assertEqual(injection.injectable, 'some_value') - - -class InjectTests(unittest.TestCase): - - """Inject decorator test cases.""" - - def test_decorated(self): - """Test `inject()` decorated callback.""" - provider1 = NewInstance(object) - provider2 = NewInstance(list) - - @inject(KwArg('a', provider1)) - @inject(KwArg('b', provider2)) - def test(a, b): - return a, b - - a1, b1 = test() - a2, b2 = test() - - self.assertIsInstance(a1, object) - self.assertIsInstance(a2, object) - self.assertIsNot(a1, a2) - - self.assertIsInstance(b1, list) - self.assertIsInstance(b2, list) - self.assertIsNot(b1, b2) - - def test_decorated_kwargs_priority(self): - """Test `inject()` decorated callback kwargs priority.""" - provider1 = NewInstance(object) - provider2 = NewInstance(list) - object_a = object() - - @inject(KwArg('a', provider1)) - @inject(KwArg('b', provider2)) - def test(a, b): - return a, b - - a1, b1 = test(a=object_a) - a2, b2 = test(a=object_a) - - self.assertIsInstance(a1, object) - self.assertIsInstance(a2, object) - self.assertIs(a1, object_a) - self.assertIs(a2, object_a) - - self.assertIsInstance(b1, list) - self.assertIsInstance(b2, list) - self.assertIsNot(b1, b2) - - def test_decorated_with_args(self): - """Test `inject()` decorated callback with args.""" - provider = NewInstance(list) - object_a = object() - - @inject(KwArg('b', provider)) - def test(a, b): - return a, b - - a1, b1 = test(object_a) - a2, b2 = test(object_a) - - self.assertIsInstance(a1, object) - self.assertIsInstance(a2, object) - self.assertIs(a1, object_a) - self.assertIs(a2, object_a) - - self.assertIsInstance(b1, list) - self.assertIsInstance(b2, list) - self.assertIsNot(b1, b2) - - def test_decorate_with_not_injection(self): - """Test `inject()` decorator with not an injection instance.""" - self.assertRaises(Error, inject, object)