From 39774f506e3a5a7d5c94cc83c2d18f8a94e036d8 Mon Sep 17 00:00:00 2001 From: AlexCatarino Date: Fri, 27 Mar 2020 23:59:32 +0000 Subject: [PATCH 1/4] Version Bump From 1.0.5.29 to 1.0.5.30 --- .bumpversion.cfg | 2 +- conda.recipe/meta.yaml | 2 +- setup.py | 2 +- src/SharedAssemblyInfo.cs | 2 +- src/clrmodule/ClrModule.cs | 2 +- src/runtime/resources/clr.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index f0a2abed2..01b9ae991 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.0.5.29 +current_version = 1.0.5.30 parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\.(?P[a-z]+)(?P\d+))? serialize = {major}.{minor}.{patch}.{release}{dev} diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml index 61f336080..e2c407435 100644 --- a/conda.recipe/meta.yaml +++ b/conda.recipe/meta.yaml @@ -1,6 +1,6 @@ package: name: pythonnet - version: "1.0.5.29" + version: "1.0.5.30" build: skip: True # [not win] diff --git a/setup.py b/setup.py index 512771fa9..8e72acc96 100644 --- a/setup.py +++ b/setup.py @@ -485,7 +485,7 @@ def run(self): setup( name="pythonnet", - version="1.0.5.29", + version="1.0.5.30", description=".Net and Mono integration for Python", url='https://pythonnet.github.io/', license='MIT', diff --git a/src/SharedAssemblyInfo.cs b/src/SharedAssemblyInfo.cs index a8f3149df..3d2192f56 100644 --- a/src/SharedAssemblyInfo.cs +++ b/src/SharedAssemblyInfo.cs @@ -25,4 +25,4 @@ // Version Information. Keeping it simple. May need to revisit for Nuget // See: https://codingforsmarties.wordpress.com/2016/01/21/how-to-version-assemblies-destined-for-nuget/ // AssemblyVersion can only be numeric -[assembly: AssemblyVersion("1.0.5.29")] +[assembly: AssemblyVersion("1.0.5.30")] diff --git a/src/clrmodule/ClrModule.cs b/src/clrmodule/ClrModule.cs index 6387df7e5..62ec1ad42 100644 --- a/src/clrmodule/ClrModule.cs +++ b/src/clrmodule/ClrModule.cs @@ -53,7 +53,7 @@ public static void initclr() { #if USE_PYTHON_RUNTIME_VERSION // Has no effect until SNK works. Keep updated anyways. - Version = new Version("1.0.5.29"), + Version = new Version("1.0.5.30"), #endif CultureInfo = CultureInfo.InvariantCulture }; diff --git a/src/runtime/resources/clr.py b/src/runtime/resources/clr.py index 01d0d409b..3e35b13ab 100644 --- a/src/runtime/resources/clr.py +++ b/src/runtime/resources/clr.py @@ -2,7 +2,7 @@ Code in this module gets loaded into the main clr module. """ -__version__ = "1.0.5.29" +__version__ = "1.0.5.30" class clrproperty(object): From 755d867cb44e8222b784afe8082e939487efe858 Mon Sep 17 00:00:00 2001 From: AlexCatarino Date: Sat, 28 Mar 2020 00:01:53 +0000 Subject: [PATCH 2/4] Implements DictionaryObject Enables `__contains__` and `__len`__ for CLR classes that implements a dictionary. --- src/runtime/Python.Runtime.csproj | 1 + src/runtime/classmanager.cs | 5 + src/runtime/dictionaryobject.cs | 141 ++++++++++++++++++++++++++++ src/testing/Python.Test.csproj | 1 + src/testing/dictionarytest.cs | 106 +++++++++++++++++++++ src/tests/test_dictionary.py | 148 ++++++++++++++++++++++++++++++ 6 files changed, 402 insertions(+) create mode 100644 src/runtime/dictionaryobject.cs create mode 100644 src/testing/dictionarytest.cs create mode 100644 src/tests/test_dictionary.py diff --git a/src/runtime/Python.Runtime.csproj b/src/runtime/Python.Runtime.csproj index 1db48d28c..d1bdccf07 100644 --- a/src/runtime/Python.Runtime.csproj +++ b/src/runtime/Python.Runtime.csproj @@ -76,6 +76,7 @@ + diff --git a/src/runtime/classmanager.cs b/src/runtime/classmanager.cs index 83495cec4..5aff82d02 100644 --- a/src/runtime/classmanager.cs +++ b/src/runtime/classmanager.cs @@ -87,6 +87,11 @@ private static ClassBase CreateClass(Type type) impl = new ArrayObject(type); } + else if (type.IsDictionary()) + { + impl = new DictionaryObject(type); + } + else if (type.IsInterface) { impl = new InterfaceObject(type); diff --git a/src/runtime/dictionaryobject.cs b/src/runtime/dictionaryobject.cs new file mode 100644 index 000000000..8338578c4 --- /dev/null +++ b/src/runtime/dictionaryobject.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Python.Runtime +{ + /// + /// Implements a Python type for managed dictionaries. This type is essentially + /// the same as a ClassObject, except that it provides sequence semantics + /// to support natural dictionary usage (__contains__ and __len__) from Python. + /// + internal class DictionaryObject : ClassObject + { + private static Dictionary, MethodInfo> methodsByType = new Dictionary, MethodInfo>(); + private static Dictionary methodMap = new Dictionary + { + { "mp_length", "Count" }, + { "sq_contains", "ContainsKey" } + }; + + public List MappedMethods { get; } = new List(); + + internal DictionaryObject(Type tp) : base(tp) + { + if (!tp.IsDictionary()) + { + throw new ArgumentException("object is not a dict"); + } + + foreach (var name in methodMap) + { + var key = Tuple.Create(type, name.Value); + MethodInfo method; + if (!methodsByType.TryGetValue(key, out method)) + { + method = tp.GetMethod(name.Value); + if (method == null) + { + method = tp.GetMethod($"get_{name.Value}"); + } + if (method == null) + { + continue; + } + methodsByType.Add(key, method); + } + + MappedMethods.Add(name.Key); + } + } + + internal override bool CanSubclass() => false; + + /// + /// Implements __len__ for dictionary types. + /// + public static int mp_length(IntPtr ob) + { + var obj = (CLRObject)GetManagedObject(ob); + var self = obj.inst; + + MethodInfo methodInfo; + if (!TryGetMethodInfo(self.GetType(), "Count", out methodInfo)) + { + return 0; + } + + return (int)methodInfo.Invoke(self, null); + } + + /// + /// Implements __contains__ for dictionary types. + /// + public static int sq_contains(IntPtr ob, IntPtr v) + { + var obj = (CLRObject)GetManagedObject(ob); + var self = obj.inst; + + MethodInfo methodInfo; + if (!TryGetMethodInfo(self.GetType(), "ContainsKey", out methodInfo)) + { + return 0; + } + + var parameters = methodInfo.GetParameters(); + object arg; + if (!Converter.ToManaged(v, parameters[0].ParameterType, out arg, false)) + { + Exceptions.SetError(Exceptions.TypeError, + $"invalid parameter type for sq_contains: should be {Converter.GetTypeByAlias(v)}, found {parameters[0].ParameterType}"); + } + + return (bool)methodInfo.Invoke(self, new[] { arg }) ? 1 : 0; + } + + private static bool TryGetMethodInfo(Type type, string alias, out MethodInfo methodInfo) + { + var key = Tuple.Create(type, alias); + + if (!methodsByType.TryGetValue(key, out methodInfo)) + { + Exceptions.SetError(Exceptions.TypeError, + $"{nameof(type)} does not define {alias} method"); + + return false; + } + + return true; + } + } + + public static class DictionaryObjectExtension + { + public static bool IsDictionary(this Type type) + { + var iEnumerableType = typeof(IEnumerable<>); + var keyValuePairType = typeof(KeyValuePair<,>); + + var interfaces = type.GetInterfaces(); + foreach (var i in interfaces) + { + if (i.IsGenericType && + i.GetGenericTypeDefinition() == iEnumerableType) + { + var arguments = i.GetGenericArguments(); + if (arguments.Length != 1) continue; + + var a = arguments[0]; + if (a.IsGenericType && + a.GetGenericTypeDefinition() == keyValuePairType && + a.GetGenericArguments().Length == 2) + { + return true; + } + } + } + + return false; + } + } +} diff --git a/src/testing/Python.Test.csproj b/src/testing/Python.Test.csproj index ff9c247fa..434da99e1 100644 --- a/src/testing/Python.Test.csproj +++ b/src/testing/Python.Test.csproj @@ -70,6 +70,7 @@ pdbonly + diff --git a/src/testing/dictionarytest.cs b/src/testing/dictionarytest.cs new file mode 100644 index 000000000..a7fa3497d --- /dev/null +++ b/src/testing/dictionarytest.cs @@ -0,0 +1,106 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Python.Test +{ + /// + /// Supports units tests for dictionary __contains__ and __len__ + /// + public class PublicDictionaryTest + { + public IDictionary items; + + public PublicDictionaryTest() + { + items = new int[5] { 0, 1, 2, 3, 4 } + .ToDictionary(k => k.ToString(), v => v); + } + } + + + public class ProtectedDictionaryTest + { + protected IDictionary items; + + public ProtectedDictionaryTest() + { + items = new int[5] { 0, 1, 2, 3, 4 } + .ToDictionary(k => k.ToString(), v => v); + } + } + + + public class InternalDictionaryTest + { + internal IDictionary items; + + public InternalDictionaryTest() + { + items = new int[5] { 0, 1, 2, 3, 4 } + .ToDictionary(k => k.ToString(), v => v); + } + } + + + public class PrivateDictionaryTest + { + private IDictionary items; + + public PrivateDictionaryTest() + { + items = new int[5] { 0, 1, 2, 3, 4 } + .ToDictionary(k => k.ToString(), v => v); + } + } + + public class InheritedDictionaryTest : IDictionary + { + private readonly IDictionary items; + + public InheritedDictionaryTest() + { + items = new int[5] { 0, 1, 2, 3, 4 } + .ToDictionary(k => k.ToString(), v => v); + } + + public int this[string key] + { + get { return items[key]; } + set { items[key] = value; } + } + + public ICollection Keys => items.Keys; + + public ICollection Values => items.Values; + + public int Count => items.Count; + + public bool IsReadOnly => false; + + public void Add(string key, int value) => items.Add(key, value); + + public void Add(KeyValuePair item) => items.Add(item); + + public void Clear() => items.Clear(); + + public bool Contains(KeyValuePair item) => items.Contains(item); + + public bool ContainsKey(string key) => items.ContainsKey(key); + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + items.CopyTo(array, arrayIndex); + } + + public IEnumerator> GetEnumerator() => items.GetEnumerator(); + + public bool Remove(string key) => items.Remove(key); + + public bool Remove(KeyValuePair item) => items.Remove(item); + + public bool TryGetValue(string key, out int value) => items.TryGetValue(key, out value); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/src/tests/test_dictionary.py b/src/tests/test_dictionary.py new file mode 100644 index 000000000..98c3cfb20 --- /dev/null +++ b/src/tests/test_dictionary.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- + +"""Test support for managed dictionaries.""" + +import Python.Test as Test +import System +import pytest + +from ._compat import PY2, UserList, long, range, unichr + + +def test_public_dict(): + """Test public dict.""" + ob = Test.PublicDictionaryTest() + items = ob.items + + assert len(items) == 5 + + assert items['0'] == 0 + assert items['4'] == 4 + + items['0'] = 8 + assert items['0'] == 8 + + items['4'] = 9 + assert items['4'] == 9 + + items['-4'] = 0 + assert items['-4'] == 0 + + items['-1'] = 4 + assert items['-1'] == 4 + +def test_protected_dict(): + """Test protected dict.""" + ob = Test.ProtectedDictionaryTest() + items = ob.items + + assert len(items) == 5 + + assert items['0'] == 0 + assert items['4'] == 4 + + items['0'] = 8 + assert items['0'] == 8 + + items['4'] = 9 + assert items['4'] == 9 + + items['-4'] = 0 + assert items['-4'] == 0 + + items['-1'] = 4 + assert items['-1'] == 4 + +def test_internal_dict(): + """Test internal dict.""" + + with pytest.raises(AttributeError): + ob = Test.InternalDictionaryTest() + _ = ob.items + +def test_private_dict(): + """Test private dict.""" + + with pytest.raises(AttributeError): + ob = Test.PrivateDictionaryTest() + _ = ob.items + +def test_dict_contains(): + """Test dict support for __contains__.""" + + ob = Test.PublicDictionaryTest() + items = ob.items + + assert '0' in items + assert '1' in items + assert '2' in items + assert '3' in items + assert '4' in items + + assert not ('5' in items) + assert not ('-1' in items) + +def test_dict_abuse(): + """Test dict abuse.""" + _class = Test.PublicDictionaryTest + ob = Test.PublicDictionaryTest() + + with pytest.raises(AttributeError): + del _class.__getitem__ + + with pytest.raises(AttributeError): + del ob.__getitem__ + + with pytest.raises(AttributeError): + del _class.__setitem__ + + with pytest.raises(AttributeError): + del ob.__setitem__ + + with pytest.raises(TypeError): + Test.PublicArrayTest.__getitem__(0, 0) + + with pytest.raises(TypeError): + Test.PublicArrayTest.__setitem__(0, 0, 0) + + with pytest.raises(TypeError): + desc = Test.PublicArrayTest.__dict__['__getitem__'] + desc(0, 0) + + with pytest.raises(TypeError): + desc = Test.PublicArrayTest.__dict__['__setitem__'] + desc(0, 0, 0) + +def test_InheritedDictionary(): + """Test class that inherited from IDictionary.""" + items = Test.InheritedDictionaryTest() + + assert len(items) == 5 + + assert items['0'] == 0 + assert items['4'] == 4 + + items['0'] = 8 + assert items['0'] == 8 + + items['4'] = 9 + assert items['4'] == 9 + + items['-4'] = 0 + assert items['-4'] == 0 + + items['-1'] = 4 + assert items['-1'] == 4 + +def test_InheritedDictionary_contains(): + """Test dict support for __contains__ in class that inherited from IDictionary""" + items = Test.InheritedDictionaryTest() + + assert '0' in items + assert '1' in items + assert '2' in items + assert '3' in items + assert '4' in items + + assert not ('5' in items) + assert not ('-1' in items) \ No newline at end of file From 074d01e132fd2a4c3651e5e986bd15eca46234fb Mon Sep 17 00:00:00 2001 From: AlexCatarino Date: Tue, 31 Mar 2020 00:54:40 +0100 Subject: [PATCH 3/4] Changes DictionaryObject to KeyValuePairEnumerableObject It is more generical. Verifies the existence of `Count` and `ContainsKey` that are necessary for `__len__` and `__contains__`. --- src/runtime/Python.Runtime.csproj | 2 +- src/runtime/classmanager.cs | 4 +- ...ect.cs => keyvaluepairenumerableobject.cs} | 62 +++++++++---------- 3 files changed, 31 insertions(+), 37 deletions(-) rename src/runtime/{dictionaryobject.cs => keyvaluepairenumerableobject.cs} (68%) diff --git a/src/runtime/Python.Runtime.csproj b/src/runtime/Python.Runtime.csproj index d1bdccf07..3ef2ad471 100644 --- a/src/runtime/Python.Runtime.csproj +++ b/src/runtime/Python.Runtime.csproj @@ -76,7 +76,7 @@ - + diff --git a/src/runtime/classmanager.cs b/src/runtime/classmanager.cs index 5aff82d02..837cdd16e 100644 --- a/src/runtime/classmanager.cs +++ b/src/runtime/classmanager.cs @@ -87,9 +87,9 @@ private static ClassBase CreateClass(Type type) impl = new ArrayObject(type); } - else if (type.IsDictionary()) + else if (type.IsKeyValuePairEnumerable()) { - impl = new DictionaryObject(type); + impl = new KeyValuePairEnumerableObject(type); } else if (type.IsInterface) diff --git a/src/runtime/dictionaryobject.cs b/src/runtime/keyvaluepairenumerableobject.cs similarity index 68% rename from src/runtime/dictionaryobject.cs rename to src/runtime/keyvaluepairenumerableobject.cs index 8338578c4..3f7afc852 100644 --- a/src/runtime/dictionaryobject.cs +++ b/src/runtime/keyvaluepairenumerableobject.cs @@ -5,11 +5,12 @@ namespace Python.Runtime { /// - /// Implements a Python type for managed dictionaries. This type is essentially - /// the same as a ClassObject, except that it provides sequence semantics - /// to support natural dictionary usage (__contains__ and __len__) from Python. + /// Implements a Python type for managed KeyValuePairEnumerable (dictionaries). + /// This type is essentially the same as a ClassObject, except that it provides + /// sequence semantics to support natural dictionary usage (__contains__ and __len__) + /// from Python. /// - internal class DictionaryObject : ClassObject + internal class KeyValuePairEnumerableObject : ClassObject { private static Dictionary, MethodInfo> methodsByType = new Dictionary, MethodInfo>(); private static Dictionary methodMap = new Dictionary @@ -20,11 +21,11 @@ internal class DictionaryObject : ClassObject public List MappedMethods { get; } = new List(); - internal DictionaryObject(Type tp) : base(tp) + internal KeyValuePairEnumerableObject(Type tp) : base(tp) { - if (!tp.IsDictionary()) + if (!tp.IsKeyValuePairEnumerable()) { - throw new ArgumentException("object is not a dict"); + throw new ArgumentException("object is not a KeyValuePair Enumerable"); } foreach (var name in methodMap) @@ -59,11 +60,8 @@ public static int mp_length(IntPtr ob) var obj = (CLRObject)GetManagedObject(ob); var self = obj.inst; - MethodInfo methodInfo; - if (!TryGetMethodInfo(self.GetType(), "Count", out methodInfo)) - { - return 0; - } + var key = Tuple.Create(self.GetType(), "Count"); + var methodInfo = methodsByType[key]; return (int)methodInfo.Invoke(self, null); } @@ -76,11 +74,8 @@ public static int sq_contains(IntPtr ob, IntPtr v) var obj = (CLRObject)GetManagedObject(ob); var self = obj.inst; - MethodInfo methodInfo; - if (!TryGetMethodInfo(self.GetType(), "ContainsKey", out methodInfo)) - { - return 0; - } + var key = Tuple.Create(self.GetType(), "ContainsKey"); + var methodInfo = methodsByType[key]; var parameters = methodInfo.GetParameters(); object arg; @@ -92,29 +87,15 @@ public static int sq_contains(IntPtr ob, IntPtr v) return (bool)methodInfo.Invoke(self, new[] { arg }) ? 1 : 0; } - - private static bool TryGetMethodInfo(Type type, string alias, out MethodInfo methodInfo) - { - var key = Tuple.Create(type, alias); - - if (!methodsByType.TryGetValue(key, out methodInfo)) - { - Exceptions.SetError(Exceptions.TypeError, - $"{nameof(type)} does not define {alias} method"); - - return false; - } - - return true; - } } - public static class DictionaryObjectExtension + public static class KeyValuePairEnumerableObjectExtension { - public static bool IsDictionary(this Type type) + public static bool IsKeyValuePairEnumerable(this Type type) { var iEnumerableType = typeof(IEnumerable<>); var keyValuePairType = typeof(KeyValuePair<,>); + var requiredMethods = new[] { "ContainsKey", "Count" }; var interfaces = type.GetInterfaces(); foreach (var i in interfaces) @@ -130,6 +111,19 @@ public static bool IsDictionary(this Type type) a.GetGenericTypeDefinition() == keyValuePairType && a.GetGenericArguments().Length == 2) { + foreach (var requiredMethod in requiredMethods) + { + var method = type.GetMethod(requiredMethod); + if (method == null) + { + method = type.GetMethod($"get_{requiredMethod}"); + if (method == null) + { + return false; + } + } + } + return true; } } From 49245d6f13514cfab8655875929000426eb8104a Mon Sep 17 00:00:00 2001 From: AlexCatarino Date: Tue, 31 Mar 2020 02:00:44 +0100 Subject: [PATCH 4/4] Addresses Peer-Review: Perfomance Improments --- src/runtime/keyvaluepairenumerableobject.cs | 57 ++++++--------------- 1 file changed, 17 insertions(+), 40 deletions(-) diff --git a/src/runtime/keyvaluepairenumerableobject.cs b/src/runtime/keyvaluepairenumerableobject.cs index 3f7afc852..c1644442c 100644 --- a/src/runtime/keyvaluepairenumerableobject.cs +++ b/src/runtime/keyvaluepairenumerableobject.cs @@ -13,41 +13,32 @@ namespace Python.Runtime internal class KeyValuePairEnumerableObject : ClassObject { private static Dictionary, MethodInfo> methodsByType = new Dictionary, MethodInfo>(); - private static Dictionary methodMap = new Dictionary - { - { "mp_length", "Count" }, - { "sq_contains", "ContainsKey" } - }; - - public List MappedMethods { get; } = new List(); + private static List requiredMethods = new List { "Count", "ContainsKey" }; - internal KeyValuePairEnumerableObject(Type tp) : base(tp) + internal static bool VerifyMethodRequirements(Type type) { - if (!tp.IsKeyValuePairEnumerable()) - { - throw new ArgumentException("object is not a KeyValuePair Enumerable"); - } - - foreach (var name in methodMap) + foreach (var requiredMethod in requiredMethods) { - var key = Tuple.Create(type, name.Value); - MethodInfo method; - if (!methodsByType.TryGetValue(key, out method)) + var method = type.GetMethod(requiredMethod); + if (method == null) { - method = tp.GetMethod(name.Value); + method = type.GetMethod($"get_{requiredMethod}"); if (method == null) { - method = tp.GetMethod($"get_{name.Value}"); + return false; } - if (method == null) - { - continue; - } - methodsByType.Add(key, method); } - MappedMethods.Add(name.Key); + var key = Tuple.Create(type, requiredMethod); + methodsByType.Add(key, method); } + + return true; + } + + internal KeyValuePairEnumerableObject(Type tp) : base(tp) + { + } internal override bool CanSubclass() => false; @@ -95,7 +86,6 @@ public static bool IsKeyValuePairEnumerable(this Type type) { var iEnumerableType = typeof(IEnumerable<>); var keyValuePairType = typeof(KeyValuePair<,>); - var requiredMethods = new[] { "ContainsKey", "Count" }; var interfaces = type.GetInterfaces(); foreach (var i in interfaces) @@ -111,20 +101,7 @@ public static bool IsKeyValuePairEnumerable(this Type type) a.GetGenericTypeDefinition() == keyValuePairType && a.GetGenericArguments().Length == 2) { - foreach (var requiredMethod in requiredMethods) - { - var method = type.GetMethod(requiredMethod); - if (method == null) - { - method = type.GetMethod($"get_{requiredMethod}"); - if (method == null) - { - return false; - } - } - } - - return true; + return KeyValuePairEnumerableObject.VerifyMethodRequirements(type); } } }