Skip to content

Commit adc1577

Browse files
authored
5.x: propagate null_check to maps in lists (#1128)
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. Backport of #1127 to 5.x branch.
1 parent d2037b8 commit adc1577

File tree

5 files changed

+59
-31
lines changed

5 files changed

+59
-31
lines changed

.github/workflows/test.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ on:
88
jobs:
99
test:
1010

11-
runs-on: ubuntu-latest
11+
runs-on: ubuntu-20.04
1212
strategy:
1313
matrix:
1414
python-version: ['3.6', '3.7', '3.8', 'pypy-3.6']

docs/release_notes.rst

+5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
Release Notes
44
=============
55

6+
v5.3.4
7+
----------
8+
* Make serialization :code:`null_check=False` propagate to maps inside lists (#1128).
9+
10+
611
v5.3.3
712
----------
813
* Fix :py:class:`~pynamodb.pagination.PageIterator` and :py:class:`~pynamodb.pagination.ResultIterator`

pynamodb/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@
77
"""
88
__author__ = 'Jharrod LaFon'
99
__license__ = 'MIT'
10-
__version__ = '5.3.3'
10+
__version__ = '5.3.4'

pynamodb/attributes.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ def _container_serialize(self, null_check: bool = True) -> Dict[str, Dict[str, A
352352
raise
353353

354354
if value is not None:
355-
if isinstance(attr, MapAttribute):
355+
if isinstance(attr, (ListAttribute, MapAttribute)):
356356
attr_value = attr.serialize(value, null_check=null_check)
357357
else:
358358
attr_value = attr.serialize(value)
@@ -1123,7 +1123,7 @@ def __init__(
11231123
raise ValueError("'of' must be a subclass of Attribute")
11241124
self.element_type = of
11251125

1126-
def serialize(self, values):
1126+
def serialize(self, values, *, null_check: bool = True):
11271127
"""
11281128
Encode the given list of objects into a list of AttributeValue types.
11291129
"""
@@ -1133,7 +1133,10 @@ def serialize(self, values):
11331133
if self.element_type and v is not None and not isinstance(attr_class, self.element_type):
11341134
raise ValueError("List elements must be of type: {}".format(self.element_type.__name__))
11351135
attr_type = attr_class.attr_type
1136-
attr_value = attr_class.serialize(v)
1136+
if isinstance(attr_class, (ListAttribute, MapAttribute)):
1137+
attr_value = attr_class.serialize(v, null_check=null_check)
1138+
else:
1139+
attr_value = attr_class.serialize(v)
11371140
if attr_value is None:
11381141
# When attribute values serialize to "None" (e.g. empty sets) we store {"NULL": True} in DynamoDB.
11391142
attr_type = NULL

tests/test_model.py

+46-26
Original file line numberDiff line numberDiff line change
@@ -391,29 +391,29 @@ class Meta:
391391
is_human = BooleanAttribute()
392392

393393

394-
class TreeLeaf2(MapAttribute):
394+
class TreeLeaf(MapAttribute):
395395
value = NumberAttribute()
396396

397397

398-
class TreeLeaf1(MapAttribute):
398+
class TreeNode2(MapAttribute):
399399
value = NumberAttribute()
400-
left = TreeLeaf2()
401-
right = TreeLeaf2()
400+
left = TreeLeaf()
401+
right = TreeLeaf()
402402

403403

404-
class TreeLeaf(MapAttribute):
404+
class TreeNode1(MapAttribute):
405405
value = NumberAttribute()
406-
left = TreeLeaf1()
407-
right = TreeLeaf1()
406+
left = TreeNode2()
407+
right = TreeNode2()
408408

409409

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

414414
tree_key = UnicodeAttribute(hash_key=True)
415-
left = TreeLeaf()
416-
right = TreeLeaf()
415+
left = TreeNode1()
416+
right = TreeNode1()
417417

418418

419419
class ExplicitRawMapModel(Model):
@@ -2911,41 +2911,61 @@ def test_deserializing_new_style_bool_true_works(self):
29112911
self.assertTrue(item.is_human)
29122912

29132913
def test_serializing_map_with_null_check(self):
2914-
item = TreeModel(
2914+
class TreeModelWithList(TreeModel):
2915+
leaves = ListAttribute(of=TreeLeaf)
2916+
2917+
item = TreeModelWithList(
29152918
tree_key='test',
2916-
left=TreeLeaf(
2919+
left=TreeNode1(
29172920
value=42,
2918-
left=TreeLeaf1(
2921+
left=TreeNode2(
29192922
value=42,
2920-
left=TreeLeaf2(value=42),
2921-
right=TreeLeaf2(value=42),
2923+
left=TreeLeaf(value=42),
2924+
right=TreeLeaf(value=42),
29222925
),
2923-
right=TreeLeaf1(
2926+
right=TreeNode2(
29242927
value=42,
2925-
left=TreeLeaf2(value=42),
2926-
right=TreeLeaf2(value=42),
2928+
left=TreeLeaf(value=42),
2929+
right=TreeLeaf(value=42),
29272930
),
29282931
),
2929-
right=TreeLeaf(
2932+
right=TreeNode1(
29302933
value=42,
2931-
left=TreeLeaf1(
2934+
left=TreeNode2(
29322935
value=42,
2933-
left=TreeLeaf2(value=42),
2934-
right=TreeLeaf2(value=42),
2936+
left=TreeLeaf(value=42),
2937+
right=TreeLeaf(value=42),
29352938
),
2936-
right=TreeLeaf1(
2939+
right=TreeNode2(
29372940
value=42,
2938-
left=TreeLeaf2(value=42),
2939-
right=TreeLeaf2(value=42),
2941+
left=TreeLeaf(value=42),
2942+
right=TreeLeaf(value=42),
29402943
),
29412944
),
2945+
leaves=[
2946+
TreeLeaf(value=42),
2947+
],
29422948
)
29432949
item.serialize(null_check=False)
29442950

29452951
# now let's nullify an attribute a few levels deep to test that `null_check` propagates
29462952
item.left.left.left.value = None
29472953
item.serialize(null_check=False)
29482954

2955+
# now with null check
2956+
with pytest.raises(Exception, match="Attribute 'left.value' cannot be None"):
2957+
item.serialize(null_check=True)
2958+
2959+
# now let's nullify an attribute of a map in a list to test that `null_check` propagates
2960+
item.left.left.left.value = 42
2961+
item.leaves[0].value = None
2962+
item.serialize(null_check=False)
2963+
2964+
# now with null check
2965+
with pytest.raises(Exception, match=r"Attribute 'value' cannot be None"):
2966+
item.serialize(null_check=True)
2967+
2968+
29492969
def test_deserializing_map_four_layers_deep_works(self):
29502970
fake_db = self.database_mocker(TreeModel,
29512971
TREE_MODEL_TABLE_DATA,
@@ -3401,8 +3421,8 @@ def test_subclassed_map_attribute_with_map_attributes_member_with_dict_init(self
34013421
def test_subclassed_map_attribute_with_map_attribute_member_with_initialized_instance_init(self):
34023422
left = self._get_bin_tree()
34033423
right = self._get_bin_tree(multiplier=2)
3404-
left_instance = TreeLeaf(**left)
3405-
right_instance = TreeLeaf(**right)
3424+
left_instance = TreeNode1(**left)
3425+
right_instance = TreeNode1(**right)
34063426
actual = TreeModel(tree_key='key', left=left_instance, right=right_instance)
34073427
self.assertEqual(actual.left.left.right.value, left_instance.left.right.value)
34083428
self.assertEqual(actual.left.left.value, left_instance.left.value)

0 commit comments

Comments
 (0)