diff --git a/.generator/requirements.in b/.generator/requirements.in index cb9f2bad32c0..24d9cd64e588 100644 --- a/.generator/requirements.in +++ b/.generator/requirements.in @@ -1,5 +1,5 @@ click -gapic-generator==1.30.13 # https://github.com/googleapis/gapic-generator-python/releases/tag/v1.30.13 +-e /usr/local/google/home/omairn/git/googleapis/google-cloud-python/packages/gapic-generator nox starlark-pyo3>=2025.1 build diff --git a/.librarian/generate-request.json b/.librarian/generate-request.json new file mode 100644 index 000000000000..8e95ec16bdb9 --- /dev/null +++ b/.librarian/generate-request.json @@ -0,0 +1,25 @@ +{ + "id": "google-cloud-dialogflow", + "version": "2.47.0", + "apis": [ + { + "path": "google/cloud/dialogflow/v2beta1", + "service_config": "dialogflow_v2beta1.yaml" + }, + { + "path": "google/cloud/dialogflow/v2", + "service_config": "dialogflow_v2.yaml" + } + ], + "source_roots": [ + "packages/google-cloud-dialogflow" + ], + "preserve_regex": [ + "packages/google-cloud-dialogflow/CHANGELOG.md", + "docs/CHANGELOG.md" + ], + "remove_regex": [ + "packages/google-cloud-dialogflow/" + ], + "tag_format": "{id}-v{version}" +} \ No newline at end of file diff --git a/packages/gapic-generator/gapic/schema/api.py b/packages/gapic-generator/gapic/schema/api.py index 4ee478a64398..ee97ed330604 100644 --- a/packages/gapic-generator/gapic/schema/api.py +++ b/packages/gapic-generator/gapic/schema/api.py @@ -170,9 +170,12 @@ def resource_messages(self) -> Mapping[str, wrappers.MessageType]: if msg.options.Extensions[resource_pb2.resource].type ) return collections.OrderedDict( - itertools.chain( - file_resource_messages, - resource_messages, + sorted( + itertools.chain( + file_resource_messages, + resource_messages, + ), + key=lambda item: item[0] ) ) diff --git a/packages/gapic-generator/gapic/schema/wrappers.py b/packages/gapic-generator/gapic/schema/wrappers.py index f654d0a82d18..6801baf00a3d 100644 --- a/packages/gapic-generator/gapic/schema/wrappers.py +++ b/packages/gapic-generator/gapic/schema/wrappers.py @@ -685,10 +685,36 @@ def resource_path(self) -> Optional[str]: If there are multiple paths, returns the first one.""" return next(iter(self.options.Extensions[resource_pb2.resource].pattern), None) + def _apply_domain_heuristic(self, raw_type: str) -> str: + """Determines if a resource is foreign and adds a prefix to prevent + [no-redef] AST collisions.""" + if not raw_type: + return "" + + if "/" not in raw_type: + return raw_type + + # Extract the root domain and final resource name, bypassing any nested paths. + # (e.g., "ces.googleapis.com/Project/Location/Tool" -> "ces.googleapis.com" and "Tool") + resource_parts = raw_type.split('/') + domain, short_name = resource_parts[0], resource_parts[-1] + domain_prefix = domain.split('.', 1)[0] + + try: + native_package = self.meta.address.package + # 2. If the domain prefix isn't natively in the package namespace, it's foreign + if domain_prefix and native_package and domain_prefix not in native_package: + return f"{domain_prefix}_{short_name}" + except (AttributeError, TypeError): + # 3. Safe fallback if meta, address, or package are missing/None on this wrapper + pass + + return short_name + @property def resource_type(self) -> Optional[str]: resource = self.options.Extensions[resource_pb2.resource] - return resource.type[resource.type.find("/") + 1 :] if resource else None + return self._apply_domain_heuristic(resource.type) if resource else None @property def resource_type_full_path(self) -> Optional[str]: @@ -2278,9 +2304,9 @@ def names(self) -> FrozenSet[str]: return frozenset(answer) @utils.cached_property - def resource_messages(self) -> FrozenSet[MessageType]: + def resource_messages(self) -> Sequence['MessageType']: """Returns all the resource message types used in all - request and response fields in the service.""" + request and response fields in the service, deterministically sorted.""" def gen_resources(message): if message.resource_path: @@ -2301,7 +2327,7 @@ def gen_indirect_resources_used(message): if resource: yield resource - return frozenset( + unique_messages = frozenset( msg for method in self.methods.values() for msg in chain( @@ -2316,6 +2342,13 @@ def gen_indirect_resources_used(message): ) ) + return tuple( + sorted( + unique_messages, + key=lambda m: m.resource_type_full_path or m.name + ) + ) + @utils.cached_property def resource_messages_dict(self) -> Dict[str, MessageType]: """Returns a dict from resource reference to diff --git a/packages/gapic-generator/tests/unit/schema/test_api.py b/packages/gapic-generator/tests/unit/schema/test_api.py index 9d2dab2ec6a1..9d94e8251523 100644 --- a/packages/gapic-generator/tests/unit/schema/test_api.py +++ b/packages/gapic-generator/tests/unit/schema/test_api.py @@ -1740,17 +1740,17 @@ def test_file_level_resources(): expected = collections.OrderedDict( ( ( - "nomenclature.linnaen.com/Species", + "nomenclature.linnaen.com/Phylum", wrappers.CommonResource( - type_name="nomenclature.linnaen.com/Species", - pattern="families/{family}/genera/{genus}/species/{species}", + type_name="nomenclature.linnaen.com/Phylum", + pattern="kingdoms/{kingdom}/phyla/{phylum}", ).message_type, ), ( - "nomenclature.linnaen.com/Phylum", + "nomenclature.linnaen.com/Species", wrappers.CommonResource( - type_name="nomenclature.linnaen.com/Phylum", - pattern="kingdoms/{kingdom}/phyla/{phylum}", + type_name="nomenclature.linnaen.com/Species", + pattern="families/{family}/genera/{genus}/species/{species}", ).message_type, ), ) @@ -1767,7 +1767,7 @@ def test_file_level_resources(): # 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()) + expected = tuple(expected.values()) actual = service.resource_messages assert actual == expected @@ -1822,7 +1822,7 @@ def test_resources_referenced_but_not_typed(reference_attr="type"): name_resource_opts.child_type = species_resource_opts.type api_schema = api.API.build([fdp], package="nomenclature.linneaen.v1") - expected = {api_schema.messages["nomenclature.linneaen.v1.Species"]} + expected = (api_schema.messages["nomenclature.linneaen.v1.Species"],) actual = api_schema.services[ "nomenclature.linneaen.v1.SpeciesService" ].resource_messages diff --git a/packages/gapic-generator/tests/unit/schema/wrappers/test_message.py b/packages/gapic-generator/tests/unit/schema/wrappers/test_message.py index a13f62c02b9b..22c865b4dd27 100644 --- a/packages/gapic-generator/tests/unit/schema/wrappers/test_message.py +++ b/packages/gapic-generator/tests/unit/schema/wrappers/test_message.py @@ -188,7 +188,7 @@ def test_resource_path(): resource.pattern.append("kingdoms/{kingdom}/phyla/{phylum}/classes/{klass}") resource.pattern.append("kingdoms/{kingdom}/divisions/{division}/classes/{klass}") resource.type = "taxonomy.biology.com/Class" - message = make_message("Squid", options=options) + message = make_message("Squid", options=options, package="taxonomy.biology.v1") assert message.resource_path == "kingdoms/{kingdom}/phyla/{phylum}/classes/{klass}" assert message.resource_path_args == ["kingdom", "phylum", "klass"] @@ -201,7 +201,7 @@ def test_resource_path_with_wildcard(): resource.pattern.append("kingdoms/{kingdom}/phyla/{phylum}/classes/{klass=**}") resource.pattern.append("kingdoms/{kingdom}/divisions/{division}/classes/{klass}") resource.type = "taxonomy.biology.com/Class" - message = make_message("Squid", options=options) + message = make_message("Squid", options=options, package="taxonomy.biology.v1") assert ( message.resource_path == "kingdoms/{kingdom}/phyla/{phylum}/classes/{klass=**}" @@ -230,7 +230,7 @@ def test_resource_path_pure_wildcard(): resource = options.Extensions[resource_pb2.resource] resource.pattern.append("*") resource.type = "taxonomy.biology.com/Class" - message = make_message("Squid", options=options) + message = make_message("Squid", options=options, package="taxonomy.biology.v1") # Pure wildcard resource names do not really help construct resources # but they are a part of the spec so we need to support them, which means at @@ -473,3 +473,60 @@ def test_extended_operation_request_response_fields(): actual = poll_request.extended_operation_response_fields assert actual == expected + + +@pytest.mark.parametrize( + "raw_type, package_tuple, expected", + [ + # 1. Empty or malformed inputs + ("", ("google", "cloud", "dialogflow", "v2"), ""), + ("Tool", ("google", "cloud", "dialogflow", "v2"), "Tool"), + + # 2. Native Resources (Prefix 'dialogflow' IS in the package tuple) + ("dialogflow.googleapis.com/Tool", ("google", "cloud", "dialogflow", "v2"), "Tool"), + ("dialogflow.googleapis.com/Project/Location/Tool", ("google", "cloud", "dialogflow", "v2"), "Tool"), + + # 3. Foreign Resources (Prefix 'ces' is NOT in the package tuple) + ("ces.googleapis.com/Tool", ("google", "cloud", "dialogflow", "v2"), "ces_Tool"), + ("ces.googleapis.com/Project/Location/Tool", ("google", "cloud", "dialogflow", "v2"), "ces_Tool"), + ] +) +def test_apply_domain_heuristic(raw_type, package_tuple, expected): + meta = metadata.Metadata( + address=metadata.Address( + name="TestMessage", + package=package_tuple, + module="test", + ) + ) + message = make_message("TestMessage", meta=meta) + + actual = message._apply_domain_heuristic(raw_type) + assert actual == expected + + +def test_apply_domain_heuristic_none_package(): + """Test the EAFP failsafe if package is explicitly None (TypeError).""" + meta = metadata.Metadata( + address=metadata.Address( + name="TestMessage", + package=None, + module="test", + ) + ) + message = make_message("TestMessage", meta=meta) + + actual = message._apply_domain_heuristic("ces.googleapis.com/Tool") + assert actual == "Tool" + + +def test_apply_domain_heuristic_missing_meta(): + """Test the EAFP failsafe if meta or address are missing (AttributeError).""" + # To test the AttributeError fallback cleanly without mocks, we can just + # pass a bare-bones Python class to the unbound method. This perfectly + # proves the try/except block safely handles totally missing attributes. + class EmptyMessage: + pass + + actual = wrappers.MessageType._apply_domain_heuristic(EmptyMessage(), "ces.googleapis.com/Tool") + assert actual == "Tool" diff --git a/packages/gapic-generator/tests/unit/schema/wrappers/test_service.py b/packages/gapic-generator/tests/unit/schema/wrappers/test_service.py index eb54577b2e51..04f7ac7308ea 100644 --- a/packages/gapic-generator/tests/unit/schema/wrappers/test_service.py +++ b/packages/gapic-generator/tests/unit/schema/wrappers/test_service.py @@ -267,12 +267,12 @@ def test_resource_messages(): ), ) - expected = { - squid_resource, + expected = ( clam_resource, - whelk_resource, squamosa_message, - } + squid_resource, + whelk_resource, + ) actual = service.resource_messages assert expected == actual @@ -557,7 +557,7 @@ def test_resource_response(): ), ) - expected = {squid_resource, clam_resource} + expected = (clam_resource, squid_resource) actual = mollusc_service.resource_messages assert expected == actual