Skip to content

Commit

Permalink
Add support for using an ordered dictionary
Browse files Browse the repository at this point in the history
This isn't part of the JMESPath spec (we use the JSON
types and a JSON object has no key order).  In order to
still accomodate this use case, I've added an Options class
that can alter how an expression is interpreted.  You can provide
a specific dict cls that should be used whenever the interpreter
creates a dict as the result of evaluating an expression.

This gives people that want this option a way to do it without
having to comply with the JMESPath spec.

Fixes #89
  • Loading branch information
jamesls committed Sep 19, 2015
1 parent 0f0cf6f commit 6be33c4
Show file tree
Hide file tree
Showing 7 changed files with 74 additions and 10 deletions.
20 changes: 20 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,26 @@ This is useful if you're going to use the same jmespath expression to
search multiple documents. This avoids having to reparse the
JMESPath expression each time you search a new document.

Options
-------

You can provide an instance of ``jmespath.Options`` to control how
a JMESPath expression is evaluated. The most common scenario for
using an ``Options`` instance is if you want to have ordered output
of your dict keys. To do this you can use either of these options::

>>> import jmespath
>>> jmespath.search('{a: a, b: b},
... mydata,
... jmespath.Options(dict_cls=collections.OrderedDict))


>>> import jmespath
>>> parsed = jmespath.compile('{a: a, b: b}')
>>> parsed.search('{a: a, b: b},
... mydata,
... jmespath.Options(dict_cls=collections.OrderedDict))


Specification
=============
Expand Down
5 changes: 3 additions & 2 deletions jmespath/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from jmespath import parser
from jmespath.visitor import Options

__version__ = '0.7.1'

Expand All @@ -7,5 +8,5 @@ def compile(expression):
return parser.Parser().parse(expression)


def search(expression, data):
return parser.Parser().parse(expression).search(data)
def search(expression, data, options=None):
return parser.Parser().parse(expression).search(data, options=options)
4 changes: 2 additions & 2 deletions jmespath/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -504,8 +504,8 @@ def __init__(self, expression, parsed):
self.expression = expression
self.parsed = parsed

def search(self, value):
interpreter = visitor.TreeInterpreter()
def search(self, value, options=None):
interpreter = visitor.TreeInterpreter(options)
result = interpreter.visit(self.parsed, value)
return result

Expand Down
22 changes: 20 additions & 2 deletions jmespath/visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,20 @@ def _is_special_integer_case(x, y):
return x is True or x is False


class Options(object):
"""Options to control how a JMESPath function is evaluated."""
def __init__(self, dict_cls):
#: The class to use when creating a dict. The interpreter
# may create dictionaries during the evalution of a JMESPath
# expression. For example, a multi-select hash will
# create a dictionary. By default we use a dict() type.
# You can set this value to change what dict type is used.
# The most common reason you would change this is if you
# want to set a collections.OrderedDict so that you can
# have predictible key ordering.
self.dict_cls = dict_cls


class _Expression(object):
def __init__(self, expression):
self.expression = expression
Expand Down Expand Up @@ -67,8 +81,12 @@ class TreeInterpreter(Visitor):
}
MAP_TYPE = dict

def __init__(self):
def __init__(self, options=None):
super(TreeInterpreter, self).__init__()
self._options = options
self._dict_cls = self.MAP_TYPE
if options is not None and options.dict_cls is not None:
self._dict_cls = self._options.dict_cls
self._functions = functions.RuntimeFunctions()
# Note that .interpreter is a property that uses
# a weakref so that the cyclic reference can be
Expand Down Expand Up @@ -167,7 +185,7 @@ def visit_literal(self, node, value):
def visit_multi_select_dict(self, node, value):
if value is None:
return None
collected = self.MAP_TYPE()
collected = self._dict_cls()
for child in node['children']:
collected[child['value']] = self.visit(child, value)
return collected
Expand Down
6 changes: 3 additions & 3 deletions tests/test_compliance.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
from nose.tools import assert_equal

import jmespath
from jmespath.visitor import TreeInterpreter
from jmespath.visitor import TreeInterpreter, Options


TEST_DIR = os.path.dirname(os.path.abspath(__file__))
COMPLIANCE_DIR = os.path.join(TEST_DIR, 'compliance')
LEGACY_DIR = os.path.join(TEST_DIR, 'legacy')
NOT_SPECIFIED = object()
TreeInterpreter.MAP_TYPE = OrderedDict
OPTIONS = Options(dict_cls=OrderedDict)


def test_compliance():
Expand Down Expand Up @@ -65,7 +65,7 @@ def _test_expression(given, expression, expected, filename):
raise AssertionError(
'jmespath expression failed to compile: "%s", error: %s"' %
(expression, e))
actual = parsed.search(given)
actual = parsed.search(given, options=OPTIONS)
expected_repr = json.dumps(expected, indent=4)
actual_repr = json.dumps(actual, indent=4)
error_msg = ("\n\n (%s) The expression '%s' was suppose to give:\n%s\n"
Expand Down
15 changes: 14 additions & 1 deletion tests/test_parser.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
#!/usr/bin/env python

import re
from tests import unittest
from tests import unittest, OrderedDict

from jmespath import parser
from jmespath import visitor
from jmespath import ast
from jmespath import exceptions

Expand Down Expand Up @@ -320,6 +321,18 @@ def test_expression_available_from_parser(self):
self.assertEqual(parsed.expression, 'foo.bar')


class TestParsedResultAddsOptions(unittest.TestCase):
def test_can_have_ordered_dict(self):
p = parser.Parser()
parsed = p.parse('{a: a, b: b, c: c}')
options = visitor.Options(dict_cls=OrderedDict)
result = parsed.search(
{"c": "c", "b": "b", "a": "a"}, options=options)
# The order should be 'a', 'b' because we're using an
# OrderedDict
self.assertEqual(list(result), ['a', 'b', 'c'])


class TestRenderGraphvizFile(unittest.TestCase):
def test_dot_file_rendered(self):
p = parser.Parser()
Expand Down
12 changes: 12 additions & 0 deletions tests/test_search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from tests import unittest, OrderedDict

import jmespath


class TestSearchOptions(unittest.TestCase):
def test_can_provide_dict_cls(self):
result = jmespath.search(
'{a: a, b: b, c: c}.*',
{'c': 'c', 'b': 'b', 'a': 'a', 'd': 'd'},
options=jmespath.Options(dict_cls=OrderedDict))
self.assertEqual(result, ['a', 'b', 'c'])

0 comments on commit 6be33c4

Please sign in to comment.