Skip to content

Commit aa2a1d6

Browse files
committed
Add __len__ and __contains__ to IDictionary that defines ContainsKey
1 parent dff82a1 commit aa2a1d6

File tree

4 files changed

+167
-0
lines changed

4 files changed

+167
-0
lines changed

src/embed_tests/ClassManagerTests.cs

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections;
23
using System.Collections.Generic;
34
using System.Linq;
45
using System.Reflection;
@@ -1003,6 +1004,132 @@ def call(instance):
10031004
}
10041005

10051006
#endregion
1007+
1008+
private static TestCaseData[] IDictionaryContainsTestCases => new TestCaseData[]
1009+
{
1010+
new(typeof(TestDictionary<string, string>)),
1011+
new(typeof(Dictionary<string, string>)),
1012+
};
1013+
1014+
[TestCaseSource(nameof(IDictionaryContainsTestCases))]
1015+
public void IDictionaryContainsMethodIsBound(Type dictType)
1016+
{
1017+
using var _ = Py.GIL();
1018+
1019+
var module = PyModule.FromString("IDictionaryContainsMethodIsBound", $@"
1020+
from clr import AddReference
1021+
AddReference(""Python.EmbeddingTest"")
1022+
1023+
from Python.EmbeddingTest import *
1024+
1025+
def contains(dictionary, key):
1026+
return key in dictionary
1027+
");
1028+
1029+
using var contains = module.GetAttr("contains");
1030+
1031+
var dictionary = (Activator.CreateInstance(dictType) as IDictionary)!;
1032+
var key1 = "key1";
1033+
dictionary.Add(key1, "value1");
1034+
1035+
using var pyDictionary = dictionary.ToPython();
1036+
using var pyKey1 = key1.ToPython();
1037+
1038+
var result = contains.Invoke(pyDictionary, pyKey1).As<bool>();
1039+
Assert.IsTrue(result);
1040+
1041+
using var pyKey2 = "key2".ToPython();
1042+
result = contains.Invoke(pyDictionary, pyKey2).As<bool>();
1043+
Assert.IsFalse(result);
1044+
}
1045+
1046+
[TestCaseSource(nameof(IDictionaryContainsTestCases))]
1047+
public void CanCheckIfNoneIsInDictionary(Type dictType)
1048+
{
1049+
using var _ = Py.GIL();
1050+
1051+
var module = PyModule.FromString("CanCheckIfNoneIsInDictionary", $@"
1052+
from clr import AddReference
1053+
AddReference(""Python.EmbeddingTest"")
1054+
1055+
from Python.EmbeddingTest import *
1056+
1057+
def contains(dictionary, key):
1058+
return key in dictionary
1059+
");
1060+
1061+
using var contains = module.GetAttr("contains");
1062+
1063+
var dictionary = (Activator.CreateInstance(dictType) as IDictionary)!;
1064+
dictionary.Add("key1", "value1");
1065+
1066+
using var pyDictionary = dictionary.ToPython();
1067+
1068+
var result = false;
1069+
Assert.DoesNotThrow(() => result = contains.Invoke(pyDictionary, PyObject.None).As<bool>());
1070+
Assert.IsFalse(result);
1071+
}
1072+
1073+
public class TestDictionary<TValue, TKey> : IDictionary
1074+
{
1075+
private readonly Dictionary<TValue, TKey> _data = new();
1076+
1077+
public object this[object key] { get => ((IDictionary)_data)[key]; set => ((IDictionary)_data)[key] = value; }
1078+
1079+
public bool IsFixedSize => ((IDictionary)_data).IsFixedSize;
1080+
1081+
public bool IsReadOnly => ((IDictionary)_data).IsReadOnly;
1082+
1083+
public ICollection Keys => ((IDictionary)_data).Keys;
1084+
1085+
public ICollection Values => ((IDictionary)_data).Values;
1086+
1087+
public int Count => ((ICollection)_data).Count;
1088+
1089+
public bool IsSynchronized => ((ICollection)_data).IsSynchronized;
1090+
1091+
public object SyncRoot => ((ICollection)_data).SyncRoot;
1092+
1093+
public void Add(object key, object value)
1094+
{
1095+
((IDictionary)_data).Add(key, value);
1096+
}
1097+
1098+
public void Clear()
1099+
{
1100+
((IDictionary)_data).Clear();
1101+
}
1102+
1103+
public bool Contains(object key)
1104+
{
1105+
return ((IDictionary)_data).Contains(key);
1106+
}
1107+
1108+
public void CopyTo(Array array, int index)
1109+
{
1110+
((ICollection)_data).CopyTo(array, index);
1111+
}
1112+
1113+
public IDictionaryEnumerator GetEnumerator()
1114+
{
1115+
return ((IDictionary)_data).GetEnumerator();
1116+
}
1117+
1118+
public void Remove(object key)
1119+
{
1120+
((IDictionary)_data).Remove(key);
1121+
}
1122+
1123+
IEnumerator IEnumerable.GetEnumerator()
1124+
{
1125+
return ((IEnumerable)_data).GetEnumerator();
1126+
}
1127+
1128+
public bool ContainsKey(TKey key)
1129+
{
1130+
return Contains(key);
1131+
}
1132+
}
10061133
}
10071134

10081135
public class NestedTestParent

src/runtime/ClassManager.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,11 @@ internal static ClassBase CreateClass(Type type)
180180
impl = new ArrayObject(type);
181181
}
182182

183+
else if (type.IsDictionary())
184+
{
185+
impl = new DictionaryObject(type);
186+
}
187+
183188
else if (type.IsKeyValuePairEnumerable())
184189
{
185190
impl = new KeyValuePairEnumerableObject(type);
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using System;
2+
using System.Collections;
3+
using System.Linq;
4+
5+
namespace Python.Runtime
6+
{
7+
/// <summary>
8+
/// Implements a Python type for managed IDictionary (dictionaries).
9+
/// This type is essentially the same as a ClassObject, except that it provides
10+
/// sequence semantics to support natural dictionary usage (__contains__ and __len__)
11+
/// from Python.
12+
/// </summary>
13+
internal class DictionaryObject : KeyValuePairEnumerableObject
14+
{
15+
internal DictionaryObject(Type tp) : base(tp)
16+
{
17+
}
18+
}
19+
20+
public static class DictionaryObjectExtension
21+
{
22+
public static bool IsDictionary(this Type type)
23+
{
24+
var iDictionaryType = typeof(IDictionary);
25+
return type.GetInterfaces().Contains(iDictionaryType) && DictionaryObject.VerifyMethodRequirements(type);
26+
}
27+
}
28+
}

src/runtime/Types/KeyValuePairEnumerableObject.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,13 @@ public static int sq_contains(BorrowedReference ob, BorrowedReference v)
7777
$"invalid parameter type for sq_contains: should be {Converter.GetTypeByAlias(v)}, found {parameters[0].ParameterType}");
7878
}
7979

80+
// If the argument is None, we return false. Python allows using None as key,
81+
// but C# doesn't and will throw, so we shortcut here
82+
if (arg == null)
83+
{
84+
return 0;
85+
}
86+
8087
return (bool)methodInfo.Invoke(self, new[] { arg }) ? 1 : 0;
8188
}
8289
}

0 commit comments

Comments
 (0)