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()