|
| 1 | +# Using and Debugging Python Scripts in Java Applications using VSCode |
| 2 | + |
| 3 | +Simple, unpackaged Python scripts can be run and shipped with Java applications. |
| 4 | +The [GraalVM Polyglot APIs](https://www.graalvm.org/latest/reference-manual/embed-languages/) make it easy to run scripts that are simply included in the Java resources. |
| 5 | + |
| 6 | +## 1. Getting Started |
| 7 | + |
| 8 | +In this guide, we will add a small Python script to calculate the similarity of two files to a JavaFX application: |
| 9 | + |
| 10 | + |
| 11 | +## 2. What you will need |
| 12 | + |
| 13 | +To complete this guide, you will need the following: |
| 14 | + |
| 15 | + * Some time on your hands |
| 16 | + * A decent text editor or IDE |
| 17 | + * A supported JDK[^1], preferably the latest [GraalVM JDK](https://graalvm.org/downloads/) |
| 18 | + |
| 19 | + [^1]: Oracle JDK 17 and OpenJDK 17 are supported with interpreter only for GraalPy, but JavaFX requires JDK 21 or newer. |
| 20 | + GraalVM JDK 21, Oracle JDK 21, OpenJDK 21 and offer GraalPy [JIT compilation](https://www.graalvm.org/latest/reference-manual/embed-languages/#runtime-optimization-support). |
| 21 | + Note: GraalVM for JDK 17 is **not supported** for GraalPy. |
| 22 | + |
| 23 | +## 3. Solution |
| 24 | + |
| 25 | +We encourage you to check out the [completed example](./) and follow with this guide step by step. |
| 26 | + |
| 27 | +## 4. Writing the application |
| 28 | + |
| 29 | +You can use either [Maven](https://openjfx.io/openjfx-docs/#maven) or [Gradle](https://openjfx.io/openjfx-docs/#gradle) to run the JavaFX example application. |
| 30 | +We will demonstrate on both build systems. |
| 31 | + |
| 32 | +## 4.1 Dependency configuration |
| 33 | + |
| 34 | +We have added the required dependencies for GraalPy in the `<dependencies>` section of the POM or to the `dependencies` block in the `build.gradle.kts` file. |
| 35 | + |
| 36 | +`pom.xml` |
| 37 | +```xml |
| 38 | +<dependency> |
| 39 | + <groupId>org.graalvm.polyglot</groupId> |
| 40 | + <artifactId>python</artifactId> <!-- ① --> |
| 41 | + <version>24.1.1</version> |
| 42 | + <type>pom</type> <!-- ② --> |
| 43 | +</dependency> |
| 44 | +<dependency> |
| 45 | + <groupId>org.graalvm.polyglot</groupId> |
| 46 | + <artifactId>polyglot</artifactId> <!-- ③ --> |
| 47 | + <version>24.1.1</version> |
| 48 | +</dependency> |
| 49 | +<dependency> |
| 50 | + <groupId>org.graalvm.tools</groupId> |
| 51 | + <artifactId>dap-tool</artifactId> <!-- ④ --> |
| 52 | + <version>24.1.1</version> |
| 53 | +</dependency> |
| 54 | +``` |
| 55 | + |
| 56 | +`build.gradle.kts` |
| 57 | +```kotlin |
| 58 | +implementation("org.graalvm.polyglot:python:24.1.1") // ① |
| 59 | +implementation("org.graalvm.polyglot:polyglot:24.1.1") // ③ |
| 60 | +implementation("org.graalvm.tools:dap-tool:24.1.1") // ④ |
| 61 | +``` |
| 62 | + |
| 63 | +❶ The `python` dependency is a meta-package that transitively depends on all resources and libraries to run GraalPy. |
| 64 | + |
| 65 | +❷ Note that the `python` package is not a JAR - it is simply a `pom` that declares more dependencies. |
| 66 | + |
| 67 | +❸ The `polyglot` dependency provides the APIs to manage and use GraalPy from Java. |
| 68 | + |
| 69 | +❹ The `dap` dependency provides a remote debugger for GraalPy that we can use when Python code is embedded in a Java application. |
| 70 | + |
| 71 | +## 4.2 Adding a Python script |
| 72 | + |
| 73 | +We can just include simple Python scripts in our resources source folder. |
| 74 | +In this example, the script contains a function that uses the Python standard library to compute the similarity between two files. |
| 75 | + |
| 76 | +`src/main/resources/compare_files.py` |
| 77 | +```python |
| 78 | +import polyglot # pyright: ignore |
| 79 | + |
| 80 | +from difflib import SequenceMatcher |
| 81 | +from os import PathLike |
| 82 | + |
| 83 | + |
| 84 | +@polyglot.export_value # ① |
| 85 | +def compare_files(a: PathLike, b: PathLike) -> float: |
| 86 | + with open(a) as file_1, open(b) as file_2: |
| 87 | + file1_data = file_1.read() |
| 88 | + file2_data = file_2.read() |
| 89 | + similarity_ratio = SequenceMatcher(None, file1_data, file2_data).ratio() |
| 90 | + return similarity_ratio |
| 91 | +``` |
| 92 | + |
| 93 | +❶ The only GraalPy-specific code here is this `polyglot.export_value` annotation, which makes the function accessible by name to the Java world. |
| 94 | + |
| 95 | +## 4.2.1 Working with GraalPy in VSCode |
| 96 | + |
| 97 | +You can use [pyenv](https://github.com/pyenv/pyenv) or [pyenv-win](https://github.com/pyenv-win/pyenv-win) with the [Python extensions](https://marketplace.visualstudio.com/items?itemName=ms-python.python) in VSCode to setup and use GraalPy during development. |
| 98 | +You can than edit and debug your Python files using the standard Python tooling. |
| 99 | + |
| 100 | + |
| 101 | + |
| 102 | + |
| 103 | + |
| 104 | +## 4.3 Creating a Python context |
| 105 | + |
| 106 | +GraalVM provides Polyglot APIs to make starting a Python context easy. |
| 107 | +We create the Python context in the JavaFX `start` method. |
| 108 | +We also override the `stop` method to close the context and free any associated resources. |
| 109 | + |
| 110 | +`App.java` |
| 111 | +```java |
| 112 | +public class App extends Application { |
| 113 | + private Context context; |
| 114 | + |
| 115 | + @Override |
| 116 | + public void stop() throws Exception { |
| 117 | + context.close(); |
| 118 | + super.stop(); |
| 119 | + } |
| 120 | + |
| 121 | + @Override |
| 122 | + public void start(Stage stage) { |
| 123 | + context = Context.newBuilder("python") |
| 124 | + .allowIO(IOAccess.newBuilder() // ① |
| 125 | + .fileSystem(FileSystem.newReadOnlyFileSystem(FileSystem.newDefaultFileSystem())) |
| 126 | + .build()) |
| 127 | + .allowPolyglotAccess(PolyglotAccess.newBuilder() // ② |
| 128 | + .allowBindingsAccess("python") |
| 129 | + .build()) |
| 130 | + // These are all the options we need to run the app |
| 131 | +``` |
| 132 | + |
| 133 | +❶ By default, GraalPy will be sandboxed completely, but our script wants to access files. |
| 134 | +Read-only access is enough for this case, so we grant no more. |
| 135 | + |
| 136 | +❷ Our script exposes the `compare_files` function by name to the Java world. |
| 137 | +We explicitly allow this as well. |
| 138 | + |
| 139 | +## 4.3 Calling the Python script from Java |
| 140 | + |
| 141 | +`App.java` |
| 142 | +```java |
| 143 | +try { |
| 144 | + context.eval(Source.newBuilder("python", App.class.getResource("/compare_files.py")).build()); // ① |
| 145 | +} catch (IOException e) { |
| 146 | + throw new RuntimeException(e); |
| 147 | +} |
| 148 | +final Value compareFiles = context.getBindings("python").getMember("compare_files"); // ② |
| 149 | + |
| 150 | +target.setOnDragDropped((event) -> { |
| 151 | + var success = false; |
| 152 | + List<File> files; |
| 153 | + if ((files = event.getDragboard().getFiles()) != null && files.size() == 2) { |
| 154 | + try { |
| 155 | + File file0 = files.get(0), file1 = files.get(1); |
| 156 | + var result = compareFiles.execute(file0.getAbsolutePath(), file1.getAbsolutePath()).asDouble(); // ③ |
| 157 | + target.setText(String.format("%s = %f x %s", file0.getName(), result, file1.getName())); |
| 158 | + success = true; |
| 159 | + } catch (RuntimeException e) { |
| 160 | + target.setText(e.getMessage()); |
| 161 | + } |
| 162 | + } |
| 163 | + resetTargetColor(target); |
| 164 | + event.setDropCompleted(success); |
| 165 | + event.consume(); |
| 166 | +}); |
| 167 | +``` |
| 168 | + |
| 169 | +❶ We can pass a resource URL to the GraalVM Polyglot [`Source`](https://docs.oracle.com/en/graalvm/enterprise/20/sdk/org/graalvm/polyglot/Source.html) API. |
| 170 | +The content is read by the `Source` object, GraalPy and the Python code do not gain access to Java resources this way. |
| 171 | + |
| 172 | +❷ Python objects are returned using a generic [`Value`](https://docs.oracle.com/en/graalvm/enterprise/20/sdk/org/graalvm/polyglot/Value.html) type. |
| 173 | + |
| 174 | +❸ As a Python function, `compare_files` can be executed. |
| 175 | +GraalPy accepts Java objects and tries to match them to the appropriate Python types. |
| 176 | +Return values are again represented as `Value`. |
| 177 | +In this case we know the result will be a Python `float`, which can be converted to a Java `double`. |
| 178 | + |
| 179 | +## 5. Running the application |
| 180 | + |
| 181 | +If you followed along with the example, you can now compile and run your application from the commandline: |
| 182 | + |
| 183 | +With Maven: |
| 184 | + |
| 185 | +```shell |
| 186 | +./mvnw compile |
| 187 | +./mvnw javafx:run |
| 188 | +``` |
| 189 | + |
| 190 | +With Gradle: |
| 191 | + |
| 192 | +```shell |
| 193 | +./gradlew assemble |
| 194 | +./gradlwe run |
| 195 | +``` |
| 196 | + |
| 197 | +## 5.1 Debugging embedded Python code |
| 198 | + |
| 199 | +Your Python code may behave differently when run in a Java embedding. |
| 200 | +This can have many reasons, from different types passed in from Java, permissions of the GraalVM Polyglot sandbox, to Python libraries assuming OS-specific process properties that Java applications do not expose. |
| 201 | + |
| 202 | +To debug Python scripts, we recommend you use VSCode. |
| 203 | +Make sure you have installed the [Python extensions](https://marketplace.visualstudio.com/items?itemName=ms-python.python). |
| 204 | +Where we build the Python context, we can add the following options to enable remote debugging: |
| 205 | + |
| 206 | +`App.java` |
| 207 | +```java |
| 208 | +.option("dap", "localhost:4711") |
| 209 | +.option("dap.Suspend", "false") |
| 210 | +``` |
| 211 | + |
| 212 | +This instructs the runtime to accept [DAP]() connections on port 4711 and continue execution. |
| 213 | +We add a debug configuration to VSCode to match: |
| 214 | + |
| 215 | +`.vscode/launch.json` |
| 216 | +```json |
| 217 | +{ |
| 218 | + "configurations": [{ |
| 219 | + "name": "GraalPy: Attach embedded", |
| 220 | + "type": "debugpy", |
| 221 | + "request": "attach", |
| 222 | + "connect": { "host": "localhost", "port": 4711 }, |
| 223 | + }] |
| 224 | +} |
| 225 | +``` |
| 226 | + |
| 227 | +When we run the application now, we will see the following output: |
| 228 | + |
| 229 | +``` |
| 230 | +[Graal DAP] Starting server and listening on localhost/127.0.0.1:4711 |
| 231 | +``` |
| 232 | + |
| 233 | +We can connect using VSCode or any other DAP client. |
| 234 | +The loaded sources can be opened to view the Python code as loaded from the Java resources. |
| 235 | +We can set breakpoints and inspect runtime state as we would expect. |
| 236 | + |
| 237 | + |
| 238 | + |
| 239 | +## 6. Next steps |
| 240 | + |
| 241 | +- Use GraalPy with popular Java frameworks, such as [Spring Boot](../graalpy-spring-boot-guide/README.md) or [Micronaut](../graalpy-micronaut-guide/README.md) |
| 242 | +- [Migrate from Jython](../graalpy-jython-guide/README.md) to GraalPy |
| 243 | +- Learn more about the Polyglot API for [embedding languages](https://www.graalvm.org/latest/reference-manual/embed-languages/) |
| 244 | +- Explore in depth with GraalPy [reference manual](https://www.graalvm.org/latest/reference-manual/python/) |
0 commit comments