Skip to content

Commit

Permalink
Add an option to bypass Address' strict normalization.
Browse files Browse the repository at this point in the history
  • Loading branch information
Kevin Deldycke committed Apr 2, 2015
1 parent 9afab92 commit 17e32bd
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 33 deletions.
2 changes: 1 addition & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ ChangeLog
0.5.3 (unreleased)
------------------

* No changes yet.
* Add an option to bypass Address' strict normalization.


0.5.2 (2015-03-30)
Expand Down
74 changes: 42 additions & 32 deletions postal_address/address.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,11 @@ class Address(object):
'line1', 'postal_code', 'city_name', 'country_code'])
assert REQUIRED_FIELDS.issubset(BASE_FIELD_IDS)

def __init__(self, **kwargs):
""" Set address' individual fields and normalize them. """
def __init__(self, strict=True, **kwargs):
""" Set address' individual fields and normalize them.
By default, normalization is ``strict``.
"""
# Only common fields are allowed to be set directly.
unknown_fields = set(kwargs).difference(self.BASE_FIELD_IDS)
if unknown_fields:
Expand All @@ -131,7 +134,7 @@ def __init__(self, **kwargs):
# Load provided fields.
self._fields.update(kwargs)
# Normalize addresses fields.
self.normalize()
self.normalize(strict=strict)

def __repr__(self):
""" Print all fields available from the address. """
Expand Down Expand Up @@ -257,11 +260,16 @@ def render(self, separator='\n'):
# Render the address block with the provided separator.
return separator.join(lines)

def normalize(self):
def normalize(self, strict=True):
""" Normalize address fields.
If values are unrecognized or invalid, they will be set to None.
By default, the normalization is ``strict``: metadata derived from
territory's parents are not allowed to overwrite valid address fields
entered by the user. If set to ``False``, territory-derived values
takes precedence over user's.
You need to call back the ``validate()`` method afterwards to properly
check that the fully-qualified address is ready for consumption.
"""
Expand Down Expand Up @@ -315,34 +323,36 @@ def normalize(self):
parent_metadata.update(subdivision_metadata(parent_subdiv))

# Parent metadata are not allowed to overwrite address fields
# if not blank.
for field_id, new_value in parent_metadata.items():
assert new_value # New metadata are not allowed to be blank.
current_value = self._fields.get(field_id)
if current_value and field_id in self.BASE_FIELD_IDS:

# Build the list of substitute values that are equivalent
# to our new normalized target.
alias_values = set([new_value])
if field_id == 'country_code':
# Allow normalization if the current country code is
# the direct parent of a subdivision which also have
# its own country code.
alias_values.add(subdivisions.get(
code=self.subdivision_code).country_code)

# Change of current value is allowed if it is a direct
# substitute to our new normalized value.
if current_value not in alias_values:
raise InvalidAddress(
inconsistent_fields=set([
tuple(sorted((
field_id, 'subdivision_code')))]),
extra_msg="{} subdivision is trying to replace "
"{}={!r} field by {}={!r}".format(
self.subdivision_code,
field_id, current_value,
field_id, new_value))
# if not blank, unless strict mode is de-activated.
if strict:
for field_id, new_value in parent_metadata.items():
# New metadata are not allowed to be blank.
assert new_value
current_value = self._fields.get(field_id)
if current_value and field_id in self.BASE_FIELD_IDS:

# Build the list of substitute values that are
# equivalent to our new normalized target.
alias_values = set([new_value])
if field_id == 'country_code':
# Allow normalization if the current country code
# is the direct parent of a subdivision which also
# have its own country code.
alias_values.add(subdivisions.get(
code=self.subdivision_code).country_code)

# Change of current value is allowed if it is a direct
# substitute to our new normalized value.
if current_value not in alias_values:
raise InvalidAddress(
inconsistent_fields=set([
tuple(sorted((
field_id, 'subdivision_code')))]),
extra_msg="{} subdivision is trying to replace"
" {}={!r} field by {}={!r}".format(
self.subdivision_code,
field_id, current_value,
field_id, new_value))

self._fields.update(parent_metadata)

Expand Down
86 changes: 86 additions & 0 deletions postal_address/tests/test_address.py
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,92 @@ def test_city_override_by_subdivision(self):
self.assertEquals(
err.inconsistent_fields, set([('city_name', 'subdivision_code')]))

def test_non_strict_mode_normalization(self):
# Test city name override by subdivision code.
address = Address(
strict=False,
line1='2 King Edward Street',
postal_code='EC1A 1HQ',
city_name='Dummy city',
subdivision_code='GB-LND')
self.assertEqual(address.line1, '2 King Edward Street')
self.assertEqual(address.line2, None)
self.assertEqual(address.postal_code, 'EC1A 1HQ')
self.assertEqual(address.city_name, 'London, City of')
self.assertEqual(address.country_code, 'GB')
self.assertEqual(address.subdivision_code, 'GB-LND')

address = Address(
strict=False,
line1='4 Bulevardul Nicolae Bålcescu',
postal_code='010051',
city_name='Dummy city',
subdivision_code='RO-B')
self.assertEqual(address.line1, '4 Bulevardul Nicolae Bålcescu')
self.assertEqual(address.line2, None)
self.assertEqual(address.postal_code, '010051')
self.assertEqual(address.city_name, 'București')
self.assertEqual(address.country_code, 'RO')
self.assertEqual(address.subdivision_code, 'RO-B')

address = Address(
strict=False,
line1='15 Ngô Quyền',
postal_code='10000',
city_name='Dummy city',
subdivision_code='VN-HN')
self.assertEqual(address.line1, '15 Ngô Quyền')
self.assertEqual(address.line2, None)
self.assertEqual(address.postal_code, '10000')
self.assertEqual(address.city_name, 'Hà Nội')
self.assertEqual(address.country_code, 'VN')
self.assertEqual(address.subdivision_code, 'VN-HN')

# Test country override by subdivision code.
address = Address(
strict=False,
line1='10, avenue des Champs Elysées',
postal_code='75008',
city_name='Paris',
country_code='FR',
subdivision_code='BE-BRU')
self.assertEqual(address.line1, '10, avenue des Champs Elysées')
self.assertEqual(address.line2, None)
self.assertEqual(address.postal_code, '75008')
self.assertEqual(address.city_name, 'Paris')
self.assertEqual(address.country_code, 'BE')
self.assertEqual(address.subdivision_code, 'BE-BRU')

address = Address(
strict=False,
line1='Barack 31',
postal_code='XXX No postal code',
city_name='Clipperton Island',
country_code='CP',
subdivision_code='FR-CP')
self.assertEqual(address.line1, 'Barack 31')
self.assertEqual(address.line2, None)
self.assertEqual(address.postal_code, 'XXX No postal code')
self.assertEqual(address.city_name, 'Clipperton Island')
self.assertEqual(address.country_code, 'FR')
self.assertEqual(address.subdivision_code, 'FR-CP')

# Test both city and country override by subdivision code.
address = Address(
strict=False,
line1='9F., No. 290, Sec. 4, Zhongxiao E. Rd.',
postal_code='10694',
city_name='Dummy city',
country_code='FR',
subdivision_code='TW-TNN')
self.assertEqual(
address.line1, '9F., No. 290, Sec. 4, Zhongxiao E. Rd.')
self.assertEqual(address.line2, None)
self.assertEqual(address.postal_code, '10694')
self.assertEqual(address.city_name, 'Tainan City')
self.assertEqual(address.country_code, 'TW')
self.assertEqual(address.subdivision_code, 'TW-TNN')

def test_rendering(self):
# Test subdivision-less rendering.
address = Address(
Expand Down

0 comments on commit 17e32bd

Please sign in to comment.