confit 0.12.0__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.12.0 → confit-0.13.0.dev0}/PKG-INFO +2 -2
  2. {confit-0.12.0 → confit-0.13.0.dev0}/confit/_version.py +5 -2
  3. {confit-0.12.0 → confit-0.13.0.dev0}/confit/cli.py +11 -13
  4. {confit-0.12.0 → confit-0.13.0.dev0}/confit/config.py +3 -10
  5. {confit-0.12.0 → confit-0.13.0.dev0}/confit/draft.py +10 -39
  6. confit-0.13.0.dev0/confit/errors.py +265 -0
  7. confit-0.13.0.dev0/confit/registry.py +534 -0
  8. confit-0.13.0.dev0/confit/typing.py +42 -0
  9. {confit-0.12.0 → confit-0.13.0.dev0}/confit.egg-info/PKG-INFO +2 -2
  10. {confit-0.12.0 → confit-0.13.0.dev0}/confit.egg-info/requires.txt +1 -1
  11. {confit-0.12.0 → confit-0.13.0.dev0}/pyproject.toml +1 -1
  12. {confit-0.12.0 → confit-0.13.0.dev0}/tests/test_as_list.py +19 -36
  13. {confit-0.12.0 → confit-0.13.0.dev0}/tests/test_cli.py +9 -16
  14. {confit-0.12.0 → confit-0.13.0.dev0}/tests/test_config_instance.py +27 -31
  15. {confit-0.12.0 → confit-0.13.0.dev0}/tests/test_validate.py +102 -89
  16. confit-0.12.0/confit/errors.py +0 -362
  17. confit-0.12.0/confit/registry.py +0 -675
  18. confit-0.12.0/confit/typing.py +0 -58
  19. {confit-0.12.0 → confit-0.13.0.dev0}/LICENSE +0 -0
  20. {confit-0.12.0 → confit-0.13.0.dev0}/README.md +0 -0
  21. {confit-0.12.0 → confit-0.13.0.dev0}/confit/__init__.py +0 -0
  22. {confit-0.12.0 → confit-0.13.0.dev0}/confit/utils/__init__.py +0 -0
  23. {confit-0.12.0 → confit-0.13.0.dev0}/confit/utils/collections.py +0 -0
  24. {confit-0.12.0 → confit-0.13.0.dev0}/confit/utils/eval.py +0 -0
  25. {confit-0.12.0 → confit-0.13.0.dev0}/confit/utils/random.py +0 -0
  26. {confit-0.12.0 → confit-0.13.0.dev0}/confit/utils/settings.py +0 -0
  27. {confit-0.12.0 → confit-0.13.0.dev0}/confit/utils/xjson.py +0 -0
  28. {confit-0.12.0 → confit-0.13.0.dev0}/confit.egg-info/SOURCES.txt +0 -0
  29. {confit-0.12.0 → confit-0.13.0.dev0}/confit.egg-info/dependency_links.txt +0 -0
  30. {confit-0.12.0 → confit-0.13.0.dev0}/confit.egg-info/top_level.txt +0 -0
  31. {confit-0.12.0 → confit-0.13.0.dev0}/setup.cfg +0 -0
  32. {confit-0.12.0 → confit-0.13.0.dev0}/tests/test_draft.py +0 -0
  33. {confit-0.12.0 → confit-0.13.0.dev0}/tests/test_registry.py +0 -0
  34. {confit-0.12.0 → confit-0.13.0.dev0}/tests/test_typing.py +0 -0
  35. {confit-0.12.0 → 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.12.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>=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
@@ -4,7 +4,7 @@ import subprocess
4
4
  from importlib import metadata
5
5
  from pathlib import Path
6
6
 
7
- _BASE_VERSION = "0.12.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"{base_version}.dev0+g{commit_hash}"
45
+ else f"{development_version}+g{commit_hash}"
43
46
  )
44
47
 
45
48
 
@@ -18,8 +18,10 @@ from typing import (
18
18
  get_origin,
19
19
  )
20
20
 
21
+ from pydantic.fields import FieldInfo
22
+
21
23
  from .config import Config, merge_from_disk
22
- from .errors import ConfitValidationError, LegacyValidationError, patch_errors
24
+ from .errors import ConfitValidationError
23
25
  from .registry import VisibleDeprecationWarning, validate_arguments
24
26
  from .utils.random import set_seed
25
27
  from .utils.settings import is_debug
@@ -370,19 +372,13 @@ class Cli:
370
372
  if default_config is not None and merge_with_default_config:
371
373
  config = Config(default_config).merge(config)
372
374
 
373
- # model_fields tells whether dotted overrides start at the command section.
374
- # For example model.date becomes script.model.date when model is a parameter.
375
- model_fields = (
376
- validated.model.model_fields
377
- if hasattr(validated.model, "model_fields")
378
- else validated.model.__fields__
379
- )
375
+ parameters = inspect.signature(fn).parameters
380
376
  for key, value in parse_overrides(overrides).items():
381
377
  if "." not in key:
382
378
  parts = (name, key)
383
379
  else:
384
380
  parts = key.split(".")
385
- if parts[0] in model_fields and parts[0] not in config:
381
+ if parts[0] in parameters and parts[0] not in config:
386
382
  parts = (name, *parts)
387
383
  current = config
388
384
  if parts[0] not in current:
@@ -392,9 +388,11 @@ class Cli:
392
388
  current[parts[-1]] = value
393
389
 
394
390
  try:
395
- default_seed = model_fields.get("seed")
396
- if default_seed is not None:
391
+ default_seed = parameters["seed"].default if "seed" in parameters else None
392
+ if isinstance(default_seed, FieldInfo):
397
393
  default_seed = default_seed.get_default()
394
+ elif default_seed is inspect.Parameter.empty:
395
+ default_seed = None
398
396
  seed = Config.resolve(
399
397
  config.get(name, {}).get("seed", default_seed),
400
398
  registry=registry,
@@ -414,8 +412,8 @@ class Cli:
414
412
  )
415
413
  return validated(**resolved_config, config_meta=config_meta)
416
414
  return validated(**resolved_config)
417
- except (LegacyValidationError, ConfitValidationError) as e:
418
- e.raw_errors = patch_errors(e.raw_errors, (name,))
415
+ except ConfitValidationError as e:
416
+ e = e.with_path((name,))
419
417
  if is_debug() or e.__cause__ is not None:
420
418
  raise e
421
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
@@ -118,13 +117,9 @@ class Config(dict):
118
117
  current = current[part]
119
118
 
120
119
  current.clear()
121
- errors = []
122
120
  for k, v in parser.items(section):
123
121
  current[loads(k)] = loads(v)
124
122
 
125
- if errors:
126
- raise ConfitValidationError(errors=errors)
127
-
128
123
  if resolve:
129
124
  return config.resolve(registry=registry)
130
125
 
@@ -464,11 +459,9 @@ class Config(dict):
464
459
  # we need to do it here
465
460
  Config._store_resolved(resolved, cfg)
466
461
  except ConfitValidationError as e:
467
- e = ConfitValidationError(
468
- errors=patch_errors(e.raw_errors, loc, params),
469
- model=e.model,
470
- name=getattr(e, "name", None),
471
- ).with_traceback(remove_lib_from_traceback(e.__traceback__))
462
+ e = e.with_path(loc).with_traceback(
463
+ remove_lib_from_traceback(e.__traceback__)
464
+ )
472
465
  if not is_debug():
473
466
  e.__cause__ = None
474
467
  e.__suppress_context__ = True
@@ -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):
@@ -0,0 +1,265 @@
1
+ import inspect
2
+ import warnings
3
+ from textwrap import indent
4
+ from typing import Callable, Tuple, Union
5
+
6
+ import pydantic
7
+
8
+ from confit.utils.collections import join_path
9
+ from confit.utils.settings import is_debug
10
+ from confit.utils.xjson import Reference
11
+
12
+ Loc = Tuple[Union[int, str]]
13
+
14
+
15
+ class MissingReference(Exception):
16
+ """
17
+ Raised when one or multiple references cannot be resolved.
18
+ """
19
+
20
+ def __init__(self, ref: Reference):
21
+ """
22
+ Parameters
23
+ ----------
24
+ ref: Reference
25
+ The reference that could not be resolved.
26
+ """
27
+ self.ref = ref
28
+ super().__init__()
29
+
30
+ def __str__(self):
31
+ """
32
+ String representation of the exception
33
+ """
34
+ return "Could not interpolate the following reference: {}".format(self.ref)
35
+
36
+
37
+ class CyclicReferenceError(Exception):
38
+ """
39
+ Raised when a cyclic reference is detected.
40
+ """
41
+
42
+ def __init__(self, path: Loc):
43
+ """
44
+ Parameters
45
+ ----------
46
+ path: Loc
47
+ The path of the cyclic reference
48
+ """
49
+ self.path = path
50
+ super().__init__()
51
+
52
+ def __str__(self):
53
+ """
54
+ String representation of the exception
55
+ """
56
+ return "Cyclic reference detected at {}".format(join_path(self.path))
57
+
58
+
59
+ def remove_lib_from_traceback(tb):
60
+ """
61
+ Remove the lib folder from the traceback
62
+ """
63
+ # compare package to module in f_globals
64
+ if is_debug():
65
+ return tb
66
+ if tb is not None and tb.tb_frame.f_globals.get("__package__") == __package__:
67
+ return remove_lib_from_traceback(tb.tb_next)
68
+ if tb is None or tb.tb_next is None:
69
+ return tb
70
+ tb.tb_next = remove_lib_from_traceback(tb.tb_next)
71
+ return tb
72
+
73
+
74
+ class ConfitValidationError(ValueError):
75
+ """
76
+ Structured validation errors with paths added by config and pipeline resolution
77
+ """
78
+
79
+ def __init__(self, errors, *, name=None, source=None, model=None):
80
+ if model is not None:
81
+ warnings.warn(
82
+ "Use source instead of model", DeprecationWarning, stacklevel=2
83
+ )
84
+ source = model
85
+ self.source = source
86
+ self.name = name or getattr(source, "__qualname__", None)
87
+ # Old EDS-NLP callers can aggregate lists of errors inside another list
88
+ self.details = [
89
+ detail
90
+ for error in errors
91
+ for detail in (
92
+ ConfitValidationError(error).details
93
+ if isinstance(error, list)
94
+ else [error]
95
+ )
96
+ ]
97
+ super().__init__(self.details)
98
+
99
+ @classmethod
100
+ def from_exception(cls, error, *, source=None, name=None, signature=None):
101
+ """
102
+ Normalize Pydantic errors before config resolution adds its parent path
103
+ """
104
+ if isinstance(error, cls):
105
+ return cls(
106
+ error.errors(), source=source or error.source, name=name or error.name
107
+ )
108
+ if not isinstance(error, pydantic.ValidationError):
109
+ return cls(
110
+ [
111
+ {
112
+ "loc": (),
113
+ "msg": str(error),
114
+ "type": "value_error",
115
+ "ctx": {"error": error},
116
+ }
117
+ ],
118
+ source=source,
119
+ name=name,
120
+ )
121
+ details = []
122
+ for detail in error.errors(include_url=False):
123
+ loc = detail["loc"]
124
+ if signature is not None and loc and isinstance(loc[0], int):
125
+ names = [
126
+ name
127
+ for name, p in signature.parameters.items()
128
+ if p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD)
129
+ ]
130
+ if loc[0] < len(names):
131
+ loc = (names[loc[0]], *loc[1:])
132
+ nested = detail.get("ctx", {}).get("error")
133
+ if isinstance(nested, cls):
134
+ target = source
135
+ # Only typed construction joins paths, separate calls keep their name
136
+ try:
137
+ for part in loc:
138
+ target = (
139
+ target.model_fields[part].annotation
140
+ if hasattr(target, "model_fields")
141
+ else inspect.signature(target).parameters[part].annotation
142
+ )
143
+ except (KeyError, AttributeError, TypeError, ValueError):
144
+ target = None
145
+ if target is not None and target is nested.source:
146
+ details.extend(nested.with_path(loc).errors())
147
+ continue
148
+ details.append({**detail, "loc": loc})
149
+ return cls(details, source=source, name=name)
150
+
151
+ def with_path(self, path, *, name=None):
152
+ """
153
+ Return an error with a parent path without modifying shared component errors
154
+ """
155
+ return type(self)(
156
+ [{**detail, "loc": (*path, *detail["loc"])} for detail in self.details],
157
+ source=self.source,
158
+ name=name or self.name,
159
+ )
160
+
161
+ @classmethod
162
+ def combine(cls, errors, *, name=None):
163
+ """
164
+ Collect failures from sibling components in their original order
165
+ """
166
+ return cls([detail for error in errors for detail in error.errors()], name=name)
167
+
168
+ def errors(self):
169
+ return [dict(detail) for detail in self.details]
170
+
171
+ def __str__(self):
172
+ lines = []
173
+ for detail in self.details:
174
+ original = detail.get("ctx", {}).get("error")
175
+ original = original if isinstance(original, BaseException) else None
176
+ msg = str(original) if original is not None else detail["msg"]
177
+ if original is None:
178
+ msg = (
179
+ "field required"
180
+ if detail["type"].startswith("missing")
181
+ else msg[:1].lower() + msg[1:]
182
+ )
183
+ if "input" in detail and not detail["type"].startswith("missing"):
184
+ value = detail["input"]
185
+ try:
186
+ preview = repr(value)
187
+ except Exception:
188
+ preview = object.__repr__(value)
189
+ preview = preview[:50] + "..." if len(preview) > 50 else preview
190
+ msg += f", got {preview} ({type(value).__name__})"
191
+ lines.append(
192
+ "-> " + ".".join(map(str, detail["loc"])) + "\n" + indent(msg, " ")
193
+ )
194
+ count = len(self.details)
195
+ name = f" for {self.name}()" if self.name is not None else ""
196
+ return (
197
+ f"{count} validation error{'s' if count != 1 else ''}{name}\n"
198
+ + "\n".join(lines)
199
+ )
200
+
201
+ @property
202
+ def raw_errors(self):
203
+ warnings.warn(
204
+ "Use errors() instead of raw_errors", DeprecationWarning, stacklevel=2
205
+ )
206
+ return self.errors()
207
+
208
+ @raw_errors.setter
209
+ def raw_errors(self, errors):
210
+ warnings.warn(
211
+ "Use with_path or combine instead of assigning raw_errors",
212
+ DeprecationWarning,
213
+ stacklevel=2,
214
+ )
215
+ self.details = type(self)(errors).details
216
+
217
+ @property
218
+ def model(self):
219
+ warnings.warn("Use source instead of model", DeprecationWarning, stacklevel=2)
220
+ return self.source
221
+
222
+ @model.setter
223
+ def model(self, model):
224
+ warnings.warn("Use source instead of model", DeprecationWarning, stacklevel=2)
225
+ self.source = model
226
+
227
+
228
+ class SignatureError(TypeError):
229
+ def __init__(self, func: Callable):
230
+ message = f"{func} must not have positional only args or duplicated kwargs"
231
+ super().__init__(message)
232
+
233
+
234
+ def patch_errors(errors, path=(), values=None, model=None, drop_names=()):
235
+ """
236
+ Adapt old EDS-NLP error calls to ConfitValidationError methods
237
+ """
238
+ warnings.warn(
239
+ "Use ConfitValidationError.from_exception and with_path "
240
+ "instead of patch_errors",
241
+ DeprecationWarning,
242
+ stacklevel=2,
243
+ )
244
+ is_exception = isinstance(errors, (pydantic.ValidationError, ConfitValidationError))
245
+ error = (
246
+ ConfitValidationError.from_exception(errors, source=model)
247
+ if is_exception
248
+ else ConfitValidationError(errors, source=model)
249
+ )
250
+ error = error.with_path(path)
251
+ if drop_names:
252
+ error = ConfitValidationError(
253
+ [
254
+ {
255
+ **detail,
256
+ "loc": tuple(
257
+ part for part in detail["loc"] if part not in drop_names
258
+ ),
259
+ }
260
+ for detail in error.errors()
261
+ ],
262
+ source=error.source,
263
+ name=error.name,
264
+ )
265
+ return error if is_exception else error.errors()