This is a followup of sorts to this question: link. 'Of sorts' because, as pointed out in the comments, this question should have been asked first.
I'm writing a parser library in Python, mostly for fun, but I do plan to use it in some of my projects. I would like to know if the foundation of this library is sound: whether it is Pythonic, performant and provides a decent API.
The entire code can be seen in this repo: link, in the frozen branch review-04-02-2018
, file core.py
. I'll post relevant portions below.
The library considers a parser any callable that takes a single State
object and returns a new one. A State
object's purpose is to keep track of three things: input that's left to parse, the portion of input that was consumed by a parser and the value of a parser chain (the idea is that a parser chain would construct some object as it goes along). Here's the State
class:
class State():
""" An object representing current parser state. """
def __init__(self, left, value=None, parsed=""):
self.value = value
self.left = left
self.parsed = parsed
def __eq__(self, other):
return (self.value == other.value and
self.left == other.left and
self.parsed == other.parsed)
def __repr__(self):
return f"State({repr(self.value)}, {repr(self.left[0:40])}, {repr(self.parsed)})"
def copy(self):
""" Shallow copy the State object. """
return State(self.left, self.value, self.parsed)
def deepcopy(self):
""" Deep copy the State object. """
return deepcopy(self)
def set(self, **kwargs):
""" Return a new State object with given attributes. """
left = kwargs.get("left", self.left)
value = kwargs.get("value", self.value)
parsed = kwargs.get("parsed", self.parsed)
return State(left, value, parsed)
def consume(self, how_many):
""" Return a new State object with 'how_many' characters consumed. """
return State(self.left[how_many:], self.value, self.left[0:how_many])
def blank(self):
"""
Return a new State object with the same 'left' but with None 'parsed'
value.
"""
return State(self.left)
def split(self, at):
"""
Split the State object in two. Return a tuple with two State objects,
the first will have 'left' up to, but not including, index 'at', the
second - starting with 'at' and until the end.
"""
first = self.copy()
first.left = self.left[:at]
second = self.copy()
second.left = self.left[at:]
return first, second
Most are, hopefully, self-explanatory. I have doubts about set
, though: the purpose is to be able to do things like lambda s: s.set(parsed="foo, left="baz", value=None)
to compensate for inability to assign things in lambdas. Could it be written cleaner?
There are also two exception classes, to signal when parsing has failed or is stopped prematurely, respectively. Here are they:
class ParsingFailure(Exception):
""" An exception of this type should be thrown if parsing fails. """
pass
class ParsingEnd(Exception):
"""
An exception of this type should be thrown if parsing ends successfully,
but early.
"""
def __init__(self, state):
super().__init__()
self.state = state
All of the above things are used in the main function of the library, parse
:
def parse(state_or_string, parser, verbose=False):
"""
Run a given parser on a given state object or a string.
Return parser's return value on success, or None on failure.
If 'verbose' is truthy, return terminating ParsingFailure exception on
failure instead of None.
"""
if isinstance(state_or_string, str):
state = State(state_or_string)
else:
state = state_or_string
try:
return parser(state)
except ParsingFailure as failure:
if verbose:
return failure
return None
except ParsingEnd as end:
return end.state
There is a problem with this, namely that it's impossible to tell apart None
returned from a failure and None
legitimately produced by a parser, but I feel in most cases None
is a pretty good indicator of a failure. This raises the question: should I somehow separate these two cases?