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:
- MDU file (
hydrolib.core.io.mdu
) - External forcing file (
hydrolib.core.io.ext
) - Initial fields and parameter file (
hydrolib.core.io.inifield
) - Structure file (
hydrolib.core.io.structure
) - 1D roughness (
hydrolib.core.io.friction
) - Cross section files (
hydrolib.core.io.crosssection
) - Storage node file (
hydrolib.core.io.storagenode
)
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. |
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 '#'. |
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"))