Skip to content

Commit bb54fc6

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 bb54fc6

File tree

7 files changed

+165
-31
lines changed

7 files changed

+165
-31
lines changed

src/embed_tests/Codecs.cs

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ static void TupleConversionsGeneric<T, TTuple>()
3131
TupleCodec<TTuple>.Register();
3232
var tuple = Activator.CreateInstance(typeof(T), 42, "42", new object());
3333
T restored = default;
34-
using (Py.GIL())
3534
using (var scope = Py.CreateScope())
3635
{
3736
void Accept(T value) => restored = value;
@@ -53,7 +52,6 @@ static void TupleConversionsObject<T, TTuple>()
5352
TupleCodec<TTuple>.Register();
5453
var tuple = Activator.CreateInstance(typeof(T), 42, "42", new object());
5554
T restored = default;
56-
using (Py.GIL())
5755
using (var scope = Py.CreateScope())
5856
{
5957
void Accept(object value) => restored = (T)value;
@@ -73,12 +71,9 @@ public void TupleRoundtripObject()
7371
static void TupleRoundtripObject<T, TTuple>()
7472
{
7573
var tuple = Activator.CreateInstance(typeof(T), 42, "42", new object());
76-
using (Py.GIL())
77-
{
78-
var pyTuple = TupleCodec<TTuple>.Instance.TryEncode(tuple);
79-
Assert.IsTrue(TupleCodec<TTuple>.Instance.TryDecode(pyTuple, out object restored));
80-
Assert.AreEqual(expected: tuple, actual: restored);
81-
}
74+
var pyTuple = TupleCodec<TTuple>.Instance.TryEncode(tuple);
75+
Assert.IsTrue(TupleCodec<TTuple>.Instance.TryDecode(pyTuple, out object restored));
76+
Assert.AreEqual(expected: tuple, actual: restored);
8277
}
8378

8479
[Test]
@@ -90,21 +85,12 @@ public void TupleRoundtripGeneric()
9085
static void TupleRoundtripGeneric<T, TTuple>()
9186
{
9287
var tuple = Activator.CreateInstance(typeof(T), 42, "42", new object());
93-
using (Py.GIL())
94-
{
95-
var pyTuple = TupleCodec<TTuple>.Instance.TryEncode(tuple);
96-
Assert.IsTrue(TupleCodec<TTuple>.Instance.TryDecode(pyTuple, out T restored));
97-
Assert.AreEqual(expected: tuple, actual: restored);
98-
}
88+
var pyTuple = TupleCodec<TTuple>.Instance.TryEncode(tuple);
89+
Assert.IsTrue(TupleCodec<TTuple>.Instance.TryDecode(pyTuple, out T restored));
90+
Assert.AreEqual(expected: tuple, actual: restored);
9991
}
10092

101-
static PyObject GetPythonIterable()
102-
{
103-
using (Py.GIL())
104-
{
105-
return PythonEngine.Eval("map(lambda x: x, [1,2,3])");
106-
}
107-
}
93+
static PyObject GetPythonIterable() => PythonEngine.Eval("map(lambda x: x, [1,2,3])");
10894

10995
[Test]
11096
public void ListDecoderTest()
@@ -330,7 +316,6 @@ public void ExceptionEncoded()
330316
PyObjectConversions.RegisterEncoder(new ValueErrorCodec());
331317
void CallMe() => throw new ValueErrorWrapper(TestExceptionMessage);
332318
var callMeAction = new Action(CallMe);
333-
using var _ = Py.GIL();
334319
using var scope = Py.CreateScope();
335320
scope.Exec(@"
336321
def call(func):
@@ -348,7 +333,6 @@ def call(func):
348333
public void ExceptionDecoded()
349334
{
350335
PyObjectConversions.RegisterDecoder(new ValueErrorCodec());
351-
using var _ = Py.GIL();
352336
using var scope = Py.CreateScope();
353337
var error = Assert.Throws<ValueErrorWrapper>(()
354338
=> PythonEngine.Exec($"raise ValueError('{TestExceptionMessage}')"));
@@ -371,6 +355,16 @@ from datetime import datetime
371355
scope.Exec("Codecs.AcceptsDateTime(datetime(2021, 1, 22))");
372356
}
373357

358+
[Test]
359+
public void ExceptionDecodedNoInstance()
360+
{
361+
PyObjectConversions.RegisterDecoder(new InstancelessExceptionDecoder());
362+
using var scope = Py.CreateScope();
363+
var error = Assert.Throws<ValueErrorWrapper>(() => PythonEngine.Exec(
364+
$"[].__iter__().__next__()"));
365+
Assert.AreEqual(TestExceptionMessage, error.Message);
366+
}
367+
374368
public static void AcceptsDateTime(DateTime v) {}
375369

376370
class ValueErrorWrapper : Exception
@@ -381,7 +375,8 @@ public ValueErrorWrapper(string message) : base(message) { }
381375
class ValueErrorCodec : IPyObjectEncoder, IPyObjectDecoder
382376
{
383377
public bool CanDecode(PyObject objectType, Type targetType)
384-
=> this.CanEncode(targetType) && objectType.Equals(PythonEngine.Eval("ValueError"));
378+
=> this.CanEncode(targetType)
379+
&& PythonReferenceComparer.Instance.Equals(objectType, PythonEngine.Eval("ValueError"));
385380

386381
public bool CanEncode(Type type) => type == typeof(ValueErrorWrapper)
387382
|| typeof(ValueErrorWrapper).IsSubclassOf(type);
@@ -399,6 +394,26 @@ public PyObject TryEncode(object value)
399394
return PythonEngine.Eval("ValueError").Invoke(error.Message.ToPython());
400395
}
401396
}
397+
398+
class InstancelessExceptionDecoder : IPyObjectDecoder
399+
{
400+
readonly PyObject PyErr = Py.Import("clr.interop").GetAttr("PyErr");
401+
402+
public bool CanDecode(PyObject objectType, Type targetType)
403+
=> PythonReferenceComparer.Instance.Equals(PyErr, objectType);
404+
405+
public bool TryDecode<T>(PyObject pyObj, out T value)
406+
{
407+
if (pyObj.HasAttr("value"))
408+
{
409+
value = default;
410+
return false;
411+
}
412+
413+
value = (T)(object)new ValueErrorWrapper(TestExceptionMessage);
414+
return true;
415+
}
416+
}
402417
}
403418

404419
/// <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)