using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;
using System.Runtime.Serialization;
using System.Security.Permissions;
using System.Text;
using Python.Runtime.Native;
namespace Python.Runtime
{
///
/// Provides a managed interface to exceptions thrown by the Python
/// runtime.
///
[Serializable]
public class PythonException : System.Exception
{
public PythonException(PyType type, PyObject? value, PyObject? traceback,
string message, Exception? innerException)
: base(message, innerException)
{
Type = type ?? throw new ArgumentNullException(nameof(type));
Value = value;
Traceback = traceback;
}
public PythonException(PyType type, PyObject? value, PyObject? traceback,
Exception? innerException)
: this(type, value, traceback, GetMessage(value, type), innerException) { }
public PythonException(PyType type, PyObject? value, PyObject? traceback)
: this(type, value, traceback, innerException: null) { }
///
/// Rethrows the last Python exception as corresponding CLR exception.
/// It is recommended to call this as throw ThrowLastAsClrException()
/// to assist control flow checks.
///
[DebuggerHidden]
internal static Exception ThrowLastAsClrException()
{
// prevent potential interop errors in this method
// from crashing process with undebuggable StackOverflowException
RuntimeHelpers.EnsureSufficientExecutionStack();
var exception = FetchCurrentOrNull(out ExceptionDispatchInfo? dispatchInfo)
?? throw new InvalidOperationException("No exception is set");
dispatchInfo?.Throw();
// when dispatchInfo is not null, this line will not be reached
throw exception;
}
internal static PythonException? FetchCurrentOrNullRaw()
{
using var _ = new Py.GILState();
Runtime.PyErr_Fetch(type: out var type, val: out var value, tb: out var traceback);
if (type.IsNull())
{
Debug.Assert(value.IsNull());
Debug.Assert(traceback.IsNull());
return null;
}
return new PythonException(
type: new PyType(type.Steal()),
value: value.MoveToPyObjectOrNull(),
traceback: traceback.MoveToPyObjectOrNull());
}
internal static PythonException FetchCurrentRaw()
=> FetchCurrentOrNullRaw()
?? throw new InvalidOperationException("No exception is set");
internal static Exception? FetchCurrentOrNull(out ExceptionDispatchInfo? dispatchInfo)
{
dispatchInfo = null;
// prevent potential interop errors in this method
// from crashing process with undebuggable StackOverflowException
RuntimeHelpers.EnsureSufficientExecutionStack();
using var _ = new Py.GILState();
Runtime.PyErr_Fetch(out var type, out var value, out var traceback);
if (type.IsNull())
{
Debug.Assert(value.IsNull());
Debug.Assert(traceback.IsNull());
return null;
}
try
{
if (TryDecodePyErr(type.Borrow(), value.BorrowNullable(), traceback.BorrowNullable()) is { } pyErr)
{
type.Dispose();
value.Dispose();
traceback.Dispose();
return pyErr;
}
}
catch
{
type.Dispose();
value.Dispose();
traceback.Dispose();
throw;
}
try
{
var normalizedValue = new NewReference(value.Borrow());
Runtime.PyErr_NormalizeException(type: ref type, val: ref normalizedValue, tb: ref traceback);
return FromPyErr(typeRef: type.Borrow(), valRef: value.Borrow(), nValRef: normalizedValue.Borrow(), tbRef: traceback.BorrowNullable(), out dispatchInfo);
}
finally
{
type.Dispose();
value.Dispose();
traceback.Dispose();
}
}
internal static Exception FetchCurrent()
=> FetchCurrentOrNull(out _)
?? throw new InvalidOperationException("No exception is set");
private static ExceptionDispatchInfo? TryGetDispatchInfo(BorrowedReference exception)
{
if (exception.IsNull) return null;
using var pyInfo = Runtime.PyObject_GetAttrString(exception, Exceptions.DispatchInfoAttribute);
if (pyInfo.IsNull())
{
if (Exceptions.ExceptionMatches(Exceptions.AttributeError))
{
Exceptions.Clear();
}
return null;
}
if (Converter.ToManagedValue(pyInfo.Borrow(), typeof(ExceptionDispatchInfo), out object? result, setError: false, out var _))
{
return (ExceptionDispatchInfo)result!;
}
return null;
}
///
/// Requires lock to be acquired elsewhere
///
private static Exception FromPyErr(BorrowedReference typeRef, BorrowedReference valRef, BorrowedReference nValRef, BorrowedReference tbRef,
out ExceptionDispatchInfo? exceptionDispatchInfo)
{
if (valRef == null) throw new ArgumentNullException(nameof(valRef));
var type = PyType.FromReference(typeRef);
var value = new PyObject(valRef);
var traceback = PyObject.FromNullableReference(tbRef);
Exception exception = null;
exceptionDispatchInfo = TryGetDispatchInfo(valRef);
if (exceptionDispatchInfo != null)
{
exception = exceptionDispatchInfo.SourceException;
exceptionDispatchInfo = null;
}
else if (ManagedType.GetManagedObject(valRef) is CLRObject { inst: Exception e })
{
exception = e;
}
else if (TryDecodePyErr(typeRef, valRef, tbRef) is { } pyErr)
{
exception = pyErr;
}
else if (PyObjectConversions.TryDecode(valRef, typeRef, typeof(Exception), out object? decoded)
&& decoded is Exception decodedException)
{
exception = decodedException;
}
if (exception is not null)
{
// Return ClrBubbledExceptions when they are bubbled from Python -> C# -> Python -> C# -> ...
// or when the traceback is not available, so we fall back to the original behavior
if (exception is ClrBubbledException || traceback is null)
{
return exception;
}
using var _ = new Py.GILState();
return new ClrBubbledException(exception, TracebackToString(traceback));
}
using var cause = Runtime.PyException_GetCause(nValRef);
Exception? inner = FromCause(cause.BorrowNullable());
return new PythonException(type, value, traceback, inner);
}
private static PyDict ToPyErrArgs(BorrowedReference typeRef, BorrowedReference valRef, BorrowedReference tbRef)
{
using var type = PyType.FromReference(typeRef);
using var value = PyObject.FromNullableReference(valRef);
using var traceback = PyObject.FromNullableReference(tbRef);
var errorDict = new PyDict();
errorDict["type"] = type;
if (value is not null) errorDict["value"] = value;
if (traceback is not null) errorDict["traceback"] = traceback;
return errorDict;
}
private static Exception? TryDecodePyErr(BorrowedReference typeRef, BorrowedReference valRef, BorrowedReference tbRef)
{
using var pyErrType = Runtime.InteropModule.GetAttr("PyErr");
using var errorDict = ToPyErrArgs(typeRef, valRef, tbRef);
using var pyErrInfo = pyErrType.Invoke(new PyTuple(), errorDict);
if (PyObjectConversions.TryDecode(pyErrInfo.Reference, pyErrType.Reference,
typeof(Exception), out object? decoded) && decoded is Exception decodedPyErrInfo)
{
return decodedPyErrInfo;
}
return null;
}
private static Exception? FromCause(BorrowedReference cause)
{
if (cause == null || cause.IsNone()) return null;
Debug.Assert(Runtime.PyObject_TypeCheck(cause, Exceptions.BaseException));
using var innerTraceback = Runtime.PyException_GetTraceback(cause);
return FromPyErr(
typeRef: Runtime.PyObject_TYPE(cause),
valRef: cause,
nValRef: cause,
tbRef: innerTraceback.BorrowNullable(),
out _);
}
private static string GetMessage(PyObject? value, PyType type)
{
if (type is null) throw new ArgumentNullException(nameof(type));
if (value != null && !value.IsNone())
{
var message = value.ToString() ?? "no message";
// Python 3.12+ eagerly normalizes the error indicator, so a SyntaxError
// reaches us as the exception instance whose str() omits the offending
// source line. Pre-3.12 we received the raw args tuple, whose str()
// included it. Re-append the source text so the message stays complete
// for callers that surface it (e.g. compile diagnostics). This is a
// no-op on new PythonException(type: Type, value: Value, traceback: Traceback,
Message, InnerException);
#region Serializable
[SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
protected PythonException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
Type = (PyType)info.GetValue(nameof(Type), typeof(PyType));
Value = (PyObject)info.GetValue(nameof(Value), typeof(PyObject));
Traceback = (PyObject)info.GetValue(nameof(Traceback), typeof(PyObject));
}
[SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
if (info == null)
{
throw new ArgumentNullException(nameof(info));
}
base.GetObjectData(info, context);
info.AddValue(nameof(Type), Type);
info.AddValue(nameof(Value), Value);
info.AddValue(nameof(Traceback), Traceback);
}
#endregion
internal bool Is(BorrowedReference type)
{
return Runtime.PyErr_GivenExceptionMatches(
given: (Value ?? Type).Reference,
typeOrTypes: type) != 0;
}
private static void CheckRuntimeIsRunning()
{
if (!PythonEngine.IsInitialized && Runtime.Py_IsInitialized() == 0)
throw new InvalidOperationException("Python runtime must be running");
}
///
/// Returns true if the current Python exception
/// matches the given exception type.
///
internal static bool CurrentMatches(BorrowedReference ob)
{
return Runtime.PyErr_ExceptionMatches(ob) != 0;
}
[DebuggerHidden]
internal static void ThrowIfIsNull(in NewReference ob)
{
if (ob.BorrowNullable() == null)
{
throw ThrowLastAsClrException();
}
}
internal static BorrowedReference ThrowIfIsNull(BorrowedReference ob)
=> Exceptions.ErrorCheck(ob);
[DebuggerHidden]
internal static void ThrowIfIsNotZero(int value)
{
if (value != 0)
{
throw ThrowLastAsClrException();
}
}
}
}