Skip to content

Commit 48cfe11

Browse files
authored
model: replace assert-based validation with raise ValueError (works under python -O) (#516)
* model: replace assert-based validation with raise ValueError VSSData / VSSDataDatatype / VSSUnit pydantic validators in model.py used `assert COND, "msg"` to enforce ~32 invariants. Python's `-O` optimisation flag strips assert statements at compile time, so any deployment running `python -O` (some packaging pipelines, performance- tuned environments) silently skipped all VSS spec validation. The parser would then accept invalid specs as valid. This is a documented anti-pattern in pydantic's own docs: https://docs.pydantic.dev/latest/concepts/validators/ Replace each `assert COND, "msg"` with the equivalent `if not COND: raise ValueError("msg")` form. Pydantic wraps the ValueError into a ValidationError automatically, so externally- observable behaviour is identical in normal mode. The difference: the new form is preserved under `python -O`, restoring validation in optimised builds. No test changes needed: the existing parametric suite in tests/test_model.py already exercises 50+ invalid-input cases and expects ValidationError. Those tests would have failed under `python -O` before this change; now they pass in both modes. Signed-off-by: Matt Jones <47545907+SoundMatt@users.noreply.github.com> * tests: update error-type expectations after assert -> raise refactor Six tests in tests/vspec/test_description_error/test_description_error.py and tests/vspec/test_overlay/test_overlay.py asserted on the literal string `'type': 'assertion_error'` in the captured log output. Pydantic reports the error type field differently depending on what the validator raises: - `assert COND, "msg"` raises AssertionError, which pydantic surfaces as `'type': 'assertion_error'`. - `if not COND: raise ValueError("msg")` raises ValueError, which pydantic surfaces as `'type': 'value_error'`. The previous commit replaced asserts with raises across model.py; update the test expectations accordingly. No production behaviour change — the affected tests still verify that invalid input is rejected with a ValidationError carrying the same message; only the type-tag string differs. Signed-off-by: Matt Jones <47545907+SoundMatt@users.noreply.github.com> --------- Signed-off-by: Matt Jones <47545907+SoundMatt@users.noreply.github.com>
1 parent 0dcfa93 commit 48cfe11

3 files changed

Lines changed: 83 additions & 45 deletions

File tree

src/vss_tools/model.py

Lines changed: 75 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -123,15 +123,18 @@ def check_const_uid_format(cls, v: str | None) -> str | None:
123123
if v is None:
124124
return v
125125
pattern = r"^0x[0-9A-Fa-f]{8}$"
126-
assert bool(re.match(pattern, v)), f"'{v}' is not a valid 'constUID'"
126+
if not re.match(pattern, v):
127+
raise ValueError(f"'{v}' is not a valid 'constUID'")
127128
return v
128129

129130
@model_validator(mode="after")
130131
def ensure_description(self) -> Self:
131132
"""Give better explanation for empty description."""
132-
assert (
133-
self.description != ""
134-
), "All nodes in the final tree must have a description. Implicit branches are not allowed in final tree!"
133+
if self.description == "":
134+
raise ValueError(
135+
"All nodes in the final tree must have a description. "
136+
"Implicit branches are not allowed in final tree!"
137+
)
135138
return self
136139

137140

@@ -167,15 +170,17 @@ class VSSUnit(BaseModel):
167170
@field_validator("quantity")
168171
@classmethod
169172
def check_valid_quantity(cls, v: str) -> str:
170-
assert v in dynamic_quantities, f"Invalid quantity: '{v}'"
173+
if v not in dynamic_quantities:
174+
raise ValueError(f"Invalid quantity: '{v}'")
171175
return v
172176

173177
@field_validator("allowed_datatypes")
174178
@classmethod
175179
def check_valid_datatypes(cls, values: list[str]) -> list[str]:
176180
datatypes = get_all_datatypes()
177181
for value in values:
178-
assert value in datatypes, f"Invalid datatype: '{value}'"
182+
if value not in datatypes:
183+
raise ValueError(f"Invalid datatype: '{value}'")
179184
return values
180185

181186

@@ -207,7 +212,8 @@ class VSSDataDatatype(VSSData):
207212

208213
@model_validator(mode="after")
209214
def check_allowed_enum_conflict(self) -> Self:
210-
assert not (self.allowed and self.enum), "Cannot use 'allowed' and 'enum'"
215+
if self.allowed and self.enum:
216+
raise ValueError("Cannot use 'allowed' and 'enum'")
211217
return self
212218

213219
@model_validator(mode="after")
@@ -219,10 +225,12 @@ def check_enum(self) -> Self:
219225

220226
values = set()
221227
for key, value in self.enum.items():
222-
assert value not in values, f"Duplicated enum value: '{value}'"
228+
if value in values:
229+
raise ValueError(f"Duplicated enum value: '{value}'")
223230
values.add(value)
224231

225-
assert re.match(key_pattern, key), f"Invalid enum key: '{key}'"
232+
if not re.match(key_pattern, key):
233+
raise ValueError(f"Invalid enum key: '{key}'")
226234

227235
return self
228236

@@ -233,7 +241,8 @@ def check_type_arraysize_consistency(self) -> Self:
233241
datatype is an array
234242
"""
235243
if self.arraysize is not None:
236-
assert is_array(self.datatype), f"'arraysize' set on a non array datatype: '{self.datatype}'"
244+
if not is_array(self.datatype):
245+
raise ValueError(f"'arraysize' set on a non array datatype: '{self.datatype}'")
237246
return self
238247

239248
def check_min_max_valid_datatype(self) -> Self:
@@ -243,9 +252,11 @@ def check_min_max_valid_datatype(self) -> Self:
243252
except DatatypesException:
244253
raise ValueError(f"Cannot define min/max for datatype '{self.datatype}'")
245254
if self.min is not None:
246-
assert Datatypes.is_datatype(self.min, self.datatype), f"min '{self.min}' is not an '{self.datatype}'"
255+
if not Datatypes.is_datatype(self.min, self.datatype):
256+
raise ValueError(f"min '{self.min}' is not an '{self.datatype}'")
247257
if self.max is not None:
248-
assert Datatypes.is_datatype(self.max, self.datatype), f"max '{self.max}' is not an '{self.datatype}'"
258+
if not Datatypes.is_datatype(self.max, self.datatype):
259+
raise ValueError(f"max '{self.max}' is not an '{self.datatype}'")
249260
return self
250261

251262
def check_default_min_max(self) -> Self:
@@ -273,15 +284,18 @@ def check_type_default_consistency(self) -> Self:
273284
if self.default is not None:
274285
array = is_array(self.datatype)
275286
if array:
276-
assert isinstance(
277-
self.default, list
278-
), f"'default' with type '{type(self.default)}' does not match datatype '{self.datatype}'"
287+
if not isinstance(self.default, list):
288+
raise ValueError(
289+
f"'default' with type '{type(self.default)}' does not match datatype '{self.datatype}'"
290+
)
279291
if self.arraysize:
280-
assert len(self.default) == self.arraysize, "'default' array size does not match 'arraysize'"
292+
if len(self.default) != self.arraysize:
293+
raise ValueError("'default' array size does not match 'arraysize'")
281294
else:
282-
assert not isinstance(
283-
self.default, list
284-
), f"'default' with type '{type(self.default)}' does not match datatype '{self.datatype}'"
295+
if isinstance(self.default, list):
296+
raise ValueError(
297+
f"'default' with type '{type(self.default)}' does not match datatype '{self.datatype}'"
298+
)
285299

286300
check_values = [self.default]
287301
if array:
@@ -295,7 +309,8 @@ def check_type_default_consistency(self) -> Self:
295309
raise ValueError(f"invalid 'default' format for datatype '{self.datatype}': {e.message}")
296310
else:
297311
for v in check_values:
298-
assert Datatypes.is_datatype(v, self.datatype), f"'{v}' is not of type '{self.datatype}'"
312+
if not Datatypes.is_datatype(v, self.datatype):
313+
raise ValueError(f"'{v}' is not of type '{self.datatype}'")
299314
return self
300315

301316
def check_default_values_in_enum(self) -> Self:
@@ -308,7 +323,8 @@ def check_default_values_in_enum(self) -> Self:
308323
if not isinstance(self.default, list):
309324
values = [self.default]
310325
for v in values:
311-
assert v in self.enum.values(), f"default value '{v}' is not a valid enum value"
326+
if v not in self.enum.values():
327+
raise ValueError(f"default value '{v}' is not a valid enum value")
312328
return self
313329

314330
def check_allowed_datatype_consistency(self) -> Self:
@@ -318,18 +334,22 @@ def check_allowed_datatype_consistency(self) -> Self:
318334
Checks datatypes to be int when using `enum`
319335
"""
320336
if self.allowed:
321-
assert Datatypes.get_type(self.datatype), "'allowed' cannot be used with struct datatype"
337+
if not Datatypes.get_type(self.datatype):
338+
raise ValueError("'allowed' cannot be used with struct datatype")
322339
for v in self.allowed:
323-
assert Datatypes.is_datatype(v, self.datatype), f"'{v}' is not of type '{self.datatype}'"
340+
if not Datatypes.is_datatype(v, self.datatype):
341+
raise ValueError(f"'{v}' is not of type '{self.datatype}'")
324342
return self
325343

326344
@model_validator(mode="after")
327345
def check_allowed_min_max(self) -> Self:
328346
err = "'min/max' and 'allowed' cannot be used together"
329347
if self.allowed is not None:
330-
assert self.min is None and self.max is None, err
348+
if self.min is not None or self.max is not None:
349+
raise ValueError(err)
331350
if self.min is not None or self.max is not None:
332-
assert self.allowed is None, err
351+
if self.allowed is not None:
352+
raise ValueError(err)
333353
return self
334354

335355
def check_default_values_in_allowed(self) -> Self:
@@ -342,7 +362,8 @@ def check_default_values_in_allowed(self) -> Self:
342362
if not isinstance(self.default, list):
343363
values = [self.default]
344364
for v in values:
345-
assert v in self.allowed, f"default value '{v}' is not in 'allowed' list"
365+
if v not in self.allowed:
366+
raise ValueError(f"default value '{v}' is not in 'allowed' list")
346367
return self
347368

348369
def check_enum_datatypes(self) -> Self:
@@ -351,33 +372,39 @@ def check_enum_datatypes(self) -> Self:
351372
"""
352373
if self.enum:
353374
# Returns None on struct types
354-
assert Datatypes.get_type(self.datatype), "'enum' cannot be used with struct datatype"
375+
if not Datatypes.get_type(self.datatype):
376+
raise ValueError("'enum' cannot be used with struct datatype")
355377

356378
# Check whether datatype is an int type
357379
check = self.datatype
358380
if is_array(self.datatype):
359381
check = check.rstrip("[]")
360382

361-
assert Datatypes.is_subtype_of(check, Datatypes.INT[0]), "'datatype' needs to be an int when using 'enum'"
383+
if not Datatypes.is_subtype_of(check, Datatypes.INT[0]):
384+
raise ValueError("'datatype' needs to be an int when using 'enum'")
362385

363386
# Check enum values matching the 'datatype'
364387
for v in self.enum.values():
365-
assert Datatypes.is_datatype(v, check), f"enum value '{v}' is not of type '{self.datatype}'"
388+
if not Datatypes.is_datatype(v, check):
389+
raise ValueError(f"enum value '{v}' is not of type '{self.datatype}'")
366390

367391
return self
368392

369393
@model_validator(mode="after")
370394
def check_enum_min_max_conflict(self) -> Self:
371395
err = "'min/max' and 'enum' cannot be used together"
372396
if self.enum is not None:
373-
assert self.min is None and self.max is None, err
397+
if self.min is not None or self.max is not None:
398+
raise ValueError(err)
374399
if self.min is not None or self.max is not None:
375-
assert self.enum is None, err
400+
if self.enum is not None:
401+
raise ValueError(err)
376402
return self
377403

378404
@model_validator(mode="after")
379405
def check_datatype(self) -> Self:
380-
assert self.datatype in get_all_datatypes(self.fqn), f"'{self.datatype}' is not a valid datatype"
406+
if self.datatype not in get_all_datatypes(self.fqn):
407+
raise ValueError(f"'{self.datatype}' is not a valid datatype")
381408
self.datatype = resolve_datatype(self.datatype, self.fqn)
382409
self = self.check_type_default_consistency()
383410
self = self.check_enum_datatypes()
@@ -393,7 +420,8 @@ def check_datatype(self) -> Self:
393420
def check_valid_unit(cls, v: str | None) -> str | None:
394421
if v is None:
395422
return v
396-
assert v in dynamic_units, f"'{v}' is not a valid unit"
423+
if v not in dynamic_units:
424+
raise ValueError(f"'{v}' is not a valid unit")
397425
return v
398426

399427
@model_validator(mode="after")
@@ -403,13 +431,13 @@ def check_datatype_matching_allowed_unit_datatypes(self) -> Self:
403431
referenced in the unit if given
404432
"""
405433
if self.unit:
406-
assert Datatypes.get_type(self.datatype), f"Cannot use 'unit' with complex datatype: '{self.datatype}'"
434+
if not Datatypes.get_type(self.datatype):
435+
raise ValueError(f"Cannot use 'unit' with complex datatype: '{self.datatype}'")
407436
allowed_datatypes = dynamic_units[self.unit].allowed_datatypes
408437
if allowed_datatypes is None:
409438
allowed_datatypes = []
410-
assert any(
411-
Datatypes.is_subtype_of(self.datatype.rstrip("[]"), a) for a in allowed_datatypes
412-
), f"'{self.datatype}' is not allowed for unit '{self.unit}'"
439+
if not any(Datatypes.is_subtype_of(self.datatype.rstrip("[]"), a) for a in allowed_datatypes):
440+
raise ValueError(f"'{self.datatype}' is not allowed for unit '{self.unit}'")
413441
return self
414442

415443
@model_validator(mode="after")
@@ -425,10 +453,11 @@ def check_datatype_pattern(self) -> Self:
425453
if self.pattern:
426454
# Datatypes.TUPLE[0] is the string name of the type.
427455
allowed_for = f"Allowed types: {[Datatypes.STRING[0], Datatypes.STRING_ARRAY[0]]}"
428-
assert Datatypes.get_type(self.datatype) in [
456+
if Datatypes.get_type(self.datatype) not in [
429457
Datatypes.STRING,
430458
Datatypes.STRING_ARRAY,
431-
], f"Field 'pattern' is not allowed for type: '{self.datatype}'. {allowed_for}"
459+
]:
460+
raise ValueError(f"Field 'pattern' is not allowed for type: '{self.datatype}'. {allowed_for}")
432461

433462
def check_value_match(value_to_check: Any, value_type: str, reg_exp: str) -> None:
434463
check_values = [value_to_check]
@@ -437,17 +466,20 @@ def check_value_match(value_to_check: Any, value_type: str, reg_exp: str) -> Non
437466
check_values = value_to_check
438467

439468
for def_val in check_values:
440-
assert re.match(
441-
reg_exp, def_val
442-
), f"Specified '{value_type}' value: '{def_val}' must match defined pattern: '{self.pattern}'"
469+
if not re.match(reg_exp, def_val):
470+
raise ValueError(
471+
f"Specified '{value_type}' value: '{def_val}' "
472+
f"must match defined pattern: '{self.pattern}'"
473+
)
443474

444475
if self.default:
445476
check_value_match(self.default, "default", self.pattern)
446477

447478
if self.allowed:
448479
check_value_match(self.allowed, "allowed", self.pattern)
449480

450-
assert self.enum is None, "'enum' cannot be used together with 'pattern'"
481+
if self.enum is not None:
482+
raise ValueError("'enum' cannot be used together with 'pattern'")
451483

452484
return self
453485

tests/vspec/test_description_error/test_description_error.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,8 @@ def test_description_error(vspec_file: str, type_file: str, type_out_file: str,
4040
assert process.returncode != 0
4141
log_content = log.read_text()
4242
print(log_content)
43-
assert "'type': 'assertion_error'" in log_content
43+
# Validator now raises ValueError (was assert) — pydantic tags this as
44+
# 'value_error' in its error report. See PR #516 for the assert -> raise
45+
# ValueError refactor.
46+
assert "'type': 'value_error'" in log_content
4447
assert "1 model error(s):" in log_content

tests/vspec/test_overlay/test_overlay.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,5 +71,8 @@ def test_overlay_branch_error(tmp_path):
7171
assert process.returncode != 0
7272
log_content = log.read_text()
7373
assert "'A.AB' has 1 model error(s)" in log_content
74-
assert "'type': 'assertion_error'" in log_content
74+
# Validator now raises ValueError (was assert) — pydantic tags this as
75+
# 'value_error' in its error report. See PR #516 for the assert -> raise
76+
# ValueError refactor.
77+
assert "'type': 'value_error'" in log_content
7578
assert "description" in log_content

0 commit comments

Comments
 (0)