Writing Parsers¶
A section parser reads one complete MSH section and stores its data in a Mesh
or a related compatibility-model object.
Parser contract¶
A parser implements AbstractParser and provides two static methods:
from typing import TextIO
from gmshparser.abstract_parser import AbstractParser
from gmshparser.mesh import Mesh
class ExampleParser(AbstractParser):
@staticmethod
def get_section_name() -> str:
return "$Example"
@staticmethod
def parse(mesh: Mesh, io: TextIO) -> None:
...
MainParser normally consumes the opening section marker before parse() is
called. A section parser must then:
- read and validate the complete payload
- verify declared record counts
- consume and validate its
$End...marker - stop immediately after the end marker
Some existing parsers also accept a stream positioned at their opening marker so they can be tested directly. Preserve that behavior when modifying such a parser.
Required reads and end markers¶
Use the shared parsing helpers instead of calling readline() for mandatory
records:
from gmshparser.parsing import expect_end_marker, read_required_line
line = read_required_line(io, "an $Example record")
...
expect_end_marker(io, "$EndExample")
read_required_line() raises UnexpectedEndOfFileError when the stream ends
inside a declared section. expect_end_marker() reports both a missing marker
and an unexpected line with the current source context.
Example parser¶
from gmshparser.errors import InvalidSectionError
from gmshparser.parsing import expect_end_marker, read_required_line
class ExampleParser(AbstractParser):
@staticmethod
def get_section_name() -> str:
return "$Example"
@staticmethod
def parse(mesh: Mesh, io: TextIO) -> None:
count_line = read_required_line(io, "$Example count")
try:
count = int(count_line)
except ValueError as error:
raise InvalidSectionError(
"$Example count must be an integer"
) from error
if count < 0:
raise InvalidSectionError("$Example count cannot be negative")
for _ in range(count):
record = read_required_line(io, "an $Example record")
...
expect_end_marker(io, "$EndExample")
Do not print parser failures. MainParser attaches the filename, current line
number, section, and source line to ParseError subclasses. Applications decide
how exceptions are logged or displayed.
Choose a specific error type¶
Use the most specific public exception that describes the malformed input:
InvalidSectionErrorfor headers, counts, generic records, and end markersInvalidNodeErrorfor node blocks and node recordsInvalidElementErrorfor element blocks and element recordsInvalidElementConnectivityErrorfor connectivity width mismatchesUnknownElementTypeErrorwhen topology metadata is required but unavailableUnsupportedVersionErrorandUnsupportedBinaryFormatErrorfor format limits
All parser errors inherit from both ParseError and ValueError.
Register the parser¶
The parser registries are version-specific:
Register a parser only for formats where its section layout applies. A parser shared by MSH 2.x and 4.x is added to both lists:
DEFAULT_PARSERS_V2 = [
MeshFormatParser,
PhysicalNamesParser,
NodesParserV2,
ElementsParserV2,
]
DEFAULT_PARSERS_V4 = [
MeshFormatParser,
PhysicalNamesParser,
EntitiesParser,
NodesParser,
ElementsParser,
]
Do not add a single DEFAULT_PARSERS list: the implementation does not use one.
For specialized callers, MainParser(parsers=[...]) accepts an explicit parser
list. That list replaces automatic version-specific selection, so it must
contain every section parser needed by the input.
Version-specific layouts¶
When the same section has different layouts, prefer separate parser classes:
class ExampleParserV2(AbstractParser):
@staticmethod
def get_section_name() -> str:
return "$Example"
@staticmethod
def parse(mesh: Mesh, io: TextIO) -> None:
...
class ExampleParserV4(AbstractParser):
@staticmethod
def get_section_name() -> str:
return "$Example"
@staticmethod
def parse(mesh: Mesh, io: TextIO) -> None:
...
Register each class in the matching parser list. This follows the existing
NodesParserV2/NodesParser and ElementsParserV2/ElementsParser split.
Testing checklist¶
- add the smallest complete valid section fixture, including its end marker
- verify the values exposed through the public data model
- cover each applicable MSH version family
- test malformed headers and numeric fields
- test truncated payloads and missing end markers
- verify the specific exception class and source-context attributes
- ensure an unrelated optional section remains harmless
- run Ruff, pytest, and the documentation build
uv run ruff format --check gmshparser tests examples
uv run ruff check gmshparser tests examples
uv run pytest
uv sync --group docs
uv run mkdocs build
Documentation checklist¶
Update:
- Supported Formats when support changes
- Error Handling for new public failures
- Architecture when routing or data-model structure changes
- the API reference for new public model methods, parsers, or exceptions
- the changelog for user-visible releases