Metadata-Version: 2.1
Name: clean-ioc
Version: 0.0.3
Summary: An IOC Container for Python 3.10+
Home-page: https://github.com/peter-daly/clean_ioc
License: MIT
Author: Peter Daly
Requires-Python: >=3.10,<4.0
Classifier: Development Status :: 4 - Beta
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Project-URL: Documentation, https://github.com/peter-daly/clean_ioc
Project-URL: Repository, https://github.com/peter-daly/clean_ioc
Description-Content-Type: text/markdown

# Clean IoC
A simple dependency injection library for python that requires nothing of your application code (except that you use typing).


## Basic Registering and resolving

There are 4 basic modes of registering a new set of classes

### Implementation

```python

class UserRepository(abc.ABC)
    @abc.abstractmethod
    def add(self, user):
        pass

class InMemoryUserRepository(UserRepository)

    def __init__(self):
        self.users = []

    def add(self, user):
        # This is obviously terrible, but it's for demo purposes
        self.users.append(user)

class SqlAlchemyUserRepository(UserRepository)

    def __init__(self):
        # Do some db stuff here
        pass

    def add(self, user):
        # Do some db stuff here
        pass

container = Container()
container.register(UserRepository, InMemoryUserRepository)


repository = container.resolve(UserRepository) # This will return an InMemoryUserRepository

```

### Concrete Class

```python

class ClientDependency
    def get_int(self):
        return 10

class Client
    def __init__(self, dep: ClientDependency)
        self.dep = dep

    def get_number(self):
        return self.dep.get_int()


container = Container()
container.register(ClientDependency)
container.register(Client)

client = container.resolve(Client)

client.get_number() # returns 10

```

### Factory

```python

class ClientDependency
    def get_int(self):
        return 10

class Client
    def __init__(self, dep: ClientDependency)
        self.dep = dep

    def get_number(self):
        return self.dep.get_int()

def client_factory(dep: ClientDependency):
    return Client(dep=dep)


container = Container()
container.register(ClientDependency)
container.register(Client, factory=client_factory)

client = container.resolve(Client)

client.get_number() # returns 10

```


### Instance

```python

class ClientDependency
    def __init__(self, num):
        self.num = num

    def get_int(self):
        return self.num

class Client
    def __init__(self, dep: ClientDependency)
        self.dep = dep

    def get_number(self):
        return self.dep.get_int()

client_dependency = ClientDependency(num=10)

container = Container()
container.register(ClientDependency, instance=client_dependency)
container.register(Client)

client = container.resolve(Client)

client.get_number() # returns 10

```

## List resolving

```python

class ClientDependency
    def __init__(self, numbers: list[int]):
        self.numbers = numbers

    def get_numbers(self):
        return self.numbers

class Client
    def __init__(self, dep: ClientDependency)
        self.dep = dep

    def get_numbers(self):
        return self.dep.get_numbers()

container = Container()
container.register(ClientDependency)
container.register(Client)
container.register(int, instance=1)
container.register(int, instance=2)
container.register(int, instance=3)

client = container.resolve(Client)

client.get_numbers() # returns [3, 2, 1]
```


## Decorators

Follows a object orientated decoration pattern, rather than a decoration annotation.
The main reason for this was to allow decotation of registered instances

```python
class Client
    def __init__(self, number: int)
        self.number = number

    def get_number(self):
        return self.number


class DoubleClientDecorator(Client):
    def __init__(self, client: Client):
        self.client = client
    def get_number(self):
        return self.client.get_number() * 2

container = Container()

container.register(Client)
container.register_decorator(Client, DoubleClientDecorator)
container.register(int, instance=10)

client = container.resolve(Client)

client.get_number() # returns 20
```

## Subclasses registration

This feature allows registartion of all subclasses of a giveb type

```python
class Client(abc.ABC)
    @abc.abstractmethod
    def get_number(self):
        pass


class TenClient(Client)
    def get_number(self):
        return 10

class TwentyClient(Client)
    def get_number(self):
        return 20

container = Container()

container.register_subclasses(Client)

ten_client = container.resolve(TenClient)
ten_client.get_number() # returns 10

twenty_client = container.resolve(TwentyClient)
twenty_client.get_number() # returns 20

# Resolve all subsclasses of Client
client = container.resolve(list[Client]) ## [TwentyClient(), TenClient()]
```


## Lifestyles
Lifestyles configure how long and resolved object says alive for
There are 4 lifestyle types

### transient
Always create a new instance

```python
container.register(Client, lifestyle=LifestyleType.transient)
```


### once_per_graph (Default behaviour)
Only create one instance throughout the resolve call

```python
container.register(Client, lifestyle=LifestyleType.once_per_graph)
```

### scoped
Only create a new instance through the life a scope. When not in a scope the behaviour is the same as **once_per_graph**

```python
container.register(Client, lifestyle=LifestyleType.scoped)
```

### singleton
Only one instance of the object is created throughout the lifespan of the container

```python
container.register(Client, lifestyle=LifestyleType.singleton)
```

*Note:*
When registering an instance, then the behaviour is always singleton

```python
container.register(int, instance=10)
```

## Open Generics

Registers all generic subclasses of the service type and allows you to resolve with the generic alias

```python
T = TypeVar("T")

class HelloCommand:
    pass

class GoodbyeCommand:
    pass

class CommandHandler(Generic[T]):
    def handle(self, command: T):
        pass

class HelloCommandHandler(CommandHandler[HelloCommand]):
    def handle(self, command: HelloCommand):
        print('HELLO')

class GoodbyeCommandHandler(CommandHandler[GoodbyeCommand]):
    def handle(self, command: GoodbyeCommand):
        print('GOODBYE')

container = Container()
container.register_open_generic(CommandHandler)

h1 = container.resolve(CommandHandler[HelloCommand])
h2 = container.resolve(CommandHandler[GoodbyeCommand])

h1.handle(HelloCommand()) # prints 'HELLO'
h2.handle(GoodbyeCommand()) # prints 'GOODBYE'

```

## Open Generic Decorators


Allows you to add decorators to your open generic registrations

```python
T = TypeVar("T")

class HelloCommand:
    pass

class GoodbyeCommand:
    pass

class CommandHandler(Generic[T]):
    def handle(self, command: T):
        pass

class HelloCommandHandler(CommandHandler[HelloCommand]):
    def handle(self, command: HelloCommand):
        print('HELLO')

class GoodbyeCommandHandler(CommandHandler[GoodbyeCommand]):
    def handle(self, command: GoodbyeCommand):
        print('GOODBYE')

class AVeryBigCommandHandlerDecorator(Generic[T]):
    def __init__(self, handler: CommandHandler[T]):
        self.handler = handler

    def handle(self, command: T):
        print('A VERY BIG')
        self.handler.handle(command=command)

container = Container()
container.register_open_generic(CommandHandler)
container.register_open_generic_decorator(CommandHandler, AVeryBigCommandHandlerDecorator)
h1 = container.resolve(CommandHandler[HelloCommand])
h2 = container.resolve(CommandHandler[GoodbyeCommand])

h1.handle(HelloCommand()) # prints 'A VERY BIG\nHELLO'
h2.handle(GoodbyeCommand()) # prints 'A VERY BIG\nGOODBYE'

```

## Scopes

Scopes are a way to create a sub container that will live for a certain lifestyle.
Some good use cases for scope would be for the lifespan of handling a http request with a web server or a message/event if working on a message based system


```python
class Client
    def __init__(self, number: int)
        return number

    def get_number(self):
        return self.resolver.resolve(int)

container.register(int, instance=2)

container.register(Client)

client = container.resolve(Client)
client.get_number() # returns 2

with container.get_scope() as scope:
    scope.register(int, instance=10)
    scoped_client = scope.resolve(Client)
    scoped_client.get_number() # returns 10
```

## Named registartions & Registartion filters

By default the last registration is what the container will return when resolve is called as below.

```python

container = Container()
container.register(int, instance=1)
container.register(int, instance=2)
container.register(int, instance=3)

number = container.resolve(int) # returns 3

```
To be more selective of what we return we can add a name to the registration and apply a registartion filter when we resolve.

A registartion filter is simply function that receives a **Registartion** and returns a **bool**

For example if we wanted to get the int named **"One"** we do the following

```python
container = Container()
container.register(int, instance=1, name="One")
container.register(int, instance=2, name="Two")
container.register(int, instance=3, name="Three")

number = container.resolve(int, filter=lambda r: r.name == "One") # returns 1
```

Clean IOC comes with a set of in built registartion filters that can be found [here](./clean_ioc/registration_filters.py)

We can get the desired behaviour as above
```python
from clean_ioc.registartion_filters import with_name

container = Container()
container.register(int, instance=1, name="One")
container.register(int, instance=2, name="Two")
container.register(int, instance=3, name="Three")

number = container.resolve(int, filter=with_name("One")) # returns 1
```

## Dependency Settings

Dependency settings are defined at registartion and allow you to define the selection or setting dependencies


```python
class Client
    def __init__(self, number=10)
        self.number = number

    def get_number(self):
        return self.number

container = Container()

container.register(int, instance=1, name="One")
container.register(int, instance=2, name="Two")

container.register(
    Client,
    name="SetsValue",
    dependency_config={"number": DependencySettings(value=50)}
)
container.register(
    Client,
    name="UsesDefaultValue"
)
container.register(
    Client,
    name="IgnoresDefaultParameterValue",
    dependency_config={"number": DependencySettings(use_default_paramater=False)}
)
container.register(
    Client,
    name="UsesRegistartionFilter",
    dependency_config={"number": DependencySettings(use_default_paramater=False, filter=with_name("One"))}
)

client1 = container.resolve(Client, filter=with_name("SetsValue"))
client2 = container.resolve(Client, filter=with_name("UsesDefaultValue"))
client3 = container.resolve(Client, filter=with_name("IgnoresDefaultParameterValue"))
client4 = container.resolve(Client, filter=with_name("UsesRegistartionFilter"))


client1.get_number() # returns 50
client2.get_number() # returns 10
client3.get_number() # returns 2
client4.get_number() # returns 1
```

The order of a dependant value is as follows
1. Setting the dependency value explicitly
    ```python
    DependencySettings(value=50)
    ```
2. Using the default parameter value if it exisis the dependency value explicitly
    ```python
    class Client
    def __init__(self, number=10)
        self.number = number
    ```
    If you don't want to use the default parameter value you can set it to false in the dependency setting
    ```python
    DependencySettings(use_default_paramater=False)
    ```
3. Going to the container registry to find a registartion using the registration filter if, if there is a default value on the dependant paramater you must explicity set.

## Accessing the Container, Scope and Resolver within dependencies

Accessing container directly

```python
class Client
    def __init__(self, container: Container)
        self.container = container

    def get_number(self):
        return self.container.resolve(int)

container.register(int, instance=2)

container.register(Client)

client = container.resolve(Client)
client.get_number() # returns 2
```

Accessing Resolver also returns the container

```python

class Client
    def __init__(self, resolver: Resolver)
        self.resolver = resolver

    def get_number(self):
        return self.resolver.resolve(int)

container.register(int, instance=2)

container.register(Client)

client = container.resolve(Client)
client.get_number() # returns 2
```

When within a scope, Resolver returns the current scope rather than the container

```python
class Client
    def __init__(self, resolver: Resolver)
        self.resolver = resolver

    def get_number(self):
        return self.resolver.resolve(int)

container.register(int, instance=2)

container.register(Client)

client = container.resolve(Client)
client.get_number() # returns 2

with container.get_scope() as scope:
    scope.register(int, instance=10)
    scoped_client = scope.resolve(Client)
    scoped_client.get_number() # returns 10
```


## Modules (BETA feature)


A module is a just a function that accepts a container, it can be used to set up common elements on the container

```python
class ClientDependency
    def get_int(self):
        return 10

class Client
    def __init__(self, dep: ClientDependency)
        self.dep = dep

    def get_number(self):
        return self.dep.get_int()

def client_module(c: Container):
    container = Container()
    container.register(ClientDependency)
    container.register(Client)

container.apply_module(client_module)

client = container.resolve(Client)

client.get_number() # returns 10
```

