Skip to content

Commit 34ebe2e

Browse files
authored
Propagate null_check to maps in lists (pynamodb#1127)
When calling model.serialize(null_check=False), we were propagating null_check=False to maps (and maps nested in maps), but not maps nested in lists.
1 parent 0ab79ce commit 34ebe2e

File tree

2 files changed

+52
-33
lines changed

2 files changed

+52
-33
lines changed

pynamodb/attributes.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,7 @@ def _container_serialize(self, null_check: bool = True) -> Dict[str, Dict[str, A
396396
raise ValueError("Attribute '{}' is not correctly typed".format(name))
397397

398398
if value is not None:
399-
if isinstance(attr, MapAttribute):
399+
if isinstance(attr, (ListAttribute, MapAttribute)):
400400
attr_value = attr.serialize(value, null_check=null_check)
401401
else:
402402
attr_value = attr.serialize(value)
@@ -1243,18 +1243,21 @@ def __init__(
12431243
raise ValueError("'of' must be a subclass of Attribute")
12441244
self.element_type = of
12451245

1246-
def serialize(self, values):
1246+
def serialize(self, values, *, null_check: bool = True):
12471247
"""
12481248
Encode the given list of objects into a list of AttributeValue types.
12491249
"""
12501250
rval = []
1251-
for idx, v in enumerate(values):
1252-
attr_class = self._get_serialize_class(v)
1253-
if self.element_type and v is not None and not isinstance(attr_class, self.element_type):
1251+
for idx, value in enumerate(values):
1252+
attr = self._get_serialize_class(value)
1253+
if self.element_type and value is not None and not isinstance(attr, self.element_type):
12541254
raise ValueError("List elements must be of type: {}".format(self.element_type.__name__))
1255-
attr_type = attr_class.attr_type
1255+
attr_type = attr.attr_type
12561256
try:
1257-
attr_value = attr_class.serialize(v)
1257+
if isinstance(attr, (ListAttribute, MapAttribute)):
1258+
attr_value = attr.serialize(value, null_check=null_check)
1259+
else:
1260+
attr_value = attr.serialize(value)
12581261
except AttributeNullError as e:
12591262
e.prepend_path(f'[{idx}]')
12601263
raise

tests/test_model.py

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -390,29 +390,29 @@ class Meta:
390390
is_human = BooleanAttribute()
391391

392392

393-
class TreeLeaf2(MapAttribute):
393+
class TreeLeaf(MapAttribute):
394394
value = NumberAttribute()
395395

396396

397-
class TreeLeaf1(MapAttribute):
397+
class TreeNode2(MapAttribute):
398398
value = NumberAttribute()
399-
left = TreeLeaf2()
400-
right = TreeLeaf2()
399+
left = TreeLeaf()
400+
right = TreeLeaf()
401401

402402

403-
class TreeLeaf(MapAttribute):
403+
class TreeNode1(MapAttribute):
404404
value = NumberAttribute()
405-
left = TreeLeaf1()
406-
right = TreeLeaf1()
405+
left = TreeNode2()
406+
right = TreeNode2()
407407

408408

409409
class TreeModel(Model):
410410
class Meta:
411411
table_name = 'TreeModelTable'
412412

413413
tree_key = UnicodeAttribute(hash_key=True)
414-
left = TreeLeaf()
415-
right = TreeLeaf()
414+
left = TreeNode1()
415+
right = TreeNode1()
416416

417417

418418
class ExplicitRawMapModel(Model):
@@ -2822,34 +2822,40 @@ def test_deserializing_new_style_bool_true_works(self):
28222822
self.assertTrue(item.is_human)
28232823

28242824
def test_serializing_map_with_null_check(self):
2825-
item = TreeModel(
2825+
class TreeModelWithList(TreeModel):
2826+
leaves = ListAttribute(of=TreeLeaf)
2827+
2828+
item = TreeModelWithList(
28262829
tree_key='test',
2827-
left=TreeLeaf(
2830+
left=TreeNode1(
28282831
value=42,
2829-
left=TreeLeaf1(
2832+
left=TreeNode2(
28302833
value=42,
2831-
left=TreeLeaf2(value=42),
2832-
right=TreeLeaf2(value=42),
2834+
left=TreeLeaf(value=42),
2835+
right=TreeLeaf(value=42),
28332836
),
2834-
right=TreeLeaf1(
2837+
right=TreeNode2(
28352838
value=42,
2836-
left=TreeLeaf2(value=42),
2837-
right=TreeLeaf2(value=42),
2839+
left=TreeLeaf(value=42),
2840+
right=TreeLeaf(value=42),
28382841
),
28392842
),
2840-
right=TreeLeaf(
2843+
right=TreeNode1(
28412844
value=42,
2842-
left=TreeLeaf1(
2845+
left=TreeNode2(
28432846
value=42,
2844-
left=TreeLeaf2(value=42),
2845-
right=TreeLeaf2(value=42),
2847+
left=TreeLeaf(value=42),
2848+
right=TreeLeaf(value=42),
28462849
),
2847-
right=TreeLeaf1(
2850+
right=TreeNode2(
28482851
value=42,
2849-
left=TreeLeaf2(value=42),
2850-
right=TreeLeaf2(value=42),
2852+
left=TreeLeaf(value=42),
2853+
right=TreeLeaf(value=42),
28512854
),
28522855
),
2856+
leaves=[
2857+
TreeLeaf(value=42),
2858+
],
28532859
)
28542860
item.serialize(null_check=True)
28552861

@@ -2861,6 +2867,16 @@ def test_serializing_map_with_null_check(self):
28612867
with pytest.raises(Exception, match="Attribute 'left.left.left.value' cannot be None"):
28622868
item.serialize(null_check=True)
28632869

2870+
# now let's nullify an attribute of a map in a list to test that `null_check` propagates
2871+
item.left.left.left.value = 42
2872+
item.leaves[0].value = None
2873+
item.serialize(null_check=False)
2874+
2875+
# now with null check
2876+
with pytest.raises(Exception, match=r"Attribute 'leaves.\[0\].value' cannot be None"):
2877+
item.serialize(null_check=True)
2878+
2879+
28642880
def test_deserializing_map_four_layers_deep_works(self):
28652881
fake_db = self.database_mocker(TreeModel,
28662882
TREE_MODEL_TABLE_DATA,
@@ -3317,8 +3333,8 @@ def test_subclassed_map_attribute_with_map_attributes_member_with_dict_init(self
33173333
def test_subclassed_map_attribute_with_map_attribute_member_with_initialized_instance_init(self):
33183334
left = self._get_bin_tree()
33193335
right = self._get_bin_tree(multiplier=2)
3320-
left_instance = TreeLeaf(**left)
3321-
right_instance = TreeLeaf(**right)
3336+
left_instance = TreeNode1(**left)
3337+
right_instance = TreeNode1(**right)
33223338
actual = TreeModel(tree_key='key', left=left_instance, right=right_instance)
33233339
self.assertEqual(actual.left.left.right.value, left_instance.left.right.value)
33243340
self.assertEqual(actual.left.left.value, left_instance.left.value)

0 commit comments

Comments
 (0)