Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does get_FOO_display() return integer value when logging info (django)?

Tags:

django

Why does get_FOO_display() return integer value when logging info (django)?

I have a model field that is using a choice to restrict its value. This works fine and I have it working everywhere within the app, except when logging information, when the get_FOO_display() method returns the underlying integer value instead of the human-readable version.

This is the model definition (abridged):

THING_ROLE_MONSTER = 0
THING_ROLE_MUMMY = 1

ROLE_CHOICES = (
    (THING_ROLE_MONSTER, u'Monster'),
    (THING_ROLE_MUMMY, u'Mummy'),
)

# definition of property within model
class Thing(models.Model):
    ...
    role = models.IntegerField(
        'Role',
        default=0,
        choices=ROLE_CHOICES
    )

If I run this within the (django) interactive shell it behaves exactly as you would expect:

>>> from frankenstein.core.models import Thing
>>> thing = Thing()
>>> thing.role = 0
>>> thing.get_role_display()
u'Monster'

However, when I use exactly the same construct within a string formatting / logging scenario I get the problem:

logger.info('New thing: <b>%s</b>', thing.get_role_display())

returns:

New thing: <b>0</b> 

Help!

[UPDATE 1]

When I run the logging within the interactive shell I get the correct output:

>>> from frankenstein.core.models import Thing
>>> import logging
>>> thing = Thing()
>>> thing.role = 0
>>> logging.info('hello %s', b.get_role_display())
INFO hello Monster

[UPDATE 2] Django internals

Following up on the answer from @joao-oliveira below, I have dug into the internals and uncovered the following.

The underlying _get_FIELD_display method in django.db.models looks like this:

def _get_FIELD_display(self, field):
    value = getattr(self, field.attname)
    return force_unicode(dict(field.flatchoices).get(value, value), strings_only=True)

If I put a breakpoint into the code, and then run ipdb I can see that I have the issue:

ipdb> thing.get_role_display()
u'1'
ipdb> thing._get_FIELD_display(thing._meta.get_field('role'))
u'1'

So, the fix hasn't changed anything. If I then try running through the _get_FIELD_display method code by hand, I get this:

ipdb> fld = thing._meta.get_field('role')
ipdb> fld.flatchoices
[(0, 'Monster'), (1, 'Mummy')]
ipdb> getattr(thing, fld.attname)
u'1'
ipdb> value = getattr(thing, fld.attname)
ipdb> dict(fld.flatchoices).get(value, value)
u'1'

Which is equivalent to saying:

ipdb> {0: 'Monster', 1: 'Mummy'}.get(u'1', u'1')
u'1'

So. The problem we have is that the method is using the string value u'1' to look up the corresponding description in the choices dictionary, but the dictionary keys are integers, and not strings. Hence we never get a match, but instead the default value, which is set to the existing value (the string).

If I manually force the cast to int, the code works as expected:

ipdb> dict(fld.flatchoices).get(int(value), value)
'Mummy'
ipdb> print 'w00t'

This is all great, but doesn't answer my original question as to why the get_foo_display method does return the right value most of the time. At some point the string (u'1') must be cast to the correct data type (1).

[UPDATE 3] The answer

Whilst an honourable mention must go to Joao for his insight, the bounty is going to Josh for pointing out the blunt fact that I am passing in the wrong value to begin with. I put this down to being an emigre from 'strongly-typed-world', where these things can't happen!

The code that I didn't include here is that the object is initialised from a django form, using the cleaned_data from a ChoiceField. The problem with this is that the output from a ChoiceField is a string, not an integer. The bit I missed is that in a loosely-typed language it is possible to set an integer property with a string, and for nothing bad to happen.

Having now looked into this, I see that I should have used the TypedChoiceField, to ensure that the output from cleaned_data is always an integer.

Thank you all.

like image 367
Hugo Rodger-Brown Avatar asked Nov 12 '12 16:11

Hugo Rodger-Brown


2 Answers

I'm really sorry if this sounds condescending, but are you 100% sure that you're setting the value to the integer 1 and not the string '1'?

I've gone diving through the internals and running some tests and the only way that the issue you're experiencing makes sense is if you're setting the value to a string. See my simple test here:

>>> from flogger.models import TestUser
>>> t = TestUser()
>>> t.status = 1
>>> t.get_status_display()
u'Admin'
>>> t.status = '1'
>>> t.get_status_display()
u'1'

Examine your view code, or whatever code is actually setting the value, and examine the output of the field directly.

As you pasted from the internal model code:

def _get_FIELD_display(self, field):
    value = getattr(self, field.attname)
    return force_unicode(dict(field.flatchoices).get(value, value), strings_only=True)

It simply gets the current value of the field, and indexes into the dictionary, and returns the value of the attribute if a lookup isn't found.

I'm guessing there were no errors previously, because the value is coerced into an integer before being inserted into the database.

Edit:

Regarding your update mentioning the type system of python. Firstly, you should be using TypedChoiceField to ensure the form verifies the type that you expect. Secondly, python is a strongly typed language, but the IntegerField does its own coercing with int() when preparing for the database.

Variables are not typed, but the values within them are. I was actually surprised that the IntegerField was coercing the string to an int also. Good lessen to learn here - check the basics first!

like image 127
Josh Smeaton Avatar answered Oct 23 '22 10:10

Josh Smeaton


Haven't tried your code, neither the @like-it answer sorry, but _get_FIELD_display from models.Model is curried in the fields to set the get_Field_display function, so thats probably why you'r getting that output

try calling the _get_FIELD_display:

logging.info('hello %s', b._get_FIELD_display(b._meta.get('role')))
like image 41
jxs Avatar answered Oct 23 '22 11:10

jxs