Find enums listed in Python DESCRIPTOR for ProtoBuf
Question:
I have received a Google ProtoBuf using Python, and am attempting to compare the value for an enum to a string representation of it. Based on this and this I should be able to use something like enum_values_by_name
to get the info I need. However, all the enum*
related attributes are empty:
>>> type(my_message)
<class 'myObjects_pb2.myObject'>
>>> my_message
# nID: 53564
# nAge: 2
# type: OBJECT_CLASS_SIGN
# view: OBJECT_VIEW_FRONT
>>> my_message.type
# 1
>>> filter(lambda s: s.startswith('enum'), dir(my_message.DESCRIPTOR))
# ['enum_types', 'enum_types_by_name', 'enum_values_by_name']
>>> my_message.DESCRIPTOR.enum_types
# []
>>> my_message.DESCRIPTOR.enum_types_by_name
# {}
>>> my_message.DESCRIPTOR.enum_values_by_name
# {}
Perhaps it is related to the fact that my protobuf is defined in many files, and the enums I want are not defined in the main file which I’m importing (but which is used to decode my_message
)?
Why am I getting these empty collections and (more importantly) how do find information about the enums?
Answers:
I don’t know why the DESCRIPTOR
for the message includes enum attributes that are not populated. (This seems like a bug to me.) However, there are (at least) two solutions to this:
1) If you know the name of the file where the enums are defined, you can ‘hack’ get the enum value by name via:
# This is the root ProtoBuf definition
from mydir import rootapi_pb2
# We happen to know that the enums are defined in myenums.proto
enum_file = rootapi_pb2.myenums__pb2 # NOTE: Extra Underscore!
enum_value = getattr(enum_file, 'OBJECT_CLASS_SIGN')
If you don’t want to rely on this hack, however, you can eventually find the enum descriptor, and thus the value from the name, via:
my_message.DESCRIPTOR.fields_by_name['type'].enum_type.values_by_name['OBJECT_CLASS_SIGN'].number
Because that’s so horrific, here it is wrapped up as a safe, re-usable function:
def enum_value(msg, field_name, enum_name):
"""Return the integer for the enum name of a field,
or None if the field does not exist, is not an enum, or the
enum does not have a value with the supplied name."""
field = msg.DESCRIPTOR.fields_by_name.get(field_name,None)
if field and field.enum_type:
enum = field.enum_type.values_by_name.get(enum_name,None)
return enum and enum.number
print(enum_value(my_message, 'type', 'OBJECT_CLASS_SIGN'))
# 1
ProtoBuf for python is super ugly…but I had to use it anyway…
I feel like the function by @Phrogz could be simplified a bit. This is the function I’ve came with:
def get_enum_name_by_number(parent, field_name):
field_value = getattr(parent, field_name)
return parent.DESCRIPTOR.fields_by_name[field_name].enum_type.values_by_number.get(field_value).name
print(my_message.type)
# 1
print(get_enum_name_by_number(my_message, 'type'))
# OBJECT_CLASS_SIGN
Fyi, if you want to go in the opposite direction, from int value to key in a protobuf enum I’ve been using this:
def lookup_template_name(template_id: int) -> str:
"""Lookup the template name from the template id"""
try:
return notify.EmailTemplate.Name(template_id)
except Exception as e:
logger.error(f"Error looking up template name: {e}")
return e
logger.info(f"Template name is: {lookup_template_name(1)}")
# 2022-11-23 16:04:11:977 INFO Template name is: EMAIL_TEMPLATE_WELCOME_EMAIL
logger.info(f"Template name is: {lookup_template_name(123456)}")
# 2022-11-23 16:04:50:334 ERROR Error looking up template name: Enum EmailTemplate has no name defined for value 123456
The only issue is that you need to already know the name of the enum (in this case EmailTemplate
)
I have received a Google ProtoBuf using Python, and am attempting to compare the value for an enum to a string representation of it. Based on this and this I should be able to use something like enum_values_by_name
to get the info I need. However, all the enum*
related attributes are empty:
>>> type(my_message)
<class 'myObjects_pb2.myObject'>
>>> my_message
# nID: 53564
# nAge: 2
# type: OBJECT_CLASS_SIGN
# view: OBJECT_VIEW_FRONT
>>> my_message.type
# 1
>>> filter(lambda s: s.startswith('enum'), dir(my_message.DESCRIPTOR))
# ['enum_types', 'enum_types_by_name', 'enum_values_by_name']
>>> my_message.DESCRIPTOR.enum_types
# []
>>> my_message.DESCRIPTOR.enum_types_by_name
# {}
>>> my_message.DESCRIPTOR.enum_values_by_name
# {}
Perhaps it is related to the fact that my protobuf is defined in many files, and the enums I want are not defined in the main file which I’m importing (but which is used to decode my_message
)?
Why am I getting these empty collections and (more importantly) how do find information about the enums?
I don’t know why the DESCRIPTOR
for the message includes enum attributes that are not populated. (This seems like a bug to me.) However, there are (at least) two solutions to this:
1) If you know the name of the file where the enums are defined, you can ‘hack’ get the enum value by name via:
# This is the root ProtoBuf definition
from mydir import rootapi_pb2
# We happen to know that the enums are defined in myenums.proto
enum_file = rootapi_pb2.myenums__pb2 # NOTE: Extra Underscore!
enum_value = getattr(enum_file, 'OBJECT_CLASS_SIGN')
If you don’t want to rely on this hack, however, you can eventually find the enum descriptor, and thus the value from the name, via:
my_message.DESCRIPTOR.fields_by_name['type'].enum_type.values_by_name['OBJECT_CLASS_SIGN'].number
Because that’s so horrific, here it is wrapped up as a safe, re-usable function:
def enum_value(msg, field_name, enum_name):
"""Return the integer for the enum name of a field,
or None if the field does not exist, is not an enum, or the
enum does not have a value with the supplied name."""
field = msg.DESCRIPTOR.fields_by_name.get(field_name,None)
if field and field.enum_type:
enum = field.enum_type.values_by_name.get(enum_name,None)
return enum and enum.number
print(enum_value(my_message, 'type', 'OBJECT_CLASS_SIGN'))
# 1
ProtoBuf for python is super ugly…but I had to use it anyway…
I feel like the function by @Phrogz could be simplified a bit. This is the function I’ve came with:
def get_enum_name_by_number(parent, field_name):
field_value = getattr(parent, field_name)
return parent.DESCRIPTOR.fields_by_name[field_name].enum_type.values_by_number.get(field_value).name
print(my_message.type)
# 1
print(get_enum_name_by_number(my_message, 'type'))
# OBJECT_CLASS_SIGN
Fyi, if you want to go in the opposite direction, from int value to key in a protobuf enum I’ve been using this:
def lookup_template_name(template_id: int) -> str:
"""Lookup the template name from the template id"""
try:
return notify.EmailTemplate.Name(template_id)
except Exception as e:
logger.error(f"Error looking up template name: {e}")
return e
logger.info(f"Template name is: {lookup_template_name(1)}")
# 2022-11-23 16:04:11:977 INFO Template name is: EMAIL_TEMPLATE_WELCOME_EMAIL
logger.info(f"Template name is: {lookup_template_name(123456)}")
# 2022-11-23 16:04:50:334 ERROR Error looking up template name: Enum EmailTemplate has no name defined for value 123456
The only issue is that you need to already know the name of the enum (in this case EmailTemplate
)