--- /dev/null
+.. change::
+ :tags: feature, sql
+ :tickets: 3906
+
+ Added support for :class:`.Enum` to persist the values of the enumeration,
+ rather than the keys, when using a Python pep-435 style enumerated object.
+ The user supplies a callable function that will return the string values to
+ be persisted. This allows enumerations against non-string values to be
+ value-persistable as well. Pull request courtesy Jon Snyder.
"""
kw.setdefault("validate_strings", impl.validate_strings)
+ kw.setdefault("values_callable", impl.values_callable)
return cls(**kw)
def _setup_for_values(self, values, objects, kw):
kw.setdefault('inherit_schema', impl.inherit_schema)
kw.setdefault('metadata', impl.metadata)
kw.setdefault('_create_events', False)
+ kw.setdefault('values_callable', impl.values_callable)
return cls(**kw)
def create(self, bind=None, checkfirst=True):
indicated as integers, are **not** used; the value of each enum can
therefore be any kind of Python object whether or not it is persistable.
+ In order to persist the values and not the names, the
+ :paramref:`.Enum.values_callable` parameter may be used. The value of
+ this parameter is a user-supplied callable, which is intended to be used
+ with a PEP-435-compliant enumerated class and returns a list of string
+ values to be persisted. For a simple enumeration that uses string values,
+ a callable such as ``lambda x: [e.value for e in x]`` is sufficient.
+
.. versionadded:: 1.1 - support for PEP-435-style enumerated
classes.
.. versionadded:: 1.1.0b2
+ :param values_callable: A callable which will be passed the PEP-435
+ compliant enumerated type, which should then return a list of string
+ values to be persisted. This allows for alternate usages such as
+ using the string value of an enum to be persisted to the database
+ instead of its name.
+
+ .. versionadded:: 1.2.3
+
"""
self._enum_init(enums, kw)
"""
self.native_enum = kw.pop('native_enum', True)
self.create_constraint = kw.pop('create_constraint', True)
+ self.values_callable = kw.pop('values_callable', None)
values, objects = self._parse_into_values(enums, kw)
self._setup_for_values(values, objects, kw)
if len(enums) == 1 and hasattr(enums[0], '__members__'):
self.enum_class = enums[0]
- values = list(self.enum_class.__members__)
- objects = [self.enum_class.__members__[k] for k in values]
+ if self.values_callable:
+ values = self.values_callable(self.enum_class)
+ else:
+ values = list(self.enum_class.__members__)
+ objects = [self.enum_class.__members__[k] for k in self.enum_class.__members__]
return values, objects
else:
self.enum_class = None
kw.setdefault('metadata', self.metadata)
kw.setdefault('_create_events', False)
kw.setdefault('native_enum', self.native_enum)
+ kw.setdefault('values_callable', self.values_callable)
assert '_enums' in kw
return impltype(**kw)
import datetime
import decimal
from sqlalchemy import types as sqltypes
+from collections import OrderedDict
class TypesTest(fixtures.TestBase,
__dialect__ = mysql.dialect()
__backend__ = True
+ class SomeEnum(object):
+ # Implements PEP 435 in the minimal fashion needed by SQLAlchemy
+ __members__ = OrderedDict()
+
+ def __init__(self, name, value):
+ self.name = name
+ self.value = value
+ self.__members__[name] = self
+ setattr(self.__class__, name, self)
+
+ one = SomeEnum('one', 1)
+ two = SomeEnum('two', 2)
+ three = SomeEnum('three', 3)
+ a_member = SomeEnum('AMember', 'a')
+ b_member = SomeEnum('BMember', 'b')
+
+ @staticmethod
+ def get_enum_string_values(some_enum):
+ return [str(v.value) for v in some_enum.__members__.values()]
+
@testing.provide_metadata
def test_enum(self):
"""Exercise the ENUM type."""
Column('e5', mysql.ENUM("a", "b")),
Column('e5generic', Enum("a", "b")),
Column('e6', mysql.ENUM("'a'", "b")),
+ Column('e7', mysql.ENUM(EnumSetTest.SomeEnum,
+ values_callable=EnumSetTest.
+ get_enum_string_values)),
+ Column('e8', mysql.ENUM(EnumSetTest.SomeEnum))
)
eq_(
eq_(
colspec(enum_table.c.e6),
"e6 ENUM('''a''','b')")
+ eq_(
+ colspec(enum_table.c.e7),
+ "e7 ENUM('1','2','3','a','b')"
+ )
+ eq_(
+ colspec(enum_table.c.e8),
+ "e8 ENUM('one','two','three','AMember','BMember')"
+ )
enum_table.create()
assert_raises(
exc.StatementError,
enum_table.insert().execute,
e1='c', e2='c', e2generic='c', e3='c',
- e4='c', e5='c', e5generic='c', e6='c')
+ e4='c', e5='c', e5generic='c', e6='c',
+ e7='c', e8='c')
enum_table.insert().execute()
enum_table.insert().execute(e1='a', e2='a', e2generic='a', e3='a',
- e4='a', e5='a', e5generic='a', e6="'a'")
+ e4='a', e5='a', e5generic='a', e6="'a'",
+ e7='a', e8='AMember')
enum_table.insert().execute(e1='b', e2='b', e2generic='b', e3='b',
- e4='b', e5='b', e5generic='b', e6='b')
+ e4='b', e5='b', e5generic='b', e6='b',
+ e7='b', e8='BMember')
res = enum_table.select().execute().fetchall()
- expected = [(None, 'a', 'a', None, 'a', None, None, None),
- ('a', 'a', 'a', 'a', 'a', 'a', 'a', "'a'"),
- ('b', 'b', 'b', 'b', 'b', 'b', 'b', 'b')]
+ expected = [(None, 'a', 'a', None, 'a', None, None, None,
+ None, None),
+ ('a', 'a', 'a', 'a', 'a', 'a', 'a', "'a'",
+ EnumSetTest.SomeEnum.AMember,
+ EnumSetTest.SomeEnum.AMember),
+ ('b', 'b', 'b', 'b', 'b', 'b', 'b', 'b',
+ EnumSetTest.SomeEnum.BMember,
+ EnumSetTest.SomeEnum.BMember)]
eq_(res, expected)
self.__members__[alias] = self
setattr(self.__class__, alias, self)
+ class SomeOtherEnum(SomeEnum):
+ __members__ = OrderedDict()
+
one = SomeEnum('one', 1)
two = SomeEnum('two', 2)
three = SomeEnum('three', 3, 'four')
+ a_member = SomeEnum('AMember', 'a')
+ b_member = SomeEnum('BMember', 'b')
+
+ other_one = SomeOtherEnum('one', 1)
+ other_two = SomeOtherEnum('two', 2)
+ other_three = SomeOtherEnum('three', 3)
+ other_a_member = SomeOtherEnum('AMember', 'a')
+ other_b_member = SomeOtherEnum('BMember', 'b')
+
+ @staticmethod
+ def get_enum_string_values(some_enum):
+ return [str(v.value) for v in some_enum.__members__.values()]
@classmethod
def define_tables(cls, metadata):
Column('someenum', Enum(cls.SomeEnum))
)
+ Table(
+ 'stdlib_enum_table2', metadata,
+ Column("id", Integer, primary_key=True),
+ Column('someotherenum',
+ Enum(cls.SomeOtherEnum,
+ values_callable=EnumTest.get_enum_string_values))
+ )
+
def test_python_type(self):
eq_(types.Enum(self.SomeEnum).python_type, self.SomeEnum)
]
)
+ def test_pep435_enum_values_callable_round_trip(self):
+ stdlib_enum_table_custom_values =\
+ self.tables['stdlib_enum_table2']
+
+ stdlib_enum_table_custom_values.insert().execute([
+ {'id': 1, 'someotherenum': self.SomeOtherEnum.AMember},
+ {'id': 2, 'someotherenum': self.SomeOtherEnum.BMember},
+ {'id': 3, 'someotherenum': self.SomeOtherEnum.AMember}
+ ])
+
+ eq_(
+ stdlib_enum_table_custom_values.select().
+ order_by(stdlib_enum_table_custom_values.c.id).execute().
+ fetchall(),
+ [
+ (1, self.SomeOtherEnum.AMember),
+ (2, self.SomeOtherEnum.BMember),
+ (3, self.SomeOtherEnum.AMember)
+ ]
+ )
+
def test_adapt(self):
from sqlalchemy.dialects.postgresql import ENUM
e1 = Enum('one', 'two', 'three', native_enum=False)
is_(e1.adapt(Enum).metadata, e1.metadata)
e1 = Enum(self.SomeEnum)
eq_(e1.adapt(ENUM).name, 'someenum')
- eq_(e1.adapt(ENUM).enums, ['one', 'two', 'three', 'four'])
+ eq_(e1.adapt(ENUM).enums,
+ ['one', 'two', 'three', 'four', 'AMember', 'BMember'])
+
+ e1_vc = Enum(self.SomeOtherEnum,
+ values_callable=EnumTest.get_enum_string_values)
+ eq_(e1_vc.adapt(ENUM).name, 'someotherenum')
+ eq_(e1_vc.adapt(ENUM).enums, ['1', '2', '3', 'a', 'b'])
@testing.provide_metadata
def test_create_metadata_bound_no_crash(self):