Skip to content

Commit 84230c9

Browse files
Add an object for SciJava scripting with Python
Requires org.scijava:scripting-python on the Java side, and calling scyjava.enable_python_scripting(context) on the Python side. Co-authored-by: Curtis Rueden <ctrueden@wisc.edu>
1 parent 06df74d commit 84230c9

File tree

3 files changed

+116
-0
lines changed

3 files changed

+116
-0
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,14 @@ FUNCTIONS
158158
Add a converter to the list used by to_python.
159159
:param converter: A Converter from java to python
160160

161+
enable_python_scripting(context)
162+
Adds a Python script runner object to the ObjectService of the given
163+
SciJava context. Intended for use in conjunction with
164+
'org.scijava:scripting-python'.
165+
166+
:param context: The org.scijava.Context containing the ObjectService
167+
where the PythonScriptRunner should be injected.
168+
161169
get_version(java_class_or_python_package) -> str
162170
Return the version of a Java class or Python package.
163171

src/scyjava/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@
112112
when_jvm_starts,
113113
when_jvm_stops,
114114
)
115+
from scyjava._script import enable_python_scripting # noqa: F401
115116
from scyjava._versions import ( # noqa: F401
116117
compare_version,
117118
get_version,

src/scyjava/_script.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
"""
2+
Logic for making Python available to Java as a SciJava scripting language.
3+
4+
For the Java side of this functionality, see
5+
https://github.com/scijava/scripting-python.
6+
"""
7+
8+
import ast
9+
import sys
10+
import threading
11+
import traceback
12+
13+
from jpype import JImplements, JOverride
14+
15+
from ._convert import to_java
16+
from ._java import jimport
17+
18+
19+
def enable_python_scripting(context):
20+
"""
21+
Adds a Python script runner object to the ObjectService of the given
22+
SciJava context. Intended for use in conjunction with
23+
'org.scijava:scripting-python'.
24+
25+
:param context: The org.scijava.Context containing the ObjectService
26+
where the PythonScriptRunner should be injected.
27+
"""
28+
ObjectService = jimport("org.scijava.object.ObjectService")
29+
30+
class ScriptContextWriter:
31+
def __init__(self, std):
32+
self._std_default = std
33+
self._thread_to_context = {}
34+
35+
def addScriptContext(self, thread, scriptContext):
36+
self._thread_to_context[thread] = scriptContext
37+
38+
def removeScriptContext(self, thread):
39+
if thread in self._thread_to_context:
40+
del self._thread_to_context[thread]
41+
42+
def flush(self):
43+
self._std_default.flush()
44+
45+
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?
54+
stdoutContextWriter = ScriptContextWriter(sys.stdout)
55+
sys.stdout = stdoutContextWriter
56+
57+
@JImplements("java.util.function.Function")
58+
class PythonScriptRunner:
59+
@JOverride
60+
def apply(self, arg):
61+
# Copy script bindings/vars into script locals.
62+
script_locals = {}
63+
for key in arg.vars.keys():
64+
script_locals[key] = arg.vars[key]
65+
66+
stdoutContextWriter.addScriptContext(
67+
threading.currentThread(), arg.scriptContext
68+
)
69+
70+
return_value = None
71+
try:
72+
# NB: Execute the block, except for the last statement,
73+
# which we evaluate instead to get its return value.
74+
# Credit: https://stackoverflow.com/a/39381428/1207769
75+
76+
block = ast.parse(str(arg.script), mode="exec")
77+
last = None
78+
if len(block.body) > 0 and hasattr(block.body[-1], "value"):
79+
# Last statement of the script looks like an expression. Evaluate!
80+
last = ast.Expression(block.body.pop().value)
81+
82+
_globals = {}
83+
exec(compile(block, "<string>", mode="exec"), _globals, script_locals)
84+
if last is not None:
85+
return_value = eval(
86+
compile(last, "<string>", mode="eval"), _globals, script_locals
87+
)
88+
except Exception:
89+
error_writer = arg.scriptContext.getErrorWriter()
90+
if error_writer is not None:
91+
error_writer.write(to_java(traceback.format_exc()))
92+
93+
stdoutContextWriter.removeScriptContext(threading.currentThread())
94+
95+
# Copy script locals back into script bindings/vars.
96+
for key in script_locals.keys():
97+
try:
98+
arg.vars[key] = to_java(script_locals[key])
99+
except Exception:
100+
error_writer = arg.scriptContext.getErrorWriter()
101+
if error_writer is not None:
102+
error_writer.write(to_java(traceback.format_exc()))
103+
104+
return to_java(return_value)
105+
106+
objectService = context.service(ObjectService)
107+
objectService.addObject(PythonScriptRunner(), "PythonScriptRunner")

0 commit comments

Comments
 (0)