def test_get_response_enumerates_services(): g = make_generator() with mock.patch.object(jinja2.FileSystemLoader, "list_templates") as lt: lt.return_value = [ "foo/%service/baz.py.j2", "molluscs/squid/sample.py.j2", ] with mock.patch.object(jinja2.Environment, "get_template") as gt: gt.return_value = jinja2.Template("Service: {{ service.name }}") cgr = g.get_response( api_schema=make_api( make_proto( descriptor_pb2.FileDescriptorProto(service=[ descriptor_pb2.ServiceDescriptorProto(name="Spam"), descriptor_pb2.ServiceDescriptorProto( name="EggsService"), ]), )), opts=Options.build(""), ) assert len(cgr.file) == 2 assert {i.name for i in cgr.file} == { "foo/spam/baz.py", "foo/eggs_service/baz.py", }
def test_get_response_divides_subpackages(): # NOTE: autogen-snippets is intentionally disabled for this test # The API schema below is incomplete and will result in errors when the # snippetgen logic tries to parse it. g = make_generator("autogen-snippets=false") api_schema = api.API.build( [ descriptor_pb2.FileDescriptorProto( name="top.proto", package="foo.v1", service=[descriptor_pb2.ServiceDescriptorProto(name="Top")], ), descriptor_pb2.FileDescriptorProto( name="a/spam/ham.proto", package="foo.v1.spam", service=[descriptor_pb2.ServiceDescriptorProto(name="Bacon")], ), descriptor_pb2.FileDescriptorProto( name="a/eggs/yolk.proto", package="foo.v1.eggs", service=[ descriptor_pb2.ServiceDescriptorProto(name="Scramble") ], ), ], package="foo.v1", ) with mock.patch.object(jinja2.FileSystemLoader, "list_templates") as lt: lt.return_value = [ "foo/%sub/types/%proto.py.j2", "foo/%sub/services/%service.py.j2", "molluscs/squid/sample.py.j2", ] with mock.patch.object(jinja2.Environment, "get_template") as gt: gt.return_value = jinja2.Template(""" {{- '' }}Subpackage: {{ '.'.join(api.subpackage_view) }} """.strip()) cgr = g.get_response(api_schema=api_schema, opts=Options.build("autogen-snippets=false")) assert len(cgr.file) == 6 assert {i.name for i in cgr.file} == { "foo/types/top.py", "foo/services/top.py", "foo/spam/types/ham.py", "foo/spam/services/bacon.py", "foo/eggs/types/yolk.py", "foo/eggs/services/scramble.py", }
def test_out_of_order_enums(): # Enums can be referenced as field types before they # are defined in the proto file. # This happens when they're a nested type within a message. messages = (make_message_pb2( name='Squid', fields=(make_field_pb2( name='base_color', type_name='google.mollusca.Chromatophore.Color', number=1, ), ), ), make_message_pb2( name='Chromatophore', enum_type=(descriptor_pb2.EnumDescriptorProto(name='Color', value=()), ), )) fd = (make_file_pb2( name='squid.proto', package='google.mollusca', messages=messages, services=(descriptor_pb2.ServiceDescriptorProto( name='SquidService', ), ), ), ) api_schema = api.API.build(fd, package='google.mollusca') field_type = ( api_schema.messages['google.mollusca.Squid'].fields['base_color'].type) enum_type = api_schema.enums['google.mollusca.Chromatophore.Color'] assert field_type == enum_type
def make_service_with_method_options( *, http_rule: http_pb2.HttpRule = None, method_signature: str = '', in_fields: typing.Tuple[desc.FieldDescriptorProto] = () ) -> wrappers.Service: # Declare a method with options enabled for long-running operations and # field headers. method = get_method( 'DoBigThing', 'foo.bar.ThingRequest', 'google.longrunning.operations_pb2.Operation', lro_response_type='foo.baz.ThingResponse', lro_metadata_type='foo.qux.ThingMetadata', in_fields=in_fields, http_rule=http_rule, method_signature=method_signature, ) # Define a service descriptor. service_pb = desc.ServiceDescriptorProto(name='ThingDoer') # Return a service object to test. return wrappers.Service( service_pb=service_pb, methods={method.name: method}, )
def test_python_modules_nested(): fd = ( make_file_pb2( name='dep.proto', package='google.dep', messages=(make_message_pb2(name='ImportedMessage', fields=()), ), ), make_file_pb2( name='common.proto', package='google.example.v1.common', messages=(make_message_pb2(name='Bar'), ), ), make_file_pb2( name='foo.proto', package='google.example.v1', messages=( make_message_pb2( name='GetFooRequest', fields=( make_field_pb2(name='primitive', number=2, type=1), make_field_pb2( name='foo', number=3, type=1, type_name='.google.example.v1.GetFooRequest.Foo', ), ), nested_type=(make_message_pb2( name='Foo', fields=(make_field_pb2( name='imported_message', number=1, type_name='.google.dep.ImportedMessage'), ), ), ), ), make_message_pb2( name='GetFooResponse', fields=(make_field_pb2( name='foo', number=1, type_name='.google.example.v1.GetFooRequest.Foo', ), ), ), ), services=(descriptor_pb2.ServiceDescriptorProto( name='FooService', method=(descriptor_pb2.MethodDescriptorProto( name='GetFoo', input_type='google.example.v1.GetFooRequest', output_type='google.example.v1.GetFooResponse', ), ), ), ), ), ) api_schema = api.API.build(fd, package='google.example.v1') assert api_schema.protos['foo.proto'].python_modules == (imp.Import( package=('google', 'dep'), module='dep_pb2'), )
def test_get_response_divides_subpackages(): g = make_generator() api_schema = api.API.build( [ descriptor_pb2.FileDescriptorProto( name="top.proto", package="foo.v1", service=[descriptor_pb2.ServiceDescriptorProto(name="Top")], ), descriptor_pb2.FileDescriptorProto( name="a/spam/ham.proto", package="foo.v1.spam", service=[descriptor_pb2.ServiceDescriptorProto(name="Bacon")], ), descriptor_pb2.FileDescriptorProto( name="a/eggs/yolk.proto", package="foo.v1.eggs", service=[descriptor_pb2.ServiceDescriptorProto( name="Scramble")], ), ], package="foo.v1", ) with mock.patch.object(jinja2.FileSystemLoader, "list_templates") as lt: lt.return_value = [ "foo/%sub/types/%proto.py.j2", "foo/%sub/services/%service.py.j2", "molluscs/squid/sample.py.j2", ] with mock.patch.object(jinja2.Environment, "get_template") as gt: gt.return_value = jinja2.Template( """ {{- '' }}Subpackage: {{ '.'.join(api.subpackage_view) }} """.strip() ) cgr = g.get_response(api_schema=api_schema, opts=options.Options.build("")) assert len(cgr.file) == 6 assert {i.name for i in cgr.file} == { "foo/types/top.py", "foo/services/top.py", "foo/spam/types/ham.py", "foo/spam/services/bacon.py", "foo/eggs/types/yolk.py", "foo/eggs/services/scramble.py", }
def test_get_response(): # Create a generator with mock data. # # We want to ensure that templates are rendered for each service, # which we prove by sending two services. file_pb2 = descriptor_pb2.FileDescriptorProto( name='bacon.proto', package='foo.bar.v1', service=[ descriptor_pb2.ServiceDescriptorProto(name='SpamService'), descriptor_pb2.ServiceDescriptorProto(name='EggsService') ], ) api_schema = make_api(make_proto(file_pb2)) g = generator.Generator(api_schema=api_schema) # Mock all the rendering methods. with mock.patch.object(g, '_render_templates') as _render_templates: _render_templates.return_value = [ plugin_pb2.CodeGeneratorResponse.File( name='template_file', content='This was a template.', ), ] # Okay, now run the `get_response` method. response = g.get_response() # First and foremost, we care that we got a valid response # object back (albeit not so much what is in it). assert isinstance(response, plugin_pb2.CodeGeneratorResponse) # Next, determine that the general API templates and service # templates were both called; the method should be called # once per service plus one for the API as a whole. assert _render_templates.call_count == len(file_pb2.service) + 1 # The service templates should have been called with the # filename transformation and the additional `service` variable. for call in _render_templates.mock_calls: _, args, kwargs = call if args[0] != g._env.loader.service_templates: continue service = kwargs['additional_context']['service'] assert isinstance(service, wrappers.Service)
def test_prior_protos(): L = descriptor_pb2.SourceCodeInfo.Location # Set up a prior proto that mimics google/protobuf/empty.proto empty_proto = api.Proto.build(make_file_pb2( name='empty.proto', package='google.protobuf', messages=(make_message_pb2(name='Empty'), ), ), file_to_generate=False, naming=make_naming()) # Set up the service with an RPC. service_pb = descriptor_pb2.ServiceDescriptorProto( name='PingService', method=(descriptor_pb2.MethodDescriptorProto( name='Ping', input_type='google.protobuf.Empty', output_type='google.protobuf.Empty', ), ), ) # Fake-document our fake stuff. locations = ( L(path=(6, 0), leading_comments='This is the PingService service.'), L(path=(6, 0, 2, 0), leading_comments='This is the Ping method.'), ) # Finally, set up the file that encompasses these. fdp = make_file_pb2( package='google.example.v1', services=(service_pb, ), locations=locations, ) # Make the proto object. proto = api.Proto.build(fdp, file_to_generate=True, prior_protos={ 'google/protobuf/empty.proto': empty_proto, }, naming=make_naming()) # Establish that our data looks correct. assert len(proto.services) == 1 assert len(empty_proto.messages) == 1 assert len(proto.messages) == 0 service = proto.services['google.example.v1.PingService'] assert service.meta.doc == 'This is the PingService service.' assert len(service.methods) == 1 method = service.methods['Ping'] assert isinstance(method.input, wrappers.MessageType) assert isinstance(method.output, wrappers.MessageType) assert method.input.name == 'Empty' assert method.output.name == 'Empty' assert method.meta.doc == 'This is the Ping method.'
def test_get_response_enumerates_services(): g = make_generator() with mock.patch.object(jinja2.FileSystemLoader, 'list_templates') as lt: lt.return_value = ['foo/$service/baz.py.j2'] with mock.patch.object(jinja2.Environment, 'get_template') as gt: gt.return_value = jinja2.Template('Service: {{ service.name }}') cgr = g.get_response(api_schema=make_api( make_proto( descriptor_pb2.FileDescriptorProto(service=[ descriptor_pb2.ServiceDescriptorProto(name='Spam'), descriptor_pb2.ServiceDescriptorProto( name='EggsService'), ]), ))) assert len(cgr.file) == 2 assert {i.name for i in cgr.file} == { 'foo/spam/baz.py', 'foo/eggs_service/baz.py', }
def test_lro(): # Set up a prior proto that mimics google/protobuf/empty.proto lro_proto = api.Proto.build(make_file_pb2( name='operations.proto', package='google.longrunning', messages=(make_message_pb2(name='Operation'), ), ), file_to_generate=False, naming=make_naming()) # Set up a method with LRO annotations. method_pb2 = descriptor_pb2.MethodDescriptorProto( name='AsyncDoThing', input_type='google.example.v3.AsyncDoThingRequest', output_type='google.longrunning.Operation', ) method_pb2.options.Extensions[operations_pb2.operation_info].MergeFrom( operations_pb2.OperationInfo( response_type='google.example.v3.AsyncDoThingResponse', metadata_type='google.example.v3.AsyncDoThingMetadata', ), ) # Set up the service with an RPC. service_pb = descriptor_pb2.ServiceDescriptorProto( name='LongRunningService', method=(method_pb2, ), ) # Set up the messages, including the annotated ones. messages = ( make_message_pb2(name='AsyncDoThingRequest', fields=()), make_message_pb2(name='AsyncDoThingResponse', fields=()), make_message_pb2(name='AsyncDoThingMetadata', fields=()), ) # Finally, set up the file that encompasses these. fdp = make_file_pb2( package='google.example.v3', messages=messages, services=(service_pb, ), ) # Make the proto object. proto = api.Proto.build(fdp, file_to_generate=True, prior_protos={ 'google/longrunning/operations.proto': lro_proto, }, naming=make_naming()) # Establish that our data looks correct. assert len(proto.services) == 1 assert len(proto.messages) == 3 assert len(lro_proto.messages) == 1
def test_get_response_divides_subpackages(): g = make_generator() api_schema = api.API.build([ descriptor_pb2.FileDescriptorProto( name='top.proto', package='foo.v1', service=[descriptor_pb2.ServiceDescriptorProto(name='Top')], ), descriptor_pb2.FileDescriptorProto( name='a/spam/ham.proto', package='foo.v1.spam', service=[descriptor_pb2.ServiceDescriptorProto(name='Bacon')], ), descriptor_pb2.FileDescriptorProto( name='a/eggs/yolk.proto', package='foo.v1.eggs', service=[descriptor_pb2.ServiceDescriptorProto(name='Scramble')], ), ], package='foo.v1') with mock.patch.object(jinja2.FileSystemLoader, 'list_templates') as lt: lt.return_value = [ 'foo/%sub/types/%proto.py.j2', 'foo/%sub/services/%service.py.j2', 'molluscs/squid/sample.py.j2', ] with mock.patch.object(jinja2.Environment, 'get_template') as gt: gt.return_value = jinja2.Template(""" {{- '' }}Subpackage: {{ '.'.join(api.subpackage_view) }} """.strip()) cgr = g.get_response(api_schema=api_schema, opts=options.Options.build('')) assert len(cgr.file) == 6 assert {i.name for i in cgr.file} == { 'foo/types/top.py', 'foo/services/top.py', 'foo/spam/types/ham.py', 'foo/spam/services/bacon.py', 'foo/eggs/types/yolk.py', 'foo/eggs/services/scramble.py', }
def test_services(): L = descriptor_pb2.SourceCodeInfo.Location # Set up messages for our RPC. request_message_pb = make_message_pb2(name='GetFooRequest', fields=(make_field_pb2(name='name', type=9, number=1), )) response_message_pb = make_message_pb2(name='GetFooResponse', fields=()) # Set up the service with an RPC. service_pb = descriptor_pb2.ServiceDescriptorProto( name='FooService', method=(descriptor_pb2.MethodDescriptorProto( name='GetFoo', input_type='google.example.v2.GetFooRequest', output_type='google.example.v2.GetFooResponse', ), ), ) # Fake-document our fake stuff. locations = ( L(path=(6, 0), leading_comments='This is the FooService service.'), L(path=(6, 0, 2, 0), leading_comments='This is the GetFoo method.'), L(path=(4, 0), leading_comments='This is the GetFooRequest message.'), L(path=(4, 1), leading_comments='This is the GetFooResponse message.'), ) # Finally, set up the file that encompasses these. fdp = make_file_pb2( package='google.example.v2', messages=(request_message_pb, response_message_pb), services=(service_pb, ), locations=locations, ) # Make the proto object. proto = api.Proto.build(fdp, file_to_generate=True) # Establish that our data looks correct. assert len(proto.services) == 1 assert len(proto.messages) == 2 service = proto.services['google.example.v2.FooService'] assert service.meta.doc == 'This is the FooService service.' assert len(service.methods) == 1 method = service.methods['GetFoo'] assert method.meta.doc == 'This is the GetFoo method.' assert isinstance(method.input, wrappers.MessageType) assert isinstance(method.output, wrappers.MessageType) assert method.input.name == 'GetFooRequest' assert method.input.meta.doc == 'This is the GetFooRequest message.' assert method.output.name == 'GetFooResponse' assert method.output.meta.doc == 'This is the GetFooResponse message.'
def test_get_filenames_with_service(): g = generator.Generator(api_schema=make_api(naming=make_naming( namespace=(), name='Spam', version='v2'), )) template_name = '$name/$service/foo.py.j2' assert g._get_filenames( template_name, context={ 'service': wrappers.Service( methods=[], service_pb=descriptor_pb2.ServiceDescriptorProto(name='Eggs'), ), }) == ('spam/eggs/foo.py', )
def test_not_target_file(): """Establish that services are not ignored for untargeted protos.""" message_pb = make_message_pb2(name='Foo', fields=(make_field_pb2(name='bar', type=3, number=1),) ) service_pb = descriptor_pb2.ServiceDescriptorProto() fdp = make_file_pb2(messages=(message_pb,), services=(service_pb,)) # Actually make the proto object. proto = api.Proto.build(fdp, file_to_generate=False, naming=make_naming()) # The proto object should have the message, but no service. assert len(proto.messages) == 1 assert len(proto.services) == 0
def make_service(name: str = 'Placeholder', host: str = '', methods: typing.Tuple[wrappers.Method] = (), scopes: typing.Tuple[str] = ()) -> wrappers.Service: # Define a service descriptor, and set a host and oauth scopes if # appropriate. service_pb = descriptor_pb2.ServiceDescriptorProto(name=name) if host: service_pb.options.Extensions[client_pb2.default_host] = host service_pb.options.Extensions[client_pb2.oauth_scopes] = ','.join(scopes) # Return a service object to test. return wrappers.Service( service_pb=service_pb, methods={m.name: m for m in methods}, )
def test_api_build(): # Put together a couple of minimal protos. fd = ( make_file_pb2( name='dep.proto', package='google.dep', messages=(make_message_pb2(name='ImportedMessage', fields=()), ), ), make_file_pb2( name='foo.proto', package='google.example.v1', messages=( make_message_pb2(name='Foo', fields=()), make_message_pb2( name='GetFooRequest', fields=(make_field_pb2( name='imported_message', number=1, type_name='google.dep.ImportedMessasge'), )), make_message_pb2(name='GetFooResponse', fields=(make_field_pb2( name='foo', number=1, type_name='google.example.v1.Foo'), )), ), services=(descriptor_pb2.ServiceDescriptorProto( name='FooService', method=(descriptor_pb2.MethodDescriptorProto( name='GetFoo', input_type='google.example.v1.GetFooRequest', output_type='google.example.v1.GetFooResponse', ), ), ), ), ), ) # Create an API with those protos. api_schema = api.API.build(fd, package='google.example.v1') # Establish that the API has the data expected. assert isinstance(api_schema, api.API) assert len(api_schema.protos) == 2 assert 'google.dep.ImportedMessage' in api_schema.messages assert 'google.example.v1.Foo' in api_schema.messages assert 'google.example.v1.GetFooRequest' in api_schema.messages assert 'google.example.v1.GetFooResponse' in api_schema.messages assert 'google.example.v1.FooService' in api_schema.services assert len(api_schema.enums) == 0
def test_get_response_ignores_unwanted_transports_and_clients(): g = make_generator() with mock.patch.object(jinja2.FileSystemLoader, "list_templates") as lt: lt.return_value = [ "foo/%service/transports/river.py.j2", "foo/%service/transports/car.py.j2", "foo/%service/transports/grpc.py.j2", "foo/%service/transports/__init__.py.j2", "foo/%service/transports/base.py.j2", "foo/%service/async_client.py.j2", "foo/%service/client.py.j2", "mollusks/squid/sample.py.j2", ] with mock.patch.object(jinja2.Environment, "get_template") as gt: gt.return_value = jinja2.Template("Service: {{ service.name }}") api_schema = make_api( make_proto( descriptor_pb2.FileDescriptorProto(service=[ descriptor_pb2.ServiceDescriptorProto( name="SomeService"), ]), )) cgr = g.get_response(api_schema=api_schema, opts=Options.build("transport=river+car")) assert len(cgr.file) == 5 assert {i.name for i in cgr.file} == { "foo/some_service/transports/river.py", "foo/some_service/transports/car.py", "foo/some_service/transports/__init__.py", "foo/some_service/transports/base.py", # Only generate async client with grpc transport "foo/some_service/client.py", } cgr = g.get_response(api_schema=api_schema, opts=Options.build("transport=grpc")) assert len(cgr.file) == 5 assert {i.name for i in cgr.file} == { "foo/some_service/transports/grpc.py", "foo/some_service/transports/__init__.py", "foo/some_service/transports/base.py", "foo/some_service/client.py", "foo/some_service/async_client.py", }
def test_get_filename_with_service(): g = make_generator() template_name = "%name/%service/foo.py.j2" assert (g._get_filename( template_name, api_schema=make_api(naming=make_naming(namespace=(), name="Spam", version="v2"), ), context={ "service": wrappers.Service( methods=[], service_pb=descriptor_pb2.ServiceDescriptorProto(name="Eggs"), visible_resources={}, ), }, ) == "spam/eggs/foo.py")
def CopyToProto(self, proto): """Copies this to a descriptor_pb2.MethodDescriptorProto. Args: proto (descriptor_pb2.MethodDescriptorProto): An empty descriptor proto. Raises: Error: If self couldn't be serialized, due to too few constructor arguments. """ if self.containing_service is not None: from google.protobuf import descriptor_pb2 service_proto = descriptor_pb2.ServiceDescriptorProto() self.containing_service.CopyToProto(service_proto) proto.CopyFrom(service_proto.method[self.index]) else: raise Error('Descriptor does not contain a service.')
def test_resources_referenced_but_not_typed(reference_attr="type"): fdp = make_file_pb2( name="nomenclature.proto", package="nomenclature.linneaen.v1", messages=( make_message_pb2(name="Species", ), make_message_pb2( name="CreateSpeciesRequest", fields=(make_field_pb2(name='species', number=1, type=9), ), ), make_message_pb2(name="CreateSpeciesResponse", ), ), services=(descriptor_pb2.ServiceDescriptorProto( name="SpeciesService", method=(descriptor_pb2.MethodDescriptorProto( name="CreateSpecies", input_type="nomenclature.linneaen.v1.CreateSpeciesRequest", output_type="nomenclature.linneaen.v1.CreateSpeciesResponse", ), ), ), ), ) # Set up the resource species_resource_opts = fdp.message_type[0].options.Extensions[ resource_pb2.resource] species_resource_opts.type = "nomenclature.linnaen.com/Species" species_resource_opts.pattern.append( "families/{family}/genera/{genus}/species/{species}") # Set up the reference name_resource_opts = fdp.message_type[1].field[0].options.Extensions[ resource_pb2.resource_reference] if reference_attr == "type": name_resource_opts.type = species_resource_opts.type else: name_resource_opts.child_type = species_resource_opts.type api_schema = api.API.build([fdp], package="nomenclature.linneaen.v1") expected = {api_schema.messages["nomenclature.linneaen.v1.Species"]} actual = api_schema.services[ "nomenclature.linneaen.v1.SpeciesService"].resource_messages assert actual == expected
def test_lro_missing_annotation(): # Set up a prior proto that mimics google/protobuf/empty.proto lro_proto = api.Proto.build(make_file_pb2( name='operations.proto', package='google.longrunning', messages=(make_message_pb2(name='Operation'), ), ), file_to_generate=False, naming=make_naming()) # Set up a method with an LRO but no annotation. method_pb2 = descriptor_pb2.MethodDescriptorProto( name='AsyncDoThing', input_type='google.example.v3.AsyncDoThingRequest', output_type='google.longrunning.Operation', ) # Set up the service with an RPC. service_pb = descriptor_pb2.ServiceDescriptorProto( name='LongRunningService', method=(method_pb2, ), ) # Set up the messages, including the annotated ones. messages = (make_message_pb2(name='AsyncDoThingRequest', fields=()), ) # Finally, set up the file that encompasses these. fdp = make_file_pb2( package='google.example.v3', messages=messages, services=(service_pb, ), ) # Make the proto object. with pytest.raises(TypeError): api.Proto.build(fdp, file_to_generate=True, prior_protos={ 'google/longrunning/operations.proto': lro_proto, }, naming=make_naming())
def test_proto_builder_constructor(): sentinel_message = descriptor_pb2.DescriptorProto() sentinel_enum = descriptor_pb2.EnumDescriptorProto() sentinel_service = descriptor_pb2.ServiceDescriptorProto() # Create a file descriptor proto. It does not matter that none # of the sentinels have actual data because this test just ensures # they are sent off to the correct methods unmodified. fdp = make_file_pb2( messages=(sentinel_message, ), enums=(sentinel_enum, ), services=(sentinel_service, ), ) # Test the load function. with mock.patch.object(api._ProtoBuilder, '_load_children') as lc: pb = api._ProtoBuilder( fdp, file_to_generate=True, naming=make_naming(), ) # There should be three total calls to load the different types # of children. assert lc.call_count == 3 # The enum type should come first. _, args, _ = lc.mock_calls[0] assert args[0][0] == sentinel_enum assert args[1] == pb._load_enum # The message type should come second. _, args, _ = lc.mock_calls[1] assert args[0][0] == sentinel_message assert args[1] == pb._load_message # The services should come third. _, args, _ = lc.mock_calls[2] assert args[0][0] == sentinel_service assert args[1] == pb._load_service
def make_service(name: str = 'Placeholder', host: str = '', scopes: typing.Tuple[str] = ()) -> wrappers.Service: # Declare a few methods, with messages in distinct packages. methods = ( get_method('DoThing', 'foo.bar.ThingRequest', 'foo.baz.ThingResponse'), get_method('Jump', 'foo.bacon.JumpRequest', 'foo.bacon.JumpResponse'), get_method('Yawn', 'a.b.v1.c.YawnRequest', 'x.y.v1.z.YawnResponse'), ) # Define a service descriptor, and set a host and oauth scopes if # appropriate. service_pb = descriptor_pb2.ServiceDescriptorProto(name=name) if host: service_pb.options.Extensions[annotations_pb2.default_host] = host service_pb.options.Extensions[annotations_pb2.oauth].scopes.extend(scopes) # Return a service object to test. return wrappers.Service( service_pb=service_pb, methods={m.name: m for m in methods}, )
def make_service( name: str = "Placeholder", host: str = "", methods: typing.Tuple[wrappers.Method] = (), scopes: typing.Tuple[str] = (), visible_resources: typing.Optional[typing.Mapping[ str, wrappers.CommonResource]] = None, ) -> wrappers.Service: visible_resources = visible_resources or {} # Define a service descriptor, and set a host and oauth scopes if # appropriate. service_pb = desc.ServiceDescriptorProto(name=name) if host: service_pb.options.Extensions[client_pb2.default_host] = host service_pb.options.Extensions[client_pb2.oauth_scopes] = ','.join(scopes) # Return a service object to test. return wrappers.Service( service_pb=service_pb, methods={m.name: m for m in methods}, visible_resources=visible_resources, )
def test_api_build(): # Put together a couple of minimal protos. fd = ( make_file_pb2( name='dep.proto', package='google.dep', messages=(make_message_pb2(name='ImportedMessage', fields=()), ), ), make_file_pb2( name='common.proto', package='google.example.v1.common', messages=(make_message_pb2(name='Bar'), ), ), make_file_pb2( name='foo.proto', package='google.example.v1', messages=( make_message_pb2(name='Foo', fields=()), make_message_pb2( name='GetFooRequest', fields=( make_field_pb2( name='imported_message', number=1, type_name='.google.dep.ImportedMessage'), make_field_pb2(name='primitive', number=2, type=1), )), make_message_pb2(name='GetFooResponse', fields=(make_field_pb2( name='foo', number=1, type_name='.google.example.v1.Foo'), )), ), services=(descriptor_pb2.ServiceDescriptorProto( name='FooService', method=(descriptor_pb2.MethodDescriptorProto( name='GetFoo', input_type='google.example.v1.GetFooRequest', output_type='google.example.v1.GetFooResponse', ), ), ), ), ), ) # Create an API with those protos. api_schema = api.API.build(fd, package='google.example.v1') # Establish that the API has the data expected. assert isinstance(api_schema, api.API) assert len(api_schema.all_protos) == 3 assert len(api_schema.protos) == 2 assert 'google.dep.ImportedMessage' not in api_schema.messages assert 'google.example.v1.common.Bar' in api_schema.messages assert 'google.example.v1.Foo' in api_schema.messages assert 'google.example.v1.GetFooRequest' in api_schema.messages assert 'google.example.v1.GetFooResponse' in api_schema.messages assert 'google.example.v1.FooService' in api_schema.services assert len(api_schema.enums) == 0 assert api_schema.protos['foo.proto'].python_modules == (imp.Import( package=('google', 'dep'), module='dep_pb2'), ) assert api_schema.requires_package(('google', 'example', 'v1')) assert not api_schema.requires_package(('elgoog', 'example', 'v1')) # Establish that the subpackages work. assert 'common' in api_schema.subpackages sub = api_schema.subpackages['common'] assert len(sub.protos) == 1 assert 'google.example.v1.common.Bar' in sub.messages assert 'google.example.v1.Foo' not in sub.messages
def test_services(): L = descriptor_pb2.SourceCodeInfo.Location # Make a silly helper method to not repeat some of the structure. def _n(method_name: str): return { 'service': 'google.example.v2.FooService', 'method': method_name, } # Set up retry information. opts = Options( retry={ 'methodConfig': [ { 'name': [_n('TimeoutableGetFoo')], 'timeout': '30s' }, { 'name': [_n('RetryableGetFoo')], 'retryPolicy': { 'maxAttempts': 3, 'initialBackoff': '%dn' % 1e6, 'maxBackoff': '60s', 'backoffMultiplier': 1.5, 'retryableStatusCodes': ['UNAVAILABLE', 'ABORTED'], } }, ] }) # Set up messages for our RPC. request_message_pb = make_message_pb2(name='GetFooRequest', fields=(make_field_pb2(name='name', type=9, number=1), )) response_message_pb = make_message_pb2(name='GetFooResponse', fields=()) # Set up the service with an RPC. service_pb = descriptor_pb2.ServiceDescriptorProto( name='FooService', method=( descriptor_pb2.MethodDescriptorProto( name='GetFoo', input_type='google.example.v2.GetFooRequest', output_type='google.example.v2.GetFooResponse', ), descriptor_pb2.MethodDescriptorProto( name='TimeoutableGetFoo', input_type='google.example.v2.GetFooRequest', output_type='google.example.v2.GetFooResponse', ), descriptor_pb2.MethodDescriptorProto( name='RetryableGetFoo', input_type='google.example.v2.GetFooRequest', output_type='google.example.v2.GetFooResponse', ), ), ) # Fake-document our fake stuff. locations = ( L(path=(6, 0), leading_comments='This is the FooService service.'), L(path=(6, 0, 2, 0), leading_comments='This is the GetFoo method.'), L(path=(4, 0), leading_comments='This is the GetFooRequest message.'), L(path=(4, 1), leading_comments='This is the GetFooResponse message.'), ) # Finally, set up the file that encompasses these. fdp = make_file_pb2( name='test.proto', package='google.example.v2', messages=(request_message_pb, response_message_pb), services=(service_pb, ), locations=locations, ) # Make the proto object. proto = api.API.build( [fdp], 'google.example.v2', opts=opts, ).protos['test.proto'] # Establish that our data looks correct. assert len(proto.services) == 1 assert len(proto.messages) == 2 service = proto.services['google.example.v2.FooService'] assert service.meta.doc == 'This is the FooService service.' assert len(service.methods) == 3 method = service.methods['GetFoo'] assert method.meta.doc == 'This is the GetFoo method.' assert isinstance(method.input, wrappers.MessageType) assert isinstance(method.output, wrappers.MessageType) assert method.input.name == 'GetFooRequest' assert method.input.meta.doc == 'This is the GetFooRequest message.' assert method.output.name == 'GetFooResponse' assert method.output.meta.doc == 'This is the GetFooResponse message.' assert not method.timeout assert not method.retry # Establish that the retry information on a timeout-able method also # looks correct. timeout_method = service.methods['TimeoutableGetFoo'] assert timeout_method.timeout == pytest.approx(30.0) assert not timeout_method.retry # Establish that the retry information on the retryable method also # looks correct. retry_method = service.methods['RetryableGetFoo'] assert retry_method.timeout is None assert retry_method.retry.max_attempts == 3 assert retry_method.retry.initial_backoff == pytest.approx(0.001) assert retry_method.retry.backoff_multiplier == pytest.approx(1.5) assert retry_method.retry.max_backoff == pytest.approx(60.0) assert retry_method.retry.retryable_exceptions == { exceptions.ServiceUnavailable, exceptions.Aborted, }
def test_dont_generate_in_code_samples(mock_gmtime, mock_generate_sample, fs): # These time values are nothing special, # they just need to be deterministic. returner = mock.MagicMock() returner.tm_year = 2112 returner.tm_mon = 6 returner.tm_mday = 1 returner.tm_hour = 13 returner.tm_min = 13 returner.tm_sec = 13 mock_gmtime.return_value = returner config_fpath = "samples.yaml" fs.create_file( config_fpath, contents=dedent(""" type: com.google.api.codegen.samplegen.v1p2.SampleConfigProto schema_version: 1.2.0 samples: - id: squid_sample rpc: IdentifyMollusc service: Mollusc.v1.Mollusc sample_type: - standalone - incode/SQUID - id: clam_sample rpc: IdentifyMollusc service: Mollusc.v1.Mollusc sample_type: - incode/CLAM - id: whelk_sample rpc: IdentifyMollusc service: Mollusc.v1.Mollusc sample_type: - standalone - id: octopus_sample rpc: IdentifyMollusc service: Mollusc.v1.Mollusc """), ) generator = make_generator(f"samples={config_fpath}") generator._env.loader = jinja2.DictLoader({"sample.py.j2": ""}) api_schema = make_api( make_proto( descriptor_pb2.FileDescriptorProto( name="mollusc.proto", package="Mollusc.v1", service=[ descriptor_pb2.ServiceDescriptorProto(name="Mollusc") ], ), ), naming=naming.NewNaming(name="Mollusc", version="v6"), ) # Note that we do NOT expect a clam sample. # There are four tests going on: # 1) Just an explicit standalone sample type. # 2) Multiple sample types, one of which is standalone. # 3) Explicit sample types but NO standalone sample type. # 4) Implicit standalone sample type. expected = CodeGeneratorResponse(file=[ CodeGeneratorResponse.File( name="samples/generated_samples/squid_sample.py", content="\n", ), CodeGeneratorResponse.File( name="samples/generated_samples/whelk_sample.py", content="\n", ), CodeGeneratorResponse.File( name="samples/generated_samples/octopus_sample.py", content="\n", ), # TODO(busunkim): Re-enable manifest generation once metadata # format has been formalized. # https://docs.google.com/document/d/1ghBam8vMj3xdoe4xfXhzVcOAIwrkbTpkMLgKc9RPD9k/edit#heading=h.sakzausv6hue # CodeGeneratorResponse.File( # name="samples/generated_samples/mollusc.v6.python.21120601.131313.manifest.yaml", # content=dedent( # """ --- # type: manifest/samples # schema_version: 3 # python: &python # environment: python # bin: python3 # base_path: samples # invocation: \'{bin} {path} @args\' # samples: # - <<: *python # sample: squid_sample # path: \'{base_path}/squid_sample.py\' # - <<: *python # sample: whelk_sample # path: \'{base_path}/whelk_sample.py\' # - <<: *python # sample: octopus_sample # path: \'{base_path}/octopus_sample.py\' # """ # ), # ), ]) expected.supported_features |= CodeGeneratorResponse.Feature.FEATURE_PROTO3_OPTIONAL actual = generator.get_response(api_schema=api_schema, opts=Options.build("")) assert actual == expected
def test_cross_file_lro(): # Protobuf annotations for longrunning operations use strings to name types. # As far as the protobuf compiler is concerned they don't reference the # _types_ at all, so the corresponding proto file that owns the types # does not need to be imported. # This creates a potential issue when building rich structures around # LRO returning methods. This test is intended to verify that the issue # is handled correctly. # Set up a prior proto that mimics google/protobuf/empty.proto lro_proto = api.Proto.build(make_file_pb2( name='operations.proto', package='google.longrunning', messages=(make_message_pb2(name='Operation'), ), ), file_to_generate=False, naming=make_naming()) # Set up a method with LRO annotations. method_pb2 = descriptor_pb2.MethodDescriptorProto( name='AsyncDoThing', input_type='google.example.v3.AsyncDoThingRequest', output_type='google.longrunning.Operation', ) method_pb2.options.Extensions[operations_pb2.operation_info].MergeFrom( operations_pb2.OperationInfo( response_type='google.example.v3.AsyncDoThingResponse', metadata_type='google.example.v3.AsyncDoThingMetadata', ), ) # Set up the service with an RPC. service_file = make_file_pb2( name='service_file.proto', package='google.example.v3', messages=(make_message_pb2(name='AsyncDoThingRequest', fields=()), ), services=(descriptor_pb2.ServiceDescriptorProto( name='LongRunningService', method=(method_pb2, ), ), )) # Set up the messages, including the annotated ones. # This file is distinct and is not explicitly imported # into the file that defines the service. messages_file = make_file_pb2( name='messages_file.proto', package='google.example.v3', messages=( make_message_pb2(name='AsyncDoThingResponse', fields=()), make_message_pb2(name='AsyncDoThingMetadata', fields=()), ), ) api_schema = api.API.build( file_descriptors=( service_file, messages_file, ), package='google.example.v3', prior_protos={ 'google/longrunning/operations.proto': lro_proto, }, ) method = (api_schema.all_protos['service_file.proto'].services[ 'google.example.v3.LongRunningService'].methods['AsyncDoThing']) assert method.lro assert method.lro.response_type.name == 'AsyncDoThingResponse' assert method.lro.metadata_type.name == 'AsyncDoThingMetadata'
def test_proto_names_import_collision_flattening(): lro_proto = api.Proto.build(make_file_pb2( name='operations.proto', package='google.longrunning', messages=(make_message_pb2(name='Operation'), ), ), file_to_generate=False, naming=make_naming()) fd = ( make_file_pb2( name='mollusc.proto', package='google.animalia.mollusca', messages=( make_message_pb2(name='Mollusc', ), make_message_pb2(name='MolluscResponse', ), make_message_pb2(name='MolluscMetadata', ), ), ), make_file_pb2( name='squid.proto', package='google.animalia.mollusca', messages=( make_message_pb2( name='IdentifySquidRequest', fields=(make_field_pb2( name='mollusc', number=1, type_name='.google.animalia.mollusca.Mollusc'), ), ), make_message_pb2( name='IdentifySquidResponse', fields=(), ), ), services=(descriptor_pb2.ServiceDescriptorProto( name='SquidIdentificationService', method=(descriptor_pb2.MethodDescriptorProto( name='IdentifyMollusc', input_type='google.animalia.mollusca.IdentifySquidRequest', output_type='google.longrunning.Operation', ), ), ), ), ), ) method_options = fd[1].service[0].method[0].options # Notice that a signature field collides with the name of an imported module method_options.Extensions[client_pb2.method_signature].append('mollusc') method_options.Extensions[operations_pb2.operation_info].MergeFrom( operations_pb2.OperationInfo( response_type='google.animalia.mollusca.MolluscResponse', metadata_type='google.animalia.mollusca.MolluscMetadata', )) api_schema = api.API.build(fd, package='google.animalia.mollusca', prior_protos={ 'google/longrunning/operations.proto': lro_proto, }) actual_imports = { ref_type.ident.python_import for service in api_schema.services.values() for method in service.methods.values() for ref_type in method.ref_types } expected_imports = { imp.Import( package=('google', 'animalia', 'mollusca', 'types'), module='mollusc', alias='gam_mollusc', ), imp.Import( package=('google', 'animalia', 'mollusca', 'types'), module='squid', ), imp.Import( package=('google', 'api_core'), module='operation', ), imp.Import( package=('google', 'api_core'), module='operation_async', ), } assert expected_imports == actual_imports method = (api_schema. services['google.animalia.mollusca.SquidIdentificationService']. methods['IdentifyMollusc']) actual_response_import = method.lro.response_type.ident.python_import expected_response_import = imp.Import( package=('google', 'animalia', 'mollusca', 'types'), module='mollusc', alias='gam_mollusc', ) assert actual_response_import == expected_response_import
# Generated by the pRPC protocol buffer compiler plugin. DO NOT EDIT! # source: test.proto from google.protobuf import descriptor_pb2 TestServiceDescription = { 'descriptor': descriptor_pb2.ServiceDescriptorProto(), 'package': 'test', } TestServiceDescription['descriptor'].ParseFromString( '0a045465737412330a044769766512112e746573742e47697665526571756573741a162e676f6f676c652e70726f746f6275662e456d707479220012340a0454616b6512162e676f6f676c652e70726f746f6275662e456d7074791a122e746573742e54616b65526573706f6e73652200122f0a044563686f12112e746573742e4563686f526571756573741a122e746573742e4563686f526573706f6e73652200' .decode('hex'))