Skip to content

Commit 13bd21a

Browse files
committed
Merge branch 'ordered-dict' into develop
* ordered-dict: Add support for using an ordered dictionary
2 parents 6996f9d + 6be33c4 commit 13bd21a

File tree

7 files changed

+74
-10
lines changed

7 files changed

+74
-10
lines changed

README.rst

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,26 @@ This is useful if you're going to use the same jmespath expression to
6969
search multiple documents. This avoids having to reparse the
7070
JMESPath expression each time you search a new document.
7171

72+
Options
73+
-------
74+
75+
You can provide an instance of ``jmespath.Options`` to control how
76+
a JMESPath expression is evaluated. The most common scenario for
77+
using an ``Options`` instance is if you want to have ordered output
78+
of your dict keys. To do this you can use either of these options::
79+
80+
>>> import jmespath
81+
>>> jmespath.search('{a: a, b: b},
82+
... mydata,
83+
... jmespath.Options(dict_cls=collections.OrderedDict))
84+
85+
86+
>>> import jmespath
87+
>>> parsed = jmespath.compile('{a: a, b: b}')
88+
>>> parsed.search('{a: a, b: b},
89+
... mydata,
90+
... jmespath.Options(dict_cls=collections.OrderedDict))
91+
7292

7393
Specification
7494
=============

jmespath/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from jmespath import parser
2+
from jmespath.visitor import Options
23

34
__version__ = '0.7.1'
45

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

910

10-
def search(expression, data):
11-
return parser.Parser().parse(expression).search(data)
11+
def search(expression, data, options=None):
12+
return parser.Parser().parse(expression).search(data, options=options)

jmespath/parser.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -504,8 +504,8 @@ def __init__(self, expression, parsed):
504504
self.expression = expression
505505
self.parsed = parsed
506506

507-
def search(self, value):
508-
interpreter = visitor.TreeInterpreter()
507+
def search(self, value, options=None):
508+
interpreter = visitor.TreeInterpreter(options)
509509
result = interpreter.visit(self.parsed, value)
510510
return result
511511

jmespath/visitor.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,20 @@ def _is_special_integer_case(x, y):
3333
return x is True or x is False
3434

3535

36+
class Options(object):
37+
"""Options to control how a JMESPath function is evaluated."""
38+
def __init__(self, dict_cls):
39+
#: The class to use when creating a dict. The interpreter
40+
# may create dictionaries during the evalution of a JMESPath
41+
# expression. For example, a multi-select hash will
42+
# create a dictionary. By default we use a dict() type.
43+
# You can set this value to change what dict type is used.
44+
# The most common reason you would change this is if you
45+
# want to set a collections.OrderedDict so that you can
46+
# have predictible key ordering.
47+
self.dict_cls = dict_cls
48+
49+
3650
class _Expression(object):
3751
def __init__(self, expression):
3852
self.expression = expression
@@ -67,8 +81,12 @@ class TreeInterpreter(Visitor):
6781
}
6882
MAP_TYPE = dict
6983

70-
def __init__(self):
84+
def __init__(self, options=None):
7185
super(TreeInterpreter, self).__init__()
86+
self._options = options
87+
self._dict_cls = self.MAP_TYPE
88+
if options is not None and options.dict_cls is not None:
89+
self._dict_cls = self._options.dict_cls
7290
self._functions = functions.RuntimeFunctions()
7391
# Note that .interpreter is a property that uses
7492
# a weakref so that the cyclic reference can be
@@ -167,7 +185,7 @@ def visit_literal(self, node, value):
167185
def visit_multi_select_dict(self, node, value):
168186
if value is None:
169187
return None
170-
collected = self.MAP_TYPE()
188+
collected = self._dict_cls()
171189
for child in node['children']:
172190
collected[child['value']] = self.visit(child, value)
173191
return collected

tests/test_compliance.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66
from nose.tools import assert_equal
77

88
import jmespath
9-
from jmespath.visitor import TreeInterpreter
9+
from jmespath.visitor import TreeInterpreter, Options
1010

1111

1212
TEST_DIR = os.path.dirname(os.path.abspath(__file__))
1313
COMPLIANCE_DIR = os.path.join(TEST_DIR, 'compliance')
1414
LEGACY_DIR = os.path.join(TEST_DIR, 'legacy')
1515
NOT_SPECIFIED = object()
16-
TreeInterpreter.MAP_TYPE = OrderedDict
16+
OPTIONS = Options(dict_cls=OrderedDict)
1717

1818

1919
def test_compliance():
@@ -65,7 +65,7 @@ def _test_expression(given, expression, expected, filename):
6565
raise AssertionError(
6666
'jmespath expression failed to compile: "%s", error: %s"' %
6767
(expression, e))
68-
actual = parsed.search(given)
68+
actual = parsed.search(given, options=OPTIONS)
6969
expected_repr = json.dumps(expected, indent=4)
7070
actual_repr = json.dumps(actual, indent=4)
7171
error_msg = ("\n\n (%s) The expression '%s' was suppose to give:\n%s\n"

tests/test_parser.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
#!/usr/bin/env python
22

33
import re
4-
from tests import unittest
4+
from tests import unittest, OrderedDict
55

66
from jmespath import parser
7+
from jmespath import visitor
78
from jmespath import ast
89
from jmespath import exceptions
910

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

322323

324+
class TestParsedResultAddsOptions(unittest.TestCase):
325+
def test_can_have_ordered_dict(self):
326+
p = parser.Parser()
327+
parsed = p.parse('{a: a, b: b, c: c}')
328+
options = visitor.Options(dict_cls=OrderedDict)
329+
result = parsed.search(
330+
{"c": "c", "b": "b", "a": "a"}, options=options)
331+
# The order should be 'a', 'b' because we're using an
332+
# OrderedDict
333+
self.assertEqual(list(result), ['a', 'b', 'c'])
334+
335+
323336
class TestRenderGraphvizFile(unittest.TestCase):
324337
def test_dot_file_rendered(self):
325338
p = parser.Parser()

tests/test_search.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from tests import unittest, OrderedDict
2+
3+
import jmespath
4+
5+
6+
class TestSearchOptions(unittest.TestCase):
7+
def test_can_provide_dict_cls(self):
8+
result = jmespath.search(
9+
'{a: a, b: b, c: c}.*',
10+
{'c': 'c', 'b': 'b', 'a': 'a', 'd': 'd'},
11+
options=jmespath.Options(dict_cls=OrderedDict))
12+
self.assertEqual(result, ['a', 'b', 'c'])

0 commit comments

Comments
 (0)