Skip to content

Commit 12defa7

Browse files
authored
Merge pull request #1543 from losttech/standard-mixins
Mixins for standard .NET collections that implement `collections.abc` interfaces
2 parents 2b0e322 + 6ae373c commit 12defa7

14 files changed

+249
-12
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ This document follows the conventions laid out in [Keep a CHANGELOG][].
1717
- Improved exception handling:
1818
- exceptions can now be converted with codecs
1919
- `InnerException` and `__cause__` are propagated properly
20+
- .NET collection types now implement standard Python collection interfaces from `collections.abc`.
21+
See [Mixins/collections.py](src/runtime/Mixins/collections.py).
2022
- .NET arrays implement Python buffer protocol
2123

2224

src/runtime/InteropConfiguration.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ namespace Python.Runtime
33
using System;
44
using System.Collections.Generic;
55

6+
using Python.Runtime.Mixins;
7+
68
public sealed class InteropConfiguration
79
{
810
internal readonly PythonBaseTypeProviderGroup pythonBaseTypeProviders
@@ -18,6 +20,7 @@ public static InteropConfiguration MakeDefault()
1820
PythonBaseTypeProviders =
1921
{
2022
DefaultBaseTypeProvider.Instance,
23+
new CollectionMixinsProvider(new Lazy<PyObject>(() => Py.Import("clr._extras.collections"))),
2124
},
2225
};
2326
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
5+
namespace Python.Runtime.Mixins
6+
{
7+
class CollectionMixinsProvider : IPythonBaseTypeProvider
8+
{
9+
readonly Lazy<PyObject> mixinsModule;
10+
public CollectionMixinsProvider(Lazy<PyObject> mixinsModule)
11+
{
12+
this.mixinsModule = mixinsModule ?? throw new ArgumentNullException(nameof(mixinsModule));
13+
}
14+
15+
public PyObject Mixins => this.mixinsModule.Value;
16+
17+
public IEnumerable<PyType> GetBaseTypes(Type type, IList<PyType> existingBases)
18+
{
19+
if (type is null)
20+
throw new ArgumentNullException(nameof(type));
21+
22+
if (existingBases is null)
23+
throw new ArgumentNullException(nameof(existingBases));
24+
25+
var interfaces = NewInterfaces(type).Select(GetDefinition).ToArray();
26+
27+
var newBases = new List<PyType>();
28+
newBases.AddRange(existingBases);
29+
30+
// dictionaries
31+
if (interfaces.Contains(typeof(IDictionary<,>)))
32+
{
33+
newBases.Add(new PyType(this.Mixins.GetAttr("MutableMappingMixin")));
34+
}
35+
else if (interfaces.Contains(typeof(IReadOnlyDictionary<,>)))
36+
{
37+
newBases.Add(new PyType(this.Mixins.GetAttr("MappingMixin")));
38+
}
39+
40+
// item collections
41+
if (interfaces.Contains(typeof(IList<>))
42+
|| interfaces.Contains(typeof(System.Collections.IList)))
43+
{
44+
newBases.Add(new PyType(this.Mixins.GetAttr("MutableSequenceMixin")));
45+
}
46+
else if (interfaces.Contains(typeof(IReadOnlyList<>)))
47+
{
48+
newBases.Add(new PyType(this.Mixins.GetAttr("SequenceMixin")));
49+
}
50+
else if (interfaces.Contains(typeof(ICollection<>))
51+
|| interfaces.Contains(typeof(System.Collections.ICollection)))
52+
{
53+
newBases.Add(new PyType(this.Mixins.GetAttr("CollectionMixin")));
54+
}
55+
else if (interfaces.Contains(typeof(System.Collections.IEnumerable)))
56+
{
57+
newBases.Add(new PyType(this.Mixins.GetAttr("IterableMixin")));
58+
}
59+
60+
// enumerators
61+
if (interfaces.Contains(typeof(System.Collections.IEnumerator)))
62+
{
63+
newBases.Add(new PyType(this.Mixins.GetAttr("IteratorMixin")));
64+
}
65+
66+
if (newBases.Count == existingBases.Count)
67+
{
68+
return existingBases;
69+
}
70+
71+
if (type.IsInterface && type.BaseType is null)
72+
{
73+
newBases.RemoveAll(@base => @base.Handle == Runtime.PyBaseObjectType);
74+
}
75+
76+
return newBases;
77+
}
78+
79+
static Type[] NewInterfaces(Type type)
80+
{
81+
var result = type.GetInterfaces();
82+
return type.BaseType != null
83+
? result.Except(type.BaseType.GetInterfaces()).ToArray()
84+
: result;
85+
}
86+
87+
static Type GetDefinition(Type type)
88+
=> type.IsGenericType ? type.GetGenericTypeDefinition() : type;
89+
}
90+
}

src/runtime/Mixins/collections.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
"""
2+
Implements collections.abc for common .NET types
3+
https://docs.python.org/3.6/library/collections.abc.html
4+
"""
5+
6+
import collections.abc as col
7+
8+
class IteratorMixin(col.Iterator):
9+
def close(self):
10+
self.Dispose()
11+
12+
class IterableMixin(col.Iterable):
13+
pass
14+
15+
class SizedMixin(col.Sized):
16+
def __len__(self): return self.Count
17+
18+
class ContainerMixin(col.Container):
19+
def __contains__(self, item): return self.Contains(item)
20+
21+
try:
22+
abc_Collection = col.Collection
23+
except AttributeError:
24+
# Python 3.5- does not have collections.abc.Collection
25+
abc_Collection = col.Container
26+
27+
class CollectionMixin(SizedMixin, IterableMixin, ContainerMixin, abc_Collection):
28+
pass
29+
30+
class SequenceMixin(CollectionMixin, col.Sequence):
31+
pass
32+
33+
class MutableSequenceMixin(SequenceMixin, col.MutableSequence):
34+
pass
35+
36+
class MappingMixin(CollectionMixin, col.Mapping):
37+
def __contains__(self, item): return self.ContainsKey(item)
38+
def keys(self): return self.Keys
39+
def items(self): return [(k,self.get(k)) for k in self.Keys]
40+
def values(self): return self.Values
41+
def __iter__(self): return self.Keys.__iter__()
42+
def get(self, key, default=None):
43+
existed, item = self.TryGetValue(key, None)
44+
return item if existed else default
45+
46+
class MutableMappingMixin(MappingMixin, col.MutableMapping):
47+
_UNSET_ = object()
48+
49+
def __delitem__(self, key):
50+
self.Remove(key)
51+
52+
def clear(self):
53+
self.Clear()
54+
55+
def pop(self, key, default=_UNSET_):
56+
existed, item = self.TryGetValue(key, None)
57+
if existed:
58+
self.Remove(key)
59+
return item
60+
elif default == self._UNSET_:
61+
raise KeyError(key)
62+
else:
63+
return default
64+
65+
def setdefault(self, key, value=None):
66+
existed, item = self.TryGetValue(key, None)
67+
if existed:
68+
return item
69+
else:
70+
self[key] = value
71+
return value
72+
73+
def update(self, items, **kwargs):
74+
if isinstance(items, col.Mapping):
75+
for key, value in items.items():
76+
self[key] = value
77+
else:
78+
for key, value in items:
79+
self[key] = value
80+
81+
for key, value in kwargs.items():
82+
self[key] = value

src/runtime/Python.Runtime.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,13 @@
3939
</ItemGroup>
4040

4141
<ItemGroup>
42-
<None Remove="resources\clr.py" />
4342
<EmbeddedResource Include="resources\clr.py">
4443
<LogicalName>clr.py</LogicalName>
4544
</EmbeddedResource>
4645
<EmbeddedResource Include="resources\interop.py">
4746
<LogicalName>interop.py</LogicalName>
4847
</EmbeddedResource>
48+
<EmbeddedResource Include="Mixins\*.py" />
4949
</ItemGroup>
5050

5151
<ItemGroup>

src/runtime/Util.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ internal static class Util
1212
internal const string MinimalPythonVersionRequired =
1313
"Only Python 3.5 or newer is supported";
1414

15+
internal const string UseOverloadWithReferenceTypes =
16+
"This API is unsafe, and will be removed in the future. Use overloads working with *Reference types";
17+
1518
internal static Int64 ReadCLong(IntPtr tp, int offset)
1619
{
1720
// On Windows, a C long is always 32 bits.

src/runtime/classbase.cs

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -360,18 +360,40 @@ public static void tp_dealloc(IntPtr ob)
360360

361361
public static int tp_clear(IntPtr ob)
362362
{
363-
ManagedType self = GetManagedObject(ob);
363+
if (GetManagedObject(ob) is { } self)
364+
{
365+
if (self.clearReentryGuard) return 0;
366+
367+
// workaround for https://bugs.python.org/issue45266
368+
self.clearReentryGuard = true;
369+
370+
try
371+
{
372+
return ClearImpl(ob, self);
373+
}
374+
finally
375+
{
376+
self.clearReentryGuard = false;
377+
}
378+
}
379+
else
380+
{
381+
return ClearImpl(ob, null);
382+
}
383+
}
364384

385+
static int ClearImpl(IntPtr ob, ManagedType self)
386+
{
365387
bool isTypeObject = Runtime.PyObject_TYPE(ob) == Runtime.PyCLRMetaType;
366388
if (!isTypeObject)
367389
{
368-
ClearObjectDict(ob);
369-
370390
int baseClearResult = BaseUnmanagedClear(ob);
371391
if (baseClearResult != 0)
372392
{
373393
return baseClearResult;
374394
}
395+
396+
ClearObjectDict(ob);
375397
}
376398
return 0;
377399
}

src/runtime/clrobject.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ internal class CLRObject : ManagedType
1212

1313
internal CLRObject(object ob, IntPtr tp)
1414
{
15-
System.Diagnostics.Debug.Assert(tp != IntPtr.Zero);
15+
Debug.Assert(tp != IntPtr.Zero);
1616
IntPtr py = Runtime.PyType_GenericAlloc(tp, 0);
1717

1818
tpHandle = tp;

src/runtime/managedtype.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ internal enum TrackTypes
2828
internal IntPtr pyHandle; // PyObject *
2929
internal IntPtr tpHandle; // PyType *
3030

31+
internal bool clearReentryGuard;
32+
3133
internal BorrowedReference ObjectReference
3234
{
3335
get
@@ -160,7 +162,7 @@ internal static bool IsInstanceOfManagedType(IntPtr ob)
160162

161163
internal static bool IsManagedType(BorrowedReference type)
162164
{
163-
var flags = (TypeFlags)Util.ReadCLong(type.DangerousGetAddress(), TypeOffset.tp_flags);
165+
var flags = PyType.GetFlags(type);
164166
return (flags & TypeFlags.HasClrInstance) != 0;
165167
}
166168

src/runtime/pyscope.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ private PyScope(IntPtr ptr, PyScopeManager manager) : base(ptr)
6666
PythonException.ThrowIfIsNull(variables);
6767

6868
int res = Runtime.PyDict_SetItem(
69-
VarsRef, PyIdentifier.__builtins__,
69+
VarsRef, new BorrowedReference(PyIdentifier.__builtins__),
7070
Runtime.PyEval_GetBuiltins()
7171
);
7272
PythonException.ThrowIfIsNotZero(res);

0 commit comments

Comments
 (0)