diff --git a/boto3/dynamodb/types.py b/boto3/dynamodb/types.py index f358b12f55..2ee9925072 100644 --- a/boto3/dynamodb/types.py +++ b/boto3/dynamodb/types.py @@ -41,6 +41,15 @@ traps=[Clamped, Overflow, Inexact, Rounded, Underflow], ) +# Context used to normalize numbers before applying DYNAMODB_CONTEXT. +# This removes trailing zeros so that a number like +# '1234567895171680000000000000000000000000' (16 significant digits but +# 40 characters) is first normalized to '1.23456789517168E+39' before +# being validated against the 38-digit precision limit. The wider +# precision (100) ensures that normalization itself never loses +# significant digits. +_NORMALIZE_CONTEXT = Context(Emin=-128, Emax=126, prec=100, traps=[]) + BINARY_TYPES = (bytearray, bytes) @@ -211,7 +220,11 @@ def _serialize_bool(self, value): return value def _serialize_n(self, value): - number = str(DYNAMODB_CONTEXT.create_decimal(value)) + number = str( + DYNAMODB_CONTEXT.create_decimal( + _NORMALIZE_CONTEXT.normalize(Decimal(value)) + ) + ) if number in ['Infinity', 'NaN']: raise TypeError('Infinity and NaN not supported') return number @@ -286,7 +299,9 @@ def _deserialize_bool(self, value): return value def _deserialize_n(self, value): - return DYNAMODB_CONTEXT.create_decimal(value) + return DYNAMODB_CONTEXT.create_decimal( + _NORMALIZE_CONTEXT.normalize(Decimal(value)) + ) def _deserialize_s(self, value): return value diff --git a/tests/unit/dynamodb/test_types.py b/tests/unit/dynamodb/test_types.py index 0a6c2a07f3..3a44f1927b 100644 --- a/tests/unit/dynamodb/test_types.py +++ b/tests/unit/dynamodb/test_types.py @@ -144,6 +144,20 @@ def test_serialize_map(self): } + def test_serialize_integer_with_trailing_zeros(self): + # Integers with trailing zeros whose string representation exceeds + # 38 characters but whose significant digits fit within the 38-digit + # precision limit should serialize without error. See GitHub #4693. + assert self.serializer.serialize( + 1234567895171680000000000000000000000000 + ) == {'N': '1.23456789517168E+39'} + + def test_serialize_decimal_with_trailing_zeros(self): + assert self.serializer.serialize( + Decimal('1234567895171680000000000000000000000000') + ) == {'N': '1.23456789517168E+39'} + + class TestDeserializer(unittest.TestCase): def setUp(self): self.deserializer = TypeDeserializer() @@ -206,3 +220,22 @@ def test_deserialize_map(self): } } ) == {'foo': 'mystring', 'bar': {'baz': Decimal('1')}} + + def test_deserialize_number_with_trailing_zeros(self): + # Numbers with trailing zeros whose string representation exceeds + # 38 characters but whose significant digits fit within the 38-digit + # precision limit should deserialize without error. See GitHub #4693. + assert self.deserializer.deserialize( + {'N': '1234567895171680000000000000000000000000'} + ) == Decimal('1.23456789517168E+39') + + def test_deserialize_negative_number_with_trailing_zeros(self): + assert self.deserializer.deserialize( + {'N': '-1234567895171680000000000000000000000000'} + ) == Decimal('-1.23456789517168E+39') + + def test_deserialize_number_set_with_trailing_zeros(self): + assert self.deserializer.deserialize( + {'NS': ['1234567895171680000000000000000000000000', '1']} + ) == {Decimal('1.23456789517168E+39'), Decimal('1')} +