Skip to content

Commit a7e4e23

Browse files
authored
feat: 📝 Enhanced documentation
1 parent b6f5d8f commit a7e4e23

12 files changed

Lines changed: 184 additions & 97 deletions

File tree

README.md

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
# python-injection
22

33
[![CI](https://github.com/100nm/python-injection/actions/workflows/ci.yml/badge.svg)](https://github.com/100nm/python-injection)
4-
[![PyPI](https://img.shields.io/pypi/v/python-injection.svg?color=blue)](https://pypi.org/project/python-injection)
4+
[![PyPI - Version](https://img.shields.io/pypi/v/python-injection.svg?color=blue)](https://pypi.org/project/python-injection)
5+
[![PyPI - Downloads](https://img.shields.io/pypi/dm/python-injection.svg?color=blue)](https://pypistats.org/packages/python-injection)
56
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
67

7-
Fast and easy dependency injection framework.
8-
98
## Installation
109

1110
⚠️ _Requires Python 3.12 or higher_
@@ -14,12 +13,22 @@ Fast and easy dependency injection framework.
1413
pip install python-injection
1514
```
1615

16+
## Features
17+
18+
* Automatic dependency resolution based on type hints.
19+
* Support for multiple dependency lifetimes: `transient`, `singleton`, `constant`, and `scoped`.
20+
* Works seamlessly in both `async` and `sync` environments.
21+
* Separation of dependency sets using modules.
22+
* Runtime switching between different sets of dependencies.
23+
* Centralized setup logic using entrypoints.
24+
* Built-in type annotation for easy integration with [`FastAPI`](https://github.com/fastapi/fastapi).
25+
* Lazy dependency resolution for optimized performance.
26+
1727
## Motivations
1828

1929
1. Easy to use
2030
2. No impact on class and function definitions
21-
3. Easily interchangeable dependencies _(depending on the runtime environment, for example)_
22-
4. No prerequisites
31+
3. No tedious configuration
2332

2433
## Quick start
2534

@@ -64,5 +73,6 @@ if __name__ == "__main__":
6473
* [**Advanced usage**](https://github.com/100nm/python-injection/tree/prod/documentation/advanced-usage.md)
6574
* [**Loaders**](https://github.com/100nm/python-injection/tree/prod/documentation/loaders.md)
6675
* [**Entrypoint**](https://github.com/100nm/python-injection/tree/prod/documentation/entrypoint.md)
67-
* [**Integrations**](https://github.com/100nm/python-injection/tree/prod/documentation/integrations.md)
76+
* [**Integrations**](https://github.com/100nm/python-injection/tree/prod/documentation/integrations)
77+
* [**FastAPI**](https://github.com/100nm/python-injection/tree/prod/documentation/integrations/fastapi.md)
6878
* [**Concrete example**](https://github.com/100nm/python-injection-example)

documentation/advanced-usage.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ def some_function(service_a: ServiceA, service_b: ServiceB):
4141

4242
### Module interconnections
4343

44+
> [!IMPORTANT]
45+
> This section contains operations that can be performed between modules. In most cases, you don't need to worry about
46+
> them.
47+
>
48+
> Instead, use [`ProfileLoader`](loaders.md#ProfileLoader) or [`load_profile`](loaders.md#load_profile), which are much
49+
> simpler and easier to understand.
50+
4451
> **Use a module**
4552
4653
When a module is used by another module, the module's dependencies are replaced by those of the module used.

documentation/entrypoint.md

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,52 @@ or `injectables`, because everything is not yet fully configured at this stage.
2020

2121
**Instruction order matters**: each configuration step applies a decorator and returns a new `Entrypoint` instance.
2222

23+
Here's all you can do with an entrypoint _(take only what you need)_:
24+
2325
```python
2426
# src/entrypoint.py
2527

28+
from enum import StrEnum, auto
29+
2630
import uvloop
27-
from injection import adefine_scope
31+
from injection import adefine_scope, mod
2832
from injection.entrypoint import AsyncEntrypoint, Entrypoint, entrypointmaker
29-
from injection.loaders import PythonModuleLoader
33+
from injection.loaders import ProfileLoader, PythonModuleLoader
34+
from pydantic_settings import BaseSettings
35+
36+
class Profile(StrEnum):
37+
DEFAULT = mod().name
38+
DEV = "dev"
39+
STAGING = "staging"
40+
PROD = "prod"
41+
42+
class SubProfile(StrEnum):
43+
CONF = "conf"
44+
45+
class Scope(StrEnum):
46+
LIFESPAN = auto()
3047

31-
@entrypointmaker
32-
def entrypoint[**P, T](self: AsyncEntrypoint[P, T]) -> Entrypoint[P, T]:
48+
@mod(SubProfile.CONF).constant
49+
class Conf(BaseSettings):
50+
profile: Profile = Profile.DEFAULT
51+
52+
@entrypointmaker(profile_loader=ProfileLoader({Profile.DEFAULT: [SubProfile.CONF]}))
53+
def entrypoint[**P, T](self: AsyncEntrypoint[P, T], conf: Conf) -> Entrypoint[P, T]:
3354
import src
3455

35-
loader = PythonModuleLoader.from_keywords("# Auto-import")
56+
profile = conf.profile
57+
keyword = "# auto-import"
58+
keywords = {
59+
f"{keyword}: {name}"
60+
for name in self.profile_loader.required_module_names(profile)
61+
}
62+
module_loader = PythonModuleLoader.from_keywords(keyword, *keywords)
3663
return (
3764
self.inject()
38-
.decorate(adefine_scope("lifespan", kind="shared"))
65+
.decorate(adefine_scope(Scope.LIFESPAN, kind="shared"))
3966
.async_to_sync(uvloop.run)
40-
.load_modules(loader, src)
67+
.load_modules(module_loader, src)
68+
.load_profile(profile)
4169
)
4270
```
4371

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Integrations
2+
3+
**Integrations make it easy to connect `python-injection` to other frameworks.**
Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
1-
# Integrations
1+
# [FastAPI](https://github.com/fastapi/fastapi)
22

3-
**Integrations make it easy to connect `python-injection` to other frameworks.**
4-
5-
## [FastAPI](https://github.com/fastapi/fastapi)
6-
7-
### Inject a dependency
3+
## Inject a dependency
84

95
Here's how to inject an instance into a FastAPI endpoint.
106

@@ -16,13 +12,13 @@ async def my_endpoint(service: Inject[MyService]) -> None:
1612
...
1713
```
1814

19-
### Useful scopes
15+
## Useful scopes
2016

2117
Two fairly common scopes in FastAPI:
2218
* **Application lifespan scope**: associate with application lifespan.
2319
* **Request scope**: associate with http request lifetime.
2420

25-
_For a better understanding of the scopes, [here's the associated documentation](scoped-dependencies.md)._
21+
_For a better understanding of the scopes, [here's the associated documentation](../scoped-dependencies.md)._
2622

2723
Here's how to configure FastAPI:
2824

documentation/loaders.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ PythonModuleLoader(predicate).load(package)
3838
Automatically imports modules whose Python script contains one of the keywords passed in parameter.
3939

4040
```python
41-
PythonModuleLoader.from_keywords("# Auto-import").load(package)
41+
PythonModuleLoader.from_keywords("# auto-import").load(package)
4242
```
4343

4444
* `startswith`

injection/__init__.pyi

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ class Module:
115115
parameter type annotations. If applied to a class, the dependencies resolved
116116
will be those of the `__init__` method.
117117
118-
With `threadsafe=True`, the injection logic is wrapped in a `threading.Lock`.
118+
With `threadsafe=True`, the injection logic is wrapped in a `threading.RLock`.
119119
"""
120120

121121
def injectable[**P, T](
@@ -171,18 +171,18 @@ class Module:
171171
registered.
172172
"""
173173

174-
def constant[T](
174+
def constant[**P, T](
175175
self,
176-
wrapped: type[T] = ...,
176+
wrapped: _Recipe[P, T] = ...,
177177
/,
178178
*,
179179
on: _TypeInfo[T] = ...,
180180
mode: Mode | ModeStr = ...,
181181
) -> Any:
182182
"""
183-
Decorator applicable to a class. It is used to indicate how the constant is
184-
constructed. At injection time, the injected instance will always be the same.
185-
Unlike `@singleton`, dependencies will not be resolved.
183+
Decorator applicable to a class or function. It is used to indicate how the
184+
constant is constructed. At injection time, the injected instance will always
185+
be the same. Unlike `@singleton`, dependencies will not be resolved.
186186
"""
187187

188188
def set_constant[T](

injection/_core/module.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -503,15 +503,15 @@ def decorator(wp: type[T]) -> type[T]:
503503

504504
return decorator(wrapped) if wrapped else decorator
505505

506-
def constant[T](
506+
def constant[**P, T](
507507
self,
508-
wrapped: type[T] | None = None,
508+
wrapped: Recipe[P, T] | None = None,
509509
/,
510510
*,
511511
on: TypeInfo[T] = (),
512512
mode: Mode | ModeStr = Mode.get_default(),
513513
) -> Any:
514-
def decorator(wp: type[T]) -> type[T]:
514+
def decorator(wp: Recipe[P, T]) -> Recipe[P, T]:
515515
lazy_instance = lazy(wp)
516516
self.injectable(
517517
lambda: ~lazy_instance,
@@ -1087,7 +1087,7 @@ def decorator(wp: Callable[_P, _T]) -> Callable[_P, _T]:
10871087
return decorator(wrapped) if wrapped else decorator
10881088

10891089
@singledispatchmethod
1090-
def on_event(self, event: Event, /) -> ContextManager[None] | None: # type: ignore[override]
1090+
def on_event(self, event: Event, /) -> ContextManager[None] | None:
10911091
return None
10921092

10931093
@on_event.register

injection/loaders.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,11 @@ def init(self) -> Self:
164164

165165
def load(self, name: str, /) -> LoadedProfile:
166166
self.init()
167-
target_module = self.__init_subsets_for(mod(name))
168-
self.module.use(target_module, priority=Priority.HIGH)
167+
168+
if not self.__is_default_module(name):
169+
target_module = self.__init_subsets_for(mod(name))
170+
self.module.use(target_module, priority=Priority.HIGH)
171+
169172
return _UserLoadedProfile(self, name)
170173

171174
def _unload(self, name: str, /) -> None:
@@ -182,6 +185,9 @@ def __init_subsets_for(self, module: Module) -> Module:
182185

183186
return module
184187

188+
def __is_default_module(self, module_name: str) -> bool:
189+
return module_name == self.module.name
190+
185191
def __is_initialized(self, module: Module) -> bool:
186192
return module.name in self.__initialized_modules
187193

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ dev = [
1515
]
1616
doc = [
1717
"fastapi",
18+
"pydantic-settings",
1819
"typer",
1920
"uvloop",
2021
]
@@ -46,6 +47,8 @@ classifiers = [
4647
"Programming Language :: Python",
4748
"Programming Language :: Python :: 3",
4849
"Programming Language :: Python :: 3 :: Only",
50+
"Programming Language :: Python :: 3.12",
51+
"Programming Language :: Python :: 3.13",
4952
"Operating System :: OS Independent",
5053
"Intended Audience :: Developers",
5154
"Natural Language :: English",

0 commit comments

Comments
 (0)