|
| 1 | +using System; |
| 2 | + |
| 3 | +using Python.Runtime.Native; |
| 4 | + |
| 5 | +namespace Python.Runtime |
| 6 | +{ |
| 7 | + /// <summary> |
| 8 | + /// Installs a miss-only <c>__getattr__</c> hook on reflected .NET types so that an |
| 9 | + /// <c>AttributeError</c> raised for a missing attribute is enriched with suggestions |
| 10 | + /// of similarly-named members — without adding any cost to the (common) successful |
| 11 | + /// attribute-access path. |
| 12 | + /// </summary> |
| 13 | + /// <remarks> |
| 14 | + /// CPython only invokes <c>__getattr__</c> after the normal attribute lookup fails, |
| 15 | + /// via the native <c>slot_tp_getattr_hook</c>: on a hit it calls the generic getattr |
| 16 | + /// directly (no managed transition); only on a miss does it call our <c>__getattr__</c>. |
| 17 | + /// pythonnet's metatype does not run CPython's slot-fixup machinery when an attribute |
| 18 | + /// is set on a type, so simply adding <c>__getattr__</c> to the type dict would not |
| 19 | + /// rewire the slot — we therefore wire <c>tp_getattro</c> to the hook manually. |
| 20 | + /// </remarks> |
| 21 | + internal static class AttributeErrorHint |
| 22 | + { |
| 23 | + // The shared __getattr__ function object installed on every eligible type. |
| 24 | + private static PyObject? _getAttr; |
| 25 | + // The managed message builder exposed to Python, kept alive for _getAttr's globals. |
| 26 | + private static PyObject? _messageBuilder; |
| 27 | + // Address of CPython's slot_tp_getattr_hook (extracted from a probe type). |
| 28 | + private static IntPtr _hookSlot; |
| 29 | + // Address of PyObject_GenericGetAttr, used to detect types we may safely redirect. |
| 30 | + private static IntPtr _genericGetAttr; |
| 31 | + |
| 32 | + private static bool IsReady => _getAttr is not null && _hookSlot != IntPtr.Zero; |
| 33 | + |
| 34 | + internal static void Initialize() |
| 35 | + { |
| 36 | + try |
| 37 | + { |
| 38 | + _genericGetAttr = Util.ReadIntPtr(Runtime.PyBaseObjectType, TypeOffset.tp_getattro); |
| 39 | + |
| 40 | + Func<PyObject, string, string> builder = ClassBase.BuildMissingAttributeMessage; |
| 41 | + _messageBuilder = builder.ToPython(); |
| 42 | + |
| 43 | + using var globals = new PyDict(); |
| 44 | + Runtime.PyDict_SetItemString(globals.Reference, "__builtins__", Runtime.PyEval_GetBuiltins()); |
| 45 | + globals["__clr_attr_msg__"] = _messageBuilder; |
| 46 | + |
| 47 | + // Define the shared hook, plus a probe class whose tp_getattro is |
| 48 | + // slot_tp_getattr_hook so we can read that function pointer. |
| 49 | + PythonEngine.Exec( |
| 50 | + "def __clr_getattr__(self, name):\n" + |
| 51 | + " raise AttributeError(__clr_attr_msg__(self, name))\n" + |
| 52 | + "class __clr_getattr_probe__:\n" + |
| 53 | + " def __getattr__(self, name):\n" + |
| 54 | + " raise AttributeError(name)\n", |
| 55 | + globals); |
| 56 | + |
| 57 | + _getAttr = globals["__clr_getattr__"]; |
| 58 | + using var probe = globals["__clr_getattr_probe__"]; |
| 59 | + _hookSlot = Util.ReadIntPtr(probe.Reference, TypeOffset.tp_getattro); |
| 60 | + } |
| 61 | + catch (Exception e) |
| 62 | + { |
| 63 | + // Degrade gracefully: without the hook, AttributeError messages are simply |
| 64 | + // not enriched. Never let this break interpreter initialization. |
| 65 | + DebugUtil.Print($"AttributeErrorHint.Initialize failed: {e}"); |
| 66 | + Shutdown(); |
| 67 | + } |
| 68 | + } |
| 69 | + |
| 70 | + /// <summary> |
| 71 | + /// Wires the miss-only hook onto <paramref name="type"/> if it still uses the |
| 72 | + /// native generic getattr. Types with a custom <c>tp_getattro</c> (dynamic |
| 73 | + /// objects, modules, interfaces, ...) handle misses themselves and are left |
| 74 | + /// untouched; derived types that inherit an already-hooked base are likewise |
| 75 | + /// skipped, since they inherit the behavior through the MRO. |
| 76 | + /// </summary> |
| 77 | + internal static void Install(BorrowedReference type) |
| 78 | + { |
| 79 | + if (!IsReady) |
| 80 | + { |
| 81 | + return; |
| 82 | + } |
| 83 | + |
| 84 | + if (Util.ReadIntPtr(type, TypeOffset.tp_getattro) != _genericGetAttr) |
| 85 | + { |
| 86 | + return; |
| 87 | + } |
| 88 | + |
| 89 | + if (Runtime.PyObject_SetAttrString(type, "__getattr__", _getAttr!.Reference) != 0) |
| 90 | + { |
| 91 | + Exceptions.Clear(); |
| 92 | + return; |
| 93 | + } |
| 94 | + |
| 95 | + Util.WriteIntPtr(type, TypeOffset.tp_getattro, _hookSlot); |
| 96 | + Runtime.PyType_Modified(type); |
| 97 | + } |
| 98 | + |
| 99 | + internal static void Shutdown() |
| 100 | + { |
| 101 | + _getAttr?.Dispose(); |
| 102 | + _getAttr = null; |
| 103 | + _messageBuilder?.Dispose(); |
| 104 | + _messageBuilder = null; |
| 105 | + _hookSlot = IntPtr.Zero; |
| 106 | + _genericGetAttr = IntPtr.Zero; |
| 107 | + } |
| 108 | + } |
| 109 | +} |
0 commit comments