Source code for pyfpm.parser

"""
Scala-like pattern syntax parser.
"""
import re
import inspect

from pyparsing import Literal, Word, Group, Combine, Suppress,\
        Forward, Optional, alphas, nums, alphanums, QuotedString,\
        quotedString, dblQuotedString, removeQuotes, delimitedList,\
        ParseException, Keyword, restOfLine, ParseFatalException

from pyfpm.pattern import build as _

def _get_caller_globals():
    frame = inspect.getouterframes(inspect.currentframe())[2][0]
    return frame.f_globals

class _IfCondition(object):
    def __init__(self, code, context):
        self.code = code
        self.context = context

    def __call__(self, **kwargs):
        return eval(self.code, self.context, kwargs)

    def __eq__(self, other):
        return (isinstance(other, _IfCondition) and
                self.__dict__ == other.__dict__)
    
    def __str__(self):
        return '_IfCondition(code=%s, context=%s)' % (
                self.code,
                self.context)

[docs]def Parser(context=None): """ Create a parser. :param context: optional context, defaults to the caller's `globals()` :type context: dict .. warning:: creating a parser is expensive! Usage and syntax examples: >>> parser = Parser() match anything anonymously: >>> parser('_') << 'whatever' Match({}) match anything and bind to a name: >>> parser('x') << 1 Match({'x': 1}) match instances of a specific type: >>> parser('_:str') << 1 >>> parser('_:int') << 1 Match({}) >>> parser('x:str') << 'abc' Match({'x': 'abc'}) match int, float, str and bool constants: >>> parser('1') << 1 Match({}) >>> parser('1.618') << 1.618 Match({}) >>> parser('"abc"') << 'abc' Match({}) >>> parser('True') << True Match({}) match lists: >>> parser('[]') << () Match({}) >>> parser('[x:int]') << [1] Match({'x': 1}) >>> parser('[a, b, _]') << [1, 2, 3] Match({'a': 1, 'b': 2}) split head vs. tail: >>> parser('a::b') << (1, 2, 3) Match({'a': 1, 'b': (2, 3)}) >>> parser('a::b::c') << (0, 1, 2, 3, 4) Match({'a': 0, 'c': (2, 3, 4), 'b': 1}) match named tuples (as if they were Scala case classes) >>> try: ... from collections import namedtuple ... Case3 = namedtuple('Case3', 'a b c') ... parser = Parser() # Case3 has to be in the context ... parser('Case3(x, y, z)') << Case3(1, 2, 3) ... except ImportError: ... from pyfpm.pattern import Match ... Match({'y': 2, 'x': 1, 'z': 3}) # no namedtuple in python < 2.6 Match({'y': 2, 'x': 1, 'z': 3}) boolean or between expressions: >>> parser('a:int|b:str') << 1 Match({'a': 1}) >>> parser('a:int|b:str') << 'hello' Match({'b': 'hello'}) nest expressions: >>> parser('[[[x:int]]]') << [[[1]]] Match({'x': 1}) >>> parser('[_:int|[], 2, 3]') << (1, 2, 3) Match({}) >>> parser('[_:int|[], 2, 3]') << ([], 2, 3) Match({}) >>> parser('[_:int|[], 2, 3]') << ([1], 2, 3) """ if context is None: context = _get_caller_globals() # parsing actions def get_type(type_name): try: t = eval(type_name, context) except NameError: raise ParseException('unknown type: %s' % type_name) if not isinstance(t, type): raise ParseException('not a type: %s' % type_name) return t def get_named_var(var_name): try: get_type(var_name) except ParseException: return _()%var_name raise ParseException('var name clashes with type: %s' % var_name) # begin grammar type_ = Word(alphas, alphanums + '._')('type_').setParseAction( lambda *args: get_type(args[-1].type_)) anon_var = Literal("_")('anon_var').setParseAction(lambda *args: _()) named_var = Word(alphas, alphanums + '_')('named_var').setParseAction( lambda *args: get_named_var(args[-1].named_var)) untyped_var = (named_var | anon_var)('untyped_var') typed_var = (untyped_var + Suppress(':') + type_)( 'typed_var').setParseAction( lambda *args: _(args[-1].type_)%args[-1].untyped_var.bound_name) var = (typed_var | untyped_var)('var') int_const = Combine(Optional('-') + Word(nums))( 'int_const').setParseAction(lambda *args: int(args[-1].int_const)) float_const = Combine(Optional('-') + Word(nums) + Literal('.') + Optional(Word(nums)) | Optional('-') + Literal('.') + Word(nums))( 'float_const').setParseAction( lambda *args: float(args[-1].float_const)) str_const = (quotedString | dblQuotedString)('str_const').setParseAction( removeQuotes) regex_const = QuotedString(quoteChar='/', escChar='\\')('regex_const' ).setParseAction(lambda *args: re.compile(args[-1].regex_const)) true = Keyword('True').setParseAction(lambda *args: _(True)) false = Keyword('False').setParseAction(lambda *args: _(False)) null = Keyword('None').setParseAction(lambda *args: _(None)) const = (float_const | int_const | str_const | regex_const | false | true | null)('const').setParseAction( lambda *args: _(args[-1].const)) scalar = Forward() pattern = Forward() head_tail = (scalar + Suppress('::') + pattern)( 'head_tail').setParseAction(lambda *args: args[-1][0] + args[-1][1]) list_item = (pattern | scalar)('list_item') list_contents = Optional(delimitedList(list_item))('list_contents') full_list = (Suppress('[') + list_contents + Suppress(']'))( 'full_list').setParseAction(lambda *args: _(list(args[-1]))) list_ = (head_tail | full_list)('list') case_class = Combine(type_ + Suppress('(') + list_contents + Suppress(')'))('case_class').setParseAction( lambda *args: _(args[-1][0].type_(*args[-1][0].list_contents))) scalar << (const | var | case_class | Suppress('(') + pattern + Suppress(')'))('scalar') or_clause = (list_ | scalar)('or_clause') or_expression = (or_clause + Suppress('|') + pattern)( 'or_expression').setParseAction( lambda *args: args[-1][0] | args[-1][1]) def conditional_pattern_action(*args): try: pattern, condition_string = args[-1] code = compile(condition_string.strip(), '<pattern_condition>', 'eval') pattern.if_(_IfCondition(code, context)) return pattern except ValueError: pass pattern << ((or_expression | or_clause) + Optional(Suppress(Keyword('if')) + restOfLine))( 'pattern').setParseAction(conditional_pattern_action) # end grammar def parse(expression): (p,) = pattern.parseString(expression, parseAll=True) return p parse.context = context parse.setDebug = pattern.setDebug return parse

Project Versions