From c8269bf0259d11eb34d3738ab2e5e9009c1979ac Mon Sep 17 00:00:00 2001 From: Mark Hiner Date: Sat, 23 Aug 2025 15:58:56 -0500 Subject: [PATCH 01/13] Rename script language to Python (pyimagej) Closes #14 --- .../plugins/scripting/python/PythonScriptLanguage.java | 5 +++-- .../scripting/python/PythonScriptSyntaxHighlighter.java | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/scijava/plugins/scripting/python/PythonScriptLanguage.java b/src/main/java/org/scijava/plugins/scripting/python/PythonScriptLanguage.java index 3896bb6..5d583e8 100644 --- a/src/main/java/org/scijava/plugins/scripting/python/PythonScriptLanguage.java +++ b/src/main/java/org/scijava/plugins/scripting/python/PythonScriptLanguage.java @@ -31,6 +31,7 @@ import java.util.Arrays; import java.util.List; + import javax.script.ScriptEngine; import org.scijava.Priority; @@ -39,13 +40,13 @@ import org.scijava.script.ScriptLanguage; /** - * An adapter for Python (scyjava) to the SciJava scripting interface. + * An adapter for Python (pyimagej) to the SciJava scripting interface. * * @author Curtis Rueden * @author Karl Duderstadt * @see ScriptEngine */ -@Plugin(type = ScriptLanguage.class, name = "Python (scyjava)", priority = Priority.VERY_LOW) +@Plugin(type = ScriptLanguage.class, name = "Python (pyimagej)", priority = Priority.VERY_LOW) public class PythonScriptLanguage extends AbstractScriptLanguage { @Override diff --git a/src/main/java/org/scijava/plugins/scripting/python/PythonScriptSyntaxHighlighter.java b/src/main/java/org/scijava/plugins/scripting/python/PythonScriptSyntaxHighlighter.java index ee7e413..b1a20df 100644 --- a/src/main/java/org/scijava/plugins/scripting/python/PythonScriptSyntaxHighlighter.java +++ b/src/main/java/org/scijava/plugins/scripting/python/PythonScriptSyntaxHighlighter.java @@ -38,7 +38,7 @@ * * @author Karl Duderstadt */ -@Plugin(type = SyntaxHighlighter.class, name = "python-(scyjava)") +@Plugin(type = SyntaxHighlighter.class, name = "python-(pyimagej)") public class PythonScriptSyntaxHighlighter extends PythonTokenMaker implements SyntaxHighlighter { From 6d9b831f734380db7fefa57e15ca9d6cfc868a76 Mon Sep 17 00:00:00 2001 From: Mark Hiner Date: Sat, 23 Aug 2025 16:16:40 -0500 Subject: [PATCH 02/13] ignore vscode --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index d1a3e9b..ae177d7 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /.project /.settings/ /target +/.vscode/ From 6427cde37df9cfab64ef324034291a11277fc12b Mon Sep 17 00:00:00 2001 From: Mark Hiner Date: Sat, 23 Aug 2025 16:22:00 -0500 Subject: [PATCH 03/13] Format code with mvn formatter:format --- .../plugins/scripting/python/Main.java | 5 +- .../scripting/python/OptionsPython.java | 63 ++++++++++--------- .../scripting/python/PythonScriptEngine.java | 25 +++++--- .../python/PythonScriptLanguage.java | 9 +-- .../python/PythonScriptSyntaxHighlighter.java | 6 +- .../scripting/python/RebuildEnvironment.java | 32 +++++----- 6 files changed, 74 insertions(+), 66 deletions(-) diff --git a/src/main/java/org/scijava/plugins/scripting/python/Main.java b/src/main/java/org/scijava/plugins/scripting/python/Main.java index a3c067d..10f1598 100644 --- a/src/main/java/org/scijava/plugins/scripting/python/Main.java +++ b/src/main/java/org/scijava/plugins/scripting/python/Main.java @@ -6,13 +6,13 @@ * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE @@ -26,6 +26,7 @@ * POSSIBILITY OF SUCH DAMAGE. * #L% */ + package org.scijava.plugins.scripting.python; import org.scijava.script.ScriptREPL; diff --git a/src/main/java/org/scijava/plugins/scripting/python/OptionsPython.java b/src/main/java/org/scijava/plugins/scripting/python/OptionsPython.java index cc65a15..1c8f234 100644 --- a/src/main/java/org/scijava/plugins/scripting/python/OptionsPython.java +++ b/src/main/java/org/scijava/plugins/scripting/python/OptionsPython.java @@ -6,13 +6,13 @@ * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE @@ -29,6 +29,13 @@ package org.scijava.plugins.scripting.python; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.LinkedHashMap; +import java.util.Map; + import org.scijava.app.AppService; import org.scijava.command.CommandService; import org.scijava.launcher.Config; @@ -40,25 +47,15 @@ import org.scijava.plugin.Plugin; import org.scijava.widget.Button; -import java.io.File; -import java.io.IOException; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.LinkedHashMap; -import java.util.Map; - /** * Options for configuring the Python environment. - * + * * @author Curtis Rueden */ -@Plugin(type = OptionsPlugin.class, menu = { - @Menu(label = MenuConstants.EDIT_LABEL, - weight = MenuConstants.EDIT_WEIGHT, - mnemonic = MenuConstants.EDIT_MNEMONIC), - @Menu(label = "Options", mnemonic = 'o'), - @Menu(label = "Python...", weight = 10), -}) +@Plugin(type = OptionsPlugin.class, menu = { @Menu( + label = MenuConstants.EDIT_LABEL, weight = MenuConstants.EDIT_WEIGHT, + mnemonic = MenuConstants.EDIT_MNEMONIC), @Menu(label = "Options", + mnemonic = 'o'), @Menu(label = "Python...", weight = 10), }) public class OptionsPython extends OptionsPlugin { @Parameter @@ -76,7 +73,8 @@ public class OptionsPython extends OptionsPlugin { @Parameter(label = "Rebuild Python environment", callback = "rebuildEnv") private Button rebuildEnvironment; - @Parameter(label = "Launch in Python mode", callback = "updatePythonConfig", persist = false) + @Parameter(label = "Launch in Python mode", callback = "updatePythonConfig", + persist = false) private boolean pythonMode; // -- OptionsPython methods -- @@ -124,11 +122,14 @@ public void load() { } if (pythonDir == null) { - // For the default Python directory, try to match the platform string used for Java installations. - final String javaPlatform = System.getProperty("scijava.app.java-platform"); - final String platform = javaPlatform != null ? javaPlatform : - System.getProperty("os.name") + "-" + System.getProperty("os.arch"); - final Path pythonPath = appService.getApp().getBaseDirectory().toPath().resolve("python").resolve(platform); + // For the default Python directory, try to match the platform + // string used for Java installations. + final String javaPlatform = System.getProperty( + "scijava.app.java-platform"); + final String platform = javaPlatform != null ? javaPlatform : System + .getProperty("os.name") + "-" + System.getProperty("os.arch"); + final Path pythonPath = appService.getApp().getBaseDirectory().toPath() + .resolve("python").resolve(platform); pythonDir = pythonPath.toFile(); } } @@ -136,16 +137,16 @@ public void load() { public void rebuildEnv() { // Use scijava.app.python-env-file system property if present. final Path appPath = appService.getApp().getBaseDirectory().toPath(); - File environmentYaml = appPath.resolve("config").resolve("environment.yml").toFile(); - final String pythonEnvFileProp = System.getProperty("scijava.app.python-env-file"); + File environmentYaml = appPath.resolve("config").resolve("environment.yml") + .toFile(); + final String pythonEnvFileProp = System.getProperty( + "scijava.app.python-env-file"); if (pythonEnvFileProp != null) { environmentYaml = OptionsPython.stringToFile(appPath, pythonEnvFileProp); } - commandService.run(RebuildEnvironment.class, true, - "environmentYaml", environmentYaml, - "targetDir", pythonDir - ); + commandService.run(RebuildEnvironment.class, true, "environmentYaml", + environmentYaml, "targetDir", pythonDir); } @Override @@ -195,8 +196,8 @@ static File stringToFile(Path baseDir, String value) { */ static String fileToString(Path baseDir, File file) { Path filePath = file.toPath(); - Path relPath = filePath.startsWith(baseDir) ? - baseDir.relativize(filePath) : filePath.toAbsolutePath(); + Path relPath = filePath.startsWith(baseDir) ? baseDir.relativize(filePath) + : filePath.toAbsolutePath(); return relPath.toString(); } } diff --git a/src/main/java/org/scijava/plugins/scripting/python/PythonScriptEngine.java b/src/main/java/org/scijava/plugins/scripting/python/PythonScriptEngine.java index 751bdee..4ad026c 100644 --- a/src/main/java/org/scijava/plugins/scripting/python/PythonScriptEngine.java +++ b/src/main/java/org/scijava/plugins/scripting/python/PythonScriptEngine.java @@ -6,13 +6,13 @@ * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE @@ -61,10 +61,10 @@ public class PythonScriptEngine extends AbstractScriptEngine { @Parameter private ObjectService objectService; - + @Parameter private LogService logService; - + public PythonScriptEngine(final Context context) { context.inject(this); setLogService(logService); @@ -83,19 +83,21 @@ public Object eval(final String script) throws ScriptException { "Python script engine, you must call scyjava.enable_scijava_scripting(context)\n" + "with this script engine's associated SciJava context before using it."); } - return pythonScriptRunner.get().apply(new Args(script, engineScopeBindings, scriptContext)); + return pythonScriptRunner.get().apply(new Args(script, engineScopeBindings, + scriptContext)); } @Override public Object eval(Reader reader) throws ScriptException { StringBuilder buf = new StringBuilder(); - char [] cbuf = new char [65536]; + char[] cbuf = new char[65536]; while (true) { try { int nChars = reader.read(cbuf); if (nChars <= 0) break; buf.append(cbuf, 0, nChars); - } catch (IOException e) { + } + catch (IOException e) { throw new ScriptException(e); } } @@ -106,8 +108,8 @@ public Object eval(Reader reader) throws ScriptException { public Bindings createBindings() { return new ScriptBindings(); } - - //Somehow just type casting did not work... + + // Somehow just type casting did not work... private static class ScriptBindings implements Bindings { private Map bindingsMap; @@ -178,11 +180,14 @@ public Object remove(Object key) { } private static class Args { + public final String script; public final Map vars; public final ScriptContext scriptContext; - public Args(final String script, final Map vars, final ScriptContext scriptContext) { + public Args(final String script, final Map vars, + final ScriptContext scriptContext) + { this.script = script; this.vars = vars; this.scriptContext = scriptContext; diff --git a/src/main/java/org/scijava/plugins/scripting/python/PythonScriptLanguage.java b/src/main/java/org/scijava/plugins/scripting/python/PythonScriptLanguage.java index 5d583e8..f1c4417 100644 --- a/src/main/java/org/scijava/plugins/scripting/python/PythonScriptLanguage.java +++ b/src/main/java/org/scijava/plugins/scripting/python/PythonScriptLanguage.java @@ -6,13 +6,13 @@ * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE @@ -46,7 +46,8 @@ * @author Karl Duderstadt * @see ScriptEngine */ -@Plugin(type = ScriptLanguage.class, name = "Python (pyimagej)", priority = Priority.VERY_LOW) +@Plugin(type = ScriptLanguage.class, name = "Python (pyimagej)", + priority = Priority.VERY_LOW) public class PythonScriptLanguage extends AbstractScriptLanguage { @Override @@ -63,5 +64,5 @@ public List getExtensions() { public ScriptEngine getScriptEngine() { return new PythonScriptEngine(getContext()); } - + } diff --git a/src/main/java/org/scijava/plugins/scripting/python/PythonScriptSyntaxHighlighter.java b/src/main/java/org/scijava/plugins/scripting/python/PythonScriptSyntaxHighlighter.java index b1a20df..d9452df 100644 --- a/src/main/java/org/scijava/plugins/scripting/python/PythonScriptSyntaxHighlighter.java +++ b/src/main/java/org/scijava/plugins/scripting/python/PythonScriptSyntaxHighlighter.java @@ -6,13 +6,13 @@ * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE @@ -40,7 +40,7 @@ */ @Plugin(type = SyntaxHighlighter.class, name = "python-(pyimagej)") public class PythonScriptSyntaxHighlighter extends PythonTokenMaker implements -SyntaxHighlighter + SyntaxHighlighter { // Everything implemented in PythonTokenMaker } diff --git a/src/main/java/org/scijava/plugins/scripting/python/RebuildEnvironment.java b/src/main/java/org/scijava/plugins/scripting/python/RebuildEnvironment.java index 6a61a63..ba704ca 100644 --- a/src/main/java/org/scijava/plugins/scripting/python/RebuildEnvironment.java +++ b/src/main/java/org/scijava/plugins/scripting/python/RebuildEnvironment.java @@ -6,13 +6,13 @@ * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE @@ -29,6 +29,13 @@ package org.scijava.plugins.scripting.python; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.stream.Stream; + import org.apposed.appose.Appose; import org.apposed.appose.Builder; import org.scijava.app.AppService; @@ -38,16 +45,9 @@ import org.scijava.plugin.Parameter; import org.scijava.plugin.Plugin; -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Comparator; -import java.util.stream.Stream; - /** * SciJava command wrapper to build a Python environment. - * + * * @author Curtis Rueden */ @Plugin(type = Command.class, label = "Rebuild Python environment") @@ -90,12 +90,12 @@ public void run() { if (targetDir.exists()) targetDir.renameTo(backupDir); // Build the new environment. try { - Builder builder = Appose - .file(environmentYaml, "environment.yml") - .subscribeOutput(this::report) - .subscribeError(this::report) - .subscribeProgress((msg, cur, max) -> Splash.update(msg, (double) cur / max)); - System.err.println("Building Python environment"); // HACK: stderr stream triggers console window show. + Builder builder = Appose.file(environmentYaml, "environment.yml") + .subscribeOutput(this::report).subscribeError(this::report) + .subscribeProgress((msg, cur, max) -> Splash.update(msg, (double) cur / + max)); + System.err.println("Building Python environment"); + // HACK: stderr stream triggers console window show. Splash.show(); builder.build(targetDir); } From 409316b22fcc065aaf4efeaf3af845091517a358 Mon Sep 17 00:00:00 2001 From: Mark Hiner Date: Sat, 23 Aug 2025 16:40:00 -0500 Subject: [PATCH 04/13] Rename "Rebuild Python env" button to "Build...*" Rebuild implies to me it already exists, which it may not --- .../org/scijava/plugins/scripting/python/OptionsPython.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/scijava/plugins/scripting/python/OptionsPython.java b/src/main/java/org/scijava/plugins/scripting/python/OptionsPython.java index 1c8f234..fce9f6a 100644 --- a/src/main/java/org/scijava/plugins/scripting/python/OptionsPython.java +++ b/src/main/java/org/scijava/plugins/scripting/python/OptionsPython.java @@ -70,7 +70,7 @@ public class OptionsPython extends OptionsPlugin { @Parameter(label = "Python environment directory", persist = false) private File pythonDir; - @Parameter(label = "Rebuild Python environment", callback = "rebuildEnv") + @Parameter(label = "Build Python environment", callback = "rebuildEnv") private Button rebuildEnvironment; @Parameter(label = "Launch in Python mode", callback = "updatePythonConfig", From 4058aeaa8bafeb3a81ce0c78c4902f9d1ae98620 Mon Sep 17 00:00:00 2001 From: Mark Hiner Date: Sat, 23 Aug 2025 16:46:14 -0500 Subject: [PATCH 05/13] Notify user about backing up existing env --- .../scripting/python/RebuildEnvironment.java | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/scijava/plugins/scripting/python/RebuildEnvironment.java b/src/main/java/org/scijava/plugins/scripting/python/RebuildEnvironment.java index ba704ca..0c570e2 100644 --- a/src/main/java/org/scijava/plugins/scripting/python/RebuildEnvironment.java +++ b/src/main/java/org/scijava/plugins/scripting/python/RebuildEnvironment.java @@ -44,6 +44,8 @@ import org.scijava.log.Logger; import org.scijava.plugin.Parameter; import org.scijava.plugin.Plugin; +import org.scijava.ui.DialogPrompt; +import org.scijava.ui.UIService; /** * SciJava command wrapper to build a Python environment. @@ -65,13 +67,31 @@ public class RebuildEnvironment implements Command { @Parameter(label = "Target directory") private File targetDir; + @Parameter(required = false) + private UIService uiService; + // -- OptionsPython methods -- @Override public void run() { final File backupDir = new File(targetDir.getPath() + ".old"); + if (targetDir.exists()) { + boolean confirmed = true; + if (uiService != null) { + String msg = + "The environment directory already exists. If you continue, it will be renamed to '" + + backupDir.getName() + + "' (and any previous backup will be deleted). Continue?"; + DialogPrompt.Result result = uiService.showDialog(msg, + "Confirm Environment Rebuild", + DialogPrompt.MessageType.QUESTION_MESSAGE, + DialogPrompt.OptionType.YES_NO_OPTION); + confirmed = result == DialogPrompt.Result.YES_OPTION; + } + if (!confirmed) return; + } + // Delete the previous backup environment recursively. if (backupDir.exists()) { - // Delete the previous backup environment recursively. try (Stream x = Files.walk(backupDir.toPath())) { x.sorted(Comparator.reverseOrder()).forEach(p -> { try { From a5b9075cbcf1918764127e061c6d188d25c3ce88 Mon Sep 17 00:00:00 2001 From: Mark Hiner Date: Sat, 23 Aug 2025 16:53:50 -0500 Subject: [PATCH 06/13] Notify user how to reset python mode In case python mode fails Closes #7 Closes #4 --- .../scripting/python/OptionsPython.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/main/java/org/scijava/plugins/scripting/python/OptionsPython.java b/src/main/java/org/scijava/plugins/scripting/python/OptionsPython.java index fce9f6a..4d955d6 100644 --- a/src/main/java/org/scijava/plugins/scripting/python/OptionsPython.java +++ b/src/main/java/org/scijava/plugins/scripting/python/OptionsPython.java @@ -45,6 +45,8 @@ import org.scijava.plugin.Menu; import org.scijava.plugin.Parameter; import org.scijava.plugin.Plugin; +import org.scijava.ui.DialogPrompt; +import org.scijava.ui.UIService; import org.scijava.widget.Button; /** @@ -77,6 +79,11 @@ public class OptionsPython extends OptionsPlugin { persist = false) private boolean pythonMode; + @Parameter(required = false) + private UIService uiService; + + private boolean initialPythonMode = false; + // -- OptionsPython methods -- public File getPythonDir() { @@ -132,6 +139,9 @@ public void load() { .resolve("python").resolve(platform); pythonDir = pythonPath.toFile(); } + + // Store the initial value of pythonMode for later comparison + initialPythonMode = pythonMode; } public void rebuildEnv() { @@ -176,6 +186,16 @@ public void save() { // Proceed gracefully if config file cannot be written. log.debug(exc); } + + // Warn the user if pythonMode was just enabled and wasn't before + if (!initialPythonMode && pythonMode && uiService != null) { + String msg = + "You have just enabled Python mode. Please restart for these changes to take effect!\n\n" + + "If Fiji fails to start, try deleting your configuration file and restarting.\n\nConfiguration file: " + + configFile; + uiService.showDialog(msg, "Python Mode Enabled", + DialogPrompt.MessageType.WARNING_MESSAGE); + } } // -- Utility methods -- From bf9ef5471c0260b35936b6751b76acf45889ea9b Mon Sep 17 00:00:00 2001 From: Mark Hiner Date: Sat, 23 Aug 2025 17:04:33 -0500 Subject: [PATCH 07/13] Notify user of success when building python env Closes #11 --- .../plugins/scripting/python/RebuildEnvironment.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/scijava/plugins/scripting/python/RebuildEnvironment.java b/src/main/java/org/scijava/plugins/scripting/python/RebuildEnvironment.java index 0c570e2..ecc7b93 100644 --- a/src/main/java/org/scijava/plugins/scripting/python/RebuildEnvironment.java +++ b/src/main/java/org/scijava/plugins/scripting/python/RebuildEnvironment.java @@ -114,10 +114,16 @@ public void run() { .subscribeOutput(this::report).subscribeError(this::report) .subscribeProgress((msg, cur, max) -> Splash.update(msg, (double) cur / max)); - System.err.println("Building Python environment"); // HACK: stderr stream triggers console window show. + System.err.println("Building Python environment"); Splash.show(); builder.build(targetDir); + // Notify user of success + if (uiService != null) { + uiService.showDialog( + "Python environment setup was successful and is ready to use!", + "Environment Ready", DialogPrompt.MessageType.INFORMATION_MESSAGE); + } } catch (IOException exc) { log.error("Failed to build Python environment", exc); From 626ebb2a8727cbf5d403c89559830ab1adcc0c90 Mon Sep 17 00:00:00 2001 From: Mark Hiner Date: Sat, 23 Aug 2025 17:12:11 -0500 Subject: [PATCH 08/13] Auto-build environment when turning on python mode If it doesn't already exist --- .../org/scijava/plugins/scripting/python/OptionsPython.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/scijava/plugins/scripting/python/OptionsPython.java b/src/main/java/org/scijava/plugins/scripting/python/OptionsPython.java index 4d955d6..2225879 100644 --- a/src/main/java/org/scijava/plugins/scripting/python/OptionsPython.java +++ b/src/main/java/org/scijava/plugins/scripting/python/OptionsPython.java @@ -187,10 +187,13 @@ public void save() { log.debug(exc); } + if (pythonMode && (pythonDir == null || !pythonDir.exists())) { + rebuildEnv(); + } // Warn the user if pythonMode was just enabled and wasn't before if (!initialPythonMode && pythonMode && uiService != null) { String msg = - "You have just enabled Python mode. Please restart for these changes to take effect!\n\n" + + "You have just enabled Python mode. Please restart for these changes to take effect! (after your python environment initializes, if needed)\n\n" + "If Fiji fails to start, try deleting your configuration file and restarting.\n\nConfiguration file: " + configFile; uiService.showDialog(msg, "Python Mode Enabled", From efdb4bdaabd3d1ccc12bdce902dffe24321608cf Mon Sep 17 00:00:00 2001 From: Mark Hiner Date: Sat, 23 Aug 2025 17:28:34 -0500 Subject: [PATCH 09/13] Extract env file method --- .../plugins/scripting/python/OptionsPython.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/scijava/plugins/scripting/python/OptionsPython.java b/src/main/java/org/scijava/plugins/scripting/python/OptionsPython.java index 2225879..f35191c 100644 --- a/src/main/java/org/scijava/plugins/scripting/python/OptionsPython.java +++ b/src/main/java/org/scijava/plugins/scripting/python/OptionsPython.java @@ -145,18 +145,25 @@ public void load() { } public void rebuildEnv() { - // Use scijava.app.python-env-file system property if present. + File environmentYaml = getEnvironmentYamlFile(); + commandService.run(RebuildEnvironment.class, true, "environmentYaml", + environmentYaml, "targetDir", pythonDir); + } + + /** + * Returns the File for the environment.yml, using the system property if set. + */ + private File getEnvironmentYamlFile() { final Path appPath = appService.getApp().getBaseDirectory().toPath(); File environmentYaml = appPath.resolve("config").resolve("environment.yml") .toFile(); final String pythonEnvFileProp = System.getProperty( "scijava.app.python-env-file"); if (pythonEnvFileProp != null) { - environmentYaml = OptionsPython.stringToFile(appPath, pythonEnvFileProp); + environmentYaml = stringToFile(appPath, pythonEnvFileProp); } + return environmentYaml; - commandService.run(RebuildEnvironment.class, true, "environmentYaml", - environmentYaml, "targetDir", pythonDir); } @Override From 4f2d9087c80bbb0092e81f096ac53e123d779210 Mon Sep 17 00:00:00 2001 From: Mark Hiner Date: Sat, 23 Aug 2025 17:50:39 -0500 Subject: [PATCH 10/13] Allow environment editing in python options Closes #9 --- .../scripting/python/OptionsPython.java | 122 +++++++++++++++++- 1 file changed, 120 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/scijava/plugins/scripting/python/OptionsPython.java b/src/main/java/org/scijava/plugins/scripting/python/OptionsPython.java index f35191c..4a747dc 100644 --- a/src/main/java/org/scijava/plugins/scripting/python/OptionsPython.java +++ b/src/main/java/org/scijava/plugins/scripting/python/OptionsPython.java @@ -35,6 +35,7 @@ import java.nio.file.Paths; import java.util.LinkedHashMap; import java.util.Map; +import java.util.StringJoiner; import org.scijava.app.AppService; import org.scijava.command.CommandService; @@ -48,6 +49,7 @@ import org.scijava.ui.DialogPrompt; import org.scijava.ui.UIService; import org.scijava.widget.Button; +import org.scijava.widget.TextWidget; /** * Options for configuring the Python environment. @@ -72,6 +74,14 @@ public class OptionsPython extends OptionsPlugin { @Parameter(label = "Python environment directory", persist = false) private File pythonDir; + @Parameter(label = "Conda dependencies", style = TextWidget.AREA_STYLE, + persist = false) + private String condaDependencies; + + @Parameter(label = "Pip dependencies", style = TextWidget.AREA_STYLE, + persist = false) + private String pipDependencies; + @Parameter(label = "Build Python environment", callback = "rebuildEnv") private Button rebuildEnvironment; @@ -83,6 +93,8 @@ public class OptionsPython extends OptionsPlugin { private UIService uiService; private boolean initialPythonMode = false; + private String initialCondaDependencies; + private String initialPipDependencies; // -- OptionsPython methods -- @@ -142,10 +154,70 @@ public void load() { // Store the initial value of pythonMode for later comparison initialPythonMode = pythonMode; + + // Populate condaDependencies and pipDependencies from environment.yml + condaDependencies = ""; + pipDependencies = ""; + java.util.Set pipBlacklist = new java.util.HashSet<>(); + pipBlacklist.add("appose-python"); + pipBlacklist.add("pyimagej"); + File envFile = getEnvironmentYamlFile(); + if (envFile.exists()) { + try { + java.util.List lines = java.nio.file.Files.readAllLines(envFile + .toPath()); + boolean inDeps = false, inPip = false; + StringJoiner condaDeps = new StringJoiner("\n"); + StringJoiner pipDeps = new StringJoiner("\n"); + for (String line : lines) { + String trimmed = line.trim(); + if (trimmed.startsWith("#") || trimmed.isEmpty()) { + // Ignore empty and comment lines + continue; + } + if (trimmed.startsWith("dependencies:")) { + inDeps = true; + continue; + } + if (inDeps && trimmed.startsWith("- pip")) { + inPip = true; + continue; + } + if (inDeps && trimmed.startsWith("- ") && !inPip) { + String dep = trimmed.substring(2).trim(); + if (!dep.equals("pip")) condaDeps.add(dep); + continue; + } + if (inPip && trimmed.startsWith("- ")) { + String pipDep = trimmed.substring(2).trim(); + boolean blacklisted = false; + for (String bad : pipBlacklist) { + if (pipDep.contains(bad)) { + blacklisted = true; + break; + } + } + if (!blacklisted) pipDeps.add(pipDep); + continue; + } + if (inDeps && !trimmed.startsWith("- ") && !trimmed.isEmpty()) + inDeps = false; + if (inPip && (!trimmed.startsWith("- ") || trimmed.isEmpty())) inPip = + false; + } + condaDependencies = condaDeps.toString().trim(); + pipDependencies = pipDeps.toString().trim(); + initialCondaDependencies = condaDependencies; + initialPipDependencies = pipDependencies; + } + catch (Exception e) { + log.debug("Could not read environment.yml: " + e.getMessage()); + } + } } public void rebuildEnv() { - File environmentYaml = getEnvironmentYamlFile(); + File environmentYaml = writeEnvironmentYaml(); commandService.run(RebuildEnvironment.class, true, "environmentYaml", environmentYaml, "targetDir", pythonDir); } @@ -163,7 +235,6 @@ private File getEnvironmentYamlFile() { environmentYaml = stringToFile(appPath, pythonEnvFileProp); } return environmentYaml; - } @Override @@ -197,6 +268,9 @@ public void save() { if (pythonMode && (pythonDir == null || !pythonDir.exists())) { rebuildEnv(); } + else { + writeEnvironmentYaml(); + } // Warn the user if pythonMode was just enabled and wasn't before if (!initialPythonMode && pythonMode && uiService != null) { String msg = @@ -208,6 +282,50 @@ public void save() { } } + private File writeEnvironmentYaml() { + File envFile = getEnvironmentYamlFile(); + + // skip writing if nothing has changed + if (initialCondaDependencies.equals(condaDependencies) && + initialPipDependencies.equals(pipDependencies)) return envFile; + + // Update initial dependencies to detect future changes + initialCondaDependencies = condaDependencies; + initialPipDependencies = pipDependencies; + + // Write environment.yml from condaDependencies and pipDependencies + try { + String name = "fiji"; + String[] channels = { "conda-forge" }; + String pyimagej = "pyimagej>=1.7.0"; + String apposePython = + "git+https://github.com/apposed/appose-python.git@efe6dadb2242ca45820fcbb7aeea2096f99f9cb2"; + StringBuilder yml = new StringBuilder(); + yml.append("name: ").append(name).append("\nchannels:\n"); + for (String ch : channels) + yml.append(" - ").append(ch).append("\n"); + yml.append("dependencies:\n"); + for (String dep : condaDependencies.split("\n")) { + String trimmed = dep.trim(); + if (!trimmed.isEmpty()) yml.append(" - ").append(trimmed).append("\n"); + } + yml.append(" - pip\n"); + yml.append(" - pip:\n"); + for (String dep : pipDependencies.split("\n")) { + String trimmed = dep.trim(); + if (!trimmed.isEmpty()) yml.append(" - ").append(trimmed).append( + "\n"); + } + yml.append(" - ").append(pyimagej).append("\n"); + yml.append(" - ").append(apposePython).append("\n"); + java.nio.file.Files.write(envFile.toPath(), yml.toString().getBytes()); + } + catch (Exception e) { + log.debug("Could not write environment.yml: " + e.getMessage()); + } + return envFile; + } + // -- Utility methods -- /** From 21037bf211a7ad30c28b30de783123c1f2933a81 Mon Sep 17 00:00:00 2001 From: Mark Hiner Date: Sat, 23 Aug 2025 18:17:41 -0500 Subject: [PATCH 11/13] Update python text to look friendlier Closes #15 --- .../scripting/python/RebuildEnvironment.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/scijava/plugins/scripting/python/RebuildEnvironment.java b/src/main/java/org/scijava/plugins/scripting/python/RebuildEnvironment.java index ecc7b93..ebc3147 100644 --- a/src/main/java/org/scijava/plugins/scripting/python/RebuildEnvironment.java +++ b/src/main/java/org/scijava/plugins/scripting/python/RebuildEnvironment.java @@ -111,11 +111,13 @@ public void run() { // Build the new environment. try { Builder builder = Appose.file(environmentYaml, "environment.yml") - .subscribeOutput(this::report).subscribeError(this::report) + .subscribeOutput(this::reportMsg).subscribeError(this::reportErr) .subscribeProgress((msg, cur, max) -> Splash.update(msg, (double) cur / max)); + // HACK: stderr stream triggers console window show. - System.err.println("Building Python environment"); + System.err.println(); + log.info("Building Python environment"); Splash.show(); builder.build(targetDir); // Notify user of success @@ -130,8 +132,13 @@ public void run() { } } - private void report(String s) { + private void reportErr(String s) { + if (s.isEmpty()) System.err.print("."); + else log.error(s); + } + + private void reportMsg(String s) { if (s.isEmpty()) System.err.print("."); - else System.err.print(s); + else log.info(s); } } From bc24da14410bb6ea6efebe37eb7d37eb20c0b704 Mon Sep 17 00:00:00 2001 From: Mark Hiner Date: Sat, 23 Aug 2025 18:37:16 -0500 Subject: [PATCH 12/13] Open Python options when scripts fail Closes #13 --- .../scripting/python/PythonScriptEngine.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/scijava/plugins/scripting/python/PythonScriptEngine.java b/src/main/java/org/scijava/plugins/scripting/python/PythonScriptEngine.java index 4ad026c..8abb24b 100644 --- a/src/main/java/org/scijava/plugins/scripting/python/PythonScriptEngine.java +++ b/src/main/java/org/scijava/plugins/scripting/python/PythonScriptEngine.java @@ -44,6 +44,7 @@ import javax.script.ScriptException; import org.scijava.Context; +import org.scijava.command.CommandService; import org.scijava.log.LogService; import org.scijava.object.ObjectService; import org.scijava.plugin.Parameter; @@ -65,6 +66,9 @@ public class PythonScriptEngine extends AbstractScriptEngine { @Parameter private LogService logService; + @Parameter + private CommandService commandService; + public PythonScriptEngine(final Context context) { context.inject(this); setLogService(logService); @@ -78,10 +82,13 @@ public Object eval(final String script) throws ScriptException { .filter(obj -> "PythonScriptRunner".equals(objectService.getName(obj)))// .findFirst(); if (!pythonScriptRunner.isPresent()) { + // Try to help user by running OptionsPython plugin + if (commandService != null) { + commandService.run(OptionsPython.class, true); + } throw new IllegalStateException(// - "The PythonScriptRunner could not be found in the ObjectService. To use the\n" + - "Python script engine, you must call scyjava.enable_scijava_scripting(context)\n" + - "with this script engine's associated SciJava context before using it."); + "The PythonScriptRunner could not be found.\n" + + "To use the Python script engine, you must launch your application in Python mode."); } return pythonScriptRunner.get().apply(new Args(script, engineScopeBindings, scriptContext)); From 4009690aed2b7f2e333e9417581890dddf1b609c Mon Sep 17 00:00:00 2001 From: Mark Hiner Date: Mon, 25 Aug 2025 08:35:16 -0500 Subject: [PATCH 13/13] Add CellposeStardist template Adapted from: https://github.com/imagej/pyimagej/blob/f96da00a37ee7b765c6fd3e94ddd78d14b8e330f/doc/Cellpose-StarDist-Segmentation.ipynb --- .../PyImageJ/CellposeStarDistSegmentation.py | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 src/main/resources/script_templates/PyImageJ/CellposeStarDistSegmentation.py diff --git a/src/main/resources/script_templates/PyImageJ/CellposeStarDistSegmentation.py b/src/main/resources/script_templates/PyImageJ/CellposeStarDistSegmentation.py new file mode 100644 index 0000000..09e8cc7 --- /dev/null +++ b/src/main/resources/script_templates/PyImageJ/CellposeStarDistSegmentation.py @@ -0,0 +1,140 @@ +#@ ImageJ ij + +''' +Note that this script requires a Python environment that includes StarDist and Cellpose +StarDist currently only supports NumPy 1.x, which necessitates using TensorFlow 2.15 or earlier +TensorFlow 2.15 itself requires python 3.11 or earlier + +You can rebuild your Python environment by using: +Edit > Options > Python… + +The following configuration was used to develop this script: + +--Conda dependencies-- +python=3.11 +numpy=1.26.4 + +--Pip dependencies-- +tensorflow==2.15 +cellpose==4.0.6 +stardist==0.9.0 +csbdeep==0.8.0 +''' + +import sys +import imagej.convert as convert +import numpy as np +import matplotlib.pyplot as plt +from cellpose import models +from csbdeep.utils import normalize +from stardist.models import StarDist2D +import scyjava as sj + +def filter_index_image(narr:np.ndarray, min_size:int, max_size:int): + """ + Filter an index image's labels with a pixel size range. + """ + unique = np.unique(narr) + for label in unique: + if label == 0: + # skip the background + continue + + # create a crop for each label + bbox = get_bounding_box(np.where(narr == label)) + bbox_crop = narr[bbox[0]:bbox[2] + 1, bbox[1]:bbox[3] + 1].copy() + bbox_crop[bbox_crop != label] = 0 + + # get the number of pixels in label + bbox_crop = bbox_crop.astype(bool) + label_size = np.sum(bbox_crop) + + if not min_size <= label_size <= max_size: + narr[narr == label] = 0 + + return narr + +def get_bounding_box(indices: np.ndarray): + """ + Get the bounding box coordinates from a the label indices. + """ + # get min and max bounds of indices array + min_row = np.min(indices[0]) + min_col = np.min(indices[1]) + max_row = np.max(indices[0]) + max_col = np.max(indices[1]) + + return (min_row, min_col, max_row, max_col) + +# open image data and convert to Python from Java +#TODO does this connection need to be closed? +data = ij.io().open('https://media.imagej.net/pyimagej/3d/hela_a3g.tif') +xdata = ij.py.from_java(data) + +# show the first channel +ij.ui().show("nucleus", ij.py.to_java(xdata[:, :, 0])) + +# show the second channel +ij.ui().show("cytoplasm", ij.py.to_java(xdata[:, :, 1] * 125)) + +# run StarDist on nuclei channel +model = StarDist2D.from_pretrained('2D_versatile_fluo') +nuc_labels, _ = model.predict_instances(normalize(xdata[:, :, 0])) + +# run Cellpose on cytoplasm (grayscale) +model = models.CellposeModel(gpu=False, model_type='cyto') +ch = [0, 0] +cyto_labels = model.eval(xdata[:, :, 1].data, channels=ch, diameter=72.1) + +# show the stardist results +ij.ui().show("StarDist results", ij.py.to_java(nuc_labels)) +ij.IJ.run("mpl-viridis", ""); + +# show the second channel +ij.ui().show("Cellpose results", ij.py.to_java(cyto_labels[0])) +ij.IJ.run("mpl-viridis", ""); + +# filter the stardist results and display +filter_index_image(nuc_labels, 500, 10000) +ij.ui().show("StarDist filtered", ij.py.to_java(nuc_labels)) +ij.IJ.run("mpl-viridis", ""); + +# ensure ROI Manager exists +rm = ij.RoiManager.getInstance() +if rm is None: + ij.IJ.run("ROI Manager...") + rm = ij.RoiManager.getInstance() + +# Reset the ROI manager +rm.reset() + +# convert to ImgLib2 ROI in a ROITree +nuc_roi_tree = convert.index_img_to_roi_tree(ij, nuc_labels) +cyto_roi_tree = convert.index_img_to_roi_tree(ij, cyto_labels[0]) + +# print the contents of the ROITree (nuclear ROIs) +len(nuc_roi_tree.children()) +for i in range(len(nuc_roi_tree.children())): + print(nuc_roi_tree.children().get(i).data()) + +# display the input data, select channel 2 and enhance the contrast +data_title = "hela_a3g.tif" +ij.ui().show(data_title, data) +imp = ij.WindowManager.getImage(data_title) +imp.setC(2) +ij.IJ.run(imp, "Enhance Contrast", "saturated=0.35") + +# convert a single ImgLib2 roi to a legacy ImageJ ROI with the ConvertService. +imglib_polygon_roi = nuc_roi_tree.children().get(0).data() +ij_polygon_roi = ij.convert().convert(imglib_polygon_roi, sj.jimport('ij.gui.PolygonRoi')) +print(type(ij_polygon_roi)) + +# convert index images to ImageJ ROI in RoiManager +#TODO any way to color the selections? We can use Colors... but it appears to be global and the last one run wins +#ij.IJ.run(imp, "Colors...", "foreground=blue background=black selection=red"); +convert.index_img_to_roi_manager(ij, nuc_labels) +convert.index_img_to_roi_manager(ij, cyto_labels[0]) + +#TODO this pops an unnecessary display at the end but if I don't make it the last line the ROIs don't show +rm.moveRoisToOverlay(imp) +rm.runCommand(imp, "Show All") \ No newline at end of file