Search code examples
graalvmgraalpython

How to make my Java classes visible for Python imports


I'm trying to migrate Python scripts execution in my Java projects from Jython to GraalVM Polyglot. The problem I'm facing that I don't know how to make my Java classes visible for imports from those Python scripts. The following JUnit test demonstrates that script works well with Jython, but does not with GraalVM Pythong support:

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) {

    }

}

Dependencies for the project are:

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'
}

According to official GraalVM docs we can use some --python.EmulateJython command line arg. But I'm not sure how to pass it from JUnit tests or. Plus this option does not feel right since we are moving away from Jython altogether, therefore it is expected to have some more smooth way to configure that Polyglot Context to be aware of our Java classes.

So, the question is: what to do with GraalVM Polyglot configuration in Java to make our Java classes available from Python script imports?

UPDATE

With the help from Steves I have moved a little bit. So, now my script looks like this:

import java
            
MyData = java.type("org.my.graalvm.python.issue.GraalVmPythonTests.MyData")
            
result = MyData("some data")

Even with this execution:

@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);
    }
}

it still fails like this:

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)

Apparently something else is missing in the configuration or so.

With .option("python.EmulateJython", "true") it works after this modification to the script:

        from org.my.graalvm.python.issue.GraalVmPythonTests import MyData
        
        result = MyData('some data')

Where test body is like this now:

@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"));
    }
}

UPDATE 2

So, apparently inner classes are not resolved that way in Python. So, here is a working example without 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) {

    }

}

Solution

  • Importing Java classes using standard Python import statement is supported only for java.* packages. By using the EmulateJython option you can turn this on for all Java packages, but it has some performance overhead, hence it's not enabled by default. More about Jython migration: https://www.graalvm.org/latest/reference-manual/python/Modern-Python-on-JVM/#importing-a-java-package

    In your example you can enable that option by passing it like this: Context.newBuilder().option("python.EmulateJython", "true")

    It's indeed best to move away from Jython specific code eventually. Without the Jython compatibility mode you can access Java classes using java.type(fullyQualifiedName). See this for more info: https://www.graalvm.org/latest/reference-manual/python/Interoperability/#interacting-with-java-from-python-scripts