From 7413c0559ee74c895a85920eb3ff2bac710ea7a9 Mon Sep 17 00:00:00 2001 From: ohmayr Date: Tue, 13 Jan 2026 08:34:05 +0000 Subject: [PATCH 1/3] chore: skip naming analysis during initial type discovery --- gapic/schema/api.py | 10 +++- tests/unit/schema/test_api.py | 87 +++++++++++++++++++++++++++++++++-- 2 files changed, 92 insertions(+), 5 deletions(-) diff --git a/gapic/schema/api.py b/gapic/schema/api.py index b05f6b6dec..22f88ed21e 100644 --- a/gapic/schema/api.py +++ b/gapic/schema/api.py @@ -114,6 +114,7 @@ def build( opts: Options = Options(), prior_protos: Optional[Mapping[str, "Proto"]] = None, load_services: bool = True, + skip_context_analysis: bool = False, all_resources: Optional[Mapping[str, wrappers.MessageType]] = None, ) -> "Proto": """Build and return a Proto instance. @@ -130,6 +131,8 @@ def build( load_services (bool): Toggle whether the proto file should load its services. Not doing so enables a two-pass fix for LRO response and metadata types in certain situations. + skip_context_analysis (bool): Toggle whether to skip context + analysis. Defaults to False. """ return _ProtoBuilder( file_descriptor, @@ -138,6 +141,7 @@ def build( opts=opts, prior_protos=prior_protos or {}, load_services=load_services, + skip_context_analysis=skip_context_analysis, all_resources=all_resources or {}, ).proto @@ -463,8 +467,8 @@ def disambiguate_keyword_sanitize_fname( naming=naming, opts=opts, prior_protos=pre_protos, - # Ugly, ugly hack. load_services=False, + skip_context_analysis=True, ) # A file descriptor's file-level resources are NOT visible to any importers. @@ -1102,6 +1106,7 @@ def __init__( opts: Options = Options(), prior_protos: Optional[Mapping[str, Proto]] = None, load_services: bool = True, + skip_context_analysis: bool = False, all_resources: Optional[Mapping[str, wrappers.MessageType]] = None, ): self.proto_messages: Dict[str, wrappers.MessageType] = {} @@ -1111,6 +1116,7 @@ def __init__( self.file_to_generate = file_to_generate self.prior_protos = prior_protos or {} self.opts = opts + self.skip_context_analysis = skip_context_analysis # Iterate over the documentation and place it into a dictionary. # @@ -1217,7 +1223,7 @@ def proto(self) -> Proto: # If this is not a file being generated, we do not need to # do anything else. - if not self.file_to_generate: + if not self.file_to_generate or self.skip_context_analysis: return naive visited_messages: Set[wrappers.MessageType] = set() diff --git a/tests/unit/schema/test_api.py b/tests/unit/schema/test_api.py index 300e8cab89..f643e03a2e 100644 --- a/tests/unit/schema/test_api.py +++ b/tests/unit/schema/test_api.py @@ -4430,9 +4430,9 @@ def test_method_settings_unsupported_auto_populated_field_field_info_format_rais the format of the field is `IPV4`. """ field_options = descriptor_pb2.FieldOptions() - field_options.Extensions[field_info_pb2.field_info].format = ( - field_info_pb2.FieldInfo.Format.Value("IPV4") - ) + field_options.Extensions[ + field_info_pb2.field_info + ].format = field_info_pb2.FieldInfo.Format.Value("IPV4") squid = make_field_pb2( name="squid", type="TYPE_STRING", options=field_options, number=1 ) @@ -4499,3 +4499,84 @@ def test_method_settings_invalid_multiple_issues(): assert re.match(".*squid.*not.*uuid4.*", error_yaml[method_example1][1].lower()) assert re.match(".*octopus.*not.*uuid4.*", error_yaml[method_example1][2].lower()) assert re.match(".*method.*not found.*", error_yaml[method_example2][0].lower()) + + +def test_proto_build_skip_context_analysis(): + """Test that Proto.build works with skip_context_analysis=True.""" + fdp = make_file_pb2( + name="my_proto.proto", + package="google.example.v1", + messages=(make_message_pb2(name="MyMessage"),), + ) + + # When skip_context_analysis is True, it should still return a Proto object + # but skip the context analysis phase (resolving collisions etc which happens in with_context). + # Since we can't easily check internal state of "naive" vs "context-aware" without + # relying on implementation details, we at least ensure it runs and returns a Proto. + + proto = api.Proto.build( + fdp, file_to_generate=True, naming=make_naming(), skip_context_analysis=True + ) + + assert isinstance(proto, api.Proto) + assert "google.example.v1.MyMessage" in proto.messages + assert proto.file_to_generate is True + + +def test_api_build_uses_skip_context_analysis(): + """Test that API.build calls Proto.build with skip_context_analysis=True in the first pass.""" + + fd = make_file_pb2( + name="test.proto", + package="google.example.v1", + messages=(make_message_pb2(name="Msg"),), + ) + + with mock.patch.object( + api.Proto, "build", side_effect=api.Proto.build + ) as mock_proto_build: + api.API.build([fd], package="google.example.v1") + + # API.build makes 2 passes (plus maybe more for other things). + # The first pass should have skip_context_analysis=True. + + # Gather all calls + calls = mock_proto_build.call_args_list + + # There should be at least 2 calls for our file (Pass 1 and Pass 2). + # We look for the one with skip_context_analysis=True. + + found_skip = False + for call in calls: + # call.kwargs contains arguments + if call.kwargs.get("skip_context_analysis") is True: + found_skip = True + # Also verify load_services is False as per current implementation of Pass 1 + assert call.kwargs.get("load_services") is False + break + + assert found_skip, "Proto.build was not called with skip_context_analysis=True" + + +def test_proto_builder_skip_context_analysis_property(): + """Test that _ProtoBuilder sets the skip_context_analysis property correctly.""" + fdp = descriptor_pb2.FileDescriptorProto( + name="my_proto_file.proto", + package="google.example.v1", + ) + + builder = api._ProtoBuilder( + fdp, + file_to_generate=True, + naming=make_naming(), + skip_context_analysis=True, + ) + + assert builder.skip_context_analysis is True + + # Verify that .proto property returns the naive proto when skip_context_analysis is True + # We can check if the return value is the same as the 'naive' one constructed inside. + # But since we can't access 'naive' local variable, we verify behavior. + + proto = builder.proto + assert isinstance(proto, api.Proto) From 528afab664c3d5137e19edab00b42980c4fa5e99 Mon Sep 17 00:00:00 2001 From: ohmayr Date: Tue, 13 Jan 2026 08:41:16 +0000 Subject: [PATCH 2/3] fix lint --- tests/unit/schema/test_api.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/unit/schema/test_api.py b/tests/unit/schema/test_api.py index f643e03a2e..67afa4ace0 100644 --- a/tests/unit/schema/test_api.py +++ b/tests/unit/schema/test_api.py @@ -2611,16 +2611,16 @@ def test_mixin_api_methods_locations(): assert len(ms) == 2 m1 = ms["ListLocations"] m1.options.ClearExtension(annotations_pb2.http) - m1.options.Extensions[annotations_pb2.http].selector = ( - "google.cloud.location.Locations.ListLocations" - ) + m1.options.Extensions[ + annotations_pb2.http + ].selector = "google.cloud.location.Locations.ListLocations" m1.options.Extensions[annotations_pb2.http].get = "/v1/{name=examples/*}/*" m1.options.Extensions[annotations_pb2.http].body = "*" m2 = ms["GetLocation"] m2.options.ClearExtension(annotations_pb2.http) - m2.options.Extensions[annotations_pb2.http].selector = ( - "google.cloud.location.Locations.GetLocation" - ) + m2.options.Extensions[ + annotations_pb2.http + ].selector = "google.cloud.location.Locations.GetLocation" m2.options.Extensions[annotations_pb2.http].get = "/v1/{name=examples/*}/*" m2.options.Extensions[annotations_pb2.http].body = "*" api_schema = api.API.build(fd, "google.example.v1", opts=opts) @@ -3032,9 +3032,9 @@ def get_file_descriptor_proto_for_tests( """ field_options = descriptor_pb2.FieldOptions() - field_options.Extensions[field_info_pb2.field_info].format = ( - field_info_pb2.FieldInfo.Format.Value("UUID4") - ) + field_options.Extensions[ + field_info_pb2.field_info + ].format = field_info_pb2.FieldInfo.Format.Value("UUID4") fd = ( make_file_pb2( @@ -4213,9 +4213,9 @@ def test_read_method_settings_from_service_yaml(): } cli_options = Options(service_yaml_config=service_yaml_config) field_options = descriptor_pb2.FieldOptions() - field_options.Extensions[field_info_pb2.field_info].format = ( - field_info_pb2.FieldInfo.Format.Value("UUID4") - ) + field_options.Extensions[ + field_info_pb2.field_info + ].format = field_info_pb2.FieldInfo.Format.Value("UUID4") squid = make_field_pb2( name="squid", type="TYPE_STRING", options=field_options, number=1 @@ -4461,9 +4461,9 @@ def test_method_settings_invalid_multiple_issues(): # - Not annotated with google.api.field_info.format = UUID4 # - Not of type string # - Required field - field_options.Extensions[field_info_pb2.field_info].format = ( - field_info_pb2.FieldInfo.Format.Value("IPV4") - ) + field_options.Extensions[ + field_info_pb2.field_info + ].format = field_info_pb2.FieldInfo.Format.Value("IPV4") squid = make_field_pb2( name="squid", type="TYPE_INT32", options=field_options, number=1 ) From a2812e4ddce2c6e10af58463323cd48a462bb277 Mon Sep 17 00:00:00 2001 From: ohmayr Date: Tue, 13 Jan 2026 08:43:50 +0000 Subject: [PATCH 3/3] fix lint --- tests/unit/schema/test_api.py | 36 +++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/tests/unit/schema/test_api.py b/tests/unit/schema/test_api.py index 67afa4ace0..a70e26ac99 100644 --- a/tests/unit/schema/test_api.py +++ b/tests/unit/schema/test_api.py @@ -2611,16 +2611,16 @@ def test_mixin_api_methods_locations(): assert len(ms) == 2 m1 = ms["ListLocations"] m1.options.ClearExtension(annotations_pb2.http) - m1.options.Extensions[ - annotations_pb2.http - ].selector = "google.cloud.location.Locations.ListLocations" + m1.options.Extensions[annotations_pb2.http].selector = ( + "google.cloud.location.Locations.ListLocations" + ) m1.options.Extensions[annotations_pb2.http].get = "/v1/{name=examples/*}/*" m1.options.Extensions[annotations_pb2.http].body = "*" m2 = ms["GetLocation"] m2.options.ClearExtension(annotations_pb2.http) - m2.options.Extensions[ - annotations_pb2.http - ].selector = "google.cloud.location.Locations.GetLocation" + m2.options.Extensions[annotations_pb2.http].selector = ( + "google.cloud.location.Locations.GetLocation" + ) m2.options.Extensions[annotations_pb2.http].get = "/v1/{name=examples/*}/*" m2.options.Extensions[annotations_pb2.http].body = "*" api_schema = api.API.build(fd, "google.example.v1", opts=opts) @@ -3032,9 +3032,9 @@ def get_file_descriptor_proto_for_tests( """ field_options = descriptor_pb2.FieldOptions() - field_options.Extensions[ - field_info_pb2.field_info - ].format = field_info_pb2.FieldInfo.Format.Value("UUID4") + field_options.Extensions[field_info_pb2.field_info].format = ( + field_info_pb2.FieldInfo.Format.Value("UUID4") + ) fd = ( make_file_pb2( @@ -4213,9 +4213,9 @@ def test_read_method_settings_from_service_yaml(): } cli_options = Options(service_yaml_config=service_yaml_config) field_options = descriptor_pb2.FieldOptions() - field_options.Extensions[ - field_info_pb2.field_info - ].format = field_info_pb2.FieldInfo.Format.Value("UUID4") + field_options.Extensions[field_info_pb2.field_info].format = ( + field_info_pb2.FieldInfo.Format.Value("UUID4") + ) squid = make_field_pb2( name="squid", type="TYPE_STRING", options=field_options, number=1 @@ -4430,9 +4430,9 @@ def test_method_settings_unsupported_auto_populated_field_field_info_format_rais the format of the field is `IPV4`. """ field_options = descriptor_pb2.FieldOptions() - field_options.Extensions[ - field_info_pb2.field_info - ].format = field_info_pb2.FieldInfo.Format.Value("IPV4") + field_options.Extensions[field_info_pb2.field_info].format = ( + field_info_pb2.FieldInfo.Format.Value("IPV4") + ) squid = make_field_pb2( name="squid", type="TYPE_STRING", options=field_options, number=1 ) @@ -4461,9 +4461,9 @@ def test_method_settings_invalid_multiple_issues(): # - Not annotated with google.api.field_info.format = UUID4 # - Not of type string # - Required field - field_options.Extensions[ - field_info_pb2.field_info - ].format = field_info_pb2.FieldInfo.Format.Value("IPV4") + field_options.Extensions[field_info_pb2.field_info].format = ( + field_info_pb2.FieldInfo.Format.Value("IPV4") + ) squid = make_field_pb2( name="squid", type="TYPE_INT32", options=field_options, number=1 )