# Copyright 2024-2026 Jae Hoon Seo
# Marine Structural Mechanics and Integrity Lab (SMI Lab), Inha University
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Unified parse-error base class for every ``pybmodes.io.*`` reader.
Every input-format parser ``pybmodes`` ships (BModes ``.bmi`` /
section-properties ``.dat`` / OpenFAST ElastoDyn / SubDyn / HydroDyn /
MoorDyn ``.dat`` / WAMIT ``.1`` ``.hst`` / BModes ``.out`` reference
output / WISDEM / WindIO ontology ``.yaml``) previously raised either a
bare :class:`ValueError` with a format-specific message or, in the
case of :mod:`pybmodes.io.out_parser`, the legacy
:class:`BModeOutParseError`. That made downstream
``try / except ValueError`` callers correct but indiscriminate, and
the per-error file / line context was unstructured prose.
:class:`ParseError` is the unified base. It inherits :class:`ValueError`
so existing ``except ValueError`` callers continue to catch every
parse error untouched — the inheritance addition is non-breaking.
Each parser will (incrementally) start raising :class:`ParseError`
subclasses with structured ``file`` / ``line`` / ``column`` /
``context`` fields. Callers that want file / line context can switch
to ``except ParseError`` and read the typed fields; callers that
only need "the file is broken" stay on ``except ValueError``.
Examples
--------
Catch a parse error and pull out the file / line / column context::
from pybmodes.io.errors import ParseError
from pybmodes.io.bmi import read_bmi
try:
bmi = read_bmi("malformed.bmi")
except ParseError as err:
print(f"parse failed in {err.file}:{err.line}: {err}")
print(err.format_diagnostic()) # one-line diagnostic string
Keep an older ``except ValueError`` pattern working — :class:`ParseError`
is a subclass so the catch still triggers::
try:
bmi = read_bmi("malformed.bmi")
except ValueError as err:
print(f"oops: {err}") # also catches every ParseError
"""
from __future__ import annotations
from dataclasses import dataclass
[docs]
@dataclass(eq=False)
class ParseError(ValueError):
"""Base class for every ``pybmodes.io.*`` parser exception.
Inherits :class:`ValueError` so ``except ValueError`` catches it
unchanged. Inheriting parsers add structured context fields below.
Attributes
----------
message : str
The human-readable description of the failure. Same as
``str(err)``.
file : pathlib.Path-compatible str | None
The source file the parser was reading from, when known.
``None`` for in-memory parses (e.g. yaml from a string).
line : int | None
1-based line number where the error was detected, when
known. The parser is encouraged to populate this even for
token-level errors — fall back to the start of the
containing block.
column : int | None
1-based column number where the error was detected, when
known. Optional; many parsers don't track column.
context : str | None
A short snippet — typically the offending line or token —
that gives the reader visual confirmation of *what* the
parser tripped on. Up to one or two lines; the parser
should truncate large blobs.
"""
message: str
file: str | None = None
line: int | None = None
column: int | None = None
context: str | None = None
def __post_init__(self) -> None:
# Cooperate with ValueError's positional-argument convention:
# ``str(err)`` should return ``message`` directly so existing
# f-string error formatting keeps working.
super().__init__(self.message)
def __str__(self) -> str:
return self.message
[docs]
@dataclass(eq=False)
class BMIParseError(ParseError):
"""Raised by :func:`pybmodes.io.bmi.read_bmi` and the companion
section-properties parser when the input deck is malformed.
The parser is line-oriented; ``line`` is the 1-based row in the
source file. ``context`` is the offending line (truncated).
"""
[docs]
@dataclass(eq=False)
class ElastoDynParseError(ParseError):
"""Raised by the ElastoDyn deck reader
(:mod:`pybmodes.io.elastodyn_reader` / the private
``pybmodes.io._elastodyn`` sub-package) on malformed input.
"""
[docs]
@dataclass(eq=False)
class SubDynParseError(ParseError):
"""Raised by :mod:`pybmodes.io.subdyn_reader` when the SubDyn
joints / members / reaction-joint block can't be parsed."""
[docs]
@dataclass(eq=False)
class WAMITParseError(ParseError):
"""Raised by :class:`pybmodes.io.wamit_reader.HydroDynReader` and
the underlying ``.1`` / ``.hst`` readers on malformed WAMIT
output (bad re-dimensionalisation, missing files behind
``PotFile``, asymmetric matrices that can't be mirrored)."""
[docs]
@dataclass(eq=False)
class MoorDynParseError(ParseError):
"""Raised by :meth:`pybmodes.mooring.MooringSystem.from_moordyn`
when a MoorDyn ``.dat`` carries an unrecognised layout or a
point-ID column ordering the parser can't auto-detect."""
[docs]
@dataclass(eq=False)
class WindIOParseError(ParseError):
"""Raised by the WindIO ontology readers
(:mod:`pybmodes.io.windio` / :mod:`pybmodes.io.windio_blade` /
:mod:`pybmodes.io.windio_floating`) on schema-drift or
malformed published blocks.
Includes both ``components.<component>`` lookup failures and the
"elastic_properties block is present but unparseable" path that
``elastic="auto"`` / ``"file"`` distinguish."""
__all__ = [
"BMIParseError",
"ElastoDynParseError",
"MoorDynParseError",
"ParseError",
"SubDynParseError",
"WAMITParseError",
"WindIOParseError",
]