Package: pyrsec
PyrsecSimple parser combinator made in PythonIn the journey of creating a parser combinator in python while being as type safe as
possible we are here now. I don't recommend you use this for anything important but for
exploration and fun. This library is a mostly undocumented, bare bone implementation of
a parser combinator, no error recovery is currently in place, onlyNoneis returned in
case the parser can't continue. I basically started with a minimum implementation while
adding a basicjsonparser as a test and kept adding functionality as needed.pipinstallpyrsecA Json parser as an exampleYou should be able to inspect the types of the variables in the following code>>>frompyrsecimportParsecLets define the type of our json values,>>>fromtypingimportUnion,List,Dict# because 3.8 and 3.9 🙄>>># Recursive type alias 👀. See how we will not parse `floats` here.>>># Also at this level we can't still reference JSON recursively, idk why.>>>JSON=Union[bool,int,None,str,List["JSON"],Dict[str,"JSON"]]and the type of our parser. Since this is a parser that will outputJSONvalues its
type will beParsec[JSON].>>># To be defined later>>>json_:Parsec[JSON]>>># For recursive parsers like `list_` and `dict_`>>>deferred_json_=Parsec.from_deferred(lambda:json_)Lets bring up a few basic parsers.>>>importre>>>true=Parsec.from_string("true").map(lambda_:True)>>>false=Parsec.from_string("false").map(lambda_:False)>>>null=Parsec.from_string("null").map(lambda_:None)>>>number=Parsec.from_re(re.compile(r"-?\d+")).map(int)>>>true("true")(True,'')>>>false("false")(False,'')>>>null("null")(None,'')>>>number("42")(42,'')We need to be able to parse character sequences, lets keep it simple.The operators>>and<<are used to discard the part that the arrow is not pointing
at. They are meant to work well withParsecinstances. In this case only the result of
the middle parserParsec.from_re(re.compile(r"[^\"]*"))is returned from thestringparser.If what you want instead is to concatenate the results you should see the&operator.
(wait for the pair definition).>>>quote=Parsec.from_string('"').ignore()>>>string=quote>>Parsec.from_re(re.compile(r"[^\"]*"))<<quote>>>string('"foo"')('foo','')See how the quotes got discarded?Also, missing a quote would mean a parsing error.>>>string('foo"'),string('"bar')(None,None)Lets get a little bit more serious with the lists.Spaces are always optional onjsonstrings. Other basic tokens are also needed.>>>space=Parsec.from_re(re.compile(r"\s*")).ignore()>>>comma=Parsec.from_string(",").ignore()>>>opened_square_bracket=Parsec.from_string("[").ignore()>>>closed_square_bracket=Parsec.from_string("]").ignore()And finally, thelistparser. We need to use a deferred value here because the
definition is recursive but the wholejsonparser is still not available.>>>list_=(...opened_square_bracket...>>(deferred_json_.sep_by(comma))# See here?...<<closed_square_bracket...)Lets create an incomplete one.>>>json_=space>>(true|false|number|null|string|list_)<<spaceLets try it then!>>>list_("[]")([],'')>>>list_("[1, true, false, []]")([1,True,False,[]],'')Defining a dict should be pretty easy by now. Maybe thepairparser is interesting
because its use of&.Some tokens,>>>opened_bracket=Parsec.from_string("{").ignore()>>>closed_bracket=Parsec.from_string("}").ignore()>>>colon=Parsec.from_string(":").ignore()Andpair, notice that the type ofpairwill beParsec[tuple[str, JSON]].>>>pair=((space>>string<<space)<<colon)&deferred_json_>>>pair('"foo": [123]')(('foo',[123]),'')Thedictparser will finally be pretty close to thelistone.>>>dict_=(...opened_bracket...>>pair.sep_by(comma).map(lambdaxs:dict(xs))...<<closed_bracket...)And finally lets redefine thejsonparser to embrace the full beauty of it.>>>json_=space>>(true|false|number|null|string|list_|dict_)<<space>>>json_("""... {...     "json_parser": [true]... }... """)({'json_parser':[True]},'')Enjoy!