Skip to content

Commit 5d684d1

Browse files
committed
(improvement) serializers: address PR review feedback
- Chain original exception in _raise_bind_serialize_error (raise from exc) - Change _check_int32_range to raise struct.error instead of OverflowError, matching the behaviour of struct.pack('>i', value) - Clarify docstrings for _check_float_range/_check_int32_range - Expand _raise_bind_serialize_error docstring with specific exception types - Document __getitem__ requirement in vector serialize methods - Move io and uvint_pack imports to module scope in serializers.pyx - Add struct import to serializers.pyx for struct.error - Fix test_plain_path_overflow_error_wrapped docstring (struct.error, not OverflowError) - Update OverflowSerializer stub to raise struct.error - Replace name-mangled __serializers with _cached_serializers
1 parent 25433dd commit 5d684d1

3 files changed

Lines changed: 56 additions & 35 deletions

File tree

cassandra/query.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -552,16 +552,16 @@ def _serializers(self):
552552
if self.column_encryption_policy:
553553
return None
554554
try:
555-
return self.__serializers
555+
return self._cached_serializers
556556
except AttributeError:
557557
pass
558558
if _HAVE_CYTHON_SERIALIZERS and self.column_metadata:
559-
self.__serializers = _cython_make_serializers(
559+
self._cached_serializers = _cython_make_serializers(
560560
[col.type for col in self.column_metadata]
561561
)
562562
else:
563-
self.__serializers = None
564-
return self.__serializers
563+
self._cached_serializers = None
564+
return self._cached_serializers
565565

566566
@classmethod
567567
def from_message(
@@ -663,13 +663,21 @@ def __str__(self):
663663

664664

665665
def _raise_bind_serialize_error(col_spec, value, exc):
666-
"""Wrap serialization errors with column context for all bind loop paths."""
666+
"""Wrap TypeError, struct.error, or OverflowError with column context.
667+
668+
Called from all three bind loop paths (CE, Cython, plain Python) to
669+
provide a uniform error message that includes the column name and
670+
expected type. struct.error arises from int32 out-of-range values;
671+
OverflowError from float out-of-range values. Other exception types
672+
(e.g. ValueError from VectorType dimension mismatch) propagate
673+
without wrapping.
674+
"""
667675
actual_type = type(value)
668676
message = (
669677
'Received an argument of invalid type for column "%s". '
670678
"Expected: %s, Got: %s; (%s)" % (col_spec.name, col_spec.type, actual_type, exc)
671679
)
672-
raise TypeError(message)
680+
raise TypeError(message) from exc
673681

674682

675683
class BoundStatement(Statement):
@@ -826,7 +834,7 @@ def bind(self, values):
826834
else:
827835
col_bytes = col_spec.type.serialize(value, proto_version)
828836
self.values[idx] = col_bytes
829-
# OverflowError: Cython int32/float casts may raise on out-of-range values
837+
# struct.error: int32 out-of-range; OverflowError: float out-of-range
830838
except (TypeError, struct.error, OverflowError) as exc:
831839
_raise_bind_serialize_error(col_spec, value, exc)
832840
idx += 1
@@ -850,7 +858,7 @@ def bind(self, values):
850858
try:
851859
col_bytes = ser.serialize(value, proto_version)
852860
self.values[idx] = col_bytes
853-
# OverflowError: Cython int32/float casts may raise on out-of-range values
861+
# struct.error: int32 out-of-range; OverflowError: float out-of-range
854862
except (TypeError, struct.error, OverflowError) as exc:
855863
_raise_bind_serialize_error(col_spec, value, exc)
856864
idx += 1
@@ -871,7 +879,7 @@ def bind(self, values):
871879
try:
872880
col_bytes = col_spec.type.serialize(value, proto_version)
873881
self.values[idx] = col_bytes
874-
# OverflowError: Cython int32/float casts may raise on out-of-range values
882+
# struct.error: int32 out-of-range; OverflowError: float out-of-range
875883
except (TypeError, struct.error, OverflowError) as exc:
876884
_raise_bind_serialize_error(col_spec, value, exc)
877885
idx += 1

cassandra/serializers.pyx

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ from libc.math cimport isinf, isnan
3434
from cpython.bytes cimport PyBytes_FromStringAndSize
3535

3636
from cassandra import cqltypes
37+
import io
38+
import struct
39+
from cassandra.marshal import uvint_pack
3740

3841
cdef bint is_little_endian
3942
from cassandra.util import is_little_endian
@@ -60,9 +63,9 @@ cdef class Serializer:
6063
cdef inline void _check_float_range(double value) except *:
6164
"""Raise OverflowError for finite values outside float32 range.
6265
63-
This matches the behavior of struct.pack('>f', value), which raises
64-
OverflowError (via struct.error) for values that cannot be represented
65-
as a 32-bit IEEE 754 float. inf, -inf, and nan pass through unchanged.
66+
Matches the behaviour of struct.pack('>f', value), which raises
67+
OverflowError for values that cannot be represented as a 32-bit
68+
IEEE 754 float. inf, -inf, and nan pass through unchanged.
6669
"""
6770
if not isinf(value) and not isnan(value):
6871
if value > <double>FLT_MAX or value < -<double>FLT_MAX:
@@ -76,17 +79,16 @@ cdef inline void _check_float_range(double value) except *:
7679
# ---------------------------------------------------------------------------
7780

7881
cdef inline void _check_int32_range(object value) except *:
79-
"""Raise OverflowError for values outside the signed int32 range.
82+
"""Raise struct.error for values outside the signed int32 range.
8083
81-
This matches the behavior of struct.pack('>i', value), which raises
82-
struct.error for values outside [-2147483648, 2147483647]. The check
83-
must be done on the Python int *before* the C-level <int32_t> cast,
84-
which would silently truncate.
84+
Matches the behaviour of struct.pack('>i', value), which raises
85+
struct.error for out-of-range values. The check must be done on the
86+
Python int *before* the C-level <int32_t> cast, which would silently
87+
truncate.
8588
"""
8689
if value > 2147483647 or value < -2147483648:
87-
raise OverflowError(
88-
"Value %r out of range for int32 "
89-
"(must be between -2147483648 and 2147483647)" % (value,)
90+
raise struct.error(
91+
"'i' format requires -2147483648 <= number <= 2147483647"
9092
)
9193

9294

@@ -222,7 +224,11 @@ cdef class SerVectorType(Serializer):
222224
return self._serialize_generic(value, protocol_version)
223225

224226
cdef inline bytes _serialize_float(self, object values):
225-
"""Serialize a list of floats into a contiguous big-endian buffer."""
227+
"""Serialize a sequence of floats into a contiguous big-endian buffer.
228+
229+
Note: uses index-based access (values[i]) rather than iteration, so
230+
the input must support __getitem__ (e.g. list or tuple).
231+
"""
226232
cdef Py_ssize_t i
227233
cdef Py_ssize_t buf_size = self.vector_size * 4
228234
if buf_size == 0:
@@ -255,7 +261,11 @@ cdef class SerVectorType(Serializer):
255261
free(buf)
256262

257263
cdef inline bytes _serialize_double(self, object values):
258-
"""Serialize a list of doubles into a contiguous big-endian buffer."""
264+
"""Serialize a sequence of doubles into a contiguous big-endian buffer.
265+
266+
Note: uses index-based access (values[i]) rather than iteration, so
267+
the input must support __getitem__ (e.g. list or tuple).
268+
"""
259269
cdef Py_ssize_t i
260270
cdef Py_ssize_t buf_size = self.vector_size * 8
261271
if buf_size == 0:
@@ -291,7 +301,11 @@ cdef class SerVectorType(Serializer):
291301
free(buf)
292302

293303
cdef inline bytes _serialize_int32(self, object values):
294-
"""Serialize a list of int32 values into a contiguous big-endian buffer."""
304+
"""Serialize a sequence of int32 values into a contiguous big-endian buffer.
305+
306+
Note: uses index-based access (values[i]) rather than iteration, so
307+
the input must support __getitem__ (e.g. list or tuple).
308+
"""
295309
cdef Py_ssize_t i
296310
cdef Py_ssize_t buf_size = self.vector_size * 4
297311
if buf_size == 0:
@@ -325,9 +339,6 @@ cdef class SerVectorType(Serializer):
325339

326340
cdef inline bytes _serialize_generic(self, object values, int protocol_version):
327341
"""Fallback: element-by-element Python serialization for non-optimized types."""
328-
import io
329-
from cassandra.marshal import uvint_pack
330-
331342
serialized_size = self.subtype.serial_size()
332343
buf = io.BytesIO()
333344
for item in values:

tests/unit/test_parameter_binding.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
import unittest
16+
import struct
1617
import pytest
1718

1819
from cassandra.encoder import Encoder
@@ -244,20 +245,20 @@ def serialize(self, value, protocol_version):
244245

245246

246247
class OverflowSerializer:
247-
"""Stub that raises OverflowError, mimicking Cython <int32_t> cast overflow."""
248+
"""Stub that raises struct.error, mimicking Cython int32 range check."""
248249

249250
def __init__(self, cqltype):
250251
self.cqltype = cqltype
251252

252253
def serialize(self, value, protocol_version):
253-
raise OverflowError("value too large to convert to int32_t")
254+
raise struct.error("'i' format requires -2147483648 <= number <= 2147483647")
254255

255256

256257
class CythonBindPathTest(unittest.TestCase):
257258
"""Tests for the Cython serializer fast path in BoundStatement.bind().
258259
259260
These tests inject stub serializers via the PreparedStatement's cached
260-
__serializers attribute to exercise the Cython bind branch without
261+
_cached_serializers attribute to exercise the Cython bind branch without
261262
requiring compiled Cython.
262263
"""
263264

@@ -275,9 +276,9 @@ def _make_prepared(self, column_metadata, serializers=None):
275276
result_metadata=None,
276277
result_metadata_id=None,
277278
)
278-
# Inject directly into the name-mangled cache attribute used by
279-
# the _serializers property, bypassing the lazy initialization.
280-
prepared._PreparedStatement__serializers = serializers
279+
# Inject directly into the cache attribute used by the _serializers
280+
# property, bypassing the lazy initialization.
281+
prepared._cached_serializers = serializers
281282
return prepared
282283

283284
def test_cython_path_normal_serialization(self):
@@ -321,7 +322,7 @@ def test_cython_path_unset_value(self):
321322
assert bound.values[1] == UNSET_VALUE
322323

323324
def test_cython_path_overflow_error_wrapped(self):
324-
"""OverflowError from Cython cast is caught and wrapped with column context."""
325+
"""struct.error from Cython int32 range check is caught and wrapped with column context."""
325326
column_metadata = [ColumnMetadata("keyspace", "cf", "v0", Int32Type)]
326327
serializers = [OverflowSerializer(Int32Type)]
327328
prepared = self._make_prepared(column_metadata, serializers)
@@ -348,7 +349,8 @@ def test_cython_path_type_error_wrapped(self):
348349
assert "Int32Type" in msg
349350

350351
def test_plain_path_overflow_error_wrapped(self):
351-
"""OverflowError in the plain Python path is also caught and wrapped."""
352+
"""Out-of-range int in the plain Python path raises struct.error (caught
353+
alongside OverflowError) and is wrapped with column context."""
352354
column_metadata = [ColumnMetadata("keyspace", "cf", "v0", Int32Type)]
353355
# Force the plain Python path (no Cython serializers)
354356
prepared = self._make_prepared(column_metadata, serializers=None)
@@ -383,7 +385,7 @@ def _make_prepared(
383385
result_metadata=None,
384386
result_metadata_id=None,
385387
)
386-
prepared._PreparedStatement__serializers = serializers
388+
prepared._cached_serializers = serializers
387389
return prepared
388390

389391
def _three_column_metadata(self):

0 commit comments

Comments
 (0)