pyfcstm.utils.validate
Validation framework utilities for model validation workflows.
This module provides a lightweight validation framework for Python models. It
defines base exceptions for validation failures and a mixin-style interface
(IValidatable) that allows classes to register validation rules and
perform aggregated validation.
The module contains the following main components:
ValidationError- Exception for a single validation rule failureModelValidationError- Aggregated exception for multiple failuresIValidatable- Mixin interface for defining and running validators
Example:
>>> from pyfcstm.utils.validate import IValidatable, ValidationError
>>>
>>> class MyModel(IValidatable):
... def __init__(self, value: int):
... self.value = value
...
... def _validate_positive(self) -> None:
... if self.value <= 0:
... raise ValidationError("Value must be positive.")
...
... __validators__ = [_validate_positive]
...
>>> model = MyModel(1)
>>> model.validate()
>>> bad_model = MyModel(0)
>>> try:
... bad_model.validate()
... except Exception as err:
... print(type(err).__name__)
ModelValidationError
Span
- class pyfcstm.utils.validate.Span(line: int, column: int, end_line: int | None = None, end_column: int | None = None)[source]
Source-code location used as the anchor for a
ModelDiagnostic.All coordinates are 1-based.
end_line/end_columnare optional; when omitted the span is treated as pointing at a single source position.- Parameters:
line (int) – 1-based source line where the diagnostic anchor begins.
column (int) – 1-based source column where the diagnostic anchor begins.
end_line (int, optional) – 1-based source line where the diagnostic anchor ends, defaults to
None.end_column (int, optional) – 1-based source column where the diagnostic anchor ends, defaults to
None.
Example:
>>> from pyfcstm.utils.validate import Span >>> span = Span(line=3, column=12) >>> span.line, span.column (3, 12) >>> Span(line=3, column=12, end_line=3, end_column=20).end_column 20
ModelDiagnostic
- class pyfcstm.utils.validate.ModelDiagnostic(code: str, severity: ~typing.Literal['error', 'warning'], message: str, span: ~pyfcstm.utils.validate.Span | None = None, refs: ~typing.Dict[str, ~typing.Any] = <factory>)[source]
Structured semantic or design-health diagnostic produced by the model layer.
Designed as the stable contract between
pyfcstm.modeland downstream consumers such as IDE integrations, LLM agent loops, and evaluation scripts. Consumers should dispatch oncodeonly and treatmessageas human-facing text that may change between releases.The full set of valid
codevalues together with theirrefspayload schema lives inpyfcstm.diagnostics.codes(loaded frompyfcstm/diagnostics/codes.yaml); this dataclass intentionally does not validatecodeat construction time so that experimental codes can be emitted in tests.severityis enforced at construction time to be one of'error'/'warning'— a typo such as'Error'would otherwise causeis_error()to silently returnFalseand skew downstream dispatch.The dataclass is frozen so the public contract surface (
code/severity/message/span) cannot be mutated after construction.refsremains a mutabledictbecause PR-2 needs to populate it during emit — downstream consumers should treat it as read-only.- Parameters:
code (str) – Stable diagnostic code, e.g.
'E_UNDEFINED_VAR','W_DEADLOCK_LEAF'. Always treated as the public contract.severity (str) – Either
'error'or'warning'.message (str) – Human-readable rendering of the diagnostic. Not part of the contract — downstream tools must not regex-match against it.
span (pyfcstm.utils.validate.Span, optional) – Optional source position, defaults to
None.refs (Dict[str, Any]) – Structured payload keyed by field names defined in
codes.yamlfor this code. Defaults to an empty dict.
Example:
>>> from pyfcstm.utils.validate import ModelDiagnostic, Span >>> diag = ModelDiagnostic( ... code='E_UNDEFINED_VAR', ... severity='error', ... message="Unknown guard variable 'unknown_var' in transition", ... span=Span(line=5, column=21), ... refs={'var_name': 'unknown_var', 'referenced_in': 'guard'}, ... ) >>> diag.code 'E_UNDEFINED_VAR' >>> diag.is_error() True >>> diag.refs['var_name'] 'unknown_var'
- format_line() str[source]
Render this diagnostic as a single human-readable summary line.
Used by
ModelValidationError._build_summary_message()so that both legacyValidationErrorentries and structuredModelDiagnosticentries share the same[code] messageprefix style in aggregated error output.- Returns:
A one-line
[severity/code] messagerepresentation.- Return type:
str
ValidationError
- class pyfcstm.utils.validate.ValidationError[source]
Base exception class for validation errors.
This exception should be raised when a single validation rule fails. It is designed to be caught and collected by
IValidatable.Example:
>>> raise ValidationError("Invalid value.") Traceback (most recent call last): ... ValidationError: Invalid value.
ModelValidationError
- class pyfcstm.utils.validate.ModelValidationError(errors: List[ValidationError] | None = None, diagnostics: List[ModelDiagnostic] | None = None, message: str | None = None)[source]
Exception class for aggregating multiple validation errors.
This exception contains a list of
ValidationErrorinstances and formats them into a readable error message.It multi-inherits from
SyntaxErrorfor backwards compatibility: callers that previously caughtSyntaxErrorraised frompyfcstm.modelcontinue to work after later PRs in the Layer 1 refactor switch raise sites to this class.In addition to the legacy
errorslist, instances may also carry adiagnosticslist of structuredModelDiagnosticobjects when raised from the new collect-mode pipeline. Existing callers that do not readdiagnosticscontinue to work as before.- Parameters:
errors (List[ValidationError], optional) – List of validation errors that occurred, defaults to
None.diagnostics (List[ModelDiagnostic], optional) – List of structured diagnostics, defaults to
None.message (str, optional) – Pre-formatted summary message, defaults to
None. When omitted, a summary is built fromerrors/diagnostics.
- Variables:
errors (List[ValidationError]) – Stored validation errors.
diagnostics (List[ModelDiagnostic]) – Stored structured diagnostics.
Example:
>>> err = ModelValidationError([ValidationError("A"), ValidationError("B")]) >>> "2 errors" in str(err) True >>> isinstance(err, SyntaxError) True >>> err.diagnostics []
- __init__(errors: List[ValidationError] | None = None, diagnostics: List[ModelDiagnostic] | None = None, message: str | None = None) None[source]
ModelValueError
- class pyfcstm.utils.validate.ModelValueError(errors: List[ValidationError] | None = None, diagnostics: List[ModelDiagnostic] | None = None, message: str | None = None)[source]
Raised by model lookup APIs when an input string is syntactically invalid as a value (empty, bad format, dotted path that exceeds the root state, …).
Multi-inherits from
ModelValidationError(which itself multi-inheritsSyntaxError) and fromValueError, so:except ValueError:continues to catch this — preserving the pre-PR-3 behavior at every call site that already handlesValueErrorraised fromState.resolve_event/StateMachine.resolve_event.except SyntaxError:continues to catch too (inherited viaModelValidationError), keeping the PR-2 multi-inheritance contract consistent.The new
except ModelValueError:(orModelValidationError) lets diagnostic-aware consumers dispatch on the typed class.
All structured diagnostics emitted via this exception carry
code='E_EVENT_REF_INVALID'per the contract inpyfcstm/diagnostics/codes.yaml.Example:
>>> from pyfcstm.utils.validate import ModelValueError >>> try: ... raise ModelValueError(message="invalid ref") ... except ValueError as e: ... isinstance(e, ModelValueError) True
ModelLookupError
- class pyfcstm.utils.validate.ModelLookupError(errors: List[ValidationError] | None = None, diagnostics: List[ModelDiagnostic] | None = None, message: str | None = None)[source]
Raised by model lookup APIs when a state path or event name cannot be found in the live state hierarchy.
Multi-inherits
ModelValidationErrorandLookupError, so:except LookupError:continues to catch this — preserving the pre-PR-3 behavior at every call site that already handlesLookupErrorraised fromState.resolve_event/StateMachine.resolve_event.except SyntaxError:andexcept ModelValidationError:both catch as well (inherited path).
Structured diagnostics emitted via this exception carry
code='E_EVENT_NOT_FOUND'per the contract inpyfcstm/diagnostics/codes.yaml.Example:
>>> from pyfcstm.utils.validate import ModelLookupError >>> try: ... raise ModelLookupError(message="state not found") ... except LookupError as e: ... isinstance(e, ModelLookupError) True
IValidatable
- class pyfcstm.utils.validate.IValidatable[source]
Interface class for implementing validatable objects.
Classes inheriting from
IValidatableshould define their validation rules in the__validators__class variable as a list of validator methods. Each validator should accept the instance as the sole parameter and raiseValidationErrorif the rule fails.- Variables:
__validators__ – List of validator functions to be applied
Example:
>>> class MyModel(IValidatable): ... def _validate_non_empty(self) -> None: ... if not getattr(self, "value", None): ... raise ValidationError("Value is empty.") ... ... __validators__ = [_validate_non_empty] ... >>> model = MyModel() >>> try: ... model.validate() ... except ModelValidationError as err: ... isinstance(err.errors[0], ValidationError) True
- validate() None[source]
Validate the object using all registered validators.
- Raises:
ModelValidationError – If any validation errors occur
Example:
>>> class MyModel(IValidatable): ... def _validate(self) -> None: ... raise ValidationError("Always invalid.") ... ... __validators__ = [_validate] ... >>> try: ... MyModel().validate() ... except ModelValidationError as err: ... len(err.errors) 1