Skip to content

Commit 96db7db

Browse files
jmphillidanielhochman
authored andcommitted
Add support for native boolean attributes (#149)
LegacyBooleanAttribute also provided if previously using a boolean as a hash or range key. The new native class can deserialize legacy number type boolean attributes. A save will rewrite as a native boolean.
1 parent ebbf334 commit 96db7db

File tree

7 files changed

+289
-21
lines changed

7 files changed

+289
-21
lines changed

pynamodb/attributes.py

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from delorean import Delorean, parse
88
from pynamodb.constants import (
99
STRING, NUMBER, BINARY, UTC, DATETIME_FORMAT, BINARY_SET, STRING_SET, NUMBER_SET,
10-
DEFAULT_ENCODING
10+
DEFAULT_ENCODING, BOOLEAN, ATTR_TYPE_MAP, NUMBER_SHORT
1111
)
1212

1313

@@ -58,6 +58,10 @@ def deserialize(self, value):
5858
"""
5959
return value
6060

61+
def get_value(self, value):
62+
serialized_dynamo_type = ATTR_TYPE_MAP[self.attr_type]
63+
return value.get(serialized_dynamo_type)
64+
6165

6266
class SetMixin(object):
6367
"""
@@ -186,18 +190,17 @@ def deserialize(self, value):
186190
return json.loads(value, strict=False)
187191

188192

189-
class BooleanAttribute(Attribute):
193+
class LegacyBooleanAttribute(Attribute):
190194
"""
191-
A class for boolean attributes
195+
A class for legacy boolean attributes
192196
193-
This attribute type uses a number attribute to save space
197+
Previous versions of this library serialized bools as numbers.
198+
This class allows you to continue to use that functionality.
194199
"""
200+
195201
attr_type = NUMBER
196202

197203
def serialize(self, value):
198-
"""
199-
Encodes True as 1, False as 0
200-
"""
201204
if value is None:
202205
return None
203206
elif value:
@@ -206,12 +209,35 @@ def serialize(self, value):
206209
return json.dumps(0)
207210

208211
def deserialize(self, value):
209-
"""
210-
Encode
211-
"""
212212
return bool(json.loads(value))
213213

214214

215+
class BooleanAttribute(Attribute):
216+
"""
217+
A class for boolean attributes
218+
"""
219+
attr_type = BOOLEAN
220+
221+
def serialize(self, value):
222+
if value is None:
223+
return None
224+
elif value:
225+
return True
226+
else:
227+
return False
228+
229+
def deserialize(self, value):
230+
return bool(value)
231+
232+
def get_value(self, value):
233+
# we need this for legacy compatibility.
234+
# previously, BOOL was serialized as N
235+
value_to_deserialize = super(BooleanAttribute, self).get_value(value)
236+
if value_to_deserialize is None:
237+
value_to_deserialize = json.loads(value.get(NUMBER_SHORT, 0))
238+
return value_to_deserialize
239+
240+
215241
class NumberSetAttribute(SetMixin, Attribute):
216242
"""
217243
A number set attribute

pynamodb/compat.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,4 @@ def assertListEqual(self, list1, list2, msg=None):
5858

5959
class NullHandler(logging.Handler):
6060
def emit(self, record):
61-
pass
61+
pass

pynamodb/constants.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@
8181
NUMBER_SET_SHORT = 'NS'
8282
BINARY_SHORT = 'B'
8383
BINARY_SET_SHORT = 'BS'
84+
BOOLEAN = 'BOOL'
85+
BOOLEAN_SHORT = 'BOOL'
8486
STRING = 'String'
8587
STRING_SET = 'StringSet'
8688
NUMBER = 'Number'
@@ -100,7 +102,8 @@
100102
NUMBER_SHORT: NUMBER,
101103
NUMBER_SET_SHORT: NUMBER_SET,
102104
BINARY_SHORT: BINARY,
103-
BINARY_SET_SHORT: BINARY_SET
105+
BINARY_SET_SHORT: BINARY_SET,
106+
BOOLEAN: BOOLEAN_SHORT
104107
}
105108
# Constants needed for creating indexes
106109
LOCAL_SECONDARY_INDEX = 'LocalSecondaryIndex'

pynamodb/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,7 @@ def from_raw_data(cls, data):
447447
for name, value in mutable_data.items():
448448
attr = cls._get_attributes().get(name, None)
449449
if attr:
450-
kwargs[name] = attr.deserialize(value.get(ATTR_TYPE_MAP[attr.attr_type]))
450+
kwargs[name] = attr.deserialize(attr.get_value(value))
451451
return cls(*args, **kwargs)
452452

453453
@classmethod

pynamodb/tests/data.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,3 +496,105 @@
496496
}
497497
]
498498
]
499+
500+
BOOLEAN_CONVERSION_MODEL_TABLE_DATA_OLD_STYLE = {
501+
'Table': {
502+
'ItemCount': 0, 'TableName': 'BooleanConversionTable',
503+
'ProvisionedThroughput': {
504+
'ReadCapacityUnits': 2,
505+
'WriteCapacityUnits': 2,
506+
'NumberOfDecreasesToday': 0
507+
},
508+
'CreationDateTime': 1391471876.86,
509+
'TableStatus': 'ACTIVE',
510+
'AttributeDefinitions': [
511+
{
512+
'AttributeName': 'user_name',
513+
'AttributeType': 'S'
514+
},
515+
{
516+
'AttributeName': 'is_human',
517+
'AttributeType': 'N'
518+
}
519+
],
520+
'KeySchema': [
521+
{
522+
'AttributeName': 'user_name', 'KeyType': 'HASH'
523+
}
524+
],
525+
'TableSizeBytes': 0
526+
}
527+
}
528+
529+
BOOLEAN_CONVERSION_MODEL_TABLE_DATA = {
530+
'Table': {
531+
'ItemCount': 0, 'TableName': 'BooleanConversionTable',
532+
'ProvisionedThroughput': {
533+
'ReadCapacityUnits': 2,
534+
'WriteCapacityUnits': 2,
535+
'NumberOfDecreasesToday': 0
536+
},
537+
'CreationDateTime': 1391471876.86,
538+
'TableStatus': 'ACTIVE',
539+
'AttributeDefinitions': [
540+
{
541+
'AttributeName': 'user_name',
542+
'AttributeType': 'S'
543+
},
544+
{
545+
'AttributeName': 'is_human',
546+
'AttributeType': 'BOOL'
547+
}
548+
],
549+
'KeySchema': [
550+
{
551+
'AttributeName': 'user_name', 'KeyType': 'HASH'
552+
}
553+
],
554+
'TableSizeBytes': 0
555+
}
556+
}
557+
558+
BOOLEAN_CONVERSION_MODEL_OLD_STYLE_TRUE_ITEM_DATA = {
559+
'Item': {
560+
'user_name': {
561+
'S': 'justin'
562+
},
563+
'is_human': {
564+
'N': '1'
565+
}
566+
}
567+
}
568+
569+
BOOLEAN_CONVERSION_MODEL_OLD_STYLE_FALSE_ITEM_DATA = {
570+
'Item': {
571+
'user_name': {
572+
'S': 'alf'
573+
},
574+
'is_human': {
575+
'N': '0'
576+
}
577+
}
578+
}
579+
580+
BOOLEAN_CONVERSION_MODEL_NEW_STYLE_TRUE_ITEM_DATA = {
581+
'Item': {
582+
'user_name': {
583+
'S': 'justin'
584+
},
585+
'is_human': {
586+
'BOOL': True
587+
}
588+
}
589+
}
590+
591+
BOOLEAN_CONVERSION_MODEL_NEW_STYLE_FALSE_ITEM_DATA = {
592+
'Item': {
593+
'user_name': {
594+
'S': 'alf'
595+
},
596+
'is_human': {
597+
'BOOL': False
598+
}
599+
}
600+
}

pynamodb/tests/test_attributes.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
BinarySetAttribute, BinaryAttribute, NumberSetAttribute, NumberAttribute,
1515
UnicodeAttribute, UnicodeSetAttribute, UTCDateTimeAttribute, BooleanAttribute,
1616
JSONAttribute, DEFAULT_ENCODING, NUMBER, STRING, STRING_SET, NUMBER_SET, BINARY_SET,
17-
BINARY)
17+
BINARY, BOOLEAN)
1818

1919

2020
class AttributeTestModel(Model):
@@ -380,7 +380,7 @@ def test_boolean_attribute(self):
380380
attr = BooleanAttribute()
381381
self.assertIsNotNone(attr)
382382

383-
self.assertEqual(attr.attr_type, NUMBER)
383+
self.assertEqual(attr.attr_type, BOOLEAN)
384384
attr = BooleanAttribute(default=True)
385385
self.assertEqual(attr.default, True)
386386

@@ -389,8 +389,8 @@ def test_boolean_serialize(self):
389389
BooleanAttribute.serialize
390390
"""
391391
attr = BooleanAttribute()
392-
self.assertEqual(attr.serialize(True), json.dumps(1))
393-
self.assertEqual(attr.serialize(False), json.dumps(0))
392+
self.assertEqual(attr.serialize(True), True)
393+
self.assertEqual(attr.serialize(False), False)
394394
self.assertEqual(attr.serialize(None), None)
395395

396396
def test_boolean_deserialize(self):
@@ -399,7 +399,9 @@ def test_boolean_deserialize(self):
399399
"""
400400
attr = BooleanAttribute()
401401
self.assertEqual(attr.deserialize('1'), True)
402-
self.assertEqual(attr.deserialize('0'), False)
402+
self.assertEqual(attr.deserialize('0'), True)
403+
self.assertEqual(attr.deserialize(True), True)
404+
self.assertEqual(attr.deserialize(False), False)
403405

404406

405407
class JSONAttributeTestCase(TestCase):

0 commit comments

Comments
 (0)