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
19 changes: 17 additions & 2 deletions boto3/dynamodb/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
33 changes: 33 additions & 0 deletions tests/unit/dynamodb/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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')}