From e6d0e680dd1a8dc105efa3a0a4ac80e0b9e1bc4f Mon Sep 17 00:00:00 2001 From: Jiangge Zhang Date: Thu, 15 Jan 2015 16:32:32 +0800 Subject: [PATCH 1/4] add support for built-in enum or backported enum34. --- docs/data_types.rst | 7 +++ setup.py | 1 + sqlalchemy_utils/__init__.py | 2 + sqlalchemy_utils/types/__init__.py | 2 + sqlalchemy_utils/types/enum.py | 70 ++++++++++++++++++++++++++++++ tests/types/test_enum.py | 53 ++++++++++++++++++++++ 6 files changed, 135 insertions(+) create mode 100644 sqlalchemy_utils/types/enum.py create mode 100644 tests/types/test_enum.py diff --git a/docs/data_types.rst b/docs/data_types.rst index 9501570..28fc066 100644 --- a/docs/data_types.rst +++ b/docs/data_types.rst @@ -45,6 +45,13 @@ EncryptedType .. autoclass:: EncryptedType +EnumType +^^^^^^^^ + +.. module:: sqlalchemy_utils.types.enum + +.. autoclass:: EnumType + JSONType ^^^^^^^^ diff --git a/setup.py b/setup.py index fa822fa..bac51a7 100644 --- a/setup.py +++ b/setup.py @@ -45,6 +45,7 @@ extras_require = { 'password': ['passlib >= 1.6, < 2.0'], 'color': ['colour>=0.0.4'], 'ipaddress': ['ipaddr'] if not PY3 else [], + 'enum': ['enum34'] if sys.version_info < (3, 4) else [], 'timezone': ['python-dateutil'], 'url': ['furl >= 0.4.1'], 'encrypted': ['cryptography>=0.6'] diff --git a/sqlalchemy_utils/__init__.py b/sqlalchemy_utils/__init__.py index 0e97c92..368ee1c 100644 --- a/sqlalchemy_utils/__init__.py +++ b/sqlalchemy_utils/__init__.py @@ -65,6 +65,7 @@ from .types import ( DateTimeRangeType, EmailType, EncryptedType, + EnumType, instrumented_list, InstrumentedList, IntRangeType, @@ -144,6 +145,7 @@ __all__ = ( DateTimeRangeType, EmailType, EncryptedType, + EnumType, ExpressionParser, ImproperlyConfigured, InstrumentedList, diff --git a/sqlalchemy_utils/types/__init__.py b/sqlalchemy_utils/types/__init__.py index 9e37990..706c85f 100644 --- a/sqlalchemy_utils/types/__init__.py +++ b/sqlalchemy_utils/types/__init__.py @@ -6,6 +6,7 @@ from .color import ColorType from .country import CountryType, Country from .email import EmailType from .encrypted import EncryptedType +from .enum import EnumType from .ip_address import IPAddressType from .json import JSONType from .locale import LocaleType @@ -36,6 +37,7 @@ __all__ = ( DateTimeRangeType, EmailType, EncryptedType, + EnumType, IntRangeType, IPAddressType, JSONType, diff --git a/sqlalchemy_utils/types/enum.py b/sqlalchemy_utils/types/enum.py new file mode 100644 index 0000000..afb828d --- /dev/null +++ b/sqlalchemy_utils/types/enum.py @@ -0,0 +1,70 @@ +from __future__ import absolute_import + +try: + from enum import Enum +except ImportError: + Enum = None + +from sqlalchemy import types +from sqlalchemy_utils.exceptions import ImproperlyConfigured +from .scalar_coercible import ScalarCoercible + + +class EnumType(types.TypeDecorator, ScalarCoercible): + """ + EnumType offers way of integrating with :mod:`enum` in the standard + library of Python 3.4+ or the enum34_ backported package on PyPI. + + .. _enum34: https://pypi.python.org/pypi/enum34 + + :: + + from enum import Enum + from sqlalchemy_utils import EnumType + + + class OrderStatus(Enum): + unpaid = 1 + paid = 2 + + + class Order(Base): + __tablename__ = 'order' + id = sa.Column(sa.Integer, autoincrement=True) + status = sa.Column(EnumType(OrderStatus)) + + + order = Order() + order.status = OrderStatus.unpaid + session.add(order) + session.commit() + + assert user.status is OrderStatus.unpaid + assert user.status.value == 1 + assert user.status.name == 'paid' + """ + + impl = types.Integer + + def __init__(self, enum_class, impl=None, *args, **kwargs): + if Enum is None: + raise ImproperlyConfigured( + "'enum34' package is required to use 'EnumType' in Python " + "< 3.4") + if not issubclass(enum_class, Enum): + raise ImproperlyConfigured( + "EnumType needs a class of enum defined.") + + super(EnumType, self).__init__(*args, **kwargs) + self.enum_class = enum_class + if impl is not None: + self.impl = types.Integer + + def process_bind_param(self, value, dialect): + return self.enum_class(value).value if value else None + + def process_result_value(self, value, dialect): + return self.enum_class(value) if value else None + + def _coerce(self, value): + return self.enum_class(value) if value else None diff --git a/tests/types/test_enum.py b/tests/types/test_enum.py new file mode 100644 index 0000000..8d94251 --- /dev/null +++ b/tests/types/test_enum.py @@ -0,0 +1,53 @@ +from pytest import mark +import sqlalchemy as sa +from sqlalchemy_utils.types import enum +from tests import TestCase + + +@mark.skipif('enum.Enum is None') +class TestEnumType(TestCase): + def create_models(self): + class OrderStatus(enum.Enum): + unpaid = 1 + paid = 2 + + class Order(self.Base): + __tablename__ = 'document' + id_ = sa.Column(sa.Integer, primary_key=True) + status = sa.Column( + enum.EnumType(OrderStatus), default=OrderStatus.unpaid) + + def __repr__(self): + return 'Order(%r, %r)' % (self.id_, self.status) + + def pay(self): + self.status = OrderStatus.paid + + self.OrderStatus = OrderStatus + self.Order = Order + + def test_parameter_processing(self): + order = self.Order() + + self.session.add(order) + self.session.commit() + + order = self.session.query(self.Order).first() + assert order.status is self.OrderStatus.unpaid + assert order.status.value == 1 + + order.pay() + self.session.commit() + + order = self.session.query(self.Order).first() + assert order.status is self.OrderStatus.paid + assert order.status.value == 2 + + def test_parameter_coercing(self): + order = self.Order() + order.status = 2 + + self.session.add(order) + self.session.commit() + + assert order.status is self.OrderStatus.paid From d98956f5a78cc3c83777be0291b55d2f6d9cb367 Mon Sep 17 00:00:00 2001 From: Jiangge Zhang Date: Sun, 18 Jan 2015 18:54:06 +0800 Subject: [PATCH 2/4] make ChoiceType be compatible with Enum type. --- docs/data_types.rst | 7 -- sqlalchemy_utils/__init__.py | 2 - sqlalchemy_utils/types/__init__.py | 2 - sqlalchemy_utils/types/choice.py | 123 ++++++++++++++++++++++++++--- sqlalchemy_utils/types/enum.py | 70 ---------------- tests/types/test_choice.py | 53 ++++++++++++- tests/types/test_enum.py | 53 ------------- 7 files changed, 165 insertions(+), 145 deletions(-) delete mode 100644 sqlalchemy_utils/types/enum.py delete mode 100644 tests/types/test_enum.py diff --git a/docs/data_types.rst b/docs/data_types.rst index 28fc066..9501570 100644 --- a/docs/data_types.rst +++ b/docs/data_types.rst @@ -45,13 +45,6 @@ EncryptedType .. autoclass:: EncryptedType -EnumType -^^^^^^^^ - -.. module:: sqlalchemy_utils.types.enum - -.. autoclass:: EnumType - JSONType ^^^^^^^^ diff --git a/sqlalchemy_utils/__init__.py b/sqlalchemy_utils/__init__.py index 368ee1c..0e97c92 100644 --- a/sqlalchemy_utils/__init__.py +++ b/sqlalchemy_utils/__init__.py @@ -65,7 +65,6 @@ from .types import ( DateTimeRangeType, EmailType, EncryptedType, - EnumType, instrumented_list, InstrumentedList, IntRangeType, @@ -145,7 +144,6 @@ __all__ = ( DateTimeRangeType, EmailType, EncryptedType, - EnumType, ExpressionParser, ImproperlyConfigured, InstrumentedList, diff --git a/sqlalchemy_utils/types/__init__.py b/sqlalchemy_utils/types/__init__.py index 706c85f..9e37990 100644 --- a/sqlalchemy_utils/types/__init__.py +++ b/sqlalchemy_utils/types/__init__.py @@ -6,7 +6,6 @@ from .color import ColorType from .country import CountryType, Country from .email import EmailType from .encrypted import EncryptedType -from .enum import EnumType from .ip_address import IPAddressType from .json import JSONType from .locale import LocaleType @@ -37,7 +36,6 @@ __all__ = ( DateTimeRangeType, EmailType, EncryptedType, - EnumType, IntRangeType, IPAddressType, JSONType, diff --git a/sqlalchemy_utils/types/choice.py b/sqlalchemy_utils/types/choice.py index a085250..cb9be0d 100644 --- a/sqlalchemy_utils/types/choice.py +++ b/sqlalchemy_utils/types/choice.py @@ -2,6 +2,10 @@ from sqlalchemy import types import six from ..exceptions import ImproperlyConfigured from .scalar_coercible import ScalarCoercible +try: + from enum import Enum +except ImportError: + Enum = None class Choice(object): @@ -29,14 +33,21 @@ class Choice(object): class ChoiceType(types.TypeDecorator, ScalarCoercible): """ - ChoiceType offers way of having fixed set of choices for given column. - Columns with ChoiceTypes are automatically coerced to Choice objects. + ChoiceType offers way of having fixed set of choices for given column. It + could work with a list of tuple (a collection of key-value pairs), or + integrate with :mod:`enum` in the standard library of Python 3.4+ (the + enum34_ backported package on PyPI is compatible too for ``< 3.4``). + .. _enum34: https://pypi.python.org/pypi/enum34 + + Columns with ChoiceTypes are automatically coerced to Choice objects while + a list of tuple been passed to the constructor. If a subclass of + :class:`enum.Enum` is passed, columns will be coerced to :class:`enum.Enum` + objects instead. :: - - class User(self.Base): + class User(Base): TYPES = [ (u'admin', u'Admin'), (u'regular-user', u'Regular user') @@ -51,6 +62,25 @@ class ChoiceType(types.TypeDecorator, ScalarCoercible): user = User(type=u'admin') user.type # Choice(type='admin', value=u'Admin') + Or:: + + import enum + + + class UserType(enum.Enum): + admin = 1 + regular = 2 + + + class User(Base): + __tablename__ = 'user' + id = sa.Column(sa.Integer, primary_key=True) + name = sa.Column(sa.Unicode(255)) + type = sa.Column(ChoiceType(UserType, impl=sa.Integer())) + + + user = User(type=1) + user.type # ChoiceType is very useful when the rendered values change based on user's @@ -61,7 +91,7 @@ class ChoiceType(types.TypeDecorator, ScalarCoercible): from babel import lazy_gettext as _ - class User(self.Base): + class User(Base): TYPES = [ (u'admin', _(u'Admin')), (u'regular-user', _(u'Regular user')) @@ -77,17 +107,46 @@ class ChoiceType(types.TypeDecorator, ScalarCoercible): user.type # Choice(type='admin', value=u'Admin') print user.type # u'Admin' + + Or:: + + from enum import Enum + from babel import lazy_gettext as _ + + + class UserType(Enum): + admin = 1 + regular = 2 + + + UserType.admin.label = _(u'Admin') + UserType.regular.label = _(u'Regular user') + + + class User(Base): + __tablename__ = 'user' + id = sa.Column(sa.Integer, primary_key=True) + name = sa.Column(sa.Unicode(255)) + type = sa.Column(ChoiceType(TYPES)) + + + user = User(type=UserType.admin) + user.type # + + print user.type.label # u'Admin' """ impl = types.Unicode(255) def __init__(self, choices, impl=None): - if not choices: - raise ImproperlyConfigured( - 'ChoiceType needs list of choices defined.' - ) self.choices = choices - self.choices_dict = dict(choices) + + if Enum is not None and \ + isinstance(choices, type) and issubclass(choices, Enum): + self.type_impl = EnumTypeImpl(enum_class=choices) + else: + self.type_impl = ChoiceTypeImpl(choices=choices) + if impl: self.impl = impl @@ -95,6 +154,26 @@ class ChoiceType(types.TypeDecorator, ScalarCoercible): def python_type(self): return self.impl.python_type + def _coerce(self, value): + return self.type_impl._coerce(value) + + def process_bind_param(self, value, dialect): + return self.type_impl.process_bind_param(value, dialect) + + def process_result_value(self, value, dialect): + return self.type_impl.process_result_value(value, dialect) + + +class ChoiceTypeImpl(object): + """The implementation for the ``Choice`` usage.""" + + def __init__(self, choices): + if not choices: + raise ImproperlyConfigured( + 'ChoiceType needs list of choices defined.' + ) + self.choices_dict = dict(choices) + def _coerce(self, value): if value is None: return value @@ -111,3 +190,27 @@ class ChoiceType(types.TypeDecorator, ScalarCoercible): if value: return Choice(value, self.choices_dict[value]) return value + + +class EnumTypeImpl(object): + """The implementation for the ``Enum`` usage.""" + + def __init__(self, enum_class): + if Enum is None: + raise ImproperlyConfigured( + "'enum34' package is required to use 'EnumType' in Python " + "< 3.4") + if not issubclass(enum_class, Enum): + raise ImproperlyConfigured( + "EnumType needs a class of enum defined.") + + self.enum_class = enum_class + + def _coerce(self, value): + return self.enum_class(value) if value else None + + def process_bind_param(self, value, dialect): + return self.enum_class(value).value if value else None + + def process_result_value(self, value, dialect): + return self.enum_class(value) if value else None diff --git a/sqlalchemy_utils/types/enum.py b/sqlalchemy_utils/types/enum.py deleted file mode 100644 index afb828d..0000000 --- a/sqlalchemy_utils/types/enum.py +++ /dev/null @@ -1,70 +0,0 @@ -from __future__ import absolute_import - -try: - from enum import Enum -except ImportError: - Enum = None - -from sqlalchemy import types -from sqlalchemy_utils.exceptions import ImproperlyConfigured -from .scalar_coercible import ScalarCoercible - - -class EnumType(types.TypeDecorator, ScalarCoercible): - """ - EnumType offers way of integrating with :mod:`enum` in the standard - library of Python 3.4+ or the enum34_ backported package on PyPI. - - .. _enum34: https://pypi.python.org/pypi/enum34 - - :: - - from enum import Enum - from sqlalchemy_utils import EnumType - - - class OrderStatus(Enum): - unpaid = 1 - paid = 2 - - - class Order(Base): - __tablename__ = 'order' - id = sa.Column(sa.Integer, autoincrement=True) - status = sa.Column(EnumType(OrderStatus)) - - - order = Order() - order.status = OrderStatus.unpaid - session.add(order) - session.commit() - - assert user.status is OrderStatus.unpaid - assert user.status.value == 1 - assert user.status.name == 'paid' - """ - - impl = types.Integer - - def __init__(self, enum_class, impl=None, *args, **kwargs): - if Enum is None: - raise ImproperlyConfigured( - "'enum34' package is required to use 'EnumType' in Python " - "< 3.4") - if not issubclass(enum_class, Enum): - raise ImproperlyConfigured( - "EnumType needs a class of enum defined.") - - super(EnumType, self).__init__(*args, **kwargs) - self.enum_class = enum_class - if impl is not None: - self.impl = types.Integer - - def process_bind_param(self, value, dialect): - return self.enum_class(value).value if value else None - - def process_result_value(self, value, dialect): - return self.enum_class(value) if value else None - - def _coerce(self, value): - return self.enum_class(value) if value else None diff --git a/tests/types/test_choice.py b/tests/types/test_choice.py index 50ae141..5badad6 100644 --- a/tests/types/test_choice.py +++ b/tests/types/test_choice.py @@ -1,7 +1,8 @@ from flexmock import flexmock -from pytest import raises +from pytest import raises, mark import sqlalchemy as sa from sqlalchemy_utils import ChoiceType, Choice, ImproperlyConfigured +from sqlalchemy_utils.types.choice import Enum from tests import TestCase @@ -76,3 +77,53 @@ class TestChoiceTypeWithCustomUnderlyingType(TestCase): def test_init_type(self): type_ = ChoiceType([(1, u'something')], impl=sa.Integer) assert type_.impl == sa.Integer + + +@mark.skipif('Enum is None') +class TestEnumType(TestCase): + def create_models(self): + class OrderStatus(Enum): + unpaid = 1 + paid = 2 + + class Order(self.Base): + __tablename__ = 'order' + id_ = sa.Column(sa.Integer, primary_key=True) + status = sa.Column( + ChoiceType(OrderStatus, impl=sa.Integer()), + default=OrderStatus.unpaid) + + def __repr__(self): + return 'Order(%r, %r)' % (self.id_, self.status) + + def pay(self): + self.status = OrderStatus.paid + + self.OrderStatus = OrderStatus + self.Order = Order + + def test_parameter_processing(self): + order = self.Order() + + self.session.add(order) + self.session.commit() + + order = self.session.query(self.Order).first() + assert order.status is self.OrderStatus.unpaid + assert order.status.value == 1 + + order.pay() + self.session.commit() + + order = self.session.query(self.Order).first() + assert order.status is self.OrderStatus.paid + assert order.status.value == 2 + + def test_parameter_coercing(self): + order = self.Order() + order.status = 2 + + self.session.add(order) + self.session.commit() + + assert order.status is self.OrderStatus.paid diff --git a/tests/types/test_enum.py b/tests/types/test_enum.py deleted file mode 100644 index 8d94251..0000000 --- a/tests/types/test_enum.py +++ /dev/null @@ -1,53 +0,0 @@ -from pytest import mark -import sqlalchemy as sa -from sqlalchemy_utils.types import enum -from tests import TestCase - - -@mark.skipif('enum.Enum is None') -class TestEnumType(TestCase): - def create_models(self): - class OrderStatus(enum.Enum): - unpaid = 1 - paid = 2 - - class Order(self.Base): - __tablename__ = 'document' - id_ = sa.Column(sa.Integer, primary_key=True) - status = sa.Column( - enum.EnumType(OrderStatus), default=OrderStatus.unpaid) - - def __repr__(self): - return 'Order(%r, %r)' % (self.id_, self.status) - - def pay(self): - self.status = OrderStatus.paid - - self.OrderStatus = OrderStatus - self.Order = Order - - def test_parameter_processing(self): - order = self.Order() - - self.session.add(order) - self.session.commit() - - order = self.session.query(self.Order).first() - assert order.status is self.OrderStatus.unpaid - assert order.status.value == 1 - - order.pay() - self.session.commit() - - order = self.session.query(self.Order).first() - assert order.status is self.OrderStatus.paid - assert order.status.value == 2 - - def test_parameter_coercing(self): - order = self.Order() - order.status = 2 - - self.session.add(order) - self.session.commit() - - assert order.status is self.OrderStatus.paid From eed9ed0ee3a61991aae1185335debfc321e88ec3 Mon Sep 17 00:00:00 2001 From: Konsta Vesterinen Date: Sun, 1 Mar 2015 12:43:09 +0200 Subject: [PATCH 3/4] Add style fixes --- sqlalchemy_utils/types/choice.py | 13 +++++++++---- tests/types/test_choice.py | 3 ++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/sqlalchemy_utils/types/choice.py b/sqlalchemy_utils/types/choice.py index cb9be0d..e93fb45 100644 --- a/sqlalchemy_utils/types/choice.py +++ b/sqlalchemy_utils/types/choice.py @@ -141,8 +141,11 @@ class ChoiceType(types.TypeDecorator, ScalarCoercible): def __init__(self, choices, impl=None): self.choices = choices - if Enum is not None and \ - isinstance(choices, type) and issubclass(choices, Enum): + if ( + Enum is not None and + isinstance(choices, type) and + issubclass(choices, Enum) + ): self.type_impl = EnumTypeImpl(enum_class=choices) else: self.type_impl = ChoiceTypeImpl(choices=choices) @@ -199,10 +202,12 @@ class EnumTypeImpl(object): if Enum is None: raise ImproperlyConfigured( "'enum34' package is required to use 'EnumType' in Python " - "< 3.4") + "< 3.4" + ) if not issubclass(enum_class, Enum): raise ImproperlyConfigured( - "EnumType needs a class of enum defined.") + "EnumType needs a class of enum defined." + ) self.enum_class = enum_class diff --git a/tests/types/test_choice.py b/tests/types/test_choice.py index 5badad6..d82177e 100644 --- a/tests/types/test_choice.py +++ b/tests/types/test_choice.py @@ -91,7 +91,8 @@ class TestEnumType(TestCase): id_ = sa.Column(sa.Integer, primary_key=True) status = sa.Column( ChoiceType(OrderStatus, impl=sa.Integer()), - default=OrderStatus.unpaid) + default=OrderStatus.unpaid + ) def __repr__(self): return 'Order(%r, %r)' % (self.id_, self.status) From 358d8d77a96743744be9d0873aecbb2f1dc55deb Mon Sep 17 00:00:00 2001 From: Konsta Vesterinen Date: Sun, 1 Mar 2015 12:44:13 +0200 Subject: [PATCH 4/4] Bump version --- CHANGES.rst | 6 ++++++ sqlalchemy_utils/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 4b4a23d..9cf5f05 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.29.7 (2015-03-01) +^^^^^^^^^^^^^^^^^^^ + +- Added Enum representation support for ChoiceType + + 0.29.6 (2015-02-03) ^^^^^^^^^^^^^^^^^^^ diff --git a/sqlalchemy_utils/__init__.py b/sqlalchemy_utils/__init__.py index 0d1eb70..8329d2b 100644 --- a/sqlalchemy_utils/__init__.py +++ b/sqlalchemy_utils/__init__.py @@ -87,7 +87,7 @@ from .types import ( from .models import Timestamp -__version__ = '0.29.6' +__version__ = '0.29.7' __all__ = (