dataconf 3.5.0__tar.gz → 3.7.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dataconf
3
- Version: 3.5.0
3
+ Version: 3.7.0
4
4
  Summary: Simple dataclasses configuration management for Python with hocon/json/yaml/properties/env-vars/dict support.
5
5
  Home-page: https://github.com/zifeo/dataconf
6
6
  License: MPL-2.0
@@ -15,9 +15,9 @@ Classifier: Programming Language :: Python :: 3.11
15
15
  Classifier: Programming Language :: Python :: 3.12
16
16
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
17
  Requires-Dist: PyYAML (>=6.0.2,<7.0.0)
18
- Requires-Dist: isodate (>=0.6.1,<0.8.0)
18
+ Requires-Dist: isodate (>=0.7.2,<0.8.0)
19
19
  Requires-Dist: pyhocon (>=0.3.61,<0.4.0)
20
- Requires-Dist: pyparsing (>=3.2.1,<4.0.0)
20
+ Requires-Dist: pyparsing (>=3.2.3,<4.0.0)
21
21
  Requires-Dist: python-dateutil (>=2.9.0.post0,<3.0.0)
22
22
  Project-URL: Repository, https://github.com/zifeo/dataconf
23
23
  Description-Content-Type: text/markdown
@@ -45,13 +45,14 @@ class Multi:
45
45
  self.kwargs = kwargs
46
46
 
47
47
  def env(self, prefix: str, **kwargs) -> "Multi":
48
- self.strict = False
49
48
  data = env_vars_parse(prefix, os.environ)
50
- return self.dict(data, **kwargs)
49
+ strict = kwargs.pop("strict", False)
50
+ return self.dict(data, strict=strict, **kwargs)
51
51
 
52
52
  def dict(self, obj: Dict[str, Any], **kwargs) -> "Multi":
53
53
  conf = ConfigFactory.from_dict(obj)
54
- return Multi(self.confs + [conf], self.strict, **kwargs)
54
+ strict = kwargs.pop("strict", self.strict)
55
+ return Multi(self.confs + [conf], strict, **kwargs)
55
56
 
56
57
  def string(self, s: str, loader: str = HOCON, **kwargs) -> "Multi":
57
58
  if loader == YAML:
@@ -42,6 +42,21 @@ if PY310up:
42
42
 
43
43
  NoneType = type(None)
44
44
 
45
+ _BOOL_TRUE = {"true", "1", "yes", "on", "t", "y"}
46
+ _BOOL_FALSE = {"false", "0", "no", "off", "f", "n"}
47
+
48
+
49
+ def __parse_bool(value: Any):
50
+ if isinstance(value, bool):
51
+ return value
52
+ if isinstance(value, str):
53
+ normalised = value.strip().lower()
54
+ if normalised in _BOOL_TRUE:
55
+ return True
56
+ if normalised in _BOOL_FALSE:
57
+ return False
58
+ return None
59
+
45
60
 
46
61
  def __parse_type(value: Any, clazz: Type, path: str, check: bool):
47
62
  try:
@@ -50,14 +65,16 @@ def __parse_type(value: Any, clazz: Type, path: str, check: bool):
50
65
  except TypeError:
51
66
  pass
52
67
 
53
- raise TypeConfigException(f"expected type {clazz} at {path}, got {type(value)}")
68
+ raise TypeConfigException(
69
+ f"expected type {clazz} at {path_to_str(path)}, got {type(value)}"
70
+ )
54
71
 
55
72
 
56
73
  def is_union(origin):
57
74
  return origin is Union or (PY310up and origin is UnionType)
58
75
 
59
76
 
60
- def is_optional(type: Type):
77
+ def is_optional(type: Type | Any | str):
61
78
  # Optional = Union[T, NoneType]
62
79
  return is_union(get_origin(type)) and NoneType in get_args(type)
63
80
 
@@ -66,7 +83,7 @@ def __parse(value: any, clazz: Type, path: str, strict: bool, ignore_unexpected:
66
83
  if is_dataclass(clazz):
67
84
  if not isinstance(value, ConfigTree):
68
85
  raise TypeConfigException(
69
- f"expected type {clazz} at {path}, got {type(value)}"
86
+ f"expected type {clazz} at {path_to_str(path)}, got {type(value)}"
70
87
  )
71
88
 
72
89
  fs = {}
@@ -96,15 +113,34 @@ def __parse(value: any, clazz: Type, path: str, strict: bool, ignore_unexpected:
96
113
  # Optional not found
97
114
  fs[f.name] = None
98
115
 
116
+ elif is_dataclass(f.type):
117
+ if all(
118
+ not isinstance(field.default, _MISSING_TYPE)
119
+ or not isinstance(field.default_factory, _MISSING_TYPE)
120
+ or is_optional(field.type)
121
+ for field in fields(f.type)
122
+ ):
123
+ fs[f.name] = f.type(
124
+ **{
125
+ field.name: None
126
+ for field in fields(f.type)
127
+ if is_optional(field.type)
128
+ }
129
+ )
130
+ else:
131
+ raise MalformedConfigException(
132
+ f'expected type {clazz} at {path_to_str(path)}, no field "{f.name}" found and {f.type} cannot be implicitly created'
133
+ )
134
+
99
135
  else:
100
136
  raise MalformedConfigException(
101
- f"expected type {clazz} at {path}, no {f.name} found in dataclass"
137
+ f'expected type {clazz} at {path_to_str(path)}, no field "{f.name}" found'
102
138
  )
103
139
 
104
140
  unexpected_keys = value.keys() - {renamings.get(k, k) for k in fs.keys()}
105
141
  if len(unexpected_keys) > 0 and not ignore_unexpected:
106
142
  raise UnexpectedKeysException(
107
- f'unexpected key(s) "{", ".join(unexpected_keys)}" detected for type {clazz} at {path}'
143
+ f'unexpected key(s) "{", ".join(unexpected_keys)}" detected for type {clazz} at {path_to_str(path)}'
108
144
  )
109
145
 
110
146
  return clazz(**fs)
@@ -114,7 +150,9 @@ def __parse(value: any, clazz: Type, path: str, strict: bool, ignore_unexpected:
114
150
 
115
151
  if origin is list:
116
152
  if value is None:
117
- raise MalformedConfigException(f"expected list at {path} but received None")
153
+ raise MalformedConfigException(
154
+ f"expected list at {path_to_str(path)} but received None"
155
+ )
118
156
 
119
157
  if len(args) != 1:
120
158
  raise MissingTypeException("expected list with type information: List[?]")
@@ -128,7 +166,7 @@ def __parse(value: any, clazz: Type, path: str, strict: bool, ignore_unexpected:
128
166
  if origin is tuple:
129
167
  if value is None:
130
168
  raise MalformedConfigException(
131
- f"expected tuple at {path} but received None"
169
+ f"expected tuple at {path_to_str(path)} but received None"
132
170
  )
133
171
 
134
172
  if len(args) < 1:
@@ -181,18 +219,18 @@ def __parse(value: any, clazz: Type, path: str, strict: bool, ignore_unexpected:
181
219
  return None
182
220
 
183
221
  raise TypeConfigException(
184
- f"expected one of {', '.join(map(str, args))} at {path}, got {type(value)}"
222
+ f"expected one of {', '.join(map(str, args))} at {path_to_str(path)}, got {type(value)}"
185
223
  )
186
224
 
187
225
  if clazz is bool:
188
- if not strict and value is not None:
189
- try:
190
- value = bool(value)
191
- except ValueError:
192
- pass
193
- return __parse_type(value, clazz, path, isinstance(value, bool))
226
+ parsed = __parse_bool(value)
227
+ if parsed is not None:
228
+ return parsed
229
+ return __parse_type(value, clazz, path, False)
194
230
 
195
231
  if clazz is int:
232
+ if isinstance(value, bool):
233
+ return __parse_type(value, clazz, path, False)
196
234
  if not strict and value is not None:
197
235
  try:
198
236
  value = int(value)
@@ -201,6 +239,8 @@ def __parse(value: any, clazz: Type, path: str, strict: bool, ignore_unexpected:
201
239
  return __parse_type(value, clazz, path, isinstance(value, int))
202
240
 
203
241
  if clazz is float:
242
+ if isinstance(value, bool):
243
+ return __parse_type(value, clazz, path, False)
204
244
  if not strict and value is not None:
205
245
  try:
206
246
  value = float(value)
@@ -220,13 +260,15 @@ def __parse(value: any, clazz: Type, path: str, strict: bool, ignore_unexpected:
220
260
  return value
221
261
 
222
262
  if isclass(clazz) and (issubclass(clazz, Enum) or issubclass(clazz, IntEnum)):
223
- if isinstance(value, int):
263
+ if isinstance(value, int) and not isinstance(value, bool):
224
264
  return clazz.__call__(value)
225
265
  elif issubclass(clazz, str):
226
266
  return clazz(value)
227
267
  elif isinstance(value, str):
228
268
  return clazz.__getitem__(value)
229
- raise TypeConfigException(f"expected str or int at {path}, got {type(value)}")
269
+ raise TypeConfigException(
270
+ f"expected str or int at {path_to_str(path)}, got {type(value)}"
271
+ )
230
272
 
231
273
  if isclass(clazz) and issubclass(clazz, Path):
232
274
  return clazz.__call__(value)
@@ -235,7 +277,7 @@ def __parse(value: any, clazz: Type, path: str, strict: bool, ignore_unexpected:
235
277
  if value in args:
236
278
  return value
237
279
  raise TypeConfigException(
238
- f"expected one of {', '.join(map(str, args))} at {path}, got {value}"
280
+ f"expected one of {', '.join(map(str, args))} at {path_to_str(path)}, got {value}"
239
281
  )
240
282
 
241
283
  if clazz is datetime:
@@ -244,7 +286,7 @@ def __parse(value: any, clazz: Type, path: str, strict: bool, ignore_unexpected:
244
286
  return isoparse(dt)
245
287
  except ValueError as e:
246
288
  raise ParseException(
247
- f"expected type {clazz} at {path}, cannot parse due to {e}"
289
+ f"expected type {clazz} at {path_to_str(path)}, cannot parse due to {e}"
248
290
  )
249
291
 
250
292
  if clazz is timedelta:
@@ -258,7 +300,7 @@ def __parse(value: any, clazz: Type, path: str, strict: bool, ignore_unexpected:
258
300
  return duration
259
301
  except ValueError as e:
260
302
  raise ParseException(
261
- f"expected type {clazz} at {path}, cannot parse due to {e}"
303
+ f"expected type {clazz} at {path_to_str(path)}, cannot parse due to {e}"
262
304
  )
263
305
 
264
306
  if clazz is relativedelta:
@@ -292,17 +334,19 @@ def __parse(value: any, clazz: Type, path: str, strict: bool, ignore_unexpected:
292
334
  elif len(child_successes) > 1:
293
335
  matching_classes = "\n- ".join(map(lambda x: x[0].__name__, child_successes))
294
336
  raise AmbiguousSubclassException(
295
- f"""multiple subtypes of {clazz} matched at {path}, use '_type' to disambiguate:\n- {matching_classes}"""
337
+ f"""multiple subtypes of {clazz} matched at {path_to_str(path)}, use '_type' to disambiguate:\n- {matching_classes}"""
296
338
  )
297
339
 
298
340
  # no need to check length; false if empty
299
341
  if child_failures:
300
342
  failures = "\n- ".join([str(c) for c in child_failures])
301
343
  raise TypeConfigException(
302
- f"expected type {clazz} at {path}, failed subclasses:\n- {failures}"
344
+ f"expected type {clazz} at {path_to_str(path)}, failed subclasses:\n- {failures}"
303
345
  )
304
346
 
305
- raise TypeConfigException(f"expected type {clazz} at {path}, got {type(value)}")
347
+ raise TypeConfigException(
348
+ f"expected type {clazz} at {path_to_str(path)}, got {type(value)}"
349
+ )
306
350
 
307
351
 
308
352
  def __generate(value: object, path: str):
@@ -399,6 +443,10 @@ def __env_vars_parse(prefix: str, obj: Dict[str, Any]):
399
443
  return ret
400
444
 
401
445
 
446
+ def path_to_str(path: str) -> str:
447
+ return path if len(path) > 0 else "root"
448
+
449
+
402
450
  def __cli_parse(argv: List[str]):
403
451
  kvs = {}
404
452
  i = 0
@@ -1,6 +1,6 @@
1
1
  import sys
2
2
 
3
- __version__ = "3.5.0"
3
+ __version__ = "3.7.0"
4
4
 
5
5
  PY310up = sys.version_info >= (3, 10)
6
6
  PY311up = sys.version_info >= (3, 11)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "dataconf"
3
- version = "3.5.0"
3
+ version = "3.7.0"
4
4
  description = "Simple dataclasses configuration management for Python with hocon/json/yaml/properties/env-vars/dict support."
5
5
  authors = []
6
6
  license = "MPL-2.0"
@@ -35,20 +35,20 @@ dataconf = 'dataconf.cli:run'
35
35
  python = "^3.9"
36
36
  python-dateutil = "^2.9.0.post0"
37
37
  PyYAML = "^6.0.2"
38
- isodate = ">=0.6.1,<0.8.0"
38
+ isodate = "^0.7.2"
39
39
  pyhocon = "^0.3.61"
40
- pyparsing = "^3.2.1"
40
+ pyparsing = "^3.2.3"
41
41
 
42
42
  [tool.poetry.group.dev.dependencies]
43
- pytest = ">=8.3.5"
43
+ pytest = ">=8.4.1"
44
44
  pre-commit = ">=4.2.0"
45
- commitizen = ">=4.4.1"
46
- pytest-httpserver = "^1.1.2"
47
- ruff = ">=0.11.2"
45
+ commitizen = ">=4.8.3"
46
+ pytest-httpserver = "^1.1.3"
47
+ ruff = ">=0.12.7"
48
48
 
49
49
  [tool.commitizen]
50
50
  name = "cz_conventional_commits"
51
- version = "3.5.0"
51
+ version = "3.7.0"
52
52
  tag_format = "v$major.$minor.$patch$prerelease"
53
53
  version_files = [
54
54
  "pyproject.toml:version",
File without changes
File without changes
File without changes
File without changes
File without changes