From f0f894f2057de780362e11c07f6e0b81fa8c0273 Mon Sep 17 00:00:00 2001 From: Konsta Vesterinen Date: Wed, 8 May 2013 15:48:58 +0300 Subject: [PATCH] Added coercion_listener --- CHANGES.rst | 6 +++++ docs/index.rst | 40 ++++++++++++++++++++++++++++++++- requirements-dev.txt | 1 - requirements.txt | 1 - setup.py | 5 ++--- sqlalchemy_utils/__init__.py | 2 ++ sqlalchemy_utils/listeners.py | 19 ++++++++++++++++ sqlalchemy_utils/types.py | 18 +++++++++++++++ tests/test_coercion_listener.py | 39 ++++++++++++++++++++++++++++++++ 9 files changed, 125 insertions(+), 6 deletions(-) create mode 100644 sqlalchemy_utils/listeners.py create mode 100644 tests/test_coercion_listener.py diff --git a/CHANGES.rst b/CHANGES.rst index a792440..67bb247 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,12 @@ Changelog Here you can see the full list of changes between each SQLAlchemy-Utils release. +0.11.0 (2013-05-08) +^^^^^^^^^^^^^^^^^^^ + +- Added coercion_listener + + 0.10.0 (2013-04-29) ^^^^^^^^^^^^^^^^^^^ diff --git a/docs/index.rst b/docs/index.rst index 85f67f4..245b440 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,7 +6,45 @@ SQLAlchemy-Utils ================ -SQLAlchemy-Utils provides various utility classes and functions for SQLAlchemy. +SQLAlchemy-Utils provides custom data types and various utility functions for SQLAlchemy. + +Using automatic data coercion +----------------------------- + +SQLAlchemy-Utils provides various new data types for SQLAlchemy and in order to gain full +advantage of these datatypes you should use coercion_listener. Setting up the listener is easy: + +:: + + import sqlalchemy as sa + from sqlalchemy_utils import coercion_listener + + + sa.event.listen(sa.orm.mapper, 'mapper_configured', coercion_listener) + + +The listener automatically detects SQLAlchemy-Utils compatible data types and coerces all attributes +using these types to appropriate objects. + + +Example +:: + from colour import Color + from sqlalchemy_utils import ColorType + + + class Document(Base): + __tablename__ = 'player' + id = db.Column(db.Integer, autoincrement=True) + name = db.Column(db.Unicode(50)) + background_color = db.Column(ColorType) + + + document = Document() + document.background_color = 'F5F5F5' + document.background_color # Color object + session.commit() + ScalarListType -------------- diff --git a/requirements-dev.txt b/requirements-dev.txt index 23b7fac..e2929c0 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,4 +3,3 @@ pytest==2.2.3 Pygments==1.2 Jinja2==2.3 docutils>=0.10 -phonenumbers>=5.4b1 diff --git a/requirements.txt b/requirements.txt index 7e90aa4..8516366 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ SQLAlchemy>=0.7.8 -psycopg2>=2.4.6 phonenumbers>=5.4b1 colour==0.0.2 diff --git a/setup.py b/setup.py index fcb6e2f..860932e 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ SQLAlchemy-Utils ---------------- -Various utility functions for SQLAlchemy. +Various utility functions and custom data types for SQLAlchemy. """ from setuptools import setup, Command @@ -24,7 +24,7 @@ class PyTest(Command): setup( name='SQLAlchemy-Utils', - version='0.10.0', + version='0.11.0', url='https://github.com/kvesteri/sqlalchemy-utils', license='BSD', author='Konsta Vesterinen', @@ -39,7 +39,6 @@ setup( platforms='any', install_requires=[ 'SQLAlchemy>=0.7.8', - 'psycopg2>=2.4.6', 'phonenumbers>=5.4b1', 'colour==0.0.2' ], diff --git a/sqlalchemy_utils/__init__.py b/sqlalchemy_utils/__init__.py index 59cdcab..0f6ab21 100644 --- a/sqlalchemy_utils/__init__.py +++ b/sqlalchemy_utils/__init__.py @@ -1,4 +1,5 @@ from .functions import sort_query, defer_except, escape_like +from .listeners import coercion_listener from .merge import merge, Merger from .types import ( ColorType, @@ -17,6 +18,7 @@ from .types import ( __all__ = ( + coercion_listener, sort_query, defer_except, escape_like, diff --git a/sqlalchemy_utils/listeners.py b/sqlalchemy_utils/listeners.py new file mode 100644 index 0000000..243e35b --- /dev/null +++ b/sqlalchemy_utils/listeners.py @@ -0,0 +1,19 @@ +import sqlalchemy as sa + + +def coercion_listener(mapper, class_): + """ + Auto assigns coercing listener for all class properties which are of coerce + capable type. + """ + for prop in mapper.iterate_properties: + try: + listener = prop.columns[0].type.coercion_listener + except AttributeError: + continue + sa.event.listen( + getattr(class_, prop.key), + 'set', + listener, + retval=True + ) diff --git a/sqlalchemy_utils/types.py b/sqlalchemy_utils/types.py index c215df9..f0bc990 100644 --- a/sqlalchemy_utils/types.py +++ b/sqlalchemy_utils/types.py @@ -84,6 +84,11 @@ class PhoneNumberType(types.TypeDecorator): return PhoneNumber(value, self.country_code) return value + def coercion_listener(self, target, value, oldvalue, initiator): + if value is not None and not isinstance(value, PhoneNumber): + value = PhoneNumber(value, country_code=self.country_code) + return value + class ColorType(types.TypeDecorator): """ @@ -107,6 +112,11 @@ class ColorType(types.TypeDecorator): return Color(value) return value + def coercion_listener(self, target, value, oldvalue, initiator): + if value is not None and not isinstance(value, Color): + value = Color(value) + return value + class ScalarListException(Exception): pass @@ -174,6 +184,14 @@ class NumberRangeType(types.TypeDecorator): return NumberRange.from_normalized_str(value) return value + def coercion_listener(self, target, value, oldvalue, initiator): + if value is not None and not isinstance(value, NumberRange): + if isinstance(value, basestring): + value = NumberRange.from_normalized_str(value) + else: + raise TypeError + return value + class NumberRangeException(Exception): pass diff --git a/tests/test_coercion_listener.py b/tests/test_coercion_listener.py new file mode 100644 index 0000000..68ad509 --- /dev/null +++ b/tests/test_coercion_listener.py @@ -0,0 +1,39 @@ +from colour import Color +import sqlalchemy as sa +from sqlalchemy_utils import ( + ColorType, + NumberRangeType, + NumberRange, + PhoneNumberType, + PhoneNumber, + coercion_listener +) +from tests import DatabaseTestCase + + +class TestCoercionListener(DatabaseTestCase): + 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)