From cb2d751cea12bc701f88e381f47b773867e180f4 Mon Sep 17 00:00:00 2001 From: Ryan Leckey Date: Wed, 10 Jul 2013 01:03:01 -0700 Subject: [PATCH 1/3] Add an ImproperlyConfigured exception. - To be used to signal when a type is being used without a required library. --- sqlalchemy_utils/__init__.py | 2 ++ sqlalchemy_utils/exceptions.py | 8 ++++++++ 2 files changed, 10 insertions(+) create mode 100644 sqlalchemy_utils/exceptions.py diff --git a/sqlalchemy_utils/__init__.py b/sqlalchemy_utils/__init__.py index f18b14e..cf659af 100644 --- a/sqlalchemy_utils/__init__.py +++ b/sqlalchemy_utils/__init__.py @@ -1,3 +1,4 @@ +from .exceptions import ImproperlyConfigured from .functions import ( sort_query, defer_except, escape_like, primary_keys, table_name ) @@ -26,6 +27,7 @@ __version__ = '0.14.4' __all__ = ( + ImproperlyConfigured, coercion_listener, sort_query, defer_except, diff --git a/sqlalchemy_utils/exceptions.py b/sqlalchemy_utils/exceptions.py new file mode 100644 index 0000000..6744366 --- /dev/null +++ b/sqlalchemy_utils/exceptions.py @@ -0,0 +1,8 @@ +"""Global SQLAlchemy-Utils exception classes. +""" + +class ImproperlyConfigured(Exception): + """ + SQLAlchemy-Utils is improperly configured; normally due to usage of + a utility that depends on a missing library. + """ From 08c38fd58b3858acc5da50eb6dbe0a0bb18fc09c Mon Sep 17 00:00:00 2001 From: Ryan Leckey Date: Wed, 10 Jul 2013 01:32:57 -0700 Subject: [PATCH 2/3] Remove explicit dependencies. --- .travis.yml | 2 +- setup.py | 6 ++-- sqlalchemy_utils/types/color.py | 16 ++++++++++- sqlalchemy_utils/types/phone_number.py | 23 +++++++++++++-- tests/__init__.py | 6 +--- tests/test_coercion_listener.py | 39 -------------------------- tests/test_color.py | 17 +++++++++-- tests/test_number_range.py | 13 ++++++++- tests/test_phonenumber_type.py | 23 ++++++++++++++- tests/test_utility_functions.py | 2 +- 10 files changed, 91 insertions(+), 56 deletions(-) delete mode 100644 tests/test_coercion_listener.py diff --git a/.travis.yml b/.travis.yml index a3033fb..c5a04a9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,6 @@ python: - 2.7 - 3.3 install: - - pip install -q -e ".[test]" --use-mirrors + - pip install -q -e ".[test,color,phone]" --use-mirrors script: - python setup.py test diff --git a/setup.py b/setup.py index 316bf8a..c61996c 100644 --- a/setup.py +++ b/setup.py @@ -45,8 +45,6 @@ setup( install_requires=[ 'six', 'SQLAlchemy>=0.8.0', - 'phonenumbers3k==5.6b1', - 'colour>=0.0.3' ], extras_require={ 'test': [ @@ -55,7 +53,9 @@ setup( 'Jinja2>=2.3', 'docutils>=0.10', 'flexmock>=0.9.7', - ] + ], + 'phone': ['phonenumbers3k==5.6b1'], + 'color': ['colour>=0.0.3'] }, cmdclass={'test': PyTest}, classifiers=[ diff --git a/sqlalchemy_utils/types/color.py b/sqlalchemy_utils/types/color.py index 5e539ea..6f77816 100644 --- a/sqlalchemy_utils/types/color.py +++ b/sqlalchemy_utils/types/color.py @@ -1,6 +1,15 @@ import six -from colour import Color from sqlalchemy import types +from sqlalchemy_utils import ImproperlyConfigured + + +try: + import colour + from colour import Color + +except ImportError: + colour = None + Color = None class ColorType(types.TypeDecorator): @@ -12,6 +21,11 @@ class ColorType(types.TypeDecorator): impl = types.Unicode(20) def __init__(self, max_length=20, *args, **kwargs): + # Bail if colour is not found. + if colour is None: + raise ImproperlyConfigured( + "'colour' is required to use 'ColorType'") + super(ColorType, self).__init__(*args, **kwargs) self.impl = types.Unicode(max_length) diff --git a/sqlalchemy_utils/types/phone_number.py b/sqlalchemy_utils/types/phone_number.py index 425d058..01ce9af 100644 --- a/sqlalchemy_utils/types/phone_number.py +++ b/sqlalchemy_utils/types/phone_number.py @@ -1,9 +1,18 @@ import six -import phonenumbers from sqlalchemy import types +from sqlalchemy_utils import ImproperlyConfigured -class PhoneNumber(phonenumbers.phonenumber.PhoneNumber): +try: + import phonenumbers + from phonenumbers.phonenumber import PhoneNumber as BasePhoneNumber + +except ImportError: + phonenumbers = None + BasePhoneNumber = object + + +class PhoneNumber(BasePhoneNumber): ''' Extends a PhoneNumber class from `Python phonenumbers library`_. Adds different phone number formats to attributes, so they can be easily used @@ -21,6 +30,11 @@ class PhoneNumber(phonenumbers.phonenumber.PhoneNumber): Country code of the phone number. ''' def __init__(self, raw_number, country_code=None): + # Bail if phonenumbers is not found. + if phonenumbers is None: + raise ImproperlyConfigured( + "'phonenumbers' is required to use 'PhoneNumber'") + self._phone_number = phonenumbers.parse(raw_number, country_code) super(PhoneNumber, self).__init__( country_code=self._phone_number.country_code, @@ -66,6 +80,11 @@ class PhoneNumberType(types.TypeDecorator): impl = types.Unicode(20) def __init__(self, country_code='US', max_length=20, *args, **kwargs): + # Bail if phonenumbers is not found. + if phonenumbers is None: + raise ImproperlyConfigured( + "'phonenumbers' is required to use 'PhoneNumberType'") + super(PhoneNumberType, self).__init__(*args, **kwargs) self.country_code = country_code self.impl = types.Unicode(max_length) diff --git a/tests/__init__.py b/tests/__init__.py index 3a117ec..12c3d2c 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -5,10 +5,7 @@ from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy_utils import ( - InstrumentedList, - PhoneNumberType, -) +from sqlalchemy_utils import InstrumentedList @sa.event.listens_for(sa.engine.Engine, 'before_cursor_execute') @@ -47,7 +44,6 @@ class TestCase(object): __tablename__ = 'user' id = sa.Column(sa.Integer, autoincrement=True, primary_key=True) name = sa.Column(sa.Unicode(255)) - phone_number = sa.Column(PhoneNumberType()) class Category(self.Base): __tablename__ = 'category' diff --git a/tests/test_coercion_listener.py b/tests/test_coercion_listener.py deleted file mode 100644 index 4f978f0..0000000 --- a/tests/test_coercion_listener.py +++ /dev/null @@ -1,39 +0,0 @@ -from colour import Color -import sqlalchemy as sa -from sqlalchemy_utils import ( - ColorType, - NumberRangeType, - NumberRange, - PhoneNumberType, - PhoneNumber, - coercion_listener -) -from tests import TestCase - - -class TestCoercionListener(TestCase): - def create_models(self): - class User(self.Base): - __tablename__ = 'user' - id = sa.Column(sa.Integer, primary_key=True) - fav_color = sa.Column(ColorType) - phone_number = sa.Column(PhoneNumberType(country_code='FI')) - number_of_friends = sa.Column(NumberRangeType) - - def __repr__(self): - return 'User(%r)' % self.id - - self.User = User - sa.event.listen( - sa.orm.mapper, 'mapper_configured', coercion_listener - ) - - def test_scalar_attributes_get_coerced_to_objects(self): - user = self.User( - fav_color='white', - phone_number='050111222', - number_of_friends='[12, 18]' - ) - assert isinstance(user.fav_color, Color) - assert isinstance(user.phone_number, PhoneNumber) - assert isinstance(user.number_of_friends, NumberRange) diff --git a/tests/test_color.py b/tests/test_color.py index 46d8aeb..de82cb2 100644 --- a/tests/test_color.py +++ b/tests/test_color.py @@ -1,9 +1,11 @@ -from colour import Color +from pytest import mark import sqlalchemy as sa -from sqlalchemy_utils import ColorType +from sqlalchemy_utils import ColorType, coercion_listener +from sqlalchemy_utils.types import color from tests import TestCase +@mark.xfail('color.colour is None') class TestColorType(TestCase): def create_models(self): class Document(self.Base): @@ -15,8 +17,12 @@ class TestColorType(TestCase): return 'Document(%r)' % self.id self.Document = Document + sa.event.listen(sa.orm.mapper, 'mapper_configured', coercion_listener) + def test_color_parameter_processing(self): + from colour import Color + document = self.Document( bg_color=Color(u'white') ) @@ -26,3 +32,10 @@ class TestColorType(TestCase): document = self.session.query(self.Document).first() assert document.bg_color.hex == Color(u'white').hex + + def test_scalar_attributes_get_coerced_to_objects(self): + from colour import Color + + document = self.Document(bg_color='white') + + assert isinstance(document.bg_color, Color) diff --git a/tests/test_number_range.py b/tests/test_number_range.py index d4d63b2..3e00d86 100644 --- a/tests/test_number_range.py +++ b/tests/test_number_range.py @@ -1,7 +1,12 @@ import sqlalchemy as sa from pytest import raises -from sqlalchemy_utils import NumberRangeType, NumberRange, NumberRangeException from tests import TestCase +from sqlalchemy_utils import ( + NumberRangeType, + NumberRange, + NumberRangeException, + coercion_listener +) class TestNumberRangeType(TestCase): @@ -15,6 +20,7 @@ class TestNumberRangeType(TestCase): return 'Building(%r)' % self.id self.Building = Building + sa.event.listen(sa.orm.mapper, 'mapper_configured', coercion_listener) def test_save_number_range(self): building = self.Building( @@ -42,6 +48,11 @@ class TestNumberRangeType(TestCase): building = self.session.query(self.Building).first() assert building.persons_at_night is None + def test_scalar_attributes_get_coerced_to_objects(self): + building = self.Building(persons_at_night='[12, 18]') + + assert isinstance(building.persons_at_night, NumberRange) + class TestNumberRange(object): def test_equality_operator(self): diff --git a/tests/test_phonenumber_type.py b/tests/test_phonenumber_type.py index 41bbfcd..661182a 100644 --- a/tests/test_phonenumber_type.py +++ b/tests/test_phonenumber_type.py @@ -1,7 +1,11 @@ +from pytest import mark from tests import TestCase -from sqlalchemy_utils import PhoneNumber +import sqlalchemy as sa +from sqlalchemy_utils import PhoneNumberType, PhoneNumber, coercion_listener +from sqlalchemy_utils.types import phone_number +@mark.xfail('phone_number.phonenumbers is None') class TestPhoneNumber(object): def setup_method(self, method): self.valid_phone_numbers = [ @@ -45,7 +49,19 @@ class TestPhoneNumber(object): assert phone_number.__str__() == phone_number.national.encode('utf-8') +@mark.xfail('phone_number.phonenumbers is None') class TestPhoneNumberType(TestCase): + + def create_models(self): + class User(self.Base): + __tablename__ = 'user' + id = sa.Column(sa.Integer, autoincrement=True, primary_key=True) + name = sa.Column(sa.Unicode(255)) + phone_number = sa.Column(PhoneNumberType()) + + self.User = User + sa.event.listen(sa.orm.mapper, 'mapper_configured', coercion_listener) + def setup_method(self, method): super(TestPhoneNumberType, self).setup_method(method) self.phone_number = PhoneNumber( @@ -83,3 +99,8 @@ class TestPhoneNumberType(TestCase): {'param': user.id} ) assert result.first()[0] is None + + def test_scalar_attributes_get_coerced_to_objects(self): + user = self.User(phone_number='050111222') + + assert isinstance(user.phone_number, PhoneNumber) diff --git a/tests/test_utility_functions.py b/tests/test_utility_functions.py index 17fb538..30f307f 100644 --- a/tests/test_utility_functions.py +++ b/tests/test_utility_functions.py @@ -17,7 +17,7 @@ class TestDeferExcept(TestCase): class TestFindNonIndexedForeignKeys(TestCase): - dns = 'postgres://postgres@localhost/sqlalchemy_utils_test' + # dns = 'postgres://postgres@localhost/sqlalchemy_utils_test' def create_models(self): class User(self.Base): From 416342afca4b28c75b7c11faebb82b5e9c903f48 Mon Sep 17 00:00:00 2001 From: Konsta Vesterinen Date: Wed, 10 Jul 2013 08:36:27 +0300 Subject: [PATCH 3/3] Added tests for TSVector autoloading --- sqlalchemy_utils/types/__init__.py | 4 ++++ tests/test_tsvector_type.py | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/sqlalchemy_utils/types/__init__.py b/sqlalchemy_utils/types/__init__.py index 5586b7c..5cf5c49 100644 --- a/sqlalchemy_utils/types/__init__.py +++ b/sqlalchemy_utils/types/__init__.py @@ -1,6 +1,7 @@ from functools import wraps from sqlalchemy.orm.collections import InstrumentedList as _InstrumentedList from sqlalchemy import types +from sqlalchemy.dialects.postgresql.base import ischema_names from .color import ColorType from .email import EmailType from .ip_address import IPAddressType @@ -37,6 +38,9 @@ class TSVectorType(types.UserDefinedType): return 'tsvector' +ischema_names['tsvector'] = TSVectorType + + class InstrumentedList(_InstrumentedList): """Enhanced version of SQLAlchemy InstrumentedList. Provides some additional functionality.""" diff --git a/tests/test_tsvector_type.py b/tests/test_tsvector_type.py index b6b4b76..cfcfbb6 100644 --- a/tests/test_tsvector_type.py +++ b/tests/test_tsvector_type.py @@ -4,6 +4,8 @@ from tests import TestCase class TestTSVector(TestCase): + dns = 'postgres://postgres@localhost/sqlalchemy_utils_test' + def create_models(self): class User(self.Base): __tablename__ = 'user' @@ -18,3 +20,13 @@ class TestTSVector(TestCase): def test_generates_table(self): assert 'search_index' in self.User.__table__.c + + def test_type_autoloading(self): + reflected_metadata = sa.schema.MetaData() + table = sa.schema.Table( + 'user', + reflected_metadata, + autoload=True, + autoload_with=self.engine + ) + assert isinstance(table.c['search_index'].type, TSVectorType)