Skip to content

Commit

Permalink
Feature (0.1.0): Added all files
Browse files Browse the repository at this point in the history
  • Loading branch information
leandrodesouzadev committed Jun 6, 2022
0 parents commit 5207e29
Show file tree
Hide file tree
Showing 18 changed files with 843 additions and 0 deletions.
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Packaging
.pdm.toml
__pypackages__

# Cache
__pycache__
*.pyc
.mypy_cache
.pytest_cache
19 changes: 19 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Pytest",
"type": "python",
"request": "launch",
"module": "pytest",
"args": ["tests", "-v" , "-x"],
"env": {
"PYTHONPATH": "src:__pypackages__/3.9/lib"
},
"justMyCode": true,
},
]
}
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"python.autoComplete.extraPaths": ["__pypackages__/3.9/lib"],
"python.analysis.extraPaths": ["__pypackages__/3.9/lib"],
}
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2022 Plathanus-tech

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
129 changes: 129 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Inject-It

Simple dependency injection facility using python decorators.
`inject_it` aims to:

- Keep dependencies creation separated from usage;
- Keep dependencies swappable for test environments;

## How to use

You can inject dependencies in two main ways:

1- Creating it first, in a main.py like style;
2- Using a function to create the dependency.

### Creating the dependency first aproach

Say you have a dependency for a object of type `SomeService`. Then before you start your application, you create a instance
of this object and want to inject it to a function. So you can have:

```python
# service.py
class SomeService:
# Your service stuff
def do_stuff(self):
print("Doing stuff")


# main.py
from service import SomeService
from inject_it import register_dependency

service = SomeService()
register_dependency(service)
# More code later...
```

The code above will register the type of `service` and bound this to the `service` instance.
So in another file that requires `SomeService` we will use the `requires` decorator, like:

```python
# worker.py
from service import SomeService
from inject_it import requires

@requires(SomeService)
def your_function(some_argument: str, another_argument: int, s: SomeService):
s.do_stuff()

# main.py
from service import SomeService
from inject_it import register_dependency

service = SomeService()
register_dependency(service)

from worker import your_function
your_function("abc", another_argument=1)
```

By using the `reguires` decorator on `your_function` passing `SomeService` as a dependency, inject_it will inject the `service` instance
into the function for you.

## How injection works

`inject_it` `requires` decorator instropects the signature of the decorated function to inject the dependencies. So it does some checks to see if the decorated function is correctly annotated. We also check if you also not given the dependency that will be injected, so we dont override for some reason your call.

## The requires decorator

The `requires` decorator also accepts more than one dependency for function. So you can do:

```python

from inject_it import requires


@requires(str, int, float)
def totally_injected_function(a: int, b: str, c: float):
pass

# In this case you can call the function like this
totally_injected_function()
```

The code above works, but in the snippet above for simplicity we didn't called `register_dependency`, so this snippet as is will raise an `inject_it.exceptions.DependencyNotRegistered`.

## Creating the dependency on a provider function

You can also define a dependency `provider`. That is a function that will return the dependency object. This is useful if you need a different instance everytime. Using the same example from before:

```python
# main.py
from service import SomeService
from inject_it import provider


@provider(SomeService)
def some_service_provider():
# On a real example,on this approach you probably would load some env-variables, config, etc.
return SomeService()

```

In this example, everytime a function `requires` for `SomeService` this function will be called. If it's expensive to create the object, you can cache it. You do it like:

```python
# main.py
from service import SomeService
from inject_it import provider


@provider(SomeService, cache_dependency=True) # <-
def some_service_provider():
# On a real example,on this approach you probably would load some env-variables, config, etc.
return SomeService()

```

This will have the same effect as calling `register_dependency` after creating the object.

Your provider can also `requires` a dependency, but it must be registered before it.

## Limitations

For the moment, you can only have one dependency for each type. So you can't have like two different `str` dependencies. When you register the second `str` you will be overriding the first. You can work around this by using specific types, instead of primitive types.

# Testing

Testing is made easy with `inject-it`, you just have to register your `mock`, `fake`, `stub` before calling your function. If you are using pytest, use fixtures.
9 changes: 9 additions & 0 deletions inject_it/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from . import decorators, exceptions, register, stubs
from .decorators import requires, provider
from .exceptions import (
DependencyNotRegistered,
InvalidDependency,
InvalidFunctionSignature,
InjectedKwargAlreadyGiven,
)
from .register import register_dependency
50 changes: 50 additions & 0 deletions inject_it/_checks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import inspect
from typing import Any

from .exceptions import (
InjectedKwargAlreadyGiven,
InvalidFunctionSignature,
InvalidFunctionDecoration,
ProviderReturnValueTypeMismatch,
)
from .stubs import Kwargs, Types, Class


def wrapped_callable(fnc):
if not callable(fnc):
raise InvalidFunctionDecoration(
"Did you forgot to call the decorator in the wrapped function?"
)


def at_least_one_type_required(*types: Class):
if not types:
raise InvalidFunctionDecoration("At least one type is required to be injected.")


def provider_returned_expected_type(obj: Any, type_: Class):
if type(obj) is not type_:
raise ProviderReturnValueTypeMismatch(
f"Invalid return type. Expected {type_} received: {type(obj)}"
)


def no_duplicated_type_annotation_for_types_on_sig(
sig: inspect.Signature, types: Types
):
types_annotations = [
s.annotation for s in sig.parameters.values() if s.annotation in types
]
non_duplicated_annotations = set(types_annotations)
if len(non_duplicated_annotations) != len(types_annotations):
raise InvalidFunctionSignature(
"The function signature has duplicated dependencies type definitions"
)


def no_injected_and_called_kwarg(original_kwargs: Kwargs, injected_kwargs: Kwargs):
for inj_kw in injected_kwargs:
if inj_kw in original_kwargs:
raise InjectedKwargAlreadyGiven(
f"The argument: {inj_kw} cannot be injected, it was already given somewhere else."
)
41 changes: 41 additions & 0 deletions inject_it/_injector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import inspect
from typing import Any, Type
from . import _checks
from .exceptions import DependencyNotRegistered, InvalidFunctionSignature
from .stubs import Dependencies, Providers, Types, Kwargs


dependencies: Dependencies = {}
providers: Providers = {}


def _get_dependency(t: Type) -> Any:
from inject_it.register import register_dependency

dep = dependencies.get(t, Ellipsis)
if dep is not Ellipsis:
return dep

provider = providers.get(t)
if not provider:
raise DependencyNotRegistered(
f"Could not found an dependency for: {t}. Did you forgot to register it?"
)
dependency = provider.fnc()
_checks.provider_returned_expected_type(
obj=dependency, type_=provider.expected_return_type
)
if provider.cache_dependency:
register_dependency(dependency)
return dependency


def get_injected_kwargs_for_signature(sig: inspect.Signature, types: Types) -> Kwargs:
param_for_type = {s.annotation: p for p, s in sig.parameters.items()}
for typ in types:
if typ not in param_for_type:
raise InvalidFunctionSignature(
f"The type {typ} was not found on the function signature. Did you forgot to type annotate it?"
)

return {param_for_type[t]: _get_dependency(t) for t in types}
76 changes: 76 additions & 0 deletions inject_it/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import inspect
from functools import wraps
from typing import Callable

from inject_it.register import register_provider

from .stubs import Class, DecoratedFunction, Function
from ._injector import get_injected_kwargs_for_signature
from . import _checks


def requires(*types: Class):
"""Decorates a function and on runtime injects the dependencies for the given `types` into the
function. The dependency will be injected on the parameter that has a type annotation for the
matching type. The others typed/non-typed arguments that has no match to types will remain from
your original call.
Usage:
```python
@requires(str, int)
def your_func(required_arg: float, some_arg: str, other_arg: int):
...
# Later ...
# Calling using keyword arguments
your_func(required_arg=1.0)
# Positional arguments
your_func(1.0)
```
"""

def decorator(fnc: DecoratedFunction) -> Callable[[DecoratedFunction], Function]:
_checks.wrapped_callable(fnc)
_checks.at_least_one_type_required(*types)
sig = inspect.signature(fnc)
_checks.no_duplicated_type_annotation_for_types_on_sig(sig=sig, types=types)

@wraps(fnc)
def decorated(*args, **kwargs):
injected_kwargs = get_injected_kwargs_for_signature(sig=sig, types=types)
_checks.no_injected_and_called_kwarg(
original_kwargs=kwargs, injected_kwargs=injected_kwargs
)
kwargs.update(injected_kwargs)
return fnc(*args, **kwargs)

return decorated

return decorator


def provider(type_: Class, cache_dependency: bool = False):
"""Register a function that should return a dependency for the given type.
If `cache_dependency` is `True` then the function will be only called once.
"""

def decorator(fnc: DecoratedFunction) -> Callable[[DecoratedFunction], Function]:
_checks.wrapped_callable(fnc)
register_provider(
type_=type_,
fnc=fnc,
cache_dependency=cache_dependency,
)

@wraps(fnc)
def decorated(*args, **kwargs):
return fnc(*args, **kwargs)

return decorated

return decorator
25 changes: 25 additions & 0 deletions inject_it/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Base exceptions


class DependencyNotRegistered(LookupError):
pass


class InvalidDependency(ValueError):
pass


class InjectedKwargAlreadyGiven(TypeError):
pass


class InvalidFunctionSignature(TypeError):
pass


class InvalidFunctionDecoration(TypeError):
pass


class ProviderReturnValueTypeMismatch(TypeError):
pass
Loading

0 comments on commit 5207e29

Please sign in to comment.