Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add new ClassEnquirer for declaring Python packages #553

Merged
merged 5 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions src/main/java/jep/AllowPythonClassEnquirer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
* Copyright (c) 2024 JEP AUTHORS.
*
* This file is licensed under the the zlib/libpng License.
*
* This software is provided 'as-is', without any express or implied
* warranty. In no event will the authors be held liable for any
* damages arising from the use of this software.
*
* Permission is granted to anyone to use this software for any
* purpose, including commercial applications, and to alter it and
* redistribute it freely, subject to the following restrictions:
*
* 1. The origin of this software must not be misrepresented; you
* must not claim that you wrote the original software. If you use
* this software in a product, an acknowledgment in the product
* documentation would be appreciated but is not required.
*
* 2. Altered source versions must be plainly marked as such, and
* must not be misrepresented as being the original software.
*
* 3. This notice may not be removed or altered from any source
* distribution.
*/
package jep;

import java.util.HashSet;
import java.util.Set;

/**
* <p>
* A {@link ClassEnquirer} that defines specific packages as Python packages.
* This implementation takes a delegate ClassEnquirer to delegate all import
* determinations as Java or Python except in the cases of the specific Python
* package names provided. Imports of the specified packages in the embedded
* Python interpreter will use the Python importer and not the Java importer.
* This is useful when you have package name conflicts between Java and Python.
* Some examples are projects like py4j and tensorflow which have Python package
* names matching Java package names. In those cases, using the default
* ClassEnquirer of {@link ClassList} will lead to the Java packages on the
* classpath taking precedence on imports instead of the Python package. That
* can be solved by using this ClassEnquirer with a delegate of ClassList's
* instance.
* </p>
*
* @author Nate Jensen
*
* @since 4.2.1
*/
public class AllowPythonClassEnquirer implements ClassEnquirer {

protected ClassEnquirer delegate;

protected Set<String> pyPkgNames = new HashSet<>();

/**
* Constructor
*
* @param delegate
* the ClassEnquirer instance to delegate method calls to except
* in the case of the specified Python package names
* @param pythonPackageNames
* the names of Python packages that should not be treated as a
* potential import from Java and instead should immediately
* default to being imported by the normal Python importer
*/
public AllowPythonClassEnquirer(ClassEnquirer delegate,
String... pythonPackageNames) {
this.delegate = delegate;
for (String pyPkg : pythonPackageNames) {
pyPkgNames.add(pyPkg);
}
}

@Override
public boolean isJavaPackage(String name) {
for (String pyPkg : pyPkgNames) {
if (name.equals(pyPkg) || name.startsWith(pyPkg + ".")) {
return false;
}
}

return delegate.isJavaPackage(name);
}

@Override
public String[] getClassNames(String pkgName) {
return delegate.getClassNames(pkgName);
}

@Override
public String[] getSubPackages(String pkgName) {
return delegate.getSubPackages(pkgName);
}

}
132 changes: 132 additions & 0 deletions src/test/java/jep/test/TestAllowPythonEnquirer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package jep.test;

import jep.AllowPythonClassEnquirer;
import jep.ClassEnquirer;
import jep.Interpreter;
import jep.JepConfig;
import jep.JepException;

/**
* Tests for the AllowPythonClassEnquirer.
*
* Created: August 2024
*
* @author Nate Jensen
*/
public class TestAllowPythonEnquirer {

/**
* Mock naive ClassEnquirer that thinks Java packages are re and java.
*/
public static class ReAndJavaClassEnquirer implements ClassEnquirer {

@Override
public boolean isJavaPackage(String name) {
return "re".equals(name) || "java".equals(name)
|| name.startsWith("java.");
}

@Override
public String[] getClassNames(String pkgName) {
return null;
}

@Override
public String[] getSubPackages(String pkgName) {
return null;
}

}

public static void main(String[] args) throws JepException {
/*
* Test that the re and java enquirer fails to import a Python type that
* exists
*/
JepConfig config = new JepConfig();
ClassEnquirer reJavaEnquirer = new ReAndJavaClassEnquirer();
config.setClassEnquirer(reJavaEnquirer);
try (Interpreter interp = config.createSubInterpreter()) {

boolean gotClassNotFoundException = false;
try {
interp.exec("import sys");
interp.exec("if 're' in sys.modules:\n"
+ " sys.modules.pop('re')");
interp.exec("from re import Pattern");
} catch (JepException e) {
if (e.getCause() instanceof ClassNotFoundException) {
/*
* Tested ok, we expected a failure to import as it tried to
* import Pattern from re package as if it was a Java class.
* ClassNotFoundException indicates it was a failure to
* import from Java.
*/
gotClassNotFoundException = true;
} else {
throw e;
}
}

if (!gotClassNotFoundException) {
System.err.println(
"Expected a failed Java import of 'from re import Pattern'");
System.exit(1);
}
}

/*
* Test that the allow python enquirer does not delegate to the re and
* java enquirer when encountering re
*/
config = new JepConfig();
config.setClassEnquirer(
new AllowPythonClassEnquirer(reJavaEnquirer, "re"));
try (Interpreter interp = config.createSubInterpreter()) {
interp.exec("from re import Pattern");
/*
* This should still work as java.util.HashMap is on the classpath
* and the enquirer should indicate it's a Java import
*/
interp.exec("from java.util import HashMap");
} catch (Exception e) {
e.printStackTrace();
System.err.println(
"Expected a successful Python import of 'from re import Pattern'"
+ " and a successful Java import of 'from java.util import HashMap");
System.exit(1);
}

/*
* Test that even if a Java package is available, if it's declared as a
* Python package then it will go to the Python importer.
*/
config = new JepConfig();
config.setClassEnquirer(
new AllowPythonClassEnquirer(reJavaEnquirer, "java.lang.ref"));
try (Interpreter interp = config.createSubInterpreter()) {
interp.exec("moduleNotFound = False");
interp.exec("try:\n" + " from java.lang.ref import Reference\n"
+ "except ModuleNotFoundError as e:\n"
+ " moduleNotFound = True");
boolean moduleNotFoundErrorWasRaised = interp
.getValue("moduleNotFound", Boolean.class);
if (!moduleNotFoundErrorWasRaised) {
System.err.println("Expected ModuleNotFoundError when running "
+ "'from java.lang.ref import Reference'"
+ " with ClassEnquirer that considers the "
+ "java.lang.ref package as a Python package");
System.exit(1);
}

/*
* Verify java.lang.reflect still works even though java.lang.ref
* was declared as a Python package (i.e. so it's a little smarter
* than doing just a String.startsWith()). It would throw an
* uncaught exception if the import failed.
*/
interp.exec("from java.lang.reflect import Method");
}
}

}
9 changes: 9 additions & 0 deletions src/test/python/test_allow_python_enquirer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import unittest

from jep_pipe import jep_pipe
from jep_pipe import build_java_process_cmd

class TestAllowPythonEnquirer(unittest.TestCase):

def test_allow_python_enquirer(self):
jep_pipe(build_java_process_cmd('jep.test.TestAllowPythonEnquirer'))