ENH Separate ns variable in eval_code into globals and locals (#1083)

This commit is contained in:
Hood Chatham 2021-01-10 10:06:27 -08:00 committed by GitHub
parent 7b45762a32
commit f772ca8ebf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 122 additions and 62 deletions

View File

@ -32,6 +32,8 @@
- Reduce the size of the core pyodide package
[#987](https://github.com/iodide-project/pyodide/pull/987).
- Updated packages: bleach 3.2.1, packaging 20.8
- `eval_code` now accepts separate `globals` and `locals` parameters.
[#1083](https://github.com/iodide-project/pyodide/pull/1083)
### Fixed
- getattr and dir on JsProxy now report consistent results and include all names defined on the Python dictionary backing JsProxy. [#1017](https://github.com/iodide-project/pyodide/pull/1017)

View File

@ -49,17 +49,25 @@ class CodeRunner:
Parameters
----------
ns
`locals()` or `globals()` context where to execute code.
This namespace is updated by the subsequent calls to `run()`.
mode
'last_expr' , 'last_expr_or_assign' or 'none',
specifying what should be evaluated and what should be executed.
'last_expr' will return the last expression
'last_expr_or_assign' will return the last expression
or the last (named) assignment.
'none' will always return `None`.
Other values will be interpreted as 'none'.
globals
The global scope in which to execute code. This is used as the `exec`
`globals` parameter. See
[the exec documentation](https://docs.python.org/3/library/functions.html#exec)
for more info.
locals
The local scope in which to execute code. This is used as the `exec`
`locals` parameter. As with `exec`, if `locals` is absent, it is set equal
to `globals`. See
[the exec documentation](https://docs.python.org/3/library/functions.html#exec)
for more info.
return_mode
Specifies what should be returned, must be one of 'last_expr',
'last_expr_or_assign' or `None`. On other values an exception is raised.
'last_expr' -- return the last expression
'last_expr_or_assign' -- return the last expression or the last
(named) assignment.
'none' -- always return `None`.
quiet_trailing_semicolon
wether a trailing semicolon should 'quiet' the result or not.
Setting this to `True` (default) mimic the CPython's interpret
@ -81,15 +89,19 @@ class CodeRunner:
def __init__(
self,
ns: Optional[Dict[str, Any]] = None,
mode: str = "last_expr",
globals: Optional[Dict[str, Any]] = None,
locals: Optional[Dict[str, Any]] = None,
return_mode: str = "last_expr",
quiet_trailing_semicolon: bool = True,
filename: str = "<exec>",
):
self.ns = ns if ns is not None else {}
self.globals = globals if globals is not None else {}
self.locals = locals if locals is not None else self.globals
self.quiet_trailing_semicolon = quiet_trailing_semicolon
self.filename = filename
self.mode = mode
if return_mode not in ["last_expr", "last_expr_or_assign", "none", None]:
raise ValueError(f"Unrecognized return_mode {return_mode!r}")
self.return_mode = return_mode
def quiet(self, code: str) -> bool:
"""
@ -134,9 +146,9 @@ class CodeRunner:
def _last_assign_to_expr(self, mod: ast.Module):
"""
Implementation of 'last_expr_or_assign' mode.
Implementation of 'last_expr_or_assign' return_mode.
It modify the supplyied AST module so that the last
statement's value can be returned in 'last_expr' mode.
statement's value can be returned in 'last_expr' return_mode.
"""
# Largely inspired from IPython:
# https://github.com/ipython/ipython/blob/3587f5bb6c8570e7bbb06cf5f7e3bc9b9467355a/IPython/core/interactiveshell.py#L3229
@ -176,20 +188,21 @@ class CodeRunner:
if not mod.body:
return None, None
if self.mode == "last_expr_or_assign":
if self.return_mode == "last_expr_or_assign":
# If the last statement is a named assignment, add an extra
# expression to the end with just the L-value so that we can
# handle it with the last_expr code.
self._last_assign_to_expr(mod)
# we extract last expression
last_expr = None
if (
self.mode.startswith("last_expr") # last_expr or last_expr_or_assign
self.return_mode.startswith("last_expr") # last_expr or last_expr_or_assign
and isinstance(mod.body[-1], (ast.Expr, ast.Await))
and not self.quiet(code)
):
last_expr = ast.Expression(mod.body.pop().value) # type: ignore
else:
last_expr = None # type: ignore
# we compile
mod = compile(mod, self.filename, "exec", flags=flags) # type: ignore
@ -212,18 +225,18 @@ class CodeRunner:
return `None`.
If the last statement is an expression, return the
result of the expression.
Use the `mode` and `quiet_trailing_semicolon` parameters in the
Use the `return_mode` and `quiet_trailing_semicolon` parameters in the
constructor to modify this default behavior.
"""
mod, last_expr = self._split_and_compile(code)
# running first part
if mod is not None:
exec(mod, self.ns, self.ns)
exec(mod, self.globals, self.locals)
# evaluating last expression
if last_expr is not None:
return eval(last_expr, self.ns, self.ns)
return eval(last_expr, self.globals, self.locals)
async def run_async(self, code: str) -> Any:
""" //!\\ WARNING //!\\
@ -237,13 +250,13 @@ class CodeRunner:
)
# running first part
if mod is not None:
coro = eval(mod, self.ns, self.ns)
coro = eval(mod, self.globals, self.locals)
if iscoroutine(coro):
await coro
# evaluating last expression
if last_expr is not None:
res = eval(last_expr, self.ns, self.ns)
res = eval(last_expr, self.globals, self.locals)
if iscoroutine(res):
res = await res
return res
@ -251,8 +264,9 @@ class CodeRunner:
def eval_code(
code: str,
ns: Dict[str, Any],
mode: str = "last_expr",
globals: Optional[Dict[str, Any]] = None,
locals: Optional[Dict[str, Any]] = None,
return_mode: str = "last_expr",
quiet_trailing_semicolon: bool = True,
filename: str = "<exec>",
) -> Any:
@ -262,20 +276,29 @@ def eval_code(
----------
code
the Python code to run.
ns
`locals()` or `globals()` context where to execute code.
mode
'last_expr' , 'last_expr_or_assign' or 'none',
specifying what should be evaluated and what should be executed.
'last_expr' will return the last expression
'last_expr_or_assign' will return the last expression
or the last (named) assignment.
'none' will always return `None`.
Other values will be interpreted as 'none'.
globals
The global scope in which to execute code. This is used as the `exec`
`globals` parameter. See
[the exec documentation](https://docs.python.org/3/library/functions.html#exec)
for more info.
locals
The local scope in which to execute code. This is used as the `exec`
`locals` parameter. As with `exec`, if `locals` is absent, it is set equal
to `globals`. See
[the exec documentation](https://docs.python.org/3/library/functions.html#exec)
for more info.
return_mode
Specifies what should be returned, must be one of 'last_expr',
'last_expr_or_assign' or `None`. On other values an exception is raised.
'last_expr' -- return the last expression
'last_expr_or_assign' -- return the last expression or the last
(named) assignment.
'none' -- always return `None`.
quiet_trailing_semicolon
wether a trailing semicolon should 'quiet' the result or not.
Setting this to `True` (default) mimic the CPython's interpret
behavior ; whereas setting it to `False` mimic the IPython's
whether a trailing semicolon should 'quiet' the result or not.
Setting this to `True` (default) mimic the CPython's interpret
behavior ; whereas setting it to `False` mimic the IPython's
filename:
file from which the code was read.
@ -284,16 +307,23 @@ def eval_code(
If the last nonwhitespace character of code is a semicolon return `None`.
If the last statement is an expression, return the
result of the expression.
Use the `mode` and `quiet_trailing_semicolon` parameters to modify
Use the `return_mode` and `quiet_trailing_semicolon` parameters to modify
this default behavior.
"""
return CodeRunner(ns, mode, quiet_trailing_semicolon, filename).run(code)
return CodeRunner(
globals=globals,
locals=locals,
return_mode=return_mode,
quiet_trailing_semicolon=quiet_trailing_semicolon,
filename=filename,
).run(code)
async def _eval_code_async(
async def eval_code_async(
code: str,
ns: Dict[str, Any],
mode: str = "last_expr",
globals: Optional[Dict[str, Any]] = None,
locals: Optional[Dict[str, Any]] = None,
return_mode: str = "last_expr",
quiet_trailing_semicolon: bool = True,
filename: str = "<exec>",
) -> Any:
@ -307,9 +337,13 @@ async def _eval_code_async(
- add tests
"""
raise NotImplementedError("Async is not yet supported in Pyodide.")
return await CodeRunner(ns, mode, quiet_trailing_semicolon, filename).run_async(
code
)
return await CodeRunner(
globals=globals,
locals=locals,
return_mode=return_mode,
quiet_trailing_semicolon=quiet_trailing_semicolon,
filename=filename,
).run_async(code)
def find_imports(code: str) -> List[str]:

View File

@ -1,3 +1,4 @@
import pytest
from pathlib import Path
import sys
from textwrap import dedent
@ -68,7 +69,7 @@ def test_eval_code():
assert eval_code("x=7", ns) is None
assert ns["x"] == 7
# default mode ('last_expr'), semicolon
# default return_mode ('last_expr'), semicolon
assert eval_code("1+1;", ns) is None
assert eval_code("1+1#;", ns) == 2
assert eval_code("5-2 # comment with trailing semicolon ;", ns) == 3
@ -77,20 +78,23 @@ def test_eval_code():
assert eval_code("4//2;\n", ns) is None
assert eval_code("2**1;\n\n", ns) is None
# 'last_expr_or_assign' mode, semicolon
assert eval_code("1 + 1", ns, mode="last_expr_or_assign") == 2
assert eval_code("x = 1 + 1", ns, mode="last_expr_or_assign") == 2
assert eval_code("a = 5 ; a += 1", ns, mode="last_expr_or_assign") == 6
assert eval_code("a = 5 ; a += 1;", ns, mode="last_expr_or_assign") is None
assert eval_code("l = [1, 1, 2] ; l[0] = 0", ns, mode="last_expr_or_assign") is None
assert eval_code("a = b = 2", ns, mode="last_expr_or_assign") == 2
# 'last_expr_or_assign' return_mode, semicolon
assert eval_code("1 + 1", ns, return_mode="last_expr_or_assign") == 2
assert eval_code("x = 1 + 1", ns, return_mode="last_expr_or_assign") == 2
assert eval_code("a = 5 ; a += 1", ns, return_mode="last_expr_or_assign") == 6
assert eval_code("a = 5 ; a += 1;", ns, return_mode="last_expr_or_assign") is None
assert (
eval_code("l = [1, 1, 2] ; l[0] = 0", ns, return_mode="last_expr_or_assign")
is None
)
assert eval_code("a = b = 2", ns, return_mode="last_expr_or_assign") == 2
# 'none' mode, (useless) semicolon
assert eval_code("1 + 1", ns, mode="none") is None
assert eval_code("x = 1 + 1", ns, mode="none") is None
assert eval_code("a = 5 ; a += 1", ns, mode="none") is None
assert eval_code("a = 5 ; a += 1;", ns, mode="none") is None
assert eval_code("l = [1, 1, 2] ; l[0] = 0", ns, mode="none") is None
# 'none' return_mode, (useless) semicolon
assert eval_code("1 + 1", ns, return_mode="none") is None
assert eval_code("x = 1 + 1", ns, return_mode="none") is None
assert eval_code("a = 5 ; a += 1", ns, return_mode="none") is None
assert eval_code("a = 5 ; a += 1;", ns, return_mode="none") is None
assert eval_code("l = [1, 1, 2] ; l[0] = 0", ns, return_mode="none") is None
# with 'quiet_trailing_semicolon' set to False
assert eval_code("1+1;", ns, quiet_trailing_semicolon=False) == 2
@ -109,6 +113,26 @@ def test_eval_code():
assert eval_code("2**1;\n\n", ns, quiet_trailing_semicolon=False) == 2
def test_eval_code_locals():
globals = {}
eval_code("x=2", globals, {})
with pytest.raises(NameError):
eval_code("x", globals, {})
locals = {}
eval_code("import sys; sys.getrecursionlimit()", globals, locals)
with pytest.raises(NameError):
eval_code("sys.getrecursionlimit()", globals, {})
eval_code("sys.getrecursionlimit()", globals, locals)
eval_code(
"from importlib import invalidate_caches; invalidate_caches()", globals, locals
)
with pytest.raises(NameError):
eval_code("invalidate_caches()", globals, globals)
eval_code("invalidate_caches()", globals, locals)
def test_monkeypatch_eval_code(selenium):
selenium.run(
"""