Skip to content

Commit a5f844f

Browse files
authored
Merge pull request #44 from alexei/bugfix-properly_handle_empty_or_otherwise_missing_values
Properly handle empty or otherwise missing values
2 parents 5cf284b + 1bf8416 commit a5f844f

File tree

10 files changed

+72
-30
lines changed

10 files changed

+72
-30
lines changed

adapters/adapters.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ def adapt(self, data=None):
3636
instance = self.get_instance()
3737
for field_name, field in self.fields.items():
3838
value = field.get_attribute(data or self.data)
39-
if value is undefined:
40-
continue
4139
adapted_value = field.adapt(value)
40+
if adapted_value is undefined:
41+
continue
4242
if isinstance(instance, collections.Mapping):
4343
instance[field_name] = adapted_value
4444
else:

adapters/base.py

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# -*- coding: utf-8 -*-
22

33
from .helpers import get_attribute
4-
from .utils import undefined
4+
from .utils import EMPTY_VALUES, undefined
55

66

77
class BaseField(object):
@@ -32,23 +32,34 @@ def bind(self, field_name, adapter):
3232
else:
3333
self.lookup_attrs = self.source.split('.')
3434

35+
@property
36+
def is_bound(self):
37+
return hasattr(self, 'field_name') and hasattr(self, 'adapter')
38+
3539
def get_attribute(self, obj):
36-
value = get_attribute(obj, self.lookup_attrs)
37-
if value is undefined:
40+
return get_attribute(obj, self.lookup_attrs)
41+
42+
def adapt(self, data):
43+
if data in EMPTY_VALUES or data is undefined:
3844
if self.default is not undefined:
3945
return self.default
4046
elif self.required:
41-
raise ValueError((
42-
"Required value not found for field "
43-
"`{adapter_name}.{field_name}`. Provide a default value."
44-
).format(
45-
adapter_name=self.adapter.__class__.__name__,
46-
field_name=self.field_name,
47-
))
47+
if self.is_bound:
48+
error = (
49+
"Required value not found for field "
50+
"`{adapter_name}.{field_name}`. "
51+
"Provide a default value."
52+
).format(
53+
adapter_name=self.adapter.__class__.__name__,
54+
field_name=self.field_name,
55+
)
56+
else:
57+
error = "Required value not found. Provide a default value."
58+
raise ValueError(error)
4859
else:
4960
return undefined
5061
else:
51-
return value
62+
return self.prepare(data)
5263

53-
def adapt(self, data):
64+
def prepare(self, data):
5465
return data

adapters/fields.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from decimal import Decimal
66

77
from .base import BaseField
8+
from .utils import EMPTY_VALUES, undefined
89

910

1011
__all__ = [
@@ -36,16 +37,21 @@ def get_attribute(self, obj):
3637

3738
class BooleanField(BaseField):
3839
def adapt(self, data):
40+
if data in EMPTY_VALUES or data is undefined:
41+
data = False
42+
return super(BooleanField, self).adapt(data)
43+
44+
def prepare(self, data):
3945
return bool(data)
4046

4147

4248
class CharField(BaseField):
43-
def adapt(self, data):
49+
def prepare(self, data):
4450
return str(data)
4551

4652

4753
class DateField(BaseField):
48-
def adapt(self, data):
54+
def prepare(self, data):
4955
if isinstance(data, datetime.date):
5056
return data
5157
elif isinstance(data, str):
@@ -55,7 +61,7 @@ def adapt(self, data):
5561

5662

5763
class DateTimeField(BaseField):
58-
def adapt(self, data):
64+
def prepare(self, data):
5965
if isinstance(data, datetime.datetime):
6066
return data
6167
elif isinstance(data, str):
@@ -65,7 +71,7 @@ def adapt(self, data):
6571

6672

6773
class DecimalField(BaseField):
68-
def adapt(self, data):
74+
def prepare(self, data):
6975
return Decimal(data)
7076

7177

@@ -74,17 +80,17 @@ class VerbatimField(BaseField):
7480

7581

7682
class FloatField(BaseField):
77-
def adapt(self, data):
83+
def prepare(self, data):
7884
return float(data)
7985

8086

8187
class IntField(BaseField):
82-
def adapt(self, data):
88+
def prepare(self, data):
8389
return int(data)
8490

8591

8692
class TimeField(BaseField):
87-
def adapt(self, data):
93+
def prepare(self, data):
8894
if isinstance(data, datetime.time):
8995
return data
9096
elif isinstance(data, str):

adapters/utils.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@
33
import collections
44

55

6-
__all__ = ['BindingDict', 'undefined']
6+
__all__ = ['BindingDict', 'EMPTY_VALUES', 'undefined']
7+
8+
9+
EMPTY_VALUES = [None, '', [], (), {}]
10+
11+
12+
class undefined:
13+
pass
714

815

916
class BindingDict(collections.MutableMapping):
@@ -29,7 +36,3 @@ def __len__(self):
2936

3037
def __repr__(self):
3138
return dict.__repr__(self.fields)
32-
33-
34-
class undefined:
35-
pass

build.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/bash
2+
3+
python setup.py bdist_wheel

publish.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/bash
2+
3+
twine upload dist/* -r pypi

setup.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@
99
setup(
1010
name='python-adapters',
1111
packages=['adapters'],
12-
version='1.0.1',
12+
version='2.0.0',
1313
description='Python adapters',
1414
author='Alexei',
1515
author_email='hello@alexei.ro',
1616
url='https://github.com/alexei/python-adapters',
17-
download_url='https://github.com/alexei/python-adapters/archive/1.0.1.tar.gz', # noqa
18-
keywords=['adapter pattern']
17+
download_url='https://github.com/alexei/python-adapters/archive/2.0.0.tar.gz', # noqa
18+
keywords=['adapter pattern'],
19+
install_requires=['python-dateutil>=2.6.0'],
1920
)
File renamed without changes.

tests/test_adapter.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ def test_object_to_object(self):
2929
'country': 'US',
3030
})
3131
})
32-
3332
self.assertEqual(actual.first_name, expected.first_name)
3433
self.assertEqual(actual.last_name, expected.last_name)
3534
self.assertEqual(actual.address.line1, expected.address.line1)

tests/test_fields.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,19 @@ def test_adapter_method_field(self):
139139
'avg': 3,
140140
}
141141
self.assertDictEqual(actual, expected)
142+
143+
def test_optional_field(self):
144+
data = [None, '', [], (), {}]
145+
expected = adapters.utils.undefined
146+
field = adapters.IntField(required=False)
147+
for entry in data:
148+
actual = field.adapt(entry)
149+
self.assertEqual(actual, expected)
150+
151+
def test_optional_field_with_default(self):
152+
data = [None, '', [], (), {}]
153+
expected = 42
154+
field = adapters.IntField(required=False, default=expected)
155+
for entry in data:
156+
actual = field.adapt(entry)
157+
self.assertEqual(actual, expected)

0 commit comments

Comments
 (0)