diff options
| author | robot-piglet <[email protected]> | 2026-02-06 22:34:50 +0300 |
|---|---|---|
| committer | robot-piglet <[email protected]> | 2026-02-06 22:52:06 +0300 |
| commit | d584f96e49dbfe28c5293ac7c830dceaf886484e (patch) | |
| tree | 01a491a5829e57d4c8e8a168f62ff756e18fbcbe /contrib/python/jmespath | |
| parent | 20815db1eef8a55d50d396413f6e6ceb24c7c911 (diff) | |
Intermediate changes
commit_hash:b5d76ab428f75253e9c745952720fbcb00a960c8
Diffstat (limited to 'contrib/python/jmespath')
| -rw-r--r-- | contrib/python/jmespath/py3/.dist-info/METADATA | 13 | ||||
| -rw-r--r-- | contrib/python/jmespath/py3/LICENSE | 21 | ||||
| -rw-r--r-- | contrib/python/jmespath/py3/LICENSE.txt | 20 | ||||
| -rw-r--r-- | contrib/python/jmespath/py3/jmespath/__init__.py | 2 | ||||
| -rw-r--r-- | contrib/python/jmespath/py3/jmespath/functions.py | 2 | ||||
| -rw-r--r-- | contrib/python/jmespath/py3/jmespath/parser.py | 31 | ||||
| -rw-r--r-- | contrib/python/jmespath/py3/tests/test_compliance.py | 27 | ||||
| -rw-r--r-- | contrib/python/jmespath/py3/tests/test_custom_functions.py | 25 | ||||
| -rw-r--r-- | contrib/python/jmespath/py3/tests/test_functions.py | 57 | ||||
| -rw-r--r-- | contrib/python/jmespath/py3/tests/test_lexer.py | 160 | ||||
| -rw-r--r-- | contrib/python/jmespath/py3/tests/test_search.py | 64 | ||||
| -rw-r--r-- | contrib/python/jmespath/py3/ya.make | 2 |
12 files changed, 369 insertions, 55 deletions
diff --git a/contrib/python/jmespath/py3/.dist-info/METADATA b/contrib/python/jmespath/py3/.dist-info/METADATA index 00fb771cc17..465f77aad5e 100644 --- a/contrib/python/jmespath/py3/.dist-info/METADATA +++ b/contrib/python/jmespath/py3/.dist-info/METADATA @@ -1,26 +1,27 @@ Metadata-Version: 2.1 Name: jmespath -Version: 1.0.1 +Version: 1.1.0 Summary: JSON Matching Expressions Home-page: https://github.com/jmespath/jmespath.py Author: James Saryerwinnie Author-email: [email protected] License: MIT -Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Natural Language :: English Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.7 -Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy -Requires-Python: >=3.7 +Requires-Python: >=3.9 +License-File: LICENSE JMESPath ======== @@ -236,5 +237,3 @@ Discuss Join us on our `Gitter channel <https://gitter.im/jmespath/chat>`__ if you want to chat or if you have any questions. - - diff --git a/contrib/python/jmespath/py3/LICENSE b/contrib/python/jmespath/py3/LICENSE new file mode 100644 index 00000000000..9c520c6bbff --- /dev/null +++ b/contrib/python/jmespath/py3/LICENSE @@ -0,0 +1,21 @@ +MIT License + +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 without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/contrib/python/jmespath/py3/LICENSE.txt b/contrib/python/jmespath/py3/LICENSE.txt deleted file mode 100644 index aa689285366..00000000000 --- a/contrib/python/jmespath/py3/LICENSE.txt +++ /dev/null @@ -1,20 +0,0 @@ -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 -without limitation the rights to use, copy, modify, merge, publish, dis- -tribute, sublicense, and/or sell copies of the Software, and to permit -persons to whom the Software is furnished to do so, subject to the fol- -lowing conditions: - -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL- -ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -IN THE SOFTWARE. diff --git a/contrib/python/jmespath/py3/jmespath/__init__.py b/contrib/python/jmespath/py3/jmespath/__init__.py index c2439e37d47..baf73827e77 100644 --- a/contrib/python/jmespath/py3/jmespath/__init__.py +++ b/contrib/python/jmespath/py3/jmespath/__init__.py @@ -1,7 +1,7 @@ from jmespath import parser from jmespath.visitor import Options -__version__ = '1.0.1' +__version__ = '1.1.0' def compile(expression): diff --git a/contrib/python/jmespath/py3/jmespath/functions.py b/contrib/python/jmespath/py3/jmespath/functions.py index 11ab56aca2e..627b569d821 100644 --- a/contrib/python/jmespath/py3/jmespath/functions.py +++ b/contrib/python/jmespath/py3/jmespath/functions.py @@ -168,7 +168,7 @@ class Functions(metaclass=FunctionRegistry): @signature({'types': ['array-number']}) def _func_avg(self, arg): if arg: - return sum(arg) / float(len(arg)) + return sum(arg) / len(arg) else: return None diff --git a/contrib/python/jmespath/py3/jmespath/parser.py b/contrib/python/jmespath/py3/jmespath/parser.py index 4706688040a..cc8e804e606 100644 --- a/contrib/python/jmespath/py3/jmespath/parser.py +++ b/contrib/python/jmespath/py3/jmespath/parser.py @@ -25,8 +25,6 @@ A few notes on the implementation. consuming from the token iterator one token at a time. """ -import random - from jmespath import lexer from jmespath.compat import with_repr_method from jmespath import ast @@ -73,7 +71,7 @@ class Parser(object): # The _MAX_SIZE most recent expressions are cached in # _CACHE dict. _CACHE = {} - _MAX_SIZE = 128 + _MAX_SIZE = 512 def __init__(self, lookahead=2): self.tokenizer = None @@ -82,13 +80,26 @@ class Parser(object): self._index = 0 def parse(self, expression): - cached = self._CACHE.get(expression) - if cached is not None: - return cached + try: + return self._CACHE[expression] + except KeyError: + pass parsed_result = self._do_parse(expression) + if len(self._CACHE) >= self._MAX_SIZE: + try: + del self._CACHE[next(iter(self._CACHE))] + except (KeyError, StopIteration, RuntimeError): + # KeyError - Another thread else already deleted the key. + # RuntimeError - Another modified the cache. + # StopIteration - (Unlikely) Cache is empty. + # + # If we encounter an error we should NOT be adding to the + # cache. To ensure we do not exceed self._MAX_SIZE, we + # can only add to the cache if we successfully removed + # an element from the cache, otherwise this can grow + # unbounded. + return parsed_result self._CACHE[expression] = parsed_result - if len(self._CACHE) > self._MAX_SIZE: - self._free_cache_entries() return parsed_result def _do_parse(self, expression): @@ -488,10 +499,6 @@ class Parser(object): raise exceptions.ParseError( lex_position, actual_value, actual_type, message) - def _free_cache_entries(self): - for key in random.sample(list(self._CACHE.keys()), int(self._MAX_SIZE / 2)): - self._CACHE.pop(key, None) - @classmethod def purge(cls): """Clear the expression compilation cache.""" diff --git a/contrib/python/jmespath/py3/tests/test_compliance.py b/contrib/python/jmespath/py3/tests/test_compliance.py index cff40b014c9..c35b8f9c15f 100644 --- a/contrib/python/jmespath/py3/tests/test_compliance.py +++ b/contrib/python/jmespath/py3/tests/test_compliance.py @@ -48,19 +48,20 @@ def _walk_files(): def load_cases(full_path): - all_test_data = json.load(open(full_path), object_pairs_hook=OrderedDict) - for test_data in all_test_data: - given = test_data['given'] - for case in test_data['cases']: - if 'result' in case: - test_type = 'result' - elif 'error' in case: - test_type = 'error' - elif 'bench' in case: - test_type = 'bench' - else: - raise RuntimeError("Unknown test type: %s" % json.dumps(case)) - yield (given, test_type, case) + with open(full_path, 'r', encoding='utf-8') as f: + all_test_data = json.load(f, object_pairs_hook=OrderedDict) + for test_data in all_test_data: + given = test_data['given'] + for case in test_data['cases']: + if 'result' in case: + test_type = 'result' + elif 'error' in case: + test_type = 'error' + elif 'bench' in case: + test_type = 'bench' + else: + raise RuntimeError(f"Unknown test type: {json.dumps(case)}") + yield (given, test_type, case) @pytest.mark.parametrize( diff --git a/contrib/python/jmespath/py3/tests/test_custom_functions.py b/contrib/python/jmespath/py3/tests/test_custom_functions.py new file mode 100644 index 00000000000..9932908b693 --- /dev/null +++ b/contrib/python/jmespath/py3/tests/test_custom_functions.py @@ -0,0 +1,25 @@ +import unittest + +import jmespath +from jmespath import functions + + +class CustomFunctions(functions.Functions): + @functions.signature({'types': ['string', 'array', 'object', 'null']}) + def _func_length0(self, s): + return 0 if s is None else len(s) + + +class TestCustomFunctions(unittest.TestCase): + def setUp(self): + self.options = jmespath.Options(custom_functions=CustomFunctions()) + + def test_null_to_nonetype(self): + data = { + 'a': { + 'b': [1, 2, 3] + } + } + + self.assertEqual(jmespath.search('length0(a.b)', data, self.options), 3) + self.assertEqual(jmespath.search('length0(a.c)', data, self.options), 0) diff --git a/contrib/python/jmespath/py3/tests/test_functions.py b/contrib/python/jmespath/py3/tests/test_functions.py new file mode 100644 index 00000000000..a352d69eb42 --- /dev/null +++ b/contrib/python/jmespath/py3/tests/test_functions.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python +from tests import unittest +from datetime import datetime, timedelta +import json + +import jmespath +from jmespath import exceptions + + +class TestFunctions(unittest.TestCase): + + def test_can_max_datetimes(self): + # This is python specific behavior, but JMESPath does not specify what + # you should do with language specific types. We're going to add the + # ability that ``to_string`` will always default to str()'ing values it + # doesn't understand. + data = [datetime.now(), datetime.now() + timedelta(seconds=1)] + result = jmespath.search('max([*].to_string(@))', data) + self.assertEqual(json.loads(result), str(data[-1])) + + def test_type_error_messages(self): + with self.assertRaises(exceptions.JMESPathTypeError) as e: + jmespath.search('length(@)', 2) + exception = e.exception + # 1. Function name should be in error message + self.assertIn('length()', str(exception)) + # 2. Mention it's an invalid type + self.assertIn('invalid type for value: 2', str(exception)) + # 3. Mention the valid types: + self.assertIn("expected one of: ['string', 'array', 'object']", + str(exception)) + # 4. Mention the actual type. + self.assertIn('received: "number"', str(exception)) + + def test_singular_in_error_message(self): + with self.assertRaises(exceptions.ArityError) as e: + jmespath.search('length(@, @)', [0, 1]) + exception = e.exception + self.assertEqual( + str(exception), + 'Expected 1 argument for function length(), received 2') + + def test_error_message_is_pluralized(self): + with self.assertRaises(exceptions.ArityError) as e: + jmespath.search('sort_by(@)', [0, 1]) + exception = e.exception + self.assertEqual( + str(exception), + 'Expected 2 arguments for function sort_by(), received 1') + + def test_variadic_is_pluralized(self): + with self.assertRaises(exceptions.VariadictArityError) as e: + jmespath.search('not_null()', 'foo') + exception = e.exception + self.assertEqual( + str(exception), + 'Expected at least 1 argument for function not_null(), received 0') diff --git a/contrib/python/jmespath/py3/tests/test_lexer.py b/contrib/python/jmespath/py3/tests/test_lexer.py new file mode 100644 index 00000000000..fbae0608813 --- /dev/null +++ b/contrib/python/jmespath/py3/tests/test_lexer.py @@ -0,0 +1,160 @@ +from tests import unittest + +from jmespath import lexer +from jmespath.exceptions import LexerError, EmptyExpressionError + + +class TestRegexLexer(unittest.TestCase): + + def setUp(self): + self.lexer = lexer.Lexer() + + def assert_tokens(self, actual, expected): + # The expected tokens only need to specify the + # type and value. The line/column numbers are not + # checked, and we use assertEqual for the tests + # that check those line numbers. + stripped = [] + for item in actual: + stripped.append({'type': item['type'], 'value': item['value']}) + # Every tokenization should end in eof, so we automatically + # check that value, strip it off the end, and then + # verify the remaining tokens against the expected. + # That way the tests don't need to add eof to every + # assert_tokens call. + self.assertEqual(stripped[-1]['type'], 'eof') + stripped.pop() + self.assertEqual(stripped, expected) + + def test_empty_string(self): + with self.assertRaises(EmptyExpressionError): + list(self.lexer.tokenize('')) + + def test_field(self): + tokens = list(self.lexer.tokenize('foo')) + self.assert_tokens(tokens, [{'type': 'unquoted_identifier', + 'value': 'foo'}]) + + def test_number(self): + tokens = list(self.lexer.tokenize('24')) + self.assert_tokens(tokens, [{'type': 'number', + 'value': 24}]) + + def test_negative_number(self): + tokens = list(self.lexer.tokenize('-24')) + self.assert_tokens(tokens, [{'type': 'number', + 'value': -24}]) + + def test_quoted_identifier(self): + tokens = list(self.lexer.tokenize('"foobar"')) + self.assert_tokens(tokens, [{'type': 'quoted_identifier', + 'value': "foobar"}]) + + def test_json_escaped_value(self): + tokens = list(self.lexer.tokenize('"\u2713"')) + self.assert_tokens(tokens, [{'type': 'quoted_identifier', + 'value': u"\u2713"}]) + + def test_number_expressions(self): + tokens = list(self.lexer.tokenize('foo.bar.baz')) + self.assert_tokens(tokens, [ + {'type': 'unquoted_identifier', 'value': 'foo'}, + {'type': 'dot', 'value': '.'}, + {'type': 'unquoted_identifier', 'value': 'bar'}, + {'type': 'dot', 'value': '.'}, + {'type': 'unquoted_identifier', 'value': 'baz'}, + ]) + + def test_space_separated(self): + tokens = list(self.lexer.tokenize('foo.bar[*].baz | a || b')) + self.assert_tokens(tokens, [ + {'type': 'unquoted_identifier', 'value': 'foo'}, + {'type': 'dot', 'value': '.'}, + {'type': 'unquoted_identifier', 'value': 'bar'}, + {'type': 'lbracket', 'value': '['}, + {'type': 'star', 'value': '*'}, + {'type': 'rbracket', 'value': ']'}, + {'type': 'dot', 'value': '.'}, + {'type': 'unquoted_identifier', 'value': 'baz'}, + {'type': 'pipe', 'value': '|'}, + {'type': 'unquoted_identifier', 'value': 'a'}, + {'type': 'or', 'value': '||'}, + {'type': 'unquoted_identifier', 'value': 'b'}, + ]) + + def test_literal(self): + tokens = list(self.lexer.tokenize('`[0, 1]`')) + self.assert_tokens(tokens, [ + {'type': 'literal', 'value': [0, 1]}, + ]) + + def test_literal_string(self): + tokens = list(self.lexer.tokenize('`foobar`')) + self.assert_tokens(tokens, [ + {'type': 'literal', 'value': "foobar"}, + ]) + + def test_literal_number(self): + tokens = list(self.lexer.tokenize('`2`')) + self.assert_tokens(tokens, [ + {'type': 'literal', 'value': 2}, + ]) + + def test_literal_with_invalid_json(self): + with self.assertRaises(LexerError): + list(self.lexer.tokenize('`foo"bar`')) + + def test_literal_with_empty_string(self): + tokens = list(self.lexer.tokenize('``')) + self.assert_tokens(tokens, [{'type': 'literal', 'value': ''}]) + + def test_position_information(self): + tokens = list(self.lexer.tokenize('foo')) + self.assertEqual( + tokens, + [{'type': 'unquoted_identifier', 'value': 'foo', + 'start': 0, 'end': 3}, + {'type': 'eof', 'value': '', 'start': 3, 'end': 3}] + ) + + def test_position_multiple_tokens(self): + tokens = list(self.lexer.tokenize('foo.bar')) + self.assertEqual( + tokens, + [{'type': 'unquoted_identifier', 'value': 'foo', + 'start': 0, 'end': 3}, + {'type': 'dot', 'value': '.', + 'start': 3, 'end': 4}, + {'type': 'unquoted_identifier', 'value': 'bar', + 'start': 4, 'end': 7}, + {'type': 'eof', 'value': '', + 'start': 7, 'end': 7}, + ] + ) + + def test_adds_quotes_when_invalid_json(self): + tokens = list(self.lexer.tokenize('`{{}`')) + self.assertEqual( + tokens, + [{'type': 'literal', 'value': '{{}', + 'start': 0, 'end': 4}, + {'type': 'eof', 'value': '', + 'start': 5, 'end': 5} + ] + ) + + def test_unknown_character(self): + with self.assertRaises(LexerError) as e: + tokens = list(self.lexer.tokenize('foo[0^]')) + + def test_bad_first_character(self): + with self.assertRaises(LexerError): + tokens = list(self.lexer.tokenize('^foo[0]')) + + def test_unknown_character_with_identifier(self): + with self.assertRaisesRegex(LexerError, "Unknown token"): + list(self.lexer.tokenize('foo-bar')) + + +if __name__ == '__main__': + unittest.main() diff --git a/contrib/python/jmespath/py3/tests/test_search.py b/contrib/python/jmespath/py3/tests/test_search.py new file mode 100644 index 00000000000..4832079ba71 --- /dev/null +++ b/contrib/python/jmespath/py3/tests/test_search.py @@ -0,0 +1,64 @@ +import sys +import decimal +from tests import unittest, OrderedDict + +import jmespath +import jmespath.functions + + +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']) + + def test_can_provide_custom_functions(self): + class CustomFunctions(jmespath.functions.Functions): + @jmespath.functions.signature( + {'types': ['number']}, + {'types': ['number']}) + def _func_custom_add(self, x, y): + return x + y + + @jmespath.functions.signature( + {'types': ['number']}, + {'types': ['number']}) + def _func_my_subtract(self, x, y): + return x - y + + + options = jmespath.Options(custom_functions=CustomFunctions()) + self.assertEqual( + jmespath.search('custom_add(`1`, `2`)', {}, options=options), + 3 + ) + self.assertEqual( + jmespath.search('my_subtract(`10`, `3`)', {}, options=options), + 7 + ) + # Should still be able to use the original functions without + # any interference from the CustomFunctions class. + self.assertEqual( + jmespath.search('length(`[1, 2]`)', {}), 2 + ) + + + +class TestPythonSpecificCases(unittest.TestCase): + def test_can_compare_strings(self): + # This is python specific behavior that's not in the official spec + # yet, but this was regression from 0.9.0 so it's been added back. + self.assertTrue(jmespath.search('a < b', {'a': '2016', 'b': '2017'})) + + @unittest.skipIf(not hasattr(sys, 'maxint'), 'Test requires long() type') + def test_can_handle_long_ints(self): + result = sys.maxint + 1 + self.assertEqual(jmespath.search('[?a >= `1`].a', [{'a': result}]), + [result]) + + def test_can_handle_decimals_as_numeric_type(self): + result = decimal.Decimal('3') + self.assertEqual(jmespath.search('[?a >= `1`].a', [{'a': result}]), + [result]) diff --git a/contrib/python/jmespath/py3/ya.make b/contrib/python/jmespath/py3/ya.make index 8042d15beb1..eac0d048aa0 100644 --- a/contrib/python/jmespath/py3/ya.make +++ b/contrib/python/jmespath/py3/ya.make @@ -2,7 +2,7 @@ PY3_LIBRARY() -VERSION(1.0.1) +VERSION(1.1.0) LICENSE(MIT) |
