From cd69b3a221a3932bc07933cd63c2adecd1893e30 Mon Sep 17 00:00:00 2001 From: Dominik Deren Date: Tue, 16 Sep 2025 10:43:43 +0200 Subject: [PATCH 1/3] Fix get method in models.py to not throw an exception when returning an empty item. Fix for issue https://github.com/pynamodb/PynamoDB/issues/1287#issuecomment-3281642349 When getting a model value with `attributes_to_get` parameter, set in a way where it could return an empty object, the get method is erronously throwing an exception even though the object exists. This fixes the issue. --- pynamodb/models.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pynamodb/models.py b/pynamodb/models.py index c75e99b3..cfbab057 100644 --- a/pynamodb/models.py +++ b/pynamodb/models.py @@ -549,9 +549,8 @@ def get( attributes_to_get=attributes_to_get, ) if data: - item_data = data.get(ITEM) - if item_data: - return cls.from_raw_data(item_data) + if ITEM in data: + return cls.from_raw_data(data.get(ITEM)) raise cls.DoesNotExist() @classmethod From 75442807e372021f4abe81448e7557fd91a4eff9 Mon Sep 17 00:00:00 2001 From: Dominik Deren Date: Tue, 16 Sep 2025 23:18:02 +0200 Subject: [PATCH 2/3] fix: shortening the check in get method, and adding unit tests for new behavior. --- pynamodb/models.py | 5 ++-- tests/test_model.py | 62 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/pynamodb/models.py b/pynamodb/models.py index cfbab057..7dc19584 100644 --- a/pynamodb/models.py +++ b/pynamodb/models.py @@ -548,9 +548,8 @@ def get( consistent_read=consistent_read, attributes_to_get=attributes_to_get, ) - if data: - if ITEM in data: - return cls.from_raw_data(data.get(ITEM)) + if data and ITEM in data: + return cls.from_raw_data(data.get(ITEM)) raise cls.DoesNotExist() @classmethod diff --git a/tests/test_model.py b/tests/test_model.py index da54303b..79a65c5c 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -1752,6 +1752,68 @@ def fake_dynamodb(*args): self.assertEqual(item.overidden_user_name, CUSTOM_ATTR_NAME_ITEM_DATA['Item']['user_name']['S']) self.assertEqual(item.overidden_user_id, CUSTOM_ATTR_NAME_ITEM_DATA['Item']['user_id']['S']) + def test_get_with_empty_item_from_attributes_to_get(self): + """ + Test that get method doesn't raise DoesNotExist when attributes_to_get + returns an empty Item dictionary but the item exists in DynamoDB. + """ + + def fake_dynamodb(*args): + kwargs = args[1] + if kwargs == {'TableName': UserModel.Meta.table_name}: + return MODEL_TABLE_DATA + elif 'Key' in kwargs and 'ProjectionExpression' in kwargs: + # This simulates DynamoDB returning an empty Item when the requested + # attributes don't exist or are empty, but the item itself exists + return { + 'Item': {} # Empty item dictionary + } + return MODEL_TABLE_DATA + + fake_db = MagicMock() + fake_db.side_effect = fake_dynamodb + + with patch(PATCH_METHOD, new=fake_db) as req: + # This should not raise DoesNotExist even though the Item is empty + # because the item exists in DynamoDB, just the requested attributes are empty + try: + item = UserModel.get( + 'foo', + 'bar', + attributes_to_get=['nonexistent_attribute'] + ) + # The item should be returned with empty attributes + self.assertIsNotNone(item) + # Verify that the get method was called with the right parameters + self.assertTrue(req.called) + call_args = req.call_args[0][1] + self.assertIn('Key', call_args) + self.assertIn('ProjectionExpression', call_args) + except DoesNotExist: + self.fail("get method should not raise DoesNotExist when DynamoDB returns empty Item but item exists") + + def test_get_with_no_item_still_raises_does_not_exist(self): + """ + Test that get method still raises DoesNotExist when no item is returned from DynamoDB + """ + + def fake_dynamodb(*args): + kwargs = args[1] + if kwargs == {'TableName': UserModel.Meta.table_name}: + return MODEL_TABLE_DATA + elif 'Key' in kwargs: + # This simulates DynamoDB returning no item at all + return {} # No Item key in response + return MODEL_TABLE_DATA + + fake_db = MagicMock() + fake_db.side_effect = fake_dynamodb + + with patch(PATCH_METHOD, new=fake_db): + # This should still raise DoesNotExist when no item is found + with self.assertRaises(DoesNotExist): + UserModel.get('foo', 'bar') + def test_batch_get(self): """ Model.batch_get From 5a25159883ff6ca45e7f5119bba16d3552d3b722 Mon Sep 17 00:00:00 2001 From: Dominik Deren Date: Thu, 18 Sep 2025 09:25:06 +0200 Subject: [PATCH 3/3] Updating tests --- tests/test_model.py | 50 +++++++++------------------------------------ 1 file changed, 10 insertions(+), 40 deletions(-) diff --git a/tests/test_model.py b/tests/test_model.py index 79a65c5c..705f444f 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -1766,7 +1766,7 @@ def fake_dynamodb(*args): # This simulates DynamoDB returning an empty Item when the requested # attributes don't exist or are empty, but the item itself exists return { - 'Item': {} # Empty item dictionary + 'Item': {} } return MODEL_TABLE_DATA @@ -1774,45 +1774,15 @@ def fake_dynamodb(*args): fake_db.side_effect = fake_dynamodb with patch(PATCH_METHOD, new=fake_db) as req: - # This should not raise DoesNotExist even though the Item is empty - # because the item exists in DynamoDB, just the requested attributes are empty - try: - item = UserModel.get( - 'foo', - 'bar', - attributes_to_get=['nonexistent_attribute'] - ) - # The item should be returned with empty attributes - self.assertIsNotNone(item) - # Verify that the get method was called with the right parameters - self.assertTrue(req.called) - call_args = req.call_args[0][1] - self.assertIn('Key', call_args) - self.assertIn('ProjectionExpression', call_args) - except DoesNotExist: - self.fail("get method should not raise DoesNotExist when DynamoDB returns empty Item but item exists") - - def test_get_with_no_item_still_raises_does_not_exist(self): - """ - Test that get method still raises DoesNotExist when no item is returned from DynamoDB - """ - - def fake_dynamodb(*args): - kwargs = args[1] - if kwargs == {'TableName': UserModel.Meta.table_name}: - return MODEL_TABLE_DATA - elif 'Key' in kwargs: - # This simulates DynamoDB returning no item at all - return {} # No Item key in response - return MODEL_TABLE_DATA - - fake_db = MagicMock() - fake_db.side_effect = fake_dynamodb - - with patch(PATCH_METHOD, new=fake_db): - # This should still raise DoesNotExist when no item is found - with self.assertRaises(DoesNotExist): - UserModel.get('foo', 'bar') + item = UserModel.get( + 'foo', + 'bar', + attributes_to_get=['nonexistent_attribute'] + ) + # The item should be returned with empty attributes + self.assertIsNotNone(item) + self.assertTrue(isinstance(item, UserModel)) + self.assertIsNone(item.get_attributes().get('nonexistent_attribute')) def test_batch_get(self): """