Skip to content

Commit e4941fc

Browse files
committed
JEP12-compatibility is now a flag
1 parent 579bda1 commit e4941fc

File tree

11 files changed

+106
-101
lines changed

11 files changed

+106
-101
lines changed

jmespath/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
__version__ = '1.1.0rc1'
55

66

7-
def compile(expression):
8-
return parser.Parser().parse(expression)
7+
def compile(expression, options=None):
8+
return parser.Parser().parse(expression, options=options)
99

1010

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

jmespath/lexer.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import warnings
33
from json import loads
44

5+
from jmespath.visitor import Options
56
from jmespath.exceptions import LexerError, EmptyExpressionError
67

78

@@ -29,7 +30,14 @@ class Lexer(object):
2930
u'\u00f7': 'divide',
3031
}
3132

32-
def tokenize(self, expression):
33+
def __init__(self):
34+
self._enable_legacy_literals = False
35+
36+
def tokenize(self, expression, options=None):
37+
if (options is not None):
38+
self._enable_legacy_literals= \
39+
options.enable_legacy_literals
40+
3341
self._initialize_for_expression(expression)
3442
while self._current is not None:
3543
if self._current in self.SIMPLE_TOKENS:
@@ -184,21 +192,30 @@ def _consume_until(self, delimiter):
184192

185193
def _consume_literal(self):
186194
start = self._position
187-
lexeme = self._consume_until('`').replace('\\`', '`')
195+
token = self._consume_until('`')
196+
lexeme = token.replace('\\`', '`')
197+
parsed_json = None
188198
try:
189199
# Assume it is valid JSON and attempt to parse.
190200
parsed_json = loads(lexeme)
191201
except ValueError:
202+
error = LexerError(lexer_position=start,
203+
lexer_value=self._expression[start:],
204+
message="Bad token %s `{}`".format(token))
205+
206+
if not self._enable_legacy_literals:
207+
raise error
208+
return
209+
192210
try:
193211
# Invalid JSON values should be converted to quoted
194212
# JSON strings during the JEP-12 deprecation period.
195213
parsed_json = loads('"%s"' % lexeme.lstrip())
196214
warnings.warn("deprecated string literal syntax",
197215
PendingDeprecationWarning)
198216
except ValueError:
199-
raise LexerError(lexer_position=start,
200-
lexer_value=self._expression[start:],
201-
message="Bad token %s" % lexeme)
217+
raise error
218+
202219
token_len = self._position - start
203220
return {'type': 'literal', 'value': parsed_json,
204221
'start': start, 'end': token_len}

jmespath/parser.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,19 +89,19 @@ def __init__(self, lookahead=2):
8989
self._buffer_size = lookahead
9090
self._index = 0
9191

92-
def parse(self, expression):
92+
def parse(self, expression, options=None):
9393
cached = self._CACHE.get(expression)
9494
if cached is not None:
9595
return cached
96-
parsed_result = self._do_parse(expression)
96+
parsed_result = self._do_parse(expression, options)
9797
self._CACHE[expression] = parsed_result
9898
if len(self._CACHE) > self._MAX_SIZE:
9999
self._free_cache_entries()
100100
return parsed_result
101101

102-
def _do_parse(self, expression):
102+
def _do_parse(self, expression, options=None):
103103
try:
104-
return self._parse(expression)
104+
return self._parse(expression, options)
105105
except exceptions.LexerError as e:
106106
e.expression = expression
107107
raise
@@ -112,8 +112,8 @@ def _do_parse(self, expression):
112112
e.expression = expression
113113
raise
114114

115-
def _parse(self, expression):
116-
self.tokenizer = lexer.Lexer().tokenize(expression)
115+
def _parse(self, expression, options=None):
116+
self.tokenizer = lexer.Lexer().tokenize(expression, options)
117117
self._tokens = list(self.tokenizer)
118118
self._index = 0
119119
parsed = self._expression(binding_power=0)

jmespath/visitor.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@ def _is_actual_number(x):
5959

6060
class Options(object):
6161
"""Options to control how a JMESPath function is evaluated."""
62-
def __init__(self, dict_cls=None, custom_functions=None):
62+
def __init__(self, dict_cls=None,
63+
custom_functions=None,
64+
enable_legacy_literals=False):
65+
6366
#: The class to use when creating a dict. The interpreter
6467
# may create dictionaries during the evaluation of a JMESPath
6568
# expression. For example, a multi-select hash will
@@ -71,6 +74,12 @@ def __init__(self, dict_cls=None, custom_functions=None):
7174
self.dict_cls = dict_cls
7275
self.custom_functions = custom_functions
7376

77+
#: The flag to enable pre-JEP-12 literal compatibility.
78+
# JEP-12 deprecates `foo` -> "foo" syntax.
79+
# Valid expressions MUST use: `"foo"` -> "foo"
80+
# Setting this flag to `True` enables support for legacy syntax.
81+
self.enable_legacy_literals = enable_legacy_literals
82+
7483

7584
class _Expression(object):
7685
def __init__(self, expression, interpreter, context):

tests/compliance/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
legacy/
12
*\.json

tests/legacy/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import unittest

tests/legacy/literal.json

Lines changed: 0 additions & 56 deletions
This file was deleted.

tests/legacy/test_lexer.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from tests.legacy import unittest
2+
from tests import test_lexer
3+
4+
from jmespath import lexer
5+
from jmespath.visitor import Options
6+
from jmespath.exceptions import LexerError
7+
8+
9+
class TestLegacyRegexLexer(test_lexer.LexerUtils):
10+
11+
def setUp(self):
12+
self.lexer = lexer.Lexer()
13+
self.options = Options(enable_legacy_literals=True)
14+
15+
def tokenize(self, expression):
16+
return self.lexer.tokenize(expression, self.options)
17+
18+
def test_literal_string(self):
19+
tokens = list(self.tokenize('`foobar`'))
20+
self.assert_tokens(tokens, [
21+
{'type': 'literal', 'value': "foobar"},
22+
])
23+
24+
def test_literal_with_invalid_json(self):
25+
with self.assertRaises(LexerError):
26+
list(self.tokenize('`foo"bar`'))
27+
28+
def test_literal_with_empty_string(self):
29+
tokens = list(self.tokenize('``'))
30+
self.assert_tokens(tokens, [{'type': 'literal', 'value': ''}])
31+
32+
def test_adds_quotes_when_invalid_json(self):
33+
tokens = list(self.tokenize('`{{}`'))
34+
self.assertEqual(
35+
tokens,
36+
[{'type': 'literal', 'value': '{{}',
37+
'start': 0, 'end': 4},
38+
{'type': 'eof', 'value': '',
39+
'start': 5, 'end': 5}
40+
]
41+
)
42+
43+
if __name__ == '__main__':
44+
unittest.main()

tests/test_compliance.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
COMPLIANCE_DIR = os.path.join(TEST_DIR, 'compliance')
1313
LEGACY_DIR = os.path.join(TEST_DIR, 'legacy')
1414
NOT_SPECIFIED = object()
15-
OPTIONS = Options(dict_cls=OrderedDict)
15+
COMPLIANCE_OPTIONS = Options(dict_cls=OrderedDict)
16+
LEGACY_OPTIONS = Options(dict_cls=OrderedDict, enable_legacy_literals=True)
1617

1718

1819
def _compliance_tests(requested_test_type):
@@ -69,16 +70,22 @@ def load_cases(full_path):
6970
)
7071
def test_expression(given, expression, expected, filename):
7172
import jmespath.parser
73+
74+
options = COMPLIANCE_OPTIONS \
75+
if filename.find('legacy_') == -1 \
76+
else LEGACY_OPTIONS
77+
7278
try:
73-
parsed = jmespath.compile(expression)
79+
parsed = jmespath.compile(expression, options)
7480
except ValueError as e:
7581
raise AssertionError(
7682
'jmespath expression failed to compile: "%s", error: %s"' %
7783
(expression, e))
78-
actual = parsed.search(given, options=OPTIONS)
84+
85+
actual = parsed.search(given, options=options)
7986
expected_repr = json.dumps(expected, indent=4)
8087
actual_repr = json.dumps(actual, indent=4)
81-
error_msg = ("\n\n (%s) The expression '%s' was suppose to give:\n%s\n"
88+
error_msg = ("\n\n (%s) The expression '%s' was supposed to give:\n%s\n"
8289
"Instead it matched:\n%s\nparsed as:\n%s\ngiven:\n%s" % (
8390
filename, expression, expected_repr,
8491
actual_repr, pformat(parsed.parsed),

0 commit comments

Comments
 (0)