Skip to content

Commit 243ae23

Browse files
authored
Merge pull request #55 from scijava/python-script-runner
Add an object for SciJava scripting with Python
2 parents 568bb58 + 013c260 commit 243ae23

5 files changed

Lines changed: 140 additions & 2 deletions

File tree

.github/workflows/build.yml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,18 +84,27 @@ jobs:
8484
shell: bash -l {0}
8585
steps:
8686
- uses: actions/checkout@v2
87+
- name: Cache conda
88+
uses: actions/cache@v2
89+
env:
90+
# Increase this value to reset cache if dev-environment.yml has not changed
91+
CACHE_NUMBER: 0
92+
with:
93+
path: ~/conda_pkgs_dir
94+
key:
95+
${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-${{ hashFiles('dev-environment.yml') }}
8796
- uses: conda-incubator/setup-miniconda@v2
8897
with:
8998
# Create env with dev packages
9099
auto-update-conda: true
91100
python-version: 3.9
101+
miniforge-variant: Mambaforge
92102
environment-file: dev-environment.yml
93103
# Activate scyjava-dev environment
94104
activate-environment: scyjava-dev
95105
auto-activate-base: false
96106
# Use mamba for faster setup
97107
use-mamba: true
98-
mamba-version: "*"
99108
- name: Test scyjava
100109
run: |
101110
bin/test.sh --cov-report=xml --cov=.

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/_java.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,6 @@ def start_jvm(options=None) -> None:
204204
or not os.environ["JAVA_HOME"]
205205
or not os.path.isdir(os.environ["JAVA_HOME"])
206206
):
207-
208207
_logger.debug("JAVA_HOME not set. Will try to infer it from sys.path.")
209208

210209
libjvm_win = Path("Library") / "jre" / "bin" / "server" / "jvm.dll"

src/scyjava/_script.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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.Supplier")
58+
class PythonObjectSupplier:
59+
def __init__(self, obj):
60+
self.obj = obj
61+
62+
@JOverride
63+
def get(self):
64+
return self.obj
65+
66+
@JImplements("java.util.function.Function")
67+
class PythonScriptRunner:
68+
@JOverride
69+
def apply(self, arg):
70+
# Copy script bindings/vars into script locals.
71+
script_locals = {}
72+
for key in arg.vars.keys():
73+
script_locals[key] = arg.vars[key]
74+
75+
stdoutContextWriter.addScriptContext(
76+
threading.currentThread(), arg.scriptContext
77+
)
78+
79+
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
100+
)
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()))
105+
106+
stdoutContextWriter.removeScriptContext(threading.currentThread())
107+
108+
# Copy script locals back into script bindings/vars.
109+
for key in script_locals.keys():
110+
try:
111+
arg.vars[key] = to_java(script_locals[key])
112+
except Exception:
113+
arg.vars[key] = PythonObjectSupplier(script_locals[key])
114+
# error_writer = arg.scriptContext.getErrorWriter()
115+
# if error_writer is not None:
116+
# error_writer.write(to_java(traceback.format_exc()))
117+
118+
return to_java(return_value)
119+
120+
objectService = context.service(ObjectService)
121+
objectService.addObject(PythonScriptRunner(), "PythonScriptRunner")

0 commit comments

Comments
 (0)