diff --git a/mssql_python/mssql_python.pyi b/mssql_python/mssql_python.pyi index 5df22d20..cefdbc6f 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 diff --git a/mssql_python/type.py b/mssql_python/type.py index 157c6e2f..2b1b392d 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,15 +121,17 @@ 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 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 +140,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 4828d72e..8d92882d 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():