Skip to content

Commit 8a3647d

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 8a3647d

File tree

7 files changed

+159
-8
lines changed

7 files changed

+159
-8
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: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
#nullable enable
12
using System;
3+
using System.IO;
24
using System.Runtime.InteropServices;
35

46
namespace Python.Runtime
@@ -41,5 +43,27 @@ internal static void WriteCLong(IntPtr type, int offset, Int64 flags)
4143
/// </summary>
4244
internal static IntPtr Coalesce(this IntPtr primary, IntPtr fallback)
4345
=> primary == IntPtr.Zero ? fallback : primary;
46+
47+
/// <summary>
48+
/// Gets substring after last occurrence of <paramref name="symbol"/>
49+
/// </summary>
50+
internal static string? AfterLast(this string str, char symbol)
51+
{
52+
if (str is null)
53+
throw new ArgumentNullException(nameof(str));
54+
55+
int last = str.LastIndexOf(symbol);
56+
return last >= 0 ? str.Substring(last + 1) : null;
57+
}
58+
59+
internal static string ReadStringResource(this System.Reflection.Assembly assembly, string resourceName)
60+
{
61+
if (assembly is null) throw new ArgumentNullException(nameof(assembly));
62+
if (string.IsNullOrEmpty(resourceName)) throw new ArgumentNullException(nameof(resourceName));
63+
64+
using var stream = assembly.GetManifestResourceStream(resourceName);
65+
using var reader = new StreamReader(stream);
66+
return reader.ReadToEnd();
67+
}
4468
}
4569
}

src/runtime/pythonengine.cs

Lines changed: 30 additions & 7 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;
@@ -229,13 +230,11 @@ public static void Initialize(IEnumerable<string> args, bool setSysArgv = true,
229230
Runtime.PyDict_SetItemString(module_globals, "__builtins__", builtins);
230231

231232
Assembly assembly = Assembly.GetExecutingAssembly();
232-
using (Stream stream = assembly.GetManifestResourceStream("clr.py"))
233-
using (var reader = new StreamReader(stream))
234-
{
235-
// add the contents of clr.py to the module
236-
string clr_py = reader.ReadToEnd();
237-
Exec(clr_py, module_globals, locals.Reference);
238-
}
233+
// add the contents of clr.py to the module
234+
string clr_py = assembly.ReadStringResource("clr.py");
235+
Exec(clr_py, module_globals, locals.Reference);
236+
237+
LoadSubmodule(module_globals, "clr.interop", "interop.py");
239238

240239
// add the imported module to the clr module, and copy the API functions
241240
// and decorators into the main clr module.
@@ -258,6 +257,30 @@ public static void Initialize(IEnumerable<string> args, bool setSysArgv = true,
258257
}
259258
}
260259

260+
static BorrowedReference DefineModule(string name)
261+
{
262+
var module = Runtime.PyImport_AddModule(name);
263+
var module_globals = Runtime.PyModule_GetDict(module);
264+
var builtins = Runtime.PyEval_GetBuiltins();
265+
Runtime.PyDict_SetItemString(module_globals, "__builtins__", builtins);
266+
return module;
267+
}
268+
269+
static void LoadSubmodule(BorrowedReference targetModuleDict, string fullName, string resourceName)
270+
{
271+
string memberName = fullName.AfterLast('.');
272+
Debug.Assert(memberName != null);
273+
274+
var module = DefineModule(fullName);
275+
var module_globals = Runtime.PyModule_GetDict(module);
276+
277+
Assembly assembly = Assembly.GetExecutingAssembly();
278+
string pyCode = assembly.ReadStringResource(resourceName);
279+
Exec(pyCode, module_globals.DangerousGetAddress(), module_globals.DangerousGetAddress());
280+
281+
Runtime.PyDict_SetItemString(targetModuleDict, memberName, module);
282+
}
283+
261284
static void OnDomainUnload(object _, EventArgs __)
262285
{
263286
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: 10 additions & 0 deletions
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

0 commit comments

Comments
 (0)