Metadata-Version: 2.1
Name: flying-ioc
Version: 1.0.2
Summary: IoC for Humans
Home-page: https://github.com/ClanPlay/Python_IoC
Author: Asaf Semo, Dmitry Voronenkov
Author-email: asafsemo@gmail.com
License: UNKNOWN
Platform: UNKNOWN
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Description-Content-Type: text/markdown

# Inversion of control (IoC) for Humans - written in Python
[Inversion of control](https://en.wikipedia.org/wiki/Inversion_of_control)

## How to use it
pip install flying-ioc

``` {.sourceCode .python}
ioc = IocManager()

ioc.set_class(cls=HelperWrapper, singleton=True)
ioc.set_class(cls=GRHelperService, singleton=True)
ioc.set_class(name='api', cls=GRApiClient, singleton=True, thread_local=True)

gr_service: GRHelperService = ioc.GRHelperService
gr_service.start()
```

## Features
* Support getting an object as an attribute of IoC manager
* Initializing a class argument by the argument's class name and if not present by the argument name
* Support for inheritance - initializing arguments needed by parent classes
* Support mapping of values, classes, factories
* Support configuration of mapping like singleton, class per thread
* Support for @NotInject decorator

## Attribute
``` {.sourceCode .python}
gr_service: GRHelperService = ioc.GRHelperService
gr_service.start()
```
### Initializing a class
``` {.sourceCode .python}
class ClassA:
    pass

class ClassB:
    pass

class ClassC:
    pass

class ExampleClass:
    def __init__(self, arg1: ClassA, arg2, arg3: ClassC):
        assert arg1.__class__ == ClassA
        assert arg2.__class__ == ClassB
        assert arg3.__class__ == ClassC


def test_arguments():
    ioc = IocManager()
    ioc.set_class(cls=ClassA)
    ioc.set_class(name='arg2', cls=ClassB)
    ioc.set_class(name='arg3', cls=ClassC)

    ioc.set_class(cls=ExampleClass)

    assert ioc.ExampleClass.__class__ == ExampleClass
```

## Support for inheritance
``` {.sourceCode .python}
class ClassA:
    pass

class ClassB:
    pass

class ClassC:
    pass

class ParentD:
    def __init__(self, arg1: ClassA, **kwargs):
        self._arg1 = arg1

class ParentE(ParentD):
    def __init__(self, arg2: ClassB, **kwargs):
        super().__init__(**kwargs)
        self._arg2 = arg2

class ExampleClass(ParentE):
    def __init__(self, arg3: ClassC, **kwargs):
        super().__init__(**kwargs)
        assert self._arg1.__class__ == ClassA
        assert self._arg2.__class__ == ClassB
        assert arg3.__class__ == ClassC

def test_arguments():
    ioc = IocManager()
    ioc.set_class(cls=ClassA)
    ioc.set_class(cls=ClassB)
    ioc.set_class(cls=ClassC)

    ioc.set_class(cls=ExampleClass)

    assert ioc.ExampleClass.__class__ == ExampleClass
```

## Values
``` {.sourceCode .python}
class ClassA:
    pass


class ExampleClass:
    def __init__(self, value_text, value_class):
        assert value_text == 'Some text'
        assert value_class.__class__ == ClassA


def test_arguments():
    ioc = IocManager()
    ioc.set_value(name='value_text', value='Some text')
    ioc.set_value(name='value_class', value=ClassA())

    ioc.set_class(cls=ExampleClass)

    assert ioc.ExampleClass.__class__ == ExampleClass
```

## Factory
``` {.sourceCode .python}
class ClassA:
    pass

class ClassB:
    pass

class ClassC:
    pass

class Factory(IocFactory):
    @staticmethod
    def get_instance(ioc_manager: IocManager, name: str, frame_info: inspect.FrameInfo):
        if frame_info.function == 'test_factory_1':
            return ioc_manager.ClassA

        if name == 'factory1':
            return ioc_manager.ClassB

        return ioc_manager.ClassC

ioc = IocManager()
ioc.set_class(cls=ClassA)
ioc.set_class(cls=ClassB)
ioc.set_class(cls=ClassC)
ioc.set_factory(name='factory1', cls=Factory)
ioc.set_factory(name='factory2', cls=Factory)

def test_factory_1():
    assert ioc.factory1.__class__ == ClassA
    assert ioc.factory2.__class__ == ClassA

def test_factory_2():
    assert ioc.factory1.__class__ == ClassB
    assert ioc.factory2.__class__ == ClassC
```
## Singleton
``` {.sourceCode .python}
class ClassA:
    pass

class ClassB:
    pass

def test_singleton():
    ioc = IocManager()
    ioc.set_class(cls=ClassA)
    ioc.set_class(cls=ClassB, singleton=True)

    assert ioc.ClassA != ioc.ClassA
    assert ioc.ClassB == ioc.ClassB
```
## Class per thread
``` {.sourceCode .python}
class ClassA:
        pass

def _set_vars(ioc: IocManager, storage: dict):
    def wrapped():
        storage['singleton1'] = ioc.singleton1
        storage['singleton2'] = ioc.singleton2

    return wrapped

def test_class_per_thread():
    ioc = IocManager()

    ioc.set_class(name='singleton1', cls=ClassA, singleton=True)
    ioc.set_class(name='singleton2', cls=ClassA, singleton=True, thread_local=True)

    assert ioc.singleton1 == ioc.singleton1
    assert ioc.singleton2 == ioc.singleton2

    thread_storage = {}
    thread = threading.Thread(target=_set_vars(ioc, thread_storage))
    thread.start()
    thread.join()

    assert ioc.singleton1 == thread_storage['singleton1']
    assert ioc.singleton2 != thread_storage['singleton2']
```
## @NotInject decorator
``` {.sourceCode .python}
class ClassA:
    pass

class ClassB:
    pass

class Parent:
    def __init__(self, arg1: ClassA, **kwargs):
        super().__init__(**kwargs)
        self._arg1 = arg1

@NotInject(['arg1'])
class ExampleClass(Parent):
    def __init__(self, arg2: ClassB, **kwargs):
        arg1 = ClassA()
        super().__init__(arg1, **kwargs)
        assert self._arg1 == arg1
        assert arg2.__class__ == ClassB

def test_not_inject():
    ioc = IocManager()
    ioc.set_class(cls=ClassA)
    ioc.set_class(cls=ClassB)

    ioc.set_class(cls=ExampleClass)

    assert ioc.ExampleClass.__class__ == ExampleClass
```
If there is no @NotInject, IoC manager will add arg1 to kwargs when it initializes ExampleClass and there will be an exception.
To avoid this issue you can use @NotInject decorator and pass a list of argument names to it, to skip initialization of the arguments.
## Exceptions
``` {.sourceCode .python}
class ClassA:
    pass

class ClassB:
    pass

class ClassC:
    pass

class Parent:
    def __init__(self, arg1: ClassA, **kwargs):
        super().__init__(**kwargs)
        self._arg1 = arg1

class ExampleClass1(Parent):
    def __init__(self, arg2: ClassB, **kwargs):
        arg1 = ClassA()
        super().__init__(arg1, **kwargs)
        assert self._arg1 == arg1
        assert arg2.__class__ == ClassB

class ExampleClass2:
    def __init__(self, arg1: ClassC):
        pass

class ExampleClass3(ExampleClass2):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)

ioc = IocManager()
ioc.set_class(cls=ClassA)
ioc.set_class(cls=ClassB)

def test_exception_container_not_defined():
    with pytest.raises(AttributeError) as e:
        ioc.NotExists
    assert e.value.args[0] == "Name 'NotExists' does not exist"

def test_exception_missing_not_inject():
    with pytest.raises(TypeError) as e:
        ioc.set_class(cls=ExampleClass1)
        ioc.ExampleClass1
    assert e.value.args[0] == "__init__() got multiple values for argument 'arg1'"

def test_exception_arg_is_not_defined():
    with pytest.raises(RuntimeError) as e:
        ioc.set_class(cls=ExampleClass2)
        ioc.ExampleClass2
    assert e.value.args[0].args[0] == "Can't get a container neither by class name ClassC, neither by arg name arg1"

def test_exception_arg_for_parent_is_not_defined():
    with pytest.raises(TypeError) as e:
        ioc.set_class(cls=ExampleClass3)
        ioc.ExampleClass3
    assert e.value.args[0] == "__init__() missing 1 required positional argument: 'arg1'"
```

