appctx 0.2.0__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- appctx-0.2.2/PKG-INFO +272 -0
- appctx-0.2.2/README.md +245 -0
- {appctx-0.2.0 → appctx-0.2.2}/src/appctx/__init__.py +1 -1
- {appctx-0.2.0 → appctx-0.2.2}/src/appctx/container.py +4 -4
- appctx-0.2.2/src/appctx.egg-info/PKG-INFO +272 -0
- {appctx-0.2.0 → appctx-0.2.2}/tests/test_container.py +51 -0
- {appctx-0.2.0 → appctx-0.2.2}/tests/test_post_construct.py +2 -0
- appctx-0.2.0/PKG-INFO +0 -511
- appctx-0.2.0/README.md +0 -484
- appctx-0.2.0/src/appctx.egg-info/PKG-INFO +0 -511
- {appctx-0.2.0 → appctx-0.2.2}/pyproject.toml +0 -0
- {appctx-0.2.0 → appctx-0.2.2}/setup.cfg +0 -0
- {appctx-0.2.0 → appctx-0.2.2}/src/appctx/decorators.py +0 -0
- {appctx-0.2.0 → appctx-0.2.2}/src/appctx/py.typed +0 -0
- {appctx-0.2.0 → appctx-0.2.2}/src/appctx.egg-info/SOURCES.txt +0 -0
- {appctx-0.2.0 → appctx-0.2.2}/src/appctx.egg-info/dependency_links.txt +0 -0
- {appctx-0.2.0 → appctx-0.2.2}/src/appctx.egg-info/requires.txt +0 -0
- {appctx-0.2.0 → appctx-0.2.2}/src/appctx.egg-info/top_level.txt +0 -0
- {appctx-0.2.0 → appctx-0.2.2}/tests/test_decorators.py +0 -0
appctx-0.2.2/PKG-INFO
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: appctx
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: Spring style dependency injection for python
|
|
5
|
+
Author-email: wssccc <wssccc@qq.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/wssccc/appctx
|
|
8
|
+
Project-URL: Repository, https://github.com/wssccc/appctx
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/wssccc/appctx/issues
|
|
10
|
+
Project-URL: Documentation, https://github.com/wssccc/appctx#readme
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Python: >=3.8
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
22
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
23
|
+
Requires-Dist: black>=22.0; extra == "dev"
|
|
24
|
+
Requires-Dist: flake8>=5.0; extra == "dev"
|
|
25
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
26
|
+
Requires-Dist: isort>=5.0; extra == "dev"
|
|
27
|
+
|
|
28
|
+
# AppCtx
|
|
29
|
+
|
|
30
|
+
Spring-style dependency injection for Python
|
|
31
|
+
|
|
32
|
+
[](https://python.org)
|
|
33
|
+
[](https://github.com/wssccc/appctx/blob/main/LICENSE)
|
|
34
|
+
[](https://pypi.org/project/appctx/)
|
|
35
|
+
|
|
36
|
+
## Overview
|
|
37
|
+
|
|
38
|
+
AppCtx is a lightweight dependency injection (DI) container for Python, inspired by the Spring Framework. It uses decorator-based bean registration and auto-wiring via type annotations to manage dependencies in a clean, testable way.
|
|
39
|
+
|
|
40
|
+
**Python Requirements**: 3.8+ | **License**: MIT
|
|
41
|
+
|
|
42
|
+
### Features
|
|
43
|
+
|
|
44
|
+
- 🚀 **Decorator-based registration** — `@bean` / `@component` to register, `@post_construct` for lifecycle hooks
|
|
45
|
+
- 🔄 **Auto-wiring** — Dependencies resolved from type annotations automatically
|
|
46
|
+
- 🔍 **Circular dependency detection** — Caught at startup, not runtime
|
|
47
|
+
- 📦 **Lightweight** — Zero mandatory dependencies beyond stdlib
|
|
48
|
+
- 🐍 **Pythonic API** — Feels natural in Python, not a Java port
|
|
49
|
+
- 🏗️ **Multiple contexts** — Isolated containers for different scopes
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install appctx
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Quick Start
|
|
58
|
+
|
|
59
|
+
### 1. Define and register beans
|
|
60
|
+
|
|
61
|
+
Use `@bean` on functions that create your objects. Dependencies are declared via type annotations:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from appctx import bean, get_bean, refresh
|
|
65
|
+
|
|
66
|
+
class DatabaseService:
|
|
67
|
+
def __init__(self, connection_string: str = "sqlite:///default.db"):
|
|
68
|
+
self.connection_string = connection_string
|
|
69
|
+
|
|
70
|
+
def connect(self):
|
|
71
|
+
return f"Connected to {self.connection_string}"
|
|
72
|
+
|
|
73
|
+
class UserService:
|
|
74
|
+
def __init__(self, db: DatabaseService):
|
|
75
|
+
self.db = db
|
|
76
|
+
|
|
77
|
+
def get_user(self, user_id: int):
|
|
78
|
+
return f"User {user_id} from {self.db.connect()}"
|
|
79
|
+
|
|
80
|
+
# Register beans
|
|
81
|
+
@bean
|
|
82
|
+
def database_service():
|
|
83
|
+
return DatabaseService("postgresql://localhost/myapp")
|
|
84
|
+
|
|
85
|
+
@bean
|
|
86
|
+
def user_service(db: DatabaseService): # Auto-injected by type
|
|
87
|
+
return UserService(db)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 2. Initialize and use
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
# Initialize the container — resolves all dependencies
|
|
94
|
+
refresh()
|
|
95
|
+
|
|
96
|
+
# Retrieve the root application object (the only place you should call get_bean)
|
|
97
|
+
user_svc = get_bean(UserService)
|
|
98
|
+
print(user_svc.get_user(123))
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
> **Note:** `get_bean()` is only needed at the **entry point** to bootstrap your application. All other dependencies are injected automatically via constructor or function parameters — just like Spring.
|
|
102
|
+
|
|
103
|
+
### Class-style beans
|
|
104
|
+
|
|
105
|
+
Decorate classes directly — the container instantiates them with resolved dependencies:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from appctx import bean, get_bean, refresh
|
|
109
|
+
|
|
110
|
+
@bean
|
|
111
|
+
class EmailService:
|
|
112
|
+
def __init__(self):
|
|
113
|
+
self.server = "smtp.example.com"
|
|
114
|
+
|
|
115
|
+
def send_email(self, to: str, subject: str):
|
|
116
|
+
return f"Email sent to {to} via {self.server}"
|
|
117
|
+
|
|
118
|
+
@bean
|
|
119
|
+
class NotificationService:
|
|
120
|
+
def __init__(self, email: EmailService):
|
|
121
|
+
self.email = email
|
|
122
|
+
|
|
123
|
+
def notify(self, user: str, message: str):
|
|
124
|
+
return self.email.send_email(user, "Notification", message)
|
|
125
|
+
|
|
126
|
+
refresh()
|
|
127
|
+
|
|
128
|
+
# Entry point — the only place you should call get_bean
|
|
129
|
+
notification_svc = get_bean(NotificationService)
|
|
130
|
+
print(notification_svc.notify("user@example.com", "Hello World!"))
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Post-construct lifecycle
|
|
134
|
+
|
|
135
|
+
Use `@post_construct` to run initialization logic after all beans are created:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from appctx import post_construct
|
|
139
|
+
|
|
140
|
+
class DatabaseService:
|
|
141
|
+
def __init__(self):
|
|
142
|
+
self.connection = None
|
|
143
|
+
|
|
144
|
+
@post_construct
|
|
145
|
+
def init(self):
|
|
146
|
+
self.connection = create_connection() # Called automatically after construction
|
|
147
|
+
|
|
148
|
+
@bean
|
|
149
|
+
def database_service():
|
|
150
|
+
return DatabaseService()
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
> **Note:** `@post_construct` methods run after **all** beans are created, so you can safely reference other beans during initialization.
|
|
154
|
+
|
|
155
|
+
### How It Works
|
|
156
|
+
|
|
157
|
+
1. **Bean Registration**: Use the `@bean` decorator to register functions/classes that create beans
|
|
158
|
+
2. **Auto-wiring**: Dependencies are resolved from type annotations and injected automatically — no manual `get_bean()` calls needed
|
|
159
|
+
3. **Container Initialization**: Call `refresh()` to instantiate all beans in dependency order
|
|
160
|
+
4. **Entry Point**: Use `get_bean()` **once** at the application root to retrieve the assembled object graph
|
|
161
|
+
|
|
162
|
+
## Core Concepts
|
|
163
|
+
|
|
164
|
+
| Concept | Description |
|
|
165
|
+
|---|---|
|
|
166
|
+
| **Bean** | An object managed by the container |
|
|
167
|
+
| **ApplicationContext** | The DI container that holds and wires beans |
|
|
168
|
+
| **`@bean`** | Decorator that registers a function/class as a bean factory (alias: `@component`) |
|
|
169
|
+
| **`refresh()`** | Initializes the container, instantiates all beans in dependency order |
|
|
170
|
+
| **`get_bean(T)`** | Retrieve a bean by type or name (low-level; prefer auto-wiring) |
|
|
171
|
+
| **`get_beans(T)`** | Retrieve all beans of a given type (low-level; prefer auto-wiring) |
|
|
172
|
+
|
|
173
|
+
### Dependency Resolution Rules
|
|
174
|
+
|
|
175
|
+
1. **Positional arguments** → resolved by type annotation
|
|
176
|
+
2. **Keyword-only arguments** (`*, name`) → resolved by parameter name, falls back to default
|
|
177
|
+
3. **`**kwargs`** → receives all remaining beans not consumed by other parameters
|
|
178
|
+
|
|
179
|
+
## Global Default Context
|
|
180
|
+
|
|
181
|
+
AppCtx provides a **global default context** for convenience, similar to Spring's application context. The top-level functions (`bean`, `refresh`, `get_bean`, `add`) operate on this shared instance:
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
from appctx import bean, refresh, get_bean
|
|
185
|
+
|
|
186
|
+
# These all use the same global ApplicationContext behind the scenes
|
|
187
|
+
@bean
|
|
188
|
+
def my_service():
|
|
189
|
+
return MyService()
|
|
190
|
+
|
|
191
|
+
refresh()
|
|
192
|
+
app = get_bean(MyService)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Custom Context (Isolation)
|
|
196
|
+
|
|
197
|
+
For libraries or tests that need isolation, create your own `ApplicationContext`:
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
from appctx import ApplicationContext
|
|
201
|
+
|
|
202
|
+
ctx = ApplicationContext()
|
|
203
|
+
|
|
204
|
+
@ctx.bean
|
|
205
|
+
def my_service():
|
|
206
|
+
return MyService()
|
|
207
|
+
|
|
208
|
+
ctx.refresh()
|
|
209
|
+
app = ctx.get_bean(MyService)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
> **Note:** `@bean` on the global context registers beans immediately when the module is imported. For custom contexts, use `@ctx.bean` to register against that specific instance.
|
|
213
|
+
|
|
214
|
+
## Organizing Beans Across Modules
|
|
215
|
+
|
|
216
|
+
AppCtx does **not** perform automatic package scanning. Instead, beans are registered at import time — when Python loads a module, any `@bean` decorators execute and register with the target context.
|
|
217
|
+
|
|
218
|
+
To wire beans across multiple modules, simply **import** them before calling `refresh()`:
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
# main.py
|
|
222
|
+
from appctx import bean, refresh, get_bean
|
|
223
|
+
|
|
224
|
+
# Import modules so their @bean decorators fire
|
|
225
|
+
import myapp.services.user # registers UserService
|
|
226
|
+
import myapp.services.email # registers EmailService
|
|
227
|
+
import myapp.config # registers ConfigService
|
|
228
|
+
|
|
229
|
+
refresh()
|
|
230
|
+
app = get_bean(UserService)
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
You can also use `add()` to register a plain function or class (without the `@bean` decorator):
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
from appctx import add
|
|
237
|
+
|
|
238
|
+
def some_service():
|
|
239
|
+
return SomeService()
|
|
240
|
+
|
|
241
|
+
add(some_service) # Equivalent to @bean on some_service
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
> **Note:** For modules, simply importing them is sufficient — the `@bean` decorators fire at import time. `add(module)` is available as a semantic marker but the import alone triggers registration.
|
|
245
|
+
|
|
246
|
+
## Best Practices
|
|
247
|
+
|
|
248
|
+
1. **Rely on auto-wiring, avoid `get_bean`** — Like Spring, let the container inject dependencies via function/constructor parameters. Reserve `get_bean()` only for bootstrapping the root application object.
|
|
249
|
+
2. **Use type annotations** — They are the primary mechanism for dependency resolution. Always annotate positional parameters.
|
|
250
|
+
3. **Single responsibility per bean** — Each bean should have one clear purpose.
|
|
251
|
+
4. **Prefer constructor injection** — Dependencies go in `__init__` parameters, not hidden inside methods.
|
|
252
|
+
5. **Keep beans stateless when possible** — Makes testing and reasoning easier.
|
|
253
|
+
6. **Use `@post_construct` for initialization** — Not `__init__` side-effects. Post-construct runs after all beans exist, allowing cross-bean setup.
|
|
254
|
+
7. **Separate configuration from logic** — Use dedicated config beans instead of hardcoding values.
|
|
255
|
+
8. **Design for testability** — Beans that accept dependencies via constructor are trivially mockable in tests.
|
|
256
|
+
|
|
257
|
+
## Documentation
|
|
258
|
+
|
|
259
|
+
- **[API Reference](docs/api-reference.md)** — Full API: decorators, container ops, dependency resolution, error handling
|
|
260
|
+
- **[Development Guide](docs/development.md)** — Setup, testing, code quality, contributing
|
|
261
|
+
- **[Release Guide](RELEASE.md)** — Release process for maintainers
|
|
262
|
+
- **[Changelog](CHANGELOG.md)** — Version history
|
|
263
|
+
|
|
264
|
+
## License
|
|
265
|
+
|
|
266
|
+
MIT — see [LICENSE](https://github.com/wssccc/appctx/blob/main/LICENSE).
|
|
267
|
+
|
|
268
|
+
## Links
|
|
269
|
+
|
|
270
|
+
- **Repository**: [github.com/wssccc/appctx](https://github.com/wssccc/appctx)
|
|
271
|
+
- **PyPI**: [pypi.org/project/appctx](https://pypi.org/project/appctx/)
|
|
272
|
+
- **Issues**: [github.com/wssccc/appctx/issues](https://github.com/wssccc/appctx/issues)
|
appctx-0.2.2/README.md
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# AppCtx
|
|
2
|
+
|
|
3
|
+
Spring-style dependency injection for Python
|
|
4
|
+
|
|
5
|
+
[](https://python.org)
|
|
6
|
+
[](https://github.com/wssccc/appctx/blob/main/LICENSE)
|
|
7
|
+
[](https://pypi.org/project/appctx/)
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
AppCtx is a lightweight dependency injection (DI) container for Python, inspired by the Spring Framework. It uses decorator-based bean registration and auto-wiring via type annotations to manage dependencies in a clean, testable way.
|
|
12
|
+
|
|
13
|
+
**Python Requirements**: 3.8+ | **License**: MIT
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
- 🚀 **Decorator-based registration** — `@bean` / `@component` to register, `@post_construct` for lifecycle hooks
|
|
18
|
+
- 🔄 **Auto-wiring** — Dependencies resolved from type annotations automatically
|
|
19
|
+
- 🔍 **Circular dependency detection** — Caught at startup, not runtime
|
|
20
|
+
- 📦 **Lightweight** — Zero mandatory dependencies beyond stdlib
|
|
21
|
+
- 🐍 **Pythonic API** — Feels natural in Python, not a Java port
|
|
22
|
+
- 🏗️ **Multiple contexts** — Isolated containers for different scopes
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install appctx
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
### 1. Define and register beans
|
|
33
|
+
|
|
34
|
+
Use `@bean` on functions that create your objects. Dependencies are declared via type annotations:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from appctx import bean, get_bean, refresh
|
|
38
|
+
|
|
39
|
+
class DatabaseService:
|
|
40
|
+
def __init__(self, connection_string: str = "sqlite:///default.db"):
|
|
41
|
+
self.connection_string = connection_string
|
|
42
|
+
|
|
43
|
+
def connect(self):
|
|
44
|
+
return f"Connected to {self.connection_string}"
|
|
45
|
+
|
|
46
|
+
class UserService:
|
|
47
|
+
def __init__(self, db: DatabaseService):
|
|
48
|
+
self.db = db
|
|
49
|
+
|
|
50
|
+
def get_user(self, user_id: int):
|
|
51
|
+
return f"User {user_id} from {self.db.connect()}"
|
|
52
|
+
|
|
53
|
+
# Register beans
|
|
54
|
+
@bean
|
|
55
|
+
def database_service():
|
|
56
|
+
return DatabaseService("postgresql://localhost/myapp")
|
|
57
|
+
|
|
58
|
+
@bean
|
|
59
|
+
def user_service(db: DatabaseService): # Auto-injected by type
|
|
60
|
+
return UserService(db)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 2. Initialize and use
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
# Initialize the container — resolves all dependencies
|
|
67
|
+
refresh()
|
|
68
|
+
|
|
69
|
+
# Retrieve the root application object (the only place you should call get_bean)
|
|
70
|
+
user_svc = get_bean(UserService)
|
|
71
|
+
print(user_svc.get_user(123))
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
> **Note:** `get_bean()` is only needed at the **entry point** to bootstrap your application. All other dependencies are injected automatically via constructor or function parameters — just like Spring.
|
|
75
|
+
|
|
76
|
+
### Class-style beans
|
|
77
|
+
|
|
78
|
+
Decorate classes directly — the container instantiates them with resolved dependencies:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from appctx import bean, get_bean, refresh
|
|
82
|
+
|
|
83
|
+
@bean
|
|
84
|
+
class EmailService:
|
|
85
|
+
def __init__(self):
|
|
86
|
+
self.server = "smtp.example.com"
|
|
87
|
+
|
|
88
|
+
def send_email(self, to: str, subject: str):
|
|
89
|
+
return f"Email sent to {to} via {self.server}"
|
|
90
|
+
|
|
91
|
+
@bean
|
|
92
|
+
class NotificationService:
|
|
93
|
+
def __init__(self, email: EmailService):
|
|
94
|
+
self.email = email
|
|
95
|
+
|
|
96
|
+
def notify(self, user: str, message: str):
|
|
97
|
+
return self.email.send_email(user, "Notification", message)
|
|
98
|
+
|
|
99
|
+
refresh()
|
|
100
|
+
|
|
101
|
+
# Entry point — the only place you should call get_bean
|
|
102
|
+
notification_svc = get_bean(NotificationService)
|
|
103
|
+
print(notification_svc.notify("user@example.com", "Hello World!"))
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Post-construct lifecycle
|
|
107
|
+
|
|
108
|
+
Use `@post_construct` to run initialization logic after all beans are created:
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from appctx import post_construct
|
|
112
|
+
|
|
113
|
+
class DatabaseService:
|
|
114
|
+
def __init__(self):
|
|
115
|
+
self.connection = None
|
|
116
|
+
|
|
117
|
+
@post_construct
|
|
118
|
+
def init(self):
|
|
119
|
+
self.connection = create_connection() # Called automatically after construction
|
|
120
|
+
|
|
121
|
+
@bean
|
|
122
|
+
def database_service():
|
|
123
|
+
return DatabaseService()
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
> **Note:** `@post_construct` methods run after **all** beans are created, so you can safely reference other beans during initialization.
|
|
127
|
+
|
|
128
|
+
### How It Works
|
|
129
|
+
|
|
130
|
+
1. **Bean Registration**: Use the `@bean` decorator to register functions/classes that create beans
|
|
131
|
+
2. **Auto-wiring**: Dependencies are resolved from type annotations and injected automatically — no manual `get_bean()` calls needed
|
|
132
|
+
3. **Container Initialization**: Call `refresh()` to instantiate all beans in dependency order
|
|
133
|
+
4. **Entry Point**: Use `get_bean()` **once** at the application root to retrieve the assembled object graph
|
|
134
|
+
|
|
135
|
+
## Core Concepts
|
|
136
|
+
|
|
137
|
+
| Concept | Description |
|
|
138
|
+
|---|---|
|
|
139
|
+
| **Bean** | An object managed by the container |
|
|
140
|
+
| **ApplicationContext** | The DI container that holds and wires beans |
|
|
141
|
+
| **`@bean`** | Decorator that registers a function/class as a bean factory (alias: `@component`) |
|
|
142
|
+
| **`refresh()`** | Initializes the container, instantiates all beans in dependency order |
|
|
143
|
+
| **`get_bean(T)`** | Retrieve a bean by type or name (low-level; prefer auto-wiring) |
|
|
144
|
+
| **`get_beans(T)`** | Retrieve all beans of a given type (low-level; prefer auto-wiring) |
|
|
145
|
+
|
|
146
|
+
### Dependency Resolution Rules
|
|
147
|
+
|
|
148
|
+
1. **Positional arguments** → resolved by type annotation
|
|
149
|
+
2. **Keyword-only arguments** (`*, name`) → resolved by parameter name, falls back to default
|
|
150
|
+
3. **`**kwargs`** → receives all remaining beans not consumed by other parameters
|
|
151
|
+
|
|
152
|
+
## Global Default Context
|
|
153
|
+
|
|
154
|
+
AppCtx provides a **global default context** for convenience, similar to Spring's application context. The top-level functions (`bean`, `refresh`, `get_bean`, `add`) operate on this shared instance:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from appctx import bean, refresh, get_bean
|
|
158
|
+
|
|
159
|
+
# These all use the same global ApplicationContext behind the scenes
|
|
160
|
+
@bean
|
|
161
|
+
def my_service():
|
|
162
|
+
return MyService()
|
|
163
|
+
|
|
164
|
+
refresh()
|
|
165
|
+
app = get_bean(MyService)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Custom Context (Isolation)
|
|
169
|
+
|
|
170
|
+
For libraries or tests that need isolation, create your own `ApplicationContext`:
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
from appctx import ApplicationContext
|
|
174
|
+
|
|
175
|
+
ctx = ApplicationContext()
|
|
176
|
+
|
|
177
|
+
@ctx.bean
|
|
178
|
+
def my_service():
|
|
179
|
+
return MyService()
|
|
180
|
+
|
|
181
|
+
ctx.refresh()
|
|
182
|
+
app = ctx.get_bean(MyService)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
> **Note:** `@bean` on the global context registers beans immediately when the module is imported. For custom contexts, use `@ctx.bean` to register against that specific instance.
|
|
186
|
+
|
|
187
|
+
## Organizing Beans Across Modules
|
|
188
|
+
|
|
189
|
+
AppCtx does **not** perform automatic package scanning. Instead, beans are registered at import time — when Python loads a module, any `@bean` decorators execute and register with the target context.
|
|
190
|
+
|
|
191
|
+
To wire beans across multiple modules, simply **import** them before calling `refresh()`:
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
# main.py
|
|
195
|
+
from appctx import bean, refresh, get_bean
|
|
196
|
+
|
|
197
|
+
# Import modules so their @bean decorators fire
|
|
198
|
+
import myapp.services.user # registers UserService
|
|
199
|
+
import myapp.services.email # registers EmailService
|
|
200
|
+
import myapp.config # registers ConfigService
|
|
201
|
+
|
|
202
|
+
refresh()
|
|
203
|
+
app = get_bean(UserService)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
You can also use `add()` to register a plain function or class (without the `@bean` decorator):
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
from appctx import add
|
|
210
|
+
|
|
211
|
+
def some_service():
|
|
212
|
+
return SomeService()
|
|
213
|
+
|
|
214
|
+
add(some_service) # Equivalent to @bean on some_service
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
> **Note:** For modules, simply importing them is sufficient — the `@bean` decorators fire at import time. `add(module)` is available as a semantic marker but the import alone triggers registration.
|
|
218
|
+
|
|
219
|
+
## Best Practices
|
|
220
|
+
|
|
221
|
+
1. **Rely on auto-wiring, avoid `get_bean`** — Like Spring, let the container inject dependencies via function/constructor parameters. Reserve `get_bean()` only for bootstrapping the root application object.
|
|
222
|
+
2. **Use type annotations** — They are the primary mechanism for dependency resolution. Always annotate positional parameters.
|
|
223
|
+
3. **Single responsibility per bean** — Each bean should have one clear purpose.
|
|
224
|
+
4. **Prefer constructor injection** — Dependencies go in `__init__` parameters, not hidden inside methods.
|
|
225
|
+
5. **Keep beans stateless when possible** — Makes testing and reasoning easier.
|
|
226
|
+
6. **Use `@post_construct` for initialization** — Not `__init__` side-effects. Post-construct runs after all beans exist, allowing cross-bean setup.
|
|
227
|
+
7. **Separate configuration from logic** — Use dedicated config beans instead of hardcoding values.
|
|
228
|
+
8. **Design for testability** — Beans that accept dependencies via constructor are trivially mockable in tests.
|
|
229
|
+
|
|
230
|
+
## Documentation
|
|
231
|
+
|
|
232
|
+
- **[API Reference](docs/api-reference.md)** — Full API: decorators, container ops, dependency resolution, error handling
|
|
233
|
+
- **[Development Guide](docs/development.md)** — Setup, testing, code quality, contributing
|
|
234
|
+
- **[Release Guide](RELEASE.md)** — Release process for maintainers
|
|
235
|
+
- **[Changelog](CHANGELOG.md)** — Version history
|
|
236
|
+
|
|
237
|
+
## License
|
|
238
|
+
|
|
239
|
+
MIT — see [LICENSE](https://github.com/wssccc/appctx/blob/main/LICENSE).
|
|
240
|
+
|
|
241
|
+
## Links
|
|
242
|
+
|
|
243
|
+
- **Repository**: [github.com/wssccc/appctx](https://github.com/wssccc/appctx)
|
|
244
|
+
- **PyPI**: [pypi.org/project/appctx](https://pypi.org/project/appctx/)
|
|
245
|
+
- **Issues**: [github.com/wssccc/appctx/issues](https://github.com/wssccc/appctx/issues)
|
|
@@ -35,8 +35,8 @@ class ApplicationContext:
|
|
|
35
35
|
"""Get all beans of a specific type."""
|
|
36
36
|
return self.bean_types_map[_type]
|
|
37
37
|
|
|
38
|
-
def _instantiate(self, bean_def: Any) ->
|
|
39
|
-
"""Instantiate a bean definition."""
|
|
38
|
+
def _instantiate(self, bean_def: Any) -> bool:
|
|
39
|
+
"""Instantiate a bean definition. Returns True on success, False if dependencies are not ready."""
|
|
40
40
|
is_class = inspect.isclass(bean_def)
|
|
41
41
|
if is_class:
|
|
42
42
|
spec = inspect.getfullargspec(bean_def.__init__)
|
|
@@ -53,8 +53,8 @@ class ApplicationContext:
|
|
|
53
53
|
self.bean_names_map[bean_def.__name__] = obj
|
|
54
54
|
self.bean_types_map[type(obj)].append(obj)
|
|
55
55
|
|
|
56
|
-
return
|
|
57
|
-
return
|
|
56
|
+
return True
|
|
57
|
+
return False
|
|
58
58
|
|
|
59
59
|
def _call_post_construct(self, obj: Any) -> None:
|
|
60
60
|
"""Call post_construct methods on the object if they exist."""
|