Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Simply apply the decorators and the package takes care of the rest.
```python
from injection import injectable, inject, singleton


@singleton
class Printer:
def __init__(self):
Expand All @@ -29,6 +30,7 @@ class Printer:
self.history.append(message)
print(message)


@injectable
class Service:
def __init__(self, printer: Printer):
Expand All @@ -37,10 +39,12 @@ class Service:
def hello(self):
self.printer.print("Hello world!")


@inject
def main(service: Service):
service.hello()


if __name__ == "__main__":
main()
```
6 changes: 4 additions & 2 deletions docs/guides/imports.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,12 @@ For more control over which modules get imported, use `PythonModuleLoader` with
from injection.loaders import PythonModuleLoader
from src import adapters, services


def predicate(module_name: str) -> bool:
# Only import modules containing "impl" in their name
return "impl" in module_name


PythonModuleLoader(predicate).load(adapters, services)
```

Expand Down Expand Up @@ -67,7 +69,7 @@ This last approach is particularly useful for explicitly marking which modules s
```python
# auto-import


@injectable(on=AbstractDependency)
class Dependency(AbstractDependency):
...
class Dependency(AbstractDependency): ...
```
14 changes: 8 additions & 6 deletions docs/guides/main-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ from injection import adefine_scope
from injection.entrypoint import AsyncEntrypoint, Entrypoint, entrypointmaker
from injection.loaders import PythonModuleLoader


@entrypointmaker
def entrypoint[**P, T](self: AsyncEntrypoint[P, T]) -> Entrypoint[P, T]:
import src
Expand All @@ -32,6 +33,7 @@ async def main(dependency: Dependency):
# All setup is automatically applied
...


if __name__ == "__main__":
main()
```
Expand Down Expand Up @@ -60,6 +62,7 @@ from injection.loaders import ProfileLoader, PythonModuleLoader

profile_loader = ProfileLoader(...)


@entrypointmaker(profile_loader=profile_loader)
def entrypoint[**P, T](self: Entrypoint[P, T]) -> Entrypoint[P, T]:
import src
Expand All @@ -84,27 +87,26 @@ from injection.entrypoint import Entrypoint, entrypointmaker
from injection.loaders import PythonModuleLoader
from os import getenv


@dataclass
class Config:
profile: Profile


@constant
def _config_factory() -> Config:
profile = Profile(getenv("PROFILE", "development"))
return Config(profile)


@entrypointmaker(profile_loader=profile_loader)
def entrypoint[**P, T](self: Entrypoint[P, T], config: Config) -> Entrypoint[P, T]:
import src

profile = config.profile # Use config to determine profile
suffixes = self.profile_loader.required_module_names(profile)
module_loader = PythonModuleLoader.endswith(*suffixes)
return (
self.inject()
.load_profile(profile)
.load_modules(module_loader, src)
)
return self.inject().load_profile(profile).load_modules(module_loader, src)
```

In this example, `config` is resolved from the default module and used to dynamically load the appropriate profile.
Expand Down
12 changes: 8 additions & 4 deletions docs/guides/profiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ Storing your profile names in a `StrEnum` is recommended for type safety and con
from enum import StrEnum
from injection import mod


class Profile(StrEnum):
DEV = "development"
PROD = "production"
STAGING = "staging"


# Get a module for a specific profile
dev_module = mod(Profile.DEV)
```
Expand Down Expand Up @@ -44,10 +46,12 @@ For more complex scenarios where profiles share common subsets of dependencies,
```python
from injection.loaders import ProfileLoader

profile_loader = ProfileLoader({
Profile.DEV: [SubProfile.STUB],
Profile.TEST: [SubProfile.STUB],
})
profile_loader = ProfileLoader(
{
Profile.DEV: [SubProfile.STUB],
Profile.TEST: [SubProfile.STUB],
}
)

profile_loader.load(Profile.DEV)
```
Expand Down
43 changes: 23 additions & 20 deletions docs/guides/register-dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ A new instance is created every time the dependency is resolved.
```python
from injection import injectable


@injectable
class Dependency:
...
class Dependency: ...
```

## Singleton
Expand All @@ -21,9 +21,9 @@ A single instance is created and shared across the entire application.
```python
from injection import singleton


@singleton
class Dependency:
...
class Dependency: ...
```

## Constant
Expand All @@ -33,10 +33,12 @@ Register a pre-existing value as a dependency.
from dataclasses import dataclass
from injection import set_constant


@dataclass(frozen=True)
class Settings:
api_key: str


settings = set_constant(Settings("<secret_api_key>"))
```

Expand All @@ -53,9 +55,9 @@ For lazy constants, use the `@constant` decorator:
```python
from injection import constant


@constant
class LazySettings:
...
class LazySettings: ...
```

## Factories
Expand All @@ -66,8 +68,9 @@ _Make sure not to forget the return type annotation._
```python
from injection import injectable

class Dependency:
...

class Dependency: ...


@injectable
def _dependency_factory() -> Dependency:
Expand All @@ -85,12 +88,12 @@ Register an implementation for an abstract class or protocol.
from injection import injectable
from abc import ABC

class AbstractDependency(ABC):
...

class AbstractDependency(ABC): ...


@injectable(on=AbstractDependency)
class Dependency(AbstractDependency):
...
class Dependency(AbstractDependency): ...
```

## Scoped
Expand All @@ -99,9 +102,9 @@ A single instance is created per scope. Using a `StrEnum` for scope names is rec
```python
from injection import scoped


@scoped("<scope_name>")
class Dependency:
...
class Dependency: ...
```

## Scoped with context manager
Expand All @@ -114,12 +117,12 @@ Scoped dependencies can be registered using generator functions (sync or async)
from collections.abc import Iterator
from injection import scoped


class Dependency:
def open(self):
...
def open(self): ...

def close(self): ...

def close(self):
...

@scoped("<scope_name>")
def dependency_factory() -> Iterator[Dependency]:
Expand All @@ -137,7 +140,7 @@ Register a dependency for a specific profile. Using a `StrEnum` for profile name
```python
from injection import mod


@mod("<profile_name>").injectable
class Dependency:
...
class Dependency: ...
```
18 changes: 11 additions & 7 deletions docs/guides/resolve-dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ The `@inject` decorator automatically resolves function parameters based on thei
```python
from injection import inject


@inject
def function(dependency: Dependency):
...
def function(dependency: Dependency): ...


function() # You can now call `function` without arguments
```
Expand All @@ -23,17 +24,17 @@ When `@inject` is applied to an async function, it can resolve dependencies that
Static type checkers like mypy will complain about missing arguments when calling injected functions:
```python
@inject
def function(dependency: Dependency):
...
def function(dependency: Dependency): ...


function() # ❌ mypy error: Missing positional argument "dependency" in call to "function" [call-arg]
```

To fix this, provide a default value for injected parameters:
```python
@inject
def function(dependency: Dependency = NotImplemented):
...
def function(dependency: Dependency = NotImplemented): ...


function() # ✅ OK
```
Expand All @@ -47,6 +48,7 @@ The `@asfunction` decorator provides an alternative to `@inject` for cases where
from injection import asfunction
from typing import NamedTuple


@asfunction
class Function(NamedTuple):
dependency: Dependency
Expand All @@ -55,6 +57,7 @@ class Function(NamedTuple):
# Use self.dependency here
...


# Call with only the runtime parameters
Function("foo", "bar", "baz")
```
Expand Down Expand Up @@ -126,9 +129,10 @@ The async version of `get_lazy_instance` returns an awaitable instead of an inve
```python
from injection import LazyInstance


class Class:
dependency = LazyInstance(Dependency)

def do_something(self):
self.dependency.some_method() # Resolved on every access
```
Expand Down
4 changes: 3 additions & 1 deletion docs/guides/scopes.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,14 @@ from injection import MappedScope

type Locale = str


@dataclass
class Bindings:
locale: Locale

scope = MappedScope("<scope_name>")


with Bindings("fr_FR").scope.define():
# Dependencies can now access the locale binding
...
Expand Down
4 changes: 4 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Simply apply the decorators and the package takes care of the rest.
```python
from injection import injectable, inject, singleton


@singleton
class Printer:
def __init__(self):
Expand All @@ -46,6 +47,7 @@ class Printer:
self.history.append(message)
print(message)


@injectable
class Service:
def __init__(self, printer: Printer):
Expand All @@ -54,10 +56,12 @@ class Service:
def hello(self):
self.printer.print("Hello world!")


@inject
def main(service: Service):
service.hello()


if __name__ == "__main__":
main()
```
Loading