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
4063// Compare CPython PyBaseExceptionObject in pyerrors.c
4164public 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