diff --git a/src/hsd/dict.py b/src/hsd/dict.py index 847aab0..c2521fe 100644 --- a/src/hsd/dict.py +++ b/src/hsd/dict.py @@ -12,6 +12,7 @@ from hsd.common import HSD_ATTRIB_NAME, np, ATTRIB_SUFFIX, HSD_ATTRIB_SUFFIX, HsdError,\ QUOTING_CHARS, SPECIAL_CHARS from hsd.eventhandler import HsdEventHandler, HsdEventPrinter +from hsd.interrupts import Interrupt _ItemType = Union[float, complex, int, bool, str] @@ -67,7 +68,7 @@ class HsdDictBuilder(HsdEventHandler): Args: flatten_data: Whether multiline data in the HSD input should be - flattened into a single list. Othewise a list of lists is created, with one list for + flattened into a single list. Otherwise a list of lists is created, with one list for every line (default). lower_tag_names: Whether tag names should be all converted to lower case (to ease case insensitive processing). Default: False. If set and include_hsd_attribs is also set, @@ -161,6 +162,13 @@ def add_text(self, text): self._data = self._text_to_data(text) + def add_interrupt(self, interrupt): + if self._curblock or self._data is not None: + msg = "Data appeared in an invalid context" + raise HsdError(msg) + self._data = interrupt + + def _text_to_data(self, txt: str) -> _DataType: data = [] for line in txt.split("\n"): @@ -242,6 +250,11 @@ def walk(self, dictobj): self.walk(item) self._eventhandler.close_tag(key) + elif isinstance(value, Interrupt): + self._eventhandler.open_tag(key, attrib, hsdattrib) + self._eventhandler.add_interrupt(value) + self._eventhandler.close_tag(key) + else: self._eventhandler.open_tag(key, attrib, hsdattrib) self._eventhandler.add_text(_to_text(value)) diff --git a/src/hsd/eventhandler.py b/src/hsd/eventhandler.py index ab82886..71c41ac 100644 --- a/src/hsd/eventhandler.py +++ b/src/hsd/eventhandler.py @@ -10,6 +10,7 @@ from abc import ABC, abstractmethod from typing import Optional +from hsd.interrupts import Interrupt class HsdEventHandler(ABC): @@ -43,6 +44,13 @@ def add_text(self, text: str): text: Text in the current tag. """ + @abstractmethod + def add_interrupt(self, interrupt: Interrupt): + """Adds interrupts to the current tag. + + Args: + interrupt: Instance of the Interrupt class or its children. + """ class HsdEventPrinter(HsdEventHandler): @@ -75,3 +83,8 @@ def close_tag(self, tagname: str): def add_text(self, text: str): indentstr = self._indentlevel * self._indentstr print(f"{indentstr}Received text: {text}") + + def add_interrupt(self, interrupt: Interrupt): + indentstr = self._indentlevel * self._indentstr + print(f"{indentstr}Received interrupt: type '{type(interrupt)}' to " + f"file '{interrupt.file}'") diff --git a/src/hsd/formatter.py b/src/hsd/formatter.py index 65d4c18..8b35277 100644 --- a/src/hsd/formatter.py +++ b/src/hsd/formatter.py @@ -8,15 +8,16 @@ """ from typing import List, TextIO, Union -from hsd.common import HSD_ATTRIB_EQUAL, HSD_ATTRIB_NAME +from hsd.common import HSD_ATTRIB_EQUAL, HSD_ATTRIB_NAME, HsdError from hsd.eventhandler import HsdEventHandler +from hsd.interrupts import Interrupt, IncludeHsd, IncludeText _INDENT_STR = " " class HsdFormatter(HsdEventHandler): - """Implements an even driven HSD formatter. + """Implements an event driven HSD formatter. Args: fobj: File like object to write the formatted output to. @@ -106,10 +107,36 @@ def add_text(self, text: str): self._followed_by_equal[-1] = True else: self._indent_level += 1 - indentstr = self._indent_level * _INDENT_STR + indentstr = self._indent_level * _INDENT_STR self._fobj.write(f" {{\n{indentstr}") text = text.replace("\n", "\n" + indentstr) self._fobj.write(text) self._fobj.write("\n") self._nr_children[-1] += 1 + + + def add_interrupt(self, interrupt: Interrupt): + + if isinstance(interrupt, IncludeHsd): + operator = "<<+" + elif isinstance(interrupt, IncludeText): + operator = "<<<" + else: + msg = ("The 'HsdFormatter' does not support Interrupts of " + f"type '{type(interrupt)}' !") + raise HsdError(msg) + + equal = self._followed_by_equal[-1] + if equal: + self._fobj.write(" = ") + self._followed_by_equal[-1] = True + else: + self._indent_level += 1 + indentstr = self._indent_level * _INDENT_STR + self._fobj.write(f" {{\n{indentstr}") + + text = operator + ' "' + interrupt.file + '"' + self._fobj.write(text) + self._fobj.write("\n") + self._nr_children[-1] += 1 diff --git a/src/hsd/interrupts.py b/src/hsd/interrupts.py new file mode 100644 index 0000000..7a64053 --- /dev/null +++ b/src/hsd/interrupts.py @@ -0,0 +1,27 @@ +#--------------------------------------------------------------------------------------------------# +# hsd-python: package for manipulating HSD-formatted data in Python # +# Copyright (C) 2011 - 2023 DFTB+ developers group # +# Licensed under the BSD 2-clause license. # +#--------------------------------------------------------------------------------------------------# +# +""" +Contains hsd interrupts +""" + +from hsd.common import unquote + + +class Interrupt: + """General class for interrupts""" + + def __init__(self, file): + self.file = unquote(file.strip()) + + +class IncludeText(Interrupt): + """class for dealing with text interrupts""" + pass + +class IncludeHsd(Interrupt): + """class for dealing with hsd interrupts""" + pass diff --git a/src/hsd/io.py b/src/hsd/io.py index fe751bc..d64c668 100644 --- a/src/hsd/io.py +++ b/src/hsd/io.py @@ -18,7 +18,8 @@ def load(hsdfile: Union[TextIO, str], lower_tag_names: bool = False, - include_hsd_attribs: bool = False, flatten_data: bool = False) -> dict: + include_hsd_attribs: bool = False, flatten_data: bool = False, + include_file: bool = True) -> dict: """Loads a file with HSD-formatted data into a Python dictionary Args: @@ -36,6 +37,7 @@ def load(hsdfile: Union[TextIO, str], lower_tag_names: bool = False, flatten_data: Whether multiline data in the HSD input should be flattened into a single list. Othewise a list of lists is created, with one list for every line (default). + include_file: Whether files via "<<<"/"<<+" should be included or not Returns: Dictionary representing the HSD data. @@ -45,7 +47,7 @@ def load(hsdfile: Union[TextIO, str], lower_tag_names: bool = False, """ dictbuilder = HsdDictBuilder(lower_tag_names=lower_tag_names, flatten_data=flatten_data, include_hsd_attribs=include_hsd_attribs) - parser = HsdParser(eventhandler=dictbuilder) + parser = HsdParser(eventhandler=dictbuilder, include_file=include_file) if isinstance(hsdfile, str): with open(hsdfile, "r") as hsddescr: parser.parse(hsddescr) @@ -56,8 +58,8 @@ def load(hsdfile: Union[TextIO, str], lower_tag_names: bool = False, def load_string( hsdstr: str, lower_tag_names: bool = False, - include_hsd_attribs: bool = False, flatten_data: bool = False - ) -> dict: + include_hsd_attribs: bool = False, flatten_data: bool = False, + include_file: bool = True) -> dict: """Loads a string with HSD-formatted data into a Python dictionary. Args: @@ -75,6 +77,7 @@ def load_string( flatten_data: Whether multiline data in the HSD input should be flattened into a single list. Othewise a list of lists is created, with one list for every line (default). + include_file: Whether files via "<<<"/"<<+" should be included or not Returns: Dictionary representing the HSD data. @@ -130,7 +133,8 @@ def load_string( """ fobj = io.StringIO(hsdstr) - return load(fobj, lower_tag_names, include_hsd_attribs, flatten_data) + return load(fobj, lower_tag_names, include_hsd_attribs, flatten_data, + include_file) def dump(data: dict, hsdfile: Union[TextIO, str], diff --git a/src/hsd/parser.py b/src/hsd/parser.py index 4ab36d8..4ce2dec 100644 --- a/src/hsd/parser.py +++ b/src/hsd/parser.py @@ -10,7 +10,7 @@ from typing import Optional, TextIO, Union from hsd import common from hsd.eventhandler import HsdEventHandler, HsdEventPrinter - +from hsd.interrupts import IncludeHsd, IncludeText SYNTAX_ERROR = 1 UNCLOSED_TAG_ERROR = 2 @@ -50,11 +50,13 @@ class HsdParser: {'Temperature': 100, 'Temperature.attrib': 'Kelvin'}}}}} """ - def __init__(self, eventhandler: Optional[HsdEventHandler] = None): + def __init__(self, eventhandler: Optional[HsdEventHandler] = None, + include_file: bool = True): """Initializes the parser. Args: eventhandler: Instance of the HsdEventHandler class or its children. + include_file: Whether files via "<<<"/"<<+" should be included or not """ if eventhandler is None: self._eventhandler = HsdEventPrinter() @@ -75,6 +77,7 @@ def __init__(self, eventhandler: Optional[HsdEventHandler] = None): self._has_child = True # Whether current node has a child already self._has_text = False # whether current node contains text already self._oldbefore = "" # buffer for tagname + self._include_file = include_file # Whether files via "<<<"/"<<+" should be included or not def parse(self, fobj: Union[TextIO, str]): @@ -216,10 +219,19 @@ def _parse(self, line): if txtinc: self._text("".join(self._buffer) + before) self._buffer = [] - self._eventhandler.add_text(self._include_txt(after[2:])) + if self._include_file: + text = self._include_txt(after[2:]) + self._eventhandler.add_text(text) + else: + interrupt = IncludeText(after[2:]) + self._eventhandler.add_interrupt(interrupt) break if hsdinc: - self._include_hsd(after[2:]) + if self._include_file: + self._include_hsd(after[2:]) + else: + interrupt = IncludeHsd(after[2:]) + self._eventhandler.add_interrupt(interrupt) break self._buffer.append(before + sign) diff --git a/test/test_parser.py b/test/test_parser.py index be3185b..1fe6fc5 100644 --- a/test/test_parser.py +++ b/test/test_parser.py @@ -109,6 +109,8 @@ def close_tag(self, tagname): def add_text(self, text): self.events.append((_ADD_TEXT_EVENT, text)) + def add_interrupt(self, interrupt): + pass @pytest.mark.parametrize( "hsd_input,expected_events",