-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 5207e29
Showing
18 changed files
with
843 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}, | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"], | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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." | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.