Skip to content

Commit 9b75d60

Browse files
committed
Add example for embedding simple python scripts and debugging them using VSCode
1 parent 1958c9a commit 9b75d60

25 files changed

+1460
-0
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: Test GraalPy Scripts Guide
2+
on:
3+
push:
4+
paths:
5+
- 'graalpy/graalpy-scripts-debug/**'
6+
- '.github/workflows/graalpy-scripts-debug.yml'
7+
pull_request:
8+
paths:
9+
- 'graalpy/graalpy-scripts-debug/**'
10+
- '.github/workflows/graalpy-scripts-debug.yml'
11+
workflow_dispatch:
12+
permissions:
13+
contents: read
14+
jobs:
15+
run:
16+
name: 'graalpy-scripts-debug'
17+
runs-on: ubuntu-latest
18+
timeout-minutes: 15
19+
steps:
20+
- uses: actions/checkout@v4
21+
- uses: graalvm/setup-graalvm@v1
22+
with:
23+
java-version: '23.0.1'
24+
distribution: 'graalvm'
25+
github-token: ${{ secrets.GITHUB_TOKEN }}
26+
cache: 'maven'
27+
- name: Build, test, and run 'graalpy-scripts-debug' using Maven
28+
run: |
29+
cd graalpy/graalpy-scripts-debug
30+
./mvnw --no-transfer-progress test
31+
- name: Build, test, and run 'graalpy-scripts-debug' using Gradle
32+
run: |
33+
cd graalpy/graalpy-scripts-debug
34+
./gradlew test
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#
2+
# https://help.github.com/articles/dealing-with-line-endings/
3+
#
4+
# Linux start script should use lf
5+
/gradlew text eol=lf
6+
7+
# These are Windows script files and should use crlf
8+
*.bat text eol=crlf
9+
10+
# Binary files should be left untouched
11+
*.jar binary
12+
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Ignore Gradle project-specific cache directory
2+
.gradle
3+
4+
# Ignore Gradle build output directory
5+
build
6+
7+
# Ignore maven build output directory
8+
target
9+
10+
# Ignore JDTLS build directory
11+
bin
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
wrapperVersion=3.3.2
18+
distributionType=only-script
19+
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"recommendations": [
3+
"ms-python.python",
4+
"vscjava.vscode-java-pack"
5+
]
6+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"configurations": [{
3+
"name": "GraalPy: Attach embedded",
4+
"type": "debugpy",
5+
"request": "attach",
6+
"connect": { "host": "localhost", "port": 4711 },
7+
}]
8+
}
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
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+
![Screenshot of the app](screenshot.png)
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+
![Gif animation of installing GraalPy with pyenv](./graalpy-vscode-pyenv.gif)
101+
![Gif animation of using GraalPy in VSCode](./graalpy-vscode-select.gif)
102+
![Gif animation of debugging with GraalPy in VSCode](./graalpy-vscode-debug.gif)
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+
![Gif animation debugging GraalPy in Java in VSCode](./graalpy-vscode-dap-debug.gif)
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/)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
plugins {
2+
application;
3+
id("org.openjfx.javafxplugin") version "0.1.0"
4+
}
5+
6+
javafx {
7+
version = "23.0.1"
8+
modules = listOf("javafx.controls")
9+
}
10+
11+
repositories {
12+
// Use Maven Central for resolving dependencies.
13+
mavenCentral()
14+
}
15+
16+
dependencies {
17+
implementation("org.graalvm.polyglot:python:24.1.1") //
18+
implementation("org.graalvm.polyglot:polyglot:24.1.1") //
19+
implementation("org.graalvm.tools:dap-tool:24.1.1") //
20+
21+
// Use JUnit Jupiter for testing.
22+
testImplementation("org.junit.jupiter:junit-jupiter:5.11.0")
23+
24+
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
25+
}
26+
27+
application {
28+
// Define the main class for the application.
29+
mainClass = "com.example.App"
30+
}
31+
32+
tasks.named<Test>("test") {
33+
// Use JUnit Platform for unit tests.
34+
useJUnitPlatform()
35+
}
686 KB
Loading
1.18 MB
Loading

0 commit comments

Comments
 (0)