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
11 changes: 6 additions & 5 deletions boto3/dynamodb/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
MAP = 'M'
LIST = 'L'


# Retained for backwards compatibility; serializer/deserializer no longer use
# this context for number handling.
DYNAMODB_CONTEXT = Context(
Emin=-128,
Emax=126,
Expand Down Expand Up @@ -211,10 +212,10 @@ def _serialize_bool(self, value):
return value

def _serialize_n(self, value):
number = str(DYNAMODB_CONTEXT.create_decimal(value))
if number in ['Infinity', 'NaN']:
decimal_value = Decimal(value)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is less familiar code for me, so I'm still trying to reason about all of this.

One alternate solution here could just be removing the rounded trap. Here's the test code I wrote to check it will work:

import decimal
import boto3
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('table-name')

print(table.put_item(Item={
    'primary_key': "test",
    #This breaks with the `decimal.Rounded` trap, but is fine with the `decimal.Inexact trap`,
    # the difference being that inexact will allow "rounding" if it's not losing precision.
    # Adding a 1 on the end would break both as expected
    'precise_number': decimal.Decimal("0.12345678901234567890123456789012345678000000")
}))

print(table.get_item(Key={"primary_key": "test"}))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also shifts where an exception happens, so a customer relying on us to raise any of the former exceptions (such as if the number was too big for DDB to except). For a hamfisted example:

import decimal
import boto3

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('test-table')
biiiiig = "1e500"

try:
    print(table.put_item(Item={
        'primary_key': "test_biiiiig",
        'precise_number': decimal.Decimal(biiiiig)
    }))
except decimal.Overflow as e:
    print("Hi end user, we are sorry but that number is too big for us to store in our database")
    raise


# Before, we would never reach this code path, but now we will start getting a client error.
#  That's not caught above.  
response = table.get_item(Key={"primary_key": "test_biiiiig"})
print("Your number is: " + str(response.get('Item').get('precise_number')))

Outside of this, I think this is a safe change, but I prefer not to break customer's try/except blocks when we can avoid it. What do you think?

if decimal_value.is_nan() or decimal_value.is_infinite():
raise TypeError('Infinity and NaN not supported')
return number
return str(decimal_value)

def _serialize_s(self, value):
return value
Expand Down Expand Up @@ -286,7 +287,7 @@ def _deserialize_bool(self, value):
return value

def _deserialize_n(self, value):
return DYNAMODB_CONTEXT.create_decimal(value)
return Decimal(value)

def _deserialize_s(self, value):
return value
Expand Down
32 changes: 32 additions & 0 deletions tests/unit/dynamodb/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ def test_serialize_NaN_error(self):
with pytest.raises(TypeError, match=r'Infinity and NaN not supported'):
self.serializer.serialize(Decimal('NaN'))

def test_serialize_infinity_error(self):
with pytest.raises(TypeError, match=r'Infinity and NaN not supported'):
self.serializer.serialize(Decimal('-Infinity'))

def test_serialize_string(self):
assert self.serializer.serialize('foo') == {'S': 'foo'}

Expand Down Expand Up @@ -143,6 +147,21 @@ def test_serialize_map(self):
'M': {'foo': {'S': 'bar'}, 'baz': {'M': {'biz': {'N': '1'}}}}
}

def test_serialize_large_number_with_trailing_zeros(self):
assert self.serializer.serialize(
1234567895171680000000000000000000000000
) == {'N': '1234567895171680000000000000000000000000'}

def test_serialize_large_decimal_with_trailing_zeros(self):
assert self.serializer.serialize(
Decimal('1234567895171680000000000000000000000000')
) == {'N': '1234567895171680000000000000000000000000'}

def test_serialize_large_decimal_defers_validation_to_dynamodb(self):
assert self.serializer.serialize(Decimal('9' * 39)) == {
'N': '999999999999999999999999999999999999999'
}


class TestDeserializer(unittest.TestCase):
def setUp(self):
Expand Down Expand Up @@ -206,3 +225,16 @@ def test_deserialize_map(self):
}
}
) == {'foo': 'mystring', 'bar': {'baz': Decimal('1')}}

def test_deserialize_large_number_with_trailing_zeros(self):
# Numbers with trailing zeros that exceed 38 total digits but have
# <=38 significant digits are valid in DynamoDB and must deserialize.
large_num = '1234567895171680000000000000000000000000'
result = self.deserializer.deserialize({'N': large_num})
assert result == Decimal(large_num)
assert str(result) == large_num

def test_deserialize_and_serialize_large_number_with_trailing_zeros(self):
large_num = '1234567895171680000000000000000000000000'
result = self.deserializer.deserialize({'N': large_num})
assert TypeSerializer().serialize(result) == {'N': large_num}
Loading