Skip to content

Commit b9620f2

Browse files
authored
Merge pull request #16 from scijava/usability-changes
* Enable inline environment editing * Open python settings when running python scripts in non-python mode * Streamline environment building to happen automatically when turning on python mode, and notify user when overwriting an environment * Tell user to restart after turning on python mode, and how to recover if restart fails * Rebrand as PyImageJ
2 parents 4bb7d15 + 4009690 commit b9620f2

File tree

8 files changed

+416
-78
lines changed

8 files changed

+416
-78
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
/.project
44
/.settings/
55
/target
6+
/.vscode/

src/main/java/org/scijava/plugins/scripting/python/Main.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
* %%
77
* Redistribution and use in source and binary forms, with or without
88
* modification, are permitted provided that the following conditions are met:
9-
*
9+
*
1010
* 1. Redistributions of source code must retain the above copyright notice,
1111
* this list of conditions and the following disclaimer.
1212
* 2. Redistributions in binary form must reproduce the above copyright notice,
1313
* this list of conditions and the following disclaimer in the documentation
1414
* and/or other materials provided with the distribution.
15-
*
15+
*
1616
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
1717
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
1818
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@@ -26,6 +26,7 @@
2626
* POSSIBILITY OF SUCH DAMAGE.
2727
* #L%
2828
*/
29+
2930
package org.scijava.plugins.scripting.python;
3031

3132
import org.scijava.script.ScriptREPL;

src/main/java/org/scijava/plugins/scripting/python/OptionsPython.java

Lines changed: 184 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
* %%
77
* Redistribution and use in source and binary forms, with or without
88
* modification, are permitted provided that the following conditions are met:
9-
*
9+
*
1010
* 1. Redistributions of source code must retain the above copyright notice,
1111
* this list of conditions and the following disclaimer.
1212
* 2. Redistributions in binary form must reproduce the above copyright notice,
1313
* this list of conditions and the following disclaimer in the documentation
1414
* and/or other materials provided with the distribution.
15-
*
15+
*
1616
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
1717
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
1818
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@@ -29,6 +29,14 @@
2929

3030
package org.scijava.plugins.scripting.python;
3131

32+
import java.io.File;
33+
import java.io.IOException;
34+
import java.nio.file.Path;
35+
import java.nio.file.Paths;
36+
import java.util.LinkedHashMap;
37+
import java.util.Map;
38+
import java.util.StringJoiner;
39+
3240
import org.scijava.app.AppService;
3341
import org.scijava.command.CommandService;
3442
import org.scijava.launcher.Config;
@@ -38,27 +46,20 @@
3846
import org.scijava.plugin.Menu;
3947
import org.scijava.plugin.Parameter;
4048
import org.scijava.plugin.Plugin;
49+
import org.scijava.ui.DialogPrompt;
50+
import org.scijava.ui.UIService;
4151
import org.scijava.widget.Button;
42-
43-
import java.io.File;
44-
import java.io.IOException;
45-
import java.nio.file.Path;
46-
import java.nio.file.Paths;
47-
import java.util.LinkedHashMap;
48-
import java.util.Map;
52+
import org.scijava.widget.TextWidget;
4953

5054
/**
5155
* Options for configuring the Python environment.
52-
*
56+
*
5357
* @author Curtis Rueden
5458
*/
55-
@Plugin(type = OptionsPlugin.class, menu = {
56-
@Menu(label = MenuConstants.EDIT_LABEL,
57-
weight = MenuConstants.EDIT_WEIGHT,
58-
mnemonic = MenuConstants.EDIT_MNEMONIC),
59-
@Menu(label = "Options", mnemonic = 'o'),
60-
@Menu(label = "Python...", weight = 10),
61-
})
59+
@Plugin(type = OptionsPlugin.class, menu = { @Menu(
60+
label = MenuConstants.EDIT_LABEL, weight = MenuConstants.EDIT_WEIGHT,
61+
mnemonic = MenuConstants.EDIT_MNEMONIC), @Menu(label = "Options",
62+
mnemonic = 'o'), @Menu(label = "Python...", weight = 10), })
6263
public class OptionsPython extends OptionsPlugin {
6364

6465
@Parameter
@@ -73,12 +74,28 @@ public class OptionsPython extends OptionsPlugin {
7374
@Parameter(label = "Python environment directory", persist = false)
7475
private File pythonDir;
7576

76-
@Parameter(label = "Rebuild Python environment", callback = "rebuildEnv")
77+
@Parameter(label = "Conda dependencies", style = TextWidget.AREA_STYLE,
78+
persist = false)
79+
private String condaDependencies;
80+
81+
@Parameter(label = "Pip dependencies", style = TextWidget.AREA_STYLE,
82+
persist = false)
83+
private String pipDependencies;
84+
85+
@Parameter(label = "Build Python environment", callback = "rebuildEnv")
7786
private Button rebuildEnvironment;
7887

79-
@Parameter(label = "Launch in Python mode", callback = "updatePythonConfig", persist = false)
88+
@Parameter(label = "Launch in Python mode", callback = "updatePythonConfig",
89+
persist = false)
8090
private boolean pythonMode;
8191

92+
@Parameter(required = false)
93+
private UIService uiService;
94+
95+
private boolean initialPythonMode = false;
96+
private String initialCondaDependencies;
97+
private String initialPipDependencies;
98+
8299
// -- OptionsPython methods --
83100

84101
public File getPythonDir() {
@@ -124,28 +141,100 @@ public void load() {
124141
}
125142

126143
if (pythonDir == null) {
127-
// For the default Python directory, try to match the platform string used for Java installations.
128-
final String javaPlatform = System.getProperty("scijava.app.java-platform");
129-
final String platform = javaPlatform != null ? javaPlatform :
130-
System.getProperty("os.name") + "-" + System.getProperty("os.arch");
131-
final Path pythonPath = appService.getApp().getBaseDirectory().toPath().resolve("python").resolve(platform);
144+
// For the default Python directory, try to match the platform
145+
// string used for Java installations.
146+
final String javaPlatform = System.getProperty(
147+
"scijava.app.java-platform");
148+
final String platform = javaPlatform != null ? javaPlatform : System
149+
.getProperty("os.name") + "-" + System.getProperty("os.arch");
150+
final Path pythonPath = appService.getApp().getBaseDirectory().toPath()
151+
.resolve("python").resolve(platform);
132152
pythonDir = pythonPath.toFile();
133153
}
154+
155+
// Store the initial value of pythonMode for later comparison
156+
initialPythonMode = pythonMode;
157+
158+
// Populate condaDependencies and pipDependencies from environment.yml
159+
condaDependencies = "";
160+
pipDependencies = "";
161+
java.util.Set<String> pipBlacklist = new java.util.HashSet<>();
162+
pipBlacklist.add("appose-python");
163+
pipBlacklist.add("pyimagej");
164+
File envFile = getEnvironmentYamlFile();
165+
if (envFile.exists()) {
166+
try {
167+
java.util.List<String> lines = java.nio.file.Files.readAllLines(envFile
168+
.toPath());
169+
boolean inDeps = false, inPip = false;
170+
StringJoiner condaDeps = new StringJoiner("\n");
171+
StringJoiner pipDeps = new StringJoiner("\n");
172+
for (String line : lines) {
173+
String trimmed = line.trim();
174+
if (trimmed.startsWith("#") || trimmed.isEmpty()) {
175+
// Ignore empty and comment lines
176+
continue;
177+
}
178+
if (trimmed.startsWith("dependencies:")) {
179+
inDeps = true;
180+
continue;
181+
}
182+
if (inDeps && trimmed.startsWith("- pip")) {
183+
inPip = true;
184+
continue;
185+
}
186+
if (inDeps && trimmed.startsWith("- ") && !inPip) {
187+
String dep = trimmed.substring(2).trim();
188+
if (!dep.equals("pip")) condaDeps.add(dep);
189+
continue;
190+
}
191+
if (inPip && trimmed.startsWith("- ")) {
192+
String pipDep = trimmed.substring(2).trim();
193+
boolean blacklisted = false;
194+
for (String bad : pipBlacklist) {
195+
if (pipDep.contains(bad)) {
196+
blacklisted = true;
197+
break;
198+
}
199+
}
200+
if (!blacklisted) pipDeps.add(pipDep);
201+
continue;
202+
}
203+
if (inDeps && !trimmed.startsWith("- ") && !trimmed.isEmpty())
204+
inDeps = false;
205+
if (inPip && (!trimmed.startsWith("- ") || trimmed.isEmpty())) inPip =
206+
false;
207+
}
208+
condaDependencies = condaDeps.toString().trim();
209+
pipDependencies = pipDeps.toString().trim();
210+
initialCondaDependencies = condaDependencies;
211+
initialPipDependencies = pipDependencies;
212+
}
213+
catch (Exception e) {
214+
log.debug("Could not read environment.yml: " + e.getMessage());
215+
}
216+
}
134217
}
135218

136219
public void rebuildEnv() {
137-
// Use scijava.app.python-env-file system property if present.
220+
File environmentYaml = writeEnvironmentYaml();
221+
commandService.run(RebuildEnvironment.class, true, "environmentYaml",
222+
environmentYaml, "targetDir", pythonDir);
223+
}
224+
225+
/**
226+
* Returns the File for the environment.yml, using the system property if set.
227+
*/
228+
private File getEnvironmentYamlFile() {
138229
final Path appPath = appService.getApp().getBaseDirectory().toPath();
139-
File environmentYaml = appPath.resolve("config").resolve("environment.yml").toFile();
140-
final String pythonEnvFileProp = System.getProperty("scijava.app.python-env-file");
230+
File environmentYaml = appPath.resolve("config").resolve("environment.yml")
231+
.toFile();
232+
final String pythonEnvFileProp = System.getProperty(
233+
"scijava.app.python-env-file");
141234
if (pythonEnvFileProp != null) {
142-
environmentYaml = OptionsPython.stringToFile(appPath, pythonEnvFileProp);
235+
environmentYaml = stringToFile(appPath, pythonEnvFileProp);
143236
}
144-
145-
commandService.run(RebuildEnvironment.class, true,
146-
"environmentYaml", environmentYaml,
147-
"targetDir", pythonDir
148-
);
237+
return environmentYaml;
149238
}
150239

151240
@Override
@@ -175,6 +264,66 @@ public void save() {
175264
// Proceed gracefully if config file cannot be written.
176265
log.debug(exc);
177266
}
267+
268+
if (pythonMode && (pythonDir == null || !pythonDir.exists())) {
269+
rebuildEnv();
270+
}
271+
else {
272+
writeEnvironmentYaml();
273+
}
274+
// Warn the user if pythonMode was just enabled and wasn't before
275+
if (!initialPythonMode && pythonMode && uiService != null) {
276+
String msg =
277+
"You have just enabled Python mode. Please restart for these changes to take effect! (after your python environment initializes, if needed)\n\n" +
278+
"If Fiji fails to start, try deleting your configuration file and restarting.\n\nConfiguration file: " +
279+
configFile;
280+
uiService.showDialog(msg, "Python Mode Enabled",
281+
DialogPrompt.MessageType.WARNING_MESSAGE);
282+
}
283+
}
284+
285+
private File writeEnvironmentYaml() {
286+
File envFile = getEnvironmentYamlFile();
287+
288+
// skip writing if nothing has changed
289+
if (initialCondaDependencies.equals(condaDependencies) &&
290+
initialPipDependencies.equals(pipDependencies)) return envFile;
291+
292+
// Update initial dependencies to detect future changes
293+
initialCondaDependencies = condaDependencies;
294+
initialPipDependencies = pipDependencies;
295+
296+
// Write environment.yml from condaDependencies and pipDependencies
297+
try {
298+
String name = "fiji";
299+
String[] channels = { "conda-forge" };
300+
String pyimagej = "pyimagej>=1.7.0";
301+
String apposePython =
302+
"git+https://github.com/apposed/appose-python.git@efe6dadb2242ca45820fcbb7aeea2096f99f9cb2";
303+
StringBuilder yml = new StringBuilder();
304+
yml.append("name: ").append(name).append("\nchannels:\n");
305+
for (String ch : channels)
306+
yml.append(" - ").append(ch).append("\n");
307+
yml.append("dependencies:\n");
308+
for (String dep : condaDependencies.split("\n")) {
309+
String trimmed = dep.trim();
310+
if (!trimmed.isEmpty()) yml.append(" - ").append(trimmed).append("\n");
311+
}
312+
yml.append(" - pip\n");
313+
yml.append(" - pip:\n");
314+
for (String dep : pipDependencies.split("\n")) {
315+
String trimmed = dep.trim();
316+
if (!trimmed.isEmpty()) yml.append(" - ").append(trimmed).append(
317+
"\n");
318+
}
319+
yml.append(" - ").append(pyimagej).append("\n");
320+
yml.append(" - ").append(apposePython).append("\n");
321+
java.nio.file.Files.write(envFile.toPath(), yml.toString().getBytes());
322+
}
323+
catch (Exception e) {
324+
log.debug("Could not write environment.yml: " + e.getMessage());
325+
}
326+
return envFile;
178327
}
179328

180329
// -- Utility methods --
@@ -195,8 +344,8 @@ static File stringToFile(Path baseDir, String value) {
195344
*/
196345
static String fileToString(Path baseDir, File file) {
197346
Path filePath = file.toPath();
198-
Path relPath = filePath.startsWith(baseDir) ?
199-
baseDir.relativize(filePath) : filePath.toAbsolutePath();
347+
Path relPath = filePath.startsWith(baseDir) ? baseDir.relativize(filePath)
348+
: filePath.toAbsolutePath();
200349
return relPath.toString();
201350
}
202351
}

0 commit comments

Comments
 (0)