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