diff --git a/README.md b/README.md index fd8fdf1f..78fb8a21 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Concept example of objects catalogs. from objects import AbstractCatalog from objects.providers import Singleton, NewInstance from objects.injections import InitArg, Attribute + import sqlite3 @@ -121,6 +122,7 @@ Concept example of objects overrides. from objects import AbstractCatalog, overrides from objects.providers import Singleton, NewInstance from objects.injections import InitArg, Attribute + import sqlite3 @@ -130,6 +132,7 @@ class ObjectA(object): self.db = db +# Mock of example class. class ObjectAMock(ObjectA): pass @@ -183,6 +186,7 @@ Concept example of objects catalogs. from objects import AbstractCatalog from objects.providers import Singleton, NewInstance, ExternalDependency from objects.injections import InitArg, Attribute + import sqlite3 @@ -231,3 +235,197 @@ assert a1 is not a2 assert b1 is not b2 assert a1.db is a2.db is b1.db is b2.db is Catalog.database() ``` + +Example of objects catalog with scoped provider: + +```python +""" +Scoped provider examples. +""" + +from objects import AbstractCatalog +from objects.providers import Singleton, Scoped +from objects.injections import InitArg, Attribute + +import sqlite3 + + +class ObjectA(object): + def __init__(self, db): + self.db = db + + +# Catalog of objects providers. +class Catalog(AbstractCatalog): + """ + Objects catalog. + """ + + database = Singleton(sqlite3.Connection, + InitArg('database', ':memory:'), + Attribute('row_factory', sqlite3.Row)) + """ :type: (objects.Provider) -> sqlite3.Connection """ + + object_a = Scoped(ObjectA, + InitArg('db', database)) + """ :type: (objects.Provider) -> ObjectA """ + + +# Making scope using `with` statement. +with Catalog.object_a as object_a_provider: + object_a1 = object_a_provider() + object_a2 = object_a_provider() + + assert object_a1 is object_a2 + assert object_a1.db is object_a2.db + +# Making another one scope using `with` statement. +with Catalog.object_a as object_a_provider: + object_a3 = object_a_provider() + object_a4 = object_a_provider() + + assert object_a3 is object_a4 + assert object_a3.db is object_a4.db + + assert (object_a1 is not object_a3) and \ + (object_a1 is not object_a4) + assert (object_a2 is not object_a3) and \ + (object_a2 is not object_a4) + + +# Making one more scope using provider methods. +Catalog.object_a.in_scope() + +object_a5 = Catalog.object_a() +object_a6 = Catalog.object_a() + +assert object_a5 is object_a6 +assert object_a5.db is object_a6.db + +assert (object_a1 is not object_a3) and \ + (object_a1 is not object_a4) and \ + (object_a1 is not object_a5) and \ + (object_a1 is not object_a6) +assert (object_a2 is not object_a3) and \ + (object_a2 is not object_a4) and \ + (object_a2 is not object_a5) and \ + (object_a2 is not object_a6) +``` + +Example of objects catalog with callable provider: + +```python +""" +Callable provider examples. +""" + +from objects import AbstractCatalog +from objects.providers import Singleton, Callable +from objects.injections import Injection, InitArg, Attribute + +import sqlite3 + + +# Some example function. +def consuming_function(arg, db): + return arg, db + + +# Catalog of objects providers. +class Catalog(AbstractCatalog): + """ + Objects catalog. + """ + + database = Singleton(sqlite3.Connection, + InitArg('database', ':memory:'), + Attribute('row_factory', sqlite3.Row)) + """ :type: (objects.Provider) -> sqlite3.Connection """ + + consuming_function = Callable(consuming_function, + Injection('db', database)) + """ :type: (objects.Provider) -> consuming_function """ + + +# Some calls. +arg1, db1 = Catalog.consuming_function(1) +arg2, db2 = Catalog.consuming_function(2) +arg3, db3 = Catalog.consuming_function(3) + +# Some asserts. +assert db1 is db2 is db3 +assert arg1 == 1 +assert arg2 == 2 +assert arg3 == 3 +``` + +Example of objects catalog with config provider: + +```python +""" +Config provider examples. +""" + +from objects import AbstractCatalog +from objects.providers import Config, NewInstance +from objects.injections import InitArg + + +# Some example class. +class ObjectA(object): + def __init__(self, setting_one, setting_two, setting_three): + self.setting_one = setting_one + self.setting_two = setting_two + self.setting_three = setting_three + + +# Catalog of objects providers. +class Catalog(AbstractCatalog): + """ + Objects catalog. + """ + + config = Config() + """ :type: (objects.Config) """ + + object_a = NewInstance(ObjectA, + InitArg('setting_one', config.SETTING_ONE), + InitArg('setting_two', config.SETTING_TWO), + InitArg('setting_three', config.GLOBAL.SETTING_THREE)) + """ :type: (objects.Provider) -> ObjectA """ + + +# Setting config value and making some tests. +Catalog.config.update_from({ + 'SETTING_ONE': 1, + 'SETTING_TWO': 2, + 'GLOBAL': { + 'SETTING_THREE': 3 + } +}) + +object_a1 = Catalog.object_a() + +assert object_a1.setting_one == 1 +assert object_a1.setting_two == 2 +assert object_a1.setting_three == 3 + +# Changing config value one more time and making some tests. +Catalog.config.update_from({ + 'SETTING_ONE': 11, + 'SETTING_TWO': 22, + 'GLOBAL': { + 'SETTING_THREE': 33 + } +}) + +object_a2 = Catalog.object_a() + +assert object_a2.setting_one == 11 +assert object_a2.setting_two == 22 +assert object_a2.setting_three == 33 + +assert object_a1.setting_one == 1 +assert object_a1.setting_two == 2 +assert object_a1.setting_three == 3 +``` diff --git a/VERSION b/VERSION index 0c62199f..0d91a54c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.1 +0.3.0 diff --git a/examples/callable_provider.py b/examples/callable_provider.py new file mode 100644 index 00000000..3e037c81 --- /dev/null +++ b/examples/callable_provider.py @@ -0,0 +1,42 @@ +""" +Callable provider examples. +""" + +from objects import AbstractCatalog +from objects.providers import Singleton, Callable +from objects.injections import Injection, InitArg, Attribute + +import sqlite3 + + +# Some example function. +def consuming_function(arg, db): + return arg, db + + +# Catalog of objects providers. +class Catalog(AbstractCatalog): + """ + Objects catalog. + """ + + database = Singleton(sqlite3.Connection, + InitArg('database', ':memory:'), + Attribute('row_factory', sqlite3.Row)) + """ :type: (objects.Provider) -> sqlite3.Connection """ + + consuming_function = Callable(consuming_function, + Injection('db', database)) + """ :type: (objects.Provider) -> consuming_function """ + + +# Some calls. +arg1, db1 = Catalog.consuming_function(1) +arg2, db2 = Catalog.consuming_function(2) +arg3, db3 = Catalog.consuming_function(3) + +# Some asserts. +assert db1 is db2 is db3 +assert arg1 == 1 +assert arg2 == 2 +assert arg3 == 3 diff --git a/examples/concept.py b/examples/concept.py index 34714fd1..f44eb8fe 100644 --- a/examples/concept.py +++ b/examples/concept.py @@ -5,6 +5,7 @@ Concept example of objects catalogs. from objects import AbstractCatalog from objects.providers import Singleton, NewInstance from objects.injections import InitArg, Attribute + import sqlite3 diff --git a/examples/config_provider.py b/examples/config_provider.py new file mode 100644 index 00000000..599293b6 --- /dev/null +++ b/examples/config_provider.py @@ -0,0 +1,66 @@ +""" +Config provider examples. +""" + +from objects import AbstractCatalog +from objects.providers import Config, NewInstance +from objects.injections import InitArg + + +# Some example class. +class ObjectA(object): + def __init__(self, setting_one, setting_two, setting_three): + self.setting_one = setting_one + self.setting_two = setting_two + self.setting_three = setting_three + + +# Catalog of objects providers. +class Catalog(AbstractCatalog): + """ + Objects catalog. + """ + + config = Config() + """ :type: (objects.Config) """ + + object_a = NewInstance(ObjectA, + InitArg('setting_one', config.SETTING_ONE), + InitArg('setting_two', config.SETTING_TWO), + InitArg('setting_three', config.GLOBAL.SETTING_THREE)) + """ :type: (objects.Provider) -> ObjectA """ + + +# Setting config value and making some tests. +Catalog.config.update_from({ + 'SETTING_ONE': 1, + 'SETTING_TWO': 2, + 'GLOBAL': { + 'SETTING_THREE': 3 + } +}) + +object_a1 = Catalog.object_a() + +assert object_a1.setting_one == 1 +assert object_a1.setting_two == 2 +assert object_a1.setting_three == 3 + +# Changing config value one more time and making some tests. +Catalog.config.update_from({ + 'SETTING_ONE': 11, + 'SETTING_TWO': 22, + 'GLOBAL': { + 'SETTING_THREE': 33 + } +}) + +object_a2 = Catalog.object_a() + +assert object_a2.setting_one == 11 +assert object_a2.setting_two == 22 +assert object_a2.setting_three == 33 + +assert object_a1.setting_one == 1 +assert object_a1.setting_two == 2 +assert object_a1.setting_three == 3 diff --git a/examples/external_dependency.py b/examples/external_dependency.py index a19e0c95..b5ae4cc4 100644 --- a/examples/external_dependency.py +++ b/examples/external_dependency.py @@ -5,6 +5,7 @@ Concept example of objects catalogs. from objects import AbstractCatalog from objects.providers import Singleton, NewInstance, ExternalDependency from objects.injections import InitArg, Attribute + import sqlite3 diff --git a/examples/overrides.py b/examples/overrides.py index 6a6d5ebb..4b4dc4db 100644 --- a/examples/overrides.py +++ b/examples/overrides.py @@ -6,6 +6,7 @@ Concept example of objects overrides. from objects import AbstractCatalog, overrides from objects.providers import Singleton, NewInstance from objects.injections import InitArg, Attribute + import sqlite3 @@ -15,6 +16,7 @@ class ObjectA(object): self.db = db +# Mock of example class. class ObjectAMock(ObjectA): pass diff --git a/examples/scoped_provider.py b/examples/scoped_provider.py new file mode 100644 index 00000000..45dd602c --- /dev/null +++ b/examples/scoped_provider.py @@ -0,0 +1,71 @@ +""" +Scoped provider examples. +""" + +from objects import AbstractCatalog +from objects.providers import Singleton, Scoped +from objects.injections import InitArg, Attribute + +import sqlite3 + + +class ObjectA(object): + def __init__(self, db): + self.db = db + + +# Catalog of objects providers. +class Catalog(AbstractCatalog): + """ + Objects catalog. + """ + + database = Singleton(sqlite3.Connection, + InitArg('database', ':memory:'), + Attribute('row_factory', sqlite3.Row)) + """ :type: (objects.Provider) -> sqlite3.Connection """ + + object_a = Scoped(ObjectA, + InitArg('db', database)) + """ :type: (objects.Provider) -> ObjectA """ + + +# Making scope using `with` statement. +with Catalog.object_a as object_a_provider: + object_a1 = object_a_provider() + object_a2 = object_a_provider() + + assert object_a1 is object_a2 + assert object_a1.db is object_a2.db + +# Making another one scope using `with` statement. +with Catalog.object_a as object_a_provider: + object_a3 = object_a_provider() + object_a4 = object_a_provider() + + assert object_a3 is object_a4 + assert object_a3.db is object_a4.db + + assert (object_a1 is not object_a3) and \ + (object_a1 is not object_a4) + assert (object_a2 is not object_a3) and \ + (object_a2 is not object_a4) + + +# Making one more scope using provider methods. +Catalog.object_a.in_scope() + +object_a5 = Catalog.object_a() +object_a6 = Catalog.object_a() + +assert object_a5 is object_a6 +assert object_a5.db is object_a6.db + +assert (object_a1 is not object_a3) and \ + (object_a1 is not object_a4) and \ + (object_a1 is not object_a5) and \ + (object_a1 is not object_a6) +assert (object_a2 is not object_a3) and \ + (object_a2 is not object_a4) and \ + (object_a2 is not object_a5) and \ + (object_a2 is not object_a6) diff --git a/objects/catalog.py b/objects/catalog.py index bd265052..56826245 100644 --- a/objects/catalog.py +++ b/objects/catalog.py @@ -33,14 +33,14 @@ class AbstractCatalog(object): return attribute @classmethod - def __all_providers__(cls): + def __all_providers__(cls, provider_type=Provider): """ Returns set of all class providers. """ providers = set() for attr_name in set(dir(cls)) - set(dir(AbstractCatalog)): provider = getattr(cls, attr_name) - if not isinstance(provider, Provider): + if not isinstance(provider, provider_type): continue providers.add((attr_name, provider)) return providers diff --git a/objects/providers.py b/objects/providers.py index 2c5ef271..abbf75da 100644 --- a/objects/providers.py +++ b/objects/providers.py @@ -3,7 +3,7 @@ Standard providers. """ from collections import Iterable -from .injections import InitArg, Attribute, Method +from .injections import Injection, InitArg, Attribute, Method class Provider(object): @@ -108,6 +108,60 @@ class Singleton(NewInstance): self.instance = super(Singleton, self).__call__(*args, **kwargs) return self.instance + def _reset_instance(self): + """ + Resets instance. + """ + self.instance = None + + +class Scoped(Singleton): + """ + Scoped provider will create instance once for every scope and return it on every call. + """ + + def __init__(self, *args, **kwargs): + """ + Initializer. + """ + self.is_in_scope = None + super(Scoped, self).__init__(*args, **kwargs) + + def in_scope(self): + """ + Sets provider in "in scope" state. + """ + self.is_in_scope = True + self._reset_instance() + + def out_of_scope(self): + """ + Sets provider in "out of scope" state. + """ + self.is_in_scope = False + self._reset_instance() + + def __call__(self, *args, **kwargs): + """ + Returns provided instance. + """ + if not self.is_in_scope: + raise RuntimeError('Trying to provide {} while provider is not in scope'.format(self.provides)) + return super(Scoped, self).__call__(*args, **kwargs) + + def __enter__(self): + """ + With __enter__() implementation. Makes provider to be in scope. + """ + self.in_scope() + return self + + def __exit__(self, *_): + """ + With __exit__() implementation. Makes provider to be out of scope. + """ + self.out_of_scope() + class ExternalDependency(Provider): """ @@ -193,3 +247,97 @@ class Value(_StaticProvider): """ Value provider provides value. """ + + +class Callable(Provider): + """ + Callable provider will provide callable calls with some predefined + dependencies injections. + """ + + def __init__(self, calls, *injections): + """ + Initializer. + """ + self.calls = calls + self.injections = fetch_injections(injections, Injection) + super(Callable, self).__init__() + + def __call__(self, *args, **kwargs): + """ + Returns provided instance. + """ + if self.__overridden_by__: + return self.__overridden_by__[-1].__call__(*args, **kwargs) + + injections = prepare_injections(self.injections) + injections = dict(injections) + injections.update(kwargs) + + return self.calls(*args, **injections) + + +class _DeferredConfig(Provider): + """ + Deferred config providers provide an value from the root config object. + """ + + def __init__(self, paths, root_config): + """ + Initializer. + """ + self.paths = paths + self.root_config = root_config + super(_DeferredConfig, self).__init__() + + def __getattr__(self, item): + """ + Returns instance of deferred config. + """ + return _DeferredConfig(paths=self.paths + (item,), + root_config=self.root_config) + + def __call__(self, *args, **kwargs): + """ + Returns provided instance. + """ + return self.root_config(self.paths) + + +class Config(Provider): + """ + Config provider provides dict values. Also config provider creates + deferred config objects for all undefined attribute calls. + """ + + def __init__(self, value=None): + """ + Initializer. + """ + if not value: + value = dict() + self.value = value + super(Config, self).__init__() + + def update_from(self, value): + """ + Updates current value from another one. + """ + self.value.update(value) + + def __getattr__(self, item): + """ + Returns instance of deferred config. + """ + return _DeferredConfig(paths=(item,), + root_config=self) + + def __call__(self, paths=None): + """ + Returns provided instance. + """ + value = self.value + if paths: + for path in paths: + value = value[path] + return value