diff --git a/AUTHORS.md b/AUTHORS.md
index e42a456ae..26285bf6a 100644
--- a/AUTHORS.md
+++ b/AUTHORS.md
@@ -12,6 +12,7 @@
## Contributors
+- Alex Earl ([@slide](https://github.com/slide))
- Alex Helms ([@alexhelms](https://github.com/alexhelms))
- Alexandre Catarino([@AlexCatarino](https://github.com/AlexCatarino))
- Arvid JB ([@ArvidJB](https://github.com/ArvidJB))
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5c999d668..1fd2b1dcf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@ This document follows the conventions laid out in [Keep a CHANGELOG][].
- Added automatic NuGet package generation in appveyor and local builds
- Added function that sets Py_NoSiteFlag to 1.
- Added support for Jetson Nano.
+- Added support for __len__ for .NET classes that implement ICollection
### Changed
diff --git a/src/runtime/Python.Runtime.csproj b/src/runtime/Python.Runtime.csproj
index 02656e51e..0c2f912de 100644
--- a/src/runtime/Python.Runtime.csproj
+++ b/src/runtime/Python.Runtime.csproj
@@ -142,6 +142,7 @@
+
diff --git a/src/runtime/arrayobject.cs b/src/runtime/arrayobject.cs
index c37295704..1ef318473 100644
--- a/src/runtime/arrayobject.cs
+++ b/src/runtime/arrayobject.cs
@@ -244,16 +244,5 @@ public static int sq_contains(IntPtr ob, IntPtr v)
return 0;
}
-
-
- ///
- /// Implements __len__ for array types.
- ///
- public static int mp_length(IntPtr ob)
- {
- var self = (CLRObject)GetManagedObject(ob);
- var items = self.inst as Array;
- return items.Length;
- }
}
}
diff --git a/src/runtime/slots/mp_length.cs b/src/runtime/slots/mp_length.cs
new file mode 100644
index 000000000..b0a2e8d79
--- /dev/null
+++ b/src/runtime/slots/mp_length.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+
+namespace Python.Runtime.Slots
+{
+ internal static class mp_length_slot
+ {
+ ///
+ /// Implements __len__ for classes that implement ICollection
+ /// (this includes any IList implementer or Array subclass)
+ ///
+ public static int mp_length(IntPtr ob)
+ {
+ var co = ManagedType.GetManagedObject(ob) as CLRObject;
+ if (co == null)
+ {
+ Exceptions.RaiseTypeError("invalid object");
+ }
+
+ // first look for ICollection implementation directly
+ if (co.inst is ICollection c)
+ {
+ return c.Count;
+ }
+
+ Type clrType = co.inst.GetType();
+
+ // now look for things that implement ICollection directly (non-explicitly)
+ PropertyInfo p = clrType.GetProperty("Count");
+ if (p != null && clrType.GetInterfaces().Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(ICollection<>)))
+ {
+ return (int)p.GetValue(co.inst, null);
+ }
+
+ // finally look for things that implement the interface explicitly
+ var iface = clrType.GetInterfaces().FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(ICollection<>));
+ if (iface != null)
+ {
+ p = iface.GetProperty(nameof(ICollection.Count));
+ return (int)p.GetValue(co.inst, null);
+ }
+
+ Exceptions.SetError(Exceptions.TypeError, $"object of type '{clrType.Name}' has no len()");
+ return -1;
+ }
+ }
+}
diff --git a/src/runtime/typemanager.cs b/src/runtime/typemanager.cs
index 4427305e6..97e6032cd 100644
--- a/src/runtime/typemanager.cs
+++ b/src/runtime/typemanager.cs
@@ -1,9 +1,11 @@
using System;
using System.Collections;
using System.Collections.Generic;
+using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using Python.Runtime.Platform;
+using Python.Runtime.Slots;
namespace Python.Runtime
{
@@ -153,6 +155,13 @@ internal static IntPtr CreateType(ManagedType impl, Type clrType)
Marshal.WriteIntPtr(type, TypeOffset.tp_itemsize, IntPtr.Zero);
Marshal.WriteIntPtr(type, TypeOffset.tp_dictoffset, (IntPtr)tp_dictoffset);
+ // add a __len__ slot for inheritors of ICollection and ICollection<>
+ if (typeof(ICollection).IsAssignableFrom(clrType) || clrType.GetInterfaces().Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(ICollection<>)))
+ {
+ InitializeSlot(type, TypeOffset.mp_length, typeof(mp_length_slot).GetMethod(nameof(mp_length_slot.mp_length)));
+ }
+
+ // we want to do this after the slot stuff above in case the class itself implements a slot method
InitializeSlots(type, impl.GetType());
if (base_ != IntPtr.Zero)
@@ -193,6 +202,12 @@ internal static IntPtr CreateType(ManagedType impl, Type clrType)
return type;
}
+ static void InitializeSlot(IntPtr type, int slotOffset, MethodInfo method)
+ {
+ IntPtr thunk = Interop.GetThunk(method);
+ Marshal.WriteIntPtr(type, slotOffset, thunk);
+ }
+
internal static IntPtr CreateSubType(IntPtr py_name, IntPtr py_base_type, IntPtr py_dict)
{
// Utility to create a subtype of a managed type with the ability for the
diff --git a/src/testing/Python.Test.csproj b/src/testing/Python.Test.csproj
index 6bf5c2d22..515fd928c 100644
--- a/src/testing/Python.Test.csproj
+++ b/src/testing/Python.Test.csproj
@@ -92,6 +92,7 @@
+
diff --git a/src/testing/mp_lengthtest.cs b/src/testing/mp_lengthtest.cs
new file mode 100644
index 000000000..a4f3e8c25
--- /dev/null
+++ b/src/testing/mp_lengthtest.cs
@@ -0,0 +1,171 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+namespace Python.Test
+{
+ public class MpLengthCollectionTest : ICollection
+ {
+ private readonly List items;
+
+ public MpLengthCollectionTest()
+ {
+ SyncRoot = new object();
+ items = new List
+ {
+ 1,
+ 2,
+ 3
+ };
+ }
+
+ public int Count => items.Count;
+
+ public object SyncRoot { get; private set; }
+
+ public bool IsSynchronized => false;
+
+ public void CopyTo(Array array, int index)
+ {
+ throw new NotImplementedException();
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public class MpLengthExplicitCollectionTest : ICollection
+ {
+ private readonly List items;
+ private readonly object syncRoot;
+
+ public MpLengthExplicitCollectionTest()
+ {
+ syncRoot = new object();
+ items = new List
+ {
+ 9,
+ 10
+ };
+ }
+ int ICollection.Count => items.Count;
+
+ object ICollection.SyncRoot => syncRoot;
+
+ bool ICollection.IsSynchronized => false;
+
+ void ICollection.CopyTo(Array array, int index)
+ {
+ throw new NotImplementedException();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public class MpLengthGenericCollectionTest : ICollection
+ {
+ private readonly List items;
+
+ public MpLengthGenericCollectionTest() {
+ SyncRoot = new object();
+ items = new List();
+ }
+
+ public int Count => items.Count;
+
+ public object SyncRoot { get; private set; }
+
+ public bool IsSynchronized => false;
+
+ public bool IsReadOnly => false;
+
+ public void Add(T item)
+ {
+ items.Add(item);
+ }
+
+ public void Clear()
+ {
+ items.Clear();
+ }
+
+ public bool Contains(T item)
+ {
+ return items.Contains(item);
+ }
+
+ public void CopyTo(T[] array, int arrayIndex)
+ {
+ items.CopyTo(array, arrayIndex);
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ return ((IEnumerable)items).GetEnumerator();
+ }
+
+ public bool Remove(T item)
+ {
+ return items.Remove(item);
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return items.GetEnumerator();
+ }
+ }
+
+ public class MpLengthExplicitGenericCollectionTest : ICollection
+ {
+ private readonly List items;
+
+ public MpLengthExplicitGenericCollectionTest()
+ {
+ items = new List();
+ }
+
+ int ICollection.Count => items.Count;
+
+ bool ICollection.IsReadOnly => false;
+
+ public void Add(T item)
+ {
+ items.Add(item);
+ }
+
+ void ICollection.Clear()
+ {
+ items.Clear();
+ }
+
+ bool ICollection.Contains(T item)
+ {
+ return items.Contains(item);
+ }
+
+ void ICollection.CopyTo(T[] array, int arrayIndex)
+ {
+ items.CopyTo(array, arrayIndex);
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return items.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return ((IEnumerable)items).GetEnumerator();
+ }
+
+ bool ICollection.Remove(T item)
+ {
+ return items.Remove(item);
+ }
+ }
+}
diff --git a/src/tests/test_mp_length.py b/src/tests/test_mp_length.py
new file mode 100644
index 000000000..c96ac77d1
--- /dev/null
+++ b/src/tests/test_mp_length.py
@@ -0,0 +1,49 @@
+# -*- coding: utf-8 -*-
+
+"""Test __len__ for .NET classes implementing ICollection/ICollection."""
+
+import System
+import pytest
+from Python.Test import MpLengthCollectionTest, MpLengthExplicitCollectionTest, MpLengthGenericCollectionTest, MpLengthExplicitGenericCollectionTest
+
+def test_simple___len__():
+ """Test __len__ for simple ICollection implementers"""
+ import System
+ import System.Collections.Generic
+ l = System.Collections.Generic.List[int]()
+ assert len(l) == 0
+ l.Add(5)
+ l.Add(6)
+ assert len(l) == 2
+
+ d = System.Collections.Generic.Dictionary[int, int]()
+ assert len(d) == 0
+ d.Add(4, 5)
+ assert len(d) == 1
+
+ a = System.Array[int]([0,1,2,3])
+ assert len(a) == 4
+
+def test_custom_collection___len__():
+ """Test __len__ for custom collection class"""
+ s = MpLengthCollectionTest()
+ assert len(s) == 3
+
+def test_custom_collection_explicit___len__():
+ """Test __len__ for custom collection class that explicitly implements ICollection"""
+ s = MpLengthExplicitCollectionTest()
+ assert len(s) == 2
+
+def test_custom_generic_collection___len__():
+ """Test __len__ for custom generic collection class"""
+ s = MpLengthGenericCollectionTest[int]()
+ s.Add(1)
+ s.Add(2)
+ assert len(s) == 2
+
+def test_custom_generic_collection_explicit___len__():
+ """Test __len__ for custom generic collection that explicity implements ICollection"""
+ s = MpLengthExplicitGenericCollectionTest[int]()
+ s.Add(1)
+ s.Add(10)
+ assert len(s) == 2