Skip to content
This repository has been archived by the owner on Jul 17, 2024. It is now read-only.

Commit

Permalink
feat: Allow jpyinterpreter to create annotations on classes, fields a…
Browse files Browse the repository at this point in the history
…nd methods

- Fields annotations are created using Annotated in type hints
- Method annotations are created using Annotated in a method's return
  type hints
- Class annotations are created using a decorator which stores the
  annotation in a dict
- Changed values in type hint dictionary from PythonLikeType to TypeHint
- TypeHint is a record containing the type and any Java annotations
- Java annotations are stored as instances of AnnotationMetadata,
  a record containing the annotation type and value of its attributes
  • Loading branch information
Christopher-Chianelli committed Mar 14, 2024
1 parent 4349d20 commit 6282186
Show file tree
Hide file tree
Showing 12 changed files with 416 additions and 63 deletions.
4 changes: 3 additions & 1 deletion create-stubs.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
import jpype.imports # noqa
import ai.timefold.solver.core.api # noqa
import ai.timefold.solver.core.config # noqa
import ai.timefold.jpyinterpreter # noqa
import java.lang # noqa
import java.time # noqa
import java.util # noqa

stubgenj.generateJavaStubs([java.lang, java.time, java.util, ai.timefold.solver.core.api, ai.timefold.solver.core.config],
stubgenj.generateJavaStubs([java.lang, java.time, java.util, ai.timefold.solver.core.api,
ai.timefold.solver.core.config, ai.timefold.jpyinterpreter],
useStubsSuffix=True)

Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package ai.timefold.jpyinterpreter;

import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.util.Map;

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;

public record AnnotationMetadata(Class<? extends Annotation> annotationType, Map<String, Object> annotationValueMap) {
public void addAnnotationTo(ClassVisitor classVisitor) {
visitAnnotation(classVisitor.visitAnnotation(Type.getDescriptor(annotationType), true));
}

public void addAnnotationTo(FieldVisitor fieldVisitor) {
visitAnnotation(fieldVisitor.visitAnnotation(Type.getDescriptor(annotationType), true));
}

public void addAnnotationTo(MethodVisitor methodVisitor) {
visitAnnotation(methodVisitor.visitAnnotation(Type.getDescriptor(annotationType), true));
}

private void visitAnnotation(AnnotationVisitor annotationVisitor) {
for (var entry : annotationValueMap.entrySet()) {
var annotationAttributeName = entry.getKey();
var annotationAttributeValue = entry.getValue();

visitAnnotationAttribute(annotationVisitor, annotationAttributeName, annotationAttributeValue);
}
annotationVisitor.visitEnd();
}

private void visitAnnotationAttribute(AnnotationVisitor annotationVisitor, String attributeName, Object attributeValue) {
if (attributeValue instanceof Number
|| attributeValue instanceof Boolean
|| attributeValue instanceof Character
|| attributeValue instanceof String) {
annotationVisitor.visit(attributeName, attributeValue);
return;
}

if (attributeValue instanceof Class<?> clazz) {
annotationVisitor.visit(attributeName, Type.getType(clazz));
return;
}

if (attributeValue instanceof AnnotationMetadata annotationMetadata) {
annotationMetadata.visitAnnotation(
annotationVisitor.visitAnnotation(attributeName, Type.getDescriptor(annotationMetadata.annotationType)));
return;
}

if (attributeValue instanceof Enum<?> enumValue) {
annotationVisitor.visitEnum(attributeName, Type.getDescriptor(enumValue.getClass()),
enumValue.name());
return;
}

if (attributeValue.getClass().isArray()) {
var arrayAnnotationVisitor = annotationVisitor.visitArray(attributeName);
var arrayLength = Array.getLength(attributeValue);
for (int i = 0; i < arrayLength; i++) {
visitAnnotationAttribute(arrayAnnotationVisitor, attributeName, Array.get(attributeValue, i));
}
arrayAnnotationVisitor.visitEnd();
return;
}
throw new IllegalArgumentException("Annotation of type %s has an illegal value %s for attribute %s."
.formatted(annotationType, attributeValue, attributeName));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ public static <T> T translatePythonBytecode(PythonCompiledFunction pythonCompile
PythonLikeTuple annotationTuple = pythonCompiledFunction.typeAnnotations.entrySet()
.stream()
.map(entry -> PythonLikeTuple.fromList(List.of(PythonString.valueOf(entry.getKey()),
entry.getValue() != null ? entry.getValue() : BuiltinTypes.BASE_TYPE)))
entry.getValue() != null ? entry.getValue().type() : BuiltinTypes.BASE_TYPE)))
.collect(Collectors.toCollection(PythonLikeTuple::new));
return FunctionImplementor.createInstance(pythonCompiledFunction.defaultPositionalArguments,
pythonCompiledFunction.defaultKeywordArguments,
Expand All @@ -161,7 +161,7 @@ public static <T> T translatePythonBytecode(PythonCompiledFunction pythonCompile
translatePythonBytecodeToClass(pythonCompiledFunction, javaFunctionalInterfaceType, genericTypeArgumentList);
PythonLikeTuple annotationTuple = pythonCompiledFunction.typeAnnotations.entrySet()
.stream()
.map(entry -> PythonLikeTuple.fromList(List.of(PythonString.valueOf(entry.getKey()), entry.getValue())))
.map(entry -> PythonLikeTuple.fromList(List.of(PythonString.valueOf(entry.getKey()), entry.getValue().type())))
.collect(Collectors.toCollection(PythonLikeTuple::new));
return FunctionImplementor.createInstance(pythonCompiledFunction.defaultPositionalArguments,
pythonCompiledFunction.defaultKeywordArguments,
Expand Down Expand Up @@ -216,7 +216,7 @@ public static <T> T translatePythonBytecodeToInstance(PythonCompiledFunction pyt
Class<T> compiledClass = translatePythonBytecodeToClass(pythonCompiledFunction, methodDescriptor, isVirtual);
PythonLikeTuple annotationTuple = pythonCompiledFunction.typeAnnotations.entrySet()
.stream()
.map(entry -> PythonLikeTuple.fromList(List.of(PythonString.valueOf(entry.getKey()), entry.getValue())))
.map(entry -> PythonLikeTuple.fromList(List.of(PythonString.valueOf(entry.getKey()), entry.getValue().type())))
.collect(Collectors.toCollection(PythonLikeTuple::new));
return FunctionImplementor.createInstance(pythonCompiledFunction.defaultPositionalArguments,
pythonCompiledFunction.defaultKeywordArguments,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ public class PythonClassTranslator {
// $ is illegal in variables/methods in Python
public static String TYPE_FIELD_NAME = "$TYPE";
public static String CPYTHON_TYPE_FIELD_NAME = "$CPYTHON_TYPE";
private static String JAVA_FIELD_PREFIX = "$field$";
private static String JAVA_METHOD_PREFIX = "$method$";

public static PythonLikeType translatePythonClass(PythonCompiledClass pythonCompiledClass) {
String maybeClassName =
Expand Down Expand Up @@ -144,6 +146,10 @@ public static PythonLikeType translatePythonClass(PythonCompiledClass pythonComp
classWriter.visit(Opcodes.V11, Modifier.PUBLIC, internalClassName, null,
superClassType.getJavaTypeInternalName(), interfaces);

for (var annotation : pythonCompiledClass.annotations) {
annotation.addAnnotationTo(classWriter);
}

pythonCompiledClass.staticAttributeNameToObject.forEach(pythonLikeType::$setAttribute);

classWriter.visitField(Modifier.PUBLIC | Modifier.STATIC, TYPE_FIELD_NAME, Type.getDescriptor(PythonLikeType.class),
Expand All @@ -157,15 +163,28 @@ public static PythonLikeType translatePythonClass(PythonCompiledClass pythonComp
pythonLikeType.$setAttribute(staticAttributeEntry.getKey(), staticAttributeEntry.getValue());
}

for (var attributeName : pythonCompiledClass.typeAnnotations.keySet()) {
if (pythonLikeType.$getAttributeOrNull(attributeName) == null) {
instanceAttributeSet.add(attributeName);
}
}

Map<String, PythonLikeType> attributeNameToTypeMap = new HashMap<>();
for (String attributeName : instanceAttributeSet) {
PythonLikeType type = pythonCompiledClass.typeAnnotations.getOrDefault(attributeName, BuiltinTypes.BASE_TYPE);
var typeHint = pythonCompiledClass.typeAnnotations.getOrDefault(attributeName,
TypeHint.withoutAnnotations(BuiltinTypes.BASE_TYPE));
PythonLikeType type = typeHint.type();
if (type == null) { // null might be in __annotations__
type = BuiltinTypes.BASE_TYPE;
}
String javaFieldTypeDescriptor = 'L' + type.getJavaTypeInternalName() + ';';
attributeNameToTypeMap.put(attributeName, type);
classWriter.visitField(Modifier.PUBLIC, getJavaFieldName(attributeName), javaFieldTypeDescriptor, null, null);
var fieldVisitor = classWriter.visitField(Modifier.PUBLIC, getJavaFieldName(attributeName), javaFieldTypeDescriptor,
null, null);
for (var annotation : typeHint.annotationList()) {
annotation.addAnnotationTo(fieldVisitor);
}
fieldVisitor.visitEnd();
FieldDescriptor fieldDescriptor =
new FieldDescriptor(attributeName, getJavaFieldName(attributeName), internalClassName,
javaFieldTypeDescriptor, type, true);
Expand Down Expand Up @@ -264,7 +283,7 @@ public static PythonLikeType translatePythonClass(PythonCompiledClass pythonComp
pythonLikeType.$setAttribute("__module__", PythonString.valueOf(pythonCompiledClass.module));

PythonLikeDict annotations = new PythonLikeDict();
pythonCompiledClass.typeAnnotations.forEach((name, type) -> annotations.put(PythonString.valueOf(name), type));
pythonCompiledClass.typeAnnotations.forEach((name, type) -> annotations.put(PythonString.valueOf(name), type.type()));
pythonLikeType.$setAttribute("__annotations__", annotations);

PythonLikeTuple mro = new PythonLikeTuple();
Expand Down Expand Up @@ -347,19 +366,19 @@ public static void setSelfStaticInstances(PythonCompiledClass pythonCompiledClas
}

public static String getJavaFieldName(String pythonFieldName) {
return "$field$" + pythonFieldName;
return JAVA_FIELD_PREFIX + pythonFieldName;
}

public static String getPythonFieldName(String javaFieldName) {
return javaFieldName.substring("$field$".length());
return javaFieldName.substring(JAVA_FIELD_PREFIX.length());
}

public static String getJavaMethodName(String pythonMethodName) {
return "$method$" + pythonMethodName;
return JAVA_METHOD_PREFIX + pythonMethodName;
}

public static String getPythonMethodName(String javaMethodName) {
return javaMethodName.substring("$method$".length());
return javaMethodName.substring(JAVA_METHOD_PREFIX.length());
}

private static Class<?> createBytecodeForMethodAndSetOnClass(String className, PythonLikeType pythonLikeType,
Expand Down Expand Up @@ -673,6 +692,15 @@ private static PythonLikeFunction createConstructor(String classInternalName,
}
}

private static void addAnnotationsToMethod(PythonCompiledFunction function, MethodVisitor methodVisitor) {
var returnTypeHint = function.typeAnnotations.get("return");
if (returnTypeHint != null) {
for (var annotation : returnTypeHint.annotationList()) {
annotation.addAnnotationTo(methodVisitor);
}
}
}

private static void createInstanceMethod(PythonLikeType pythonLikeType, ClassWriter classWriter, String internalClassName,
String methodName, PythonCompiledFunction function) {
InterfaceDeclaration interfaceDeclaration = getInterfaceForInstancePythonFunction(internalClassName, function);
Expand All @@ -693,7 +721,8 @@ private static void createInstanceMethod(PythonLikeType pythonLikeType, ClassWri
MethodVisitor methodVisitor =
classWriter.visitMethod(Modifier.PUBLIC, javaMethodName, javaMethodDescriptor, null, null);

createMethodBody(internalClassName, javaMethodName, javaParameterTypes, interfaceDeclaration.methodDescriptor, function,
createInstanceOrStaticMethodBody(internalClassName, javaMethodName, javaParameterTypes,
interfaceDeclaration.methodDescriptor, function,
interfaceDeclaration.interfaceName, interfaceDescriptor, methodVisitor);

pythonLikeType.addMethod(methodName,
Expand Down Expand Up @@ -722,7 +751,9 @@ private static void createStaticMethod(PythonLikeType pythonLikeType, ClassWrite
for (int i = 0; i < function.totalArgCount(); i++) {
javaParameterTypes[i] = Type.getType('L' + parameterPythonTypeList.get(i).getJavaTypeInternalName() + ';');
}
createMethodBody(internalClassName, javaMethodName, javaParameterTypes, interfaceDeclaration.methodDescriptor, function,

createInstanceOrStaticMethodBody(internalClassName, javaMethodName, javaParameterTypes,
interfaceDeclaration.methodDescriptor, function,
interfaceDeclaration.interfaceName, interfaceDescriptor, methodVisitor);

pythonLikeType.addMethod(methodName,
Expand All @@ -746,8 +777,10 @@ private static void createClassMethod(PythonLikeType pythonLikeType, ClassWriter
classWriter.visitMethod(Modifier.PUBLIC | Modifier.STATIC, javaMethodName, javaMethodDescriptor, null, null);

for (int i = 0; i < function.getParameterTypes().size(); i++) {
methodVisitor.visitParameter("parameter" + i, 0);
methodVisitor.visitParameter(function.co_varnames.get(i), 0);
}

addAnnotationsToMethod(function, methodVisitor);
methodVisitor.visitCode();

methodVisitor.visitFieldInsn(Opcodes.GETSTATIC, internalClassName, javaMethodName, interfaceDescriptor);
Expand Down Expand Up @@ -775,13 +808,15 @@ private static void createClassMethod(PythonLikeType pythonLikeType, ClassWriter
parameterTypes));
}

private static void createMethodBody(String internalClassName, String javaMethodName, Type[] javaParameterTypes,
private static void createInstanceOrStaticMethodBody(String internalClassName, String javaMethodName,
Type[] javaParameterTypes,
String methodDescriptorString,
PythonCompiledFunction function, String interfaceInternalName, String interfaceDescriptor,
MethodVisitor methodVisitor) {
for (int i = 0; i < javaParameterTypes.length; i++) {
methodVisitor.visitParameter("parameter" + i, 0);
methodVisitor.visitParameter(function.co_varnames.get(i), 0);
}
addAnnotationsToMethod(function, methodVisitor);
methodVisitor.visitCode();

methodVisitor.visitFieldInsn(Opcodes.GETSTATIC, internalClassName, javaMethodName, interfaceDescriptor);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,15 @@ public class PythonCompiledClass {

public String className;

/**
* The annotations on the type
*/
public List<AnnotationMetadata> annotations;

/**
* Type annotations for fields
*/
public Map<String, PythonLikeType> typeAnnotations;
public Map<String, TypeHint> typeAnnotations;

/**
* The binary type of this PythonCompiledClass;
Expand All @@ -47,6 +52,9 @@ public class PythonCompiledClass {
*/
public Map<String, OpaquePythonReference> staticAttributeNameToClassInstance;

public PythonCompiledClass() {
}

public String getGeneratedClassBaseName() {
if (module == null || module.isEmpty()) {
return JavaIdentifierUtils.sanitizeClassName((qualifiedName != null) ? qualifiedName : "PythonClass");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public class PythonCompiledFunction {
* Type annotations for the parameters and return.
* (return is stored under the "return" key).
*/
public Map<String, PythonLikeType> typeAnnotations;
public Map<String, TypeHint> typeAnnotations;

/**
* Default positional arguments
Expand Down Expand Up @@ -155,17 +155,22 @@ public List<PythonLikeType> getParameterTypes() {

for (int i = 0; i < totalArgCount(); i++) {
String parameterName = co_varnames.get(i);
PythonLikeType parameterType = typeAnnotations.get(parameterName);
if (parameterType == null) { // map may have nulls
parameterType = defaultType;
var parameterTypeHint = typeAnnotations.get(parameterName);
PythonLikeType parameterType = defaultType;
if (parameterTypeHint != null) {
parameterType = parameterTypeHint.type();
}
out.add(parameterType);
}
return out;
}

public Optional<PythonLikeType> getReturnType() {
return Optional.ofNullable(typeAnnotations.get("return"));
var returnTypeHint = typeAnnotations.get("return");
if (returnTypeHint == null) {
return Optional.empty();
}
return Optional.of(returnTypeHint.type());
}

public String getAsmMethodDescriptorString() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package ai.timefold.jpyinterpreter;

import java.util.List;

import ai.timefold.jpyinterpreter.types.PythonLikeType;

public record TypeHint(PythonLikeType type, List<AnnotationMetadata> annotationList) {
public static TypeHint withoutAnnotations(PythonLikeType type) {
return new TypeHint(type, List.of());
}

}
14 changes: 9 additions & 5 deletions jpyinterpreter/src/main/python/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@
This module acts as an interface to the Python bytecode to Java bytecode interpreter
"""
from .jvm_setup import init, set_class_output_directory
from .python_to_java_bytecode_translator import translate_python_bytecode_to_java_bytecode, \
translate_python_class_to_java_class, convert_to_java_python_like_object, force_update_type, \
get_java_type_for_python_type, unwrap_python_like_object, as_java, as_untyped_java, as_typed_java, is_c_native, \
is_current_python_version_supported, check_current_python_version_supported, is_python_version_supported, \
_force_as_java_generator
from .python_to_java_bytecode_translator import (JavaAnnotation, add_class_annotation,
translate_python_bytecode_to_java_bytecode,
translate_python_class_to_java_class,
convert_to_java_python_like_object, force_update_type,
get_java_type_for_python_type, unwrap_python_like_object, as_java,
as_untyped_java, as_typed_java, is_c_native,
is_current_python_version_supported,
check_current_python_version_supported, is_python_version_supported,
_force_as_java_generator)
Loading

0 comments on commit 6282186

Please sign in to comment.