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.
Files changed (35) hide show
  1. {confit-0.11.1 → confit-0.13.0.dev0}/PKG-INFO +25 -2
  2. {confit-0.11.1 → confit-0.13.0.dev0}/README.md +23 -0
  3. {confit-0.11.1 → confit-0.13.0.dev0}/confit/_version.py +5 -2
  4. {confit-0.11.1 → confit-0.13.0.dev0}/confit/cli.py +223 -35
  5. {confit-0.11.1 → confit-0.13.0.dev0}/confit/config.py +17 -12
  6. {confit-0.11.1 → confit-0.13.0.dev0}/confit/draft.py +10 -39
  7. confit-0.13.0.dev0/confit/errors.py +265 -0
  8. confit-0.13.0.dev0/confit/registry.py +534 -0
  9. confit-0.13.0.dev0/confit/typing.py +42 -0
  10. {confit-0.11.1 → confit-0.13.0.dev0}/confit.egg-info/PKG-INFO +25 -2
  11. {confit-0.11.1 → confit-0.13.0.dev0}/confit.egg-info/requires.txt +1 -1
  12. {confit-0.11.1 → confit-0.13.0.dev0}/pyproject.toml +1 -1
  13. {confit-0.11.1 → confit-0.13.0.dev0}/tests/test_as_list.py +19 -36
  14. {confit-0.11.1 → confit-0.13.0.dev0}/tests/test_cli.py +180 -18
  15. {confit-0.11.1 → confit-0.13.0.dev0}/tests/test_config_instance.py +42 -31
  16. {confit-0.11.1 → confit-0.13.0.dev0}/tests/test_validate.py +102 -89
  17. confit-0.11.1/confit/errors.py +0 -362
  18. confit-0.11.1/confit/registry.py +0 -675
  19. confit-0.11.1/confit/typing.py +0 -58
  20. {confit-0.11.1 → confit-0.13.0.dev0}/LICENSE +0 -0
  21. {confit-0.11.1 → confit-0.13.0.dev0}/confit/__init__.py +0 -0
  22. {confit-0.11.1 → confit-0.13.0.dev0}/confit/utils/__init__.py +0 -0
  23. {confit-0.11.1 → confit-0.13.0.dev0}/confit/utils/collections.py +0 -0
  24. {confit-0.11.1 → confit-0.13.0.dev0}/confit/utils/eval.py +0 -0
  25. {confit-0.11.1 → confit-0.13.0.dev0}/confit/utils/random.py +0 -0
  26. {confit-0.11.1 → confit-0.13.0.dev0}/confit/utils/settings.py +0 -0
  27. {confit-0.11.1 → confit-0.13.0.dev0}/confit/utils/xjson.py +0 -0
  28. {confit-0.11.1 → confit-0.13.0.dev0}/confit.egg-info/SOURCES.txt +0 -0
  29. {confit-0.11.1 → confit-0.13.0.dev0}/confit.egg-info/dependency_links.txt +0 -0
  30. {confit-0.11.1 → confit-0.13.0.dev0}/confit.egg-info/top_level.txt +0 -0
  31. {confit-0.11.1 → confit-0.13.0.dev0}/setup.cfg +0 -0
  32. {confit-0.11.1 → confit-0.13.0.dev0}/tests/test_draft.py +0 -0
  33. {confit-0.11.1 → confit-0.13.0.dev0}/tests/test_registry.py +0 -0
  34. {confit-0.11.1 → confit-0.13.0.dev0}/tests/test_typing.py +0 -0
  35. {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.11.1
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>=1.2
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.11.1"
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"{base_version}.dev0+g{commit_hash}"
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, LegacyValidationError, patch_errors
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
- commands_help = "\n".join(
123
- ["Commands:", *(f" {name}" for name in self.commands)]
124
- )
125
-
126
- # The command name is optional for single command apps.
127
- # args is left with only config paths and overrides.
128
- if args and args[0] in self.commands:
129
- name = args.pop(0)
130
- command = self.commands[name]
131
- elif len(self.commands) == 1:
132
- name = next(iter(self.commands))
133
- command = self.commands[name]
134
- elif args and args[0] in {"--help", "-h"}:
135
- print(commands_help)
136
- raise SystemExit(0)
137
- else:
138
- raise Exception("Missing command")
139
-
140
- if not args and len(self.commands) > 1:
141
- print(commands_help)
142
- raise SystemExit(0)
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
- # model_fields tells whether dotted overrides start at the command section.
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 model_fields and parts[0] not in config:
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 = model_fields.get("seed")
206
- if default_seed is not None:
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 (LegacyValidationError, ConfitValidationError) as e:
228
- e.raw_errors = patch_errors(e.raw_errors, (name,))
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
- cfg = (cfg or Config()).merge(RESOLVED_TO_CONFIG[o])
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 = ConfitValidationError(
458
- errors=patch_errors(e.raw_errors, loc, params),
459
- model=e.model,
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 RESOLVED_TO_CONFIG dict
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
- ErrorWrapper(
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
- e = to_legacy_error(e, None)
119
- e = ConfitValidationError(
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 ConfitValidationError(
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):