Skip to content

Suggest similar member names on AttributeError for .NET objects#124

Merged
jhonabreul merged 5 commits into
QuantConnect:masterfrom
jhonabreul:attribute-error-suggestions
Jul 1, 2026
Merged

Suggest similar member names on AttributeError for .NET objects#124
jhonabreul merged 5 commits into
QuantConnect:masterfrom
jhonabreul:attribute-error-suggestions

Conversation

@jhonabreul

@jhonabreul jhonabreul commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

When Python code accesses a missing attribute on a .NET object, the resulting AttributeError now includes a Did you mean ...? hint listing similarly-named members of the managed type, in snake_case (matching the fork's PEP8-style API surface):

>>> System.String("x").lenght
AttributeError: 'String' object has no attribute 'lenght' Did you mean: 'length'?

The suggestion is computed from the .NET type's members and only appears when an attribute is actually missing.

Design — miss-only __getattr__ hook (zero hot-path cost)

The enrichment runs through a miss-only __getattr__ hook, so there is no cost on the common, successful attribute-access path:

  • CPython only invokes __getattr__ after the normal lookup fails (via the native slot_tp_getattr_hook). On a hit it calls the generic getattr directly with no managed transition; only on a miss does it call our handler.
  • pythonnet's metatype does not run CPython's slot-fixup when attributes are set on a type, so AttributeErrorHint wires tp_getattro to the hook manually (the hook address is read from a probe class at startup). Only types still using the native generic getattr are redirected — dynamic objects, modules and interfaces (which have their own tp_getattro) are left untouched, and DynamicClassObject enriches on its own miss path.
  • Candidates are ranked by case-insensitive Levenshtein distance plus substring containment; dunder names are skipped (they are probed internally by CPython and are never user typos). Member names are emitted in snake_case, reusing the existing ToSnakeCase helpers so const/static-readonly members render as UPPER_CASE.
  • Works across all supported Python versions (3.7–3.11) — it does not depend on the AttributeError.name/.obj attributes that only exist in 3.10+.

Performance

Measured on System.Version property access, best-of-N:

Path master miss-only hook
hit (successful access) ~baseline ~baseline (no managed transition added)
miss (absent attribute) fast native raise reflection + Levenshtein (rare)

An earlier tp_getattro (__getattribute__) implementation was also benchmarked; it added a consistent ~17–19 ns per attribute access on every access. A property-access-dominated Lean algorithm (~117M C# property gets) reproduced this at ~19 ns/access (~9% on that pathological loop). Across realistic Lean algorithms and a 50-algorithm regression subset, that overhead was within run-to-run noise (< 0.1% aggregate). The miss-only hook in this PR removes even that per-access cost, keeping the hit path at native speed.

Changes

  • AttributeErrorHint.cs — sets up and installs the shared miss-only __getattr__ hook on reflected types.
  • ClassBase.cs — builds the enriched message / snake_case member suggestions (Levenshtein ranking, dunder skip); shared by the hook and by DynamicClassObject.
  • TypeManager.cs / PythonEngine.cs — install the hook per reflected type; initialize/teardown.
  • DynamicClassObject.cs — enriches its own attribute-miss path.
  • Tests: tests/test_class.py and src/embed_tests/TestPropertyAccess.cs.

🤖 Generated with Claude Code

When accessing a missing attribute on a .NET object from Python, enrich
the resulting AttributeError message with a "Did you mean ...?" hint
listing similarly-named members of the managed type.

The work is done inside tp_getattro where the object and attribute name
are available directly, so it does not depend on the AttributeError
.name/.obj attributes (Python 3.10+) and works across all supported
Python versions (3.7-3.11). It only runs on the exceptional miss path,
keeping normal attribute access untouched, and always re-raises an
AttributeError so hasattr()/getattr(default) keep working.

- ClassObject gets a tp_getattro that delegates to PyObject_GenericGetAttr
  and appends suggestions on a miss (inherited by EnumObject, LookUpObject,
  ExceptionClassObject, ClassDerivedObject).
- DynamicClassObject appends suggestions on its RuntimeBinder miss path.
- Shared helpers live in ClassBase (Levenshtein-based ranking, dunder
  names skipped, original CPython message preserved).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
jhonabreul and others added 4 commits June 30, 2026 15:00
The fork exposes .NET members under PEP8-style snake_case aliases, so the
"Did you mean ...?" suggestions now use that form (e.g. 'length' instead
of 'Length'). Conversion reuses the existing ToSnakeCase helpers, so const
and static-readonly members are rendered UPPER_CASE to match how they are
exposed to Python.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Style cleanup: prefer var over explicit types where the type is apparent
from the right-hand side. Also drops two now-unnecessary null-forgiving
operators that the compiler's flow analysis already proves non-null.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the suggestion logic for regular reflected types off the hot
attribute-access path. Previously ClassObject overrode tp_getattro
(__getattribute__), so every successful attribute access paid a managed
round-trip (~17 ns/access measured). Instead, install a shared
__getattr__ on each reflected type, which CPython only invokes on a miss
via slot_tp_getattr_hook; hits go straight through the native generic
getattr with no managed transition.

pythonnet's metatype does not run CPython's slot-fixup when attributes
are set on a type, so AttributeErrorHint wires tp_getattro to the hook
manually (the hook address is read from a probe class). Only types still
using the native generic getattr are redirected, so dynamic objects,
modules and interfaces (which have their own tp_getattro) are untouched;
DynamicClassObject keeps enriching on its own miss path.

Benchmark (System.Version, best-of-N, ns/access):
  hit  path:  ~109 ns  (baseline ~105; tp_getattro was ~122)
  miss path:  ~11 us   (rare; reflection + Levenshtein, as before)

The hot-path overhead is effectively eliminated; the extra miss-path
cost only applies when an attribute is actually absent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jhonabreul jhonabreul merged commit e9a2338 into QuantConnect:master Jul 1, 2026
4 checks passed
@jhonabreul jhonabreul deleted the attribute-error-suggestions branch July 1, 2026 16:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants