Skip to content

Commit 6077554

Browse files
jahnvi480sumitmsft
andauthored
FIX: Making type objects and constructor compatible with pyodbc (#157)
### Work Item / Issue Reference <!-- IMPORTANT: Please follow the PR template guidelines below. For mssql-python maintainers: Insert your ADO Work Item ID below (e.g. AB#37452) For external contributors: Insert Github Issue number below (e.g. #149) Only one reference is required - either GitHub issue OR ADO Work Item. --> <!-- mssql-python maintainers: ADO Work Item --> > [AB#38059](https://sqlclientdrivers.visualstudio.com/c6d89619-62de-46a0-8b46-70b92a84d85e/_workitems/edit/38059) ------------------------------------------------------------------- ### Summary This pull request refactors type objects in `mssql_python/type.py` to inherit from their respective Python built-in types, simplifies constructors, and updates related tests to align with the new implementation. Additionally, it modifies timestamp and time handling functions for better compatibility and enhances the `Binary` function to handle multiple input types. ### Refactoring of type objects: - [`mssql_python/type.py`](diffhunk://#diff-57fcb124a0b5958b9034fbd0c3ee1bc65e4140e1671e642d5dcb5aabd60d55a7L12-R55): Changed type classes (`STRING`, `BINARY`, `NUMBER`, `DATETIME`, `ROWID`) to inherit from Python built-in types (`str`, `bytearray`, `float`, `datetime.datetime`, `int`) and replaced `__init__` methods with `__new__` methods for direct instantiation. ### Updates to utility functions: - [`mssql_python/type.py`](diffhunk://#diff-57fcb124a0b5958b9034fbd0c3ee1bc65e4140e1671e642d5dcb5aabd60d55a7L93-R113): Modified `TimeFromTicks` to use `time.localtime` instead of `time.gmtime` and updated `TimestampFromTicks` to remove the UTC timezone. Enhanced `Binary` to handle both `str` and `bytes` inputs, with fallback for other types by converting to string first. ### Updates to tests: - [`tests/test_002_types.py`](diffhunk://#diff-15437630102d01c37a02763e0080246da102ccedaeea931d7c433470ff0fb009L6-R19): Updated tests for type objects (`STRING`, `BINARY`, `NUMBER`, `DATETIME`, `ROWID`) to validate against their respective Python built-in types instead of custom attributes. - [`tests/test_002_types.py`](diffhunk://#diff-15437630102d01c37a02763e0080246da102ccedaeea931d7c433470ff0fb009L47-R58): Adjusted `test_time_from_ticks` and `test_timestamp_from_ticks` to reflect changes in `TimeFromTicks` and `TimestampFromTicks` behavior. Improved `test_binary_constructor` to test compatibility with both `bytes` and `bytearray`. --------- Co-authored-by: Jahnvi Thakkar <jathakkar@microsoft.com> Co-authored-by: Sumit Sarabhai <sumitsar@microsoft.com>
1 parent 03c3670 commit 6077554

File tree

2 files changed

+66
-38
lines changed

2 files changed

+66
-38
lines changed

mssql_python/type.py

Lines changed: 51 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,50 +9,50 @@
99

1010

1111
# Type Objects
12-
class STRING:
12+
class STRING(str):
1313
"""
1414
This type object is used to describe columns in a database that are string-based (e.g. CHAR).
1515
"""
1616

17-
def __init__(self) -> None:
18-
self.type = "STRING"
17+
def __new__(cls):
18+
return str.__new__(cls, "")
1919

2020

21-
class BINARY:
21+
class BINARY(bytearray):
2222
"""
2323
This type object is used to describe (long)
2424
binary columns in a database (e.g. LONG, RAW, BLOBs).
2525
"""
2626

27-
def __init__(self) -> None:
28-
self.type = "BINARY"
27+
def __new__(cls):
28+
return bytearray.__new__(cls)
2929

3030

31-
class NUMBER:
31+
class NUMBER(float):
3232
"""
3333
This type object is used to describe numeric columns in a database.
3434
"""
3535

36-
def __init__(self) -> None:
37-
self.type = "NUMBER"
36+
def __new__(cls):
37+
return float.__new__(cls, 0.0)
3838

3939

40-
class DATETIME:
40+
class DATETIME(datetime.datetime):
4141
"""
4242
This type object is used to describe date/time columns in a database.
4343
"""
4444

45-
def __init__(self) -> None:
46-
self.type = "DATETIME"
45+
def __new__(cls, year: int = 1, month: int = 1, day: int = 1):
46+
return datetime.datetime.__new__(cls, year, month, day)
4747

4848

49-
class ROWID:
49+
class ROWID(int):
5050
"""
51-
This type object is used to describe the Row ID column in a database.
51+
This type object is used to describe the "Row ID" column in a database.
5252
"""
5353

54-
def __init__(self) -> None:
55-
self.type = "ROWID"
54+
def __new__(cls):
55+
return int.__new__(cls, 0)
5656

5757

5858
# Type Constructors
@@ -90,18 +90,44 @@ def TimeFromTicks(ticks: int) -> datetime.time:
9090
"""
9191
Generates a time object from ticks.
9292
"""
93-
return datetime.time(*time.gmtime(ticks)[3:6])
93+
return datetime.time(*time.localtime(ticks)[3:6])
9494

9595

9696
def TimestampFromTicks(ticks: int) -> datetime.datetime:
9797
"""
9898
Generates a timestamp object from ticks.
9999
"""
100-
return datetime.datetime.fromtimestamp(ticks, datetime.timezone.utc)
101-
102-
103-
def Binary(string: str) -> bytes:
104-
"""
105-
Converts a string to bytes using UTF-8 encoding.
106-
"""
107-
return bytes(string, "utf-8")
100+
return datetime.datetime.fromtimestamp(ticks)
101+
102+
103+
def Binary(value) -> bytes:
104+
"""
105+
Converts a string or bytes to bytes for use with binary database columns.
106+
107+
This function follows the DB-API 2.0 specification and pyodbc compatibility.
108+
It accepts only str and bytes/bytearray types to ensure type safety.
109+
110+
Args:
111+
value: A string (str) or bytes-like object (bytes, bytearray)
112+
113+
Returns:
114+
bytes: The input converted to bytes
115+
116+
Raises:
117+
TypeError: If the input type is not supported
118+
119+
Examples:
120+
Binary("hello") # Returns b"hello"
121+
Binary(b"hello") # Returns b"hello"
122+
Binary(bytearray(b"hi")) # Returns b"hi"
123+
"""
124+
if isinstance(value, bytes):
125+
return value
126+
elif isinstance(value, bytearray):
127+
return bytes(value)
128+
elif isinstance(value, str):
129+
return value.encode("utf-8")
130+
else:
131+
# Raise TypeError for unsupported types to improve type safety
132+
raise TypeError(f"Cannot convert type {type(value).__name__} to bytes. "
133+
f"Binary() only accepts str, bytes, or bytearray objects.")

tests/test_002_types.py

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
import pytest
22
import datetime
3+
import time
34
from mssql_python.type import STRING, BINARY, NUMBER, DATETIME, ROWID, Date, Time, Timestamp, DateFromTicks, TimeFromTicks, TimestampFromTicks, Binary
45

56
def test_string_type():
6-
assert STRING().type == "STRING", "STRING type mismatch"
7+
assert STRING() == str(), "STRING type mismatch"
8+
79

810
def test_binary_type():
9-
assert BINARY().type == "BINARY", "BINARY type mismatch"
11+
assert BINARY() == bytearray(), "BINARY type mismatch"
1012

1113
def test_number_type():
12-
assert NUMBER().type == "NUMBER", "NUMBER type mismatch"
14+
assert NUMBER() == float(), "NUMBER type mismatch"
1315

1416
def test_datetime_type():
15-
assert DATETIME().type == "DATETIME", "DATETIME type mismatch"
17+
assert DATETIME(2025, 1, 1) == datetime.datetime(2025, 1, 1), "DATETIME type mismatch"
1618

1719
def test_rowid_type():
18-
assert ROWID().type == "ROWID", "ROWID type mismatch"
20+
assert ROWID() == int(), "ROWID type mismatch"
1921

2022
def test_date_constructor():
2123
date = Date(2023, 10, 5)
@@ -41,18 +43,18 @@ def test_date_from_ticks():
4143
assert date == datetime.date(2023, 10, 5), "DateFromTicks returned incorrect date"
4244

4345
def test_time_from_ticks():
44-
ticks = 1696500000 # Corresponds to 10:00:00
45-
time = TimeFromTicks(ticks)
46-
assert isinstance(time, datetime.time), "TimeFromTicks did not return a time object"
47-
assert time == datetime.time(10, 0, 0), "TimeFromTicks returned incorrect time"
46+
ticks = 1696500000 # Corresponds to local
47+
time_var = TimeFromTicks(ticks)
48+
assert isinstance(time_var, datetime.time), "TimeFromTicks did not return a time object"
49+
assert time_var == datetime.time(*time.localtime(ticks)[3:6]), "TimeFromTicks returned incorrect time"
4850

4951
def test_timestamp_from_ticks():
50-
ticks = 1696500000 # Corresponds to 2023-10-05 10:00:00
52+
ticks = 1696500000 # Corresponds to 2023-10-05 local time
5153
timestamp = TimestampFromTicks(ticks)
5254
assert isinstance(timestamp, datetime.datetime), "TimestampFromTicks did not return a datetime object"
53-
assert timestamp == datetime.datetime(2023, 10, 5, 10, 0, 0, tzinfo=datetime.timezone.utc), "TimestampFromTicks returned incorrect timestamp"
55+
assert timestamp == datetime.datetime.fromtimestamp(ticks), "TimestampFromTicks returned incorrect timestamp"
5456

5557
def test_binary_constructor():
56-
binary = Binary("test")
57-
assert isinstance(binary, bytes), "Binary constructor did not return a bytes object"
58+
binary = Binary("test".encode('utf-8'))
59+
assert isinstance(binary, (bytes, bytearray)), "Binary constructor did not return a bytes object"
5860
assert binary == b"test", "Binary constructor returned incorrect bytes"

0 commit comments

Comments
 (0)