From 681f5e2a8a8b2a3758c441af24085bc1783d328a Mon Sep 17 00:00:00 2001 From: Leo Date: Fri, 30 Oct 2026 00:51:39 -0700 Subject: [PATCH] Allow default options on compiled expressions --- README.rst | 13 +++++++++++++ jmespath/__init__.py | 7 +++++-- jmespath/parser.py | 5 ++++- tests/test_search.py | 45 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 67 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 642ff879..e39167bd 100644 --- a/README.rst +++ b/README.rst @@ -101,6 +101,19 @@ of your dict keys. To do this you can use either of these options: ... jmespath.Options(dict_cls=collections.OrderedDict)) +Options can also be supplied when compiling an expression, so repeated +searches use the same defaults: + +.. code:: python + + >>> parsed = jmespath.compile('{a: a, b: b}', + ... options=jmespath.Options(dict_cls=collections.OrderedDict)) + >>> parsed.search(mydata) + +Passing ``options`` to ``parsed.search()`` overrides the compiled defaults +for that search only. Other compiled expressions and ``jmespath.search()`` +are unaffected. + Custom Functions ~~~~~~~~~~~~~~~~ diff --git a/jmespath/__init__.py b/jmespath/__init__.py index baf73827..8c60e3f6 100644 --- a/jmespath/__init__.py +++ b/jmespath/__init__.py @@ -4,8 +4,11 @@ __version__ = '1.1.0' -def compile(expression): - return parser.Parser().parse(expression) +def compile(expression, options=None): + parsed = parser.Parser().parse(expression) + if options is None: + return parsed + return parser.ParsedResult(parsed.expression, parsed.parsed, options=options) def search(expression, data, options=None): diff --git a/jmespath/parser.py b/jmespath/parser.py index cc8e804e..20032923 100644 --- a/jmespath/parser.py +++ b/jmespath/parser.py @@ -507,11 +507,14 @@ def purge(cls): @with_repr_method class ParsedResult(object): - def __init__(self, expression, parsed): + def __init__(self, expression, parsed, options=None): self.expression = expression self.parsed = parsed + self._options = options def search(self, value, options=None): + if options is None: + options = self._options interpreter = visitor.TreeInterpreter(options) result = interpreter.visit(self.parsed, value) return result diff --git a/tests/test_search.py b/tests/test_search.py index 4832079b..65044fbc 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -7,6 +7,51 @@ class TestSearchOptions(unittest.TestCase): + def test_compile_uses_default_options(self): + expression = jmespath.compile( + '{a: a, b: b}', + options=jmespath.Options(dict_cls=OrderedDict)) + result = expression.search({'a': 1, 'b': 2}) + self.assertIsInstance(result, OrderedDict) + self.assertEqual(list(result.items()), [('a', 1), ('b', 2)]) + + def test_search_options_override_compiled_defaults(self): + expression = jmespath.compile( + '{a: a}', options=jmespath.Options(dict_cls=OrderedDict)) + result = expression.search( + {'a': 1}, options=jmespath.Options(dict_cls=dict)) + self.assertIs(type(result), dict) + self.assertIsInstance(expression.search({'a': 2}), OrderedDict) + + def test_compiled_defaults_do_not_leak_between_expressions(self): + class OtherDict(dict): + pass + + plain = jmespath.compile('{a: a}') + ordered = jmespath.compile( + '{a: a}', options=jmespath.Options(dict_cls=OrderedDict)) + other = jmespath.compile( + '{a: a}', options=jmespath.Options(dict_cls=OtherDict)) + self.assertIsInstance(ordered.search({'a': 1}), OrderedDict) + self.assertIsInstance(other.search({'a': 1}), OtherDict) + self.assertIs(type(plain.search({'a': 1})), dict) + self.assertIs(type(jmespath.compile('{a: a}').search({'a': 1})), dict) + self.assertIs(type(jmespath.search('{a: a}', {'a': 1})), dict) + + def test_compile_with_custom_functions(self): + class CustomFunctions(jmespath.functions.Functions): + @jmespath.functions.signature({'types': ['number']}) + def _func_double(self, number): + return number * 2 + + expression = jmespath.compile( + 'double(value)', + options=jmespath.Options(custom_functions=CustomFunctions())) + self.assertEqual(expression.search({'value': 3}), 6) + self.assertEqual(expression.search({'value': 5}), 10) + with self.assertRaises(jmespath.exceptions.UnknownFunctionError): + jmespath.search('double(value)', {'value': 3}) + def test_can_provide_dict_cls(self): result = jmespath.search( '{a: a, b: b, c: c}.*',