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..a70e26ac99 100644 --- a/tests/unit/schema/test_api.py +++ b/tests/unit/schema/test_api.py @@ -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)