Skip to content

Commit 9c6a0b7

Browse files
committed
Add in-memory caching around AST parsing.
1 parent 697bdfc commit 9c6a0b7

File tree

2 files changed

+55
-44
lines changed

2 files changed

+55
-44
lines changed

django_unicorn/call_method_parser.py

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import ast
22
import logging
3+
from functools import lru_cache
34
from typing import Any, Dict, List, Tuple
45
from uuid import UUID
56

@@ -13,7 +14,7 @@
1314

1415
logger = logging.getLogger(__name__)
1516

16-
# Lambdas that attempt to convert something that failed while being parsed by `ast.parse`.
17+
# Lambdas that attempt to convert something that failed while being parsed by `ast.literal_eval`.
1718
CASTERS = [
1819
lambda a: parse_datetime(a),
1920
lambda a: parse_time(a),
@@ -27,23 +28,6 @@ class InvalidKwarg(Exception):
2728
pass
2829

2930

30-
def eval_arg(arg):
31-
try:
32-
arg = ast.literal_eval(arg)
33-
except SyntaxError:
34-
for caster in CASTERS:
35-
try:
36-
casted_value = caster(arg)
37-
38-
if casted_value:
39-
arg = casted_value
40-
break
41-
except ValueError:
42-
pass
43-
44-
return arg
45-
46-
4731
def _get_expr_string(expr: ast.expr) -> str:
4832
"""
4933
Builds a string based on traversing `ast.Attribute` and `ast.Name` expressions.
@@ -79,6 +63,32 @@ def _get_expr_string(expr: ast.expr) -> str:
7963
return expr_str
8064

8165

66+
@lru_cache(maxsize=128)
67+
def eval_value(value):
68+
"""
69+
Uses `ast.literal_eval` to parse strings into an appropriate Python primative.
70+
71+
Also returns an appropriate object for strings that look like they represent datetime,
72+
date, time, duration, or UUID.
73+
"""
74+
75+
try:
76+
value = ast.literal_eval(value)
77+
except SyntaxError:
78+
for caster in CASTERS:
79+
try:
80+
casted_value = caster(value)
81+
82+
if casted_value:
83+
value = casted_value
84+
break
85+
except ValueError:
86+
pass
87+
88+
return value
89+
90+
91+
@lru_cache(maxsize=128)
8292
def parse_kwarg(kwarg: str, raise_if_unparseable=False) -> Dict[str, Any]:
8393
"""
8494
Parses a potential kwarg as a string into a dictionary.
@@ -104,21 +114,22 @@ def parse_kwarg(kwarg: str, raise_if_unparseable=False) -> Dict[str, Any]:
104114
target = assign.targets[0]
105115
key = _get_expr_string(target)
106116

107-
return {key: ast.literal_eval(assign.value)}
117+
return {key: eval_value(assign.value)}
108118
except ValueError:
109119
if raise_if_unparseable:
110120
raise
111121

112122
# The value can be a template variable that will get set from the context when
113-
# the templatetag is rendered
114-
val = _get_expr_string(assign.value)
115-
return {target.id: val}
123+
# the templatetag is rendered, so just return the expr as a string.
124+
value = _get_expr_string(assign.value)
125+
return {target.id: value}
116126
else:
117127
raise InvalidKwarg(f"'{kwarg}' is invalid")
118128
except SyntaxError:
119129
raise InvalidKwarg(f"'{kwarg}' could not be parsed")
120130

121131

132+
@lru_cache(maxsize=128)
122133
def parse_call_method_name(call_method_name: str) -> Tuple[str, List[Any]]:
123134
"""
124135
Parses the method name from the request payload into a set of parameters to pass to a method.
@@ -145,10 +156,10 @@ def parse_call_method_name(call_method_name: str) -> Tuple[str, List[Any]]:
145156
if tree.body and isinstance(tree.body[0].value, ast.Call):
146157
call = tree.body[0].value
147158
method_name = call.func.id
148-
args = [eval_arg(arg) for arg in call.args]
159+
args = [eval_value(arg) for arg in call.args]
149160

150161
# Not returned, but might be usable
151-
kwargs = {kw.arg: eval_arg(kw.value) for kw in call.keywords}
162+
kwargs = {kw.arg: eval_value(kw.value) for kw in call.keywords}
152163

153164
# Add "$" back to special functions
154165
if dollar_func:

tests/call_method_parser/test_parse_args.py

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33

44
import pytest
55

6-
from django_unicorn.call_method_parser import eval_arg
6+
from django_unicorn.call_method_parser import eval_value
77

88

99
def test_args():
1010
expected = (1, 2)
11-
actual = eval_arg("1, 2")
11+
actual = eval_value("1, 2")
1212

1313
assert actual == expected
1414
assert isinstance(actual[0], int)
@@ -17,79 +17,79 @@ def test_args():
1717

1818
def test_single_quote_str_arg():
1919
expected = "1"
20-
actual = eval_arg("'1'")
20+
actual = eval_value("'1'")
2121

2222
assert actual == expected
2323
assert isinstance(actual, str)
2424

2525

2626
def test_str_with_space_arg():
2727
expected = "django unicorn"
28-
actual = eval_arg("'django unicorn'")
28+
actual = eval_value("'django unicorn'")
2929

3030
assert actual == expected
3131
assert isinstance(actual, str)
3232

3333

3434
def test_str_with_extra_single_quote():
3535
expected = "django's unicorn"
36-
actual = eval_arg("'django\\'s unicorn'")
36+
actual = eval_value("'django\\'s unicorn'")
3737

3838
assert actual == expected
3939
assert isinstance(actual, str)
4040

4141

4242
def test_str_with_extra_double_quote():
4343
expected = 'django "unicorn"'
44-
actual = eval_arg("'django \"unicorn\"'")
44+
actual = eval_value("'django \"unicorn\"'")
4545

4646
assert actual == expected
4747
assert isinstance(actual[0], str)
4848

4949

5050
def test_str_with_comma():
5151
expected = "'a', b'"
52-
actual = eval_arg("'a', b'")
52+
actual = eval_value("'a', b'")
5353

5454
assert actual == expected
5555
assert isinstance(actual[0], str)
5656

5757

5858
def test_str_with_stop_character():
5959
expected = "'a'} b'"
60-
actual = eval_arg("'a'} b'")
60+
actual = eval_value("'a'} b'")
6161

6262
assert actual == expected
6363
assert isinstance(actual[0], str)
6464

6565

6666
def test_double_quote_str_arg():
6767
expected = "string"
68-
actual = eval_arg('"string"')
68+
actual = eval_value('"string"')
6969

7070
assert actual == expected
7171
assert isinstance(actual, str)
7272

7373

7474
def test_args_with_single_quote_dict():
7575
expected = (1, {"2": 3})
76-
actual = eval_arg("1, {'2': 3}")
76+
actual = eval_value("1, {'2': 3}")
7777

7878
assert actual == expected
7979
assert isinstance(actual[1], dict)
8080

8181

8282
def test_args_with_double_quote_dict():
8383
expected = (1, {"2": 3})
84-
actual = eval_arg('1, {"2": 3}')
84+
actual = eval_value('1, {"2": 3}')
8585

8686
assert actual == expected
8787
assert isinstance(actual[1], dict)
8888

8989

9090
def test_args_with_nested_dict():
9191
expected = (1, {"2": {"3": 4}})
92-
actual = eval_arg("1, {'2': { '3': 4 }}")
92+
actual = eval_value("1, {'2': { '3': 4 }}")
9393

9494
assert actual == expected
9595
assert isinstance(actual[1], dict)
@@ -98,59 +98,59 @@ def test_args_with_nested_dict():
9898

9999
def test_args_with_nested_list_3():
100100
expected = ([1, ["2", "3"], 4], 9)
101-
actual = eval_arg("[1, ['2', '3'], 4], 9")
101+
actual = eval_value("[1, ['2', '3'], 4], 9")
102102

103103
assert actual == expected
104104

105105

106106
def test_args_with_nested_tuple():
107107
expected = (9, (1, ("2", "3"), 4))
108-
actual = eval_arg("9, (1, ('2', '3'), 4)")
108+
actual = eval_value("9, (1, ('2', '3'), 4)")
109109

110110
assert actual == expected
111111

112112

113113
def test_args_with_nested_objects():
114114
expected = ([0, 1], {"2 2": {"3": 4}}, (5, 6, [7, 8]))
115-
actual = eval_arg("[0, 1], {'2 2': { '3': 4 }}, (5, 6, [7, 8])")
115+
actual = eval_value("[0, 1], {'2 2': { '3': 4 }}, (5, 6, [7, 8])")
116116

117117
assert actual == expected
118118

119119

120120
def test_list_args():
121121
expected = (1, [2, "3"])
122-
actual = eval_arg("1, [2, '3']")
122+
actual = eval_value("1, [2, '3']")
123123

124124
assert actual == expected
125125

126126

127127
def test_datetime():
128128
expected = datetime(2020, 9, 12, 1, 1, 1)
129-
actual = eval_arg("2020-09-12T01:01:01")
129+
actual = eval_value("2020-09-12T01:01:01")
130130

131131
assert actual == expected
132132
assert isinstance(actual, datetime)
133133

134134

135135
def test_uuid():
136136
expected = UUID("90144cb9-fc47-476d-b124-d543b0cff091")
137-
actual = eval_arg("90144cb9-fc47-476d-b124-d543b0cff091")
137+
actual = eval_value("90144cb9-fc47-476d-b124-d543b0cff091")
138138

139139
assert actual == expected
140140
assert isinstance(actual, UUID)
141141

142142

143143
def test_float():
144144
expected = [1, 5.4]
145-
actual = eval_arg("[1, 5.4]")
145+
actual = eval_value("[1, 5.4]")
146146

147147
assert actual == expected
148148
assert isinstance(actual[1], float)
149149

150150

151151
def test_set():
152152
expected = {1, 5}
153-
actual = eval_arg("{1, 5}")
153+
actual = eval_value("{1, 5}")
154154

155155
assert actual == expected
156156
assert isinstance(actual, set)

0 commit comments

Comments
 (0)