Skip to content

Commit a532424

Browse files
committed
fix: restore messages not attached to rpc in selective_gapic_generation
1 parent 9065368 commit a532424

5 files changed

Lines changed: 466 additions & 225 deletions

File tree

packages/gapic-generator/gapic/schema/api.py

Lines changed: 112 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -259,114 +259,60 @@ def disambiguate(self, string: str) -> str:
259259
return self.disambiguate(f"_{string}")
260260
return string
261261

262-
def add_to_address_allowlist(
262+
def with_selective_generation(
263263
self,
264264
*,
265-
address_allowlist: Set["metadata.Address"],
266-
method_allowlist: Set[str],
267-
resource_messages: Dict[str, "wrappers.MessageType"],
268-
) -> None:
269-
"""Adds to the set of Addresses of wrapper objects to be included in selective GAPIC generation.
270-
271-
This method is used to create an allowlist of addresses to be used to filter out unneeded
272-
services, methods, messages, and enums at a later step.
273-
274-
Args:
275-
address_allowlist (Set[metadata.Address]): A set of allowlisted metadata.Address
276-
objects to add to. Only the addresses of the allowlisted methods, the services
277-
containing these methods, and messages/enums those methods use will be part of the
278-
final address_allowlist. The set may be modified during this call.
279-
method_allowlist (Set[str]): An allowlist of fully-qualified method names.
280-
resource_messages (Dict[str, wrappers.MessageType]): A dictionary mapping the unified
281-
resource type name of a resource message to the corresponding MessageType object
282-
representing that resource message. Only resources with a message representation
283-
should be included in the dictionary.
284-
Returns:
285-
None
286-
"""
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-
}
293-
for service in self.services.values():
294-
service.add_to_address_allowlist(
295-
address_allowlist=address_allowlist,
296-
method_allowlist=method_allowlist,
297-
resource_messages=resource_messages,
298-
services_in_proto=services_in_proto,
299-
)
300-
301-
def prune_messages_for_selective_generation(
302-
self, *, address_allowlist: Set["metadata.Address"]
265+
generate_omitted_as_internal: bool,
266+
public_methods: Set[str],
267+
excluded_addresses: Set["metadata.Address"],
303268
) -> 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.
269+
"""Returns a version of this Proto for selective generation.
310270
311271
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.
272+
generate_omitted_as_internal (bool): Whether to mark omitted methods as internal.
273+
public_methods (Set[str]): The set of fully-qualified method names to keep as public.
274+
excluded_addresses (Set[metadata.Address]): The set of addresses to exclude from generation.
275+
315276
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.
277+
Optional[Proto]: A version of this Proto with services/methods filtered.
278+
Returns None if the Proto becomes empty and generate_omitted_as_internal is False.
319279
"""
320-
# Once the address allowlist has been created, it suffices to only
321-
# prune items at 2 different levels to truncate the Proto object:
280+
services = {}
281+
for k, v in self.services.items():
282+
new_v = v.with_selective_generation(
283+
generate_omitted_as_internal=generate_omitted_as_internal,
284+
public_methods=public_methods,
285+
excluded_addresses=excluded_addresses)
286+
if new_v:
287+
services[k] = new_v
288+
289+
# We only prune messages/enums from protos that are not dependencies.
290+
# A message or enum is excluded IF AND ONLY IF:
291+
# 1. It is a top-level request or response message for an omitted RPC.
292+
# 2. It is NOT reachable from any publicly allowed RPC.
322293
#
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-
294+
# This ensures that shared messages, messages not attached to any RPC,
295+
# and messages reachable via other paths (like LRO response types) are KEPT.
335296
all_messages = {
336-
k: v for k, v in self.all_messages.items() if v.ident in address_allowlist
297+
k: v for k, v in self.all_messages.items() if v.ident not in excluded_addresses
337298
}
338299

339300
all_enums = {
340-
k: v for k, v in self.all_enums.items() if v.ident in address_allowlist
301+
k: v for k, v in self.all_enums.items() if v.ident not in excluded_addresses
341302
}
342303

343-
if not services and not all_messages and not all_enums:
304+
# If the proto becomes empty after pruning, we return None to signal
305+
# that it should be excluded from generation.
306+
if not generate_omitted_as_internal and not services and not all_messages and not all_enums:
344307
return None
345308

346309
return dataclasses.replace(
347-
self, services=services, all_messages=all_messages, all_enums=all_enums
310+
self,
311+
services=services,
312+
all_messages=all_messages,
313+
all_enums=all_enums,
348314
)
349315

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-
370316

371317
@dataclasses.dataclass(frozen=True)
372318
class API:
@@ -530,37 +476,82 @@ def disambiguate_keyword_sanitize_fname(
530476
k: v for k, v in api.all_protos.items() if k not in api.protos
531477
}
532478

533-
if selective_gapic_settings.generate_omitted_as_internal:
534-
for name, proto in api.protos.items():
535-
new_all_protos[name] = proto.with_internal_methods(
536-
public_methods=selective_gapic_methods
537-
)
538-
else:
539-
all_resource_messages = collections.ChainMap(
540-
*(proto.resource_messages for proto in protos.values())
541-
)
479+
all_resource_messages = collections.ChainMap(
480+
*(proto.resource_messages for proto in api.all_protos.values())
481+
)
542482

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():
549-
proto.add_to_address_allowlist(
550-
address_allowlist=address_allowlist,
483+
# Create a global map of services to support cross-proto lookup
484+
# for extended LROs.
485+
#
486+
# Note: This is keyed by the Address object itself (which is
487+
# hashable by its proto name) to ensure compatibility with
488+
# Address.resolve() lookups in wrappers.py.
489+
all_services: Dict[metadata.Address, wrappers.Service] = {}
490+
for p in api.all_protos.values():
491+
for s in p.services.values():
492+
all_services[s.meta.address] = s
493+
494+
# Calculate addresses of omitted RPCs and their top-level request/response messages.
495+
# These are "candidates" for exclusion.
496+
#
497+
# We only consider top-level request/response messages of omitted RPCs as
498+
# candidates for exclusion. This is conservative: it ensures that:
499+
# - Messages NOT used by any RPC are KEPT (e.g. for user convenience).
500+
# - Messages shared between an omitted and a kept RPC are KEPT.
501+
# - Messages reachable from a kept RPC but NOT as a top-level request/response
502+
# (e.g. nested messages) are KEPT.
503+
candidate_excluded_addresses: Set["metadata.Address"] = set([])
504+
for proto in api.all_protos.values():
505+
for service in proto.services.values():
506+
for method in service.methods.values():
507+
if method.ident.proto not in selective_gapic_methods:
508+
# Candidate for exclusion: the method itself and its direct request/response types.
509+
candidate_excluded_addresses.add(method.meta.address)
510+
candidate_excluded_addresses.add(method.input.ident)
511+
candidate_excluded_addresses.add(method.output.ident)
512+
513+
# If this is an LRO, add its response and metadata types to candidates.
514+
if method.lro:
515+
candidate_excluded_addresses.add(method.lro.response_type.ident)
516+
candidate_excluded_addresses.add(method.lro.metadata_type.ident)
517+
518+
# If this is an extended LRO, add its request and operation types to candidates.
519+
if method.extended_lro:
520+
candidate_excluded_addresses.add(method.extended_lro.request_type.ident)
521+
candidate_excluded_addresses.add(method.extended_lro.operation_type.ident)
522+
523+
# Calculate publicly reachable addresses (API-wide).
524+
# This includes all types reachable from the allowlisted (public) methods.
525+
public_rpc_addresses: Set["metadata.Address"] = set([])
526+
for proto in api.all_protos.values():
527+
for service in proto.services.values():
528+
service.add_to_address_allowlist(
529+
address_allowlist=public_rpc_addresses,
551530
method_allowlist=selective_gapic_methods,
552531
resource_messages=all_resource_messages,
532+
services_in_proto=all_services,
553533
)
554534

555-
# We only prune services/messages/enums from protos that are not dependencies.
556-
for name, proto in api.protos.items():
557-
proto_to_generate = (
558-
proto.prune_messages_for_selective_generation(
559-
address_allowlist=address_allowlist
560-
)
561-
)
562-
if proto_to_generate:
563-
new_all_protos[name] = proto_to_generate
535+
# Addresses to exclude: those that are candidates for exclusion but NOT
536+
# reachable from any PUBLIC RPC.
537+
#
538+
# This set difference effectively "vets" the candidates. If a candidate
539+
# message is actually reachable from a public RPC, it's removed from
540+
# the exclusion list.
541+
excluded_addresses = (
542+
candidate_excluded_addresses - public_rpc_addresses
543+
if not selective_gapic_settings.generate_omitted_as_internal
544+
else set([])
545+
)
546+
547+
for name, proto in api.protos.items():
548+
proto_to_generate = proto.with_selective_generation(
549+
generate_omitted_as_internal=selective_gapic_settings.generate_omitted_as_internal,
550+
public_methods=selective_gapic_methods,
551+
excluded_addresses=excluded_addresses,
552+
)
553+
if proto_to_generate:
554+
new_all_protos[name] = proto_to_generate
564555

565556
api = cls(
566557
naming=naming,
@@ -1514,8 +1505,8 @@ def _maybe_get_lro(
15141505
response_key = service_address.resolve(op.response_type)
15151506
metadata_key = service_address.resolve(op.metadata_type)
15161507
lro = wrappers.OperationInfo(
1517-
response_type=self.api_messages[response_key],
1518-
metadata_type=self.api_messages[metadata_key],
1508+
response_type=self.api_messages[response_key.proto],
1509+
metadata_type=self.api_messages[metadata_key.proto],
15191510
)
15201511

15211512
return lro
@@ -1558,7 +1549,7 @@ def _maybe_get_extended_lro(
15581549
operation_request_key = service_address.resolve(
15591550
operation_polling_method_pb.input_type.lstrip(".")
15601551
)
1561-
operation_request_message = self.api_messages[operation_request_key]
1552+
operation_request_message = self.api_messages[operation_request_key.proto]
15621553

15631554
operation_type = service_address.resolve(
15641555
operation_polling_method_pb.output_type.lstrip(".")
@@ -1568,12 +1559,12 @@ def _maybe_get_extended_lro(
15681559
raise ValueError(
15691560
f"Inconsistent return types between extended lro method '{meth_pb.name}'"
15701561
f" and extended lro polling method '{operation_polling_method_pb.name}':"
1571-
f" '{method_output_type}' and '{operation_type}'"
1562+
f" '{method_output_type.proto}' and '{operation_type.proto}'"
15721563
)
15731564

1574-
operation_message = self.api_messages[operation_type]
1565+
operation_message = self.api_messages[operation_type.proto]
15751566
if not operation_message.is_extended_operation:
1576-
raise ValueError(f"Message is not an extended operation: {operation_type}")
1567+
raise ValueError(f"Message is not an extended operation: {operation_type.proto}")
15771568

15781569
return wrappers.ExtendedOperationInfo(
15791570
request_type=operation_request_message,

packages/gapic-generator/gapic/schema/metadata.py

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -61,20 +61,22 @@ def __eq__(self, other) -> bool:
6161
# We don't want to use api_naming or collisions to determine equality,
6262
# so defer to the parent class's eq method.
6363
# This is an fairly important optimization for large APIs.
64-
return super().__eq__(other)
64+
#
65+
# Note: Address objects are considered equal if they represent the same
66+
# proto element (i.e. same package, parent, and name). This allows
67+
# consistent lookups in dictionaries even if other metadata (like module)
68+
# differs. We use the `.proto` property as the canonical identifier.
69+
if not isinstance(other, Address):
70+
return False
71+
72+
return self.proto == other.proto
6573

6674
def __hash__(self):
67-
# Do NOT include collisions; they are not relevant.
68-
return hash(
69-
(
70-
self.name,
71-
self.module,
72-
self.module_path,
73-
self.package,
74-
self.parent,
75-
self.api_naming,
76-
)
77-
)
75+
# Do NOT include collisions or api_naming; they are not relevant
76+
# to equality and including them violates the hash contract.
77+
#
78+
# We hash based on the canonical proto identifier to ensure consistency with __eq__.
79+
return hash(self.proto)
7880

7981
def __str__(self) -> str:
8082
"""Return the Python identifier for this type.
@@ -339,7 +341,7 @@ def rel(self, address: "Address") -> str:
339341
# Return the usual `module.Name`.
340342
return str(self)
341343

342-
def resolve(self, selector: str) -> str:
344+
def resolve(self, selector: str) -> "Address":
343345
"""Resolve a potentially-relative protobuf selector.
344346
345347
This takes a protobuf selector which may be fully-qualified
@@ -354,11 +356,25 @@ def resolve(self, selector: str) -> str:
354356
or relative.
355357
356358
Returns:
357-
str: An absolute selector.
359+
~.Address: An absolute selector.
360+
361+
Note: The returned Address object is "partial"; it contains the
362+
correct package and name (and thus the correct `.proto` identifier),
363+
but other metadata like `module` or `parent` is reset. This is
364+
sufficient for looking up the full object in API-wide maps.
358365
"""
359366
if "." not in selector:
360-
return f"{'.'.join(self.package)}.{selector}"
361-
return selector
367+
selector = f"{'.'.join(self.package)}.{selector}"
368+
369+
parts = selector.split(".")
370+
return dataclasses.replace(
371+
self,
372+
name=parts[-1],
373+
package=tuple(parts[:-1]),
374+
parent=(),
375+
module="",
376+
module_path=(),
377+
)
362378

363379
@cached_proto_context
364380
def with_context(self, *, collisions: Set[str]) -> "Address":

0 commit comments

Comments
 (0)