2

I tried to implement this as a choices variable for a model:

>>> choices = (
...         (1, 'Primary'), (2, 'Secondary'), (3, 'Tertiary')
...         )

But it failed with this quasi-cryptic error:

characters.CharacterArcanumLink.priority: (fields.E005) 'choices' must
be an iterable containing (actual value, human readable name) tuples.

Lets see,

  1. choices is an iterable? Check.
  2. containing (actual value, human readable name) tuples? Check: The Actual value is an integer, and the 'human readable name' is a string.

Looks ok.

Digging into the code, it looks like new checks are done for Django 1.7:

def _check_choices(self):
        if self.choices:
            if (isinstance(self.choices, six.string_types) or
                    not is_iterable(self.choices)):
                return [
                    checks.Error(
                        "'choices' must be an iterable (e.g., a list or tuple).",
                        hint=None,
                        obj=self,
                        id='fields.E004',
                    )
                ]
            elif any(isinstance(choice, six.string_types) or
                     not is_iterable(choice) or len(choice) != 2
                     for choice in self.choices):
                return [
                    checks.Error(
                        ("'choices' must be an iterable containing "
                         "(actual value, human readable name) tuples."),
                        hint=None,
                        obj=self,
                        id='fields.E005',
                    )
                ]
            else:
                return []
        else:
            return []

Its the second check, after the elif that looks fishy. Lets try that isinstance on my choices tuple above:

>>> any(isinstance(choice, six.string_types) for choice in choices)
False

No good! How about with some modification?

>>> any(isinstance(choice[1], six.string_types) for choice in choices)
True
>>>

Excellent.

My question is, have I missed something? I'm sure this used to be possible. Why is it no longer possible? Am I implementing this wrongly?

I've also opened a ticket on code.djangoproject, if that helps?


The code that trigger it comes in two parts:

class Trait(models.Model):
    PRIORITY_CHOICES = (
        (None, '')
        )
    MIN = 0
    MAX = 5
    current_value = IntegerRangeField(min_value=MIN, max_value=MAX)
    maximum_value = IntegerRangeField(min_value=MIN, max_value=MAX)
    priority = models.PositiveSmallIntegerField(choices=PRIORITY_CHOICES, default=None)
    class Meta:
        abstract = True

And:

class CharacterSkillLink(CharacterLink, SkillLink, Trait):
    PRIORITY_CHOICES = (
        (1, 'Primary'), (2, 'Secondary'), (3, 'Tertiary')
        )
    speciality = models.CharField(max_length=200)
ferrangb
  • 2,012
  • 2
  • 20
  • 34
AncientSwordRage
  • 7,086
  • 19
  • 90
  • 173
  • @Pureferret This isn't the actual `choices`, is it? I can't reproduce the bug, and if you ask me, the check is doing exactly what it's supposed to do. – knbk Feb 03 '15 at 17:25
  • @knbk I've added the code that actually triggers this. I might be a subtler bug. – AncientSwordRage Feb 03 '15 at 17:28

1 Answers1

4

The issue is when you have a single choice - the outer brackets don't construct a tuple, just provide grouping. The creation of a tuple is provided by the comma, not the brackets.

((None, '')) == (None, '')

You should leave a trailing comma to signify that you want a single-item tuple (or use a list):

PRIORITY_CHOICES = ((None, ''), )
Gareth Latty
  • 86,389
  • 17
  • 178
  • 183
  • This however should never be evaluated, it's from an abstract class. That seems to fix it though. Bizzare. – AncientSwordRage Feb 03 '15 at 17:29
  • 1
    @Pureferret It probably is still checked by the check framework, as it can't guarantee that all subclasses will override the value. It won't produce any bugs in production, though, if it _is_ always overridden. – knbk Feb 03 '15 at 17:30
  • @knbk but why do my errors point to each subclass? The check must happen after subclassing but before PRIORITY_CHOICES is overridden. – AncientSwordRage Feb 03 '15 at 17:31
  • 1
    Python class variables don't override like that - they are literally just variables on the class object. When you create a subclass and assign `PRIORITY_CHOICES`, it doesn't change the value of `priority`. – Gareth Latty Feb 03 '15 at 17:34
  • @Pureferret Scratch that, it _will_ lead to bugs. The field is constructed during the creation of the abstract base class by the metaclass. Subclasses don't create a new field, they copy the field from their parent class. You can't override the choices like that. It might be possible using a callback or property, but I'm not too sure. – knbk Feb 03 '15 at 17:34
  • @knbk I've seen it 'advertised' elsewhere on SO so long as the superclass is abstract... I'm not sure how else to do this.... – AncientSwordRage Feb 03 '15 at 17:38
  • @Pureferret It will work if you define the field itself on the subclass. Then, you can provide a default value in an (abstract) base class, and override the choices if necessary. I don't know how it's supposed to work if you define the field on the base class, though. Could you post a link to that SO answer? – knbk Feb 03 '15 at 17:40
  • @knbk Found it: http://stackoverflow.com/q/5911155/1075247 Ok so it doesn't advocate it, but I 'tried it' and and it worked. – AncientSwordRage Feb 03 '15 at 17:43