Skip to content
This repository was archived by the owner on Oct 6, 2025. It is now read-only.

Commit 5fd92c8

Browse files
feat: Allow jpyinterpreter to create annotations on classes, fields and methods (#21)
- 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
1 parent 4349d20 commit 5fd92c8

25 files changed

+480
-127
lines changed

create-stubs.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@
99
import jpype.imports # noqa
1010
import ai.timefold.solver.core.api # noqa
1111
import ai.timefold.solver.core.config # noqa
12+
import ai.timefold.jpyinterpreter # noqa
1213
import java.lang # noqa
1314
import java.time # noqa
1415
import java.util # noqa
1516

16-
stubgenj.generateJavaStubs([java.lang, java.time, java.util, ai.timefold.solver.core.api, ai.timefold.solver.core.config],
17+
stubgenj.generateJavaStubs([java.lang, java.time, java.util, ai.timefold.solver.core.api,
18+
ai.timefold.solver.core.config, ai.timefold.jpyinterpreter],
1719
useStubsSuffix=True)
1820

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package ai.timefold.jpyinterpreter;
2+
3+
import java.lang.annotation.Annotation;
4+
import java.lang.reflect.Array;
5+
import java.util.Map;
6+
7+
import org.objectweb.asm.AnnotationVisitor;
8+
import org.objectweb.asm.ClassVisitor;
9+
import org.objectweb.asm.FieldVisitor;
10+
import org.objectweb.asm.MethodVisitor;
11+
import org.objectweb.asm.Type;
12+
13+
public record AnnotationMetadata(Class<? extends Annotation> annotationType, Map<String, Object> annotationValueMap) {
14+
public void addAnnotationTo(ClassVisitor classVisitor) {
15+
visitAnnotation(classVisitor.visitAnnotation(Type.getDescriptor(annotationType), true));
16+
}
17+
18+
public void addAnnotationTo(FieldVisitor fieldVisitor) {
19+
visitAnnotation(fieldVisitor.visitAnnotation(Type.getDescriptor(annotationType), true));
20+
}
21+
22+
public void addAnnotationTo(MethodVisitor methodVisitor) {
23+
visitAnnotation(methodVisitor.visitAnnotation(Type.getDescriptor(annotationType), true));
24+
}
25+
26+
private void visitAnnotation(AnnotationVisitor annotationVisitor) {
27+
for (var entry : annotationValueMap.entrySet()) {
28+
var annotationAttributeName = entry.getKey();
29+
var annotationAttributeValue = entry.getValue();
30+
31+
visitAnnotationAttribute(annotationVisitor, annotationAttributeName, annotationAttributeValue);
32+
}
33+
annotationVisitor.visitEnd();
34+
}
35+
36+
private void visitAnnotationAttribute(AnnotationVisitor annotationVisitor, String attributeName, Object attributeValue) {
37+
if (attributeValue instanceof Number
38+
|| attributeValue instanceof Boolean
39+
|| attributeValue instanceof Character
40+
|| attributeValue instanceof String) {
41+
annotationVisitor.visit(attributeName, attributeValue);
42+
return;
43+
}
44+
45+
if (attributeValue instanceof Class<?> clazz) {
46+
annotationVisitor.visit(attributeName, Type.getType(clazz));
47+
return;
48+
}
49+
50+
if (attributeValue instanceof AnnotationMetadata annotationMetadata) {
51+
annotationMetadata.visitAnnotation(
52+
annotationVisitor.visitAnnotation(attributeName, Type.getDescriptor(annotationMetadata.annotationType)));
53+
return;
54+
}
55+
56+
if (attributeValue instanceof Enum<?> enumValue) {
57+
annotationVisitor.visitEnum(attributeName, Type.getDescriptor(enumValue.getClass()),
58+
enumValue.name());
59+
return;
60+
}
61+
62+
if (attributeValue.getClass().isArray()) {
63+
var arrayAnnotationVisitor = annotationVisitor.visitArray(attributeName);
64+
var arrayLength = Array.getLength(attributeValue);
65+
for (int i = 0; i < arrayLength; i++) {
66+
visitAnnotationAttribute(arrayAnnotationVisitor, attributeName, Array.get(attributeValue, i));
67+
}
68+
arrayAnnotationVisitor.visitEnd();
69+
return;
70+
}
71+
throw new IllegalArgumentException("Annotation of type %s has an illegal value %s for attribute %s."
72+
.formatted(annotationType, attributeValue, attributeName));
73+
}
74+
}

jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonBytecodeToJavaBytecodeTranslator.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,8 @@ public static <T> T translatePythonBytecode(PythonCompiledFunction pythonCompile
145145
Class<T> compiledClass = translatePythonBytecodeToClass(pythonCompiledFunction, javaFunctionalInterfaceType);
146146
PythonLikeTuple annotationTuple = pythonCompiledFunction.typeAnnotations.entrySet()
147147
.stream()
148-
.map(entry -> PythonLikeTuple.fromList(List.of(PythonString.valueOf(entry.getKey()),
149-
entry.getValue() != null ? entry.getValue() : BuiltinTypes.BASE_TYPE)))
148+
.map(entry -> PythonLikeTuple.fromItems(PythonString.valueOf(entry.getKey()),
149+
entry.getValue() != null ? entry.getValue().type() : BuiltinTypes.BASE_TYPE))
150150
.collect(Collectors.toCollection(PythonLikeTuple::new));
151151
return FunctionImplementor.createInstance(pythonCompiledFunction.defaultPositionalArguments,
152152
pythonCompiledFunction.defaultKeywordArguments,
@@ -161,7 +161,7 @@ public static <T> T translatePythonBytecode(PythonCompiledFunction pythonCompile
161161
translatePythonBytecodeToClass(pythonCompiledFunction, javaFunctionalInterfaceType, genericTypeArgumentList);
162162
PythonLikeTuple annotationTuple = pythonCompiledFunction.typeAnnotations.entrySet()
163163
.stream()
164-
.map(entry -> PythonLikeTuple.fromList(List.of(PythonString.valueOf(entry.getKey()), entry.getValue())))
164+
.map(entry -> PythonLikeTuple.fromItems(PythonString.valueOf(entry.getKey()), entry.getValue().type()))
165165
.collect(Collectors.toCollection(PythonLikeTuple::new));
166166
return FunctionImplementor.createInstance(pythonCompiledFunction.defaultPositionalArguments,
167167
pythonCompiledFunction.defaultKeywordArguments,
@@ -216,7 +216,7 @@ public static <T> T translatePythonBytecodeToInstance(PythonCompiledFunction pyt
216216
Class<T> compiledClass = translatePythonBytecodeToClass(pythonCompiledFunction, methodDescriptor, isVirtual);
217217
PythonLikeTuple annotationTuple = pythonCompiledFunction.typeAnnotations.entrySet()
218218
.stream()
219-
.map(entry -> PythonLikeTuple.fromList(List.of(PythonString.valueOf(entry.getKey()), entry.getValue())))
219+
.map(entry -> PythonLikeTuple.fromItems(PythonString.valueOf(entry.getKey()), entry.getValue().type()))
220220
.collect(Collectors.toCollection(PythonLikeTuple::new));
221221
return FunctionImplementor.createInstance(pythonCompiledFunction.defaultPositionalArguments,
222222
pythonCompiledFunction.defaultKeywordArguments,

jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonClassTranslator.java

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ public class PythonClassTranslator {
5656
// $ is illegal in variables/methods in Python
5757
public static String TYPE_FIELD_NAME = "$TYPE";
5858
public static String CPYTHON_TYPE_FIELD_NAME = "$CPYTHON_TYPE";
59+
private static String JAVA_FIELD_PREFIX = "$field$";
60+
private static String JAVA_METHOD_PREFIX = "$method$";
5961

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

149+
for (var annotation : pythonCompiledClass.annotations) {
150+
annotation.addAnnotationTo(classWriter);
151+
}
152+
147153
pythonCompiledClass.staticAttributeNameToObject.forEach(pythonLikeType::$setAttribute);
148154

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

166+
for (var attributeName : pythonCompiledClass.typeAnnotations.keySet()) {
167+
if (pythonLikeType.$getAttributeOrNull(attributeName) == null) {
168+
instanceAttributeSet.add(attributeName);
169+
}
170+
}
171+
160172
Map<String, PythonLikeType> attributeNameToTypeMap = new HashMap<>();
161173
for (String attributeName : instanceAttributeSet) {
162-
PythonLikeType type = pythonCompiledClass.typeAnnotations.getOrDefault(attributeName, BuiltinTypes.BASE_TYPE);
174+
var typeHint = pythonCompiledClass.typeAnnotations.getOrDefault(attributeName,
175+
TypeHint.withoutAnnotations(BuiltinTypes.BASE_TYPE));
176+
PythonLikeType type = typeHint.type();
163177
if (type == null) { // null might be in __annotations__
164178
type = BuiltinTypes.BASE_TYPE;
165179
}
166180
String javaFieldTypeDescriptor = 'L' + type.getJavaTypeInternalName() + ';';
167181
attributeNameToTypeMap.put(attributeName, type);
168-
classWriter.visitField(Modifier.PUBLIC, getJavaFieldName(attributeName), javaFieldTypeDescriptor, null, null);
182+
var fieldVisitor = classWriter.visitField(Modifier.PUBLIC, getJavaFieldName(attributeName), javaFieldTypeDescriptor,
183+
null, null);
184+
for (var annotation : typeHint.annotationList()) {
185+
annotation.addAnnotationTo(fieldVisitor);
186+
}
187+
fieldVisitor.visitEnd();
169188
FieldDescriptor fieldDescriptor =
170189
new FieldDescriptor(attributeName, getJavaFieldName(attributeName), internalClassName,
171190
javaFieldTypeDescriptor, type, true);
@@ -264,7 +283,7 @@ public static PythonLikeType translatePythonClass(PythonCompiledClass pythonComp
264283
pythonLikeType.$setAttribute("__module__", PythonString.valueOf(pythonCompiledClass.module));
265284

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

270289
PythonLikeTuple mro = new PythonLikeTuple();
@@ -347,19 +366,19 @@ public static void setSelfStaticInstances(PythonCompiledClass pythonCompiledClas
347366
}
348367

349368
public static String getJavaFieldName(String pythonFieldName) {
350-
return "$field$" + pythonFieldName;
369+
return JAVA_FIELD_PREFIX + pythonFieldName;
351370
}
352371

353372
public static String getPythonFieldName(String javaFieldName) {
354-
return javaFieldName.substring("$field$".length());
373+
return javaFieldName.substring(JAVA_FIELD_PREFIX.length());
355374
}
356375

357376
public static String getJavaMethodName(String pythonMethodName) {
358-
return "$method$" + pythonMethodName;
377+
return JAVA_METHOD_PREFIX + pythonMethodName;
359378
}
360379

361380
public static String getPythonMethodName(String javaMethodName) {
362-
return javaMethodName.substring("$method$".length());
381+
return javaMethodName.substring(JAVA_METHOD_PREFIX.length());
363382
}
364383

365384
private static Class<?> createBytecodeForMethodAndSetOnClass(String className, PythonLikeType pythonLikeType,
@@ -673,6 +692,15 @@ private static PythonLikeFunction createConstructor(String classInternalName,
673692
}
674693
}
675694

695+
private static void addAnnotationsToMethod(PythonCompiledFunction function, MethodVisitor methodVisitor) {
696+
var returnTypeHint = function.typeAnnotations.get("return");
697+
if (returnTypeHint != null) {
698+
for (var annotation : returnTypeHint.annotationList()) {
699+
annotation.addAnnotationTo(methodVisitor);
700+
}
701+
}
702+
}
703+
676704
private static void createInstanceMethod(PythonLikeType pythonLikeType, ClassWriter classWriter, String internalClassName,
677705
String methodName, PythonCompiledFunction function) {
678706
InterfaceDeclaration interfaceDeclaration = getInterfaceForInstancePythonFunction(internalClassName, function);
@@ -693,7 +721,8 @@ private static void createInstanceMethod(PythonLikeType pythonLikeType, ClassWri
693721
MethodVisitor methodVisitor =
694722
classWriter.visitMethod(Modifier.PUBLIC, javaMethodName, javaMethodDescriptor, null, null);
695723

696-
createMethodBody(internalClassName, javaMethodName, javaParameterTypes, interfaceDeclaration.methodDescriptor, function,
724+
createInstanceOrStaticMethodBody(internalClassName, javaMethodName, javaParameterTypes,
725+
interfaceDeclaration.methodDescriptor, function,
697726
interfaceDeclaration.interfaceName, interfaceDescriptor, methodVisitor);
698727

699728
pythonLikeType.addMethod(methodName,
@@ -722,7 +751,9 @@ private static void createStaticMethod(PythonLikeType pythonLikeType, ClassWrite
722751
for (int i = 0; i < function.totalArgCount(); i++) {
723752
javaParameterTypes[i] = Type.getType('L' + parameterPythonTypeList.get(i).getJavaTypeInternalName() + ';');
724753
}
725-
createMethodBody(internalClassName, javaMethodName, javaParameterTypes, interfaceDeclaration.methodDescriptor, function,
754+
755+
createInstanceOrStaticMethodBody(internalClassName, javaMethodName, javaParameterTypes,
756+
interfaceDeclaration.methodDescriptor, function,
726757
interfaceDeclaration.interfaceName, interfaceDescriptor, methodVisitor);
727758

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

748779
for (int i = 0; i < function.getParameterTypes().size(); i++) {
749-
methodVisitor.visitParameter("parameter" + i, 0);
780+
methodVisitor.visitParameter(function.co_varnames.get(i), 0);
750781
}
782+
783+
addAnnotationsToMethod(function, methodVisitor);
751784
methodVisitor.visitCode();
752785

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

778-
private static void createMethodBody(String internalClassName, String javaMethodName, Type[] javaParameterTypes,
811+
private static void createInstanceOrStaticMethodBody(String internalClassName, String javaMethodName,
812+
Type[] javaParameterTypes,
779813
String methodDescriptorString,
780814
PythonCompiledFunction function, String interfaceInternalName, String interfaceDescriptor,
781815
MethodVisitor methodVisitor) {
782816
for (int i = 0; i < javaParameterTypes.length; i++) {
783-
methodVisitor.visitParameter("parameter" + i, 0);
817+
methodVisitor.visitParameter(function.co_varnames.get(i), 0);
784818
}
819+
addAnnotationsToMethod(function, methodVisitor);
785820
methodVisitor.visitCode();
786821

787822
methodVisitor.visitFieldInsn(Opcodes.GETSTATIC, internalClassName, javaMethodName, interfaceDescriptor);

jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonCompiledClass.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,15 @@ public class PythonCompiledClass {
2121

2222
public String className;
2323

24+
/**
25+
* The annotations on the type
26+
*/
27+
public List<AnnotationMetadata> annotations;
28+
2429
/**
2530
* Type annotations for fields
2631
*/
27-
public Map<String, PythonLikeType> typeAnnotations;
32+
public Map<String, TypeHint> typeAnnotations;
2833

2934
/**
3035
* The binary type of this PythonCompiledClass;

jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonCompiledFunction.java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public class PythonCompiledFunction {
4646
* Type annotations for the parameters and return.
4747
* (return is stored under the "return" key).
4848
*/
49-
public Map<String, PythonLikeType> typeAnnotations;
49+
public Map<String, TypeHint> typeAnnotations;
5050

5151
/**
5252
* Default positional arguments
@@ -155,17 +155,22 @@ public List<PythonLikeType> getParameterTypes() {
155155

156156
for (int i = 0; i < totalArgCount(); i++) {
157157
String parameterName = co_varnames.get(i);
158-
PythonLikeType parameterType = typeAnnotations.get(parameterName);
159-
if (parameterType == null) { // map may have nulls
160-
parameterType = defaultType;
158+
var parameterTypeHint = typeAnnotations.get(parameterName);
159+
PythonLikeType parameterType = defaultType;
160+
if (parameterTypeHint != null) {
161+
parameterType = parameterTypeHint.type();
161162
}
162163
out.add(parameterType);
163164
}
164165
return out;
165166
}
166167

167168
public Optional<PythonLikeType> getReturnType() {
168-
return Optional.ofNullable(typeAnnotations.get("return"));
169+
var returnTypeHint = typeAnnotations.get("return");
170+
if (returnTypeHint == null) {
171+
return Optional.empty();
172+
}
173+
return Optional.of(returnTypeHint.type());
169174
}
170175

171176
public String getAsmMethodDescriptorString() {
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package ai.timefold.jpyinterpreter;
2+
3+
import java.util.Collections;
4+
import java.util.List;
5+
6+
import ai.timefold.jpyinterpreter.types.PythonLikeType;
7+
8+
public record TypeHint(PythonLikeType type, List<AnnotationMetadata> annotationList) {
9+
public static TypeHint withoutAnnotations(PythonLikeType type) {
10+
return new TypeHint(type, Collections.emptyList());
11+
}
12+
13+
}

0 commit comments

Comments
 (0)