Mercurial > jpath
changeset 0:f39bd05f3be9
initial commit
| author | sheep@wintermute |
|---|---|
| date | Wed, 14 Apr 2010 10:04:35 +0200 |
| parents | |
| children | 1e04d0ab763b |
| files | .hgignore jpath.py |
| diffstat | 2 files changed, 224 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.hgignore Wed Apr 14 10:04:35 2010 +0200 @@ -0,0 +1,9 @@ +syntax: glob + +.* +*~ +*.pyc +*.pyo +*.swp +build +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/jpath.py Wed Apr 14 10:04:35 2010 +0200 @@ -0,0 +1,215 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Access nested dicts and lists using JSON-like path notation. +""" + +# 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 itertools +import operator + + +class Token: + """Base class for tokens.""" + + # list of all token classes marked with @Token.use + tokens = {} + + @classmethod + def use(cls, token_class): + """Marks a token class to be used in tokenizer.""" + + cls.tokens[token_class.name] = token_class + + @classmethod + def tokenize(cls, jpath): + """ + Breaks a string into tokens. + + >>> [(t.name, t.value) for t in Token.tokenize("ziew")] + [('fragment', 'ziew')] + >>> [(t.name, t.value) for t in Token.tokenize("ziew.yawn")] + [('fragment', 'ziew'), ('fragment', 'yawn')] + >>> [(t.name, t.value) for t in Token.tokenize('ziew["yawn"]')] + [('fragment', 'ziew'), ('element', 'yawn')] + >>> [(t.name, t.value) for t in Token.tokenize('ziew[0]')] + [('fragment', 'ziew'), ('index', 0)] + >>> [(t.name, t.value) for t in Token.tokenize("ziew.yawn+")] + [('fragment', 'ziew'), ('fragment', 'yawn'), ('error', '+')] + """ + + tokens_re = re.compile(r'|'.join( + r'(?P<{0}>{1})'.format(token.name, token.regexp) + for token in sorted(cls.tokens.values(), + key=operator.attrgetter("priority")) + )) + for match in 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 cls.tokens[name](**params) + + def parse(self, tokens, data): + """ + Default parsing method for most tokens: parse the subtree + under token's value in data. + """ + + if not tokens: + yield data[self.value] + else: + token, *tokens = tokens + for value in token.parse(tokens, data[self.value]): + yield value + + +@Token.use +class FragmentToken(Token): + """foo.bar.baz""" + + regexp = r'(^|\.)(?P<fragment_name>[a-zA-Z0-9_]+)' + name = 'fragment' + priority = 10 + + def __init__(self, fragment, fragment_name): + self.value = fragment_name + + +@Token.use +class ElementToken(Token): + """["foo"]["bar"]["baz"], with quote escaping""" + + regexp = r'\["(?P<element_name>([^"]|\\")*)"\]' + name = 'element' + priority = 10 + + 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'\"', '"') + + +@Token.use +class IndexToken(Token): + """[0][1][-1]""" + + regexp = r'\[(?P<index_value>-?[0-9]+)\]' + name = 'index' + priority = 10 + + def __init__(self, index, index_value): + self.value = int(index_value) + + +@Token.use +class WildcardToken(Token): + """*[*].*, may return multiple results""" + + regexp = r'(^|\.)\*|\[\*\]' + name = 'wildcard' + priority = 10 + + def __init__(self, wildcard): + self.value = None + + def parse(self, tokens, data): + try: + data = data.values() + except AttributeError: + pass + if not tokens: + for item in data: + yield item + else: + token, *tokens = tokens + for item in data: + try: + for value in token.parse(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 + + +@Token.use +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 + + +def get_all(jpath, data): + """ + Gets list of all the values matching 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] + """ + + tokens = list(Token.tokenize(jpath)) + if not tokens: + return [data] + token, *tokens = tokens + return list(token.parse(tokens, data)) + +def get(jpath, data): + """ + Gets 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()
