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:

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_column are 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.model and downstream consumers such as IDE integrations, LLM agent loops, and evaluation scripts. Consumers should dispatch on code only and treat message as human-facing text that may change between releases.

The full set of valid code values together with their refs payload schema lives in pyfcstm.diagnostics.codes (loaded from pyfcstm/diagnostics/codes.yaml); this dataclass intentionally does not validate code at construction time so that experimental codes can be emitted in tests.

severity is enforced at construction time to be one of 'error' / 'warning' — a typo such as 'Error' would otherwise cause is_error() to silently return False and skew downstream dispatch.

The dataclass is frozen so the public contract surface (code / severity / message / span) cannot be mutated after construction. refs remains a mutable dict because 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.yaml for 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 legacy ValidationError entries and structured ModelDiagnostic entries share the same [code] message prefix style in aggregated error output.

Returns:

A one-line [severity/code] message representation.

Return type:

str

is_error() bool[source]

Return True if this diagnostic has error severity.

Returns:

True for severity == 'error', False otherwise.

Return type:

bool

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 ValidationError instances and formats them into a readable error message.

It multi-inherits from SyntaxError for backwards compatibility: callers that previously caught SyntaxError raised from pyfcstm.model continue to work after later PRs in the Layer 1 refactor switch raise sites to this class.

In addition to the legacy errors list, instances may also carry a diagnostics list of structured ModelDiagnostic objects when raised from the new collect-mode pipeline. Existing callers that do not read diagnostics continue 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 from errors / diagnostics.

Variables:

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-inherits SyntaxError) and from ValueError, so:

  • except ValueError: continues to catch this — preserving the pre-PR-3 behavior at every call site that already handles ValueError raised from State.resolve_event / StateMachine.resolve_event.

  • except SyntaxError: continues to catch too (inherited via ModelValidationError), keeping the PR-2 multi-inheritance contract consistent.

  • The new except ModelValueError: (or ModelValidationError) 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 in pyfcstm/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 ModelValidationError and LookupError, so:

  • except LookupError: continues to catch this — preserving the pre-PR-3 behavior at every call site that already handles LookupError raised from State.resolve_event / StateMachine.resolve_event.

  • except SyntaxError: and except ModelValidationError: both catch as well (inherited path).

Structured diagnostics emitted via this exception carry code='E_EVENT_NOT_FOUND' per the contract in pyfcstm/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 IValidatable should 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 raise ValidationError if 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