8000 Add mp_length slot for .NET classes implementing ICollection/ICollection<T> by slide · Pull Request #994 · pythonnet/pythonnet · GitHub
[go: up one dir, main page]

Skip to content

Add mp_length slot for .NET classes implementing ICollection/ICollection<T> #994

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 10 commits into from
Dec 16, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions src/runtime/Python.Runtime.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@
<Compile Include="Util.cs" />
<Compile Include="platform\Types.cs" />
<Compile Include="platform\LibraryLoader.cs" />
<Compile Include="slots\mp_length.cs" />
</ItemGroup>
<ItemGroup Condition=" '$(PythonInteropFile)' != '' ">
<Compile Include="$(PythonInteropFile)" />
Expand Down
11 changes: 0 additions & 11 deletions src/runtime/arrayobject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -244,16 +244,5 @@ public static int sq_contains(IntPtr ob, IntPtr v)

return 0;
}


/// <summary>
/// Implements __len__ for array types.
/// </summary>
public static int mp_length(IntPtr ob)
{
var self = (CLRObject)GetManagedObject(ob);
var items = self.inst as Array;
return items.Length;
}
}
}
50 changes: 50 additions & 0 deletions src/runtime/slots/mp_length.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Implements __len__ for classes that implement ICollection
/// (this includes any IList implementer or Array subclass)
/// </summary>
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<T> directly (non-explicitly)
PropertyInfo p = clrType.GetProperty("Count");
if (p != null && clrType.GetInterfaces().Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(ICollection<>)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Too many reflections every times, you can try like this.

    class Program
    {
        interface ICountGetter
        {
            int GetCount(object obj);
        }

        class CountGetter<Cls, T> : ICountGetter
        {
            private Func<Cls, int> _getter;

            public CountGetter(Func<Cls, int> getter)
            {
                _getter = getter;
            }

            public int GetCount(object obj)
            {
                return _getter((Cls)obj);
            }
        }

        static void Main(string[] args)
        {
            var obj = new MyClass();
            var type = obj.GetType();
            var declType = type.GetInterfaces().FirstOrDefault(
                t => t.IsGenericType
                && t.GetGenericTypeDefinition() == typeof(ICollection<>));
            if (declType == null)
            {
                // set error and return
            }
            var pi = declType.GetProperty("Count");
            var elemType = declType.GenericTypeArguments[0];
            var getterType = typeof(CountGetter<,>).MakeGenericType(declType, elemType);
            var getterFuncType = typeof(Func<,>).MakeGenericType(declType, typeof(int));
            var getterFunc = Delegate.CreateDelegate(getterFuncType, pi.GetMethod);
            var getter = (ICountGetter)Activator.CreateInstance(getterType, getterFunc);
            // You should cache the ([decltype] = getter), it makes you no reflection costs when you call GetCount next time.
            int count = getter.GetCount(obj);
            Console.WriteLine(count);
        }
    }

Copy link
Member
@lostmsu lostmsu Nov 21, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, but I feel like it would be good to have feature + tests first. It can be optimized later.

{
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<int>.Count));
return (int)p.GetValue(co.inst, null);
}

Exceptions.SetError(Exceptions.TypeError, $"object of type '{clrType.Name}' has no len()");
return -1;
}
}
}
15 changes: 15 additions & 0 deletions src/runtime/typemanager.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/testing/Python.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
<Compile Include="doctest.cs" />
<Compile Include="subclasstest.cs" />
<Compile Include="ReprTest.cs" />
<Compile Include="mp_lengthtest.cs" />
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.CSharp" />
Expand Down
171 changes: 171 additions & 0 deletions src/testing/mp_lengthtest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
using System;
using System.Collections;
using System.Collections.Generic;

namespace Python.Test
{
public class MpLengthCollectionTest : ICollection
{
private readonly List<int> items;

public MpLengthCollectionTest()
{
SyncRoot = new object();
items = new List<int>
{
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<int> items;
private readonly object syncRoot;

public MpLengthExplicitCollectionTest()
{
syncRoot = new object();
items = new List<int>
{
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<T> : ICollection<T>
{
private readonly List<T> items;

public MpLengthGenericCollectionTest() {
SyncRoot = new object();
items = new List<T>();
}

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<T> IEnumerable<T>.GetEnumerator()
{
return items.GetEnumerator();
}
}

public class MpLengthExplicitGenericCollectionTest<T> : ICollection<T>
{
private readonly List<T> items;

public MpLengthExplicitGenericCollectionTest()
{
items = new List<T>();
}

int ICollection<T>.Count => items.Count;

bool ICollection<T>.IsReadOnly => false;

public void Add(T item)
{
items.Add(item);
}

void ICollection<T>.Clear()
{
items.Clear();
}

bool ICollection<T>.Contains(T item)
{
return items.Contains(item);
}

void ICollection<T>.CopyTo(T[] array, int arrayIndex)
{
items.CopyTo(array, arrayIndex);
}

IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
return items.GetEnumerator();
}

IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable)items).GetEnumerator();
}

bool ICollection<T>.Remove(T item)
{
return items.Remove(item);
}
}
}
49 changes: 49 additions & 0 deletions src/tests/test_mp_length.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-

"""Test __len__ for .NET classes implementing ICollection/ICollection<T>."""

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<T>"""
s = MpLengthExplicitGenericCollectionTest[int]()
s.Add(1)
s.Add(10)
assert len(s) == 2
0