hydr8 0.1.0__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.
- hydr8-0.1.0/.github/workflows/publish.yml +31 -0
- hydr8-0.1.0/.gitignore +3 -0
- hydr8-0.1.0/PKG-INFO +212 -0
- hydr8-0.1.0/README.md +204 -0
- hydr8-0.1.0/pyproject.toml +14 -0
- hydr8-0.1.0/src/hydr8/__init__.py +4 -0
- hydr8-0.1.0/src/hydr8/_decorator.py +177 -0
- hydr8-0.1.0/src/hydr8/_resolver.py +54 -0
- hydr8-0.1.0/src/hydr8/_store.py +38 -0
- hydr8-0.1.0/tests/test_decorator.py +238 -0
- hydr8-0.1.0/tests/test_import.py +4 -0
- hydr8-0.1.0/tests/test_resolver.py +115 -0
- hydr8-0.1.0/tests/test_store.py +50 -0
- hydr8-0.1.0/uv.lock +359 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
python-version: ["3.8", "3.10", "3.12"]
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: astral-sh/setup-uv@v4
|
|
17
|
+
- run: uv run --python ${{ matrix.python-version }} pytest tests/ -v
|
|
18
|
+
|
|
19
|
+
publish:
|
|
20
|
+
needs: test
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
permissions:
|
|
23
|
+
id-token: write
|
|
24
|
+
environment:
|
|
25
|
+
name: pypi
|
|
26
|
+
url: https://pypi.org/p/hydr8
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v4
|
|
29
|
+
- uses: astral-sh/setup-uv@v4
|
|
30
|
+
- run: uv build
|
|
31
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
hydr8-0.1.0/.gitignore
ADDED
hydr8-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hydr8
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Decorator-based config injection for Hydra
|
|
5
|
+
Requires-Python: >=3.8
|
|
6
|
+
Requires-Dist: omegaconf>=2.1
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
|
|
9
|
+
# hydr8
|
|
10
|
+
|
|
11
|
+
Decorator-based config injection for [Hydra](https://hydra.cc/).
|
|
12
|
+
|
|
13
|
+
hydr8 lets you push Hydra config (or any config as long as it's a dict) values into function parameters automatically, so your functions stay clean and testable without manually threading `cfg` everywhere.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install hydr8
|
|
19
|
+
# or
|
|
20
|
+
uv add hydr8
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import hydra
|
|
27
|
+
from omegaconf import DictConfig
|
|
28
|
+
import hydr8
|
|
29
|
+
|
|
30
|
+
@hydr8.use("db")
|
|
31
|
+
def connect(host: str, port: int):
|
|
32
|
+
print(f"Connecting to {host}:{port}")
|
|
33
|
+
|
|
34
|
+
@hydra.main(config_path="conf", config_name="config", version_base=None)
|
|
35
|
+
def main(cfg: DictConfig):
|
|
36
|
+
hydr8.init(cfg)
|
|
37
|
+
connect() # host and port injected from cfg.db
|
|
38
|
+
|
|
39
|
+
if __name__ == "__main__":
|
|
40
|
+
main()
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
With a config like:
|
|
44
|
+
|
|
45
|
+
```yaml
|
|
46
|
+
db:
|
|
47
|
+
host: localhost
|
|
48
|
+
port: 5432
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
`hydr8.use()` is the core function. It can be used as a **decorator** to inject config into function parameters, or called **directly** to access a config sub-tree as a dict.
|
|
54
|
+
|
|
55
|
+
### As a decorator
|
|
56
|
+
|
|
57
|
+
#### Explicit path
|
|
58
|
+
|
|
59
|
+
Pass a dot-separated path to resolve a specific config node. Config keys are matched to function parameter names. Extra config keys that don't match any parameter are silently ignored.
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
@hydr8.use("db.postgres")
|
|
63
|
+
def connect(host: str, port: int, user: str):
|
|
64
|
+
...
|
|
65
|
+
|
|
66
|
+
connect() # all three injected from cfg.db.postgres
|
|
67
|
+
connect(host="remote") # host overridden, port and user from config
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
List indexing is supported:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
@hydr8.use("db.replicas[0]")
|
|
74
|
+
def connect(host: str, port: int):
|
|
75
|
+
...
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
#### Implicit path (auto-resolve)
|
|
79
|
+
|
|
80
|
+
When no path is given, hydr8 derives it from the function's `__module__`. If the first segment of the module isn't a top-level config key, it's treated as the project name and stripped — so auto-resolve works whether you run with `python -m` or `python file.py`:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
# In myproject/data/loaders.py
|
|
84
|
+
@hydr8.use()
|
|
85
|
+
def build_loader(batch_size: int, shuffle: bool):
|
|
86
|
+
...
|
|
87
|
+
# Resolves to cfg.data.loaders
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
```yaml
|
|
91
|
+
# config.yaml
|
|
92
|
+
data:
|
|
93
|
+
loaders:
|
|
94
|
+
batch_size: 32
|
|
95
|
+
shuffle: true
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
By default, `scope="module"` — the path resolves to the module's config node, and config keys are matched to function parameters. Multiple functions in the same module share the same config node.
|
|
99
|
+
|
|
100
|
+
With `scope="fn"`, the function's qualname is appended to the path:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
# In myproject/data/loaders.py
|
|
104
|
+
@hydr8.use(scope="fn")
|
|
105
|
+
def build_loader(batch_size: int, shuffle: bool):
|
|
106
|
+
...
|
|
107
|
+
# Resolves to cfg.data.loaders.build_loader
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
```yaml
|
|
111
|
+
# config.yaml
|
|
112
|
+
data:
|
|
113
|
+
loaders:
|
|
114
|
+
build_loader:
|
|
115
|
+
batch_size: 32
|
|
116
|
+
shuffle: true
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
This works with methods too:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
# In myproject/db/client.py
|
|
123
|
+
class Client:
|
|
124
|
+
@hydr8.use(scope="fn")
|
|
125
|
+
def __init__(self, host: str, port: int):
|
|
126
|
+
self.host = host
|
|
127
|
+
self.port = port
|
|
128
|
+
# Resolves to cfg.db.client.Client.__init__
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
#### `as_dict` mode
|
|
132
|
+
|
|
133
|
+
Pass the entire resolved sub-config as a single dict argument instead of matching individual keys:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
@hydr8.use("db.postgres", as_dict="config")
|
|
137
|
+
def connect(config: dict):
|
|
138
|
+
host = config["host"]
|
|
139
|
+
port = config["port"]
|
|
140
|
+
...
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
#### Caller overrides
|
|
144
|
+
|
|
145
|
+
Caller-provided arguments always take precedence over injected config. If every required parameter is supplied by the caller, config is never accessed at all:
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
@hydr8.use("db")
|
|
149
|
+
def connect(host: str, port: int):
|
|
150
|
+
...
|
|
151
|
+
|
|
152
|
+
connect(host="remote") # port from config, host = "remote"
|
|
153
|
+
connect("localhost", 5432) # config not accessed
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### As a direct call
|
|
157
|
+
|
|
158
|
+
`hydr8.use("path")` returns a lazy, dict-like proxy. The config is resolved on first access, not at call time, so you can call `use()` before `init()`.
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
import hydr8
|
|
162
|
+
|
|
163
|
+
db = hydr8.use("db")
|
|
164
|
+
|
|
165
|
+
hydr8.init(cfg)
|
|
166
|
+
db["host"] # "localhost"
|
|
167
|
+
db["port"] # 5432
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
This is useful when you want to read config values without decorating a function:
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
def connect():
|
|
174
|
+
db = hydr8.use("db")
|
|
175
|
+
engine = create_engine(f"postgresql://{db['host']}:{db['port']}")
|
|
176
|
+
...
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
An explicit path is required when using `use()` as a direct call. Calling `use()` without a path and accessing it raises `TypeError`, since there is no function to derive the path from.
|
|
180
|
+
|
|
181
|
+
## Testing
|
|
182
|
+
|
|
183
|
+
### Option A: Supply all arguments directly
|
|
184
|
+
|
|
185
|
+
When every required parameter is provided by the caller, config injection is skipped entirely — no `init` needed:
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
def test_connect():
|
|
189
|
+
assert connect("localhost", 5432) == expected
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Option B: `override` context manager
|
|
193
|
+
|
|
194
|
+
Temporarily replace the global config for a test:
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
from hydr8 import override
|
|
198
|
+
|
|
199
|
+
def test_connect():
|
|
200
|
+
with override({"db": {"host": "test-host", "port": 9999}}):
|
|
201
|
+
result = connect()
|
|
202
|
+
assert result == expected
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## API reference
|
|
206
|
+
|
|
207
|
+
| Function | Description |
|
|
208
|
+
|---|---|
|
|
209
|
+
| `init(cfg)` | Store the config globally (accepts any dict or OmegaConf DictConfig) |
|
|
210
|
+
| `get()` | Retrieve the stored config (raises `RuntimeError` if uninitialized) |
|
|
211
|
+
| `override(overrides)` | Context manager that temporarily replaces the config |
|
|
212
|
+
| `use(path, *, as_dict, scope)` | Decorator or direct config accessor for a config sub-tree |
|
hydr8-0.1.0/README.md
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# hydr8
|
|
2
|
+
|
|
3
|
+
Decorator-based config injection for [Hydra](https://hydra.cc/).
|
|
4
|
+
|
|
5
|
+
hydr8 lets you push Hydra config (or any config as long as it's a dict) values into function parameters automatically, so your functions stay clean and testable without manually threading `cfg` everywhere.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install hydr8
|
|
11
|
+
# or
|
|
12
|
+
uv add hydr8
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
import hydra
|
|
19
|
+
from omegaconf import DictConfig
|
|
20
|
+
import hydr8
|
|
21
|
+
|
|
22
|
+
@hydr8.use("db")
|
|
23
|
+
def connect(host: str, port: int):
|
|
24
|
+
print(f"Connecting to {host}:{port}")
|
|
25
|
+
|
|
26
|
+
@hydra.main(config_path="conf", config_name="config", version_base=None)
|
|
27
|
+
def main(cfg: DictConfig):
|
|
28
|
+
hydr8.init(cfg)
|
|
29
|
+
connect() # host and port injected from cfg.db
|
|
30
|
+
|
|
31
|
+
if __name__ == "__main__":
|
|
32
|
+
main()
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
With a config like:
|
|
36
|
+
|
|
37
|
+
```yaml
|
|
38
|
+
db:
|
|
39
|
+
host: localhost
|
|
40
|
+
port: 5432
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
`hydr8.use()` is the core function. It can be used as a **decorator** to inject config into function parameters, or called **directly** to access a config sub-tree as a dict.
|
|
46
|
+
|
|
47
|
+
### As a decorator
|
|
48
|
+
|
|
49
|
+
#### Explicit path
|
|
50
|
+
|
|
51
|
+
Pass a dot-separated path to resolve a specific config node. Config keys are matched to function parameter names. Extra config keys that don't match any parameter are silently ignored.
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
@hydr8.use("db.postgres")
|
|
55
|
+
def connect(host: str, port: int, user: str):
|
|
56
|
+
...
|
|
57
|
+
|
|
58
|
+
connect() # all three injected from cfg.db.postgres
|
|
59
|
+
connect(host="remote") # host overridden, port and user from config
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
List indexing is supported:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
@hydr8.use("db.replicas[0]")
|
|
66
|
+
def connect(host: str, port: int):
|
|
67
|
+
...
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
#### Implicit path (auto-resolve)
|
|
71
|
+
|
|
72
|
+
When no path is given, hydr8 derives it from the function's `__module__`. If the first segment of the module isn't a top-level config key, it's treated as the project name and stripped — so auto-resolve works whether you run with `python -m` or `python file.py`:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
# In myproject/data/loaders.py
|
|
76
|
+
@hydr8.use()
|
|
77
|
+
def build_loader(batch_size: int, shuffle: bool):
|
|
78
|
+
...
|
|
79
|
+
# Resolves to cfg.data.loaders
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
```yaml
|
|
83
|
+
# config.yaml
|
|
84
|
+
data:
|
|
85
|
+
loaders:
|
|
86
|
+
batch_size: 32
|
|
87
|
+
shuffle: true
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
By default, `scope="module"` — the path resolves to the module's config node, and config keys are matched to function parameters. Multiple functions in the same module share the same config node.
|
|
91
|
+
|
|
92
|
+
With `scope="fn"`, the function's qualname is appended to the path:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
# In myproject/data/loaders.py
|
|
96
|
+
@hydr8.use(scope="fn")
|
|
97
|
+
def build_loader(batch_size: int, shuffle: bool):
|
|
98
|
+
...
|
|
99
|
+
# Resolves to cfg.data.loaders.build_loader
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
```yaml
|
|
103
|
+
# config.yaml
|
|
104
|
+
data:
|
|
105
|
+
loaders:
|
|
106
|
+
build_loader:
|
|
107
|
+
batch_size: 32
|
|
108
|
+
shuffle: true
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
This works with methods too:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
# In myproject/db/client.py
|
|
115
|
+
class Client:
|
|
116
|
+
@hydr8.use(scope="fn")
|
|
117
|
+
def __init__(self, host: str, port: int):
|
|
118
|
+
self.host = host
|
|
119
|
+
self.port = port
|
|
120
|
+
# Resolves to cfg.db.client.Client.__init__
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
#### `as_dict` mode
|
|
124
|
+
|
|
125
|
+
Pass the entire resolved sub-config as a single dict argument instead of matching individual keys:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
@hydr8.use("db.postgres", as_dict="config")
|
|
129
|
+
def connect(config: dict):
|
|
130
|
+
host = config["host"]
|
|
131
|
+
port = config["port"]
|
|
132
|
+
...
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
#### Caller overrides
|
|
136
|
+
|
|
137
|
+
Caller-provided arguments always take precedence over injected config. If every required parameter is supplied by the caller, config is never accessed at all:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
@hydr8.use("db")
|
|
141
|
+
def connect(host: str, port: int):
|
|
142
|
+
...
|
|
143
|
+
|
|
144
|
+
connect(host="remote") # port from config, host = "remote"
|
|
145
|
+
connect("localhost", 5432) # config not accessed
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### As a direct call
|
|
149
|
+
|
|
150
|
+
`hydr8.use("path")` returns a lazy, dict-like proxy. The config is resolved on first access, not at call time, so you can call `use()` before `init()`.
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
import hydr8
|
|
154
|
+
|
|
155
|
+
db = hydr8.use("db")
|
|
156
|
+
|
|
157
|
+
hydr8.init(cfg)
|
|
158
|
+
db["host"] # "localhost"
|
|
159
|
+
db["port"] # 5432
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
This is useful when you want to read config values without decorating a function:
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
def connect():
|
|
166
|
+
db = hydr8.use("db")
|
|
167
|
+
engine = create_engine(f"postgresql://{db['host']}:{db['port']}")
|
|
168
|
+
...
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
An explicit path is required when using `use()` as a direct call. Calling `use()` without a path and accessing it raises `TypeError`, since there is no function to derive the path from.
|
|
172
|
+
|
|
173
|
+
## Testing
|
|
174
|
+
|
|
175
|
+
### Option A: Supply all arguments directly
|
|
176
|
+
|
|
177
|
+
When every required parameter is provided by the caller, config injection is skipped entirely — no `init` needed:
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
def test_connect():
|
|
181
|
+
assert connect("localhost", 5432) == expected
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Option B: `override` context manager
|
|
185
|
+
|
|
186
|
+
Temporarily replace the global config for a test:
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
from hydr8 import override
|
|
190
|
+
|
|
191
|
+
def test_connect():
|
|
192
|
+
with override({"db": {"host": "test-host", "port": 9999}}):
|
|
193
|
+
result = connect()
|
|
194
|
+
assert result == expected
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## API reference
|
|
198
|
+
|
|
199
|
+
| Function | Description |
|
|
200
|
+
|---|---|
|
|
201
|
+
| `init(cfg)` | Store the config globally (accepts any dict or OmegaConf DictConfig) |
|
|
202
|
+
| `get()` | Retrieve the stored config (raises `RuntimeError` if uninitialized) |
|
|
203
|
+
| `override(overrides)` | Context manager that temporarily replaces the config |
|
|
204
|
+
| `use(path, *, as_dict, scope)` | Decorator or direct config accessor for a config sub-tree |
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "hydr8"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Decorator-based config injection for Hydra"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.8"
|
|
7
|
+
dependencies = ["omegaconf>=2.1"]
|
|
8
|
+
|
|
9
|
+
[build-system]
|
|
10
|
+
requires = ["hatchling"]
|
|
11
|
+
build-backend = "hatchling.build"
|
|
12
|
+
|
|
13
|
+
[dependency-groups]
|
|
14
|
+
dev = ["pytest"]
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import inspect
|
|
5
|
+
from typing import Any, Callable, Iterator, TypeVar
|
|
6
|
+
|
|
7
|
+
from ._store import get
|
|
8
|
+
from ._resolver import resolve, resolve_auto
|
|
9
|
+
|
|
10
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class _ConfigProxy:
|
|
14
|
+
"""Returned by ``use()``. Acts as both a decorator and a lazy config dict."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, path: str | None, as_dict: str | None, scope: str) -> None:
|
|
17
|
+
self._path = path
|
|
18
|
+
self._as_dict = as_dict
|
|
19
|
+
self._scope = scope
|
|
20
|
+
self._resolved: dict[str, Any] | None = None
|
|
21
|
+
|
|
22
|
+
def _resolve(self) -> dict[str, Any]:
|
|
23
|
+
if self._resolved is None:
|
|
24
|
+
cfg = get()
|
|
25
|
+
if self._path is None:
|
|
26
|
+
raise TypeError(
|
|
27
|
+
"Cannot resolve config as a function without an explicit path. "
|
|
28
|
+
"Pass a path to use(), e.g. use('db')."
|
|
29
|
+
)
|
|
30
|
+
self._resolved = resolve(cfg, self._path)
|
|
31
|
+
return self._resolved
|
|
32
|
+
|
|
33
|
+
# -- decorator mode --
|
|
34
|
+
|
|
35
|
+
def __call__(self, fn: F) -> F:
|
|
36
|
+
path = self._path
|
|
37
|
+
as_dict = self._as_dict
|
|
38
|
+
scope = self._scope
|
|
39
|
+
sig = inspect.signature(fn)
|
|
40
|
+
param_names = set(sig.parameters)
|
|
41
|
+
|
|
42
|
+
@functools.wraps(fn)
|
|
43
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
44
|
+
bound = sig.bind_partial(*args, **kwargs)
|
|
45
|
+
supplied = set(bound.arguments)
|
|
46
|
+
|
|
47
|
+
required = {
|
|
48
|
+
name
|
|
49
|
+
for name, p in sig.parameters.items()
|
|
50
|
+
if p.default is inspect.Parameter.empty
|
|
51
|
+
and p.kind
|
|
52
|
+
not in (
|
|
53
|
+
inspect.Parameter.VAR_POSITIONAL,
|
|
54
|
+
inspect.Parameter.VAR_KEYWORD,
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
if required <= supplied:
|
|
58
|
+
return fn(*args, **kwargs)
|
|
59
|
+
|
|
60
|
+
cfg = get()
|
|
61
|
+
if path is None:
|
|
62
|
+
resolved = resolve_auto(cfg, fn, scope)
|
|
63
|
+
else:
|
|
64
|
+
resolved = resolve(cfg, path)
|
|
65
|
+
|
|
66
|
+
if as_dict is not None:
|
|
67
|
+
if as_dict not in supplied:
|
|
68
|
+
kwargs[as_dict] = resolved
|
|
69
|
+
else:
|
|
70
|
+
for key, value in resolved.items():
|
|
71
|
+
if key in param_names and key not in supplied:
|
|
72
|
+
kwargs[key] = value
|
|
73
|
+
|
|
74
|
+
return fn(*args, **kwargs)
|
|
75
|
+
|
|
76
|
+
return wrapper # type: ignore[return-value]
|
|
77
|
+
|
|
78
|
+
# -- dict-like mode --
|
|
79
|
+
|
|
80
|
+
def __getitem__(self, key: str) -> Any:
|
|
81
|
+
return self._resolve()[key]
|
|
82
|
+
|
|
83
|
+
def __contains__(self, key: object) -> bool:
|
|
84
|
+
return key in self._resolve()
|
|
85
|
+
|
|
86
|
+
def __iter__(self) -> Iterator[str]:
|
|
87
|
+
return iter(self._resolve())
|
|
88
|
+
|
|
89
|
+
def __len__(self) -> int:
|
|
90
|
+
return len(self._resolve())
|
|
91
|
+
|
|
92
|
+
def keys(self) -> Any:
|
|
93
|
+
return self._resolve().keys()
|
|
94
|
+
|
|
95
|
+
def values(self) -> Any:
|
|
96
|
+
return self._resolve().values()
|
|
97
|
+
|
|
98
|
+
def items(self) -> Any:
|
|
99
|
+
return self._resolve().items()
|
|
100
|
+
|
|
101
|
+
def __repr__(self) -> str:
|
|
102
|
+
try:
|
|
103
|
+
return repr(self._resolve())
|
|
104
|
+
except Exception:
|
|
105
|
+
return f"_ConfigProxy(path={self._path!r})"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def use(
|
|
109
|
+
path: str | None = None,
|
|
110
|
+
*,
|
|
111
|
+
as_dict: str | None = None,
|
|
112
|
+
scope: str = "module",
|
|
113
|
+
) -> _ConfigProxy:
|
|
114
|
+
"""Access a config sub-tree — as a decorator or a function.
|
|
115
|
+
|
|
116
|
+
Returns a proxy that can be used in two ways:
|
|
117
|
+
|
|
118
|
+
**As a decorator** — injects config values into function parameters::
|
|
119
|
+
|
|
120
|
+
@use("db")
|
|
121
|
+
def connect(host: str, port: int):
|
|
122
|
+
...
|
|
123
|
+
|
|
124
|
+
connect() # host and port injected from cfg.db
|
|
125
|
+
connect(host="other") # caller args take precedence
|
|
126
|
+
|
|
127
|
+
When ``path`` is ``None`` (the default), the config path is derived
|
|
128
|
+
automatically from the function's module path. If the first segment
|
|
129
|
+
of ``__module__`` isn't a top-level config key it is treated as the
|
|
130
|
+
project name and stripped, so auto-resolve works whether you run with
|
|
131
|
+
``python -m`` or ``python file.py``::
|
|
132
|
+
|
|
133
|
+
# In myproject/data/loaders.py
|
|
134
|
+
@use()
|
|
135
|
+
def build_loader(batch_size: int, shuffle: bool):
|
|
136
|
+
...
|
|
137
|
+
# resolves to cfg.data.loaders (scope="module", the default)
|
|
138
|
+
|
|
139
|
+
With ``scope="fn"``, the function's ``__qualname__`` is appended::
|
|
140
|
+
|
|
141
|
+
@use(scope="fn")
|
|
142
|
+
def build_loader(batch_size: int, shuffle: bool):
|
|
143
|
+
...
|
|
144
|
+
# resolves to cfg.data.loaders.build_loader
|
|
145
|
+
|
|
146
|
+
With ``as_dict``, the entire sub-config is passed as a single kwarg
|
|
147
|
+
instead of matching individual keys to parameters::
|
|
148
|
+
|
|
149
|
+
@use("db", as_dict="config")
|
|
150
|
+
def connect(config: dict):
|
|
151
|
+
host = config["host"]
|
|
152
|
+
|
|
153
|
+
**As a function** — returns a lazy, dict-like view of the config node.
|
|
154
|
+
Requires an explicit ``path``::
|
|
155
|
+
|
|
156
|
+
db = use("db")
|
|
157
|
+
db["host"] # "localhost"
|
|
158
|
+
dict(db) # {"host": "localhost", "port": 5432}
|
|
159
|
+
"host" in db # True
|
|
160
|
+
|
|
161
|
+
The config is resolved lazily on first access, so ``use("db")`` can be
|
|
162
|
+
called before ``init()``.
|
|
163
|
+
|
|
164
|
+
Calling ``use()`` without a path and accessing it as a function raises
|
|
165
|
+
``TypeError``, since there is no function to derive the path from.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
path: Dot-separated config path (e.g. ``"db.postgres"``). When
|
|
169
|
+
``None``, the path is derived from the decorated function's
|
|
170
|
+
module (decorator mode only).
|
|
171
|
+
as_dict: When set, the resolved sub-config is passed as a single
|
|
172
|
+
kwarg with this name (decorator mode only).
|
|
173
|
+
scope: Controls auto-resolve granularity (decorator mode only).
|
|
174
|
+
``"module"`` (default) resolves to the module's config node.
|
|
175
|
+
``"fn"`` appends the function's qualname.
|
|
176
|
+
"""
|
|
177
|
+
return _ConfigProxy(path, as_dict, scope)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable
|
|
4
|
+
|
|
5
|
+
from omegaconf import DictConfig, OmegaConf
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def resolve(cfg: DictConfig, path: str) -> dict[str, Any]:
|
|
9
|
+
"""Traverse *cfg* along the dot-separated *path* and return a plain dict.
|
|
10
|
+
|
|
11
|
+
Raises ``KeyError`` if any segment is missing.
|
|
12
|
+
"""
|
|
13
|
+
node = OmegaConf.select(cfg, path, throw_on_missing=True)
|
|
14
|
+
if node is None:
|
|
15
|
+
raise KeyError(f"Config path {path!r} not found")
|
|
16
|
+
if not OmegaConf.is_dict(node):
|
|
17
|
+
raise TypeError(
|
|
18
|
+
f"Config path {path!r} resolved to a leaf value, not a mapping"
|
|
19
|
+
)
|
|
20
|
+
return OmegaConf.to_container(node, resolve=True) # type: ignore[return-value]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def resolve_auto(
|
|
24
|
+
cfg: DictConfig, fn: Callable[..., Any], scope: str = "module"
|
|
25
|
+
) -> dict[str, Any]:
|
|
26
|
+
"""Derive the config path from *fn*'s module (and optionally qualname), then resolve.
|
|
27
|
+
|
|
28
|
+
If the first segment of ``__module__`` is not a top-level key in *cfg*,
|
|
29
|
+
it is assumed to be the project/package name and is stripped. This makes
|
|
30
|
+
auto-resolve work regardless of whether the code was invoked with
|
|
31
|
+
``python -m`` (which includes the package prefix) or ``python file.py``
|
|
32
|
+
(which does not).
|
|
33
|
+
|
|
34
|
+
When *scope* is ``"module"`` (the default), only the module path is used::
|
|
35
|
+
|
|
36
|
+
myproject.data.loaders -> data.loaders
|
|
37
|
+
|
|
38
|
+
When *scope* is ``"fn"``, the function's ``__qualname__`` is appended::
|
|
39
|
+
|
|
40
|
+
myproject.data.loaders + build_loader -> data.loaders.build_loader
|
|
41
|
+
"""
|
|
42
|
+
parts = fn.__module__.split(".")
|
|
43
|
+
|
|
44
|
+
# If the first segment isn't a top-level config key, it's the project
|
|
45
|
+
# name — strip it.
|
|
46
|
+
if len(parts) > 1 and parts[0] not in cfg:
|
|
47
|
+
parts = parts[1:]
|
|
48
|
+
|
|
49
|
+
if scope == "fn":
|
|
50
|
+
path = ".".join(parts) + "." + fn.__qualname__
|
|
51
|
+
else:
|
|
52
|
+
path = ".".join(parts)
|
|
53
|
+
|
|
54
|
+
return resolve(cfg, path)
|