Skip to content

Commit f813238

Browse files
committed
Avoid loop in initialisation of exception types
1 parent a607f0f commit f813238

File tree

12 files changed

+297
-123
lines changed

12 files changed

+297
-123
lines changed

rt4core/src/kernelTest/java/uk/co/farowl/vsj4/runtime/TypeInitTest.java

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

@@ -41,6 +41,7 @@
4141
* JVM. (See the {@code kernelTest} target in the build.)
4242
*/
4343
@DisplayName("Without any preparation")
44+
@SuppressWarnings("static-method")
4445
class TypeInitTest {
4546

4647
/**
@@ -49,7 +50,7 @@ class TypeInitTest {
4950
* subclass with {@code @BeforeAll}.
5051
*/
5152
@BeforeAll
52-
static void setUpClass() {};
53+
static void setUpClass() {}
5354

5455
/** After setUp() a type exists for {@code object}. */
5556
@Test
@@ -97,7 +98,6 @@ void lookup_type_type() {
9798
}
9899

99100
/** After setUp() all type implementations have the same type. */
100-
@SuppressWarnings("static-method")
101101
@Test
102102
@DisplayName("Subclasses of PyType share a type object")
103103
void type_subclasses_share_type() {
@@ -119,7 +119,6 @@ void type_subclasses_share_type() {
119119
* {@code Double} as a found Java type before the Python type
120120
* {@code float} can adopt it.
121121
*/
122-
@SuppressWarnings("static-method")
123122
@Test
124123
@DisplayName("we can look up a type for Double.class")
125124
void lookup_type_double() {
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright (c)2025 Jython Developers.
2+
// Licensed to PSF under a contributor agreement.
3+
package uk.co.farowl.vsj4.runtime;
4+
5+
import static org.junit.jupiter.api.Assertions.assertNotNull;
6+
7+
import org.junit.jupiter.api.BeforeAll;
8+
import org.junit.jupiter.api.DisplayName;
9+
import org.junit.jupiter.api.Test;
10+
11+
/**
12+
* Test that the Python type system comes into operation in a consistent
13+
* state after referencing the class {@link PyNameError}. Most of the
14+
* tests are in the superclass.
15+
*/
16+
@DisplayName("After getting PyNameError.TYPE ...")
17+
@SuppressWarnings("static-method")
18+
class TypeInitTestNameError extends TypeInitTest {
19+
20+
/** Initialised in {@link #setUpClass()}. */
21+
static PyType type;
22+
23+
/** Start by creating an instance of {@code PyNameError}. */
24+
@BeforeAll
25+
static void setUpClass() { type = PyNameError.TYPE; }
26+
27+
@Test
28+
@DisplayName("PyNameError.TYPE is not null")
29+
void type_not_null() { assertNotNull(type); }
30+
31+
@Test
32+
@DisplayName("PyExc.NameError is not null")
33+
void name_error_not_null() { assertNotNull(PyExc.NameError); }
34+
35+
@Test
36+
@DisplayName("PyExc.KeyError is not null")
37+
void key_error_not_null() { assertNotNull(PyExc.KeyError); }
38+
39+
@Test
40+
@DisplayName("PyExc.UnboundLocalError is not null")
41+
void unbound_local_error_not_null() {
42+
assertNotNull(PyExc.UnboundLocalError);
43+
}
44+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright (c)2025 Jython Developers.
2+
// Licensed to PSF under a contributor agreement.
3+
package uk.co.farowl.vsj4.runtime;
4+
5+
import static org.junit.jupiter.api.Assertions.assertNotNull;
6+
7+
import org.junit.jupiter.api.BeforeAll;
8+
import org.junit.jupiter.api.DisplayName;
9+
import org.junit.jupiter.api.Test;
10+
11+
/**
12+
* Test that the Python type system comes into operation in a consistent
13+
* state after referencing a type through {@link PyExc}. Most of the
14+
* tests are in the superclass.
15+
*/
16+
@DisplayName("After getting PyExc.UnboundLocalError ...")
17+
@SuppressWarnings("static-method")
18+
class TypeInitTestPyExc extends TypeInitTest {
19+
20+
/** Initialised in {@link #setUpClass()}. */
21+
static PyType type;
22+
23+
/** Start by touching the type object {@code UnboundLocalError}. */
24+
@BeforeAll
25+
static void setUpClass() { type = PyExc.UnboundLocalError; }
26+
27+
@Test
28+
@DisplayName("The result is not null")
29+
void object_not_null() { assertNotNull(type); }
30+
31+
@Test
32+
@DisplayName("PyExc.TypeError is not null")
33+
void typeerror_not_null() { assertNotNull(PyExc.TypeError); }
34+
35+
@Test
36+
@DisplayName("PyExc.StopIteration is not null")
37+
void stopiter_not_null() { assertNotNull(PyExc.StopIteration); }
38+
39+
@Test
40+
@DisplayName("PyExc.KeyError is not null")
41+
void keyerror_not_null() { assertNotNull(PyExc.KeyError); }
42+
}

rt4core/src/kernelTest/java/uk/co/farowl/vsj4/runtime/TypeInitTestReentrant.java

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

@@ -25,7 +25,7 @@ class TypeInitTestReentrant extends TypeInitTest {
2525

2626
/** Start by using a complex Python type defined in Java. */
2727
@BeforeAll
28-
static void setUpClass() { object = new MyOther(); };
28+
static void setUpClass() { object = new MyOther(); }
2929

3030
/** A simple type defined in Java. */
3131
static class MyClass {

rt4core/src/kernelTest/java/uk/co/farowl/vsj4/runtime/TypeInitTestRegistry.java

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

@@ -21,5 +21,5 @@ class TypeInitTestRegistry extends TypeInitTest {
2121

2222
/** Start by touching the type registry singleton itself. */
2323
@BeforeAll
24-
static void setUpClass() { registry = PyType.registry; };
24+
static void setUpClass() { registry = PyType.registry; }
2525
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright (c)2025 Jython Developers.
2+
// Licensed to PSF under a contributor agreement.
3+
package uk.co.farowl.vsj4.runtime;
4+
5+
import static org.junit.jupiter.api.Assertions.assertNotNull;
6+
7+
import org.junit.jupiter.api.BeforeAll;
8+
import org.junit.jupiter.api.DisplayName;
9+
import org.junit.jupiter.api.Test;
10+
11+
/**
12+
* Test that the Python type system comes into operation in a consistent
13+
* state after creating an instance of {@link PyStopIteration}, which
14+
* references a type through {@link PyExc} in its static initialisation.
15+
* Most of the tests are in the superclass.
16+
*/
17+
@DisplayName("After constructing a PyStopIteration ...")
18+
@SuppressWarnings("static-method")
19+
class TypeInitTestStopIteration extends TypeInitTest {
20+
21+
/** Initialised in {@link #setUpClass()}. */
22+
static Object object;
23+
24+
/** Start by creating an instance of {@code PyStopIteration}. */
25+
@BeforeAll
26+
static void setUpClass() { object = new PyStopIteration(); }
27+
28+
@Test
29+
@DisplayName("PyStopIteration.TYPE is not null")
30+
void type_not_null() { assertNotNull(PyStopIteration.TYPE); }
31+
32+
@Test
33+
@DisplayName("PyExc.TypeError is not null")
34+
void typeerror_not_null() { assertNotNull(PyExc.TypeError); }
35+
36+
@Test
37+
@DisplayName("PyExc.StopIteration is not null")
38+
void stopiter_not_null() { assertNotNull(PyExc.StopIteration); }
39+
}

rt4core/src/main/java/uk/co/farowl/vsj4/runtime/PyAttributeError.java

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

@@ -12,7 +12,7 @@ public class PyAttributeError extends PyBaseException {
1212
/** The type object of Python {@code AttributeError} exceptions. */
1313
public static final PyType TYPE = PyType.fromSpec(
1414
new TypeSpec("AttributeError", MethodHandles.lookup())
15-
.base(PyExc.Exception)
15+
.base(Exception)
1616
.add(Feature.REPLACEABLE, Feature.IMMUTABLE)
1717
.doc("Attribute not found."));
1818

rt4core/src/main/java/uk/co/farowl/vsj4/runtime/PyBaseException.java

Lines changed: 113 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,33 @@
2424
* {@link #setType(Object)} with types represented by the same Java
2525
* class (which obviously cannot change).
2626
* <p>
27-
* A Java {@code try-catch} construct intended to catch Python
28-
* exceptions should catch {@code PyBaseException}. If it is intended to
29-
* catch only specific kinds of Python exception it must examine the
30-
* type and re-throw the unwanted exceptions.
27+
* CPython prohibits class-assignment involving built-in types directly.
28+
* For example {@code FloatingPointError().__class__ = E} and its
29+
* converse are not allowed. There seems to be no structural reason to
30+
* prohibit it, but we do so for compatibility.
31+
* <p>
32+
* The implementation follows CPython closely, where the implementation
33+
* of many exception types is shared with multiple others. This allows
34+
* multiple inheritance and class assignment amongst user-defined
35+
* exceptions, with diverse built-in bases, in ways that may be
36+
* surprising. The following is valid in Python: <pre>
37+
* class TE(TypeError): __slots__=()
38+
* class FPE(FloatingPointError): __slots__=()
39+
* TE().__class__ = FPE
40+
* class E(ZeroDivisionError, TypeError): __slots__=()
41+
* E().__class__ = FPE
42+
* </pre>In order to meet expectations set by CPython, the Java
43+
* representation in Java is correspondingly shared. For example
44+
* {@code TypeError}, {@code FloatingPointError} and
45+
* {@code ZeroDivisionError} must share a representation. In fact they
46+
* are defined in this class, to share the representation of
47+
* {@code BaseException}.
48+
* <p>
49+
* Since different Python exceptions are represented by the same
50+
* classes, we cannot use a Java {@code catch} clause to select them. We
51+
* must catch the representation class of the intended Python exception,
52+
* and re-throw it if it does not match. Method {@link #only(PyType)} is
53+
* provided to make this simpler.
3154
*
3255
* @implNote It would have been convenient, when catching exceptions in
3356
* Java, if the different classes of Python exception could have
@@ -40,11 +63,14 @@
4063
// Compare CPython PyBaseExceptionObject in pyerrors.c
4164
public class PyBaseException extends RuntimeException
4265
implements WithClassAssignment, WithDict {
43-
private static final long serialVersionUID = 1L;
66+
67+
/** Allow the type system package access. */
68+
private static final MethodHandles.Lookup LOOKUP = MethodHandles
69+
.lookup().dropLookupMode(MethodHandles.Lookup.PRIVATE);
4470

4571
/** The type object of Python {@code BaseException} exceptions. */
46-
public static final PyType TYPE = PyType.fromSpec(
47-
new TypeSpec("BaseException", MethodHandles.lookup())
72+
public static final PyType TYPE =
73+
PyType.fromSpec(new TypeSpec("BaseException", LOOKUP)
4874
.add(Feature.REPLACEABLE, Feature.IMMUTABLE)
4975
.doc("Common base class for all exceptions"));
5076

@@ -55,8 +81,6 @@ public class PyBaseException extends RuntimeException
5581
// XXX dictionary required
5682
Map<Object, Object> dict;
5783

58-
// XXX align constructor more directly to CPython.
59-
6084
/**
6185
* The arguments given to the constructor, which is also the
6286
* arguments from {@code __new__} or {@code __init__}. Not
@@ -237,6 +261,85 @@ protected Object __repr__() throws Throwable {
237261
return sj.toString();
238262
}
239263

240-
// plumbing -------------------------------------------------------
264+
// Python exceptions sharing this representation -----------------
241265

266+
/**
267+
* Permit a sub-class to create a type object for a built-in
268+
* exception that extends a single base, with the addition of no
269+
* fields or methods, and therefore has the same Java representation
270+
* as its base.
271+
*
272+
* @param excbase the base (parent) exception
273+
* @param excname the name of the new exception
274+
* @param excdoc a documentation string for the new exception type
275+
* @return the type object for the new exception type
276+
*/
277+
// Compare CPython SimpleExtendsException in exceptions.c
278+
// ... or (same to us) MiddlingExtendsException
279+
protected static PyType extendsException(PyType excbase,
280+
String excname, String excdoc) {
281+
TypeSpec spec = new TypeSpec(excname, LOOKUP).base(excbase)
282+
// Share the same Java representation class as base
283+
.primary(excbase.javaClass())
284+
// This will be a replaceable type.
285+
.add(Feature.REPLACEABLE, Feature.IMMUTABLE)
286+
.doc(excdoc);
287+
return PyType.fromSpec(spec);
288+
}
289+
290+
/** {@code Exception} extends {@link PyBaseException}. */
291+
protected static PyType Exception =
292+
extendsException(TYPE, "Exception",
293+
"Common base class for all non-exit exceptions.");
294+
/** {@code TypeError} extends {@code Exception}. */
295+
protected static PyType TypeError = extendsException(Exception,
296+
"TypeError", "Inappropriate argument type.");
297+
/** {@code LookupError} extends {@code Exception}. */
298+
299+
protected static PyType LookupError = extendsException(Exception,
300+
"LookupError", "Base class for lookup errors.");
301+
/** {@code IndexError} extends {@code LookupError}. */
302+
protected static PyType IndexError = extendsException(LookupError,
303+
"IndexError", "Sequence index out of range.");
304+
/** {@code ValueError} extends {@link Exception}. */
305+
protected static PyType ValueError =
306+
extendsException(Exception, "ValueError",
307+
"Inappropriate argument value (of correct type).");
308+
309+
/** {@code ArithmeticError} extends {@link Exception}. */
310+
protected static PyType ArithmeticError =
311+
extendsException(Exception, "ArithmeticError",
312+
"Base class for arithmetic errors.");
313+
/** {@code FloatingPointError} extends {@link ArithmeticError}. */
314+
protected static PyType FloatingPointError =
315+
extendsException(ArithmeticError, "FloatingPointError",
316+
"Floating point operation failed.");
317+
/** {@code OverflowError} extends {@link ArithmeticError}. */
318+
protected static PyType OverflowError =
319+
extendsException(ArithmeticError, "OverflowError",
320+
"Result too large to be represented.");
321+
/** {@code ZeroDivisionError} extends {@link ArithmeticError}. */
322+
protected static PyType ZeroDivisionError = extendsException(
323+
ArithmeticError, "ZeroDivisionError",
324+
"Second argument to a division or modulo operation was zero.");
325+
326+
/*
327+
* Warnings are Exception objects, but do not get thrown (I think),
328+
* being used as "categories" in the warnings module.
329+
*/
330+
/** {@code Warning} extends {@link Exception}. */
331+
protected static PyType Warning = extendsException(Exception,
332+
"Warning", "Base class for warning categories.");
333+
/** {@code DeprecationWarning} extends {@link Warning}. */
334+
protected static PyType DeprecationWarning = extendsException(
335+
Warning, "DeprecationWarning",
336+
"Base class for warnings about deprecated features.");
337+
/** {@code RuntimeWarning} extends {@link Warning}. */
338+
protected static PyType RuntimeWarning = extendsException(Warning,
339+
"RuntimeWarning",
340+
"Base class for warnings about dubious runtime behavior.");
341+
342+
// plumbing ------------------------------------------------------
343+
344+
private static final long serialVersionUID = 1L;
242345
}

0 commit comments

Comments
 (0)