diff --git a/docs/examples.md b/docs/examples.md index 4ea97e01..3031c0d3 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -39,7 +39,7 @@ Because the input and output types are more complex, we've used a list of tuple This demo generates the following C# method signature: ```csharp -public (IEnumerable>, double) CalculateKmeansInertia(IEnumerable<(long, long)> data, long nClusters); +public (IReadOnlyList>, double) CalculateKmeansInertia(IReadOnlyList<(long, long)> data, long nClusters); ``` ## Phi-3 inference demo diff --git a/docs/reference.md b/docs/reference.md index 99c5ef94..4f8c998e 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -11,10 +11,10 @@ CSnakes supports the following typed scenarios: | `str` | `string` | | `bytes` | `byte[]` | | `bool` | `bool` | -| `list[T]` | `IEnumerable` | +| `list[T]` | `IReadOnlyList` | | `dict[K, V]` | `IReadOnlyDictionary` | | `tuple[T1, T2, ...]` | `(T1, T2, ...)` | -| `typing.Sequence[T]` | `IEnumerable` | +| `typing.Sequence[T]` | `IReadOnlyList` | | `typing.Dict[K, V]` | `IReadOnlyDictionary` | | `typing.Mapping[K, V]` | `IReadOnlyDictionary` | | `typing.Tuple[T1, T2, ...]` | `(T1, T2, ...)` | diff --git a/samples/Aspire/CSnakesAspire.ApiService/CSnakesAspire.ApiService.csproj b/samples/Aspire/CSnakesAspire.ApiService/CSnakesAspire.ApiService.csproj index ed7f7b3d..fcbf5acb 100644 --- a/samples/Aspire/CSnakesAspire.ApiService/CSnakesAspire.ApiService.csproj +++ b/samples/Aspire/CSnakesAspire.ApiService/CSnakesAspire.ApiService.csproj @@ -15,7 +15,7 @@ - + diff --git a/samples/Aspire/CSnakesAspire.AppHost/CSnakesAspire.AppHost.csproj b/samples/Aspire/CSnakesAspire.AppHost/CSnakesAspire.AppHost.csproj index 88285624..7953197a 100644 --- a/samples/Aspire/CSnakesAspire.AppHost/CSnakesAspire.AppHost.csproj +++ b/samples/Aspire/CSnakesAspire.AppHost/CSnakesAspire.AppHost.csproj @@ -1,4 +1,4 @@ - + Exe @@ -15,8 +15,8 @@ - - + + diff --git a/samples/Aspire/CSnakesAspire.ServiceDefaults/CSnakesAspire.ServiceDefaults.csproj b/samples/Aspire/CSnakesAspire.ServiceDefaults/CSnakesAspire.ServiceDefaults.csproj index 16712f26..2609cf58 100644 --- a/samples/Aspire/CSnakesAspire.ServiceDefaults/CSnakesAspire.ServiceDefaults.csproj +++ b/samples/Aspire/CSnakesAspire.ServiceDefaults/CSnakesAspire.ServiceDefaults.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/CSnakes.Runtime.Tests/Converter/ListConverterTest.cs b/src/CSnakes.Runtime.Tests/Converter/ListConverterTest.cs index 1ffb3709..1b08418c 100644 --- a/src/CSnakes.Runtime.Tests/Converter/ListConverterTest.cs +++ b/src/CSnakes.Runtime.Tests/Converter/ListConverterTest.cs @@ -2,11 +2,7 @@ public class ListConverterTest : ConverterTestBase { - [Fact] - public void IEnumerableConverter() => - RunTest>(["Hell0", "W0rld"]); - [Fact] public void ListConverter() => - RunTest>([123456, 123562]); + RunTest>([123456, 123562]); } diff --git a/src/CSnakes.Runtime.Tests/Python/PyDictionaryTests.cs b/src/CSnakes.Runtime.Tests/Python/PyDictionaryTests.cs new file mode 100644 index 00000000..8d1c5e71 --- /dev/null +++ b/src/CSnakes.Runtime.Tests/Python/PyDictionaryTests.cs @@ -0,0 +1,102 @@ +using CSnakes.Runtime.Python; + +namespace CSnakes.Runtime.Tests.Python; +public class PyDictionaryTests : RuntimeTestBase +{ + [Fact] + public void TestIndex() + { + var testDict = new Dictionary() + { + ["Hello"] = "World?", + ["Foo"] = "Bar" + }; + var pyObject = PyObject.From(testDict); + var pyDict = pyObject.As>(); + Assert.NotNull(pyDict); + Assert.Equal("World?", pyDict["Hello"]); + Assert.Equal("Bar", pyDict["Foo"]); + // Try twice + Assert.Equal("World?", pyDict["Hello"]); + } + + [Fact] + public void TestKeys() + { + var testDict = new Dictionary() + { + ["Hello"] = "World?", + ["Foo"] = "Bar" + }; + var pyObject = PyObject.From(testDict); + var pyDict = pyObject.As>(); + Assert.NotNull(pyDict); + Assert.Equal(2, pyDict.Count); + Assert.Contains("Hello", pyDict.Keys); + Assert.Contains("Foo", pyDict.Keys); + } + + [Fact] + public void TestValues() + { + var testDict = new Dictionary() + { + ["Hello"] = "World?", + ["Foo"] = "Bar" + }; + var pyObject = PyObject.From(testDict); + var pyDict = pyObject.As>(); + Assert.NotNull(pyDict); + Assert.Equal(2, pyDict.Count); + Assert.Contains("World?", pyDict.Values); + Assert.Contains("Bar", pyDict.Values); + } + + [Fact] + public void TestCount() + { + var testDict = new Dictionary() + { + ["Hello"] = "World?", + ["Foo"] = "Bar" + }; + var pyObject = PyObject.From(testDict); + var pyDict = pyObject.As>(); + Assert.NotNull(pyDict); + Assert.Equal(2, pyDict.Count); + } + + [Fact] + public void TestContainsKey() + { + var testDict = new Dictionary() + { + ["Hello"] = "World?", + ["Foo"] = "Bar" + }; + var pyObject = PyObject.From(testDict); + var pyDict = pyObject.As>(); + Assert.NotNull(pyDict); + Assert.True(pyDict.ContainsKey("Hello")); + Assert.True(pyDict.ContainsKey("Foo")); + Assert.False(pyDict.ContainsKey("Bar")); + } + + [Fact] + public void TestTryGetValue() + { + var testDict = new Dictionary() + { + ["Hello"] = "World?", + ["Foo"] = "Bar" + }; + var pyObject = PyObject.From(testDict); + var pyDict = pyObject.As>(); + Assert.NotNull(pyDict); + Assert.True(pyDict.TryGetValue("Hello", out var value)); + Assert.Equal("World?", value); + Assert.True(pyDict.TryGetValue("Foo", out value)); + Assert.Equal("Bar", value); + Assert.False(pyDict.TryGetValue("Bar", out _)); + } +} \ No newline at end of file diff --git a/src/CSnakes.Runtime.Tests/Python/PyObjectTests.cs b/src/CSnakes.Runtime.Tests/Python/PyObjectTests.cs index bfcf96de..5e42a58f 100644 --- a/src/CSnakes.Runtime.Tests/Python/PyObjectTests.cs +++ b/src/CSnakes.Runtime.Tests/Python/PyObjectTests.cs @@ -1,5 +1,4 @@ using CSnakes.Runtime.Python; -using System.Collections; namespace CSnakes.Runtime.Tests.Python; public class PyObjectTests : RuntimeTestBase @@ -159,22 +158,4 @@ public void TestObjectNotStrictInequality(object? o1, object? o2, bool expectedL Assert.Equal(expectedLT, obj1 <= obj2); Assert.Equal(expectedGT, obj1 >= obj2); } - - [Fact] - public void TestAsCollection() - { - using PyObject o = PyObject.From>(new[] { "Hello", "World" })!; - var collection = o.AsCollection, string>(); - Assert.NotNull(collection); - Assert.Equal(2, collection!.Count()); - } - - [Fact] - public void TestAsDictionary() - { - using PyObject o = PyObject.From>(new Dictionary { { "Hello", "World" } })!; - var dictionary = o.AsDictionary, string, string>(); - Assert.NotNull(dictionary); - Assert.Equal("World", dictionary!["Hello"]); - } } \ No newline at end of file diff --git a/src/CSnakes.Runtime/CPython/Dict.cs b/src/CSnakes.Runtime/CPython/Dict.cs index 29e048da..b07d43e6 100644 --- a/src/CSnakes.Runtime/CPython/Dict.cs +++ b/src/CSnakes.Runtime/CPython/Dict.cs @@ -61,6 +61,22 @@ internal static nint PyDict_GetItem(PyObject dict, PyObject key) return result; } + /// + /// Does the dictionary contain the key? Raises exception on failure + /// + /// + /// + /// + internal static bool PyDict_Contains(PyObject dict, PyObject key) + { + int result = PyDict_Contains_(dict, key); + if(result == -1) + { + throw PyObject.ThrowPythonExceptionAsClrException(); + } + return result == 1; + } + /// /// Return the object from dictionary p which has a key `key`. /// Return NULL if the key key is not present, but without setting an exception. @@ -93,4 +109,13 @@ internal static nint PyDict_GetItem(PyObject dict, PyObject key) /// New reference to the items(). [LibraryImport(PythonLibraryName)] internal static partial nint PyDict_Items(PyObject dict); + + [LibraryImport(PythonLibraryName, EntryPoint = "PyDict_Contains")] + private static partial int PyDict_Contains_(PyObject dict, PyObject key); + + [LibraryImport(PythonLibraryName)] + internal static partial nint PyDict_Keys(PyObject dict); + + [LibraryImport(PythonLibraryName)] + internal static partial nint PyDict_Values(PyObject dict); } diff --git a/src/CSnakes.Runtime/CPython/Iter.cs b/src/CSnakes.Runtime/CPython/Iter.cs new file mode 100644 index 00000000..47db503a --- /dev/null +++ b/src/CSnakes.Runtime/CPython/Iter.cs @@ -0,0 +1,18 @@ +using CSnakes.Runtime.Python; +using System.Runtime.InteropServices; + +namespace CSnakes.Runtime.CPython; +internal unsafe partial class CPythonAPI +{ + /// + /// Return the next value from the iterator o. + /// The object must be an iterator according to PyIter_Check() + /// (it is up to the caller to check this). + /// If there are no remaining values, returns NULL with no exception set. + /// If an error occurs while retrieving the item, returns NULL and passes along the exception. + /// + /// + /// New refernce to the next item + [LibraryImport(PythonLibraryName)] + internal static partial nint PyIter_Next(PyObject iter); +} diff --git a/src/CSnakes.Runtime/CPython/Mapping.cs b/src/CSnakes.Runtime/CPython/Mapping.cs index f7382b7f..493ae04f 100644 --- a/src/CSnakes.Runtime/CPython/Mapping.cs +++ b/src/CSnakes.Runtime/CPython/Mapping.cs @@ -54,4 +54,23 @@ internal static bool PyMapping_SetItem(PyObject dict, PyObject key, PyObject val /// New reference to the items(). [LibraryImport(PythonLibraryName)] internal static partial nint PyMapping_Items(PyObject dict); + + [LibraryImport(PythonLibraryName)] + internal static partial nint PyMapping_Keys(PyObject dict); + + [LibraryImport(PythonLibraryName)] + internal static partial nint PyMapping_Values(PyObject dict); + + [LibraryImport(PythonLibraryName)] + internal static partial nint PyMapping_Size(PyObject dict); + + /// + /// Return 1 if the mapping object has the key key and 0 otherwise. + /// This is equivalent to the Python expression key in o. This function always succeeds. + /// + /// + /// + /// + [LibraryImport(PythonLibraryName)] + internal static partial int PyMapping_HasKey(PyObject dict, PyObject key); } diff --git a/src/CSnakes.Runtime/PyObjectTypeConverter.Dictionary.cs b/src/CSnakes.Runtime/PyObjectTypeConverter.Dictionary.cs index c8f1c0ef..4bf18d8a 100644 --- a/src/CSnakes.Runtime/PyObjectTypeConverter.Dictionary.cs +++ b/src/CSnakes.Runtime/PyObjectTypeConverter.Dictionary.cs @@ -1,92 +1,23 @@ -using CSnakes.Runtime.CPython; using CSnakes.Runtime.Python; using System.Collections; -using System.Collections.ObjectModel; namespace CSnakes.Runtime; internal partial class PyObjectTypeConverter { private static object ConvertToDictionary(PyObject pyObject, Type destinationType, bool useMappingProtocol = false) { - using PyObject items = useMappingProtocol ? - PyObject.Create(CPythonAPI.PyMapping_Items(pyObject)) : - PyObject.Create(CPythonAPI.PyDict_Items(pyObject)); - Type keyType = destinationType.GetGenericArguments()[0]; Type valueType = destinationType.GetGenericArguments()[1]; if (!knownDynamicTypes.TryGetValue(destinationType, out DynamicTypeInfo? typeInfo)) { - Type dictType = typeof(Dictionary<,>).MakeGenericType(keyType, valueType); - Type returnType = typeof(ReadOnlyDictionary<,>).MakeGenericType(keyType, valueType); + Type dictType = typeof(PyDictionary<,>).MakeGenericType(keyType, valueType); - typeInfo = new(returnType.GetConstructor([dictType])!, dictType.GetConstructor([typeof(int)])!); + typeInfo = new(dictType.GetConstructor([typeof(PyObject)])!); knownDynamicTypes[destinationType] = typeInfo; } - nint itemsLength = CPythonAPI.PyList_Size(items); - IDictionary dict = (IDictionary)typeInfo.TransientTypeConstructor!.Invoke([(int)itemsLength]); - - for (nint i = 0; i < itemsLength; i++) - { - // TODO: We make 3 heap allocations per item here. - // 1. The item, which could be inlined as it's only used to call PyTuple_GetItem. - // 2. The key, which we need to recursively convert -- although if this is a string, which it mostly is, then we _could_ avoid this. - // 3. The value, which we need to recursively convert. - nint kvpTuple = CPythonAPI.PyList_GetItem(items, i); - - // Optimize keys as string because this is the most common case. - if (keyType == typeof(string)) - { - nint itemKey = CPythonAPI.PyTuple_GetItemWithNewRefRaw(kvpTuple, 0); - using PyObject value = PyObject.Create(CPythonAPI.PyTuple_GetItemWithNewRefRaw(kvpTuple, 1)); - - string? keyAsString = CPythonAPI.PyUnicode_AsUTF8Raw(itemKey); - if (keyAsString is null) - { - CPythonAPI.Py_DecRefRaw(itemKey); - throw PyObject.ThrowPythonExceptionAsClrException(); - } - object? convertedValue = value.As(valueType); - - dict.Add(keyAsString!, convertedValue); - CPythonAPI.Py_DecRefRaw(itemKey); - } else - { - using PyObject key = PyObject.Create(CPythonAPI.PyTuple_GetItemWithNewRefRaw(kvpTuple, 0)); - using PyObject value = PyObject.Create(CPythonAPI.PyTuple_GetItemWithNewRefRaw(kvpTuple, 1)); - - object? convertedKey = key.As(keyType); - object? convertedValue = value.As(valueType); - - dict.Add(convertedKey!, convertedValue); - } - CPythonAPI.Py_DecRefRaw(kvpTuple); - } - - return typeInfo.ReturnTypeConstructor.Invoke([dict]); - } - - internal static IReadOnlyDictionary ConvertToDictionary(PyObject pyObject) where TKey : notnull - { - using PyObject items = PyObject.Create(CPythonAPI.PyMapping_Items(pyObject)); - - var dict = new Dictionary(); - nint itemsLength = CPythonAPI.PyList_Size(items); - for (nint i = 0; i < itemsLength; i++) - { - nint kvpTuple = CPythonAPI.PyList_GetItem(items, i); - using PyObject key = PyObject.Create(CPythonAPI.PyTuple_GetItemWithNewRefRaw(kvpTuple, 0)); - using PyObject value = PyObject.Create(CPythonAPI.PyTuple_GetItemWithNewRefRaw(kvpTuple, 1)); - - TKey convertedKey = key.As(); - TValue convertedValue = value.As(); - - dict.Add(convertedKey, convertedValue); - CPythonAPI.Py_DecRefRaw(kvpTuple); - } - - return dict; + return typeInfo.ReturnTypeConstructor.Invoke([pyObject.Clone()]); } internal static PyObject ConvertFromDictionary(IDictionary dictionary) diff --git a/src/CSnakes.Runtime/PyObjectTypeConverter.KnownTypes.cs b/src/CSnakes.Runtime/PyObjectTypeConverter.KnownTypes.cs index 01e3c132..e64f94b7 100644 --- a/src/CSnakes.Runtime/PyObjectTypeConverter.KnownTypes.cs +++ b/src/CSnakes.Runtime/PyObjectTypeConverter.KnownTypes.cs @@ -4,7 +4,7 @@ namespace CSnakes.Runtime; internal partial class PyObjectTypeConverter { - static readonly Type listType = typeof(IReadOnlyCollection<>); + static readonly Type listType = typeof(IReadOnlyList<>); static readonly Type collectionType = typeof(IEnumerable); static readonly Type dictionaryType = typeof(IReadOnlyDictionary<,>); static readonly Type pyObjectType = typeof(PyObject); diff --git a/src/CSnakes.Runtime/PyObjectTypeConverter.List.cs b/src/CSnakes.Runtime/PyObjectTypeConverter.List.cs index 465ef7e4..89e0a275 100644 --- a/src/CSnakes.Runtime/PyObjectTypeConverter.List.cs +++ b/src/CSnakes.Runtime/PyObjectTypeConverter.List.cs @@ -1,4 +1,3 @@ -using CSnakes.Runtime.CPython; using CSnakes.Runtime.Python; using System.Collections; using System.Runtime.InteropServices; @@ -6,63 +5,18 @@ namespace CSnakes.Runtime; internal partial class PyObjectTypeConverter { - private static ICollection ConvertToList(PyObject pyObject, Type destinationType) + private static object ConvertToList(PyObject pyObject, Type destinationType) { Type genericArgument = destinationType.GetGenericArguments()[0]; - nint listSize = CPythonAPI.PySequence_Size(pyObject); if (!knownDynamicTypes.TryGetValue(destinationType, out DynamicTypeInfo? typeInfo)) { - Type listType = typeof(List<>).MakeGenericType(genericArgument); - typeInfo = new(listType.GetConstructor([typeof(int)])!); + Type listType = typeof(PyList<>).MakeGenericType(genericArgument); + typeInfo = new(listType.GetConstructor([typeof(PyObject)])!); knownDynamicTypes[destinationType] = typeInfo; } - IList list = (IList)typeInfo.ReturnTypeConstructor.Invoke([(int)listSize]); - - for (var i = 0; i < listSize; i++) - { - using PyObject item = PyObject.Create(CPythonAPI.PyList_GetItem(pyObject, i)); - list.Add(item.As(genericArgument)); - } - - return list; - } - - private static ICollection ConvertToListFromSequence(PyObject pyObject, Type destinationType) - { - Type genericArgument = destinationType.GetGenericArguments()[0]; - - nint listSize = CPythonAPI.PySequence_Size(pyObject); - if (!knownDynamicTypes.TryGetValue(destinationType, out DynamicTypeInfo? typeInfo)) - { - Type listType = typeof(List<>).MakeGenericType(genericArgument); - typeInfo = new(listType.GetConstructor([typeof(int)])!); - knownDynamicTypes[destinationType] = typeInfo; - } - - IList list = (IList)typeInfo.ReturnTypeConstructor.Invoke([(int)listSize]); - - for (var i = 0; i < listSize; i++) - { - using PyObject item = PyObject.Create(CPythonAPI.PySequence_GetItem(pyObject, i)); - list.Add(item.As(genericArgument)); - } - - return list; - } - - internal static IReadOnlyCollection ConvertToCollection(PyObject pyObject) - { - nint listSize = CPythonAPI.PySequence_Size(pyObject); - var list = new List((int)listSize); - for (var i = 0; i < listSize; i++) - { - using PyObject item = PyObject.Create(CPythonAPI.PySequence_GetItem(pyObject, i)); - list.Add(item.As()); - } - - return list; + return typeInfo.ReturnTypeConstructor.Invoke([pyObject.Clone()]); } internal static PyObject ConvertFromList(ICollection e) diff --git a/src/CSnakes.Runtime/PyObjectTypeConverter.Tuple.cs b/src/CSnakes.Runtime/PyObjectTypeConverter.Tuple.cs index a9b9a898..f2d10028 100644 --- a/src/CSnakes.Runtime/PyObjectTypeConverter.Tuple.cs +++ b/src/CSnakes.Runtime/PyObjectTypeConverter.Tuple.cs @@ -84,4 +84,21 @@ internal static ITuple ConvertToTuple(PyObject pyObj, Type destinationType) return (ITuple)typeInfo.ReturnTypeConstructor.Invoke([.. clrValues]); } + + internal static KeyValuePair ConvertToKeyValuePair(PyObject pyObj) + { + if (!CPythonAPI.IsPyTuple(pyObj)) + { + throw new InvalidCastException($"Cannot convert {pyObj.GetPythonType()} to a keyvaluepair."); + } + if (CPythonAPI.PyTuple_Size(pyObj) != 2) + { + throw new InvalidDataException("Tuple must have exactly 2 elements to be converted to a KeyValuePair."); + } + + using PyObject key = PyObject.Create(CPythonAPI.PyTuple_GetItemWithNewRef(pyObj, 0)); + using PyObject value = PyObject.Create(CPythonAPI.PyTuple_GetItemWithNewRef(pyObj, 1)); + + return new KeyValuePair(key.As(), value.As()); + } } \ No newline at end of file diff --git a/src/CSnakes.Runtime/PyObjectTypeConverter.cs b/src/CSnakes.Runtime/PyObjectTypeConverter.cs index 3ec49e07..e791d14c 100644 --- a/src/CSnakes.Runtime/PyObjectTypeConverter.cs +++ b/src/CSnakes.Runtime/PyObjectTypeConverter.cs @@ -31,7 +31,7 @@ public static object PyObjectToManagedType(PyObject pyObject, Type destinationTy if (CPythonAPI.IsPySequence(pyObject) && IsAssignableToGenericType(destinationType, listType)) { - return ConvertToListFromSequence(pyObject, destinationType); + return ConvertToList(pyObject, destinationType); } throw new InvalidCastException($"Attempting to cast {destinationType} from {pyObject.GetPythonType()}"); diff --git a/src/CSnakes.Runtime/Python/GeneratorIterator.cs b/src/CSnakes.Runtime/Python/GeneratorIterator.cs index 59cab376..65c4edd7 100644 --- a/src/CSnakes.Runtime/Python/GeneratorIterator.cs +++ b/src/CSnakes.Runtime/Python/GeneratorIterator.cs @@ -49,14 +49,13 @@ public bool MoveNext() } } - public void Reset() => throw new NotImplementedException(); - + public void Reset() => throw new NotSupportedException(); public TYield Send(TSend value) { try { - using PyObject sendValue = PyObject.From(value) !; + using PyObject sendValue = PyObject.From(value); using PyObject result = sendPyFunction.Call(sendValue); current = result.As(); return current; diff --git a/src/CSnakes.Runtime/Python/PyDictionary.cs b/src/CSnakes.Runtime/Python/PyDictionary.cs new file mode 100644 index 00000000..56b2f3ca --- /dev/null +++ b/src/CSnakes.Runtime/Python/PyDictionary.cs @@ -0,0 +1,104 @@ +using CSnakes.Runtime.CPython; +using System.Collections; +using System.Diagnostics.CodeAnalysis; + +namespace CSnakes.Runtime.Python; + +internal class PyDictionary(PyObject dictionary) : IReadOnlyDictionary, IDisposable + where TKey : notnull +{ + private readonly Dictionary _dictionary = []; + private readonly PyObject _dictionaryObject = dictionary; + + public TValue this[TKey key] + { + get + { + if (_dictionary.TryGetValue(key, out TValue? value)) + { + return value; + } + using (GIL.Acquire()) + { + using PyObject keyPyObject = PyObject.From(key); + using PyObject pyObjValue = PyObject.Create(CPythonAPI.PyMapping_GetItem(_dictionaryObject, keyPyObject)); + TValue managedValue = pyObjValue.As(); + + _dictionary[key] = managedValue; + return managedValue; + } + } + } + + public IEnumerable Keys + { + get + { + using (GIL.Acquire()) + { + return new PyEnumerable(PyObject.Create(CPythonAPI.PyMapping_Keys(_dictionaryObject))); + } + } + } + + public IEnumerable Values + { + get + { + using (GIL.Acquire()) + { + return new PyEnumerable(PyObject.Create(CPythonAPI.PyMapping_Values(_dictionaryObject))); + } + } + } + + public int Count + { + get + { + using (GIL.Acquire()) + { + return (int)CPythonAPI.PyMapping_Size(_dictionaryObject); + } + } + } + + public bool ContainsKey(TKey key) + { + if (_dictionary.ContainsKey(key)) + { + return true; + } + + using (GIL.Acquire()) + { + using PyObject keyPyObject = PyObject.From(key); + return CPythonAPI.PyMapping_HasKey(_dictionaryObject, keyPyObject) == 1; + } + } + + public void Dispose() => _dictionaryObject.Dispose(); + + public IEnumerator> GetEnumerator() + { + using (GIL.Acquire()) + { + using var items = PyObject.Create(CPythonAPI.PyMapping_Items(_dictionaryObject)); + return new PyKeyValuePairEnumerable(items).GetEnumerator(); + } + } + + public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) + { + if (ContainsKey(key)) + { + value = this[key]; + return true; + } + + value = default; + return false; + } + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); +} diff --git a/src/CSnakes.Runtime/Python/PyEnumerable.cs b/src/CSnakes.Runtime/Python/PyEnumerable.cs new file mode 100644 index 00000000..d1d922b3 --- /dev/null +++ b/src/CSnakes.Runtime/Python/PyEnumerable.cs @@ -0,0 +1,51 @@ +using CSnakes.Runtime.CPython; +using System.Collections; + +namespace CSnakes.Runtime.Python; + +internal class PyEnumerable : IEnumerable, IEnumerator, IDisposable +{ + private readonly PyObject _pyIterator; + private TValue current = default!; + + internal PyEnumerable(PyObject pyObject) + { + using (GIL.Acquire()) + { + _pyIterator = pyObject.GetIter(); + } + } + + public TValue Current => current; + + object IEnumerator.Current => current!; + + public void Dispose() => _pyIterator.Dispose(); + + public IEnumerator GetEnumerator() => this; + + public bool MoveNext() + { + using (GIL.Acquire()) + { + nint result = CPythonAPI.PyIter_Next(_pyIterator); + if (result == IntPtr.Zero && CPythonAPI.PyErr_Occurred()) + { + throw PyObject.ThrowPythonExceptionAsClrException(); + } + + if (result == IntPtr.Zero) + { + return false; + } + + using PyObject pyObject = PyObject.Create(result); + current = pyObject.As(); + return true; + } + } + + public void Reset() => throw new NotSupportedException("Python iterators cannot be reset"); + + IEnumerator IEnumerable.GetEnumerator() => this; +} diff --git a/src/CSnakes.Runtime/Python/PyKeyValuePairEnumerable.cs b/src/CSnakes.Runtime/Python/PyKeyValuePairEnumerable.cs new file mode 100644 index 00000000..40a641d3 --- /dev/null +++ b/src/CSnakes.Runtime/Python/PyKeyValuePairEnumerable.cs @@ -0,0 +1,51 @@ +using CSnakes.Runtime.CPython; +using System.Collections; + +namespace CSnakes.Runtime.Python; + +internal class PyKeyValuePairEnumerable : IEnumerable>, IEnumerator> +{ + private readonly PyObject _pyIterator; + private KeyValuePair current = default!; + + internal PyKeyValuePairEnumerable(PyObject pyObject) + { + using (GIL.Acquire()) + { + _pyIterator = pyObject.GetIter(); + } + } + + public KeyValuePair Current => current; + + object IEnumerator.Current => current!; + + public void Dispose() => _pyIterator.Dispose(); + + public IEnumerator> GetEnumerator() => this; + + public bool MoveNext() + { + using (GIL.Acquire()) + { + nint result = CPythonAPI.PyIter_Next(_pyIterator); + if (result == IntPtr.Zero && CPythonAPI.PyErr_Occurred()) + { + throw PyObject.ThrowPythonExceptionAsClrException(); + } + + if (result == IntPtr.Zero) + { + return false; + } + + using PyObject pyObject = PyObject.Create(result); + current = pyObject.As(); + return true; + } + } + + public void Reset() => throw new NotSupportedException("Python iterators cannot be reset"); + + IEnumerator IEnumerable.GetEnumerator() => this; +} diff --git a/src/CSnakes.Runtime/Python/PyList.cs b/src/CSnakes.Runtime/Python/PyList.cs new file mode 100644 index 00000000..2d55d762 --- /dev/null +++ b/src/CSnakes.Runtime/Python/PyList.cs @@ -0,0 +1,57 @@ +using CSnakes.Runtime.CPython; +using System.Collections; + +namespace CSnakes.Runtime.Python; + +internal class PyList : IReadOnlyList, IDisposable +{ + private readonly PyObject _listObject; + + // If someone fetches the same index multiple times, we cache the result to avoid multiple round trips to Python + private readonly Dictionary _convertedItems = new(); + + public PyList(PyObject listObject) => _listObject = listObject; + + public TItem this[int index] + { + get + { + if (_convertedItems.TryGetValue(index, out TItem? cachedValue)) + { + return cachedValue; + } + + using (GIL.Acquire()) + { + using PyObject value = PyObject.Create(CPythonAPI.PySequence_GetItem(_listObject, index)); + TItem result = value.As(); + _convertedItems[index] = result; + return result; + } + } + } + + public int Count + { + get + { + using (GIL.Acquire()) + { + return (int)CPythonAPI.PySequence_Size(_listObject); + } + } + } + + public void Dispose() => _listObject.Dispose(); + + public IEnumerator GetEnumerator() + { + // TODO: If someone fetches the same index multiple times, we cache the result to avoid multiple round trips to Python + using (GIL.Acquire()) + { + return new PyEnumerable(_listObject); + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/src/CSnakes.Runtime/Python/PyObject.cs b/src/CSnakes.Runtime/Python/PyObject.cs index c631651a..d5d4ab45 100644 --- a/src/CSnakes.Runtime/Python/PyObject.cs +++ b/src/CSnakes.Runtime/Python/PyObject.cs @@ -48,7 +48,8 @@ protected override bool ReleaseHandle() { CPythonAPI.Py_DecRefRaw(handle); } - } else + } + else { // Probably in the GC finalizer thread, instead of causing GIL contention, put this on a queue to be processed later. GIL.QueueForDisposal(handle); @@ -149,11 +150,8 @@ public virtual bool HasAttr(string name) } } - /// - /// Get the iterator for the object. This is equivalent to iter(obj) in Python. - /// - /// The iterator object (new ref) - public virtual PyObject GetIter() + + internal virtual PyObject GetIter() { RaiseOnPythonNotInitialized(); using (GIL.Acquire()) @@ -162,6 +160,19 @@ public virtual PyObject GetIter() } } + /// + /// Calls iter() on the object and returns an IEnumerable that yields values of type T. + /// + /// The type for each item in the iterator + /// + public IEnumerable AsEnumerable() + { + using (GIL.Acquire()) + { + return new PyEnumerable(this); + } + } + /// /// Get the results of the repr() function on the object. /// @@ -439,6 +450,20 @@ public override string ToString() public T As() => (T)As(typeof(T)); + /// + /// Unpack a tuple of 2 elements into a KeyValuePair + /// + /// The type of the key + /// The type of the value + /// + public KeyValuePair As() + { + using (GIL.Acquire()) + { + return PyObjectTypeConverter.ConvertToKeyValuePair(this); + } + } + internal object As(Type type) { using (GIL.Acquire()) @@ -460,26 +485,6 @@ var t when t.IsAssignableTo(typeof(IGeneratorIterator)) => PyObjectTypeConverter } } - public IReadOnlyCollection AsCollection() where TCollection : IReadOnlyCollection - { - using (GIL.Acquire()) - { - return PyObjectTypeConverter.ConvertToCollection(this); - } - } - public IReadOnlyDictionary AsDictionary() where TDict : IReadOnlyDictionary where TKey : notnull - { - using (GIL.Acquire()) - { - return PyObjectTypeConverter.ConvertToDictionary(this); - } - } - - public IReadOnlyCollection As() where TCollection : IReadOnlyCollection => - PyObjectTypeConverter.ConvertToCollection(this); - public IReadOnlyDictionary As() where TDict : IReadOnlyDictionary where TKey : notnull => - PyObjectTypeConverter.ConvertToDictionary(this); - public static PyObject From(T value) { using (GIL.Acquire()) diff --git a/src/CSnakes.Runtime/PythonRuntimeException.cs b/src/CSnakes.Runtime/PythonRuntimeException.cs index 5ec437a4..6f4e8c17 100644 --- a/src/CSnakes.Runtime/PythonRuntimeException.cs +++ b/src/CSnakes.Runtime/PythonRuntimeException.cs @@ -39,7 +39,7 @@ private static string[] FormatPythonStackTrace(PyObject pythonStackTrace) using var formatTbFunction = tracebackModule.GetAttr("format_tb"); using var formattedStackTrace = formatTbFunction.Call(pythonStackTrace); - return [.. formattedStackTrace.As>()]; + return [.. formattedStackTrace.As>()]; } } diff --git a/src/CSnakes.Tests/GeneratedSignatureTests.cs b/src/CSnakes.Tests/GeneratedSignatureTests.cs index 1190e027..fd0bcf33 100644 --- a/src/CSnakes.Tests/GeneratedSignatureTests.cs +++ b/src/CSnakes.Tests/GeneratedSignatureTests.cs @@ -20,10 +20,10 @@ public class GeneratedSignatureTests(TestEnvironment testEnv) : IClassFixture float:\n ...\n", "double HelloWorld(string name)")] [InlineData("def hello_world(name: str) -> int:\n ...\n", "long HelloWorld(string name)")] [InlineData("def hello_world(name: str, age: int) -> str:\n ...\n", "string HelloWorld(string name, long age)")] - [InlineData("def hello_world(numbers: list[float]) -> list[int]:\n ...\n", "IReadOnlyCollection HelloWorld(IReadOnlyCollection numbers)")] - [InlineData("def hello_world(numbers: List[float]) -> List[int]:\n ...\n", "IReadOnlyCollection HelloWorld(IReadOnlyCollection numbers)")] + [InlineData("def hello_world(numbers: list[float]) -> list[int]:\n ...\n", "IReadOnlyList HelloWorld(IReadOnlyList numbers)")] + [InlineData("def hello_world(numbers: List[float]) -> List[int]:\n ...\n", "IReadOnlyList HelloWorld(IReadOnlyList numbers)")] [InlineData("def hello_world(value: tuple[int]) -> None:\n ...\n", "void HelloWorld(ValueTuple value)")] - [InlineData("def hello_world(a: bool, b: str, c: list[tuple[int, float]]) -> bool: \n ...\n", "bool HelloWorld(bool a, string b, IReadOnlyCollection<(long, double)> c)")] + [InlineData("def hello_world(a: bool, b: str, c: list[tuple[int, float]]) -> bool: \n ...\n", "bool HelloWorld(bool a, string b, IReadOnlyList<(long, double)> c)")] [InlineData("def hello_world(a: bool = True, b: str = None) -> bool: \n ...\n", "bool HelloWorld(bool a = true, string? b = null)")] [InlineData("def hello_world(a: bytes, b: bool = False, c: float = 0.1) -> None: \n ...\n", "void HelloWorld(byte[] a, bool b = false, double c = 0.1)")] [InlineData("def hello_world(a: str = 'default') -> None: \n ...\n", "void HelloWorld(string a = \"default\")")] diff --git a/src/CSnakes.Tests/TypeReflectionTests.cs b/src/CSnakes.Tests/TypeReflectionTests.cs index 57e41068..08919054 100644 --- a/src/CSnakes.Tests/TypeReflectionTests.cs +++ b/src/CSnakes.Tests/TypeReflectionTests.cs @@ -11,34 +11,34 @@ public class TypeReflectionTests [InlineData("str", "string")] [InlineData("float", "double")] [InlineData("bool", "bool")] - [InlineData("list[int]", "IReadOnlyCollection")] - [InlineData("list[str]", "IReadOnlyCollection")] - [InlineData("list[float]", "IReadOnlyCollection")] - [InlineData("list[bool]", "IReadOnlyCollection")] - [InlineData("list[object]", "IReadOnlyCollection")] + [InlineData("list[int]", "IReadOnlyList")] + [InlineData("list[str]", "IReadOnlyList")] + [InlineData("list[float]", "IReadOnlyList")] + [InlineData("list[bool]", "IReadOnlyList")] + [InlineData("list[object]", "IReadOnlyList")] [InlineData("tuple[int, int]", "(long,long)")] [InlineData("tuple[str, str]", "(string,string)")] [InlineData("tuple[float, float]", "(double,double)")] [InlineData("tuple[bool, bool]", "(bool,bool)")] [InlineData("tuple[str, Any]", "(string,PyObject)")] - [InlineData("tuple[str, list[int]]", "(string,IReadOnlyCollection)")] + [InlineData("tuple[str, list[int]]", "(string,IReadOnlyList)")] [InlineData("dict[str, int]", "IReadOnlyDictionary")] [InlineData("tuple[int, int, tuple[int, int]]", "(long,long,(long,long))")] public void AsPredefinedType(string pythonType, string expectedType) => ParsingTestInternal(pythonType, expectedType); [Theory] - [InlineData("List[int]", "IReadOnlyCollection")] - [InlineData("List[str]", "IReadOnlyCollection")] - [InlineData("List[float]", "IReadOnlyCollection")] - [InlineData("List[bool]", "IReadOnlyCollection")] - [InlineData("List[object]", "IReadOnlyCollection")] + [InlineData("List[int]", "IReadOnlyList")] + [InlineData("List[str]", "IReadOnlyList")] + [InlineData("List[float]", "IReadOnlyList")] + [InlineData("List[bool]", "IReadOnlyList")] + [InlineData("List[object]", "IReadOnlyList")] [InlineData("Tuple[int, int]", "(long,long)")] [InlineData("Tuple[str, str]", "(string,string)")] [InlineData("Tuple[float, float]", "(double,double)")] [InlineData("Tuple[bool, bool]", "(bool,bool)")] [InlineData("Tuple[str, Any]", "(string,PyObject)")] - [InlineData("Tuple[str, list[int]]", "(string,IReadOnlyCollection)")] + [InlineData("Tuple[str, list[int]]", "(string,IReadOnlyList)")] [InlineData("Dict[str, int]", "IReadOnlyDictionary")] [InlineData("Tuple[int, int, Tuple[int, int]]", "(long,long,(long,long))")] public void AsPredefinedTypeOldTypeNames(string pythonType, string expectedType) => diff --git a/src/CSnakes/Reflection/TypeReflection.cs b/src/CSnakes/Reflection/TypeReflection.cs index 36b6610f..a21fdcd7 100644 --- a/src/CSnakes/Reflection/TypeReflection.cs +++ b/src/CSnakes/Reflection/TypeReflection.cs @@ -70,7 +70,7 @@ private static TypeSyntax CreateTupleType(PythonTypeSpec[] tupleTypes) SyntaxFactory.Token(SyntaxKind.CloseParenToken)); } - private static TypeSyntax CreateListType(PythonTypeSpec genericOf) => CreateGenericType("IReadOnlyCollection", [AsPredefinedType(genericOf)]); + private static TypeSyntax CreateListType(PythonTypeSpec genericOf) => CreateGenericType("IReadOnlyList", [AsPredefinedType(genericOf)]); internal static TypeSyntax CreateGenericType(string typeName, IEnumerable genericArguments) => SyntaxFactory.GenericName( diff --git a/src/Integration.Tests/DictsTests.cs b/src/Integration.Tests/DictsTests.cs index e0072656..f7d010a1 100644 --- a/src/Integration.Tests/DictsTests.cs +++ b/src/Integration.Tests/DictsTests.cs @@ -17,7 +17,7 @@ public void TestDicts_TestDictStrListInt() { var testDicts = Env.TestDicts(); - IReadOnlyDictionary> testListDict = new Dictionary> { { "hello", new List { 1, 2, 3 } }, { "world2", new List { 4, 5, 6 } } }; + IReadOnlyDictionary> testListDict = new Dictionary> { { "hello", new List { 1, 2, 3 } }, { "world2", new List { 4, 5, 6 } } }; var result = testDicts.TestDictStrListInt(testListDict); Assert.Equal(new List { 1, 2, 3 }, result["hello"]); Assert.Equal(new List { 4, 5, 6 }, result["world2"]);