diff --git a/README.md b/README.md index d12430e7..8723c207 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ -Objects -======= +# Objects -Python catalogs of objects providers +Dependency management tool for Python projects. [![Latest Version](https://pypip.in/version/Objects/badge.svg)](https://pypi.python.org/pypi/Objects/) [![Downloads](https://pypip.in/download/Objects/badge.svg)](https://pypi.python.org/pypi/Objects/) @@ -11,10 +10,140 @@ Python catalogs of objects providers [![Supported Python versions](https://pypip.in/py_versions/Objects/badge.svg)](https://pypi.python.org/pypi/Objects/) [![Supported Python implementations](https://pypip.in/implementation/Objects/badge.svg)](https://pypi.python.org/pypi/Objects/) -Example: +## Introduction + +Python ecosystem consists of a big amount of various classes, functions and +objects that could be used for applications development. Each of them has its +own role. + +Modern Python applications are mostly the composition of well-known open +source systems, frameworks, libraries and some turnkey functionality. + +When application goes bigger, its amount of objects and their dependencies +also increased extremely fast and became hard to maintain. + +`Objects` is designed to be developer's friendly tool for managing objects +and their dependencies in formal, pretty way. Main idea of `Objects` is to +keep dependencies under control. + +## Entities + +Current section describes main `Objects` entities and their interaction. + +### Providers + +Providers are strategies of accessing objects. + +All providers are callable. They describe how particular objects will be +provided. For example: ```python -"""Concept example of `Objects`.""" +"""`NewInstance` and `Singleton` providers example.""" + +from objects.providers import NewInstance +from objects.providers import Singleton + + +# NewInstance provider will create 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 + +# Singleton provider will create new instance of specified class on first call, +# and return 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 +``` + +### Injections + +Injections are additional instructions, that are used for determining +dependencies of objects. + +Objects can take dependencies in various forms. Some objects take init +arguments, other are using attributes or methods to be initialized. Injection, +in terms of `Objects`, is an instruction how to provide dependency for the +particular object. + +Every Python object could be an injection's value. Special case is a `Objects` +provider as an injection's value. In such case, injection value is a result of +injectable provider call (every time injection is done). + +Injections are used by providers. + +```python +"""`KwArg` and `Attribute` 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 +assert object_a_1.get_one() == object_a_2.get_one() == 1 +``` + +### Catalogs + +Catalogs are named set of providers. + +`Objects` catalogs can be used for grouping of providers by some +kind of rules. In example below, there are two catalogs: +`Resources` and `Models`. + +`Resources` catalog is used to group all common application resources like +database connection and various api clients, while `Models` catalog is used +for application model providers only. + +```python +"""Catalogs example.""" + +import sqlite3 +import httplib from objects.catalog import AbstractCatalog @@ -23,28 +152,229 @@ from objects.providers import NewInstance from objects.injections import KwArg from objects.injections import Attribute + + +class SomeModel(object): + + """SomeModel has dependency on database and api client. + + Dependencies need to be injected via init args. + """ + + def __init__(self, database, api_client): + """Initializer.""" + self.database = database + self.api_client = api_client + + def api_request(self): + """Make api request.""" + self.api_client.request('GET', '/') + return self.api_client.getresponse() + + def get_one(self): + """Select one from database and return it.""" + return self.database.execute('SELECT 1').fetchone()[0] + + +class Resources(AbstractCatalog): + + """Resource providers catalog.""" + + database = Singleton(sqlite3.Connection, + KwArg('database', ':memory:'), + KwArg('timeout', 30), + KwArg('detect_types', True), + KwArg('isolation_level', 'EXCLUSIVE'), + Attribute('row_factory', sqlite3.Row)) + + api_client = Singleton(httplib.HTTPConnection, + KwArg('host', 'example.com'), + KwArg('port', 80), + KwArg('timeout', 10)) + + +class Models(AbstractCatalog): + + """Model providers catalog.""" + + some_model = NewInstance(SomeModel, + KwArg('database', Resources.database), + KwArg('api_client', Resources.api_client)) + + +# Creating `SomeModel` instance. +some_model = Models.some_model() + +# Making some asserts. +assert some_model.get_one() == 1 +assert some_model.api_request().status == 200 +``` + +## Advanced usage + +Below you can find some variants of advanced usage of `Objects`. + +### Inject decorator + +`@inject` decorator could be used for patching any callable with injection. +Any Python object will be injected 'as is', except `Objects` providers, +that will be called to provide injectable value. + + +```python +"""`@inject` decorator example.""" + +from objects.providers import NewInstance + +from objects.injections import KwArg from objects.injections import inject + +new_object = NewInstance(object) + + +@inject(KwArg('object_a', new_object)) +@inject(KwArg('some_setting', 1334)) +def example_callback(object_a, some_setting): + """This function has dependencies on object a and b. + + Dependencies are injected using `@inject` decorator. + """ + assert isinstance(object_a, object) + assert some_setting == 1334 + + +example_callback() +example_callback() +``` + +### Overriding providers + +Any provider can be overridden by another provider. + +Example: + +```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): - """Example class ObjectA, that has dependency on database.""" + """ObjectA has dependency on database.""" - def __init__(self, db): + 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.""" - self.db = db + + 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 -class ObjectB(object): +# 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)) - """Example class ObjectB, that has dependencies on ObjectA and database.""" +object_a = NewInstance(ObjectA, + KwArg('database', database)) - def __init__(self, a, db): + +# 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 +``` + +### Overriding catalogs + +Any catalog can be overridden by another catalog. + +Example: + +```python +"""Catalog overriding example.""" + +import sqlite3 + +from objects.catalog import AbstractCatalog +from objects.catalog import override + +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.""" - self.a = a - self.db = db + + 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 class Catalog(AbstractCatalog): @@ -53,35 +383,28 @@ class Catalog(AbstractCatalog): database = Singleton(sqlite3.Connection, KwArg('database', ':memory:'), + KwArg('timeout', 30), + KwArg('detect_types', True), + KwArg('isolation_level', 'EXCLUSIVE'), Attribute('row_factory', sqlite3.Row)) - """:type: (objects.Provider) -> sqlite3.Connection""" object_a = NewInstance(ObjectA, - KwArg('db', database)) - """:type: (objects.Provider) -> ObjectA""" - - object_b = NewInstance(ObjectB, - KwArg('a', object_a), - KwArg('db', database)) - """:type: (objects.Provider) -> ObjectB""" + KwArg('database', database)) -# Catalog static provides. -a1, a2 = Catalog.object_a(), Catalog.object_a() -b1, b2 = Catalog.object_b(), Catalog.object_b() +@override(Catalog) +class SandboxCatalog(Catalog): -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() + """Sandbox objects catalog with some mocks that overrides Catalog.""" + + object_a = NewInstance(ObjectAMock) -# Example of inline injections. -@inject(KwArg('a', Catalog.object_a)) -@inject(KwArg('b', Catalog.object_b)) -@inject(KwArg('database', Catalog.database)) -def example(a, b, database): - assert a.db is b.db is database is Catalog.database() +# Creating several `ObjectA` instances. +object_a_1 = Catalog.object_a() +object_a_2 = Catalog.object_a() - -example() +# 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/readme/catalogs.py b/examples/readme/catalogs.py new file mode 100644 index 00000000..30194f85 --- /dev/null +++ b/examples/readme/catalogs.py @@ -0,0 +1,68 @@ +"""Catalogs example.""" + +import sqlite3 +import httplib + +from objects.catalog import AbstractCatalog + +from objects.providers import Singleton +from objects.providers import NewInstance + +from objects.injections import KwArg +from objects.injections import Attribute + + +class SomeModel(object): + + """SomeModel has dependency on database and api client. + + Dependencies need to be injected via init args. + """ + + def __init__(self, database, api_client): + """Initializer.""" + self.database = database + self.api_client = api_client + + def api_request(self): + """Make api request.""" + self.api_client.request('GET', '/') + return self.api_client.getresponse() + + def get_one(self): + """Select one from database and return it.""" + return self.database.execute('SELECT 1').fetchone()[0] + + +class Resources(AbstractCatalog): + + """Resource providers catalog.""" + + database = Singleton(sqlite3.Connection, + KwArg('database', ':memory:'), + KwArg('timeout', 30), + KwArg('detect_types', True), + KwArg('isolation_level', 'EXCLUSIVE'), + Attribute('row_factory', sqlite3.Row)) + + api_client = Singleton(httplib.HTTPConnection, + KwArg('host', 'example.com'), + KwArg('port', 80), + KwArg('timeout', 10)) + + +class Models(AbstractCatalog): + + """Model providers catalog.""" + + some_model = NewInstance(SomeModel, + KwArg('database', Resources.database), + KwArg('api_client', Resources.api_client)) + + +# Creating `SomeModel` instance. +some_model = Models.some_model() + +# Making some asserts. +assert some_model.get_one() == 1 +assert some_model.api_request().status == 200 diff --git a/examples/readme/inject_decorator.py b/examples/readme/inject_decorator.py new file mode 100644 index 00000000..702f2727 --- /dev/null +++ b/examples/readme/inject_decorator.py @@ -0,0 +1,24 @@ +"""`@inject` decorator example.""" + +from objects.providers import NewInstance + +from objects.injections import KwArg +from objects.injections import inject + + +new_object = NewInstance(object) + + +@inject(KwArg('object_a', new_object)) +@inject(KwArg('some_setting', 1334)) +def example_callback(object_a, some_setting): + """This function has dependencies on object a and b. + + Dependencies are injected using `@inject` decorator. + """ + assert isinstance(object_a, object) + assert some_setting == 1334 + + +example_callback() +example_callback() diff --git a/examples/readme/injections.py b/examples/readme/injections.py new file mode 100644 index 00000000..52590a7e --- /dev/null +++ b/examples/readme/injections.py @@ -0,0 +1,45 @@ +"""`KwArg` and `Attribute` 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 +assert object_a_1.get_one() == object_a_2.get_one() == 1 diff --git a/examples/readme/overriding_catalog.py b/examples/readme/overriding_catalog.py new file mode 100644 index 00000000..657def1a --- /dev/null +++ b/examples/readme/overriding_catalog.py @@ -0,0 +1,77 @@ +"""Catalog overriding example.""" + +import sqlite3 + +from objects.catalog import AbstractCatalog +from objects.catalog import override + +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 + + +class Catalog(AbstractCatalog): + + """Catalog of objects 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)) + + +@override(Catalog) +class SandboxCatalog(Catalog): + + """Sandbox objects catalog with some mocks that overrides Catalog.""" + + object_a = NewInstance(ObjectAMock) + + +# Creating several `ObjectA` instances. +object_a_1 = Catalog.object_a() +object_a_2 = Catalog.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/readme/overriding_provider.py b/examples/readme/overriding_provider.py new file mode 100644 index 00000000..41ff0fe3 --- /dev/null +++ b/examples/readme/overriding_provider.py @@ -0,0 +1,66 @@ +"""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/readme/providers.py b/examples/readme/providers.py new file mode 100644 index 00000000..ce662c38 --- /dev/null +++ b/examples/readme/providers.py @@ -0,0 +1,23 @@ +"""`NewInstance` and `Singleton` providers example.""" + +from objects.providers import NewInstance +from objects.providers import Singleton + + +# NewInstance provider will create 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 + +# Singleton provider will create new instance of specified class on first call, +# and return 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 diff --git a/objects/__init__.py b/objects/__init__.py index 1486e968..b6d6d6bb 100644 --- a/objects/__init__.py +++ b/objects/__init__.py @@ -1,4 +1,7 @@ -"""Objects.""" +"""Objects. + +Dependency management tool for Python projects. +""" from .catalog import AbstractCatalog from .catalog import override diff --git a/setup.py b/setup.py index 5eb50a30..0946aa08 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ if __name__ == '__main__': setup( name='Objects', version=version, - description='Python catalogs of objects providers', + description='Dependency management tool for Python projects', long_description=description, author='Roman Mogilatov', author_email='rmogilatov@gmail.com', @@ -40,15 +40,16 @@ if __name__ == '__main__': packages=['objects'], zip_safe=True, install_requires=requirements, - # keywords=['Dependency injection', - # 'Dependency injection container', - # 'DI', - # 'DIC', - # 'Dependency injector', - # 'Inversion of Control', - # 'Inversion of Control container', - # 'IoC', - # 'IoC container'], + keywords=['Dependency management', + 'Dependency injection', + 'Dependency injection container', + 'DI', + 'DIC', + 'Dependency injector', + 'Inversion of Control', + 'Inversion of Control container', + 'IoC', + 'IoC container'], classifiers=[ 'Development Status :: 3 - Alpha', 'Intended Audience :: Developers',