diff --git a/src/embed_tests/TestPropertyAccess.cs b/src/embed_tests/TestPropertyAccess.cs
index 1c9d0e7fd..8dba383d6 100644
--- a/src/embed_tests/TestPropertyAccess.cs
+++ b/src/embed_tests/TestPropertyAccess.cs
@@ -1129,44 +1129,6 @@ def GetValue(self, fixture):
}
}
- [Test]
- public void TestGetMisspelledDynamicObjectPropertySuggestsSimilarMembers()
- {
- dynamic model = PyModule.FromString("module", @"
-from clr import AddReference
-AddReference(""Python.EmbeddingTest"")
-AddReference(""System"")
-
-from Python.EmbeddingTest import *
-
-class TestGetMisspelledDynamicObjectPropertySuggestsSimilarMembers:
- def GetValue(self, fixture):
- try:
- # 'non_dynamic_propertyy' is a near miss of the snake_case alias of the
- # real 'NonDynamicProperty' member.
- prop = fixture.non_dynamic_propertyy
- except AttributeError as e:
- return e
-
- return None
-").GetAttr("TestGetMisspelledDynamicObjectPropertySuggestsSimilarMembers").Invoke();
-
- dynamic fixture = new DynamicFixture();
-
- using (Py.GIL())
- {
- var result = model.GetValue(fixture) as PyObject;
- Assert.IsFalse(result.IsNone());
- Assert.AreEqual(result.PyType, Exceptions.AttributeError);
-
- // Suggestions are emitted in snake_case, matching the fork's PEP8-style API.
- var message = result.ToString();
- Assert.That(message, Does.Contain("non_dynamic_propertyy"));
- Assert.That(message, Does.Contain("Did you mean"));
- Assert.That(message, Does.Contain("non_dynamic_property"));
- }
- }
-
public class CSharpTestClass
{
public string CSharpProperty { get; set; }
diff --git a/src/perf_tests/Python.PerformanceTests.csproj b/src/perf_tests/Python.PerformanceTests.csproj
index 1260a79fd..17af4024c 100644
--- a/src/perf_tests/Python.PerformanceTests.csproj
+++ b/src/perf_tests/Python.PerformanceTests.csproj
@@ -13,7 +13,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
compile
@@ -25,7 +25,7 @@
-
+
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/Properties/AssemblyInfo.cs b/src/runtime/Properties/AssemblyInfo.cs
index bca5261f0..06f73394d 100644
--- a/src/runtime/Properties/AssemblyInfo.cs
+++ b/src/runtime/Properties/AssemblyInfo.cs
@@ -4,5 +4,5 @@
[assembly: InternalsVisibleTo("Python.EmbeddingTest, PublicKey=00240000048000009400000006020000002400005253413100040000110000005ffd8f49fb44ab0641b3fd8d55e749f716e6dd901032295db641eb98ee46063cbe0d4a1d121ef0bc2af95f8a7438d7a80a3531316e6b75c2dae92fb05a99f03bf7e0c03980e1c3cfb74ba690aca2f3339ef329313bcc5dccced125a4ffdc4531dcef914602cd5878dc5fbb4d4c73ddfbc133f840231343e013762884d6143189")]
[assembly: InternalsVisibleTo("Python.Test, PublicKey=00240000048000009400000006020000002400005253413100040000110000005ffd8f49fb44ab0641b3fd8d55e749f716e6dd901032295db641eb98ee46063cbe0d4a1d121ef0bc2af95f8a7438d7a80a3531316e6b75c2dae92fb05a99f03bf7e0c03980e1c3cfb74ba690aca2f3339ef329313bcc5dccced125a4ffdc4531dcef914602cd5878dc5fbb4d4c73ddfbc133f840231343e013762884d6143189")]
-[assembly: AssemblyVersion("2.0.55")]
-[assembly: AssemblyFileVersion("2.0.55")]
+[assembly: AssemblyVersion("2.0.54")]
+[assembly: AssemblyFileVersion("2.0.54")]
diff --git a/src/runtime/Python.Runtime.csproj b/src/runtime/Python.Runtime.csproj
index c9dbda45a..953fdcba0 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.54
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..590c870b5 100644
--- a/src/runtime/Types/ClassBase.cs
+++ b/src/runtime/Types/ClassBase.cs
@@ -611,213 +611,5 @@ protected virtual void OnDeserialization(object sender)
}
void IDeserializationCallback.OnDeserialization(object sender) => this.OnDeserialization(sender);
-
- ///
- /// If an AttributeError is currently set as the result of a missing
- /// attribute lookup on a .NET object, rewrites its message to append a list
- /// of similarly-named members of the managed type (a "Did you mean ...?" hint).
- /// This is a no-op when there is no AttributeError set, when the object is not
- /// a CLR object, or when no similarly-named members exist. It only runs on the
- /// exceptional (miss) path, so the reflection cost is not on the hot path.
- ///
- internal static void AppendAttributeErrorSuggestions(BorrowedReference ob, BorrowedReference key)
- {
- if (!Exceptions.ExceptionMatches(Exceptions.AttributeError))
- {
- return;
- }
-
- var name = Runtime.GetManagedString(key);
- if (string.IsNullOrEmpty(name))
- {
- return;
- }
-
- var hint = GetSuggestionHint(ob, name);
- if (hint.Length == 0)
- {
- return;
- }
-
- // Keep the original AttributeError message and append our hint to it.
- Runtime.PyErr_Fetch(out var errType, out var errValue, out var errTraceback);
- try
- {
- var baseMessage = GetErrorMessage(errValue.BorrowNullable(), name);
- Exceptions.SetError(Exceptions.AttributeError, baseMessage + hint);
- }
- finally
- {
- errType.Dispose();
- errValue.Dispose();
- errTraceback.Dispose();
- }
- }
-
- ///
- /// 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)
- {
- using var str = Runtime.PyObject_Str(value);
- if (!str.IsNull())
- {
- var managed = Runtime.GetManagedString(str.Borrow());
- if (!string.IsNullOrEmpty(managed))
- {
- return managed;
- }
- }
- // PyObject_Str may itself have failed; do not let that error leak out.
- Exceptions.Clear();
- }
- return $"object has no attribute '{fallbackName}'";
- }
-
- private static List GetSimilarMemberNames(Type type, string name)
- {
- const int MaxSuggestions = 5;
- var threshold = Math.Max(2, name.Length / 3);
-
- var seen = new HashSet(StringComparer.Ordinal);
- var scored = new List<(string Name, int Distance)>();
-
- var members = type.GetMembers(BindingFlags.Public | BindingFlags.Instance
- | BindingFlags.Static | BindingFlags.FlattenHierarchy);
- foreach (var member in members)
- {
- // Skip property/event accessors, operators and other special-name methods,
- // as well as compiler-generated members; none are accessible by name.
- if (member is MethodBase { IsSpecialName: true })
- {
- continue;
- }
-
- if (member.Name.Length == 0 || member.Name[0] == '<')
- {
- continue;
- }
-
- // Suggest the snake_case alias, since that is the fork's PEP8-style
- // public API surface (members are exposed in both Pascal and snake case).
- var candidate = ToSnakeCaseMemberName(member);
- if (!seen.Add(candidate))
- {
- continue;
- }
-
- var distance = LevenshteinDistance(name, candidate);
- var related = distance <= threshold
- || candidate.IndexOf(name, StringComparison.OrdinalIgnoreCase) >= 0
- || name.IndexOf(candidate, StringComparison.OrdinalIgnoreCase) >= 0;
- if (related)
- {
- scored.Add((candidate, distance));
- }
- }
-
- return scored
- .OrderBy(t => t.Distance)
- .ThenBy(t => t.Name, StringComparer.OrdinalIgnoreCase)
- .Take(MaxSuggestions)
- .Select(t => t.Name)
- .ToList();
- }
-
- private static string ToSnakeCaseMemberName(MemberInfo member)
- {
- // Use the field/property overloads so const and static-readonly members
- // are converted to UPPER_CASE, matching how they are exposed to Python.
- return member switch
- {
- FieldInfo fieldInfo => fieldInfo.ToSnakeCase(),
- PropertyInfo propertyInfo => propertyInfo.ToSnakeCase(),
- _ => member.Name.ToSnakeCase(),
- };
- }
-
- private static int LevenshteinDistance(string a, string b)
- {
- a = a.ToLowerInvariant();
- b = b.ToLowerInvariant();
- var n = a.Length;
- var m = b.Length;
- if (n == 0) return m;
- if (m == 0) return n;
-
- var prev = new int[m + 1];
- var curr = new int[m + 1];
- for (var j = 0; j <= m; j++) prev[j] = j;
-
- for (var i = 1; i <= n; i++)
- {
- curr[0] = i;
- for (var j = 1; j <= m; j++)
- {
- var cost = a[i - 1] == b[j - 1] ? 0 : 1;
- curr[j] = Math.Min(Math.Min(curr[j - 1] + 1, prev[j] + 1), prev[j - 1] + cost);
- }
- (prev, curr) = (curr, prev);
- }
- return prev[m];
- }
}
}
diff --git a/src/runtime/Types/DynamicClassObject.cs b/src/runtime/Types/DynamicClassObject.cs
index 621a6f423..cb6fd5650 100644
--- a/src/runtime/Types/DynamicClassObject.cs
+++ b/src/runtime/Types/DynamicClassObject.cs
@@ -80,10 +80,7 @@ public static NewReference tp_getattro(BorrowedReference ob, BorrowedReference k
}
catch (RuntimeBinder.RuntimeBinderException)
{
- // The attribute is neither a static member nor a dynamic property.
- // AttributeError was already raised in Python side (by the generic
- // getattr above) and was not cleared; enrich it with member suggestions.
- AppendAttributeErrorSuggestions(ob, key);
+ // Do nothing, AttributeError was already raised in Python side and it was not cleared.
}
// Catch C# exceptions and raise them as Python exceptions.
catch (Exception exception)
diff --git a/tests/test_class.py b/tests/test_class.py
index 4f0effecd..ec275d752 100644
--- a/tests/test_class.py
+++ b/tests/test_class.py
@@ -66,44 +66,6 @@ def test_non_exported():
_ = Test.NonExportable
-def test_missing_attribute_suggests_similar_members():
- """A missing attribute on a .NET object should suggest similarly-named members.
-
- Suggestions are emitted in snake_case, matching the fork's PEP8-style API.
- """
- s = System.String("this is a test")
-
- # 'lenght' is a transposition of 'length' (the snake_case alias of the real
- # 'Length' member), so it should be suggested.
- with pytest.raises(AttributeError) as exc_info:
- _ = s.lenght
-
- message = str(exc_info.value)
- assert "lenght" in message
- assert "Did you mean" in message
- assert "length" in message
-
-
-def test_missing_attribute_no_similar_members():
- """A missing attribute with no similar members keeps the standard message."""
- s = System.String("this is a test")
-
- with pytest.raises(AttributeError) as exc_info:
- _ = s.completely_unrelated_xyzzy
-
- message = str(exc_info.value)
- assert "completely_unrelated_xyzzy" in message
- assert "Did you mean" not in message
-
-
-def test_missing_attribute_hasattr_still_false():
- """Enriching the AttributeError must not break hasattr() (it must stay False)."""
- s = System.String("this is a test")
-
- assert not hasattr(s, "Lenght")
- assert hasattr(s, "Length")
-
-
def test_basic_subclass():
"""Test basic subclass of a managed class."""
from System.Collections import Hashtable