Skip to content

Commit

Permalink
fmt
Browse files Browse the repository at this point in the history
  • Loading branch information
RB387 committed Mar 25, 2024
1 parent 84aa5da commit 92b3828
Show file tree
Hide file tree
Showing 23 changed files with 299 additions and 249 deletions.
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@

Dependency Injector that makes your life easier with built-in support of FastAPI, Celery (and it can be integrated with everything)

What are the problems with FastAPI’s dependency injector?
1) It forces you to use global variables.
What are the problems with FastAPI’s dependency injector?
1) It forces you to use global variables.
2) You need to write an endless number of fabrics with startup logic
3) It makes your project highly dependent on FastAPI’s injector by using “Depends” everywhere.

Expand Down Expand Up @@ -211,7 +211,7 @@ def hello_world(service: Provide[MyInterface]) -> dict:
}
```

Using `injector.bind`, you can bind implementations that will be injected everywhere the bound interface is used.
Using `injector.bind`, you can bind implementations that will be injected everywhere the bound interface is used.

## Integration with Celery

Expand Down Expand Up @@ -247,22 +247,22 @@ app = Celery(
class CalculatorTaskDeps(BaseCeleryConnectableDeps):
calculator: Calculator


class CalculatorTask(InjectableCeleryTask):
deps: CalculatorTaskDeps

async def run(self, x: int, y: int, smart_processor: SmartProcessor = Provide()):
return smart_processor.process(
await self.deps.calculator.calculate(x, y)
)


app.register_task(CalculatorTask)
```

### Limitations
You could notice that in these examples tasks are using Python async/await.
`InjectableCeleryTask` provides support for writing async code. However, it still executes code synchronously.
You could notice that in these examples tasks are using Python async/await.
`InjectableCeleryTask` provides support for writing async code. However, it still executes code synchronously.
**Due to this, getting results from async tasks is not possible in the following cases:**
* When the `task_always_eager` config flag is enabled and task creation occurs inside the running event loop (e.g., inside an async FastAPI endpoint)
* When calling the `.apply()` method inside running event loop (e.g., inside an async FastAPI endpoint)
Expand Down Expand Up @@ -297,7 +297,7 @@ def client(injector: DependencyInjector, service_mock: InjectableMock):
with TestClient(app) as client:
yield client


def test_http_handler(client):
resp = client.post('/hello-world')

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ build-backend = "poetry.core.masonry.api"

[tool.ruff]
target-version = "py38" # The lowest supported version
line-length = 100

[tool.ruff.lint]
# By default, enable all the lint rules.
Expand Down
15 changes: 6 additions & 9 deletions src/magic_di/_connectable.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@ class ConnectableProtocol(Protocol):
"""
Interface for injectable clients.
Adding these methods to your class will allow it to be dependency injectable.
The dependency injector uses duck typing to check that the class implements the interface.
The dependency injector uses duck typing to check that the class
implements the interface.
This means that you do not need to inherit from this protocol.
"""

async def __connect__(self) -> None:
...
async def __connect__(self) -> None: ...

async def __disconnect__(self) -> None:
...
async def __disconnect__(self) -> None: ...


class Connectable:
Expand All @@ -23,8 +22,6 @@ class Connectable:
without adding these empty methods.
"""

async def __connect__(self) -> None:
...
async def __connect__(self) -> None: ...

async def __disconnect__(self) -> None:
...
async def __disconnect__(self) -> None: ...
58 changes: 33 additions & 25 deletions src/magic_di/_container.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
from __future__ import annotations

import functools
import inspect
from dataclasses import dataclass
from threading import Lock
from typing import Generic, Iterable, Type, TypeVar, cast
from typing import Generic, Iterable, TypeVar, cast

T = TypeVar("T")


@dataclass(frozen=True)
class Dependency(Generic[T]):
object: Type[T]
object: type[T]
instance: T | None = None


class SingletonDependencyContainer:
def __init__(self):
self._deps: dict[Type[T], Dependency[T]] = {}
self._deps: dict[type[T], Dependency[T]] = {}
self._lock: Lock = Lock()

def add(self, obj: Type[T], **kwargs) -> Type[T]:
def add(self, obj: type[T], **kwargs) -> type[T]:
with self._lock:
if dep := self._get(obj):
return dep
Expand All @@ -38,33 +40,33 @@ def add(self, obj: Type[T], **kwargs) -> Type[T]:

return wrapped

def get(self, obj: Type[T]) -> Type[T] | None:
def get(self, obj: type[T]) -> type[T] | None:
with self._lock:
return self._get(obj)

def iter_instances(self, *, reverse: bool = False) -> Iterable[tuple[Type[T], T]]:
def iter_instances(self, *, reverse: bool = False) -> Iterable[tuple[type[T], T]]:
with self._lock:
deps_iter: Iterable = list(
reversed(self._deps.values()) if reverse else self._deps.values()
reversed(self._deps.values()) if reverse else self._deps.values(),
)

for dep in deps_iter:
if dep.instance:
yield dep.object, dep.instance

def _get(self, obj: Type[T]) -> Type[T] | None:
def _get(self, obj: type[T]) -> type[T] | None:
dep = self._deps.get(obj)
return dep.object if dep else None


def _wrap(obj: Type[T], *args, **kwargs) -> Type[T]:
def _wrap(obj: type[T], *args, **kwargs) -> type[T]:
if not inspect.isclass(obj):
partial = functools.wraps(obj)(functools.partial(obj, *args, **kwargs))
return cast(Type[T], partial)
return cast(type[T], partial)

_instance = None

def __new__(cls):
def new(_):
nonlocal _instance

if _instance is not None:
Expand All @@ -73,20 +75,28 @@ def __new__(cls):
_instance = obj(*args, **kwargs)
return _instance

# This class wraps obj and creates a singleton class that uses *args and **kwargs for initialization.
# Please note that it also doesn’t allow passing additional args after creating this partial.
# It was made to make it more obvious that you can’t create two different instances with different parameters.
# This class wraps obj and creates a singleton class
# that uses *args and **kwargs for initialization.
#
# Please note that it also doesn't allow passing additional args
# after creating this partial.
# It was made to make it more obvious that you can't create
# two different instances with different parameters.
#
# Example:
# injected_redis = injector.inject(Redis)
# injected_redis() # works
# injected_redis(timeout=10) # doesnt work
# >>> injected_redis = injector.inject(Redis)
# >>> injected_redis() # works
# >>> injected_redis(timeout=10) # doesn't work
#
# Here we manually create a new class using the `type` metaclass to prevent possible overrides of it.
# We copy all object attributes (__dict__) so that upon inspection,
# Here we manually create a new class using the `type` metaclass
# to prevent possible overrides of it.
# We copy all object attributes (__dict__)
# so that upon inspection,
# the class should look exactly like the wrapped class.
# However, we override the __new__ method to return an instance of the original class.
# However, we override the __new__ method
# to return an instance of the original class.
# Since the original class was not modified, it will use its own metaclass.
SingletonPartialCls = functools.wraps(
return functools.wraps(
obj,
updated=(),
)(
Expand All @@ -95,9 +105,7 @@ def __new__(cls):
(obj,),
{
**obj.__dict__,
"__new__": __new__,
"__new__": new,
},
)
),
)

return SingletonPartialCls
Loading

0 comments on commit 92b3828

Please sign in to comment.