Skip to content

Commit 9d0c9ee

Browse files
committed
fixes to locals(), exec() and eval() so that various scoping scenarios work correctly
1 parent edde13c commit 9d0c9ee

File tree

3 files changed

+152
-19
lines changed

3 files changed

+152
-19
lines changed

custom_components/pyscript/eval.py

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,22 @@ async def eval_func(arg_str, eval_globals=None, eval_locals=None):
9393
eval_ast.sym_table = eval_globals
9494
else:
9595
eval_ast.sym_table_stack = ast_ctx.sym_table_stack.copy()
96-
eval_ast.sym_table = ast_ctx.sym_table
97-
eval_ast.curr_func = ast_ctx.curr_func
96+
if ast_ctx.sym_table == ast_ctx.global_sym_table:
97+
eval_ast.sym_table = ast_ctx.sym_table
98+
else:
99+
eval_ast.sym_table = ast_ctx.sym_table.copy()
100+
eval_ast.sym_table.update(ast_ctx.user_locals)
101+
to_delete = set()
102+
for var, value in eval_ast.sym_table.items():
103+
if isinstance(value, EvalLocalVar):
104+
if value.is_defined():
105+
eval_ast.sym_table[var] = value.get()
106+
else:
107+
to_delete.add(var)
108+
for var in to_delete:
109+
del eval_ast.sym_table[var]
110+
111+
eval_ast.curr_func = None
98112
try:
99113
eval_result = await eval_ast.aeval(eval_ast.ast)
100114
except Exception as err:
@@ -106,7 +120,17 @@ async def eval_func(arg_str, eval_globals=None, eval_locals=None):
106120
+ eval_ast.exception_long
107121
)
108122
raise
109-
ast_ctx.curr_func = eval_ast.curr_func
123+
#
124+
# save variables only in the locals scope
125+
#
126+
if eval_globals is None and eval_ast.sym_table != ast_ctx.sym_table:
127+
for var, value in eval_ast.sym_table.items():
128+
if var in ast_ctx.global_sym_table and value == ast_ctx.global_sym_table[var]:
129+
continue
130+
if var not in ast_ctx.sym_table and (
131+
ast_ctx.curr_func is None or var not in ast_ctx.curr_func.local_names
132+
):
133+
ast_ctx.user_locals[var] = value
110134
return eval_result
111135

112136
return eval_func
@@ -135,7 +159,20 @@ def ast_locals_factory(ast_ctx):
135159
"""Generate a locals() function with given ast_ctx."""
136160

137161
async def locals_func():
138-
return ast_ctx.sym_table
162+
if ast_ctx.sym_table == ast_ctx.global_sym_table:
163+
return ast_ctx.sym_table
164+
local_sym_table = ast_ctx.sym_table.copy()
165+
local_sym_table.update(ast_ctx.user_locals)
166+
to_delete = set()
167+
for var, value in local_sym_table.items():
168+
if isinstance(value, EvalLocalVar):
169+
if value.is_defined():
170+
local_sym_table[var] = value.get()
171+
else:
172+
to_delete.add(var)
173+
for var in to_delete:
174+
del local_sym_table[var]
175+
return local_sym_table
139176

140177
return locals_func
141178

@@ -698,6 +735,8 @@ async def call(self, ast_ctx, *args, **kwargs):
698735
self.exception_obj = None
699736
self.exception_long = None
700737
prev_func = ast_ctx.curr_func
738+
save_user_locals = ast_ctx.user_locals
739+
ast_ctx.user_locals = {}
701740
ast_ctx.curr_func = self
702741
del args, kwargs
703742
for arg1 in self.func_def.body:
@@ -710,6 +749,7 @@ async def call(self, ast_ctx, *args, **kwargs):
710749
if ast_ctx.get_exception_obj():
711750
break
712751
ast_ctx.curr_func = prev_func
752+
ast_ctx.user_locals = save_user_locals
713753
ast_ctx.code_str, ast_ctx.code_list = code_str, code_list
714754
if prev_sym_table is not None:
715755
(
@@ -798,6 +838,7 @@ def __init__(self, name, global_ctx, logger_name=None):
798838
self.sym_table_stack = []
799839
self.sym_table = self.global_sym_table
800840
self.local_sym_table = {}
841+
self.user_locals = {}
801842
self.curr_func = None
802843
self.filename = name
803844
self.code_str = None

docs/new_features.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ Bug fixes since 1.2.1 include:
5757
- Timeouts that implement time triggers might infrequenctly occur a tiny time before the target time. A fix was added
5858
to do an additional short timeout when there is an early timeout, to make sure any time trigger occurs at or shortly
5959
after the target time (and never before).
60+
- Fixes to ``locals()``, ``exec()`` and ``eval()`` so that various scoping scenarios work correctly.
6061
- When exception text is created, ensure lineno is inside code_list[]; with lambda function or eval it might not be.
6162
- An exception is raised when a function is called with unexpected keyword parameters that don't have corresponding
6263
keyword arguments (however, the trigger parameter names are excluded from this check, since trigger functions

tests/test_unit_eval.py

Lines changed: 106 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,98 @@
198198
["eval('1+2')", 3],
199199
["x = 5; eval('2 * x')", 10],
200200
["x = 5; exec('x = 2 * x'); x", 10],
201+
[
202+
"""
203+
def func():
204+
x = 5
205+
exec('x = 2 * x')
206+
return x
207+
func()
208+
""",
209+
5,
210+
],
211+
["x = 5; locals()['x'] = 10; x", 10],
212+
["x = 5; globals()['x'] = 10; x", 10],
213+
[
214+
"""
215+
def func():
216+
x = 5
217+
locals()['x'] = 10
218+
return x
219+
func()
220+
""",
221+
5,
222+
],
223+
[
224+
"""
225+
bar = 100
226+
bar2 = 50
227+
bar3 = [0]
228+
def func(bar=6):
229+
def foo(bar=6):
230+
bar += 2
231+
bar5 = 100
232+
exec("bar2 += 1; bar += 10; bar3[0] = 1234 + bar + bar2; bar4 = 123; bar5 += 10")
233+
bar += 2
234+
del bar5
235+
return [bar, bar2, bar3, eval('bar2'), eval('bar4'), locals()]
236+
return foo(bar)
237+
[func(), func(5), bar, bar2]
238+
""",
239+
[
240+
[10, 50, [1302], 51, 123, {"bar": 10, "bar2": 51, "bar4": 123}],
241+
[9, 50, [1302], 51, 123, {"bar": 9, "bar2": 51, "bar4": 123}],
242+
100,
243+
50,
244+
],
245+
],
246+
[
247+
"""
248+
bar = 100
249+
bar2 = 50
250+
bar3 = [0]
251+
def func(bar=6):
252+
bar2 = 10
253+
def foo(bar=6):
254+
nonlocal bar2
255+
bar += 2
256+
exec("bar2 += 1; bar += 10; bar3[0] = 1234 + bar2")
257+
return [bar, bar2, bar3, eval('bar2'), locals()]
258+
return foo(bar)
259+
[func(), func(5), bar, bar2]
260+
""",
261+
[[8, 10, [1245], 10, {"bar": 8, "bar2": 10}], [7, 10, [1245], 10, {"bar": 7, "bar2": 10}], 100, 50],
262+
],
263+
[
264+
"""
265+
x = 10
266+
def foo():
267+
x = 5
268+
del x
269+
return eval("x")
270+
foo()
271+
""",
272+
10,
273+
],
274+
[
275+
"""
276+
bar = 100
277+
bar2 = 50
278+
bar3 = [0]
279+
def func(bar=6):
280+
bar2 = 10
281+
def foo(bar=6):
282+
nonlocal bar2
283+
bar += 2
284+
del bar
285+
exec("bar2 += 1; bar += 10; bar3[0] = 1234 + bar2 + bar")
286+
return [bar3, eval('bar'), eval('bar2'), locals()]
287+
del bar2
288+
return foo(bar)
289+
[func(), func(5), bar, bar2]
290+
""",
291+
[[[1395], 100, 50, {}], [[1395], 100, 50, {}], 100, 50],
292+
],
201293
["eval('xyz', {'xyz': 10})", 10],
202294
["g = {'xyz': 10}; eval('xyz', g, {})", 10],
203295
["g = {'xyz': 10}; eval('xyz', {}, g)", 10],
@@ -352,20 +444,19 @@ def foo(bar=6):
352444
""",
353445
[8, 7, 100],
354446
],
355-
# eval()/exec() scoping is broken; remove test until fixed
356-
# [
357-
# """
358-
# bar = 100
359-
# def foo(bar=6):
360-
# bar += 2
361-
# del bar
362-
# return eval('bar')
363-
# bar += 5
364-
# return 1000
365-
# [foo(), foo(5), bar]
366-
# """,
367-
# [100, 100, 100],
368-
# ],
447+
[
448+
"""
449+
bar = 100
450+
def foo(bar=6):
451+
bar += 2
452+
del bar
453+
return eval('bar')
454+
bar += 5
455+
return 1000
456+
[foo(), foo(5), bar]
457+
""",
458+
[100, 100, 100],
459+
],
369460
[
370461
"""
371462
bar = 100
@@ -1371,7 +1462,7 @@ def func():
13711462
eval('1 + y')
13721463
func()
13731464
""",
1374-
"Exception in test line 4 column 9: Exception in func(), eval() line 1 column 4: name 'y' is not defined",
1465+
"Exception in test line 4 column 9: Exception in eval() line 1 column 4: name 'y' is not defined",
13751466
],
13761467
[
13771468
"""

0 commit comments

Comments
 (0)