Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion mssql_python/mssql_python.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 15 additions & 9 deletions mssql_python/type.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import datetime
import time
from typing import Union


# Type Objects
Expand Down Expand Up @@ -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
Expand All @@ -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."
)
37 changes: 31 additions & 6 deletions tests/test_002_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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():
Expand Down
Loading