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_undefined_type(): fd = (make_file_pb2( name='mollusc.proto', package='google.mollusca', messages=(make_message_pb2(name='Mollusc', fields=(make_field_pb2( name='class', type_name='google.mollusca.Class', number=1, ), )), ), ), ) with pytest.raises(TypeError): api.API.build(fd, package='google.mollusca')
def test_proto_names(): # 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='Bar', 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='Baz', fields=(make_field_pb2( name='foo', number=1, type_name='.google.example.v1.Foo'), )), ), ), ) # Create an API with those protos. api_schema = api.API.build(fd, package='google.example.v1') proto = api_schema.protos['foo.proto'] assert proto.names == { 'Foo', 'Bar', 'Baz', 'foo', 'imported_message', 'primitive' } assert proto.disambiguate('enum') == 'enum' assert proto.disambiguate('foo') == '_foo'
def test_proto_names_import_collision(): # Put together a couple of minimal protos. fd = ( make_file_pb2( name='a/b/c/spam.proto', package='a.b.c', messages=(make_message_pb2(name='ImportedMessage', fields=()),), ), make_file_pb2( name='x/y/z/spam.proto', package='x.y.z', messages=(make_message_pb2(name='OtherMessage', fields=()),), ), make_file_pb2( name='foo.proto', package='google.example.v1', messages=( make_message_pb2(name='Foo', fields=()), make_message_pb2(name='Bar', fields=( make_field_pb2(name='imported_message', number=1, type_name='.a.b.c.ImportedMessage'), make_field_pb2(name='other_message', number=2, type_name='.x.y.z.OtherMessage'), make_field_pb2(name='primitive', number=3, type=1), )), make_message_pb2(name='Baz', fields=( make_field_pb2(name='foo', number=1, type_name='.google.example.v1.Foo'), )), ), ), ) # Create an API with those protos. api_schema = api.API.build(fd, package='google.example.v1') proto = api_schema.protos['foo.proto'] assert proto.names == {'Foo', 'Bar', 'Baz', 'foo', 'imported_message', 'other_message', 'primitive', 'spam'}
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_messages_reverse_declaration_order(): # Test that if a message is used as a field higher in the same file, # that things still work. message_pbs = ( make_message_pb2(name='Foo', fields=( make_field_pb2(name='bar', number=1, type_name='.google.example.v3.Bar'), ), ), make_message_pb2(name='Bar'), ) fdp = make_file_pb2( messages=message_pbs, package='google.example.v3', ) # Make the proto object. proto = api.Proto.build(fdp, file_to_generate=True, naming=make_naming()) # Get the message. assert len(proto.messages) == 2 Foo = proto.messages['google.example.v3.Foo'] assert Foo.fields['bar'].message == proto.messages['google.example.v3.Bar']
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 test_messages_recursive(): # Test that if a message is used inside itself, that things will still # work. message_pbs = (make_message_pb2( name='Foo', fields=(make_field_pb2(name='foo', number=1, type_name='.google.example.v3.Foo'), ), ), ) fdp = make_file_pb2( messages=message_pbs, package='google.example.v3', ) # Make the proto object. proto = api.Proto.build(fdp, file_to_generate=True, naming=make_naming()) # Get the message. assert len(proto.messages) == 1 Foo = proto.messages['google.example.v3.Foo'] assert Foo.fields['foo'].message == proto.messages['google.example.v3.Foo']
def test_top_level_enum(): # Test that a nested enum works properly. message_pbs = (make_message_pb2(name='Coleoidea', enum_type=(make_enum_pb2( 'Superorder', 'Decapodiformes', 'Octopodiformes', 'Palaeoteuthomorpha', ), )), ) enum_pbs = (make_enum_pb2( 'Order', 'Gastropoda', 'Bivalvia', 'Cephalopoda', ), ) fds = (make_file_pb2( messages=message_pbs, enums=enum_pbs, package='google.example.v3', ), ) api_schema = api.API.build(fds, package='google.example.v3') actual = [e.name for e in api_schema.top_level_enums.values()] expected = ['Order'] assert expected == actual
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_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_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_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
def test_file_level_resources(): fdp = make_file_pb2( name="nomenclature.proto", package="nomenclature.linneaen.v1", messages=( 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", ), ), ), ), ) res_pb2 = fdp.options.Extensions[resource_pb2.resource_definition] definitions = [ ("nomenclature.linnaen.com/Species", "families/{family}/genera/{genus}/species/{species}"), ("nomenclature.linnaen.com/Phylum", "kingdoms/{kingdom}/phyla/{phylum}"), ] for type_, pattern in definitions: resource_definition = res_pb2.add() resource_definition.type = type_ resource_definition.pattern.append(pattern) species_field = fdp.message_type[0].field[0] resource_reference = species_field.options.Extensions[ resource_pb2.resource_reference] resource_reference.type = "nomenclature.linnaen.com/Species" api_schema = api.API.build([fdp], package='nomenclature.linneaen.v1') actual = api_schema.protos['nomenclature.proto'].resource_messages expected = collections.OrderedDict(( ("nomenclature.linnaen.com/Species", wrappers.CommonResource( type_name="nomenclature.linnaen.com/Species", pattern="families/{family}/genera/{genus}/species/{species}"). message_type), ("nomenclature.linnaen.com/Phylum", wrappers.CommonResource( type_name="nomenclature.linnaen.com/Phylum", pattern="kingdoms/{kingdom}/phyla/{phylum}").message_type), )) assert actual == expected # The proto file _owns_ the file level resources, but the service needs to # see them too because the client class owns all the helper methods. service = api_schema.services["nomenclature.linneaen.v1.SpeciesService"] actual = service.visible_resources assert actual == expected # The service doesn't own any method that owns a message that references # Phylum, so the service doesn't count it among its resource messages. expected.pop("nomenclature.linnaen.com/Phylum") expected = frozenset(expected.values()) actual = service.resource_messages assert actual == expected
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'), )