Skip to content

INI format

The ini module provides the generic logic for parsing Deltares ini based files, such as the mdu, structures files, as well as more complex files such as the boundary condition (bc) files.

Note that specific attribute files that employ this ini format often have their own dedicated module (and separate API doc page). These include:

Following below is the documentation for the INI format base classes.

Model

DataBlockINIBasedModel (INIBasedModel) pydantic-model

DataBlockINIBasedModel defines the base model for ini models with datablocks.

INIBasedModel (BaseModel, ABC) pydantic-model

INIBasedModel defines the base model for ini models

INIBasedModel instances can be created from Section instances obtained through parsing ini documents. It further supports adding arbitrary fields to it, which will be written to file. Lastly, no arbitrary types are allowed for the defined fields.

Attributes:

Name Type Description
comments Optional[Comments]

Optional Comments if defined by the user.

Comments (BaseModel, ABC) pydantic-model

Comments defines the comments of an INIBasedModel

INIModel (FileModel) pydantic-model

INI Model representation.

Parser

Parser

Parser defines a generic Parser for Deltares ini files.

The Parser can be configured with a ParserConfig object.

__init__(self, config: ParserConfig) -> None special

Creates a new Parser configured with the provided config

Parameters:

Name Type Description Default
config ParserConfig

The configuration of this Parser

required
Source code in hydrolib/core/io/ini/parser.py
def __init__(self, config: ParserConfig) -> None:
    """Creates a new Parser configured with the provided config

    Args:
        config (ParserConfig): The configuration of this Parser
    """
    self._config = config
    self._document = Document()
    self._current_section: Optional[_IntermediateSection] = None
    self._current_header_block: Optional[_IntermediateCommentBlock] = None

    self._state = self._StateType.NO_SECTION_FOUND
    self._line_index = 0

    # TODO add invalid blocks
    self._feed_line: Dict[
        Parser._StateType, List[Tuple[Callable[[str], bool], Callable[[str], None]]]
    ] = {
        Parser._StateType.NO_SECTION_FOUND: [
            (self._is_comment, self._handle_header_comment),
            (self._is_section_header, self._handle_next_section_header),
        ],
        Parser._StateType.PARSING_PROPERTIES: [
            (self._is_comment, self._handle_section_comment),
            (self._is_section_header, self._handle_next_section_header),
            (self._is_property, self._handle_property),
            (self._is_datarow, self._handle_new_datarow),
        ],
        Parser._StateType.PARSING_DATABLOCK: [
            (self._is_section_header, self._handle_next_section_header),
            (self._is_datarow, self._handle_datarow),
        ],
    }

    self._handle_emptyline: Dict[Parser._StateType, Callable[[], None]] = {
        self._StateType.NO_SECTION_FOUND: self._finish_current_header_block,
        self._StateType.PARSING_PROPERTIES: self._noop,
        self._StateType.PARSING_DATABLOCK: self._noop,
    }

feed_line(self, line: str) -> None

Parse the next line with this Parser.

Parameters:

Name Type Description Default
line str

The line to parse

required
Source code in hydrolib/core/io/ini/parser.py
def feed_line(self, line: str) -> None:
    """Parse the next line with this Parser.

    Args:
        line (str): The line to parse
    """
    if not self._is_empty_line(line):
        for (is_line_type, handle_line_type) in self._feed_line[self._state]:
            if is_line_type(line):
                handle_line_type(line)
                break
        else:
            # handle exception
            pass
    else:
        self._handle_emptyline[self._state]()

    self._increment_line()

finalize(self) -> Document

Finalize parsing and return the constructed Document.

Returns:

Type Description
Document

A Document describing the parsed ini file.

Source code in hydrolib/core/io/ini/parser.py
def finalize(self) -> Document:
    """Finalize parsing and return the constructed Document.

    Returns:
        Document:
            A Document describing the parsed ini file.
    """
    # TODO handle invalid block
    self._finish_current_header_block()
    self._finalise_current_section()
    return self._document

parse(filepath: Path, config: ParserConfig = None) -> Document classmethod

Parses an INI file without a specific model type and returns it as a Document.

Parameters:

Name Type Description Default
filepath Path

File path to the INI-format file.

required
config ParserConfig

Parser configuration to use. Defaults to None.

None

Returns:

Type Description
Document

Representation of the parsed INI-file.

Source code in hydrolib/core/io/ini/parser.py
@classmethod
def parse(cls, filepath: Path, config: ParserConfig = None) -> Document:
    """
    Parses an INI file without a specific model type and returns it as a Document.

    Args:
        filepath (Path): File path to the INI-format file.
        config (ParserConfig, optional): Parser configuration to use. Defaults to None.

    Returns:
        Document: Representation of the parsed INI-file.
    """
    if not config:
        config = ParserConfig()
    parser = cls(config)

    progline = re.compile(
        r"^([^#]*=\s*)([^#]*)(#.*)?"
    )  # matches whole line: "Field = Value Maybe more # optional comment"
    progfloat = re.compile(
        r"([\d.]+)([dD])([+\-]?\d+)"
    )  # matches a float value: 1d9, 1D-3, 1.D+4, etc.

    with filepath.open() as f:
        for line in f:
            # Replace Fortran scientific notation for doubles
            # Match number d/D +/- number (e.g. 1d-05 or 1.23D+01 or 1.d-4)
            match = progline.match(line)
            if match:  # Only process value
                line = (
                    match.group(1)
                    + progfloat.sub(r"\1e\3", match.group(2))
                    + str(match.group(3) or "")
                )
            else:  # Process full line
                line = progfloat.sub(r"\1e\3", line)

            parser.feed_line(line)

    return parser.finalize()

parse_as_dict(filepath: Path, config: ParserConfig = None) -> dict classmethod

Parses an INI file without a specific model type and returns it as a dictionary.

Parameters:

Name Type Description Default
filepath Path

File path to the INI-format file.

required
config ParserConfig

Parser configuration to use. Defaults to None.

None

Returns:

Type Description
dict

Representation of the parsed INI-file.

Source code in hydrolib/core/io/ini/parser.py
@classmethod
def parse_as_dict(cls, filepath: Path, config: ParserConfig = None) -> dict:
    """
    Parses an INI file without a specific model type and returns it as a dictionary.

    Args:
        filepath (Path): File path to the INI-format file.
        config (ParserConfig, optional): Parser configuration to use. Defaults to None.

    Returns:
        dict: Representation of the parsed INI-file.
    """
    return cls.parse(filepath, config).flatten()

ParserConfig (BaseModel) pydantic-model

ParserConfig defines the configuration options of the Parser

Note that we cannot set both allow_only_keywords and parse_datablocks to True because we cannot distinguish between datablocks and key only properties. As such this will lead to a validation error.

Attributes:

Name Type Description
allow_only_keywords bool

Whether to allow properties with only keys (no '=' or value). Defaults to True.

parse_datablocks bool

Whether to allow parsing of datablocks at the bottom of sections. Defaults to False.

parse_comments bool

Whether we allow parsing of comments defined with the comment_delimeter. Defaults to True.

comment_delimiter str

The character or sequence of character used to define a comment. Defaults to '#'.

Serializer

MaxLengths (BaseModel) pydantic-model

MaxLengths defines the maxmimum lengths of the parts of a section

Attributes:

Name Type Description
key int

The maximum length of all the keys of the properties within a section. If no properties are present it should be 0.

value int

The maximum length of all the non None values of the properties within a section. If no properties are present, or all values are None, it should be 0.

datablock Optional[Sequence[int]]

The maximum length of the values of each column of the Datablock. If no datablock is present it defaults to None.

from_section(section: Section) -> MaxLengths classmethod

Generate a MaxLengths instance from the given Section

Parameters:

Name Type Description Default
section Section

The section of which the MaxLengths are calculated

required

Returns:

Type Description
MaxLengths

The MaxLengths corresponding with the provided section

Source code in hydrolib/core/io/ini/serializer.py
@classmethod
def from_section(cls, section: Section) -> "MaxLengths":
    """Generate a MaxLengths instance from the given Section

    Args:
        section (Section): The section of which the MaxLengths are calculated

    Returns:
        MaxLengths: The MaxLengths corresponding with the provided section
    """
    properties = list(p for p in section.content if isinstance(p, Property))

    keys = (prop.key for prop in properties)
    values = (prop.value for prop in properties if prop.value is not None)

    max_key_length = max((len(k) for k in keys), default=0)
    max_value_length = max((len(v) for v in values), default=0)
    max_datablock_lengths = MaxLengths._of_datablock(section.datablock)

    return cls(
        key=max_key_length,
        value=max_value_length,
        datablock=max_datablock_lengths,
    )

SectionSerializer

SectionSerializer provides the serialize method to serialize a Section

The entrypoint of this method is the serialize method, which will construct an actual instance and serializes the Section with it.

__init__(self, config: SerializerConfig, max_length: MaxLengths) special

Create a new SectionSerializer

Parameters:

Name Type Description Default
config SerializerConfig

The config describing the serialization options

required
max_length MaxLengths

The max lengths of the section being serialized

required
Source code in hydrolib/core/io/ini/serializer.py
def __init__(self, config: SerializerConfig, max_length: MaxLengths):
    """Create a new SectionSerializer

    Args:
        config (SerializerConfig): The config describing the serialization options
        max_length (MaxLengths): The max lengths of the section being serialized
    """
    self._config = config
    self._max_length = max_length

config: SerializerConfig property readonly

The SerializerConfig used while serializing the section.

max_length: MaxLengths property readonly

The MaxLengths of the Section being serialized by this SectionSerializer.

serialize(section: Section, config: SerializerConfig) -> Iterable[str] classmethod

Serialize the provided section with the given config

Parameters:

Name Type Description Default
section Section

The section to serialize

required
config SerializerConfig

The config describing the serialization options

required

Returns:

Type Description
Lines

The iterable lines of the serialized section

Source code in hydrolib/core/io/ini/serializer.py
@classmethod
def serialize(cls, section: Section, config: SerializerConfig) -> Lines:
    """Serialize the provided section with the given config

    Args:
        section (Section): The section to serialize
        config (SerializerConfig): The config describing the serialization options

    Returns:
        Lines: The iterable lines of the serialized section
    """
    serializer = cls(config, MaxLengths.from_section(section))
    return serializer._serialize_section(section)

Serializer

Serializer serializes Document to its corresponding lines.

__init__(self, config: SerializerConfig) special

Creates a new Serializer with the provided configuration.

Parameters:

Name Type Description Default
config SerializerConfig

The configuration of this Serializer.

required
Source code in hydrolib/core/io/ini/serializer.py
def __init__(self, config: SerializerConfig):
    """Creates a new Serializer with the provided configuration.

    Args:
        config (SerializerConfig): The configuration of this Serializer.
    """
    self._config = config

serialize(self, document: Document) -> Iterable[str]

Serialize the provided document into an iterable of lines.

Parameters:

Name Type Description Default
document Document

The Document to serialize.

required

Returns:

Type Description
Lines

An iterable returning each line of the serialized Document.

Source code in hydrolib/core/io/ini/serializer.py
def serialize(self, document: Document) -> Lines:
    """Serialize the provided document into an iterable of lines.

    Args:
        document (Document): The Document to serialize.

    Returns:
        Lines: An iterable returning each line of the serialized Document.
    """
    header_iterable = self._serialize_document_header(document.header_comment)

    serialize_section = lambda s: SectionSerializer.serialize(s, self._config)
    sections = (serialize_section(section) for section in document.sections)
    sections_with_spacing = Serializer._interweave(sections, [""])
    sections_iterable = chain.from_iterable(sections_with_spacing)

    return chain(header_iterable, sections_iterable)

SerializerConfig (BaseModel) pydantic-model

SerializerConfig defines the configuration options of the Serializer

Attributes:

Name Type Description
section_indent int

The number of spaces with which whole sections should be indented. Defaults to 0.

property_indent int

The number of spaces with which properties should be indented relative to the section header (i.e. the full indent equals the section_indent plus property_indent). Defaults to 4.

datablock_indent int

The number of spaces with which datablock rows are indented relative to the section header (i.e. the full indent equals the section_indent plus datablock_indent). Defaults to 8.

datablock_spacing int

The number of spaces between datablock columns. Note that there might be additional offset to ensure . is lined out. Defaults to 4.

comment_delimiter str

The character used to delimit comments. Defaults to '#'.

total_datablock_indent: int property readonly

The combined datablock indentation, i.e. section_indent + datablock_indent

total_property_indent: int property readonly

The combined property indentation, i.e. section_indent + property_indent

write_ini(path: Path, document: Document, config: Optional[hydrolib.core.io.ini.serializer.SerializerConfig] = None) -> None

Write the provided document to the specified path

If the provided path already exists, it will be overwritten. If the parent folder do not exist, they will be created.

Parameters:

Name Type Description Default
path Path

The path to which the document should be written.

required
document Document

The document to serialize to the specified path.

required
config Optional[SerializerConfig]

An optional configuration of the serializer. If none provided, it will default to the standard SerializerConfig. Defaults to None.

None
Source code in hydrolib/core/io/ini/serializer.py
def write_ini(
    path: Path,
    document: Document,
    config: Optional[SerializerConfig] = None,
) -> None:
    """Write the provided document to the specified path

    If the provided path already exists, it will be overwritten. If the parent folder
    do not exist, they will be created.

    Args:
        path (Path): The path to which the document should be written.
        document (Document): The document to serialize to the specified path.
        config (Optional[SerializerConfig], optional):
            An optional configuration of the serializer. If none provided, it will
            default to the standard SerializerConfig. Defaults to None.
    """
    if config is None:
        config = SerializerConfig()

    serializer = Serializer(config)

    path.parent.mkdir(parents=True, exist_ok=True)

    with path.open("wb") as f:

        for line in serializer.serialize(document):
            f.write((line + "\n").encode("utf8"))
Back to top