From 0b1c0ee88c4bcf1cf2deb263536cfd3df829944f Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Thu, 10 Sep 2026 22:27:28 +0200 Subject: [PATCH] fix(python): support bytes() on generic and bool arrays __bytes__ returned NotImplemented for generic and bool storage, and since bytes() rejects a non-bytes result instead of falling back to the iterable protocol, Guid(byte[]), Convert.ToBase64String and Encoding.GetString all failed whenever the array came from a generic function. --- src/fable-library-py/src/array.rs | 13 ++++++--- src/fable-library-py/tests/test_array.py | 35 ++++++++++++++++++++++++ tests/Python/TestConvert.fs | 8 ++++++ tests/Python/TestGuid.fs | 10 +++++++ 4 files changed, 62 insertions(+), 4 deletions(-) diff --git a/src/fable-library-py/src/array.rs b/src/fable-library-py/src/array.rs index 4e7ff4e4d8..8d68d631f3 100644 --- a/src/fable-library-py/src/array.rs +++ b/src/fable-library-py/src/array.rs @@ -665,8 +665,6 @@ impl FSharpArray { /// For Int8/UInt8 arrays, the result is a direct byte representation. /// For larger types (Int16, Int32, Float64, etc.), each element occupies /// multiple bytes in native byte order. - /// - /// Returns NotImplemented for String and PyObject arrays. pub fn __bytes__(&self, py: Python<'_>) -> PyResult> { match &self.storage { // For UInt8/Int8 arrays, we can create bytes directly @@ -756,8 +754,15 @@ impl FSharpArray { }); Ok(bytes.into()) } - // For non-numeric types, return NotImplemented - _ => Ok(py.NotImplemented()), + NativeArray::PyObject(vec) => { + let list = PyList::new(py, vec.iter().map(|item| item.bind(py)))?; + Ok(py.get_type::().call1((list,))?.unbind()) + } + NativeArray::Bool(vec) => { + let bytes = + PyBytes::new(py, &vec.iter().map(|b| u8::from(*b)).collect::>()); + Ok(bytes.into()) + } } } diff --git a/src/fable-library-py/tests/test_array.py b/src/fable-library-py/tests/test_array.py index cefdcf7d79..15224ef478 100644 --- a/src/fable-library-py/tests/test_array.py +++ b/src/fable-library-py/tests/test_array.py @@ -1646,3 +1646,38 @@ def test_generic_copy_round_trip(elements: list[int]) -> None: if elements: copied[0] = "changed" assert list(arr) == list(elements) + + +def test_bytes_from_generic_array() -> None: + arr = Array(array_type="Generic", elements=[byte(1), byte(2), byte(255)]) + assert bytes(arr) == b"\x01\x02\xff" + + +def test_bytes_from_empty_generic_array() -> None: + assert bytes(Array(array_type="Generic", elements=[])) == b"" + + +@pytest.mark.parametrize( + "elements, error", + [ + (["a"], TypeError), + ([None], TypeError), + ([1.5], TypeError), + ([300], ValueError), + ([-1], ValueError), + ], +) +def test_bytes_from_generic_array_rejects_non_bytes(elements: list[Any], error: type[Exception]) -> None: + arr = Array(array_type="Generic", elements=elements) + with pytest.raises(error): + bytes(arr) + + +def test_bytes_from_bool_array() -> None: + arr = Array(array_type="Bool", elements=[True, False, True]) + assert bytes(arr) == b"\x01\x00\x01" + + +def test_bytes_from_bool_array_matches_generic_array() -> None: + elements = [True, False, True] + assert bytes(Array(array_type="Bool", elements=elements)) == bytes(Array(array_type="Generic", elements=elements)) diff --git a/tests/Python/TestConvert.fs b/tests/Python/TestConvert.fs index 0ab2b74a89..95dba9c4cb 100644 --- a/tests/Python/TestConvert.fs +++ b/tests/Python/TestConvert.fs @@ -1056,6 +1056,14 @@ let ``test Convert.ToBase64String works`` () = Convert.ToBase64String(bytes) |> equal "AgQGCAoMDhASFA==" +let private genericOfList<'T> (xs: 'T list) : 'T[] = Array.ofList xs + +[] +let ``test Convert.ToBase64String works on an array built by a generic function`` () = + let bytes = genericOfList [ 2uy; 4uy; 6uy; 8uy; 10uy; 12uy; 14uy; 16uy; 18uy; 20uy ] + Convert.ToBase64String(bytes) + |> equal "AgQGCAoMDhASFA==" + [] let ``test Convert.FromBase64String works`` () = Convert.FromBase64String("AgQGCAoMDhASFA==") diff --git a/tests/Python/TestGuid.fs b/tests/Python/TestGuid.fs index f58410474b..16cfc2ca5e 100644 --- a/tests/Python/TestGuid.fs +++ b/tests/Python/TestGuid.fs @@ -164,6 +164,16 @@ let ``test Convert byte array built from a list or seq to Guid works`` () = Guid(Array.ofList bytes).ToString() |> equal "96258006-c4ba-4a7f-80c4-de7f2b2898c5" Guid(Array.ofSeq (Seq.ofList bytes)).ToString() |> equal "96258006-c4ba-4a7f-80c4-de7f2b2898c5" +let private genericOfList<'T> (xs: 'T list) : 'T[] = Array.ofList xs + +let private genericOfSeq<'T> (xs: 'T seq) : 'T[] = Array.ofSeq xs + +[] +let ``test Convert byte array built by a generic function to Guid works`` () = + let bytes = id [6uy; 128uy; 37uy; 150uy; 186uy; 196uy; 127uy; 74uy; 128uy; 196uy; 222uy; 127uy; 43uy; 40uy; 152uy; 197uy] + Guid(genericOfList bytes).ToString() |> equal "96258006-c4ba-4a7f-80c4-de7f2b2898c5" + Guid(genericOfSeq (Seq.ofList bytes)).ToString() |> equal "96258006-c4ba-4a7f-80c4-de7f2b2898c5" + // [] // let ``Guid.ToString works with formats`` () = // let g = Guid.Parse("96258006-c4ba-4a7f-80c4-de7f2b2898c5")