summaryrefslogtreecommitdiff
diff options
-rw-r--r--LICENSE.txt2
-rw-r--r--PKG-INFO12
-rw-r--r--README.rst7
-rwxr-xr-xbin/jp70
-rw-r--r--debian/changelog6
-rw-r--r--jmespath.egg-info/PKG-INFO12
-rw-r--r--jmespath.egg-info/SOURCES.txt2
-rw-r--r--jmespath.egg-info/requires.txt1
-rw-r--r--jmespath/__init__.py10
-rw-r--r--jmespath/ast.py727
-rw-r--r--jmespath/compat.py33
-rw-r--r--jmespath/exceptions.py106
-rw-r--r--jmespath/lexer.py191
-rw-r--r--jmespath/parser.py605
-rw-r--r--setup.py28
-rw-r--r--tests/__init__.py24
-rw-r--r--tests/test_ast.py351
-rw-r--r--tests/test_compliance.py64
-rw-r--r--tests/test_parser.py74
19 files changed, 1738 insertions, 587 deletions
diff --git a/LICENSE.txt b/LICENSE.txt
index 07d9e8c..aa68928 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,3 +1,5 @@
+Copyright (c) 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved
+
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
diff --git a/PKG-INFO b/PKG-INFO
index bc21be4..7bfe79a 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 1.1
Name: jmespath
-Version: 0.2.1
+Version: 0.4.1
Summary: JSON Matching Expressions
Home-page: https://github.com/boto/jmespath
Author: James Saryerwinnie
@@ -9,7 +9,7 @@ License: UNKNOWN
Description: JMESPath
========
- JMESPath (pronounced "jaymz path") allows you to declaratively specify how to
+ JMESPath (pronounced ``\ˈjāmz path\``) allows you to declaratively specify how to
extract elements from a JSON document.
For example, given this document::
@@ -33,7 +33,7 @@ Description: JMESPath
The expression: ``foo.bar[*].name`` will return ["one", "two"].
Negative indexing is also supported (-1 refers to the last element
in the list). Given the data above, the expression
- ``foo.bar[-1].name`` will return ["two"].
+ ``foo.bar[-1].name`` will return "two".
The ``*`` can also be used for hash types::
@@ -90,11 +90,8 @@ Description: JMESPath
You can also use the ``jmespath.parser.Parser`` class directly
if you want more control.
-
- .. _RFC4234: http://tools.ietf.org/html/rfc4234
-
Platform: UNKNOWN
-Classifier: Development Status :: 3 - Alpha
+Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Natural Language :: English
Classifier: License :: OSI Approved :: MIT License
@@ -102,5 +99,4 @@ Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 2.6
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.2
Classifier: Programming Language :: Python :: 3.3
diff --git a/README.rst b/README.rst
index 73536fd..144b88e 100644
--- a/README.rst
+++ b/README.rst
@@ -1,7 +1,7 @@
JMESPath
========
-JMESPath (pronounced "jaymz path") allows you to declaratively specify how to
+JMESPath (pronounced ``\ˈjāmz path\``) allows you to declaratively specify how to
extract elements from a JSON document.
For example, given this document::
@@ -25,7 +25,7 @@ syntax::
The expression: ``foo.bar[*].name`` will return ["one", "two"].
Negative indexing is also supported (-1 refers to the last element
in the list). Given the data above, the expression
-``foo.bar[-1].name`` will return ["two"].
+``foo.bar[-1].name`` will return "two".
The ``*`` can also be used for hash types::
@@ -81,6 +81,3 @@ and reuse them to perform repeated searches::
You can also use the ``jmespath.parser.Parser`` class directly
if you want more control.
-
-
-.. _RFC4234: http://tools.ietf.org/html/rfc4234
diff --git a/bin/jp b/bin/jp
index f7fecb5..ebc8b86 100755
--- a/bin/jp
+++ b/bin/jp
@@ -1,17 +1,63 @@
#!/usr/bin/env python
-import jmespath
import sys
import json
+import argparse
+
+import jmespath
+from jmespath import exceptions
+from jmespath.compat import OrderedDict
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument('expression')
+ parser.add_argument('-o', '--ordered', action='store_true',
+ help='Preserve the order of hash keys, which '
+ 'are normally unordered.')
+ parser.add_argument('-f', '--filename',
+ help=('The filename containing the input data. '
+ 'If a filename is not given then data is '
+ 'read from stdin.'))
+ parser.add_argument('--ast', action='store_true',
+ help=('Pretty print the AST, do not search the data.'))
+ args = parser.parse_args()
+ expression = args.expression
+ if args.ast:
+ # Only print the AST
+ expression = jmespath.compile(args.expression)
+ sys.stdout.write(str(expression))
+ sys.stdout.write('\n')
+ return 0
+ if args.filename:
+ with open(args.filename, 'r') as f:
+ data = json.load(f)
+ else:
+ data = sys.stdin.read()
+ if args.ordered:
+ data = json.loads(data, object_pairs_hook=OrderedDict)
+ else:
+ data = json.loads(data)
+ try:
+ sys.stdout.write(json.dumps(
+ jmespath.search(expression, data), indent=4))
+ sys.stdout.write('\n')
+ except exceptions.ArityError as e:
+ sys.stderr.write("invalid-arity: %s\n" % e)
+ return 1
+ except exceptions.JMESPathTypeError as e:
+ sys.stderr.write("invalid-type: %s\n" % e)
+ return 1
+ except exceptions.UnknownFunctionError as e:
+ sys.stderr.write("unknown-function: %s\n" % e)
+ return 1
+ except exceptions.LexerError as e:
+ sys.stderr.write("syntax-error: %s\n" % e)
+ return 1
+ except exceptions.ParseError as e:
+ sys.stderr.write("syntax-error: %s\n" % e)
+ return 1
+
-if not len(sys.argv) >= 2:
- sys.stderr.write('usage: jp <expression> <filepath>\n\n')
- sys.exit(1)
-expression = sys.argv[1]
-if len(sys.argv) == 3:
- with open(sys.argv[2], 'r') as f:
- data = json.load(f)
-else:
- data = json.load(sys.stdin)
-sys.stdout.write(json.dumps(jmespath.search(expression, data), indent=4))
-sys.stdout.write('\n')
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/debian/changelog b/debian/changelog
index d8d7924..8c87835 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+python-jmespath (0.4.1-1) unstable; urgency=medium
+
+ * New upstream release
+
+ -- TANIGUCHI Takaki <takaki@debian.org> Mon, 23 Jun 2014 00:35:03 +0900
+
python-jmespath (0.2.1-1) unstable; urgency=low
* Initial release (Closes: #734131)
diff --git a/jmespath.egg-info/PKG-INFO b/jmespath.egg-info/PKG-INFO
index bc21be4..7bfe79a 100644
--- a/jmespath.egg-info/PKG-INFO
+++ b/jmespath.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 1.1
Name: jmespath
-Version: 0.2.1
+Version: 0.4.1
Summary: JSON Matching Expressions
Home-page: https://github.com/boto/jmespath
Author: James Saryerwinnie
@@ -9,7 +9,7 @@ License: UNKNOWN
Description: JMESPath
========
- JMESPath (pronounced "jaymz path") allows you to declaratively specify how to
+ JMESPath (pronounced ``\ˈjāmz path\``) allows you to declaratively specify how to
extract elements from a JSON document.
For example, given this document::
@@ -33,7 +33,7 @@ Description: JMESPath
The expression: ``foo.bar[*].name`` will return ["one", "two"].
Negative indexing is also supported (-1 refers to the last element
in the list). Given the data above, the expression
- ``foo.bar[-1].name`` will return ["two"].
+ ``foo.bar[-1].name`` will return "two".
The ``*`` can also be used for hash types::
@@ -90,11 +90,8 @@ Description: JMESPath
You can also use the ``jmespath.parser.Parser`` class directly
if you want more control.
-
- .. _RFC4234: http://tools.ietf.org/html/rfc4234
-
Platform: UNKNOWN
-Classifier: Development Status :: 3 - Alpha
+Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Natural Language :: English
Classifier: License :: OSI Approved :: MIT License
@@ -102,5 +99,4 @@ Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 2.6
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.2
Classifier: Programming Language :: Python :: 3.3
diff --git a/jmespath.egg-info/SOURCES.txt b/jmespath.egg-info/SOURCES.txt
index 37b19d4..b32cbc1 100644
--- a/jmespath.egg-info/SOURCES.txt
+++ b/jmespath.egg-info/SOURCES.txt
@@ -6,12 +6,12 @@ bin/jp
jmespath/__init__.py
jmespath/ast.py
jmespath/compat.py
+jmespath/exceptions.py
jmespath/lexer.py
jmespath/parser.py
jmespath.egg-info/PKG-INFO
jmespath.egg-info/SOURCES.txt
jmespath.egg-info/dependency_links.txt
-jmespath.egg-info/requires.txt
jmespath.egg-info/top_level.txt
tests/__init__.py
tests/test_ast.py
diff --git a/jmespath.egg-info/requires.txt b/jmespath.egg-info/requires.txt
deleted file mode 100644
index 7163743..0000000
--- a/jmespath.egg-info/requires.txt
+++ /dev/null
@@ -1 +0,0 @@
-ply==3.4 \ No newline at end of file
diff --git a/jmespath/__init__.py b/jmespath/__init__.py
index a0d870a..9847342 100644
--- a/jmespath/__init__.py
+++ b/jmespath/__init__.py
@@ -1,11 +1,11 @@
from jmespath import parser
-__version__ = '0.2.1'
+__version__ = '0.4.1'
-def compile(expression, debug=False):
- return parser.Parser(debug=debug).parse(expression)
+def compile(expression):
+ return parser.Parser().parse(expression)
-def search(expression, data, debug=False):
- return parser.Parser(debug=debug).parse(expression).search(data)
+def search(expression, data):
+ return parser.Parser().parse(expression).search(data)
diff --git a/jmespath/ast.py b/jmespath/ast.py
index 3cafe83..64d4ed3 100644
--- a/jmespath/ast.py
+++ b/jmespath/ast.py
@@ -1,29 +1,74 @@
+import operator
+import math
+import json
+
+from jmespath.compat import with_repr_method
+from jmespath.compat import string_type as STRING_TYPE
+from jmespath.compat import zip_longest
+from jmespath.exceptions import JMESPathTypeError, UnknownFunctionError
+
+
+NUMBER_TYPE = (float, int)
+_VARIADIC = object()
+
+
+# python types -> jmespath types
+TYPES_MAP = {
+ 'bool': 'boolean',
+ 'list': 'array',
+ 'dict': 'object',
+ 'NoneType': 'null',
+ 'unicode': 'string',
+ 'str': 'string',
+ 'float': 'number',
+ 'int': 'number',
+ 'OrderedDict': 'object',
+ '_Projection': 'array',
+ '_Expression': 'expref',
+}
+
+
+# jmespath types -> python types
+REVERSE_TYPES_MAP = {
+ 'boolean': ('bool',),
+ 'array': ('list', '_Projection'),
+ 'object': ('dict', 'OrderedDict',),
+ 'null': ('None',),
+ 'string': ('unicode', 'str'),
+ 'number': ('float', 'int'),
+ 'expref': ('_Expression',),
+}
+
+
+class _Arg(object):
+ __slots__ = ('types',)
+
+ def __init__(self, types=None):
+ self.types = types
+
+
+@with_repr_method
class AST(object):
- VALUE_METHODS = []
+
+ def __init__(self):
+ self.children = []
def search(self, value):
pass
- def _get_value_method(self, value):
- # This will find the appropriate getter method
- # based on the passed in value.
- for method_name in self.VALUE_METHODS:
- method = getattr(value, method_name, None)
- if method is not None:
- return method
-
def pretty_print(self, indent=''):
return super(AST, self).__repr__()
def __repr__(self):
return self.pretty_print()
- def __eq__(self, other):
- return (isinstance(other, self.__class__)
- and self.__dict__ == other.__dict__)
- def __ne__(self, other):
- return not self.__eq__(other)
+class Identity(AST):
+ def search(self, value):
+ return value
+
+ def pretty_print(self, indent=''):
+ return "%sIdentity()" % indent
class SubExpression(AST):
@@ -36,72 +81,78 @@ class SubExpression(AST):
"""
def __init__(self, parent, child):
- self.parent = parent
- self.child = child
+ self.children = [parent, child]
def search(self, value):
# To evaluate a subexpression we first evaluate the parent object
# and then feed the match of the parent node into the child node.
- sub_value = self.parent.search(value)
- found = self.child.search(sub_value)
+ sub_value = self.children[0].search(value)
+ found = self.children[1].search(sub_value)
return found
def pretty_print(self, indent=''):
sub_indent = indent + ' ' * 4
- return "%sSubExpression(\n%s%s,\n%s%s)" % (
- indent,
- sub_indent, self.parent.pretty_print(sub_indent),
- sub_indent, self.child.pretty_print(sub_indent))
+ return "%s%s(\n%s%s,\n%s%s)" % (
+ indent, self.__class__.__name__,
+ sub_indent, self.children[0].pretty_print(sub_indent),
+ sub_indent, self.children[1].pretty_print(sub_indent))
+
+
+# This is used just to differentiate between
+# subexpressions and indexexpressions (wildcards can hang
+# off of an indexexpression).
+class IndexExpression(SubExpression):
+ pass
class Field(AST):
- VALUE_METHODS = ['get']
def __init__(self, name):
self.name = name
+ self.children = []
def pretty_print(self, indent=''):
return "%sField(%s)" % (indent, self.name)
def search(self, value):
- method = self._get_value_method(value)
- if method is not None:
- return method(self.name)
+ if value is not None:
+ try:
+ return value.get(self.name)
+ except AttributeError:
+ return None
class BaseMultiField(AST):
def __init__(self, nodes):
- self.nodes = nodes
+ self.children = list(nodes)
def search(self, value):
if value is None:
return None
- method = self._get_value_method(value)
- if method is not None:
- return method(self.nodes)
- else:
- return self._multi_get(value)
+ return self._multi_get(value)
+
+ def _multi_get(self, value):
+ # Subclasses must define this method.
+ raise NotImplementedError("_multi_get")
def pretty_print(self, indent=''):
- return "%s%s(%s)" % (indent, self.__class__.__name__, self.nodes)
+ return "%s%s(%s)" % (indent, self.__class__.__name__, self.children)
class MultiFieldDict(BaseMultiField):
- VALUE_METHODS = ['multi_get']
def _multi_get(self, value):
collected = {}
- for node in self.nodes:
+ for node in self.children:
collected[node.key_name] = node.search(value)
return collected
class MultiFieldList(BaseMultiField):
- VALUE_METHODS = ['multi_get_list']
def _multi_get(self, value):
collected = []
- for node in self.nodes:
+ for node in self.children:
collected.append(node.search(value))
return collected
@@ -109,20 +160,20 @@ class MultiFieldList(BaseMultiField):
class KeyValPair(AST):
def __init__(self, key_name, node):
self.key_name = key_name
- self.node = node
+ self.children = [node]
def search(self, value):
- return self.node.search(value)
+ return self.children[0].search(value)
def pretty_print(self, indent=''):
- return "%sKeyValPair(key_name=%s, node=%s)" % (indent, self.key_name,
- self.node)
+ return "%sKeyValPair(key_name=%s, node=%s)" % (
+ indent, self.key_name, self.children[0])
class Index(AST):
- VALUE_METHODS = ['get_index', '__getitem__']
def __init__(self, index):
+ super(Index, self).__init__()
self.index = index
def pretty_print(self, indent=''):
@@ -133,142 +184,528 @@ class Index(AST):
# want to support that.
if not isinstance(value, list):
return None
- method = self._get_value_method(value)
- if method is not None:
- try:
- return method(self.index)
- except IndexError:
- pass
+ try:
+ return value[self.index]
+ except IndexError:
+ return None
-class WildcardIndex(AST):
- """Represents a wildcard index.
+class ORExpression(AST):
+ def __init__(self, first, remaining):
+ self.children = [first, remaining]
- For example::
+ def search(self, value):
+ matched = self.children[0].search(value)
+ if self._is_false(matched):
+ matched = self.children[1].search(value)
+ return matched
- foo[*] -> SubExpression(Field(foo), WildcardIndex())
+ def _is_false(self, value):
+ # This looks weird, but we're explicitly using equality checks
+ # because the truth/false values are different between
+ # python and jmespath.
+ return (value == '' or value == [] or value == {} or value is None or
+ value == False)
+
+ def pretty_print(self, indent=''):
+ return "%sORExpression(%s, %s)" % (indent, self.children[0],
+ self.children[1])
+
+
+class FilterExpression(AST):
+
+ def __init__(self, expression):
+ self.children = [expression]
- """
def search(self, value):
if not isinstance(value, list):
return None
- return _Projection(value)
+ result = []
+ for element in value:
+ if self.children[0].search(element):
+ result.append(element)
+ return result
def pretty_print(self, indent=''):
- return "%sIndex(*)" % indent
+ return '%sFilterExpression(%s)' % (indent, self.children[0])
-class WildcardValues(AST):
- """Represents a wildcard on the values of a JSON object.
+class Literal(AST):
- For example::
+ def __init__(self, literal_value):
+ super(Literal, self).__init__()
+ self.literal_value = literal_value
- foo.* -> SubExpression(Field(foo), WildcardValues())
+ def search(self, value):
+ return self.literal_value
- """
+ def pretty_print(self, indent=''):
+ return '%sLiteral(%s)' % (indent, self.literal_value)
+
+
+class Comparator(AST):
+ # Subclasses must define the operation function.
+ operation = None
+
+ def __init__(self, first, second):
+ self.children = [first, second]
+
+ def search(self, data):
+ return self.operation(self.children[0].search(data),
+ self.children[1].search(data))
+
+ def pretty_print(self, indent=''):
+ return '%s%s(%s, %s)' % (indent, self.__class__.__name__,
+ self.children[0], self.children[1])
+
+
+class OPEquals(Comparator):
+ def _equals(self, first, second):
+ if self._is_special_integer_case(first, second):
+ return False
+ else:
+ return first == second
+
+ def _is_special_integer_case(self, first, second):
+ # We need to special case comparing 0 or 1 to
+ # True/False. While normally comparing any
+ # integer other than 0/1 to True/False will always
+ # return False. However 0/1 have this:
+ # >>> 0 == True
+ # False
+ # >>> 0 == False
+ # True
+ # >>> 1 == True
+ # True
+ # >>> 1 == False
+ # False
+ #
+ # Also need to consider that:
+ # >>> 0 in [True, False]
+ # True
+ if first is 0 or first is 1:
+ return second is True or second is False
+ elif second is 0 or second is 1:
+ return first is True or first is False
+
+ operation = _equals
+
+
+class OPNotEquals(OPEquals):
+ def _not_equals(self, first, second):
+ return not super(OPNotEquals, self)._equals(first, second)
+
+ operation = _not_equals
+
+
+class OPLessThan(Comparator):
+ operation = operator.lt
+
+
+class OPLessThanEquals(Comparator):
+ operation = operator.le
+
+
+class OPGreaterThan(Comparator):
+ operation = operator.gt
+
+
+class OPGreaterThanEquals(Comparator):
+ operation = operator.ge
+
+
+class CurrentNode(AST):
def search(self, value):
+ return value
+
+
+class FunctionExpression(AST):
+
+ def __init__(self, name, args):
+ self.name = name
+ # The .children attribute is to support homogeneous
+ # children nodes, but .args is a better name for all the
+ # code that uses the children, so we support both.
+ self.children = args
+ self.args = args
try:
- return _Projection(value.values())
+ self.function = getattr(self, '_func_%s' % name)
except AttributeError:
+ raise UnknownFunctionError("Unknown function: %s" % self.name)
+ self.arity = self.function.arity
+ self.variadic = self.function.variadic
+ self.function = self._resolve_arguments_wrapper(self.function)
+
+ def pretty_print(self, indent=''):
+ return "%sFunctionExpression(name=%s, args=%s)" % (
+ indent, self.name, self.args)
+
+ def search(self, value):
+ return self.function(value)
+
+ def _resolve_arguments_wrapper(self, function):
+ def _call_with_resolved_args(value):
+ # Before calling the function, we have two things to do:
+ # 1. Resolve the arguments (evaluate the arg expressions
+ # against the passed in input.
+ # 2. Type check the arguments
+ resolved_args = []
+ for arg_expression, arg_spec in zip_longest(
+ self.args, function.argspec,
+ fillvalue=function.argspec[-1]):
+ # 1. Resolve the arguments.
+ current = arg_expression.search(value)
+ # 2. Type check (provided we have type information).
+ if arg_spec.types is not None:
+ _type_check(arg_spec.types, current)
+ resolved_args.append(current)
+ return function(*resolved_args)
+
+ def _get_allowed_pytypes(types):
+ allowed_types = []
+ allowed_subtypes = []
+ for t in types:
+ type_ = t.split('-', 1)
+ if len(type_) == 2:
+ type_, subtype = type_
+ allowed_subtypes.append(REVERSE_TYPES_MAP[subtype])
+ else:
+ type_ = type_[0]
+ allowed_types.extend(REVERSE_TYPES_MAP[type_])
+ return allowed_types, allowed_subtypes
+
+ def _type_check(types, current):
+ # Type checking involves checking the top level type,
+ # and in the case of arrays, potentially checking the types
+ # of each element.
+ allowed_types, allowed_subtypes = _get_allowed_pytypes(types)
+ # We're not using isinstance() on purpose.
+ # The type model for jmespath does not map
+ # 1-1 with python types (booleans are considered
+ # integers in python for example).
+ actual_typename = type(current).__name__
+ if actual_typename not in allowed_types:
+ raise JMESPathTypeError(self.name, current,
+ TYPES_MAP.get(actual_typename,
+ 'unknown'),
+ types)
+ # If we're dealing with a list type, we can have
+ # additional restrictions on the type of the list
+ # elements (for example a function can require a
+ # list of numbers or a list of strings).
+ # Arrays are the only types that can have subtypes.
+ if allowed_subtypes:
+ _subtype_check(current, allowed_subtypes, types)
+
+ def _subtype_check(current, allowed_subtypes, types):
+ if len(allowed_subtypes) == 1:
+ # The easy case, we know up front what type
+ # we need to validate.
+ allowed_subtypes = allowed_subtypes[0]
+ for element in current:
+ actual_typename = type(element).__name__
+ if actual_typename not in allowed_subtypes:
+ raise JMESPathTypeError(self.name, element,
+ actual_typename,
+ types)
+ elif len(allowed_subtypes) > 1 and current:
+ # Dynamic type validation. Based on the first
+ # type we see, we validate that the remaining types
+ # match.
+ first = type(current[0]).__name__
+ for subtypes in allowed_subtypes:
+ if first in subtypes:
+ allowed = subtypes
+ break
+ else:
+ raise JMESPathTypeError(self.name, current[0],
+ first, types)
+ for element in current:
+ actual_typename = type(element).__name__
+ if actual_typename not in allowed:
+ raise JMESPathTypeError(self.name, element,
+ actual_typename,
+ types)
+
+ return _call_with_resolved_args
+
+ def signature(*arguments, **kwargs):
+ def _record_arity(func):
+ func.arity = len(arguments)
+ func.variadic = kwargs.get('variadic', False)
+ func.argspec = arguments
+ return func
+ return _record_arity
+
+ @signature(_Arg(), variadic=True)
+ def _func_not_null(self, *arguments):
+ for argument in arguments:
+ if argument is not None:
+ return argument
+
+ @signature(_Arg(types=['number']))
+ def _func_abs(self, arg):
+ return abs(arg)
+
+ @signature(_Arg(types=['array-number']))
+ def _func_avg(self, arg):
+ return sum(arg) / float(len(arg))
+
+ @signature(_Arg())
+ def _func_to_string(self, arg):
+ if isinstance(arg, STRING_TYPE):
+ return arg
+ else:
+ return json.dumps(arg)
+
+ @signature(_Arg())
+ def _func_to_number(self, arg):
+ if isinstance(arg, (list, dict, bool)):
+ return None
+ elif arg is None:
+ return None
+ elif isinstance(arg, (int, float)):
+ return arg
+ else:
+ try:
+ if '.' in arg:
+ return float(arg)
+ else:
+ return int(arg)
+ except ValueError:
+ return None
+
+ @signature(_Arg(types=['array', 'string']), _Arg())
+ def _func_contains(self, subject, search):
+ return search in subject
+
+ @signature(_Arg(types=['string', 'array', 'object']))
+ def _func_length(self, arg):
+ return len(arg)
+
+ @signature(_Arg(types=['number']))
+ def _func_ceil(self, arg):
+ return math.ceil(arg)
+
+ @signature(_Arg(types=['number']))
+ def _func_floor(self, arg):
+ return math.floor(arg)
+
+ @signature(_Arg(types=['string']), _Arg(types=['array-string']))
+ def _func_join(self, separator, array):
+ return separator.join(array)
+
+ @signature(_Arg(types=['array-number']))
+ def _func_max(self, arg):
+ if arg:
+ return max(arg)
+ else:
+ return None
+
+ @signature(_Arg(types=['array-number']))
+ def _func_min(self, arg):
+ if arg:
+ return min(arg)
+ else:
return None
+ @signature(_Arg(types=['array-string', 'array-number']))
+ def _func_sort(self, arg):
+ return list(sorted(arg))
+
+ @signature(_Arg(types=['array-number']))
+ def _func_sum(self, arg):
+ return sum(arg)
+
+ @signature(_Arg(types=['object']))
+ def _func_keys(self, arg):
+ # To be consistent with .values()
+ # should we also return the indices of a list?
+ return list(arg.keys())
+
+ @signature(_Arg(types=['object']))
+ def _func_values(self, arg):
+ return list(arg.values())
+
+ @signature(_Arg())
+ def _func_type(self, arg):
+ if isinstance(arg, STRING_TYPE):
+ return "string"
+ elif isinstance(arg, bool):
+ return "boolean"
+ elif isinstance(arg, list):
+ return "array"
+ elif isinstance(arg, dict):
+ return "object"
+ elif isinstance(arg, (float, int)):
+ return "number"
+ elif arg is None:
+ return "null"
+
+ def _create_key_func(self, expression, allowed_types):
+ py_types = []
+ for type_ in allowed_types:
+ py_types.extend(REVERSE_TYPES_MAP[type_])
+ def keyfunc(x):
+ result = expression.search(x)
+ type_name = type(result).__name__
+ if type_name not in py_types:
+ raise JMESPathTypeError(self.name,
+ result,
+ type_name,
+ allowed_types)
+ return result
+ return keyfunc
+
+ @signature(_Arg(types=['array']), _Arg(types=['expref']))
+ def _func_sort_by(self, array, expref):
+ # sort_by allows for the expref to be either a number of
+ # a string, so we have some special logic to handle this.
+ # We evaluate the first array element and verify that it's
+ # either a string of a number. We then create a key function
+ # that validates that type, which requires that remaining array
+ # elements resolve to the same type as the first element.
+ if not array:
+ return array
+ required_type = TYPES_MAP.get(
+ type(expref.search(array[0])).__name__)
+ if required_type not in ['number', 'string']:
+ raise JMESPathTypeError(self.name,
+ array[0],
+ required_type,
+ ['string', 'number'])
+ keyfunc = self._create_key_func(expref, [required_type])
+ return list(sorted(array, key=keyfunc))
+
+ @signature(_Arg(types=['array']), _Arg(types=['expref']))
+ def _func_max_by(self, array, expref):
+ keyfunc = self._create_key_func(expref, ['number'])
+ return max(array, key=keyfunc)
+
+ @signature(_Arg(types=['array']), _Arg(types=['expref']))
+ def _func_min_by(self, array, expref):
+ keyfunc = self._create_key_func(expref, ['number'])
+ return min(array, key=keyfunc)
+
+
+class ExpressionReference(AST):
+ def __init__(self, expression):
+ self.children = [expression]
+
+ def search(self, value):
+ return _Expression(self.children[0])
+
+
+class _Expression(AST):
+ def __init__(self, expression):
+ self.expression = expression
+
+ def search(self, value):
+ return self.expression.search(value)
+
+
+class Pipe(AST):
+ def __init__(self, parent, child):
+ self.children = [parent, child]
+
+ def search(self, value):
+ left = self.children[0].search(value)
+ return self.children[1].search(left)
+
def pretty_print(self, indent=''):
- return "%sWildcardValues()" % indent
+ sub_indent = indent + ' ' * 4
+ return "%s%s(\n%s%s,\n%s%s)" % (
+ indent, self.__class__.__name__,
+ sub_indent, self.children[0].pretty_print(sub_indent),
+ sub_indent, self.children[1].pretty_print(sub_indent))
+
+class Projection(AST):
+ def __init__(self, left, right):
+ self.children = [left, right]
-class ListElements(AST):
def search(self, value):
- if isinstance(value, list):
- # reduce inner list elements into
- # a single list.
- merged_list = []
- for element in value:
- if isinstance(element, list):
- merged_list.extend(element)
- else:
- merged_list.append(element)
- return _Projection(merged_list)
+ base = self._evaluate_left_child(value)
+ if base is None:
+ return None
else:
+ collected = self._evaluate_right_child(base)
+ return collected
+
+ def _evaluate_left_child(self, value):
+ base = self.children[0].search(value)
+ if isinstance(base, list):
+ return base
+ else:
+ # Invalid type, so we return None.
return None
+ def _evaluate_right_child(self, value):
+ collected = []
+ for element in value:
+ current = self.children[1].search(element)
+ if current is not None:
+ collected.append(current)
+ return collected
+
def pretty_print(self, indent=''):
- return "%sListElements()" % indent
+ sub_indent = indent + ' ' * 4
+ return "%s%s(\n%s%s,\n%s%s)" % (
+ indent, self.__class__.__name__,
+ sub_indent, self.children[0].pretty_print(sub_indent),
+ sub_indent, self.children[1].pretty_print(sub_indent))
-class _Projection(list):
- def __init__(self, elements):
- self.extend(elements)
+class ValueProjection(Projection):
+ def _evaluate_left_child(self, value):
+ base_hash = self.children[0].search(value)
+ try:
+ return base_hash.values()
+ except AttributeError:
+ return None
- def get(self, value):
- results = self.__class__([])
- for element in self:
- try:
- result = element.get(value)
- except AttributeError:
- continue
- if result is not None:
- if isinstance(result, list):
- result = self.__class__(result)
- results.append(result)
- return results
-
- def get_index(self, index):
- matches = []
- for el in self:
- if not isinstance(el, list):
- continue
- try:
- matches.append(el[index])
- except (IndexError, TypeError):
- pass
- return self.__class__(matches)
-
- def multi_get(self, nodes):
- results = self.__class__([])
- for element in self:
- if isinstance(element, self.__class__):
- result = element.multi_get(nodes)
- else:
- result = {}
- for node in nodes:
- result[node.key_name] = node.search(element)
- results.append(result)
- return results
-
- def multi_get_list(self, nodes):
- results = self.__class__([])
- for element in self:
- if isinstance(element, self.__class__):
- result = element.multi_get_list(nodes)
- else:
- result = []
- for node in nodes:
- result.append(node.search(element))
- results.append(result)
- return results
-
- def values(self):
- results = self.__class__([])
- for element in self:
- try:
- current = self.__class__(element.values())
- results.append(current)
- except AttributeError:
- continue
- return results
+class FilterProjection(Projection):
+ # A filter projection is a left projection that
+ # filter elements against an expression before allowing
+ # them to be right evaluated.
+ def __init__(self, left, right, comparator):
+ self.children = [left, right, comparator]
-class ORExpression(AST):
- def __init__(self, first, remaining):
- self.first = first
- self.remaining = remaining
+ def _evaluate_right_child(self, value):
+ result = []
+ for element in value:
+ if self.children[2].search(element):
+ result.append(element)
+ return super(FilterProjection, self)._evaluate_right_child(result)
- def search(self, value):
- matched = self.first.search(value)
- if matched is None:
- matched = self.remaining.search(value)
- return matched
+ def pretty_print(self, indent=''):
+ sub_indent = indent + ' ' * 4
+ return "%s%s(\n%s%s,\n%s%s,\n%s%s)" % (
+ indent, self.__class__.__name__,
+ sub_indent, self.children[0].pretty_print(sub_indent),
+ sub_indent, self.children[2].pretty_print(sub_indent),
+ sub_indent, self.children[1].pretty_print(sub_indent),
+ )
+
+
+class Flatten(AST):
+ def __init__(self, element):
+ self.children = [element]
def pretty_print(self, indent=''):
- return "%sORExpression(%s, %s)" % (indent, self.first,
- self.remaining)
+ return "%s%s(%s)" % (
+ indent, self.__class__.__name__,
+ self.children[0].pretty_print(indent).lstrip())
+
+ def search(self, value):
+ original = self.children[0].search(value)
+ if not isinstance(original, list):
+ return None
+ merged_list = []
+ for element in original:
+ if isinstance(element, list):
+ merged_list.extend(element)
+ else:
+ merged_list.append(element)
+ return merged_list
diff --git a/jmespath/compat.py b/jmespath/compat.py
index 9f81717..910b652 100644
--- a/jmespath/compat.py
+++ b/jmespath/compat.py
@@ -3,6 +3,11 @@ import sys
PY2 = sys.version_info[0] == 2
if PY2:
+ text_type = unicode
+ string_type = basestring
+ from itertools import izip_longest as zip_longest
+ LR_TABLE = 'jmespath._lrtable'
+
def with_str_method(cls):
"""Class decorator that handles __str__ compat between py2 and py3."""
# In python2, the __str__ should be __unicode__
@@ -12,7 +17,35 @@ if PY2:
return self.__unicode__().encode('utf-8')
cls.__str__ = __str__
return cls
+ def with_repr_method(cls):
+ """Class decorator that handle __repr__ with py2 and py3."""
+ # This is almost the same thing as with_str_method *except*
+ # it uses the unicode_escape encoding. This also means we need to be
+ # careful encoding the input multiple times, so we only encode
+ # if we get a unicode type.
+ original_repr_method = cls.__repr__
+ def __repr__(self):
+ original_repr = original_repr_method(self)
+ if isinstance(original_repr, text_type):
+ original_repr = original_repr.encode('unicode_escape')
+ return original_repr
+ cls.__repr__ = __repr__
+ return cls
else:
+ LR_TABLE = 'jmespath._lrtable3'
+ text_type = str
+ string_type = str
+ from itertools import zip_longest
def with_str_method(cls):
# In python3, we don't need to do anything, we return a str type.
return cls
+ def with_repr_method(cls):
+ return cls
+
+
+if sys.version_info[:2] == (2, 6):
+ from ordereddict import OrderedDict
+ import simplejson as json
+else:
+ from collections import OrderedDict
+ import json
diff --git a/jmespath/exceptions.py b/jmespath/exceptions.py
new file mode 100644
index 0000000..b8cb678
--- /dev/null
+++ b/jmespath/exceptions.py
@@ -0,0 +1,106 @@
+from jmespath.compat import with_str_method
+
+
+class JMESPathError(ValueError):
+ pass
+
+
+@with_str_method
+class ParseError(JMESPathError):
+ _ERROR_MESSAGE = 'Invalid jmespath expression'
+ def __init__(self, lex_position, token_value, token_type,
+ msg=_ERROR_MESSAGE):
+ super(ParseError, self).__init__(lex_position, token_value, token_type)
+ self.lex_position = lex_position
+ self.token_value = token_value
+ self.token_type = token_type.upper()
+ self.msg = msg
+ # Whatever catches the ParseError can fill in the full expression
+ self.expression = None
+
+ def __str__(self):
+ # self.lex_position +1 to account for the starting double quote char.
+ underline = ' ' * (self.lex_position + 1) + '^'
+ return (
+ '%s: Parse error at column %s near '
+ 'token "%s" (%s) for expression:\n"%s"\n%s' % (
+ self.msg, self.lex_position, self.token_value, self.token_type,
+ self.expression, underline))
+
+
+@with_str_method
+class IncompleteExpressionError(ParseError):
+ def set_expression(self, expression):
+ self.expression = expression
+ self.lex_position = len(expression)
+ self.token_type = None
+ self.token_value = None
+
+ def __str__(self):
+ # self.lex_position +1 to account for the starting double quote char.
+ underline = ' ' * (self.lex_position + 1) + '^'
+ return (
+ 'Invalid jmespath expression: Incomplete expression:\n'
+ '"%s"\n%s' % (self.expression, underline))
+
+
+@with_str_method
+class LexerError(ParseError):
+ def __init__(self, lexer_position, lexer_value, message, expression=None):
+ self.lexer_position = lexer_position
+ self.lexer_value = lexer_value
+ self.message = message
+ super(LexerError, self).__init__(lexer_position,
+ lexer_value,
+ message)
+ # Whatever catches LexerError can set this.
+ self.expression = expression
+
+ def __str__(self):
+ underline = ' ' * self.lexer_position + '^'
+ return 'Bad jmespath expression: %s:\n%s\n%s' % (
+ self.message, self.expression, underline)
+
+
+@with_str_method
+class ArityError(ParseError):
+ def __init__(self, function_node):
+ self.expected_arity = function_node.arity
+ self.actual_arity = len(function_node.args)
+ self.function_name = function_node.name
+ self.expression = None
+
+ def __str__(self):
+ return ("Expected %s arguments for function %s, "
+ "received %s" % (self.expected_arity,
+ self.function_name,
+ self.actual_arity))
+
+
+@with_str_method
+class VariadictArityError(ArityError):
+ def __str__(self):
+ return ("Expected at least %s arguments for function %s, "
+ "received %s" % (self.expected_arity,
+ self.function_name,
+ self.actual_arity))
+
+
+@with_str_method
+class JMESPathTypeError(JMESPathError):
+ def __init__(self, function_name, current_value, actual_type,
+ expected_types):
+ self.function_name = function_name
+ self.current_value = current_value
+ self.actual_type = actual_type
+ self.expected_types = expected_types
+
+ def __str__(self):
+ return ('In function %s(), invalid type for value: %s, '
+ 'expected one of: %s, received: "%s"' % (
+ self.function_name, self.current_value,
+ self.expected_types, self.actual_type))
+
+
+class UnknownFunctionError(JMESPathError):
+ pass
diff --git a/jmespath/lexer.py b/jmespath/lexer.py
index 4a98caf..7e0b003 100644
--- a/jmespath/lexer.py
+++ b/jmespath/lexer.py
@@ -1,75 +1,136 @@
import re
from json import loads
-from jmespath.compat import with_str_method
+from jmespath.exceptions import LexerError
-@with_str_method
-class LexerError(ValueError):
- def __init__(self, lexer_position, lexer_value, message):
- self.lexer_position = lexer_position
- self.lexer_value = lexer_value
- self.message = message
- super(LexerError, self).__init__(lexer_position,
- lexer_value,
- message)
- # Whatever catches LexerError can set this.
- self.expression = None
- def __str__(self):
- underline = ' ' * self.lexer_position + '^'
- return 'Bad jmespath expression: %s:\n%s\n%s' % (
- self.message, self.expression, underline)
+class Lexer(object):
+ TOKENS = (
+ r'(?P<number>-?\d+)|'
+ r'(?P<unquoted_identifier>([a-zA-Z_][a-zA-Z_0-9]*))|'
+ r'(?P<quoted_identifier>("(?:\\\\|\\"|[^"])*"))|'
+ r'(?P<literal>(`(?:\\\\|\\`|[^`])*`))|'
+ r'(?P<filter>\[\?)|'
+ r'(?P<or>\|\|)|'
+ r'(?P<pipe>\|)|'
+ r'(?P<ne>!=)|'
+ r'(?P<rbrace>\})|'
+ r'(?P<eq>==)|'
+ r'(?P<dot>\.)|'
+ r'(?P<star>\*)|'
+ r'(?P<gte>>=)|'
+ r'(?P<lparen>\()|'
+ r'(?P<lbrace>\{)|'
+ r'(?P<lte><=)|'
+ r'(?P<flatten>\[\])|'
+ r'(?P<rbracket>\])|'
+ r'(?P<lbracket>\[)|'
+ r'(?P<rparen>\))|'
+ r'(?P<comma>,)|'
+ r'(?P<colon>:)|'
+ r'(?P<lt><)|'
+ r'(?P<expref>&)|'
+ r'(?P<gt>>)|'
+ r'(?P<current>@)|'
+ r'(?P<skip>[ \t]+)'
+ )
+ def __init__(self):
+ self.master_regex = re.compile(self.TOKENS)
+ def tokenize(self, expression):
+ previous_column = 0
+ for match in self.master_regex.finditer(expression):
+ value = match.group()
+ start = match.start()
+ end = match.end()
+ if match.lastgroup == 'skip':
+ # Ignore whitespace.
+ previous_column = end
+ continue
+ if start != previous_column:
+ bad_value = expression[previous_column:start]
+ # Try to give a good error message.
+ if bad_value == '"':
+ raise LexerError(
+ lexer_position=previous_column,
+ lexer_value=value,
+ message='Starting quote is missing the ending quote',
+ expression=expression)
+ raise LexerError(lexer_position=previous_column,
+ lexer_value=value,
+ message='Unknown character',
+ expression=expression)
+ previous_column = end
+ token_type = match.lastgroup
+ handler = getattr(self, '_token_%s' % token_type.lower(), None)
+ if handler is not None:
+ value = handler(value, start, end)
+ yield {'type': token_type, 'value': value, 'start': start, 'end': end}
+ # At the end of the loop make sure we've consumed all the input.
+ # If we haven't then we have unidentified characters.
+ if end != len(expression):
+ msg = "Unknown characters at the end of the expression"
+ raise LexerError(lexer_position=end,
+ lexer_value='',
+ message=msg, expression=expression)
+ else:
+ yield {'type': 'eof', 'value': '',
+ 'start': len(expression), 'end': len(expression)}
-class LexerDefinition(object):
- reflags = re.DOTALL
- reserved = {}
- tokens = (
- 'STAR',
- 'DOT',
- 'LBRACKET',
- 'RBRACKET',
- 'LBRACE',
- 'RBRACE',
- 'OR',
- 'NUMBER',
- 'IDENTIFIER',
- 'COMMA',
- 'COLON',
- ) + tuple(reserved.values())
+ def _token_number(self, value, start, end):
+ return int(value)
- t_STAR = r'\*'
- t_DOT = r'\.'
- t_LBRACKET = r'\['
- t_RBRACKET = r'\]'
- t_LBRACE = r'\{'
- t_RBRACE = r'\}'
- t_OR = r'\|\|'
- t_COMMA = r','
- t_COLON = r':'
- t_ignore = ' '
+ def _token_quoted_identifier(self, value, start, end):
+ try:
+ return loads(value)
+ except ValueError as e:
+ error_message = str(e).split(':')[0]
+ raise LexerError(lexer_position=start,
+ lexer_value=value,
+ message=error_message)
- def t_NUMBER(self, t):
- r'-?\d+'
- t.value = int(t.value)
- return t
+ def _token_literal(self, value, start, end):
+ actual_value = value[1:-1]
+ actual_value = actual_value.replace('\\`', '`').lstrip()
+ # First, if it looks like JSON then we parse it as
+ # JSON and any json parsing errors propogate as lexing
+ # errors.
+ if self._looks_like_json(actual_value):
+ try:
+ return loads(actual_value)
+ except ValueError:
+ raise LexerError(lexer_position=start,
+ lexer_value=value,
+ message="Bad token %s" % value)
+ else:
+ potential_value = '"%s"' % actual_value
+ try:
+ # There's a shortcut syntax where string literals
+ # don't have to be quoted. This is only true if the
+ # string doesn't start with chars that could start a valid
+ # JSON value.
+ return loads(potential_value)
+ except ValueError:
+ raise LexerError(lexer_position=start,
+ lexer_value=value,
+ message="Bad token %s" % value)
- def t_IDENTIFIER(self, t):
- r'(([a-zA-Z_][a-zA-Z_0-9]*)|("(?:\\"|[^"])*"))'
- t.type = self.reserved.get(t.value, 'IDENTIFIER')
- i = 0
- if t.value[0] == '"' and t.value[-1] == '"':
- t.value = loads(t.value)
- return t
- return t
-
- def t_error(self, t):
- # Try to be helpful in the case where they have a missing
- # quote char.
- if t.value.startswith('"'):
- raise LexerError(
- lexer_position=t.lexpos,
- lexer_value=t.value,
- message=("Bad token '%s': starting quote is missing "
- "the ending quote" % t.value))
- raise ValueError("Illegal token value '%s'" % t.value)
+ def _looks_like_json(self, value):
+ # Figure out if the string "value" starts with something
+ # that looks like json.
+ if not value:
+ return False
+ elif value[0] in ['"', '{', '[']:
+ return True
+ elif value in ['true', 'false', 'null']:
+ return True
+ elif value[0] in ['-', '0', '1', '2', '3', '4', '5',
+ '6', '7', '8', '9']:
+ # Then this is JSON, return True.
+ try:
+ loads(value)
+ return True
+ except ValueError:
+ return False
+ else:
+ return False
diff --git a/jmespath/parser.py b/jmespath/parser.py
index 8f37a53..389c3c4 100644
--- a/jmespath/parser.py
+++ b/jmespath/parser.py
@@ -1,197 +1,452 @@
-import random
+"""Top down operator precedence parser.
-import ply.yacc
-import ply.lex
+This is an implementation of Vaughan R. Pratt's
+"Top Down Operator Precedence" parser.
+(http://dl.acm.org/citation.cfm?doid=512927.512931).
+
+These are some additional resources that help explain the
+general idea behind a Pratt parser:
+
+* http://effbot.org/zone/simple-top-down-parsing.htm
+* http://javascript.crockford.com/tdop/tdop.html
+* http://journal.stuffwithstuff.com/2011/03/19/pratt-parsers-expression-parsing-made-easy/
+
+A few notes on the implementation.
+
+* All the nud/led tokens are on the Parser class itself, and are dispatched
+ using getattr(). This keeps all the parsing logic contained to a single class.
+* We use two passes through the data. One to create a list of token,
+ then one pass through the tokens to create the AST. While the lexer actually
+ yields tokens, we convert it to a list so we can easily implement two tokens
+ of lookahead. A previous implementation used a fixed circular buffer, but it
+ was significantly slower. Also, the average jmespath expression typically
+ does not have a large amount of token so this is not an issue. And
+ interestingly enough, creating a token list first is actually faster than
+ consuming from the token iterator one token at a time.
+
+"""
+import random
-from jmespath import ast
from jmespath import lexer
-from jmespath.compat import with_str_method
-
-
-@with_str_method
-class ParseError(ValueError):
- def __init__(self, lex_position, token_value, token_type):
- super(ParseError, self).__init__(lex_position, token_value, token_type)
- self.lex_position = lex_position
- self.token_value = token_value
- self.token_type = token_type
- # Whatever catches the ParseError can fill in the full expression
- self.expression = None
-
- def __str__(self):
- # self.lex_position +1 to account for the starting double quote char.
- underline = ' ' * (self.lex_position + 1) + '^'
- return (
- 'Invalid jmespath expression: Parse error at column %s near '
- 'token "%s" (%s) for expression:\n"%s"\n%s' % (
- self.lex_position, self.token_value, self.token_type,
- self.expression, underline))
-
-
-class IncompleteExpressionError(ParseError):
- def set_expression(self, expression):
- self.expression = expression
- self.lex_position = len(expression)
- self.token_type = None
- self.token_value = None
-
- def __str__(self):
- # self.lex_position +1 to account for the starting double quote char.
- underline = ' ' * (self.lex_position + 1) + '^'
- return (
- 'Invalid jmespath expression: Incomplete expression:\n'
- '"%s"\n%s' % (self.expression, underline))
-
-
-
-class Grammar(object):
- precedence = (
- ('right', 'DOT', 'LBRACKET'),
- )
-
- def p_jmespath_subexpression(self, p):
- """ expression : expression DOT expression
- | STAR
- """
- if len(p) == 2:
- # Then this is the STAR rule.
- p[0] = ast.WildcardValues()
- else:
- # This is the expression DOT expression rule.
- p[0] = ast.SubExpression(p[1], p[3])
-
- def p_jmespath_index(self, p):
- """expression : expression bracket-spec
- | bracket-spec
- """
- if len(p) == 3:
- p[0] = ast.SubExpression(p[1], p[2])
- elif len(p) == 2:
- # Otherwise this is just a bracket-spec, which is valid as a root
- # level node (e.g. [2]) so we just assign the root node to the
- # bracket-spec.
- p[0] = p[1]
-
- def p_jmespath_bracket_specifier(self, p):
- """bracket-spec : LBRACKET STAR RBRACKET
- | LBRACKET NUMBER RBRACKET
- | LBRACKET RBRACKET
- """
- if len(p) == 3:
- p[0] = ast.ListElements()
- elif p[2] == '*':
- p[0] = ast.WildcardIndex()
- else:
- p[0] = ast.Index(p[2])
-
- def p_jmespath_identifier(self, p):
- """expression : IDENTIFIER
- | NUMBER
- """
- p[0] = ast.Field(p[1])
-
- def p_jmespath_multiselect(self, p):
- """expression : LBRACE keyval-exprs RBRACE
- """
- p[0] = ast.MultiFieldDict(p[2])
-
- def p_jmespath_multiselect_list(self, p):
- """expression : LBRACKET expressions RBRACKET
- """
- p[0] = ast.MultiFieldList(p[2])
-
- def p_jmespath_keyval_exprs(self, p):
- """keyval-exprs : keyval-exprs COMMA keyval-expr
- | keyval-expr
- """
- if len(p) == 2:
- p[0] = [p[1]]
- elif len(p) == 4:
- p[1].append(p[3])
- p[0] = p[1]
-
- def p_jmespath_keyval_expr(self, p):
- """keyval-expr : IDENTIFIER COLON expression
- """
- p[0] = ast.KeyValPair(p[1], p[3])
-
- def p_jmespath_multiple_expressions(self, p):
- """expressions : expressions COMMA expression
- | expression
- """
- if len(p) == 2:
- p[0] = [p[1]]
- elif len(p) == 4:
- p[1].append(p[3])
- p[0] = p[1]
-
- def p_jmespath_or_expression(self, p):
- """expression : expression OR expression"""
- p[0] = ast.ORExpression(p[1], p[3])
-
- def p_error(self, t):
- if t is not None:
- raise ParseError(t.lexpos, t.value, t.type)
- else:
- raise IncompleteExpressionError(None, None, None)
+from jmespath.compat import with_repr_method
+from jmespath import ast
+from jmespath import exceptions
class Parser(object):
- # The _max_size most recent expressions are cached in
- # _cache dict.
- _cache = {}
- _max_size = 64
-
- def __init__(self, lexer_definition=None, grammar=None,
- debug=False):
- if lexer_definition is None:
- lexer_definition = lexer.LexerDefinition
- if grammar is None:
- grammar = Grammar
- self._lexer_definition = lexer_definition
- self._grammar = grammar
- self.tokens = self._lexer_definition.tokens
- self._debug = debug
+ BINDING_POWER = {
+ 'eof': 0,
+ 'unquoted_identifier': 0,
+ 'quoted_identifier': 0,
+ 'rbracket': 0,
+ 'rparen': 0,
+ 'comma': 0,
+ 'rbrace': 0,
+ 'number': 0,
+ 'current': 0,
+ 'expref': 0,
+ 'pipe': 1,
+ 'eq': 2,
+ 'gt': 2,
+ 'lt': 2,
+ 'gte': 2,
+ 'lte': 2,
+ 'ne': 2,
+ 'or': 5,
+ 'flatten': 6,
+ 'star': 20,
+ 'dot': 40,
+ 'lbrace': 50,
+ 'filter': 50,
+ 'lbracket': 55,
+ 'lparen': 60,
+ }
+ # The _MAX_SIZE most recent expressions are cached in
+ # _CACHE dict.
+ _CACHE = {}
+ _MAX_SIZE = 128
+
+ def __init__(self, lookahead=2):
+ self.tokenizer = None
+ self._tokens = [None] * lookahead
+ self._buffer_size = lookahead
+ self._index = 0
def parse(self, expression):
- cached = self._cache.get(expression)
+ cached = self._CACHE.get(expression)
if cached is not None:
return cached
- lexer = ply.lex.lex(module=self._lexer_definition(),
- debug=self._debug,
- reflags=self._lexer_definition.reflags)
- grammar = self._grammar()
- grammar.tokens = self._lexer_definition.tokens
- parser = ply.yacc.yacc(module=grammar, debug=self._debug,
- write_tables=False)
- parsed = self._parse_expression(parser=parser, expression=expression,
- lexer_obj=lexer)
- self._cache[expression] = parsed
- if len(self._cache) > self._max_size:
+ parsed_result = self._do_parse(expression)
+ self._CACHE[expression] = parsed_result
+ if len(self._CACHE) > self._MAX_SIZE:
self._free_cache_entries()
- return parsed
+ return parsed_result
- def _parse_expression(self, parser, expression, lexer_obj):
+ def _do_parse(self, expression):
try:
- parsed = parser.parse(input=expression, lexer=lexer_obj)
- return parsed
- except lexer.LexerError as e:
+ return self._parse(expression)
+ except exceptions.LexerError as e:
e.expression = expression
- raise e
- except IncompleteExpressionError as e:
+ raise
+ except exceptions.IncompleteExpressionError as e:
e.set_expression(expression)
- raise e
- except ParseError as e:
+ raise
+ except exceptions.ParseError as e:
e.expression = expression
- raise e
+ raise
+
+ def _parse(self, expression):
+ self.tokenizer = lexer.Lexer().tokenize(expression)
+ self._tokens = list(self.tokenizer)
+ self._index = 0
+ parsed = self._expression(binding_power=0)
+ if not self._current_token() == 'eof':
+ t = self._lookahead_token(0)
+ raise exceptions.ParseError(t['start'], t['value'], t['type'],
+ "Unexpected token: %s" % t['value'])
+ return ParsedResult(expression, parsed)
+
+ def _expression(self, binding_power=0):
+ left_token = self._lookahead_token(0)
+ self._advance()
+ nud_function = getattr(
+ self, '_token_nud_%s' % left_token['type'],
+ self._error_nud_token)
+ left = nud_function(left_token)
+ current_token = self._current_token()
+ while binding_power < self.BINDING_POWER[current_token]:
+ led = getattr(self, '_token_led_%s' % current_token, None)
+ if led is None:
+ error_token = self._lookahead_token(0)
+ self._error_led_token(error_token)
+ else:
+ self._advance()
+ left = led(left)
+ current_token = self._current_token()
+ return left
+
+ def _token_nud_literal(self, token):
+ return ast.Literal(token['value'])
+
+ def _token_nud_unquoted_identifier(self, token):
+ return ast.Field(token['value'])
+
+ def _token_nud_quoted_identifier(self, token):
+ field = ast.Field(token['value'])
+ # You can't have a quoted identifier as a function
+ # name.
+ if self._current_token() == 'lparen':
+ t = self._lookahead_token(0)
+ raise exceptions.ParseError(
+ 0, t['value'], t['type'],
+ 'Quoted identifier not allowed for function names.')
+ return field
+
+ def _token_nud_star(self, token):
+ left = ast.Identity()
+ if self._current_token() == 'rbracket':
+ right = ast.Identity()
+ else:
+ right = self._parse_projection_rhs(self.BINDING_POWER['star'])
+ return ast.ValueProjection(left, right)
+
+ def _token_nud_filter(self, token):
+ return self._token_led_filter(ast.Identity())
+
+ def _token_nud_lbrace(self, token):
+ return self._parse_multi_select_hash()
+
+ def _token_nud_flatten(self, token):
+ left = ast.Flatten(ast.Identity())
+ right = self._parse_projection_rhs(
+ self.BINDING_POWER['flatten'])
+ return ast.Projection(left, right)
+
+ def _token_nud_lbracket(self, token):
+ if self._current_token() == 'number':
+ node = ast.Index(self._lookahead_token(0)['value'])
+ self._advance()
+ self._match('rbracket')
+ return node
+ elif self._current_token() == 'star' and self._lookahead(1) == 'rbracket':
+ self._advance()
+ self._advance()
+ right = self._parse_projection_rhs(self.BINDING_POWER['star'])
+ return ast.Projection(ast.Identity(), right)
+ else:
+ return self._parse_multi_select_list()
+
+ def _token_nud_expref(self, token):
+ expression = self._expression(self.BINDING_POWER['expref'])
+ return ast.ExpressionReference(expression)
+
+ def _token_led_dot(self, left):
+ if not self._current_token() == 'star':
+ right = self._parse_dot_rhs(self.BINDING_POWER['dot'])
+ return ast.SubExpression(left, right)
+ else:
+ # We're creating a projection.
+ self._advance()
+ right = self._parse_projection_rhs(
+ self.BINDING_POWER['dot'])
+ return ast.ValueProjection(left, right)
+
+ def _token_led_pipe(self, left):
+ right = self._expression(self.BINDING_POWER['pipe'])
+ return ast.Pipe(left, right)
+
+ def _token_led_or(self, left):
+ right = self._expression(self.BINDING_POWER['or'])
+ return ast.ORExpression(left, right)
+
+ def _token_led_lparen(self, left):
+ name = left.name
+ args = []
+ while not self._current_token() == 'rparen':
+ if self._current_token() == 'current':
+ expression = ast.CurrentNode()
+ self._advance()
+ else:
+ expression = self._expression()
+ if self._current_token() == 'comma':
+ self._match('comma')
+ args.append(expression)
+ self._match('rparen')
+ function_node = ast.FunctionExpression(name, args)
+ if function_node.variadic:
+ if len(function_node.args) < function_node.arity:
+ raise exceptions.VariadictArityError(function_node)
+ elif function_node.arity != len(function_node.args):
+ raise exceptions.ArityError(function_node)
+ return function_node
+
+ def _token_led_filter(self, left):
+ # Filters are projections.
+ condition = self._expression(0)
+ self._match('rbracket')
+ if self._current_token() == 'flatten':
+ right = ast.Identity()
+ else:
+ right = self._parse_projection_rhs(self.BINDING_POWER['filter'])
+ return ast.FilterProjection(left, right, condition)
+
+ def _token_led_eq(self, left):
+ return self._parse_comparator(left, 'eq')
+
+ def _token_led_ne(self, left):
+ return self._parse_comparator(left, 'ne')
+
+ def _token_led_gt(self, left):
+ return self._parse_comparator(left, 'gt')
+
+ def _token_led_gte(self, left):
+ return self._parse_comparator(left, 'gte')
+
+ def _token_led_lt(self, left):
+ return self._parse_comparator(left, 'lt')
+
+ def _token_led_lte(self, left):
+ return self._parse_comparator(left, 'lte')
+
+ def _token_led_flatten(self, left):
+ left = ast.Flatten(left)
+ right = self._parse_projection_rhs(
+ self.BINDING_POWER['flatten'])
+ return ast.Projection(left, right)
+
+ def _token_led_lbracket(self, left):
+ token = self._lookahead_token(0)
+ if token['type'] == 'number':
+ self._match('number')
+ right = ast.Index(token['value'])
+ self._match('rbracket')
+ return ast.IndexExpression(left, right)
+ else:
+ # We have a projection
+ self._match('star')
+ self._match('rbracket')
+ right = self._parse_projection_rhs(self.BINDING_POWER['star'])
+ return ast.Projection(left, right)
+
+ def _parse_comparator(self, left, comparator):
+ op_map = {
+ 'lt': ast.OPLessThan,
+ 'lte': ast.OPLessThanEquals,
+ 'eq': ast.OPEquals,
+ 'gt': ast.OPGreaterThan,
+ 'gte': ast.OPGreaterThanEquals,
+ 'ne': ast.OPNotEquals,
+ }
+ right = self._expression(self.BINDING_POWER[comparator])
+ return op_map[comparator](left, right)
+
+ def _parse_multi_select_list(self):
+ expressions = []
+ while not self._current_token() == 'rbracket':
+ expression = self._expression()
+ expressions.append(expression)
+ if self._current_token() == 'comma':
+ self._match('comma')
+ self._assert_not_token('rbracket')
+ self._match('rbracket')
+ return ast.MultiFieldList(expressions)
+
+ def _parse_multi_select_hash(self):
+ pairs = []
+ while True:
+ key_token = self._lookahead_token(0)
+ # Before getting the token value, verify it's
+ # an identifier.
+ self._match_multiple_tokens(
+ token_types=['quoted_identifier', 'unquoted_identifier'])
+ key_name = key_token['value']
+ self._match('colon')
+ value = self._expression(0)
+ node = ast.KeyValPair(key_name=key_name, node=value)
+ pairs.append(node)
+ if self._current_token() == 'comma':
+ self._match('comma')
+ elif self._current_token() == 'rbrace':
+ self._match('rbrace')
+ break
+ return ast.MultiFieldDict(nodes=pairs)
+
+ def _parse_projection_rhs(self, binding_power):
+ # Parse the right hand side of the projection.
+ if self.BINDING_POWER[self._current_token()] < 10:
+ # BP of 10 are all the tokens that stop a projection.
+ right = ast.Identity()
+ elif self._current_token() == 'lbracket':
+ right = self._expression(binding_power)
+ elif self._current_token() == 'filter':
+ right = self._expression(binding_power)
+ elif self._current_token() == 'dot':
+ self._match('dot')
+ right = self._parse_dot_rhs(binding_power)
+ else:
+ t = self._lookahead_token(0)
+ lex_position = t['start']
+ actual_value = t['value']
+ actual_type = t['type']
+ raise exceptions.ParseError(lex_position, actual_value,
+ actual_type, 'syntax error')
+ return right
+
+ def _parse_dot_rhs(self, binding_power):
+ # From the grammar:
+ # expression '.' ( identifier /
+ # multi-select-list /
+ # multi-select-hash /
+ # function-expression /
+ # *
+ # In terms of tokens that means that after a '.',
+ # you can have:
+ lookahead = self._current_token()
+ # Common case "foo.bar", so first check for an identifier.
+ if lookahead in ['quoted_identifier', 'unquoted_identifier', 'star']:
+ return self._expression(binding_power)
+ elif lookahead == 'lbracket':
+ self._match('lbracket')
+ return self._parse_multi_select_list()
+ elif lookahead == 'lbrace':
+ self._match('lbrace')
+ return self._parse_multi_select_hash()
+ else:
+ t = self._lookahead_token(0)
+ allowed = ['quoted_identifier', 'unquoted_identifier',
+ 'lbracket', 'lbrace']
+ lex_position = t['start']
+ actual_value = t['value']
+ actual_type = t['type']
+ raise exceptions.ParseError(
+ lex_position, actual_value, actual_type,
+ "Expecting: %s, got: %s" % (allowed,
+ actual_type))
+
+ def _assert_not_token(self, *token_types):
+ if self._current_token() in token_types:
+ t = self._lookahead_token(0)
+ lex_position = t['start']
+ actual_value = t['value']
+ actual_type = t['type']
+ raise exceptions.ParseError(
+ lex_position, actual_value, actual_type,
+ "Token %s not allowed to be: %s" % (actual_type, token_types))
+
+ def _error_nud_token(self, token):
+ raise exceptions.ParseError(token['start'], token['value'],
+ token['type'], 'Invalid token.')
+
+ def _error_led_token(self, token):
+ raise exceptions.ParseError(token['start'], token['value'],
+ token['type'], 'Invalid token')
+
+ def _match(self, token_type=None):
+ if self._current_token() == token_type:
+ self._advance()
+ else:
+ t = self._lookahead_token(0)
+ lex_position = t['start']
+ actual_value = t['value']
+ actual_type = t['type']
+ if actual_type == 'eof':
+ raise exceptions.IncompleteExpressionError(
+ lex_position, actual_value, actual_type)
+ else:
+ message = 'Expecting: %s, got: %s' % (token_type,
+ actual_type)
+ raise exceptions.ParseError(
+ lex_position, actual_value, actual_type, message)
+
+ def _match_multiple_tokens(self, token_types):
+ if self._current_token() not in token_types:
+ t = self._lookahead_token(0)
+ lex_position = t['start']
+ actual_value = t['value']
+ actual_type = t['type']
+ if actual_type == 'eof':
+ raise exceptions.IncompleteExpressionError(
+ lex_position, actual_value, actual_type)
+ else:
+ message = 'Expecting: %s, got: %s' % (token_types,
+ actual_type)
+ raise exceptions.ParseError(
+ lex_position, actual_value, actual_type, message)
+ self._advance()
+
+ def _advance(self):
+ self._index += 1
+
+ def _current_token(self):
+ return self._tokens[self._index]['type']
+
+ def _lookahead(self, number):
+ return self._tokens[self._index + number]['type']
+
+ def _lookahead_token(self, number):
+ return self._tokens[self._index + number]
def _free_cache_entries(self):
- # This logic is borrowed from the new regex library which
- # uses similar eviction strategies.
- for key in random.sample(self._cache.keys(), int(self._max_size / 2)):
- del self._cache[key]
+ for key in random.sample(self._CACHE.keys(), int(self._MAX_SIZE / 2)):
+ del self._CACHE[key]
@classmethod
def purge(cls):
"""Clear the expression compilation cache."""
- cls._cache.clear()
+ cls._CACHE.clear()
+
+
+@with_repr_method
+class ParsedResult(object):
+ def __init__(self, expression, parsed):
+ self.expression = expression
+ self.parsed = parsed
+
+ def search(self, value):
+ return self.parsed.search(value)
+
+ def pretty_print(self, indent=''):
+ return self.parsed.pretty_print(indent=indent)
+
+ def __repr__(self):
+ return repr(self.parsed)
diff --git a/setup.py b/setup.py
index 7a7f92d..5b42dc2 100644
--- a/setup.py
+++ b/setup.py
@@ -2,25 +2,40 @@
import os
import sys
+import io
from setuptools import setup, find_packages
+requires = []
+
+
+if sys.version_info[:2] == (2, 6):
+ # For python2.6 we have a few other dependencies.
+ # First we need an ordered dictionary so we use the
+ # 2.6 backport.
+ requires.append('ordereddict==1.1')
+ # Then we need simplejson. This is because we need
+ # a json version that allows us to specify we want to
+ # use an ordereddict instead of a normal dict for the
+ # JSON objects. The 2.7 json module has this. For 2.6
+ # we need simplejson.
+ requires.append('simplejson==3.3.0')
+
+
setup(
name='jmespath',
- version='0.2.1',
+ version='0.4.1',
description='JSON Matching Expressions',
- long_description=open('README.rst').read(),
+ long_description=io.open('README.rst', encoding='utf-8').read(),
author='James Saryerwinnie',
author_email='js@jamesls.com',
url='https://github.com/boto/jmespath',
scripts=['bin/jp'],
packages=find_packages(exclude=['tests']),
- install_requires=[
- 'ply==3.4',
- ],
+ install_requires=requires,
classifiers=(
- 'Development Status :: 3 - Alpha',
+ 'Development Status :: 5 - Production/Stable',
'Intended Audience :: Developers',
'Natural Language :: English',
'License :: OSI Approved :: MIT License',
@@ -28,7 +43,6 @@ setup(
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
- 'Programming Language :: Python :: 3.2',
'Programming Language :: Python :: 3.3',
),
)
diff --git a/tests/__init__.py b/tests/__init__.py
index 609ff6c..d86946c 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1,4 +1,5 @@
import sys
+from jmespath import ast
# The unittest module got a significant overhaul
@@ -14,3 +15,26 @@ else:
from collections import OrderedDict
+# Helper method used to create an s-expression
+# of the AST to make unit test assertions easier.
+# You get a nice string diff on assert failures.
+def as_s_expression(node):
+ parts = []
+ _as_s_expression(node, parts)
+ return ''.join(parts)
+
+
+def _as_s_expression(node, parts):
+ parts.append("(%s" % (node.__class__.__name__.lower()))
+ if isinstance(node, ast.Field):
+ parts.append(" %s" % node.name)
+ elif isinstance(node, ast.FunctionExpression):
+ parts.append(" %s" % node.name)
+ elif isinstance(node, ast.KeyValPair):
+ parts.append(" %s" % node.key_name)
+ for child in node.children:
+ parts.append(" ")
+ _as_s_expression(child, parts)
+ parts.append(")")
+
+
diff --git a/tests/test_ast.py b/tests/test_ast.py
index fc221a2..2e0aaa1 100644
--- a/tests/test_ast.py
+++ b/tests/test_ast.py
@@ -1,7 +1,6 @@
#!/usr/bin/env python
-import unittest
-from tests import OrderedDict
+from tests import OrderedDict, unittest
from jmespath import ast
@@ -96,12 +95,6 @@ class TestAST(unittest.TestCase):
{'foo': ['one', {'bar': ['zero', 'one']}]})
self.assertEqual(match, 'one')
- def test_index_with_star(self):
- # jmespath: foo[*]
- child = ast.SubExpression(ast.Field('foo'), ast.WildcardIndex())
- match = child.search({'foo': ['one', 'two']})
- self.assertEqual(match, ['one', 'two'])
-
def test_associative(self):
data = {'foo': {'bar': ['one']}}
# jmespath: foo.bar[0]
@@ -114,79 +107,15 @@ class TestAST(unittest.TestCase):
self.assertEqual(first.search(data), 'one')
self.assertEqual(second.search(data), 'one')
- def test_wildcard_branches_on_dict_values(self):
- data = {'foo': {'bar': {'get': 'one'}, 'baz': {'get': 'two'}}}
- # ast for "foo.*.get"
- expression = ast.SubExpression(
- ast.SubExpression(ast.Field('foo'), ast.WildcardValues()),
- ast.Field('get'))
- match = expression.search(data)
- self.assertEqual(sorted(match), ['one', 'two'])
- self.assertEqual(expression.search({'foo': [{'bar': 'one'}]}), None)
-
- def test_wildcard_dot_wildcard(self):
- _ = OrderedDict
- data = _([(
- "top1", _({
- "sub1": _({"foo": "one"})
- })),
- ("top2", _({
- "sub1": _({"foo": "two"})
- })),
- ("top3", _({
- "sub3": _({"notfoo": "notfoo"})
- }))
- ])
- # ast for "*.*"
- expression = ast.SubExpression(
- ast.WildcardValues(), ast.WildcardValues())
- match = expression.search(data)
- self.assertEqual(match, [[{'foo': 'one'}], [{'foo': 'two'}],
- [{'notfoo': 'notfoo'}]])
-
- def test_wildcard_with_field_node(self):
- data = {
- "top1": {
- "sub1": {"foo": "one"}
- },
- "top2": {
- "sub1": {"foo": "two"}
- },
- "top3": {
- "sub3": {"notfoo": "notfoo"}
- }
- }
- # ast for "*.*.foo"
- expression = ast.SubExpression(
- ast.WildcardValues(), ast.SubExpression(ast.WildcardValues(),
- ast.Field('foo')))
- match = expression.search(data)
- self.assertEqual(sorted(match), sorted([[],
- ['one'],
- ['two']]))
-
def test_wildcard_branches_with_index(self):
# foo[*].bar
- child = ast.SubExpression(
- ast.SubExpression(ast.Field('foo'), ast.WildcardIndex()),
- ast.Field('bar')
- )
+ child = ast.Projection(
+ ast.Field('foo'), ast.Field('bar'))
match = child.search(
{'foo': [{'bar': 'one'}, {'bar': 'two'}]})
self.assertTrue(isinstance(match, list))
self.assertEqual(match, ['one', 'two'])
- def test_index_with_multi_match(self):
- # foo[*].bar[0]
- child = ast.SubExpression(
- ast.SubExpression(ast.Field('foo'), ast.WildcardIndex()),
- ast.SubExpression(
- ast.Field('bar'),
- ast.Index(0)))
- data = {'foo': [{'bar': ['one', 'two']}, {'bar': ['three', 'four']}]}
- match = child.search(data)
- self.assertEqual(match, ['one', 'three'])
-
def test_or_expression(self):
# foo or bar
field_foo = ast.Field('foo')
@@ -226,57 +155,19 @@ class TestAST(unittest.TestCase):
subexpr.search({'foo': {'bar': 1, 'baz': 2, 'qux': 3}}),
[1, 2])
- def test_multiselect_list_wildcard(self):
- data = {
- 'foo': {
- 'ignore1': {
- 'one': 1, 'two': 2, 'three': 3,
- },
- 'ignore2': {
- 'one': 1, 'two': 2, 'three': 3,
- },
- }
- }
- expr = ast.SubExpression(
- ast.Field("foo"),
- ast.SubExpression(
- ast.WildcardValues(),
- ast.MultiFieldList([ast.Field("one"), ast.Field("two")])))
- self.assertEqual(expr.search(data), [[1, 2], [1, 2]])
-
- def test_wildcard_values_index_not_a_list(self):
- parsed = ast.SubExpression(
- ast.WildcardValues(),
- ast.SubExpression(ast.Field("foo"), ast.Index(0)))
- data = {"a": {"foo": 1}, "b": {"foo": 1}, "c": {"bar": 1}}
- self.assertEqual(parsed.search(data), [])
-
- def test_wildcard_values_index_does_exist(self):
- parsed = ast.SubExpression(
- ast.WildcardValues(),
- ast.SubExpression(ast.Field("foo"), ast.Index(0)))
- data = {"a": {"foo": [1]}, "b": {"foo": 1}, "c": {"bar": 1}}
- self.assertEqual(parsed.search(data), [1])
-
def test_flattened_wildcard(self):
- parsed = ast.SubExpression(
- # foo[].bar
- ast.SubExpression(ast.Field("foo"), ast.ListElements()),
- ast.Field("bar"))
+ # foo[].bar
+ parsed = ast.Projection(ast.Flatten(ast.Field('foo')),
+ ast.Field('bar'))
data = {'foo': [{'bar': 1}, {'bar': 2}, {'bar': 3}]}
self.assertEqual(parsed.search(data), [1, 2, 3])
def test_multiple_nested_wildcards(self):
# foo[].bar[].baz
- parsed = ast.SubExpression(
- ast.SubExpression(
- ast.Field("foo"),
- ast.ListElements()),
- ast.SubExpression(
- ast.SubExpression(
- ast.Field("bar"),
- ast.ListElements()),
- ast.Field("baz")))
+ parsed = ast.Projection(
+ ast.Flatten(ast.Projection(ast.Flatten(ast.Field('foo')),
+ ast.Field('bar'))),
+ ast.Field('baz'))
data = {
"foo": [
{"bar": [{"baz": 1}, {"baz": 2}]},
@@ -286,15 +177,11 @@ class TestAST(unittest.TestCase):
self.assertEqual(parsed.search(data), [1, 2, 3, 4])
def test_multiple_nested_wildcards_with_list_values(self):
- parsed = ast.SubExpression(
- ast.SubExpression(
- ast.Field("foo"),
- ast.ListElements()),
- ast.SubExpression(
- ast.SubExpression(
- ast.Field("bar"),
- ast.ListElements()),
- ast.Field("baz")))
+ # foo[].bar[].baz
+ parsed = ast.Projection(
+ ast.Flatten(ast.Projection(ast.Flatten(ast.Field('foo')),
+ ast.Field('bar'))),
+ ast.Field('baz'))
data = {
"foo": [
{"bar": [{"baz": [1]}, {"baz": [2]}]},
@@ -305,16 +192,216 @@ class TestAST(unittest.TestCase):
def test_flattened_multiselect_list(self):
# foo[].[bar,baz]
- field_foo = ast.Field('foo')
- parent = ast.SubExpression(field_foo, ast.ListElements())
field_bar = ast.Field('bar')
field_baz = ast.Field('baz')
multiselect = ast.MultiFieldList([field_bar, field_baz])
- subexpr = ast.SubExpression(parent, multiselect)
+ projection = ast.Projection(
+ ast.Flatten(ast.Field('foo')),
+ multiselect)
self.assertEqual(
- subexpr.search({'foo': [{'bar': 1, 'baz': 2, 'qux': 3}]}),
+ projection.search({'foo': [{'bar': 1, 'baz': 2, 'qux': 3}]}),
[[1, 2]])
+ def test_flattened_multiselect_with_none(self):
+ # foo[].[bar,baz]
+ field_bar = ast.Field('bar')
+ field_baz = ast.Field('baz')
+ multiselect = ast.MultiFieldList([field_bar, field_baz])
+ projection = ast.Projection(
+ ast.Flatten(ast.Field('foo')),
+ multiselect)
+ self.assertEqual(
+ projection.search({'bar': [{'bar': 1, 'baz': 2, 'qux': 3}]}),
+ None)
+
+ def test_operator_eq(self):
+ field_foo = ast.Field('foo')
+ field_foo2 = ast.Field('foo')
+ eq = ast.OPEquals(field_foo, field_foo2)
+ self.assertTrue(eq.search({'foo': 'bar'}))
+
+ def test_operator_not_equals(self):
+ field_foo = ast.Field('foo')
+ field_bar = ast.Field('bar')
+ eq = ast.OPNotEquals(field_foo, field_bar)
+ self.assertTrue(eq.search({'foo': '1', 'bar': '2'}))
+
+ def test_operator_lt(self):
+ field_foo = ast.Field('foo')
+ field_bar = ast.Field('bar')
+ eq = ast.OPLessThan(field_foo, field_bar)
+ self.assertTrue(eq.search({'foo': 1, 'bar': 2}))
+
+ def test_filter_expression(self):
+ # foo[?bar==`yes`]
+ field_foo = ast.Field('foo')
+ field_bar = ast.Field('bar')
+ literal = ast.Literal('yes')
+ eq = ast.OPEquals(field_bar, literal)
+ filter_expression = ast.FilterExpression(eq)
+ full_expression = ast.SubExpression(field_foo, filter_expression)
+ match = full_expression.search(
+ {'foo': [{'bar': 'yes', 'v': 1},
+ {'bar': 'no', 'v': 2},
+ {'bar': 'yes', 'v': 3},]})
+ self.assertEqual(match, [{'bar': 'yes', 'v': 1},
+ {'bar': 'yes', 'v': 3}])
+
+ def test_projection_simple(self):
+ # foo[*].bar
+ field_foo = ast.Field('foo')
+ field_bar = ast.Field('bar')
+ projection = ast.Projection(field_foo, field_bar)
+ data = {'foo': [{'bar': 1}, {'bar': 2}, {'bar': 3}]}
+ self.assertEqual(projection.search(data), [1, 2, 3])
+
+ def test_projection_no_left(self):
+ # [*].bar
+ field_bar = ast.Field('bar')
+ projection = ast.Projection(ast.Identity(), field_bar)
+ data = [{'bar': 1}, {'bar': 2}, {'bar': 3}]
+ self.assertEqual(projection.search(data), [1, 2, 3])
+
+ def test_projection_no_right(self):
+ # foo[*]
+ field_foo = ast.Field('foo')
+ projection = ast.Projection(field_foo, ast.Identity())
+ data = {'foo': [{'bar': 1}, {'bar': 2}, {'bar': 3}]}
+ self.assertEqual(projection.search(data),
+ [{'bar': 1}, {'bar': 2}, {'bar': 3}])
+
+ def test_bare_projection(self):
+ # [*]
+ projection = ast.Projection(ast.Identity(), ast.Identity())
+ data = [{'bar': 1}, {'bar': 2}, {'bar': 3}]
+ self.assertEqual(projection.search(data), data)
+
+ def test_base_projection_on_invalid_type(self):
+ # [*]
+ data = {'foo': [{'bar': 1}, {'bar': 2}, {'bar': 3}]}
+ projection = ast.Projection(ast.Identity(), ast.Identity())
+ # search() should return None because the evaluated
+ # type is a dict, not a list.
+ self.assertIsNone(projection.search(data))
+
+ def test_multiple_projections(self):
+ # foo[*].bar[*].baz
+ field_foo = ast.Field('foo')
+ field_bar = ast.Field('bar')
+ field_baz = ast.Field('baz')
+ second_projection = ast.Projection(field_bar, field_baz)
+ first_projection = ast.Projection(field_foo, second_projection)
+ data = {
+ 'foo': [
+ {'bar': [{'baz': 1}, {'baz': 2}, {'baz': 3}],
+ 'other': 1},
+ {'bar': [{'baz': 4}, {'baz': 5}, {'baz': 6}],
+ 'other': 2},
+ ]
+ }
+ self.assertEqual(first_projection.search(data),
+ [[1, 2, 3], [4, 5, 6]])
+
+ def test_values_projection(self):
+ # foo.*.bar
+ field_foo = ast.Field('foo')
+ field_bar = ast.Field('bar')
+ projection = ast.ValueProjection(field_foo, field_bar)
+ data = {
+ 'foo': {
+ 'a': {'bar': 1},
+ 'b': {'bar': 2},
+ 'c': {'bar': 3},
+ }
+
+ }
+ result = list(sorted(projection.search(data)))
+ self.assertEqual(result, [1, 2, 3])
+
+ def test_root_value_projection(self):
+ # *
+ projection = ast.ValueProjection(ast.Identity(), ast.Identity())
+ data = {
+ 'a': 1,
+ 'b': 2,
+ 'c': 3,
+ }
+ result = list(sorted(projection.search(data)))
+ self.assertEqual(result, [1, 2, 3])
+
+ def test_no_left_node_value_projection(self):
+ # *.bar
+ field_bar = ast.Field('bar')
+ projection = ast.ValueProjection(ast.Identity(), field_bar)
+ data = {
+ 'a': {'bar': 1},
+ 'b': {'bar': 2},
+ 'c': {'bar': 3},
+ }
+ result = list(sorted(projection.search(data)))
+ self.assertEqual(result, [1, 2, 3])
+
+ def test_no_right_node_value_projection(self):
+ # foo.*
+ field_foo = ast.Field('foo')
+ projection = ast.ValueProjection(field_foo, ast.Identity())
+ data = {
+ 'foo': {
+ 'a': 1,
+ 'b': 2,
+ 'c': 3,
+ }
+ }
+ result = list(sorted(projection.search(data)))
+ self.assertEqual(result, [1, 2, 3])
+
+ def test_filter_projection(self):
+ # foo[?bar==`1`].baz
+ field_foo = ast.Field('foo')
+ field_bar = ast.Field('bar')
+ field_baz = ast.Field('baz')
+ literal = ast.Literal(1)
+ comparator = ast.OPEquals(field_bar, literal)
+ filter_projection = ast.FilterProjection(field_foo, field_baz, comparator)
+ data = {
+ 'foo': [{'bar': 1}, {'bar': 2}, {'bar': 1, 'baz': 3}]
+ }
+ result = filter_projection.search(data)
+ self.assertEqual(result, [3])
+
+ def test_nested_filter_projection(self):
+ data = {
+ "reservations": [
+ {
+ "instances": [
+ {
+ "foo": 1,
+ "bar": 2
+ },
+ {
+ "foo": 1,
+ "bar": 3
+ },
+ {
+ "foo": 1,
+ "bar": 2
+ },
+ {
+ "foo": 2,
+ "bar": 1
+ }
+ ]
+ }
+ ]
+ }
+ projection = ast.Projection(
+ ast.Flatten(ast.Field('reservations')),
+ ast.FilterProjection(
+ ast.Field('instances'),
+ ast.Identity(),
+ ast.OPEquals(ast.Field('bar'), ast.Literal(1))))
+ self.assertEqual(projection.search(data), [[{'bar': 1, 'foo': 2}]])
+
if __name__ == '__main__':
unittest.main()
diff --git a/tests/test_compliance.py b/tests/test_compliance.py
index 57ffbeb..27d5d45 100644
--- a/tests/test_compliance.py
+++ b/tests/test_compliance.py
@@ -11,15 +11,35 @@ import jmespath
TEST_DIR = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
'compliance')
+NOT_SPECIFIED = object()
def test_compliance():
- for root, dirnames, filenames in os.walk(TEST_DIR):
- for filename in filenames:
- if filename.endswith('.json'):
- full_path = os.path.join(root, filename)
- for given, expression, result in _load_cases(full_path):
- yield _test_expression, given, expression, result, filename
+ for full_path in _walk_files():
+ if full_path.endswith('.json'):
+ for given, expression, result, error in _load_cases(full_path):
+ if error is NOT_SPECIFIED and result is not NOT_SPECIFIED:
+ yield (_test_expression, given, expression,
+ result, os.path.basename(full_path))
+ elif result is NOT_SPECIFIED and error is not NOT_SPECIFIED:
+ yield (_test_error_expression, given, expression,
+ error, os.path.basename(full_path))
+ else:
+ parts = (given, expression, result, error)
+ raise RuntimeError("Invalid test description: %s" % parts)
+
+
+def _walk_files():
+ # Check for a shortcut when running the tests interactively.
+ # If a JMESPATH_TEST is defined, that file is used as the
+ # only test to run. Useful when doing feature development.
+ single_file = os.environ.get('JMESPATH_TEST')
+ if single_file is not None:
+ yield os.path.abspath(single_file)
+ else:
+ for root, dirnames, filenames in os.walk(TEST_DIR):
+ for filename in filenames:
+ yield os.path.join(root, filename)
def _load_cases(full_path):
@@ -27,10 +47,13 @@ def _load_cases(full_path):
for test_data in all_test_data:
given = test_data['given']
for case in test_data['cases']:
- yield given, case['expression'], case['result']
+ yield (given, case['expression'],
+ case.get('result', NOT_SPECIFIED),
+ case.get('error', NOT_SPECIFIED))
def _test_expression(given, expression, expected, filename):
+ import jmespath.parser
try:
parsed = jmespath.compile(expression)
except ValueError as e:
@@ -40,7 +63,28 @@ def _test_expression(given, expression, expected, filename):
actual = parsed.search(given)
expected_repr = json.dumps(expected, indent=4)
actual_repr = json.dumps(actual, indent=4)
- error_msg = ("\n(%s) The expression '%s' was suppose to give: %s.\n"
- "Instead it matched: %s\nparsed as:\n%s" % (
- filename, expression, expected_repr, actual_repr, parsed))
+ error_msg = ("\n\n (%s) The expression '%s' was suppose to give:\n%s\n"
+ "Instead it matched:\n%s\nparsed as:\n%s\ngiven:\n%s" % (
+ filename, expression, expected_repr,
+ actual_repr, parsed, json.dumps(given, indent=4)))
+ error_msg = error_msg.replace(r'\n', '\n')
assert_equal(actual, expected, error_msg)
+
+
+def _test_error_expression(given, expression, error, filename):
+ import jmespath.parser
+ if error not in ('syntax', 'invalid-type',
+ 'unknown-function', 'invalid-arity'):
+ raise RuntimeError("Unknown error type '%s'" % error)
+ try:
+ parsed = jmespath.compile(expression)
+ parsed.search(given)
+ except ValueError as e:
+ # Test passes, it raised a parse error as expected.
+ pass
+ else:
+ error_msg = ("\n\n (%s) The expression '%s' was suppose to be a "
+ "syntax error, but it successfully parsed as:\n\n%s" % (
+ filename, expression, parsed))
+ error_msg = error_msg.replace(r'\n', '\n')
+ raise AssertionError(error_msg)
diff --git a/tests/test_parser.py b/tests/test_parser.py
index 1bcceb0..65fcfb2 100644
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -1,10 +1,12 @@
#!/usr/bin/env python
-from tests import unittest
+from tests import unittest, as_s_expression
from jmespath import parser
from jmespath import ast
from jmespath import lexer
+from jmespath import compat
+from jmespath import exceptions
class TestParser(unittest.TestCase):
@@ -32,9 +34,9 @@ class TestParser(unittest.TestCase):
def test_quoted_subexpression(self):
parsed = self.parser.parse('"foo"."bar"')
- self.assertIsInstance(parsed, ast.SubExpression)
- self.assertEqual(parsed.parent.name, 'foo')
- self.assertEqual(parsed.child.name, 'bar')
+ self.assertIsInstance(parsed.parsed, ast.SubExpression)
+ self.assertEqual(parsed.parsed.children[0].name, 'foo')
+ self.assertEqual(parsed.parsed.children[1].name, 'bar')
def test_wildcard(self):
parsed = self.parser.parse('foo[*]')
@@ -65,6 +67,17 @@ class TestParser(unittest.TestCase):
parsed = self.parser.parse('foo || bar')
self.assertEqual(repr(parsed), 'ORExpression(Field(foo), Field(bar))')
+ def test_unicode_literals_escaped(self):
+ parsed = self.parser.parse(r'`"\u2713"`')
+ if compat.PY2:
+ self.assertEqual(repr(parsed), r'Literal(\u2713)')
+ else:
+ self.assertEqual(repr(parsed), u'Literal(\u2713)')
+
+ def test_unicode_pretty_print(self):
+ parsed = self.parser.parse(r'`"\u2713"`')
+ self.assertEqual(parsed.pretty_print(), u'Literal(\u2713)')
+
def test_multiselect(self):
parsed = self.parser.parse('foo.{bar: bar,baz: baz}')
self.assertEqual(
@@ -90,7 +103,7 @@ class TestErrorMessages(unittest.TestCase):
self.parser = parser.Parser()
def assert_error_message(self, expression, error_message,
- exception=parser.ParseError):
+ exception=exceptions.ParseError):
try:
self.parser.parse(expression)
except exception as e:
@@ -105,12 +118,12 @@ class TestErrorMessages(unittest.TestCase):
"ParseError not raised for bad expression: %s" % expression)
def test_bad_parse(self):
- with self.assertRaises(parser.ParseError):
+ with self.assertRaises(exceptions.ParseError):
parsed = self.parser.parse('foo]baz')
def test_bad_parse_error_message(self):
error_message = (
- 'Invalid jmespath expression: Parse error at column 3 '
+ 'Unexpected token: ]: Parse error at column 3 '
'near token "]" (RBRACKET) for expression:\n'
'"foo]baz"\n'
' ^')
@@ -125,12 +138,25 @@ class TestErrorMessages(unittest.TestCase):
def test_bad_lexer_values(self):
error_message = (
- 'Bad jmespath expression: Bad token \'"bar\': '
- 'starting quote is missing the ending quote:\n'
+ 'Bad jmespath expression: '
+ 'Starting quote is missing the ending quote:\n'
'foo."bar\n'
' ^')
self.assert_error_message('foo."bar', error_message,
- exception=lexer.LexerError)
+ exception=exceptions.LexerError)
+
+ def test_bad_lexer_literal_value_with_json_object(self):
+ error_message = ('Bad jmespath expression: '
+ 'Bad token `{{}`:\n`{{}`\n^')
+ self.assert_error_message('`{{}`', error_message,
+ exception=exceptions.LexerError)
+
+
+ def test_bad_unicode_string(self):
+ error_message = ('Bad jmespath expression: '
+ 'Invalid \\uXXXX escape:\n"\\uAZ12"\n^')
+ self.assert_error_message(r'"\uAZ12"', error_message,
+ exception=exceptions.LexerError)
class TestParserWildcards(unittest.TestCase):
@@ -261,13 +287,35 @@ class TestParserCaching(unittest.TestCase):
p = parser.Parser()
compiled = []
compiled2 = []
- for i in range(parser.Parser._max_size + 1):
+ for i in range(parser.Parser._MAX_SIZE + 1):
compiled.append(p.parse('foo%s' % i))
# Rerun the test and half of these entries should be from the
# cache but they should still be equal to compiled.
- for i in range(parser.Parser._max_size + 1):
+ for i in range(parser.Parser._MAX_SIZE + 1):
compiled2.append(p.parse('foo%s' % i))
- self.assertEqual(compiled, compiled2)
+ self.assertEqual(len(compiled), len(compiled2))
+ s = as_s_expression
+ self.assertEqual(
+ [s(expr.parsed) for expr in compiled],
+ [s(expr.parsed) for expr in compiled2])
+
+ def test_cache_purge(self):
+ p = parser.Parser()
+ first = p.parse('foo')
+ cached = p.parse('foo')
+ p.purge()
+ second = p.parse('foo')
+ self.assertEqual(as_s_expression(first.parsed),
+ as_s_expression(second.parsed))
+ self.assertEqual(as_s_expression(first.parsed),
+ as_s_expression(cached.parsed))
+
+
+class TestParserAddsExpressionAttribute(unittest.TestCase):
+ def test_expression_available_from_parser(self):
+ p = parser.Parser()
+ parsed = p.parse('foo.bar')
+ self.assertEqual(parsed.expression, 'foo.bar')
if __name__ == '__main__':