Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions src/embed_tests/TestPropertyAccess.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1129,6 +1129,44 @@ 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; }
Expand Down
4 changes: 2 additions & 2 deletions src/perf_tests/Python.PerformanceTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.*" />
<PackageReference Include="quantconnect.pythonnet" Version="2.0.54" GeneratePathProperty="true">
<PackageReference Include="quantconnect.pythonnet" Version="2.0.55" GeneratePathProperty="true">
<IncludeAssets>compile</IncludeAssets>
</PackageReference>
</ItemGroup>
Expand All @@ -25,7 +25,7 @@
</Target>

<Target Name="CopyBaseline" AfterTargets="Build">
<Copy SourceFiles="$(NuGetPackageRoot)quantconnect.pythonnet\2.0.54\lib\net10.0\Python.Runtime.dll" DestinationFolder="$(OutDir)baseline" />
<Copy SourceFiles="$(NuGetPackageRoot)quantconnect.pythonnet\2.0.55\lib\net10.0\Python.Runtime.dll" DestinationFolder="$(OutDir)baseline" />
</Target>

<Target Name="CopyNewBuild" AfterTargets="Build">
Expand Down
109 changes: 109 additions & 0 deletions src/runtime/AttributeErrorHint.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
using System;

using Python.Runtime.Native;

namespace Python.Runtime
{
/// <summary>
/// Installs a miss-only <c>__getattr__</c> hook on reflected .NET types so that an
/// <c>AttributeError</c> raised for a missing attribute is enriched with suggestions
/// of similarly-named members — without adding any cost to the (common) successful
/// attribute-access path.
/// </summary>
/// <remarks>
/// CPython only invokes <c>__getattr__</c> after the normal attribute lookup fails,
/// via the native <c>slot_tp_getattr_hook</c>: on a hit it calls the generic getattr
/// directly (no managed transition); only on a miss does it call our <c>__getattr__</c>.
/// pythonnet's metatype does not run CPython's slot-fixup machinery when an attribute
/// is set on a type, so simply adding <c>__getattr__</c> to the type dict would not
/// rewire the slot — we therefore wire <c>tp_getattro</c> to the hook manually.
/// </remarks>
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<PyObject, string, string> 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();
}
}

/// <summary>
/// Wires the miss-only hook onto <paramref name="type"/> if it still uses the
/// native generic getattr. Types with a custom <c>tp_getattro</c> (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.
/// </summary>
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;
}
}
}
4 changes: 2 additions & 2 deletions src/runtime/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
[assembly: InternalsVisibleTo("Python.EmbeddingTest, PublicKey=00240000048000009400000006020000002400005253413100040000110000005ffd8f49fb44ab0641b3fd8d55e749f716e6dd901032295db641eb98ee46063cbe0d4a1d121ef0bc2af95f8a7438d7a80a3531316e6b75c2dae92fb05a99f03bf7e0c03980e1c3cfb74ba690aca2f3339ef329313bcc5dccced125a4ffdc4531dcef914602cd5878dc5fbb4d4c73ddfbc133f840231343e013762884d6143189")]
[assembly: InternalsVisibleTo("Python.Test, PublicKey=00240000048000009400000006020000002400005253413100040000110000005ffd8f49fb44ab0641b3fd8d55e749f716e6dd901032295db641eb98ee46063cbe0d4a1d121ef0bc2af95f8a7438d7a80a3531316e6b75c2dae92fb05a99f03bf7e0c03980e1c3cfb74ba690aca2f3339ef329313bcc5dccced125a4ffdc4531dcef914602cd5878dc5fbb4d4c73ddfbc133f840231343e013762884d6143189")]

[assembly: AssemblyVersion("2.0.54")]
[assembly: AssemblyFileVersion("2.0.54")]
[assembly: AssemblyVersion("2.0.55")]
[assembly: AssemblyFileVersion("2.0.55")]
2 changes: 1 addition & 1 deletion src/runtime/Python.Runtime.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<RootNamespace>Python.Runtime</RootNamespace>
<AssemblyName>Python.Runtime</AssemblyName>
<PackageId>QuantConnect.pythonnet</PackageId>
<Version>2.0.54</Version>
<Version>2.0.55</Version>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<RepositoryUrl>https://github.com/pythonnet/pythonnet</RepositoryUrl>
Expand Down
7 changes: 7 additions & 0 deletions src/runtime/PythonEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,10 @@ public static void Initialize(IEnumerable<string> 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)
Expand Down Expand Up @@ -369,6 +373,9 @@ public static void Shutdown()
AppDomain.CurrentDomain.ProcessExit -= OnProcessExit;

ExecuteShutdownHandlers();

AttributeErrorHint.Shutdown();

// Remember to shut down the runtime.
Runtime.Shutdown();

Expand Down
4 changes: 4 additions & 0 deletions src/runtime/TypeManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,10 @@ 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);
}

Expand Down
Loading
Loading