Skip to content

Commit c5607ec

Browse files
committed
enabled decoding instanceless exceptions
Added new class clr.interop.PyErr with optional type, value, and traceback attributes. User can register decoders for it, that would let them decode instanceless (and even typeless) Python exceptions. These decoders will be invoked before the regular exception instance decoders.
1 parent ee0ab7f commit c5607ec

File tree

7 files changed

+147
-2
lines changed

7 files changed

+147
-2
lines changed

src/embed_tests/Codecs.cs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,17 @@ from datetime import datetime
371371
scope.Exec("Codecs.AcceptsDateTime(datetime(2021, 1, 22))");
372372
}
373373

374+
[Test]
375+
public void ExceptionDecodedNoInstance()
376+
{
377+
PyObjectConversions.RegisterDecoder(new InstancelessExceptionDecoder());
378+
using var _ = Py.GIL();
379+
using var scope = Py.CreateScope();
380+
var error = Assert.Throws<ValueErrorWrapper>(() => PythonEngine.Exec(
381+
$"[].__iter__().__next__()"));
382+
Assert.AreEqual(TestExceptionMessage, error.Message);
383+
}
384+
374385
public static void AcceptsDateTime(DateTime v) {}
375386

376387
class ValueErrorWrapper : Exception
@@ -381,7 +392,8 @@ public ValueErrorWrapper(string message) : base(message) { }
381392
class ValueErrorCodec : IPyObjectEncoder, IPyObjectDecoder
382393
{
383394
public bool CanDecode(PyObject objectType, Type targetType)
384-
=> this.CanEncode(targetType) && objectType.Equals(PythonEngine.Eval("ValueError"));
395+
=> this.CanEncode(targetType)
396+
&& PythonReferenceComparer.Instance.Equals(objectType, PythonEngine.Eval("ValueError"));
385397

386398
public bool CanEncode(Type type) => type == typeof(ValueErrorWrapper)
387399
|| typeof(ValueErrorWrapper).IsSubclassOf(type);
@@ -399,6 +411,26 @@ public PyObject TryEncode(object value)
399411
return PythonEngine.Eval("ValueError").Invoke(error.Message.ToPython());
400412
}
401413
}
414+
415+
class InstancelessExceptionDecoder : IPyObjectDecoder
416+
{
417+
readonly PyObject PyErr = Py.Import("clr.interop").GetAttr("PyErr");
418+
419+
public bool CanDecode(PyObject objectType, Type targetType)
420+
=> PythonReferenceComparer.Instance.Equals(PyErr, objectType);
421+
422+
public bool TryDecode<T>(PyObject pyObj, out T value)
423+
{
424+
if (pyObj.HasAttr("value"))
425+
{
426+
value = default;
427+
return false;
428+
}
429+
430+
value = (T)(object)new ValueErrorWrapper(TestExceptionMessage);
431+
return true;
432+
}
433+
}
402434
}
403435

404436
/// <summary>

src/runtime/Python.Runtime.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@
4343
<EmbeddedResource Include="resources\clr.py">
4444
<LogicalName>clr.py</LogicalName>
4545
</EmbeddedResource>
46+
<EmbeddedResource Include="resources\interop.py">
47+
<LogicalName>interop.py</LogicalName>
48+
</EmbeddedResource>
4649
</ItemGroup>
4750

4851
<ItemGroup>

src/runtime/Util.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,17 @@ internal static void WriteCLong(IntPtr type, int offset, Int64 flags)
4141
/// </summary>
4242
internal static IntPtr Coalesce(this IntPtr primary, IntPtr fallback)
4343
=> primary == IntPtr.Zero ? fallback : primary;
44+
45+
/// <summary>
46+
/// Gets substring after last occurrence of <paramref name="symbol"/>
47+
/// </summary>
48+
internal static string AfterLast(this string str, char symbol)
49+
{
50+
if (str is null)
51+
throw new ArgumentNullException(nameof(str));
52+
53+
int last = str.LastIndexOf(symbol);
54+
return last >= 0 ? str.Substring(last + 1) : null;
55+
}
4456
}
4557
}

src/runtime/pythonengine.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Diagnostics;
34
using System.IO;
45
using System.Linq;
56
using System.Reflection;
@@ -237,6 +238,8 @@ public static void Initialize(IEnumerable<string> args, bool setSysArgv = true,
237238
Exec(clr_py, module_globals, locals.Reference);
238239
}
239240

241+
LoadSubmodule(module_globals, "clr.interop", "interop.py");
242+
240243
// add the imported module to the clr module, and copy the API functions
241244
// and decorators into the main clr module.
242245
Runtime.PyDict_SetItemString(clr_dict, "_extras", module);
@@ -258,6 +261,32 @@ public static void Initialize(IEnumerable<string> args, bool setSysArgv = true,
258261
}
259262
}
260263

264+
static BorrowedReference DefineModule(string name)
265+
{
266+
var module = Runtime.PyImport_AddModule(name);
267+
var module_globals = Runtime.PyModule_GetDict(module);
268+
var builtins = Runtime.PyEval_GetBuiltins();
269+
Runtime.PyDict_SetItemString(module_globals, "__builtins__", builtins);
270+
return module;
271+
}
272+
273+
static void LoadSubmodule(BorrowedReference targetModuleDict, string fullName, string resourceName)
274+
{
275+
string memberName = fullName.AfterLast('.');
276+
Debug.Assert(memberName != null);
277+
Assembly assembly = Assembly.GetExecutingAssembly();
278+
var module = DefineModule(fullName);
279+
var module_globals = Runtime.PyModule_GetDict(module);
280+
using (var stream = assembly.GetManifestResourceStream(resourceName))
281+
using (var reader = new StreamReader(stream))
282+
{
283+
string pyCode = reader.ReadToEnd();
284+
Exec(pyCode, module_globals.DangerousGetAddress(), module_globals.DangerousGetAddress());
285+
}
286+
287+
Runtime.PyDict_SetItemString(targetModuleDict, memberName, module);
288+
}
289+
261290
static void OnDomainUnload(object _, EventArgs __)
262291
{
263292
Shutdown();

src/runtime/pythonexception.cs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ public PythonException(PyType type, PyObject? value, PyObject? traceback)
3737
/// </summary>
3838
internal static Exception ThrowLastAsClrException()
3939
{
40+
// prevent potential interop errors in this method
41+
// from crashing process with undebuggable StackOverflowException
42+
RuntimeHelpers.EnsureSufficientExecutionStack();
43+
4044
var exception = FetchCurrentOrNull(out ExceptionDispatchInfo? dispatchInfo)
4145
?? throw new InvalidOperationException("No exception is set");
4246
dispatchInfo?.Throw();
@@ -83,6 +87,24 @@ internal static PythonException FetchCurrentRaw()
8387
return null;
8488
}
8589

90+
try
91+
{
92+
if (TryDecodePyErr(type, value, traceback) is { } pyErr)
93+
{
94+
type.Dispose();
95+
value.Dispose();
96+
traceback.Dispose();
97+
return pyErr;
98+
}
99+
}
100+
catch
101+
{
102+
type.Dispose();
103+
value.Dispose();
104+
traceback.Dispose();
105+
throw;
106+
}
107+
86108
Runtime.PyErr_NormalizeException(type: ref type, val: ref value, tb: ref traceback);
87109

88110
try
@@ -153,6 +175,11 @@ private static Exception FromPyErr(BorrowedReference typeRef, BorrowedReference
153175
return e;
154176
}
155177

178+
if (TryDecodePyErr(typeRef, valRef, tbRef) is { } pyErr)
179+
{
180+
return pyErr;
181+
}
182+
156183
if (PyObjectConversions.TryDecode(valRef, typeRef, typeof(Exception), out object decoded)
157184
&& decoded is Exception decodedException)
158185
{
@@ -164,6 +191,28 @@ private static Exception FromPyErr(BorrowedReference typeRef, BorrowedReference
164191
return new PythonException(type, value, traceback, inner);
165192
}
166193

194+
private static Exception? TryDecodePyErr(BorrowedReference typeRef, BorrowedReference valRef, BorrowedReference tbRef)
195+
{
196+
using var type = PyType.FromReference(typeRef);
197+
using var value = PyObject.FromNullableReference(valRef);
198+
using var traceback = PyObject.FromNullableReference(tbRef);
199+
200+
using var errorDict = new PyDict();
201+
if (typeRef != null) errorDict["type"] = type;
202+
if (valRef != null) errorDict["value"] = value;
203+
if (tbRef != null) errorDict["traceback"] = traceback;
204+
205+
using var pyErrType = Runtime.InteropModule.GetAttr("PyErr");
206+
using var pyErrInfo = pyErrType.Invoke(new PyTuple(), errorDict);
207+
if (PyObjectConversions.TryDecode(pyErrInfo.Reference, pyErrType.Reference,
208+
typeof(Exception), out object decoded) && decoded is Exception decodedPyErrInfo)
209+
{
210+
return decodedPyErrInfo;
211+
}
212+
213+
return null;
214+
}
215+
167216
private static Exception? FromCause(BorrowedReference cause)
168217
{
169218
if (cause == null || cause.IsNone()) return null;

src/runtime/resources/interop.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
_UNSET = object()
2+
3+
class PyErr:
4+
def __init__(self, type=_UNSET, value=_UNSET, traceback=_UNSET):
5+
if not(type is _UNSET):
6+
self.type = type
7+
if not(value is _UNSET):
8+
self.value = value
9+
if not(traceback is _UNSET):
10+
self.traceback = traceback

src/runtime/runtime.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,8 @@ internal static void Initialize(bool initSigs = false, ShutdownMode mode = Shutd
178178
}
179179
XDecref(item);
180180
AssemblyManager.UpdatePath();
181+
182+
clrInterop = GetModuleLazy("clr.interop");
181183
}
182184

183185
private static void InitPyMembers()
@@ -406,6 +408,11 @@ internal static void Shutdown()
406408
Shutdown(mode);
407409
}
408410

411+
private static Lazy<PyObject> GetModuleLazy(string moduleName)
412+
=> moduleName is null
413+
? throw new ArgumentNullException(nameof(moduleName))
414+
: new Lazy<PyObject>(() => PyModule.Import(moduleName), isThreadSafe: false);
415+
409416
internal static ShutdownMode GetDefaultShutdownMode()
410417
{
411418
string modeEvn = Environment.GetEnvironmentVariable("PYTHONNET_SHUTDOWN_MODE");
@@ -574,6 +581,9 @@ private static void MoveClrInstancesOnwershipToPython()
574581
internal static IntPtr PyNone;
575582
internal static IntPtr Error;
576583

584+
private static Lazy<PyObject> clrInterop;
585+
internal static PyObject InteropModule => clrInterop.Value;
586+
577587
internal static BorrowedReference CLRMetaType => new BorrowedReference(PyCLRMetaType);
578588

579589
public static PyObject None
@@ -2038,7 +2048,7 @@ internal static bool PyType_IsSameAsOrSubtype(BorrowedReference type, BorrowedRe
20382048
internal static NewReference PyType_FromSpecWithBases(in NativeTypeSpec spec, BorrowedReference bases) => Delegates.PyType_FromSpecWithBases(in spec, bases);
20392049

20402050
/// <summary>
2041-
/// Finalize a type object. This should be called on all type objects to finish their initialization. This function is responsible for adding inherited slots from a types base class. Return 0 on success, or return -1 and sets an exception on error.
2051+
/// Finalize a type object. This should be called on all type objects to finish their initialization. This function is responsible for adding inherited slots from a types base class. Return 0 on success, or return -1 and sets an exception on error.
20422052
/// </summary>
20432053

20442054
internal static int PyType_Ready(IntPtr type) => Delegates.PyType_Ready(type);

0 commit comments

Comments
 (0)