Source code for boolean_parser.parsers.base

# !/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Filename: base.py
# Project: parsers
# Author: Brian Cherinka
# Created: Sunday, 17th February 2019 3:43:08 pm
# License: BSD 3-clause "New" or "Revised" License
# Copyright (c) 2019 Brian Cherinka
# Last Modified: Friday, 1st March 2019 2:18:11 pm
# Modified By: Brian Cherinka


from __future__ import print_function, division, absolute_import
import copy
import six
import pyparsing as pp
from pyparsing import ParseException
from boolean_parser.actions.boolean import BoolNot, BoolAnd, BoolOr
from boolean_parser.clauses import condition, between_cond, words
from boolean_parser.actions.clause import Condition, Word


[docs]class BooleanParserException(Exception): pass
[docs]class Parser(object): ''' Core Parser class for parsing strings into objects A core Parser class that can parse strings into a set of objects based on a defined set of string clause elements, and actions to perform for each clause. ''' _bools = [BoolNot, BoolAnd, BoolOr] _clauses = [] _clause = None def __init__(self, value=None): self.original_input = value self._expression = None if self.original_input: self._expression = self.parse() @property def params(self): ''' The extracted parameters from the parsed string ''' if isinstance(self._expression, Condition): return self._expression.fullname if self._expression else None return self._expression.params if self._expression else None @property def conditions(self): ''' The extracted conditions from the parsed string ''' if isinstance(self._expression, Condition): return self._expression if self._expression else None return self._expression.conditions if self._expression else None
[docs] def parse(self, value=None): ''' Parse a string conditional Calls ``parseString`` on the ``pyparsing`` clause element to parse the input string into a ``pyparsing.ParseResults`` object. Parameters: value: str The string expression to parse Returns: A pyparsing.ParseResults object Example: >>> from boolean_parser.parsers import Parser >>> pp = Parser() >>> pp.parse('x > 1') >>> x>1 ''' value = value or self.original_input assert value is not None, 'There must be some input to parse' assert isinstance(value, six.string_types), 'input must be a string' try: expression = self._parser.parseString(value)[0] except ParseException as e: raise BooleanParserException("Parsing syntax error ({0}) at line:{1}, " "col:{2}".format(e.markInputline(), e.lineno, e.col)) else: self._expression = expression self.original_input = value return expression
def __repr__(self): return f'<Parser(input="{self.original_input or ""}")>'
[docs] @classmethod def build_parser(cls, clauses=None, actions=None, bools=None): ''' Builds a new boolean parser Constructs a new boolean Parser class given a set of clauses, actions, and boolean objects. Clauses are individual ``pyparsing`` elements that represent string clauses to pattern match on. Actions are functions or classes set on each clause element that control how that clause is parsed. See :ref:`clauses` for the available `pyparsing` clause elements. Assigns the default boolean classes, ``[BoolNot, BoolAnd, BoolOr]`` to the :py:func:`pyparsing.infixNotation` such that NOTs->ANDs->ORs. If ``bools`` is specified instead, uses those object classes to handle boolean logic. ``bools`` must be a list of length 3 containing classes for boolean "not", "and", and "or" logic in that order. Parameters: clauses: list A list of pyparsing clause elements actions: list A list of actions to attach to each clause element bools: list A list of Boolean classes to use to handle boolean logic Example: >>> from boolean_parser.parsers import Parser >>> from boolean_parser.clauses import condition, words >>> from boolean_parser.actions.clause import Condition, Word >>> >>> # Assign the parsing order precedence for clauses >>> clauses = [condition, words] >>> >>> # Create a list of Actions for each clause in clauses >>> actions = [Condition, Word] >>> >>> # build the Parser with these clauses and actions >>> Parser.build_parser(clauses=clauses, actions=actions) ''' # set clauses and actions clauses = clauses or cls._clauses assert clauses is not None, 'A list of clauses must be provided' cls.set_clauses(clauses) cls.build_clause() assert cls._clause is not None, 'A singular clause must be built from clauses' if actions: cls.set_parse_actions(clauses=clauses, actions=actions) # assign the combined clause to the recursive token pattern matcher where_exp = pp.Forward() where_exp <<= cls._clause # extract the Boolean precendent clauses bools = bools or cls._bools assert len( bools) == 3, 'there must be a set of "not, and, or" boolean precedent classes' bnot, band, bor = bools # build the expression parser cls._parser = pp.infixNotation(where_exp, [ (pp.CaselessLiteral("not"), 1, pp.opAssoc.RIGHT, bnot), (pp.CaselessLiteral("and"), 2, pp.opAssoc.LEFT, band), (pp.CaselessLiteral("or"), 2, pp.opAssoc.LEFT, bor), ])
[docs] @classmethod def set_parse_actions(cls, mapping=None, clauses=None, actions=None): ''' Attach actions to a pyparsing clause element ``pyparsing`` clause elements can have optional actions set with the ``setParseAction`` which control how each clause is parsed. This maps a list of actions onto a list of clauses. If ``mapping`` is used, it must be a list of tuples containing which action to map to which clause. Otherwise ``clauses`` and ``actions`` must be provided as equal-length lists which contain, for each item, what action(s) to attach to the corresponding clause. Parameters: mapping: list of tuples A list of tuples containing a (clause, action) mapping. clauses: list A list of clauses actions: list A list of actions to attach to each clause Example: >>> from boolean_parser.parsers import Parser >>> from boolean_parser.clauses import condition, words >>> from boolean_parser.actions.clause import Condition, Word >>> clauses = [condition, words] >>> actions = [Condition, Word] >>> Parser.set_parse_actions(clauses=clauses, actions=actions) ''' if not mapping: assert clauses and actions, 'clauses and actions must both be specified' assert isinstance(clauses, list), 'clauses must be a list' assert isinstance(actions, list), 'actions must be a list' assert len(clauses) == len(actions), 'clauses and actions must be the same length' mapping = zip(clauses, actions) else: assert isinstance(mapping, list), 'mapping must be a list' assert isinstance(mapping[0], (tuple, list)), 'mapping item must be a list or tuple' # requires clauses (a list) assert cls._clauses, 'class clauses must be set. Call cls.set_clauses.' assert mapping is not None, 'a mapping between clauses and actions must be provided' # use reprs for list index check to bypass wonky list/equality clause comparisons clause_reprs = [repr(c) for c in cls._clauses] for item in mapping: clause, action = item assert repr(clause) in clause_reprs, 'clause must be included in list of class clauses' idx = clause_reprs.index(repr(clause)) action = action if isinstance(action, (list, tuple)) else [action] cls._clauses[idx].setParseAction(*action)
[docs] @classmethod def build_clause(cls, clauses=None): ''' Build a single clause from a list of clauses using pp.MatchFirst Merges a list of clauses into a single clause using :py:class:`pyparsing.MatchFirst`. This is equivalent to "clause = clause1 | clause2 | clause3`. The clause precedence the Parser uses will be the order they appear in the list. The default is to use the attached Parser._clauses list. Parameters: clauses: list A list of clauses to merge into a single clause ''' clauses = clauses or cls._clauses assert isinstance(clauses, list), 'clauses must be a list' clauses = pp.MatchFirst(clauses) cls._clause = clauses
[docs] @classmethod def set_clauses(cls, clauses): ''' Sets the list of clauses to use Parameters: clauses: list A list of clauses to attach to the Parser ''' assert isinstance(clauses, list), 'clauses must be a list' cls._clauses = [copy.copy(c) for c in clauses]
# Set parse actions on conditions and build the Parser clauses = [condition, between_cond, words] actions = [Condition, Condition, Word] Parser.build_parser(clauses=clauses, actions=actions)