confit 0.11.1__tar.gz → 0.13.0.dev0__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.
- {confit-0.11.1 → confit-0.13.0.dev0}/PKG-INFO +25 -2
- {confit-0.11.1 → confit-0.13.0.dev0}/README.md +23 -0
- {confit-0.11.1 → confit-0.13.0.dev0}/confit/_version.py +5 -2
- {confit-0.11.1 → confit-0.13.0.dev0}/confit/cli.py +223 -35
- {confit-0.11.1 → confit-0.13.0.dev0}/confit/config.py +17 -12
- {confit-0.11.1 → confit-0.13.0.dev0}/confit/draft.py +10 -39
- confit-0.13.0.dev0/confit/errors.py +265 -0
- confit-0.13.0.dev0/confit/registry.py +534 -0
- confit-0.13.0.dev0/confit/typing.py +42 -0
- {confit-0.11.1 → confit-0.13.0.dev0}/confit.egg-info/PKG-INFO +25 -2
- {confit-0.11.1 → confit-0.13.0.dev0}/confit.egg-info/requires.txt +1 -1
- {confit-0.11.1 → confit-0.13.0.dev0}/pyproject.toml +1 -1
- {confit-0.11.1 → confit-0.13.0.dev0}/tests/test_as_list.py +19 -36
- {confit-0.11.1 → confit-0.13.0.dev0}/tests/test_cli.py +180 -18
- {confit-0.11.1 → confit-0.13.0.dev0}/tests/test_config_instance.py +42 -31
- {confit-0.11.1 → confit-0.13.0.dev0}/tests/test_validate.py +102 -89
- confit-0.11.1/confit/errors.py +0 -362
- confit-0.11.1/confit/registry.py +0 -675
- confit-0.11.1/confit/typing.py +0 -58
- {confit-0.11.1 → confit-0.13.0.dev0}/LICENSE +0 -0
- {confit-0.11.1 → confit-0.13.0.dev0}/confit/__init__.py +0 -0
- {confit-0.11.1 → confit-0.13.0.dev0}/confit/utils/__init__.py +0 -0
- {confit-0.11.1 → confit-0.13.0.dev0}/confit/utils/collections.py +0 -0
- {confit-0.11.1 → confit-0.13.0.dev0}/confit/utils/eval.py +0 -0
- {confit-0.11.1 → confit-0.13.0.dev0}/confit/utils/random.py +0 -0
- {confit-0.11.1 → confit-0.13.0.dev0}/confit/utils/settings.py +0 -0
- {confit-0.11.1 → confit-0.13.0.dev0}/confit/utils/xjson.py +0 -0
- {confit-0.11.1 → confit-0.13.0.dev0}/confit.egg-info/SOURCES.txt +0 -0
- {confit-0.11.1 → confit-0.13.0.dev0}/confit.egg-info/dependency_links.txt +0 -0
- {confit-0.11.1 → confit-0.13.0.dev0}/confit.egg-info/top_level.txt +0 -0
- {confit-0.11.1 → confit-0.13.0.dev0}/setup.cfg +0 -0
- {confit-0.11.1 → confit-0.13.0.dev0}/tests/test_draft.py +0 -0
- {confit-0.11.1 → confit-0.13.0.dev0}/tests/test_registry.py +0 -0
- {confit-0.11.1 → confit-0.13.0.dev0}/tests/test_typing.py +0 -0
- {confit-0.11.1 → confit-0.13.0.dev0}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: confit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13.0.dev0
|
|
4
4
|
Summary: Smart configuration framework
|
|
5
5
|
Author-email: Perceval Wajsburt <perceval.wajsburt@aphp.fr>, Thomas Petit-Jean <thomas.petitjean@aphp.fr>, Adam Remaki <adam.remaki@aphp.fr>, Alice Calliger <alice.calliger@aphp.fr>
|
|
6
6
|
License: Copyright 2023 Assistance Publique - Hôpitaux de Paris
|
|
@@ -20,7 +20,7 @@ Description-Content-Type: text/markdown
|
|
|
20
20
|
License-File: LICENSE
|
|
21
21
|
Requires-Dist: catalogue>=2.0.7
|
|
22
22
|
Requires-Dist: lark>=1.1.5
|
|
23
|
-
Requires-Dist: pydantic
|
|
23
|
+
Requires-Dist: pydantic<3,>=2.1.1
|
|
24
24
|
Requires-Dist: pydantic-core>=0.20
|
|
25
25
|
Requires-Dist: PyYAML
|
|
26
26
|
Requires-Dist: griffe
|
|
@@ -247,6 +247,29 @@ ConfitValidationError: 2 validation errors for __main__.func()
|
|
|
247
247
|
|
|
248
248
|
Visit the [documentation](https://aphp.github.io/confit/) for more information!
|
|
249
249
|
|
|
250
|
+
### Subcommands
|
|
251
|
+
|
|
252
|
+
Confit applications can be composed into groups of subcommands:
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
app = Cli()
|
|
256
|
+
training = Cli()
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@training.command(name="run")
|
|
260
|
+
def run(epochs: int = 10):
|
|
261
|
+
print(f"Training for {epochs} epochs")
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
app.add_subcommands(training, name="training")
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Run the nested command with:
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
python script.py training run --config config.yml --epochs 20
|
|
271
|
+
```
|
|
272
|
+
|
|
250
273
|
## Acknowledgement
|
|
251
274
|
|
|
252
275
|
We would like to thank [Assistance Publique – Hôpitaux de Paris](https://www.aphp.fr/)
|
|
@@ -219,6 +219,29 @@ ConfitValidationError: 2 validation errors for __main__.func()
|
|
|
219
219
|
|
|
220
220
|
Visit the [documentation](https://aphp.github.io/confit/) for more information!
|
|
221
221
|
|
|
222
|
+
### Subcommands
|
|
223
|
+
|
|
224
|
+
Confit applications can be composed into groups of subcommands:
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
app = Cli()
|
|
228
|
+
training = Cli()
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@training.command(name="run")
|
|
232
|
+
def run(epochs: int = 10):
|
|
233
|
+
print(f"Training for {epochs} epochs")
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
app.add_subcommands(training, name="training")
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Run the nested command with:
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
python script.py training run --config config.yml --epochs 20
|
|
243
|
+
```
|
|
244
|
+
|
|
222
245
|
## Acknowledgement
|
|
223
246
|
|
|
224
247
|
We would like to thank [Assistance Publique – Hôpitaux de Paris](https://www.aphp.fr/)
|
|
@@ -4,7 +4,7 @@ import subprocess
|
|
|
4
4
|
from importlib import metadata
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
|
-
_BASE_VERSION = "0.
|
|
7
|
+
_BASE_VERSION = "0.13.0.dev0"
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def get_version(base_version: str = _BASE_VERSION) -> str:
|
|
@@ -36,10 +36,13 @@ def get_version(base_version: str = _BASE_VERSION) -> str:
|
|
|
36
36
|
except (OSError, subprocess.CalledProcessError):
|
|
37
37
|
tag = None
|
|
38
38
|
|
|
39
|
+
development_version = (
|
|
40
|
+
base_version if ".dev" in base_version else f"{base_version}.dev0"
|
|
41
|
+
)
|
|
39
42
|
return (
|
|
40
43
|
base_version
|
|
41
44
|
if tag in {base_version, f"v{base_version}"}
|
|
42
|
-
else f"{
|
|
45
|
+
else f"{development_version}+g{commit_hash}"
|
|
43
46
|
)
|
|
44
47
|
|
|
45
48
|
|
|
@@ -2,6 +2,7 @@ import importlib
|
|
|
2
2
|
import inspect
|
|
3
3
|
import re
|
|
4
4
|
import sys
|
|
5
|
+
import warnings
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from types import UnionType
|
|
7
8
|
from typing import (
|
|
@@ -17,9 +18,11 @@ from typing import (
|
|
|
17
18
|
get_origin,
|
|
18
19
|
)
|
|
19
20
|
|
|
21
|
+
from pydantic.fields import FieldInfo
|
|
22
|
+
|
|
20
23
|
from .config import Config, merge_from_disk
|
|
21
|
-
from .errors import ConfitValidationError
|
|
22
|
-
from .registry import validate_arguments
|
|
24
|
+
from .errors import ConfitValidationError
|
|
25
|
+
from .registry import VisibleDeprecationWarning, validate_arguments
|
|
23
26
|
from .utils.random import set_seed
|
|
24
27
|
from .utils.settings import is_debug
|
|
25
28
|
from .utils.xjson import loads
|
|
@@ -75,6 +78,11 @@ class Cli:
|
|
|
75
78
|
|
|
76
79
|
def __init__(self, *args: Any, **kwargs: Any):
|
|
77
80
|
self.commands = {}
|
|
81
|
+
self.subcommands = []
|
|
82
|
+
self.typer_args = args
|
|
83
|
+
self.typer_kwargs = kwargs
|
|
84
|
+
self.typer_cli = None
|
|
85
|
+
self.typer_warning_emitted = False
|
|
78
86
|
|
|
79
87
|
# User code calls this as a decorator to register one command.
|
|
80
88
|
# The stored metadata is used by main.
|
|
@@ -104,14 +112,188 @@ class Cli:
|
|
|
104
112
|
"fn": fn,
|
|
105
113
|
"validated": validated,
|
|
106
114
|
"help": help,
|
|
115
|
+
"typer_options": {
|
|
116
|
+
"cls": cls,
|
|
117
|
+
"context_settings": context_settings,
|
|
118
|
+
"help": help,
|
|
119
|
+
"epilog": epilog,
|
|
120
|
+
"short_help": short_help,
|
|
121
|
+
"options_metavar": options_metavar,
|
|
122
|
+
"add_help_option": add_help_option,
|
|
123
|
+
"no_args_is_help": no_args_is_help,
|
|
124
|
+
"hidden": hidden,
|
|
125
|
+
"deprecated": deprecated,
|
|
126
|
+
"rich_help_panel": rich_help_panel,
|
|
127
|
+
},
|
|
107
128
|
"registry": registry,
|
|
108
129
|
"default_config": default_config,
|
|
109
130
|
"merge_with_default_config": merge_with_default_config,
|
|
110
131
|
}
|
|
132
|
+
self.typer_cli = None
|
|
111
133
|
return validated
|
|
112
134
|
|
|
113
135
|
return wrapper
|
|
114
136
|
|
|
137
|
+
def add_subcommands(
|
|
138
|
+
self,
|
|
139
|
+
cli: "Cli",
|
|
140
|
+
*,
|
|
141
|
+
name: Optional[str] = None,
|
|
142
|
+
help: Optional[str] = None,
|
|
143
|
+
short_help: Optional[str] = None,
|
|
144
|
+
hidden: bool = False,
|
|
145
|
+
deprecated: bool = False,
|
|
146
|
+
) -> None:
|
|
147
|
+
"""Add another Confit CLI as a group or flattened command entries"""
|
|
148
|
+
if not isinstance(cli, Cli):
|
|
149
|
+
raise TypeError("add_subcommands expects a Confit Cli instance")
|
|
150
|
+
self.subcommands.append(
|
|
151
|
+
{
|
|
152
|
+
"cli": cli,
|
|
153
|
+
"name": name,
|
|
154
|
+
"help": help,
|
|
155
|
+
"short_help": short_help,
|
|
156
|
+
"hidden": hidden,
|
|
157
|
+
"deprecated": deprecated,
|
|
158
|
+
}
|
|
159
|
+
)
|
|
160
|
+
self.typer_cli = None
|
|
161
|
+
|
|
162
|
+
def add_typer(
|
|
163
|
+
self,
|
|
164
|
+
cli: "Cli",
|
|
165
|
+
*,
|
|
166
|
+
name: Optional[str] = None,
|
|
167
|
+
help: Optional[str] = None,
|
|
168
|
+
short_help: Optional[str] = None,
|
|
169
|
+
hidden: bool = False,
|
|
170
|
+
deprecated: bool = False,
|
|
171
|
+
) -> None:
|
|
172
|
+
"""Add Confit subcommands through the deprecated Typer compatible name"""
|
|
173
|
+
warnings.warn(
|
|
174
|
+
"Cli.add_typer() is deprecated, use Cli.add_subcommands() instead.",
|
|
175
|
+
VisibleDeprecationWarning,
|
|
176
|
+
stacklevel=2,
|
|
177
|
+
)
|
|
178
|
+
self.add_subcommands(
|
|
179
|
+
cli,
|
|
180
|
+
name=name,
|
|
181
|
+
help=help,
|
|
182
|
+
short_help=short_help,
|
|
183
|
+
hidden=hidden,
|
|
184
|
+
deprecated=deprecated,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def command_entries(self):
|
|
188
|
+
"""Return the command and group entries used by dispatch and help"""
|
|
189
|
+
entries = {
|
|
190
|
+
name: {"kind": "command", "cli": self, "command": command}
|
|
191
|
+
for name, command in self.commands.items()
|
|
192
|
+
}
|
|
193
|
+
for group in self.subcommands:
|
|
194
|
+
if group["name"] is None:
|
|
195
|
+
entries.update(group["cli"].command_entries())
|
|
196
|
+
else:
|
|
197
|
+
entries[group["name"]] = {
|
|
198
|
+
"kind": "group",
|
|
199
|
+
"cli": group["cli"],
|
|
200
|
+
"group": group,
|
|
201
|
+
}
|
|
202
|
+
return entries
|
|
203
|
+
|
|
204
|
+
def format_commands_help(self):
|
|
205
|
+
"""Render the visible commands and groups for the current CLI level"""
|
|
206
|
+
lines = ["Commands:"]
|
|
207
|
+
for name, entry in self.command_entries().items():
|
|
208
|
+
if entry["kind"] == "command":
|
|
209
|
+
options = entry["command"]["typer_options"]
|
|
210
|
+
else:
|
|
211
|
+
options = entry["group"]
|
|
212
|
+
if options["hidden"]:
|
|
213
|
+
continue
|
|
214
|
+
description = options.get("short_help") or options.get("help")
|
|
215
|
+
suffix = " (deprecated)" if options["deprecated"] else ""
|
|
216
|
+
if description:
|
|
217
|
+
description = inspect.cleandoc(description).splitlines()[0]
|
|
218
|
+
lines.append(f" {name}{suffix} {description}")
|
|
219
|
+
else:
|
|
220
|
+
lines.append(f" {name}{suffix}")
|
|
221
|
+
return "\n".join(lines)
|
|
222
|
+
|
|
223
|
+
def get_typer_cli(self):
|
|
224
|
+
"""Build and cache the Typer representation read by external Typer apps"""
|
|
225
|
+
if self.typer_cli is not None:
|
|
226
|
+
return self.typer_cli
|
|
227
|
+
try:
|
|
228
|
+
typer = importlib.import_module("typer")
|
|
229
|
+
except ImportError as e:
|
|
230
|
+
raise ImportError(
|
|
231
|
+
"Typer interoperability requires the application to install "
|
|
232
|
+
"and declare a dependency on 'typer'."
|
|
233
|
+
) from e
|
|
234
|
+
|
|
235
|
+
if not self.typer_warning_emitted:
|
|
236
|
+
warnings.warn(
|
|
237
|
+
"Adapting a Confit Cli for Typer is deprecated and will be removed "
|
|
238
|
+
"in the next major Confit release.",
|
|
239
|
+
VisibleDeprecationWarning,
|
|
240
|
+
stacklevel=3,
|
|
241
|
+
)
|
|
242
|
+
self.typer_warning_emitted = True
|
|
243
|
+
|
|
244
|
+
typer_cli = typer.Typer(*self.typer_args, **self.typer_kwargs)
|
|
245
|
+
for name, command in self.commands.items():
|
|
246
|
+
options = dict(command["typer_options"])
|
|
247
|
+
options["context_settings"] = {
|
|
248
|
+
**(options["context_settings"] or {}),
|
|
249
|
+
"ignore_unknown_options": True,
|
|
250
|
+
"allow_extra_args": True,
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
def create_callback(command_name, command_record):
|
|
254
|
+
def callback(
|
|
255
|
+
ctx: typer.Context,
|
|
256
|
+
config: Optional[List[Path]] = None,
|
|
257
|
+
):
|
|
258
|
+
return self.run_command(
|
|
259
|
+
command_name,
|
|
260
|
+
command_record,
|
|
261
|
+
config,
|
|
262
|
+
list(ctx.args),
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
return callback
|
|
266
|
+
|
|
267
|
+
typer_cli.command(name=name, **options)(create_callback(name, command))
|
|
268
|
+
|
|
269
|
+
for group in self.subcommands:
|
|
270
|
+
typer_cli.add_typer(
|
|
271
|
+
group["cli"],
|
|
272
|
+
name=group["name"],
|
|
273
|
+
help=group["help"],
|
|
274
|
+
short_help=group["short_help"],
|
|
275
|
+
hidden=group["hidden"],
|
|
276
|
+
deprecated=group["deprecated"],
|
|
277
|
+
)
|
|
278
|
+
self.typer_cli = typer_cli
|
|
279
|
+
return typer_cli
|
|
280
|
+
|
|
281
|
+
@property
|
|
282
|
+
def registered_commands(self):
|
|
283
|
+
return self.get_typer_cli().registered_commands
|
|
284
|
+
|
|
285
|
+
@property
|
|
286
|
+
def registered_groups(self):
|
|
287
|
+
return self.get_typer_cli().registered_groups
|
|
288
|
+
|
|
289
|
+
@property
|
|
290
|
+
def registered_callback(self):
|
|
291
|
+
return self.get_typer_cli().registered_callback
|
|
292
|
+
|
|
293
|
+
@property
|
|
294
|
+
def info(self):
|
|
295
|
+
return self.get_typer_cli().info
|
|
296
|
+
|
|
115
297
|
def __call__(self, args: Optional[List[str]] = None):
|
|
116
298
|
return self.main(args=args)
|
|
117
299
|
|
|
@@ -119,27 +301,37 @@ class Cli:
|
|
|
119
301
|
# It either prints help or executes the selected command.
|
|
120
302
|
def main(self, args: Optional[List[str]] = None):
|
|
121
303
|
args = list(sys.argv[1:] if args is None else args)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
304
|
+
cli = self
|
|
305
|
+
command = None
|
|
306
|
+
name = None
|
|
307
|
+
group_help = None
|
|
308
|
+
while command is None:
|
|
309
|
+
entries = cli.command_entries()
|
|
310
|
+
commands_help = cli.format_commands_help()
|
|
311
|
+
if group_help:
|
|
312
|
+
commands_help = f"{inspect.cleandoc(group_help)}\n\n{commands_help}"
|
|
313
|
+
implicit_command = (
|
|
314
|
+
cli is self and not self.subcommands and len(entries) == 1
|
|
315
|
+
)
|
|
316
|
+
if args and args[0] in {"--help", "-h"} and not implicit_command:
|
|
317
|
+
print(commands_help)
|
|
318
|
+
raise SystemExit(0)
|
|
319
|
+
if args and args[0] in entries:
|
|
320
|
+
name = args.pop(0)
|
|
321
|
+
entry = entries[name]
|
|
322
|
+
if entry["kind"] == "group":
|
|
323
|
+
cli = entry["cli"]
|
|
324
|
+
group_help = entry["group"]["help"]
|
|
325
|
+
continue
|
|
326
|
+
command = entry["command"]
|
|
327
|
+
elif implicit_command:
|
|
328
|
+
name, entry = next(iter(entries.items()))
|
|
329
|
+
command = entry["command"]
|
|
330
|
+
else:
|
|
331
|
+
if not args:
|
|
332
|
+
print(commands_help)
|
|
333
|
+
raise SystemExit(0)
|
|
334
|
+
raise Exception("Missing command")
|
|
143
335
|
|
|
144
336
|
if any(arg in {"--help", "-h"} for arg in args):
|
|
145
337
|
print(add_config_overrides_help(command["help"], command["fn"]))
|
|
@@ -180,19 +372,13 @@ class Cli:
|
|
|
180
372
|
if default_config is not None and merge_with_default_config:
|
|
181
373
|
config = Config(default_config).merge(config)
|
|
182
374
|
|
|
183
|
-
|
|
184
|
-
# For example model.date becomes script.model.date when model is a parameter.
|
|
185
|
-
model_fields = (
|
|
186
|
-
validated.model.model_fields
|
|
187
|
-
if hasattr(validated.model, "model_fields")
|
|
188
|
-
else validated.model.__fields__
|
|
189
|
-
)
|
|
375
|
+
parameters = inspect.signature(fn).parameters
|
|
190
376
|
for key, value in parse_overrides(overrides).items():
|
|
191
377
|
if "." not in key:
|
|
192
378
|
parts = (name, key)
|
|
193
379
|
else:
|
|
194
380
|
parts = key.split(".")
|
|
195
|
-
if parts[0] in
|
|
381
|
+
if parts[0] in parameters and parts[0] not in config:
|
|
196
382
|
parts = (name, *parts)
|
|
197
383
|
current = config
|
|
198
384
|
if parts[0] not in current:
|
|
@@ -202,9 +388,11 @@ class Cli:
|
|
|
202
388
|
current[parts[-1]] = value
|
|
203
389
|
|
|
204
390
|
try:
|
|
205
|
-
default_seed =
|
|
206
|
-
if default_seed
|
|
391
|
+
default_seed = parameters["seed"].default if "seed" in parameters else None
|
|
392
|
+
if isinstance(default_seed, FieldInfo):
|
|
207
393
|
default_seed = default_seed.get_default()
|
|
394
|
+
elif default_seed is inspect.Parameter.empty:
|
|
395
|
+
default_seed = None
|
|
208
396
|
seed = Config.resolve(
|
|
209
397
|
config.get(name, {}).get("seed", default_seed),
|
|
210
398
|
registry=registry,
|
|
@@ -224,8 +412,8 @@ class Cli:
|
|
|
224
412
|
)
|
|
225
413
|
return validated(**resolved_config, config_meta=config_meta)
|
|
226
414
|
return validated(**resolved_config)
|
|
227
|
-
except
|
|
228
|
-
e
|
|
415
|
+
except ConfitValidationError as e:
|
|
416
|
+
e = e.with_path((name,))
|
|
229
417
|
if is_debug() or e.__cause__ is not None:
|
|
230
418
|
raise e
|
|
231
419
|
print("Validation error:", str(e))
|
|
@@ -15,7 +15,6 @@ from confit.errors import (
|
|
|
15
15
|
ConfitValidationError,
|
|
16
16
|
CyclicReferenceError,
|
|
17
17
|
MissingReference,
|
|
18
|
-
patch_errors,
|
|
19
18
|
remove_lib_from_traceback,
|
|
20
19
|
)
|
|
21
20
|
from confit.utils.collections import flatten_sections, join_path, split_path
|
|
@@ -24,6 +23,7 @@ from confit.utils.settings import is_debug
|
|
|
24
23
|
from confit.utils.xjson import Reference, dumps, loads
|
|
25
24
|
|
|
26
25
|
RESOLVED_TO_CONFIG = WeakKeyDictionary()
|
|
26
|
+
RESOLVED_TO_CONFIGS = WeakKeyDictionary()
|
|
27
27
|
|
|
28
28
|
Loc = Tuple[Union[int, str]]
|
|
29
29
|
T = TypeVar("T")
|
|
@@ -117,13 +117,9 @@ class Config(dict):
|
|
|
117
117
|
current = current[part]
|
|
118
118
|
|
|
119
119
|
current.clear()
|
|
120
|
-
errors = []
|
|
121
120
|
for k, v in parser.items(section):
|
|
122
121
|
current[loads(k)] = loads(v)
|
|
123
122
|
|
|
124
|
-
if errors:
|
|
125
|
-
raise ConfitValidationError(errors=errors)
|
|
126
|
-
|
|
127
123
|
if resolve:
|
|
128
124
|
return config.resolve(registry=registry)
|
|
129
125
|
|
|
@@ -207,6 +203,9 @@ class Config(dict):
|
|
|
207
203
|
Try to convert non-serializable objects using the RESOLVED_TO_CONFIG object
|
|
208
204
|
back to their original catalogue + params form
|
|
209
205
|
|
|
206
|
+
Objects can set `__confit_serialization_origin__` to `"first"` to use
|
|
207
|
+
their first recorded origin instead of the latest origin
|
|
208
|
+
|
|
210
209
|
We try to preserve referential equalities between non dict/list/tuple
|
|
211
210
|
objects by serializing subsequent references to the same object as references
|
|
212
211
|
to its first occurrence in the tree.
|
|
@@ -258,7 +257,13 @@ class Config(dict):
|
|
|
258
257
|
return type(o)(rec(v, (*path, i)) for i, v in enumerate(o))
|
|
259
258
|
cfg = None
|
|
260
259
|
try:
|
|
261
|
-
|
|
260
|
+
try:
|
|
261
|
+
origins = RESOLVED_TO_CONFIGS[o]
|
|
262
|
+
origin = getattr(o, "__confit_serialization_origin__", "last")
|
|
263
|
+
origin_config = origins[0] if origin == "first" else origins[-1]
|
|
264
|
+
except KeyError:
|
|
265
|
+
origin_config = RESOLVED_TO_CONFIG[o]
|
|
266
|
+
cfg = (cfg or Config()).merge(origin_config)
|
|
262
267
|
except (KeyError, TypeError):
|
|
263
268
|
pass
|
|
264
269
|
try:
|
|
@@ -454,11 +459,9 @@ class Config(dict):
|
|
|
454
459
|
# we need to do it here
|
|
455
460
|
Config._store_resolved(resolved, cfg)
|
|
456
461
|
except ConfitValidationError as e:
|
|
457
|
-
e =
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
name=getattr(e, "name", None),
|
|
461
|
-
).with_traceback(remove_lib_from_traceback(e.__traceback__))
|
|
462
|
+
e = e.with_path(loc).with_traceback(
|
|
463
|
+
remove_lib_from_traceback(e.__traceback__)
|
|
464
|
+
)
|
|
462
465
|
if not is_debug():
|
|
463
466
|
e.__cause__ = None
|
|
464
467
|
e.__suppress_context__ = True
|
|
@@ -598,7 +601,7 @@ class Config(dict):
|
|
|
598
601
|
@classmethod
|
|
599
602
|
def _store_resolved(cls, resolved: Any, config: Dict[str, Any]):
|
|
600
603
|
"""
|
|
601
|
-
Adds a resolved object to the
|
|
604
|
+
Adds a resolved object and its origin to the serialization mappings
|
|
602
605
|
for later retrieval during serialization
|
|
603
606
|
([`.serialize`][confit.config.Config.serialize])
|
|
604
607
|
|
|
@@ -609,6 +612,8 @@ class Config(dict):
|
|
|
609
612
|
"""
|
|
610
613
|
try:
|
|
611
614
|
RESOLVED_TO_CONFIG[resolved] = config
|
|
615
|
+
origins = RESOLVED_TO_CONFIGS.setdefault(resolved, [])
|
|
616
|
+
origins.append(config)
|
|
612
617
|
except TypeError:
|
|
613
618
|
pass
|
|
614
619
|
|
|
@@ -11,22 +11,12 @@ from typing import (
|
|
|
11
11
|
)
|
|
12
12
|
|
|
13
13
|
import pydantic
|
|
14
|
+
from pydantic_core import core_schema
|
|
14
15
|
from typing_extensions import ParamSpec, Protocol
|
|
15
16
|
|
|
16
|
-
from confit.errors import
|
|
17
|
-
ConfitValidationError,
|
|
18
|
-
ErrorWrapper,
|
|
19
|
-
patch_errors,
|
|
20
|
-
to_legacy_error,
|
|
21
|
-
)
|
|
17
|
+
from confit.errors import ConfitValidationError
|
|
22
18
|
from confit.typing import cast
|
|
23
19
|
|
|
24
|
-
if pydantic.VERSION >= "2":
|
|
25
|
-
from pydantic_core import core_schema
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
PYDANTIC_V1 = pydantic.VERSION.split(".")[0] == "1"
|
|
29
|
-
|
|
30
20
|
P = ParamSpec("P")
|
|
31
21
|
R = TypeVar("R", covariant=True)
|
|
32
22
|
|
|
@@ -94,14 +84,9 @@ class MetaDraft(type):
|
|
|
94
84
|
|
|
95
85
|
def validate(cls, value, config=None):
|
|
96
86
|
if not isinstance(value, Draft):
|
|
97
|
-
raise ConfitValidationError(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
exc=TypeError(f"Expected {cls}, got {value.__class__}"),
|
|
101
|
-
loc=(),
|
|
102
|
-
),
|
|
103
|
-
],
|
|
104
|
-
model=cls,
|
|
87
|
+
raise ConfitValidationError.from_exception(
|
|
88
|
+
TypeError(f"Expected {cls}, got {value.__class__}"),
|
|
89
|
+
source=cls,
|
|
105
90
|
name=cls.__name__,
|
|
106
91
|
)
|
|
107
92
|
actual = value._func
|
|
@@ -115,23 +100,13 @@ class MetaDraft(type):
|
|
|
115
100
|
else: # pragma: no cover
|
|
116
101
|
cast(Union[Type[cls.type_], Callable[..., cls.type_]], actual)
|
|
117
102
|
except pydantic.ValidationError as e:
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
ErrorWrapper(
|
|
122
|
-
exc=TypeError(f"Expected {cls}, got {Draft[actual]}"),
|
|
123
|
-
loc=e.raw_errors[0]._loc,
|
|
124
|
-
),
|
|
125
|
-
],
|
|
126
|
-
model=cls,
|
|
103
|
+
raise ConfitValidationError.from_exception(
|
|
104
|
+
TypeError(f"Expected {cls}, got {Draft[actual]}"),
|
|
105
|
+
source=cls,
|
|
127
106
|
name=cls.__name__,
|
|
128
|
-
)
|
|
129
|
-
raise e
|
|
107
|
+
).with_path(e.errors()[0]["loc"])
|
|
130
108
|
return value
|
|
131
109
|
|
|
132
|
-
def __get_validators__(cls):
|
|
133
|
-
yield cls.validate
|
|
134
|
-
|
|
135
110
|
def __get_pydantic_core_schema__(cls, source, handler):
|
|
136
111
|
return core_schema.no_info_plain_validator_function(cls.validate)
|
|
137
112
|
|
|
@@ -183,11 +158,7 @@ class Draft(Generic[R], metaclass=MetaDraft):
|
|
|
183
158
|
try:
|
|
184
159
|
res = self._func(**{**kwargs, **self._kwargs})
|
|
185
160
|
except ConfitValidationError as e:
|
|
186
|
-
raise
|
|
187
|
-
patch_errors(e.raw_errors, self._loc, self._kwargs),
|
|
188
|
-
model=e.model,
|
|
189
|
-
name=getattr(e, "name", None),
|
|
190
|
-
).with_traceback(e.__traceback__)
|
|
161
|
+
raise e.with_path(self._loc).with_traceback(e.__traceback__)
|
|
191
162
|
return res
|
|
192
163
|
|
|
193
164
|
def _raise_draft_error(self):
|