Skip to content

Commit 5fa2e7d

Browse files
committed
Extend test to more complex inheritance
A second layer reveals a bug, and that we aren't using the generic handle enough (given we do not propagate change to subclasses).
1 parent 9b26bf9 commit 5fa2e7d

File tree

2 files changed

+124
-41
lines changed

2 files changed

+124
-41
lines changed

rt4core/src/main/java/uk/co/farowl/vsj4/kernel/ReplaceableType.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
// Copyright (c)2025 Jython Developers.
1+
// Copyright (c)2026 Jython Developers.
22
// Licensed to PSF under a contributor agreement.
33
package uk.co.farowl.vsj4.kernel;
44

55
import java.util.List;
66

7+
import uk.co.farowl.vsj4.types.TypeFlag;
8+
79
/**
810
* A Python type object used where multiple Python types share a single
911
* representation in Java, making them all acceptable for assignment to
@@ -53,7 +55,9 @@ public BaseType pythonType(Object x) {
5355
}
5456

5557
@Override
56-
public boolean isMutable() { return false; }
58+
public boolean isMutable() {
59+
return !features.contains(TypeFlag.IMMUTABLE);
60+
}
5761

5862
@Override
5963
public boolean isIntExact() { return false; }

rt4core/src/test/java/uk/co/farowl/vsj4/core/UnaryCallSiteTest.java

Lines changed: 118 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,6 @@
3434
import uk.co.farowl.vsj4.kernel.SpecialMethod;
3535
import uk.co.farowl.vsj4.kernel.SpecialMethod.Signature;
3636
import uk.co.farowl.vsj4.support.InterpreterError;
37-
import uk.co.farowl.vsj4.types.Exposed.PythonMethod;
38-
import uk.co.farowl.vsj4.types.TypeSpec;
3937

4038
/**
4139
* Test of the mechanism for invoking and updating unary call sites on a
@@ -67,20 +65,20 @@ static interface ThrowingUnaryFunction {
6765
abstract static class AbstractNumericTest {
6866

6967
/**
70-
* A Python subclass of {@code int} defined as if in
71-
* Python.<pre>
72-
* MyInt = type("MyInt", (int,), {})
68+
* A Python subclass defined as if in Python.<pre>
69+
* MyInt = type(name, bases, {})
7370
* </pre>The type object we get from this should be a shared
7471
* one.
7572
*/
76-
static PyType createMyInt() {
77-
logger.atTrace().setMessage("Make fresh MyInt type").log();
73+
static PyType createType(String name, PyType... bases) {
74+
logger.atTrace().setMessage("Make fresh '{}' type")
75+
.addArgument(name).log();
7876
try {
79-
return (PyType)PyType.TYPE().call("MyInt",
80-
Py.tuple(PyLong.TYPE), Py.dict());
77+
return (PyType)PyType.TYPE().call(name,
78+
PyTuple.from(bases), Py.dict());
8179
} catch (Throwable e) {
82-
throw new InterpreterError(e,
83-
"Failed to make MyInt type");
80+
throw new InterpreterError(e, "Failed to make %s type",
81+
name);
8482
}
8583
}
8684

@@ -136,13 +134,15 @@ static Stream<Arguments> numberExamples() {
136134
*/
137135
static Stream<Arguments> numberExamplesCustom() {
138136

139-
PyType MyInt = createMyInt();
140-
Object objA = newInstance(MyInt, 7);
141-
Object objB = newInstance(MyInt, -8);
142-
143137
logger.atTrace().setMessage(
144138
"Make stream of numberExample() with custom type")
145139
.log();
140+
141+
// Create a sub-class of int and two instances
142+
PyType MyInt = createType("MyInt", PyLong.TYPE);
143+
Object objA = newInstance(MyInt, 7);
144+
Object objB = newInstance(MyInt, -8);
145+
146146
List<Arguments> examples = new LinkedList<>();
147147

148148
examples.addAll(//
@@ -160,6 +160,58 @@ static Stream<Arguments> numberExamplesCustom() {
160160
return examples.stream();
161161
}
162162

163+
/**
164+
* Build a stream of examples to exercise the parameterised
165+
* numerical tests including instances of a custom type.
166+
*
167+
* @return stream of
168+
* {@link #numberExample(String, String, ThrowingUnaryFunction, List)
169+
* numberExample} returns
170+
*/
171+
static Stream<Arguments> numberExamplesCustom2() {
172+
173+
logger.atTrace().setMessage(
174+
"Make stream of numberExample() with two custom types")
175+
.log();
176+
177+
// Create sub-classes of int and an instances of each
178+
PyType MyInt = createType("MyInt", PyLong.TYPE);
179+
Object objA = newInstance(MyInt, 7);
180+
181+
PyType MyInt2 = createType("MyInt2", MyInt);
182+
Object objB = newInstance(MyInt2, -8);
183+
184+
/*
185+
* Override a method MyInt. MyInt2 should see it by
186+
* inheritance. The simplest thing for us is to steal a
187+
* different int method: MyInt.__neg__ = int.__float__ . The
188+
* exact response to this (but probably not the test) will
189+
* change if we implement lookup caching in type objects.
190+
*/
191+
try {
192+
Object neg = Abstract.getAttr(PyLong.TYPE, "__float__");
193+
Abstract.setAttr(MyInt, "__neg__", neg);
194+
} catch (Throwable e) {
195+
throw new InterpreterError(e,
196+
"Failed to update custom type");
197+
}
198+
199+
List<Arguments> examples = new LinkedList<>();
200+
201+
examples.addAll(//
202+
numberExamples("negative", PyNumber::negative, 42,
203+
objA, objB));
204+
examples.addAll(//
205+
numberExamples("absolute", PyNumber::absolute, 42,
206+
-42, 0, true, objA, objB));
207+
examples.addAll(// Not cached
208+
numberExamples("positive", PyNumber::positive, 42,
209+
-42, 0, false, -1e42, Integer.MIN_VALUE,
210+
objA, objB));
211+
212+
return examples.stream();
213+
}
214+
163215
private static List<Arguments> numberExamples(String name,
164216
ThrowingUnaryFunction ref, Object... values) {
165217
// Inflate values to a list of multiple representations
@@ -265,7 +317,7 @@ private static void inflate(List<Object> reps, int v) {
265317

266318
/** Test of numerical operations on float and int types. */
267319
@Nested
268-
@DisplayName("numerical operations")
320+
@DisplayName("encountering built-in types")
269321
class NumericTest extends AbstractNumericTest {
270322
/**
271323
* Invoke a special method call site and compare it to the
@@ -274,7 +326,7 @@ class NumericTest extends AbstractNumericTest {
274326
*
275327
* @throws Throwable unexpectedly
276328
*/
277-
@DisplayName("match abstract API")
329+
@DisplayName("matches abstract API")
278330
@ParameterizedTest(name = "\"{0}\" {1}")
279331
@MethodSource("numberExamples")
280332
void testMatchSpecial(String name, String mix,
@@ -301,7 +353,7 @@ void testMatchSpecial(String name, String mix,
301353
*
302354
* @throws Throwable unexpectedly
303355
*/
304-
@DisplayName("fallback as expected")
356+
@DisplayName("falls back as expected")
305357
@ParameterizedTest(name = "\"{0}\" {1}")
306358
@MethodSource("numberExamples")
307359
void testFallbackCounts(String name, String mix,
@@ -344,9 +396,12 @@ void testFallbackCounts(String name, String mix,
344396
}
345397
}
346398

347-
/** Test of numerical operations on float, int and custom types. */
399+
/**
400+
* Test of numerical operations on float, int and two custom types
401+
* related by inheritance.
402+
*/
348403
@Nested
349-
@DisplayName("numerical operations (custom)")
404+
@DisplayName("encountering built-in and derived types")
350405
class NumericTestCustom extends NumericTest {
351406
/**
352407
* Invoke a special method call site and compare it to the
@@ -356,7 +411,7 @@ class NumericTestCustom extends NumericTest {
356411
* @throws Throwable unexpectedly
357412
*/
358413
@Override
359-
@DisplayName("match abstract API")
414+
@DisplayName("matches abstract API")
360415
@ParameterizedTest(name = "\"{0}\" {1}")
361416
@MethodSource("numberExamplesCustom")
362417
void testMatchSpecial(String name, String mix,
@@ -375,7 +430,7 @@ void testMatchSpecial(String name, String mix,
375430
* @throws Throwable unexpectedly
376431
*/
377432
@Override
378-
@DisplayName("fallback as expected")
433+
@DisplayName("falls back as expected")
379434
@ParameterizedTest(name = "\"{0}\" {1}")
380435
@MethodSource("numberExamplesCustom")
381436
void testFallbackCounts(String name, String mix,
@@ -385,6 +440,47 @@ void testFallbackCounts(String name, String mix,
385440
}
386441
}
387442

443+
/** Test of numerical operations on float, int and custom types. */
444+
@Nested
445+
@DisplayName("encountering built-in and two derived types")
446+
class NumericTestCustom2 extends NumericTest {
447+
/**
448+
* Invoke a special method call site and compare it to the
449+
* result from the abstract API for the presented values in
450+
* order.
451+
*
452+
* @throws Throwable unexpectedly
453+
*/
454+
@Override
455+
@DisplayName("matches abstract API")
456+
@ParameterizedTest(name = "\"{0}\" {1}")
457+
@MethodSource("numberExamplesCustom2")
458+
void testMatchSpecial(String name, String mix,
459+
ThrowingUnaryFunction ref, UnaryOpCallSite cs,
460+
List<Object> values) throws Throwable {
461+
super.testMatchSpecial(name, mix, ref, cs, values);
462+
}
463+
464+
/**
465+
* Invoke a special method call site for the presented values in
466+
* order, examining fall-back and new specialisations added as
467+
* we go along. This is sensitive to the strategy used by the
468+
* call site, so as that changes, change the test to match the
469+
* intent.
470+
*
471+
* @throws Throwable unexpectedly
472+
*/
473+
@Override
474+
@DisplayName("falls back as expected")
475+
@ParameterizedTest(name = "\"{0}\" {1}")
476+
@MethodSource("numberExamplesCustom2")
477+
void testFallbackCounts(String name, String mix,
478+
ThrowingUnaryFunction ref, UnaryOpCallSite cs,
479+
List<Object> values) throws Throwable {
480+
super.testFallbackCounts(name, mix, ref, cs, values);
481+
}
482+
}
483+
388484
/**
389485
* Test invocation of {@code __repr__} call site on accepted
390486
* {@code float} classes.
@@ -516,21 +612,4 @@ void invert_float_error() {
516612
assertEquals(floats.size(), cs.fallbackCount, "fallback calls");
517613
assertEquals(0, cs.chainLength, "chain length");
518614
}
519-
520-
/**
521-
* A Python type defined in Java with some exposed special and other
522-
* methods.
523-
*/
524-
static class MyIntOperations {
525-
static PyType TYPE = PyType.fromSpec(
526-
new TypeSpec("MyIntOps", MethodHandles.lookup()));
527-
528-
static Object __neg__(Object self) { return 42; }
529-
530-
@PythonMethod
531-
static Object _abs(Object self) {
532-
return PyLong.asInt(self) * 2;
533-
}
534-
}
535-
536615
}

0 commit comments

Comments
 (0)