Skip to content

Mixins for standard collections that implement collections.abc interfaces #1543

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.

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
merged 9 commits into from
Sep 23, 2021
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ This document follows the conventions laid out in [Keep a CHANGELOG][].
- Improved exception handling:
- exceptions can now be converted with codecs
- `InnerException` and `__cause__` are propagated properly
- .NET collection types now implement standard Python collection interfaces from `collections.abc`.
See [Mixins/collections.py](src/runtime/Mixins/collections.py).
- .NET arrays implement Python buffer protocol


Expand Down
3 changes: 3 additions & 0 deletions src/runtime/InteropConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ namespace Python.Runtime
using System;
using System.Collections.Generic;

using Python.Runtime.Mixins;

public sealed class InteropConfiguration
{
internal readonly PythonBaseTypeProviderGroup pythonBaseTypeProviders
Expand All @@ -18,6 +20,7 @@ public static InteropConfiguration MakeDefault()
PythonBaseTypeProviders =
{
DefaultBaseTypeProvider.Instance,
new CollectionMixinsProvider(new Lazy<PyObject>(() => Py.Import("clr._extras.collections"))),
},
};
}
Expand Down
90 changes: 90 additions & 0 deletions src/runtime/Mixins/CollectionMixinsProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace Python.Runtime.Mixins
{
class CollectionMixinsProvider : IPythonBaseTypeProvider
{
readonly Lazy<PyObject> mixinsModule;
public CollectionMixinsProvider(Lazy<PyObject> mixinsModule)
{
this.mixinsModule = mixinsModule ?? throw new ArgumentNullException(nameof(mixinsModule));
}

public PyObject Mixins => this.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));

var interfaces = NewInterfaces(type).Select(GetDefinition).ToArray();

var newBases = new List<PyType>();
newBases.AddRange(existingBases);

// dictionaries
if (interfaces.Contains(typeof(IDictionary<,>)))
{
newBases.Add(new PyType(this.Mixins.GetAttr("MutableMappingMixin")));
}
else if (interfaces.Contains(typeof(IReadOnlyDictionary<,>)))
{
newBases.Add(new PyType(this.Mixins.GetAttr("MappingMixin")));
}

// item collections
if (interfaces.Contains(typeof(IList<>))
|| interfaces.Contains(typeof(System.Collections.IList)))
{
newBases.Add(new PyType(this.Mixins.GetAttr("MutableSequenceMixin")));
}
else if (interfaces.Contains(typeof(IReadOnlyList<>)))
{
newBases.Add(new PyType(this.Mixins.GetAttr("SequenceMixin")));
}
else if (interfaces.Contains(typeof(ICollection<>))
|| interfaces.Contains(typeof(System.Collections.ICollection)))
{
newBases.Add(new PyType(this.Mixins.GetAttr("CollectionMixin")));
}
else if (interfaces.Contains(typeof(System.Collections.IEnumerable)))
{
newBases.Add(new PyType(this.Mixins.GetAttr("IterableMixin")));
}

// enumerators
if (interfaces.Contains(typeof(System.Collections.IEnumerator)))
{
newBases.Add(new PyType(this.Mixins.GetAttr("IteratorMixin")));
}

if (newBases.Count == existingBases.Count)
{
return existingBases;
}

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

return newBases;
}

static Type[] NewInterfaces(Type type)
{
var result = type.GetInterfaces();
return type.BaseType != null
? result.Except(type.BaseType.GetInterfaces()).ToArray()
: result;
}

static Type GetDefinition(Type type)
=> type.IsGenericType ? type.GetGenericTypeDefinition() : type;
}
}
82 changes: 82 additions & 0 deletions src/runtime/Mixins/collections.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""
Implements collections.abc for common .NET types
https://docs.python.org/3.6/library/collections.abc.html
"""

import collections.abc as col

class IteratorMixin(col.Iterator):
def close(self):
self.Dispose()

class IterableMixin(col.Iterable):
pass

class SizedMixin(col.Sized):
def __len__(self): return self.Count

class ContainerMixin(col.Container):
def __contains__(self, item): return self.Contains(item)

try:
abc_Collection = col.Collection
except AttributeError:
# Python 3.5- does not have collections.abc.Collection
abc_Collection = col.Container

class CollectionMixin(SizedMixin, IterableMixin, ContainerMixin, abc_Collection):
pass

class SequenceMixin(CollectionMixin, col.Sequence):
pass

class MutableSequenceMixin(SequenceMixin, col.MutableSequence):
pass

class MappingMixin(CollectionMixin, col.Mapping):
def __contains__(self, item): return self.ContainsKey(item)
def keys(self): return self.Keys
def items(self): return [(k,self.get(k)) for k in self.Keys]
def values(self): return self.Values
def __iter__(self): return self.Keys.__iter__()
def get(self, key, default=None):
existed, item = self.TryGetValue(key, None)
return item if existed else default

class MutableMappingMixin(MappingMixin, col.MutableMapping):
_UNSET_ = object()

def __delitem__(self, key):
self.Remove(key)

def clear(self):
self.Clear()

def pop(self, key, default=_UNSET_):
existed, item = self.TryGetValue(key, None)
if existed:
self.Remove(key)
return item
elif default == self._UNSET_:
raise KeyError(key)
else:
return default

def setdefault(self, key, value=None):
existed, item = self.TryGetValue(key, None)
if existed:
return item
else:
self[key] = value
return value

def update(self, items, **kwargs):
if isinstance(items, col.Mapping):
for key, value in items.items():
self[key] = value
else:
for key, value in items:
self[key] = value

for key, value in kwargs.items():
self[key] = value
2 changes: 1 addition & 1 deletion src/runtime/Python.Runtime.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@
</ItemGroup>

<ItemGroup>
<None Remove="resources\clr.py" />
<EmbeddedResource Include="resources\clr.py">
<LogicalName>clr.py</LogicalName>
</EmbeddedResource>
<EmbeddedResource Include="resources\interop.py">
<LogicalName>interop.py</LogicalName>
</EmbeddedResource>
<EmbeddedResource Include="Mixins\*.py" />
</ItemGroup>

<ItemGroup>
Expand Down
3 changes: 3 additions & 0 deletions src/runtime/Util.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ internal static class Util
internal const string MinimalPythonVersionRequired =
"Only Python 3.5 or newer is supported";

internal const string UseOverloadWithReferenceTypes =
"This API is unsafe, and will be removed in the future. Use overloads working with *Reference types";

internal static Int64 ReadCLong(IntPtr tp, int offset)
{
// On Windows, a C long is always 32 bits.
Expand Down
28 changes: 25 additions & 3 deletions src/runtime/classbase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -360,18 +360,40 @@ public static void tp_dealloc(IntPtr ob)

public static int tp_clear(IntPtr ob)
{
ManagedType self = GetManagedObject(ob);
if (GetManagedObject(ob) is { } self)
{
if (self.clearReentryGuard) return 0;

// workaround for https://bugs.python.org/issue45266
self.clearReentryGuard = true;

try
{
return ClearImpl(ob, self);
}
finally
{
self.clearReentryGuard = false;
}
}
else
{
return ClearImpl(ob, null);
}
}

static int ClearImpl(IntPtr ob, ManagedType self)
{
bool isTypeObject = Runtime.PyObject_TYPE(ob) == Runtime.PyCLRMetaType;
if (!isTypeObject)
{
ClearObjectDict(ob);

int baseClearResult = BaseUnmanagedClear(ob);
if (baseClearResult != 0)
{
return baseClearResult;
}

ClearObjectDict(ob);
}
return 0;
}
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/clrobject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ internal class CLRObject : ManagedType

internal CLRObject(object ob, IntPtr tp)
{
System.Diagnostics.Debug.Assert(tp != IntPtr.Zero);
Debug.Assert(tp != IntPtr.Zero);
IntPtr py = Runtime.PyType_GenericAlloc(tp, 0);

tpHandle = tp;
Expand Down
4 changes: 3 additions & 1 deletion src/runtime/managedtype.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ internal enum TrackTypes
internal IntPtr pyHandle; // PyObject *
internal IntPtr tpHandle; // PyType *

internal bool clearReentryGuard;

internal BorrowedReference ObjectReference
{
get
Expand Down Expand Up @@ -160,7 +162,7 @@ internal static bool IsInstanceOfManagedType(IntPtr ob)

internal static bool IsManagedType(BorrowedReference type)
{
var flags = (TypeFlags)Util.ReadCLong(type.DangerousGetAddress(), TypeOffset.tp_flags);
var flags = PyType.GetFlags(type);
return (flags & TypeFlags.HasClrInstance) != 0;
}

Expand Down
2 changes: 1 addition & 1 deletion src/runtime/pyscope.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ private PyScope(IntPtr ptr, PyScopeManager manager) : base(ptr)
PythonException.ThrowIfIsNull(variables);

int res = Runtime.PyDict_SetItem(
VarsRef, PyIdentifier.__builtins__,
VarsRef, new BorrowedReference(PyIdentifier.__builtins__),
Runtime.PyEval_GetBuiltins()
);
PythonException.ThrowIfIsNotZero(res);
Expand Down
18 changes: 14 additions & 4 deletions src/runtime/pythonengine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -224,10 +224,8 @@ public static void Initialize(IEnumerable<string> args, bool setSysArgv = true,
var locals = new PyDict();
try
{
BorrowedReference module = Runtime.PyImport_AddModule("clr._extras");
BorrowedReference module = DefineModule("clr._extras");
BorrowedReference module_globals = Runtime.PyModule_GetDict(module);
BorrowedReference builtins = Runtime.PyEval_GetBuiltins();
Runtime.PyDict_SetItemString(module_globals, "__builtins__", builtins);

Assembly assembly = Assembly.GetExecutingAssembly();
// add the contents of clr.py to the module
Expand All @@ -236,6 +234,8 @@ public static void Initialize(IEnumerable<string> args, bool setSysArgv = true,

LoadSubmodule(module_globals, "clr.interop", "interop.py");

LoadMixins(module_globals);

// add the imported module to the clr module, and copy the API functions
// and decorators into the main clr module.
Runtime.PyDict_SetItemString(clr_dict, "_extras", module);
Expand Down Expand Up @@ -281,6 +281,16 @@ static void LoadSubmodule(BorrowedReference targetModuleDict, string fullName, s
Runtime.PyDict_SetItemString(targetModuleDict, memberName, module);
}

static void LoadMixins(BorrowedReference targetModuleDict)
{
foreach (string nested in new[] { "collections" })
{
LoadSubmodule(targetModuleDict,
fullName: "clr._extras." + nested,
resourceName: typeof(PythonEngine).Namespace + ".Mixins." + nested + ".py");
}
}

static void OnDomainUnload(object _, EventArgs __)
{
Shutdown();
Expand Down Expand Up @@ -641,7 +651,7 @@ internal static PyObject RunString(string code, BorrowedReference globals, Borro
{
globals = tempGlobals = NewReference.DangerousFromPointer(Runtime.PyDict_New());
Runtime.PyDict_SetItem(
globals, PyIdentifier.__builtins__,
globals, new BorrowedReference(PyIdentifier.__builtins__),
Runtime.PyEval_GetBuiltins()
);
}
Expand Down
6 changes: 6 additions & 0 deletions src/runtime/pytype.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ internal IntPtr GetSlot(TypeSlotID slot)
return Exceptions.ErrorCheckIfNull(result);
}

internal static TypeFlags GetFlags(BorrowedReference type)
{
Debug.Assert(TypeOffset.tp_flags > 0);
return (TypeFlags)Util.ReadCLong(type.DangerousGetAddress(), TypeOffset.tp_flags);
}

internal static BorrowedReference GetBase(BorrowedReference type)
{
Debug.Assert(IsType(type));
Expand Down
3 changes: 2 additions & 1 deletion src/runtime/runtime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1689,6 +1689,7 @@ internal static BorrowedReference PyDict_GetItemString(BorrowedReference pointer
/// <summary>
/// Return 0 on success or -1 on failure.
/// </summary>
[Obsolete]
internal static int PyDict_SetItem(BorrowedReference dict, IntPtr key, BorrowedReference value) => Delegates.PyDict_SetItem(dict, new BorrowedReference(key), value);
/// <summary>
/// Return 0 on success or -1 on failure.
Expand Down Expand Up @@ -2052,7 +2053,7 @@ internal static bool PyType_IsSameAsOrSubtype(BorrowedReference type, BorrowedRe
internal static NewReference PyType_FromSpecWithBases(in NativeTypeSpec spec, BorrowedReference bases) => Delegates.PyType_FromSpecWithBases(in spec, bases);

/// <summary>
/// Finalize a type object. This should be called on all type objects to finish their initialization. This function is responsible for adding inherited slots from a types base class. Return 0 on success, or return -1 and sets an exception on error.
/// Finalize a type object. This should be called on all type objects to finish their initialization. This function is responsible for adding inherited slots from a types base class. Return 0 on success, or return -1 and sets an exception on error.
/// </summary>

internal static int PyType_Ready(IntPtr type) => Delegates.PyType_Ready(type);
Expand Down
16 changes: 16 additions & 0 deletions tests/test_collection_mixins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import System.Collections.Generic as C

def test_contains():
l = C.List[int]()
l.Add(42)
assert 42 in l
assert 43 not in l

def test_dict_items():
d = C.Dictionary[int, str]()
d[42] = "a"
items = d.items()
assert len(items) == 1
k,v = items[0]
assert k == 42
assert v == "a"