Ordering enum value in python - python

I would like to be able to arrange the ordering of Enum. Has somebody suggestions how this can be solved?
The following Enum meta class is using:
class EnumMeta(type):
def __new__(typ, name, bases, attrs):
cls_attrs = {}
cls_choices = []
for attr_name, value in attrs.items():
cls_attrs[attr_name] = attr_name.lower()
if not attr_name.startswith("__"):
cls_choices.append((attr_name.lower(), value))
def choices(cls):
return cls_choices
def values(cls, value=None):
if value is None:
return {choice[0]: unicode(choice[1]) for choice in cls.choices()}
elif isinstance(value, list):
return {choice[0]: unicode(choice[1]) for choice in cls.choices() if choice[0] in value}
else:
return unicode(dict(cls.choices()).get(value))
def keys(cls, nil=False):
items = [item[0] for item in cls.choices()]
if nil:
items.append('')
return items
def combined_length(cls):
return len(",".join(cls.values().keys()))
def max_length(cls):
return max(map(len, cls.values().keys()))
cls_attrs['choices'] = classmethod(choices)
cls_attrs['values'] = classmethod(values)
cls_attrs['keys'] = classmethod(keys)
cls_attrs['combined_length'] = classmethod(combined_length)
cls_attrs['max_length'] = classmethod(max_length)
return type(name, bases, cls_attrs)
An example of an Enum is as follow:
class SideHemType:
__ordering__ = ['double', 'single']
__metaclass__ = EnumMeta
Single = "Single side hem for opaque fabrics"
Double = "Double side hem for transparent fabrics"
class TestEnumOrdering:
print SideHemType.keys()
print SideHemType.values()
By printing the Enum SideHemType first Double is printed and then Single. But I would like first Single and then Double.

If you are using Python3.4 you can use the new enum.Enum type, which remembers the order the enum members are declared in.
If you are using an earlier Python, you should use the enum34 package available from PyPI, which supports Pythons back to 2.4.
The enum34 package, if used in Python3, also remembers the order of member declarations. If used in Python 2 it supports an extra _order_ attribute:
from enum import Enum
class SideHemType(Enum):
_order_ = 'Single Double' # only needed in Python 2
Single = "Single side hem for opaque fabrics"
Double = "Double side hem for transparent fabrics"
#classmethod
def combined_length(cls):
return len(",".join(mbr.name for mbr in cls))
#classmethod
def max_length(cls):
return max(map(len, (mbr.name for mbr in cls)))
print list(SideHemType) # [SideHemType.Single, SideHemType.Double]
print SideHemType.Double.value # "Double side hem for transparent fabrics"

Use IntEnum from the enum package and use the integer values to specify the order that you want:
class Shape(IntEnum):
CIRCLE = 1
SQUARE = 2
Shape.CIRCLE < Shape.SQUARE
Prints True.

How about this
class MyEnum(enum.Enum):
first_item = 'bla bla'
whatever = 'blubb'
another_one = 'blobb'
def __lt__(self, other: 'MyEnum'):
if self == other:
return False
# the following works because the order of elements in the definition is preserved
for elem in MyEnum:
if self == elem:
return True
elif other == elem:
return False
raise RuntimeError('Bug: we should never arrive here') # I just like being pedantic

Your Enum loses the ordering in 3 places. First the attributes on the class body are stored in a dictionary, then you copy the items into another dictionary. Finally your values() returns a 3rd dictionary. A dictionary does not save ordering, and it is impossible to get the ordering of the attributes within the class body.
With this system the easiest is to have a variable
__ordering__ = [ 'single', 'double' ]
And make the values() return a list of tuples (like dict.items()).
class EnumMeta(type):
def __new__(typ, name, bases, attrs):
cls_attrs = {}
cls_choices = {}
for attr_name, value in attrs.items():
cls_attrs[attr_name] = attr_name.lower()
if not attr_name.startswith("__"):
cls_choices[attr_name.lower()] = value
ordering = attrs.get('__ordering__')
if ordering == None:
ordering = sorted(cls_choices.keys())
def choices(cls):
return dict(cls_choices)
def values(cls, value=None):
if value is None:
return [ (k, cls_choices[k] ) for k in ordering ]
elif not isinstance(value, basestring):
return [ (k, cls_choices[k] ) for k in value ]
else:
return unicode(cls_choices.get(value))
def keys(cls, nil=False):
items = list(ordering)
if nil:
items.append('')
return items
def combined_length(cls):
return len(",".join(cls.values().keys()))
def max_length(cls):
return max(map(len, cls.values().keys()))
cls_attrs['choices'] = classmethod(choices)
cls_attrs['values'] = classmethod(values)
cls_attrs['keys'] = classmethod(keys)
cls_attrs['combined_length'] = classmethod(combined_length)
cls_attrs['max_length'] = classmethod(max_length)
return type(name, bases, cls_attrs)
class SideHemType:
__ordering__ = ['double', 'single']
__metaclass__ = EnumMeta
Single = "Single side hem for opaque fabrics"
Double = "Double side hem for transparent fabrics"
print SideHemType.keys()
print SideHemType.values()

Related

Can't loop through dict_keys in #property method in python 3?

So I have the following code:
#property
def mod_list(self) -> List[Modifier]:
mods = []
print(len(self.statuses)) #Prints 0??? Update method prints the actual number when called??? Also means it *is* getting called properly when it's getting accessed
for status in self.statuses: # I've tried calling the keys() method on the dict but that doesn't work either
print("hello") #Doesn't print, indicating that it isn't looping
mods.extend(status.mods) # Note: statuses dict uses StatusEffect objects as keys, with values being the number of turns left before that status is removed; StatusEffects all possess a 'mods' property that is initialized to '[]' and can only be made up of modifiers
return mods
I don't understand why it can't access the keys of the dict? Even if I remove the decorator and call it instead of accessing it?
Especially when this method works properly?
def update(self):
deletion = []
print(len(self.statuses)) #Prints actual number of keys????
for name in self.statuses.keys():
print(name.name, self.statuses[name]) #Prints normally whenever update is called???
if hasattr(name, "turn_effect"):
name.turn_effect(self.entity)
self.statuses[name] -= 1
if self.statuses[name] < 1:
deletion.append(name)
...
for status in deletion:
del self.statuses[status]
Why isn't it working properly? And how do I fix it?
Edit: I managed to recreate the issue below, I think it might have to do with 'deepcopy' in the spawn method since I couldn't recreate the issue from scratch until I implemented and used the spawn method.
from __future__ import annotations
from typing import Dict, List
from copy import copy, deepcopy
class Entity:
def __init__(self, name:str, **kwargs:Component):
self.name = name
self.components:Dict[str, Component] = {}
for name, component in kwargs.items():
self.add_component(name, component)
def add_component(self, name:str, component:Component):
self.components[name] = component
component.entity = self
def update(self):
for comp in self.components.values():
comp.update()
def spawn(self):
return deepcopy(self)
class Component:
__entity: Entity
#property
def entity(self) -> Entity:
return self.__entity
#entity.setter
def entity(self, entity:Entity):
if hasattr(self, "__entity") and self.__entity is not None:
self.entity.remove_component(self)
self.__entity = entity
def update(self):
"""Placeholder method for component update methods"""
class StatusList(Component):
entity: Entity
def __init__(self) -> None:
self.statuses:Dict[StatusEffect, int] = {}
def add_status(self, status:StatusEffect, turns:int=1):
self.statuses[status] = turns
def update(self):
deletion = []
print(len(self.statuses.keys()))
for name in self.statuses.keys():
print(name.name, self.statuses[name])
if hasattr(name, "turn_effect"):
name.turn_effect(self.entity)
self.statuses[name] -= 1
if self.statuses[name] < 1:
deletion.append(name)
for status in deletion:
del self.statuses[status]
#property
def mod_list(self) -> List[Modifier]:
mods = []
print(len(self.statuses))
for status in self.statuses:
print("hello")
mods.extend(status.mods)
return mods
class StatusEffect:
name:str
turn_effect: function
mods:List[Modifier] = []
def apply(self, entity:Entity, turns:int=1):
if "status_list" in entity.components.keys():
entity.components["status_list"].add_status(self.copy(), turns)
def copy(self): #I specifically defined this method in the original code in case I need to modify it in the future
return copy(self)
class StatList(Component):
entity: Entity
stat_record: List[Stat] = []
def __init__(self, **stats:Stat) -> None:
for name, stat in stats.items():
stat.stat_list = self
stat.name = name
self.stat_record.append(stat)
def get_stat(self, name:str) -> Optional[Stat]:
for stat in self.stat_record:
if name == stat.name:
return stat
def get_stat_name(self, stat:Stat) -> Optional[str]:
if stat in record:
return stat.name
class Stat:
name:str
base_value:int
def __init__(self, base:int=0):
self.base_value = base
#property
def entity(self) -> Entity:
return self.stat_list.entity
#property
def current_value(self) -> int:
value = self.base_value
for mod in self.get_modifiers():
value += mod.value
return int(value)
def get_modifiers(self):
for component in self.entity.components.values():
if hasattr(component, "mod_list"):
for mod in component.mod_list:
if mod.stat == self.name:
yield mod
class Modifier:
stat: str
value: Union[int, float]
def __init__(self, stat:str, value:Union[int, float]):
self.stat = stat
self.value = value
rage = StatusEffect()
rage.name = "Rage"
rage.turn_effect = lambda entity : print(f"{entity.name} is enraged")
rage.mods = [
Modifier("atk", 5)
]
player = Entity(
name="Player",
stat_list=StatList(atk=Stat(5)),
status_list=StatusList()
).spawn()
rage.apply(player, 10)
while True:
player.update()
player.components["stat_list"].get_stat("atk").current_value
input()
Unfortunately, using copy() in the spawn method would result in entities created that way sharing status effects, stats, etc., which really defeats the purpose of spawning new entities
Edit 2: Modified spawn method to use copy and to copy all components, have to add guard clauses now but it works.

How to decrypted [<__main__.Food object at 0x1097ba828>

I am a python newbie. I want display actual names,values and calories instead of [<__main__.Food object at 0x1097ba828>, <__main__.Food object at 0x1097ba860>, <__main__.Food object at 0x1097ba898>] I know this question is very simple,but it would be a great help if you could let me know the answer!
class Food(object):
def __init__(self,n,v,w):
self.name = n
self.value = v
self.calories = w
def getValue(self):
return self.value
def getCal(self):
return self.calories
def density(self):
return self.getValue()/self.getCal()
def __str__(self):
return '<__main__.Food: '+self.name +' '+ self.value+' ' + self.calories
def buildMenu(self):
menu = []
for i in range(len(values)):
menu.append(Food(self.name[i], self.value[i], self.calories[i]))
return menu
names=['burger','fries','coke']
values=[1,2,3]
calories=[100,200,300]
if __name__ == '__main__':
new = Food(names, values, calories)
print(new.buildMenu())
Thank you!
I made two code changes to get what I think you're looking for. The first is to convert values to strings in your str function. The second is to use that.
def __str__(self):
return '<__main__.Food: '+ str(self.name) +' '+ str(self.value)+' ' + str(self.calories)
and
print (str(new)) #instead of print(new.buildMenu())
Now the output is:
<main.Food: ['burger', 'fries', 'coke'] [1, 2, 3] [100, 200, 300]
This is how I would do it, noting that we've created two classes: a separate Food and Menu class. The Menu class has an add method that appends to its foodItems property, though I don't feel like that's really necessary since we can just do direct property assignment:
m.foodItems = < some list of Food objects >
I've removed the confusing buildMenu method from the Food class, and defined __str__ methods for both classes:
class Food(object):
def __init__(self,n,v,w):
self.name = n
self.value = v
self.calories = w
def getValue(self):
return self.value
def getCal(self):
return self.calories
def density(self):
return self.getValue()/self.getCal()
def __str__(self):
return '\t'.join([self.name, str(self.value), str(self.calories)])
class Menu(object):
def __init__(self):
self.foodItems = []
def add(self, foodItem):
self.foodItems.append(foodItem)
def __str__(self):
"""
prints the food items
"""
s = 'Item\tValue\tCalories\n'
s += '\n'.join(str(f) for f in self.foodItems)
return s
names=['burger','fries','coke']
values=[1,2,3]
calories=[100,200,300]
m = Menu()
items = list(Food(n,v,c) for n,v,c in zip(names,values,calories))
m.foodItems = items
print(m)
And outputs like:
The issue you have is that you're printing a list of Food instances, not a single instance at a time. The list type's __str__ operator calls repr on the items the list contains, not str, so your __str__ method does not get run.
A simple fix is to just rename your __str__ method to __repr__.
I'd note that it's a bit strange that you're building a Food instance with lists of values for name, value and calories, just so that you can call a method on it to make a list of Food instances with the individual values. A more Pythoic approach would be to pass the lists to a classmethod that returns the list of instances, without the intermediate instance needing to exist:
#classmethod
def buildMenu(cls, names, values, calories):
menu = []
for i in range(len(values)): # consider using zip instead of looping over indexes
menu.append(cls(names[i], values[i], calories[i]))
return menu
You'd call it on the class:
if __name__ == '__main__':
print(Food.buildMenu(names, values, calories))

getattr and setattr on nested subobjects / chained properties?

I have an object (Person) that has multiple subobjects (Pet, Residence) as properties. I want to be able to dynamically set the properties of these subobjects like so:
class Person(object):
def __init__(self):
self.pet = Pet()
self.residence = Residence()
class Pet(object):
def __init__(self,name='Fido',species='Dog'):
self.name = name
self.species = species
class Residence(object):
def __init__(self,type='House',sqft=None):
self.type = type
self.sqft=sqft
if __name__=='__main__':
p=Person()
setattr(p,'pet.name','Sparky')
setattr(p,'residence.type','Apartment')
print p.__dict__
Currently I get the wrong output: {'pet': <__main__.Pet object at 0x10c5ec050>, 'residence': <__main__.Residence object at 0x10c5ec0d0>, 'pet.name': 'Sparky', 'residence.type': 'Apartment'}
As you can see, instead of setting the name attribute on the Pet subobject of the Person, a new attribute pet.name is created on the Person.
I cannot specify person.pet to setattr() because different sub-objects will be set by the same method, which parses some text and fills in the object attributes if/when a relevant key is found.
Is there a easy/builtin way to accomplish this?
Or perhaps I need to write a recursive function to parse the string and call getattr() multiple times until the necessary subobject is found and then call setattr() on that found subobject?
You could use functools.reduce:
import functools
def rsetattr(obj, attr, val):
pre, _, post = attr.rpartition('.')
return setattr(rgetattr(obj, pre) if pre else obj, post, val)
# using wonder's beautiful simplification: https://stackoverflow.com/questions/31174295/getattr-and-setattr-on-nested-objects/31174427?noredirect=1#comment86638618_31174427
def rgetattr(obj, attr, *args):
def _getattr(obj, attr):
return getattr(obj, attr, *args)
return functools.reduce(_getattr, [obj] + attr.split('.'))
rgetattr and rsetattr are drop-in replacements for getattr and setattr,
which can also handle dotted attr strings.
import functools
class Person(object):
def __init__(self):
self.pet = Pet()
self.residence = Residence()
class Pet(object):
def __init__(self,name='Fido',species='Dog'):
self.name = name
self.species = species
class Residence(object):
def __init__(self,type='House',sqft=None):
self.type = type
self.sqft=sqft
def rsetattr(obj, attr, val):
pre, _, post = attr.rpartition('.')
return setattr(rgetattr(obj, pre) if pre else obj, post, val)
def rgetattr(obj, attr, *args):
def _getattr(obj, attr):
return getattr(obj, attr, *args)
return functools.reduce(_getattr, [obj] + attr.split('.'))
if __name__=='__main__':
p = Person()
print(rgetattr(p, 'pet.favorite.color', 'calico'))
# 'calico'
try:
# Without a default argument, `rgetattr`, like `getattr`, raises
# AttributeError when the dotted attribute is missing
print(rgetattr(p, 'pet.favorite.color'))
except AttributeError as err:
print(err)
# 'Pet' object has no attribute 'favorite'
rsetattr(p, 'pet.name', 'Sparky')
rsetattr(p, 'residence.type', 'Apartment')
print(p.__dict__)
print(p.pet.name)
# Sparky
print(p.residence.type)
# Apartment
For an out of the box solution, you can use operator.attrgetter:
from operator import attrgetter
attrgetter(dotted_path)(obj)
For one parent and one child:
if __name__=='__main__':
p = Person()
parent, child = 'pet.name'.split('.')
setattr(getattr(p, parent), child, 'Sparky')
parent, child = 'residence.type'.split('.')
setattr(getattr(p, parent), child, 'Sparky')
print p.__dict__
This is simpler than the other answers for this particular use case.
unutbu's answer (https://stackoverflow.com/a/31174427/2683842) has a "bug". After getattr() fails and is replaced by default, it continues calling getattr on default.
Example: rgetattr(object(), "nothing.imag", 1) should equal 1 in my opinion, but it returns 0:
getattr(object(), 'nothing', 1) == 1.
getattr(1, 'imag', 1) == 0 (since 1 is real and has no complex component).
Solution
I modified rgetattr to return default at the first missing attribute:
import functools
DELIMITER = "."
def rgetattr(obj, path: str, *default):
"""
:param obj: Object
:param path: 'attr1.attr2.etc'
:param default: Optional default value, at any point in the path
:return: obj.attr1.attr2.etc
"""
attrs = path.split(DELIMITER)
try:
return functools.reduce(getattr, attrs, obj)
except AttributeError:
if default:
return default[0]
raise
This should be a
def getNestedAttr(obj,nestedParam):
next = obj
for p in nestedParam.split('.'):
next = getattr(next,p)
return next
class Issue : pass
issue = Issue()
issue.status = Issue()
issue.status.name = "Hello"
getattr(issue,'status.name')
'''
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Issue' object has no attribute 'status.name'
'''
getNestedAttr(issue,'status.name')
#'Hello'
simple solution
I made a simple version based on ubntu's answer called magicattr that also works on attrs, lists, and dicts by parsing and walking the ast.
For example, with this class:
class Person:
settings = {
'autosave': True,
'style': {
'height': 30,
'width': 200
},
'themes': ['light', 'dark']
}
def __init__(self, name, age, friends):
self.name = name
self.age = age
self.friends = friends
bob = Person(name="Bob", age=31, friends=[])
jill = Person(name="Jill", age=29, friends=[bob])
jack = Person(name="Jack", age=28, friends=[bob, jill])
You can do this
# Nothing new
assert magicattr.get(bob, 'age') == 31
# Lists
assert magicattr.get(jill, 'friends[0].name') == 'Bob'
assert magicattr.get(jack, 'friends[-1].age') == 29
# Dict lookups
assert magicattr.get(jack, 'settings["style"]["width"]') == 200
# Combination of lookups
assert magicattr.get(jack, 'settings["themes"][-2]') == 'light'
assert magicattr.get(jack, 'friends[-1].settings["themes"][1]') == 'dark'
# Setattr
magicattr.set(bob, 'settings["style"]["width"]', 400)
assert magicattr.get(bob, 'settings["style"]["width"]') == 400
# Nested objects
magicattr.set(bob, 'friends', [jack, jill])
assert magicattr.get(jack, 'friends[0].friends[0]') == jack
magicattr.set(jill, 'friends[0].age', 32)
assert bob.age == 32
It also won't let you/someone call functions or assign a value since it doesn't use eval or allow Assign/Call nodes.
with pytest.raises(ValueError) as e:
magicattr.get(bob, 'friends = [1,1]')
# Nice try, function calls are not allowed
with pytest.raises(ValueError):
magicattr.get(bob, 'friends.pop(0)')
And a easy to understand three-liner based on jimbo1qaz's answer, reduced to the very limit:
def rgetattr(obj, path, default):
try:
return functools.reduce(getattr, path.split(), obj)
except AttributeError:
return default
Usage:
>>> class O(object):
... pass
... o = O()
... o.first = O()
... o.first.second = O()
... o.first.second.third = 42
... rgetattr(o, 'first second third', None)
42
Just keep in mind that "space" is not a typical delimiter for this use case.
Thanks for the accepted answer above. It was helpful.
In case anyone wants to extend the use for hasattr use the code below:
def rhasattr(obj, attr):
_nested_attrs = attr.split(".")
_curr_obj = obj
for _a in _nested_attrs[:-1]:
if hasattr(_curr_obj, _a):
_curr_obj = getattr(_curr_obj, _a)
else:
return False
return hasattr(_curr_obj, _nested_attrs[-1])
Ok so while typing the question I had an idea of how to do this and it seems to work fine. Here is what I came up with:
def set_attribute(obj, path_string, new_value):
parts = path_string.split('.')
final_attribute_index = len(parts)-1
current_attribute = obj
i = 0
for part in parts:
new_attr = getattr(current_attribute, part, None)
if current_attribute is None:
print 'Error %s not found in %s' % (part, current_attribute)
break
if i == final_attribute_index:
setattr(current_attribute, part, new_value)
current_attribute = new_attr
i+=1
def get_attribute(obj, path_string):
parts = path_string.split('.')
final_attribute_index = len(parts)-1
current_attribute = obj
i = 0
for part in parts:
new_attr = getattr(current_attribute, part, None)
if current_attribute is None:
print 'Error %s not found in %s' % (part, current_attribute)
return None
if i == final_attribute_index:
return getattr(current_attribute, part)
current_attribute = new_attr
i += 1
I guess this solves my question, but I am still curious if there is a better way to do this?
I feel like this has to be something pretty common in OOP and python, so I'm surprised gatattr and setattr do not support this natively.
Here's something similar to ChaimG's answer, but it works with an arbitrary number of cases. However, it only supports get attributes, not setting them.
requested_attr = 'pet.name'
parent = Person()
sub_names = requested_attr.split('.')
sub = None
for sub_name in sub_names:
try:
sub = parent.__getattribute__(sub_name)
parent = sub
except AttributeError:
raise Exception("The panel doesn't have an attribute that matches your request!")
pets_name = sub
I just love recursive functions
def rgetattr(obj,attr):
_this_func = rgetattr
sp = attr.split('.',1)
if len(sp)==1:
l,r = sp[0],''
else:
l,r = sp
obj = getattr(obj,l)
if r:
obj = _this_func(obj,r)
return obj
I know this post is pretty old but below code might help some one.
def getNestedObjectValue(obj={}, attr=""):
splittedFields = attr.split(".")
nestedValue = ""
previousValue = ""
for field in splittedFields:
previousValue = nestedValue
nestedValue = (
obj.get(field) if previousValue == "" else previousValue.get(field)
)
return nestedValue
print(
getNestedObjectValue(
obj={
"name": "ADASDASD",
"properties": {"somefield": {"value": "zxczxcxczxcxzc"}},
},
attr="properties.somefield.value",
)
)
Output
PS C:\myprograms\samples> python .\sample.py
zxczxcxczxcxzc

GDB pretty-printing: returning string from a children()'s iterator, but displayed as a char[]

I have a handy class that I use to allow me to easily add a set of "summariser" functions to a GDB pretty printer (for example, a Rect class could have an [Area] field, computed by Python). it then prints all the existing children as well, so you can see everything at once.
class SummaryAndFieldIterator:
"""
Iterator to first go through a list of summariser functions,
then display all the fields in the object in order
"""
def __init__ (self, obj, summaries):
self.count = 0
self.obj = obj;
self.summaries = summaries;
self.keys = sorted(obj.type.iterkeys())
def __iter__(self):
return self
def __next__(self):
if (self.count >= len(self.keys) + len(self.summaries)):
raise StopIteration
elif self.count < len(self.summaries):
name, retVal = self.summaries[self.count](self.obj)
# FIXME: this doesn't seem to work when a string is returned
# in retVal?
result = "[%s]" % name, retVal
else:
field = self.count - len(self.summaries)
result = self.keys[field], self.obj[self.keys[field]]
self.count += 1
return result
next = __next__
class MyObjectPrinter:
def __init__(self, val):
self.val = val
def get_int(self):
return "meaning", 42
def get_string(self):
return "hoopiness", "Forty-two"
def children(self):
return SummaryAndFieldIterator(self.val, [self.get_string])
This works very well for the summarisers which return numeric values, but for strings, it ends up displaying as an array, so that I get
NAME VALUE
myobj {..}
|-->[meaning] 42
|-->[hoopiness]
|-->[0] 'F'
|-->[1] 'o'
.....
|-->real_field 34234
This is presumably becuase the string that comes from
name, retVal = self.summaries[self.count](self.obj)
does not generate a sufficiently "stringy" gdb.Value object when it is returned by SummaryAndFieldIterator's __next__ method. Adjusting the display_hint() method of MyObjectPrinter doesn't seem to have any effect (but I doubt it would, as this is the child, not the object).
Anyone know how to return a string from the children() iterator and get it to display as a string?
Okay, apparently this may be a bug related to the way that GDB/MI communicates with pretty-printers, Bugzilla created here : https://sourceware.org/bugzilla/show_bug.cgi?id=18282

Serialize and De-serialize a Django Enum field to accept Numerical and Text representation

I'm trying to create an enum field in Django that, upon a GET request will return the text representation of the enum and upon a POST or PATCH request will convert the text representation to the corresponding integer before saving.
The
transform_<field>()
method works nicely for converting the integer enum value to its corresponding string, but I can't figure out a better way of converting the string into it's corresponding integer other than hacking the
validate_<field>()
method.
Is there a better way of doing this? Please see code below
Models file
class Status(enum.Enum):
RUNNING = 0
COMPLETED = 1
labels = {
RUNNING: 'Running',
COMPLETED: 'Completed'
}
translation = {v: k for k, v in labels.iteritems()}
class Job(models.Model):
status = enum.EnumField(Status)
Serializer
class JobSeralizer(serializers.ModelSerailzer):
status = seralizers.CharField(max_length=32, default=Status.QUEUED)
def transform_status(self, obj, value):
return JobStatus.labels[value]
def validate_status(self, attrs, source):
"""Allow status to take numeric or character representation of status
"""
status = attrs[source]
if status in JobStatus.translation:
attrs[source] = JobStatus.translation[status]
elif status.isdigit():
attrs[source] = int(status)
else:
raise serializers.ValidationError("'%s' not a valid status" % status)
return attrs
As OP stated, you can do this easily using custom fields in drf v3.x. Here's a quick example of a generic custom field used to convert values <-> labels (e.g. enum values <-> textual representation):
class KeyValueField(serializers.Field):
""" A field that takes a field's value as the key and returns
the associated value for serialization """
labels = {}
inverted_labels = {}
def __init__(self, labels, *args, **kwargs):
self.labels = labels
# Check to make sure the labels dict is reversible, otherwise
# deserialization may produce unpredictable results
inverted = {}
for k, v in labels.iteritems():
if v in inverted:
raise ValueError(
'The field is not deserializable with the given labels.'
' Please ensure that labels map 1:1 with values'
)
inverted[v] = k
self.inverted_labels = inverted
return super(KeyValueField, self).__init__(*args, **kwargs)
def to_representation(self, obj):
if type(obj) is list:
return [self.labels.get(o, None) for o in obj]
else:
return self.labels.get(obj, None)
def to_internal_value(self, data):
if type(data) is list:
return [self.inverted_labels.get(o, None) for o in data]
else:
return self.inverted_labels.get(data, None)
The field initialization would look something like this:
class MySerializer(serializers.Serializer):
afield = KeyValueField(labels={0:'enum text 0', 1:'enum text 1'})

Categories

Resources