Estou tentando migrar a execução de scripts Python em meus projetos Java do Jython para o GraalVM Polyglot. O problema que estou enfrentando é que não sei como tornar minhas classes Java visíveis para importações desses scripts Python. O teste JUnit a seguir demonstra que o script funciona bem com o Jython, mas não com o suporte Pythong do GraalVM:
package org.my.graalvm.python.issue;
import javax.script.Bindings;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import javax.script.SimpleBindings;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Source;
import org.junit.jupiter.api.Test;
public class GraalVmPythonTests {
static final String SCRIPT = """
import org.my.graalvm.python.issue.GraalVmPythonTests.MyData as MyData
result=MyData('some data')
""";
@Test
void runPythonScriptWithJavaImportOnJython() throws ScriptException {
ScriptEngine scriptEngine = new ScriptEngineManager().getEngineByName("python");
Bindings bindings = new SimpleBindings();
scriptEngine.eval(SCRIPT, bindings);
System.out.println(bindings.get("result"));
}
@Test
void runPythonScriptWithJavaImportOnGraalVmPolyglot() {
try (Context context = Context.newBuilder().allowAllAccess(true).build()) {
Source source = Source.create("python", SCRIPT);
Object result = context.eval(source).as(Object.class);
System.out.println(result);
}
}
public record MyData(String value) {
}
}
As dependências do projeto são:
dependencies {
testImplementation 'org.python:jython-standalone:2.7.4'
testImplementation 'org.graalvm.sdk:graal-sdk:24.0.2'
testImplementation 'org.graalvm.polyglot:python:24.0.2'
testImplementation 'org.junit.jupiter:junit-jupiter:5.11.1'
}
De acordo com a documentação oficial do GraalVM, podemos usar algum --python.EmulateJython
argumento de linha de comando. Mas não tenho certeza de como passá-lo de testes JUnit ou. Além disso, essa opção não parece correta, pois estamos nos afastando completamente do Jython, portanto, espera-se que haja uma maneira mais suave de configurar o Polyglot Context
para estar ciente de nossas classes Java.
Então, a questão é: o que fazer com a configuração do GraalVM Polyglot em Java para tornar nossas classes Java disponíveis a partir de importações de scripts Python?
ATUALIZAR
Com a ajuda de Steves, eu me movi um pouco. Então, agora meu script está assim:
import java
MyData = java.type("org.my.graalvm.python.issue.GraalVmPythonTests.MyData")
result = MyData("some data")
Mesmo com esta execução:
@Test
void runPythonScriptWithJavaImportOnGraalVmPolyglot() {
Context.Builder contextBuilder =
Context.newBuilder("python")
.allowAllAccess(true)
.hostClassLoader(getClass().getClassLoader());
try (Context context = contextBuilder.build()) {
Source source = Source.create("python", SCRIPT);
Object result = context.eval(source).as(Object.class);
System.out.println(result);
}
}
ainda falha assim:
KeyError: host symbol org.my.graalvm.python.issue.GraalVmPythonTests.MyData is not defined or access has been denied
at <python> <module>(Unknown)
at org.graalvm.polyglot.Context.eval(Context.java:402)
at org.my.graalvm.python.issue.GraalVmPythonTests.runPythonScriptWithJavaImportOnGraalVmPolyglot(GraalVmPythonTests.java:40)
Aparentemente, algo mais está faltando na configuração.
Com .option("python.EmulateJython", "true")
ele funciona após essa modificação no script:
from org.my.graalvm.python.issue.GraalVmPythonTests import MyData
result = MyData('some data')
Onde o corpo de teste está assim agora:
@Test
void runPythonScriptWithJavaImportOnGraalVmPolyglot() {
Context.Builder contextBuilder =
Context.newBuilder()
.allowAllAccess(true)
.option("python.EmulateJython", "true");
try (Context context = contextBuilder.build()) {
Source source = Source.create("python", SCRIPT);
Object result = context.eval(source).as(Object.class);
System.out.println(((Map<?, ?>) result).get("result"));
}
}
ATUALIZAÇÃO 2
Então, aparentemente classes internas não são resolvidas dessa forma em Python. Então, aqui está um exemplo funcional sem Jython:
package org.my.graalvm.python.issue;
import java.util.Map;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Source;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
public class GraalVmPythonTests {
static final String SCRIPT = """
import java
GraalVmPythonTests = java.type("org.my.graalvm.python.issue.GraalVmPythonTests")
result = GraalVmPythonTests.MyData('some data')
""";
@Test
void runPythonScriptWithJavaImportOnGraalVmPolyglot() {
Context.Builder contextBuilder =
Context.newBuilder()
.allowAllAccess(true);
try (Context context = contextBuilder.build()) {
Source source = Source.create("python", SCRIPT);
Object result = context.eval(source).as(Object.class);
assertInstanceOf(MyData.class, (((Map<?, ?>) result).get("result")));
}
}
public record MyData(String value) {
}
}
Importar classes Java usando a instrução de importação Python padrão é suportado apenas para pacotes java.*. Ao usar a opção EmulateJython, você pode ativar isso para todos os pacotes Java, mas tem alguma sobrecarga de desempenho, portanto, não é habilitado por padrão. Mais sobre a migração Jython: https://www.graalvm.org/latest/reference-manual/python/Modern-Python-on-JVM/#importing-a-java-package
No seu exemplo, você pode habilitar essa opção passando-a assim:
Context.newBuilder().option("python.EmulateJython", "true")
É realmente melhor se afastar do código específico do Jython eventualmente. Sem o modo de compatibilidade do Jython, você pode acessar classes Java usando
java.type(fullyQualifiedName)
. Veja isto para mais informações: https://www.graalvm.org/latest/reference-manual/python/Interoperability/#interacting-with-java-from-python-scripts