Skip to content

Commit

Permalink
Improved documentation for singleton provider and minor fixes (#92)
Browse files Browse the repository at this point in the history
* Fixed AttrGetter bug with fastapi.

* Changed AttrGetter to raise an exception.

* Rework Attrgetter exception.

* Added __init__ call for parent classes and improved some docstrings.

* Improve singleton documentation.

* Fixed typos.

* Reformatted code.

* Added pre-commit.

* Changed pre-commit to use local config.

---------

Co-authored-by: Alexander <e1634240@student.tuwien.ac.at>
  • Loading branch information
alexanderlazarev0 and Alexander authored Sep 29, 2024
1 parent 18e16fb commit 4704255
Show file tree
Hide file tree
Showing 10 changed files with 104 additions and 9 deletions.
10 changes: 10 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
repos:
- repo: local
hooks:
- id: lint
name: lint
entry: just
args: [lint]
language: system
types: [python]
pass_filenames: false
78 changes: 69 additions & 9 deletions docs/providers/singleton.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,77 @@
# Singleton
- Initialized only once, but without teardown logic.
- Class or simple function is allowed.
# Singleton Provider

Singleton providers resolve the dependency only once and cache the resolved instance for future injections.

## How it works

```python
import dataclasses

import random
from that_depends import providers

def some_function():
"""Generate number between 0.0 and 1.0"""
return random.random()

# create a Singleton provider
prov = providers.Singleton(some_function)

# provider with call `some_func` and cache the return value
prov.sync_resolve() # 0.3
# provider with return the cached value
prov.sync_resolve() # 0.3
```

## Example with `pydantic-settings`

Lets say we are storing our application configuration using [pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/):

```python
from pydantic_settings import BaseSettings
from that_depends import BaseContainer, providers

class DatabaseConfig(BaseModel):
address: str = "127.0.0.1"
port: int = 5432
db_name: str = "postgres"

@dataclasses.dataclass(kw_only=True, slots=True)
class SingletonFactory:
dep1: bool
class Settings(BaseSettings):
auth_key: str = "my_auth_key"
db: DatabaseConfig = DatabaseConfig()
```

Because we do not want to resolve the configuration each time it is used in our application, we provide it using the `Singleton` provider.

class DIContainer(BaseContainer):
singleton = providers.Singleton(SingletonFactory, dep1=True)
```python
async def get_db_connection(address: str, port:int, db_name: str) -> Connection:
...

class MyContainer(BaseContainer):
config = providers.Singleton(Settings)
# provide connection arguments and create a connection provider
db_connection = providers.AsyncFactory(
get_db_connection, config.db.address, config.db.port, config.db_name:
)
```

Now we can inject our database connection where it required using `@inject`:

```python
from that_depends import inject, Provide

@inject
async def with_db_connection(conn: Connection = Provide[MyContainer.db_connection]):
...
```

Of course we can also resolve the whole configuration without accessing attributes by running:

```python
# sync resolution
config: Settings = MyContainer.config.sync_resolve()
# async resolution
config: Settings = await MyContainer.config.async_resolve()
# inject the configuration into a function
async def with_config(config: Settings = Provide[MyContainer.config]):
assert config.auth_key == "my_auth_key"
```
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ dev-dependencies = [
"ruff",
"mypy==1.10.1",
"typing-extensions",
"pre-commit"
]

[build-system]
Expand Down
1 change: 1 addition & 0 deletions that_depends/providers/attr_getter.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class AttrGetter(
__slots__ = "_provider", "_attrs"

def __init__(self, provider: AbstractProvider[T], attr_name: str) -> None:
super().__init__()
self._provider = provider
self._attrs = [attr_name]

Expand Down
17 changes: 17 additions & 0 deletions that_depends/providers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
class AbstractProvider(typing.Generic[T_co], abc.ABC):
"""Abstract Provider Class."""

def __init__(self) -> None:
super().__init__()
self._override: typing.Any = None

@abc.abstractmethod
async def async_resolve(self) -> T_co:
"""Resolve dependency asynchronously."""
Expand Down Expand Up @@ -45,6 +49,7 @@ def cast(self) -> T_co:
"""Returns self, but cast to the type of the provided value.
This helps to pass providers as input to other providers while avoiding type checking errors:
:example:
class A: ...
Expand All @@ -62,6 +67,12 @@ class ResourceContext(typing.Generic[T_co]):
__slots__ = "context_stack", "instance", "resolving_lock", "is_async"

def __init__(self, is_async: bool) -> None:
"""Create a new ResourceContext instance.
:param is_async: Whether the ResourceContext was created in an async context.
For example within a ``async with container_context(): ...`` statement.
:type is_async: bool
"""
self.instance: T_co | None = None
self.resolving_lock: typing.Final = asyncio.Lock()
self.context_stack: contextlib.AsyncExitStack | contextlib.ExitStack | None = None
Expand All @@ -80,6 +91,7 @@ def is_context_stack_sync(
return isinstance(context_stack, contextlib.ExitStack)

async def tear_down(self) -> None:
"""Async tear down the context stack."""
if self.context_stack is None:
return

Expand All @@ -91,6 +103,10 @@ async def tear_down(self) -> None:
self.instance = None

def sync_tear_down(self) -> None:
"""Sync tear down the context stack.
:raises RuntimeError: If the context stack is async and the tear down is called in sync mode.
"""
if self.context_stack is None:
return

Expand All @@ -110,6 +126,7 @@ def __init__(
*args: P.args,
**kwargs: P.kwargs,
) -> None:
super().__init__()
if inspect.isasyncgenfunction(creator):
self._is_async = True
elif inspect.isgeneratorfunction(creator):
Expand Down
2 changes: 2 additions & 0 deletions that_depends/providers/collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class List(AbstractProvider[list[T]]):
__slots__ = ("_providers",)

def __init__(self, *providers: AbstractProvider[T]) -> None:
super().__init__()
self._providers: typing.Final = providers

async def async_resolve(self) -> list[T]:
Expand All @@ -26,6 +27,7 @@ class Dict(AbstractProvider[dict[str, T]]):
__slots__ = ("_providers",)

def __init__(self, **providers: AbstractProvider[T]) -> None:
super().__init__()
self._providers: typing.Final = providers

async def async_resolve(self) -> dict[str, T]:
Expand Down
1 change: 1 addition & 0 deletions that_depends/providers/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class Factory(AbstractFactory[T]):
__slots__ = "_factory", "_args", "_kwargs", "_override"

def __init__(self, factory: type[T] | typing.Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> None:
super().__init__()
self._factory: typing.Final = factory
self._args: typing.Final = args
self._kwargs: typing.Final = kwargs
Expand Down
1 change: 1 addition & 0 deletions that_depends/providers/object.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class Object(AbstractProvider[T]):
__slots__ = ("_obj",)

def __init__(self, obj: T) -> None:
super().__init__()
self._obj: typing.Final = obj

async def async_resolve(self) -> T:
Expand Down
1 change: 1 addition & 0 deletions that_depends/providers/selector.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class Selector(AbstractProvider[T]):
__slots__ = "_selector", "_providers", "_override"

def __init__(self, selector: typing.Callable[[], str], **providers: AbstractProvider[T]) -> None:
super().__init__()
self._selector: typing.Final = selector
self._providers: typing.Final = providers
self._override = None
Expand Down
1 change: 1 addition & 0 deletions that_depends/providers/singleton.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class Singleton(AbstractProvider[T]):
__slots__ = "_factory", "_args", "_kwargs", "_override", "_instance", "_resolving_lock"

def __init__(self, factory: type[T] | typing.Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> None:
super().__init__()
self._factory: typing.Final = factory
self._args: typing.Final = args
self._kwargs: typing.Final = kwargs
Expand Down

0 comments on commit 4704255

Please sign in to comment.