#!/usr/bin/env python

class Choices(object):
    """
    Convenience class for Django model choices which will use a [Small]IntegerField
    for storage.
    
        >>> statuses = Choices(
        ...     ('live', 'Live'),
        ...     ('draft', 'Draft'),
        ...     ('hidden', 'Not Live'),
        ... )
        >>> statuses.live
        0
        >>> statuses.hidden
        2
        >>> statuses.get_choices()
        ((0, 'Live'), (1, 'Draft'), (2, 'Not Live'))
    
    This is then useful for use in a model field with the choices attribute.
    
        >>> from django.db import models
        >>> class Entry(models.Model):
        ...     STATUSES = Choices(
        ...         ('live', 'Live'),
        ...         ('draft', 'Draft'),
        ...         ('hidden', 'Not Live'),
        ...     )
        ...     status = models.SmallIntegerField(choices=STATUSES.get_choices(),
        ...                                       default=STATUSES.live)
        
    It's also useful later when you need to filter by your choices.
    
        >>> live_entries = Entry.objects.filter(status=Entries.STATUSES.live)
                                              
    """
    
    def __init__(self, *args):
        super(Choices, self).__init__()
        self.__dict__['_keys'], self.__dict__['_values'] = zip(*args)
    
    def __getattr__(self, name):
        # have to iterate manually to avoid conversion to a list for < 2.6 compat
        for i, k in enumerate(self._keys):
            if k == name:
                return i
        raise AttributeError("No attribute %r." % name)
    
    def __setattr__(self, name, value):
        raise AttributeError("%r object does not support attribute assignment" % self.__class__.__name__)
    
    def __delattr__(self, name):
        raise AttributeError("%r object does not support attribute deletion" % self.__class__.__name__)
    
    def __getitem__(self, name):
        try:
            return self._values[getattr(self, name)]
        except AttributeError:
            raise KeyError(name)
    
    def __contains__(self, name):
        return name in self._keys
    
    def __repr__(self):
        return repr(self.items())
    
    def items(self):
        return tuple(zip(self._keys, self._values))
    
    def keys(self):
        # no need to copy since _keys is a tuple
        return self._keys
    
    def values(self):
        # no need to copy since _values is a tuple
        return self._values
    
    def get_choices(self):
        return tuple(enumerate(self._values))

# tests

if __name__ == '__main__':
    import unittest
    
    class ChoicesTests(unittest.TestCase):
        
        def setUp(self):
            self.c = Choices(
                ('stuff', 'Stuff'),
                ('whatnot', 'What-not'),
                ('things', 'Awesome Things'),
            ) 
        
        def test_choice_attributes(self):
            self.assertEqual(self.c.stuff, 0)
            self.assertEqual(self.c.whatnot, 1)
            self.assertEqual(self.c.things, 2)
            
        def test_choice_values(self):
            self.assertEqual(self.c['stuff'], 'Stuff')
            self.assertEqual(self.c['whatnot'], 'What-not')
            self.assertEqual(self.c['things'], 'Awesome Things')
        
        def test_model_choices(self):
            correct_choices = (
                (0, 'Stuff'),
                (1, 'What-not'),
                (2, 'Awesome Things'),
            )
            self.assertEqual(self.c.get_choices(), correct_choices)
        
        def test_missing_attrs(self):
            self.assertRaises(AttributeError, lambda: self.c.missing)
            self.assertRaises(KeyError, lambda: self.c['missing'])
        
        def _del_attr(self):
            del self.c.stuff
        
        def _del_item(self):
            del self.c['stuff']
        
        def _set_attr(self):
            self.c.stuff = 'other stuff'
        
        def _set_item(self):
            self.c['stuff'] = 'other stuff'
        
        def test_immutable(self):
            self.assertRaises(AttributeError, self._del_attr)
            self.assertRaises(AttributeError, self._set_attr)
            self.assertRaises(TypeError, self._del_item)
            self.assertRaises(TypeError, self._set_item)
            
    unittest.main()