diff --git a/LICENSE b/LICENSE index c1beed0..36d474a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2020 Bálint Aradi, Universität Bremen +Copyright (c) 2011-2020 DFTB+ developers group All rights reserved. diff --git a/README.rst b/README.rst index eb7089d..cafbdf4 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,7 @@ HSD — Human-friendly Structured Data ************************************ -This Python package contains utilities to write (and soon also to read) files in +This Python package contains utilities to read and write files in the Human-friendly Structured Data (HSD) format. It is licensed under the *BSD 2-clause license*. diff --git a/src/hsd/__init__.py b/src/hsd/__init__.py new file mode 100644 index 0000000..87c7544 --- /dev/null +++ b/src/hsd/__init__.py @@ -0,0 +1,13 @@ +#------------------------------------------------------------------------------# +# hsd: package for manipulating HSD-formatted data # +# Copyright (C) 2011 - 2020 DFTB+ developers group # +# # +# See the LICENSE file for terms of usage and distribution. # +#------------------------------------------------------------------------------# +# +""" +Central module for the hsd package +""" +from .dump import dump, dumps +from .parser import HsdParser +from .dictbuilder import HsdDictBuilder diff --git a/src/hsd/common.py b/src/hsd/common.py new file mode 100644 index 0000000..6669d13 --- /dev/null +++ b/src/hsd/common.py @@ -0,0 +1,66 @@ +#------------------------------------------------------------------------------# +# hsd: package for manipulating HSD-formatted data # +# Copyright (C) 2011 - 2020 DFTB+ developers group # +# # +# See the LICENSE file for terms of usage and distribution. # +#------------------------------------------------------------------------------# +# +""" +Implements common functionalities for the HSD package +""" + + +class HsdException(Exception): + """Base class for exceptions in the HSD package.""" + pass + + +class HsdQueryError(HsdException): + """Base class for errors detected by the HsdQuery object. + + + Attributes: + filename: Name of the file where error occured (or empty string). + line: Line where the error occurred (or -1). + tag: Name of the tag with the error (or empty string). + """ + + def __init__(self, msg="", node=None): + """Initializes the exception. + + Args: + msg: Error message + node: HSD element where error occured (optional). + """ + super().__init__(msg) + if node is not None: + self.tag = node.gethsd(HSDATTR_TAG, node.tag) + self.file = node.gethsd(HSDATTR_FILE, -1) + self.line = node.gethsd(HSDATTR_LINE, None) + else: + self.tag = "" + self.file = -1 + self.line = None + + +class HsdParserError(HsdException): + """Base class for parser related errors.""" + pass + + +def unquote(txt): + """Giving string without quotes if enclosed in those.""" + if len(txt) >= 2 and (txt[0] in "\"'") and txt[-1] == txt[0]: + return txt[1:-1] + return txt + + +# Name for default attribute (when attribute name is not specified) +DEFAULT_ATTRIBUTE = "attribute" + + +HSDATTR_PROC = "processed" +HSDATTR_EQUAL = "equal" +HSDATTR_FILE = "file" +HSDATTR_LINE = "line" +HSDATTR_TAG = "tag" diff --git a/src/hsd/dictbuilder.py b/src/hsd/dictbuilder.py new file mode 100644 index 0000000..e4804ef --- /dev/null +++ b/src/hsd/dictbuilder.py @@ -0,0 +1,101 @@ +#------------------------------------------------------------------------------# +# hsd: package for manipulating HSD-formatted data # +# Copyright (C) 2011 - 2020 DFTB+ developers group # +# # +# See the LICENSE file for terms of usage and distribution. # +#------------------------------------------------------------------------------# +# +""" +Contains an event-driven builder for dictionary based (JSON-like) structure +""" +import re +from .parser import HsdEventHandler + +__all__ = ['HsdDictBuilder'] + + +_TOKEN_PATTERN = re.compile(r""" +(?:\s*(?:^|(?<=\s))(?P[+-]?[0-9]+)(?:\s*$|\s+)) +| +(?:\s*(?:^|(?<=\s)) +(?P[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?)(?:$|(?=\s+))) +| +(?:\s*(?:^|(?<=\s))(?P[Yy][Ee][Ss]|[Nn][Oo])(?:$|(?=\s+))) +| +(?:(?P(?P['"]).*?(?P=quote)) | (?P.+?)(?:$|\s+)) +""", re.VERBOSE | re.MULTILINE) + + +class HsdDictBuilder(HsdEventHandler): + """Deserializes HSD into nested dictionaries""" + + def __init__(self, flatten_data=False): + HsdEventHandler.__init__(self) + self._hsddict = {} + self._curblock = self._hsddict + self._parentblocks = [] + self._data = None + self._flatten_data = flatten_data + + + def open_tag(self, tagname, options, hsdoptions): + for attrname, attrvalue in options.items(): + self._curblock[tagname + '.' + attrname] = attrvalue + self._parentblocks.append(self._curblock) + self._curblock = {} + + + def close_tag(self, tagname): + parentblock = self._parentblocks.pop(-1) + prevcontent = parentblock.get(tagname) + if prevcontent is not None and not isinstance(prevcontent, list): + prevcontent = [prevcontent] + parentblock[tagname] = prevcontent + if self._data is None: + content = self._curblock + else: + content = self._data + self._data = None + if prevcontent is None: + parentblock[tagname] = content + else: + prevcontent.append(content) + self._curblock = parentblock + + + def add_text(self, text): + self._data = self._text_to_data(text) + + + @property + def hsddict(self): + """Returns the dictionary which has been built""" + return self._hsddict + + + def _text_to_data(self, txt): + data = [] + for line in txt.split("\n"): + if self._flatten_data: + linedata = data + else: + linedata = [] + for match in _TOKEN_PATTERN.finditer(line.strip()): + if match.group("int"): + linedata.append(int(match.group("int"))) + elif match.group("float"): + linedata.append(float(match.group("float"))) + elif match.group("logical"): + lowlog = match.group("logical").lower() + linedata.append(lowlog == "yes") + elif match.group("str"): + linedata.append(match.group("str")) + elif match.group("qstr"): + linedata.append(match.group("qstr")) + if not self._flatten_data: + data.append(linedata) + if len(data) == 1: + if isinstance(data[0], list) and len(data[0]) == 1: + return data[0][0] + return data[0] + return data diff --git a/src/hsd.py b/src/hsd/dump.py similarity index 66% rename from src/hsd.py rename to src/hsd/dump.py index d4477c5..916a6d9 100644 --- a/src/hsd.py +++ b/src/hsd/dump.py @@ -1,16 +1,16 @@ -#!/usr/bin/env python3 #------------------------------------------------------------------------------# # hsd: package for manipulating HSD-formatted data # -# Copyright (C) 2020 Bálint Aradi, Universität Bremen # +# Copyright (C) 2011 - 2020 DFTB+ developers group # # # # See the LICENSE file for terms of usage and distribution. # #------------------------------------------------------------------------------# # """ -Provides functionality to convert Python structures to HSD +Provides functionality to dump Python structures to HSD """ import io import numpy as np +from .common import DEFAULT_ATTRIBUTE __all__ = ['dump', 'dumps'] @@ -20,8 +20,11 @@ # String quoting delimiters (must be at least two) _QUOTING_CHARS = "\"'" -# Suffix for appending attributes -_ATTRIBUTE_SUFFIX = ".attribute" +# Special characters +_SPECIAL_CHARS = "{}[]= " + + +_ATTRIBUTE_SUFFIX = "." + DEFAULT_ATTRIBUTE def dump(obj, fobj): @@ -130,63 +133,14 @@ def _item_to_hsd(item): def _str_to_hsd(string): - is_present = [qc in string for qc in _QUOTING_CHARS] - if sum(is_present) > 1: + present = [qc in string for qc in _QUOTING_CHARS] + nquotetypes = sum(present) + delimiter = "" + if not nquotetypes and True in [sc in string for sc in _SPECIAL_CHARS]: + delimiter = _QUOTING_CHARS[0] + elif nquotetypes == 1 and string[0] not in _QUOTING_CHARS: + delimiter = _QUOTING_CHARS[1] if present[0] else _QUOTING_CHARS[0] + elif nquotetypes > 1: msg = "String '{}' can not be quoted correctly".format(string) raise ValueError(msg) - delimiter = _QUOTING_CHARS[0] if not is_present[0] else _QUOTING_CHARS[1] return delimiter + string + delimiter - - - -if __name__ == "__main__": - INPUT = { - "Driver": {}, - "Hamiltonian": { - "DFTB": { - "Scc": True, - "SccTolerance": 1e-10, - "MaxSccIterations": 1000, - "Mixer": { - "Broyden": {} - }, - "MaxAngularMomentum": { - "O": "p", - "H": "s" - }, - "Filling": { - "Fermi": { - "Temperature": 1e-8, - "Temperature.attribute": "Kelvin" - } - }, - "KPointsAndWeights": { - "SupercellFolding": [[2, 0, 0], [0, 2, 0], [0, 0, 2], - [0.5, 0.5, 0.5]] - }, - "ElectricField": { - "PointCharges": { - "CoordsAndCharges": np.array( - [[-0.94, -9.44, 1.2, 1.0], - [-0.94, -9.44, 1.2, -1.0]]) - } - }, - "SelectSomeAtoms": [1, 2, "3:-3"] - } - }, - "Analysis": { - "ProjectStates": { - "Region": [ - { - "Atoms": [1, 2, 3], - "Label": "region1", - }, - { - "Atoms": np.array([1, 2, 3]), - "Label": "region2", - } - ] - } - } - } - print(dumps(INPUT)) diff --git a/src/hsd/parser.py b/src/hsd/parser.py new file mode 100644 index 0000000..751b090 --- /dev/null +++ b/src/hsd/parser.py @@ -0,0 +1,367 @@ +#------------------------------------------------------------------------------# +# hsd: package for manipulating HSD-formatted data # +# Copyright (C) 2011 - 2020 DFTB+ developers group # +# # +# See the LICENSE file for terms of usage and distribution. # +#------------------------------------------------------------------------------# +# +""" +Contains the event-generating HSD-parser. +""" +from collections import OrderedDict +import hsd.common as common + + +__all__ = ["HsdParser", + "SYNTAX_ERROR", "UNCLOSED_TAG_ERROR", "UNCLOSED_OPTION_ERROR", + "UNCLOSED_QUOTATION_ERROR", "ORPHAN_TEXT_ERROR"] + +SYNTAX_ERROR = 1 +UNCLOSED_TAG_ERROR = 2 +UNCLOSED_OPTION_ERROR = 3 +UNCLOSED_QUOTATION_ERROR = 4 +ORPHAN_TEXT_ERROR = 5 + +_GENERAL_SPECIALS = "{}[]<=\"'#;" +_OPTION_SPECIALS = ",]=\"'#{};" + + +class HsdEventHandler: + """Base class for event handler implementing simple printing""" + + def __init__(self): + """Initializes the default event handler""" + self._indentlevel = 0 + self._indentstr = " " + + + def open_tag(self, tagname, options, hsdoptions): + """Handler which is called when a tag is opened. + + It should be overriden in the application to handle the event in a + customized way. + + Args: + tagname: Name of the tag which had been opened. + options: Dictionary of the options (attributes) of the tag. + hsdoptions: Dictionary of the options created during the processing + in the hsd-parser. + """ + indentstr = self._indentlevel * self._indentstr + print("{}OPENING TAG: {}".format(indentstr, tagname)) + print("{}OPTIONS: {}".format(indentstr, str(options))) + print("{}HSD OPTIONS: {}".format(indentstr, str(hsdoptions))) + self._indentlevel += 1 + + + def close_tag(self, tagname): + """Handler which is called when a tag is closed. + + It should be overriden in the application to handle the event in a + customized way. + + Args: + tagname: Name of the tag which had been closed. + """ + indentstr = self._indentlevel * self._indentstr + print("{}CLOSING TAG: {}".format(indentstr, tagname)) + self._indentlevel -= 1 + + + def add_text(self, text): + """Handler which is called with the text found inside a tag. + + It should be overriden in the application to handle the event in a + customized way. + + Args: + text: Text in the current tag. + """ + indentstr = self._indentlevel * self._indentstr + print("{}Received text: {}".format(indentstr, text)) + + +class HsdParser: + """Event based parser for the HSD format. + + The methods `open_tag()`, `close_tag()`, `add_text()` + and `_handle_error()` should be overridden by the actual application. + """ + + def __init__(self, defattrib=common.DEFAULT_ATTRIBUTE, eventhandler=None): + """Initializes the parser. + + Args: + defattrib: Name of the default attribute (default: 'attribute') + eventhandler: Instance of the HsdEventHandler class or its children. + """ + if eventhandler is None: + self._eventhandler = HsdEventHandler() + else: + self._eventhandler = eventhandler + + self._fname = "" # Name of file being processed + self._defattrib = defattrib.lower() # def. attribute name + self._checkstr = _GENERAL_SPECIALS # special characters to look for + self._oldcheckstr = "" # buffer fo checkstr + self._opened_tags = [] # info about opened tags + self._buffer = [] # buffering plain text between lines + self._options = OrderedDict() # options for current tag + self._hsdoptions = OrderedDict() # hsd-options for current tag + self._key = "" # current option name + self._currline = 0 # nr. of current line in file + self._after_equal_sign = False # last tag was opened with equal sign + self._inside_option = False # parser inside option specification + self._inside_quote = False # parser inside quotation + self._has_child = False + self._oldbefore = "" + + + def feed(self, fobj): + """Feeds the parser with data. + + Args: + fobj: File like object or name of a file containing the data. + """ + isfilename = isinstance(fobj, str) + if isfilename: + fp = open(fobj, "r") + self._fname = fobj + else: + fp = fobj + for line in fp.readlines(): + self._parse(line) + self._currline += 1 + if isfilename: + fp.close() + + # Check for errors + if self._opened_tags: + line0 = self._opened_tags[-1][1] + else: + line0 = 0 + if self._inside_quote: + self._error(UNCLOSED_QUOTATION_ERROR, (line0, self._currline)) + elif self._inside_option: + self._error(UNCLOSED_OPTION_ERROR, (line0, self._currline)) + elif self._opened_tags: + self._error(UNCLOSED_TAG_ERROR, (line0, line0)) + elif ("".join(self._buffer)).strip(): + self._error(ORPHAN_TEXT_ERROR, (line0, self._currline)) + + + def _parse(self, line): + """Parses a given line.""" + + while True: + sign, before, after = _splitbycharset(line, self._checkstr) + + # End of line + if not sign: + if self._inside_quote: + self._buffer.append(before) + elif self._after_equal_sign: + self._text("".join(self._buffer) + before.strip()) + self._closetag() + self._after_equal_sign = False + elif not self._inside_option: + self._buffer.append(before) + elif before.strip(): + self._error(SYNTAX_ERROR, (self._currline, self._currline)) + break + + # Special character is escaped + elif before.endswith("\\") and not before.endswith("\\\\"): + self._buffer.append(before + sign) + + # Equal sign outside option specification + elif sign == "=" and not self._inside_option: + # Ignore if followed by "{" (DFTB+ compatibility) + if after.lstrip().startswith("{"): + self._oldbefore = before + else: + self._has_child = True + self._hsdoptions[common.HSDATTR_EQUAL] = True + self._starttag(before, False) + self._after_equal_sign = True + + # Equal sign inside option specification + elif sign == "=": + self._key = before.strip() + self._buffer = [] + + # Opening tag by curly brace + elif sign == "{" and not self._inside_option: + self._has_child = True + self._starttag(before, self._after_equal_sign) + self._buffer = [] + self._after_equal_sign = False + + # Closing tag by curly brace + elif sign == "}" and not self._inside_option: + self._text("".join(self._buffer) + before) + self._buffer = [] + # If 'test { a = 12 }' occurs, curly brace closes two tags + if self._after_equal_sign: + self._after_equal_sign = False + self._closetag() + self._closetag() + + # Closing tag by semicolon + elif (sign == ";" and self._after_equal_sign + and not self._inside_option): + self._after_equal_sign = False + self._text(before) + self._closetag() + + # Comment line + elif sign == "#": + self._buffer.append(before) + after = "" + + # Opening option specification + elif sign == "[" and not self._inside_option: + if "".join(self._buffer).strip(): + self._error(SYNTAX_ERROR, (self._currline, self._currline)) + self._oldbefore = before + self._buffer = [] + self._inside_option = True + self._key = "" + self._opened_tags.append(("[", self._currline, None)) + self._checkstr = _OPTION_SPECIALS + + # Closing option specification + elif sign == "]" and self._inside_option: + value = "".join(self._buffer) + before + key = self._key.lower() if self._key else self._defattrib + self._options[key] = value.strip() + self._inside_option = False + self._buffer = [] + self._opened_tags.pop() + self._checkstr = _GENERAL_SPECIALS + + # Quoting strings + elif sign == "'" or sign == '"': + if self._inside_quote: + self._checkstr = self._oldcheckstr + self._inside_quote = False + self._buffer.append(before + sign) + self._opened_tags.pop() + else: + self._oldcheckstr = self._checkstr + self._checkstr = sign + self._inside_quote = True + self._buffer.append(before + sign) + self._opened_tags.append(('"', self._currline, None)) + + # Closing attribute specification + elif sign == "," and self._inside_option: + value = "".join(self._buffer) + before + key = self._key.lower() if self._key else self._defattrib + self._options[key] = value.strip() + + # Interrupt + elif (sign == "<" and not self._inside_option + and not self._after_equal_sign): + txtinc = after.startswith("<<") + hsdinc = after.startswith("<+") + if txtinc: + self._text("".join(self._buffer) + before) + self._buffer = [] + self._eventhandler.add_text(self._include_txt(after[2:])) + break + elif hsdinc: + self._include_hsd(after[2:]) + break + else: + self._buffer.append(before + sign) + + else: + self._error(SYNTAX_ERROR, (self._currline, self._currline)) + + line = after + + + def _text(self, text): + stripped = text.strip() + if stripped: + self._eventhandler.add_text(stripped) + + + def _starttag(self, tagname, closeprev): + txt = "".join(self._buffer) + if txt: + self._text(txt) + tagname_stripped = tagname.strip() + if self._oldbefore: + if tagname_stripped: + self._error(SYNTAX_ERROR, (self._currline, self._currline)) + else: + tagname_stripped = self._oldbefore.strip() + if len(tagname_stripped.split()) > 1: + self._error(SYNTAX_ERROR, (self._currline, self._currline)) + self._hsdoptions[common.HSDATTR_LINE] = self._currline + self._hsdoptions[common.HSDATTR_TAG] = tagname_stripped + tagname_stripped = tagname_stripped.lower() + self._eventhandler.open_tag(tagname_stripped, self._options, + self._hsdoptions) + self._opened_tags.append( + (tagname_stripped, self._currline, closeprev, self._has_child)) + self._buffer = [] + self._oldbefore = "" + self._has_child = False + self._options = OrderedDict() + self._hsdoptions = OrderedDict() + + + def _closetag(self): + if not self._opened_tags: + self._error(SYNTAX_ERROR, (0, self._currline)) + self._buffer = [] + tag, _, closeprev, self._has_child = self._opened_tags.pop() + self._eventhandler.close_tag(tag) + if closeprev: + self._closetag() + + + def _include_hsd(self, fname): + fname = common.unquote(fname.strip()) + parser = HsdParser(defattrib=self._defattrib, + eventhandler=self._eventhandler) + parser.feed(fname) + + + @staticmethod + def _include_txt(fname): + fname = common.unquote(fname.strip()) + fp = open(fname, "r") + txt = fp.read() + fp.close() + return txt + + + def _error(self, errorcode, lines): + error_msg = ( + "Parsing error ({}) between lines {} - {} in file '{}'.".format( + errorcode, lines[0] + 1, lines[1] + 1, self._fname)) + raise common.HsdParserError(error_msg) + + + +def _splitbycharset(txt, charset): + """Splits a string at the first occurrence of a character in a set. + + Args: + txt: Text to split. + chars: Chars to look for. + + Returns: + Tuple (char, before, after). Char is the character which had been found + (or empty string if nothing was found). Before is the substring before + the splitting character (or the entire string). After is the substring + after the splitting character (or empty string). + """ + for firstpos, char in enumerate(txt): + if char in charset: + return txt[firstpos], txt[:firstpos], txt[firstpos + 1:] + return '', txt, '' diff --git a/test/test.hsd b/test/test.hsd new file mode 100644 index 0000000..4141c15 --- /dev/null +++ b/test/test.hsd @@ -0,0 +1,58 @@ +Geometry { + GenFormat = { + 3 C + O H + 1 1 0.0 0.0 0.0 + 2 2 0.0 0.5 0.5 + 3 2 0.0 0.5 -0.5 + } +} +Driver {} +Hamiltonian { + DFTB { + Scc = Yes + SccTolerance = 1e-10 + MaxSccIterations = 1000 + Mixer { + Broyden {} + } + MaxAngularMomentum { + O = "p" + H = "s" + } + Filling { + Fermi { + Temperature [Kelvin] = 1e-08 + } + } + KPointsAndWeights { + SupercellFolding = { + 2 0 0 + 0 2 0 + 0 0 2 + 0.5 0.5 0.5 + } + } + ElectricField { + PointCharges { + CoordsAndCharges = { + -0.94 -9.44 1.2 1.0 + -0.94 -9.44 1.2 -1.0 + } + } + } + SelectSomeAtoms = 1 2 " 3 : -3 " + } +} +Analysis { + ProjectStates { + Region { + Atoms = 1 2 3 + Label = "region1" + } + Region { + Atoms = 1 2 3 + Label = "region2" + } + } +} diff --git a/test/test_dictbuilder.py b/test/test_dictbuilder.py new file mode 100644 index 0000000..88a4a22 --- /dev/null +++ b/test/test_dictbuilder.py @@ -0,0 +1,37 @@ +#!/bin/env python3 +#------------------------------------------------------------------------------# +# hsd: package for manipulating HSD-formatted data # +# Copyright (C) 2011 - 2020 DFTB+ developers group # +# # +# See the LICENSE file for terms of usage and distribution. # +#------------------------------------------------------------------------------# +# +import hsd + +def test_dictbuilder(): + dictbuilder = hsd.HsdDictBuilder() + parser = hsd.HsdParser(eventhandler=dictbuilder) + with open("test.hsd", "r") as fobj: + parser.feed(fobj) + pyrep = dictbuilder.hsddict + print("** Python structure without data flattening:\n") + print(pyrep) + print("\n** Turning back to HSD:\n") + print(hsd.dumps(pyrep)) + + +def test_dictbuilder_flat(): + dictbuilder = hsd.HsdDictBuilder(flatten_data=True) + parser = hsd.HsdParser(eventhandler=dictbuilder) + with open("test.hsd", "r") as fobj: + parser.feed(fobj) + pyrep = dictbuilder.hsddict + print("** Python structure with data flattening:\n") + print(pyrep) + print("\n** Turning back to HSD:\n") + print(hsd.dumps(pyrep)) + + +if __name__ == '__main__': + test_dictbuilder() + test_dictbuilder_flat() diff --git a/test/test_dump.py b/test/test_dump.py new file mode 100644 index 0000000..aca21c0 --- /dev/null +++ b/test/test_dump.py @@ -0,0 +1,62 @@ +#!/bin/env python3 +#------------------------------------------------------------------------------# +# hsd: package for manipulating HSD-formatted data # +# Copyright (C) 2011 - 2020 DFTB+ developers group # +# # +# See the LICENSE file for terms of usage and distribution. # +#------------------------------------------------------------------------------# +# +import numpy as np +import hsd + +if __name__ == "__main__": + INPUT = { + "Driver": {}, + "Hamiltonian": { + "DFTB": { + "Scc": True, + "SccTolerance": 1e-10, + "MaxSccIterations": 1000, + "Mixer": { + "Broyden": {} + }, + "MaxAngularMomentum": { + "O": "p", + "H": "s" + }, + "Filling": { + "Fermi": { + "Temperature": 1e-8, + "Temperature.attribute": "Kelvin" + } + }, + "KPointsAndWeights": { + "SupercellFolding": [[2, 0, 0], [0, 2, 0], [0, 0, 2], + [0.5, 0.5, 0.5]] + }, + "ElectricField": { + "PointCharges": { + "CoordsAndCharges": np.array( + [[-0.94, -9.44, 1.2, 1.0], + [-0.94, -9.44, 1.2, -1.0]]) + } + }, + "SelectSomeAtoms": [1, 2, "3:-3"] + } + }, + "Analysis": { + "ProjectStates": { + "Region": [ + { + "Atoms": [1, 2, 3], + "Label": "region1", + }, + { + "Atoms": np.array([1, 2, 3]), + "Label": "region2", + } + ] + } + } + } + print(hsd.dumps(INPUT)) diff --git a/test/test_parser.py b/test/test_parser.py new file mode 100644 index 0000000..2280839 --- /dev/null +++ b/test/test_parser.py @@ -0,0 +1,19 @@ +#!/bin/env python3 +#------------------------------------------------------------------------------# +# hsd: package for manipulating HSD-formatted data # +# Copyright (C) 2011 - 2020 DFTB+ developers group # +# # +# See the LICENSE file for terms of usage and distribution. # +#------------------------------------------------------------------------------# +# +import hsd + + +def test_parser(): + parser = hsd.HsdParser() + with open("test.hsd", "r") as fobj: + parser.feed(fobj) + + +if __name__ == '__main__': + test_parser()