Skip to content

Commit cccbaf7

Browse files
jhonabreulclaude
andauthored
Use tp_getattro for AttributeError suggestions instead of the __getattr__ hook (#126)
* Use tp_getattro instead of the miss-only __getattr__ hook Reimplement the AttributeError member-name suggestions on top of a ClassObject.tp_getattro override, replacing the miss-only __getattr__ hook (AttributeErrorHint) that was merged in #124. The hook installed a shared __getattr__ on every reflected type and manually rewired tp_getattro to CPython's slot_tp_getattr_hook. That surgery is significantly more invasive for no measurable real-world benefit: the per-access cost it avoids (~17 ns) is lost in the noise on realistic Lean workloads. This version keeps the enrichment entirely in a tp_getattro override that delegates to PyObject_GenericGetAttr and only does work on a miss, and drops all the slot manipulation. Behaviour and messages are unchanged (snake_case "Did you mean ...?" suggestions); the existing Python and embedding tests are untouched and still pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Bump version to 2.0.56 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent e9a2338 commit cccbaf7

6 files changed

Lines changed: 27 additions & 183 deletions

File tree

src/runtime/AttributeErrorHint.cs

Lines changed: 0 additions & 109 deletions
This file was deleted.

src/runtime/Python.Runtime.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<RootNamespace>Python.Runtime</RootNamespace>
66
<AssemblyName>Python.Runtime</AssemblyName>
77
<PackageId>QuantConnect.pythonnet</PackageId>
8-
<Version>2.0.55</Version>
8+
<Version>2.0.56</Version>
99
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
1010
<PackageLicenseFile>LICENSE</PackageLicenseFile>
1111
<RepositoryUrl>https://github.com/pythonnet/pythonnet</RepositoryUrl>

src/runtime/PythonEngine.cs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -263,10 +263,6 @@ public static void Initialize(IEnumerable<string> args, bool setSysArgv = true,
263263
}
264264

265265
ImportHook.UpdateCLRModuleDict();
266-
267-
// Set up the miss-only __getattr__ hook used to enrich AttributeError
268-
// messages on reflected .NET types with member-name suggestions.
269-
AttributeErrorHint.Initialize();
270266
}
271267

272268
static BorrowedReference DefineModule(string name)
@@ -373,9 +369,6 @@ public static void Shutdown()
373369
AppDomain.CurrentDomain.ProcessExit -= OnProcessExit;
374370

375371
ExecuteShutdownHandlers();
376-
377-
AttributeErrorHint.Shutdown();
378-
379372
// Remember to shut down the runtime.
380373
Runtime.Shutdown();
381374

src/runtime/TypeManager.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -303,10 +303,6 @@ internal static void InitializeClass(PyType type, ClassBase impl, Type clrType)
303303

304304
Runtime.PyType_Modified(type.Reference);
305305

306-
// Enrich AttributeError messages for missing attributes with member-name
307-
// suggestions, via a miss-only __getattr__ hook (no hot-path cost).
308-
AttributeErrorHint.Install(type.Reference);
309-
310306
//DebugUtil.DumpType(type);
311307
}
312308

src/runtime/Types/ClassBase.cs

Lines changed: 11 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -628,13 +628,20 @@ internal static void AppendAttributeErrorSuggestions(BorrowedReference ob, Borro
628628
}
629629

630630
var name = Runtime.GetManagedString(key);
631-
if (string.IsNullOrEmpty(name))
631+
// Skip empty and dunder names: the latter are probed internally by CPython
632+
// (e.g. __iter__, __len__) and are never user-facing typos worth helping with.
633+
if (string.IsNullOrEmpty(name) || name.StartsWith("__", StringComparison.Ordinal))
632634
{
633635
return;
634636
}
635637

636-
var hint = GetSuggestionHint(ob, name);
637-
if (hint.Length == 0)
638+
if (GetManagedObject(ob) is not CLRObject clrObj || clrObj.inst is null)
639+
{
640+
return;
641+
}
642+
643+
var suggestions = GetSimilarMemberNames(clrObj.inst.GetType(), name);
644+
if (suggestions.Count == 0)
638645
{
639646
return;
640647
}
@@ -644,6 +651,7 @@ internal static void AppendAttributeErrorSuggestions(BorrowedReference ob, Borro
644651
try
645652
{
646653
var baseMessage = GetErrorMessage(errValue.BorrowNullable(), name);
654+
var hint = " Did you mean: " + string.Join(", ", suggestions.Select(s => $"'{s}'")) + "?";
647655
Exceptions.SetError(Exceptions.AttributeError, baseMessage + hint);
648656
}
649657
finally
@@ -654,65 +662,6 @@ internal static void AppendAttributeErrorSuggestions(BorrowedReference ob, Borro
654662
}
655663
}
656664

657-
/// <summary>
658-
/// Builds the full message for an <c>AttributeError</c> raised for a missing
659-
/// attribute on a .NET object, including any "Did you mean ...?" hint. Used by
660-
/// the miss-only <c>__getattr__</c> hook installed on reflected types (see
661-
/// <see cref="AttributeErrorHint"/>), where the original error has already been
662-
/// cleared, so the base message is reconstructed here.
663-
/// </summary>
664-
internal static string BuildMissingAttributeMessage(PyObject self, string name)
665-
{
666-
var typeName = "object";
667-
try
668-
{
669-
using var pyType = self.GetPythonType();
670-
typeName = pyType.Name;
671-
}
672-
catch
673-
{
674-
// fall back to the generic type name
675-
}
676-
677-
var message = $"'{typeName}' object has no attribute '{name}'";
678-
try
679-
{
680-
return message + GetSuggestionHint(self.Reference, name);
681-
}
682-
catch
683-
{
684-
// never let suggestion building turn into a different exception
685-
return message;
686-
}
687-
}
688-
689-
/// <summary>
690-
/// Returns " Did you mean: 'x', 'y'?" listing similarly-named members of the
691-
/// managed object, or an empty string when there is nothing to suggest. Dunder
692-
/// names are skipped: they are probed internally by CPython (e.g. __iter__,
693-
/// __len__) and are never user-facing typos worth helping with.
694-
/// </summary>
695-
private static string GetSuggestionHint(BorrowedReference ob, string name)
696-
{
697-
if (string.IsNullOrEmpty(name) || name.StartsWith("__", StringComparison.Ordinal))
698-
{
699-
return string.Empty;
700-
}
701-
702-
if (GetManagedObject(ob) is not CLRObject clrObj || clrObj.inst is null)
703-
{
704-
return string.Empty;
705-
}
706-
707-
var suggestions = GetSimilarMemberNames(clrObj.inst.GetType(), name);
708-
if (suggestions.Count == 0)
709-
{
710-
return string.Empty;
711-
}
712-
713-
return " Did you mean: " + string.Join(", ", suggestions.Select(s => $"'{s}'")) + "?";
714-
}
715-
716665
private static string GetErrorMessage(BorrowedReference value, string fallbackName)
717666
{
718667
if (value != null)

src/runtime/Types/ClassObject.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,21 @@ public override void InitializeSlots(BorrowedReference pyType, SlotsHolder slots
167167
protected virtual NewReference NewObjectToPython(object obj, BorrowedReference tp)
168168
=> CLRObject.GetReference(obj, tp);
169169

170+
/// <summary>
171+
/// Type __getattro__ implementation. Delegates to the generic CLR attribute
172+
/// lookup, but enriches the AttributeError raised for a missing attribute with
173+
/// suggestions of similarly-named members of the managed type.
174+
/// </summary>
175+
public static NewReference tp_getattro(BorrowedReference ob, BorrowedReference key)
176+
{
177+
var result = Runtime.PyObject_GenericGetAttr(ob, key);
178+
if (result.IsNull())
179+
{
180+
AppendAttributeErrorSuggestions(ob, key);
181+
}
182+
return result;
183+
}
184+
170185
private static NewReference NewEnum(Type type, BorrowedReference args, BorrowedReference tp)
171186
{
172187
nint argCount = Runtime.PyTuple_Size(args);

0 commit comments

Comments
 (0)