GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Implement support for DLR get/set by filmor · Pull Request #2706 · pythonnet/pythonnet

Implement support for DLR get/set #2706

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

Sign up for GitHub

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
filmor merged 5 commits into master from dlr
May 15, 2026

Uh oh!

There was an error while loading. Please reload this page.

Merged

Implement support for DLR get/set #2706

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
5 commits
Select commit Hold shift + click to select a range
5dacfb4
Implement support for DLR get/set
filmor Apr 18, 2026
69ba861
Use explicit IsNull on BorrowedReference
filmor May 5, 2026
2408c43
Catch errors in setting/deleting properties
filmor May 7, 2026
8b12b56
Propagate exceptions from TryGetMember in tp_getattro_dlr_proxy (#2718)
greateggsgreg May 11, 2026
3a08990
Cache HasClrMember and align DLR setter exception path
greateggsgreg May 12, 2026
File filter
Filter by extension

Clear filters
Conversations
Failed to load comments.
Loading

Uh oh!

There was an error while loading. Please reload this page.

Jump to
Jump to file
Failed to load files.
Loading

Uh oh!

There was an error while loading. Please reload this page.

Diff view
Diff view

There are no files selected for viewing

1 change: 1 addition & 0 deletions 1 src/runtime/InteropConfiguration.cs
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public static InteropConfiguration MakeDefault()
{
DefaultBaseTypeProvider.Instance,
new CollectionMixinsProvider(new Lazy<PyObject>(() => Py.Import("clr._extras.collections"))),
new DynamicObjectMixinsProvider(new Lazy<PyObject>(() => Py.Import("clr._extras.dlr"))),
},
};
}
Expand Down
47 changes: 47 additions & 0 deletions 47 src/runtime/Mixins/DynamicObjectMixinsProvider.cs
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.Dynamic;

namespace Python.Runtime.Mixins;

class DynamicObjectMixinsProvider : IPythonBaseTypeProvider, IDisposable
{
readonly Lazy<PyObject> mixinsModule;

public DynamicObjectMixinsProvider(Lazy<PyObject> mixinsModule) =>
this.mixinsModule = mixinsModule ?? throw new ArgumentNullException(nameof(mixinsModule));

public PyObject Mixins => mixinsModule.Value;

public IEnumerable<PyType> GetBaseTypes(Type type, IList<PyType> existingBases)
{
if (type is null)
throw new ArgumentNullException(nameof(type));

if (existingBases is null)
throw new ArgumentNullException(nameof(existingBases));

if (!typeof(IDynamicMetaObjectProvider).IsAssignableFrom(type))
return existingBases;

var newBases = new List<PyType>(existingBases)
{
new(Mixins.GetAttr("DynamicMetaObjectProviderMixin"))
};

if (type.IsInterface && type.BaseType is null)
{
newBases.RemoveAll(@base => PythonReferenceComparer.Instance.Equals(@base, Runtime.PyBaseObjectType));
}

return newBases;
}

public void Dispose()
{
if (this.mixinsModule.IsValueCreated)
{
this.mixinsModule.Value.Dispose();
}
}
}
18 changes: 18 additions & 0 deletions 18 src/runtime/Mixins/dlr.py
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""
Implements helpers for Dynamic Language Runtime (DLR) types.
"""

class DynamicMetaObjectProviderMixin:
def __dir__(self):
names = set(super().__dir__())

get_dynamic_member_names = getattr(self, "GetDynamicMemberNames", None)
if callable(get_dynamic_member_names):
try:
for name in get_dynamic_member_names():
if isinstance(name, str):
names.add(name)
except Exception:
pass

return list(sorted(names))
2 changes: 1 addition & 1 deletion 2 src/runtime/PythonEngine.cs
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ static void LoadSubmodule(BorrowedReference targetModuleDict, string fullName, s

static void LoadMixins(BorrowedReference targetModuleDict)
{
foreach (string nested in new[] { "collections" })
foreach (string nested in new[] { "collections", "dlr" })
{
LoadSubmodule(targetModuleDict,
fullName: "clr._extras." + nested,
Expand Down
192 changes: 192 additions & 0 deletions 192 src/runtime/TypeManager.cs
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Diagnostics;

using Python.Runtime.Native;
using Python.Runtime.StateSerialization;

Expand Down Expand Up @@ -37,10 +40,190 @@ internal class TypeManager
"tp_clear",
};

static readonly DynamicObjectMemberAccessor dynamicMemberAccessor = new();

// tp_getattro_dlr_proxy / tp_setattro_dlr_proxy hit HasClrMember on every
// attribute access; cache the reflection result per (Type, name).
static readonly ConcurrentDictionary<(Type, string), bool> _hasClrMemberCache = new();

static bool HasClrMember(object instance, string memberName) =>
_hasClrMemberCache.GetOrAdd(
(instance.GetType(), memberName),
k => k.Item1.GetMember(k.Item2, BindingFlags.Public | BindingFlags.Instance).Length > 0);

static bool IsPythonSpecialAttributeName(string memberName) =>
memberName.Length > 4 && memberName.StartsWith("__") && memberName.EndsWith("__");

static bool TryGetDynamicInstance(BorrowedReference ob, out object instance, out IDynamicMetaObjectProvider dynamicObject)
{
if (ManagedType.GetManagedObject(ob) is CLRObject co && co.inst is IDynamicMetaObjectProvider coDynamic)
{
instance = co.inst;
dynamicObject = coDynamic;
return true;
}

if (Converter.ToManaged(ob, typeof(IDynamicMetaObjectProvider), out object? managedDynamic, false)
&& managedDynamic is IDynamicMetaObjectProvider convertedDynamic)
{
instance = managedDynamic;
dynamicObject = convertedDynamic;
return true;
}

if (Converter.ToManaged(ob, typeof(object), out object? managedInstance, false)
&& managedInstance is IDynamicMetaObjectProvider boxedDynamic)
{
instance = managedInstance;
dynamicObject = boxedDynamic;
return true;
}

instance = null!;
dynamicObject = null!;
return false;
}

public static NewReference tp_getattro_dlr_proxy(BorrowedReference ob, BorrowedReference key)
{
var isDynamic = TryGetDynamicInstance(ob, out object instance, out IDynamicMetaObjectProvider dynamicObject);

// The whole DLR machinery only makes sense with string keys and dynamic objects
if (!isDynamic || !Runtime.PyString_Check(key))
{
return Runtime.PyObject_GenericGetAttr(ob, key);
}

string memberName = Runtime.GetManagedString(key)!;

// Forward requests to GetDynamicMemberNames to the mixin implementation
if (memberName == nameof(DynamicObjectMemberAccessor.GetDynamicMemberNames)
&& !HasClrMember(instance, memberName))
{
using var pyMemberNames = new Func<IReadOnlyCollection<string>>(
() => dynamicMemberAccessor.GetDynamicMemberNames(dynamicObject)
).ToPython();
return pyMemberNames.NewReferenceOrNull();
}

// Now, first try to access the Python attribute
var attr = Runtime.PyObject_GenericGetAttr(ob, key);
if (!attr.IsNull())
return attr;

// attr is null, so an exception must be set. If that exception is not an AttributeError,
// we return from this function immediately without clearing. All later returns until the
// very end will lead to the AttributeError getting raised.
if (Runtime.PyErr_ExceptionMatches(Exceptions.AttributeError) == 0)
{
return default;
}

if (HasClrMember(instance, memberName) || IsPythonSpecialAttributeName(memberName))
{
return default;
}

bool resolved = false;
object? value = null;
try
{
resolved = dynamicMemberAccessor.TryGetMember(dynamicObject, memberName, out value);
}
catch (Exception e)
{
// Avoid wrapping the CLR exception via Converter.ToPython here: that would trigger
// CLR type initialisation which can re-enter this slot on the same live object,
// causing infinite recursion. A plain RuntimeError with the message is safe.
Runtime.PyErr_Clear();
Exceptions.SetError(Exceptions.RuntimeError, e.Message);
return default;
}

if (!resolved)
{
return default;
}

Runtime.PyErr_Clear();

using var pyValue = value.ToPython();
return pyValue.NewReferenceOrNull();
}

public static int tp_setattro_dlr_proxy(BorrowedReference ob, BorrowedReference key, BorrowedReference val)
{
var isDynamic = TryGetDynamicInstance(ob, out object instance, out IDynamicMetaObjectProvider dynamicObject);

// The whole DLR machinery only makes sense with string keys and dynamic objects
if (!isDynamic || !Runtime.PyString_Check(key))
{
return Runtime.PyObject_GenericSetAttr(ob, key, val);
}

string memberName = Runtime.GetManagedString(key)!;

// For Python-derived types (IPythonDerivedType), the Python descriptor protocol
// (e.g. @property setters) takes priority over DLR member storage.
if (instance is IPythonDerivedType)
{
int pyResult = Runtime.PyObject_GenericSetAttr(ob, key, val);
if (pyResult == 0)
return 0;

if (Runtime.PyErr_ExceptionMatches(Exceptions.AttributeError) == 0)
return pyResult;

Runtime.PyErr_Clear();
// Fall through to DLR fallback below
}

if (!HasClrMember(instance, memberName) && !IsPythonSpecialAttributeName(memberName))
{
// Try DLR member storage first
bool handled;

try
{
if (val.IsNull)
{
handled = dynamicMemberAccessor.TryDeleteMember(dynamicObject, memberName);
}
else
{
object? managedValue = null;
if (val != Runtime.PyNone && !Converter.ToManaged(val, typeof(object), out managedValue, true))
return -1;

handled = dynamicMemberAccessor.TrySetMember(dynamicObject, memberName, managedValue);
if (!handled)
{
Exceptions.SetError(Exceptions.AttributeError, $"'{instance.GetType().Name}' object has no attribute '{memberName}'");
return -1;
}
}
}
catch (Exception e)
{
// Same reasoning as the getter: avoid Converter.ToPython(e) to keep this
// slot re-entry-safe on live dynamic objects.
Exceptions.SetError(Exceptions.RuntimeError, e.Message);
return -1;
}

if (handled)
return 0;
}

// Fall back to Python attribute setting
return Runtime.PyObject_GenericSetAttr(ob, key, val);
}

internal static void Initialize()
{
Debug.Assert(cache.Count == 0, "Cache should be empty",
"Some errors may occurred on last shutdown");
dynamicMemberAccessor.Clear();
using (var plainType = SlotHelper.CreateObjectType())
{
subtype_traverse = Util.ReadIntPtr(plainType.Borrow(), TypeOffset.tp_traverse);
Expand All @@ -64,6 +247,8 @@ internal static void RemoveTypes()
}
}

dynamicMemberAccessor.Clear();

foreach (var type in cache.Values)
{
type.Dispose();
Expand Down Expand Up @@ -313,6 +498,13 @@ internal static void InitializeClass(PyType type, ClassBase impl, Type clrType)
throw PythonException.ThrowLastAsClrException();
}

if (typeof(IDynamicMetaObjectProvider).IsAssignableFrom(clrType))
{
InitializeSlot(type, TypeOffset.tp_getattro, new Interop.BB_N(tp_getattro_dlr_proxy), slotsHolder);
InitializeSlot(type, TypeOffset.tp_setattro, new Interop.BBB_I32(tp_setattro_dlr_proxy), slotsHolder);
Runtime.PyType_Modified(type.Reference);
}

var dict = Util.ReadRef(type, TypeOffset.tp_dict);
string mn = clrType.Namespace ?? "";
using (var mod = Runtime.PyString_FromString(mn))
Expand Down
37 changes: 37 additions & 0 deletions 37 src/runtime/Types/ClassDerived.cs
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Dynamic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
Expand Down Expand Up @@ -232,6 +233,13 @@ internal static Type CreateDerivedType(string name,
continue;
}

// Avoid re-entrant DLR binder recursion when Python derives from
// DynamicObject-based types (including overrides in intermediate bases).
if (IsDynamicObjectHookMethod(method))
{
continue;
}

// skip if this property has already been overridden
if ((method.Name.StartsWith("get_") || method.Name.StartsWith("set_"))
&& pyProperties.Contains(method.Name.Substring(4)))
Expand Down Expand Up @@ -300,6 +308,35 @@ internal static Type CreateDerivedType(string name,
return type;
}

static bool IsDynamicObjectHookMethod(MethodInfo method)
{
MethodInfo origin = method.GetBaseDefinition();
Type? originType = origin.DeclaringType;
if (originType == typeof(DynamicObject))
{
return origin.Name switch
{
nameof(DynamicObject.TryGetMember)
or nameof(DynamicObject.TrySetMember)
or nameof(DynamicObject.TryDeleteMember)
or nameof(DynamicObject.TryInvokeMember)
or nameof(DynamicObject.TryConvert)
or nameof(DynamicObject.TryGetIndex)
or nameof(DynamicObject.TrySetIndex)
or nameof(DynamicObject.GetDynamicMemberNames)
or nameof(IDynamicMetaObjectProvider.GetMetaObject) => true,
_ => false,
};
}

if (originType == typeof(IDynamicMetaObjectProvider))
{
return origin.Name == nameof(IDynamicMetaObjectProvider.GetMetaObject);
}

return false;
}

/// <summary>
/// Add a constructor override that calls the python ctor after calling the base type constructor.
/// </summary>
Expand Down
Loading
Oops, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

Loading
Oops, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

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.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.