mirror of https://github.com/pyodide/pyodide.git
ENH Separate ns variable in eval_code into globals and locals (#1083)
This commit is contained in:
parent
7b45762a32
commit
f772ca8ebf
|
@ -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)
|
||||
|
|
|
@ -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]:
|
||||
|
|
|
@ -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(
|
||||
"""
|
||||
|
|
Loading…
Reference in New Issue