From 2e7ea11f80210459106f9780e5f013e2a0381d29 Mon Sep 17 00:00:00 2001 From: Dov Shlachter Date: Wed, 10 Mar 2021 17:14:41 -0800 Subject: [PATCH] feat: update templates to permit enum aliases (#809) Certain APIs like RecommendationEngine use multiple enum variant monikers to reference the same value. Achieving this in protos requires explicit support from proto-plus and the generated surface. Hand testing indicates compliance. Bump the min proto-plus version for generated clients. --- .../%name/%version/%sub/types/_enum.py.j2 | 3 +++ gapic/ads-templates/setup.py.j2 | 2 +- gapic/schema/wrappers.py | 17 +++++++++++++++-- .../%name_%version/%sub/types/_enum.py.j2 | 3 +++ gapic/templates/setup.py.j2 | 2 +- test_utils/test_utils.py | 2 ++ tests/unit/schema/wrappers/test_enums.py | 18 +++++++++++++++--- 7 files changed, 40 insertions(+), 7 deletions(-) diff --git a/gapic/ads-templates/%namespace/%name/%version/%sub/types/_enum.py.j2 b/gapic/ads-templates/%namespace/%name/%version/%sub/types/_enum.py.j2 index c9f4cb0c4f..73994a158c 100644 --- a/gapic/ads-templates/%namespace/%name/%version/%sub/types/_enum.py.j2 +++ b/gapic/ads-templates/%namespace/%name/%version/%sub/types/_enum.py.j2 @@ -1,5 +1,8 @@ class {{ enum.name }}({{ p }}.Enum): r"""{{ enum.meta.doc|rst(indent=4) }}""" + {% if enum.enum_pb.HasField("options") -%} + _pb_options = {{ enum.options_dict }} + {% endif -%} {% for enum_value in enum.values -%} {{ enum_value.name }} = {{ enum_value.number }} {% endfor -%} diff --git a/gapic/ads-templates/setup.py.j2 b/gapic/ads-templates/setup.py.j2 index 18f06803dd..111ce8fb40 100644 --- a/gapic/ads-templates/setup.py.j2 +++ b/gapic/ads-templates/setup.py.j2 @@ -19,7 +19,7 @@ setuptools.setup( 'google-api-core >= 1.22.2, < 2.0.0dev', 'googleapis-common-protos >= 1.5.8', 'grpcio >= 1.10.0', - 'proto-plus >= 1.4.0', + 'proto-plus >= 1.15.0', {%- if api.requires_package(('google', 'iam', 'v1')) %} 'grpc-google-iam-v1', {%- endif %} diff --git a/gapic/schema/wrappers.py b/gapic/schema/wrappers.py index 812630720b..f6ae04ea3e 100644 --- a/gapic/schema/wrappers.py +++ b/gapic/schema/wrappers.py @@ -39,6 +39,7 @@ from google.api import resource_pb2 from google.api_core import exceptions # type: ignore from google.protobuf import descriptor_pb2 # type: ignore +from google.protobuf.json_format import MessageToDict # type: ignore from gapic import utils from gapic.schema import metadata @@ -413,7 +414,7 @@ def get_field(self, *field_path: str, # Get the first field in the path. first_field = field_path[0] cursor = self.fields[first_field + - ('_' if first_field in utils.RESERVED_NAMES else '')] + ('_' if first_field in utils.RESERVED_NAMES else '')] # Base case: If this is the last field in the path, return it outright. if len(field_path) == 1: @@ -536,6 +537,18 @@ def with_context(self, *, collisions: FrozenSet[str]) -> 'EnumType': meta=self.meta.with_context(collisions=collisions), ) + @property + def options_dict(self) -> Dict: + """Return the EnumOptions (if present) as a dict. + + This is a hack to support a pythonic structure representation for + the generator templates. + """ + return MessageToDict( + self.enum_pb.options, + preserving_proto_field_name=True + ) + @dataclasses.dataclass(frozen=True) class PythonType: @@ -869,7 +882,7 @@ def paged_result_field(self) -> Optional[Field]: # The request must have page_token and next_page_token as they keep track of pages for source, source_type, name in ((self.input, str, 'page_token'), - (self.output, str, 'next_page_token')): + (self.output, str, 'next_page_token')): field = source.fields.get(name, None) if not field or field.type != source_type: return None diff --git a/gapic/templates/%namespace/%name_%version/%sub/types/_enum.py.j2 b/gapic/templates/%namespace/%name_%version/%sub/types/_enum.py.j2 index c9f4cb0c4f..73994a158c 100644 --- a/gapic/templates/%namespace/%name_%version/%sub/types/_enum.py.j2 +++ b/gapic/templates/%namespace/%name_%version/%sub/types/_enum.py.j2 @@ -1,5 +1,8 @@ class {{ enum.name }}({{ p }}.Enum): r"""{{ enum.meta.doc|rst(indent=4) }}""" + {% if enum.enum_pb.HasField("options") -%} + _pb_options = {{ enum.options_dict }} + {% endif -%} {% for enum_value in enum.values -%} {{ enum_value.name }} = {{ enum_value.number }} {% endfor -%} diff --git a/gapic/templates/setup.py.j2 b/gapic/templates/setup.py.j2 index 94af4ae760..f7ed0a9923 100644 --- a/gapic/templates/setup.py.j2 +++ b/gapic/templates/setup.py.j2 @@ -28,7 +28,7 @@ setuptools.setup( install_requires=( 'google-api-core[grpc] >= 1.22.2, < 2.0.0dev', 'libcst >= 0.2.5', - 'proto-plus >= 1.4.0', + 'proto-plus >= 1.15.0', {%- if api.requires_package(('google', 'iam', 'v1')) or opts.add_iam_methods %} 'grpc-google-iam-v1', {%- endif %} diff --git a/test_utils/test_utils.py b/test_utils/test_utils.py index beab26518f..2aafab454a 100644 --- a/test_utils/test_utils.py +++ b/test_utils/test_utils.py @@ -290,6 +290,7 @@ def make_enum( module: str = 'baz', values: typing.Tuple[str, int] = (), meta: metadata.Metadata = None, + options: desc.EnumOptions = None, ) -> wrappers.EnumType: enum_value_pbs = [ desc.EnumValueDescriptorProto(name=i[0], number=i[1]) @@ -298,6 +299,7 @@ def make_enum( enum_pb = desc.EnumDescriptorProto( name=name, value=enum_value_pbs, + options=options, ) return wrappers.EnumType( enum_pb=enum_pb, diff --git a/tests/unit/schema/wrappers/test_enums.py b/tests/unit/schema/wrappers/test_enums.py index 3debb5603b..2eeb9c043f 100644 --- a/tests/unit/schema/wrappers/test_enums.py +++ b/tests/unit/schema/wrappers/test_enums.py @@ -37,6 +37,18 @@ def test_enum_value_properties(): def test_enum_ident(): - message = make_enum('Baz', package='foo.v1', module='bar') - assert str(message.ident) == 'bar.Baz' - assert message.ident.sphinx == 'foo.v1.bar.Baz' + enum = make_enum('Baz', package='foo.v1', module='bar') + assert str(enum.ident) == 'bar.Baz' + assert enum.ident.sphinx == 'foo.v1.bar.Baz' + + +def test_enum_options_dict(): + cephalopod = make_enum("Cephalopod", package="animalia.v1", + module="mollusca", options={"allow_alias": True}) + assert isinstance(cephalopod.enum_pb.options, descriptor_pb2.EnumOptions) + assert cephalopod.options_dict == {"allow_alias": True} + + bivalve = make_enum("Bivalve", package="animalia.v1", + module="mollusca") + assert isinstance(bivalve.enum_pb.options, descriptor_pb2.EnumOptions) + assert bivalve.options_dict == {}