From 1afa5cf6f73d4e9d88df6b6c94a2804c14a0db28 Mon Sep 17 00:00:00 2001 From: Gaurav Sharma <223556219+Copilot@users.noreply.github.com> Date: Wed, 2 Sep 2026 15:14:48 +0530 Subject: [PATCH 1/3] FIX: accept memoryview in Binary() Binary() rejected memoryview with a TypeError, so any DB-API caller that hands the driver a buffer-protocol value failed. Django's BinaryField gives the driver a memoryview, so BinaryField writes and its serializer roundtrips all broke. Accept memoryview via tobytes(), matching pyodbc and the DB-API convention. (GH-739) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- mssql_python/type.py | 21 +++++++++++++-------- tests/test_002_types.py | 37 +++++++++++++++++++++++++++++++------ 2 files changed, 44 insertions(+), 14 deletions(-) diff --git a/mssql_python/type.py b/mssql_python/type.py index 157c6e2f3..862269ba3 100644 --- a/mssql_python/type.py +++ b/mssql_python/type.py @@ -122,13 +122,15 @@ def TimestampFromTicks(ticks: int) -> datetime.datetime: def Binary(value) -> bytes: """ - Converts a string or bytes to bytes for use with binary database columns. + Converts a string or bytes-like object to bytes for binary database columns. - This function follows the DB-API 2.0 specification. - It accepts only str and bytes/bytearray types to ensure type safety. + This function follows the DB-API 2.0 specification. It accepts str, + bytes, bytearray, and memoryview. memoryview is accepted so buffer-protocol + values (for example the memoryview Django hands a BinaryField) round-trip the + same way pyodbc handles them. Args: - value: A string (str) or bytes-like object (bytes, bytearray) + value: A string (str) or bytes-like object (bytes, bytearray, memoryview) Returns: bytes: The input converted to bytes @@ -137,18 +139,21 @@ def Binary(value) -> bytes: TypeError: If the input type is not supported Examples: - Binary("hello") # Returns b"hello" - Binary(b"hello") # Returns b"hello" - Binary(bytearray(b"hi")) # Returns b"hi" + Binary("hello") # Returns b"hello" + Binary(b"hello") # Returns b"hello" + Binary(bytearray(b"hi")) # Returns b"hi" + Binary(memoryview(b"hi")) # Returns b"hi" """ if isinstance(value, bytes): return value if isinstance(value, bytearray): return bytes(value) + if isinstance(value, memoryview): + return value.tobytes() if isinstance(value, str): return value.encode("utf-8") # Raise TypeError for unsupported types to improve type safety raise TypeError( f"Cannot convert type {type(value).__name__} to bytes. " - f"Binary() only accepts str, bytes, or bytearray objects." + f"Binary() only accepts str, bytes, bytearray, or memoryview objects." ) diff --git a/tests/test_002_types.py b/tests/test_002_types.py index 4828d72ea..8d92882d3 100644 --- a/tests/test_002_types.py +++ b/tests/test_002_types.py @@ -122,37 +122,60 @@ def test_binary_string_encoding(): assert result == b"Hello\nWorld\t!", "String with special characters should encode properly" +def test_binary_memoryview(): + """Binary() accepts memoryview (GH-739): Django BinaryField and DB-API buffers.""" + result = Binary(memoryview(b"\x01\x02\x03")) + assert isinstance(result, bytes), "memoryview should be converted to bytes" + assert result == b"\x01\x02\x03", "memoryview content should be preserved" + + # Empty memoryview + assert Binary(memoryview(b"")) == b"" + + # memoryview over a bytearray round-trips the same bytes + assert Binary(memoryview(bytearray(b"hello"))) == b"hello" + + def test_binary_unsupported_types_error(): """Test Binary() TypeError for unsupported types (Lines 138-141).""" # Test integer type with pytest.raises(TypeError) as exc_info: Binary(123) assert "Cannot convert type int to bytes" in str(exc_info.value) - assert "Binary() only accepts str, bytes, or bytearray objects" in str(exc_info.value) + assert "Binary() only accepts str, bytes, bytearray, or memoryview objects" in str( + exc_info.value + ) # Test float type with pytest.raises(TypeError) as exc_info: Binary(3.14) assert "Cannot convert type float to bytes" in str(exc_info.value) - assert "Binary() only accepts str, bytes, or bytearray objects" in str(exc_info.value) + assert "Binary() only accepts str, bytes, bytearray, or memoryview objects" in str( + exc_info.value + ) # Test list type with pytest.raises(TypeError) as exc_info: Binary([1, 2, 3]) assert "Cannot convert type list to bytes" in str(exc_info.value) - assert "Binary() only accepts str, bytes, or bytearray objects" in str(exc_info.value) + assert "Binary() only accepts str, bytes, bytearray, or memoryview objects" in str( + exc_info.value + ) # Test dict type with pytest.raises(TypeError) as exc_info: Binary({"key": "value"}) assert "Cannot convert type dict to bytes" in str(exc_info.value) - assert "Binary() only accepts str, bytes, or bytearray objects" in str(exc_info.value) + assert "Binary() only accepts str, bytes, bytearray, or memoryview objects" in str( + exc_info.value + ) # Test None type with pytest.raises(TypeError) as exc_info: Binary(None) assert "Cannot convert type NoneType to bytes" in str(exc_info.value) - assert "Binary() only accepts str, bytes, or bytearray objects" in str(exc_info.value) + assert "Binary() only accepts str, bytes, bytearray, or memoryview objects" in str( + exc_info.value + ) # Test custom object type class CustomObject: @@ -161,7 +184,9 @@ class CustomObject: with pytest.raises(TypeError) as exc_info: Binary(CustomObject()) assert "Cannot convert type CustomObject to bytes" in str(exc_info.value) - assert "Binary() only accepts str, bytes, or bytearray objects" in str(exc_info.value) + assert "Binary() only accepts str, bytes, bytearray, or memoryview objects" in str( + exc_info.value + ) def test_binary_comprehensive_coverage(): From ac46eae18e46c2ac4c419ca8aac5c44d24be80c7 Mon Sep 17 00:00:00 2001 From: Gaurav Sharma <223556219+Copilot@users.noreply.github.com> Date: Wed, 2 Sep 2026 15:23:50 +0530 Subject: [PATCH 2/3] CHORE: add memoryview to Binary() type stub Match the runtime signature so type checkers accept Binary(memoryview(...)). (GH-739) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- mssql_python/mssql_python.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mssql_python/mssql_python.pyi b/mssql_python/mssql_python.pyi index 5df22d203..cefdbc6f7 100644 --- a/mssql_python/mssql_python.pyi +++ b/mssql_python/mssql_python.pyi @@ -92,7 +92,7 @@ def Timestamp( def DateFromTicks(ticks: int) -> datetime.date: ... def TimeFromTicks(ticks: int) -> datetime.time: ... def TimestampFromTicks(ticks: int) -> datetime.datetime: ... -def Binary(value: Union[str, bytes, bytearray]) -> bytes: ... +def Binary(value: Union[str, bytes, bytearray, memoryview]) -> bytes: ... # DB-API 2.0 Exception Hierarchy # https://www.python.org/dev/peps/pep-0249/#exceptions From 44582ba7e2de69f118d4d98fa017b96ee7cf50ff Mon Sep 17 00:00:00 2001 From: Gaurav Sharma <223556219+Copilot@users.noreply.github.com> Date: Wed, 2 Sep 2026 16:41:49 +0530 Subject: [PATCH 3/3] CHORE: annotate Binary() runtime signature type.py ships py.typed, so annotate the runtime parameter directly (Union[str, bytes, bytearray, memoryview]) to make the accepted-input contract explicit at the source, matching the stub. (GH-739) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- mssql_python/type.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mssql_python/type.py b/mssql_python/type.py index 862269ba3..2b1b392d1 100644 --- a/mssql_python/type.py +++ b/mssql_python/type.py @@ -6,6 +6,7 @@ import datetime import time +from typing import Union # Type Objects @@ -120,7 +121,7 @@ def TimestampFromTicks(ticks: int) -> datetime.datetime: return datetime.datetime.fromtimestamp(ticks) -def Binary(value) -> bytes: +def Binary(value: Union[str, bytes, bytearray, memoryview]) -> bytes: """ Converts a string or bytes-like object to bytes for binary database columns.