Ich möchte für eine Applikation hierarchische Konfigurationsdateien einlesen können.
Der Syntax den ich mir dafür vorgestellt habe, ist folgender:
Code: Alles auswählen
# Kommentare sind erlaubt
#Ein Block zeichnet sich dadurch aus, dass die betreffende Zeile von einem Doppelpunkt abgeschlossen wird und dass er der einzige auf dieser Zeile ist.
Block:
#Der erste Doppelpunkt trennt Key und Value
Key1: Value1
# Die Zeile oben gibt vor, um wieviel die im Block enthaltenen Elemente eingerückt sein müssen. (Wie in Python)
Key2: Value2
# Kommentare beeinflussen die Hierarchie nicht.
Verschachtelter Block:
# Auch Leerzeilen sind erlaubt.
# Weitere Doppelpunkte werden Normal dem Wert zugeordnet.
Key1: Wer1MitDoppelPunkt:
Block2:
etc:etc
Zum Code: Er ist noch nicht ganz fertig. Das Parsen funktioniert soweit ganz gut, Exceptions werden geworfen wenn eine Zeile falsch eingerückt ist. Werte können jedoch noch nicht wieder ausgelesen werden, das sollte jedoch nur etwas kleines sein. Wichtiger ist es mir vielmehr, ein paar Meinungen zur Struktur zu bekommen, oder gänzlich andere Ansätze.
Den Namen 'Meta' habe ich gewählt, da die Dateien, die ich parse, etwas über die Applikation aussagen werden. Nun gibt es folgende Klassen:
- Meta: Repräsentiert eine Sammlung von ineinander verschachtelten Blöcken.
- MetaParser: transformiert eine Liste von Zeilen in eine Meta-Struktur
- MetaLine: repräsentiert eine Zeile während dem Parsen und bestimmt, ob die Zeile ein Kommentar/Block ist etc
Ich habe gesehen, dass es die Pakete SimpleParse bzw pyarsing gibt. Was denkt ihr zu diesen? Wäre die Benutzung derer besser geeignet für das, was ich mache und weniger fehleranfällig?
Code: Alles auswählen
#{ Meta Exceptions
class MetaSyntaxError(Exception):
"""Basic Exception when MetaFile does not conform to syntax."""
class MetaIndentationError(SyntaxError):
"""Syntax Error: Wrong indentation."""
pass
#}
class Meta(object): #{
"""Meta Syntax parser."""
def __init__(self, root_block=None):
self._block = root_block or MetaBlock()
#{ magic
def __len__(self):
return len(self._block)
def __iter__(self):
return iter(self._block)
#} magic
#}
class MetaLine(object): #{
"""Line in a Meta config file."""
def __init__(self, number, string):
self._number = number
self._string = string
self._is_comment = string.lstrip().startswith('#')
self._indent = len(string) - len(string.lstrip())
#{ Properties
@property
def is_comment(self):
"""True or false depending whether line is comment."""
return self._is_comment
@property
def is_empty(self):
"""True or false if line is empty or contains only whitespace."""
return len(self._string.strip()) == 0
@property
def indent_level(self):
"""Returns current indentation level."""
return self._indent
@property
def raw(self):
"""The unprocessed line as found in the input."""
return self._string
@property
def is_block(self):
"""True/False: One colon only at the right end."""
stripped = self._string.strip()
return stripped.count(':') and stripped.endswith(':')
#}
def __str__(self):
return self.raw
#}
#{ Parsing helper functions
def _ignore_indent_for(metaline):
"""Does the indent of this line affect the hierarchy?"""
if metaline.is_comment:
return True
if metaline.is_empty:
return True
return False
def _manufacture_item(metaline):
"""Creates the appropriate MetaItem from a MetaLine."""
if metaline.is_comment:
return MetaComment(metaline)
elif metaline.is_block:
return MetaBlock(metaline)
elif metaline.is_empty:
return MetaSpacer(metaline)
else:
return MetaPair(metaline)
#}
class MetaParser(object): #{
"""Parse lines into a Meta structure."""
def __init__(self):
self._block_stack = [MetaBlock()]
self._indent_stack = []
def parse_lines(self, lines):
"""Pass a list of individual lines."""
for number, line in enumerate(lines):
metaline = MetaLine(number, line)
self._add_line(metaline)
return Meta(self._block_stack[0])
#{ #protected
def _add_line(self, line):
"""Convert MetaLine into MetaItem and add."""
while self._belongs_to_outer_block(line):
self._restore_block()
if self._belongs_to_current_block(line):
self._add_line_to_current_block(line)
else:
raise MetaIndentationError("Invalid indent level on line: %s" %
line)
def _restore_block(self):
"""Make previous block active by popping from stacks."""
self._block_stack.pop()
self._indent_stack.pop()
def _add_line_to_current_block(self, line):
"""Create item from line and add to block."""
item = _manufacture_item(line)
self._current_block.add_item(item)
self._update_indent_stack(line)
if isinstance(item, MetaBlock):
self._block_stack.append(item)
def _update_indent_stack(self, line):
"""Add indent to _index_stack if appropriate."""
missing_indent = len(self._indent_stack) < len(self._block_stack)
dont_ignore = not _ignore_indent_for(line)
if dont_ignore and missing_indent:
self._indent_stack.append(line.indent_level)
def _belongs_to_current_block(self, line):
"""Does a item belong to this block based on the indentation?"""
if _ignore_indent_for(line):
return True
if self._current_indent == line.indent_level:
return True
if self._current_indent is None:
if not self._on_root_block:
return self._is_first_block_member_indented(line)
return True
return False
def _is_first_block_member_indented(self, line):
"""First member of new block must be indented more."""
try:
block_indent = self._indent_stack[-1]
return line.indent_level > block_indent
except IndexError:
return False
def _belongs_to_outer_block(self, line):
"""Is the line's indentation bigger than the other owned item's?"""
print "Comparing", line.indent_level, "to", self._current_indent
return line.indent_level < self._current_indent
#}
#{ properties
@property
def _current_block(self):
"""Last added block."""
return self._block_stack[-1]
@property
def _on_root_block(self):
"""Any nested blocks open?"""
return len(self._block_stack) == 1
@property
def _current_indent(self):
"""Last level of current path of absolute indent levels."""
current_index = len(self._block_stack) - 1
try:
return self._indent_stack[current_index]
except IndexError:
return None
#} properties
#} class MetaParser
#{ cls: MetaItem, MetaSpacer, MetaPair
class MetaItem(object):
"""Base class for MetaItems."""
def __init__(self, metaline):
self._metaline = metaline
@property
def indent_level(self):
"""Amount of blank spaces from the left."""
return self._metaline.indent_level
class MetaSpacer(MetaItem):
"""Represents an empty line."""
pass
class MetaComment(MetaItem):
"""Comment in a MetaFile."""
pass
class MetaPair(MetaItem):
"""Key:Value pair."""
pass
#} cls: MetaItem, MetaSpacer, MetaPair
class MetaBlock(object): #{
"""Hiearchical block in a MetaFile.
A block is introduced by a line that contains only a single colon which is
at the very right of a line.
Each block can contain an unlimited amount of MetaLines or MetaBlocks,
which in turn can contain further MetaBlocks.
"""
def __init__(self, metaline=None):
self._items = []
self._metaline = metaline
def add_item(self, item):
"""Insert new contained item at the end."""
self._items.append(item)
#{ Magic
def __iter__(self):
return iter(self._items)
def __len__(self):
return len(self._items)
#}
#}