mirror of https://github.com/pyodide/pyodide.git
ENH Customizable eval_code behavior (#1056)
This commit is contained in:
parent
9c7ff5658f
commit
1bfe31a41c
|
@ -8,7 +8,7 @@ import ast
|
|||
from asyncio import iscoroutine
|
||||
from io import StringIO
|
||||
from textwrap import dedent
|
||||
from typing import Dict, List, Any, Tuple
|
||||
from typing import Dict, List, Any, Tuple, Optional
|
||||
import tokenize
|
||||
|
||||
|
||||
|
@ -52,6 +52,21 @@ class CodeRunner:
|
|||
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'.
|
||||
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
|
||||
interpret behavior.
|
||||
filename:
|
||||
file from which the code was read.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
@ -64,15 +79,22 @@ class CodeRunner:
|
|||
6
|
||||
"""
|
||||
|
||||
def __init__(self, ns: Dict[str, Any] = None):
|
||||
def __init__(
|
||||
self,
|
||||
ns: Optional[Dict[str, Any]] = None,
|
||||
mode: str = "last_expr",
|
||||
quiet_trailing_semicolon: bool = True,
|
||||
filename: str = "<exec>",
|
||||
):
|
||||
self.ns = ns if ns is not None else {}
|
||||
self.filename = "<exec>"
|
||||
self.quiet_trailing_semicolon = quiet_trailing_semicolon
|
||||
self.filename = filename
|
||||
self.mode = mode
|
||||
|
||||
def quiet(self, code: str) -> bool:
|
||||
"""
|
||||
Does the last nonwhitespace character of code is a semicolon?
|
||||
|
||||
This can be overridden to customize the way run() is silenced.
|
||||
If `quiet_trailing_semicolon` is set tot True in the constructor,
|
||||
does the last nonwhitespace character of code is a semicolon?
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
@ -82,10 +104,15 @@ class CodeRunner:
|
|||
True
|
||||
>>> CodeRunner().quiet('1 + 1 # comment ;')
|
||||
False
|
||||
>>> CodeRunner(quiet_trailing_semicolon=False).quiet('1 + 1 ;')
|
||||
False
|
||||
"""
|
||||
# largely inspired from IPython:
|
||||
# https://github.com/ipython/ipython/blob/86d24741188b0cedd78ab080d498e775ed0e5272/IPython/core/displayhook.py#L84
|
||||
|
||||
if not self.quiet_trailing_semicolon:
|
||||
return False
|
||||
|
||||
# We need to wrap tokens in a buffer because:
|
||||
# "Tokenize requires one argument, readline, which must be
|
||||
# a callable object which provides the same interface as the
|
||||
|
@ -105,6 +132,31 @@ class CodeRunner:
|
|||
|
||||
return False
|
||||
|
||||
def _last_assign_to_expr(self, mod: ast.Module):
|
||||
"""
|
||||
Implementation of 'last_expr_or_assign' mode.
|
||||
It modify the supplyied AST module so that the last
|
||||
statement's value can be returned in 'last_expr' mode.
|
||||
"""
|
||||
# Largely inspired from IPython:
|
||||
# https://github.com/ipython/ipython/blob/3587f5bb6c8570e7bbb06cf5f7e3bc9b9467355a/IPython/core/interactiveshell.py#L3229
|
||||
|
||||
last_node = mod.body[-1]
|
||||
|
||||
if isinstance(last_node, ast.Assign):
|
||||
# In this case there can be multiple targets as in `a = b = 1`.
|
||||
# We just take the first one.
|
||||
target = last_node.targets[0]
|
||||
elif isinstance(last_node, (ast.AugAssign, ast.AnnAssign)):
|
||||
target = last_node.target
|
||||
else:
|
||||
return
|
||||
if isinstance(target, ast.Name):
|
||||
last_node = ast.Expr(ast.Name(target.id, ast.Load()))
|
||||
mod.body.append(last_node)
|
||||
# Update the line numbers shown in error messages.
|
||||
ast.fix_missing_locations(mod)
|
||||
|
||||
def _split_and_compile(self, code: str, flags: int = 0x0) -> Tuple[Any, Any]:
|
||||
"""
|
||||
Split code in two parts, everything but last expression and
|
||||
|
@ -124,9 +176,19 @@ class CodeRunner:
|
|||
if not mod.body:
|
||||
return None, None
|
||||
|
||||
if self.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 isinstance(mod.body[-1], (ast.Expr, ast.Await)) and not self.quiet(code):
|
||||
if (
|
||||
self.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
|
||||
|
||||
# we compile
|
||||
|
@ -137,7 +199,7 @@ class CodeRunner:
|
|||
return mod, last_expr
|
||||
|
||||
def run(self, code: str) -> Any:
|
||||
"""
|
||||
"""Runs a code string.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
@ -150,6 +212,8 @@ 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
|
||||
constructor to modify this default behavior.
|
||||
"""
|
||||
mod, last_expr = self._split_and_compile(code)
|
||||
|
||||
|
@ -185,7 +249,13 @@ class CodeRunner:
|
|||
return res
|
||||
|
||||
|
||||
def eval_code(code: str, ns: Dict[str, Any]) -> Any:
|
||||
def eval_code(
|
||||
code: str,
|
||||
ns: Dict[str, Any],
|
||||
mode: str = "last_expr",
|
||||
quiet_trailing_semicolon: bool = True,
|
||||
filename: str = "<exec>",
|
||||
) -> Any:
|
||||
"""Runs a code string.
|
||||
|
||||
Parameters
|
||||
|
@ -194,17 +264,39 @@ def eval_code(code: str, ns: Dict[str, Any]) -> Any:
|
|||
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'.
|
||||
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
|
||||
filename:
|
||||
file from which the code was read.
|
||||
|
||||
Returns
|
||||
-------
|
||||
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
|
||||
this default behavior.
|
||||
"""
|
||||
return CodeRunner(ns).run(code)
|
||||
return CodeRunner(ns, mode, quiet_trailing_semicolon, filename).run(code)
|
||||
|
||||
|
||||
async def _eval_code_async(code: str, ns: Dict[str, Any]) -> Any:
|
||||
async def _eval_code_async(
|
||||
code: str,
|
||||
ns: Dict[str, Any],
|
||||
mode: str = "last_expr",
|
||||
quiet_trailing_semicolon: bool = True,
|
||||
filename: str = "<exec>",
|
||||
) -> Any:
|
||||
""" //!\\ WARNING //!\\
|
||||
This is not working yet. For use once we add an EventLoop.
|
||||
|
||||
|
@ -215,7 +307,9 @@ async def _eval_code_async(code: str, ns: Dict[str, Any]) -> Any:
|
|||
- add tests
|
||||
"""
|
||||
raise NotImplementedError("Async is not yet supported in Pyodide.")
|
||||
return await CodeRunner(ns).run_async(code)
|
||||
return await CodeRunner(ns, mode, quiet_trailing_semicolon, filename).run_async(
|
||||
code
|
||||
)
|
||||
|
||||
|
||||
def find_imports(code: str) -> List[str]:
|
||||
|
|
|
@ -28,6 +28,24 @@ def test_code_runner():
|
|||
assert not runner.quiet("1+1#;")
|
||||
assert not runner.quiet("5-2 # comment with trailing semicolon ;")
|
||||
assert runner.run("4//2\n") == 2
|
||||
assert runner.run("4//2;") is None
|
||||
assert runner.run("x = 2\nx") == 2
|
||||
assert runner.run("def f(x):\n return x*x+1\n[f(x) for x in range(6)]") == [
|
||||
1,
|
||||
2,
|
||||
5,
|
||||
10,
|
||||
17,
|
||||
26,
|
||||
]
|
||||
|
||||
# with 'quiet_trailing_semicolon' set to False
|
||||
runner = CodeRunner(quiet_trailing_semicolon=False)
|
||||
assert not runner.quiet("1+1;")
|
||||
assert not runner.quiet("1+1#;")
|
||||
assert not runner.quiet("5-2 # comment with trailing semicolon ;")
|
||||
assert runner.run("4//2\n") == 2
|
||||
assert runner.run("4//2;") == 2
|
||||
|
||||
|
||||
def test_eval_code():
|
||||
|
@ -50,6 +68,7 @@ def test_eval_code():
|
|||
assert eval_code("x=7", ns) is None
|
||||
assert ns["x"] == 7
|
||||
|
||||
# default 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
|
||||
|
@ -58,6 +77,37 @@ 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
|
||||
|
||||
# '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
|
||||
|
||||
# with 'quiet_trailing_semicolon' set to False
|
||||
assert eval_code("1+1;", ns, quiet_trailing_semicolon=False) == 2
|
||||
assert eval_code("1+1#;", ns, quiet_trailing_semicolon=False) == 2
|
||||
assert (
|
||||
eval_code(
|
||||
"5-2 # comment with trailing semicolon ;",
|
||||
ns,
|
||||
quiet_trailing_semicolon=False,
|
||||
)
|
||||
== 3
|
||||
)
|
||||
assert eval_code("4//2\n", ns, quiet_trailing_semicolon=False) == 2
|
||||
assert eval_code("2**1\n\n", ns, quiet_trailing_semicolon=False) == 2
|
||||
assert eval_code("4//2;\n", ns, quiet_trailing_semicolon=False) == 2
|
||||
assert eval_code("2**1;\n\n", ns, quiet_trailing_semicolon=False) == 2
|
||||
|
||||
|
||||
def test_monkeypatch_eval_code(selenium):
|
||||
selenium.run(
|
||||
|
|
Loading…
Reference in New Issue