Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
parser.py528 linesDownload Raw Back to jmespath
1"""Top down operator precedence parser.2 3This is an implementation of Vaughan R. Pratt's4"Top Down Operator Precedence" parser.5(http://dl.acm.org/citation.cfm?doid=512927.512931).6 7These are some additional resources that help explain the8general idea behind a Pratt parser:9 10* http://effbot.org/zone/simple-top-down-parsing.htm11* http://javascript.crockford.com/tdop/tdop.html12 13A few notes on the implementation.14 15* All the nud/led tokens are on the Parser class itself, and are dispatched16  using getattr().  This keeps all the parsing logic contained to a single17  class.18* We use two passes through the data.  One to create a list of token,19  then one pass through the tokens to create the AST.  While the lexer actually20  yields tokens, we convert it to a list so we can easily implement two tokens21  of lookahead.  A previous implementation used a fixed circular buffer, but it22  was significantly slower.  Also, the average jmespath expression typically23  does not have a large amount of token so this is not an issue.  And24  interestingly enough, creating a token list first is actually faster than25  consuming from the token iterator one token at a time.26 27"""28import random29 30from jmespath import lexer31from jmespath.compat import with_repr_method32from jmespath import ast33from jmespath import exceptions34from jmespath import visitor35 36 37class Parser(object):38    BINDING_POWER = {39        'eof': 0,40        'unquoted_identifier': 0,41        'quoted_identifier': 0,42        'literal': 0,43        'rbracket': 0,44        'rparen': 0,45        'comma': 0,46        'rbrace': 0,47        'number': 0,48        'current': 0,49        'expref': 0,50        'colon': 0,51        'pipe': 1,52        'or': 2,53        'and': 3,54        'eq': 5,55        'gt': 5,56        'lt': 5,57        'gte': 5,58        'lte': 5,59        'ne': 5,60        'flatten': 9,61        # Everything above stops a projection.62        'star': 20,63        'filter': 21,64        'dot': 40,65        'not': 45,66        'lbrace': 50,67        'lbracket': 55,68        'lparen': 60,69    }70    # The maximum binding power for a token that can stop71    # a projection.72    _PROJECTION_STOP = 1073    # The _MAX_SIZE most recent expressions are cached in74    # _CACHE dict.75    _CACHE = {}76    _MAX_SIZE = 12877 78    def __init__(self, lookahead=2):79        self.tokenizer = None80        self._tokens = [None] * lookahead81        self._buffer_size = lookahead82        self._index = 083 84    def parse(self, expression):85        cached = self._CACHE.get(expression)86        if cached is not None:87            return cached88        parsed_result = self._do_parse(expression)89        self._CACHE[expression] = parsed_result90        if len(self._CACHE) > self._MAX_SIZE:91            self._free_cache_entries()92        return parsed_result93 94    def _do_parse(self, expression):95        try:96            return self._parse(expression)97        except exceptions.LexerError as e:98            e.expression = expression99            raise100        except exceptions.IncompleteExpressionError as e:101            e.set_expression(expression)102            raise103        except exceptions.ParseError as e:104            e.expression = expression105            raise106 107    def _parse(self, expression):108        self.tokenizer = lexer.Lexer().tokenize(expression)109        self._tokens = list(self.tokenizer)110        self._index = 0111        parsed = self._expression(binding_power=0)112        if not self._current_token() == 'eof':113            t = self._lookahead_token(0)114            raise exceptions.ParseError(t['start'], t['value'], t['type'],115                                        "Unexpected token: %s" % t['value'])116        return ParsedResult(expression, parsed)117 118    def _expression(self, binding_power=0):119        left_token = self._lookahead_token(0)120        self._advance()121        nud_function = getattr(122            self, '_token_nud_%s' % left_token['type'],123            self._error_nud_token)124        left = nud_function(left_token)125        current_token = self._current_token()126        while binding_power < self.BINDING_POWER[current_token]:127            led = getattr(self, '_token_led_%s' % current_token, None)128            if led is None:129                error_token = self._lookahead_token(0)130                self._error_led_token(error_token)131            else:132                self._advance()133                left = led(left)134                current_token = self._current_token()135        return left136 137    def _token_nud_literal(self, token):138        return ast.literal(token['value'])139 140    def _token_nud_unquoted_identifier(self, token):141        return ast.field(token['value'])142 143    def _token_nud_quoted_identifier(self, token):144        field = ast.field(token['value'])145        # You can't have a quoted identifier as a function146        # name.147        if self._current_token() == 'lparen':148            t = self._lookahead_token(0)149            raise exceptions.ParseError(150                0, t['value'], t['type'],151                'Quoted identifier not allowed for function names.')152        return field153 154    def _token_nud_star(self, token):155        left = ast.identity()156        if self._current_token() == 'rbracket':157            right = ast.identity()158        else:159            right = self._parse_projection_rhs(self.BINDING_POWER['star'])160        return ast.value_projection(left, right)161 162    def _token_nud_filter(self, token):163        return self._token_led_filter(ast.identity())164 165    def _token_nud_lbrace(self, token):166        return self._parse_multi_select_hash()167 168    def _token_nud_lparen(self, token):169        expression = self._expression()170        self._match('rparen')171        return expression172 173    def _token_nud_flatten(self, token):174        left = ast.flatten(ast.identity())175        right = self._parse_projection_rhs(176            self.BINDING_POWER['flatten'])177        return ast.projection(left, right)178 179    def _token_nud_not(self, token):180        expr = self._expression(self.BINDING_POWER['not'])181        return ast.not_expression(expr)182 183    def _token_nud_lbracket(self, token):184        if self._current_token() in ['number', 'colon']:185            right = self._parse_index_expression()186            # We could optimize this and remove the identity() node.187            # We don't really need an index_expression node, we can188            # just use emit an index node here if we're not dealing189            # with a slice.190            return self._project_if_slice(ast.identity(), right)191        elif self._current_token() == 'star' and \192                self._lookahead(1) == 'rbracket':193            self._advance()194            self._advance()195            right = self._parse_projection_rhs(self.BINDING_POWER['star'])196            return ast.projection(ast.identity(), right)197        else:198            return self._parse_multi_select_list()199 200    def _parse_index_expression(self):201        # We're here:202        # [<current>203        #  ^204        #  | current token205        if (self._lookahead(0) == 'colon' or206                self._lookahead(1) == 'colon'):207            return self._parse_slice_expression()208        else:209            # Parse the syntax [number]210            node = ast.index(self._lookahead_token(0)['value'])211            self._advance()212            self._match('rbracket')213            return node214 215    def _parse_slice_expression(self):216        # [start:end:step]217        # Where start, end, and step are optional.218        # The last colon is optional as well.219        parts = [None, None, None]220        index = 0221        current_token = self._current_token()222        while not current_token == 'rbracket' and index < 3:223            if current_token == 'colon':224                index += 1225                if index == 3:226                    self._raise_parse_error_for_token(227                        self._lookahead_token(0), 'syntax error')228                self._advance()229            elif current_token == 'number':230                parts[index] = self._lookahead_token(0)['value']231                self._advance()232            else:233                self._raise_parse_error_for_token(234                    self._lookahead_token(0), 'syntax error')235            current_token = self._current_token()236        self._match('rbracket')237        return ast.slice(*parts)238 239    def _token_nud_current(self, token):240        return ast.current_node()241 242    def _token_nud_expref(self, token):243        expression = self._expression(self.BINDING_POWER['expref'])244        return ast.expref(expression)245 246    def _token_led_dot(self, left):247        if not self._current_token() == 'star':248            right = self._parse_dot_rhs(self.BINDING_POWER['dot'])249            if left['type'] == 'subexpression':250                left['children'].append(right)251                return left252            else:253                return ast.subexpression([left, right])254        else:255            # We're creating a projection.256            self._advance()257            right = self._parse_projection_rhs(258                self.BINDING_POWER['dot'])259            return ast.value_projection(left, right)260 261    def _token_led_pipe(self, left):262        right = self._expression(self.BINDING_POWER['pipe'])263        return ast.pipe(left, right)264 265    def _token_led_or(self, left):266        right = self._expression(self.BINDING_POWER['or'])267        return ast.or_expression(left, right)268 269    def _token_led_and(self, left):270        right = self._expression(self.BINDING_POWER['and'])271        return ast.and_expression(left, right)272 273    def _token_led_lparen(self, left):274        if left['type'] != 'field':275            #  0 - first func arg or closing paren.276            # -1 - '(' token277            # -2 - invalid function "name".278            prev_t = self._lookahead_token(-2)279            raise exceptions.ParseError(280                prev_t['start'], prev_t['value'], prev_t['type'],281                "Invalid function name '%s'" % prev_t['value'])282        name = left['value']283        args = []284        while not self._current_token() == 'rparen':285            expression = self._expression()286            if self._current_token() == 'comma':287                self._match('comma')288            args.append(expression)289        self._match('rparen')290        function_node = ast.function_expression(name, args)291        return function_node292 293    def _token_led_filter(self, left):294        # Filters are projections.295        condition = self._expression(0)296        self._match('rbracket')297        if self._current_token() == 'flatten':298            right = ast.identity()299        else:300            right = self._parse_projection_rhs(self.BINDING_POWER['filter'])301        return ast.filter_projection(left, right, condition)302 303    def _token_led_eq(self, left):304        return self._parse_comparator(left, 'eq')305 306    def _token_led_ne(self, left):307        return self._parse_comparator(left, 'ne')308 309    def _token_led_gt(self, left):310        return self._parse_comparator(left, 'gt')311 312    def _token_led_gte(self, left):313        return self._parse_comparator(left, 'gte')314 315    def _token_led_lt(self, left):316        return self._parse_comparator(left, 'lt')317 318    def _token_led_lte(self, left):319        return self._parse_comparator(left, 'lte')320 321    def _token_led_flatten(self, left):322        left = ast.flatten(left)323        right = self._parse_projection_rhs(324            self.BINDING_POWER['flatten'])325        return ast.projection(left, right)326 327    def _token_led_lbracket(self, left):328        token = self._lookahead_token(0)329        if token['type'] in ['number', 'colon']:330            right = self._parse_index_expression()331            if left['type'] == 'index_expression':332                # Optimization: if the left node is an index expr,333                # we can avoid creating another node and instead just add334                # the right node as a child of the left.335                left['children'].append(right)336                return left337            else:338                return self._project_if_slice(left, right)339        else:340            # We have a projection341            self._match('star')342            self._match('rbracket')343            right = self._parse_projection_rhs(self.BINDING_POWER['star'])344            return ast.projection(left, right)345 346    def _project_if_slice(self, left, right):347        index_expr = ast.index_expression([left, right])348        if right['type'] == 'slice':349            return ast.projection(350                index_expr,351                self._parse_projection_rhs(self.BINDING_POWER['star']))352        else:353            return index_expr354 355    def _parse_comparator(self, left, comparator):356        right = self._expression(self.BINDING_POWER[comparator])357        return ast.comparator(comparator, left, right)358 359    def _parse_multi_select_list(self):360        expressions = []361        while True:362            expression = self._expression()363            expressions.append(expression)364            if self._current_token() == 'rbracket':365                break366            else:367                self._match('comma')368        self._match('rbracket')369        return ast.multi_select_list(expressions)370 371    def _parse_multi_select_hash(self):372        pairs = []373        while True:374            key_token = self._lookahead_token(0)375            # Before getting the token value, verify it's376            # an identifier.377            self._match_multiple_tokens(378                token_types=['quoted_identifier', 'unquoted_identifier'])379            key_name = key_token['value']380            self._match('colon')381            value = self._expression(0)382            node = ast.key_val_pair(key_name=key_name, node=value)383            pairs.append(node)384            if self._current_token() == 'comma':385                self._match('comma')386            elif self._current_token() == 'rbrace':387                self._match('rbrace')388                break389        return ast.multi_select_dict(nodes=pairs)390 391    def _parse_projection_rhs(self, binding_power):392        # Parse the right hand side of the projection.393        if self.BINDING_POWER[self._current_token()] < self._PROJECTION_STOP:394            # BP of 10 are all the tokens that stop a projection.395            right = ast.identity()396        elif self._current_token() == 'lbracket':397            right = self._expression(binding_power)398        elif self._current_token() == 'filter':399            right = self._expression(binding_power)400        elif self._current_token() == 'dot':401            self._match('dot')402            right = self._parse_dot_rhs(binding_power)403        else:404            self._raise_parse_error_for_token(self._lookahead_token(0),405                                              'syntax error')406        return right407 408    def _parse_dot_rhs(self, binding_power):409        # From the grammar:410        # expression '.' ( identifier /411        #                  multi-select-list /412        #                  multi-select-hash /413        #                  function-expression /414        #                  *415        # In terms of tokens that means that after a '.',416        # you can have:417        lookahead = self._current_token()418        # Common case "foo.bar", so first check for an identifier.419        if lookahead in ['quoted_identifier', 'unquoted_identifier', 'star']:420            return self._expression(binding_power)421        elif lookahead == 'lbracket':422            self._match('lbracket')423            return self._parse_multi_select_list()424        elif lookahead == 'lbrace':425            self._match('lbrace')426            return self._parse_multi_select_hash()427        else:428            t = self._lookahead_token(0)429            allowed = ['quoted_identifier', 'unquoted_identifier',430                       'lbracket', 'lbrace']431            msg = (432                "Expecting: %s, got: %s" % (allowed, t['type'])433            )434            self._raise_parse_error_for_token(t, msg)435 436    def _error_nud_token(self, token):437        if token['type'] == 'eof':438            raise exceptions.IncompleteExpressionError(439                token['start'], token['value'], token['type'])440        self._raise_parse_error_for_token(token, 'invalid token')441 442    def _error_led_token(self, token):443        self._raise_parse_error_for_token(token, 'invalid token')444 445    def _match(self, token_type=None):446        # inline'd self._current_token()447        if self._current_token() == token_type:448            # inline'd self._advance()449            self._advance()450        else:451            self._raise_parse_error_maybe_eof(452                token_type, self._lookahead_token(0))453 454    def _match_multiple_tokens(self, token_types):455        if self._current_token() not in token_types:456            self._raise_parse_error_maybe_eof(457                token_types, self._lookahead_token(0))458        self._advance()459 460    def _advance(self):461        self._index += 1462 463    def _current_token(self):464        return self._tokens[self._index]['type']465 466    def _lookahead(self, number):467        return self._tokens[self._index + number]['type']468 469    def _lookahead_token(self, number):470        return self._tokens[self._index + number]471 472    def _raise_parse_error_for_token(self, token, reason):473        lex_position = token['start']474        actual_value = token['value']475        actual_type = token['type']476        raise exceptions.ParseError(lex_position, actual_value,477                                    actual_type, reason)478 479    def _raise_parse_error_maybe_eof(self, expected_type, token):480        lex_position = token['start']481        actual_value = token['value']482        actual_type = token['type']483        if actual_type == 'eof':484            raise exceptions.IncompleteExpressionError(485                lex_position, actual_value, actual_type)486        message = 'Expecting: %s, got: %s' % (expected_type,487                                              actual_type)488        raise exceptions.ParseError(489            lex_position, actual_value, actual_type, message)490 491    def _free_cache_entries(self):492        for key in random.sample(list(self._CACHE.keys()), int(self._MAX_SIZE / 2)):493            self._CACHE.pop(key, None)494 495    @classmethod496    def purge(cls):497        """Clear the expression compilation cache."""498        cls._CACHE.clear()499 500 501@with_repr_method502class ParsedResult(object):503    def __init__(self, expression, parsed):504        self.expression = expression505        self.parsed = parsed506 507    def search(self, value, options=None):508        interpreter = visitor.TreeInterpreter(options)509        result = interpreter.visit(self.parsed, value)510        return result511 512    def _render_dot_file(self):513        """Render the parsed AST as a dot file.514 515        Note that this is marked as an internal method because516        the AST is an implementation detail and is subject517        to change.  This method can be used to help troubleshoot518        or for development purposes, but is not considered part519        of the public supported API.  Use at your own risk.520 521        """522        renderer = visitor.GraphvizVisitor()523        contents = renderer.visit(self.parsed)524        return contents525 526    def __repr__(self):527        return repr(self.parsed)528 
codekingpro/portable-devtools · Team Ai