diff --git a/README.md b/README.md index f9c4162cd1ae6075b34bd3d962ca30662b8e6f71..da6405618f113e288abc612d0319b3afef3853ee 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ http://icy.bioimageanalysis.org Nate Jensen & Ben Steffensmeier *Plugin conversion* -Amandine Tournay +Amandine Tournay & Carlos Garcia Lopez de Haro # Note @@ -84,58 +84,66 @@ Until JEP 4.0.3, for Linux users, you need to set temporarily the environment va It is due to some difficulties with Java and C projects that dlopen libraries. ***It has been fixed for the next version planned for Fall 2022.*** +# Usage + +To enable this plugin in Icy, you need to configure the path to your selected Python. +Go in the search bar and search for Python3Preferences. + + + +<!-- TODO: Add capture of JEP Button in the tool tab of Icy --> + +If you are a user of Conda, you select one of the environments available: + + +Or if you are a user of a classical Python installation or if you are using a Virtual environment, you can select the home directory +of your Python: + + +**There you are !** +Click on apply or save, and you are ready to use Python in Icy ! + # Utility functions +**Reminder**: When you open a new Python instance, do NOT FORGET to close when you are done ! :D + ```java +import icy.system.IcyExceptionHandler; import jep.JepConfig; +import jep.JepException; import jep.SubInterpreter; -import plugins.atournay.jep.JepUtils; -import plugins.atournay.jep.PythonUtils; +import plugins.atournay.jep.exec.PythonExec; +import plugins.atournay.jep.utils.JepUtils; +import plugins.atournay.jep.utils.PythonUtils; import plugins.atournay.jep.JepPlugin; public class Test() { public static void main(String[] args) { - // Check if a String is a system path - JepPlugin.getInstance().isPath("<path>"); - - // Get a list of Conda environments with their full path - PythonUtils.getInstance().detectCondaEnvironments(); - - // Get a list of only conda environment names - PythonUtils.getInstance().getCondaEnvNames(); - - // Find the Python execution file by the root directory of Python or check the full path if it is given - PythonUtils.getInstance().findPythonExecutable("<path>", Boolean.TRUE || Boolean.FALSE); - - // Retrieve the site-packages directory from the selected Python - PythonUtils.getInstance().setSitePackagesDirectory("<Python path>"); - - // Retrieve the running JEP file from the site-packages directory of the selected Python - JepUtils.getInstance().findJepLib("<site-packages path>"); - - // Instantiate JEP - JepUtils.getInstance().setJepPath("<JEP path>", "<Python root path>"); - - // Open a Python instance (to use with a try-catch(JepException) - SubInterpreter python = JepUtils.getInstance().openSubPython(); - // Same but adding a JEP configuration object to manage better the link between Java and Python (see JavaDoc) - SubInterpreter python2 = JepUtils.getInstance().openSubPython(new JepConfig()); - - // Running some Python code - python2.exec("x = 5"); - python2.runScript("<Python script file path>"); - // Prints 5 - System.out.println(python2.getValue("x")); - - // Send data to Python - python2.set("y", 10); - // Prints 10 on the Python output stream - python2.exec("print(y)"); - - // Close a Python instance - JepUtils.getInstance().closeSubPython(python); - // Always close an opened instance ! - JepUtils.getInstance().closeSubPython(python2); + try { + // Open a Python instance + SubInterpreter python = new PythonExec().getInterpreter(); + // Same but adding a JEP configuration object to manage better the link between Java and Python (see JavaDoc) + SubInterpreter python2 = new PythonExec(new JepConfig()).getInterpreter(); + + // Running some Python code + python2.exec("x = 5"); + python2.runScript("<Python script file path>"); + // Prints 5 + System.out.println(python2.getValue("x")); + + // Send data to Python + python2.set("y", 10); + // Prints 10 on the Python output stream + python2.exec("print(y)"); + + // Close a Python instance + python.close(); + // Always close an opened instance ! + python2.close(); + } + catch (JepException e) { + IcyExceptionHandler.showErrorMessage(new JepException(e), true, true); + } } } ``` \ No newline at end of file diff --git a/pom.xml b/pom.xml index 7a162b52b01af2eab463f96b270b2faa13b603c6..4d4c165fd3850ed4eab70a4bd1ef5af50e5f07bb 100644 --- a/pom.xml +++ b/pom.xml @@ -6,10 +6,10 @@ <!-- Inherited Icy Parent POM --> <parent> - <artifactId>pom-icy</artifactId> + <artifactId>pom-icy</artifactId> <groupId>org.bioimageanalysis.icy</groupId> - <version>2.0.0</version> - </parent> + <version>2.1.0</version> + </parent> <!-- Project Information --> <artifactId>icy-jep</artifactId> @@ -17,7 +17,7 @@ <packaging>jar</packaging> - <name>JEP - Java Embedded Python fot Icy</name> + <name>JEP - Java Embedded Python for Icy</name> <description>Jep embeds CPython in Java through JNI. Fork to include the library in Icy.</description> <url>https://icy.bioimageanalysis.org/plugin/jep-java-embedded-python/</url> <inceptionYear>2022</inceptionYear> @@ -74,15 +74,6 @@ <artifact-to-include>jep</artifact-to-include> </properties> - <profiles> - <profile> - <id>icy-plugin-extract-library</id> - <activation> - <activeByDefault>true</activeByDefault> - </activation> - </profile> - </profiles> - <!-- List of project's dependencies --> <dependencies> <dependency> diff --git a/src/main/java/plugins/atournay/jep/JepPlugin.java b/src/main/java/plugins/atournay/jep/JepPlugin.java index e4e08f4e60bf8a71d2dbb7ebe0d3556cce174d3a..afce2fce7eee66489a9cecba39a7b0ba68c3dac9 100644 --- a/src/main/java/plugins/atournay/jep/JepPlugin.java +++ b/src/main/java/plugins/atournay/jep/JepPlugin.java @@ -1,15 +1,16 @@ package plugins.atournay.jep; import icy.plugin.abstract_.Plugin; +import icy.plugin.interface_.PluginDaemon; import icy.plugin.interface_.PluginLibrary; +import plugins.atournay.jep.prefs.JepPreferences; +import plugins.atournay.jep.utils.JepUtils; -import java.util.regex.Pattern; - -public class JepPlugin extends Plugin implements PluginLibrary { +public class JepPlugin extends Plugin implements PluginLibrary, PluginDaemon { private static JepPlugin instance; - private JepPlugin() { + public JepPlugin() { } public static JepPlugin getInstance() { @@ -20,18 +21,18 @@ public class JepPlugin extends Plugin implements PluginLibrary { return instance; } - /** - * @param path The path to test - * @return The result if the String is really a path (compatible Windows, MacOS & Linux) - */ - public boolean isPath(String path) { - if (Pattern.compile("([A-Za-z]:|[/\\\\])+.(\\w)+.*").matcher(path).find()) { - return true; - } - else { - System.err.println("The input does not correspond to a System path. Please retry."); - System.out.println(); - return false; + @Override + public void init() { + if (!JepPreferences.getInstance().getJepPath().isEmpty()) { + JepUtils.getInstance().setJepPath(JepPreferences.getInstance().getJepPath(), JepPreferences.getInstance().getPythonRoot()); } } + + @Override + public void run() { + } + + @Override + public void stop() { + } } diff --git a/src/main/java/plugins/atournay/jep/PythonUtils.java b/src/main/java/plugins/atournay/jep/PythonUtils.java deleted file mode 100644 index 476a1493211fe398a578b01b7938b6d82709857e..0000000000000000000000000000000000000000 --- a/src/main/java/plugins/atournay/jep/PythonUtils.java +++ /dev/null @@ -1,148 +0,0 @@ -package plugins.atournay.jep; - -import java.io.BufferedReader; -import java.io.File; -import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Set; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -public class PythonUtils { - private static PythonUtils instance; - - private PythonUtils() { - } - - public static PythonUtils getInstance() { - if (instance == null) { - instance = new PythonUtils(); - } - - return instance; - } - - /** - * List all Conda environments available on the computers - * - * @return List of conda environments (Key = Name of environment; Value = Path of the environment) - */ - public HashMap<String, String> detectCondaEnvironments() { - HashMap<String, String> condaEnvs = new HashMap<>(); - String[] cmd = new String[]{"conda", "env", "list"}; - - try { - Process process = new ProcessBuilder(cmd).start(); - - BufferedReader stdOut = new BufferedReader(new InputStreamReader(process.getInputStream())); - - Pattern pattern = Pattern.compile("#|([^.A-z]conda)|(environments:)|\\*| "); - - List<String> stdOutStr = stdOut.lines().flatMap(pattern::splitAsStream) - .collect(Collectors.toList()) - .stream().filter(e -> !e.equals("")).collect(Collectors.toList()); - - for (int i = 0; i < stdOutStr.size(); i += 2) { - condaEnvs.put(stdOutStr.get(i), stdOutStr.get(i + 1)); - } - } - catch (IOException e) { - throw new RuntimeException(e); - } - - return condaEnvs; - } - - /** - * Get Conda environment names available on the computer - * - * @return List of all Conda environment names - */ - public Set<String> getCondaEnvNames() { - return detectCondaEnvironments().keySet(); - } - - /** - * The Python executable file in Windows is python.exe and in Unix systems /python (which can be linked to a specific version file) - * - * @param path The path to a directory where the Python executable must be found - * @param isDirectory Set if the path is ending by a directory (true) or a file (false) - * @return The full path of the Python execution file - */ - public String findPythonExecutable(String path, boolean isDirectory) { - if (isDirectory) { - File[] pythonFiles = new File(path).listFiles((file, name) -> name.equals("bin") && file.isDirectory()); - - if (pythonFiles != null) { - for (File binDirectory : pythonFiles) { - File[] binFiles = binDirectory.listFiles((file, name) -> name.matches("python?(?:\\.exe|$)")); - - if (binFiles != null) { - return Arrays.stream(binFiles).findFirst().map(File::getAbsolutePath).orElse(null); - } - } - } - - return null; - } - else { - if (Files.exists(Paths.get(path))) { - return path; - } - else { - return null; - } - } - } - - /** - * @param pythonExecutionPath Path to the Python home directory - * @return The path to the site-packages directory - */ - public String setSitePackagesDirectory(String pythonExecutionPath) { - String terminalLine; - String[] command = new String[]{pythonExecutionPath, "-c", "import sys; print([p for p in sys.path if p.endswith(\"site-packages\")])"}; - String sitePackagesPath = ""; - - try { - // Execute terminal command to find the site-packages directory - Process process = Runtime.getRuntime().exec(command); - - BufferedReader streamInput = new BufferedReader(new InputStreamReader(process.getInputStream())); - BufferedReader streamError = new BufferedReader(new InputStreamReader(process.getErrorStream())); - - // Execution result stream output - while ((terminalLine = streamInput.readLine()) != null) { - String[] spList = terminalLine.replaceAll("[\\[\\]' ]", "").split(","); - - /* - With classical Python interpreter (non-isolated environment like Venv and Conda with its consorts), there can be multiple site-packages folder. - So, we must find which one has JEP inside - */ - for (String spPath : spList) { - String isThereJEP = JepUtils.getInstance().findJepLib(spPath); - - if (isThereJEP != null) { - sitePackagesPath = spPath; - break; - } - } - } - - // Error stream output - while ((terminalLine = streamError.readLine()) != null) { - System.err.println(terminalLine); - } - } - catch (IOException e) { - e.printStackTrace(); - } - - return sitePackagesPath; - } -} diff --git a/src/main/java/plugins/atournay/jep/CustomClassEnquirer.java b/src/main/java/plugins/atournay/jep/exec/DefaultClassEnquirer.java similarity index 92% rename from src/main/java/plugins/atournay/jep/CustomClassEnquirer.java rename to src/main/java/plugins/atournay/jep/exec/DefaultClassEnquirer.java index b0f2ef10c96edffc8e8f43310ec24e891d6294d0..aeecf80f1637d304d0a28d1615f01a01f2dd39d3 100644 --- a/src/main/java/plugins/atournay/jep/CustomClassEnquirer.java +++ b/src/main/java/plugins/atournay/jep/exec/DefaultClassEnquirer.java @@ -1,4 +1,4 @@ -package plugins.atournay.jep; +package plugins.atournay.jep.exec; import jep.ClassEnquirer; import jep.ClassList; @@ -11,7 +11,7 @@ import jep.ClassList; * * @author Amandine Tournay */ -public class CustomClassEnquirer implements ClassEnquirer { +public class DefaultClassEnquirer implements ClassEnquirer { /** * Verification of a scanned package and confirm if it is a package from Java. * In case of conflict, and we want to load only the Python version of it, it is possible to force to return false. diff --git a/src/main/java/plugins/atournay/jep/exec/PythonExec.java b/src/main/java/plugins/atournay/jep/exec/PythonExec.java new file mode 100644 index 0000000000000000000000000000000000000000..c359815baf5364a797ec6695153de3043a0c17b1 --- /dev/null +++ b/src/main/java/plugins/atournay/jep/exec/PythonExec.java @@ -0,0 +1,80 @@ +package plugins.atournay.jep.exec; + +import icy.system.IcyExceptionHandler; +import jep.JepConfig; +import jep.JepException; +import jep.SubInterpreter; + +import java.io.Closeable; + +/** + * Class that wraps a Python JEP {@see jep.SubInterpreter} instance. An object of this class returns the interpreter that can be used to call Python code. + * This class also contains some methods that call Python code to carry out different tasks such as installing modules from Python + * + * @author Carlos Garcia Lopez de Haro + * @author Amandine Tournay + */ +public class PythonExec implements Closeable { + private SubInterpreter interpreter; + private JepConfig config; + + /** + * Open a new instance of JEP to consume Python code or scripts + */ + public PythonExec() { + config = new JepConfig(); + config.setClassEnquirer(new DefaultClassEnquirer()); + + try { + interpreter = new SubInterpreter(config); + } + catch (JepException e) { + IcyExceptionHandler.showErrorMessage(new JepException(e), true, true); + } + } + + /** + * Opens a new instance of JEP to consume Python code or scripts with a custom configuration + * @param customConfig Override JEP configuration + */ + public PythonExec(JepConfig customConfig) { + config = customConfig; + interpreter = new SubInterpreter(config); + } + + /** + * Installation of Python packages with the <code>pip install</code> command through Python + * + * @param packages Name of the packages to install through pip. + * It can be either the package name (i.e. numpy) or a specific version of a package (i.e. numpy==1.23.1) separated by a space + * @see plugins.atournay.jep.utils.PythonUtils#installPythonPackage(String) to install packages through the terminal + */ + public void installPythonPackage(String packages) { + String command = "try:" + System.lineSeparator(); + command += "\tfrom pip import main as pipmain" + System.lineSeparator(); + command += "except:" + System.lineSeparator(); + command += "\tfrom pip._internal.main import main as pipmain" + System.lineSeparator(); + command += "pipmain(['install', '" + packages + "'])" + System.lineSeparator(); + + interpreter.exec(command); + } + + public SubInterpreter getInterpreter() { + return interpreter; + } + + public JepConfig getConfig() { + return config; + } + + /** + * Close the current running instance of Python + * <b>To reopen a new one, instantiate a new PythonExec class</b> + */ + @Override + public void close() { + if (interpreter != null) { + interpreter.close(); + } + } +} diff --git a/src/main/java/plugins/atournay/jep/prefs/JepPreferences.java b/src/main/java/plugins/atournay/jep/prefs/JepPreferences.java new file mode 100644 index 0000000000000000000000000000000000000000..6cae41188f8701eec03a0df484a87a6f7e06d990 --- /dev/null +++ b/src/main/java/plugins/atournay/jep/prefs/JepPreferences.java @@ -0,0 +1,96 @@ +package plugins.atournay.jep.prefs; + +import icy.preferences.PluginPreferences; +import icy.preferences.XMLPreferences; +import plugins.atournay.jep.ui.Python3Preferences; + +/** + * Class managing all Python preferences for Icy + * @see Python3Preferences for UI + * @author Amandine Tournay + */ +public class JepPreferences { + private static JepPreferences instance; + private static final String PREF_ID = "python3"; + private static final String PYTHON_ROOT = "pythonRoot"; + private static final String PYTHON_EXEC = "pythonExec"; + private static final String JEP_PATH = "jepPath"; + + /* + * Preference Node to encapsulate Python 3 settings + */ + private static XMLPreferences prefPython; + + private JepPreferences() { + load(); + } + + public static JepPreferences getInstance() { + if (instance == null) { + instance = new JepPreferences(); + } + + return instance; + } + + /** + * Load preferences for Python 3 in Icy settings + */ + public void load() { + prefPython = PluginPreferences.getPreferences().node(PREF_ID); + } + + /** + * Retrieve all preferences related to Python + * @return Preferences from Python Node + */ + public XMLPreferences getPythonPreferences() { + return prefPython; + } + + /** + * Retrieve from Python Node preferences the Python root path + * @return Full path to Python home directory + */ + public String getPythonRoot() { + return getPythonPreferences().get(PYTHON_ROOT, ""); + } + /** + * Set to the Python Node preferences the path to Python + * @param path Full path to the Python home directory + */ + public void setPythonRoot(String path) { + getPythonPreferences().put(PYTHON_ROOT, path); + } + + /** + * Retrieve from Python Node preferences the path to the Python executable file + * @return Full path to Python executable file + */ + public String getPythonExecPath() { + return getPythonPreferences().get(PYTHON_EXEC, ""); + } + + /** + * Set to the Python Node preferences the path to the Python executable file + * @param path Full path to the Python executable file + */ + public void setPythonExecPath(String path) { + getPythonPreferences().put(PYTHON_EXEC, path); + } + + /** + * Retrieve from Python Node preferences the path to the JEP file (libjep) + * @return Full path to JEP library file + */ + public String getJepPath() { + return getPythonPreferences().get(JEP_PATH, ""); + } + /** + * Set to the Python Node preferences the path to the JEP file + * @param path Full path to JEP library file (libjep) + */ + public void setJepPath(String path) { + getPythonPreferences().put(JEP_PATH, path); + } +} diff --git a/src/main/java/plugins/atournay/jep/ui/JepPreferencesWindow.java b/src/main/java/plugins/atournay/jep/ui/JepPreferencesWindow.java new file mode 100644 index 0000000000000000000000000000000000000000..8456cb713366b0c836123de3dd6cc622ec2a1801 --- /dev/null +++ b/src/main/java/plugins/atournay/jep/ui/JepPreferencesWindow.java @@ -0,0 +1,351 @@ +package plugins.atournay.jep.ui; + +import icy.gui.component.IcyTextField; +import icy.gui.dialog.MessageDialog; +import icy.gui.frame.IcyFrame; +import plugins.atournay.jep.prefs.JepPreferences; +import plugins.atournay.jep.utils.JepUtils; +import plugins.atournay.jep.utils.PythonUtils; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Class to create a new window in Icy to set JEP + * + * @author Amandine Tournay + */ +public class JepPreferencesWindow extends IcyFrame implements ActionListener { + IcyFrame frame = IcyFrame.findIcyFrame(getInternalFrame()); + private JButton customPythonBrowseButton, applyButton, saveButton, cancelButton; + private JComboBox<String> condaEnvCombo; + private JRadioButton condaRadioButton, customPythonRadioButton; + private IcyTextField customPythonPathText; + private Set<String> condaEnvList; + + public JepPreferencesWindow() { + super("JEP configuration", true, true, true, true); + + // UI + initialize(); + + // Listeners + condaRadioButton.addActionListener(this); + customPythonRadioButton.addActionListener(this); + customPythonBrowseButton.addActionListener(this); + applyButton.addActionListener(this); + saveButton.addActionListener(this); + cancelButton.addActionListener(this); + + // Update UI with saved settings + validate(); + load(); + } + + private void initialize() { + JPanel mainPanel = new JPanel(createGridLayout()); + mainPanel.setLayout(createGridLayout()); + + condaEnvList = PythonUtils.getInstance().getCondaEnvNames(); + + GridBagConstraints condaCons = createConstraints(0, 2, 2., GridBagConstraints.FIRST_LINE_START, new int[]{15, 0, 8, 5}); + + // First choice -> use a Conda environment + condaRadioButton = createRadioButton("Use a Conda (Anaconda, Miniconda, Mamba, etc...) environment"); + mainPanel.add(condaRadioButton, condaCons); + + if (condaEnvList != null) { + condaEnvCombo = createComboBox(condaEnvList.toArray(new String[0])); + } + else { + condaRadioButton.setEnabled(false); + condaEnvCombo = createComboBox(new String[]{}); + } + + condaEnvCombo.setEnabled(false); + condaCons.gridx = 2; + condaCons.weightx = 0.; + condaCons.gridwidth = 1; + condaCons.fill = GridBagConstraints.NONE; + condaCons.insets = new Insets(15, 0, 0, 0); + mainPanel.add(condaEnvCombo, condaCons); + + // Second choice -> use another Python installed + customPythonRadioButton = createRadioButton("Set manually the path to the Python home directory"); + mainPanel.add(customPythonRadioButton, createConstraints(1, 1, 1., GridBagConstraints.FIRST_LINE_START, new int[]{0, 0, 8, 0})); + + GridBagConstraints customPythonConstraints = createConstraints(2, 2, 2., GridBagConstraints.FIRST_LINE_START, new int[]{0, 0, 0, 5}); + + customPythonPathText = createTextInput(); + customPythonPathText.setEnabled(false); + mainPanel.add(customPythonPathText, customPythonConstraints); + + customPythonBrowseButton = createButton("Browse"); + customPythonBrowseButton.setEnabled(false); + customPythonConstraints.gridx = 2; + customPythonConstraints.gridwidth = 1; + customPythonConstraints.weightx = 1.; + customPythonConstraints.weighty = 1.; + customPythonConstraints.fill = GridBagConstraints.NONE; + mainPanel.add(customPythonBrowseButton, customPythonConstraints); + + // Group radio buttons for selection + ButtonGroup radioGroup = createRadioGroup(); + radioGroup.add(condaRadioButton); + radioGroup.add(customPythonRadioButton); + + if (condaEnvList == null) { + JLabel condaNotInstalled = new JLabel("Conda is currently not installed"); + condaNotInstalled.setFont(new Font(condaNotInstalled.getFont().getName(), Font.ITALIC, condaNotInstalled.getFont().getSize())); + + mainPanel.add(condaNotInstalled, createConstraints(3, 2, 1., GridBagConstraints.CENTER, new int[]{0, 0, 0, 0})); + } + + // State buttons + Box buttonsBox = Box.createHorizontalBox(); + GridBagConstraints buttonsConstraints = createConstraints(condaEnvList != null ? 3 : 4, 1, 0., GridBagConstraints.LAST_LINE_END, new int[]{0, 5, 5, 5}); + buttonsConstraints.fill = GridBagConstraints.NONE; + + applyButton = createButton("Apply"); + buttonsBox.add(applyButton); + + buttonsBox.add(Box.createHorizontalStrut(5)); + + saveButton = createButton("Ok"); + buttonsConstraints.gridx++; + buttonsBox.add(saveButton); + + buttonsBox.add(Box.createHorizontalStrut(5)); + + cancelButton = createButton("Cancel"); + buttonsConstraints.gridx++; + buttonsBox.add(cancelButton); + + mainPanel.add(buttonsBox, buttonsConstraints); + + setContentPane(mainPanel); + } + + private void load() { + String pythonHomePath = JepPreferences.getInstance().getPythonRoot(); + + if (!pythonHomePath.isEmpty()) { + if (pythonHomePath.contains("conda")) { + condaRadioButton.setSelected(true); + condaEnvCombo.setEnabled(true); + + for (String envName : PythonUtils.getInstance().getCondaEnvNames()) { + Pattern pattern = Pattern.compile(envName); + Matcher matcher = pattern.matcher(pythonHomePath); + + if (matcher.find()) { + condaEnvCombo.setSelectedItem(envName); + } + } + } + else { + customPythonRadioButton.setSelected(true); + customPythonPathText.setEnabled(true); + customPythonPathText.setText(pythonHomePath); + customPythonBrowseButton.setEnabled(true); + } + } + } + + private boolean apply() { + String pythonHomePath = ""; + + if (condaRadioButton.isSelected()) { + pythonHomePath = PythonUtils.getInstance().detectCondaEnvironments().get((String) condaEnvCombo.getSelectedItem()); + } + if (customPythonRadioButton.isSelected()) { + pythonHomePath = customPythonPathText.getText(); + } + + String pythonExecutablePath = PythonUtils.getInstance().findPythonExecutable(pythonHomePath, true); + + if (pythonExecutablePath != null) { + JepPreferences.getInstance().setPythonRoot(pythonHomePath); + JepPreferences.getInstance().setPythonExecPath(pythonExecutablePath); + + String sitePackageDirectory = PythonUtils.getInstance().setSitePackagesDirectory(); + String jepPath = JepUtils.getInstance().findJepLib(sitePackageDirectory); + + if (jepPath == null) { + PythonUtils.getInstance().installPythonPackage("wheel numpy jep"); + jepPath = JepUtils.getInstance().findJepLib(sitePackageDirectory); + } + + JepPreferences.getInstance().setJepPath(jepPath); + } + else { + MessageDialog.showDialog("Error while finding Python", "Python could not be found. Please select an other directory.", MessageDialog.ERROR_MESSAGE); + + return false; + } + + MessageDialog.showDialog("JEP configuration saved", "JEP settings has been successfully saved", MessageDialog.INFORMATION_MESSAGE); + + return true; + } + + private void save() { + if (apply()) { + frame.dispose(); + } + } + + private void cancel() { + frame.dispose(); + } + + @Override + public void actionPerformed(ActionEvent actionEvent) { + if (actionEvent.getSource().equals(condaRadioButton)) { + if (condaEnvList != null) { + condaEnvCombo.setEnabled(condaRadioButton.isSelected()); + } + else { + condaEnvCombo.setEnabled(false); + } + + customPythonPathText.setEnabled(false); + customPythonBrowseButton.setEnabled(false); + } + else if (actionEvent.getSource().equals(customPythonRadioButton)) { + if (customPythonRadioButton.isSelected()) { + condaEnvCombo.setEnabled(false); + customPythonPathText.setEnabled(true); + customPythonBrowseButton.setEnabled(true); + } + else { + condaEnvCombo.setEnabled(false); + customPythonPathText.setEnabled(false); + customPythonBrowseButton.setEnabled(false); + } + } + else if (actionEvent.getSource().equals(customPythonBrowseButton)) { + createDirectorySelection(); + } + else if (actionEvent.getSource().equals(applyButton)) { + apply(); + } + else if (actionEvent.getSource().equals(saveButton)) { + save(); + } + else if (actionEvent.getSource().equals(cancelButton)) { + cancel(); + } + } + + /** + * Delegate function to create a GridBagLayout + * + * @return New instance of GridBagLayout + */ + private GridBagLayout createGridLayout() { + return new GridBagLayout(); + } + + /** + * Delegate function to add constraints for a grid cell + * + * @param y Position in vertical axis of the grid + * @param width Width of the cell in the grid + * @param anchor Position the cell in a specific location of the grid + * @param insets Add margins to the cell with Insets class (top, left, bottom right) + * @return New instance of GridBagConstraints + */ + private GridBagConstraints createConstraints(int y, int width, double weightX, int anchor, int[] insets) { + GridBagConstraints constraints = new GridBagConstraints(); + constraints.gridx = 0; + constraints.gridy = y; + constraints.gridwidth = width; + constraints.gridheight = 1; + constraints.fill = GridBagConstraints.HORIZONTAL; + constraints.anchor = anchor; + constraints.insets = new Insets(insets[0], insets[1], insets[2], insets[3]); + constraints.weightx = weightX; + constraints.weighty = 0.0; + + return constraints; + } + + /** + * Delegate function to add a ButtonGroup + * + * @return New instance of ButtonGroup + */ + private ButtonGroup createRadioGroup() { + return new ButtonGroup(); + } + + /** + * Delegate function to create a JRadioButton + * + * @param text Text to add in radio button + * @return New instance of JRadioButton + */ + private JRadioButton createRadioButton(String text) { + return new JRadioButton(text); + } + + /** + * Delegate function to create a new JRadioButton + * + * @return New instance of JButton + */ + private JButton createButton(String text) { + return new JButton(text); + } + + /** + * Delegate function to create a new JComboBox with different kind of data + * + * @param content Array of element to list + * @param <T> Generic type + * @return New instance of JComboBox + */ + private <T> JComboBox<T> createComboBox(T[] content) { + return new JComboBox<>(content); + } + + /** + * Delegate function to create a new IcyTextField (JTextField) + * + * @return New instance of IcyTextField + */ + private IcyTextField createTextInput() { + return new IcyTextField(""); + } + + /** + * Delegate function to manage JFileChooser + */ + private void createDirectorySelection() { + String pythonPathInput = customPythonPathText.getText(); + + JFileChooser selectPythonDirectoryDiag = new JFileChooser(); + selectPythonDirectoryDiag.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + selectPythonDirectoryDiag.setAcceptAllFileFilterUsed(false); + selectPythonDirectoryDiag.setDialogTitle("Set Python home directory"); + + if (!pythonPathInput.isEmpty()) { + Path pythonHomePath = Paths.get(pythonPathInput); + boolean isDirectory = pythonHomePath.toFile().isDirectory(); + + selectPythonDirectoryDiag.setCurrentDirectory(isDirectory ? pythonHomePath.toFile() : pythonHomePath.getParent().toFile()); + } + + if (selectPythonDirectoryDiag.showOpenDialog(customPythonBrowseButton) == JFileChooser.APPROVE_OPTION) { + customPythonPathText.setText(selectPythonDirectoryDiag.getSelectedFile().getAbsolutePath()); + } + } +} diff --git a/src/main/java/plugins/atournay/jep/ui/Python3Preferences.java b/src/main/java/plugins/atournay/jep/ui/Python3Preferences.java new file mode 100644 index 0000000000000000000000000000000000000000..63379f662bc2161162f1aa3a8a45f6af79a108dd --- /dev/null +++ b/src/main/java/plugins/atournay/jep/ui/Python3Preferences.java @@ -0,0 +1,31 @@ +package plugins.atournay.jep.ui; + +import icy.gui.frame.IcyFrameAdapter; +import icy.gui.frame.IcyFrameEvent; +import icy.plugin.abstract_.PluginActionable; +import icy.plugin.interface_.PluginBundled; +import plugins.atournay.jep.JepPlugin; + +public class Python3Preferences extends PluginActionable implements PluginBundled { + @Override + public void run() { + final JepPreferencesWindow frameWindow = new JepPreferencesWindow(); + frameWindow.setSize(700, 180); + frameWindow.setResizable(false); + frameWindow.setVisible(true); + frameWindow.addToDesktopPane(); + frameWindow.requestFocus(); + + frameWindow.addFrameListener(new IcyFrameAdapter() { + @Override + public void icyFrameClosed(IcyFrameEvent icyFrameEvent) { + frameWindow.removeFrameListener(this); + } + }); + } + + @Override + public String getMainPluginClassName() { + return JepPlugin.class.getName(); + } +} diff --git a/src/main/java/plugins/atournay/jep/JepUtils.java b/src/main/java/plugins/atournay/jep/utils/JepUtils.java similarity index 72% rename from src/main/java/plugins/atournay/jep/JepUtils.java rename to src/main/java/plugins/atournay/jep/utils/JepUtils.java index b68bcf9c0561e69438b6316b94019d34ec5c644f..ff28e489dcb7d7c3206fd88ced17d1b5c42fcf18 100644 --- a/src/main/java/plugins/atournay/jep/JepUtils.java +++ b/src/main/java/plugins/atournay/jep/utils/JepUtils.java @@ -1,6 +1,7 @@ -package plugins.atournay.jep; +package plugins.atournay.jep.utils; import jep.*; +import plugins.atournay.jep.exec.DefaultClassEnquirer; import java.io.File; @@ -12,7 +13,6 @@ import java.io.File; public class JepUtils { private static JepUtils instance; private JepConfig jepConfig; - private Interpreter jepInterpreter; private JepUtils() { } @@ -34,8 +34,6 @@ public class JepUtils { File jepDir = new File(sitePackagesPath, "jep"); if (!jepDir.isDirectory()) { - System.err.println("JEP could not be found. Please check if you have installed JEP before."); - System.out.println(); return null; } else { @@ -60,7 +58,7 @@ public class JepUtils { public void setJepPath(String jepPath, String pythonRoot) throws JepException { setJepConfig(new JepConfig()); - getJepConfig().setClassEnquirer(new CustomClassEnquirer()); + getJepConfig().setClassEnquirer(new DefaultClassEnquirer()); if (jepPath.matches(".*conda.*|.*venv")) { PyConfig pyConfig = new PyConfig(); @@ -73,23 +71,6 @@ public class JepUtils { MainInterpreter.setJepLibraryPath(jepPath); } - /** - * Open global Python instance (<b>ONLY ONE PER JVM !!!</b>) - * <b>THIS FUNCTION IS NOT RECOMMENDED</b> - */ - public void openPython() { - setJepInterpreter(new SharedInterpreter()); - } - - /** - * Close global Python instance - * <b>THIS FUNCTION IS NOT RECOMMENDED AS IT IS FOR THE GLOBAL INTERPRETER</b> - * {@link JepUtils#openPython()} - */ - public void closePython() { - getJepInterpreter().close(); - } - /** * Create a new Python instance * @@ -102,7 +83,7 @@ public class JepUtils { /** * Create a new Python instance * - * @param jepConfig JEP configurations {@link jep.JepConfig} and example at {@link plugins.atournay.jep.CustomClassEnquirer} + * @param jepConfig JEP configurations {@link jep.JepConfig} and example at {@link DefaultClassEnquirer} * @return Python interpreter to execute some Python commands, retrieve values, etc... */ public SubInterpreter openSubPython(JepConfig jepConfig) { @@ -127,12 +108,4 @@ public class JepUtils { private void setJepConfig(JepConfig newJepConfig) { this.jepConfig = newJepConfig; } - - public Interpreter getJepInterpreter() { - return jepInterpreter; - } - - private void setJepInterpreter(Interpreter jepInterpreter) { - this.jepInterpreter = jepInterpreter; - } } diff --git a/src/main/java/plugins/atournay/jep/utils/PythonUtils.java b/src/main/java/plugins/atournay/jep/utils/PythonUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..70baf2cb6f5e905ce1e5b587a9b354a6c84498af --- /dev/null +++ b/src/main/java/plugins/atournay/jep/utils/PythonUtils.java @@ -0,0 +1,249 @@ +package plugins.atournay.jep.utils; + +import icy.gui.dialog.MessageDialog; +import icy.gui.frame.IcyFrame; +import icy.system.IcyExceptionHandler; +import icy.system.thread.ThreadUtil; +import plugins.atournay.jep.prefs.JepPreferences; + +import javax.swing.*; +import java.awt.*; +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; +import java.util.*; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class PythonUtils { + private static PythonUtils instance; + + private PythonUtils() { + } + + public static PythonUtils getInstance() { + if (instance == null) { + instance = new PythonUtils(); + } + + return instance; + } + + /** + * List all Conda environments available on the computers + * + * @return List of conda environments (Key = Name of environment; Value = Path of the environment) + */ + public HashMap<String, String> detectCondaEnvironments() { + HashMap<String, String> condaEnvs = new HashMap<>(); + String[] cmd = new String[]{"conda", "env", "list"}; + + try { + Process process = new ProcessBuilder(cmd).start(); + + BufferedReader stdOut = new BufferedReader(new InputStreamReader(process.getInputStream())); + + Pattern pattern = Pattern.compile("#|([^.A-z]conda)|(environments:)|\\*| "); + + List<String> stdOutStr = stdOut.lines().flatMap(pattern::splitAsStream) + .collect(Collectors.toList()) + .stream().filter(e -> !e.equals("")).collect(Collectors.toList()); + + for (int i = 0; i < stdOutStr.size(); i += 2) { + condaEnvs.put(stdOutStr.get(i), stdOutStr.get(i + 1)); + } + } + catch (IOException e) { + return null; + } + + return condaEnvs; + } + + /** + * Get Conda environment names available on the computer + * + * @return List of all Conda environment names + */ + public Set<String> getCondaEnvNames() { + return detectCondaEnvironments() != null ? detectCondaEnvironments().keySet() : null; + } + + /** + * The Python executable file in Windows is python.exe and in Unix systems /python (which can be linked to a specific version file) + * + * @param path The path to a directory where the Python executable must be found + * @param isDirectory Set if the path is ending by a directory (true) or a file (false) + * @return The full path of the Python execution file + */ + public String findPythonExecutable(String path, boolean isDirectory) { + if (isDirectory) { + File pathToTest = new File(path); + File[] pythonFiles = Utils.getInstance().isWindows() ? pathToTest.listFiles(File::isFile) : pathToTest.listFiles((file, name) -> name.equals("bin") && file.isDirectory()); + + if (pythonFiles != null) { + if (Utils.getInstance().isWindows()) { + File foundPython = Arrays.stream(pythonFiles) + .filter(x -> Pattern.compile("python?3?(?:\\.exe|$)").matcher(x.getAbsolutePath()).find()) + .findFirst().orElse(null); + + return foundPython != null ? foundPython.getAbsolutePath() : null; + } + else { + for (File binDirectory : pythonFiles) { + File[] binFiles = binDirectory.listFiles((file, name) -> name.matches("python?3?(?:\\.exe|$)")); + + if (binFiles != null) { + return Arrays.stream(binFiles).findFirst().map(File::getAbsolutePath).orElse(null); + } + } + } + } + } + else { + if (Files.exists(Paths.get(path))) { + return path; + } + else { + return null; + } + } + + return null; + } + + /** + * @return The path to the site-packages directory + */ + public String setSitePackagesDirectory() { + String terminalLine; + String[] command = new String[]{JepPreferences.getInstance().getPythonExecPath(), "-c", "import sys; print([p for p in sys.path if p.endswith(\\\"site-packages\\\")])"}; + String sitePackagesPath = ""; + + try { + // Execute terminal command to find the site-packages directory + Process process = Runtime.getRuntime().exec(command); + + BufferedReader streamInput = new BufferedReader(new InputStreamReader(process.getInputStream())); + BufferedReader streamError = new BufferedReader(new InputStreamReader(process.getErrorStream())); + + // Execution result stream output + while ((terminalLine = streamInput.readLine()) != null) { + String[] spList = terminalLine.replaceAll("[\\[\\]' ]", "").split(","); + + for (String spPath : spList) { + System.out.println(spPath); + if (spPath.contains("site-packages")) { + sitePackagesPath = spPath; + break; + } + } + } + + // Error stream output + while ((terminalLine = streamError.readLine()) != null) { + System.err.println(terminalLine); + } + } + catch (IOException e) { + e.printStackTrace(); + } + + return sitePackagesPath; + } + + /** + * Install Python packages through pip via the terminal of the computer + * @see plugins.atournay.jep.exec.PythonExec#installPythonPackage(String) to install a Python package through Python + * + * @param packageNames Name of the packages to install through pip. + * It can be either the package name (i.e. numpy) or a specific version of a package (i.e. numpy==1.23.1) separated by a space + */ + public void installPythonPackage(String packageNames) { + // Create command to execute (i.e. /path/to/python -m pip install package1 package2 ...) + String pythonExec = Utils.getInstance().isWindows() ? JepPreferences.getInstance().getPythonExecPath().replace("\\", "\\\\") : JepPreferences.getInstance().getPythonExecPath(); + ArrayList<String> packageList = Arrays.stream(packageNames.trim().split(" ")).filter(s -> !s.equals("")).map(String::trim).collect(Collectors.toCollection(ArrayList::new)); + ArrayList<String> commandLine = new ArrayList<>(); + commandLine.add(pythonExec); + commandLine.add("-m"); + commandLine.add("pip"); + commandLine.add("install"); + commandLine.addAll(packageList); + + // Create window to get output console + final IcyFrame[] installFrame = new IcyFrame[1]; + JTextArea outputArea = new JTextArea(); + + ThreadUtil.invokeNow(() -> { + installFrame[0] = new IcyFrame("Installing Python packages " + packageNames, false, false); + installFrame[0].getContentPane().setLayout(new GridLayout()); + installFrame[0].setSize(1000, 300); + installFrame[0].setVisible(true); + installFrame[0].addToDesktopPane(); + installFrame[0].requestFocus(); + + installFrame[0].getContentPane().add(outputArea); + }); + + ThreadUtil.bgRun(() -> { + // Execute installation with pip + ProcessBuilder command = new ProcessBuilder(commandLine); + + // Retrieve output + Process sysProc; + BufferedReader bufferedReader; + BufferedReader bufferedErrReader; + try { + sysProc = command.start(); + bufferedReader = new BufferedReader(new InputStreamReader(sysProc.getInputStream())); + bufferedErrReader = new BufferedReader(new InputStreamReader(sysProc.getErrorStream())); + } + catch (IOException e) { + throw new RuntimeException(e); + } + + try { + String line, lineError; + + // Update window with new output element + while ((line = bufferedReader.readLine()) != null) { + System.out.println(line); + + String finalLine = line; + ThreadUtil.invokeLater(() -> { + outputArea.append(finalLine); + outputArea.append("\n"); + installFrame[0].validate(); + }); + } + + while ((lineError = bufferedErrReader.readLine()) != null) { + System.out.println(lineError); + + String finalLineError = lineError; + ThreadUtil.invokeLater(() -> { + outputArea.append(finalLineError); + outputArea.append(""); + installFrame[0].validate(); + }); + } + + sysProc.waitFor(); + + Thread.sleep(2000); + + // Close window + ThreadUtil.invokeLater(() -> installFrame[0].dispose()); + } + catch (IOException | InterruptedException e) { + MessageDialog.showDialog("Error while trying to install " + packageNames, e.toString(), MessageDialog.ERROR_MESSAGE); + + IcyExceptionHandler.showErrorMessage(new RuntimeException(e), true); + } + }); + } +} diff --git a/src/main/java/plugins/atournay/jep/utils/Utils.java b/src/main/java/plugins/atournay/jep/utils/Utils.java new file mode 100644 index 0000000000000000000000000000000000000000..2bdfd3c7c0d84d36f4c99c87266b7d0072666847 --- /dev/null +++ b/src/main/java/plugins/atournay/jep/utils/Utils.java @@ -0,0 +1,48 @@ +package plugins.atournay.jep.utils; + +import java.util.regex.Pattern; + +public class Utils { + private static Utils instance; + + private Utils() {} + + public static Utils getInstance() { + if (instance == null) { + instance = new Utils(); + } + + return instance; + } + + /** + * @param path The path to test + * @return The result if the String is really a path (compatible Windows, MacOS & Linux) + */ + public boolean isPath(String path) { + if (Pattern.compile("([A-Za-z]:|[/\\\\])+.(\\w)+.*").matcher(path).find()) { + return true; + } + else { + System.err.println("The input does not correspond to a System path. Please retry."); + System.out.println(); + return false; + } + } + + public String getOS() { + return System.getProperty("os.name").toLowerCase(); + } + + public boolean isWindows() { + return getOS().contains("win"); + } + + public boolean isMacOS() { + return getOS().startsWith("mac"); + } + + public boolean isLinux() { + return getOS().startsWith("nix") || getOS().contains("nux") || getOS().contains("aix"); + } +} diff --git a/src/main/resources/readMePictures/Python - search.png b/src/main/resources/readMePictures/Python - search.png new file mode 100644 index 0000000000000000000000000000000000000000..45cca8852383946d7e1c46acf138356576785cd7 Binary files /dev/null and b/src/main/resources/readMePictures/Python - search.png differ diff --git a/src/main/resources/readMePictures/Python - set with Conda.png b/src/main/resources/readMePictures/Python - set with Conda.png new file mode 100644 index 0000000000000000000000000000000000000000..9e0c047d17aacf5cb0b5bd089d3f2c9ba56cdd7c Binary files /dev/null and b/src/main/resources/readMePictures/Python - set with Conda.png differ diff --git a/src/main/resources/readMePictures/Python - set with custom path.png b/src/main/resources/readMePictures/Python - set with custom path.png new file mode 100644 index 0000000000000000000000000000000000000000..e79ceebfa0bf13ab363175e28ac14842384ee498 Binary files /dev/null and b/src/main/resources/readMePictures/Python - set with custom path.png differ