diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..5b52e112d56e80b2398f6945138897bb80dbc414 --- /dev/null +++ b/.gitignore @@ -0,0 +1,139 @@ +### Java template +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +### VisualStudioCode template +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 +.idea/ + + *.iml + *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Eclipse template +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.recommenders + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# PyDev specific (Python IDE for Eclipse) +*.pydevproject + +# CDT-specific (C/C++ Development Tooling) +.cproject + +# CDT- autotools +.autotools + +# Java annotation processor (APT) +.factorypath + +# PDT-specific (PHP Development Tools) +.buildpath + +# sbteclipse plugin +.target + +# Tern plugin +.tern-project + +# TeXlipse plugin +.texlipse + +# STS (Spring Tool Suite) +.springBeans + +# Code Recommenders +.recommenders/ + +# Annotation Processing +.apt_generated/ +.apt_generated_test/ + +# Scala IDE specific (Scala & Java development for Eclipse) +.cache-main +.scala_dependencies +.worksheet + +# Uncomment this line if you wish to ignore the project description file. +# Typically, this file would be tracked if it contains build/dependency configurations: +#.project + diff --git a/README.md b/README.md index c50eeb46f521ffb44454a9d86e00667cd4d8e436..f9c4162cd1ae6075b34bd3d962ca30662b8e6f71 100644 --- a/README.md +++ b/README.md @@ -17,11 +17,11 @@ Jep embeds CPython in Java through JNI. Some benefits of embedding CPython in a JVM: * Using the native Python interpreter may be much faster than alternatives. - * Python is mature, well supported, and well documented. + * Python is mature, well-supported, and well documented. * Access to high quality Python modules, both native CPython extensions and Python-based. * Compilers and assorted Python tools are as mature as the language. * Python is an interpreted language, enabling scripting of established Java code without requiring recompilation. - * Both Java and Python are cross platform, enabling deployment to different operating systems. + * Both Java and Python are cross-platform, enabling deployment to different operating systems. ## Installation instructions @@ -76,4 +76,66 @@ http://icy.bioimageanalysis.org Nate Jensen & Ben Steffensmeier *Plugin conversion* -Amandine Tournay \ No newline at end of file +Amandine Tournay + +# Note + +Until JEP 4.0.3, for Linux users, you need to set temporarily the environment variable `LD_PRELOAD` to your `libpythonx.xx.so` file. +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.*** + +# Utility functions + +```java +import jep.JepConfig; +import jep.SubInterpreter; +import plugins.atournay.jep.JepUtils; +import plugins.atournay.jep.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); + } +} +``` \ No newline at end of file diff --git a/pom.xml b/pom.xml index 0af994b1a269ed2829c887447a1f958fa898f4a5..232859d8ac27e777638d63a52ffa89b1d6cd7b4a 100644 --- a/pom.xml +++ b/pom.xml @@ -39,7 +39,6 @@ <developer> <id>ndjensen</id> <name>Nate Jensen</name> - <url>https://mywebpage.net</url> <roles> <role>founder</role> <role>lead</role> @@ -60,6 +59,14 @@ <role>support</role> </roles> </developer> + <developer> + <id>atournay</id> + <name>Amandine Tournay</name> + <url>https://research.pasteur.fr/fr/member/amandine-tournay/</url> + <roles> + <role>contributor</role> + </roles> + </developer> </developers> <!-- Project properties --> diff --git a/src/main/java/plugins/atournay/jep/CustomClassEnquirer.java b/src/main/java/plugins/atournay/jep/CustomClassEnquirer.java new file mode 100644 index 0000000000000000000000000000000000000000..b0f2ef10c96edffc8e8f43310ec24e891d6294d0 --- /dev/null +++ b/src/main/java/plugins/atournay/jep/CustomClassEnquirer.java @@ -0,0 +1,36 @@ +package plugins.atournay.jep; + +import jep.ClassEnquirer; +import jep.ClassList; + +/** + * Class to override the management of the default class loader configuration. + * Jep scans the Java classpath to be able to import those classes in Python. + * But, if a library or a class is available on both sides AND if the path is the same, it will create a conflict. + * To prevent this, we can choose which one to load. + * + * @author Amandine Tournay + */ +public class CustomClassEnquirer 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. + * + * @param name Name of the package + * @return True if it corresponds to a package from Java + */ + @Override + public boolean isJavaPackage(String name) { + return !(name.equals("tensorflow")) && ClassList.getInstance().isJavaPackage(name); + } + + @Override + public String[] getClassNames(String packageName) { + return ClassList.getInstance().getClassNames(packageName); + } + + @Override + public String[] getSubPackages(String packageName) { + return ClassList.getInstance().getSubPackages(packageName); + } +} diff --git a/src/main/java/plugins/atournay/jep/JepPlugin.java b/src/main/java/plugins/atournay/jep/JepPlugin.java index 7ff14128c19580c121bf456113338b561943b3a9..143506fedfead7b6f6f2080c0bec47cbc34b528c 100644 --- a/src/main/java/plugins/atournay/jep/JepPlugin.java +++ b/src/main/java/plugins/atournay/jep/JepPlugin.java @@ -1,7 +1,38 @@ package plugins.atournay.jep; +import com.drew.lang.annotations.NotNull; import icy.plugin.abstract_.Plugin; import icy.plugin.interface_.PluginLibrary; +import java.util.regex.Pattern; + public class JepPlugin extends Plugin implements PluginLibrary { + private static JepPlugin instance; + + + private JepPlugin() { + } + + public static JepPlugin getInstance() { + if (instance == null) { + instance = new JepPlugin(); + } + + 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(@NotNull 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; + } + } } diff --git a/src/main/java/plugins/atournay/jep/JepUtils.java b/src/main/java/plugins/atournay/jep/JepUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..7cb443b6c4c2055bbbe3c7dba4d40183ee0327e6 --- /dev/null +++ b/src/main/java/plugins/atournay/jep/JepUtils.java @@ -0,0 +1,139 @@ +package plugins.atournay.jep; + +import com.drew.lang.annotations.NotNull; +import jep.*; + +import java.io.File; + +/** + * Utility functions to configure JEP + * + * @author Amandine Tournay + */ +public class JepUtils { + private static JepUtils instance; + private JepConfig jepConfig; + private Interpreter jepInterpreter; + + private JepUtils() { + } + + public static JepUtils getInstance() { + if (instance == null) { + instance = new JepUtils(); + } + + return instance; + } + + /** + * @param sitePackagesPath Path to the site-packages directory + * @return Path to JEP + */ + public String findJepLib(@NotNull String sitePackagesPath) { + String jetPath = ""; + 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 { + String[] libFiles = {"libjep.so", "libjep.jnilib", "jep.ddl"}; + + for (String libFile : libFiles) { + File lib = new File(jepDir, libFile); + + if (lib.isFile()) jetPath = lib.getAbsolutePath(); + } + } + + return jetPath; + } + + /** + * Set the path of the JEP library either the OS + * + * @param jepPath Path the JEP execution file (libjep.(so|jnilib|ddl)) + * @param pythonRoot Path to the Python root directory + */ + public void setJepPath(@NotNull String jepPath, @NotNull String pythonRoot) throws JepException { + setJepConfig(new JepConfig()); + + getJepConfig().setClassEnquirer(new CustomClassEnquirer()); + + if (jepPath.matches(".*conda.*|.*venv")) { + PyConfig pyConfig = new PyConfig(); + pyConfig.setPythonHome(pythonRoot); + + MainInterpreter.setInitParams(pyConfig); + } + + SharedInterpreter.setConfig(getJepConfig()); + 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> + * {@see plugins.atournay.jep.JepUtils#closePython()} + */ + public void closePython() { + getJepInterpreter().close(); + } + + /** + * Create a new Python instance + * + * @return Python interpreter to execute some Python commands, retrieve values, etc... + */ + public SubInterpreter openSubPython() { + return openSubPython(new JepConfig()); + } + + /** + * Create a new Python instance + * + * @param jepConfig JEP configurations {@see jep.JepConfig} and example at {@see plugins.atournay.jep.CustomClassEnquirer} + * @return Python interpreter to execute some Python commands, retrieve values, etc... + */ + public SubInterpreter openSubPython(@NotNull JepConfig jepConfig) { + return new SubInterpreter(jepConfig); + } + + /** + * Close a running Python sub interpreter + * + * @param subInterpreter Running Python instance + */ + public void closeSubPython(SubInterpreter subInterpreter) { + subInterpreter.close(); + } + + // GETTERS - SETTERS + + public JepConfig getJepConfig() { + return jepConfig; + } + + private void setJepConfig(@NotNull JepConfig newJepConfig) { + this.jepConfig = newJepConfig; + } + + public Interpreter getJepInterpreter() { + return jepInterpreter; + } + + private void setJepInterpreter(@NotNull Interpreter jepInterpreter) { + this.jepInterpreter = jepInterpreter; + } +} diff --git a/src/main/java/plugins/atournay/jep/PythonUtils.java b/src/main/java/plugins/atournay/jep/PythonUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..b355c017561ffe47b47bd644a869629382b3f3b5 --- /dev/null +++ b/src/main/java/plugins/atournay/jep/PythonUtils.java @@ -0,0 +1,150 @@ +package plugins.atournay.jep; + +import com.drew.lang.annotations.NotNull; + +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(@NotNull String path, @NotNull 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(@NotNull 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; + } +}