Skip to content

Commit

Permalink
feat: allow_alias for enums
Browse files Browse the repository at this point in the history
Certain APIs, e.g. recommendationengine, have enums where variants are
aliased, i.e. different names map to the same integer value.

Allowing this behavior in proto plus for the cpp protobuf runtime
requires constructing and passing the correct options.
  • Loading branch information
software-dov committed Mar 10, 2021
1 parent 03a90b0 commit 69b96fa
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .github/sync-repo-settings.yaml
Expand Up @@ -12,7 +12,7 @@ branchProtectionRules:
- 'unit (3.7)'
- 'unit (3.7, cpp)'
- 'unit (3.8)'
- 'unit (3.9, cpp)'
# - 'unit (3.9, cpp)' # Don't have binary wheels for 3.9 cpp protobuf yet
- 'unit (3.9)'
- 'cla/google'
requiredApprovingReviewCount: 1
Expand Down
16 changes: 16 additions & 0 deletions proto/enums.py
Expand Up @@ -24,6 +24,14 @@
class ProtoEnumMeta(enum.EnumMeta):
"""A metaclass for building and registering protobuf enums."""

@classmethod
def __prepare__(mcls, cls, bases):
# The _ignore_ attribute must be set before any of the enum attributes
# are added because _ignore_ cannot specify already set names.
enum_dict = super().__prepare__(cls, bases)
enum_dict["_ignore_"] = ["_pb_options"]
return enum_dict

def __new__(mcls, name, bases, attrs):
# Do not do any special behavior for `proto.Enum` itself.
if bases[0] == enum.IntEnum:
Expand All @@ -47,6 +55,13 @@ def __new__(mcls, name, bases, attrs):
filename = _file_info._FileInfo.proto_file_name(
attrs.get("__module__", name.lower())
)

# Retrieve any enum options.
# We expect something that looks like an EnumOptions message,
# either an actual instance or a dict-like representation.
opts = attrs.pop("_pb_options", {})

# Make the descriptor.
enum_desc = descriptor_pb2.EnumDescriptorProto(
name=name,
# Note: the superclass ctor removes the variants, so get them now.
Expand All @@ -60,6 +75,7 @@ def __new__(mcls, name, bases, attrs):
),
key=lambda v: v.number,
),
options=opts,
)

file_info = _file_info._FileInfo.maybe_add_descriptor(filename, package)
Expand Down
39 changes: 39 additions & 0 deletions tests/test_fields_enum.py
Expand Up @@ -12,7 +12,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import os
import proto
import pytest
import sys


Expand Down Expand Up @@ -353,3 +355,40 @@ class Task(proto.Message):
t = Task(weekday="TUESDAY")
t2 = Task.deserialize(Task.serialize(t))
assert t == t2


if os.environ.get("PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION", "python") == "cpp":
# This test only works, and is only relevant, with the cpp runtime.
# Python just doesn't give a care and lets it work anyway.
def test_enum_alias_bad():
# Certain enums may shadow the different enum monikers with the same value.
# This is generally discouraged, and protobuf will object by default,
# but will explicitly allow this behavior if the enum is defined with
# the `allow_alias` option set.
with pytest.raises(TypeError):

# The wrapper message is a hack to avoid manifest wrangling to
# define the enum.
class BadMessage(proto.Message):
class BadEnum(proto.Enum):
UNKNOWN = 0
DEFAULT = 0

bad_dup_enum = proto.Field(proto.ENUM, number=1, enum=BadEnum)


def test_enum_alias_good():
# Have to split good and bad enum alias into two tests so that the generated
# file descriptor is properly created.
# For the python runtime, aliases are allowed by default, but we want to
# make sure that the options don't cause problems.
# For the cpp runtime, we need to verify that we can in fact define aliases.
class GoodMessage(proto.Message):
class GoodEnum(proto.Enum):
_pb_options = {"allow_alias": True}
UNKNOWN = 0
DEFAULT = 0

good_dup_enum = proto.Field(proto.ENUM, number=1, enum=GoodEnum)

assert GoodMessage.GoodEnum.UNKNOWN == GoodMessage.GoodEnum.DEFAULT == 0

0 comments on commit 69b96fa

Please sign in to comment.