@@ -265,6 +265,7 @@ def add_to_address_allowlist(
265265 address_allowlist : Set ["metadata.Address" ],
266266 method_allowlist : Set [str ],
267267 resource_messages : Dict [str , "wrappers.MessageType" ],
268+ services_in_proto : Dict [str , "wrappers.Service" ],
268269 ) -> None :
269270 """Adds to the set of Addresses of wrapper objects to be included in selective GAPIC generation.
270271
@@ -281,15 +282,12 @@ def add_to_address_allowlist(
281282 resource type name of a resource message to the corresponding MessageType object
282283 representing that resource message. Only resources with a message representation
283284 should be included in the dictionary.
285+ services_in_proto (Dict[str, wrappers.Service]): A dictionary mapping the names of Service
286+ objects in the proto containing this method to the Service objects. This is necessary
287+ for traversing the operation service in the case of extended LROs.
284288 Returns:
285289 None
286290 """
287- # The method.operation_service for an extended LRO is not fully qualified, so we
288- # truncate the service names accordingly so they can be found in
289- # method.add_to_address_allowlist
290- services_in_proto = {
291- service .name : service for service in self .services .values ()
292- }
293291 for service in self .services .values ():
294292 service .add_to_address_allowlist (
295293 address_allowlist = address_allowlist ,
@@ -298,75 +296,60 @@ def add_to_address_allowlist(
298296 services_in_proto = services_in_proto ,
299297 )
300298
301- def prune_messages_for_selective_generation (
302- self , * , address_allowlist : Set ["metadata.Address" ]
299+ def with_selective_generation (
300+ self ,
301+ * ,
302+ generate_omitted_as_internal : bool ,
303+ public_methods : Set [str ],
304+ excluded_addresses : Set ["metadata.Address" ],
303305 ) -> Optional ["Proto" ]:
304- """Returns a truncated version of this Proto.
305-
306- Only the services, messages, and enums contained in the allowlist
307- of visited addresses are included in the returned object. If there
308- are no services, messages, or enums left, and no file level resources,
309- return None.
306+ """Returns a version of this Proto for selective generation.
310307
311308 Args:
312- address_allowlist (Set[metadata.Address]): A set of allowlisted metadata.Address
313- objects to filter against. Objects with addresses not the allowlist will be
314- removed from the returned Proto.
309+ generate_omitted_as_internal (bool): Whether to mark omitted methods as internal.
310+ public_methods (Set[str]): The set of fully-qualified method names to keep as public.
311+ excluded_addresses (Set[metadata.Address]): The set of addresses to exclude from generation.
312+
315313 Returns:
316- Optional[Proto]: A truncated version of this proto. If there are no services, messages,
317- or enums left after the truncation process and there are no file level resources,
318- returns None.
314+ Optional[Proto]: A version of this Proto with services/methods filtered.
315+ Returns None if the Proto becomes empty and generate_omitted_as_internal is False.
319316 """
320- # Once the address allowlist has been created, it suffices to only
321- # prune items at 2 different levels to truncate the Proto object:
317+ services = {}
318+ for k , v in self .services .items ():
319+ new_v = v .with_selective_generation (
320+ generate_omitted_as_internal = generate_omitted_as_internal ,
321+ public_methods = public_methods ,
322+ excluded_addresses = excluded_addresses )
323+ if new_v :
324+ services [k ] = new_v
325+
326+ # We only prune messages/enums from protos that are not dependencies.
327+ # A message or enum is excluded IF AND ONLY IF:
328+ # 1. It is a top-level request or response message for an omitted RPC.
329+ # 2. It is NOT reachable from any publicly allowed RPC.
322330 #
323- # 1. At the Proto level, we remove unnecessary services, messages,
324- # and enums.
325- # 2. For allowlisted services, at the Service level, we remove
326- # non-allowlisted methods.
327- services = {
328- k : v .prune_messages_for_selective_generation (
329- address_allowlist = address_allowlist
330- )
331- for k , v in self .services .items ()
332- if v .meta .address in address_allowlist
333- }
334-
331+ # This ensures that shared messages, messages not attached to any RPC,
332+ # and messages reachable via other paths (like LRO response types) are KEPT.
335333 all_messages = {
336- k : v for k , v in self .all_messages .items () if v .ident in address_allowlist
334+ k : v for k , v in self .all_messages .items () if v .ident not in excluded_addresses
337335 }
338336
339337 all_enums = {
340- k : v for k , v in self .all_enums .items () if v .ident in address_allowlist
338+ k : v for k , v in self .all_enums .items () if v .ident not in excluded_addresses
341339 }
342340
343- if not services and not all_messages and not all_enums :
341+ # If the proto becomes empty after pruning, we return None to signal
342+ # that it should be excluded from generation.
343+ if not generate_omitted_as_internal and not services and not all_messages and not all_enums :
344344 return None
345345
346346 return dataclasses .replace (
347- self , services = services , all_messages = all_messages , all_enums = all_enums
347+ self ,
348+ services = services ,
349+ all_messages = all_messages ,
350+ all_enums = all_enums ,
348351 )
349352
350- def with_internal_methods (self , * , public_methods : Set [str ]) -> "Proto" :
351- """Returns a version of this Proto with some Methods marked as internal.
352-
353- The methods not in the public_methods set will be marked as internal and
354- services containing these methods will also be marked as internal by extension.
355- (See :meth:`Service.is_internal` for more details).
356-
357- Args:
358- public_methods (Set[str]): An allowlist of fully-qualified method names.
359- Methods not in this allowlist will be marked as internal.
360- Returns:
361- Proto: A version of this Proto with Method objects corresponding to methods
362- not in `public_methods` marked as internal.
363- """
364- services = {
365- k : v .with_internal_methods (public_methods = public_methods )
366- for k , v in self .services .items ()
367- }
368- return dataclasses .replace (self , services = services )
369-
370353
371354@dataclasses .dataclass (frozen = True )
372355class API :
@@ -532,32 +515,79 @@ def disambiguate_keyword_sanitize_fname(
532515
533516 if selective_gapic_settings .generate_omitted_as_internal :
534517 for name , proto in api .protos .items ():
535- new_all_protos [name ] = proto .with_internal_methods (
536- public_methods = selective_gapic_methods
518+ new_all_protos [name ] = proto .with_selective_generation (
519+ generate_omitted_as_internal = True ,
520+ public_methods = selective_gapic_methods ,
521+ excluded_addresses = set ([]),
537522 )
538523 else :
539- all_resource_messages = collections .ChainMap (
540- * (proto .resource_messages for proto in protos .values ())
541- )
542-
543- # Prepare a list of addresses to include in selective generation,
544- # then prune each Proto object. We look at metadata.Addresses, not objects, because
545- # objects that refer to the same thing in the proto are different Python objects
546- # in memory.
547- address_allowlist : Set ["metadata.Address" ] = set ([])
548- for proto in api .protos .values ():
524+ all_resource_messages = dict (collections .ChainMap (
525+ * (proto .resource_messages for proto in api .all_protos .values ())
526+ ))
527+
528+ # Create a global map of services to support cross-proto lookup
529+ # for extended LROs.
530+ #
531+ # Note: This is keyed by the fully qualified proto name (as a string)
532+ # to ensure compatibility with Address.resolve() lookups in wrappers.py.
533+ all_services : Dict [str , wrappers .Service ] = {}
534+ for p in api .all_protos .values ():
535+ for s in p .services .values ():
536+ all_services [s .meta .address .proto ] = s
537+
538+ # Calculate addresses of omitted RPCs and their top-level request/response messages.
539+ # These are "candidates" for exclusion.
540+ #
541+ # We only consider top-level request/response messages of omitted RPCs as
542+ # candidates for exclusion. This is conservative: it ensures that:
543+ # - Messages NOT used by any RPC are KEPT (e.g. for user convenience).
544+ # - Messages shared between an omitted and a kept RPC are KEPT.
545+ # - Messages reachable from a kept RPC but NOT as a top-level request/response
546+ # (e.g. nested messages) are KEPT.
547+ candidate_excluded_addresses : Set ["metadata.Address" ] = set ([])
548+ for proto in api .all_protos .values ():
549+ for service in proto .services .values ():
550+ for method in service .methods .values ():
551+ if method .ident .proto not in selective_gapic_methods :
552+ # Candidate for exclusion: the method itself and its direct request/response types.
553+ candidate_excluded_addresses .add (method .meta .address )
554+ candidate_excluded_addresses .add (method .input .ident )
555+ candidate_excluded_addresses .add (method .output .ident )
556+
557+ # If this is an LRO, add its response and metadata types to candidates.
558+ if method .lro :
559+ candidate_excluded_addresses .add (method .lro .response_type .ident )
560+ candidate_excluded_addresses .add (method .lro .metadata_type .ident )
561+
562+ # If this is an extended LRO, add its request and operation types to candidates.
563+ if method .extended_lro :
564+ candidate_excluded_addresses .add (method .extended_lro .request_type .ident )
565+ candidate_excluded_addresses .add (method .extended_lro .operation_type .ident )
566+
567+ # Calculate publicly reachable addresses (API-wide).
568+ # This includes all types reachable from the allowlisted (public) methods.
569+ public_rpc_addresses : Set ["metadata.Address" ] = set ([])
570+ for proto in api .all_protos .values ():
549571 proto .add_to_address_allowlist (
550- address_allowlist = address_allowlist ,
572+ address_allowlist = public_rpc_addresses ,
551573 method_allowlist = selective_gapic_methods ,
552574 resource_messages = all_resource_messages ,
575+ services_in_proto = all_services ,
553576 )
554577
555- # We only prune services/messages/enums from protos that are not dependencies.
578+ # Addresses to exclude: those that are candidates for exclusion but NOT
579+ # reachable from any PUBLIC RPC.
580+ #
581+ # This set difference effectively "vets" the candidates. If a candidate
582+ # message is actually reachable from a public RPC, it's removed from
583+ # the exclusion list.
584+ excluded_addresses = candidate_excluded_addresses - public_rpc_addresses
585+
556586 for name , proto in api .protos .items ():
557- proto_to_generate = (
558- proto . prune_messages_for_selective_generation (
559- address_allowlist = address_allowlist
560- )
587+ proto_to_generate = proto . with_selective_generation (
588+ generate_omitted_as_internal = False ,
589+ public_methods = selective_gapic_methods ,
590+ excluded_addresses = excluded_addresses ,
561591 )
562592 if proto_to_generate :
563593 new_all_protos [name ] = proto_to_generate
0 commit comments