diff --git a/src/runtime/AttributeErrorHint.cs b/src/runtime/AttributeErrorHint.cs
deleted file mode 100644
index f7feec985..000000000
--- a/src/runtime/AttributeErrorHint.cs
+++ /dev/null
@@ -1,109 +0,0 @@
-using System;
-
-using Python.Runtime.Native;
-
-namespace Python.Runtime
-{
- ///
- /// Installs a miss-only __getattr__ hook on reflected .NET types so that an
- /// AttributeError raised for a missing attribute is enriched with suggestions
- /// of similarly-named members — without adding any cost to the (common) successful
- /// attribute-access path.
- ///
- ///
- /// CPython only invokes __getattr__ after the normal attribute lookup fails,
- /// via the native slot_tp_getattr_hook: on a hit it calls the generic getattr
- /// directly (no managed transition); only on a miss does it call our __getattr__.
- /// pythonnet's metatype does not run CPython's slot-fixup machinery when an attribute
- /// is set on a type, so simply adding __getattr__ to the type dict would not
- /// rewire the slot — we therefore wire tp_getattro to the hook manually.
- ///
- internal static class AttributeErrorHint
- {
- // The shared __getattr__ function object installed on every eligible type.
- private static PyObject? _getAttr;
- // The managed message builder exposed to Python, kept alive for _getAttr's globals.
- private static PyObject? _messageBuilder;
- // Address of CPython's slot_tp_getattr_hook (extracted from a probe type).
- private static IntPtr _hookSlot;
- // Address of PyObject_GenericGetAttr, used to detect types we may safely redirect.
- private static IntPtr _genericGetAttr;
-
- private static bool IsReady => _getAttr is not null && _hookSlot != IntPtr.Zero;
-
- internal static void Initialize()
- {
- try
- {
- _genericGetAttr = Util.ReadIntPtr(Runtime.PyBaseObjectType, TypeOffset.tp_getattro);
-
- Func builder = ClassBase.BuildMissingAttributeMessage;
- _messageBuilder = builder.ToPython();
-
- using var globals = new PyDict();
- Runtime.PyDict_SetItemString(globals.Reference, "__builtins__", Runtime.PyEval_GetBuiltins());
- globals["__clr_attr_msg__"] = _messageBuilder;
-
- // Define the shared hook, plus a probe class whose tp_getattro is
- // slot_tp_getattr_hook so we can read that function pointer.
- PythonEngine.Exec(
- "def __clr_getattr__(self, name):\n" +
- " raise AttributeError(__clr_attr_msg__(self, name))\n" +
- "class __clr_getattr_probe__:\n" +
- " def __getattr__(self, name):\n" +
- " raise AttributeError(name)\n",
- globals);
-
- _getAttr = globals["__clr_getattr__"];
- using var probe = globals["__clr_getattr_probe__"];
- _hookSlot = Util.ReadIntPtr(probe.Reference, TypeOffset.tp_getattro);
- }
- catch (Exception e)
- {
- // Degrade gracefully: without the hook, AttributeError messages are simply
- // not enriched. Never let this break interpreter initialization.
- DebugUtil.Print($"AttributeErrorHint.Initialize failed: {e}");
- Shutdown();
- }
- }
-
- ///
- /// Wires the miss-only hook onto if it still uses the
- /// native generic getattr. Types with a custom tp_getattro (dynamic
- /// objects, modules, interfaces, ...) handle misses themselves and are left
- /// untouched; derived types that inherit an already-hooked base are likewise
- /// skipped, since they inherit the behavior through the MRO.
- ///
- internal static void Install(BorrowedReference type)
- {
- if (!IsReady)
- {
- return;
- }
-
- if (Util.ReadIntPtr(type, TypeOffset.tp_getattro) != _genericGetAttr)
- {
- return;
- }
-
- if (Runtime.PyObject_SetAttrString(type, "__getattr__", _getAttr!.Reference) != 0)
- {
- Exceptions.Clear();
- return;
- }
-
- Util.WriteIntPtr(type, TypeOffset.tp_getattro, _hookSlot);
- Runtime.PyType_Modified(type);
- }
-
- internal static void Shutdown()
- {
- _getAttr?.Dispose();
- _getAttr = null;
- _messageBuilder?.Dispose();
- _messageBuilder = null;
- _hookSlot = IntPtr.Zero;
- _genericGetAttr = IntPtr.Zero;
- }
- }
-}
diff --git a/src/runtime/Python.Runtime.csproj b/src/runtime/Python.Runtime.csproj
index c9dbda45a..f06f19706 100644
--- a/src/runtime/Python.Runtime.csproj
+++ b/src/runtime/Python.Runtime.csproj
@@ -5,7 +5,7 @@
Python.Runtime
Python.Runtime
QuantConnect.pythonnet
- 2.0.55
+ 2.0.56
false
LICENSE
https://github.com/pythonnet/pythonnet
diff --git a/src/runtime/PythonEngine.cs b/src/runtime/PythonEngine.cs
index 677a44978..eb0c98ce9 100644
--- a/src/runtime/PythonEngine.cs
+++ b/src/runtime/PythonEngine.cs
@@ -263,10 +263,6 @@ public static void Initialize(IEnumerable args, bool setSysArgv = true,
}
ImportHook.UpdateCLRModuleDict();
-
- // Set up the miss-only __getattr__ hook used to enrich AttributeError
- // messages on reflected .NET types with member-name suggestions.
- AttributeErrorHint.Initialize();
}
static BorrowedReference DefineModule(string name)
@@ -373,9 +369,6 @@ public static void Shutdown()
AppDomain.CurrentDomain.ProcessExit -= OnProcessExit;
ExecuteShutdownHandlers();
-
- AttributeErrorHint.Shutdown();
-
// Remember to shut down the runtime.
Runtime.Shutdown();
diff --git a/src/runtime/TypeManager.cs b/src/runtime/TypeManager.cs
index cbaa730ca..3b75738b2 100644
--- a/src/runtime/TypeManager.cs
+++ b/src/runtime/TypeManager.cs
@@ -303,10 +303,6 @@ internal static void InitializeClass(PyType type, ClassBase impl, Type clrType)
Runtime.PyType_Modified(type.Reference);
- // Enrich AttributeError messages for missing attributes with member-name
- // suggestions, via a miss-only __getattr__ hook (no hot-path cost).
- AttributeErrorHint.Install(type.Reference);
-
//DebugUtil.DumpType(type);
}
diff --git a/src/runtime/Types/ClassBase.cs b/src/runtime/Types/ClassBase.cs
index 7e831d17f..617baae49 100644
--- a/src/runtime/Types/ClassBase.cs
+++ b/src/runtime/Types/ClassBase.cs
@@ -628,13 +628,20 @@ internal static void AppendAttributeErrorSuggestions(BorrowedReference ob, Borro
}
var name = Runtime.GetManagedString(key);
- if (string.IsNullOrEmpty(name))
+ // Skip empty and dunder names: the latter are probed internally by CPython
+ // (e.g. __iter__, __len__) and are never user-facing typos worth helping with.
+ if (string.IsNullOrEmpty(name) || name.StartsWith("__", StringComparison.Ordinal))
{
return;
}
- var hint = GetSuggestionHint(ob, name);
- if (hint.Length == 0)
+ if (GetManagedObject(ob) is not CLRObject clrObj || clrObj.inst is null)
+ {
+ return;
+ }
+
+ var suggestions = GetSimilarMemberNames(clrObj.inst.GetType(), name);
+ if (suggestions.Count == 0)
{
return;
}
@@ -644,6 +651,7 @@ internal static void AppendAttributeErrorSuggestions(BorrowedReference ob, Borro
try
{
var baseMessage = GetErrorMessage(errValue.BorrowNullable(), name);
+ var hint = " Did you mean: " + string.Join(", ", suggestions.Select(s => $"'{s}'")) + "?";
Exceptions.SetError(Exceptions.AttributeError, baseMessage + hint);
}
finally
@@ -654,65 +662,6 @@ internal static void AppendAttributeErrorSuggestions(BorrowedReference ob, Borro
}
}
- ///
- /// Builds the full message for an AttributeError raised for a missing
- /// attribute on a .NET object, including any "Did you mean ...?" hint. Used by
- /// the miss-only __getattr__ hook installed on reflected types (see
- /// ), where the original error has already been
- /// cleared, so the base message is reconstructed here.
- ///
- internal static string BuildMissingAttributeMessage(PyObject self, string name)
- {
- var typeName = "object";
- try
- {
- using var pyType = self.GetPythonType();
- typeName = pyType.Name;
- }
- catch
- {
- // fall back to the generic type name
- }
-
- var message = $"'{typeName}' object has no attribute '{name}'";
- try
- {
- return message + GetSuggestionHint(self.Reference, name);
- }
- catch
- {
- // never let suggestion building turn into a different exception
- return message;
- }
- }
-
- ///
- /// Returns " Did you mean: 'x', 'y'?" listing similarly-named members of the
- /// managed object, or an empty string when there is nothing to suggest. Dunder
- /// names are skipped: they are probed internally by CPython (e.g. __iter__,
- /// __len__) and are never user-facing typos worth helping with.
- ///
- private static string GetSuggestionHint(BorrowedReference ob, string name)
- {
- if (string.IsNullOrEmpty(name) || name.StartsWith("__", StringComparison.Ordinal))
- {
- return string.Empty;
- }
-
- if (GetManagedObject(ob) is not CLRObject clrObj || clrObj.inst is null)
- {
- return string.Empty;
- }
-
- var suggestions = GetSimilarMemberNames(clrObj.inst.GetType(), name);
- if (suggestions.Count == 0)
- {
- return string.Empty;
- }
-
- return " Did you mean: " + string.Join(", ", suggestions.Select(s => $"'{s}'")) + "?";
- }
-
private static string GetErrorMessage(BorrowedReference value, string fallbackName)
{
if (value != null)
diff --git a/src/runtime/Types/ClassObject.cs b/src/runtime/Types/ClassObject.cs
index b57378a32..48a975898 100644
--- a/src/runtime/Types/ClassObject.cs
+++ b/src/runtime/Types/ClassObject.cs
@@ -167,6 +167,21 @@ public override void InitializeSlots(BorrowedReference pyType, SlotsHolder slots
protected virtual NewReference NewObjectToPython(object obj, BorrowedReference tp)
=> CLRObject.GetReference(obj, tp);
+ ///
+ /// Type __getattro__ implementation. Delegates to the generic CLR attribute
+ /// lookup, but enriches the AttributeError raised for a missing attribute with
+ /// suggestions of similarly-named members of the managed type.
+ ///
+ public static NewReference tp_getattro(BorrowedReference ob, BorrowedReference key)
+ {
+ var result = Runtime.PyObject_GenericGetAttr(ob, key);
+ if (result.IsNull())
+ {
+ AppendAttributeErrorSuggestions(ob, key);
+ }
+ return result;
+ }
+
private static NewReference NewEnum(Type type, BorrowedReference args, BorrowedReference tp)
{
nint argCount = Runtime.PyTuple_Size(args);