Skip to content

Commit fbf8cb5

Browse files
committed
Be more conservative about overriding sys.stdout
Unfortunately, in Jupyter notebooks (at least with ipykernel 6.x), there is already stdout redirection going on, to support printing outputs into the browser below the cell. And for some reason, even though the existing sys.stdout we are overriding *is* an ipykernel output object, it somehow stops being able to yield output below the cells after we replace it. Maybe Jupyter replaces the stdout more than once, and we have a stale object instead of the latest/current one? Regardless, we want to avoid overriding stdout permanently, in favor of using the contextlib.redirect_stdout function to override it only during script evaluation. It's not clear that this function will actually do the right thing if/when multiple Python scripts are executed concurrently, since effectively there will be nested `with redirect_stdout` blocks in that scenario. But this change should make Jupyter notebooks behave better again in the vastly most common case where SciJava Python scripts are not being executed (because why would you execute a SciJava Python script from inside a Python kernel Jupyter notebook, when you can just run the code directly in the cell?).
1 parent 932e76d commit fbf8cb5

1 file changed

Lines changed: 38 additions & 34 deletions

File tree

src/scyjava/_script.py

Lines changed: 38 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import sys
1010
import threading
1111
import traceback
12+
from contextlib import redirect_stdout
1213

1314
from jpype import JImplements, JOverride
1415

@@ -40,19 +41,17 @@ def removeScriptContext(self, thread):
4041
del self._thread_to_context[thread]
4142

4243
def flush(self):
43-
self._std_default.flush()
44+
self._writer().flush()
4445

4546
def write(self, s):
46-
if threading.currentThread() in self._thread_to_context:
47-
self._thread_to_context[threading.currentThread()].getWriter().write(
48-
to_java(s)
49-
)
50-
else:
51-
self._std_default.write(s)
52-
53-
# Q: Is there a better way to manage stdout in conjunction with the script runner?
47+
self._writer().write(s)
48+
49+
def _writer(self):
50+
return self._thread_to_context.get(
51+
threading.currentThread(), self._std_default
52+
)
53+
5454
stdoutContextWriter = ScriptContextWriter(sys.stdout)
55-
sys.stdout = stdoutContextWriter
5655

5756
@JImplements("java.util.function.Supplier")
5857
class PythonObjectSupplier:
@@ -77,31 +76,36 @@ def apply(self, arg):
7776
)
7877

7978
return_value = None
80-
try:
81-
# NB: Execute the block, except for the last statement,
82-
# which we evaluate instead to get its return value.
83-
# Credit: https://stackoverflow.com/a/39381428/1207769
84-
85-
block = ast.parse(str(arg.script), mode="exec")
86-
last = None
87-
if (
88-
len(block.body) > 0
89-
and hasattr(block.body[-1], "value")
90-
and not isinstance(block.body[-1], ast.Assign)
91-
):
92-
# Last statement of the script looks like an expression. Evaluate!
93-
last = ast.Expression(block.body.pop().value)
94-
95-
_globals = {}
96-
exec(compile(block, "<string>", mode="exec"), _globals, script_locals)
97-
if last is not None:
98-
return_value = eval(
99-
compile(last, "<string>", mode="eval"), _globals, script_locals
79+
with redirect_stdout(stdoutContextWriter):
80+
try:
81+
# NB: Execute the block, except for the last statement,
82+
# which we evaluate instead to get its return value.
83+
# Credit: https://stackoverflow.com/a/39381428/1207769
84+
85+
block = ast.parse(str(arg.script), mode="exec")
86+
last = None
87+
if (
88+
len(block.body) > 0
89+
and hasattr(block.body[-1], "value")
90+
and not isinstance(block.body[-1], ast.Assign)
91+
):
92+
# Last statement looks like an expression. Evaluate!
93+
last = ast.Expression(block.body.pop().value)
94+
95+
_globals = {}
96+
exec(
97+
compile(block, "<string>", mode="exec"), _globals, script_locals
10098
)
101-
except Exception:
102-
error_writer = arg.scriptContext.getErrorWriter()
103-
if error_writer is not None:
104-
error_writer.write(to_java(traceback.format_exc()))
99+
if last is not None:
100+
return_value = eval(
101+
compile(last, "<string>", mode="eval"),
102+
_globals,
103+
script_locals,
104+
)
105+
except Exception:
106+
error_writer = arg.scriptContext.getErrorWriter()
107+
if error_writer is not None:
108+
error_writer.write(to_java(traceback.format_exc()))
105109

106110
stdoutContextWriter.removeScriptContext(threading.currentThread())
107111

0 commit comments

Comments
 (0)