Mercurial > jpath
view jpath.py @ 28:0a216b982fd1 default tip
Bump version to 1.6
| author | Radomir Dopieralski <sheep-devel@sheep.art.pl> |
|---|---|
| date | Mon, 22 Jan 2018 15:45:20 +0100 |
| parents | a607cb7f6333 |
| children |
line wrap: on
line source
#!/usr/bin/env python # -*- coding: utf-8 -*- """ Access nested dicts and lists using JSON-like path notation. Note that this code is written for python 3. The notation is as follows: You can just write the names of components in your path like you would in javascript: foo.bar.baz To access arrays or names with spaces in them, use the '[]' notation. You can use negative indices with arrays to count from the end. ["foo"]["bar"]["baz"] array[-1].attr [3] You can use the wildcard character '*' to iterate over all elements: foo.*.baz ["foo"][*]["baz"] This may return more or less than a single element. Use get to get the first one, and get_all to get a list of all possibilities. If you want to skip any number of elements in path, use two dots '..': foo..baz You can slice the arrays just like in python: array[1:-1:2] Finally, you can filter the elements: foo(bar.baz=true) foo.bar(baz>0).baz foo(bar="yawn").bar At the moment only =, >, <, >=, <= and != operators are available, and you can only use them with strings, integers and boolean values. This code was written in STX Next. """ # We use very simple top-down parsing using recursive function calls. # This may break in python for very very very long jpaths. import re import operator __all__ = ['get', 'get_all'] __version__ = '1.6' # Make it work both in python 2.x and 3.x try: basestring except NameError: basestring = str def _pop_token(tokens): try: token = tokens.pop() except IndexError: token = NoneToken() return token class Tokenizer: # list of all token classes marked with @Tokenizer.token tokens = {} def __init__(self): self.tokens_re = re.compile(r'|'.join( r'(?P<{0}>{1})'.format(token.name, token.regexp) for token in sorted(self.tokens.values(), key=operator.attrgetter("priority")) )) @classmethod def token(cls, token_class): """Marks a token class to be used in tokenizer.""" cls.tokens[token_class.name] = token_class return cls def tokenize(self, jpath): """ Breaks a string into tokens. >>> [(t.name, t.value) for t in TOKENIZER.tokenize("ziew")] [('fragment', 'ziew')] >>> [(t.name, t.value) for t in TOKENIZER.tokenize("ziew.yawn")] [('fragment', 'ziew'), ('fragment', 'yawn')] >>> [(t.name, t.value) for t in TOKENIZER.tokenize('ziew["yawn"]')] [('fragment', 'ziew'), ('element', 'yawn')] >>> [(t.name, t.value) for t in TOKENIZER.tokenize('ziew[0]')] [('fragment', 'ziew'), ('index', 0)] >>> [(t.name, t.value) for t in TOKENIZER.tokenize("ziew.yawn+")] [('fragment', 'ziew'), ('fragment', 'yawn'), ('error', '+')] """ for match in self.tokens_re.finditer(jpath): name = match.lastgroup if match.group(name): params = dict(p for p in match.groupdict().items() if p[1] is not None) yield self.tokens[name](**params) class Token: """Base class for tokens.""" @staticmethod def apply(tokens, data): token = _pop_token(tokens) for value in token.parse(tokens, data): yield value def parse(self, tokens, data): """ Default parsing method for most tokens: parse the subtree under token's value in data. """ try: value = data[self.value] except (IndexError, KeyError): return [] return Token.apply(tokens, value) @Tokenizer.token class FragmentToken(Token): """foo.bar.baz""" regexp = r'(^|(?<=\.\.)|\.)(?P<fragment_name>[a-zA-Z0-9_]+)' name = 'fragment' priority = 50 def __init__(self, fragment, fragment_name): self.value = fragment_name @Tokenizer.token class ElementToken(Token): """["foo"]["bar"]["baz"], with quote escaping""" regexp = r'\["(?P<element_name>([^"]|\\")*)"\]' name = 'element' priority = 50 def __init__(self, element, element_name): """ >>> ElementToken(r'["ziew"]', r'ziew').value 'ziew' >>> ElementToken('[r'["\"quotes\""]', r'\"quotes\"').value '"quotes"' """ # Un-escape escaped double quotes. self.value = element_name.replace(r'\"', '"') @Tokenizer.token class IndexToken(Token): """[0][1][-1]""" regexp = r'\[(?P<index_value>-?[0-9]+)\]' name = 'index' priority = 50 def __init__(self, index, index_value): self.value = int(index_value) @Tokenizer.token class SliceToken(Token): """[0:-1:2], may return multiple results""" regexp = (r'\[(?P<slice_start>-?[0-9]+)?:(?P<slice_end>-?[0-9]+)?' r'(:(?P<slice_step>-?[0-9]+))?\]') name = '_slice' priority = 50 def __init__(self, _slice, slice_start=None, slice_end=None, slice_step=None): self.start = int(slice_start) if slice_start is not None else None self.end = int(slice_end) if slice_end is not None else None self.step = int(slice_step) if slice_step is not None else None def parse(self, tokens, data): return Token.apply(tokens, data[self.start:self.end:self.step]) @Tokenizer.token class FilterToken(Token): """(foo=3)""" regexp = (r'[(](?P<filter_path>[^(<=>)!]+)' r'\s*(?P<filter_operator>[<=>]|>=|<=|!=|==)\s*' r'(?P<filter_value>"([^"]|\\")*"|-?[0-9]|true|false)[)]') name = '_filter' priority = 50 op = { '=': operator.eq, '>': operator.gt, '<': operator.lt, '!=': operator.ne, '>=': operator.ge, '<=': operator.le, '==': operator.eq } def __init__(self, _filter, filter_path, filter_operator, filter_value): """ >>> FilterToken(r'(foo="b\"ar")', r'foo', r'=', r'"b\"ar"').value 'b"ar' >>> FilterToken(r'(bar=-1)', r'bar', r'=', r'-1').value -1 """ self.path = filter_path if filter_value.startswith('"'): self.value = filter_value[1:-1].replace(r'\"', '"') elif filter_value in ('true', 'false'): self.value = filter_value == 'true' else: self.value = int(filter_value) self.operator = filter_operator def parse(self, tokens, data): token = _pop_token(tokens) try: # If it's a dict, iterate over values, not keys. data = data.values() except AttributeError: pass for item in data: try: item_value = get(self.path, item) except (TypeError, IndexError, KeyError): # Skip the branches that don't have the value we look for. continue if self.op[self.operator](item_value, self.value): for value in token.parse(tokens, item): yield value @Tokenizer.token class WildcardToken(Token): """*[*].*, may return multiple results""" regexp = r'(^|(?<=\.\.)|\.)\*|\[\*\]' name = 'wildcard' priority = 50 def __init__(self, wildcard): self.value = None def parse(self, tokens, data): token = _pop_token(tokens) try: # If it's a dict, iterate over values, not keys. data = data.values() except AttributeError: pass for item in data: try: for value in token.parse(list(tokens), item): yield value except (TypeError, IndexError, KeyError): # Ignore errors inside branches, because some # of the branches may have different structure # than our jpath wants. pass @Tokenizer.token class EllipsisToken(Token): """.., may return multiple results""" regexp = r'\.\.' name = 'ellipsis' priority = 60 def __init__(self, ellipsis): self.value = None def parse(self, tokens, data): def dive(data): yield data stack = [data] while stack: data = stack.pop() try: # If it's a dict, iterate over values, not keys. data = data.values() except AttributeError: pass orig_data = data try: # Skip non-iterable datatypes. data = iter(data) except TypeError: continue for item in data: if isinstance(item, basestring): # Strings are nasty, because they can be iterated # recursively idefinitely. continue if item is orig_data: # We are iterating over something that behaves like # the strings, refuse to descend. break stack.append(item) yield item token = _pop_token(tokens) for item in dive(data): try: for value in token.parse(list(tokens), item): yield value except (TypeError, IndexError, KeyError): # Ignore errors inside branches, because some # of the branches may have different structure # than our jpath wants. pass @Tokenizer.token class ErrorToken(Token): """Catchall for everything that didn't match other tokens.""" regexp = r'.*' name = 'error' priority = 100 def __init__(self, error): self.value = error def parse(self, tokens, data): raise SyntaxError("Unexpexted '{0}' in jpath.".format(self.value)) yield None class NoneToken(Token): """Special token for marking lack of tokens.""" def parse(self, tokens, data): yield data # We only ever need one tokenizer TOKENIZER = Tokenizer() def get_all(jpath, data): """ Get list of all elements matched by given jpath in the data datastructure. >>> get_all('', 1) [1] >>> get_all('[1]', [1, 2]) [2] >>> get_all('[*]', [1, 2]) [1, 2] >>> get_all('*', [1, 2]) [1, 2] >>> get_all('*[0]', [1, [2, 4], 3]) [2] >>> get_all('*[0]', [1, [2, 4], 3, []]) [2] >>> get_all('*.ziew', [1, {'ziew':2}, [3], []]) [2] >>> get_all('..ziew', [1, 'ziew', {'ziew': 3, 'yawn': [{'ziew': 4}]}]) [3, 4] >>> get_all('a.b', {'a': {'b': 3, 'c': {'b': 4}}}) [3] >>> get_all('a..b', {'a': {'b': 3, 'c': {'b': 4}}}) [3, 4] >>> get_all('[1:]', [1, 2, 3]) [[2, 3]] >>> get_all('[:-1]', [1, 2, 3]) [[1, 2]] >>> get_all('[::2]', [1, 2, 3, 4]) [[1, 3]] >>> get_all('[::2][0]', [[1, 2], [3, 4], [5, 6]]) [[1, 2]] >>> get_all('(x=1).y', [{'x':1, 'y':2}, {'x':0, 'y':3}, {'x':1, 'y':4}]) [2, 4] >>> get_all('(x>0).y', [{'x':1, 'y':2}, {'x':0, 'y':3}, {'x':1, 'y':4}]) [2, 4] >>> get_all('..', {'a':1, 'x':[2], 'c':[{'b': '3'}]}) [{'a': 1, 'x': [2], 'c': [{'b': '3'}]}, 1, [2], [{'b': '3'}], {'b': '3'}, 2] >>> get_all('a[*].b.c', {'a': [{'b': {'c': 0}}, {'b': {'c': 1}}]}) [0, 1] >>> get_all('a', {}) [] """ tokens = list(TOKENIZER.tokenize(jpath)) # Popping from the end of the list is faster. tokens.reverse() return list(Token.apply(tokens, data)) def get(jpath, data): """ Get the first element matched by given jpath in the data datastructure. >>> get('', 3) 3 >>> get('ziew', {"ziew": 3}) 3 >>> get('ziew.yawn', {"ziew": {"yawn": 3}}) 3 >>> get('ziew["yawn"]', {"ziew": {"yawn": 3}}) 3 >>> get('ziew[2]', {"ziew": [1, 2, 3]}) 3 >>> get('[2]', [1, 2, 3]) 3 >>> get('[*]', [1, 2, 3]) 1 """ return get_all(jpath, data)[0] if __name__ == "__main__": # Run tests when started. import doctest doctest.testmod()
