Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for using an ordered dictionary #94

Merged
merged 1 commit into from
Sep 23, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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'])