Use a native __getattr__ hook for AttributeError suggestions (GIL-safe re-land of the miss-only hook)#129
Merged
jhonabreul merged 2 commits intoJul 6, 2026
Conversation
Revert the code changes of QuantConnect#126, restoring the AttributeErrorHint miss-only __getattr__ hook approach from QuantConnect#124: successful attribute accesses go straight through CPython's native generic getattr with no managed transition; only a miss enters managed code to build the "Did you mean ...?" suggestions. The package <Version> is left at 2.0.56 (not reverted). Note: as restored here, the hook still has the off-GIL callback defect that crashed Lean's CI on 2.0.55 (the __clr_attr_msg__ delegate is invoked through MethodBinder, which releases the GIL); it is fixed in the follow-up commit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The __getattr__ hook restored from QuantConnect#124 exposed the hint builder to Python as a .NET delegate (Func<PyObject, string, string>). Python invoked it through DelegateObject.tp_call -> MethodBinder.Invoke, and MethodBinder releases the GIL around every reflected invocation (allow_threads defaults to true). The callback therefore ran CPython C-API calls (GetPythonType, GetManagedString, ...) without holding the GIL on every attribute miss (hasattr, getattr with default, typos). It usually survived by luck, but segfaulted whenever the pythonnet Finalizer fired mid-callback: Finalizer.DisposeAll() starts with PyErr_Fetch, which dereferences the current thread state - NULL when the GIL is not held. This is what crashed Lean's CI test host on 2.0.55 after all 35k tests passed. Replace the Python-function-plus-delegate pair with a native method descriptor: a PyMethodDef (METH_VARARGS) around a managed thunk, turned into __getattr__ via PyDescr_NewMethod (newly bound). CPython's slot_tp_getattr_hook now calls the managed hook directly as a native method call with the GIL held - MethodBinder is never involved. The PyMethodDef and thunk are allocated once and kept for the process lifetime, since descriptors reference them and can outlive engine shutdown bookkeeping. Verified with an instrumented build (PyGILState_Check inside BuildMissingAttributeMessage): the delegate-based hook reports "GIL held: False" on every miss; this version reports "GIL held: True". Also survives a 20s stress run of concurrent attribute misses plus finalizer churn across 6 threads, and behaves identically for hasattr/ getattr-with-default, dunder probes, Python subclasses with their own __getattr__, and suggestion messages. Add a regression test asserting the installed __getattr__ is a native method_descriptor, which is the property that keeps the callback out of MethodBinder's allow-threads path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Martin-Molinero
approved these changes
Jul 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this implement/fix?
Re-lands the miss-only
__getattr__hook for AttributeError member-name suggestions (the #124 approach, which #126 had replaced with atp_getattrooverride), and fixes the off-GIL defect that crashed Lean's CI on 2.0.55.Why re-land the hook: with the hook, successful attribute accesses go straight through CPython's native generic getattr with no managed transition — only an actual miss enters managed code. The #126
tp_getattrooverride put a managed round-trip on every attribute access, hit or miss.The 2.0.55 crash (see first commit): the original hook exposed the hint builder to Python as a .NET delegate (
Func<PyObject, string, string>). Python invoked it throughDelegateObject.tp_call→MethodBinder.Invoke, andMethodBinderreleases the GIL around every reflected invocation (allow_threadsdefaults totrue). The callback therefore ran CPython C-API calls without holding the GIL on every attribute miss (hasattr,getattrwith default, typos). It usually survived by luck, but segfaulted whenever the pythonnetFinalizerfired mid-callback:Finalizer.DisposeAll()starts withPyErr_Fetch, which dereferences the current thread state — NULL when the GIL is not held. This is what crashed Lean's CI test host after all 35k tests passed.The fix (second commit): the
__getattr__installed on reflected types is now a native method descriptor — a process-lifetimePyMethodDef(METH_VARARGS) around a managed thunk, created withPyDescr_NewMethod(newly bound inRuntime.Delegates). CPython'sslot_tp_getattr_hookcalls the managed hook directly as a native method call with the GIL held;MethodBinderis never involved.User-facing messages are unchanged from 2.0.55/2.0.56:
Does this close any currently open issues?
No open issue; this addresses the Lean CI test-host crash observed with 2.0.55 (
PyErr_Fetch(tstate=NULL)underFinalizer.DisposeAllunderBuildMissingAttributeMessage).Verification performed
PyGILState_Check()insideBuildMissingAttributeMessage: the delegate-based hook (first commit alone) reportsGIL held: Falseon every miss; with the fix it reportsGIL held: True.quantconnect-stubsshadowing theMicrosoftnamespace, identical on master).hasattr/getattr-with-default, dunder probes (pickle/copy machinery), Python subclasses defining their own__getattr__, and suggestions inherited by Python subclasses of .NET types all behave as before.A new regression test (
test_missing_attribute_hook_is_native) asserts the installed__getattr__is a nativemethod_descriptor, the property that keeps the callback out ofMethodBinder's GIL-releasing path.