wrap NumberParseException so it isn't cast
SQLAlchemy automatically catches exceptions from types and wraps them as a `StatementError` if they don't extend from the `DontWrapMixin`. This PR creates a new exception type that combines the native phonenumbers exception with the sqlalchemy mixin.
This commit is contained in:

committed by
Konsta Vesterinen

parent
57b552c7e4
commit
f51583d75b
@@ -82,6 +82,7 @@ from .types import ( # noqa
|
|||||||
Password,
|
Password,
|
||||||
PasswordType,
|
PasswordType,
|
||||||
PhoneNumber,
|
PhoneNumber,
|
||||||
|
PhoneNumberParseException,
|
||||||
PhoneNumberType,
|
PhoneNumberType,
|
||||||
register_composites,
|
register_composites,
|
||||||
remove_composite_listeners,
|
remove_composite_listeners,
|
||||||
|
@@ -20,7 +20,11 @@ from .pg_composite import ( # noqa
|
|||||||
register_composites,
|
register_composites,
|
||||||
remove_composite_listeners
|
remove_composite_listeners
|
||||||
)
|
)
|
||||||
from .phone_number import PhoneNumber, PhoneNumberType # noqa
|
from .phone_number import ( # noqa
|
||||||
|
PhoneNumber,
|
||||||
|
PhoneNumberParseException,
|
||||||
|
PhoneNumberType
|
||||||
|
)
|
||||||
from .range import ( # noqa
|
from .range import ( # noqa
|
||||||
DateRangeType,
|
DateRangeType,
|
||||||
DateTimeRangeType,
|
DateTimeRangeType,
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
from sqlalchemy import types
|
import six
|
||||||
|
from sqlalchemy import exc, types
|
||||||
|
|
||||||
from ..exceptions import ImproperlyConfigured
|
from ..exceptions import ImproperlyConfigured
|
||||||
from ..utils import str_coercible
|
from ..utils import str_coercible
|
||||||
@@ -7,9 +8,23 @@ from .scalar_coercible import ScalarCoercible
|
|||||||
try:
|
try:
|
||||||
import phonenumbers
|
import phonenumbers
|
||||||
from phonenumbers.phonenumber import PhoneNumber as BasePhoneNumber
|
from phonenumbers.phonenumber import PhoneNumber as BasePhoneNumber
|
||||||
|
from phonenumbers.phonenumberutil import NumberParseException
|
||||||
except ImportError:
|
except ImportError:
|
||||||
phonenumbers = None
|
phonenumbers = None
|
||||||
BasePhoneNumber = object
|
BasePhoneNumber = object
|
||||||
|
NumberParseException = Exception
|
||||||
|
|
||||||
|
|
||||||
|
class PhoneNumberParseException(NumberParseException, exc.DontWrapMixin):
|
||||||
|
'''
|
||||||
|
Wraps exceptions from phonenumbers with SQLAlchemy's DontWrapMixin
|
||||||
|
so we get more meaningful exceptions on validation failure instead of the
|
||||||
|
StatementException
|
||||||
|
|
||||||
|
Clients can catch this as either a PhoneNumberParseException or
|
||||||
|
NumberParseException from the phonenumbers library.
|
||||||
|
'''
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@str_coercible
|
@str_coercible
|
||||||
@@ -62,9 +77,23 @@ class PhoneNumber(BasePhoneNumber):
|
|||||||
# Bail if phonenumbers is not found.
|
# Bail if phonenumbers is not found.
|
||||||
if phonenumbers is None:
|
if phonenumbers is None:
|
||||||
raise ImproperlyConfigured(
|
raise ImproperlyConfigured(
|
||||||
"'phonenumbers' is required to use 'PhoneNumber'")
|
"'phonenumbers' is required to use 'PhoneNumber'"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
self._phone_number = phonenumbers.parse(raw_number, region)
|
self._phone_number = phonenumbers.parse(raw_number, region)
|
||||||
|
except NumberParseException as e:
|
||||||
|
# Wrap exception so SQLAlchemy doesn't swallow it as a
|
||||||
|
# StatementError
|
||||||
|
#
|
||||||
|
# Worth noting that if -1 shows up as the error_type
|
||||||
|
# it's likely because the API has changed upstream and these
|
||||||
|
# bindings need to be updated.
|
||||||
|
raise PhoneNumberParseException(
|
||||||
|
getattr(e, 'error_type', -1),
|
||||||
|
six.text_type(e)
|
||||||
|
)
|
||||||
|
|
||||||
super(PhoneNumber, self).__init__(
|
super(PhoneNumber, self).__init__(
|
||||||
country_code=self._phone_number.country_code,
|
country_code=self._phone_number.country_code,
|
||||||
national_number=self._phone_number.national_number,
|
national_number=self._phone_number.national_number,
|
||||||
@@ -132,7 +161,8 @@ class PhoneNumberType(types.TypeDecorator, ScalarCoercible):
|
|||||||
# Bail if phonenumbers is not found.
|
# Bail if phonenumbers is not found.
|
||||||
if phonenumbers is None:
|
if phonenumbers is None:
|
||||||
raise ImproperlyConfigured(
|
raise ImproperlyConfigured(
|
||||||
"'phonenumbers' is required to use 'PhoneNumberType'")
|
"'phonenumbers' is required to use 'PhoneNumberType'"
|
||||||
|
)
|
||||||
|
|
||||||
super(PhoneNumberType, self).__init__(*args, **kwargs)
|
super(PhoneNumberType, self).__init__(*args, **kwargs)
|
||||||
self.region = region
|
self.region = region
|
||||||
|
@@ -2,7 +2,12 @@ import pytest
|
|||||||
import six
|
import six
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from sqlalchemy_utils import PhoneNumber, PhoneNumberType, types # noqa
|
from sqlalchemy_utils import ( # noqa
|
||||||
|
PhoneNumber,
|
||||||
|
PhoneNumberParseException,
|
||||||
|
PhoneNumberType,
|
||||||
|
types
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -77,6 +82,23 @@ class TestPhoneNumber(object):
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def test_invalid_phone_numbers_throw_dont_wrap_exception(
|
||||||
|
self,
|
||||||
|
session,
|
||||||
|
User
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
session.execute(
|
||||||
|
User.__table__.insert().values(
|
||||||
|
name='Someone',
|
||||||
|
phone_number='abc'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except PhoneNumberParseException:
|
||||||
|
pass
|
||||||
|
except:
|
||||||
|
assert False
|
||||||
|
|
||||||
def test_phone_number_attributes(self):
|
def test_phone_number_attributes(self):
|
||||||
number = PhoneNumber('+358401234567')
|
number = PhoneNumber('+358401234567')
|
||||||
assert number.e164 == u'+358401234567'
|
assert number.e164 == u'+358401234567'
|
||||||
|
Reference in New Issue
Block a user