-
-
Notifications
You must be signed in to change notification settings - Fork 780
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Simplifying eval_code and avoid injecting dummy variable. #1041
Conversation
Thanks @casatir. My code was first-thing-that-works code that I have repurposed several times and so it has vestigial crap from supporting several different sets of features that are absent from this code and also things in it that were never necessary. Most of what you've done I think is an improvement, however the definition of |
I think several different methods are used in IPython:
IPython's support for |
Your version of You could use the splitting method like: if len(mod.body):
coroutine_or_garbage = eval(compile(mod, "<exec>", "exec", flags=compile_flags), ns, ns)
if iscoroutine(coroutine_or_garbage):
await coroutine_or_garbage
if last_expr is not None:
result = eval(compile(last_expr, "<exec>", "eval", flags=compile_flags), ns, ns)
if iscoroutine(result):
result = await result
return result but this code clearly cannot be shared between |
So I think the only design constraint from the original code that you are missing is that in the
Only way you could run into trouble is if someone is trying to use |
Lastly, we should talk about what the eventual API this code should support will look like when Pyodide is run on a WebWorker. The API I have in mind looks like: Executor.run_python(code : str) -> Execution
execution.set_stdout_callback(callback : (str) -> void ) -> void
execution.set_stderr_callback(callback : (str) -> void ) -> void
async executor.syntax_check() -> Option<SyntaxError>
async executor.result() -> Result<any, any> In the lifecycle of an execution there are four relevant types of events: stdout writes, stderr writes, whether the syntax check passes, and when the code has run to completion. stdout writes and stderr writes each happen 0 or more times, the syntax check and getting the result (either a value or an error). This API makes it quite easy for the UI to do special handling for syntax errors which is usually desirable. |
Thanks @hoodmane for the review.
That's what I thought, I wrote it blindly: how do you test it? By the way, is your version really working? if iscoroutine(res):
await res shouldn't it be: if iscoroutine(res):
return await res In one hand, it is important to prepare
Can't see this in IPython's code, at least not the way you do it. Maybe you talk about this? The temporary variable is in a separate namespace.
Your right, I miss this point. Wouldn't it be cleaner to wrap it in an
Imagine a custom namespace ( Your WebWorker API sounds good but does it has an impact on the way |
As far as I understand, the common part in def eval_code(...):
runner = CodeRunner(...)
return runner.run()
async def eval_code_async(...):
runner = CodeRunner(...)
return await runner.run_async() with the class |
Yup, both in a native repl and in pyodide. The pyodide version you may try out here: from js_wrappers.async_js import wrap_promise
resp = await wrap_promise((await wrap_promise(fetch("api/charts/does-this-exist"))).json())
resp.code # ==> 'get-chart::failed::not-found' Since the version I dumped in here is not tested it is best assumed to be broken by the "if it isn't tested it is broken" principle, but it is very similar to well-tested stuff in my private code so I have confidence.
return await res For some annoying reason
I found that their wrapper function approach causes bugs with self assignment. I think this is what happened: >>> x = 5
>>> x = x + 1 # ==> Unbound Local Error: Local variable x referred to before assignment Whereas >>> x = 5
... x = x + 1 works fine. This is weird, I assume that their version of the code must work, but whenever I try it I get subtle bugs. It's worth pointing out that in |
There is absolutely value in this. In this case, in order to support async/await there is at least one significant PR and several minor fixes that need to be made to core, another PR to add a web loop, and eval_code_async needs to work. Once we have all the pieces, we will presumably need an extra PR to work out kinks. |
You either need to parse the code twice or you need to add some extra logic just after |
Use a dummy webloop (stolen from someone in the #245 discussion): class WebLoop(AbstractEventLoop):
def call_soon(self, coro, arg=None):
try:
x = coro.send(arg)
x = x.then(partial(self.call_soon, coro))
x.catch(partial(self.fail,coro))
except StopIteration as result:
pass
class WrappedPromise:
def __init__(self, promise):
self.promise = promise
def __await__(self):
x = yield self.promise
return x
def wrap_promise(promise):
return WrappedPromise(promise) |
If you like I can add some tests for I've been more focused on fixing |
I don't know where does this comes from but it sounds difficult with multiple contributors. Everyone should look at tests to know what is broken? I would prefer the "if it's broken it should be mentioned (and maybe raise an error)" principle.
Are you sure about this? import asyncio
async def foo() -> str:
return 'bar'
async def bar() -> str:
return await foo()
asyncio.run(bar()) gives me
Your answer does not explain why it needs to put tricky code in
This is not what I am saying. I think that we should minimize the "pollution" of existing code with features that require several PR. This does not ease contributions. Atomicity may be the goal. If this can't be achieved, it should be developed in a separate branch before being merged into master (as async support), but that's a personal point of view. |
I was sure about it, but now I am sure that it is wrong. =) It seems that |
Sorry, I think you misunderstood what I was saying. I am absolutely against adding broken or untested code without a warning. We want our code not to broken and "if it isn't tested we assume it is broken" so that means we must test. (Though it's important to remember that testing code does not prevent it from being broken: the converse that "if it is tested it works" is completely wrong.)
The options are duplication or pollution.
Maintaining a separate branch is a lot of work, I've been spending a lot of time maintaining pending PRs as it is. I think as long as this project is maintained by unpaid volunteers, there is a limit to what you can expect. |
I think the main problem here is that my previous PR was highly unpolished and it was merged before I addressed these significant and reasonable complaints you have about it. |
I think this is sort of fair, this |
That's what you did. Maybe I missed the warning.
You're right, branching is a kind of duplication. I prefer duplication.
Understanding overly complicated code also takes time.
But you seemed in a hurry: #876 (comment)
You mean since Pyodide has tricky parts, we should not polish elsewhere?
Replace "policing" by "polishing" and you exactly get the purpose of this PR, yes. I have a lot of respect for your implication in Pyodide and all I want is to make it better, at my own pace. |
No that's correct I added untested code without a warning, and I haven't been meaning to imply that I didn't do that. It was a mistake, I'm sorry. Humans are fallible. Look, I'm good with your improvements, but I am pretty sure we are going to have a webloop in the next release and I see no reason this file shouldn't be set up with an |
src/pyodide-py/pyodide/_base.py
Outdated
return ns.pop(target_name) | ||
# we extract last expression | ||
last_expr = None | ||
if isinstance(mod.body[-1], (ast.Expr, ast.Await)) and not quiet_code(code): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should combine
isinstance(mod.body[-1], (ast.Expr, ast.Await))
into quiet_code
, probably rename quiet_code
to should_code_return_value
or something like that. Then we can keep my tests but apply them to should_code_return_value
.
I agree that it would be good to simplify that code, and add more tests. But asyncio features are indeed very likely be included once #880 , #958, and possibly additional PRs are accepted. We can probably make a META PR with everything to make sure the combination is working as expected, but in the end it will be better to merge them separately as they are now, for clearer history and attribution. So how do we move forward from here?
Could we avoid that code churn and just add more tests in a separate PR? And improve @casatir don't hesitate to comment on/review PRs in general if you have any concerns. |
I could open another PR on top of this branch with my opinion if you like. |
Here is a proposition to save
Thanks, time is lacking. |
63da7a3
to
aaa9ed7
Compare
@rth I'm fine to merge this, I can delete the spurious |
src/pyodide-py/pyodide/_base.py
Outdated
@@ -43,41 +43,137 @@ def open_url(url: str) -> StringIO: | |||
return StringIO(req.response) | |||
|
|||
|
|||
def quiet_code(code: str) -> bool: | |||
class CodeRunner(object): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In Python 3,
class CodeRunner(object): | |
class CodeRunner: |
is enough.
src/pyodide-py/pyodide/_base.py
Outdated
Parameters | ||
---------- | ||
code | ||
the Python code to run. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the Python code to run. | |
the Python code to run. |
src/pyodide-py/pyodide/_base.py
Outdated
code | ||
the Python code to run. | ||
ns | ||
`locals()` or `globals()` context where to execute code. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
`locals()` or `globals()` context where to execute code. | |
`locals()` or `globals()` context where to execute code. |
I really don't like the import warnings
warnings.warn("asyncio is an experimental and untested feature, things may go wrong!") |
It doesn't matter much either way, I think. We will fix that once the other pieces of the asyncio support are integrated (hopefully soon). |
Right, well Speaking of which, |
src/pyodide-py/pyodide/_base.py
Outdated
""" | ||
|
||
def __init__(self, code: str, ns: Dict[str, Any]): | ||
def __init__(self, ns: Dict[str, Any] = {}): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should better make it None by default, then set to {}
if it's None, to avoid the issue of https://stackoverflow.com/a/5592432/179127
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Of course! My mistake! Never put a mutable as default argument!
src/pyodide-py/pyodide/_base.py
Outdated
""" | ||
Constructor. | ||
|
||
Parameters | ||
---------- | ||
code | ||
the Python code to run. | ||
ns | ||
`locals()` or `globals()` context where to execute code. | ||
""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This docstring should be merged with class docstring. __init__
doesn't need one.
Also we should add a note that after the first call to run
, the ns will be modified inplace (as far as I can tell) which will impact subsequent code evaluation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right. Is Parameters
allowed in class docstring?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
||
def quiet(self) -> bool: | ||
def quiet(self, code: str) -> bool: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can probably even be a @staticmethod
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the future, the quiet
behavior can be impacted by options from the constructor (see #1056).
|
||
|
||
def eval_code(code: str, ns: Dict[str, Any]) -> None: | ||
def __init__(self, ns: Dict[str, Any] = None): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Type annotations need an Optional
here, but this can be addressed in a follow up PR. mypy in CI says that everything is OK which is very suspicious.
#876 brings interesting features to
eval_code
. However, I am not anAST
expert but the resulting code looks overly complicated to me:<EXEC-LAST-EXPRESSION>
to assign last expression considered good practice?eval_code
and_eval_code_async
doesn't follow the DRY principlemod.body
splitting was nice and clear and can be kept_adjust_ast_to_store_result
and_adjust_ast_to_store_result_helper
can be avoided (no need to test them!)This PR aims at recovering the clearness of
eval_code
while supporting the semicolon feature and factorizingeval_code
and_eval_code_async
.A review by @hoodmane will be nice as he probably wrote it like this for some reason that eludes me.
Lastly, I should mention that this splitting method is what is used in IPython apart from the fact that last expression is evaluated in
single
mode (the result is then caught bysys.displayhook
).