From f55d40f55c694bd610948990e5f3c208715b498d Mon Sep 17 00:00:00 2001 From: ian-coccimiglio Date: Thu, 20 Mar 2025 16:00:40 -0700 Subject: [PATCH 1/3] Fix python scripting to allow global imports and added testfile --- src/scyjava/_script.py | 3 +- tests/it/script_scope.py | 61 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 tests/it/script_scope.py diff --git a/src/scyjava/_script.py b/src/scyjava/_script.py index ec73f906..7ce10d60 100644 --- a/src/scyjava/_script.py +++ b/src/scyjava/_script.py @@ -91,7 +91,8 @@ def apply(self, arg): # Last statement looks like an expression. Evaluate! last = ast.Expression(block.body.pop().value) - _globals = {} + _globals = {name: module for name, module in sys.modules.items() if name != '__main__'} + exec( compile(block, "", mode="exec"), _globals, script_locals ) diff --git a/tests/it/script_scope.py b/tests/it/script_scope.py new file mode 100644 index 00000000..9965d7f1 --- /dev/null +++ b/tests/it/script_scope.py @@ -0,0 +1,61 @@ +""" +Test the enable_python_scripting function, but here explictly testing import scope for declared functions. +""" + +import sys + +import scyjava + +scyjava.config.endpoints.extend( + ["org.scijava:scijava-common:2.94.2", "org.scijava:scripting-python:MANAGED"] +) + +# Create minimal SciJava context with a ScriptService. +Context = scyjava.jimport("org.scijava.Context") +ScriptService = scyjava.jimport("org.scijava.script.ScriptService") +# HACK: Avoid "[ERROR] Cannot create plugin" spam. +WidgetService = scyjava.jimport("org.scijava.widget.WidgetService") +ctx = Context(ScriptService, WidgetService) + +# Enable the Python script language. +scyjava.enable_python_scripting(ctx) + +# Assert that the Python script language is available. +ss = ctx.service("org.scijava.script.ScriptService") +lang = ss.getLanguageByName("Python") +assert lang is not None and "Python" in lang.getNames() + +# Construct a script. +script = """ +#@ int age +#@output String cbrt_age +import math + +def calculate_cbrt(age): + return round(math.cbrt(age)) + +cbrt_age = calculate_cbrt(age) +# cbrt_age = round(math.cbrt(age)) +f"The rounded cube root of my age is {cbrt_age}" +""" +StringReader = scyjava.jimport("java.io.StringReader") +ScriptInfo = scyjava.jimport("org.scijava.script.ScriptInfo") +info = ScriptInfo(ctx, "script.py", StringReader(script)) +info.setLanguage(lang) + +# Run the script. +future = ss.run(info, True, "age", 13) +try: + module = future.get() + outputs = module.getOutputs() + statement = outputs["cbrt_age"] + return_value = module.getReturnValue() +except Exception as e: + sys.stderr.write("-- SCRIPT EXECUTION FAILED --\n") + trace = scyjava.jstacktrace(e) + if trace: + sys.stderr.write(f"{trace}\n") + raise e + +assert return_value == "The rounded cube root of my age is 2" +assert statement == "2" From e260032af6f47fa2e440628e66f9d54413d8f6f6 Mon Sep 17 00:00:00 2001 From: ian-coccimiglio Date: Fri, 21 Mar 2025 00:26:07 -0700 Subject: [PATCH 2/3] Updated tests, fixed scoping issues --- src/scyjava/_script.py | 12 ++++++++---- tests/it/script_scope.py | 3 ++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/scyjava/_script.py b/src/scyjava/_script.py index 7ce10d60..0ba1b280 100644 --- a/src/scyjava/_script.py +++ b/src/scyjava/_script.py @@ -91,15 +91,19 @@ def apply(self, arg): # Last statement looks like an expression. Evaluate! last = ast.Expression(block.body.pop().value) - _globals = {name: module for name, module in sys.modules.items() if name != '__main__'} - + # _globals = {name: module for name, module in sys.modules.items() if name != '__main__'} + # _globals = {__builtins__: builtins, '__name__': '__main__','__file__': '', '__package__': None,} + # _globals.update(globals()) + # _globals = None + # _globals = locals() + script_globals = script_locals exec( - compile(block, "", mode="exec"), _globals, script_locals + compile(block, "", mode="exec"), script_globals, script_locals ) if last is not None: return_value = eval( compile(last, "", mode="eval"), - _globals, + script_globals, script_locals, ) except Exception: diff --git a/tests/it/script_scope.py b/tests/it/script_scope.py index 9965d7f1..2374fb8f 100644 --- a/tests/it/script_scope.py +++ b/tests/it/script_scope.py @@ -29,6 +29,7 @@ script = """ #@ int age #@output String cbrt_age +import numpy as np import math def calculate_cbrt(age): @@ -57,5 +58,5 @@ def calculate_cbrt(age): sys.stderr.write(f"{trace}\n") raise e -assert return_value == "The rounded cube root of my age is 2" assert statement == "2" +assert return_value == "The rounded cube root of my age is 2" From e02e167e8ca59b2889e69fbe12edf4496ffb2332 Mon Sep 17 00:00:00 2001 From: ian-coccimiglio Date: Fri, 21 Mar 2025 00:32:55 -0700 Subject: [PATCH 3/3] Added documentation --- src/scyjava/_script.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/scyjava/_script.py b/src/scyjava/_script.py index 0ba1b280..2d86cd8e 100644 --- a/src/scyjava/_script.py +++ b/src/scyjava/_script.py @@ -90,20 +90,18 @@ def apply(self, arg): ): # Last statement looks like an expression. Evaluate! last = ast.Expression(block.body.pop().value) - - # _globals = {name: module for name, module in sys.modules.items() if name != '__main__'} - # _globals = {__builtins__: builtins, '__name__': '__main__','__file__': '', '__package__': None,} - # _globals.update(globals()) - # _globals = None - # _globals = locals() - script_globals = script_locals + # See here for why this implementation: https://docs.python.org/3/library/functions.html#exec + # When `exec` gets two separate objects as *globals* and *locals*, the code will be executed as if it were embedded in a class definition. + # This means functions and classes defined in the executed code will not be able to access variables assigned at the top level + # (as the “top level” variables are treated as class variables in a class definition). + _globals = script_locals exec( - compile(block, "", mode="exec"), script_globals, script_locals + compile(block, "", mode="exec"), _globals, script_locals ) if last is not None: return_value = eval( compile(last, "", mode="eval"), - script_globals, + _globals, script_locals, ) except Exception: