Cache and extend AttributeError member-name suggestions (static members and enum values)#130
Merged
jhonabreul merged 5 commits intoJul 7, 2026
Conversation
Building the "Did you mean ...?" hint for a missing attribute reflected over the managed type's full member set (GetMembers with FlattenHierarchy), snake_cased every member, and ran a Levenshtein scan -- on every miss, with no caching. getattr(obj, name, default) and hasattr trigger it too, since the work happens on the miss path before CPython suppresses the error. A workload that probes the same missing names repeatedly (e.g. a per-bar getattr(self, "_optional", None) on a .NET-derived object) therefore paid the full O(members) reflection + ranking cost on every access. Add two caches in ClassBase: - _candidateNameCache (Type -> string[]): the reflected, deduplicated snake_case member names, computed once per type. - _suggestionCache ((Type, name) -> string[]): the ranked suggestion list, memoized so repeated misses of the same name are a dictionary lookup. Suggestions and error messages are unchanged; only the repeated computation is removed. On a real Lean multi-symbol minute backtest the suggestion path dominated ~93% of OnData CPU; with this change the backtest goes from not finishing (aborted, >15x slower) to ~93s, on par with the last build without the suggestion feature (~90s), with identical results. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Martin-Molinero
approved these changes
Jul 6, 2026
Store the fully-built " Did you mean: ...?" hint (empty when there is nothing to suggest) in _suggestionCache rather than the ranked string[]. The hint is now assembled once inside ComputeSimilarMemberNames and memoized per (type, missing-name); GetSuggestionHint just returns the cached string and appends it, dropping the per-miss Count check, Select and string.Join. Behavior and message text are unchanged; a repeated miss is now a single dictionary lookup returning the ready-made hint. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ollection Move the _candidateNameCache and _suggestionCache declarations up to the class field block with the other fields. In GetCandidateMemberNames, collect the snake_case names into a single HashSet (named names) instead of a HashSet plus a List, and return the set directly; deduplication and storage are the same collection. Candidate iteration order no longer matters -- suggestions are ordered by edit distance and then by name. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Martin-Molinero
approved these changes
Jul 7, 2026
…sses A missing attribute on a reflected type object -- a mistyped static member or enum value such as DayOfWeek.Sundey -- previously raised the bare CPython "type object 'X' has no attribute 'Y'" with no hint, because the miss hook was installed on reflected types (governing their instances) but type-object access is governed by the CLR metatype. Install the same miss-only __getattr__ hook on the CLR metatype (allowing the redirect when tp_getattro is type_getattro, not just the generic getattr), so a type-object miss is enriched the same way instance misses are. BuildMissing AttributeMessage now resolves the target from either a CLRObject (instance) or a ClassBase (type object) and uses CPython's "type object 'T'" wording for the latter. Suggestions reuse the existing ranking/cache and the snake_case convention Python exposes members under (ToSnakeCaseMemberName): methods become lower_snake while enum values, consts and static-readonly members become UPPER_SNAKE, e.g. DayOfWeek.Sundey -> "Did you mean: 'SUNDAY'?", Math.PII -> 'PI', String.Empy -> 'EMPTY'. All suggested names resolve. Hits, imports and hasattr on type objects are unaffected (the hook is miss-only). Adds tests for enum, static const, static-readonly field and static method misses, the no-similar case and hasattr, in test_enum.py and test_class.py. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bump the package <Version>, AssemblyVersion/AssemblyFileVersion and the perf-test baseline reference to 2.0.58 for the AttributeError suggestion caching and the static/enum suggestion extension in this PR. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Martin-Molinero
approved these changes
Jul 7, 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? Explain your changes.
Two related changes to the AttributeError "Did you mean ...?" suggestions (added in #124 / #129):
1. Cache the suggestions (performance fix).
Building the hint reflects over the managed type's entire member set (
GetMemberswithFlattenHierarchy), snake_cases every member, and runs a Levenshtein scan. This ran on every miss with no caching — andgetattr(obj, name, default)/hasattrtrigger it too, because the work happens on the miss path before CPython suppresses the error. A workload that probes the same missing names repeatedly — e.g. a per-bargetattr(self, "_optional", None)on a .NET-derived object — pays the fullO(members)reflection + ranking cost on every call.Two caches in
ClassBasefix this:_candidateNameCache(Type -> HashSet<string>): the deduplicated snake_case member names, computed once per type._suggestionCache((Type, name) -> string): the fully-built" Did you mean: ...?"hint (empty when there is nothing to suggest), memoized per(type, missing-name).A repeated miss is now a single dictionary lookup. Suggestions and message text are unchanged by this part.
Measured on a real Lean multi-symbol minute backtest (a Python
QCAlgorithmsubclass doing many per-bar optional-attribute probes), profiled withdotnet-trace: the suggestion path (GetAttrHook -> reflection + LevenshteinDistance + ToSnakeCase) accounted for ~93% of OnData CPU. With the cache the same backtest goes from not finishing (aborted, >15× slower) to ~93s, on par with the last build without the suggestion feature (~90s), with identical results.2. Extend suggestions to type-object misses (static members and enum values).
A missing attribute on a reflected type object — a mistyped static member or enum value such as
DayOfWeek.Sundey— previously raised the bare CPythontype object 'X' has no attribute 'Y'with no hint, because the miss hook was installed on reflected types (which govern their instances), whereas type-object access is governed by the CLR metatype.This installs the same miss-only
__getattr__hook on the CLR metatype (allowing the redirect whentp_getattroistype_getattro, not only the generic getattr), so a type-object miss is enriched the same way instance misses are. It stays miss-only, so hits, submodule imports andhasattron type objects are unaffected.BuildMissingAttributeMessageresolves the target from either aCLRObject(instance) or aClassBase(type object) and uses CPython'stype object 'T'wording for the latter.Suggestions reuse the existing ranking and the snake_case convention Python exposes members under (
ToSnakeCaseMemberName): methods becomelower_snake, while enum values, consts and static-readonly members becomeUPPER_SNAKE. Examples of the new messages:Every suggested name resolves (
DayOfWeek.SUNDAY,String.EMPTY,Math.sqrt, ...). Instance-attribute suggestions are unchanged (obj.lenght -> 'length').Does this close any currently open issues?
No pythonnet issue is open. Part 1 fixes the backtest performance regression introduced with the AttributeError suggestion feature (#124 / #129); part 2 extends that feature to static/enum members.
Any other comments?
Part 1 is behavior-preserving (identical messages). Part 2 only changes the previously unhinted type-object miss path, adding suggestions and matching CPython's
type object 'T'wording.Bumps the package version to 2.0.58 (
<Version>,AssemblyVersion/AssemblyFileVersionand the perf-test baseline reference).Checklist
AUTHORSCHANGELOG