Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 27 additions & 39 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,47 +1,35 @@
sudo: false

language: python

python:
- 2.7
- 3.3
- 3.4
- 3.5
python: 3.5

env:
- DJANGO=1.6
- DJANGO=1.7
- DJANGO=1.8

matrix:
exclude:
- python: 3.5
env: DJANGO=1.6
- python: 3.5
env: DJANGO=1.7
include:
- python: 3.4
env: DJANGO=1.8 COVERAGE=true COVERALLS_REPO_TOKEN=LdECqqwg7eelQx9w8gvooUZCFIaCqGZCv
allow_failures:
- env: COVERAGE=true
matrix:
- ENV=py27-django15
- ENV=py27-django16
- ENV=py27-django17
- ENV=py27-django18
- ENV=py27-django19
- ENV=py27-django110
- ENV=py27-django111
- ENV=py33-django15
- ENV=py33-django16
- ENV=py33-django17
- ENV=py33-django18
- ENV=py34-django15
- ENV=py34-django16
- ENV=py34-django17
- ENV=py34-django18
- ENV=py34-django19
- ENV=py34-django110
- ENV=py34-django111
- ENV=py35-django18
- ENV=py35-django19
- ENV=py35-django110
- ENV=py35-django111
- ENV=coverage

install:
- pip install flake8
- pip install -q "Django>=$DJANGO,<$DJANGO.99"
- make install

script:
- make flake8
- make test

after_script:
- if [ "$COVERAGE" == "true" ]; then
pip install --quiet python-coveralls;
make coverage;
coverage report;
coveralls --ignore-errors;
fi
- pip install tox

notifications:
email:
- [email protected]
script: tox -e $ENV
26 changes: 26 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,32 @@ Create an Enum-class and pass it as first argument to the Django model EnumField
class Beer(models.Model):
style = enum.EnumField(BeerStyle, default=BeerStyle.LAGER)

You can also set default value on your enum class using ``__default__``
attribute
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I agree on the ambiguity that this introduces. To quote the zen of Python "There should be one-- and preferably only one --obvious way to do it."

What bother me most is that having two ways to declare the default means I'll have to check both every time the field does not have a default declared.

Assume the following scenario: I've inherited a code-base, and I'm going through its models and I see:

from .enums import BeerStyle


class Beer(models.Model):
  style = enum.EnumField(BeerStyle)

Now I have to check 2 places to find out if there is a default: the field declaration above, and the enum declaration. This for every single time the EnumField is used with default.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, Enum itself has only one place to set default i.e. via __default__ attribute. Previously, one would need to override .default() method (still possible) and it was not obvious/expected that the first value is the default one. One could restore the old behaviour by subclassing Enum whenever it's needed:

from django_enumfield import enum

class FirstDefaultEnum(enum.Enum):
    def default(cls):
        return list(cls)[0]

EnumField on the other hand may be independent enough to override the default just if it's needed. For example, the same enum may be used in various places and who knows maybe someone will need to override the default just in one of them. I believe one expects this to be possible in a field.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont see any mention of __default__ on the python docs (https://docs.python.org/3/library/enum.html)

I think we should keep our Enum subclass as close as possible to the original, without supercharging it further with custom methods or properties and add the features we need only to the field instead.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see the convenience of declaring __default__ just once, but I'm not sure that's an advantage in the long run. I'd rather be explicit than save some keystrokes.

Plus, I would make the argument that the default is a property of the field, and therefore belongs to the field declaration. Just in the same spirit as Field.choices works. The Enum is data, while default= is behaviour.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, so one could define a custom field for certain enum in case it's used in many places. Sure, let me discuss it with my colleagues to see if everyone is happy about it and redo the commits 😄


.. code:: python

class BeerStyle(enum.Enum):
LAGER = 0
STOUT = 1
WEISSBIER = 2

__default__ = LAGER


class Beer(models.Model):
style_default_lager = enum.EnumField(BeerStyle)
style_default_stout = enum.EnumField(BeerStyle, default=BeerStyle.STOUT)


When you set __default__ attribute, you can access default value via
``.default()`` method of your enum class

.. code:: python

assert BeerStyle.default() == BeerStyle.LAGER


.. code:: python

Beer.objects.create(style=BeerStyle.STOUT)
Expand Down
8 changes: 6 additions & 2 deletions django_enumfield/db/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ class EnumField(base):

def __init__(self, enum, *args, **kwargs):
kwargs['choices'] = enum.choices()
if 'default' not in kwargs:
kwargs['default'] = enum.default()
if enum.default() is not None:
kwargs.setdefault('default', enum.default())
self.enum = enum
super(EnumField, self).__init__(self, *args, **kwargs)

Expand Down Expand Up @@ -78,6 +78,8 @@ def _setup_validation(self, sender, **kwargs):
enum = self.enum

def set_enum(self, new_value):
if isinstance(new_value, models.NOT_PROVIDED):
new_value = None
if hasattr(self, private_att_name):
# Fetch previous value from private enum attribute.
old_value = getattr(self, private_att_name)
Expand All @@ -88,13 +90,15 @@ def set_enum(self, new_value):
if new_value is not None and not isinstance(new_value, Enum):
new_value = enum.get(new_value)
setattr(self, private_att_name, new_value)
self.__dict__[att_name] = new_value
# Run validation for new value.
validators.validate_valid_transition(enum, old_value, new_value)

def get_enum(self):
return getattr(self, private_att_name)

def delete_enum(self):
self.__dict__[att_name] = None
return setattr(self, private_att_name, None)

if not sender._meta.abstract:
Expand Down
8 changes: 6 additions & 2 deletions django_enumfield/enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class Enum(NativeIntEnum):
""" A container for holding and restoring enum values """

__labels__ = {}
__default__ = None
__transitions__ = {}

def __ge__(self, other):
Expand Down Expand Up @@ -80,13 +81,16 @@ def choices(cls, blank=False):

@classmethod
def default(cls):
""" Default Enum value. Override this method if you need another default value.
""" Default Enum value. Set default value to `__default__` attribute
of your enum class or override this method if you need another
default value.
Usage:
IntegerField(choices=my_enum.choices(), default=my_enum.default(), ...
:return Default value, which is the first one by default.
:rtype: enum member
"""
return tuple(cls)[0]
if cls.__default__ is not None:
return cls(cls.__default__)

@classmethod
def field(cls, **kwargs):
Expand Down
17 changes: 17 additions & 0 deletions django_enumfield/tests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ class LampState(Enum):
OFF = 0
ON = 1

__default__ = OFF


class Lamp(models.Model):
state = EnumField(LampState)
Expand All @@ -29,7 +31,18 @@ class PersonStatus(Enum):
}


class PersonStatusDefault(Enum):
UNBORN = 0
ALIVE = 1
DEAD = 2
REANIMATED = 3
VOID = 4

__default__ = UNBORN


class Person(models.Model):
example = models.CharField(max_length=100, default='foo')
status = EnumField(PersonStatus, default=PersonStatus.ALIVE)

def save(self, *args, **kwargs):
Expand All @@ -42,12 +55,16 @@ class BeerStyle(Enum):
STOUT = 1
WEISSBIER = 2

__default__ = LAGER


class BeerState(Enum):
FIZZY = 0
STALE = 1
EMPTY = 2

__default__ = FIZZY


class Beer(models.Model):
style = EnumField(BeerStyle)
Expand Down
46 changes: 37 additions & 9 deletions django_enumfield/tests/test_enum.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.test.client import RequestFactory
from django.db import IntegrityError
from django.db.models.fields import NOT_PROVIDED
from django.forms import ModelForm, TypedChoiceField
from django.test import TestCase
from django.utils import six
Expand All @@ -8,18 +9,23 @@
from django_enumfield.enum import Enum, BlankEnum
from django_enumfield.exceptions import InvalidStatusOperationError
from django_enumfield.tests.models import Person, PersonStatus, Lamp, \
LampState, Beer, BeerStyle, BeerState, LabelBeer
LampState, Beer, BeerStyle, BeerState, LabelBeer, PersonStatusDefault


class EnumFieldTest(TestCase):

def test_enum_field_init(self):
field = EnumField(PersonStatus)
self.assertEqual(field.default, PersonStatus.UNBORN)
self.assertEqual(len(PersonStatus.choices()), len(field.choices))
field = EnumField(PersonStatus, default=PersonStatus.ALIVE)
self.assertEqual(field.default, PersonStatus.ALIVE)
field = EnumField(PersonStatus, default=None)
self.assertEqual(field.default, None)
for enum, default in {
PersonStatus: NOT_PROVIDED,
PersonStatusDefault: PersonStatusDefault.UNBORN,
}.items():
field = EnumField(enum)
self.assertEqual(field.default, default)
self.assertEqual(len(enum.choices()), len(field.choices))
field = EnumField(enum, default=enum.ALIVE)
self.assertEqual(field.default, enum.ALIVE)
field = EnumField(enum, default=None)
self.assertEqual(field.default, None)

def test_enum_field_save(self):
# Test model with EnumField WITHOUT __transitions__
Expand Down Expand Up @@ -83,6 +89,24 @@ def test_enum_field_del_save(self):
self.assertEqual(beer.state, None)
self.assertEqual(beer.style, BeerStyle.STOUT)

def test_enum_field_modelform_create(self):
class PersonForm(ModelForm):
class Meta:
model = Person
fields = ('status',)

request_factory = RequestFactory()
request = request_factory.post('', data={'status': '2'})
form = PersonForm(request.POST)
self.assertTrue(isinstance(form.fields['status'], TypedChoiceField))
self.assertTrue(form.is_valid())
person = form.save()
self.assertTrue(person.status, PersonStatus.DEAD)

request = request_factory.post('', data={'status': '99'})
form = PersonForm(request.POST, instance=person)
self.assertFalse(form.is_valid())

def test_enum_field_modelform(self):
person = Person.objects.create()

Expand Down Expand Up @@ -148,7 +172,11 @@ def test_choices(self):
self.assertEqual(blank, (BlankEnum.BLANK.value, BlankEnum.BLANK))

def test_default(self):
self.assertEqual(PersonStatus.default(), PersonStatus.UNBORN)
for enum, default in {
PersonStatus: None,
PersonStatusDefault: PersonStatusDefault.UNBORN,
}.items():
self.assertEqual(enum.default(), default)

def test_field(self):
self.assertTrue(isinstance(PersonStatus.field(), EnumField))
Expand Down
1 change: 1 addition & 0 deletions django_enumfield/tests/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
urlpatterns = ()
2 changes: 2 additions & 0 deletions run_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ def main():

from django.test.utils import get_runner
test_runner = get_runner(settings)(verbosity=2, interactive=True)
if '--failfast' in sys.argv:
test_runner.failfast = True
failures = test_runner.run_tests(['django_enumfield'])
sys.exit(failures)

Expand Down
Loading