From c44f77e2fc1aba2405b2847b2c2d1aea558b12f8 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Tue, 27 Oct 2026 11:12:41 -0400 Subject: [PATCH 1/7] fix(gapic-generator): default client_options.api_endpoint to transport host for OpenTelemetry spans --- .../services/%service/transports/base.py.j2 | 4 ++ .../%name_%version/%sub/test_%service.py.j2 | 11 +++++- .../services/asset_service/transports/base.py | 4 ++ .../unit/gapic/asset_v1/test_asset_service.py | 11 +++++- .../iam_credentials/transports/base.py | 4 ++ .../credentials_v1/test_iam_credentials.py | 11 +++++- .../services/eventarc/transports/base.py | 4 ++ .../unit/gapic/eventarc_v1/test_eventarc.py | 11 +++++- .../config_service_v2/transports/base.py | 4 ++ .../logging_service_v2/transports/base.py | 4 ++ .../metrics_service_v2/transports/base.py | 4 ++ .../logging_v2/test_config_service_v2.py | 11 +++++- .../logging_v2/test_logging_service_v2.py | 11 +++++- .../logging_v2/test_metrics_service_v2.py | 11 +++++- .../config_service_v2/transports/base.py | 4 ++ .../logging_service_v2/transports/base.py | 4 ++ .../metrics_service_v2/transports/base.py | 4 ++ .../logging_v2/test_config_service_v2.py | 11 +++++- .../logging_v2/test_logging_service_v2.py | 11 +++++- .../logging_v2/test_metrics_service_v2.py | 11 +++++- .../services/cloud_redis/transports/base.py | 4 ++ .../unit/gapic/redis_v1/test_cloud_redis.py | 11 +++++- .../services/cloud_redis/transports/base.py | 4 ++ .../unit/gapic/redis_v1/test_cloud_redis.py | 11 +++++- .../services/echo/transports/base.py | 4 ++ .../services/identity/transports/base.py | 4 ++ .../services/messaging/transports/base.py | 4 ++ .../transports/base.py | 4 ++ .../sequence_service/transports/base.py | 4 ++ .../services/testing/transports/base.py | 4 ++ .../unit/gapic/showcase_v1beta1/test_echo.py | 11 +++++- .../gapic/showcase_v1beta1/test_identity.py | 11 +++++- .../gapic/showcase_v1beta1/test_messaging.py | 11 +++++- .../test_resumable_upload_service.py | 11 +++++- .../showcase_v1beta1/test_sequence_service.py | 11 +++++- .../gapic/showcase_v1beta1/test_testing.py | 11 +++++- .../transports/base.py | 4 ++ .../test_storage_batch_operations.py | 11 +++++- .../system/telemetry_requirements_matrix.csv | 38 +++++++++---------- 39 files changed, 285 insertions(+), 38 deletions(-) diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 index 9c93aaea9c1d..c92f85240e71 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 @@ -162,6 +162,10 @@ class {{ service.name }}Transport(abc.ABC): host += ':443' self._host = host + if isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + client_options.api_endpoint = self._host + elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 index 136319c7ced0..b2caae9d084e 100644 --- a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 +++ b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 @@ -2489,9 +2489,18 @@ def test_{{ service.name|snake_case }}_base_transport_wrap_method_modern_api_cor # 2. Base transport: kind raises NotImplementedError and is cleanly omitted with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: + options = client_options.ClientOptions() base_transport = transports.{{ service.name }}Transport( - credentials=ga_credentials.AnonymousCredentials() + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert base_transport._client_options.api_endpoint == base_transport._host + dict_options = {} + dict_transport = transports.{{ service.name }}Transport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=dict_options, ) + assert dict_transport._client_options["api_endpoint"] == dict_transport._host func = mock.MagicMock() base_transport._wrap_method(func) assert "kind" not in mock_wrap.call_args.kwargs diff --git a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/base.py b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/base.py index 515c173224af..7ef7a0e4e885 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/base.py @@ -133,6 +133,10 @@ def __init__( host += ':443' self._host = host + if isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + client_options.api_endpoint = self._host + elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py b/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py index 17fc4c8a5c07..fd8a79e7aef9 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py +++ b/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py @@ -18424,9 +18424,18 @@ def test_asset_service_base_transport_wrap_method_modern_api_core(monkeypatch): # 2. Base transport: kind raises NotImplementedError and is cleanly omitted with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: + options = client_options.ClientOptions() base_transport = transports.AssetServiceTransport( - credentials=ga_credentials.AnonymousCredentials() + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert base_transport._client_options.api_endpoint == base_transport._host + dict_options = {} + dict_transport = transports.AssetServiceTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=dict_options, ) + assert dict_transport._client_options["api_endpoint"] == dict_transport._host func = mock.MagicMock() base_transport._wrap_method(func) assert "kind" not in mock_wrap.call_args.kwargs diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/base.py b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/base.py index 9ad2c974a10c..5c7092980c25 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/base.py @@ -130,6 +130,10 @@ def __init__( host += ':443' self._host = host + if isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + client_options.api_endpoint = self._host + elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py b/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py index f39f23ababfa..14e98a3fccd0 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py +++ b/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py @@ -4470,9 +4470,18 @@ def test_iam_credentials_base_transport_wrap_method_modern_api_core(monkeypatch) # 2. Base transport: kind raises NotImplementedError and is cleanly omitted with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: + options = client_options.ClientOptions() base_transport = transports.IAMCredentialsTransport( - credentials=ga_credentials.AnonymousCredentials() + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert base_transport._client_options.api_endpoint == base_transport._host + dict_options = {} + dict_transport = transports.IAMCredentialsTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=dict_options, ) + assert dict_transport._client_options["api_endpoint"] == dict_transport._host func = mock.MagicMock() base_transport._wrap_method(func) assert "kind" not in mock_wrap.call_args.kwargs diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/base.py b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/base.py index 963fc7ddb134..76dd62c0ebdc 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/base.py @@ -145,6 +145,10 @@ def __init__( host += ':443' self._host = host + if isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + client_options.api_endpoint = self._host + elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py b/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py index b5e055c25ec9..515a7282b27c 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py @@ -33527,9 +33527,18 @@ def test_eventarc_base_transport_wrap_method_modern_api_core(monkeypatch): # 2. Base transport: kind raises NotImplementedError and is cleanly omitted with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: + options = client_options.ClientOptions() base_transport = transports.EventarcTransport( - credentials=ga_credentials.AnonymousCredentials() + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert base_transport._client_options.api_endpoint == base_transport._host + dict_options = {} + dict_transport = transports.EventarcTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=dict_options, ) + assert dict_transport._client_options["api_endpoint"] == dict_transport._host func = mock.MagicMock() base_transport._wrap_method(func) assert "kind" not in mock_wrap.call_args.kwargs diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/base.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/base.py index 4a5dbf0b6ca1..c4e987f0a05f 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/base.py @@ -136,6 +136,10 @@ def __init__( host += ':443' self._host = host + if isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + client_options.api_endpoint = self._host + elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/base.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/base.py index 4f1882897e01..281bdfb4dcf9 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/base.py @@ -136,6 +136,10 @@ def __init__( host += ':443' self._host = host + if isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + client_options.api_endpoint = self._host + elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py index 98e6b8826af6..51d79ccd5212 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py @@ -136,6 +136,10 @@ def __init__( host += ':443' self._host = host + if isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + client_options.api_endpoint = self._host + elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py index acea90dd0a47..1ddf2acb85aa 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py @@ -14021,9 +14021,18 @@ def test_config_service_v2_base_transport_wrap_method_modern_api_core(monkeypatc # 2. Base transport: kind raises NotImplementedError and is cleanly omitted with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: + options = client_options.ClientOptions() base_transport = transports.ConfigServiceV2Transport( - credentials=ga_credentials.AnonymousCredentials() + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert base_transport._client_options.api_endpoint == base_transport._host + dict_options = {} + dict_transport = transports.ConfigServiceV2Transport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=dict_options, ) + assert dict_transport._client_options["api_endpoint"] == dict_transport._host func = mock.MagicMock() base_transport._wrap_method(func) assert "kind" not in mock_wrap.call_args.kwargs diff --git a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py index 433ff946bd8b..40d012fc2707 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py @@ -4461,9 +4461,18 @@ def test_logging_service_v2_base_transport_wrap_method_modern_api_core(monkeypat # 2. Base transport: kind raises NotImplementedError and is cleanly omitted with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: + options = client_options.ClientOptions() base_transport = transports.LoggingServiceV2Transport( - credentials=ga_credentials.AnonymousCredentials() + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert base_transport._client_options.api_endpoint == base_transport._host + dict_options = {} + dict_transport = transports.LoggingServiceV2Transport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=dict_options, ) + assert dict_transport._client_options["api_endpoint"] == dict_transport._host func = mock.MagicMock() base_transport._wrap_method(func) assert "kind" not in mock_wrap.call_args.kwargs diff --git a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py index c3809e05816a..9a0e9dee2119 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py @@ -4261,9 +4261,18 @@ def test_metrics_service_v2_base_transport_wrap_method_modern_api_core(monkeypat # 2. Base transport: kind raises NotImplementedError and is cleanly omitted with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: + options = client_options.ClientOptions() base_transport = transports.MetricsServiceV2Transport( - credentials=ga_credentials.AnonymousCredentials() + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert base_transport._client_options.api_endpoint == base_transport._host + dict_options = {} + dict_transport = transports.MetricsServiceV2Transport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=dict_options, ) + assert dict_transport._client_options["api_endpoint"] == dict_transport._host func = mock.MagicMock() base_transport._wrap_method(func) assert "kind" not in mock_wrap.call_args.kwargs diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/base.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/base.py index 4a5dbf0b6ca1..c4e987f0a05f 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/base.py @@ -136,6 +136,10 @@ def __init__( host += ':443' self._host = host + if isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + client_options.api_endpoint = self._host + elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/base.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/base.py index 4f1882897e01..281bdfb4dcf9 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/base.py @@ -136,6 +136,10 @@ def __init__( host += ':443' self._host = host + if isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + client_options.api_endpoint = self._host + elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py index 98e6b8826af6..51d79ccd5212 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py @@ -136,6 +136,10 @@ def __init__( host += ':443' self._host = host + if isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + client_options.api_endpoint = self._host + elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py index 5d7d971258ae..2f25f8e5fae4 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py @@ -14021,9 +14021,18 @@ def test_config_service_v2_base_transport_wrap_method_modern_api_core(monkeypatc # 2. Base transport: kind raises NotImplementedError and is cleanly omitted with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: + options = client_options.ClientOptions() base_transport = transports.ConfigServiceV2Transport( - credentials=ga_credentials.AnonymousCredentials() + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert base_transport._client_options.api_endpoint == base_transport._host + dict_options = {} + dict_transport = transports.ConfigServiceV2Transport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=dict_options, ) + assert dict_transport._client_options["api_endpoint"] == dict_transport._host func = mock.MagicMock() base_transport._wrap_method(func) assert "kind" not in mock_wrap.call_args.kwargs diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py index 433ff946bd8b..40d012fc2707 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py @@ -4461,9 +4461,18 @@ def test_logging_service_v2_base_transport_wrap_method_modern_api_core(monkeypat # 2. Base transport: kind raises NotImplementedError and is cleanly omitted with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: + options = client_options.ClientOptions() base_transport = transports.LoggingServiceV2Transport( - credentials=ga_credentials.AnonymousCredentials() + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert base_transport._client_options.api_endpoint == base_transport._host + dict_options = {} + dict_transport = transports.LoggingServiceV2Transport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=dict_options, ) + assert dict_transport._client_options["api_endpoint"] == dict_transport._host func = mock.MagicMock() base_transport._wrap_method(func) assert "kind" not in mock_wrap.call_args.kwargs diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py index 5fc12c22406b..ee51e53c7ac5 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py @@ -4261,9 +4261,18 @@ def test_metrics_service_v2_base_transport_wrap_method_modern_api_core(monkeypat # 2. Base transport: kind raises NotImplementedError and is cleanly omitted with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: + options = client_options.ClientOptions() base_transport = transports.MetricsServiceV2Transport( - credentials=ga_credentials.AnonymousCredentials() + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert base_transport._client_options.api_endpoint == base_transport._host + dict_options = {} + dict_transport = transports.MetricsServiceV2Transport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=dict_options, ) + assert dict_transport._client_options["api_endpoint"] == dict_transport._host func = mock.MagicMock() base_transport._wrap_method(func) assert "kind" not in mock_wrap.call_args.kwargs diff --git a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/base.py b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/base.py index 7164fc0bfe5f..7cfc53db8511 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/base.py @@ -133,6 +133,10 @@ def __init__( host += ':443' self._host = host + if isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + client_options.api_endpoint = self._host + elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py b/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py index e2215a295660..8bc3a47bb884 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py @@ -13315,9 +13315,18 @@ def test_cloud_redis_base_transport_wrap_method_modern_api_core(monkeypatch): # 2. Base transport: kind raises NotImplementedError and is cleanly omitted with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: + options = client_options.ClientOptions() base_transport = transports.CloudRedisTransport( - credentials=ga_credentials.AnonymousCredentials() + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert base_transport._client_options.api_endpoint == base_transport._host + dict_options = {} + dict_transport = transports.CloudRedisTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=dict_options, ) + assert dict_transport._client_options["api_endpoint"] == dict_transport._host func = mock.MagicMock() base_transport._wrap_method(func) assert "kind" not in mock_wrap.call_args.kwargs diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/base.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/base.py index da37256c19e4..f54e929dbabe 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/base.py @@ -133,6 +133,10 @@ def __init__( host += ':443' self._host = host + if isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + client_options.api_endpoint = self._host + elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py index 7b29386993ee..a37acdf612fd 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py @@ -8535,9 +8535,18 @@ def test_cloud_redis_base_transport_wrap_method_modern_api_core(monkeypatch): # 2. Base transport: kind raises NotImplementedError and is cleanly omitted with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: + options = client_options.ClientOptions() base_transport = transports.CloudRedisTransport( - credentials=ga_credentials.AnonymousCredentials() + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert base_transport._client_options.api_endpoint == base_transport._host + dict_options = {} + dict_transport = transports.CloudRedisTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=dict_options, ) + assert dict_transport._client_options["api_endpoint"] == dict_transport._host func = mock.MagicMock() base_transport._wrap_method(func) assert "kind" not in mock_wrap.call_args.kwargs diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/base.py index f5e34a9f9aa7..92a40305f088 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/base.py @@ -134,6 +134,10 @@ def __init__( host += ':443' self._host = host + if isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + client_options.api_endpoint = self._host + elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/base.py index 8f44d529f6dd..4b878b2d2ec8 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/base.py @@ -134,6 +134,10 @@ def __init__( host += ':443' self._host = host + if isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + client_options.api_endpoint = self._host + elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/base.py index c783a24ea331..80c282a05668 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/base.py @@ -135,6 +135,10 @@ def __init__( host += ':443' self._host = host + if isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + client_options.api_endpoint = self._host + elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/base.py index b9ca386076b2..a08b085bc2b0 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/base.py @@ -135,6 +135,10 @@ def __init__( host += ':443' self._host = host + if isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + client_options.api_endpoint = self._host + elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/base.py index f6b79c09fa5f..ebd1c47d1d1f 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/base.py @@ -135,6 +135,10 @@ def __init__( host += ':443' self._host = host + if isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + client_options.api_endpoint = self._host + elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/base.py index f0fc08e3ef55..dcc895574f0e 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/base.py @@ -134,6 +134,10 @@ def __init__( host += ':443' self._host = host + if isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + client_options.api_endpoint = self._host + elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_echo.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_echo.py index 526eb660d4ff..e30741c8bca0 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_echo.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_echo.py @@ -9794,9 +9794,18 @@ def test_echo_base_transport_wrap_method_modern_api_core(monkeypatch): # 2. Base transport: kind raises NotImplementedError and is cleanly omitted with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: + options = client_options.ClientOptions() base_transport = transports.EchoTransport( - credentials=ga_credentials.AnonymousCredentials() + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert base_transport._client_options.api_endpoint == base_transport._host + dict_options = {} + dict_transport = transports.EchoTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=dict_options, ) + assert dict_transport._client_options["api_endpoint"] == dict_transport._host func = mock.MagicMock() base_transport._wrap_method(func) assert "kind" not in mock_wrap.call_args.kwargs diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_identity.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_identity.py index 28a7dd420d23..e4898cf8a353 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_identity.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_identity.py @@ -6901,9 +6901,18 @@ def test_identity_base_transport_wrap_method_modern_api_core(monkeypatch): # 2. Base transport: kind raises NotImplementedError and is cleanly omitted with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: + options = client_options.ClientOptions() base_transport = transports.IdentityTransport( - credentials=ga_credentials.AnonymousCredentials() + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert base_transport._client_options.api_endpoint == base_transport._host + dict_options = {} + dict_transport = transports.IdentityTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=dict_options, ) + assert dict_transport._client_options["api_endpoint"] == dict_transport._host func = mock.MagicMock() base_transport._wrap_method(func) assert "kind" not in mock_wrap.call_args.kwargs diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_messaging.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_messaging.py index 84906c414aa2..cfbac6a4f059 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_messaging.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_messaging.py @@ -11801,9 +11801,18 @@ def test_messaging_base_transport_wrap_method_modern_api_core(monkeypatch): # 2. Base transport: kind raises NotImplementedError and is cleanly omitted with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: + options = client_options.ClientOptions() base_transport = transports.MessagingTransport( - credentials=ga_credentials.AnonymousCredentials() + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert base_transport._client_options.api_endpoint == base_transport._host + dict_options = {} + dict_transport = transports.MessagingTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=dict_options, ) + assert dict_transport._client_options["api_endpoint"] == dict_transport._host func = mock.MagicMock() base_transport._wrap_method(func) assert "kind" not in mock_wrap.call_args.kwargs diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py index 3d2f99ea1ef7..f710af316ca5 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py @@ -4494,9 +4494,18 @@ def test_resumable_upload_service_base_transport_wrap_method_modern_api_core(mon # 2. Base transport: kind raises NotImplementedError and is cleanly omitted with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: + options = client_options.ClientOptions() base_transport = transports.ResumableUploadServiceTransport( - credentials=ga_credentials.AnonymousCredentials() + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert base_transport._client_options.api_endpoint == base_transport._host + dict_options = {} + dict_transport = transports.ResumableUploadServiceTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=dict_options, ) + assert dict_transport._client_options["api_endpoint"] == dict_transport._host func = mock.MagicMock() base_transport._wrap_method(func) assert "kind" not in mock_wrap.call_args.kwargs diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_sequence_service.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_sequence_service.py index 444a6f91969a..8b7e3acf7c69 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_sequence_service.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_sequence_service.py @@ -7583,9 +7583,18 @@ def test_sequence_service_base_transport_wrap_method_modern_api_core(monkeypatch # 2. Base transport: kind raises NotImplementedError and is cleanly omitted with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: + options = client_options.ClientOptions() base_transport = transports.SequenceServiceTransport( - credentials=ga_credentials.AnonymousCredentials() + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert base_transport._client_options.api_endpoint == base_transport._host + dict_options = {} + dict_transport = transports.SequenceServiceTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=dict_options, ) + assert dict_transport._client_options["api_endpoint"] == dict_transport._host func = mock.MagicMock() base_transport._wrap_method(func) assert "kind" not in mock_wrap.call_args.kwargs diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_testing.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_testing.py index dbda59a14f09..3e51476de5ac 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_testing.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_testing.py @@ -7772,9 +7772,18 @@ def test_testing_base_transport_wrap_method_modern_api_core(monkeypatch): # 2. Base transport: kind raises NotImplementedError and is cleanly omitted with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: + options = client_options.ClientOptions() base_transport = transports.TestingTransport( - credentials=ga_credentials.AnonymousCredentials() + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert base_transport._client_options.api_endpoint == base_transport._host + dict_options = {} + dict_transport = transports.TestingTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=dict_options, ) + assert dict_transport._client_options["api_endpoint"] == dict_transport._host func = mock.MagicMock() base_transport._wrap_method(func) assert "kind" not in mock_wrap.call_args.kwargs diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/base.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/base.py index 5cf929454825..f84099367870 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/base.py @@ -135,6 +135,10 @@ def __init__( host += ':443' self._host = host + if isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + client_options.api_endpoint = self._host + elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py index c1f5ed51ac5c..0b74ccb93597 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py @@ -8471,9 +8471,18 @@ def test_storage_batch_operations_base_transport_wrap_method_modern_api_core(mon # 2. Base transport: kind raises NotImplementedError and is cleanly omitted with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: + options = client_options.ClientOptions() base_transport = transports.StorageBatchOperationsTransport( - credentials=ga_credentials.AnonymousCredentials() + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert base_transport._client_options.api_endpoint == base_transport._host + dict_options = {} + dict_transport = transports.StorageBatchOperationsTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=dict_options, ) + assert dict_transport._client_options["api_endpoint"] == dict_transport._host func = mock.MagicMock() base_transport._wrap_method(func) assert "kind" not in mock_wrap.call_args.kwargs diff --git a/packages/gapic-generator/tests/system/telemetry_requirements_matrix.csv b/packages/gapic-generator/tests/system/telemetry_requirements_matrix.csv index 6b52380bc2c6..9aa07baf72aa 100644 --- a/packages/gapic-generator/tests/system/telemetry_requirements_matrix.csv +++ b/packages/gapic-generator/tests/system/telemetry_requirements_matrix.csv @@ -6,28 +6,28 @@ F1.4,Client Timeout (HTTP),T4,0,HTTP/REST,Client Timeout,1,CLIENT,POST /v1beta1/ F1.5a,Retry Recovery - Attempt 1 (HTTP),T4,0,HTTP/REST,Retry Recovery,2,CLIENT,POST /v1beta1/{name=sequences/*},ERROR,http,NOT SET,NOT SET,POST,503,NOT SET,googleapis.com,/v1beta1/{name=sequences/*},localhost,7469,503,Temporary glitch,NOT SET,T3.span_id,url.full F1.5b,Retry Recovery - Attempt 2 (HTTP),T4,1,HTTP/REST,Retry Recovery,2,CLIENT,POST /v1beta1/{name=sequences/*},UNSET,http,NOT SET,NOT SET,POST,200,NOT SET,googleapis.com,/v1beta1/{name=sequences/*},localhost,7469,NOT SET,NOT SET,NOT SET,T3.span_id,url.full F1.6,Tracing Off (gRPC),T4,N/A,gRPC,Tracing Off,0,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,No spans emitted -F1.7,Unary Success (gRPC),T4,0,gRPC,Happy Path,1,CLIENT,google.showcase.v1beta1.Echo/Echo,UNSET,grpc,google.showcase.v1beta1.Echo/Echo,OK,NOT SET,NOT SET,0,googleapis.com,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,T3.span_id,"rpc.system, rpc.service" -F1.8,Server Error (gRPC),T4,0,gRPC,Server Failure,1,CLIENT,google.showcase.v1beta1.Echo/Echo,ERROR,grpc,google.showcase.v1beta1.Echo/Echo,NOT SET,NOT SET,NOT SET,3,googleapis.com,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,T3.span_id,"rpc.system, rpc.service" -F1.9,Client Timeout (gRPC),T4,0,gRPC,Client Timeout,1,CLIENT,google.showcase.v1beta1.Echo/Block,ERROR,grpc,google.showcase.v1beta1.Echo/Block,NOT SET,NOT SET,NOT SET,4,googleapis.com,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,T3.span_id,"rpc.system, rpc.service" -F1.10a,Retry Recovery - Attempt 1 (gRPC),T4,0,gRPC,Retry Recovery,2,CLIENT,google.showcase.v1beta1.SequenceService/AttemptSequence,ERROR,grpc,google.showcase.v1beta1.SequenceService/AttemptSequence,NOT SET,NOT SET,NOT SET,14,googleapis.com,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,T3.span_id,"rpc.system, rpc.service" -F1.10b,Retry Recovery - Attempt 2 (gRPC),T4,1,gRPC,Retry Recovery,2,CLIENT,google.showcase.v1beta1.SequenceService/AttemptSequence,UNSET,grpc,google.showcase.v1beta1.SequenceService/AttemptSequence,OK,NOT SET,NOT SET,0,googleapis.com,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,T3.span_id,"rpc.system, rpc.service" +F1.7,Unary Success (gRPC),T4,0,gRPC,Happy Path,1,CLIENT,google.showcase.v1beta1.Echo/Echo,UNSET,grpc,google.showcase.v1beta1.Echo/Echo,OK,NOT SET,NOT SET,0,googleapis.com,NOT SET,localhost,7469,NOT SET,NOT SET,NOT SET,T3.span_id,"rpc.system, rpc.service" +F1.8,Server Error (gRPC),T4,0,gRPC,Server Failure,1,CLIENT,google.showcase.v1beta1.Echo/Echo,ERROR,grpc,google.showcase.v1beta1.Echo/Echo,NOT SET,NOT SET,NOT SET,3,googleapis.com,NOT SET,localhost,7469,NOT SET,NOT SET,NOT SET,T3.span_id,"rpc.system, rpc.service" +F1.9,Client Timeout (gRPC),T4,0,gRPC,Client Timeout,1,CLIENT,google.showcase.v1beta1.Echo/Block,ERROR,grpc,google.showcase.v1beta1.Echo/Block,NOT SET,NOT SET,NOT SET,4,googleapis.com,NOT SET,localhost,7469,NOT SET,NOT SET,NOT SET,T3.span_id,"rpc.system, rpc.service" +F1.10a,Retry Recovery - Attempt 1 (gRPC),T4,0,gRPC,Retry Recovery,2,CLIENT,google.showcase.v1beta1.SequenceService/AttemptSequence,ERROR,grpc,google.showcase.v1beta1.SequenceService/AttemptSequence,NOT SET,NOT SET,NOT SET,14,googleapis.com,NOT SET,localhost,7469,NOT SET,NOT SET,NOT SET,T3.span_id,"rpc.system, rpc.service" +F1.10b,Retry Recovery - Attempt 2 (gRPC),T4,1,gRPC,Retry Recovery,2,CLIENT,google.showcase.v1beta1.SequenceService/AttemptSequence,UNSET,grpc,google.showcase.v1beta1.SequenceService/AttemptSequence,OK,NOT SET,NOT SET,0,googleapis.com,NOT SET,localhost,7469,NOT SET,NOT SET,NOT SET,T3.span_id,"rpc.system, rpc.service" F2.1,Tracing Off (HTTP),T3,N/A,HTTP/REST,Tracing Off,0,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,No spans emitted -F2.2,Unary Success (HTTP),T3,0,HTTP/REST,Happy Path,1,INTERNAL,google.showcase.v1beta1.Echo/Echo,UNSET,http,google.showcase.v1beta1.Echo/Echo,OK,NOT SET,200,NOT SET,googleapis.com,/v1beta1/echo:echo,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,None (Root),Strict ceiling (no unknown attrs) -F2.3,Server Error (HTTP),T3,0,HTTP/REST,Server Failure,1,INTERNAL,google.showcase.v1beta1.Echo/Echo,ERROR,http,google.showcase.v1beta1.Echo/Echo,BAD_REQUEST,NOT SET,400,NOT SET,googleapis.com,/v1beta1/echo:echo,NOT SET,NOT SET,400,Simulated unretryable invalid argument error.,NOT SET,None (Root),"gcp.errors.domain, gcp.errors.metadata.*" -F2.4,Client Timeout (HTTP),T3,0,HTTP/REST,Client Timeout,1,INTERNAL,google.showcase.v1beta1.Echo/Block,ERROR,http,google.showcase.v1beta1.Echo/Block,ReadTimeout,NOT SET,NOT SET,NOT SET,googleapis.com,/v1beta1/echo:block,NOT SET,NOT SET,ReadTimeout,"HTTPConnectionPool(host='localhost', port=7469): Read timed out. (read timeout=0.2)",NOT SET,None (Root),gcp.errors.domain +F2.2,Unary Success (HTTP),T3,0,HTTP/REST,Happy Path,1,INTERNAL,google.showcase.v1beta1.Echo/Echo,UNSET,http,google.showcase.v1beta1.Echo/Echo,OK,NOT SET,200,NOT SET,googleapis.com,/v1beta1/echo:echo,localhost,7469,NOT SET,NOT SET,NOT SET,None (Root),Strict ceiling (no unknown attrs) +F2.3,Server Error (HTTP),T3,0,HTTP/REST,Server Failure,1,INTERNAL,google.showcase.v1beta1.Echo/Echo,ERROR,http,google.showcase.v1beta1.Echo/Echo,BAD_REQUEST,NOT SET,400,NOT SET,googleapis.com,/v1beta1/echo:echo,localhost,7469,400,Simulated unretryable invalid argument error.,NOT SET,None (Root),"gcp.errors.domain, gcp.errors.metadata.*" +F2.4,Client Timeout (HTTP),T3,0,HTTP/REST,Client Timeout,1,INTERNAL,google.showcase.v1beta1.Echo/Block,ERROR,http,google.showcase.v1beta1.Echo/Block,ReadTimeout,NOT SET,NOT SET,NOT SET,googleapis.com,/v1beta1/echo:block,localhost,7469,ReadTimeout,"HTTPConnectionPool(host='localhost', port=7469): Read timed out. (read timeout=0.2)",NOT SET,None (Root),gcp.errors.domain F2.5,Tracing Off (gRPC),T3,N/A,gRPC,Tracing Off,0,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,No spans emitted -F2.6,Unary Success (gRPC),T3,0,gRPC,Happy Path,1,INTERNAL,google.showcase.v1beta1.Echo/Echo,UNSET,grpc,google.showcase.v1beta1.Echo/Echo,OK,NOT SET,NOT SET,NOT SET,googleapis.com,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,None (Root),Strict ceiling (no unknown attrs) -F2.7,Server Error (gRPC),T3,0,gRPC,Server Failure,1,INTERNAL,google.showcase.v1beta1.Echo/Echo,ERROR,grpc,google.showcase.v1beta1.Echo/Echo,INVALID_ARGUMENT,NOT SET,NOT SET,NOT SET,googleapis.com,NOT SET,NOT SET,NOT SET,INVALID_ARGUMENT,Simulated unretryable invalid argument error.,NOT SET,None (Root),"gcp.errors.domain, gcp.errors.metadata.*" -F2.8,Client Timeout (gRPC),T3,0,gRPC,Client Timeout,1,INTERNAL,google.showcase.v1beta1.Echo/Block,ERROR,grpc,google.showcase.v1beta1.Echo/Block,DEADLINE_EXCEEDED,NOT SET,NOT SET,NOT SET,googleapis.com,NOT SET,NOT SET,NOT SET,DEADLINE_EXCEEDED,Deadline Exceeded,NOT SET,None (Root),gcp.errors.domain -F3.1a,Retry Succeeds - Root (HTTP),T3,0,HTTP/REST,Retry Recovery,1,INTERNAL,google.showcase.v1beta1.SequenceService/AttemptSequence,UNSET,http,google.showcase.v1beta1.SequenceService/AttemptSequence,OK,NOT SET,200,NOT SET,googleapis.com,/v1beta1/{name=sequences/*},NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,None (Root),Parent aggregates OK; T4 children nested under T3 +F2.6,Unary Success (gRPC),T3,0,gRPC,Happy Path,1,INTERNAL,google.showcase.v1beta1.Echo/Echo,UNSET,grpc,google.showcase.v1beta1.Echo/Echo,OK,NOT SET,NOT SET,NOT SET,googleapis.com,NOT SET,localhost,7469,NOT SET,NOT SET,NOT SET,None (Root),Strict ceiling (no unknown attrs) +F2.7,Server Error (gRPC),T3,0,gRPC,Server Failure,1,INTERNAL,google.showcase.v1beta1.Echo/Echo,ERROR,grpc,google.showcase.v1beta1.Echo/Echo,INVALID_ARGUMENT,NOT SET,NOT SET,NOT SET,googleapis.com,NOT SET,localhost,7469,INVALID_ARGUMENT,Simulated unretryable invalid argument error.,NOT SET,None (Root),"gcp.errors.domain, gcp.errors.metadata.*" +F2.8,Client Timeout (gRPC),T3,0,gRPC,Client Timeout,1,INTERNAL,google.showcase.v1beta1.Echo/Block,ERROR,grpc,google.showcase.v1beta1.Echo/Block,DEADLINE_EXCEEDED,NOT SET,NOT SET,NOT SET,googleapis.com,NOT SET,localhost,7469,DEADLINE_EXCEEDED,Deadline Exceeded,NOT SET,None (Root),gcp.errors.domain +F3.1a,Retry Succeeds - Root (HTTP),T3,0,HTTP/REST,Retry Recovery,1,INTERNAL,google.showcase.v1beta1.SequenceService/AttemptSequence,UNSET,http,google.showcase.v1beta1.SequenceService/AttemptSequence,OK,NOT SET,200,NOT SET,googleapis.com,/v1beta1/{name=sequences/*},localhost,7469,NOT SET,NOT SET,NOT SET,None (Root),Parent aggregates OK; T4 children nested under T3 F3.1b,Retry Succeeds - Attempt 1 (HTTP),T4,0,HTTP/REST,Retry Recovery,2,CLIENT,POST /v1beta1/{name=sequences/*},ERROR,http,NOT SET,NOT SET,POST,503,NOT SET,googleapis.com,/v1beta1/{name=sequences/*},localhost,7469,503,Temporary glitch,NOT SET,T3.span_id,Parent aggregates OK; T4 children nested under T3 F3.1c,Retry Succeeds - Attempt 2 (HTTP),T4,1,HTTP/REST,Retry Recovery,2,CLIENT,POST /v1beta1/{name=sequences/*},UNSET,http,NOT SET,NOT SET,POST,200,NOT SET,googleapis.com,/v1beta1/{name=sequences/*},localhost,7469,NOT SET,NOT SET,NOT SET,T3.span_id,Parent aggregates OK; T4 children nested under T3 -F3.2a,Retries Exhausted - Root (HTTP),T3,0,HTTP/REST,Retries Exhausted,1,INTERNAL,google.showcase.v1beta1.SequenceService/AttemptSequence,ERROR,http,google.showcase.v1beta1.SequenceService/AttemptSequence,UNAVAILABLE,NOT SET,503,NOT SET,googleapis.com,/v1beta1/{name=sequences/*},NOT SET,NOT SET,503,Persistent outage,NOT SET,None (Root),Parent aggregates UNAVAILABLE; all T4 children nested under T3 +F3.2a,Retries Exhausted - Root (HTTP),T3,0,HTTP/REST,Retries Exhausted,1,INTERNAL,google.showcase.v1beta1.SequenceService/AttemptSequence,ERROR,http,google.showcase.v1beta1.SequenceService/AttemptSequence,UNAVAILABLE,NOT SET,503,NOT SET,googleapis.com,/v1beta1/{name=sequences/*},localhost,7469,503,Persistent outage,NOT SET,None (Root),Parent aggregates UNAVAILABLE; all T4 children nested under T3 F3.2b,Retries Exhausted - Attempt 1 (HTTP),T4,0,HTTP/REST,Retries Exhausted,2,CLIENT,POST /v1beta1/{name=sequences/*},ERROR,http,NOT SET,NOT SET,POST,503,NOT SET,googleapis.com,/v1beta1/{name=sequences/*},localhost,7469,503,Persistent outage,NOT SET,T3.span_id,Parent aggregates UNAVAILABLE; all T4 children nested under T3 F3.2c,Retries Exhausted - Attempt 2 (HTTP),T4,1,HTTP/REST,Retries Exhausted,2,CLIENT,POST /v1beta1/{name=sequences/*},ERROR,http,NOT SET,NOT SET,POST,503,NOT SET,googleapis.com,/v1beta1/{name=sequences/*},localhost,7469,503,Persistent outage,NOT SET,T3.span_id,Parent aggregates UNAVAILABLE; all T4 children nested under T3 -F3.3a,Retry Succeeds - Root (gRPC),T3,0,gRPC,Retry Recovery,1,INTERNAL,google.showcase.v1beta1.SequenceService/AttemptSequence,UNSET,grpc,google.showcase.v1beta1.SequenceService/AttemptSequence,OK,NOT SET,NOT SET,NOT SET,googleapis.com,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,None (Root),Parent aggregates OK; T4 children nested under T3 -F3.3b,Retry Succeeds - Attempt 1 (gRPC),T4,0,gRPC,Retry Recovery,2,CLIENT,google.showcase.v1beta1.SequenceService/AttemptSequence,ERROR,grpc,google.showcase.v1beta1.SequenceService/AttemptSequence,NOT SET,NOT SET,NOT SET,14,googleapis.com,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,T3.span_id,Parent aggregates OK; T4 children nested under T3 -F3.3c,Retry Succeeds - Attempt 2 (gRPC),T4,1,gRPC,Retry Recovery,2,CLIENT,google.showcase.v1beta1.SequenceService/AttemptSequence,UNSET,grpc,google.showcase.v1beta1.SequenceService/AttemptSequence,OK,NOT SET,NOT SET,0,googleapis.com,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,T3.span_id,Parent aggregates OK; T4 children nested under T3 -F3.4a,Retries Exhausted - Root (gRPC),T3,0,gRPC,Retries Exhausted,1,INTERNAL,google.showcase.v1beta1.SequenceService/AttemptSequence,ERROR,grpc,google.showcase.v1beta1.SequenceService/AttemptSequence,UNAVAILABLE,NOT SET,NOT SET,NOT SET,googleapis.com,NOT SET,NOT SET,NOT SET,UNAVAILABLE,Persistent outage,NOT SET,None (Root),Parent aggregates UNAVAILABLE; all T4 children nested under T3 -F3.4b,Retries Exhausted - Attempt 1 (gRPC),T4,0,gRPC,Retries Exhausted,2,CLIENT,google.showcase.v1beta1.SequenceService/AttemptSequence,ERROR,grpc,google.showcase.v1beta1.SequenceService/AttemptSequence,NOT SET,NOT SET,NOT SET,14,googleapis.com,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,T3.span_id,Parent aggregates UNAVAILABLE; all T4 children nested under T3 -F3.4c,Retries Exhausted - Attempt 2 (gRPC),T4,1,gRPC,Retries Exhausted,2,CLIENT,google.showcase.v1beta1.SequenceService/AttemptSequence,ERROR,grpc,google.showcase.v1beta1.SequenceService/AttemptSequence,NOT SET,NOT SET,NOT SET,14,googleapis.com,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,T3.span_id,Parent aggregates UNAVAILABLE; all T4 children nested under T3 +F3.3a,Retry Succeeds - Root (gRPC),T3,0,gRPC,Retry Recovery,1,INTERNAL,google.showcase.v1beta1.SequenceService/AttemptSequence,UNSET,grpc,google.showcase.v1beta1.SequenceService/AttemptSequence,OK,NOT SET,NOT SET,NOT SET,googleapis.com,NOT SET,localhost,7469,NOT SET,NOT SET,NOT SET,None (Root),Parent aggregates OK; T4 children nested under T3 +F3.3b,Retry Succeeds - Attempt 1 (gRPC),T4,0,gRPC,Retry Recovery,2,CLIENT,google.showcase.v1beta1.SequenceService/AttemptSequence,ERROR,grpc,google.showcase.v1beta1.SequenceService/AttemptSequence,NOT SET,NOT SET,NOT SET,14,googleapis.com,NOT SET,localhost,7469,NOT SET,NOT SET,NOT SET,T3.span_id,Parent aggregates OK; T4 children nested under T3 +F3.3c,Retry Succeeds - Attempt 2 (gRPC),T4,1,gRPC,Retry Recovery,2,CLIENT,google.showcase.v1beta1.SequenceService/AttemptSequence,UNSET,grpc,google.showcase.v1beta1.SequenceService/AttemptSequence,OK,NOT SET,NOT SET,0,googleapis.com,NOT SET,localhost,7469,NOT SET,NOT SET,NOT SET,T3.span_id,Parent aggregates OK; T4 children nested under T3 +F3.4a,Retries Exhausted - Root (gRPC),T3,0,gRPC,Retries Exhausted,1,INTERNAL,google.showcase.v1beta1.SequenceService/AttemptSequence,ERROR,grpc,google.showcase.v1beta1.SequenceService/AttemptSequence,UNAVAILABLE,NOT SET,NOT SET,NOT SET,googleapis.com,NOT SET,localhost,7469,UNAVAILABLE,Persistent outage,NOT SET,None (Root),Parent aggregates UNAVAILABLE; all T4 children nested under T3 +F3.4b,Retries Exhausted - Attempt 1 (gRPC),T4,0,gRPC,Retries Exhausted,2,CLIENT,google.showcase.v1beta1.SequenceService/AttemptSequence,ERROR,grpc,google.showcase.v1beta1.SequenceService/AttemptSequence,NOT SET,NOT SET,NOT SET,14,googleapis.com,NOT SET,localhost,7469,NOT SET,NOT SET,NOT SET,T3.span_id,Parent aggregates UNAVAILABLE; all T4 children nested under T3 +F3.4c,Retries Exhausted - Attempt 2 (gRPC),T4,1,gRPC,Retries Exhausted,2,CLIENT,google.showcase.v1beta1.SequenceService/AttemptSequence,ERROR,grpc,google.showcase.v1beta1.SequenceService/AttemptSequence,NOT SET,NOT SET,NOT SET,14,googleapis.com,NOT SET,localhost,7469,NOT SET,NOT SET,NOT SET,T3.span_id,Parent aggregates UNAVAILABLE; all T4 children nested under T3 From d7cc0472885f5c5ce17f9a48af13445194672a2a Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Fri, 30 Oct 2026 10:37:05 -0400 Subject: [PATCH 2/7] fix(gapic-generator): shallow-copy client_options and handle None when OTel enabled --- .../services/%service/transports/base.py.j2 | 18 +++- .../%name_%version/%sub/test_%service.py.j2 | 85 ++++++++++++++++--- .../services/asset_service/transports/base.py | 18 +++- .../unit/gapic/asset_v1/test_asset_service.py | 85 ++++++++++++++++--- .../iam_credentials/transports/base.py | 18 +++- .../credentials_v1/test_iam_credentials.py | 85 ++++++++++++++++--- .../services/eventarc/transports/base.py | 18 +++- .../unit/gapic/eventarc_v1/test_eventarc.py | 85 ++++++++++++++++--- .../config_service_v2/transports/base.py | 18 +++- .../logging_service_v2/transports/base.py | 18 +++- .../metrics_service_v2/transports/base.py | 18 +++- .../logging_v2/test_config_service_v2.py | 85 ++++++++++++++++--- .../logging_v2/test_logging_service_v2.py | 85 ++++++++++++++++--- .../logging_v2/test_metrics_service_v2.py | 85 ++++++++++++++++--- .../config_service_v2/transports/base.py | 18 +++- .../logging_service_v2/transports/base.py | 18 +++- .../metrics_service_v2/transports/base.py | 18 +++- .../logging_v2/test_config_service_v2.py | 85 ++++++++++++++++--- .../logging_v2/test_logging_service_v2.py | 85 ++++++++++++++++--- .../logging_v2/test_metrics_service_v2.py | 85 ++++++++++++++++--- .../services/cloud_redis/transports/base.py | 18 +++- .../unit/gapic/redis_v1/test_cloud_redis.py | 85 ++++++++++++++++--- .../services/cloud_redis/transports/base.py | 18 +++- .../unit/gapic/redis_v1/test_cloud_redis.py | 85 ++++++++++++++++--- .../services/echo/transports/base.py | 18 +++- .../services/identity/transports/base.py | 18 +++- .../services/messaging/transports/base.py | 18 +++- .../transports/base.py | 18 +++- .../sequence_service/transports/base.py | 18 +++- .../services/testing/transports/base.py | 18 +++- .../unit/gapic/showcase_v1beta1/test_echo.py | 85 ++++++++++++++++--- .../gapic/showcase_v1beta1/test_identity.py | 85 ++++++++++++++++--- .../gapic/showcase_v1beta1/test_messaging.py | 85 ++++++++++++++++--- .../test_resumable_upload_service.py | 85 ++++++++++++++++--- .../showcase_v1beta1/test_sequence_service.py | 85 ++++++++++++++++--- .../gapic/showcase_v1beta1/test_testing.py | 85 ++++++++++++++++--- .../transports/base.py | 18 +++- .../test_storage_batch_operations.py | 85 ++++++++++++++++--- 38 files changed, 1748 insertions(+), 209 deletions(-) diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 index c92f85240e71..9546b58ca1fb 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 @@ -3,11 +3,13 @@ {% block content %} import abc +import copy from typing import {% if service.any_extended_operations_methods %}Any, {% endif %}Awaitable, Callable, Dict, Optional, Sequence, Union {% set package_path = api.naming.module_namespace|join('.') + "." + api.naming.versioned_module_name %} from {{package_path}} import gapic_version as package_version from {{package_path}}._compat import ( + _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) @@ -162,9 +164,23 @@ class {{ service.name }}Transport(abc.ABC): host += ':443' self._host = host - if isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + # If the transport was instantiated directly with client_options=None + # while OpenTelemetry tracing is enabled via environment variable, + # create a ClientOptions instance with the resolved transport host so + # method and wire spans can record server.address and server.port. + if ( + client_options is None + and _observability is not None + and _observability.is_otel_capabilities_enabled(None) + ): + client_options = client_options_lib.ClientOptions(api_endpoint=self._host) + # And here we check for the presence of an api_endpoint attr OR key, + # shallow-copying before populating to avoid mutating caller-owned state. + elif isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + client_options = copy.copy(client_options) client_options.api_endpoint = self._host elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options = copy.copy(client_options) client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 index b2caae9d084e..04d4d104620d 100644 --- a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 +++ b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 @@ -2489,24 +2489,89 @@ def test_{{ service.name|snake_case }}_base_transport_wrap_method_modern_api_cor # 2. Base transport: kind raises NotImplementedError and is cleanly omitted with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - options = client_options.ClientOptions() base_transport = transports.{{ service.name }}Transport( - credentials=ga_credentials.AnonymousCredentials(), - client_options=options, - ) - assert base_transport._client_options.api_endpoint == base_transport._host - dict_options = {} - dict_transport = transports.{{ service.name }}Transport( - credentials=ga_credentials.AnonymousCredentials(), - client_options=dict_options, + credentials=ga_credentials.AnonymousCredentials() ) - assert dict_transport._client_options["api_endpoint"] == dict_transport._host func = mock.MagicMock() base_transport._wrap_method(func) assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs +@pytest.mark.parametrize( + "otel_enabled,expected_endpoint", + [ + pytest.param(False, None, id="otel_disabled"), + pytest.param(True, "default.example.com:443", id="otel_enabled"), + ], +) +def test_{{ service.name|snake_case }}_base_transport_client_options_none( + monkeypatch, otel_enabled, expected_endpoint +): + fake_observability = mock.MagicMock() + fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled + monkeypatch.setattr(transports.base, "_observability", fake_observability) + + transport = transports.{{ service.name }}Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=None, + ) + assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + + +@pytest.mark.parametrize( + "initial_endpoint,expected_endpoint", + [ + pytest.param(None, "default.example.com:443", id="default_from_host"), + pytest.param( + "custom.example.com:443", + "custom.example.com:443", + id="preserve_explicit", + ), + ], +) +def test_{{ service.name|snake_case }}_base_transport_client_options_object( + initial_endpoint, expected_endpoint +): + options = client_options.ClientOptions(api_endpoint=initial_endpoint) + transport = transports.{{ service.name }}Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options.api_endpoint == expected_endpoint + assert options.api_endpoint == initial_endpoint + + +@pytest.mark.parametrize( + "initial_options,expected_options", + [ + pytest.param( + {}, + {"api_endpoint": "default.example.com:443"}, + id="default_from_host", + ), + pytest.param( + {"api_endpoint": "custom.example.com:443"}, + {"api_endpoint": "custom.example.com:443"}, + id="preserve_explicit", + ), + ], +) +def test_{{ service.name|snake_case }}_base_transport_client_options_dict( + initial_options, expected_options +): + options = dict(initial_options) + transport = transports.{{ service.name }}Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options == expected_options + assert options == initial_options + + def test_{{ service.name|snake_case }}_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): diff --git a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/base.py b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/base.py index 7ef7a0e4e885..a40ac40c6553 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/base.py @@ -14,10 +14,12 @@ # limitations under the License. # import abc +import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.asset_v1 import gapic_version as package_version from google.cloud.asset_v1._compat import ( + _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) @@ -133,9 +135,23 @@ def __init__( host += ':443' self._host = host - if isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + # If the transport was instantiated directly with client_options=None + # while OpenTelemetry tracing is enabled via environment variable, + # create a ClientOptions instance with the resolved transport host so + # method and wire spans can record server.address and server.port. + if ( + client_options is None + and _observability is not None + and _observability.is_otel_capabilities_enabled(None) + ): + client_options = client_options_lib.ClientOptions(api_endpoint=self._host) + # And here we check for the presence of an api_endpoint attr OR key, + # shallow-copying before populating to avoid mutating caller-owned state. + elif isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + client_options = copy.copy(client_options) client_options.api_endpoint = self._host elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options = copy.copy(client_options) client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py b/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py index fd8a79e7aef9..c34dd1104a94 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py +++ b/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py @@ -18424,24 +18424,89 @@ def test_asset_service_base_transport_wrap_method_modern_api_core(monkeypatch): # 2. Base transport: kind raises NotImplementedError and is cleanly omitted with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - options = client_options.ClientOptions() base_transport = transports.AssetServiceTransport( - credentials=ga_credentials.AnonymousCredentials(), - client_options=options, - ) - assert base_transport._client_options.api_endpoint == base_transport._host - dict_options = {} - dict_transport = transports.AssetServiceTransport( - credentials=ga_credentials.AnonymousCredentials(), - client_options=dict_options, + credentials=ga_credentials.AnonymousCredentials() ) - assert dict_transport._client_options["api_endpoint"] == dict_transport._host func = mock.MagicMock() base_transport._wrap_method(func) assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs +@pytest.mark.parametrize( + "otel_enabled,expected_endpoint", + [ + pytest.param(False, None, id="otel_disabled"), + pytest.param(True, "default.example.com:443", id="otel_enabled"), + ], +) +def test_asset_service_base_transport_client_options_none( + monkeypatch, otel_enabled, expected_endpoint +): + fake_observability = mock.MagicMock() + fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled + monkeypatch.setattr(transports.base, "_observability", fake_observability) + + transport = transports.AssetServiceTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=None, + ) + assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + + +@pytest.mark.parametrize( + "initial_endpoint,expected_endpoint", + [ + pytest.param(None, "default.example.com:443", id="default_from_host"), + pytest.param( + "custom.example.com:443", + "custom.example.com:443", + id="preserve_explicit", + ), + ], +) +def test_asset_service_base_transport_client_options_object( + initial_endpoint, expected_endpoint +): + options = client_options.ClientOptions(api_endpoint=initial_endpoint) + transport = transports.AssetServiceTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options.api_endpoint == expected_endpoint + assert options.api_endpoint == initial_endpoint + + +@pytest.mark.parametrize( + "initial_options,expected_options", + [ + pytest.param( + {}, + {"api_endpoint": "default.example.com:443"}, + id="default_from_host", + ), + pytest.param( + {"api_endpoint": "custom.example.com:443"}, + {"api_endpoint": "custom.example.com:443"}, + id="preserve_explicit", + ), + ], +) +def test_asset_service_base_transport_client_options_dict( + initial_options, expected_options +): + options = dict(initial_options) + transport = transports.AssetServiceTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options == expected_options + assert options == initial_options + + def test_asset_service_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/base.py b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/base.py index 5c7092980c25..39d1b8bf9763 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/base.py @@ -14,10 +14,12 @@ # limitations under the License. # import abc +import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.iam.credentials_v1 import gapic_version as package_version from google.iam.credentials_v1._compat import ( + _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) @@ -130,9 +132,23 @@ def __init__( host += ':443' self._host = host - if isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + # If the transport was instantiated directly with client_options=None + # while OpenTelemetry tracing is enabled via environment variable, + # create a ClientOptions instance with the resolved transport host so + # method and wire spans can record server.address and server.port. + if ( + client_options is None + and _observability is not None + and _observability.is_otel_capabilities_enabled(None) + ): + client_options = client_options_lib.ClientOptions(api_endpoint=self._host) + # And here we check for the presence of an api_endpoint attr OR key, + # shallow-copying before populating to avoid mutating caller-owned state. + elif isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + client_options = copy.copy(client_options) client_options.api_endpoint = self._host elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options = copy.copy(client_options) client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py b/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py index 14e98a3fccd0..752a9b818f79 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py +++ b/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py @@ -4470,24 +4470,89 @@ def test_iam_credentials_base_transport_wrap_method_modern_api_core(monkeypatch) # 2. Base transport: kind raises NotImplementedError and is cleanly omitted with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - options = client_options.ClientOptions() base_transport = transports.IAMCredentialsTransport( - credentials=ga_credentials.AnonymousCredentials(), - client_options=options, - ) - assert base_transport._client_options.api_endpoint == base_transport._host - dict_options = {} - dict_transport = transports.IAMCredentialsTransport( - credentials=ga_credentials.AnonymousCredentials(), - client_options=dict_options, + credentials=ga_credentials.AnonymousCredentials() ) - assert dict_transport._client_options["api_endpoint"] == dict_transport._host func = mock.MagicMock() base_transport._wrap_method(func) assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs +@pytest.mark.parametrize( + "otel_enabled,expected_endpoint", + [ + pytest.param(False, None, id="otel_disabled"), + pytest.param(True, "default.example.com:443", id="otel_enabled"), + ], +) +def test_iam_credentials_base_transport_client_options_none( + monkeypatch, otel_enabled, expected_endpoint +): + fake_observability = mock.MagicMock() + fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled + monkeypatch.setattr(transports.base, "_observability", fake_observability) + + transport = transports.IAMCredentialsTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=None, + ) + assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + + +@pytest.mark.parametrize( + "initial_endpoint,expected_endpoint", + [ + pytest.param(None, "default.example.com:443", id="default_from_host"), + pytest.param( + "custom.example.com:443", + "custom.example.com:443", + id="preserve_explicit", + ), + ], +) +def test_iam_credentials_base_transport_client_options_object( + initial_endpoint, expected_endpoint +): + options = client_options.ClientOptions(api_endpoint=initial_endpoint) + transport = transports.IAMCredentialsTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options.api_endpoint == expected_endpoint + assert options.api_endpoint == initial_endpoint + + +@pytest.mark.parametrize( + "initial_options,expected_options", + [ + pytest.param( + {}, + {"api_endpoint": "default.example.com:443"}, + id="default_from_host", + ), + pytest.param( + {"api_endpoint": "custom.example.com:443"}, + {"api_endpoint": "custom.example.com:443"}, + id="preserve_explicit", + ), + ], +) +def test_iam_credentials_base_transport_client_options_dict( + initial_options, expected_options +): + options = dict(initial_options) + transport = transports.IAMCredentialsTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options == expected_options + assert options == initial_options + + def test_iam_credentials_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/base.py b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/base.py index 76dd62c0ebdc..e4a5b3ecae76 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/base.py @@ -14,10 +14,12 @@ # limitations under the License. # import abc +import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.eventarc_v1 import gapic_version as package_version from google.cloud.eventarc_v1._compat import ( + _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) @@ -145,9 +147,23 @@ def __init__( host += ':443' self._host = host - if isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + # If the transport was instantiated directly with client_options=None + # while OpenTelemetry tracing is enabled via environment variable, + # create a ClientOptions instance with the resolved transport host so + # method and wire spans can record server.address and server.port. + if ( + client_options is None + and _observability is not None + and _observability.is_otel_capabilities_enabled(None) + ): + client_options = client_options_lib.ClientOptions(api_endpoint=self._host) + # And here we check for the presence of an api_endpoint attr OR key, + # shallow-copying before populating to avoid mutating caller-owned state. + elif isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + client_options = copy.copy(client_options) client_options.api_endpoint = self._host elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options = copy.copy(client_options) client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py b/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py index 515a7282b27c..c6faa631319b 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py @@ -33527,24 +33527,89 @@ def test_eventarc_base_transport_wrap_method_modern_api_core(monkeypatch): # 2. Base transport: kind raises NotImplementedError and is cleanly omitted with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - options = client_options.ClientOptions() base_transport = transports.EventarcTransport( - credentials=ga_credentials.AnonymousCredentials(), - client_options=options, - ) - assert base_transport._client_options.api_endpoint == base_transport._host - dict_options = {} - dict_transport = transports.EventarcTransport( - credentials=ga_credentials.AnonymousCredentials(), - client_options=dict_options, + credentials=ga_credentials.AnonymousCredentials() ) - assert dict_transport._client_options["api_endpoint"] == dict_transport._host func = mock.MagicMock() base_transport._wrap_method(func) assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs +@pytest.mark.parametrize( + "otel_enabled,expected_endpoint", + [ + pytest.param(False, None, id="otel_disabled"), + pytest.param(True, "default.example.com:443", id="otel_enabled"), + ], +) +def test_eventarc_base_transport_client_options_none( + monkeypatch, otel_enabled, expected_endpoint +): + fake_observability = mock.MagicMock() + fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled + monkeypatch.setattr(transports.base, "_observability", fake_observability) + + transport = transports.EventarcTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=None, + ) + assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + + +@pytest.mark.parametrize( + "initial_endpoint,expected_endpoint", + [ + pytest.param(None, "default.example.com:443", id="default_from_host"), + pytest.param( + "custom.example.com:443", + "custom.example.com:443", + id="preserve_explicit", + ), + ], +) +def test_eventarc_base_transport_client_options_object( + initial_endpoint, expected_endpoint +): + options = client_options.ClientOptions(api_endpoint=initial_endpoint) + transport = transports.EventarcTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options.api_endpoint == expected_endpoint + assert options.api_endpoint == initial_endpoint + + +@pytest.mark.parametrize( + "initial_options,expected_options", + [ + pytest.param( + {}, + {"api_endpoint": "default.example.com:443"}, + id="default_from_host", + ), + pytest.param( + {"api_endpoint": "custom.example.com:443"}, + {"api_endpoint": "custom.example.com:443"}, + id="preserve_explicit", + ), + ], +) +def test_eventarc_base_transport_client_options_dict( + initial_options, expected_options +): + options = dict(initial_options) + transport = transports.EventarcTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options == expected_options + assert options == initial_options + + def test_eventarc_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/base.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/base.py index c4e987f0a05f..cb1732a8c974 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/base.py @@ -14,10 +14,12 @@ # limitations under the License. # import abc +import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.logging_v2 import gapic_version as package_version from google.cloud.logging_v2._compat import ( + _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) @@ -136,9 +138,23 @@ def __init__( host += ':443' self._host = host - if isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + # If the transport was instantiated directly with client_options=None + # while OpenTelemetry tracing is enabled via environment variable, + # create a ClientOptions instance with the resolved transport host so + # method and wire spans can record server.address and server.port. + if ( + client_options is None + and _observability is not None + and _observability.is_otel_capabilities_enabled(None) + ): + client_options = client_options_lib.ClientOptions(api_endpoint=self._host) + # And here we check for the presence of an api_endpoint attr OR key, + # shallow-copying before populating to avoid mutating caller-owned state. + elif isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + client_options = copy.copy(client_options) client_options.api_endpoint = self._host elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options = copy.copy(client_options) client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/base.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/base.py index 281bdfb4dcf9..c0de6b00c70b 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/base.py @@ -14,10 +14,12 @@ # limitations under the License. # import abc +import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.logging_v2 import gapic_version as package_version from google.cloud.logging_v2._compat import ( + _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) @@ -136,9 +138,23 @@ def __init__( host += ':443' self._host = host - if isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + # If the transport was instantiated directly with client_options=None + # while OpenTelemetry tracing is enabled via environment variable, + # create a ClientOptions instance with the resolved transport host so + # method and wire spans can record server.address and server.port. + if ( + client_options is None + and _observability is not None + and _observability.is_otel_capabilities_enabled(None) + ): + client_options = client_options_lib.ClientOptions(api_endpoint=self._host) + # And here we check for the presence of an api_endpoint attr OR key, + # shallow-copying before populating to avoid mutating caller-owned state. + elif isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + client_options = copy.copy(client_options) client_options.api_endpoint = self._host elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options = copy.copy(client_options) client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py index 51d79ccd5212..630294a64d2e 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py @@ -14,10 +14,12 @@ # limitations under the License. # import abc +import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.logging_v2 import gapic_version as package_version from google.cloud.logging_v2._compat import ( + _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) @@ -136,9 +138,23 @@ def __init__( host += ':443' self._host = host - if isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + # If the transport was instantiated directly with client_options=None + # while OpenTelemetry tracing is enabled via environment variable, + # create a ClientOptions instance with the resolved transport host so + # method and wire spans can record server.address and server.port. + if ( + client_options is None + and _observability is not None + and _observability.is_otel_capabilities_enabled(None) + ): + client_options = client_options_lib.ClientOptions(api_endpoint=self._host) + # And here we check for the presence of an api_endpoint attr OR key, + # shallow-copying before populating to avoid mutating caller-owned state. + elif isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + client_options = copy.copy(client_options) client_options.api_endpoint = self._host elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options = copy.copy(client_options) client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py index 1ddf2acb85aa..a8eff4f6e0f9 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py @@ -14021,24 +14021,89 @@ def test_config_service_v2_base_transport_wrap_method_modern_api_core(monkeypatc # 2. Base transport: kind raises NotImplementedError and is cleanly omitted with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - options = client_options.ClientOptions() base_transport = transports.ConfigServiceV2Transport( - credentials=ga_credentials.AnonymousCredentials(), - client_options=options, - ) - assert base_transport._client_options.api_endpoint == base_transport._host - dict_options = {} - dict_transport = transports.ConfigServiceV2Transport( - credentials=ga_credentials.AnonymousCredentials(), - client_options=dict_options, + credentials=ga_credentials.AnonymousCredentials() ) - assert dict_transport._client_options["api_endpoint"] == dict_transport._host func = mock.MagicMock() base_transport._wrap_method(func) assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs +@pytest.mark.parametrize( + "otel_enabled,expected_endpoint", + [ + pytest.param(False, None, id="otel_disabled"), + pytest.param(True, "default.example.com:443", id="otel_enabled"), + ], +) +def test_config_service_v2_base_transport_client_options_none( + monkeypatch, otel_enabled, expected_endpoint +): + fake_observability = mock.MagicMock() + fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled + monkeypatch.setattr(transports.base, "_observability", fake_observability) + + transport = transports.ConfigServiceV2Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=None, + ) + assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + + +@pytest.mark.parametrize( + "initial_endpoint,expected_endpoint", + [ + pytest.param(None, "default.example.com:443", id="default_from_host"), + pytest.param( + "custom.example.com:443", + "custom.example.com:443", + id="preserve_explicit", + ), + ], +) +def test_config_service_v2_base_transport_client_options_object( + initial_endpoint, expected_endpoint +): + options = client_options.ClientOptions(api_endpoint=initial_endpoint) + transport = transports.ConfigServiceV2Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options.api_endpoint == expected_endpoint + assert options.api_endpoint == initial_endpoint + + +@pytest.mark.parametrize( + "initial_options,expected_options", + [ + pytest.param( + {}, + {"api_endpoint": "default.example.com:443"}, + id="default_from_host", + ), + pytest.param( + {"api_endpoint": "custom.example.com:443"}, + {"api_endpoint": "custom.example.com:443"}, + id="preserve_explicit", + ), + ], +) +def test_config_service_v2_base_transport_client_options_dict( + initial_options, expected_options +): + options = dict(initial_options) + transport = transports.ConfigServiceV2Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options == expected_options + assert options == initial_options + + def test_config_service_v2_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): diff --git a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py index 40d012fc2707..73f91e151b66 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py @@ -4461,24 +4461,89 @@ def test_logging_service_v2_base_transport_wrap_method_modern_api_core(monkeypat # 2. Base transport: kind raises NotImplementedError and is cleanly omitted with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - options = client_options.ClientOptions() base_transport = transports.LoggingServiceV2Transport( - credentials=ga_credentials.AnonymousCredentials(), - client_options=options, - ) - assert base_transport._client_options.api_endpoint == base_transport._host - dict_options = {} - dict_transport = transports.LoggingServiceV2Transport( - credentials=ga_credentials.AnonymousCredentials(), - client_options=dict_options, + credentials=ga_credentials.AnonymousCredentials() ) - assert dict_transport._client_options["api_endpoint"] == dict_transport._host func = mock.MagicMock() base_transport._wrap_method(func) assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs +@pytest.mark.parametrize( + "otel_enabled,expected_endpoint", + [ + pytest.param(False, None, id="otel_disabled"), + pytest.param(True, "default.example.com:443", id="otel_enabled"), + ], +) +def test_logging_service_v2_base_transport_client_options_none( + monkeypatch, otel_enabled, expected_endpoint +): + fake_observability = mock.MagicMock() + fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled + monkeypatch.setattr(transports.base, "_observability", fake_observability) + + transport = transports.LoggingServiceV2Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=None, + ) + assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + + +@pytest.mark.parametrize( + "initial_endpoint,expected_endpoint", + [ + pytest.param(None, "default.example.com:443", id="default_from_host"), + pytest.param( + "custom.example.com:443", + "custom.example.com:443", + id="preserve_explicit", + ), + ], +) +def test_logging_service_v2_base_transport_client_options_object( + initial_endpoint, expected_endpoint +): + options = client_options.ClientOptions(api_endpoint=initial_endpoint) + transport = transports.LoggingServiceV2Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options.api_endpoint == expected_endpoint + assert options.api_endpoint == initial_endpoint + + +@pytest.mark.parametrize( + "initial_options,expected_options", + [ + pytest.param( + {}, + {"api_endpoint": "default.example.com:443"}, + id="default_from_host", + ), + pytest.param( + {"api_endpoint": "custom.example.com:443"}, + {"api_endpoint": "custom.example.com:443"}, + id="preserve_explicit", + ), + ], +) +def test_logging_service_v2_base_transport_client_options_dict( + initial_options, expected_options +): + options = dict(initial_options) + transport = transports.LoggingServiceV2Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options == expected_options + assert options == initial_options + + def test_logging_service_v2_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): diff --git a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py index 9a0e9dee2119..7ce9cbeacc63 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py @@ -4261,24 +4261,89 @@ def test_metrics_service_v2_base_transport_wrap_method_modern_api_core(monkeypat # 2. Base transport: kind raises NotImplementedError and is cleanly omitted with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - options = client_options.ClientOptions() base_transport = transports.MetricsServiceV2Transport( - credentials=ga_credentials.AnonymousCredentials(), - client_options=options, - ) - assert base_transport._client_options.api_endpoint == base_transport._host - dict_options = {} - dict_transport = transports.MetricsServiceV2Transport( - credentials=ga_credentials.AnonymousCredentials(), - client_options=dict_options, + credentials=ga_credentials.AnonymousCredentials() ) - assert dict_transport._client_options["api_endpoint"] == dict_transport._host func = mock.MagicMock() base_transport._wrap_method(func) assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs +@pytest.mark.parametrize( + "otel_enabled,expected_endpoint", + [ + pytest.param(False, None, id="otel_disabled"), + pytest.param(True, "default.example.com:443", id="otel_enabled"), + ], +) +def test_metrics_service_v2_base_transport_client_options_none( + monkeypatch, otel_enabled, expected_endpoint +): + fake_observability = mock.MagicMock() + fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled + monkeypatch.setattr(transports.base, "_observability", fake_observability) + + transport = transports.MetricsServiceV2Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=None, + ) + assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + + +@pytest.mark.parametrize( + "initial_endpoint,expected_endpoint", + [ + pytest.param(None, "default.example.com:443", id="default_from_host"), + pytest.param( + "custom.example.com:443", + "custom.example.com:443", + id="preserve_explicit", + ), + ], +) +def test_metrics_service_v2_base_transport_client_options_object( + initial_endpoint, expected_endpoint +): + options = client_options.ClientOptions(api_endpoint=initial_endpoint) + transport = transports.MetricsServiceV2Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options.api_endpoint == expected_endpoint + assert options.api_endpoint == initial_endpoint + + +@pytest.mark.parametrize( + "initial_options,expected_options", + [ + pytest.param( + {}, + {"api_endpoint": "default.example.com:443"}, + id="default_from_host", + ), + pytest.param( + {"api_endpoint": "custom.example.com:443"}, + {"api_endpoint": "custom.example.com:443"}, + id="preserve_explicit", + ), + ], +) +def test_metrics_service_v2_base_transport_client_options_dict( + initial_options, expected_options +): + options = dict(initial_options) + transport = transports.MetricsServiceV2Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options == expected_options + assert options == initial_options + + def test_metrics_service_v2_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/base.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/base.py index c4e987f0a05f..cb1732a8c974 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/base.py @@ -14,10 +14,12 @@ # limitations under the License. # import abc +import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.logging_v2 import gapic_version as package_version from google.cloud.logging_v2._compat import ( + _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) @@ -136,9 +138,23 @@ def __init__( host += ':443' self._host = host - if isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + # If the transport was instantiated directly with client_options=None + # while OpenTelemetry tracing is enabled via environment variable, + # create a ClientOptions instance with the resolved transport host so + # method and wire spans can record server.address and server.port. + if ( + client_options is None + and _observability is not None + and _observability.is_otel_capabilities_enabled(None) + ): + client_options = client_options_lib.ClientOptions(api_endpoint=self._host) + # And here we check for the presence of an api_endpoint attr OR key, + # shallow-copying before populating to avoid mutating caller-owned state. + elif isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + client_options = copy.copy(client_options) client_options.api_endpoint = self._host elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options = copy.copy(client_options) client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/base.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/base.py index 281bdfb4dcf9..c0de6b00c70b 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/base.py @@ -14,10 +14,12 @@ # limitations under the License. # import abc +import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.logging_v2 import gapic_version as package_version from google.cloud.logging_v2._compat import ( + _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) @@ -136,9 +138,23 @@ def __init__( host += ':443' self._host = host - if isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + # If the transport was instantiated directly with client_options=None + # while OpenTelemetry tracing is enabled via environment variable, + # create a ClientOptions instance with the resolved transport host so + # method and wire spans can record server.address and server.port. + if ( + client_options is None + and _observability is not None + and _observability.is_otel_capabilities_enabled(None) + ): + client_options = client_options_lib.ClientOptions(api_endpoint=self._host) + # And here we check for the presence of an api_endpoint attr OR key, + # shallow-copying before populating to avoid mutating caller-owned state. + elif isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + client_options = copy.copy(client_options) client_options.api_endpoint = self._host elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options = copy.copy(client_options) client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py index 51d79ccd5212..630294a64d2e 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py @@ -14,10 +14,12 @@ # limitations under the License. # import abc +import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.logging_v2 import gapic_version as package_version from google.cloud.logging_v2._compat import ( + _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) @@ -136,9 +138,23 @@ def __init__( host += ':443' self._host = host - if isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + # If the transport was instantiated directly with client_options=None + # while OpenTelemetry tracing is enabled via environment variable, + # create a ClientOptions instance with the resolved transport host so + # method and wire spans can record server.address and server.port. + if ( + client_options is None + and _observability is not None + and _observability.is_otel_capabilities_enabled(None) + ): + client_options = client_options_lib.ClientOptions(api_endpoint=self._host) + # And here we check for the presence of an api_endpoint attr OR key, + # shallow-copying before populating to avoid mutating caller-owned state. + elif isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + client_options = copy.copy(client_options) client_options.api_endpoint = self._host elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options = copy.copy(client_options) client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py index 2f25f8e5fae4..daa7b8a9a8d2 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py @@ -14021,24 +14021,89 @@ def test_config_service_v2_base_transport_wrap_method_modern_api_core(monkeypatc # 2. Base transport: kind raises NotImplementedError and is cleanly omitted with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - options = client_options.ClientOptions() base_transport = transports.ConfigServiceV2Transport( - credentials=ga_credentials.AnonymousCredentials(), - client_options=options, - ) - assert base_transport._client_options.api_endpoint == base_transport._host - dict_options = {} - dict_transport = transports.ConfigServiceV2Transport( - credentials=ga_credentials.AnonymousCredentials(), - client_options=dict_options, + credentials=ga_credentials.AnonymousCredentials() ) - assert dict_transport._client_options["api_endpoint"] == dict_transport._host func = mock.MagicMock() base_transport._wrap_method(func) assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs +@pytest.mark.parametrize( + "otel_enabled,expected_endpoint", + [ + pytest.param(False, None, id="otel_disabled"), + pytest.param(True, "default.example.com:443", id="otel_enabled"), + ], +) +def test_config_service_v2_base_transport_client_options_none( + monkeypatch, otel_enabled, expected_endpoint +): + fake_observability = mock.MagicMock() + fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled + monkeypatch.setattr(transports.base, "_observability", fake_observability) + + transport = transports.ConfigServiceV2Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=None, + ) + assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + + +@pytest.mark.parametrize( + "initial_endpoint,expected_endpoint", + [ + pytest.param(None, "default.example.com:443", id="default_from_host"), + pytest.param( + "custom.example.com:443", + "custom.example.com:443", + id="preserve_explicit", + ), + ], +) +def test_config_service_v2_base_transport_client_options_object( + initial_endpoint, expected_endpoint +): + options = client_options.ClientOptions(api_endpoint=initial_endpoint) + transport = transports.ConfigServiceV2Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options.api_endpoint == expected_endpoint + assert options.api_endpoint == initial_endpoint + + +@pytest.mark.parametrize( + "initial_options,expected_options", + [ + pytest.param( + {}, + {"api_endpoint": "default.example.com:443"}, + id="default_from_host", + ), + pytest.param( + {"api_endpoint": "custom.example.com:443"}, + {"api_endpoint": "custom.example.com:443"}, + id="preserve_explicit", + ), + ], +) +def test_config_service_v2_base_transport_client_options_dict( + initial_options, expected_options +): + options = dict(initial_options) + transport = transports.ConfigServiceV2Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options == expected_options + assert options == initial_options + + def test_config_service_v2_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py index 40d012fc2707..73f91e151b66 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py @@ -4461,24 +4461,89 @@ def test_logging_service_v2_base_transport_wrap_method_modern_api_core(monkeypat # 2. Base transport: kind raises NotImplementedError and is cleanly omitted with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - options = client_options.ClientOptions() base_transport = transports.LoggingServiceV2Transport( - credentials=ga_credentials.AnonymousCredentials(), - client_options=options, - ) - assert base_transport._client_options.api_endpoint == base_transport._host - dict_options = {} - dict_transport = transports.LoggingServiceV2Transport( - credentials=ga_credentials.AnonymousCredentials(), - client_options=dict_options, + credentials=ga_credentials.AnonymousCredentials() ) - assert dict_transport._client_options["api_endpoint"] == dict_transport._host func = mock.MagicMock() base_transport._wrap_method(func) assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs +@pytest.mark.parametrize( + "otel_enabled,expected_endpoint", + [ + pytest.param(False, None, id="otel_disabled"), + pytest.param(True, "default.example.com:443", id="otel_enabled"), + ], +) +def test_logging_service_v2_base_transport_client_options_none( + monkeypatch, otel_enabled, expected_endpoint +): + fake_observability = mock.MagicMock() + fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled + monkeypatch.setattr(transports.base, "_observability", fake_observability) + + transport = transports.LoggingServiceV2Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=None, + ) + assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + + +@pytest.mark.parametrize( + "initial_endpoint,expected_endpoint", + [ + pytest.param(None, "default.example.com:443", id="default_from_host"), + pytest.param( + "custom.example.com:443", + "custom.example.com:443", + id="preserve_explicit", + ), + ], +) +def test_logging_service_v2_base_transport_client_options_object( + initial_endpoint, expected_endpoint +): + options = client_options.ClientOptions(api_endpoint=initial_endpoint) + transport = transports.LoggingServiceV2Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options.api_endpoint == expected_endpoint + assert options.api_endpoint == initial_endpoint + + +@pytest.mark.parametrize( + "initial_options,expected_options", + [ + pytest.param( + {}, + {"api_endpoint": "default.example.com:443"}, + id="default_from_host", + ), + pytest.param( + {"api_endpoint": "custom.example.com:443"}, + {"api_endpoint": "custom.example.com:443"}, + id="preserve_explicit", + ), + ], +) +def test_logging_service_v2_base_transport_client_options_dict( + initial_options, expected_options +): + options = dict(initial_options) + transport = transports.LoggingServiceV2Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options == expected_options + assert options == initial_options + + def test_logging_service_v2_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py index ee51e53c7ac5..236343bae84e 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py @@ -4261,24 +4261,89 @@ def test_metrics_service_v2_base_transport_wrap_method_modern_api_core(monkeypat # 2. Base transport: kind raises NotImplementedError and is cleanly omitted with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - options = client_options.ClientOptions() base_transport = transports.MetricsServiceV2Transport( - credentials=ga_credentials.AnonymousCredentials(), - client_options=options, - ) - assert base_transport._client_options.api_endpoint == base_transport._host - dict_options = {} - dict_transport = transports.MetricsServiceV2Transport( - credentials=ga_credentials.AnonymousCredentials(), - client_options=dict_options, + credentials=ga_credentials.AnonymousCredentials() ) - assert dict_transport._client_options["api_endpoint"] == dict_transport._host func = mock.MagicMock() base_transport._wrap_method(func) assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs +@pytest.mark.parametrize( + "otel_enabled,expected_endpoint", + [ + pytest.param(False, None, id="otel_disabled"), + pytest.param(True, "default.example.com:443", id="otel_enabled"), + ], +) +def test_metrics_service_v2_base_transport_client_options_none( + monkeypatch, otel_enabled, expected_endpoint +): + fake_observability = mock.MagicMock() + fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled + monkeypatch.setattr(transports.base, "_observability", fake_observability) + + transport = transports.MetricsServiceV2Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=None, + ) + assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + + +@pytest.mark.parametrize( + "initial_endpoint,expected_endpoint", + [ + pytest.param(None, "default.example.com:443", id="default_from_host"), + pytest.param( + "custom.example.com:443", + "custom.example.com:443", + id="preserve_explicit", + ), + ], +) +def test_metrics_service_v2_base_transport_client_options_object( + initial_endpoint, expected_endpoint +): + options = client_options.ClientOptions(api_endpoint=initial_endpoint) + transport = transports.MetricsServiceV2Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options.api_endpoint == expected_endpoint + assert options.api_endpoint == initial_endpoint + + +@pytest.mark.parametrize( + "initial_options,expected_options", + [ + pytest.param( + {}, + {"api_endpoint": "default.example.com:443"}, + id="default_from_host", + ), + pytest.param( + {"api_endpoint": "custom.example.com:443"}, + {"api_endpoint": "custom.example.com:443"}, + id="preserve_explicit", + ), + ], +) +def test_metrics_service_v2_base_transport_client_options_dict( + initial_options, expected_options +): + options = dict(initial_options) + transport = transports.MetricsServiceV2Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options == expected_options + assert options == initial_options + + def test_metrics_service_v2_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): diff --git a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/base.py b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/base.py index 7cfc53db8511..6630c8470486 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/base.py @@ -14,10 +14,12 @@ # limitations under the License. # import abc +import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.redis_v1 import gapic_version as package_version from google.cloud.redis_v1._compat import ( + _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) @@ -133,9 +135,23 @@ def __init__( host += ':443' self._host = host - if isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + # If the transport was instantiated directly with client_options=None + # while OpenTelemetry tracing is enabled via environment variable, + # create a ClientOptions instance with the resolved transport host so + # method and wire spans can record server.address and server.port. + if ( + client_options is None + and _observability is not None + and _observability.is_otel_capabilities_enabled(None) + ): + client_options = client_options_lib.ClientOptions(api_endpoint=self._host) + # And here we check for the presence of an api_endpoint attr OR key, + # shallow-copying before populating to avoid mutating caller-owned state. + elif isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + client_options = copy.copy(client_options) client_options.api_endpoint = self._host elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options = copy.copy(client_options) client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py b/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py index 8bc3a47bb884..0a084852e9fd 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py @@ -13315,24 +13315,89 @@ def test_cloud_redis_base_transport_wrap_method_modern_api_core(monkeypatch): # 2. Base transport: kind raises NotImplementedError and is cleanly omitted with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - options = client_options.ClientOptions() base_transport = transports.CloudRedisTransport( - credentials=ga_credentials.AnonymousCredentials(), - client_options=options, - ) - assert base_transport._client_options.api_endpoint == base_transport._host - dict_options = {} - dict_transport = transports.CloudRedisTransport( - credentials=ga_credentials.AnonymousCredentials(), - client_options=dict_options, + credentials=ga_credentials.AnonymousCredentials() ) - assert dict_transport._client_options["api_endpoint"] == dict_transport._host func = mock.MagicMock() base_transport._wrap_method(func) assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs +@pytest.mark.parametrize( + "otel_enabled,expected_endpoint", + [ + pytest.param(False, None, id="otel_disabled"), + pytest.param(True, "default.example.com:443", id="otel_enabled"), + ], +) +def test_cloud_redis_base_transport_client_options_none( + monkeypatch, otel_enabled, expected_endpoint +): + fake_observability = mock.MagicMock() + fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled + monkeypatch.setattr(transports.base, "_observability", fake_observability) + + transport = transports.CloudRedisTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=None, + ) + assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + + +@pytest.mark.parametrize( + "initial_endpoint,expected_endpoint", + [ + pytest.param(None, "default.example.com:443", id="default_from_host"), + pytest.param( + "custom.example.com:443", + "custom.example.com:443", + id="preserve_explicit", + ), + ], +) +def test_cloud_redis_base_transport_client_options_object( + initial_endpoint, expected_endpoint +): + options = client_options.ClientOptions(api_endpoint=initial_endpoint) + transport = transports.CloudRedisTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options.api_endpoint == expected_endpoint + assert options.api_endpoint == initial_endpoint + + +@pytest.mark.parametrize( + "initial_options,expected_options", + [ + pytest.param( + {}, + {"api_endpoint": "default.example.com:443"}, + id="default_from_host", + ), + pytest.param( + {"api_endpoint": "custom.example.com:443"}, + {"api_endpoint": "custom.example.com:443"}, + id="preserve_explicit", + ), + ], +) +def test_cloud_redis_base_transport_client_options_dict( + initial_options, expected_options +): + options = dict(initial_options) + transport = transports.CloudRedisTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options == expected_options + assert options == initial_options + + def test_cloud_redis_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/base.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/base.py index f54e929dbabe..2f0e15493d99 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/base.py @@ -14,10 +14,12 @@ # limitations under the License. # import abc +import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.redis_v1 import gapic_version as package_version from google.cloud.redis_v1._compat import ( + _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) @@ -133,9 +135,23 @@ def __init__( host += ':443' self._host = host - if isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + # If the transport was instantiated directly with client_options=None + # while OpenTelemetry tracing is enabled via environment variable, + # create a ClientOptions instance with the resolved transport host so + # method and wire spans can record server.address and server.port. + if ( + client_options is None + and _observability is not None + and _observability.is_otel_capabilities_enabled(None) + ): + client_options = client_options_lib.ClientOptions(api_endpoint=self._host) + # And here we check for the presence of an api_endpoint attr OR key, + # shallow-copying before populating to avoid mutating caller-owned state. + elif isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + client_options = copy.copy(client_options) client_options.api_endpoint = self._host elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options = copy.copy(client_options) client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py index a37acdf612fd..061d60591136 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py @@ -8535,24 +8535,89 @@ def test_cloud_redis_base_transport_wrap_method_modern_api_core(monkeypatch): # 2. Base transport: kind raises NotImplementedError and is cleanly omitted with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - options = client_options.ClientOptions() base_transport = transports.CloudRedisTransport( - credentials=ga_credentials.AnonymousCredentials(), - client_options=options, - ) - assert base_transport._client_options.api_endpoint == base_transport._host - dict_options = {} - dict_transport = transports.CloudRedisTransport( - credentials=ga_credentials.AnonymousCredentials(), - client_options=dict_options, + credentials=ga_credentials.AnonymousCredentials() ) - assert dict_transport._client_options["api_endpoint"] == dict_transport._host func = mock.MagicMock() base_transport._wrap_method(func) assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs +@pytest.mark.parametrize( + "otel_enabled,expected_endpoint", + [ + pytest.param(False, None, id="otel_disabled"), + pytest.param(True, "default.example.com:443", id="otel_enabled"), + ], +) +def test_cloud_redis_base_transport_client_options_none( + monkeypatch, otel_enabled, expected_endpoint +): + fake_observability = mock.MagicMock() + fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled + monkeypatch.setattr(transports.base, "_observability", fake_observability) + + transport = transports.CloudRedisTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=None, + ) + assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + + +@pytest.mark.parametrize( + "initial_endpoint,expected_endpoint", + [ + pytest.param(None, "default.example.com:443", id="default_from_host"), + pytest.param( + "custom.example.com:443", + "custom.example.com:443", + id="preserve_explicit", + ), + ], +) +def test_cloud_redis_base_transport_client_options_object( + initial_endpoint, expected_endpoint +): + options = client_options.ClientOptions(api_endpoint=initial_endpoint) + transport = transports.CloudRedisTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options.api_endpoint == expected_endpoint + assert options.api_endpoint == initial_endpoint + + +@pytest.mark.parametrize( + "initial_options,expected_options", + [ + pytest.param( + {}, + {"api_endpoint": "default.example.com:443"}, + id="default_from_host", + ), + pytest.param( + {"api_endpoint": "custom.example.com:443"}, + {"api_endpoint": "custom.example.com:443"}, + id="preserve_explicit", + ), + ], +) +def test_cloud_redis_base_transport_client_options_dict( + initial_options, expected_options +): + options = dict(initial_options) + transport = transports.CloudRedisTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options == expected_options + assert options == initial_options + + def test_cloud_redis_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/base.py index 92a40305f088..7982b81e3f98 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/base.py @@ -14,10 +14,12 @@ # limitations under the License. # import abc +import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.showcase_v1beta1 import gapic_version as package_version from google.showcase_v1beta1._compat import ( + _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) @@ -134,9 +136,23 @@ def __init__( host += ':443' self._host = host - if isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + # If the transport was instantiated directly with client_options=None + # while OpenTelemetry tracing is enabled via environment variable, + # create a ClientOptions instance with the resolved transport host so + # method and wire spans can record server.address and server.port. + if ( + client_options is None + and _observability is not None + and _observability.is_otel_capabilities_enabled(None) + ): + client_options = client_options_lib.ClientOptions(api_endpoint=self._host) + # And here we check for the presence of an api_endpoint attr OR key, + # shallow-copying before populating to avoid mutating caller-owned state. + elif isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + client_options = copy.copy(client_options) client_options.api_endpoint = self._host elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options = copy.copy(client_options) client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/base.py index 4b878b2d2ec8..ee98130046f1 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/base.py @@ -14,10 +14,12 @@ # limitations under the License. # import abc +import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.showcase_v1beta1 import gapic_version as package_version from google.showcase_v1beta1._compat import ( + _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) @@ -134,9 +136,23 @@ def __init__( host += ':443' self._host = host - if isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + # If the transport was instantiated directly with client_options=None + # while OpenTelemetry tracing is enabled via environment variable, + # create a ClientOptions instance with the resolved transport host so + # method and wire spans can record server.address and server.port. + if ( + client_options is None + and _observability is not None + and _observability.is_otel_capabilities_enabled(None) + ): + client_options = client_options_lib.ClientOptions(api_endpoint=self._host) + # And here we check for the presence of an api_endpoint attr OR key, + # shallow-copying before populating to avoid mutating caller-owned state. + elif isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + client_options = copy.copy(client_options) client_options.api_endpoint = self._host elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options = copy.copy(client_options) client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/base.py index 80c282a05668..b1c947a35f80 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/base.py @@ -14,10 +14,12 @@ # limitations under the License. # import abc +import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.showcase_v1beta1 import gapic_version as package_version from google.showcase_v1beta1._compat import ( + _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) @@ -135,9 +137,23 @@ def __init__( host += ':443' self._host = host - if isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + # If the transport was instantiated directly with client_options=None + # while OpenTelemetry tracing is enabled via environment variable, + # create a ClientOptions instance with the resolved transport host so + # method and wire spans can record server.address and server.port. + if ( + client_options is None + and _observability is not None + and _observability.is_otel_capabilities_enabled(None) + ): + client_options = client_options_lib.ClientOptions(api_endpoint=self._host) + # And here we check for the presence of an api_endpoint attr OR key, + # shallow-copying before populating to avoid mutating caller-owned state. + elif isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + client_options = copy.copy(client_options) client_options.api_endpoint = self._host elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options = copy.copy(client_options) client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/base.py index a08b085bc2b0..22b2609ecfcf 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/base.py @@ -14,10 +14,12 @@ # limitations under the License. # import abc +import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.showcase_v1beta1 import gapic_version as package_version from google.showcase_v1beta1._compat import ( + _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) @@ -135,9 +137,23 @@ def __init__( host += ':443' self._host = host - if isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + # If the transport was instantiated directly with client_options=None + # while OpenTelemetry tracing is enabled via environment variable, + # create a ClientOptions instance with the resolved transport host so + # method and wire spans can record server.address and server.port. + if ( + client_options is None + and _observability is not None + and _observability.is_otel_capabilities_enabled(None) + ): + client_options = client_options_lib.ClientOptions(api_endpoint=self._host) + # And here we check for the presence of an api_endpoint attr OR key, + # shallow-copying before populating to avoid mutating caller-owned state. + elif isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + client_options = copy.copy(client_options) client_options.api_endpoint = self._host elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options = copy.copy(client_options) client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/base.py index ebd1c47d1d1f..22d317b32796 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/base.py @@ -14,10 +14,12 @@ # limitations under the License. # import abc +import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.showcase_v1beta1 import gapic_version as package_version from google.showcase_v1beta1._compat import ( + _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) @@ -135,9 +137,23 @@ def __init__( host += ':443' self._host = host - if isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + # If the transport was instantiated directly with client_options=None + # while OpenTelemetry tracing is enabled via environment variable, + # create a ClientOptions instance with the resolved transport host so + # method and wire spans can record server.address and server.port. + if ( + client_options is None + and _observability is not None + and _observability.is_otel_capabilities_enabled(None) + ): + client_options = client_options_lib.ClientOptions(api_endpoint=self._host) + # And here we check for the presence of an api_endpoint attr OR key, + # shallow-copying before populating to avoid mutating caller-owned state. + elif isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + client_options = copy.copy(client_options) client_options.api_endpoint = self._host elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options = copy.copy(client_options) client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/base.py index dcc895574f0e..e6431cf277c5 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/base.py @@ -14,10 +14,12 @@ # limitations under the License. # import abc +import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.showcase_v1beta1 import gapic_version as package_version from google.showcase_v1beta1._compat import ( + _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) @@ -134,9 +136,23 @@ def __init__( host += ':443' self._host = host - if isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + # If the transport was instantiated directly with client_options=None + # while OpenTelemetry tracing is enabled via environment variable, + # create a ClientOptions instance with the resolved transport host so + # method and wire spans can record server.address and server.port. + if ( + client_options is None + and _observability is not None + and _observability.is_otel_capabilities_enabled(None) + ): + client_options = client_options_lib.ClientOptions(api_endpoint=self._host) + # And here we check for the presence of an api_endpoint attr OR key, + # shallow-copying before populating to avoid mutating caller-owned state. + elif isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + client_options = copy.copy(client_options) client_options.api_endpoint = self._host elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options = copy.copy(client_options) client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_echo.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_echo.py index e30741c8bca0..6694d584d0f2 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_echo.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_echo.py @@ -9794,24 +9794,89 @@ def test_echo_base_transport_wrap_method_modern_api_core(monkeypatch): # 2. Base transport: kind raises NotImplementedError and is cleanly omitted with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - options = client_options.ClientOptions() base_transport = transports.EchoTransport( - credentials=ga_credentials.AnonymousCredentials(), - client_options=options, - ) - assert base_transport._client_options.api_endpoint == base_transport._host - dict_options = {} - dict_transport = transports.EchoTransport( - credentials=ga_credentials.AnonymousCredentials(), - client_options=dict_options, + credentials=ga_credentials.AnonymousCredentials() ) - assert dict_transport._client_options["api_endpoint"] == dict_transport._host func = mock.MagicMock() base_transport._wrap_method(func) assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs +@pytest.mark.parametrize( + "otel_enabled,expected_endpoint", + [ + pytest.param(False, None, id="otel_disabled"), + pytest.param(True, "default.example.com:443", id="otel_enabled"), + ], +) +def test_echo_base_transport_client_options_none( + monkeypatch, otel_enabled, expected_endpoint +): + fake_observability = mock.MagicMock() + fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled + monkeypatch.setattr(transports.base, "_observability", fake_observability) + + transport = transports.EchoTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=None, + ) + assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + + +@pytest.mark.parametrize( + "initial_endpoint,expected_endpoint", + [ + pytest.param(None, "default.example.com:443", id="default_from_host"), + pytest.param( + "custom.example.com:443", + "custom.example.com:443", + id="preserve_explicit", + ), + ], +) +def test_echo_base_transport_client_options_object( + initial_endpoint, expected_endpoint +): + options = client_options.ClientOptions(api_endpoint=initial_endpoint) + transport = transports.EchoTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options.api_endpoint == expected_endpoint + assert options.api_endpoint == initial_endpoint + + +@pytest.mark.parametrize( + "initial_options,expected_options", + [ + pytest.param( + {}, + {"api_endpoint": "default.example.com:443"}, + id="default_from_host", + ), + pytest.param( + {"api_endpoint": "custom.example.com:443"}, + {"api_endpoint": "custom.example.com:443"}, + id="preserve_explicit", + ), + ], +) +def test_echo_base_transport_client_options_dict( + initial_options, expected_options +): + options = dict(initial_options) + transport = transports.EchoTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options == expected_options + assert options == initial_options + + def test_echo_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_identity.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_identity.py index e4898cf8a353..e06e5cd3203f 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_identity.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_identity.py @@ -6901,24 +6901,89 @@ def test_identity_base_transport_wrap_method_modern_api_core(monkeypatch): # 2. Base transport: kind raises NotImplementedError and is cleanly omitted with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - options = client_options.ClientOptions() base_transport = transports.IdentityTransport( - credentials=ga_credentials.AnonymousCredentials(), - client_options=options, - ) - assert base_transport._client_options.api_endpoint == base_transport._host - dict_options = {} - dict_transport = transports.IdentityTransport( - credentials=ga_credentials.AnonymousCredentials(), - client_options=dict_options, + credentials=ga_credentials.AnonymousCredentials() ) - assert dict_transport._client_options["api_endpoint"] == dict_transport._host func = mock.MagicMock() base_transport._wrap_method(func) assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs +@pytest.mark.parametrize( + "otel_enabled,expected_endpoint", + [ + pytest.param(False, None, id="otel_disabled"), + pytest.param(True, "default.example.com:443", id="otel_enabled"), + ], +) +def test_identity_base_transport_client_options_none( + monkeypatch, otel_enabled, expected_endpoint +): + fake_observability = mock.MagicMock() + fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled + monkeypatch.setattr(transports.base, "_observability", fake_observability) + + transport = transports.IdentityTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=None, + ) + assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + + +@pytest.mark.parametrize( + "initial_endpoint,expected_endpoint", + [ + pytest.param(None, "default.example.com:443", id="default_from_host"), + pytest.param( + "custom.example.com:443", + "custom.example.com:443", + id="preserve_explicit", + ), + ], +) +def test_identity_base_transport_client_options_object( + initial_endpoint, expected_endpoint +): + options = client_options.ClientOptions(api_endpoint=initial_endpoint) + transport = transports.IdentityTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options.api_endpoint == expected_endpoint + assert options.api_endpoint == initial_endpoint + + +@pytest.mark.parametrize( + "initial_options,expected_options", + [ + pytest.param( + {}, + {"api_endpoint": "default.example.com:443"}, + id="default_from_host", + ), + pytest.param( + {"api_endpoint": "custom.example.com:443"}, + {"api_endpoint": "custom.example.com:443"}, + id="preserve_explicit", + ), + ], +) +def test_identity_base_transport_client_options_dict( + initial_options, expected_options +): + options = dict(initial_options) + transport = transports.IdentityTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options == expected_options + assert options == initial_options + + def test_identity_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_messaging.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_messaging.py index cfbac6a4f059..e26295547198 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_messaging.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_messaging.py @@ -11801,24 +11801,89 @@ def test_messaging_base_transport_wrap_method_modern_api_core(monkeypatch): # 2. Base transport: kind raises NotImplementedError and is cleanly omitted with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - options = client_options.ClientOptions() base_transport = transports.MessagingTransport( - credentials=ga_credentials.AnonymousCredentials(), - client_options=options, - ) - assert base_transport._client_options.api_endpoint == base_transport._host - dict_options = {} - dict_transport = transports.MessagingTransport( - credentials=ga_credentials.AnonymousCredentials(), - client_options=dict_options, + credentials=ga_credentials.AnonymousCredentials() ) - assert dict_transport._client_options["api_endpoint"] == dict_transport._host func = mock.MagicMock() base_transport._wrap_method(func) assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs +@pytest.mark.parametrize( + "otel_enabled,expected_endpoint", + [ + pytest.param(False, None, id="otel_disabled"), + pytest.param(True, "default.example.com:443", id="otel_enabled"), + ], +) +def test_messaging_base_transport_client_options_none( + monkeypatch, otel_enabled, expected_endpoint +): + fake_observability = mock.MagicMock() + fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled + monkeypatch.setattr(transports.base, "_observability", fake_observability) + + transport = transports.MessagingTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=None, + ) + assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + + +@pytest.mark.parametrize( + "initial_endpoint,expected_endpoint", + [ + pytest.param(None, "default.example.com:443", id="default_from_host"), + pytest.param( + "custom.example.com:443", + "custom.example.com:443", + id="preserve_explicit", + ), + ], +) +def test_messaging_base_transport_client_options_object( + initial_endpoint, expected_endpoint +): + options = client_options.ClientOptions(api_endpoint=initial_endpoint) + transport = transports.MessagingTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options.api_endpoint == expected_endpoint + assert options.api_endpoint == initial_endpoint + + +@pytest.mark.parametrize( + "initial_options,expected_options", + [ + pytest.param( + {}, + {"api_endpoint": "default.example.com:443"}, + id="default_from_host", + ), + pytest.param( + {"api_endpoint": "custom.example.com:443"}, + {"api_endpoint": "custom.example.com:443"}, + id="preserve_explicit", + ), + ], +) +def test_messaging_base_transport_client_options_dict( + initial_options, expected_options +): + options = dict(initial_options) + transport = transports.MessagingTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options == expected_options + assert options == initial_options + + def test_messaging_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py index f710af316ca5..c7f22d5df1bd 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py @@ -4494,24 +4494,89 @@ def test_resumable_upload_service_base_transport_wrap_method_modern_api_core(mon # 2. Base transport: kind raises NotImplementedError and is cleanly omitted with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - options = client_options.ClientOptions() base_transport = transports.ResumableUploadServiceTransport( - credentials=ga_credentials.AnonymousCredentials(), - client_options=options, - ) - assert base_transport._client_options.api_endpoint == base_transport._host - dict_options = {} - dict_transport = transports.ResumableUploadServiceTransport( - credentials=ga_credentials.AnonymousCredentials(), - client_options=dict_options, + credentials=ga_credentials.AnonymousCredentials() ) - assert dict_transport._client_options["api_endpoint"] == dict_transport._host func = mock.MagicMock() base_transport._wrap_method(func) assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs +@pytest.mark.parametrize( + "otel_enabled,expected_endpoint", + [ + pytest.param(False, None, id="otel_disabled"), + pytest.param(True, "default.example.com:443", id="otel_enabled"), + ], +) +def test_resumable_upload_service_base_transport_client_options_none( + monkeypatch, otel_enabled, expected_endpoint +): + fake_observability = mock.MagicMock() + fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled + monkeypatch.setattr(transports.base, "_observability", fake_observability) + + transport = transports.ResumableUploadServiceTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=None, + ) + assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + + +@pytest.mark.parametrize( + "initial_endpoint,expected_endpoint", + [ + pytest.param(None, "default.example.com:443", id="default_from_host"), + pytest.param( + "custom.example.com:443", + "custom.example.com:443", + id="preserve_explicit", + ), + ], +) +def test_resumable_upload_service_base_transport_client_options_object( + initial_endpoint, expected_endpoint +): + options = client_options.ClientOptions(api_endpoint=initial_endpoint) + transport = transports.ResumableUploadServiceTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options.api_endpoint == expected_endpoint + assert options.api_endpoint == initial_endpoint + + +@pytest.mark.parametrize( + "initial_options,expected_options", + [ + pytest.param( + {}, + {"api_endpoint": "default.example.com:443"}, + id="default_from_host", + ), + pytest.param( + {"api_endpoint": "custom.example.com:443"}, + {"api_endpoint": "custom.example.com:443"}, + id="preserve_explicit", + ), + ], +) +def test_resumable_upload_service_base_transport_client_options_dict( + initial_options, expected_options +): + options = dict(initial_options) + transport = transports.ResumableUploadServiceTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options == expected_options + assert options == initial_options + + def test_resumable_upload_service_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_sequence_service.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_sequence_service.py index 8b7e3acf7c69..992e6a7abe4c 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_sequence_service.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_sequence_service.py @@ -7583,24 +7583,89 @@ def test_sequence_service_base_transport_wrap_method_modern_api_core(monkeypatch # 2. Base transport: kind raises NotImplementedError and is cleanly omitted with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - options = client_options.ClientOptions() base_transport = transports.SequenceServiceTransport( - credentials=ga_credentials.AnonymousCredentials(), - client_options=options, - ) - assert base_transport._client_options.api_endpoint == base_transport._host - dict_options = {} - dict_transport = transports.SequenceServiceTransport( - credentials=ga_credentials.AnonymousCredentials(), - client_options=dict_options, + credentials=ga_credentials.AnonymousCredentials() ) - assert dict_transport._client_options["api_endpoint"] == dict_transport._host func = mock.MagicMock() base_transport._wrap_method(func) assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs +@pytest.mark.parametrize( + "otel_enabled,expected_endpoint", + [ + pytest.param(False, None, id="otel_disabled"), + pytest.param(True, "default.example.com:443", id="otel_enabled"), + ], +) +def test_sequence_service_base_transport_client_options_none( + monkeypatch, otel_enabled, expected_endpoint +): + fake_observability = mock.MagicMock() + fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled + monkeypatch.setattr(transports.base, "_observability", fake_observability) + + transport = transports.SequenceServiceTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=None, + ) + assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + + +@pytest.mark.parametrize( + "initial_endpoint,expected_endpoint", + [ + pytest.param(None, "default.example.com:443", id="default_from_host"), + pytest.param( + "custom.example.com:443", + "custom.example.com:443", + id="preserve_explicit", + ), + ], +) +def test_sequence_service_base_transport_client_options_object( + initial_endpoint, expected_endpoint +): + options = client_options.ClientOptions(api_endpoint=initial_endpoint) + transport = transports.SequenceServiceTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options.api_endpoint == expected_endpoint + assert options.api_endpoint == initial_endpoint + + +@pytest.mark.parametrize( + "initial_options,expected_options", + [ + pytest.param( + {}, + {"api_endpoint": "default.example.com:443"}, + id="default_from_host", + ), + pytest.param( + {"api_endpoint": "custom.example.com:443"}, + {"api_endpoint": "custom.example.com:443"}, + id="preserve_explicit", + ), + ], +) +def test_sequence_service_base_transport_client_options_dict( + initial_options, expected_options +): + options = dict(initial_options) + transport = transports.SequenceServiceTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options == expected_options + assert options == initial_options + + def test_sequence_service_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_testing.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_testing.py index 3e51476de5ac..8f63d368ffd9 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_testing.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_testing.py @@ -7772,24 +7772,89 @@ def test_testing_base_transport_wrap_method_modern_api_core(monkeypatch): # 2. Base transport: kind raises NotImplementedError and is cleanly omitted with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - options = client_options.ClientOptions() base_transport = transports.TestingTransport( - credentials=ga_credentials.AnonymousCredentials(), - client_options=options, - ) - assert base_transport._client_options.api_endpoint == base_transport._host - dict_options = {} - dict_transport = transports.TestingTransport( - credentials=ga_credentials.AnonymousCredentials(), - client_options=dict_options, + credentials=ga_credentials.AnonymousCredentials() ) - assert dict_transport._client_options["api_endpoint"] == dict_transport._host func = mock.MagicMock() base_transport._wrap_method(func) assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs +@pytest.mark.parametrize( + "otel_enabled,expected_endpoint", + [ + pytest.param(False, None, id="otel_disabled"), + pytest.param(True, "default.example.com:443", id="otel_enabled"), + ], +) +def test_testing_base_transport_client_options_none( + monkeypatch, otel_enabled, expected_endpoint +): + fake_observability = mock.MagicMock() + fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled + monkeypatch.setattr(transports.base, "_observability", fake_observability) + + transport = transports.TestingTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=None, + ) + assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + + +@pytest.mark.parametrize( + "initial_endpoint,expected_endpoint", + [ + pytest.param(None, "default.example.com:443", id="default_from_host"), + pytest.param( + "custom.example.com:443", + "custom.example.com:443", + id="preserve_explicit", + ), + ], +) +def test_testing_base_transport_client_options_object( + initial_endpoint, expected_endpoint +): + options = client_options.ClientOptions(api_endpoint=initial_endpoint) + transport = transports.TestingTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options.api_endpoint == expected_endpoint + assert options.api_endpoint == initial_endpoint + + +@pytest.mark.parametrize( + "initial_options,expected_options", + [ + pytest.param( + {}, + {"api_endpoint": "default.example.com:443"}, + id="default_from_host", + ), + pytest.param( + {"api_endpoint": "custom.example.com:443"}, + {"api_endpoint": "custom.example.com:443"}, + id="preserve_explicit", + ), + ], +) +def test_testing_base_transport_client_options_dict( + initial_options, expected_options +): + options = dict(initial_options) + transport = transports.TestingTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options == expected_options + assert options == initial_options + + def test_testing_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/base.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/base.py index f84099367870..eb72570fd60e 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/base.py @@ -14,10 +14,12 @@ # limitations under the License. # import abc +import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.storagebatchoperations_v1 import gapic_version as package_version from google.cloud.storagebatchoperations_v1._compat import ( + _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) @@ -135,9 +137,23 @@ def __init__( host += ':443' self._host = host - if isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + # If the transport was instantiated directly with client_options=None + # while OpenTelemetry tracing is enabled via environment variable, + # create a ClientOptions instance with the resolved transport host so + # method and wire spans can record server.address and server.port. + if ( + client_options is None + and _observability is not None + and _observability.is_otel_capabilities_enabled(None) + ): + client_options = client_options_lib.ClientOptions(api_endpoint=self._host) + # And here we check for the presence of an api_endpoint attr OR key, + # shallow-copying before populating to avoid mutating caller-owned state. + elif isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: + client_options = copy.copy(client_options) client_options.api_endpoint = self._host elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options = copy.copy(client_options) client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py index 0b74ccb93597..5bd071d96d84 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py @@ -8471,24 +8471,89 @@ def test_storage_batch_operations_base_transport_wrap_method_modern_api_core(mon # 2. Base transport: kind raises NotImplementedError and is cleanly omitted with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - options = client_options.ClientOptions() base_transport = transports.StorageBatchOperationsTransport( - credentials=ga_credentials.AnonymousCredentials(), - client_options=options, - ) - assert base_transport._client_options.api_endpoint == base_transport._host - dict_options = {} - dict_transport = transports.StorageBatchOperationsTransport( - credentials=ga_credentials.AnonymousCredentials(), - client_options=dict_options, + credentials=ga_credentials.AnonymousCredentials() ) - assert dict_transport._client_options["api_endpoint"] == dict_transport._host func = mock.MagicMock() base_transport._wrap_method(func) assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs +@pytest.mark.parametrize( + "otel_enabled,expected_endpoint", + [ + pytest.param(False, None, id="otel_disabled"), + pytest.param(True, "default.example.com:443", id="otel_enabled"), + ], +) +def test_storage_batch_operations_base_transport_client_options_none( + monkeypatch, otel_enabled, expected_endpoint +): + fake_observability = mock.MagicMock() + fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled + monkeypatch.setattr(transports.base, "_observability", fake_observability) + + transport = transports.StorageBatchOperationsTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=None, + ) + assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + + +@pytest.mark.parametrize( + "initial_endpoint,expected_endpoint", + [ + pytest.param(None, "default.example.com:443", id="default_from_host"), + pytest.param( + "custom.example.com:443", + "custom.example.com:443", + id="preserve_explicit", + ), + ], +) +def test_storage_batch_operations_base_transport_client_options_object( + initial_endpoint, expected_endpoint +): + options = client_options.ClientOptions(api_endpoint=initial_endpoint) + transport = transports.StorageBatchOperationsTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options.api_endpoint == expected_endpoint + assert options.api_endpoint == initial_endpoint + + +@pytest.mark.parametrize( + "initial_options,expected_options", + [ + pytest.param( + {}, + {"api_endpoint": "default.example.com:443"}, + id="default_from_host", + ), + pytest.param( + {"api_endpoint": "custom.example.com:443"}, + {"api_endpoint": "custom.example.com:443"}, + id="preserve_explicit", + ), + ], +) +def test_storage_batch_operations_base_transport_client_options_dict( + initial_options, expected_options +): + options = dict(initial_options) + transport = transports.StorageBatchOperationsTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options == expected_options + assert options == initial_options + + def test_storage_batch_operations_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): From 7a186f1da201be2eeb08b1a74850349758bfc9fe Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Fri, 30 Oct 2026 12:42:43 -0400 Subject: [PATCH 3/7] test(gapic-generator): assert transport._client_options in otel channel interceptor tests --- .../unit/gapic/%name_%version/%sub/test_%service.py.j2 | 6 ++++-- .../asset/tests/unit/gapic/asset_v1/test_asset_service.py | 6 ++++-- .../tests/unit/gapic/credentials_v1/test_iam_credentials.py | 6 ++++-- .../eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py | 6 ++++-- .../tests/unit/gapic/logging_v2/test_config_service_v2.py | 6 ++++-- .../tests/unit/gapic/logging_v2/test_logging_service_v2.py | 6 ++++-- .../tests/unit/gapic/logging_v2/test_metrics_service_v2.py | 6 ++++-- .../tests/unit/gapic/logging_v2/test_config_service_v2.py | 6 ++++-- .../tests/unit/gapic/logging_v2/test_logging_service_v2.py | 6 ++++-- .../tests/unit/gapic/logging_v2/test_metrics_service_v2.py | 6 ++++-- .../redis/tests/unit/gapic/redis_v1/test_cloud_redis.py | 6 ++++-- .../tests/unit/gapic/redis_v1/test_cloud_redis.py | 6 ++++-- .../showcase/tests/unit/gapic/showcase_v1beta1/test_echo.py | 6 ++++-- .../tests/unit/gapic/showcase_v1beta1/test_identity.py | 6 ++++-- .../tests/unit/gapic/showcase_v1beta1/test_messaging.py | 6 ++++-- .../gapic/showcase_v1beta1/test_resumable_upload_service.py | 6 ++++-- .../unit/gapic/showcase_v1beta1/test_sequence_service.py | 6 ++++-- .../tests/unit/gapic/showcase_v1beta1/test_testing.py | 6 ++++-- .../test_storage_batch_operations.py | 6 ++++-- 19 files changed, 76 insertions(+), 38 deletions(-) diff --git a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 index 04d4d104620d..dd69c549fbac 100644 --- a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 +++ b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 @@ -996,7 +996,7 @@ def test_{{ service.name|snake_case }}_grpc_transport_otel_channel_interceptor() client_options=options, ) - mock_get_interceptor.assert_called_once_with(options) + mock_get_interceptor.assert_called_once_with(transport._client_options) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -1070,7 +1070,9 @@ def test_{{ service.name|snake_case }}_grpc_asyncio_transport_otel_channel_inter client_options=options, ) - mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) + mock_apply_async_interceptors.assert_called_once_with( + mock_channel, transport._client_options + ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel diff --git a/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py b/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py index c34dd1104a94..a31872a61b88 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py +++ b/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py @@ -851,7 +851,7 @@ def test_asset_service_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(options) + mock_get_interceptor.assert_called_once_with(transport._client_options) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -925,7 +925,9 @@ def test_asset_service_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) + mock_apply_async_interceptors.assert_called_once_with( + mock_channel, transport._client_options + ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py b/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py index 752a9b818f79..d3af824d005c 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py +++ b/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py @@ -841,7 +841,7 @@ def test_iam_credentials_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(options) + mock_get_interceptor.assert_called_once_with(transport._client_options) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -915,7 +915,9 @@ def test_iam_credentials_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) + mock_apply_async_interceptors.assert_called_once_with( + mock_channel, transport._client_options + ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py b/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py index c6faa631319b..556b3265ab3b 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py @@ -872,7 +872,7 @@ def test_eventarc_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(options) + mock_get_interceptor.assert_called_once_with(transport._client_options) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -946,7 +946,9 @@ def test_eventarc_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) + mock_apply_async_interceptors.assert_called_once_with( + mock_channel, transport._client_options + ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel diff --git a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py index a8eff4f6e0f9..790030306491 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py @@ -827,7 +827,7 @@ def test_config_service_v2_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(options) + mock_get_interceptor.assert_called_once_with(transport._client_options) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -901,7 +901,9 @@ def test_config_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) + mock_apply_async_interceptors.assert_called_once_with( + mock_channel, transport._client_options + ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel diff --git a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py index 73f91e151b66..a123afd9d2c2 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py @@ -828,7 +828,7 @@ def test_logging_service_v2_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(options) + mock_get_interceptor.assert_called_once_with(transport._client_options) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -902,7 +902,9 @@ def test_logging_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) + mock_apply_async_interceptors.assert_called_once_with( + mock_channel, transport._client_options + ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel diff --git a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py index 7ce9cbeacc63..5eee17c5025f 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py @@ -826,7 +826,7 @@ def test_metrics_service_v2_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(options) + mock_get_interceptor.assert_called_once_with(transport._client_options) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -900,7 +900,9 @@ def test_metrics_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) + mock_apply_async_interceptors.assert_called_once_with( + mock_channel, transport._client_options + ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py index daa7b8a9a8d2..1bbf2dd15340 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py @@ -827,7 +827,7 @@ def test_config_service_v2_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(options) + mock_get_interceptor.assert_called_once_with(transport._client_options) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -901,7 +901,9 @@ def test_config_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) + mock_apply_async_interceptors.assert_called_once_with( + mock_channel, transport._client_options + ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py index 73f91e151b66..a123afd9d2c2 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py @@ -828,7 +828,7 @@ def test_logging_service_v2_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(options) + mock_get_interceptor.assert_called_once_with(transport._client_options) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -902,7 +902,9 @@ def test_logging_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) + mock_apply_async_interceptors.assert_called_once_with( + mock_channel, transport._client_options + ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py index 236343bae84e..5430c5a16ea9 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py @@ -826,7 +826,7 @@ def test_metrics_service_v2_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(options) + mock_get_interceptor.assert_called_once_with(transport._client_options) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -900,7 +900,9 @@ def test_metrics_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) + mock_apply_async_interceptors.assert_called_once_with( + mock_channel, transport._client_options + ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel diff --git a/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py b/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py index 0a084852e9fd..eaf5fc2b0a60 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py @@ -859,7 +859,7 @@ def test_cloud_redis_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(options) + mock_get_interceptor.assert_called_once_with(transport._client_options) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -933,7 +933,9 @@ def test_cloud_redis_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) + mock_apply_async_interceptors.assert_called_once_with( + mock_channel, transport._client_options + ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py index 061d60591136..ad69829602f9 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py @@ -859,7 +859,7 @@ def test_cloud_redis_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(options) + mock_get_interceptor.assert_called_once_with(transport._client_options) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -933,7 +933,9 @@ def test_cloud_redis_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) + mock_apply_async_interceptors.assert_called_once_with( + mock_channel, transport._client_options + ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_echo.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_echo.py index 6694d584d0f2..d28b4688187d 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_echo.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_echo.py @@ -1148,7 +1148,7 @@ def test_echo_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(options) + mock_get_interceptor.assert_called_once_with(transport._client_options) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -1222,7 +1222,9 @@ def test_echo_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) + mock_apply_async_interceptors.assert_called_once_with( + mock_channel, transport._client_options + ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_identity.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_identity.py index e06e5cd3203f..bbe163a03bb1 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_identity.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_identity.py @@ -849,7 +849,7 @@ def test_identity_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(options) + mock_get_interceptor.assert_called_once_with(transport._client_options) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -923,7 +923,9 @@ def test_identity_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) + mock_apply_async_interceptors.assert_called_once_with( + mock_channel, transport._client_options + ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_messaging.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_messaging.py index e26295547198..fbbbcac1f4a4 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_messaging.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_messaging.py @@ -853,7 +853,7 @@ def test_messaging_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(options) + mock_get_interceptor.assert_called_once_with(transport._client_options) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -927,7 +927,9 @@ def test_messaging_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) + mock_apply_async_interceptors.assert_called_once_with( + mock_channel, transport._client_options + ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py index c7f22d5df1bd..8e17b5ccb41e 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py @@ -854,7 +854,7 @@ def test_resumable_upload_service_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(options) + mock_get_interceptor.assert_called_once_with(transport._client_options) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -928,7 +928,9 @@ def test_resumable_upload_service_grpc_asyncio_transport_otel_channel_intercepto client_options=options, ) - mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) + mock_apply_async_interceptors.assert_called_once_with( + mock_channel, transport._client_options + ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_sequence_service.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_sequence_service.py index 992e6a7abe4c..79303aefe964 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_sequence_service.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_sequence_service.py @@ -850,7 +850,7 @@ def test_sequence_service_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(options) + mock_get_interceptor.assert_called_once_with(transport._client_options) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -924,7 +924,9 @@ def test_sequence_service_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) + mock_apply_async_interceptors.assert_called_once_with( + mock_channel, transport._client_options + ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_testing.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_testing.py index 8f63d368ffd9..7bc47a2be1b5 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_testing.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_testing.py @@ -847,7 +847,7 @@ def test_testing_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(options) + mock_get_interceptor.assert_called_once_with(transport._client_options) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -921,7 +921,9 @@ def test_testing_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) + mock_apply_async_interceptors.assert_called_once_with( + mock_channel, transport._client_options + ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py index 5bd071d96d84..bf810d75835e 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py @@ -851,7 +851,7 @@ def test_storage_batch_operations_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(options) + mock_get_interceptor.assert_called_once_with(transport._client_options) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -925,7 +925,9 @@ def test_storage_batch_operations_grpc_asyncio_transport_otel_channel_intercepto client_options=options, ) - mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) + mock_apply_async_interceptors.assert_called_once_with( + mock_channel, transport._client_options + ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel From 583e3f3265a16a4fbc041f13c3bccc1038fa81f5 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Fri, 30 Oct 2026 17:54:58 -0400 Subject: [PATCH 4/7] refactor(observability): thread default_host explicitly instead of mutating client_options.api_endpoint --- .../%name_%version/%sub/_compat.py.j2 | 9 + .../services/%service/transports/base.py.j2 | 35 ++-- .../services/%service/transports/grpc.py.j2 | 11 +- .../%service/transports/grpc_asyncio.py.j2 | 11 +- .../%name_%version/%sub/test_%service.py.j2 | 198 ++++++++++++------ .../asset/google/cloud/asset_v1/_compat.py | 9 + .../services/asset_service/transports/base.py | 35 ++-- .../services/asset_service/transports/grpc.py | 11 +- .../asset_service/transports/grpc_asyncio.py | 11 +- .../unit/gapic/asset_v1/test_asset_service.py | 198 ++++++++++++------ .../google/iam/credentials_v1/_compat.py | 9 + .../iam_credentials/transports/base.py | 35 ++-- .../iam_credentials/transports/grpc.py | 11 +- .../transports/grpc_asyncio.py | 11 +- .../credentials_v1/test_iam_credentials.py | 198 ++++++++++++------ .../google/cloud/eventarc_v1/_compat.py | 9 + .../services/eventarc/transports/base.py | 35 ++-- .../services/eventarc/transports/grpc.py | 11 +- .../eventarc/transports/grpc_asyncio.py | 11 +- .../unit/gapic/eventarc_v1/test_eventarc.py | 198 ++++++++++++------ .../google/cloud/logging_v2/_compat.py | 9 + .../config_service_v2/transports/base.py | 35 ++-- .../config_service_v2/transports/grpc.py | 11 +- .../transports/grpc_asyncio.py | 11 +- .../logging_service_v2/transports/base.py | 35 ++-- .../logging_service_v2/transports/grpc.py | 11 +- .../transports/grpc_asyncio.py | 11 +- .../metrics_service_v2/transports/base.py | 35 ++-- .../metrics_service_v2/transports/grpc.py | 11 +- .../transports/grpc_asyncio.py | 11 +- .../logging_v2/test_config_service_v2.py | 198 ++++++++++++------ .../logging_v2/test_logging_service_v2.py | 198 ++++++++++++------ .../logging_v2/test_metrics_service_v2.py | 198 ++++++++++++------ .../google/cloud/logging_v2/_compat.py | 9 + .../config_service_v2/transports/base.py | 35 ++-- .../config_service_v2/transports/grpc.py | 11 +- .../transports/grpc_asyncio.py | 11 +- .../logging_service_v2/transports/base.py | 35 ++-- .../logging_service_v2/transports/grpc.py | 11 +- .../transports/grpc_asyncio.py | 11 +- .../metrics_service_v2/transports/base.py | 35 ++-- .../metrics_service_v2/transports/grpc.py | 11 +- .../transports/grpc_asyncio.py | 11 +- .../logging_v2/test_config_service_v2.py | 198 ++++++++++++------ .../logging_v2/test_logging_service_v2.py | 198 ++++++++++++------ .../logging_v2/test_metrics_service_v2.py | 198 ++++++++++++------ .../redis/google/cloud/redis_v1/_compat.py | 9 + .../services/cloud_redis/transports/base.py | 35 ++-- .../services/cloud_redis/transports/grpc.py | 11 +- .../cloud_redis/transports/grpc_asyncio.py | 11 +- .../unit/gapic/redis_v1/test_cloud_redis.py | 198 ++++++++++++------ .../google/cloud/redis_v1/_compat.py | 9 + .../services/cloud_redis/transports/base.py | 35 ++-- .../services/cloud_redis/transports/grpc.py | 11 +- .../cloud_redis/transports/grpc_asyncio.py | 11 +- .../unit/gapic/redis_v1/test_cloud_redis.py | 198 ++++++++++++------ .../google/showcase_v1beta1/_compat.py | 9 + .../services/echo/transports/base.py | 35 ++-- .../services/echo/transports/grpc.py | 11 +- .../services/echo/transports/grpc_asyncio.py | 11 +- .../services/identity/transports/base.py | 35 ++-- .../services/identity/transports/grpc.py | 11 +- .../identity/transports/grpc_asyncio.py | 11 +- .../services/messaging/transports/base.py | 35 ++-- .../services/messaging/transports/grpc.py | 11 +- .../messaging/transports/grpc_asyncio.py | 11 +- .../transports/base.py | 35 ++-- .../transports/grpc.py | 11 +- .../transports/grpc_asyncio.py | 11 +- .../sequence_service/transports/base.py | 35 ++-- .../sequence_service/transports/grpc.py | 11 +- .../transports/grpc_asyncio.py | 11 +- .../services/testing/transports/base.py | 35 ++-- .../services/testing/transports/grpc.py | 11 +- .../testing/transports/grpc_asyncio.py | 11 +- .../unit/gapic/showcase_v1beta1/test_echo.py | 198 ++++++++++++------ .../gapic/showcase_v1beta1/test_identity.py | 198 ++++++++++++------ .../gapic/showcase_v1beta1/test_messaging.py | 198 ++++++++++++------ .../test_resumable_upload_service.py | 198 ++++++++++++------ .../showcase_v1beta1/test_sequence_service.py | 198 ++++++++++++------ .../gapic/showcase_v1beta1/test_testing.py | 198 ++++++++++++------ .../storagebatchoperations_v1/_compat.py | 9 + .../transports/base.py | 35 ++-- .../transports/grpc.py | 11 +- .../transports/grpc_asyncio.py | 11 +- .../test_storage_batch_operations.py | 198 ++++++++++++------ .../tests/system/test_span_compliance.py | 17 ++ .../google/api_core/_observability.py | 34 ++- .../google/api_core/gapic_v1/method.py | 9 +- .../google/api_core/gapic_v1/method_async.py | 4 + .../tests/asyncio/gapic/test_method_async.py | 27 +++ .../tests/unit/gapic/test_method.py | 27 +++ .../tests/unit/test_observability.py | 85 ++++++++ 93 files changed, 3382 insertions(+), 1756 deletions(-) diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 index 39f188d6b403..c0ca7c50e49e 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 @@ -99,6 +99,15 @@ ASYNC_WRAP_METHOD_SUPPORTS_KIND = WRAP_METHOD_SUPPORTS_TRACING or hasattr( gapic_v1.method_async, "_DEFAULT_ASYNC_TRANSPORT_KIND" ) +# The `default_host` parameter in gapic_v1.method.wrap_method, +# gapic_v1.method_async.wrap_method, _observability.get_otel_interceptor, and +# _observability.apply_otel_async_channel_interceptors allows transports to pass +# their resolved default host without mutating caller-owned client_options. +SUPPORTS_DEFAULT_HOST = bool( + getattr(gapic_v1.method.wrap_method, "__kwdefaults__", None) + and "default_host" in gapic_v1.method.wrap_method.__kwdefaults__ +) + try: from google.api_core import grpc_helpers except ImportError: # pragma: NO COVER diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 index 9546b58ca1fb..8e8342dc75e8 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 @@ -3,14 +3,13 @@ {% block content %} import abc -import copy from typing import {% if service.any_extended_operations_methods %}Any, {% endif %}Awaitable, Callable, Dict, Optional, Sequence, Union {% set package_path = api.naming.module_namespace|join('.') + "." + api.naming.versioned_module_name %} from {{package_path}} import gapic_version as package_version from {{package_path}}._compat import ( - _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) _ASYNC_WRAP_METHOD_SUPPORTS_TRACING = _WRAP_METHOD_SUPPORTS_TRACING @@ -164,25 +163,13 @@ class {{ service.name }}Transport(abc.ABC): host += ':443' self._host = host - # If the transport was instantiated directly with client_options=None - # while OpenTelemetry tracing is enabled via environment variable, - # create a ClientOptions instance with the resolved transport host so - # method and wire spans can record server.address and server.port. - if ( - client_options is None - and _observability is not None - and _observability.is_otel_capabilities_enabled(None) - ): - client_options = client_options_lib.ClientOptions(api_endpoint=self._host) - # And here we check for the presence of an api_endpoint attr OR key, - # shallow-copying before populating to avoid mutating caller-owned state. - elif isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: - client_options = copy.copy(client_options) - client_options.api_endpoint = self._host - elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): - client_options = copy.copy(client_options) - client_options["api_endpoint"] = self._host self._client_options = client_options + # If a pre-constructed gRPC channel was provided to the subclass before + # super().__init__, self._host is not used to create the channel and + # cannot be trusted as the default span endpoint. + self._default_host: Optional[str] = ( + None if getattr(self, "_grpc_channel", None) is not None else self._host + ) self._wrapped_methods: Dict[Callable, Callable] = {} @property @@ -201,6 +188,8 @@ class {{ service.name }}Transport(abc.ABC): """ if _WRAP_METHOD_SUPPORTS_TRACING: kwargs["client_options"] = self._client_options + if _SUPPORTS_DEFAULT_HOST: + kwargs["default_host"] = self._default_host try: kind = self.kind except NotImplementedError: @@ -213,7 +202,7 @@ class {{ service.name }}Transport(abc.ABC): # - Era 2 (>= 2.29.0, < 2.36.0): `kind` is supported (prevents REST from # falling back to gRPC wrapping), but tracing args are not. # (These fallbacks can be removed once google-api-core < 2.36.0 is no longer supported.) - for k in ["client_options", "method_name", "is_streaming"]: + for k in ["client_options", "method_name", "is_streaming", "default_host"]: kwargs.pop(k, None) try: @@ -240,6 +229,8 @@ class {{ service.name }}Transport(abc.ABC): """ if _ASYNC_WRAP_METHOD_SUPPORTS_TRACING: kwargs["client_options"] = self._client_options + if _SUPPORTS_DEFAULT_HOST: + kwargs["default_host"] = self._default_host try: kind = self.kind except NotImplementedError: @@ -252,7 +243,7 @@ class {{ service.name }}Transport(abc.ABC): # - Era 2 (>= 2.29.0, < 2.36.0): `kind` is supported (prevents REST from # falling back to gRPC wrapping), but tracing args are not. # (These fallbacks can be removed once google-api-core < 2.36.0 is no longer supported.) - for k in ["client_options", "method_name", "is_streaming"]: + for k in ["client_options", "method_name", "is_streaming", "default_host"]: kwargs.pop(k, None) try: diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 index c6ea02ef3317..c424ff95dcb8 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 @@ -12,7 +12,11 @@ from typing import Callable, Dict, Optional, Sequence, Tuple, Union from typing import TYPE_CHECKING {% set package_path = api.naming.module_namespace|join('.') + "." + api.naming.versioned_module_name %} -from {{package_path}}._compat import _observability, apply_channel_interceptors +from {{package_path}}._compat import ( + _observability, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, + apply_channel_interceptors, +) from google.api_core import client_options as client_options_lib {% if service.has_resumable_upload_methods %} @@ -287,9 +291,12 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport): ) channel_interceptors = list(interceptors) if interceptors else [] + _otel_interceptor_kwargs = ( + {"default_host": self._default_host} if _SUPPORTS_DEFAULT_HOST else {} + ) if ( _observability is not None - and (otel_interceptor := _observability.get_otel_interceptor(self._client_options)) is not None + and (otel_interceptor := _observability.get_otel_interceptor(self._client_options, **_otel_interceptor_kwargs)) is not None and otel_interceptor not in channel_interceptors and not any(getattr(i, "_is_otel_interceptor", None) is True for i in channel_interceptors) ): diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 index 7e35880bfd7e..1e10b95e5ddf 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 @@ -13,7 +13,11 @@ from typing import {% if service.has_resumable_upload_methods %}Any, {% endif %} from typing import TYPE_CHECKING {% set package_path = api.naming.module_namespace|join('.') + "." + api.naming.versioned_module_name %} -from {{package_path}}._compat import _observability, apply_async_channel_interceptors +from {{package_path}}._compat import ( + _observability, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, + apply_async_channel_interceptors, +) from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 @@ -349,8 +353,11 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport): # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= v2.43.0. + _otel_interceptor_kwargs = ( + {"default_host": self._default_host} if _SUPPORTS_DEFAULT_HOST else {} + ) # pragma: NO COVER self._grpc_channel = _observability.apply_otel_async_channel_interceptors( - self._grpc_channel, self._client_options + self._grpc_channel, self._client_options, **_otel_interceptor_kwargs ) # pragma: NO COVER self._logged_channel = self._grpc_channel diff --git a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 index dd69c549fbac..1362dd5c2a5c 100644 --- a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 +++ b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 @@ -996,7 +996,14 @@ def test_{{ service.name|snake_case }}_grpc_transport_otel_channel_interceptor() client_options=options, ) - mock_get_interceptor.assert_called_once_with(transport._client_options) + expected_kwargs = ( + {"default_host": transport._default_host} + if transports.grpc._SUPPORTS_DEFAULT_HOST + else {} + ) + mock_get_interceptor.assert_called_once_with( + transport._client_options, **expected_kwargs + ) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -1070,8 +1077,13 @@ def test_{{ service.name|snake_case }}_grpc_asyncio_transport_otel_channel_inter client_options=options, ) + expected_kwargs = ( + {"default_host": transport._default_host} + if transports.grpc_asyncio._SUPPORTS_DEFAULT_HOST + else {} + ) mock_apply_async_interceptors.assert_called_once_with( - mock_channel, transport._client_options + mock_channel, transport._client_options, **expected_kwargs ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel @@ -2477,64 +2489,73 @@ def test_api_key_credentials(client_class, transport_class): ) -def test_{{ service.name|snake_case }}_base_transport_wrap_method_modern_api_core(monkeypatch): +@pytest.mark.parametrize( + "supports_default_host,is_concrete,expected_kind,expected_has_default_host", + [ + pytest.param( + True, + True, + "{{ opts.transport[0] }}", + True, + id="concrete_transport_supports_default_host", + ), + pytest.param( + False, + False, + None, + False, + id="base_transport_no_default_host", + ), + ], +) +def test_{{ service.name|snake_case }}_base_transport_wrap_method_modern_api_core( + monkeypatch, + supports_default_host, + is_concrete, + expected_kind, + expected_has_default_host, +): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", True) - # 1. Concrete transport: passes client_options=None and kind="{{ opts.transport[0] }}" + monkeypatch.setattr(transports.base, "_SUPPORTS_DEFAULT_HOST", supports_default_host) + transport_cls = ( + transports.{{ service.name }}{{ opts.transport[0].capitalize() }}Transport + if is_concrete + else transports.{{ service.name }}Transport + ) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - transport = transports.{{ service.name }}{{ opts.transport[0].capitalize() }}Transport( - credentials=ga_credentials.AnonymousCredentials() - ) + transport = transport_cls(credentials=ga_credentials.AnonymousCredentials()) func = mock.MagicMock() transport._wrap_method(func) - assert mock_wrap.call_args.kwargs["kind"] == "{{ opts.transport[0] }}" - assert "client_options" in mock_wrap.call_args.kwargs - - # 2. Base transport: kind raises NotImplementedError and is cleanly omitted - with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - base_transport = transports.{{ service.name }}Transport( - credentials=ga_credentials.AnonymousCredentials() - ) - func = mock.MagicMock() - base_transport._wrap_method(func) - assert "kind" not in mock_wrap.call_args.kwargs + if expected_kind: + assert mock_wrap.call_args.kwargs["kind"] == expected_kind + else: + assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs + if expected_has_default_host: + assert mock_wrap.call_args.kwargs["default_host"] == transport._default_host + else: + assert "default_host" not in mock_wrap.call_args.kwargs -@pytest.mark.parametrize( - "otel_enabled,expected_endpoint", - [ - pytest.param(False, None, id="otel_disabled"), - pytest.param(True, "default.example.com:443", id="otel_enabled"), - ], -) -def test_{{ service.name|snake_case }}_base_transport_client_options_none( - monkeypatch, otel_enabled, expected_endpoint -): - fake_observability = mock.MagicMock() - fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled - monkeypatch.setattr(transports.base, "_observability", fake_observability) - +def test_{{ service.name|snake_case }}_base_transport_client_options_none(): transport = transports.{{ service.name }}Transport( host="default.example.com:443", credentials=ga_credentials.AnonymousCredentials(), client_options=None, ) - assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + assert transport._client_options is None + assert transport._default_host == "default.example.com:443" @pytest.mark.parametrize( - "initial_endpoint,expected_endpoint", + "initial_endpoint", [ - pytest.param(None, "default.example.com:443", id="default_from_host"), - pytest.param( - "custom.example.com:443", - "custom.example.com:443", - id="preserve_explicit", - ), + pytest.param(None, id="unset_api_endpoint"), + pytest.param("custom.example.com:443", id="explicit_api_endpoint"), ], ) def test_{{ service.name|snake_case }}_base_transport_client_options_object( - initial_endpoint, expected_endpoint + initial_endpoint, ): options = client_options.ClientOptions(api_endpoint=initial_endpoint) transport = transports.{{ service.name }}Transport( @@ -2542,27 +2563,28 @@ def test_{{ service.name|snake_case }}_base_transport_client_options_object( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options.api_endpoint == expected_endpoint - assert options.api_endpoint == initial_endpoint + assert transport._client_options is options + assert transport._client_options.api_endpoint == initial_endpoint + assert transport._default_host == "default.example.com:443" @pytest.mark.parametrize( - "initial_options,expected_options", + "initial_options,expected_endpoint", [ pytest.param( {}, - {"api_endpoint": "default.example.com:443"}, - id="default_from_host", + None, + id="unset_api_endpoint", ), pytest.param( {"api_endpoint": "custom.example.com:443"}, - {"api_endpoint": "custom.example.com:443"}, - id="preserve_explicit", + "custom.example.com:443", + id="explicit_api_endpoint", ), ], ) def test_{{ service.name|snake_case }}_base_transport_client_options_dict( - initial_options, expected_options + initial_options, expected_endpoint ): options = dict(initial_options) transport = transports.{{ service.name }}Transport( @@ -2570,10 +2592,29 @@ def test_{{ service.name|snake_case }}_base_transport_client_options_dict( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options == expected_options + assert transport._client_options is options + assert transport._client_options.get("api_endpoint") == expected_endpoint + assert transport._default_host == "default.example.com:443" assert options == initial_options +def test_{{ service.name|snake_case }}_base_transport_default_host_ignored_when_custom_grpc_channel(): + class _CustomChannelTransport(transports.{{ service.name }}Transport): + def __init__(self, **kwargs): + self._ignore_credentials = True + self._grpc_channel = mock.sentinel.custom_channel + super().__init__(**kwargs) + + options = client_options.ClientOptions() + transport = _CustomChannelTransport( + host="default.example.com:443", + client_options=options, + ) + assert transport._client_options is options + assert transport._client_options.api_endpoint is None + assert transport._default_host is None + + def test_{{ service.name|snake_case }}_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): @@ -2649,27 +2690,52 @@ def test_{{ service.name|snake_case }}_base_transport_wrap_method_older_api_core {% if 'grpc' in opts.transport %} -def test_{{ service.name|snake_case }}_base_transport_wrap_async_method_modern_api_core(monkeypatch): +@pytest.mark.parametrize( + "supports_default_host,is_concrete,expected_kind,expected_has_default_host", + [ + pytest.param( + True, + True, + "grpc_asyncio", + True, + id="concrete_transport_supports_default_host", + ), + pytest.param( + False, + False, + None, + False, + id="base_transport_no_default_host", + ), + ], +) +def test_{{ service.name|snake_case }}_base_transport_wrap_async_method_modern_api_core( + monkeypatch, + supports_default_host, + is_concrete, + expected_kind, + expected_has_default_host, +): monkeypatch.setattr(transports.base, "_ASYNC_WRAP_METHOD_SUPPORTS_TRACING", True) - # 1. Concrete transport: passes client_options=None and kind="grpc_asyncio" + monkeypatch.setattr(transports.base, "_SUPPORTS_DEFAULT_HOST", supports_default_host) + transport_cls = ( + transports.{{ service.grpc_asyncio_transport_name }} + if is_concrete + else transports.{{ service.name }}Transport + ) with mock.patch.object(transports.base.gapic_v1.method_async, "wrap_method") as mock_wrap: - transport = transports.{{ service.grpc_asyncio_transport_name }}( - credentials=ga_credentials.AnonymousCredentials() - ) + transport = transport_cls(credentials=ga_credentials.AnonymousCredentials()) func = mock.MagicMock() transport._wrap_async_method(func) - assert mock_wrap.call_args.kwargs["kind"] == "grpc_asyncio" - assert "client_options" in mock_wrap.call_args.kwargs - - # 2. Base transport: kind raises NotImplementedError and is cleanly omitted - with mock.patch.object(transports.base.gapic_v1.method_async, "wrap_method") as mock_wrap: - base_transport = transports.{{ service.name }}Transport( - credentials=ga_credentials.AnonymousCredentials() - ) - func = mock.MagicMock() - base_transport._wrap_async_method(func) - assert "kind" not in mock_wrap.call_args.kwargs + if expected_kind: + assert mock_wrap.call_args.kwargs["kind"] == expected_kind + else: + assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs + if expected_has_default_host: + assert mock_wrap.call_args.kwargs["default_host"] == transport._default_host + else: + assert "default_host" not in mock_wrap.call_args.kwargs def test_{{ service.name|snake_case }}_base_transport_wrap_async_method_older_api_core_fallbacks(monkeypatch): diff --git a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/_compat.py b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/_compat.py index a4347392b042..86424516e0a2 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/_compat.py @@ -90,6 +90,15 @@ def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: # t gapic_v1.method_async, "_DEFAULT_ASYNC_TRANSPORT_KIND" ) +# The `default_host` parameter in gapic_v1.method.wrap_method, +# gapic_v1.method_async.wrap_method, _observability.get_otel_interceptor, and +# _observability.apply_otel_async_channel_interceptors allows transports to pass +# their resolved default host without mutating caller-owned client_options. +SUPPORTS_DEFAULT_HOST = bool( + getattr(gapic_v1.method.wrap_method, "__kwdefaults__", None) + and "default_host" in gapic_v1.method.wrap_method.__kwdefaults__ +) + try: from google.api_core import grpc_helpers except ImportError: # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/base.py b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/base.py index a40ac40c6553..98cf2801fa4f 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/base.py @@ -14,13 +14,12 @@ # limitations under the License. # import abc -import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.asset_v1 import gapic_version as package_version from google.cloud.asset_v1._compat import ( - _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) _ASYNC_WRAP_METHOD_SUPPORTS_TRACING = _WRAP_METHOD_SUPPORTS_TRACING @@ -135,25 +134,13 @@ def __init__( host += ':443' self._host = host - # If the transport was instantiated directly with client_options=None - # while OpenTelemetry tracing is enabled via environment variable, - # create a ClientOptions instance with the resolved transport host so - # method and wire spans can record server.address and server.port. - if ( - client_options is None - and _observability is not None - and _observability.is_otel_capabilities_enabled(None) - ): - client_options = client_options_lib.ClientOptions(api_endpoint=self._host) - # And here we check for the presence of an api_endpoint attr OR key, - # shallow-copying before populating to avoid mutating caller-owned state. - elif isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: - client_options = copy.copy(client_options) - client_options.api_endpoint = self._host - elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): - client_options = copy.copy(client_options) - client_options["api_endpoint"] = self._host self._client_options = client_options + # If a pre-constructed gRPC channel was provided to the subclass before + # super().__init__, self._host is not used to create the channel and + # cannot be trusted as the default span endpoint. + self._default_host: Optional[str] = ( + None if getattr(self, "_grpc_channel", None) is not None else self._host + ) self._wrapped_methods: Dict[Callable, Callable] = {} @property @@ -172,6 +159,8 @@ def _wrap_method(self, func, *args, **kwargs): """ if _WRAP_METHOD_SUPPORTS_TRACING: kwargs["client_options"] = self._client_options + if _SUPPORTS_DEFAULT_HOST: + kwargs["default_host"] = self._default_host try: kind = self.kind except NotImplementedError: @@ -184,7 +173,7 @@ def _wrap_method(self, func, *args, **kwargs): # - Era 2 (>= 2.29.0, < 2.36.0): `kind` is supported (prevents REST from # falling back to gRPC wrapping), but tracing args are not. # (These fallbacks can be removed once google-api-core < 2.36.0 is no longer supported.) - for k in ["client_options", "method_name", "is_streaming"]: + for k in ["client_options", "method_name", "is_streaming", "default_host"]: kwargs.pop(k, None) try: @@ -211,6 +200,8 @@ def _wrap_async_method(self, func, *args, **kwargs): """ if _ASYNC_WRAP_METHOD_SUPPORTS_TRACING: kwargs["client_options"] = self._client_options + if _SUPPORTS_DEFAULT_HOST: + kwargs["default_host"] = self._default_host try: kind = self.kind except NotImplementedError: @@ -223,7 +214,7 @@ def _wrap_async_method(self, func, *args, **kwargs): # - Era 2 (>= 2.29.0, < 2.36.0): `kind` is supported (prevents REST from # falling back to gRPC wrapping), but tracing args are not. # (These fallbacks can be removed once google-api-core < 2.36.0 is no longer supported.) - for k in ["client_options", "method_name", "is_streaming"]: + for k in ["client_options", "method_name", "is_streaming", "default_host"]: kwargs.pop(k, None) try: diff --git a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/grpc.py index 53964d2306d1..f2a7e6d3af1c 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/grpc.py @@ -20,7 +20,11 @@ from typing import Callable, Dict, Optional, Sequence, Tuple, Union from typing import TYPE_CHECKING -from google.cloud.asset_v1._compat import _observability, apply_channel_interceptors +from google.cloud.asset_v1._compat import ( + _observability, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, + apply_channel_interceptors, +) from google.api_core import client_options as client_options_lib from google.api_core import grpc_helpers @@ -277,9 +281,12 @@ def __init__(self, *, ) channel_interceptors = list(interceptors) if interceptors else [] + _otel_interceptor_kwargs = ( + {"default_host": self._default_host} if _SUPPORTS_DEFAULT_HOST else {} + ) if ( _observability is not None - and (otel_interceptor := _observability.get_otel_interceptor(self._client_options)) is not None + and (otel_interceptor := _observability.get_otel_interceptor(self._client_options, **_otel_interceptor_kwargs)) is not None and otel_interceptor not in channel_interceptors and not any(getattr(i, "_is_otel_interceptor", None) is True for i in channel_interceptors) ): diff --git a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/grpc_asyncio.py index 2d4465adcf4a..c74319862264 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/grpc_asyncio.py @@ -20,7 +20,11 @@ from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union from typing import TYPE_CHECKING -from google.cloud.asset_v1._compat import _observability, apply_async_channel_interceptors +from google.cloud.asset_v1._compat import ( + _observability, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, + apply_async_channel_interceptors, +) from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 @@ -335,8 +339,11 @@ def __init__(self, *, # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= v2.43.0. + _otel_interceptor_kwargs = ( + {"default_host": self._default_host} if _SUPPORTS_DEFAULT_HOST else {} + ) # pragma: NO COVER self._grpc_channel = _observability.apply_otel_async_channel_interceptors( - self._grpc_channel, self._client_options + self._grpc_channel, self._client_options, **_otel_interceptor_kwargs ) # pragma: NO COVER self._logged_channel = self._grpc_channel diff --git a/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py b/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py index a31872a61b88..28f2a829b425 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py +++ b/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py @@ -851,7 +851,14 @@ def test_asset_service_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(transport._client_options) + expected_kwargs = ( + {"default_host": transport._default_host} + if transports.grpc._SUPPORTS_DEFAULT_HOST + else {} + ) + mock_get_interceptor.assert_called_once_with( + transport._client_options, **expected_kwargs + ) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -925,8 +932,13 @@ def test_asset_service_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) + expected_kwargs = ( + {"default_host": transport._default_host} + if transports.grpc_asyncio._SUPPORTS_DEFAULT_HOST + else {} + ) mock_apply_async_interceptors.assert_called_once_with( - mock_channel, transport._client_options + mock_channel, transport._client_options, **expected_kwargs ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel @@ -18412,64 +18424,73 @@ def test_api_key_credentials(client_class, transport_class): ) -def test_asset_service_base_transport_wrap_method_modern_api_core(monkeypatch): +@pytest.mark.parametrize( + "supports_default_host,is_concrete,expected_kind,expected_has_default_host", + [ + pytest.param( + True, + True, + "grpc", + True, + id="concrete_transport_supports_default_host", + ), + pytest.param( + False, + False, + None, + False, + id="base_transport_no_default_host", + ), + ], +) +def test_asset_service_base_transport_wrap_method_modern_api_core( + monkeypatch, + supports_default_host, + is_concrete, + expected_kind, + expected_has_default_host, +): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", True) - # 1. Concrete transport: passes client_options=None and kind="grpc" + monkeypatch.setattr(transports.base, "_SUPPORTS_DEFAULT_HOST", supports_default_host) + transport_cls = ( + transports.AssetServiceGrpcTransport + if is_concrete + else transports.AssetServiceTransport + ) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - transport = transports.AssetServiceGrpcTransport( - credentials=ga_credentials.AnonymousCredentials() - ) + transport = transport_cls(credentials=ga_credentials.AnonymousCredentials()) func = mock.MagicMock() transport._wrap_method(func) - assert mock_wrap.call_args.kwargs["kind"] == "grpc" - assert "client_options" in mock_wrap.call_args.kwargs - - # 2. Base transport: kind raises NotImplementedError and is cleanly omitted - with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - base_transport = transports.AssetServiceTransport( - credentials=ga_credentials.AnonymousCredentials() - ) - func = mock.MagicMock() - base_transport._wrap_method(func) - assert "kind" not in mock_wrap.call_args.kwargs + if expected_kind: + assert mock_wrap.call_args.kwargs["kind"] == expected_kind + else: + assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs + if expected_has_default_host: + assert mock_wrap.call_args.kwargs["default_host"] == transport._default_host + else: + assert "default_host" not in mock_wrap.call_args.kwargs -@pytest.mark.parametrize( - "otel_enabled,expected_endpoint", - [ - pytest.param(False, None, id="otel_disabled"), - pytest.param(True, "default.example.com:443", id="otel_enabled"), - ], -) -def test_asset_service_base_transport_client_options_none( - monkeypatch, otel_enabled, expected_endpoint -): - fake_observability = mock.MagicMock() - fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled - monkeypatch.setattr(transports.base, "_observability", fake_observability) - +def test_asset_service_base_transport_client_options_none(): transport = transports.AssetServiceTransport( host="default.example.com:443", credentials=ga_credentials.AnonymousCredentials(), client_options=None, ) - assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + assert transport._client_options is None + assert transport._default_host == "default.example.com:443" @pytest.mark.parametrize( - "initial_endpoint,expected_endpoint", + "initial_endpoint", [ - pytest.param(None, "default.example.com:443", id="default_from_host"), - pytest.param( - "custom.example.com:443", - "custom.example.com:443", - id="preserve_explicit", - ), + pytest.param(None, id="unset_api_endpoint"), + pytest.param("custom.example.com:443", id="explicit_api_endpoint"), ], ) def test_asset_service_base_transport_client_options_object( - initial_endpoint, expected_endpoint + initial_endpoint, ): options = client_options.ClientOptions(api_endpoint=initial_endpoint) transport = transports.AssetServiceTransport( @@ -18477,27 +18498,28 @@ def test_asset_service_base_transport_client_options_object( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options.api_endpoint == expected_endpoint - assert options.api_endpoint == initial_endpoint + assert transport._client_options is options + assert transport._client_options.api_endpoint == initial_endpoint + assert transport._default_host == "default.example.com:443" @pytest.mark.parametrize( - "initial_options,expected_options", + "initial_options,expected_endpoint", [ pytest.param( {}, - {"api_endpoint": "default.example.com:443"}, - id="default_from_host", + None, + id="unset_api_endpoint", ), pytest.param( {"api_endpoint": "custom.example.com:443"}, - {"api_endpoint": "custom.example.com:443"}, - id="preserve_explicit", + "custom.example.com:443", + id="explicit_api_endpoint", ), ], ) def test_asset_service_base_transport_client_options_dict( - initial_options, expected_options + initial_options, expected_endpoint ): options = dict(initial_options) transport = transports.AssetServiceTransport( @@ -18505,10 +18527,29 @@ def test_asset_service_base_transport_client_options_dict( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options == expected_options + assert transport._client_options is options + assert transport._client_options.get("api_endpoint") == expected_endpoint + assert transport._default_host == "default.example.com:443" assert options == initial_options +def test_asset_service_base_transport_default_host_ignored_when_custom_grpc_channel(): + class _CustomChannelTransport(transports.AssetServiceTransport): + def __init__(self, **kwargs): + self._ignore_credentials = True + self._grpc_channel = mock.sentinel.custom_channel + super().__init__(**kwargs) + + options = client_options.ClientOptions() + transport = _CustomChannelTransport( + host="default.example.com:443", + client_options=options, + ) + assert transport._client_options is options + assert transport._client_options.api_endpoint is None + assert transport._default_host is None + + def test_asset_service_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): @@ -18583,27 +18624,52 @@ def test_asset_service_base_transport_wrap_method_older_api_core_fallbacks(monke assert "is_streaming" not in kwargs -def test_asset_service_base_transport_wrap_async_method_modern_api_core(monkeypatch): +@pytest.mark.parametrize( + "supports_default_host,is_concrete,expected_kind,expected_has_default_host", + [ + pytest.param( + True, + True, + "grpc_asyncio", + True, + id="concrete_transport_supports_default_host", + ), + pytest.param( + False, + False, + None, + False, + id="base_transport_no_default_host", + ), + ], +) +def test_asset_service_base_transport_wrap_async_method_modern_api_core( + monkeypatch, + supports_default_host, + is_concrete, + expected_kind, + expected_has_default_host, +): monkeypatch.setattr(transports.base, "_ASYNC_WRAP_METHOD_SUPPORTS_TRACING", True) - # 1. Concrete transport: passes client_options=None and kind="grpc_asyncio" + monkeypatch.setattr(transports.base, "_SUPPORTS_DEFAULT_HOST", supports_default_host) + transport_cls = ( + transports.AssetServiceGrpcAsyncIOTransport + if is_concrete + else transports.AssetServiceTransport + ) with mock.patch.object(transports.base.gapic_v1.method_async, "wrap_method") as mock_wrap: - transport = transports.AssetServiceGrpcAsyncIOTransport( - credentials=ga_credentials.AnonymousCredentials() - ) + transport = transport_cls(credentials=ga_credentials.AnonymousCredentials()) func = mock.MagicMock() transport._wrap_async_method(func) - assert mock_wrap.call_args.kwargs["kind"] == "grpc_asyncio" - assert "client_options" in mock_wrap.call_args.kwargs - - # 2. Base transport: kind raises NotImplementedError and is cleanly omitted - with mock.patch.object(transports.base.gapic_v1.method_async, "wrap_method") as mock_wrap: - base_transport = transports.AssetServiceTransport( - credentials=ga_credentials.AnonymousCredentials() - ) - func = mock.MagicMock() - base_transport._wrap_async_method(func) - assert "kind" not in mock_wrap.call_args.kwargs + if expected_kind: + assert mock_wrap.call_args.kwargs["kind"] == expected_kind + else: + assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs + if expected_has_default_host: + assert mock_wrap.call_args.kwargs["default_host"] == transport._default_host + else: + assert "default_host" not in mock_wrap.call_args.kwargs def test_asset_service_base_transport_wrap_async_method_older_api_core_fallbacks(monkeypatch): diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/_compat.py b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/_compat.py index a4347392b042..86424516e0a2 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/_compat.py @@ -90,6 +90,15 @@ def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: # t gapic_v1.method_async, "_DEFAULT_ASYNC_TRANSPORT_KIND" ) +# The `default_host` parameter in gapic_v1.method.wrap_method, +# gapic_v1.method_async.wrap_method, _observability.get_otel_interceptor, and +# _observability.apply_otel_async_channel_interceptors allows transports to pass +# their resolved default host without mutating caller-owned client_options. +SUPPORTS_DEFAULT_HOST = bool( + getattr(gapic_v1.method.wrap_method, "__kwdefaults__", None) + and "default_host" in gapic_v1.method.wrap_method.__kwdefaults__ +) + try: from google.api_core import grpc_helpers except ImportError: # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/base.py b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/base.py index 39d1b8bf9763..74346d7c2218 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/base.py @@ -14,13 +14,12 @@ # limitations under the License. # import abc -import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.iam.credentials_v1 import gapic_version as package_version from google.iam.credentials_v1._compat import ( - _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) _ASYNC_WRAP_METHOD_SUPPORTS_TRACING = _WRAP_METHOD_SUPPORTS_TRACING @@ -132,25 +131,13 @@ def __init__( host += ':443' self._host = host - # If the transport was instantiated directly with client_options=None - # while OpenTelemetry tracing is enabled via environment variable, - # create a ClientOptions instance with the resolved transport host so - # method and wire spans can record server.address and server.port. - if ( - client_options is None - and _observability is not None - and _observability.is_otel_capabilities_enabled(None) - ): - client_options = client_options_lib.ClientOptions(api_endpoint=self._host) - # And here we check for the presence of an api_endpoint attr OR key, - # shallow-copying before populating to avoid mutating caller-owned state. - elif isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: - client_options = copy.copy(client_options) - client_options.api_endpoint = self._host - elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): - client_options = copy.copy(client_options) - client_options["api_endpoint"] = self._host self._client_options = client_options + # If a pre-constructed gRPC channel was provided to the subclass before + # super().__init__, self._host is not used to create the channel and + # cannot be trusted as the default span endpoint. + self._default_host: Optional[str] = ( + None if getattr(self, "_grpc_channel", None) is not None else self._host + ) self._wrapped_methods: Dict[Callable, Callable] = {} @property @@ -169,6 +156,8 @@ def _wrap_method(self, func, *args, **kwargs): """ if _WRAP_METHOD_SUPPORTS_TRACING: kwargs["client_options"] = self._client_options + if _SUPPORTS_DEFAULT_HOST: + kwargs["default_host"] = self._default_host try: kind = self.kind except NotImplementedError: @@ -181,7 +170,7 @@ def _wrap_method(self, func, *args, **kwargs): # - Era 2 (>= 2.29.0, < 2.36.0): `kind` is supported (prevents REST from # falling back to gRPC wrapping), but tracing args are not. # (These fallbacks can be removed once google-api-core < 2.36.0 is no longer supported.) - for k in ["client_options", "method_name", "is_streaming"]: + for k in ["client_options", "method_name", "is_streaming", "default_host"]: kwargs.pop(k, None) try: @@ -208,6 +197,8 @@ def _wrap_async_method(self, func, *args, **kwargs): """ if _ASYNC_WRAP_METHOD_SUPPORTS_TRACING: kwargs["client_options"] = self._client_options + if _SUPPORTS_DEFAULT_HOST: + kwargs["default_host"] = self._default_host try: kind = self.kind except NotImplementedError: @@ -220,7 +211,7 @@ def _wrap_async_method(self, func, *args, **kwargs): # - Era 2 (>= 2.29.0, < 2.36.0): `kind` is supported (prevents REST from # falling back to gRPC wrapping), but tracing args are not. # (These fallbacks can be removed once google-api-core < 2.36.0 is no longer supported.) - for k in ["client_options", "method_name", "is_streaming"]: + for k in ["client_options", "method_name", "is_streaming", "default_host"]: kwargs.pop(k, None) try: diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/grpc.py index 31fbf17d91cb..df22772304fd 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/grpc.py @@ -20,7 +20,11 @@ from typing import Callable, Dict, Optional, Sequence, Tuple, Union from typing import TYPE_CHECKING -from google.iam.credentials_v1._compat import _observability, apply_channel_interceptors +from google.iam.credentials_v1._compat import ( + _observability, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, + apply_channel_interceptors, +) from google.api_core import client_options as client_options_lib from google.api_core import grpc_helpers @@ -282,9 +286,12 @@ def __init__(self, *, ) channel_interceptors = list(interceptors) if interceptors else [] + _otel_interceptor_kwargs = ( + {"default_host": self._default_host} if _SUPPORTS_DEFAULT_HOST else {} + ) if ( _observability is not None - and (otel_interceptor := _observability.get_otel_interceptor(self._client_options)) is not None + and (otel_interceptor := _observability.get_otel_interceptor(self._client_options, **_otel_interceptor_kwargs)) is not None and otel_interceptor not in channel_interceptors and not any(getattr(i, "_is_otel_interceptor", None) is True for i in channel_interceptors) ): diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/grpc_asyncio.py index 2465db4b3328..45d77f8dacf1 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/grpc_asyncio.py @@ -20,7 +20,11 @@ from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union from typing import TYPE_CHECKING -from google.iam.credentials_v1._compat import _observability, apply_async_channel_interceptors +from google.iam.credentials_v1._compat import ( + _observability, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, + apply_async_channel_interceptors, +) from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 @@ -340,8 +344,11 @@ def __init__(self, *, # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= v2.43.0. + _otel_interceptor_kwargs = ( + {"default_host": self._default_host} if _SUPPORTS_DEFAULT_HOST else {} + ) # pragma: NO COVER self._grpc_channel = _observability.apply_otel_async_channel_interceptors( - self._grpc_channel, self._client_options + self._grpc_channel, self._client_options, **_otel_interceptor_kwargs ) # pragma: NO COVER self._logged_channel = self._grpc_channel diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py b/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py index d3af824d005c..2134257aac7b 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py +++ b/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py @@ -841,7 +841,14 @@ def test_iam_credentials_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(transport._client_options) + expected_kwargs = ( + {"default_host": transport._default_host} + if transports.grpc._SUPPORTS_DEFAULT_HOST + else {} + ) + mock_get_interceptor.assert_called_once_with( + transport._client_options, **expected_kwargs + ) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -915,8 +922,13 @@ def test_iam_credentials_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) + expected_kwargs = ( + {"default_host": transport._default_host} + if transports.grpc_asyncio._SUPPORTS_DEFAULT_HOST + else {} + ) mock_apply_async_interceptors.assert_called_once_with( - mock_channel, transport._client_options + mock_channel, transport._client_options, **expected_kwargs ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel @@ -4458,64 +4470,73 @@ def test_api_key_credentials(client_class, transport_class): ) -def test_iam_credentials_base_transport_wrap_method_modern_api_core(monkeypatch): +@pytest.mark.parametrize( + "supports_default_host,is_concrete,expected_kind,expected_has_default_host", + [ + pytest.param( + True, + True, + "grpc", + True, + id="concrete_transport_supports_default_host", + ), + pytest.param( + False, + False, + None, + False, + id="base_transport_no_default_host", + ), + ], +) +def test_iam_credentials_base_transport_wrap_method_modern_api_core( + monkeypatch, + supports_default_host, + is_concrete, + expected_kind, + expected_has_default_host, +): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", True) - # 1. Concrete transport: passes client_options=None and kind="grpc" + monkeypatch.setattr(transports.base, "_SUPPORTS_DEFAULT_HOST", supports_default_host) + transport_cls = ( + transports.IAMCredentialsGrpcTransport + if is_concrete + else transports.IAMCredentialsTransport + ) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - transport = transports.IAMCredentialsGrpcTransport( - credentials=ga_credentials.AnonymousCredentials() - ) + transport = transport_cls(credentials=ga_credentials.AnonymousCredentials()) func = mock.MagicMock() transport._wrap_method(func) - assert mock_wrap.call_args.kwargs["kind"] == "grpc" - assert "client_options" in mock_wrap.call_args.kwargs - - # 2. Base transport: kind raises NotImplementedError and is cleanly omitted - with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - base_transport = transports.IAMCredentialsTransport( - credentials=ga_credentials.AnonymousCredentials() - ) - func = mock.MagicMock() - base_transport._wrap_method(func) - assert "kind" not in mock_wrap.call_args.kwargs + if expected_kind: + assert mock_wrap.call_args.kwargs["kind"] == expected_kind + else: + assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs + if expected_has_default_host: + assert mock_wrap.call_args.kwargs["default_host"] == transport._default_host + else: + assert "default_host" not in mock_wrap.call_args.kwargs -@pytest.mark.parametrize( - "otel_enabled,expected_endpoint", - [ - pytest.param(False, None, id="otel_disabled"), - pytest.param(True, "default.example.com:443", id="otel_enabled"), - ], -) -def test_iam_credentials_base_transport_client_options_none( - monkeypatch, otel_enabled, expected_endpoint -): - fake_observability = mock.MagicMock() - fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled - monkeypatch.setattr(transports.base, "_observability", fake_observability) - +def test_iam_credentials_base_transport_client_options_none(): transport = transports.IAMCredentialsTransport( host="default.example.com:443", credentials=ga_credentials.AnonymousCredentials(), client_options=None, ) - assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + assert transport._client_options is None + assert transport._default_host == "default.example.com:443" @pytest.mark.parametrize( - "initial_endpoint,expected_endpoint", + "initial_endpoint", [ - pytest.param(None, "default.example.com:443", id="default_from_host"), - pytest.param( - "custom.example.com:443", - "custom.example.com:443", - id="preserve_explicit", - ), + pytest.param(None, id="unset_api_endpoint"), + pytest.param("custom.example.com:443", id="explicit_api_endpoint"), ], ) def test_iam_credentials_base_transport_client_options_object( - initial_endpoint, expected_endpoint + initial_endpoint, ): options = client_options.ClientOptions(api_endpoint=initial_endpoint) transport = transports.IAMCredentialsTransport( @@ -4523,27 +4544,28 @@ def test_iam_credentials_base_transport_client_options_object( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options.api_endpoint == expected_endpoint - assert options.api_endpoint == initial_endpoint + assert transport._client_options is options + assert transport._client_options.api_endpoint == initial_endpoint + assert transport._default_host == "default.example.com:443" @pytest.mark.parametrize( - "initial_options,expected_options", + "initial_options,expected_endpoint", [ pytest.param( {}, - {"api_endpoint": "default.example.com:443"}, - id="default_from_host", + None, + id="unset_api_endpoint", ), pytest.param( {"api_endpoint": "custom.example.com:443"}, - {"api_endpoint": "custom.example.com:443"}, - id="preserve_explicit", + "custom.example.com:443", + id="explicit_api_endpoint", ), ], ) def test_iam_credentials_base_transport_client_options_dict( - initial_options, expected_options + initial_options, expected_endpoint ): options = dict(initial_options) transport = transports.IAMCredentialsTransport( @@ -4551,10 +4573,29 @@ def test_iam_credentials_base_transport_client_options_dict( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options == expected_options + assert transport._client_options is options + assert transport._client_options.get("api_endpoint") == expected_endpoint + assert transport._default_host == "default.example.com:443" assert options == initial_options +def test_iam_credentials_base_transport_default_host_ignored_when_custom_grpc_channel(): + class _CustomChannelTransport(transports.IAMCredentialsTransport): + def __init__(self, **kwargs): + self._ignore_credentials = True + self._grpc_channel = mock.sentinel.custom_channel + super().__init__(**kwargs) + + options = client_options.ClientOptions() + transport = _CustomChannelTransport( + host="default.example.com:443", + client_options=options, + ) + assert transport._client_options is options + assert transport._client_options.api_endpoint is None + assert transport._default_host is None + + def test_iam_credentials_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): @@ -4629,27 +4670,52 @@ def test_iam_credentials_base_transport_wrap_method_older_api_core_fallbacks(mon assert "is_streaming" not in kwargs -def test_iam_credentials_base_transport_wrap_async_method_modern_api_core(monkeypatch): +@pytest.mark.parametrize( + "supports_default_host,is_concrete,expected_kind,expected_has_default_host", + [ + pytest.param( + True, + True, + "grpc_asyncio", + True, + id="concrete_transport_supports_default_host", + ), + pytest.param( + False, + False, + None, + False, + id="base_transport_no_default_host", + ), + ], +) +def test_iam_credentials_base_transport_wrap_async_method_modern_api_core( + monkeypatch, + supports_default_host, + is_concrete, + expected_kind, + expected_has_default_host, +): monkeypatch.setattr(transports.base, "_ASYNC_WRAP_METHOD_SUPPORTS_TRACING", True) - # 1. Concrete transport: passes client_options=None and kind="grpc_asyncio" + monkeypatch.setattr(transports.base, "_SUPPORTS_DEFAULT_HOST", supports_default_host) + transport_cls = ( + transports.IAMCredentialsGrpcAsyncIOTransport + if is_concrete + else transports.IAMCredentialsTransport + ) with mock.patch.object(transports.base.gapic_v1.method_async, "wrap_method") as mock_wrap: - transport = transports.IAMCredentialsGrpcAsyncIOTransport( - credentials=ga_credentials.AnonymousCredentials() - ) + transport = transport_cls(credentials=ga_credentials.AnonymousCredentials()) func = mock.MagicMock() transport._wrap_async_method(func) - assert mock_wrap.call_args.kwargs["kind"] == "grpc_asyncio" - assert "client_options" in mock_wrap.call_args.kwargs - - # 2. Base transport: kind raises NotImplementedError and is cleanly omitted - with mock.patch.object(transports.base.gapic_v1.method_async, "wrap_method") as mock_wrap: - base_transport = transports.IAMCredentialsTransport( - credentials=ga_credentials.AnonymousCredentials() - ) - func = mock.MagicMock() - base_transport._wrap_async_method(func) - assert "kind" not in mock_wrap.call_args.kwargs + if expected_kind: + assert mock_wrap.call_args.kwargs["kind"] == expected_kind + else: + assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs + if expected_has_default_host: + assert mock_wrap.call_args.kwargs["default_host"] == transport._default_host + else: + assert "default_host" not in mock_wrap.call_args.kwargs def test_iam_credentials_base_transport_wrap_async_method_older_api_core_fallbacks(monkeypatch): diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/_compat.py b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/_compat.py index a4347392b042..86424516e0a2 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/_compat.py @@ -90,6 +90,15 @@ def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: # t gapic_v1.method_async, "_DEFAULT_ASYNC_TRANSPORT_KIND" ) +# The `default_host` parameter in gapic_v1.method.wrap_method, +# gapic_v1.method_async.wrap_method, _observability.get_otel_interceptor, and +# _observability.apply_otel_async_channel_interceptors allows transports to pass +# their resolved default host without mutating caller-owned client_options. +SUPPORTS_DEFAULT_HOST = bool( + getattr(gapic_v1.method.wrap_method, "__kwdefaults__", None) + and "default_host" in gapic_v1.method.wrap_method.__kwdefaults__ +) + try: from google.api_core import grpc_helpers except ImportError: # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/base.py b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/base.py index e4a5b3ecae76..66344b384807 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/base.py @@ -14,13 +14,12 @@ # limitations under the License. # import abc -import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.eventarc_v1 import gapic_version as package_version from google.cloud.eventarc_v1._compat import ( - _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) _ASYNC_WRAP_METHOD_SUPPORTS_TRACING = _WRAP_METHOD_SUPPORTS_TRACING @@ -147,25 +146,13 @@ def __init__( host += ':443' self._host = host - # If the transport was instantiated directly with client_options=None - # while OpenTelemetry tracing is enabled via environment variable, - # create a ClientOptions instance with the resolved transport host so - # method and wire spans can record server.address and server.port. - if ( - client_options is None - and _observability is not None - and _observability.is_otel_capabilities_enabled(None) - ): - client_options = client_options_lib.ClientOptions(api_endpoint=self._host) - # And here we check for the presence of an api_endpoint attr OR key, - # shallow-copying before populating to avoid mutating caller-owned state. - elif isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: - client_options = copy.copy(client_options) - client_options.api_endpoint = self._host - elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): - client_options = copy.copy(client_options) - client_options["api_endpoint"] = self._host self._client_options = client_options + # If a pre-constructed gRPC channel was provided to the subclass before + # super().__init__, self._host is not used to create the channel and + # cannot be trusted as the default span endpoint. + self._default_host: Optional[str] = ( + None if getattr(self, "_grpc_channel", None) is not None else self._host + ) self._wrapped_methods: Dict[Callable, Callable] = {} @property @@ -184,6 +171,8 @@ def _wrap_method(self, func, *args, **kwargs): """ if _WRAP_METHOD_SUPPORTS_TRACING: kwargs["client_options"] = self._client_options + if _SUPPORTS_DEFAULT_HOST: + kwargs["default_host"] = self._default_host try: kind = self.kind except NotImplementedError: @@ -196,7 +185,7 @@ def _wrap_method(self, func, *args, **kwargs): # - Era 2 (>= 2.29.0, < 2.36.0): `kind` is supported (prevents REST from # falling back to gRPC wrapping), but tracing args are not. # (These fallbacks can be removed once google-api-core < 2.36.0 is no longer supported.) - for k in ["client_options", "method_name", "is_streaming"]: + for k in ["client_options", "method_name", "is_streaming", "default_host"]: kwargs.pop(k, None) try: @@ -223,6 +212,8 @@ def _wrap_async_method(self, func, *args, **kwargs): """ if _ASYNC_WRAP_METHOD_SUPPORTS_TRACING: kwargs["client_options"] = self._client_options + if _SUPPORTS_DEFAULT_HOST: + kwargs["default_host"] = self._default_host try: kind = self.kind except NotImplementedError: @@ -235,7 +226,7 @@ def _wrap_async_method(self, func, *args, **kwargs): # - Era 2 (>= 2.29.0, < 2.36.0): `kind` is supported (prevents REST from # falling back to gRPC wrapping), but tracing args are not. # (These fallbacks can be removed once google-api-core < 2.36.0 is no longer supported.) - for k in ["client_options", "method_name", "is_streaming"]: + for k in ["client_options", "method_name", "is_streaming", "default_host"]: kwargs.pop(k, None) try: diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/grpc.py index f4cf2bfdbaff..1a4d51772a0b 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/grpc.py @@ -20,7 +20,11 @@ from typing import Callable, Dict, Optional, Sequence, Tuple, Union from typing import TYPE_CHECKING -from google.cloud.eventarc_v1._compat import _observability, apply_channel_interceptors +from google.cloud.eventarc_v1._compat import ( + _observability, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, + apply_channel_interceptors, +) from google.api_core import client_options as client_options_lib from google.api_core import grpc_helpers @@ -291,9 +295,12 @@ def __init__(self, *, ) channel_interceptors = list(interceptors) if interceptors else [] + _otel_interceptor_kwargs = ( + {"default_host": self._default_host} if _SUPPORTS_DEFAULT_HOST else {} + ) if ( _observability is not None - and (otel_interceptor := _observability.get_otel_interceptor(self._client_options)) is not None + and (otel_interceptor := _observability.get_otel_interceptor(self._client_options, **_otel_interceptor_kwargs)) is not None and otel_interceptor not in channel_interceptors and not any(getattr(i, "_is_otel_interceptor", None) is True for i in channel_interceptors) ): diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/grpc_asyncio.py index 1656b15c6881..6fb79c5eec5a 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/grpc_asyncio.py @@ -20,7 +20,11 @@ from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union from typing import TYPE_CHECKING -from google.cloud.eventarc_v1._compat import _observability, apply_async_channel_interceptors +from google.cloud.eventarc_v1._compat import ( + _observability, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, + apply_async_channel_interceptors, +) from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 @@ -349,8 +353,11 @@ def __init__(self, *, # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= v2.43.0. + _otel_interceptor_kwargs = ( + {"default_host": self._default_host} if _SUPPORTS_DEFAULT_HOST else {} + ) # pragma: NO COVER self._grpc_channel = _observability.apply_otel_async_channel_interceptors( - self._grpc_channel, self._client_options + self._grpc_channel, self._client_options, **_otel_interceptor_kwargs ) # pragma: NO COVER self._logged_channel = self._grpc_channel diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py b/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py index 556b3265ab3b..8a7296a59148 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py @@ -872,7 +872,14 @@ def test_eventarc_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(transport._client_options) + expected_kwargs = ( + {"default_host": transport._default_host} + if transports.grpc._SUPPORTS_DEFAULT_HOST + else {} + ) + mock_get_interceptor.assert_called_once_with( + transport._client_options, **expected_kwargs + ) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -946,8 +953,13 @@ def test_eventarc_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) + expected_kwargs = ( + {"default_host": transport._default_host} + if transports.grpc_asyncio._SUPPORTS_DEFAULT_HOST + else {} + ) mock_apply_async_interceptors.assert_called_once_with( - mock_channel, transport._client_options + mock_channel, transport._client_options, **expected_kwargs ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel @@ -33515,64 +33527,73 @@ def test_api_key_credentials(client_class, transport_class): ) -def test_eventarc_base_transport_wrap_method_modern_api_core(monkeypatch): +@pytest.mark.parametrize( + "supports_default_host,is_concrete,expected_kind,expected_has_default_host", + [ + pytest.param( + True, + True, + "grpc", + True, + id="concrete_transport_supports_default_host", + ), + pytest.param( + False, + False, + None, + False, + id="base_transport_no_default_host", + ), + ], +) +def test_eventarc_base_transport_wrap_method_modern_api_core( + monkeypatch, + supports_default_host, + is_concrete, + expected_kind, + expected_has_default_host, +): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", True) - # 1. Concrete transport: passes client_options=None and kind="grpc" + monkeypatch.setattr(transports.base, "_SUPPORTS_DEFAULT_HOST", supports_default_host) + transport_cls = ( + transports.EventarcGrpcTransport + if is_concrete + else transports.EventarcTransport + ) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - transport = transports.EventarcGrpcTransport( - credentials=ga_credentials.AnonymousCredentials() - ) + transport = transport_cls(credentials=ga_credentials.AnonymousCredentials()) func = mock.MagicMock() transport._wrap_method(func) - assert mock_wrap.call_args.kwargs["kind"] == "grpc" - assert "client_options" in mock_wrap.call_args.kwargs - - # 2. Base transport: kind raises NotImplementedError and is cleanly omitted - with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - base_transport = transports.EventarcTransport( - credentials=ga_credentials.AnonymousCredentials() - ) - func = mock.MagicMock() - base_transport._wrap_method(func) - assert "kind" not in mock_wrap.call_args.kwargs + if expected_kind: + assert mock_wrap.call_args.kwargs["kind"] == expected_kind + else: + assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs + if expected_has_default_host: + assert mock_wrap.call_args.kwargs["default_host"] == transport._default_host + else: + assert "default_host" not in mock_wrap.call_args.kwargs -@pytest.mark.parametrize( - "otel_enabled,expected_endpoint", - [ - pytest.param(False, None, id="otel_disabled"), - pytest.param(True, "default.example.com:443", id="otel_enabled"), - ], -) -def test_eventarc_base_transport_client_options_none( - monkeypatch, otel_enabled, expected_endpoint -): - fake_observability = mock.MagicMock() - fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled - monkeypatch.setattr(transports.base, "_observability", fake_observability) - +def test_eventarc_base_transport_client_options_none(): transport = transports.EventarcTransport( host="default.example.com:443", credentials=ga_credentials.AnonymousCredentials(), client_options=None, ) - assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + assert transport._client_options is None + assert transport._default_host == "default.example.com:443" @pytest.mark.parametrize( - "initial_endpoint,expected_endpoint", + "initial_endpoint", [ - pytest.param(None, "default.example.com:443", id="default_from_host"), - pytest.param( - "custom.example.com:443", - "custom.example.com:443", - id="preserve_explicit", - ), + pytest.param(None, id="unset_api_endpoint"), + pytest.param("custom.example.com:443", id="explicit_api_endpoint"), ], ) def test_eventarc_base_transport_client_options_object( - initial_endpoint, expected_endpoint + initial_endpoint, ): options = client_options.ClientOptions(api_endpoint=initial_endpoint) transport = transports.EventarcTransport( @@ -33580,27 +33601,28 @@ def test_eventarc_base_transport_client_options_object( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options.api_endpoint == expected_endpoint - assert options.api_endpoint == initial_endpoint + assert transport._client_options is options + assert transport._client_options.api_endpoint == initial_endpoint + assert transport._default_host == "default.example.com:443" @pytest.mark.parametrize( - "initial_options,expected_options", + "initial_options,expected_endpoint", [ pytest.param( {}, - {"api_endpoint": "default.example.com:443"}, - id="default_from_host", + None, + id="unset_api_endpoint", ), pytest.param( {"api_endpoint": "custom.example.com:443"}, - {"api_endpoint": "custom.example.com:443"}, - id="preserve_explicit", + "custom.example.com:443", + id="explicit_api_endpoint", ), ], ) def test_eventarc_base_transport_client_options_dict( - initial_options, expected_options + initial_options, expected_endpoint ): options = dict(initial_options) transport = transports.EventarcTransport( @@ -33608,10 +33630,29 @@ def test_eventarc_base_transport_client_options_dict( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options == expected_options + assert transport._client_options is options + assert transport._client_options.get("api_endpoint") == expected_endpoint + assert transport._default_host == "default.example.com:443" assert options == initial_options +def test_eventarc_base_transport_default_host_ignored_when_custom_grpc_channel(): + class _CustomChannelTransport(transports.EventarcTransport): + def __init__(self, **kwargs): + self._ignore_credentials = True + self._grpc_channel = mock.sentinel.custom_channel + super().__init__(**kwargs) + + options = client_options.ClientOptions() + transport = _CustomChannelTransport( + host="default.example.com:443", + client_options=options, + ) + assert transport._client_options is options + assert transport._client_options.api_endpoint is None + assert transport._default_host is None + + def test_eventarc_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): @@ -33686,27 +33727,52 @@ def test_eventarc_base_transport_wrap_method_older_api_core_fallbacks(monkeypatc assert "is_streaming" not in kwargs -def test_eventarc_base_transport_wrap_async_method_modern_api_core(monkeypatch): +@pytest.mark.parametrize( + "supports_default_host,is_concrete,expected_kind,expected_has_default_host", + [ + pytest.param( + True, + True, + "grpc_asyncio", + True, + id="concrete_transport_supports_default_host", + ), + pytest.param( + False, + False, + None, + False, + id="base_transport_no_default_host", + ), + ], +) +def test_eventarc_base_transport_wrap_async_method_modern_api_core( + monkeypatch, + supports_default_host, + is_concrete, + expected_kind, + expected_has_default_host, +): monkeypatch.setattr(transports.base, "_ASYNC_WRAP_METHOD_SUPPORTS_TRACING", True) - # 1. Concrete transport: passes client_options=None and kind="grpc_asyncio" + monkeypatch.setattr(transports.base, "_SUPPORTS_DEFAULT_HOST", supports_default_host) + transport_cls = ( + transports.EventarcGrpcAsyncIOTransport + if is_concrete + else transports.EventarcTransport + ) with mock.patch.object(transports.base.gapic_v1.method_async, "wrap_method") as mock_wrap: - transport = transports.EventarcGrpcAsyncIOTransport( - credentials=ga_credentials.AnonymousCredentials() - ) + transport = transport_cls(credentials=ga_credentials.AnonymousCredentials()) func = mock.MagicMock() transport._wrap_async_method(func) - assert mock_wrap.call_args.kwargs["kind"] == "grpc_asyncio" - assert "client_options" in mock_wrap.call_args.kwargs - - # 2. Base transport: kind raises NotImplementedError and is cleanly omitted - with mock.patch.object(transports.base.gapic_v1.method_async, "wrap_method") as mock_wrap: - base_transport = transports.EventarcTransport( - credentials=ga_credentials.AnonymousCredentials() - ) - func = mock.MagicMock() - base_transport._wrap_async_method(func) - assert "kind" not in mock_wrap.call_args.kwargs + if expected_kind: + assert mock_wrap.call_args.kwargs["kind"] == expected_kind + else: + assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs + if expected_has_default_host: + assert mock_wrap.call_args.kwargs["default_host"] == transport._default_host + else: + assert "default_host" not in mock_wrap.call_args.kwargs def test_eventarc_base_transport_wrap_async_method_older_api_core_fallbacks(monkeypatch): diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/_compat.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/_compat.py index a4347392b042..86424516e0a2 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/_compat.py @@ -90,6 +90,15 @@ def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: # t gapic_v1.method_async, "_DEFAULT_ASYNC_TRANSPORT_KIND" ) +# The `default_host` parameter in gapic_v1.method.wrap_method, +# gapic_v1.method_async.wrap_method, _observability.get_otel_interceptor, and +# _observability.apply_otel_async_channel_interceptors allows transports to pass +# their resolved default host without mutating caller-owned client_options. +SUPPORTS_DEFAULT_HOST = bool( + getattr(gapic_v1.method.wrap_method, "__kwdefaults__", None) + and "default_host" in gapic_v1.method.wrap_method.__kwdefaults__ +) + try: from google.api_core import grpc_helpers except ImportError: # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/base.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/base.py index cb1732a8c974..62880b00586a 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/base.py @@ -14,13 +14,12 @@ # limitations under the License. # import abc -import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.logging_v2 import gapic_version as package_version from google.cloud.logging_v2._compat import ( - _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) _ASYNC_WRAP_METHOD_SUPPORTS_TRACING = _WRAP_METHOD_SUPPORTS_TRACING @@ -138,25 +137,13 @@ def __init__( host += ':443' self._host = host - # If the transport was instantiated directly with client_options=None - # while OpenTelemetry tracing is enabled via environment variable, - # create a ClientOptions instance with the resolved transport host so - # method and wire spans can record server.address and server.port. - if ( - client_options is None - and _observability is not None - and _observability.is_otel_capabilities_enabled(None) - ): - client_options = client_options_lib.ClientOptions(api_endpoint=self._host) - # And here we check for the presence of an api_endpoint attr OR key, - # shallow-copying before populating to avoid mutating caller-owned state. - elif isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: - client_options = copy.copy(client_options) - client_options.api_endpoint = self._host - elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): - client_options = copy.copy(client_options) - client_options["api_endpoint"] = self._host self._client_options = client_options + # If a pre-constructed gRPC channel was provided to the subclass before + # super().__init__, self._host is not used to create the channel and + # cannot be trusted as the default span endpoint. + self._default_host: Optional[str] = ( + None if getattr(self, "_grpc_channel", None) is not None else self._host + ) self._wrapped_methods: Dict[Callable, Callable] = {} @property @@ -175,6 +162,8 @@ def _wrap_method(self, func, *args, **kwargs): """ if _WRAP_METHOD_SUPPORTS_TRACING: kwargs["client_options"] = self._client_options + if _SUPPORTS_DEFAULT_HOST: + kwargs["default_host"] = self._default_host try: kind = self.kind except NotImplementedError: @@ -187,7 +176,7 @@ def _wrap_method(self, func, *args, **kwargs): # - Era 2 (>= 2.29.0, < 2.36.0): `kind` is supported (prevents REST from # falling back to gRPC wrapping), but tracing args are not. # (These fallbacks can be removed once google-api-core < 2.36.0 is no longer supported.) - for k in ["client_options", "method_name", "is_streaming"]: + for k in ["client_options", "method_name", "is_streaming", "default_host"]: kwargs.pop(k, None) try: @@ -214,6 +203,8 @@ def _wrap_async_method(self, func, *args, **kwargs): """ if _ASYNC_WRAP_METHOD_SUPPORTS_TRACING: kwargs["client_options"] = self._client_options + if _SUPPORTS_DEFAULT_HOST: + kwargs["default_host"] = self._default_host try: kind = self.kind except NotImplementedError: @@ -226,7 +217,7 @@ def _wrap_async_method(self, func, *args, **kwargs): # - Era 2 (>= 2.29.0, < 2.36.0): `kind` is supported (prevents REST from # falling back to gRPC wrapping), but tracing args are not. # (These fallbacks can be removed once google-api-core < 2.36.0 is no longer supported.) - for k in ["client_options", "method_name", "is_streaming"]: + for k in ["client_options", "method_name", "is_streaming", "default_host"]: kwargs.pop(k, None) try: diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/grpc.py index 543032611b9f..2ee41c76666c 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/grpc.py @@ -20,7 +20,11 @@ from typing import Callable, Dict, Optional, Sequence, Tuple, Union from typing import TYPE_CHECKING -from google.cloud.logging_v2._compat import _observability, apply_channel_interceptors +from google.cloud.logging_v2._compat import ( + _observability, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, + apply_channel_interceptors, +) from google.api_core import client_options as client_options_lib from google.api_core import grpc_helpers @@ -277,9 +281,12 @@ def __init__(self, *, ) channel_interceptors = list(interceptors) if interceptors else [] + _otel_interceptor_kwargs = ( + {"default_host": self._default_host} if _SUPPORTS_DEFAULT_HOST else {} + ) if ( _observability is not None - and (otel_interceptor := _observability.get_otel_interceptor(self._client_options)) is not None + and (otel_interceptor := _observability.get_otel_interceptor(self._client_options, **_otel_interceptor_kwargs)) is not None and otel_interceptor not in channel_interceptors and not any(getattr(i, "_is_otel_interceptor", None) is True for i in channel_interceptors) ): diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py index c03327f2585e..c972054052bd 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py @@ -20,7 +20,11 @@ from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union from typing import TYPE_CHECKING -from google.cloud.logging_v2._compat import _observability, apply_async_channel_interceptors +from google.cloud.logging_v2._compat import ( + _observability, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, + apply_async_channel_interceptors, +) from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 @@ -335,8 +339,11 @@ def __init__(self, *, # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= v2.43.0. + _otel_interceptor_kwargs = ( + {"default_host": self._default_host} if _SUPPORTS_DEFAULT_HOST else {} + ) # pragma: NO COVER self._grpc_channel = _observability.apply_otel_async_channel_interceptors( - self._grpc_channel, self._client_options + self._grpc_channel, self._client_options, **_otel_interceptor_kwargs ) # pragma: NO COVER self._logged_channel = self._grpc_channel diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/base.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/base.py index c0de6b00c70b..76998fcd812c 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/base.py @@ -14,13 +14,12 @@ # limitations under the License. # import abc -import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.logging_v2 import gapic_version as package_version from google.cloud.logging_v2._compat import ( - _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) _ASYNC_WRAP_METHOD_SUPPORTS_TRACING = _WRAP_METHOD_SUPPORTS_TRACING @@ -138,25 +137,13 @@ def __init__( host += ':443' self._host = host - # If the transport was instantiated directly with client_options=None - # while OpenTelemetry tracing is enabled via environment variable, - # create a ClientOptions instance with the resolved transport host so - # method and wire spans can record server.address and server.port. - if ( - client_options is None - and _observability is not None - and _observability.is_otel_capabilities_enabled(None) - ): - client_options = client_options_lib.ClientOptions(api_endpoint=self._host) - # And here we check for the presence of an api_endpoint attr OR key, - # shallow-copying before populating to avoid mutating caller-owned state. - elif isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: - client_options = copy.copy(client_options) - client_options.api_endpoint = self._host - elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): - client_options = copy.copy(client_options) - client_options["api_endpoint"] = self._host self._client_options = client_options + # If a pre-constructed gRPC channel was provided to the subclass before + # super().__init__, self._host is not used to create the channel and + # cannot be trusted as the default span endpoint. + self._default_host: Optional[str] = ( + None if getattr(self, "_grpc_channel", None) is not None else self._host + ) self._wrapped_methods: Dict[Callable, Callable] = {} @property @@ -175,6 +162,8 @@ def _wrap_method(self, func, *args, **kwargs): """ if _WRAP_METHOD_SUPPORTS_TRACING: kwargs["client_options"] = self._client_options + if _SUPPORTS_DEFAULT_HOST: + kwargs["default_host"] = self._default_host try: kind = self.kind except NotImplementedError: @@ -187,7 +176,7 @@ def _wrap_method(self, func, *args, **kwargs): # - Era 2 (>= 2.29.0, < 2.36.0): `kind` is supported (prevents REST from # falling back to gRPC wrapping), but tracing args are not. # (These fallbacks can be removed once google-api-core < 2.36.0 is no longer supported.) - for k in ["client_options", "method_name", "is_streaming"]: + for k in ["client_options", "method_name", "is_streaming", "default_host"]: kwargs.pop(k, None) try: @@ -214,6 +203,8 @@ def _wrap_async_method(self, func, *args, **kwargs): """ if _ASYNC_WRAP_METHOD_SUPPORTS_TRACING: kwargs["client_options"] = self._client_options + if _SUPPORTS_DEFAULT_HOST: + kwargs["default_host"] = self._default_host try: kind = self.kind except NotImplementedError: @@ -226,7 +217,7 @@ def _wrap_async_method(self, func, *args, **kwargs): # - Era 2 (>= 2.29.0, < 2.36.0): `kind` is supported (prevents REST from # falling back to gRPC wrapping), but tracing args are not. # (These fallbacks can be removed once google-api-core < 2.36.0 is no longer supported.) - for k in ["client_options", "method_name", "is_streaming"]: + for k in ["client_options", "method_name", "is_streaming", "default_host"]: kwargs.pop(k, None) try: diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/grpc.py index b4508ffcde56..b36b20e3d16a 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/grpc.py @@ -20,7 +20,11 @@ from typing import Callable, Dict, Optional, Sequence, Tuple, Union from typing import TYPE_CHECKING -from google.cloud.logging_v2._compat import _observability, apply_channel_interceptors +from google.cloud.logging_v2._compat import ( + _observability, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, + apply_channel_interceptors, +) from google.api_core import client_options as client_options_lib from google.api_core import grpc_helpers @@ -275,9 +279,12 @@ def __init__(self, *, ) channel_interceptors = list(interceptors) if interceptors else [] + _otel_interceptor_kwargs = ( + {"default_host": self._default_host} if _SUPPORTS_DEFAULT_HOST else {} + ) if ( _observability is not None - and (otel_interceptor := _observability.get_otel_interceptor(self._client_options)) is not None + and (otel_interceptor := _observability.get_otel_interceptor(self._client_options, **_otel_interceptor_kwargs)) is not None and otel_interceptor not in channel_interceptors and not any(getattr(i, "_is_otel_interceptor", None) is True for i in channel_interceptors) ): diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py index 26b8c349c478..071fe3196e25 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py @@ -20,7 +20,11 @@ from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union from typing import TYPE_CHECKING -from google.cloud.logging_v2._compat import _observability, apply_async_channel_interceptors +from google.cloud.logging_v2._compat import ( + _observability, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, + apply_async_channel_interceptors, +) from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 @@ -333,8 +337,11 @@ def __init__(self, *, # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= v2.43.0. + _otel_interceptor_kwargs = ( + {"default_host": self._default_host} if _SUPPORTS_DEFAULT_HOST else {} + ) # pragma: NO COVER self._grpc_channel = _observability.apply_otel_async_channel_interceptors( - self._grpc_channel, self._client_options + self._grpc_channel, self._client_options, **_otel_interceptor_kwargs ) # pragma: NO COVER self._logged_channel = self._grpc_channel diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py index 630294a64d2e..5fe2ac27c9ba 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py @@ -14,13 +14,12 @@ # limitations under the License. # import abc -import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.logging_v2 import gapic_version as package_version from google.cloud.logging_v2._compat import ( - _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) _ASYNC_WRAP_METHOD_SUPPORTS_TRACING = _WRAP_METHOD_SUPPORTS_TRACING @@ -138,25 +137,13 @@ def __init__( host += ':443' self._host = host - # If the transport was instantiated directly with client_options=None - # while OpenTelemetry tracing is enabled via environment variable, - # create a ClientOptions instance with the resolved transport host so - # method and wire spans can record server.address and server.port. - if ( - client_options is None - and _observability is not None - and _observability.is_otel_capabilities_enabled(None) - ): - client_options = client_options_lib.ClientOptions(api_endpoint=self._host) - # And here we check for the presence of an api_endpoint attr OR key, - # shallow-copying before populating to avoid mutating caller-owned state. - elif isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: - client_options = copy.copy(client_options) - client_options.api_endpoint = self._host - elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): - client_options = copy.copy(client_options) - client_options["api_endpoint"] = self._host self._client_options = client_options + # If a pre-constructed gRPC channel was provided to the subclass before + # super().__init__, self._host is not used to create the channel and + # cannot be trusted as the default span endpoint. + self._default_host: Optional[str] = ( + None if getattr(self, "_grpc_channel", None) is not None else self._host + ) self._wrapped_methods: Dict[Callable, Callable] = {} @property @@ -175,6 +162,8 @@ def _wrap_method(self, func, *args, **kwargs): """ if _WRAP_METHOD_SUPPORTS_TRACING: kwargs["client_options"] = self._client_options + if _SUPPORTS_DEFAULT_HOST: + kwargs["default_host"] = self._default_host try: kind = self.kind except NotImplementedError: @@ -187,7 +176,7 @@ def _wrap_method(self, func, *args, **kwargs): # - Era 2 (>= 2.29.0, < 2.36.0): `kind` is supported (prevents REST from # falling back to gRPC wrapping), but tracing args are not. # (These fallbacks can be removed once google-api-core < 2.36.0 is no longer supported.) - for k in ["client_options", "method_name", "is_streaming"]: + for k in ["client_options", "method_name", "is_streaming", "default_host"]: kwargs.pop(k, None) try: @@ -214,6 +203,8 @@ def _wrap_async_method(self, func, *args, **kwargs): """ if _ASYNC_WRAP_METHOD_SUPPORTS_TRACING: kwargs["client_options"] = self._client_options + if _SUPPORTS_DEFAULT_HOST: + kwargs["default_host"] = self._default_host try: kind = self.kind except NotImplementedError: @@ -226,7 +217,7 @@ def _wrap_async_method(self, func, *args, **kwargs): # - Era 2 (>= 2.29.0, < 2.36.0): `kind` is supported (prevents REST from # falling back to gRPC wrapping), but tracing args are not. # (These fallbacks can be removed once google-api-core < 2.36.0 is no longer supported.) - for k in ["client_options", "method_name", "is_streaming"]: + for k in ["client_options", "method_name", "is_streaming", "default_host"]: kwargs.pop(k, None) try: diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc.py index f72fc2e40ee4..351af44bd1a2 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc.py @@ -20,7 +20,11 @@ from typing import Callable, Dict, Optional, Sequence, Tuple, Union from typing import TYPE_CHECKING -from google.cloud.logging_v2._compat import _observability, apply_channel_interceptors +from google.cloud.logging_v2._compat import ( + _observability, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, + apply_channel_interceptors, +) from google.api_core import client_options as client_options_lib from google.api_core import grpc_helpers @@ -275,9 +279,12 @@ def __init__(self, *, ) channel_interceptors = list(interceptors) if interceptors else [] + _otel_interceptor_kwargs = ( + {"default_host": self._default_host} if _SUPPORTS_DEFAULT_HOST else {} + ) if ( _observability is not None - and (otel_interceptor := _observability.get_otel_interceptor(self._client_options)) is not None + and (otel_interceptor := _observability.get_otel_interceptor(self._client_options, **_otel_interceptor_kwargs)) is not None and otel_interceptor not in channel_interceptors and not any(getattr(i, "_is_otel_interceptor", None) is True for i in channel_interceptors) ): diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py index 54ba2dbe7b1d..beaf9710314f 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py @@ -20,7 +20,11 @@ from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union from typing import TYPE_CHECKING -from google.cloud.logging_v2._compat import _observability, apply_async_channel_interceptors +from google.cloud.logging_v2._compat import ( + _observability, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, + apply_async_channel_interceptors, +) from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 @@ -333,8 +337,11 @@ def __init__(self, *, # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= v2.43.0. + _otel_interceptor_kwargs = ( + {"default_host": self._default_host} if _SUPPORTS_DEFAULT_HOST else {} + ) # pragma: NO COVER self._grpc_channel = _observability.apply_otel_async_channel_interceptors( - self._grpc_channel, self._client_options + self._grpc_channel, self._client_options, **_otel_interceptor_kwargs ) # pragma: NO COVER self._logged_channel = self._grpc_channel diff --git a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py index 790030306491..8551f5b46051 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py @@ -827,7 +827,14 @@ def test_config_service_v2_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(transport._client_options) + expected_kwargs = ( + {"default_host": transport._default_host} + if transports.grpc._SUPPORTS_DEFAULT_HOST + else {} + ) + mock_get_interceptor.assert_called_once_with( + transport._client_options, **expected_kwargs + ) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -901,8 +908,13 @@ def test_config_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) + expected_kwargs = ( + {"default_host": transport._default_host} + if transports.grpc_asyncio._SUPPORTS_DEFAULT_HOST + else {} + ) mock_apply_async_interceptors.assert_called_once_with( - mock_channel, transport._client_options + mock_channel, transport._client_options, **expected_kwargs ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel @@ -14009,64 +14021,73 @@ def test_api_key_credentials(client_class, transport_class): ) -def test_config_service_v2_base_transport_wrap_method_modern_api_core(monkeypatch): +@pytest.mark.parametrize( + "supports_default_host,is_concrete,expected_kind,expected_has_default_host", + [ + pytest.param( + True, + True, + "grpc", + True, + id="concrete_transport_supports_default_host", + ), + pytest.param( + False, + False, + None, + False, + id="base_transport_no_default_host", + ), + ], +) +def test_config_service_v2_base_transport_wrap_method_modern_api_core( + monkeypatch, + supports_default_host, + is_concrete, + expected_kind, + expected_has_default_host, +): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", True) - # 1. Concrete transport: passes client_options=None and kind="grpc" + monkeypatch.setattr(transports.base, "_SUPPORTS_DEFAULT_HOST", supports_default_host) + transport_cls = ( + transports.ConfigServiceV2GrpcTransport + if is_concrete + else transports.ConfigServiceV2Transport + ) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - transport = transports.ConfigServiceV2GrpcTransport( - credentials=ga_credentials.AnonymousCredentials() - ) + transport = transport_cls(credentials=ga_credentials.AnonymousCredentials()) func = mock.MagicMock() transport._wrap_method(func) - assert mock_wrap.call_args.kwargs["kind"] == "grpc" - assert "client_options" in mock_wrap.call_args.kwargs - - # 2. Base transport: kind raises NotImplementedError and is cleanly omitted - with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - base_transport = transports.ConfigServiceV2Transport( - credentials=ga_credentials.AnonymousCredentials() - ) - func = mock.MagicMock() - base_transport._wrap_method(func) - assert "kind" not in mock_wrap.call_args.kwargs + if expected_kind: + assert mock_wrap.call_args.kwargs["kind"] == expected_kind + else: + assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs + if expected_has_default_host: + assert mock_wrap.call_args.kwargs["default_host"] == transport._default_host + else: + assert "default_host" not in mock_wrap.call_args.kwargs -@pytest.mark.parametrize( - "otel_enabled,expected_endpoint", - [ - pytest.param(False, None, id="otel_disabled"), - pytest.param(True, "default.example.com:443", id="otel_enabled"), - ], -) -def test_config_service_v2_base_transport_client_options_none( - monkeypatch, otel_enabled, expected_endpoint -): - fake_observability = mock.MagicMock() - fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled - monkeypatch.setattr(transports.base, "_observability", fake_observability) - +def test_config_service_v2_base_transport_client_options_none(): transport = transports.ConfigServiceV2Transport( host="default.example.com:443", credentials=ga_credentials.AnonymousCredentials(), client_options=None, ) - assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + assert transport._client_options is None + assert transport._default_host == "default.example.com:443" @pytest.mark.parametrize( - "initial_endpoint,expected_endpoint", + "initial_endpoint", [ - pytest.param(None, "default.example.com:443", id="default_from_host"), - pytest.param( - "custom.example.com:443", - "custom.example.com:443", - id="preserve_explicit", - ), + pytest.param(None, id="unset_api_endpoint"), + pytest.param("custom.example.com:443", id="explicit_api_endpoint"), ], ) def test_config_service_v2_base_transport_client_options_object( - initial_endpoint, expected_endpoint + initial_endpoint, ): options = client_options.ClientOptions(api_endpoint=initial_endpoint) transport = transports.ConfigServiceV2Transport( @@ -14074,27 +14095,28 @@ def test_config_service_v2_base_transport_client_options_object( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options.api_endpoint == expected_endpoint - assert options.api_endpoint == initial_endpoint + assert transport._client_options is options + assert transport._client_options.api_endpoint == initial_endpoint + assert transport._default_host == "default.example.com:443" @pytest.mark.parametrize( - "initial_options,expected_options", + "initial_options,expected_endpoint", [ pytest.param( {}, - {"api_endpoint": "default.example.com:443"}, - id="default_from_host", + None, + id="unset_api_endpoint", ), pytest.param( {"api_endpoint": "custom.example.com:443"}, - {"api_endpoint": "custom.example.com:443"}, - id="preserve_explicit", + "custom.example.com:443", + id="explicit_api_endpoint", ), ], ) def test_config_service_v2_base_transport_client_options_dict( - initial_options, expected_options + initial_options, expected_endpoint ): options = dict(initial_options) transport = transports.ConfigServiceV2Transport( @@ -14102,10 +14124,29 @@ def test_config_service_v2_base_transport_client_options_dict( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options == expected_options + assert transport._client_options is options + assert transport._client_options.get("api_endpoint") == expected_endpoint + assert transport._default_host == "default.example.com:443" assert options == initial_options +def test_config_service_v2_base_transport_default_host_ignored_when_custom_grpc_channel(): + class _CustomChannelTransport(transports.ConfigServiceV2Transport): + def __init__(self, **kwargs): + self._ignore_credentials = True + self._grpc_channel = mock.sentinel.custom_channel + super().__init__(**kwargs) + + options = client_options.ClientOptions() + transport = _CustomChannelTransport( + host="default.example.com:443", + client_options=options, + ) + assert transport._client_options is options + assert transport._client_options.api_endpoint is None + assert transport._default_host is None + + def test_config_service_v2_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): @@ -14180,27 +14221,52 @@ def test_config_service_v2_base_transport_wrap_method_older_api_core_fallbacks(m assert "is_streaming" not in kwargs -def test_config_service_v2_base_transport_wrap_async_method_modern_api_core(monkeypatch): +@pytest.mark.parametrize( + "supports_default_host,is_concrete,expected_kind,expected_has_default_host", + [ + pytest.param( + True, + True, + "grpc_asyncio", + True, + id="concrete_transport_supports_default_host", + ), + pytest.param( + False, + False, + None, + False, + id="base_transport_no_default_host", + ), + ], +) +def test_config_service_v2_base_transport_wrap_async_method_modern_api_core( + monkeypatch, + supports_default_host, + is_concrete, + expected_kind, + expected_has_default_host, +): monkeypatch.setattr(transports.base, "_ASYNC_WRAP_METHOD_SUPPORTS_TRACING", True) - # 1. Concrete transport: passes client_options=None and kind="grpc_asyncio" + monkeypatch.setattr(transports.base, "_SUPPORTS_DEFAULT_HOST", supports_default_host) + transport_cls = ( + transports.ConfigServiceV2GrpcAsyncIOTransport + if is_concrete + else transports.ConfigServiceV2Transport + ) with mock.patch.object(transports.base.gapic_v1.method_async, "wrap_method") as mock_wrap: - transport = transports.ConfigServiceV2GrpcAsyncIOTransport( - credentials=ga_credentials.AnonymousCredentials() - ) + transport = transport_cls(credentials=ga_credentials.AnonymousCredentials()) func = mock.MagicMock() transport._wrap_async_method(func) - assert mock_wrap.call_args.kwargs["kind"] == "grpc_asyncio" - assert "client_options" in mock_wrap.call_args.kwargs - - # 2. Base transport: kind raises NotImplementedError and is cleanly omitted - with mock.patch.object(transports.base.gapic_v1.method_async, "wrap_method") as mock_wrap: - base_transport = transports.ConfigServiceV2Transport( - credentials=ga_credentials.AnonymousCredentials() - ) - func = mock.MagicMock() - base_transport._wrap_async_method(func) - assert "kind" not in mock_wrap.call_args.kwargs + if expected_kind: + assert mock_wrap.call_args.kwargs["kind"] == expected_kind + else: + assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs + if expected_has_default_host: + assert mock_wrap.call_args.kwargs["default_host"] == transport._default_host + else: + assert "default_host" not in mock_wrap.call_args.kwargs def test_config_service_v2_base_transport_wrap_async_method_older_api_core_fallbacks(monkeypatch): diff --git a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py index a123afd9d2c2..739d4d17ef02 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py @@ -828,7 +828,14 @@ def test_logging_service_v2_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(transport._client_options) + expected_kwargs = ( + {"default_host": transport._default_host} + if transports.grpc._SUPPORTS_DEFAULT_HOST + else {} + ) + mock_get_interceptor.assert_called_once_with( + transport._client_options, **expected_kwargs + ) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -902,8 +909,13 @@ def test_logging_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) + expected_kwargs = ( + {"default_host": transport._default_host} + if transports.grpc_asyncio._SUPPORTS_DEFAULT_HOST + else {} + ) mock_apply_async_interceptors.assert_called_once_with( - mock_channel, transport._client_options + mock_channel, transport._client_options, **expected_kwargs ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel @@ -4449,64 +4461,73 @@ def test_api_key_credentials(client_class, transport_class): ) -def test_logging_service_v2_base_transport_wrap_method_modern_api_core(monkeypatch): +@pytest.mark.parametrize( + "supports_default_host,is_concrete,expected_kind,expected_has_default_host", + [ + pytest.param( + True, + True, + "grpc", + True, + id="concrete_transport_supports_default_host", + ), + pytest.param( + False, + False, + None, + False, + id="base_transport_no_default_host", + ), + ], +) +def test_logging_service_v2_base_transport_wrap_method_modern_api_core( + monkeypatch, + supports_default_host, + is_concrete, + expected_kind, + expected_has_default_host, +): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", True) - # 1. Concrete transport: passes client_options=None and kind="grpc" + monkeypatch.setattr(transports.base, "_SUPPORTS_DEFAULT_HOST", supports_default_host) + transport_cls = ( + transports.LoggingServiceV2GrpcTransport + if is_concrete + else transports.LoggingServiceV2Transport + ) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - transport = transports.LoggingServiceV2GrpcTransport( - credentials=ga_credentials.AnonymousCredentials() - ) + transport = transport_cls(credentials=ga_credentials.AnonymousCredentials()) func = mock.MagicMock() transport._wrap_method(func) - assert mock_wrap.call_args.kwargs["kind"] == "grpc" - assert "client_options" in mock_wrap.call_args.kwargs - - # 2. Base transport: kind raises NotImplementedError and is cleanly omitted - with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - base_transport = transports.LoggingServiceV2Transport( - credentials=ga_credentials.AnonymousCredentials() - ) - func = mock.MagicMock() - base_transport._wrap_method(func) - assert "kind" not in mock_wrap.call_args.kwargs + if expected_kind: + assert mock_wrap.call_args.kwargs["kind"] == expected_kind + else: + assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs + if expected_has_default_host: + assert mock_wrap.call_args.kwargs["default_host"] == transport._default_host + else: + assert "default_host" not in mock_wrap.call_args.kwargs -@pytest.mark.parametrize( - "otel_enabled,expected_endpoint", - [ - pytest.param(False, None, id="otel_disabled"), - pytest.param(True, "default.example.com:443", id="otel_enabled"), - ], -) -def test_logging_service_v2_base_transport_client_options_none( - monkeypatch, otel_enabled, expected_endpoint -): - fake_observability = mock.MagicMock() - fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled - monkeypatch.setattr(transports.base, "_observability", fake_observability) - +def test_logging_service_v2_base_transport_client_options_none(): transport = transports.LoggingServiceV2Transport( host="default.example.com:443", credentials=ga_credentials.AnonymousCredentials(), client_options=None, ) - assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + assert transport._client_options is None + assert transport._default_host == "default.example.com:443" @pytest.mark.parametrize( - "initial_endpoint,expected_endpoint", + "initial_endpoint", [ - pytest.param(None, "default.example.com:443", id="default_from_host"), - pytest.param( - "custom.example.com:443", - "custom.example.com:443", - id="preserve_explicit", - ), + pytest.param(None, id="unset_api_endpoint"), + pytest.param("custom.example.com:443", id="explicit_api_endpoint"), ], ) def test_logging_service_v2_base_transport_client_options_object( - initial_endpoint, expected_endpoint + initial_endpoint, ): options = client_options.ClientOptions(api_endpoint=initial_endpoint) transport = transports.LoggingServiceV2Transport( @@ -4514,27 +4535,28 @@ def test_logging_service_v2_base_transport_client_options_object( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options.api_endpoint == expected_endpoint - assert options.api_endpoint == initial_endpoint + assert transport._client_options is options + assert transport._client_options.api_endpoint == initial_endpoint + assert transport._default_host == "default.example.com:443" @pytest.mark.parametrize( - "initial_options,expected_options", + "initial_options,expected_endpoint", [ pytest.param( {}, - {"api_endpoint": "default.example.com:443"}, - id="default_from_host", + None, + id="unset_api_endpoint", ), pytest.param( {"api_endpoint": "custom.example.com:443"}, - {"api_endpoint": "custom.example.com:443"}, - id="preserve_explicit", + "custom.example.com:443", + id="explicit_api_endpoint", ), ], ) def test_logging_service_v2_base_transport_client_options_dict( - initial_options, expected_options + initial_options, expected_endpoint ): options = dict(initial_options) transport = transports.LoggingServiceV2Transport( @@ -4542,10 +4564,29 @@ def test_logging_service_v2_base_transport_client_options_dict( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options == expected_options + assert transport._client_options is options + assert transport._client_options.get("api_endpoint") == expected_endpoint + assert transport._default_host == "default.example.com:443" assert options == initial_options +def test_logging_service_v2_base_transport_default_host_ignored_when_custom_grpc_channel(): + class _CustomChannelTransport(transports.LoggingServiceV2Transport): + def __init__(self, **kwargs): + self._ignore_credentials = True + self._grpc_channel = mock.sentinel.custom_channel + super().__init__(**kwargs) + + options = client_options.ClientOptions() + transport = _CustomChannelTransport( + host="default.example.com:443", + client_options=options, + ) + assert transport._client_options is options + assert transport._client_options.api_endpoint is None + assert transport._default_host is None + + def test_logging_service_v2_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): @@ -4620,27 +4661,52 @@ def test_logging_service_v2_base_transport_wrap_method_older_api_core_fallbacks( assert "is_streaming" not in kwargs -def test_logging_service_v2_base_transport_wrap_async_method_modern_api_core(monkeypatch): +@pytest.mark.parametrize( + "supports_default_host,is_concrete,expected_kind,expected_has_default_host", + [ + pytest.param( + True, + True, + "grpc_asyncio", + True, + id="concrete_transport_supports_default_host", + ), + pytest.param( + False, + False, + None, + False, + id="base_transport_no_default_host", + ), + ], +) +def test_logging_service_v2_base_transport_wrap_async_method_modern_api_core( + monkeypatch, + supports_default_host, + is_concrete, + expected_kind, + expected_has_default_host, +): monkeypatch.setattr(transports.base, "_ASYNC_WRAP_METHOD_SUPPORTS_TRACING", True) - # 1. Concrete transport: passes client_options=None and kind="grpc_asyncio" + monkeypatch.setattr(transports.base, "_SUPPORTS_DEFAULT_HOST", supports_default_host) + transport_cls = ( + transports.LoggingServiceV2GrpcAsyncIOTransport + if is_concrete + else transports.LoggingServiceV2Transport + ) with mock.patch.object(transports.base.gapic_v1.method_async, "wrap_method") as mock_wrap: - transport = transports.LoggingServiceV2GrpcAsyncIOTransport( - credentials=ga_credentials.AnonymousCredentials() - ) + transport = transport_cls(credentials=ga_credentials.AnonymousCredentials()) func = mock.MagicMock() transport._wrap_async_method(func) - assert mock_wrap.call_args.kwargs["kind"] == "grpc_asyncio" - assert "client_options" in mock_wrap.call_args.kwargs - - # 2. Base transport: kind raises NotImplementedError and is cleanly omitted - with mock.patch.object(transports.base.gapic_v1.method_async, "wrap_method") as mock_wrap: - base_transport = transports.LoggingServiceV2Transport( - credentials=ga_credentials.AnonymousCredentials() - ) - func = mock.MagicMock() - base_transport._wrap_async_method(func) - assert "kind" not in mock_wrap.call_args.kwargs + if expected_kind: + assert mock_wrap.call_args.kwargs["kind"] == expected_kind + else: + assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs + if expected_has_default_host: + assert mock_wrap.call_args.kwargs["default_host"] == transport._default_host + else: + assert "default_host" not in mock_wrap.call_args.kwargs def test_logging_service_v2_base_transport_wrap_async_method_older_api_core_fallbacks(monkeypatch): diff --git a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py index 5eee17c5025f..e1927e1fa019 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py @@ -826,7 +826,14 @@ def test_metrics_service_v2_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(transport._client_options) + expected_kwargs = ( + {"default_host": transport._default_host} + if transports.grpc._SUPPORTS_DEFAULT_HOST + else {} + ) + mock_get_interceptor.assert_called_once_with( + transport._client_options, **expected_kwargs + ) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -900,8 +907,13 @@ def test_metrics_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) + expected_kwargs = ( + {"default_host": transport._default_host} + if transports.grpc_asyncio._SUPPORTS_DEFAULT_HOST + else {} + ) mock_apply_async_interceptors.assert_called_once_with( - mock_channel, transport._client_options + mock_channel, transport._client_options, **expected_kwargs ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel @@ -4249,64 +4261,73 @@ def test_api_key_credentials(client_class, transport_class): ) -def test_metrics_service_v2_base_transport_wrap_method_modern_api_core(monkeypatch): +@pytest.mark.parametrize( + "supports_default_host,is_concrete,expected_kind,expected_has_default_host", + [ + pytest.param( + True, + True, + "grpc", + True, + id="concrete_transport_supports_default_host", + ), + pytest.param( + False, + False, + None, + False, + id="base_transport_no_default_host", + ), + ], +) +def test_metrics_service_v2_base_transport_wrap_method_modern_api_core( + monkeypatch, + supports_default_host, + is_concrete, + expected_kind, + expected_has_default_host, +): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", True) - # 1. Concrete transport: passes client_options=None and kind="grpc" + monkeypatch.setattr(transports.base, "_SUPPORTS_DEFAULT_HOST", supports_default_host) + transport_cls = ( + transports.MetricsServiceV2GrpcTransport + if is_concrete + else transports.MetricsServiceV2Transport + ) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - transport = transports.MetricsServiceV2GrpcTransport( - credentials=ga_credentials.AnonymousCredentials() - ) + transport = transport_cls(credentials=ga_credentials.AnonymousCredentials()) func = mock.MagicMock() transport._wrap_method(func) - assert mock_wrap.call_args.kwargs["kind"] == "grpc" - assert "client_options" in mock_wrap.call_args.kwargs - - # 2. Base transport: kind raises NotImplementedError and is cleanly omitted - with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - base_transport = transports.MetricsServiceV2Transport( - credentials=ga_credentials.AnonymousCredentials() - ) - func = mock.MagicMock() - base_transport._wrap_method(func) - assert "kind" not in mock_wrap.call_args.kwargs + if expected_kind: + assert mock_wrap.call_args.kwargs["kind"] == expected_kind + else: + assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs + if expected_has_default_host: + assert mock_wrap.call_args.kwargs["default_host"] == transport._default_host + else: + assert "default_host" not in mock_wrap.call_args.kwargs -@pytest.mark.parametrize( - "otel_enabled,expected_endpoint", - [ - pytest.param(False, None, id="otel_disabled"), - pytest.param(True, "default.example.com:443", id="otel_enabled"), - ], -) -def test_metrics_service_v2_base_transport_client_options_none( - monkeypatch, otel_enabled, expected_endpoint -): - fake_observability = mock.MagicMock() - fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled - monkeypatch.setattr(transports.base, "_observability", fake_observability) - +def test_metrics_service_v2_base_transport_client_options_none(): transport = transports.MetricsServiceV2Transport( host="default.example.com:443", credentials=ga_credentials.AnonymousCredentials(), client_options=None, ) - assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + assert transport._client_options is None + assert transport._default_host == "default.example.com:443" @pytest.mark.parametrize( - "initial_endpoint,expected_endpoint", + "initial_endpoint", [ - pytest.param(None, "default.example.com:443", id="default_from_host"), - pytest.param( - "custom.example.com:443", - "custom.example.com:443", - id="preserve_explicit", - ), + pytest.param(None, id="unset_api_endpoint"), + pytest.param("custom.example.com:443", id="explicit_api_endpoint"), ], ) def test_metrics_service_v2_base_transport_client_options_object( - initial_endpoint, expected_endpoint + initial_endpoint, ): options = client_options.ClientOptions(api_endpoint=initial_endpoint) transport = transports.MetricsServiceV2Transport( @@ -4314,27 +4335,28 @@ def test_metrics_service_v2_base_transport_client_options_object( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options.api_endpoint == expected_endpoint - assert options.api_endpoint == initial_endpoint + assert transport._client_options is options + assert transport._client_options.api_endpoint == initial_endpoint + assert transport._default_host == "default.example.com:443" @pytest.mark.parametrize( - "initial_options,expected_options", + "initial_options,expected_endpoint", [ pytest.param( {}, - {"api_endpoint": "default.example.com:443"}, - id="default_from_host", + None, + id="unset_api_endpoint", ), pytest.param( {"api_endpoint": "custom.example.com:443"}, - {"api_endpoint": "custom.example.com:443"}, - id="preserve_explicit", + "custom.example.com:443", + id="explicit_api_endpoint", ), ], ) def test_metrics_service_v2_base_transport_client_options_dict( - initial_options, expected_options + initial_options, expected_endpoint ): options = dict(initial_options) transport = transports.MetricsServiceV2Transport( @@ -4342,10 +4364,29 @@ def test_metrics_service_v2_base_transport_client_options_dict( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options == expected_options + assert transport._client_options is options + assert transport._client_options.get("api_endpoint") == expected_endpoint + assert transport._default_host == "default.example.com:443" assert options == initial_options +def test_metrics_service_v2_base_transport_default_host_ignored_when_custom_grpc_channel(): + class _CustomChannelTransport(transports.MetricsServiceV2Transport): + def __init__(self, **kwargs): + self._ignore_credentials = True + self._grpc_channel = mock.sentinel.custom_channel + super().__init__(**kwargs) + + options = client_options.ClientOptions() + transport = _CustomChannelTransport( + host="default.example.com:443", + client_options=options, + ) + assert transport._client_options is options + assert transport._client_options.api_endpoint is None + assert transport._default_host is None + + def test_metrics_service_v2_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): @@ -4420,27 +4461,52 @@ def test_metrics_service_v2_base_transport_wrap_method_older_api_core_fallbacks( assert "is_streaming" not in kwargs -def test_metrics_service_v2_base_transport_wrap_async_method_modern_api_core(monkeypatch): +@pytest.mark.parametrize( + "supports_default_host,is_concrete,expected_kind,expected_has_default_host", + [ + pytest.param( + True, + True, + "grpc_asyncio", + True, + id="concrete_transport_supports_default_host", + ), + pytest.param( + False, + False, + None, + False, + id="base_transport_no_default_host", + ), + ], +) +def test_metrics_service_v2_base_transport_wrap_async_method_modern_api_core( + monkeypatch, + supports_default_host, + is_concrete, + expected_kind, + expected_has_default_host, +): monkeypatch.setattr(transports.base, "_ASYNC_WRAP_METHOD_SUPPORTS_TRACING", True) - # 1. Concrete transport: passes client_options=None and kind="grpc_asyncio" + monkeypatch.setattr(transports.base, "_SUPPORTS_DEFAULT_HOST", supports_default_host) + transport_cls = ( + transports.MetricsServiceV2GrpcAsyncIOTransport + if is_concrete + else transports.MetricsServiceV2Transport + ) with mock.patch.object(transports.base.gapic_v1.method_async, "wrap_method") as mock_wrap: - transport = transports.MetricsServiceV2GrpcAsyncIOTransport( - credentials=ga_credentials.AnonymousCredentials() - ) + transport = transport_cls(credentials=ga_credentials.AnonymousCredentials()) func = mock.MagicMock() transport._wrap_async_method(func) - assert mock_wrap.call_args.kwargs["kind"] == "grpc_asyncio" - assert "client_options" in mock_wrap.call_args.kwargs - - # 2. Base transport: kind raises NotImplementedError and is cleanly omitted - with mock.patch.object(transports.base.gapic_v1.method_async, "wrap_method") as mock_wrap: - base_transport = transports.MetricsServiceV2Transport( - credentials=ga_credentials.AnonymousCredentials() - ) - func = mock.MagicMock() - base_transport._wrap_async_method(func) - assert "kind" not in mock_wrap.call_args.kwargs + if expected_kind: + assert mock_wrap.call_args.kwargs["kind"] == expected_kind + else: + assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs + if expected_has_default_host: + assert mock_wrap.call_args.kwargs["default_host"] == transport._default_host + else: + assert "default_host" not in mock_wrap.call_args.kwargs def test_metrics_service_v2_base_transport_wrap_async_method_older_api_core_fallbacks(monkeypatch): diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/_compat.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/_compat.py index a4347392b042..86424516e0a2 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/_compat.py @@ -90,6 +90,15 @@ def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: # t gapic_v1.method_async, "_DEFAULT_ASYNC_TRANSPORT_KIND" ) +# The `default_host` parameter in gapic_v1.method.wrap_method, +# gapic_v1.method_async.wrap_method, _observability.get_otel_interceptor, and +# _observability.apply_otel_async_channel_interceptors allows transports to pass +# their resolved default host without mutating caller-owned client_options. +SUPPORTS_DEFAULT_HOST = bool( + getattr(gapic_v1.method.wrap_method, "__kwdefaults__", None) + and "default_host" in gapic_v1.method.wrap_method.__kwdefaults__ +) + try: from google.api_core import grpc_helpers except ImportError: # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/base.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/base.py index cb1732a8c974..62880b00586a 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/base.py @@ -14,13 +14,12 @@ # limitations under the License. # import abc -import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.logging_v2 import gapic_version as package_version from google.cloud.logging_v2._compat import ( - _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) _ASYNC_WRAP_METHOD_SUPPORTS_TRACING = _WRAP_METHOD_SUPPORTS_TRACING @@ -138,25 +137,13 @@ def __init__( host += ':443' self._host = host - # If the transport was instantiated directly with client_options=None - # while OpenTelemetry tracing is enabled via environment variable, - # create a ClientOptions instance with the resolved transport host so - # method and wire spans can record server.address and server.port. - if ( - client_options is None - and _observability is not None - and _observability.is_otel_capabilities_enabled(None) - ): - client_options = client_options_lib.ClientOptions(api_endpoint=self._host) - # And here we check for the presence of an api_endpoint attr OR key, - # shallow-copying before populating to avoid mutating caller-owned state. - elif isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: - client_options = copy.copy(client_options) - client_options.api_endpoint = self._host - elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): - client_options = copy.copy(client_options) - client_options["api_endpoint"] = self._host self._client_options = client_options + # If a pre-constructed gRPC channel was provided to the subclass before + # super().__init__, self._host is not used to create the channel and + # cannot be trusted as the default span endpoint. + self._default_host: Optional[str] = ( + None if getattr(self, "_grpc_channel", None) is not None else self._host + ) self._wrapped_methods: Dict[Callable, Callable] = {} @property @@ -175,6 +162,8 @@ def _wrap_method(self, func, *args, **kwargs): """ if _WRAP_METHOD_SUPPORTS_TRACING: kwargs["client_options"] = self._client_options + if _SUPPORTS_DEFAULT_HOST: + kwargs["default_host"] = self._default_host try: kind = self.kind except NotImplementedError: @@ -187,7 +176,7 @@ def _wrap_method(self, func, *args, **kwargs): # - Era 2 (>= 2.29.0, < 2.36.0): `kind` is supported (prevents REST from # falling back to gRPC wrapping), but tracing args are not. # (These fallbacks can be removed once google-api-core < 2.36.0 is no longer supported.) - for k in ["client_options", "method_name", "is_streaming"]: + for k in ["client_options", "method_name", "is_streaming", "default_host"]: kwargs.pop(k, None) try: @@ -214,6 +203,8 @@ def _wrap_async_method(self, func, *args, **kwargs): """ if _ASYNC_WRAP_METHOD_SUPPORTS_TRACING: kwargs["client_options"] = self._client_options + if _SUPPORTS_DEFAULT_HOST: + kwargs["default_host"] = self._default_host try: kind = self.kind except NotImplementedError: @@ -226,7 +217,7 @@ def _wrap_async_method(self, func, *args, **kwargs): # - Era 2 (>= 2.29.0, < 2.36.0): `kind` is supported (prevents REST from # falling back to gRPC wrapping), but tracing args are not. # (These fallbacks can be removed once google-api-core < 2.36.0 is no longer supported.) - for k in ["client_options", "method_name", "is_streaming"]: + for k in ["client_options", "method_name", "is_streaming", "default_host"]: kwargs.pop(k, None) try: diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/grpc.py index 543032611b9f..2ee41c76666c 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/grpc.py @@ -20,7 +20,11 @@ from typing import Callable, Dict, Optional, Sequence, Tuple, Union from typing import TYPE_CHECKING -from google.cloud.logging_v2._compat import _observability, apply_channel_interceptors +from google.cloud.logging_v2._compat import ( + _observability, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, + apply_channel_interceptors, +) from google.api_core import client_options as client_options_lib from google.api_core import grpc_helpers @@ -277,9 +281,12 @@ def __init__(self, *, ) channel_interceptors = list(interceptors) if interceptors else [] + _otel_interceptor_kwargs = ( + {"default_host": self._default_host} if _SUPPORTS_DEFAULT_HOST else {} + ) if ( _observability is not None - and (otel_interceptor := _observability.get_otel_interceptor(self._client_options)) is not None + and (otel_interceptor := _observability.get_otel_interceptor(self._client_options, **_otel_interceptor_kwargs)) is not None and otel_interceptor not in channel_interceptors and not any(getattr(i, "_is_otel_interceptor", None) is True for i in channel_interceptors) ): diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py index c03327f2585e..c972054052bd 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py @@ -20,7 +20,11 @@ from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union from typing import TYPE_CHECKING -from google.cloud.logging_v2._compat import _observability, apply_async_channel_interceptors +from google.cloud.logging_v2._compat import ( + _observability, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, + apply_async_channel_interceptors, +) from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 @@ -335,8 +339,11 @@ def __init__(self, *, # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= v2.43.0. + _otel_interceptor_kwargs = ( + {"default_host": self._default_host} if _SUPPORTS_DEFAULT_HOST else {} + ) # pragma: NO COVER self._grpc_channel = _observability.apply_otel_async_channel_interceptors( - self._grpc_channel, self._client_options + self._grpc_channel, self._client_options, **_otel_interceptor_kwargs ) # pragma: NO COVER self._logged_channel = self._grpc_channel diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/base.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/base.py index c0de6b00c70b..76998fcd812c 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/base.py @@ -14,13 +14,12 @@ # limitations under the License. # import abc -import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.logging_v2 import gapic_version as package_version from google.cloud.logging_v2._compat import ( - _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) _ASYNC_WRAP_METHOD_SUPPORTS_TRACING = _WRAP_METHOD_SUPPORTS_TRACING @@ -138,25 +137,13 @@ def __init__( host += ':443' self._host = host - # If the transport was instantiated directly with client_options=None - # while OpenTelemetry tracing is enabled via environment variable, - # create a ClientOptions instance with the resolved transport host so - # method and wire spans can record server.address and server.port. - if ( - client_options is None - and _observability is not None - and _observability.is_otel_capabilities_enabled(None) - ): - client_options = client_options_lib.ClientOptions(api_endpoint=self._host) - # And here we check for the presence of an api_endpoint attr OR key, - # shallow-copying before populating to avoid mutating caller-owned state. - elif isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: - client_options = copy.copy(client_options) - client_options.api_endpoint = self._host - elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): - client_options = copy.copy(client_options) - client_options["api_endpoint"] = self._host self._client_options = client_options + # If a pre-constructed gRPC channel was provided to the subclass before + # super().__init__, self._host is not used to create the channel and + # cannot be trusted as the default span endpoint. + self._default_host: Optional[str] = ( + None if getattr(self, "_grpc_channel", None) is not None else self._host + ) self._wrapped_methods: Dict[Callable, Callable] = {} @property @@ -175,6 +162,8 @@ def _wrap_method(self, func, *args, **kwargs): """ if _WRAP_METHOD_SUPPORTS_TRACING: kwargs["client_options"] = self._client_options + if _SUPPORTS_DEFAULT_HOST: + kwargs["default_host"] = self._default_host try: kind = self.kind except NotImplementedError: @@ -187,7 +176,7 @@ def _wrap_method(self, func, *args, **kwargs): # - Era 2 (>= 2.29.0, < 2.36.0): `kind` is supported (prevents REST from # falling back to gRPC wrapping), but tracing args are not. # (These fallbacks can be removed once google-api-core < 2.36.0 is no longer supported.) - for k in ["client_options", "method_name", "is_streaming"]: + for k in ["client_options", "method_name", "is_streaming", "default_host"]: kwargs.pop(k, None) try: @@ -214,6 +203,8 @@ def _wrap_async_method(self, func, *args, **kwargs): """ if _ASYNC_WRAP_METHOD_SUPPORTS_TRACING: kwargs["client_options"] = self._client_options + if _SUPPORTS_DEFAULT_HOST: + kwargs["default_host"] = self._default_host try: kind = self.kind except NotImplementedError: @@ -226,7 +217,7 @@ def _wrap_async_method(self, func, *args, **kwargs): # - Era 2 (>= 2.29.0, < 2.36.0): `kind` is supported (prevents REST from # falling back to gRPC wrapping), but tracing args are not. # (These fallbacks can be removed once google-api-core < 2.36.0 is no longer supported.) - for k in ["client_options", "method_name", "is_streaming"]: + for k in ["client_options", "method_name", "is_streaming", "default_host"]: kwargs.pop(k, None) try: diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/grpc.py index b4508ffcde56..b36b20e3d16a 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/grpc.py @@ -20,7 +20,11 @@ from typing import Callable, Dict, Optional, Sequence, Tuple, Union from typing import TYPE_CHECKING -from google.cloud.logging_v2._compat import _observability, apply_channel_interceptors +from google.cloud.logging_v2._compat import ( + _observability, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, + apply_channel_interceptors, +) from google.api_core import client_options as client_options_lib from google.api_core import grpc_helpers @@ -275,9 +279,12 @@ def __init__(self, *, ) channel_interceptors = list(interceptors) if interceptors else [] + _otel_interceptor_kwargs = ( + {"default_host": self._default_host} if _SUPPORTS_DEFAULT_HOST else {} + ) if ( _observability is not None - and (otel_interceptor := _observability.get_otel_interceptor(self._client_options)) is not None + and (otel_interceptor := _observability.get_otel_interceptor(self._client_options, **_otel_interceptor_kwargs)) is not None and otel_interceptor not in channel_interceptors and not any(getattr(i, "_is_otel_interceptor", None) is True for i in channel_interceptors) ): diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py index 26b8c349c478..071fe3196e25 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py @@ -20,7 +20,11 @@ from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union from typing import TYPE_CHECKING -from google.cloud.logging_v2._compat import _observability, apply_async_channel_interceptors +from google.cloud.logging_v2._compat import ( + _observability, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, + apply_async_channel_interceptors, +) from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 @@ -333,8 +337,11 @@ def __init__(self, *, # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= v2.43.0. + _otel_interceptor_kwargs = ( + {"default_host": self._default_host} if _SUPPORTS_DEFAULT_HOST else {} + ) # pragma: NO COVER self._grpc_channel = _observability.apply_otel_async_channel_interceptors( - self._grpc_channel, self._client_options + self._grpc_channel, self._client_options, **_otel_interceptor_kwargs ) # pragma: NO COVER self._logged_channel = self._grpc_channel diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py index 630294a64d2e..5fe2ac27c9ba 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py @@ -14,13 +14,12 @@ # limitations under the License. # import abc -import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.logging_v2 import gapic_version as package_version from google.cloud.logging_v2._compat import ( - _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) _ASYNC_WRAP_METHOD_SUPPORTS_TRACING = _WRAP_METHOD_SUPPORTS_TRACING @@ -138,25 +137,13 @@ def __init__( host += ':443' self._host = host - # If the transport was instantiated directly with client_options=None - # while OpenTelemetry tracing is enabled via environment variable, - # create a ClientOptions instance with the resolved transport host so - # method and wire spans can record server.address and server.port. - if ( - client_options is None - and _observability is not None - and _observability.is_otel_capabilities_enabled(None) - ): - client_options = client_options_lib.ClientOptions(api_endpoint=self._host) - # And here we check for the presence of an api_endpoint attr OR key, - # shallow-copying before populating to avoid mutating caller-owned state. - elif isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: - client_options = copy.copy(client_options) - client_options.api_endpoint = self._host - elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): - client_options = copy.copy(client_options) - client_options["api_endpoint"] = self._host self._client_options = client_options + # If a pre-constructed gRPC channel was provided to the subclass before + # super().__init__, self._host is not used to create the channel and + # cannot be trusted as the default span endpoint. + self._default_host: Optional[str] = ( + None if getattr(self, "_grpc_channel", None) is not None else self._host + ) self._wrapped_methods: Dict[Callable, Callable] = {} @property @@ -175,6 +162,8 @@ def _wrap_method(self, func, *args, **kwargs): """ if _WRAP_METHOD_SUPPORTS_TRACING: kwargs["client_options"] = self._client_options + if _SUPPORTS_DEFAULT_HOST: + kwargs["default_host"] = self._default_host try: kind = self.kind except NotImplementedError: @@ -187,7 +176,7 @@ def _wrap_method(self, func, *args, **kwargs): # - Era 2 (>= 2.29.0, < 2.36.0): `kind` is supported (prevents REST from # falling back to gRPC wrapping), but tracing args are not. # (These fallbacks can be removed once google-api-core < 2.36.0 is no longer supported.) - for k in ["client_options", "method_name", "is_streaming"]: + for k in ["client_options", "method_name", "is_streaming", "default_host"]: kwargs.pop(k, None) try: @@ -214,6 +203,8 @@ def _wrap_async_method(self, func, *args, **kwargs): """ if _ASYNC_WRAP_METHOD_SUPPORTS_TRACING: kwargs["client_options"] = self._client_options + if _SUPPORTS_DEFAULT_HOST: + kwargs["default_host"] = self._default_host try: kind = self.kind except NotImplementedError: @@ -226,7 +217,7 @@ def _wrap_async_method(self, func, *args, **kwargs): # - Era 2 (>= 2.29.0, < 2.36.0): `kind` is supported (prevents REST from # falling back to gRPC wrapping), but tracing args are not. # (These fallbacks can be removed once google-api-core < 2.36.0 is no longer supported.) - for k in ["client_options", "method_name", "is_streaming"]: + for k in ["client_options", "method_name", "is_streaming", "default_host"]: kwargs.pop(k, None) try: diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc.py index f72fc2e40ee4..351af44bd1a2 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc.py @@ -20,7 +20,11 @@ from typing import Callable, Dict, Optional, Sequence, Tuple, Union from typing import TYPE_CHECKING -from google.cloud.logging_v2._compat import _observability, apply_channel_interceptors +from google.cloud.logging_v2._compat import ( + _observability, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, + apply_channel_interceptors, +) from google.api_core import client_options as client_options_lib from google.api_core import grpc_helpers @@ -275,9 +279,12 @@ def __init__(self, *, ) channel_interceptors = list(interceptors) if interceptors else [] + _otel_interceptor_kwargs = ( + {"default_host": self._default_host} if _SUPPORTS_DEFAULT_HOST else {} + ) if ( _observability is not None - and (otel_interceptor := _observability.get_otel_interceptor(self._client_options)) is not None + and (otel_interceptor := _observability.get_otel_interceptor(self._client_options, **_otel_interceptor_kwargs)) is not None and otel_interceptor not in channel_interceptors and not any(getattr(i, "_is_otel_interceptor", None) is True for i in channel_interceptors) ): diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py index 54ba2dbe7b1d..beaf9710314f 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py @@ -20,7 +20,11 @@ from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union from typing import TYPE_CHECKING -from google.cloud.logging_v2._compat import _observability, apply_async_channel_interceptors +from google.cloud.logging_v2._compat import ( + _observability, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, + apply_async_channel_interceptors, +) from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 @@ -333,8 +337,11 @@ def __init__(self, *, # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= v2.43.0. + _otel_interceptor_kwargs = ( + {"default_host": self._default_host} if _SUPPORTS_DEFAULT_HOST else {} + ) # pragma: NO COVER self._grpc_channel = _observability.apply_otel_async_channel_interceptors( - self._grpc_channel, self._client_options + self._grpc_channel, self._client_options, **_otel_interceptor_kwargs ) # pragma: NO COVER self._logged_channel = self._grpc_channel diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py index 1bbf2dd15340..94afaa35a7e2 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py @@ -827,7 +827,14 @@ def test_config_service_v2_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(transport._client_options) + expected_kwargs = ( + {"default_host": transport._default_host} + if transports.grpc._SUPPORTS_DEFAULT_HOST + else {} + ) + mock_get_interceptor.assert_called_once_with( + transport._client_options, **expected_kwargs + ) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -901,8 +908,13 @@ def test_config_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) + expected_kwargs = ( + {"default_host": transport._default_host} + if transports.grpc_asyncio._SUPPORTS_DEFAULT_HOST + else {} + ) mock_apply_async_interceptors.assert_called_once_with( - mock_channel, transport._client_options + mock_channel, transport._client_options, **expected_kwargs ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel @@ -14009,64 +14021,73 @@ def test_api_key_credentials(client_class, transport_class): ) -def test_config_service_v2_base_transport_wrap_method_modern_api_core(monkeypatch): +@pytest.mark.parametrize( + "supports_default_host,is_concrete,expected_kind,expected_has_default_host", + [ + pytest.param( + True, + True, + "grpc", + True, + id="concrete_transport_supports_default_host", + ), + pytest.param( + False, + False, + None, + False, + id="base_transport_no_default_host", + ), + ], +) +def test_config_service_v2_base_transport_wrap_method_modern_api_core( + monkeypatch, + supports_default_host, + is_concrete, + expected_kind, + expected_has_default_host, +): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", True) - # 1. Concrete transport: passes client_options=None and kind="grpc" + monkeypatch.setattr(transports.base, "_SUPPORTS_DEFAULT_HOST", supports_default_host) + transport_cls = ( + transports.ConfigServiceV2GrpcTransport + if is_concrete + else transports.ConfigServiceV2Transport + ) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - transport = transports.ConfigServiceV2GrpcTransport( - credentials=ga_credentials.AnonymousCredentials() - ) + transport = transport_cls(credentials=ga_credentials.AnonymousCredentials()) func = mock.MagicMock() transport._wrap_method(func) - assert mock_wrap.call_args.kwargs["kind"] == "grpc" - assert "client_options" in mock_wrap.call_args.kwargs - - # 2. Base transport: kind raises NotImplementedError and is cleanly omitted - with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - base_transport = transports.ConfigServiceV2Transport( - credentials=ga_credentials.AnonymousCredentials() - ) - func = mock.MagicMock() - base_transport._wrap_method(func) - assert "kind" not in mock_wrap.call_args.kwargs + if expected_kind: + assert mock_wrap.call_args.kwargs["kind"] == expected_kind + else: + assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs + if expected_has_default_host: + assert mock_wrap.call_args.kwargs["default_host"] == transport._default_host + else: + assert "default_host" not in mock_wrap.call_args.kwargs -@pytest.mark.parametrize( - "otel_enabled,expected_endpoint", - [ - pytest.param(False, None, id="otel_disabled"), - pytest.param(True, "default.example.com:443", id="otel_enabled"), - ], -) -def test_config_service_v2_base_transport_client_options_none( - monkeypatch, otel_enabled, expected_endpoint -): - fake_observability = mock.MagicMock() - fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled - monkeypatch.setattr(transports.base, "_observability", fake_observability) - +def test_config_service_v2_base_transport_client_options_none(): transport = transports.ConfigServiceV2Transport( host="default.example.com:443", credentials=ga_credentials.AnonymousCredentials(), client_options=None, ) - assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + assert transport._client_options is None + assert transport._default_host == "default.example.com:443" @pytest.mark.parametrize( - "initial_endpoint,expected_endpoint", + "initial_endpoint", [ - pytest.param(None, "default.example.com:443", id="default_from_host"), - pytest.param( - "custom.example.com:443", - "custom.example.com:443", - id="preserve_explicit", - ), + pytest.param(None, id="unset_api_endpoint"), + pytest.param("custom.example.com:443", id="explicit_api_endpoint"), ], ) def test_config_service_v2_base_transport_client_options_object( - initial_endpoint, expected_endpoint + initial_endpoint, ): options = client_options.ClientOptions(api_endpoint=initial_endpoint) transport = transports.ConfigServiceV2Transport( @@ -14074,27 +14095,28 @@ def test_config_service_v2_base_transport_client_options_object( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options.api_endpoint == expected_endpoint - assert options.api_endpoint == initial_endpoint + assert transport._client_options is options + assert transport._client_options.api_endpoint == initial_endpoint + assert transport._default_host == "default.example.com:443" @pytest.mark.parametrize( - "initial_options,expected_options", + "initial_options,expected_endpoint", [ pytest.param( {}, - {"api_endpoint": "default.example.com:443"}, - id="default_from_host", + None, + id="unset_api_endpoint", ), pytest.param( {"api_endpoint": "custom.example.com:443"}, - {"api_endpoint": "custom.example.com:443"}, - id="preserve_explicit", + "custom.example.com:443", + id="explicit_api_endpoint", ), ], ) def test_config_service_v2_base_transport_client_options_dict( - initial_options, expected_options + initial_options, expected_endpoint ): options = dict(initial_options) transport = transports.ConfigServiceV2Transport( @@ -14102,10 +14124,29 @@ def test_config_service_v2_base_transport_client_options_dict( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options == expected_options + assert transport._client_options is options + assert transport._client_options.get("api_endpoint") == expected_endpoint + assert transport._default_host == "default.example.com:443" assert options == initial_options +def test_config_service_v2_base_transport_default_host_ignored_when_custom_grpc_channel(): + class _CustomChannelTransport(transports.ConfigServiceV2Transport): + def __init__(self, **kwargs): + self._ignore_credentials = True + self._grpc_channel = mock.sentinel.custom_channel + super().__init__(**kwargs) + + options = client_options.ClientOptions() + transport = _CustomChannelTransport( + host="default.example.com:443", + client_options=options, + ) + assert transport._client_options is options + assert transport._client_options.api_endpoint is None + assert transport._default_host is None + + def test_config_service_v2_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): @@ -14180,27 +14221,52 @@ def test_config_service_v2_base_transport_wrap_method_older_api_core_fallbacks(m assert "is_streaming" not in kwargs -def test_config_service_v2_base_transport_wrap_async_method_modern_api_core(monkeypatch): +@pytest.mark.parametrize( + "supports_default_host,is_concrete,expected_kind,expected_has_default_host", + [ + pytest.param( + True, + True, + "grpc_asyncio", + True, + id="concrete_transport_supports_default_host", + ), + pytest.param( + False, + False, + None, + False, + id="base_transport_no_default_host", + ), + ], +) +def test_config_service_v2_base_transport_wrap_async_method_modern_api_core( + monkeypatch, + supports_default_host, + is_concrete, + expected_kind, + expected_has_default_host, +): monkeypatch.setattr(transports.base, "_ASYNC_WRAP_METHOD_SUPPORTS_TRACING", True) - # 1. Concrete transport: passes client_options=None and kind="grpc_asyncio" + monkeypatch.setattr(transports.base, "_SUPPORTS_DEFAULT_HOST", supports_default_host) + transport_cls = ( + transports.ConfigServiceV2GrpcAsyncIOTransport + if is_concrete + else transports.ConfigServiceV2Transport + ) with mock.patch.object(transports.base.gapic_v1.method_async, "wrap_method") as mock_wrap: - transport = transports.ConfigServiceV2GrpcAsyncIOTransport( - credentials=ga_credentials.AnonymousCredentials() - ) + transport = transport_cls(credentials=ga_credentials.AnonymousCredentials()) func = mock.MagicMock() transport._wrap_async_method(func) - assert mock_wrap.call_args.kwargs["kind"] == "grpc_asyncio" - assert "client_options" in mock_wrap.call_args.kwargs - - # 2. Base transport: kind raises NotImplementedError and is cleanly omitted - with mock.patch.object(transports.base.gapic_v1.method_async, "wrap_method") as mock_wrap: - base_transport = transports.ConfigServiceV2Transport( - credentials=ga_credentials.AnonymousCredentials() - ) - func = mock.MagicMock() - base_transport._wrap_async_method(func) - assert "kind" not in mock_wrap.call_args.kwargs + if expected_kind: + assert mock_wrap.call_args.kwargs["kind"] == expected_kind + else: + assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs + if expected_has_default_host: + assert mock_wrap.call_args.kwargs["default_host"] == transport._default_host + else: + assert "default_host" not in mock_wrap.call_args.kwargs def test_config_service_v2_base_transport_wrap_async_method_older_api_core_fallbacks(monkeypatch): diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py index a123afd9d2c2..739d4d17ef02 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py @@ -828,7 +828,14 @@ def test_logging_service_v2_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(transport._client_options) + expected_kwargs = ( + {"default_host": transport._default_host} + if transports.grpc._SUPPORTS_DEFAULT_HOST + else {} + ) + mock_get_interceptor.assert_called_once_with( + transport._client_options, **expected_kwargs + ) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -902,8 +909,13 @@ def test_logging_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) + expected_kwargs = ( + {"default_host": transport._default_host} + if transports.grpc_asyncio._SUPPORTS_DEFAULT_HOST + else {} + ) mock_apply_async_interceptors.assert_called_once_with( - mock_channel, transport._client_options + mock_channel, transport._client_options, **expected_kwargs ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel @@ -4449,64 +4461,73 @@ def test_api_key_credentials(client_class, transport_class): ) -def test_logging_service_v2_base_transport_wrap_method_modern_api_core(monkeypatch): +@pytest.mark.parametrize( + "supports_default_host,is_concrete,expected_kind,expected_has_default_host", + [ + pytest.param( + True, + True, + "grpc", + True, + id="concrete_transport_supports_default_host", + ), + pytest.param( + False, + False, + None, + False, + id="base_transport_no_default_host", + ), + ], +) +def test_logging_service_v2_base_transport_wrap_method_modern_api_core( + monkeypatch, + supports_default_host, + is_concrete, + expected_kind, + expected_has_default_host, +): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", True) - # 1. Concrete transport: passes client_options=None and kind="grpc" + monkeypatch.setattr(transports.base, "_SUPPORTS_DEFAULT_HOST", supports_default_host) + transport_cls = ( + transports.LoggingServiceV2GrpcTransport + if is_concrete + else transports.LoggingServiceV2Transport + ) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - transport = transports.LoggingServiceV2GrpcTransport( - credentials=ga_credentials.AnonymousCredentials() - ) + transport = transport_cls(credentials=ga_credentials.AnonymousCredentials()) func = mock.MagicMock() transport._wrap_method(func) - assert mock_wrap.call_args.kwargs["kind"] == "grpc" - assert "client_options" in mock_wrap.call_args.kwargs - - # 2. Base transport: kind raises NotImplementedError and is cleanly omitted - with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - base_transport = transports.LoggingServiceV2Transport( - credentials=ga_credentials.AnonymousCredentials() - ) - func = mock.MagicMock() - base_transport._wrap_method(func) - assert "kind" not in mock_wrap.call_args.kwargs + if expected_kind: + assert mock_wrap.call_args.kwargs["kind"] == expected_kind + else: + assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs + if expected_has_default_host: + assert mock_wrap.call_args.kwargs["default_host"] == transport._default_host + else: + assert "default_host" not in mock_wrap.call_args.kwargs -@pytest.mark.parametrize( - "otel_enabled,expected_endpoint", - [ - pytest.param(False, None, id="otel_disabled"), - pytest.param(True, "default.example.com:443", id="otel_enabled"), - ], -) -def test_logging_service_v2_base_transport_client_options_none( - monkeypatch, otel_enabled, expected_endpoint -): - fake_observability = mock.MagicMock() - fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled - monkeypatch.setattr(transports.base, "_observability", fake_observability) - +def test_logging_service_v2_base_transport_client_options_none(): transport = transports.LoggingServiceV2Transport( host="default.example.com:443", credentials=ga_credentials.AnonymousCredentials(), client_options=None, ) - assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + assert transport._client_options is None + assert transport._default_host == "default.example.com:443" @pytest.mark.parametrize( - "initial_endpoint,expected_endpoint", + "initial_endpoint", [ - pytest.param(None, "default.example.com:443", id="default_from_host"), - pytest.param( - "custom.example.com:443", - "custom.example.com:443", - id="preserve_explicit", - ), + pytest.param(None, id="unset_api_endpoint"), + pytest.param("custom.example.com:443", id="explicit_api_endpoint"), ], ) def test_logging_service_v2_base_transport_client_options_object( - initial_endpoint, expected_endpoint + initial_endpoint, ): options = client_options.ClientOptions(api_endpoint=initial_endpoint) transport = transports.LoggingServiceV2Transport( @@ -4514,27 +4535,28 @@ def test_logging_service_v2_base_transport_client_options_object( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options.api_endpoint == expected_endpoint - assert options.api_endpoint == initial_endpoint + assert transport._client_options is options + assert transport._client_options.api_endpoint == initial_endpoint + assert transport._default_host == "default.example.com:443" @pytest.mark.parametrize( - "initial_options,expected_options", + "initial_options,expected_endpoint", [ pytest.param( {}, - {"api_endpoint": "default.example.com:443"}, - id="default_from_host", + None, + id="unset_api_endpoint", ), pytest.param( {"api_endpoint": "custom.example.com:443"}, - {"api_endpoint": "custom.example.com:443"}, - id="preserve_explicit", + "custom.example.com:443", + id="explicit_api_endpoint", ), ], ) def test_logging_service_v2_base_transport_client_options_dict( - initial_options, expected_options + initial_options, expected_endpoint ): options = dict(initial_options) transport = transports.LoggingServiceV2Transport( @@ -4542,10 +4564,29 @@ def test_logging_service_v2_base_transport_client_options_dict( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options == expected_options + assert transport._client_options is options + assert transport._client_options.get("api_endpoint") == expected_endpoint + assert transport._default_host == "default.example.com:443" assert options == initial_options +def test_logging_service_v2_base_transport_default_host_ignored_when_custom_grpc_channel(): + class _CustomChannelTransport(transports.LoggingServiceV2Transport): + def __init__(self, **kwargs): + self._ignore_credentials = True + self._grpc_channel = mock.sentinel.custom_channel + super().__init__(**kwargs) + + options = client_options.ClientOptions() + transport = _CustomChannelTransport( + host="default.example.com:443", + client_options=options, + ) + assert transport._client_options is options + assert transport._client_options.api_endpoint is None + assert transport._default_host is None + + def test_logging_service_v2_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): @@ -4620,27 +4661,52 @@ def test_logging_service_v2_base_transport_wrap_method_older_api_core_fallbacks( assert "is_streaming" not in kwargs -def test_logging_service_v2_base_transport_wrap_async_method_modern_api_core(monkeypatch): +@pytest.mark.parametrize( + "supports_default_host,is_concrete,expected_kind,expected_has_default_host", + [ + pytest.param( + True, + True, + "grpc_asyncio", + True, + id="concrete_transport_supports_default_host", + ), + pytest.param( + False, + False, + None, + False, + id="base_transport_no_default_host", + ), + ], +) +def test_logging_service_v2_base_transport_wrap_async_method_modern_api_core( + monkeypatch, + supports_default_host, + is_concrete, + expected_kind, + expected_has_default_host, +): monkeypatch.setattr(transports.base, "_ASYNC_WRAP_METHOD_SUPPORTS_TRACING", True) - # 1. Concrete transport: passes client_options=None and kind="grpc_asyncio" + monkeypatch.setattr(transports.base, "_SUPPORTS_DEFAULT_HOST", supports_default_host) + transport_cls = ( + transports.LoggingServiceV2GrpcAsyncIOTransport + if is_concrete + else transports.LoggingServiceV2Transport + ) with mock.patch.object(transports.base.gapic_v1.method_async, "wrap_method") as mock_wrap: - transport = transports.LoggingServiceV2GrpcAsyncIOTransport( - credentials=ga_credentials.AnonymousCredentials() - ) + transport = transport_cls(credentials=ga_credentials.AnonymousCredentials()) func = mock.MagicMock() transport._wrap_async_method(func) - assert mock_wrap.call_args.kwargs["kind"] == "grpc_asyncio" - assert "client_options" in mock_wrap.call_args.kwargs - - # 2. Base transport: kind raises NotImplementedError and is cleanly omitted - with mock.patch.object(transports.base.gapic_v1.method_async, "wrap_method") as mock_wrap: - base_transport = transports.LoggingServiceV2Transport( - credentials=ga_credentials.AnonymousCredentials() - ) - func = mock.MagicMock() - base_transport._wrap_async_method(func) - assert "kind" not in mock_wrap.call_args.kwargs + if expected_kind: + assert mock_wrap.call_args.kwargs["kind"] == expected_kind + else: + assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs + if expected_has_default_host: + assert mock_wrap.call_args.kwargs["default_host"] == transport._default_host + else: + assert "default_host" not in mock_wrap.call_args.kwargs def test_logging_service_v2_base_transport_wrap_async_method_older_api_core_fallbacks(monkeypatch): diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py index 5430c5a16ea9..fc1ebd4dcc16 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py @@ -826,7 +826,14 @@ def test_metrics_service_v2_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(transport._client_options) + expected_kwargs = ( + {"default_host": transport._default_host} + if transports.grpc._SUPPORTS_DEFAULT_HOST + else {} + ) + mock_get_interceptor.assert_called_once_with( + transport._client_options, **expected_kwargs + ) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -900,8 +907,13 @@ def test_metrics_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) + expected_kwargs = ( + {"default_host": transport._default_host} + if transports.grpc_asyncio._SUPPORTS_DEFAULT_HOST + else {} + ) mock_apply_async_interceptors.assert_called_once_with( - mock_channel, transport._client_options + mock_channel, transport._client_options, **expected_kwargs ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel @@ -4249,64 +4261,73 @@ def test_api_key_credentials(client_class, transport_class): ) -def test_metrics_service_v2_base_transport_wrap_method_modern_api_core(monkeypatch): +@pytest.mark.parametrize( + "supports_default_host,is_concrete,expected_kind,expected_has_default_host", + [ + pytest.param( + True, + True, + "grpc", + True, + id="concrete_transport_supports_default_host", + ), + pytest.param( + False, + False, + None, + False, + id="base_transport_no_default_host", + ), + ], +) +def test_metrics_service_v2_base_transport_wrap_method_modern_api_core( + monkeypatch, + supports_default_host, + is_concrete, + expected_kind, + expected_has_default_host, +): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", True) - # 1. Concrete transport: passes client_options=None and kind="grpc" + monkeypatch.setattr(transports.base, "_SUPPORTS_DEFAULT_HOST", supports_default_host) + transport_cls = ( + transports.MetricsServiceV2GrpcTransport + if is_concrete + else transports.MetricsServiceV2Transport + ) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - transport = transports.MetricsServiceV2GrpcTransport( - credentials=ga_credentials.AnonymousCredentials() - ) + transport = transport_cls(credentials=ga_credentials.AnonymousCredentials()) func = mock.MagicMock() transport._wrap_method(func) - assert mock_wrap.call_args.kwargs["kind"] == "grpc" - assert "client_options" in mock_wrap.call_args.kwargs - - # 2. Base transport: kind raises NotImplementedError and is cleanly omitted - with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - base_transport = transports.MetricsServiceV2Transport( - credentials=ga_credentials.AnonymousCredentials() - ) - func = mock.MagicMock() - base_transport._wrap_method(func) - assert "kind" not in mock_wrap.call_args.kwargs + if expected_kind: + assert mock_wrap.call_args.kwargs["kind"] == expected_kind + else: + assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs + if expected_has_default_host: + assert mock_wrap.call_args.kwargs["default_host"] == transport._default_host + else: + assert "default_host" not in mock_wrap.call_args.kwargs -@pytest.mark.parametrize( - "otel_enabled,expected_endpoint", - [ - pytest.param(False, None, id="otel_disabled"), - pytest.param(True, "default.example.com:443", id="otel_enabled"), - ], -) -def test_metrics_service_v2_base_transport_client_options_none( - monkeypatch, otel_enabled, expected_endpoint -): - fake_observability = mock.MagicMock() - fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled - monkeypatch.setattr(transports.base, "_observability", fake_observability) - +def test_metrics_service_v2_base_transport_client_options_none(): transport = transports.MetricsServiceV2Transport( host="default.example.com:443", credentials=ga_credentials.AnonymousCredentials(), client_options=None, ) - assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + assert transport._client_options is None + assert transport._default_host == "default.example.com:443" @pytest.mark.parametrize( - "initial_endpoint,expected_endpoint", + "initial_endpoint", [ - pytest.param(None, "default.example.com:443", id="default_from_host"), - pytest.param( - "custom.example.com:443", - "custom.example.com:443", - id="preserve_explicit", - ), + pytest.param(None, id="unset_api_endpoint"), + pytest.param("custom.example.com:443", id="explicit_api_endpoint"), ], ) def test_metrics_service_v2_base_transport_client_options_object( - initial_endpoint, expected_endpoint + initial_endpoint, ): options = client_options.ClientOptions(api_endpoint=initial_endpoint) transport = transports.MetricsServiceV2Transport( @@ -4314,27 +4335,28 @@ def test_metrics_service_v2_base_transport_client_options_object( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options.api_endpoint == expected_endpoint - assert options.api_endpoint == initial_endpoint + assert transport._client_options is options + assert transport._client_options.api_endpoint == initial_endpoint + assert transport._default_host == "default.example.com:443" @pytest.mark.parametrize( - "initial_options,expected_options", + "initial_options,expected_endpoint", [ pytest.param( {}, - {"api_endpoint": "default.example.com:443"}, - id="default_from_host", + None, + id="unset_api_endpoint", ), pytest.param( {"api_endpoint": "custom.example.com:443"}, - {"api_endpoint": "custom.example.com:443"}, - id="preserve_explicit", + "custom.example.com:443", + id="explicit_api_endpoint", ), ], ) def test_metrics_service_v2_base_transport_client_options_dict( - initial_options, expected_options + initial_options, expected_endpoint ): options = dict(initial_options) transport = transports.MetricsServiceV2Transport( @@ -4342,10 +4364,29 @@ def test_metrics_service_v2_base_transport_client_options_dict( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options == expected_options + assert transport._client_options is options + assert transport._client_options.get("api_endpoint") == expected_endpoint + assert transport._default_host == "default.example.com:443" assert options == initial_options +def test_metrics_service_v2_base_transport_default_host_ignored_when_custom_grpc_channel(): + class _CustomChannelTransport(transports.MetricsServiceV2Transport): + def __init__(self, **kwargs): + self._ignore_credentials = True + self._grpc_channel = mock.sentinel.custom_channel + super().__init__(**kwargs) + + options = client_options.ClientOptions() + transport = _CustomChannelTransport( + host="default.example.com:443", + client_options=options, + ) + assert transport._client_options is options + assert transport._client_options.api_endpoint is None + assert transport._default_host is None + + def test_metrics_service_v2_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): @@ -4420,27 +4461,52 @@ def test_metrics_service_v2_base_transport_wrap_method_older_api_core_fallbacks( assert "is_streaming" not in kwargs -def test_metrics_service_v2_base_transport_wrap_async_method_modern_api_core(monkeypatch): +@pytest.mark.parametrize( + "supports_default_host,is_concrete,expected_kind,expected_has_default_host", + [ + pytest.param( + True, + True, + "grpc_asyncio", + True, + id="concrete_transport_supports_default_host", + ), + pytest.param( + False, + False, + None, + False, + id="base_transport_no_default_host", + ), + ], +) +def test_metrics_service_v2_base_transport_wrap_async_method_modern_api_core( + monkeypatch, + supports_default_host, + is_concrete, + expected_kind, + expected_has_default_host, +): monkeypatch.setattr(transports.base, "_ASYNC_WRAP_METHOD_SUPPORTS_TRACING", True) - # 1. Concrete transport: passes client_options=None and kind="grpc_asyncio" + monkeypatch.setattr(transports.base, "_SUPPORTS_DEFAULT_HOST", supports_default_host) + transport_cls = ( + transports.MetricsServiceV2GrpcAsyncIOTransport + if is_concrete + else transports.MetricsServiceV2Transport + ) with mock.patch.object(transports.base.gapic_v1.method_async, "wrap_method") as mock_wrap: - transport = transports.MetricsServiceV2GrpcAsyncIOTransport( - credentials=ga_credentials.AnonymousCredentials() - ) + transport = transport_cls(credentials=ga_credentials.AnonymousCredentials()) func = mock.MagicMock() transport._wrap_async_method(func) - assert mock_wrap.call_args.kwargs["kind"] == "grpc_asyncio" - assert "client_options" in mock_wrap.call_args.kwargs - - # 2. Base transport: kind raises NotImplementedError and is cleanly omitted - with mock.patch.object(transports.base.gapic_v1.method_async, "wrap_method") as mock_wrap: - base_transport = transports.MetricsServiceV2Transport( - credentials=ga_credentials.AnonymousCredentials() - ) - func = mock.MagicMock() - base_transport._wrap_async_method(func) - assert "kind" not in mock_wrap.call_args.kwargs + if expected_kind: + assert mock_wrap.call_args.kwargs["kind"] == expected_kind + else: + assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs + if expected_has_default_host: + assert mock_wrap.call_args.kwargs["default_host"] == transport._default_host + else: + assert "default_host" not in mock_wrap.call_args.kwargs def test_metrics_service_v2_base_transport_wrap_async_method_older_api_core_fallbacks(monkeypatch): diff --git a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/_compat.py b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/_compat.py index a4347392b042..86424516e0a2 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/_compat.py @@ -90,6 +90,15 @@ def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: # t gapic_v1.method_async, "_DEFAULT_ASYNC_TRANSPORT_KIND" ) +# The `default_host` parameter in gapic_v1.method.wrap_method, +# gapic_v1.method_async.wrap_method, _observability.get_otel_interceptor, and +# _observability.apply_otel_async_channel_interceptors allows transports to pass +# their resolved default host without mutating caller-owned client_options. +SUPPORTS_DEFAULT_HOST = bool( + getattr(gapic_v1.method.wrap_method, "__kwdefaults__", None) + and "default_host" in gapic_v1.method.wrap_method.__kwdefaults__ +) + try: from google.api_core import grpc_helpers except ImportError: # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/base.py b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/base.py index 6630c8470486..c0f91c8c3c84 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/base.py @@ -14,13 +14,12 @@ # limitations under the License. # import abc -import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.redis_v1 import gapic_version as package_version from google.cloud.redis_v1._compat import ( - _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) _ASYNC_WRAP_METHOD_SUPPORTS_TRACING = _WRAP_METHOD_SUPPORTS_TRACING @@ -135,25 +134,13 @@ def __init__( host += ':443' self._host = host - # If the transport was instantiated directly with client_options=None - # while OpenTelemetry tracing is enabled via environment variable, - # create a ClientOptions instance with the resolved transport host so - # method and wire spans can record server.address and server.port. - if ( - client_options is None - and _observability is not None - and _observability.is_otel_capabilities_enabled(None) - ): - client_options = client_options_lib.ClientOptions(api_endpoint=self._host) - # And here we check for the presence of an api_endpoint attr OR key, - # shallow-copying before populating to avoid mutating caller-owned state. - elif isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: - client_options = copy.copy(client_options) - client_options.api_endpoint = self._host - elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): - client_options = copy.copy(client_options) - client_options["api_endpoint"] = self._host self._client_options = client_options + # If a pre-constructed gRPC channel was provided to the subclass before + # super().__init__, self._host is not used to create the channel and + # cannot be trusted as the default span endpoint. + self._default_host: Optional[str] = ( + None if getattr(self, "_grpc_channel", None) is not None else self._host + ) self._wrapped_methods: Dict[Callable, Callable] = {} @property @@ -172,6 +159,8 @@ def _wrap_method(self, func, *args, **kwargs): """ if _WRAP_METHOD_SUPPORTS_TRACING: kwargs["client_options"] = self._client_options + if _SUPPORTS_DEFAULT_HOST: + kwargs["default_host"] = self._default_host try: kind = self.kind except NotImplementedError: @@ -184,7 +173,7 @@ def _wrap_method(self, func, *args, **kwargs): # - Era 2 (>= 2.29.0, < 2.36.0): `kind` is supported (prevents REST from # falling back to gRPC wrapping), but tracing args are not. # (These fallbacks can be removed once google-api-core < 2.36.0 is no longer supported.) - for k in ["client_options", "method_name", "is_streaming"]: + for k in ["client_options", "method_name", "is_streaming", "default_host"]: kwargs.pop(k, None) try: @@ -211,6 +200,8 @@ def _wrap_async_method(self, func, *args, **kwargs): """ if _ASYNC_WRAP_METHOD_SUPPORTS_TRACING: kwargs["client_options"] = self._client_options + if _SUPPORTS_DEFAULT_HOST: + kwargs["default_host"] = self._default_host try: kind = self.kind except NotImplementedError: @@ -223,7 +214,7 @@ def _wrap_async_method(self, func, *args, **kwargs): # - Era 2 (>= 2.29.0, < 2.36.0): `kind` is supported (prevents REST from # falling back to gRPC wrapping), but tracing args are not. # (These fallbacks can be removed once google-api-core < 2.36.0 is no longer supported.) - for k in ["client_options", "method_name", "is_streaming"]: + for k in ["client_options", "method_name", "is_streaming", "default_host"]: kwargs.pop(k, None) try: diff --git a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/grpc.py index 1a36444c1eb2..219b2278c879 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/grpc.py @@ -20,7 +20,11 @@ from typing import Callable, Dict, Optional, Sequence, Tuple, Union from typing import TYPE_CHECKING -from google.cloud.redis_v1._compat import _observability, apply_channel_interceptors +from google.cloud.redis_v1._compat import ( + _observability, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, + apply_channel_interceptors, +) from google.api_core import client_options as client_options_lib from google.api_core import grpc_helpers @@ -297,9 +301,12 @@ def __init__(self, *, ) channel_interceptors = list(interceptors) if interceptors else [] + _otel_interceptor_kwargs = ( + {"default_host": self._default_host} if _SUPPORTS_DEFAULT_HOST else {} + ) if ( _observability is not None - and (otel_interceptor := _observability.get_otel_interceptor(self._client_options)) is not None + and (otel_interceptor := _observability.get_otel_interceptor(self._client_options, **_otel_interceptor_kwargs)) is not None and otel_interceptor not in channel_interceptors and not any(getattr(i, "_is_otel_interceptor", None) is True for i in channel_interceptors) ): diff --git a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py index 6f5ff0d28056..c876375cb223 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py @@ -20,7 +20,11 @@ from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union from typing import TYPE_CHECKING -from google.cloud.redis_v1._compat import _observability, apply_async_channel_interceptors +from google.cloud.redis_v1._compat import ( + _observability, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, + apply_async_channel_interceptors, +) from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 @@ -355,8 +359,11 @@ def __init__(self, *, # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= v2.43.0. + _otel_interceptor_kwargs = ( + {"default_host": self._default_host} if _SUPPORTS_DEFAULT_HOST else {} + ) # pragma: NO COVER self._grpc_channel = _observability.apply_otel_async_channel_interceptors( - self._grpc_channel, self._client_options + self._grpc_channel, self._client_options, **_otel_interceptor_kwargs ) # pragma: NO COVER self._logged_channel = self._grpc_channel diff --git a/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py b/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py index eaf5fc2b0a60..ee17389c0d21 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py @@ -859,7 +859,14 @@ def test_cloud_redis_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(transport._client_options) + expected_kwargs = ( + {"default_host": transport._default_host} + if transports.grpc._SUPPORTS_DEFAULT_HOST + else {} + ) + mock_get_interceptor.assert_called_once_with( + transport._client_options, **expected_kwargs + ) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -933,8 +940,13 @@ def test_cloud_redis_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) + expected_kwargs = ( + {"default_host": transport._default_host} + if transports.grpc_asyncio._SUPPORTS_DEFAULT_HOST + else {} + ) mock_apply_async_interceptors.assert_called_once_with( - mock_channel, transport._client_options + mock_channel, transport._client_options, **expected_kwargs ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel @@ -13303,64 +13315,73 @@ def test_api_key_credentials(client_class, transport_class): ) -def test_cloud_redis_base_transport_wrap_method_modern_api_core(monkeypatch): +@pytest.mark.parametrize( + "supports_default_host,is_concrete,expected_kind,expected_has_default_host", + [ + pytest.param( + True, + True, + "grpc", + True, + id="concrete_transport_supports_default_host", + ), + pytest.param( + False, + False, + None, + False, + id="base_transport_no_default_host", + ), + ], +) +def test_cloud_redis_base_transport_wrap_method_modern_api_core( + monkeypatch, + supports_default_host, + is_concrete, + expected_kind, + expected_has_default_host, +): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", True) - # 1. Concrete transport: passes client_options=None and kind="grpc" + monkeypatch.setattr(transports.base, "_SUPPORTS_DEFAULT_HOST", supports_default_host) + transport_cls = ( + transports.CloudRedisGrpcTransport + if is_concrete + else transports.CloudRedisTransport + ) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - transport = transports.CloudRedisGrpcTransport( - credentials=ga_credentials.AnonymousCredentials() - ) + transport = transport_cls(credentials=ga_credentials.AnonymousCredentials()) func = mock.MagicMock() transport._wrap_method(func) - assert mock_wrap.call_args.kwargs["kind"] == "grpc" - assert "client_options" in mock_wrap.call_args.kwargs - - # 2. Base transport: kind raises NotImplementedError and is cleanly omitted - with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - base_transport = transports.CloudRedisTransport( - credentials=ga_credentials.AnonymousCredentials() - ) - func = mock.MagicMock() - base_transport._wrap_method(func) - assert "kind" not in mock_wrap.call_args.kwargs + if expected_kind: + assert mock_wrap.call_args.kwargs["kind"] == expected_kind + else: + assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs + if expected_has_default_host: + assert mock_wrap.call_args.kwargs["default_host"] == transport._default_host + else: + assert "default_host" not in mock_wrap.call_args.kwargs -@pytest.mark.parametrize( - "otel_enabled,expected_endpoint", - [ - pytest.param(False, None, id="otel_disabled"), - pytest.param(True, "default.example.com:443", id="otel_enabled"), - ], -) -def test_cloud_redis_base_transport_client_options_none( - monkeypatch, otel_enabled, expected_endpoint -): - fake_observability = mock.MagicMock() - fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled - monkeypatch.setattr(transports.base, "_observability", fake_observability) - +def test_cloud_redis_base_transport_client_options_none(): transport = transports.CloudRedisTransport( host="default.example.com:443", credentials=ga_credentials.AnonymousCredentials(), client_options=None, ) - assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + assert transport._client_options is None + assert transport._default_host == "default.example.com:443" @pytest.mark.parametrize( - "initial_endpoint,expected_endpoint", + "initial_endpoint", [ - pytest.param(None, "default.example.com:443", id="default_from_host"), - pytest.param( - "custom.example.com:443", - "custom.example.com:443", - id="preserve_explicit", - ), + pytest.param(None, id="unset_api_endpoint"), + pytest.param("custom.example.com:443", id="explicit_api_endpoint"), ], ) def test_cloud_redis_base_transport_client_options_object( - initial_endpoint, expected_endpoint + initial_endpoint, ): options = client_options.ClientOptions(api_endpoint=initial_endpoint) transport = transports.CloudRedisTransport( @@ -13368,27 +13389,28 @@ def test_cloud_redis_base_transport_client_options_object( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options.api_endpoint == expected_endpoint - assert options.api_endpoint == initial_endpoint + assert transport._client_options is options + assert transport._client_options.api_endpoint == initial_endpoint + assert transport._default_host == "default.example.com:443" @pytest.mark.parametrize( - "initial_options,expected_options", + "initial_options,expected_endpoint", [ pytest.param( {}, - {"api_endpoint": "default.example.com:443"}, - id="default_from_host", + None, + id="unset_api_endpoint", ), pytest.param( {"api_endpoint": "custom.example.com:443"}, - {"api_endpoint": "custom.example.com:443"}, - id="preserve_explicit", + "custom.example.com:443", + id="explicit_api_endpoint", ), ], ) def test_cloud_redis_base_transport_client_options_dict( - initial_options, expected_options + initial_options, expected_endpoint ): options = dict(initial_options) transport = transports.CloudRedisTransport( @@ -13396,10 +13418,29 @@ def test_cloud_redis_base_transport_client_options_dict( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options == expected_options + assert transport._client_options is options + assert transport._client_options.get("api_endpoint") == expected_endpoint + assert transport._default_host == "default.example.com:443" assert options == initial_options +def test_cloud_redis_base_transport_default_host_ignored_when_custom_grpc_channel(): + class _CustomChannelTransport(transports.CloudRedisTransport): + def __init__(self, **kwargs): + self._ignore_credentials = True + self._grpc_channel = mock.sentinel.custom_channel + super().__init__(**kwargs) + + options = client_options.ClientOptions() + transport = _CustomChannelTransport( + host="default.example.com:443", + client_options=options, + ) + assert transport._client_options is options + assert transport._client_options.api_endpoint is None + assert transport._default_host is None + + def test_cloud_redis_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): @@ -13474,27 +13515,52 @@ def test_cloud_redis_base_transport_wrap_method_older_api_core_fallbacks(monkeyp assert "is_streaming" not in kwargs -def test_cloud_redis_base_transport_wrap_async_method_modern_api_core(monkeypatch): +@pytest.mark.parametrize( + "supports_default_host,is_concrete,expected_kind,expected_has_default_host", + [ + pytest.param( + True, + True, + "grpc_asyncio", + True, + id="concrete_transport_supports_default_host", + ), + pytest.param( + False, + False, + None, + False, + id="base_transport_no_default_host", + ), + ], +) +def test_cloud_redis_base_transport_wrap_async_method_modern_api_core( + monkeypatch, + supports_default_host, + is_concrete, + expected_kind, + expected_has_default_host, +): monkeypatch.setattr(transports.base, "_ASYNC_WRAP_METHOD_SUPPORTS_TRACING", True) - # 1. Concrete transport: passes client_options=None and kind="grpc_asyncio" + monkeypatch.setattr(transports.base, "_SUPPORTS_DEFAULT_HOST", supports_default_host) + transport_cls = ( + transports.CloudRedisGrpcAsyncIOTransport + if is_concrete + else transports.CloudRedisTransport + ) with mock.patch.object(transports.base.gapic_v1.method_async, "wrap_method") as mock_wrap: - transport = transports.CloudRedisGrpcAsyncIOTransport( - credentials=ga_credentials.AnonymousCredentials() - ) + transport = transport_cls(credentials=ga_credentials.AnonymousCredentials()) func = mock.MagicMock() transport._wrap_async_method(func) - assert mock_wrap.call_args.kwargs["kind"] == "grpc_asyncio" - assert "client_options" in mock_wrap.call_args.kwargs - - # 2. Base transport: kind raises NotImplementedError and is cleanly omitted - with mock.patch.object(transports.base.gapic_v1.method_async, "wrap_method") as mock_wrap: - base_transport = transports.CloudRedisTransport( - credentials=ga_credentials.AnonymousCredentials() - ) - func = mock.MagicMock() - base_transport._wrap_async_method(func) - assert "kind" not in mock_wrap.call_args.kwargs + if expected_kind: + assert mock_wrap.call_args.kwargs["kind"] == expected_kind + else: + assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs + if expected_has_default_host: + assert mock_wrap.call_args.kwargs["default_host"] == transport._default_host + else: + assert "default_host" not in mock_wrap.call_args.kwargs def test_cloud_redis_base_transport_wrap_async_method_older_api_core_fallbacks(monkeypatch): diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/_compat.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/_compat.py index a4347392b042..86424516e0a2 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/_compat.py @@ -90,6 +90,15 @@ def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: # t gapic_v1.method_async, "_DEFAULT_ASYNC_TRANSPORT_KIND" ) +# The `default_host` parameter in gapic_v1.method.wrap_method, +# gapic_v1.method_async.wrap_method, _observability.get_otel_interceptor, and +# _observability.apply_otel_async_channel_interceptors allows transports to pass +# their resolved default host without mutating caller-owned client_options. +SUPPORTS_DEFAULT_HOST = bool( + getattr(gapic_v1.method.wrap_method, "__kwdefaults__", None) + and "default_host" in gapic_v1.method.wrap_method.__kwdefaults__ +) + try: from google.api_core import grpc_helpers except ImportError: # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/base.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/base.py index 2f0e15493d99..51cc094aab06 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/base.py @@ -14,13 +14,12 @@ # limitations under the License. # import abc -import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.redis_v1 import gapic_version as package_version from google.cloud.redis_v1._compat import ( - _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) _ASYNC_WRAP_METHOD_SUPPORTS_TRACING = _WRAP_METHOD_SUPPORTS_TRACING @@ -135,25 +134,13 @@ def __init__( host += ':443' self._host = host - # If the transport was instantiated directly with client_options=None - # while OpenTelemetry tracing is enabled via environment variable, - # create a ClientOptions instance with the resolved transport host so - # method and wire spans can record server.address and server.port. - if ( - client_options is None - and _observability is not None - and _observability.is_otel_capabilities_enabled(None) - ): - client_options = client_options_lib.ClientOptions(api_endpoint=self._host) - # And here we check for the presence of an api_endpoint attr OR key, - # shallow-copying before populating to avoid mutating caller-owned state. - elif isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: - client_options = copy.copy(client_options) - client_options.api_endpoint = self._host - elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): - client_options = copy.copy(client_options) - client_options["api_endpoint"] = self._host self._client_options = client_options + # If a pre-constructed gRPC channel was provided to the subclass before + # super().__init__, self._host is not used to create the channel and + # cannot be trusted as the default span endpoint. + self._default_host: Optional[str] = ( + None if getattr(self, "_grpc_channel", None) is not None else self._host + ) self._wrapped_methods: Dict[Callable, Callable] = {} @property @@ -172,6 +159,8 @@ def _wrap_method(self, func, *args, **kwargs): """ if _WRAP_METHOD_SUPPORTS_TRACING: kwargs["client_options"] = self._client_options + if _SUPPORTS_DEFAULT_HOST: + kwargs["default_host"] = self._default_host try: kind = self.kind except NotImplementedError: @@ -184,7 +173,7 @@ def _wrap_method(self, func, *args, **kwargs): # - Era 2 (>= 2.29.0, < 2.36.0): `kind` is supported (prevents REST from # falling back to gRPC wrapping), but tracing args are not. # (These fallbacks can be removed once google-api-core < 2.36.0 is no longer supported.) - for k in ["client_options", "method_name", "is_streaming"]: + for k in ["client_options", "method_name", "is_streaming", "default_host"]: kwargs.pop(k, None) try: @@ -211,6 +200,8 @@ def _wrap_async_method(self, func, *args, **kwargs): """ if _ASYNC_WRAP_METHOD_SUPPORTS_TRACING: kwargs["client_options"] = self._client_options + if _SUPPORTS_DEFAULT_HOST: + kwargs["default_host"] = self._default_host try: kind = self.kind except NotImplementedError: @@ -223,7 +214,7 @@ def _wrap_async_method(self, func, *args, **kwargs): # - Era 2 (>= 2.29.0, < 2.36.0): `kind` is supported (prevents REST from # falling back to gRPC wrapping), but tracing args are not. # (These fallbacks can be removed once google-api-core < 2.36.0 is no longer supported.) - for k in ["client_options", "method_name", "is_streaming"]: + for k in ["client_options", "method_name", "is_streaming", "default_host"]: kwargs.pop(k, None) try: diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/grpc.py index 287bcfc447cb..bdbdd5811d2c 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/grpc.py @@ -20,7 +20,11 @@ from typing import Callable, Dict, Optional, Sequence, Tuple, Union from typing import TYPE_CHECKING -from google.cloud.redis_v1._compat import _observability, apply_channel_interceptors +from google.cloud.redis_v1._compat import ( + _observability, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, + apply_channel_interceptors, +) from google.api_core import client_options as client_options_lib from google.api_core import grpc_helpers @@ -297,9 +301,12 @@ def __init__(self, *, ) channel_interceptors = list(interceptors) if interceptors else [] + _otel_interceptor_kwargs = ( + {"default_host": self._default_host} if _SUPPORTS_DEFAULT_HOST else {} + ) if ( _observability is not None - and (otel_interceptor := _observability.get_otel_interceptor(self._client_options)) is not None + and (otel_interceptor := _observability.get_otel_interceptor(self._client_options, **_otel_interceptor_kwargs)) is not None and otel_interceptor not in channel_interceptors and not any(getattr(i, "_is_otel_interceptor", None) is True for i in channel_interceptors) ): diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py index 9a64d47d80aa..aeb6e7aca426 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py @@ -20,7 +20,11 @@ from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union from typing import TYPE_CHECKING -from google.cloud.redis_v1._compat import _observability, apply_async_channel_interceptors +from google.cloud.redis_v1._compat import ( + _observability, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, + apply_async_channel_interceptors, +) from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 @@ -355,8 +359,11 @@ def __init__(self, *, # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= v2.43.0. + _otel_interceptor_kwargs = ( + {"default_host": self._default_host} if _SUPPORTS_DEFAULT_HOST else {} + ) # pragma: NO COVER self._grpc_channel = _observability.apply_otel_async_channel_interceptors( - self._grpc_channel, self._client_options + self._grpc_channel, self._client_options, **_otel_interceptor_kwargs ) # pragma: NO COVER self._logged_channel = self._grpc_channel diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py index ad69829602f9..948408ed8a33 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py @@ -859,7 +859,14 @@ def test_cloud_redis_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(transport._client_options) + expected_kwargs = ( + {"default_host": transport._default_host} + if transports.grpc._SUPPORTS_DEFAULT_HOST + else {} + ) + mock_get_interceptor.assert_called_once_with( + transport._client_options, **expected_kwargs + ) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -933,8 +940,13 @@ def test_cloud_redis_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) + expected_kwargs = ( + {"default_host": transport._default_host} + if transports.grpc_asyncio._SUPPORTS_DEFAULT_HOST + else {} + ) mock_apply_async_interceptors.assert_called_once_with( - mock_channel, transport._client_options + mock_channel, transport._client_options, **expected_kwargs ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel @@ -8523,64 +8535,73 @@ def test_api_key_credentials(client_class, transport_class): ) -def test_cloud_redis_base_transport_wrap_method_modern_api_core(monkeypatch): +@pytest.mark.parametrize( + "supports_default_host,is_concrete,expected_kind,expected_has_default_host", + [ + pytest.param( + True, + True, + "grpc", + True, + id="concrete_transport_supports_default_host", + ), + pytest.param( + False, + False, + None, + False, + id="base_transport_no_default_host", + ), + ], +) +def test_cloud_redis_base_transport_wrap_method_modern_api_core( + monkeypatch, + supports_default_host, + is_concrete, + expected_kind, + expected_has_default_host, +): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", True) - # 1. Concrete transport: passes client_options=None and kind="grpc" + monkeypatch.setattr(transports.base, "_SUPPORTS_DEFAULT_HOST", supports_default_host) + transport_cls = ( + transports.CloudRedisGrpcTransport + if is_concrete + else transports.CloudRedisTransport + ) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - transport = transports.CloudRedisGrpcTransport( - credentials=ga_credentials.AnonymousCredentials() - ) + transport = transport_cls(credentials=ga_credentials.AnonymousCredentials()) func = mock.MagicMock() transport._wrap_method(func) - assert mock_wrap.call_args.kwargs["kind"] == "grpc" - assert "client_options" in mock_wrap.call_args.kwargs - - # 2. Base transport: kind raises NotImplementedError and is cleanly omitted - with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - base_transport = transports.CloudRedisTransport( - credentials=ga_credentials.AnonymousCredentials() - ) - func = mock.MagicMock() - base_transport._wrap_method(func) - assert "kind" not in mock_wrap.call_args.kwargs + if expected_kind: + assert mock_wrap.call_args.kwargs["kind"] == expected_kind + else: + assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs + if expected_has_default_host: + assert mock_wrap.call_args.kwargs["default_host"] == transport._default_host + else: + assert "default_host" not in mock_wrap.call_args.kwargs -@pytest.mark.parametrize( - "otel_enabled,expected_endpoint", - [ - pytest.param(False, None, id="otel_disabled"), - pytest.param(True, "default.example.com:443", id="otel_enabled"), - ], -) -def test_cloud_redis_base_transport_client_options_none( - monkeypatch, otel_enabled, expected_endpoint -): - fake_observability = mock.MagicMock() - fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled - monkeypatch.setattr(transports.base, "_observability", fake_observability) - +def test_cloud_redis_base_transport_client_options_none(): transport = transports.CloudRedisTransport( host="default.example.com:443", credentials=ga_credentials.AnonymousCredentials(), client_options=None, ) - assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + assert transport._client_options is None + assert transport._default_host == "default.example.com:443" @pytest.mark.parametrize( - "initial_endpoint,expected_endpoint", + "initial_endpoint", [ - pytest.param(None, "default.example.com:443", id="default_from_host"), - pytest.param( - "custom.example.com:443", - "custom.example.com:443", - id="preserve_explicit", - ), + pytest.param(None, id="unset_api_endpoint"), + pytest.param("custom.example.com:443", id="explicit_api_endpoint"), ], ) def test_cloud_redis_base_transport_client_options_object( - initial_endpoint, expected_endpoint + initial_endpoint, ): options = client_options.ClientOptions(api_endpoint=initial_endpoint) transport = transports.CloudRedisTransport( @@ -8588,27 +8609,28 @@ def test_cloud_redis_base_transport_client_options_object( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options.api_endpoint == expected_endpoint - assert options.api_endpoint == initial_endpoint + assert transport._client_options is options + assert transport._client_options.api_endpoint == initial_endpoint + assert transport._default_host == "default.example.com:443" @pytest.mark.parametrize( - "initial_options,expected_options", + "initial_options,expected_endpoint", [ pytest.param( {}, - {"api_endpoint": "default.example.com:443"}, - id="default_from_host", + None, + id="unset_api_endpoint", ), pytest.param( {"api_endpoint": "custom.example.com:443"}, - {"api_endpoint": "custom.example.com:443"}, - id="preserve_explicit", + "custom.example.com:443", + id="explicit_api_endpoint", ), ], ) def test_cloud_redis_base_transport_client_options_dict( - initial_options, expected_options + initial_options, expected_endpoint ): options = dict(initial_options) transport = transports.CloudRedisTransport( @@ -8616,10 +8638,29 @@ def test_cloud_redis_base_transport_client_options_dict( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options == expected_options + assert transport._client_options is options + assert transport._client_options.get("api_endpoint") == expected_endpoint + assert transport._default_host == "default.example.com:443" assert options == initial_options +def test_cloud_redis_base_transport_default_host_ignored_when_custom_grpc_channel(): + class _CustomChannelTransport(transports.CloudRedisTransport): + def __init__(self, **kwargs): + self._ignore_credentials = True + self._grpc_channel = mock.sentinel.custom_channel + super().__init__(**kwargs) + + options = client_options.ClientOptions() + transport = _CustomChannelTransport( + host="default.example.com:443", + client_options=options, + ) + assert transport._client_options is options + assert transport._client_options.api_endpoint is None + assert transport._default_host is None + + def test_cloud_redis_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): @@ -8694,27 +8735,52 @@ def test_cloud_redis_base_transport_wrap_method_older_api_core_fallbacks(monkeyp assert "is_streaming" not in kwargs -def test_cloud_redis_base_transport_wrap_async_method_modern_api_core(monkeypatch): +@pytest.mark.parametrize( + "supports_default_host,is_concrete,expected_kind,expected_has_default_host", + [ + pytest.param( + True, + True, + "grpc_asyncio", + True, + id="concrete_transport_supports_default_host", + ), + pytest.param( + False, + False, + None, + False, + id="base_transport_no_default_host", + ), + ], +) +def test_cloud_redis_base_transport_wrap_async_method_modern_api_core( + monkeypatch, + supports_default_host, + is_concrete, + expected_kind, + expected_has_default_host, +): monkeypatch.setattr(transports.base, "_ASYNC_WRAP_METHOD_SUPPORTS_TRACING", True) - # 1. Concrete transport: passes client_options=None and kind="grpc_asyncio" + monkeypatch.setattr(transports.base, "_SUPPORTS_DEFAULT_HOST", supports_default_host) + transport_cls = ( + transports.CloudRedisGrpcAsyncIOTransport + if is_concrete + else transports.CloudRedisTransport + ) with mock.patch.object(transports.base.gapic_v1.method_async, "wrap_method") as mock_wrap: - transport = transports.CloudRedisGrpcAsyncIOTransport( - credentials=ga_credentials.AnonymousCredentials() - ) + transport = transport_cls(credentials=ga_credentials.AnonymousCredentials()) func = mock.MagicMock() transport._wrap_async_method(func) - assert mock_wrap.call_args.kwargs["kind"] == "grpc_asyncio" - assert "client_options" in mock_wrap.call_args.kwargs - - # 2. Base transport: kind raises NotImplementedError and is cleanly omitted - with mock.patch.object(transports.base.gapic_v1.method_async, "wrap_method") as mock_wrap: - base_transport = transports.CloudRedisTransport( - credentials=ga_credentials.AnonymousCredentials() - ) - func = mock.MagicMock() - base_transport._wrap_async_method(func) - assert "kind" not in mock_wrap.call_args.kwargs + if expected_kind: + assert mock_wrap.call_args.kwargs["kind"] == expected_kind + else: + assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs + if expected_has_default_host: + assert mock_wrap.call_args.kwargs["default_host"] == transport._default_host + else: + assert "default_host" not in mock_wrap.call_args.kwargs def test_cloud_redis_base_transport_wrap_async_method_older_api_core_fallbacks(monkeypatch): diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/_compat.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/_compat.py index 31817dab22a1..893b510545de 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/_compat.py @@ -95,6 +95,15 @@ def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: # t gapic_v1.method_async, "_DEFAULT_ASYNC_TRANSPORT_KIND" ) +# The `default_host` parameter in gapic_v1.method.wrap_method, +# gapic_v1.method_async.wrap_method, _observability.get_otel_interceptor, and +# _observability.apply_otel_async_channel_interceptors allows transports to pass +# their resolved default host without mutating caller-owned client_options. +SUPPORTS_DEFAULT_HOST = bool( + getattr(gapic_v1.method.wrap_method, "__kwdefaults__", None) + and "default_host" in gapic_v1.method.wrap_method.__kwdefaults__ +) + try: from google.api_core import grpc_helpers except ImportError: # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/base.py index 7982b81e3f98..165a960b54dc 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/base.py @@ -14,13 +14,12 @@ # limitations under the License. # import abc -import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.showcase_v1beta1 import gapic_version as package_version from google.showcase_v1beta1._compat import ( - _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) _ASYNC_WRAP_METHOD_SUPPORTS_TRACING = _WRAP_METHOD_SUPPORTS_TRACING @@ -136,25 +135,13 @@ def __init__( host += ':443' self._host = host - # If the transport was instantiated directly with client_options=None - # while OpenTelemetry tracing is enabled via environment variable, - # create a ClientOptions instance with the resolved transport host so - # method and wire spans can record server.address and server.port. - if ( - client_options is None - and _observability is not None - and _observability.is_otel_capabilities_enabled(None) - ): - client_options = client_options_lib.ClientOptions(api_endpoint=self._host) - # And here we check for the presence of an api_endpoint attr OR key, - # shallow-copying before populating to avoid mutating caller-owned state. - elif isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: - client_options = copy.copy(client_options) - client_options.api_endpoint = self._host - elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): - client_options = copy.copy(client_options) - client_options["api_endpoint"] = self._host self._client_options = client_options + # If a pre-constructed gRPC channel was provided to the subclass before + # super().__init__, self._host is not used to create the channel and + # cannot be trusted as the default span endpoint. + self._default_host: Optional[str] = ( + None if getattr(self, "_grpc_channel", None) is not None else self._host + ) self._wrapped_methods: Dict[Callable, Callable] = {} @property @@ -173,6 +160,8 @@ def _wrap_method(self, func, *args, **kwargs): """ if _WRAP_METHOD_SUPPORTS_TRACING: kwargs["client_options"] = self._client_options + if _SUPPORTS_DEFAULT_HOST: + kwargs["default_host"] = self._default_host try: kind = self.kind except NotImplementedError: @@ -185,7 +174,7 @@ def _wrap_method(self, func, *args, **kwargs): # - Era 2 (>= 2.29.0, < 2.36.0): `kind` is supported (prevents REST from # falling back to gRPC wrapping), but tracing args are not. # (These fallbacks can be removed once google-api-core < 2.36.0 is no longer supported.) - for k in ["client_options", "method_name", "is_streaming"]: + for k in ["client_options", "method_name", "is_streaming", "default_host"]: kwargs.pop(k, None) try: @@ -212,6 +201,8 @@ def _wrap_async_method(self, func, *args, **kwargs): """ if _ASYNC_WRAP_METHOD_SUPPORTS_TRACING: kwargs["client_options"] = self._client_options + if _SUPPORTS_DEFAULT_HOST: + kwargs["default_host"] = self._default_host try: kind = self.kind except NotImplementedError: @@ -224,7 +215,7 @@ def _wrap_async_method(self, func, *args, **kwargs): # - Era 2 (>= 2.29.0, < 2.36.0): `kind` is supported (prevents REST from # falling back to gRPC wrapping), but tracing args are not. # (These fallbacks can be removed once google-api-core < 2.36.0 is no longer supported.) - for k in ["client_options", "method_name", "is_streaming"]: + for k in ["client_options", "method_name", "is_streaming", "default_host"]: kwargs.pop(k, None) try: diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/grpc.py index 4d5fc8e2ebcd..e9f25c269d79 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/grpc.py @@ -20,7 +20,11 @@ from typing import Callable, Dict, Optional, Sequence, Tuple, Union from typing import TYPE_CHECKING -from google.showcase_v1beta1._compat import _observability, apply_channel_interceptors +from google.showcase_v1beta1._compat import ( + _observability, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, + apply_channel_interceptors, +) from google.api_core import client_options as client_options_lib from google.api_core import grpc_helpers @@ -286,9 +290,12 @@ def __init__(self, *, ) channel_interceptors = list(interceptors) if interceptors else [] + _otel_interceptor_kwargs = ( + {"default_host": self._default_host} if _SUPPORTS_DEFAULT_HOST else {} + ) if ( _observability is not None - and (otel_interceptor := _observability.get_otel_interceptor(self._client_options)) is not None + and (otel_interceptor := _observability.get_otel_interceptor(self._client_options, **_otel_interceptor_kwargs)) is not None and otel_interceptor not in channel_interceptors and not any(getattr(i, "_is_otel_interceptor", None) is True for i in channel_interceptors) ): diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/grpc_asyncio.py index c2dd99d6b774..fbe193800b6a 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/grpc_asyncio.py @@ -20,7 +20,11 @@ from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union from typing import TYPE_CHECKING -from google.showcase_v1beta1._compat import _observability, apply_async_channel_interceptors +from google.showcase_v1beta1._compat import ( + _observability, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, + apply_async_channel_interceptors, +) from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 @@ -344,8 +348,11 @@ def __init__(self, *, # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= v2.43.0. + _otel_interceptor_kwargs = ( + {"default_host": self._default_host} if _SUPPORTS_DEFAULT_HOST else {} + ) # pragma: NO COVER self._grpc_channel = _observability.apply_otel_async_channel_interceptors( - self._grpc_channel, self._client_options + self._grpc_channel, self._client_options, **_otel_interceptor_kwargs ) # pragma: NO COVER self._logged_channel = self._grpc_channel diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/base.py index ee98130046f1..61439ef3c6f7 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/base.py @@ -14,13 +14,12 @@ # limitations under the License. # import abc -import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.showcase_v1beta1 import gapic_version as package_version from google.showcase_v1beta1._compat import ( - _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) _ASYNC_WRAP_METHOD_SUPPORTS_TRACING = _WRAP_METHOD_SUPPORTS_TRACING @@ -136,25 +135,13 @@ def __init__( host += ':443' self._host = host - # If the transport was instantiated directly with client_options=None - # while OpenTelemetry tracing is enabled via environment variable, - # create a ClientOptions instance with the resolved transport host so - # method and wire spans can record server.address and server.port. - if ( - client_options is None - and _observability is not None - and _observability.is_otel_capabilities_enabled(None) - ): - client_options = client_options_lib.ClientOptions(api_endpoint=self._host) - # And here we check for the presence of an api_endpoint attr OR key, - # shallow-copying before populating to avoid mutating caller-owned state. - elif isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: - client_options = copy.copy(client_options) - client_options.api_endpoint = self._host - elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): - client_options = copy.copy(client_options) - client_options["api_endpoint"] = self._host self._client_options = client_options + # If a pre-constructed gRPC channel was provided to the subclass before + # super().__init__, self._host is not used to create the channel and + # cannot be trusted as the default span endpoint. + self._default_host: Optional[str] = ( + None if getattr(self, "_grpc_channel", None) is not None else self._host + ) self._wrapped_methods: Dict[Callable, Callable] = {} @property @@ -173,6 +160,8 @@ def _wrap_method(self, func, *args, **kwargs): """ if _WRAP_METHOD_SUPPORTS_TRACING: kwargs["client_options"] = self._client_options + if _SUPPORTS_DEFAULT_HOST: + kwargs["default_host"] = self._default_host try: kind = self.kind except NotImplementedError: @@ -185,7 +174,7 @@ def _wrap_method(self, func, *args, **kwargs): # - Era 2 (>= 2.29.0, < 2.36.0): `kind` is supported (prevents REST from # falling back to gRPC wrapping), but tracing args are not. # (These fallbacks can be removed once google-api-core < 2.36.0 is no longer supported.) - for k in ["client_options", "method_name", "is_streaming"]: + for k in ["client_options", "method_name", "is_streaming", "default_host"]: kwargs.pop(k, None) try: @@ -212,6 +201,8 @@ def _wrap_async_method(self, func, *args, **kwargs): """ if _ASYNC_WRAP_METHOD_SUPPORTS_TRACING: kwargs["client_options"] = self._client_options + if _SUPPORTS_DEFAULT_HOST: + kwargs["default_host"] = self._default_host try: kind = self.kind except NotImplementedError: @@ -224,7 +215,7 @@ def _wrap_async_method(self, func, *args, **kwargs): # - Era 2 (>= 2.29.0, < 2.36.0): `kind` is supported (prevents REST from # falling back to gRPC wrapping), but tracing args are not. # (These fallbacks can be removed once google-api-core < 2.36.0 is no longer supported.) - for k in ["client_options", "method_name", "is_streaming"]: + for k in ["client_options", "method_name", "is_streaming", "default_host"]: kwargs.pop(k, None) try: diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/grpc.py index 3a72ba37662e..038f440f28ac 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/grpc.py @@ -20,7 +20,11 @@ from typing import Callable, Dict, Optional, Sequence, Tuple, Union from typing import TYPE_CHECKING -from google.showcase_v1beta1._compat import _observability, apply_channel_interceptors +from google.showcase_v1beta1._compat import ( + _observability, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, + apply_channel_interceptors, +) from google.api_core import client_options as client_options_lib from google.api_core import grpc_helpers @@ -278,9 +282,12 @@ def __init__(self, *, ) channel_interceptors = list(interceptors) if interceptors else [] + _otel_interceptor_kwargs = ( + {"default_host": self._default_host} if _SUPPORTS_DEFAULT_HOST else {} + ) if ( _observability is not None - and (otel_interceptor := _observability.get_otel_interceptor(self._client_options)) is not None + and (otel_interceptor := _observability.get_otel_interceptor(self._client_options, **_otel_interceptor_kwargs)) is not None and otel_interceptor not in channel_interceptors and not any(getattr(i, "_is_otel_interceptor", None) is True for i in channel_interceptors) ): diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/grpc_asyncio.py index 90527f137ad8..bbc3afe07288 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/grpc_asyncio.py @@ -20,7 +20,11 @@ from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union from typing import TYPE_CHECKING -from google.showcase_v1beta1._compat import _observability, apply_async_channel_interceptors +from google.showcase_v1beta1._compat import ( + _observability, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, + apply_async_channel_interceptors, +) from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 @@ -336,8 +340,11 @@ def __init__(self, *, # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= v2.43.0. + _otel_interceptor_kwargs = ( + {"default_host": self._default_host} if _SUPPORTS_DEFAULT_HOST else {} + ) # pragma: NO COVER self._grpc_channel = _observability.apply_otel_async_channel_interceptors( - self._grpc_channel, self._client_options + self._grpc_channel, self._client_options, **_otel_interceptor_kwargs ) # pragma: NO COVER self._logged_channel = self._grpc_channel diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/base.py index b1c947a35f80..43130ac52102 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/base.py @@ -14,13 +14,12 @@ # limitations under the License. # import abc -import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.showcase_v1beta1 import gapic_version as package_version from google.showcase_v1beta1._compat import ( - _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) _ASYNC_WRAP_METHOD_SUPPORTS_TRACING = _WRAP_METHOD_SUPPORTS_TRACING @@ -137,25 +136,13 @@ def __init__( host += ':443' self._host = host - # If the transport was instantiated directly with client_options=None - # while OpenTelemetry tracing is enabled via environment variable, - # create a ClientOptions instance with the resolved transport host so - # method and wire spans can record server.address and server.port. - if ( - client_options is None - and _observability is not None - and _observability.is_otel_capabilities_enabled(None) - ): - client_options = client_options_lib.ClientOptions(api_endpoint=self._host) - # And here we check for the presence of an api_endpoint attr OR key, - # shallow-copying before populating to avoid mutating caller-owned state. - elif isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: - client_options = copy.copy(client_options) - client_options.api_endpoint = self._host - elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): - client_options = copy.copy(client_options) - client_options["api_endpoint"] = self._host self._client_options = client_options + # If a pre-constructed gRPC channel was provided to the subclass before + # super().__init__, self._host is not used to create the channel and + # cannot be trusted as the default span endpoint. + self._default_host: Optional[str] = ( + None if getattr(self, "_grpc_channel", None) is not None else self._host + ) self._wrapped_methods: Dict[Callable, Callable] = {} @property @@ -174,6 +161,8 @@ def _wrap_method(self, func, *args, **kwargs): """ if _WRAP_METHOD_SUPPORTS_TRACING: kwargs["client_options"] = self._client_options + if _SUPPORTS_DEFAULT_HOST: + kwargs["default_host"] = self._default_host try: kind = self.kind except NotImplementedError: @@ -186,7 +175,7 @@ def _wrap_method(self, func, *args, **kwargs): # - Era 2 (>= 2.29.0, < 2.36.0): `kind` is supported (prevents REST from # falling back to gRPC wrapping), but tracing args are not. # (These fallbacks can be removed once google-api-core < 2.36.0 is no longer supported.) - for k in ["client_options", "method_name", "is_streaming"]: + for k in ["client_options", "method_name", "is_streaming", "default_host"]: kwargs.pop(k, None) try: @@ -213,6 +202,8 @@ def _wrap_async_method(self, func, *args, **kwargs): """ if _ASYNC_WRAP_METHOD_SUPPORTS_TRACING: kwargs["client_options"] = self._client_options + if _SUPPORTS_DEFAULT_HOST: + kwargs["default_host"] = self._default_host try: kind = self.kind except NotImplementedError: @@ -225,7 +216,7 @@ def _wrap_async_method(self, func, *args, **kwargs): # - Era 2 (>= 2.29.0, < 2.36.0): `kind` is supported (prevents REST from # falling back to gRPC wrapping), but tracing args are not. # (These fallbacks can be removed once google-api-core < 2.36.0 is no longer supported.) - for k in ["client_options", "method_name", "is_streaming"]: + for k in ["client_options", "method_name", "is_streaming", "default_host"]: kwargs.pop(k, None) try: diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/grpc.py index 4da466085716..507ea21e57c7 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/grpc.py @@ -20,7 +20,11 @@ from typing import Callable, Dict, Optional, Sequence, Tuple, Union from typing import TYPE_CHECKING -from google.showcase_v1beta1._compat import _observability, apply_channel_interceptors +from google.showcase_v1beta1._compat import ( + _observability, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, + apply_channel_interceptors, +) from google.api_core import client_options as client_options_lib from google.api_core import grpc_helpers @@ -283,9 +287,12 @@ def __init__(self, *, ) channel_interceptors = list(interceptors) if interceptors else [] + _otel_interceptor_kwargs = ( + {"default_host": self._default_host} if _SUPPORTS_DEFAULT_HOST else {} + ) if ( _observability is not None - and (otel_interceptor := _observability.get_otel_interceptor(self._client_options)) is not None + and (otel_interceptor := _observability.get_otel_interceptor(self._client_options, **_otel_interceptor_kwargs)) is not None and otel_interceptor not in channel_interceptors and not any(getattr(i, "_is_otel_interceptor", None) is True for i in channel_interceptors) ): diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/grpc_asyncio.py index 318dff105c1a..fa1acfb38691 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/grpc_asyncio.py @@ -20,7 +20,11 @@ from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union from typing import TYPE_CHECKING -from google.showcase_v1beta1._compat import _observability, apply_async_channel_interceptors +from google.showcase_v1beta1._compat import ( + _observability, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, + apply_async_channel_interceptors, +) from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 @@ -341,8 +345,11 @@ def __init__(self, *, # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= v2.43.0. + _otel_interceptor_kwargs = ( + {"default_host": self._default_host} if _SUPPORTS_DEFAULT_HOST else {} + ) # pragma: NO COVER self._grpc_channel = _observability.apply_otel_async_channel_interceptors( - self._grpc_channel, self._client_options + self._grpc_channel, self._client_options, **_otel_interceptor_kwargs ) # pragma: NO COVER self._logged_channel = self._grpc_channel diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/base.py index 22b2609ecfcf..9464d130f3e4 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/base.py @@ -14,13 +14,12 @@ # limitations under the License. # import abc -import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.showcase_v1beta1 import gapic_version as package_version from google.showcase_v1beta1._compat import ( - _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) _ASYNC_WRAP_METHOD_SUPPORTS_TRACING = _WRAP_METHOD_SUPPORTS_TRACING @@ -137,25 +136,13 @@ def __init__( host += ':443' self._host = host - # If the transport was instantiated directly with client_options=None - # while OpenTelemetry tracing is enabled via environment variable, - # create a ClientOptions instance with the resolved transport host so - # method and wire spans can record server.address and server.port. - if ( - client_options is None - and _observability is not None - and _observability.is_otel_capabilities_enabled(None) - ): - client_options = client_options_lib.ClientOptions(api_endpoint=self._host) - # And here we check for the presence of an api_endpoint attr OR key, - # shallow-copying before populating to avoid mutating caller-owned state. - elif isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: - client_options = copy.copy(client_options) - client_options.api_endpoint = self._host - elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): - client_options = copy.copy(client_options) - client_options["api_endpoint"] = self._host self._client_options = client_options + # If a pre-constructed gRPC channel was provided to the subclass before + # super().__init__, self._host is not used to create the channel and + # cannot be trusted as the default span endpoint. + self._default_host: Optional[str] = ( + None if getattr(self, "_grpc_channel", None) is not None else self._host + ) self._wrapped_methods: Dict[Callable, Callable] = {} @property @@ -174,6 +161,8 @@ def _wrap_method(self, func, *args, **kwargs): """ if _WRAP_METHOD_SUPPORTS_TRACING: kwargs["client_options"] = self._client_options + if _SUPPORTS_DEFAULT_HOST: + kwargs["default_host"] = self._default_host try: kind = self.kind except NotImplementedError: @@ -186,7 +175,7 @@ def _wrap_method(self, func, *args, **kwargs): # - Era 2 (>= 2.29.0, < 2.36.0): `kind` is supported (prevents REST from # falling back to gRPC wrapping), but tracing args are not. # (These fallbacks can be removed once google-api-core < 2.36.0 is no longer supported.) - for k in ["client_options", "method_name", "is_streaming"]: + for k in ["client_options", "method_name", "is_streaming", "default_host"]: kwargs.pop(k, None) try: @@ -213,6 +202,8 @@ def _wrap_async_method(self, func, *args, **kwargs): """ if _ASYNC_WRAP_METHOD_SUPPORTS_TRACING: kwargs["client_options"] = self._client_options + if _SUPPORTS_DEFAULT_HOST: + kwargs["default_host"] = self._default_host try: kind = self.kind except NotImplementedError: @@ -225,7 +216,7 @@ def _wrap_async_method(self, func, *args, **kwargs): # - Era 2 (>= 2.29.0, < 2.36.0): `kind` is supported (prevents REST from # falling back to gRPC wrapping), but tracing args are not. # (These fallbacks can be removed once google-api-core < 2.36.0 is no longer supported.) - for k in ["client_options", "method_name", "is_streaming"]: + for k in ["client_options", "method_name", "is_streaming", "default_host"]: kwargs.pop(k, None) try: diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc.py index 66a696e695f0..bb49692b3852 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc.py @@ -20,7 +20,11 @@ from typing import Callable, Dict, Optional, Sequence, Tuple, Union from typing import TYPE_CHECKING -from google.showcase_v1beta1._compat import _observability, apply_channel_interceptors +from google.showcase_v1beta1._compat import ( + _observability, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, + apply_channel_interceptors, +) from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions @@ -288,9 +292,12 @@ def __init__(self, *, ) channel_interceptors = list(interceptors) if interceptors else [] + _otel_interceptor_kwargs = ( + {"default_host": self._default_host} if _SUPPORTS_DEFAULT_HOST else {} + ) if ( _observability is not None - and (otel_interceptor := _observability.get_otel_interceptor(self._client_options)) is not None + and (otel_interceptor := _observability.get_otel_interceptor(self._client_options, **_otel_interceptor_kwargs)) is not None and otel_interceptor not in channel_interceptors and not any(getattr(i, "_is_otel_interceptor", None) is True for i in channel_interceptors) ): diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py index 48a2dc9140a1..14233a480f93 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py @@ -20,7 +20,11 @@ from typing import Any, Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union from typing import TYPE_CHECKING -from google.showcase_v1beta1._compat import _observability, apply_async_channel_interceptors +from google.showcase_v1beta1._compat import ( + _observability, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, + apply_async_channel_interceptors, +) from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 @@ -349,8 +353,11 @@ def __init__(self, *, # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= v2.43.0. + _otel_interceptor_kwargs = ( + {"default_host": self._default_host} if _SUPPORTS_DEFAULT_HOST else {} + ) # pragma: NO COVER self._grpc_channel = _observability.apply_otel_async_channel_interceptors( - self._grpc_channel, self._client_options + self._grpc_channel, self._client_options, **_otel_interceptor_kwargs ) # pragma: NO COVER self._logged_channel = self._grpc_channel diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/base.py index 22d317b32796..8ab1249004dc 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/base.py @@ -14,13 +14,12 @@ # limitations under the License. # import abc -import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.showcase_v1beta1 import gapic_version as package_version from google.showcase_v1beta1._compat import ( - _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) _ASYNC_WRAP_METHOD_SUPPORTS_TRACING = _WRAP_METHOD_SUPPORTS_TRACING @@ -137,25 +136,13 @@ def __init__( host += ':443' self._host = host - # If the transport was instantiated directly with client_options=None - # while OpenTelemetry tracing is enabled via environment variable, - # create a ClientOptions instance with the resolved transport host so - # method and wire spans can record server.address and server.port. - if ( - client_options is None - and _observability is not None - and _observability.is_otel_capabilities_enabled(None) - ): - client_options = client_options_lib.ClientOptions(api_endpoint=self._host) - # And here we check for the presence of an api_endpoint attr OR key, - # shallow-copying before populating to avoid mutating caller-owned state. - elif isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: - client_options = copy.copy(client_options) - client_options.api_endpoint = self._host - elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): - client_options = copy.copy(client_options) - client_options["api_endpoint"] = self._host self._client_options = client_options + # If a pre-constructed gRPC channel was provided to the subclass before + # super().__init__, self._host is not used to create the channel and + # cannot be trusted as the default span endpoint. + self._default_host: Optional[str] = ( + None if getattr(self, "_grpc_channel", None) is not None else self._host + ) self._wrapped_methods: Dict[Callable, Callable] = {} @property @@ -174,6 +161,8 @@ def _wrap_method(self, func, *args, **kwargs): """ if _WRAP_METHOD_SUPPORTS_TRACING: kwargs["client_options"] = self._client_options + if _SUPPORTS_DEFAULT_HOST: + kwargs["default_host"] = self._default_host try: kind = self.kind except NotImplementedError: @@ -186,7 +175,7 @@ def _wrap_method(self, func, *args, **kwargs): # - Era 2 (>= 2.29.0, < 2.36.0): `kind` is supported (prevents REST from # falling back to gRPC wrapping), but tracing args are not. # (These fallbacks can be removed once google-api-core < 2.36.0 is no longer supported.) - for k in ["client_options", "method_name", "is_streaming"]: + for k in ["client_options", "method_name", "is_streaming", "default_host"]: kwargs.pop(k, None) try: @@ -213,6 +202,8 @@ def _wrap_async_method(self, func, *args, **kwargs): """ if _ASYNC_WRAP_METHOD_SUPPORTS_TRACING: kwargs["client_options"] = self._client_options + if _SUPPORTS_DEFAULT_HOST: + kwargs["default_host"] = self._default_host try: kind = self.kind except NotImplementedError: @@ -225,7 +216,7 @@ def _wrap_async_method(self, func, *args, **kwargs): # - Era 2 (>= 2.29.0, < 2.36.0): `kind` is supported (prevents REST from # falling back to gRPC wrapping), but tracing args are not. # (These fallbacks can be removed once google-api-core < 2.36.0 is no longer supported.) - for k in ["client_options", "method_name", "is_streaming"]: + for k in ["client_options", "method_name", "is_streaming", "default_host"]: kwargs.pop(k, None) try: diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/grpc.py index c26a4d8ec34c..d0b4bfcdc950 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/grpc.py @@ -20,7 +20,11 @@ from typing import Callable, Dict, Optional, Sequence, Tuple, Union from typing import TYPE_CHECKING -from google.showcase_v1beta1._compat import _observability, apply_channel_interceptors +from google.showcase_v1beta1._compat import ( + _observability, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, + apply_channel_interceptors, +) from google.api_core import client_options as client_options_lib from google.api_core import grpc_helpers @@ -281,9 +285,12 @@ def __init__(self, *, ) channel_interceptors = list(interceptors) if interceptors else [] + _otel_interceptor_kwargs = ( + {"default_host": self._default_host} if _SUPPORTS_DEFAULT_HOST else {} + ) if ( _observability is not None - and (otel_interceptor := _observability.get_otel_interceptor(self._client_options)) is not None + and (otel_interceptor := _observability.get_otel_interceptor(self._client_options, **_otel_interceptor_kwargs)) is not None and otel_interceptor not in channel_interceptors and not any(getattr(i, "_is_otel_interceptor", None) is True for i in channel_interceptors) ): diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/grpc_asyncio.py index 033643cbf9bd..37cd7e01265b 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/grpc_asyncio.py @@ -20,7 +20,11 @@ from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union from typing import TYPE_CHECKING -from google.showcase_v1beta1._compat import _observability, apply_async_channel_interceptors +from google.showcase_v1beta1._compat import ( + _observability, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, + apply_async_channel_interceptors, +) from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 @@ -339,8 +343,11 @@ def __init__(self, *, # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= v2.43.0. + _otel_interceptor_kwargs = ( + {"default_host": self._default_host} if _SUPPORTS_DEFAULT_HOST else {} + ) # pragma: NO COVER self._grpc_channel = _observability.apply_otel_async_channel_interceptors( - self._grpc_channel, self._client_options + self._grpc_channel, self._client_options, **_otel_interceptor_kwargs ) # pragma: NO COVER self._logged_channel = self._grpc_channel diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/base.py index e6431cf277c5..95c8cc1b89e5 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/base.py @@ -14,13 +14,12 @@ # limitations under the License. # import abc -import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.showcase_v1beta1 import gapic_version as package_version from google.showcase_v1beta1._compat import ( - _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) _ASYNC_WRAP_METHOD_SUPPORTS_TRACING = _WRAP_METHOD_SUPPORTS_TRACING @@ -136,25 +135,13 @@ def __init__( host += ':443' self._host = host - # If the transport was instantiated directly with client_options=None - # while OpenTelemetry tracing is enabled via environment variable, - # create a ClientOptions instance with the resolved transport host so - # method and wire spans can record server.address and server.port. - if ( - client_options is None - and _observability is not None - and _observability.is_otel_capabilities_enabled(None) - ): - client_options = client_options_lib.ClientOptions(api_endpoint=self._host) - # And here we check for the presence of an api_endpoint attr OR key, - # shallow-copying before populating to avoid mutating caller-owned state. - elif isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: - client_options = copy.copy(client_options) - client_options.api_endpoint = self._host - elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): - client_options = copy.copy(client_options) - client_options["api_endpoint"] = self._host self._client_options = client_options + # If a pre-constructed gRPC channel was provided to the subclass before + # super().__init__, self._host is not used to create the channel and + # cannot be trusted as the default span endpoint. + self._default_host: Optional[str] = ( + None if getattr(self, "_grpc_channel", None) is not None else self._host + ) self._wrapped_methods: Dict[Callable, Callable] = {} @property @@ -173,6 +160,8 @@ def _wrap_method(self, func, *args, **kwargs): """ if _WRAP_METHOD_SUPPORTS_TRACING: kwargs["client_options"] = self._client_options + if _SUPPORTS_DEFAULT_HOST: + kwargs["default_host"] = self._default_host try: kind = self.kind except NotImplementedError: @@ -185,7 +174,7 @@ def _wrap_method(self, func, *args, **kwargs): # - Era 2 (>= 2.29.0, < 2.36.0): `kind` is supported (prevents REST from # falling back to gRPC wrapping), but tracing args are not. # (These fallbacks can be removed once google-api-core < 2.36.0 is no longer supported.) - for k in ["client_options", "method_name", "is_streaming"]: + for k in ["client_options", "method_name", "is_streaming", "default_host"]: kwargs.pop(k, None) try: @@ -212,6 +201,8 @@ def _wrap_async_method(self, func, *args, **kwargs): """ if _ASYNC_WRAP_METHOD_SUPPORTS_TRACING: kwargs["client_options"] = self._client_options + if _SUPPORTS_DEFAULT_HOST: + kwargs["default_host"] = self._default_host try: kind = self.kind except NotImplementedError: @@ -224,7 +215,7 @@ def _wrap_async_method(self, func, *args, **kwargs): # - Era 2 (>= 2.29.0, < 2.36.0): `kind` is supported (prevents REST from # falling back to gRPC wrapping), but tracing args are not. # (These fallbacks can be removed once google-api-core < 2.36.0 is no longer supported.) - for k in ["client_options", "method_name", "is_streaming"]: + for k in ["client_options", "method_name", "is_streaming", "default_host"]: kwargs.pop(k, None) try: diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/grpc.py index 81aa2400356c..43ae9163f120 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/grpc.py @@ -20,7 +20,11 @@ from typing import Callable, Dict, Optional, Sequence, Tuple, Union from typing import TYPE_CHECKING -from google.showcase_v1beta1._compat import _observability, apply_channel_interceptors +from google.showcase_v1beta1._compat import ( + _observability, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, + apply_channel_interceptors, +) from google.api_core import client_options as client_options_lib from google.api_core import grpc_helpers @@ -284,9 +288,12 @@ def __init__(self, *, ) channel_interceptors = list(interceptors) if interceptors else [] + _otel_interceptor_kwargs = ( + {"default_host": self._default_host} if _SUPPORTS_DEFAULT_HOST else {} + ) if ( _observability is not None - and (otel_interceptor := _observability.get_otel_interceptor(self._client_options)) is not None + and (otel_interceptor := _observability.get_otel_interceptor(self._client_options, **_otel_interceptor_kwargs)) is not None and otel_interceptor not in channel_interceptors and not any(getattr(i, "_is_otel_interceptor", None) is True for i in channel_interceptors) ): diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/grpc_asyncio.py index 0ce4f859b8df..cdc1d93787ed 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/grpc_asyncio.py @@ -20,7 +20,11 @@ from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union from typing import TYPE_CHECKING -from google.showcase_v1beta1._compat import _observability, apply_async_channel_interceptors +from google.showcase_v1beta1._compat import ( + _observability, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, + apply_async_channel_interceptors, +) from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 @@ -342,8 +346,11 @@ def __init__(self, *, # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= v2.43.0. + _otel_interceptor_kwargs = ( + {"default_host": self._default_host} if _SUPPORTS_DEFAULT_HOST else {} + ) # pragma: NO COVER self._grpc_channel = _observability.apply_otel_async_channel_interceptors( - self._grpc_channel, self._client_options + self._grpc_channel, self._client_options, **_otel_interceptor_kwargs ) # pragma: NO COVER self._logged_channel = self._grpc_channel diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_echo.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_echo.py index d28b4688187d..b751f85b5f75 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_echo.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_echo.py @@ -1148,7 +1148,14 @@ def test_echo_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(transport._client_options) + expected_kwargs = ( + {"default_host": transport._default_host} + if transports.grpc._SUPPORTS_DEFAULT_HOST + else {} + ) + mock_get_interceptor.assert_called_once_with( + transport._client_options, **expected_kwargs + ) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -1222,8 +1229,13 @@ def test_echo_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) + expected_kwargs = ( + {"default_host": transport._default_host} + if transports.grpc_asyncio._SUPPORTS_DEFAULT_HOST + else {} + ) mock_apply_async_interceptors.assert_called_once_with( - mock_channel, transport._client_options + mock_channel, transport._client_options, **expected_kwargs ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel @@ -9782,64 +9794,73 @@ def test_api_key_credentials(client_class, transport_class): ) -def test_echo_base_transport_wrap_method_modern_api_core(monkeypatch): +@pytest.mark.parametrize( + "supports_default_host,is_concrete,expected_kind,expected_has_default_host", + [ + pytest.param( + True, + True, + "grpc", + True, + id="concrete_transport_supports_default_host", + ), + pytest.param( + False, + False, + None, + False, + id="base_transport_no_default_host", + ), + ], +) +def test_echo_base_transport_wrap_method_modern_api_core( + monkeypatch, + supports_default_host, + is_concrete, + expected_kind, + expected_has_default_host, +): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", True) - # 1. Concrete transport: passes client_options=None and kind="grpc" + monkeypatch.setattr(transports.base, "_SUPPORTS_DEFAULT_HOST", supports_default_host) + transport_cls = ( + transports.EchoGrpcTransport + if is_concrete + else transports.EchoTransport + ) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - transport = transports.EchoGrpcTransport( - credentials=ga_credentials.AnonymousCredentials() - ) + transport = transport_cls(credentials=ga_credentials.AnonymousCredentials()) func = mock.MagicMock() transport._wrap_method(func) - assert mock_wrap.call_args.kwargs["kind"] == "grpc" - assert "client_options" in mock_wrap.call_args.kwargs - - # 2. Base transport: kind raises NotImplementedError and is cleanly omitted - with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - base_transport = transports.EchoTransport( - credentials=ga_credentials.AnonymousCredentials() - ) - func = mock.MagicMock() - base_transport._wrap_method(func) - assert "kind" not in mock_wrap.call_args.kwargs + if expected_kind: + assert mock_wrap.call_args.kwargs["kind"] == expected_kind + else: + assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs + if expected_has_default_host: + assert mock_wrap.call_args.kwargs["default_host"] == transport._default_host + else: + assert "default_host" not in mock_wrap.call_args.kwargs -@pytest.mark.parametrize( - "otel_enabled,expected_endpoint", - [ - pytest.param(False, None, id="otel_disabled"), - pytest.param(True, "default.example.com:443", id="otel_enabled"), - ], -) -def test_echo_base_transport_client_options_none( - monkeypatch, otel_enabled, expected_endpoint -): - fake_observability = mock.MagicMock() - fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled - monkeypatch.setattr(transports.base, "_observability", fake_observability) - +def test_echo_base_transport_client_options_none(): transport = transports.EchoTransport( host="default.example.com:443", credentials=ga_credentials.AnonymousCredentials(), client_options=None, ) - assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + assert transport._client_options is None + assert transport._default_host == "default.example.com:443" @pytest.mark.parametrize( - "initial_endpoint,expected_endpoint", + "initial_endpoint", [ - pytest.param(None, "default.example.com:443", id="default_from_host"), - pytest.param( - "custom.example.com:443", - "custom.example.com:443", - id="preserve_explicit", - ), + pytest.param(None, id="unset_api_endpoint"), + pytest.param("custom.example.com:443", id="explicit_api_endpoint"), ], ) def test_echo_base_transport_client_options_object( - initial_endpoint, expected_endpoint + initial_endpoint, ): options = client_options.ClientOptions(api_endpoint=initial_endpoint) transport = transports.EchoTransport( @@ -9847,27 +9868,28 @@ def test_echo_base_transport_client_options_object( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options.api_endpoint == expected_endpoint - assert options.api_endpoint == initial_endpoint + assert transport._client_options is options + assert transport._client_options.api_endpoint == initial_endpoint + assert transport._default_host == "default.example.com:443" @pytest.mark.parametrize( - "initial_options,expected_options", + "initial_options,expected_endpoint", [ pytest.param( {}, - {"api_endpoint": "default.example.com:443"}, - id="default_from_host", + None, + id="unset_api_endpoint", ), pytest.param( {"api_endpoint": "custom.example.com:443"}, - {"api_endpoint": "custom.example.com:443"}, - id="preserve_explicit", + "custom.example.com:443", + id="explicit_api_endpoint", ), ], ) def test_echo_base_transport_client_options_dict( - initial_options, expected_options + initial_options, expected_endpoint ): options = dict(initial_options) transport = transports.EchoTransport( @@ -9875,10 +9897,29 @@ def test_echo_base_transport_client_options_dict( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options == expected_options + assert transport._client_options is options + assert transport._client_options.get("api_endpoint") == expected_endpoint + assert transport._default_host == "default.example.com:443" assert options == initial_options +def test_echo_base_transport_default_host_ignored_when_custom_grpc_channel(): + class _CustomChannelTransport(transports.EchoTransport): + def __init__(self, **kwargs): + self._ignore_credentials = True + self._grpc_channel = mock.sentinel.custom_channel + super().__init__(**kwargs) + + options = client_options.ClientOptions() + transport = _CustomChannelTransport( + host="default.example.com:443", + client_options=options, + ) + assert transport._client_options is options + assert transport._client_options.api_endpoint is None + assert transport._default_host is None + + def test_echo_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): @@ -9953,27 +9994,52 @@ def test_echo_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): assert "is_streaming" not in kwargs -def test_echo_base_transport_wrap_async_method_modern_api_core(monkeypatch): +@pytest.mark.parametrize( + "supports_default_host,is_concrete,expected_kind,expected_has_default_host", + [ + pytest.param( + True, + True, + "grpc_asyncio", + True, + id="concrete_transport_supports_default_host", + ), + pytest.param( + False, + False, + None, + False, + id="base_transport_no_default_host", + ), + ], +) +def test_echo_base_transport_wrap_async_method_modern_api_core( + monkeypatch, + supports_default_host, + is_concrete, + expected_kind, + expected_has_default_host, +): monkeypatch.setattr(transports.base, "_ASYNC_WRAP_METHOD_SUPPORTS_TRACING", True) - # 1. Concrete transport: passes client_options=None and kind="grpc_asyncio" + monkeypatch.setattr(transports.base, "_SUPPORTS_DEFAULT_HOST", supports_default_host) + transport_cls = ( + transports.EchoGrpcAsyncIOTransport + if is_concrete + else transports.EchoTransport + ) with mock.patch.object(transports.base.gapic_v1.method_async, "wrap_method") as mock_wrap: - transport = transports.EchoGrpcAsyncIOTransport( - credentials=ga_credentials.AnonymousCredentials() - ) + transport = transport_cls(credentials=ga_credentials.AnonymousCredentials()) func = mock.MagicMock() transport._wrap_async_method(func) - assert mock_wrap.call_args.kwargs["kind"] == "grpc_asyncio" - assert "client_options" in mock_wrap.call_args.kwargs - - # 2. Base transport: kind raises NotImplementedError and is cleanly omitted - with mock.patch.object(transports.base.gapic_v1.method_async, "wrap_method") as mock_wrap: - base_transport = transports.EchoTransport( - credentials=ga_credentials.AnonymousCredentials() - ) - func = mock.MagicMock() - base_transport._wrap_async_method(func) - assert "kind" not in mock_wrap.call_args.kwargs + if expected_kind: + assert mock_wrap.call_args.kwargs["kind"] == expected_kind + else: + assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs + if expected_has_default_host: + assert mock_wrap.call_args.kwargs["default_host"] == transport._default_host + else: + assert "default_host" not in mock_wrap.call_args.kwargs def test_echo_base_transport_wrap_async_method_older_api_core_fallbacks(monkeypatch): diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_identity.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_identity.py index bbe163a03bb1..18b3b0ca0843 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_identity.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_identity.py @@ -849,7 +849,14 @@ def test_identity_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(transport._client_options) + expected_kwargs = ( + {"default_host": transport._default_host} + if transports.grpc._SUPPORTS_DEFAULT_HOST + else {} + ) + mock_get_interceptor.assert_called_once_with( + transport._client_options, **expected_kwargs + ) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -923,8 +930,13 @@ def test_identity_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) + expected_kwargs = ( + {"default_host": transport._default_host} + if transports.grpc_asyncio._SUPPORTS_DEFAULT_HOST + else {} + ) mock_apply_async_interceptors.assert_called_once_with( - mock_channel, transport._client_options + mock_channel, transport._client_options, **expected_kwargs ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel @@ -6889,64 +6901,73 @@ def test_api_key_credentials(client_class, transport_class): ) -def test_identity_base_transport_wrap_method_modern_api_core(monkeypatch): +@pytest.mark.parametrize( + "supports_default_host,is_concrete,expected_kind,expected_has_default_host", + [ + pytest.param( + True, + True, + "grpc", + True, + id="concrete_transport_supports_default_host", + ), + pytest.param( + False, + False, + None, + False, + id="base_transport_no_default_host", + ), + ], +) +def test_identity_base_transport_wrap_method_modern_api_core( + monkeypatch, + supports_default_host, + is_concrete, + expected_kind, + expected_has_default_host, +): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", True) - # 1. Concrete transport: passes client_options=None and kind="grpc" + monkeypatch.setattr(transports.base, "_SUPPORTS_DEFAULT_HOST", supports_default_host) + transport_cls = ( + transports.IdentityGrpcTransport + if is_concrete + else transports.IdentityTransport + ) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - transport = transports.IdentityGrpcTransport( - credentials=ga_credentials.AnonymousCredentials() - ) + transport = transport_cls(credentials=ga_credentials.AnonymousCredentials()) func = mock.MagicMock() transport._wrap_method(func) - assert mock_wrap.call_args.kwargs["kind"] == "grpc" - assert "client_options" in mock_wrap.call_args.kwargs - - # 2. Base transport: kind raises NotImplementedError and is cleanly omitted - with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - base_transport = transports.IdentityTransport( - credentials=ga_credentials.AnonymousCredentials() - ) - func = mock.MagicMock() - base_transport._wrap_method(func) - assert "kind" not in mock_wrap.call_args.kwargs + if expected_kind: + assert mock_wrap.call_args.kwargs["kind"] == expected_kind + else: + assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs + if expected_has_default_host: + assert mock_wrap.call_args.kwargs["default_host"] == transport._default_host + else: + assert "default_host" not in mock_wrap.call_args.kwargs -@pytest.mark.parametrize( - "otel_enabled,expected_endpoint", - [ - pytest.param(False, None, id="otel_disabled"), - pytest.param(True, "default.example.com:443", id="otel_enabled"), - ], -) -def test_identity_base_transport_client_options_none( - monkeypatch, otel_enabled, expected_endpoint -): - fake_observability = mock.MagicMock() - fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled - monkeypatch.setattr(transports.base, "_observability", fake_observability) - +def test_identity_base_transport_client_options_none(): transport = transports.IdentityTransport( host="default.example.com:443", credentials=ga_credentials.AnonymousCredentials(), client_options=None, ) - assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + assert transport._client_options is None + assert transport._default_host == "default.example.com:443" @pytest.mark.parametrize( - "initial_endpoint,expected_endpoint", + "initial_endpoint", [ - pytest.param(None, "default.example.com:443", id="default_from_host"), - pytest.param( - "custom.example.com:443", - "custom.example.com:443", - id="preserve_explicit", - ), + pytest.param(None, id="unset_api_endpoint"), + pytest.param("custom.example.com:443", id="explicit_api_endpoint"), ], ) def test_identity_base_transport_client_options_object( - initial_endpoint, expected_endpoint + initial_endpoint, ): options = client_options.ClientOptions(api_endpoint=initial_endpoint) transport = transports.IdentityTransport( @@ -6954,27 +6975,28 @@ def test_identity_base_transport_client_options_object( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options.api_endpoint == expected_endpoint - assert options.api_endpoint == initial_endpoint + assert transport._client_options is options + assert transport._client_options.api_endpoint == initial_endpoint + assert transport._default_host == "default.example.com:443" @pytest.mark.parametrize( - "initial_options,expected_options", + "initial_options,expected_endpoint", [ pytest.param( {}, - {"api_endpoint": "default.example.com:443"}, - id="default_from_host", + None, + id="unset_api_endpoint", ), pytest.param( {"api_endpoint": "custom.example.com:443"}, - {"api_endpoint": "custom.example.com:443"}, - id="preserve_explicit", + "custom.example.com:443", + id="explicit_api_endpoint", ), ], ) def test_identity_base_transport_client_options_dict( - initial_options, expected_options + initial_options, expected_endpoint ): options = dict(initial_options) transport = transports.IdentityTransport( @@ -6982,10 +7004,29 @@ def test_identity_base_transport_client_options_dict( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options == expected_options + assert transport._client_options is options + assert transport._client_options.get("api_endpoint") == expected_endpoint + assert transport._default_host == "default.example.com:443" assert options == initial_options +def test_identity_base_transport_default_host_ignored_when_custom_grpc_channel(): + class _CustomChannelTransport(transports.IdentityTransport): + def __init__(self, **kwargs): + self._ignore_credentials = True + self._grpc_channel = mock.sentinel.custom_channel + super().__init__(**kwargs) + + options = client_options.ClientOptions() + transport = _CustomChannelTransport( + host="default.example.com:443", + client_options=options, + ) + assert transport._client_options is options + assert transport._client_options.api_endpoint is None + assert transport._default_host is None + + def test_identity_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): @@ -7060,27 +7101,52 @@ def test_identity_base_transport_wrap_method_older_api_core_fallbacks(monkeypatc assert "is_streaming" not in kwargs -def test_identity_base_transport_wrap_async_method_modern_api_core(monkeypatch): +@pytest.mark.parametrize( + "supports_default_host,is_concrete,expected_kind,expected_has_default_host", + [ + pytest.param( + True, + True, + "grpc_asyncio", + True, + id="concrete_transport_supports_default_host", + ), + pytest.param( + False, + False, + None, + False, + id="base_transport_no_default_host", + ), + ], +) +def test_identity_base_transport_wrap_async_method_modern_api_core( + monkeypatch, + supports_default_host, + is_concrete, + expected_kind, + expected_has_default_host, +): monkeypatch.setattr(transports.base, "_ASYNC_WRAP_METHOD_SUPPORTS_TRACING", True) - # 1. Concrete transport: passes client_options=None and kind="grpc_asyncio" + monkeypatch.setattr(transports.base, "_SUPPORTS_DEFAULT_HOST", supports_default_host) + transport_cls = ( + transports.IdentityGrpcAsyncIOTransport + if is_concrete + else transports.IdentityTransport + ) with mock.patch.object(transports.base.gapic_v1.method_async, "wrap_method") as mock_wrap: - transport = transports.IdentityGrpcAsyncIOTransport( - credentials=ga_credentials.AnonymousCredentials() - ) + transport = transport_cls(credentials=ga_credentials.AnonymousCredentials()) func = mock.MagicMock() transport._wrap_async_method(func) - assert mock_wrap.call_args.kwargs["kind"] == "grpc_asyncio" - assert "client_options" in mock_wrap.call_args.kwargs - - # 2. Base transport: kind raises NotImplementedError and is cleanly omitted - with mock.patch.object(transports.base.gapic_v1.method_async, "wrap_method") as mock_wrap: - base_transport = transports.IdentityTransport( - credentials=ga_credentials.AnonymousCredentials() - ) - func = mock.MagicMock() - base_transport._wrap_async_method(func) - assert "kind" not in mock_wrap.call_args.kwargs + if expected_kind: + assert mock_wrap.call_args.kwargs["kind"] == expected_kind + else: + assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs + if expected_has_default_host: + assert mock_wrap.call_args.kwargs["default_host"] == transport._default_host + else: + assert "default_host" not in mock_wrap.call_args.kwargs def test_identity_base_transport_wrap_async_method_older_api_core_fallbacks(monkeypatch): diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_messaging.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_messaging.py index fbbbcac1f4a4..b5a12bff24f6 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_messaging.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_messaging.py @@ -853,7 +853,14 @@ def test_messaging_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(transport._client_options) + expected_kwargs = ( + {"default_host": transport._default_host} + if transports.grpc._SUPPORTS_DEFAULT_HOST + else {} + ) + mock_get_interceptor.assert_called_once_with( + transport._client_options, **expected_kwargs + ) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -927,8 +934,13 @@ def test_messaging_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) + expected_kwargs = ( + {"default_host": transport._default_host} + if transports.grpc_asyncio._SUPPORTS_DEFAULT_HOST + else {} + ) mock_apply_async_interceptors.assert_called_once_with( - mock_channel, transport._client_options + mock_channel, transport._client_options, **expected_kwargs ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel @@ -11789,64 +11801,73 @@ def test_api_key_credentials(client_class, transport_class): ) -def test_messaging_base_transport_wrap_method_modern_api_core(monkeypatch): +@pytest.mark.parametrize( + "supports_default_host,is_concrete,expected_kind,expected_has_default_host", + [ + pytest.param( + True, + True, + "grpc", + True, + id="concrete_transport_supports_default_host", + ), + pytest.param( + False, + False, + None, + False, + id="base_transport_no_default_host", + ), + ], +) +def test_messaging_base_transport_wrap_method_modern_api_core( + monkeypatch, + supports_default_host, + is_concrete, + expected_kind, + expected_has_default_host, +): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", True) - # 1. Concrete transport: passes client_options=None and kind="grpc" + monkeypatch.setattr(transports.base, "_SUPPORTS_DEFAULT_HOST", supports_default_host) + transport_cls = ( + transports.MessagingGrpcTransport + if is_concrete + else transports.MessagingTransport + ) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - transport = transports.MessagingGrpcTransport( - credentials=ga_credentials.AnonymousCredentials() - ) + transport = transport_cls(credentials=ga_credentials.AnonymousCredentials()) func = mock.MagicMock() transport._wrap_method(func) - assert mock_wrap.call_args.kwargs["kind"] == "grpc" - assert "client_options" in mock_wrap.call_args.kwargs - - # 2. Base transport: kind raises NotImplementedError and is cleanly omitted - with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - base_transport = transports.MessagingTransport( - credentials=ga_credentials.AnonymousCredentials() - ) - func = mock.MagicMock() - base_transport._wrap_method(func) - assert "kind" not in mock_wrap.call_args.kwargs + if expected_kind: + assert mock_wrap.call_args.kwargs["kind"] == expected_kind + else: + assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs + if expected_has_default_host: + assert mock_wrap.call_args.kwargs["default_host"] == transport._default_host + else: + assert "default_host" not in mock_wrap.call_args.kwargs -@pytest.mark.parametrize( - "otel_enabled,expected_endpoint", - [ - pytest.param(False, None, id="otel_disabled"), - pytest.param(True, "default.example.com:443", id="otel_enabled"), - ], -) -def test_messaging_base_transport_client_options_none( - monkeypatch, otel_enabled, expected_endpoint -): - fake_observability = mock.MagicMock() - fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled - monkeypatch.setattr(transports.base, "_observability", fake_observability) - +def test_messaging_base_transport_client_options_none(): transport = transports.MessagingTransport( host="default.example.com:443", credentials=ga_credentials.AnonymousCredentials(), client_options=None, ) - assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + assert transport._client_options is None + assert transport._default_host == "default.example.com:443" @pytest.mark.parametrize( - "initial_endpoint,expected_endpoint", + "initial_endpoint", [ - pytest.param(None, "default.example.com:443", id="default_from_host"), - pytest.param( - "custom.example.com:443", - "custom.example.com:443", - id="preserve_explicit", - ), + pytest.param(None, id="unset_api_endpoint"), + pytest.param("custom.example.com:443", id="explicit_api_endpoint"), ], ) def test_messaging_base_transport_client_options_object( - initial_endpoint, expected_endpoint + initial_endpoint, ): options = client_options.ClientOptions(api_endpoint=initial_endpoint) transport = transports.MessagingTransport( @@ -11854,27 +11875,28 @@ def test_messaging_base_transport_client_options_object( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options.api_endpoint == expected_endpoint - assert options.api_endpoint == initial_endpoint + assert transport._client_options is options + assert transport._client_options.api_endpoint == initial_endpoint + assert transport._default_host == "default.example.com:443" @pytest.mark.parametrize( - "initial_options,expected_options", + "initial_options,expected_endpoint", [ pytest.param( {}, - {"api_endpoint": "default.example.com:443"}, - id="default_from_host", + None, + id="unset_api_endpoint", ), pytest.param( {"api_endpoint": "custom.example.com:443"}, - {"api_endpoint": "custom.example.com:443"}, - id="preserve_explicit", + "custom.example.com:443", + id="explicit_api_endpoint", ), ], ) def test_messaging_base_transport_client_options_dict( - initial_options, expected_options + initial_options, expected_endpoint ): options = dict(initial_options) transport = transports.MessagingTransport( @@ -11882,10 +11904,29 @@ def test_messaging_base_transport_client_options_dict( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options == expected_options + assert transport._client_options is options + assert transport._client_options.get("api_endpoint") == expected_endpoint + assert transport._default_host == "default.example.com:443" assert options == initial_options +def test_messaging_base_transport_default_host_ignored_when_custom_grpc_channel(): + class _CustomChannelTransport(transports.MessagingTransport): + def __init__(self, **kwargs): + self._ignore_credentials = True + self._grpc_channel = mock.sentinel.custom_channel + super().__init__(**kwargs) + + options = client_options.ClientOptions() + transport = _CustomChannelTransport( + host="default.example.com:443", + client_options=options, + ) + assert transport._client_options is options + assert transport._client_options.api_endpoint is None + assert transport._default_host is None + + def test_messaging_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): @@ -11960,27 +12001,52 @@ def test_messaging_base_transport_wrap_method_older_api_core_fallbacks(monkeypat assert "is_streaming" not in kwargs -def test_messaging_base_transport_wrap_async_method_modern_api_core(monkeypatch): +@pytest.mark.parametrize( + "supports_default_host,is_concrete,expected_kind,expected_has_default_host", + [ + pytest.param( + True, + True, + "grpc_asyncio", + True, + id="concrete_transport_supports_default_host", + ), + pytest.param( + False, + False, + None, + False, + id="base_transport_no_default_host", + ), + ], +) +def test_messaging_base_transport_wrap_async_method_modern_api_core( + monkeypatch, + supports_default_host, + is_concrete, + expected_kind, + expected_has_default_host, +): monkeypatch.setattr(transports.base, "_ASYNC_WRAP_METHOD_SUPPORTS_TRACING", True) - # 1. Concrete transport: passes client_options=None and kind="grpc_asyncio" + monkeypatch.setattr(transports.base, "_SUPPORTS_DEFAULT_HOST", supports_default_host) + transport_cls = ( + transports.MessagingGrpcAsyncIOTransport + if is_concrete + else transports.MessagingTransport + ) with mock.patch.object(transports.base.gapic_v1.method_async, "wrap_method") as mock_wrap: - transport = transports.MessagingGrpcAsyncIOTransport( - credentials=ga_credentials.AnonymousCredentials() - ) + transport = transport_cls(credentials=ga_credentials.AnonymousCredentials()) func = mock.MagicMock() transport._wrap_async_method(func) - assert mock_wrap.call_args.kwargs["kind"] == "grpc_asyncio" - assert "client_options" in mock_wrap.call_args.kwargs - - # 2. Base transport: kind raises NotImplementedError and is cleanly omitted - with mock.patch.object(transports.base.gapic_v1.method_async, "wrap_method") as mock_wrap: - base_transport = transports.MessagingTransport( - credentials=ga_credentials.AnonymousCredentials() - ) - func = mock.MagicMock() - base_transport._wrap_async_method(func) - assert "kind" not in mock_wrap.call_args.kwargs + if expected_kind: + assert mock_wrap.call_args.kwargs["kind"] == expected_kind + else: + assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs + if expected_has_default_host: + assert mock_wrap.call_args.kwargs["default_host"] == transport._default_host + else: + assert "default_host" not in mock_wrap.call_args.kwargs def test_messaging_base_transport_wrap_async_method_older_api_core_fallbacks(monkeypatch): diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py index 8e17b5ccb41e..51b9ec02e33c 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py @@ -854,7 +854,14 @@ def test_resumable_upload_service_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(transport._client_options) + expected_kwargs = ( + {"default_host": transport._default_host} + if transports.grpc._SUPPORTS_DEFAULT_HOST + else {} + ) + mock_get_interceptor.assert_called_once_with( + transport._client_options, **expected_kwargs + ) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -928,8 +935,13 @@ def test_resumable_upload_service_grpc_asyncio_transport_otel_channel_intercepto client_options=options, ) + expected_kwargs = ( + {"default_host": transport._default_host} + if transports.grpc_asyncio._SUPPORTS_DEFAULT_HOST + else {} + ) mock_apply_async_interceptors.assert_called_once_with( - mock_channel, transport._client_options + mock_channel, transport._client_options, **expected_kwargs ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel @@ -4482,64 +4494,73 @@ def test_api_key_credentials(client_class, transport_class): ) -def test_resumable_upload_service_base_transport_wrap_method_modern_api_core(monkeypatch): +@pytest.mark.parametrize( + "supports_default_host,is_concrete,expected_kind,expected_has_default_host", + [ + pytest.param( + True, + True, + "grpc", + True, + id="concrete_transport_supports_default_host", + ), + pytest.param( + False, + False, + None, + False, + id="base_transport_no_default_host", + ), + ], +) +def test_resumable_upload_service_base_transport_wrap_method_modern_api_core( + monkeypatch, + supports_default_host, + is_concrete, + expected_kind, + expected_has_default_host, +): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", True) - # 1. Concrete transport: passes client_options=None and kind="grpc" + monkeypatch.setattr(transports.base, "_SUPPORTS_DEFAULT_HOST", supports_default_host) + transport_cls = ( + transports.ResumableUploadServiceGrpcTransport + if is_concrete + else transports.ResumableUploadServiceTransport + ) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - transport = transports.ResumableUploadServiceGrpcTransport( - credentials=ga_credentials.AnonymousCredentials() - ) + transport = transport_cls(credentials=ga_credentials.AnonymousCredentials()) func = mock.MagicMock() transport._wrap_method(func) - assert mock_wrap.call_args.kwargs["kind"] == "grpc" - assert "client_options" in mock_wrap.call_args.kwargs - - # 2. Base transport: kind raises NotImplementedError and is cleanly omitted - with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - base_transport = transports.ResumableUploadServiceTransport( - credentials=ga_credentials.AnonymousCredentials() - ) - func = mock.MagicMock() - base_transport._wrap_method(func) - assert "kind" not in mock_wrap.call_args.kwargs + if expected_kind: + assert mock_wrap.call_args.kwargs["kind"] == expected_kind + else: + assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs + if expected_has_default_host: + assert mock_wrap.call_args.kwargs["default_host"] == transport._default_host + else: + assert "default_host" not in mock_wrap.call_args.kwargs -@pytest.mark.parametrize( - "otel_enabled,expected_endpoint", - [ - pytest.param(False, None, id="otel_disabled"), - pytest.param(True, "default.example.com:443", id="otel_enabled"), - ], -) -def test_resumable_upload_service_base_transport_client_options_none( - monkeypatch, otel_enabled, expected_endpoint -): - fake_observability = mock.MagicMock() - fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled - monkeypatch.setattr(transports.base, "_observability", fake_observability) - +def test_resumable_upload_service_base_transport_client_options_none(): transport = transports.ResumableUploadServiceTransport( host="default.example.com:443", credentials=ga_credentials.AnonymousCredentials(), client_options=None, ) - assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + assert transport._client_options is None + assert transport._default_host == "default.example.com:443" @pytest.mark.parametrize( - "initial_endpoint,expected_endpoint", + "initial_endpoint", [ - pytest.param(None, "default.example.com:443", id="default_from_host"), - pytest.param( - "custom.example.com:443", - "custom.example.com:443", - id="preserve_explicit", - ), + pytest.param(None, id="unset_api_endpoint"), + pytest.param("custom.example.com:443", id="explicit_api_endpoint"), ], ) def test_resumable_upload_service_base_transport_client_options_object( - initial_endpoint, expected_endpoint + initial_endpoint, ): options = client_options.ClientOptions(api_endpoint=initial_endpoint) transport = transports.ResumableUploadServiceTransport( @@ -4547,27 +4568,28 @@ def test_resumable_upload_service_base_transport_client_options_object( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options.api_endpoint == expected_endpoint - assert options.api_endpoint == initial_endpoint + assert transport._client_options is options + assert transport._client_options.api_endpoint == initial_endpoint + assert transport._default_host == "default.example.com:443" @pytest.mark.parametrize( - "initial_options,expected_options", + "initial_options,expected_endpoint", [ pytest.param( {}, - {"api_endpoint": "default.example.com:443"}, - id="default_from_host", + None, + id="unset_api_endpoint", ), pytest.param( {"api_endpoint": "custom.example.com:443"}, - {"api_endpoint": "custom.example.com:443"}, - id="preserve_explicit", + "custom.example.com:443", + id="explicit_api_endpoint", ), ], ) def test_resumable_upload_service_base_transport_client_options_dict( - initial_options, expected_options + initial_options, expected_endpoint ): options = dict(initial_options) transport = transports.ResumableUploadServiceTransport( @@ -4575,10 +4597,29 @@ def test_resumable_upload_service_base_transport_client_options_dict( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options == expected_options + assert transport._client_options is options + assert transport._client_options.get("api_endpoint") == expected_endpoint + assert transport._default_host == "default.example.com:443" assert options == initial_options +def test_resumable_upload_service_base_transport_default_host_ignored_when_custom_grpc_channel(): + class _CustomChannelTransport(transports.ResumableUploadServiceTransport): + def __init__(self, **kwargs): + self._ignore_credentials = True + self._grpc_channel = mock.sentinel.custom_channel + super().__init__(**kwargs) + + options = client_options.ClientOptions() + transport = _CustomChannelTransport( + host="default.example.com:443", + client_options=options, + ) + assert transport._client_options is options + assert transport._client_options.api_endpoint is None + assert transport._default_host is None + + def test_resumable_upload_service_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): @@ -4653,27 +4694,52 @@ def test_resumable_upload_service_base_transport_wrap_method_older_api_core_fall assert "is_streaming" not in kwargs -def test_resumable_upload_service_base_transport_wrap_async_method_modern_api_core(monkeypatch): +@pytest.mark.parametrize( + "supports_default_host,is_concrete,expected_kind,expected_has_default_host", + [ + pytest.param( + True, + True, + "grpc_asyncio", + True, + id="concrete_transport_supports_default_host", + ), + pytest.param( + False, + False, + None, + False, + id="base_transport_no_default_host", + ), + ], +) +def test_resumable_upload_service_base_transport_wrap_async_method_modern_api_core( + monkeypatch, + supports_default_host, + is_concrete, + expected_kind, + expected_has_default_host, +): monkeypatch.setattr(transports.base, "_ASYNC_WRAP_METHOD_SUPPORTS_TRACING", True) - # 1. Concrete transport: passes client_options=None and kind="grpc_asyncio" + monkeypatch.setattr(transports.base, "_SUPPORTS_DEFAULT_HOST", supports_default_host) + transport_cls = ( + transports.ResumableUploadServiceGrpcAsyncIOTransport + if is_concrete + else transports.ResumableUploadServiceTransport + ) with mock.patch.object(transports.base.gapic_v1.method_async, "wrap_method") as mock_wrap: - transport = transports.ResumableUploadServiceGrpcAsyncIOTransport( - credentials=ga_credentials.AnonymousCredentials() - ) + transport = transport_cls(credentials=ga_credentials.AnonymousCredentials()) func = mock.MagicMock() transport._wrap_async_method(func) - assert mock_wrap.call_args.kwargs["kind"] == "grpc_asyncio" - assert "client_options" in mock_wrap.call_args.kwargs - - # 2. Base transport: kind raises NotImplementedError and is cleanly omitted - with mock.patch.object(transports.base.gapic_v1.method_async, "wrap_method") as mock_wrap: - base_transport = transports.ResumableUploadServiceTransport( - credentials=ga_credentials.AnonymousCredentials() - ) - func = mock.MagicMock() - base_transport._wrap_async_method(func) - assert "kind" not in mock_wrap.call_args.kwargs + if expected_kind: + assert mock_wrap.call_args.kwargs["kind"] == expected_kind + else: + assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs + if expected_has_default_host: + assert mock_wrap.call_args.kwargs["default_host"] == transport._default_host + else: + assert "default_host" not in mock_wrap.call_args.kwargs def test_resumable_upload_service_base_transport_wrap_async_method_older_api_core_fallbacks(monkeypatch): diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_sequence_service.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_sequence_service.py index 79303aefe964..41181e6dd5a4 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_sequence_service.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_sequence_service.py @@ -850,7 +850,14 @@ def test_sequence_service_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(transport._client_options) + expected_kwargs = ( + {"default_host": transport._default_host} + if transports.grpc._SUPPORTS_DEFAULT_HOST + else {} + ) + mock_get_interceptor.assert_called_once_with( + transport._client_options, **expected_kwargs + ) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -924,8 +931,13 @@ def test_sequence_service_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) + expected_kwargs = ( + {"default_host": transport._default_host} + if transports.grpc_asyncio._SUPPORTS_DEFAULT_HOST + else {} + ) mock_apply_async_interceptors.assert_called_once_with( - mock_channel, transport._client_options + mock_channel, transport._client_options, **expected_kwargs ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel @@ -7571,64 +7583,73 @@ def test_api_key_credentials(client_class, transport_class): ) -def test_sequence_service_base_transport_wrap_method_modern_api_core(monkeypatch): +@pytest.mark.parametrize( + "supports_default_host,is_concrete,expected_kind,expected_has_default_host", + [ + pytest.param( + True, + True, + "grpc", + True, + id="concrete_transport_supports_default_host", + ), + pytest.param( + False, + False, + None, + False, + id="base_transport_no_default_host", + ), + ], +) +def test_sequence_service_base_transport_wrap_method_modern_api_core( + monkeypatch, + supports_default_host, + is_concrete, + expected_kind, + expected_has_default_host, +): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", True) - # 1. Concrete transport: passes client_options=None and kind="grpc" + monkeypatch.setattr(transports.base, "_SUPPORTS_DEFAULT_HOST", supports_default_host) + transport_cls = ( + transports.SequenceServiceGrpcTransport + if is_concrete + else transports.SequenceServiceTransport + ) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - transport = transports.SequenceServiceGrpcTransport( - credentials=ga_credentials.AnonymousCredentials() - ) + transport = transport_cls(credentials=ga_credentials.AnonymousCredentials()) func = mock.MagicMock() transport._wrap_method(func) - assert mock_wrap.call_args.kwargs["kind"] == "grpc" - assert "client_options" in mock_wrap.call_args.kwargs - - # 2. Base transport: kind raises NotImplementedError and is cleanly omitted - with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - base_transport = transports.SequenceServiceTransport( - credentials=ga_credentials.AnonymousCredentials() - ) - func = mock.MagicMock() - base_transport._wrap_method(func) - assert "kind" not in mock_wrap.call_args.kwargs + if expected_kind: + assert mock_wrap.call_args.kwargs["kind"] == expected_kind + else: + assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs + if expected_has_default_host: + assert mock_wrap.call_args.kwargs["default_host"] == transport._default_host + else: + assert "default_host" not in mock_wrap.call_args.kwargs -@pytest.mark.parametrize( - "otel_enabled,expected_endpoint", - [ - pytest.param(False, None, id="otel_disabled"), - pytest.param(True, "default.example.com:443", id="otel_enabled"), - ], -) -def test_sequence_service_base_transport_client_options_none( - monkeypatch, otel_enabled, expected_endpoint -): - fake_observability = mock.MagicMock() - fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled - monkeypatch.setattr(transports.base, "_observability", fake_observability) - +def test_sequence_service_base_transport_client_options_none(): transport = transports.SequenceServiceTransport( host="default.example.com:443", credentials=ga_credentials.AnonymousCredentials(), client_options=None, ) - assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + assert transport._client_options is None + assert transport._default_host == "default.example.com:443" @pytest.mark.parametrize( - "initial_endpoint,expected_endpoint", + "initial_endpoint", [ - pytest.param(None, "default.example.com:443", id="default_from_host"), - pytest.param( - "custom.example.com:443", - "custom.example.com:443", - id="preserve_explicit", - ), + pytest.param(None, id="unset_api_endpoint"), + pytest.param("custom.example.com:443", id="explicit_api_endpoint"), ], ) def test_sequence_service_base_transport_client_options_object( - initial_endpoint, expected_endpoint + initial_endpoint, ): options = client_options.ClientOptions(api_endpoint=initial_endpoint) transport = transports.SequenceServiceTransport( @@ -7636,27 +7657,28 @@ def test_sequence_service_base_transport_client_options_object( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options.api_endpoint == expected_endpoint - assert options.api_endpoint == initial_endpoint + assert transport._client_options is options + assert transport._client_options.api_endpoint == initial_endpoint + assert transport._default_host == "default.example.com:443" @pytest.mark.parametrize( - "initial_options,expected_options", + "initial_options,expected_endpoint", [ pytest.param( {}, - {"api_endpoint": "default.example.com:443"}, - id="default_from_host", + None, + id="unset_api_endpoint", ), pytest.param( {"api_endpoint": "custom.example.com:443"}, - {"api_endpoint": "custom.example.com:443"}, - id="preserve_explicit", + "custom.example.com:443", + id="explicit_api_endpoint", ), ], ) def test_sequence_service_base_transport_client_options_dict( - initial_options, expected_options + initial_options, expected_endpoint ): options = dict(initial_options) transport = transports.SequenceServiceTransport( @@ -7664,10 +7686,29 @@ def test_sequence_service_base_transport_client_options_dict( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options == expected_options + assert transport._client_options is options + assert transport._client_options.get("api_endpoint") == expected_endpoint + assert transport._default_host == "default.example.com:443" assert options == initial_options +def test_sequence_service_base_transport_default_host_ignored_when_custom_grpc_channel(): + class _CustomChannelTransport(transports.SequenceServiceTransport): + def __init__(self, **kwargs): + self._ignore_credentials = True + self._grpc_channel = mock.sentinel.custom_channel + super().__init__(**kwargs) + + options = client_options.ClientOptions() + transport = _CustomChannelTransport( + host="default.example.com:443", + client_options=options, + ) + assert transport._client_options is options + assert transport._client_options.api_endpoint is None + assert transport._default_host is None + + def test_sequence_service_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): @@ -7742,27 +7783,52 @@ def test_sequence_service_base_transport_wrap_method_older_api_core_fallbacks(mo assert "is_streaming" not in kwargs -def test_sequence_service_base_transport_wrap_async_method_modern_api_core(monkeypatch): +@pytest.mark.parametrize( + "supports_default_host,is_concrete,expected_kind,expected_has_default_host", + [ + pytest.param( + True, + True, + "grpc_asyncio", + True, + id="concrete_transport_supports_default_host", + ), + pytest.param( + False, + False, + None, + False, + id="base_transport_no_default_host", + ), + ], +) +def test_sequence_service_base_transport_wrap_async_method_modern_api_core( + monkeypatch, + supports_default_host, + is_concrete, + expected_kind, + expected_has_default_host, +): monkeypatch.setattr(transports.base, "_ASYNC_WRAP_METHOD_SUPPORTS_TRACING", True) - # 1. Concrete transport: passes client_options=None and kind="grpc_asyncio" + monkeypatch.setattr(transports.base, "_SUPPORTS_DEFAULT_HOST", supports_default_host) + transport_cls = ( + transports.SequenceServiceGrpcAsyncIOTransport + if is_concrete + else transports.SequenceServiceTransport + ) with mock.patch.object(transports.base.gapic_v1.method_async, "wrap_method") as mock_wrap: - transport = transports.SequenceServiceGrpcAsyncIOTransport( - credentials=ga_credentials.AnonymousCredentials() - ) + transport = transport_cls(credentials=ga_credentials.AnonymousCredentials()) func = mock.MagicMock() transport._wrap_async_method(func) - assert mock_wrap.call_args.kwargs["kind"] == "grpc_asyncio" - assert "client_options" in mock_wrap.call_args.kwargs - - # 2. Base transport: kind raises NotImplementedError and is cleanly omitted - with mock.patch.object(transports.base.gapic_v1.method_async, "wrap_method") as mock_wrap: - base_transport = transports.SequenceServiceTransport( - credentials=ga_credentials.AnonymousCredentials() - ) - func = mock.MagicMock() - base_transport._wrap_async_method(func) - assert "kind" not in mock_wrap.call_args.kwargs + if expected_kind: + assert mock_wrap.call_args.kwargs["kind"] == expected_kind + else: + assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs + if expected_has_default_host: + assert mock_wrap.call_args.kwargs["default_host"] == transport._default_host + else: + assert "default_host" not in mock_wrap.call_args.kwargs def test_sequence_service_base_transport_wrap_async_method_older_api_core_fallbacks(monkeypatch): diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_testing.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_testing.py index 7bc47a2be1b5..299c5d4eba41 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_testing.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_testing.py @@ -847,7 +847,14 @@ def test_testing_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(transport._client_options) + expected_kwargs = ( + {"default_host": transport._default_host} + if transports.grpc._SUPPORTS_DEFAULT_HOST + else {} + ) + mock_get_interceptor.assert_called_once_with( + transport._client_options, **expected_kwargs + ) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -921,8 +928,13 @@ def test_testing_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) + expected_kwargs = ( + {"default_host": transport._default_host} + if transports.grpc_asyncio._SUPPORTS_DEFAULT_HOST + else {} + ) mock_apply_async_interceptors.assert_called_once_with( - mock_channel, transport._client_options + mock_channel, transport._client_options, **expected_kwargs ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel @@ -7760,64 +7772,73 @@ def test_api_key_credentials(client_class, transport_class): ) -def test_testing_base_transport_wrap_method_modern_api_core(monkeypatch): +@pytest.mark.parametrize( + "supports_default_host,is_concrete,expected_kind,expected_has_default_host", + [ + pytest.param( + True, + True, + "grpc", + True, + id="concrete_transport_supports_default_host", + ), + pytest.param( + False, + False, + None, + False, + id="base_transport_no_default_host", + ), + ], +) +def test_testing_base_transport_wrap_method_modern_api_core( + monkeypatch, + supports_default_host, + is_concrete, + expected_kind, + expected_has_default_host, +): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", True) - # 1. Concrete transport: passes client_options=None and kind="grpc" + monkeypatch.setattr(transports.base, "_SUPPORTS_DEFAULT_HOST", supports_default_host) + transport_cls = ( + transports.TestingGrpcTransport + if is_concrete + else transports.TestingTransport + ) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - transport = transports.TestingGrpcTransport( - credentials=ga_credentials.AnonymousCredentials() - ) + transport = transport_cls(credentials=ga_credentials.AnonymousCredentials()) func = mock.MagicMock() transport._wrap_method(func) - assert mock_wrap.call_args.kwargs["kind"] == "grpc" - assert "client_options" in mock_wrap.call_args.kwargs - - # 2. Base transport: kind raises NotImplementedError and is cleanly omitted - with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - base_transport = transports.TestingTransport( - credentials=ga_credentials.AnonymousCredentials() - ) - func = mock.MagicMock() - base_transport._wrap_method(func) - assert "kind" not in mock_wrap.call_args.kwargs + if expected_kind: + assert mock_wrap.call_args.kwargs["kind"] == expected_kind + else: + assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs + if expected_has_default_host: + assert mock_wrap.call_args.kwargs["default_host"] == transport._default_host + else: + assert "default_host" not in mock_wrap.call_args.kwargs -@pytest.mark.parametrize( - "otel_enabled,expected_endpoint", - [ - pytest.param(False, None, id="otel_disabled"), - pytest.param(True, "default.example.com:443", id="otel_enabled"), - ], -) -def test_testing_base_transport_client_options_none( - monkeypatch, otel_enabled, expected_endpoint -): - fake_observability = mock.MagicMock() - fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled - monkeypatch.setattr(transports.base, "_observability", fake_observability) - +def test_testing_base_transport_client_options_none(): transport = transports.TestingTransport( host="default.example.com:443", credentials=ga_credentials.AnonymousCredentials(), client_options=None, ) - assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + assert transport._client_options is None + assert transport._default_host == "default.example.com:443" @pytest.mark.parametrize( - "initial_endpoint,expected_endpoint", + "initial_endpoint", [ - pytest.param(None, "default.example.com:443", id="default_from_host"), - pytest.param( - "custom.example.com:443", - "custom.example.com:443", - id="preserve_explicit", - ), + pytest.param(None, id="unset_api_endpoint"), + pytest.param("custom.example.com:443", id="explicit_api_endpoint"), ], ) def test_testing_base_transport_client_options_object( - initial_endpoint, expected_endpoint + initial_endpoint, ): options = client_options.ClientOptions(api_endpoint=initial_endpoint) transport = transports.TestingTransport( @@ -7825,27 +7846,28 @@ def test_testing_base_transport_client_options_object( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options.api_endpoint == expected_endpoint - assert options.api_endpoint == initial_endpoint + assert transport._client_options is options + assert transport._client_options.api_endpoint == initial_endpoint + assert transport._default_host == "default.example.com:443" @pytest.mark.parametrize( - "initial_options,expected_options", + "initial_options,expected_endpoint", [ pytest.param( {}, - {"api_endpoint": "default.example.com:443"}, - id="default_from_host", + None, + id="unset_api_endpoint", ), pytest.param( {"api_endpoint": "custom.example.com:443"}, - {"api_endpoint": "custom.example.com:443"}, - id="preserve_explicit", + "custom.example.com:443", + id="explicit_api_endpoint", ), ], ) def test_testing_base_transport_client_options_dict( - initial_options, expected_options + initial_options, expected_endpoint ): options = dict(initial_options) transport = transports.TestingTransport( @@ -7853,10 +7875,29 @@ def test_testing_base_transport_client_options_dict( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options == expected_options + assert transport._client_options is options + assert transport._client_options.get("api_endpoint") == expected_endpoint + assert transport._default_host == "default.example.com:443" assert options == initial_options +def test_testing_base_transport_default_host_ignored_when_custom_grpc_channel(): + class _CustomChannelTransport(transports.TestingTransport): + def __init__(self, **kwargs): + self._ignore_credentials = True + self._grpc_channel = mock.sentinel.custom_channel + super().__init__(**kwargs) + + options = client_options.ClientOptions() + transport = _CustomChannelTransport( + host="default.example.com:443", + client_options=options, + ) + assert transport._client_options is options + assert transport._client_options.api_endpoint is None + assert transport._default_host is None + + def test_testing_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): @@ -7931,27 +7972,52 @@ def test_testing_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch assert "is_streaming" not in kwargs -def test_testing_base_transport_wrap_async_method_modern_api_core(monkeypatch): +@pytest.mark.parametrize( + "supports_default_host,is_concrete,expected_kind,expected_has_default_host", + [ + pytest.param( + True, + True, + "grpc_asyncio", + True, + id="concrete_transport_supports_default_host", + ), + pytest.param( + False, + False, + None, + False, + id="base_transport_no_default_host", + ), + ], +) +def test_testing_base_transport_wrap_async_method_modern_api_core( + monkeypatch, + supports_default_host, + is_concrete, + expected_kind, + expected_has_default_host, +): monkeypatch.setattr(transports.base, "_ASYNC_WRAP_METHOD_SUPPORTS_TRACING", True) - # 1. Concrete transport: passes client_options=None and kind="grpc_asyncio" + monkeypatch.setattr(transports.base, "_SUPPORTS_DEFAULT_HOST", supports_default_host) + transport_cls = ( + transports.TestingGrpcAsyncIOTransport + if is_concrete + else transports.TestingTransport + ) with mock.patch.object(transports.base.gapic_v1.method_async, "wrap_method") as mock_wrap: - transport = transports.TestingGrpcAsyncIOTransport( - credentials=ga_credentials.AnonymousCredentials() - ) + transport = transport_cls(credentials=ga_credentials.AnonymousCredentials()) func = mock.MagicMock() transport._wrap_async_method(func) - assert mock_wrap.call_args.kwargs["kind"] == "grpc_asyncio" - assert "client_options" in mock_wrap.call_args.kwargs - - # 2. Base transport: kind raises NotImplementedError and is cleanly omitted - with mock.patch.object(transports.base.gapic_v1.method_async, "wrap_method") as mock_wrap: - base_transport = transports.TestingTransport( - credentials=ga_credentials.AnonymousCredentials() - ) - func = mock.MagicMock() - base_transport._wrap_async_method(func) - assert "kind" not in mock_wrap.call_args.kwargs + if expected_kind: + assert mock_wrap.call_args.kwargs["kind"] == expected_kind + else: + assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs + if expected_has_default_host: + assert mock_wrap.call_args.kwargs["default_host"] == transport._default_host + else: + assert "default_host" not in mock_wrap.call_args.kwargs def test_testing_base_transport_wrap_async_method_older_api_core_fallbacks(monkeypatch): diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/_compat.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/_compat.py index 31817dab22a1..893b510545de 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/_compat.py @@ -95,6 +95,15 @@ def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: # t gapic_v1.method_async, "_DEFAULT_ASYNC_TRANSPORT_KIND" ) +# The `default_host` parameter in gapic_v1.method.wrap_method, +# gapic_v1.method_async.wrap_method, _observability.get_otel_interceptor, and +# _observability.apply_otel_async_channel_interceptors allows transports to pass +# their resolved default host without mutating caller-owned client_options. +SUPPORTS_DEFAULT_HOST = bool( + getattr(gapic_v1.method.wrap_method, "__kwdefaults__", None) + and "default_host" in gapic_v1.method.wrap_method.__kwdefaults__ +) + try: from google.api_core import grpc_helpers except ImportError: # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/base.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/base.py index eb72570fd60e..b75246bbf0ff 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/base.py @@ -14,13 +14,12 @@ # limitations under the License. # import abc -import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.storagebatchoperations_v1 import gapic_version as package_version from google.cloud.storagebatchoperations_v1._compat import ( - _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) _ASYNC_WRAP_METHOD_SUPPORTS_TRACING = _WRAP_METHOD_SUPPORTS_TRACING @@ -137,25 +136,13 @@ def __init__( host += ':443' self._host = host - # If the transport was instantiated directly with client_options=None - # while OpenTelemetry tracing is enabled via environment variable, - # create a ClientOptions instance with the resolved transport host so - # method and wire spans can record server.address and server.port. - if ( - client_options is None - and _observability is not None - and _observability.is_otel_capabilities_enabled(None) - ): - client_options = client_options_lib.ClientOptions(api_endpoint=self._host) - # And here we check for the presence of an api_endpoint attr OR key, - # shallow-copying before populating to avoid mutating caller-owned state. - elif isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: - client_options = copy.copy(client_options) - client_options.api_endpoint = self._host - elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): - client_options = copy.copy(client_options) - client_options["api_endpoint"] = self._host self._client_options = client_options + # If a pre-constructed gRPC channel was provided to the subclass before + # super().__init__, self._host is not used to create the channel and + # cannot be trusted as the default span endpoint. + self._default_host: Optional[str] = ( + None if getattr(self, "_grpc_channel", None) is not None else self._host + ) self._wrapped_methods: Dict[Callable, Callable] = {} @property @@ -174,6 +161,8 @@ def _wrap_method(self, func, *args, **kwargs): """ if _WRAP_METHOD_SUPPORTS_TRACING: kwargs["client_options"] = self._client_options + if _SUPPORTS_DEFAULT_HOST: + kwargs["default_host"] = self._default_host try: kind = self.kind except NotImplementedError: @@ -186,7 +175,7 @@ def _wrap_method(self, func, *args, **kwargs): # - Era 2 (>= 2.29.0, < 2.36.0): `kind` is supported (prevents REST from # falling back to gRPC wrapping), but tracing args are not. # (These fallbacks can be removed once google-api-core < 2.36.0 is no longer supported.) - for k in ["client_options", "method_name", "is_streaming"]: + for k in ["client_options", "method_name", "is_streaming", "default_host"]: kwargs.pop(k, None) try: @@ -213,6 +202,8 @@ def _wrap_async_method(self, func, *args, **kwargs): """ if _ASYNC_WRAP_METHOD_SUPPORTS_TRACING: kwargs["client_options"] = self._client_options + if _SUPPORTS_DEFAULT_HOST: + kwargs["default_host"] = self._default_host try: kind = self.kind except NotImplementedError: @@ -225,7 +216,7 @@ def _wrap_async_method(self, func, *args, **kwargs): # - Era 2 (>= 2.29.0, < 2.36.0): `kind` is supported (prevents REST from # falling back to gRPC wrapping), but tracing args are not. # (These fallbacks can be removed once google-api-core < 2.36.0 is no longer supported.) - for k in ["client_options", "method_name", "is_streaming"]: + for k in ["client_options", "method_name", "is_streaming", "default_host"]: kwargs.pop(k, None) try: diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/grpc.py index 16792a55723a..5f56b630fa1a 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/grpc.py @@ -20,7 +20,11 @@ from typing import Callable, Dict, Optional, Sequence, Tuple, Union from typing import TYPE_CHECKING -from google.cloud.storagebatchoperations_v1._compat import _observability, apply_channel_interceptors +from google.cloud.storagebatchoperations_v1._compat import ( + _observability, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, + apply_channel_interceptors, +) from google.api_core import client_options as client_options_lib from google.api_core import grpc_helpers @@ -283,9 +287,12 @@ def __init__(self, *, ) channel_interceptors = list(interceptors) if interceptors else [] + _otel_interceptor_kwargs = ( + {"default_host": self._default_host} if _SUPPORTS_DEFAULT_HOST else {} + ) if ( _observability is not None - and (otel_interceptor := _observability.get_otel_interceptor(self._client_options)) is not None + and (otel_interceptor := _observability.get_otel_interceptor(self._client_options, **_otel_interceptor_kwargs)) is not None and otel_interceptor not in channel_interceptors and not any(getattr(i, "_is_otel_interceptor", None) is True for i in channel_interceptors) ): diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/grpc_asyncio.py index 364bc887126e..9abce3fe8581 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/grpc_asyncio.py @@ -20,7 +20,11 @@ from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union from typing import TYPE_CHECKING -from google.cloud.storagebatchoperations_v1._compat import _observability, apply_async_channel_interceptors +from google.cloud.storagebatchoperations_v1._compat import ( + _observability, + SUPPORTS_DEFAULT_HOST as _SUPPORTS_DEFAULT_HOST, + apply_async_channel_interceptors, +) from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 @@ -341,8 +345,11 @@ def __init__(self, *, # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= v2.43.0. + _otel_interceptor_kwargs = ( + {"default_host": self._default_host} if _SUPPORTS_DEFAULT_HOST else {} + ) # pragma: NO COVER self._grpc_channel = _observability.apply_otel_async_channel_interceptors( - self._grpc_channel, self._client_options + self._grpc_channel, self._client_options, **_otel_interceptor_kwargs ) # pragma: NO COVER self._logged_channel = self._grpc_channel diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py index bf810d75835e..b5d7a096f018 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py @@ -851,7 +851,14 @@ def test_storage_batch_operations_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(transport._client_options) + expected_kwargs = ( + {"default_host": transport._default_host} + if transports.grpc._SUPPORTS_DEFAULT_HOST + else {} + ) + mock_get_interceptor.assert_called_once_with( + transport._client_options, **expected_kwargs + ) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -925,8 +932,13 @@ def test_storage_batch_operations_grpc_asyncio_transport_otel_channel_intercepto client_options=options, ) + expected_kwargs = ( + {"default_host": transport._default_host} + if transports.grpc_asyncio._SUPPORTS_DEFAULT_HOST + else {} + ) mock_apply_async_interceptors.assert_called_once_with( - mock_channel, transport._client_options + mock_channel, transport._client_options, **expected_kwargs ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel @@ -8459,64 +8471,73 @@ def test_api_key_credentials(client_class, transport_class): ) -def test_storage_batch_operations_base_transport_wrap_method_modern_api_core(monkeypatch): +@pytest.mark.parametrize( + "supports_default_host,is_concrete,expected_kind,expected_has_default_host", + [ + pytest.param( + True, + True, + "grpc", + True, + id="concrete_transport_supports_default_host", + ), + pytest.param( + False, + False, + None, + False, + id="base_transport_no_default_host", + ), + ], +) +def test_storage_batch_operations_base_transport_wrap_method_modern_api_core( + monkeypatch, + supports_default_host, + is_concrete, + expected_kind, + expected_has_default_host, +): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", True) - # 1. Concrete transport: passes client_options=None and kind="grpc" + monkeypatch.setattr(transports.base, "_SUPPORTS_DEFAULT_HOST", supports_default_host) + transport_cls = ( + transports.StorageBatchOperationsGrpcTransport + if is_concrete + else transports.StorageBatchOperationsTransport + ) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - transport = transports.StorageBatchOperationsGrpcTransport( - credentials=ga_credentials.AnonymousCredentials() - ) + transport = transport_cls(credentials=ga_credentials.AnonymousCredentials()) func = mock.MagicMock() transport._wrap_method(func) - assert mock_wrap.call_args.kwargs["kind"] == "grpc" - assert "client_options" in mock_wrap.call_args.kwargs - - # 2. Base transport: kind raises NotImplementedError and is cleanly omitted - with mock.patch.object(transports.base.gapic_v1.method, "wrap_method") as mock_wrap: - base_transport = transports.StorageBatchOperationsTransport( - credentials=ga_credentials.AnonymousCredentials() - ) - func = mock.MagicMock() - base_transport._wrap_method(func) - assert "kind" not in mock_wrap.call_args.kwargs + if expected_kind: + assert mock_wrap.call_args.kwargs["kind"] == expected_kind + else: + assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs + if expected_has_default_host: + assert mock_wrap.call_args.kwargs["default_host"] == transport._default_host + else: + assert "default_host" not in mock_wrap.call_args.kwargs -@pytest.mark.parametrize( - "otel_enabled,expected_endpoint", - [ - pytest.param(False, None, id="otel_disabled"), - pytest.param(True, "default.example.com:443", id="otel_enabled"), - ], -) -def test_storage_batch_operations_base_transport_client_options_none( - monkeypatch, otel_enabled, expected_endpoint -): - fake_observability = mock.MagicMock() - fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled - monkeypatch.setattr(transports.base, "_observability", fake_observability) - +def test_storage_batch_operations_base_transport_client_options_none(): transport = transports.StorageBatchOperationsTransport( host="default.example.com:443", credentials=ga_credentials.AnonymousCredentials(), client_options=None, ) - assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + assert transport._client_options is None + assert transport._default_host == "default.example.com:443" @pytest.mark.parametrize( - "initial_endpoint,expected_endpoint", + "initial_endpoint", [ - pytest.param(None, "default.example.com:443", id="default_from_host"), - pytest.param( - "custom.example.com:443", - "custom.example.com:443", - id="preserve_explicit", - ), + pytest.param(None, id="unset_api_endpoint"), + pytest.param("custom.example.com:443", id="explicit_api_endpoint"), ], ) def test_storage_batch_operations_base_transport_client_options_object( - initial_endpoint, expected_endpoint + initial_endpoint, ): options = client_options.ClientOptions(api_endpoint=initial_endpoint) transport = transports.StorageBatchOperationsTransport( @@ -8524,27 +8545,28 @@ def test_storage_batch_operations_base_transport_client_options_object( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options.api_endpoint == expected_endpoint - assert options.api_endpoint == initial_endpoint + assert transport._client_options is options + assert transport._client_options.api_endpoint == initial_endpoint + assert transport._default_host == "default.example.com:443" @pytest.mark.parametrize( - "initial_options,expected_options", + "initial_options,expected_endpoint", [ pytest.param( {}, - {"api_endpoint": "default.example.com:443"}, - id="default_from_host", + None, + id="unset_api_endpoint", ), pytest.param( {"api_endpoint": "custom.example.com:443"}, - {"api_endpoint": "custom.example.com:443"}, - id="preserve_explicit", + "custom.example.com:443", + id="explicit_api_endpoint", ), ], ) def test_storage_batch_operations_base_transport_client_options_dict( - initial_options, expected_options + initial_options, expected_endpoint ): options = dict(initial_options) transport = transports.StorageBatchOperationsTransport( @@ -8552,10 +8574,29 @@ def test_storage_batch_operations_base_transport_client_options_dict( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options == expected_options + assert transport._client_options is options + assert transport._client_options.get("api_endpoint") == expected_endpoint + assert transport._default_host == "default.example.com:443" assert options == initial_options +def test_storage_batch_operations_base_transport_default_host_ignored_when_custom_grpc_channel(): + class _CustomChannelTransport(transports.StorageBatchOperationsTransport): + def __init__(self, **kwargs): + self._ignore_credentials = True + self._grpc_channel = mock.sentinel.custom_channel + super().__init__(**kwargs) + + options = client_options.ClientOptions() + transport = _CustomChannelTransport( + host="default.example.com:443", + client_options=options, + ) + assert transport._client_options is options + assert transport._client_options.api_endpoint is None + assert transport._default_host is None + + def test_storage_batch_operations_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): @@ -8630,27 +8671,52 @@ def test_storage_batch_operations_base_transport_wrap_method_older_api_core_fall assert "is_streaming" not in kwargs -def test_storage_batch_operations_base_transport_wrap_async_method_modern_api_core(monkeypatch): +@pytest.mark.parametrize( + "supports_default_host,is_concrete,expected_kind,expected_has_default_host", + [ + pytest.param( + True, + True, + "grpc_asyncio", + True, + id="concrete_transport_supports_default_host", + ), + pytest.param( + False, + False, + None, + False, + id="base_transport_no_default_host", + ), + ], +) +def test_storage_batch_operations_base_transport_wrap_async_method_modern_api_core( + monkeypatch, + supports_default_host, + is_concrete, + expected_kind, + expected_has_default_host, +): monkeypatch.setattr(transports.base, "_ASYNC_WRAP_METHOD_SUPPORTS_TRACING", True) - # 1. Concrete transport: passes client_options=None and kind="grpc_asyncio" + monkeypatch.setattr(transports.base, "_SUPPORTS_DEFAULT_HOST", supports_default_host) + transport_cls = ( + transports.StorageBatchOperationsGrpcAsyncIOTransport + if is_concrete + else transports.StorageBatchOperationsTransport + ) with mock.patch.object(transports.base.gapic_v1.method_async, "wrap_method") as mock_wrap: - transport = transports.StorageBatchOperationsGrpcAsyncIOTransport( - credentials=ga_credentials.AnonymousCredentials() - ) + transport = transport_cls(credentials=ga_credentials.AnonymousCredentials()) func = mock.MagicMock() transport._wrap_async_method(func) - assert mock_wrap.call_args.kwargs["kind"] == "grpc_asyncio" - assert "client_options" in mock_wrap.call_args.kwargs - - # 2. Base transport: kind raises NotImplementedError and is cleanly omitted - with mock.patch.object(transports.base.gapic_v1.method_async, "wrap_method") as mock_wrap: - base_transport = transports.StorageBatchOperationsTransport( - credentials=ga_credentials.AnonymousCredentials() - ) - func = mock.MagicMock() - base_transport._wrap_async_method(func) - assert "kind" not in mock_wrap.call_args.kwargs + if expected_kind: + assert mock_wrap.call_args.kwargs["kind"] == expected_kind + else: + assert "kind" not in mock_wrap.call_args.kwargs assert "client_options" in mock_wrap.call_args.kwargs + if expected_has_default_host: + assert mock_wrap.call_args.kwargs["default_host"] == transport._default_host + else: + assert "default_host" not in mock_wrap.call_args.kwargs def test_storage_batch_operations_base_transport_wrap_async_method_older_api_core_fallbacks(monkeypatch): diff --git a/packages/gapic-generator/tests/system/test_span_compliance.py b/packages/gapic-generator/tests/system/test_span_compliance.py index cc1a87a66a5a..df4048360dbe 100644 --- a/packages/gapic-generator/tests/system/test_span_compliance.py +++ b/packages/gapic-generator/tests/system/test_span_compliance.py @@ -64,6 +64,7 @@ from pathlib import Path from typing import Any +import grpc import pytest try: @@ -201,6 +202,18 @@ def span_exporter(monkeypatch): # --------------------------------------------------------------------------- +def _insecure_channel_factory(host: str, **kwargs: Any) -> Any: + """Callable channel factory for local Showcase gRPC daemon testing. + + Passing a callable factory instead of a pre-constructed ``grpc.Channel`` + instance ensures ``EchoGrpcTransport`` resolves ``self._host`` from its + ``DEFAULT_HOST`` ("localhost:7469") through the standard host-managed path + (``self._grpc_channel`` created from ``self._host``) while still creating an + insecure channel to the local Showcase server. + """ + return grpc.insecure_channel(host) + + def construct_observability_client( client_class, transport: str, @@ -227,11 +240,15 @@ def construct_observability_client( Returns: An instantiated, ready-to-call Showcase client instance. """ + channel = ( + _insecure_channel_factory if (transport == "grpc" and not use_mtls) else None + ) return conftest.construct_client( client_class, use_mtls=use_mtls, transport_name=transport, client_options=client_options, + channel=channel, ) diff --git a/packages/google-api-core/google/api_core/_observability.py b/packages/google-api-core/google/api_core/_observability.py index 38874b666b6e..34e1ca152c37 100644 --- a/packages/google-api-core/google/api_core/_observability.py +++ b/packages/google-api-core/google/api_core/_observability.py @@ -76,27 +76,32 @@ def is_otel_capabilities_enabled( def _extract_endpoint_attributes( client_options: ClientOptions | dict[str, Any] | None = None, + default_host: str | None = None, ) -> dict[str, Any]: """Extracts server.address, server.port (if non-default), and url.domain from client options if present. Args: client_options (Optional[Union[ClientOptions, dict[str, Any]]]): The client options object or dictionary. + default_host (Optional[str]): Fallback transport host when ``api_endpoint`` is not + set on ``client_options``. Returns: - dict[str, Any]: A dictionary containing url.domain and, if an api_endpoint is configured, - server.address and non-default server.port. + dict[str, Any]: A dictionary containing url.domain and, if an api_endpoint or default_host + is configured, server.address and non-default server.port. """ attrs: dict[str, Any] = {} endpoint = None universe_domain = None if isinstance(client_options, dict): - endpoint = client_options.get("api_endpoint") + endpoint = client_options.get("api_endpoint") or default_host universe_domain = client_options.get("universe_domain") elif client_options is not None: - endpoint = getattr(client_options, "api_endpoint", None) + endpoint = getattr(client_options, "api_endpoint", None) or default_host universe_domain = getattr(client_options, "universe_domain", None) + else: + endpoint = default_host attrs["url.domain"] = universe_domain or "googleapis.com" @@ -367,12 +372,15 @@ async def intercept_stream_stream( def get_otel_interceptor( client_options: ClientOptions | dict[str, Any] | None = None, + default_host: str | None = None, ) -> Callable[[grpc.Channel], grpc.Channel] | None: """Returns an interceptor callable that wraps a sync gRPC channel with OpenTelemetry tracing. Args: client_options (Optional[Union[ClientOptions, dict[str, Any]]]): The client options object or dictionary used for feature gating and extracting the tracer provider. + default_host (Optional[str]): Fallback transport host when ``api_endpoint`` is not + set on ``client_options``. Returns: Callable[[grpc.Channel], grpc.Channel] | None: An interceptor callable if OpenTelemetry @@ -383,7 +391,9 @@ def get_otel_interceptor( import opentelemetry.instrumentation.grpc as otel_grpc # type: ignore[import-not-found] - endpoint_attrs = _extract_endpoint_attributes(client_options) + endpoint_attrs = _extract_endpoint_attributes( + client_options, default_host=default_host + ) request_hook = _make_grpc_client_request_hook(endpoint_attrs) interceptor: ClientInterceptor = otel_grpc.client_interceptor( @@ -408,12 +418,15 @@ def otel_interceptor(channel: grpc.Channel) -> grpc.Channel: def get_otel_async_interceptor( client_options: ClientOptions | dict[str, Any] | None = None, + default_host: str | None = None, ) -> Sequence[grpc.aio.ClientInterceptor] | None: """Returns async gRPC client interceptors for OpenTelemetry tracing. Args: client_options (Optional[Union[ClientOptions, dict[str, Any]]]): The client options object or dictionary used for feature gating and extracting the tracer provider. + default_host (Optional[str]): Fallback transport host when ``api_endpoint`` is not + set on ``client_options``. Returns: Sequence[grpc.aio.ClientInterceptor] | None: Instantiated OpenTelemetry async @@ -425,7 +438,9 @@ def get_otel_async_interceptor( # Ignored by mypy: Optional dependency only loaded if early-return is skipped import opentelemetry.instrumentation.grpc as otel_grpc # type: ignore[import-not-found] - endpoint_attrs = _extract_endpoint_attributes(client_options) + endpoint_attrs = _extract_endpoint_attributes( + client_options, default_host=default_host + ) request_hook = _make_grpc_client_request_hook(endpoint_attrs) raw_interceptors = otel_grpc.aio_client_interceptors( @@ -454,6 +469,7 @@ def get_otel_async_interceptor( def apply_otel_async_channel_interceptors( channel: Any, client_options: ClientOptions | dict[str, Any] | Sequence[Any] | None = None, + default_host: str | None = None, ) -> Any: """Creates and attaches OpenTelemetry client interceptors to the front of an async gRPC channel. @@ -467,6 +483,8 @@ def apply_otel_async_channel_interceptors( client_options (Optional[Union[ClientOptions, dict[str, Any], Sequence[Any]]]): The client options used to resolve feature flags and tracer provider, or a sequence of pre-resolved interceptors. + default_host (Optional[str]): Fallback transport host when ``api_endpoint`` is not + set on ``client_options``. Returns: The channel with interceptors prepended to its internal lists, @@ -474,7 +492,9 @@ def apply_otel_async_channel_interceptors( """ otel_interceptors: Sequence[Any] | None if isinstance(client_options, (ClientOptions, dict)) or client_options is None: - otel_interceptors = get_otel_async_interceptor(client_options) + otel_interceptors = get_otel_async_interceptor( + client_options, default_host=default_host + ) elif isinstance(client_options, Sequence): otel_interceptors = client_options else: diff --git a/packages/google-api-core/google/api_core/gapic_v1/method.py b/packages/google-api-core/google/api_core/gapic_v1/method.py index 873e925ec1f7..c154d0173320 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/method.py +++ b/packages/google-api-core/google/api_core/gapic_v1/method.py @@ -295,6 +295,8 @@ class _GapicCallable(object): Client information used for metadata headers. Defaults to None. kind (str): The transport kind for the RPC method. Defaults to "grpc". Allowed values for OpenTelemetry method tracing are "grpc" and "rest". + default_host (Optional[str]): Fallback transport host when ``api_endpoint`` + is not set on ``client_options``. Defaults to None. """ _SUPPORTED_TRACING_KINDS = (_TRANSPORT_KIND_GRPC, _TRANSPORT_KIND_REST) @@ -311,6 +313,7 @@ def __init__( is_streaming=False, client_info=None, kind=_DEFAULT_TRANSPORT_KIND, + default_host=None, ): self._target = target self._retry = retry @@ -352,7 +355,7 @@ def __init__( span_name, _, _ = _extract_rpc_identity(method_name) endpoint_attrs = _observability._extract_endpoint_attributes( - client_options + client_options, default_host=default_host ) span_attributes = { "rpc.system.name": "http" if kind.startswith("rest") else "grpc", @@ -452,6 +455,7 @@ def wrap_method( method_name=None, is_streaming=False, kind=_DEFAULT_TRANSPORT_KIND, + default_host=None, ): """Wrap an RPC method with common behavior. @@ -546,6 +550,8 @@ def get_topic(name, timeout=None): Streaming methods are currently gated and do not generate Tier 3 spans. kind (str): The transport kind for the RPC method. Defaults to "grpc". Allowed values for OpenTelemetry method tracing are "grpc" and "rest". + default_host (Optional[str]): Fallback transport host when ``api_endpoint`` + is not set on ``client_options``. Defaults to None. Returns: Callable: A new callable that takes optional ``retry``, ``timeout``, @@ -578,5 +584,6 @@ def get_topic(name, timeout=None): is_streaming=is_streaming, client_info=client_info, kind=kind, + default_host=default_host, ) ) diff --git a/packages/google-api-core/google/api_core/gapic_v1/method_async.py b/packages/google-api-core/google/api_core/gapic_v1/method_async.py index 2402f9f3ff95..aa7c79ea2e10 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/method_async.py +++ b/packages/google-api-core/google/api_core/gapic_v1/method_async.py @@ -64,6 +64,7 @@ def wrap_method( client_options=None, method_name=None, is_streaming=False, + default_host=None, ): """Wrap an async RPC method with common behavior. @@ -93,6 +94,8 @@ def wrap_method( Used to identify the RPC for observability. is_streaming (bool): Whether the RPC method is streaming. Defaults to False. Streaming methods are currently gated and do not generate Tier 3 spans. + default_host (Optional[str]): Fallback transport host when ``api_endpoint`` + is not set on ``client_options``. Defaults to None. Returns: Callable: A new callable that takes optional ``retry``, ``timeout``, @@ -116,5 +119,6 @@ def wrap_method( is_streaming=is_streaming, client_info=client_info, kind=kind, + default_host=default_host, ) ) diff --git a/packages/google-api-core/tests/asyncio/gapic/test_method_async.py b/packages/google-api-core/tests/asyncio/gapic/test_method_async.py index 159eee84af8e..0108d9e8417a 100644 --- a/packages/google-api-core/tests/asyncio/gapic/test_method_async.py +++ b/packages/google-api-core/tests/asyncio/gapic/test_method_async.py @@ -469,6 +469,33 @@ async def test_wrap_method_async_otel_tracing_client_options( mock_provider.get_tracer.assert_called_once_with("google.api_core") +@pytest.mark.asyncio +async def test_wrap_method_async_otel_tracing_default_host(mock_otel): + """Proves that async wrap_method falls back to default_host when api_endpoint is unset.""" + mock_target = mock.AsyncMock(return_value="success") + + wrapped = gapic_v1.method_async.wrap_method( + mock_target, + client_options=client_options_lib.ClientOptions(), + method_name="/google.showcase.v1beta1.Echo/Echo", + default_host="localhost:7469", + ) + result = await wrapped() + + assert result == "success" + mock_otel.tracer.start_as_current_span.assert_called_once_with( + "google.showcase.v1beta1.Echo/Echo", + kind="INTERNAL", + attributes={ + "rpc.system.name": "grpc", + "rpc.method": "google.showcase.v1beta1.Echo/Echo", + "url.domain": "googleapis.com", + "server.address": "localhost", + "server.port": 7469, + }, + ) + + @pytest.mark.asyncio async def test_wrap_method_async_otel_tracing_enabled_error(mock_otel): """Proves that on async error, status code and error attributes are recorded and exception is raised.""" diff --git a/packages/google-api-core/tests/unit/gapic/test_method.py b/packages/google-api-core/tests/unit/gapic/test_method.py index a4a5f805ac91..1a2d85bc12c8 100644 --- a/packages/google-api-core/tests/unit/gapic/test_method.py +++ b/packages/google-api-core/tests/unit/gapic/test_method.py @@ -753,6 +753,33 @@ def test_wrap_method_otel_tracing_endpoint_attributes_from_client_options( ) +def test_wrap_method_otel_tracing_endpoint_attributes_from_default_host( + mock_otel, +): + """Proves that T3 spans fall back to default_host when client_options.api_endpoint is unset.""" + mock_target = mock.Mock(return_value="ok") + options = client_options_lib.ClientOptions() + wrapped = google.api_core.gapic_v1.method.wrap_method( + mock_target, + client_options=options, + method_name="/google.showcase.v1beta1.Echo/Echo", + kind="grpc", + default_host="localhost:7469", + ) + assert wrapped() == "ok" + mock_otel.tracer.start_as_current_span.assert_called_once_with( + "google.showcase.v1beta1.Echo/Echo", + kind="INTERNAL", + attributes={ + "rpc.system.name": "grpc", + "rpc.method": "google.showcase.v1beta1.Echo/Echo", + "url.domain": "googleapis.com", + "server.address": "localhost", + "server.port": 7469, + }, + ) + + def test_extract_error_attributes_standard_exception(): """Proves that _extract_error_attributes returns fallback error.type for exceptions without ErrorInfo.""" assert google.api_core.gapic_v1.method._extract_error_attributes( diff --git a/packages/google-api-core/tests/unit/test_observability.py b/packages/google-api-core/tests/unit/test_observability.py index 1f18428102b8..7cd31e95070f 100644 --- a/packages/google-api-core/tests/unit/test_observability.py +++ b/packages/google-api-core/tests/unit/test_observability.py @@ -379,6 +379,91 @@ def test_extract_endpoint_attributes(client_options, expected_attrs): assert _observability._extract_endpoint_attributes(client_options) == expected_attrs +@pytest.mark.parametrize( + "client_options,default_host,expected_attrs", + [ + pytest.param( + None, + "localhost:7469", + { + "server.address": "localhost", + "server.port": 7469, + "url.domain": "googleapis.com", + }, + id="none_options_uses_default_host", + ), + pytest.param( + {}, + "localhost:7469", + { + "server.address": "localhost", + "server.port": 7469, + "url.domain": "googleapis.com", + }, + id="empty_dict_uses_default_host", + ), + pytest.param( + ClientOptions(api_endpoint=None), + "localhost:7469", + { + "server.address": "localhost", + "server.port": 7469, + "url.domain": "googleapis.com", + }, + id="client_options_without_endpoint_uses_default_host", + ), + pytest.param( + ClientOptions(api_endpoint="custom.example.com:8443"), + "localhost:7469", + { + "server.address": "custom.example.com", + "server.port": 8443, + "url.domain": "googleapis.com", + }, + id="client_options_endpoint_overrides_default_host", + ), + pytest.param( + {"api_endpoint": "custom.example.com:8443"}, + "localhost:7469", + { + "server.address": "custom.example.com", + "server.port": 8443, + "url.domain": "googleapis.com", + }, + id="dict_endpoint_overrides_default_host", + ), + pytest.param( + ClientOptions(api_endpoint=""), + "", + { + "url.domain": "googleapis.com", + }, + id="empty_string_endpoint_and_default_host", + ), + pytest.param( + ClientOptions(api_endpoint=""), + "localhost:7469", + { + "server.address": "localhost", + "server.port": 7469, + "url.domain": "googleapis.com", + }, + id="empty_string_endpoint_falls_back_to_default_host", + ), + ], +) +def test_extract_endpoint_attributes_default_host( + client_options, default_host, expected_attrs +): + """Proves that _extract_endpoint_attributes falls back to default_host when api_endpoint is not set.""" + assert ( + _observability._extract_endpoint_attributes( + client_options, default_host=default_host + ) + == expected_attrs + ) + + def test_grpc_client_request_hook(): """Proves that _grpc_client_request_hook attaches extracted T4 attributes to recording spans, normalizes span names, sets fully qualified rpc.method, and allows legacy rpc.system to coexist. From 503a97a40a609cbb4bd3774b793a6036bc980631 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Mon, 2 Nov 2026 08:11:14 -0500 Subject: [PATCH 5/7] fix(gapic-generator): narrow SUPPORTS_DEFAULT_HOST type for mypy and align Showcase test helpers --- .../%namespace/%name_%version/%sub/_compat.py.j2 | 4 ++-- .../goldens/asset/google/cloud/asset_v1/_compat.py | 4 ++-- .../google/iam/credentials_v1/_compat.py | 4 ++-- .../eventarc/google/cloud/eventarc_v1/_compat.py | 4 ++-- .../logging/google/cloud/logging_v2/_compat.py | 4 ++-- .../google/cloud/logging_v2/_compat.py | 4 ++-- .../goldens/redis/google/cloud/redis_v1/_compat.py | 4 ++-- .../google/cloud/redis_v1/_compat.py | 4 ++-- .../showcase/google/showcase_v1beta1/_compat.py | 4 ++-- .../cloud/storagebatchoperations_v1/_compat.py | 4 ++-- packages/gapic-generator/tests/system/conftest.py | 5 +++-- .../tests/system/test_span_compliance.py | 6 +++--- .../tests/asyncio/test_observability_async.py | 14 ++++++++++---- 13 files changed, 36 insertions(+), 29 deletions(-) diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 index c0ca7c50e49e..1fe80d4ccc94 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 @@ -103,8 +103,8 @@ ASYNC_WRAP_METHOD_SUPPORTS_KIND = WRAP_METHOD_SUPPORTS_TRACING or hasattr( # gapic_v1.method_async.wrap_method, _observability.get_otel_interceptor, and # _observability.apply_otel_async_channel_interceptors allows transports to pass # their resolved default host without mutating caller-owned client_options. -SUPPORTS_DEFAULT_HOST = bool( - getattr(gapic_v1.method.wrap_method, "__kwdefaults__", None) +SUPPORTS_DEFAULT_HOST = ( + gapic_v1.method.wrap_method.__kwdefaults__ is not None and "default_host" in gapic_v1.method.wrap_method.__kwdefaults__ ) diff --git a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/_compat.py b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/_compat.py index 86424516e0a2..bcef1aa94dc9 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/_compat.py @@ -94,8 +94,8 @@ def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: # t # gapic_v1.method_async.wrap_method, _observability.get_otel_interceptor, and # _observability.apply_otel_async_channel_interceptors allows transports to pass # their resolved default host without mutating caller-owned client_options. -SUPPORTS_DEFAULT_HOST = bool( - getattr(gapic_v1.method.wrap_method, "__kwdefaults__", None) +SUPPORTS_DEFAULT_HOST = ( + gapic_v1.method.wrap_method.__kwdefaults__ is not None and "default_host" in gapic_v1.method.wrap_method.__kwdefaults__ ) diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/_compat.py b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/_compat.py index 86424516e0a2..bcef1aa94dc9 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/_compat.py @@ -94,8 +94,8 @@ def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: # t # gapic_v1.method_async.wrap_method, _observability.get_otel_interceptor, and # _observability.apply_otel_async_channel_interceptors allows transports to pass # their resolved default host without mutating caller-owned client_options. -SUPPORTS_DEFAULT_HOST = bool( - getattr(gapic_v1.method.wrap_method, "__kwdefaults__", None) +SUPPORTS_DEFAULT_HOST = ( + gapic_v1.method.wrap_method.__kwdefaults__ is not None and "default_host" in gapic_v1.method.wrap_method.__kwdefaults__ ) diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/_compat.py b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/_compat.py index 86424516e0a2..bcef1aa94dc9 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/_compat.py @@ -94,8 +94,8 @@ def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: # t # gapic_v1.method_async.wrap_method, _observability.get_otel_interceptor, and # _observability.apply_otel_async_channel_interceptors allows transports to pass # their resolved default host without mutating caller-owned client_options. -SUPPORTS_DEFAULT_HOST = bool( - getattr(gapic_v1.method.wrap_method, "__kwdefaults__", None) +SUPPORTS_DEFAULT_HOST = ( + gapic_v1.method.wrap_method.__kwdefaults__ is not None and "default_host" in gapic_v1.method.wrap_method.__kwdefaults__ ) diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/_compat.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/_compat.py index 86424516e0a2..bcef1aa94dc9 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/_compat.py @@ -94,8 +94,8 @@ def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: # t # gapic_v1.method_async.wrap_method, _observability.get_otel_interceptor, and # _observability.apply_otel_async_channel_interceptors allows transports to pass # their resolved default host without mutating caller-owned client_options. -SUPPORTS_DEFAULT_HOST = bool( - getattr(gapic_v1.method.wrap_method, "__kwdefaults__", None) +SUPPORTS_DEFAULT_HOST = ( + gapic_v1.method.wrap_method.__kwdefaults__ is not None and "default_host" in gapic_v1.method.wrap_method.__kwdefaults__ ) diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/_compat.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/_compat.py index 86424516e0a2..bcef1aa94dc9 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/_compat.py @@ -94,8 +94,8 @@ def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: # t # gapic_v1.method_async.wrap_method, _observability.get_otel_interceptor, and # _observability.apply_otel_async_channel_interceptors allows transports to pass # their resolved default host without mutating caller-owned client_options. -SUPPORTS_DEFAULT_HOST = bool( - getattr(gapic_v1.method.wrap_method, "__kwdefaults__", None) +SUPPORTS_DEFAULT_HOST = ( + gapic_v1.method.wrap_method.__kwdefaults__ is not None and "default_host" in gapic_v1.method.wrap_method.__kwdefaults__ ) diff --git a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/_compat.py b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/_compat.py index 86424516e0a2..bcef1aa94dc9 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/_compat.py @@ -94,8 +94,8 @@ def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: # t # gapic_v1.method_async.wrap_method, _observability.get_otel_interceptor, and # _observability.apply_otel_async_channel_interceptors allows transports to pass # their resolved default host without mutating caller-owned client_options. -SUPPORTS_DEFAULT_HOST = bool( - getattr(gapic_v1.method.wrap_method, "__kwdefaults__", None) +SUPPORTS_DEFAULT_HOST = ( + gapic_v1.method.wrap_method.__kwdefaults__ is not None and "default_host" in gapic_v1.method.wrap_method.__kwdefaults__ ) diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/_compat.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/_compat.py index 86424516e0a2..bcef1aa94dc9 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/_compat.py @@ -94,8 +94,8 @@ def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: # t # gapic_v1.method_async.wrap_method, _observability.get_otel_interceptor, and # _observability.apply_otel_async_channel_interceptors allows transports to pass # their resolved default host without mutating caller-owned client_options. -SUPPORTS_DEFAULT_HOST = bool( - getattr(gapic_v1.method.wrap_method, "__kwdefaults__", None) +SUPPORTS_DEFAULT_HOST = ( + gapic_v1.method.wrap_method.__kwdefaults__ is not None and "default_host" in gapic_v1.method.wrap_method.__kwdefaults__ ) diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/_compat.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/_compat.py index 893b510545de..00485372403b 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/_compat.py @@ -99,8 +99,8 @@ def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: # t # gapic_v1.method_async.wrap_method, _observability.get_otel_interceptor, and # _observability.apply_otel_async_channel_interceptors allows transports to pass # their resolved default host without mutating caller-owned client_options. -SUPPORTS_DEFAULT_HOST = bool( - getattr(gapic_v1.method.wrap_method, "__kwdefaults__", None) +SUPPORTS_DEFAULT_HOST = ( + gapic_v1.method.wrap_method.__kwdefaults__ is not None and "default_host" in gapic_v1.method.wrap_method.__kwdefaults__ ) diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/_compat.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/_compat.py index 893b510545de..00485372403b 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/_compat.py @@ -99,8 +99,8 @@ def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: # t # gapic_v1.method_async.wrap_method, _observability.get_otel_interceptor, and # _observability.apply_otel_async_channel_interceptors allows transports to pass # their resolved default host without mutating caller-owned client_options. -SUPPORTS_DEFAULT_HOST = bool( - getattr(gapic_v1.method.wrap_method, "__kwdefaults__", None) +SUPPORTS_DEFAULT_HOST = ( + gapic_v1.method.wrap_method.__kwdefaults__ is not None and "default_host" in gapic_v1.method.wrap_method.__kwdefaults__ ) diff --git a/packages/gapic-generator/tests/system/conftest.py b/packages/gapic-generator/tests/system/conftest.py index 0f5643241f4f..cd9ef16be7c0 100644 --- a/packages/gapic-generator/tests/system/conftest.py +++ b/packages/gapic-generator/tests/system/conftest.py @@ -248,6 +248,7 @@ def construct_client( credentials=ga_credentials.AnonymousCredentials(), transport_endpoint="localhost:7469", client_options=None, + channel=None, ): if use_mtls: with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "true"}): @@ -267,10 +268,10 @@ def construct_client( transport_cls = client_class.get_transport_class(transport_name) if transport_name in ["grpc", "grpc_asyncio"]: # TODO(gapic-generator-python/issues/1914): Need to test grpc transports without a channel_creator - assert channel_creator + assert channel or channel_creator transport_kwargs = { "credentials": credentials, - "channel": channel_creator(transport_endpoint), + "channel": channel or channel_creator(transport_endpoint), "client_options": client_options, } transport = transport_cls(**transport_kwargs) diff --git a/packages/gapic-generator/tests/system/test_span_compliance.py b/packages/gapic-generator/tests/system/test_span_compliance.py index df4048360dbe..bc5063d8b355 100644 --- a/packages/gapic-generator/tests/system/test_span_compliance.py +++ b/packages/gapic-generator/tests/system/test_span_compliance.py @@ -227,9 +227,9 @@ def construct_observability_client( transport configuration and OpenTelemetry client_options. Design Note: - Rather than modifying the shared system test fixture helper (`conftest.construct_client`), - wrapping it locally insulates this compliance suite with zero blast radius on existing - Showcase tests. + Wrapping the shared system test fixture helper (`conftest.construct_client`) + supplies observability test defaults (`use_mtls=False` and `_insecure_channel_factory` + for host-managed gRPC channels) without altering defaults for other Showcase tests. Args: client_class: The GAPIC client class to instantiate (e.g. EchoClient). diff --git a/packages/google-api-core/tests/asyncio/test_observability_async.py b/packages/google-api-core/tests/asyncio/test_observability_async.py index 2a2bfec7dc77..3d7e2087c26f 100644 --- a/packages/google-api-core/tests/asyncio/test_observability_async.py +++ b/packages/google-api-core/tests/asyncio/test_observability_async.py @@ -113,24 +113,30 @@ def test_apply_otel_async_channel_interceptors_sequence_prepending( def test_apply_otel_async_channel_interceptors_client_options_prepending( mock_async_channel, monkeypatch ): - """Proves that interceptors resolved from ClientOptions are prepended in order.""" + """Proves that interceptors resolved from ClientOptions and default_host are prepended in order.""" existing_interceptor = mock.Mock() mock_async_channel._unary_unary_interceptors = [existing_interceptor] interceptor1 = mock.Mock(spec=["intercept_unary_unary"]) interceptor2 = mock.Mock(spec=["intercept_unary_unary"]) - monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "true") + mock_get_interceptor = mock.create_autospec( + _observability.get_otel_async_interceptor, + spec_set=True, + return_value=[interceptor1, interceptor2], + ) monkeypatch.setattr( _observability, "get_otel_async_interceptor", - lambda opts: [interceptor1, interceptor2], + mock_get_interceptor, ) + options = _observability.ClientOptions() result = _observability.apply_otel_async_channel_interceptors( - mock_async_channel, _observability.ClientOptions() + mock_async_channel, options, default_host="localhost:7469" ) assert result is mock_async_channel + mock_get_interceptor.assert_called_once_with(options, default_host="localhost:7469") assert mock_async_channel._unary_unary_interceptors == [ interceptor1, interceptor2, From c43dd611a828e9b95bfe2209e82eeb6f623dc127 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Mon, 2 Nov 2026 09:21:34 -0500 Subject: [PATCH 6/7] refactor(gapic-generator): use inspect.signature for SUPPORTS_DEFAULT_HOST --- .../templates/%namespace/%name_%version/%sub/_compat.py.j2 | 4 ++-- .../goldens/asset/google/cloud/asset_v1/_compat.py | 4 ++-- .../goldens/credentials/google/iam/credentials_v1/_compat.py | 4 ++-- .../goldens/eventarc/google/cloud/eventarc_v1/_compat.py | 4 ++-- .../goldens/logging/google/cloud/logging_v2/_compat.py | 4 ++-- .../logging_internal/google/cloud/logging_v2/_compat.py | 4 ++-- .../goldens/redis/google/cloud/redis_v1/_compat.py | 4 ++-- .../goldens/redis_selective/google/cloud/redis_v1/_compat.py | 4 ++-- .../goldens/showcase/google/showcase_v1beta1/_compat.py | 4 ++-- .../google/cloud/storagebatchoperations_v1/_compat.py | 4 ++-- 10 files changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 index 1fe80d4ccc94..0716f72decba 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 @@ -13,6 +13,7 @@ falling back to the local implementation if not present. #} {# TODO(https://github.com/googleapis/google-cloud-python/issues/17883): Backfill compatibility functions being removed from the client layer. #} +import inspect import os import json {% if has_auto_populated_fields %} @@ -104,8 +105,7 @@ ASYNC_WRAP_METHOD_SUPPORTS_KIND = WRAP_METHOD_SUPPORTS_TRACING or hasattr( # _observability.apply_otel_async_channel_interceptors allows transports to pass # their resolved default host without mutating caller-owned client_options. SUPPORTS_DEFAULT_HOST = ( - gapic_v1.method.wrap_method.__kwdefaults__ is not None - and "default_host" in gapic_v1.method.wrap_method.__kwdefaults__ + "default_host" in inspect.signature(gapic_v1.method.wrap_method).parameters ) try: diff --git a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/_compat.py b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/_compat.py index bcef1aa94dc9..2545f6fe872e 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/_compat.py @@ -15,6 +15,7 @@ # """A compatibility module for older versions of google-api-core.""" +import inspect import os import json @@ -95,8 +96,7 @@ def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: # t # _observability.apply_otel_async_channel_interceptors allows transports to pass # their resolved default host without mutating caller-owned client_options. SUPPORTS_DEFAULT_HOST = ( - gapic_v1.method.wrap_method.__kwdefaults__ is not None - and "default_host" in gapic_v1.method.wrap_method.__kwdefaults__ + "default_host" in inspect.signature(gapic_v1.method.wrap_method).parameters ) try: diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/_compat.py b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/_compat.py index bcef1aa94dc9..2545f6fe872e 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/_compat.py @@ -15,6 +15,7 @@ # """A compatibility module for older versions of google-api-core.""" +import inspect import os import json @@ -95,8 +96,7 @@ def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: # t # _observability.apply_otel_async_channel_interceptors allows transports to pass # their resolved default host without mutating caller-owned client_options. SUPPORTS_DEFAULT_HOST = ( - gapic_v1.method.wrap_method.__kwdefaults__ is not None - and "default_host" in gapic_v1.method.wrap_method.__kwdefaults__ + "default_host" in inspect.signature(gapic_v1.method.wrap_method).parameters ) try: diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/_compat.py b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/_compat.py index bcef1aa94dc9..2545f6fe872e 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/_compat.py @@ -15,6 +15,7 @@ # """A compatibility module for older versions of google-api-core.""" +import inspect import os import json @@ -95,8 +96,7 @@ def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: # t # _observability.apply_otel_async_channel_interceptors allows transports to pass # their resolved default host without mutating caller-owned client_options. SUPPORTS_DEFAULT_HOST = ( - gapic_v1.method.wrap_method.__kwdefaults__ is not None - and "default_host" in gapic_v1.method.wrap_method.__kwdefaults__ + "default_host" in inspect.signature(gapic_v1.method.wrap_method).parameters ) try: diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/_compat.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/_compat.py index bcef1aa94dc9..2545f6fe872e 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/_compat.py @@ -15,6 +15,7 @@ # """A compatibility module for older versions of google-api-core.""" +import inspect import os import json @@ -95,8 +96,7 @@ def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: # t # _observability.apply_otel_async_channel_interceptors allows transports to pass # their resolved default host without mutating caller-owned client_options. SUPPORTS_DEFAULT_HOST = ( - gapic_v1.method.wrap_method.__kwdefaults__ is not None - and "default_host" in gapic_v1.method.wrap_method.__kwdefaults__ + "default_host" in inspect.signature(gapic_v1.method.wrap_method).parameters ) try: diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/_compat.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/_compat.py index bcef1aa94dc9..2545f6fe872e 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/_compat.py @@ -15,6 +15,7 @@ # """A compatibility module for older versions of google-api-core.""" +import inspect import os import json @@ -95,8 +96,7 @@ def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: # t # _observability.apply_otel_async_channel_interceptors allows transports to pass # their resolved default host without mutating caller-owned client_options. SUPPORTS_DEFAULT_HOST = ( - gapic_v1.method.wrap_method.__kwdefaults__ is not None - and "default_host" in gapic_v1.method.wrap_method.__kwdefaults__ + "default_host" in inspect.signature(gapic_v1.method.wrap_method).parameters ) try: diff --git a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/_compat.py b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/_compat.py index bcef1aa94dc9..2545f6fe872e 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/_compat.py @@ -15,6 +15,7 @@ # """A compatibility module for older versions of google-api-core.""" +import inspect import os import json @@ -95,8 +96,7 @@ def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: # t # _observability.apply_otel_async_channel_interceptors allows transports to pass # their resolved default host without mutating caller-owned client_options. SUPPORTS_DEFAULT_HOST = ( - gapic_v1.method.wrap_method.__kwdefaults__ is not None - and "default_host" in gapic_v1.method.wrap_method.__kwdefaults__ + "default_host" in inspect.signature(gapic_v1.method.wrap_method).parameters ) try: diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/_compat.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/_compat.py index bcef1aa94dc9..2545f6fe872e 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/_compat.py @@ -15,6 +15,7 @@ # """A compatibility module for older versions of google-api-core.""" +import inspect import os import json @@ -95,8 +96,7 @@ def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: # t # _observability.apply_otel_async_channel_interceptors allows transports to pass # their resolved default host without mutating caller-owned client_options. SUPPORTS_DEFAULT_HOST = ( - gapic_v1.method.wrap_method.__kwdefaults__ is not None - and "default_host" in gapic_v1.method.wrap_method.__kwdefaults__ + "default_host" in inspect.signature(gapic_v1.method.wrap_method).parameters ) try: diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/_compat.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/_compat.py index 00485372403b..6fa1a43d9af7 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/_compat.py @@ -15,6 +15,7 @@ # """A compatibility module for older versions of google-api-core.""" +import inspect import os import json import uuid @@ -100,8 +101,7 @@ def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: # t # _observability.apply_otel_async_channel_interceptors allows transports to pass # their resolved default host without mutating caller-owned client_options. SUPPORTS_DEFAULT_HOST = ( - gapic_v1.method.wrap_method.__kwdefaults__ is not None - and "default_host" in gapic_v1.method.wrap_method.__kwdefaults__ + "default_host" in inspect.signature(gapic_v1.method.wrap_method).parameters ) try: diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/_compat.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/_compat.py index 00485372403b..6fa1a43d9af7 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/_compat.py @@ -15,6 +15,7 @@ # """A compatibility module for older versions of google-api-core.""" +import inspect import os import json import uuid @@ -100,8 +101,7 @@ def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: # t # _observability.apply_otel_async_channel_interceptors allows transports to pass # their resolved default host without mutating caller-owned client_options. SUPPORTS_DEFAULT_HOST = ( - gapic_v1.method.wrap_method.__kwdefaults__ is not None - and "default_host" in gapic_v1.method.wrap_method.__kwdefaults__ + "default_host" in inspect.signature(gapic_v1.method.wrap_method).parameters ) try: From dd50ca80ed308435f4aafa1de39a0969d1c3a259 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Tue, 3 Nov 2026 06:24:39 -0500 Subject: [PATCH 7/7] refactor(gapic-generator): use explicit _user_provided_channel attribute for default_host resolution --- .../%sub/services/%service/transports/base.py.j2 | 4 +++- .../%sub/services/%service/transports/grpc.py.j2 | 1 + .../%sub/services/%service/transports/grpc_asyncio.py.j2 | 1 + .../unit/gapic/%name_%version/%sub/test_%service.py.j2 | 9 ++++++++- .../asset_v1/services/asset_service/transports/base.py | 4 +++- .../asset_v1/services/asset_service/transports/grpc.py | 1 + .../services/asset_service/transports/grpc_asyncio.py | 1 + .../tests/unit/gapic/asset_v1/test_asset_service.py | 9 ++++++++- .../services/iam_credentials/transports/base.py | 4 +++- .../services/iam_credentials/transports/grpc.py | 1 + .../services/iam_credentials/transports/grpc_asyncio.py | 1 + .../unit/gapic/credentials_v1/test_iam_credentials.py | 9 ++++++++- .../eventarc_v1/services/eventarc/transports/base.py | 4 +++- .../eventarc_v1/services/eventarc/transports/grpc.py | 1 + .../services/eventarc/transports/grpc_asyncio.py | 1 + .../tests/unit/gapic/eventarc_v1/test_eventarc.py | 9 ++++++++- .../services/config_service_v2/transports/base.py | 4 +++- .../services/config_service_v2/transports/grpc.py | 1 + .../config_service_v2/transports/grpc_asyncio.py | 1 + .../services/logging_service_v2/transports/base.py | 4 +++- .../services/logging_service_v2/transports/grpc.py | 1 + .../logging_service_v2/transports/grpc_asyncio.py | 1 + .../services/metrics_service_v2/transports/base.py | 4 +++- .../services/metrics_service_v2/transports/grpc.py | 1 + .../metrics_service_v2/transports/grpc_asyncio.py | 1 + .../unit/gapic/logging_v2/test_config_service_v2.py | 9 ++++++++- .../unit/gapic/logging_v2/test_logging_service_v2.py | 9 ++++++++- .../unit/gapic/logging_v2/test_metrics_service_v2.py | 9 ++++++++- .../services/config_service_v2/transports/base.py | 4 +++- .../services/config_service_v2/transports/grpc.py | 1 + .../config_service_v2/transports/grpc_asyncio.py | 1 + .../services/logging_service_v2/transports/base.py | 4 +++- .../services/logging_service_v2/transports/grpc.py | 1 + .../logging_service_v2/transports/grpc_asyncio.py | 1 + .../services/metrics_service_v2/transports/base.py | 4 +++- .../services/metrics_service_v2/transports/grpc.py | 1 + .../metrics_service_v2/transports/grpc_asyncio.py | 1 + .../unit/gapic/logging_v2/test_config_service_v2.py | 9 ++++++++- .../unit/gapic/logging_v2/test_logging_service_v2.py | 9 ++++++++- .../unit/gapic/logging_v2/test_metrics_service_v2.py | 9 ++++++++- .../redis_v1/services/cloud_redis/transports/base.py | 4 +++- .../redis_v1/services/cloud_redis/transports/grpc.py | 1 + .../services/cloud_redis/transports/grpc_asyncio.py | 1 + .../redis/tests/unit/gapic/redis_v1/test_cloud_redis.py | 9 ++++++++- .../redis_v1/services/cloud_redis/transports/base.py | 4 +++- .../redis_v1/services/cloud_redis/transports/grpc.py | 1 + .../services/cloud_redis/transports/grpc_asyncio.py | 1 + .../tests/unit/gapic/redis_v1/test_cloud_redis.py | 9 ++++++++- .../showcase_v1beta1/services/echo/transports/base.py | 4 +++- .../showcase_v1beta1/services/echo/transports/grpc.py | 1 + .../services/echo/transports/grpc_asyncio.py | 1 + .../services/identity/transports/base.py | 4 +++- .../services/identity/transports/grpc.py | 1 + .../services/identity/transports/grpc_asyncio.py | 1 + .../services/messaging/transports/base.py | 4 +++- .../services/messaging/transports/grpc.py | 1 + .../services/messaging/transports/grpc_asyncio.py | 1 + .../services/resumable_upload_service/transports/base.py | 4 +++- .../services/resumable_upload_service/transports/grpc.py | 1 + .../resumable_upload_service/transports/grpc_asyncio.py | 1 + .../services/sequence_service/transports/base.py | 4 +++- .../services/sequence_service/transports/grpc.py | 1 + .../services/sequence_service/transports/grpc_asyncio.py | 1 + .../showcase_v1beta1/services/testing/transports/base.py | 4 +++- .../showcase_v1beta1/services/testing/transports/grpc.py | 1 + .../services/testing/transports/grpc_asyncio.py | 1 + .../tests/unit/gapic/showcase_v1beta1/test_echo.py | 9 ++++++++- .../tests/unit/gapic/showcase_v1beta1/test_identity.py | 9 ++++++++- .../tests/unit/gapic/showcase_v1beta1/test_messaging.py | 9 ++++++++- .../showcase_v1beta1/test_resumable_upload_service.py | 9 ++++++++- .../unit/gapic/showcase_v1beta1/test_sequence_service.py | 9 ++++++++- .../tests/unit/gapic/showcase_v1beta1/test_testing.py | 9 ++++++++- .../services/storage_batch_operations/transports/base.py | 4 +++- .../services/storage_batch_operations/transports/grpc.py | 1 + .../storage_batch_operations/transports/grpc_asyncio.py | 1 + .../test_storage_batch_operations.py | 9 ++++++++- 76 files changed, 247 insertions(+), 38 deletions(-) diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 index 8e8342dc75e8..f74aee2690d4 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 @@ -129,6 +129,8 @@ class {{ service.name }}Transport(abc.ABC): self._scopes = scopes if not hasattr(self, "_ignore_credentials"): self._ignore_credentials: bool = False + if not hasattr(self, "_user_provided_channel"): + self._user_provided_channel: bool = False # If no credentials are provided, then determine the appropriate # defaults. @@ -168,7 +170,7 @@ class {{ service.name }}Transport(abc.ABC): # super().__init__, self._host is not used to create the channel and # cannot be trusted as the default span endpoint. self._default_host: Optional[str] = ( - None if getattr(self, "_grpc_channel", None) is not None else self._host + None if self._user_provided_channel else self._host ) self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 index c424ff95dcb8..1d689771a549 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 @@ -235,6 +235,7 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport): self._ignore_credentials = True # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._user_provided_channel = True self._ssl_channel_credentials = None else: diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 index 1e10b95e5ddf..c6ebfa185723 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 @@ -283,6 +283,7 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport): self._ignore_credentials = True # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._user_provided_channel = True self._ssl_channel_credentials = None else: if api_mtls_endpoint: diff --git a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 index 1362dd5c2a5c..ab53d4699321 100644 --- a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 +++ b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 @@ -1706,6 +1706,8 @@ def test_{{ service.name|snake_case }}_grpc_transport_channel(): assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" assert transport._ssl_channel_credentials == None + assert transport._user_provided_channel is True + assert transport._default_host is None def test_{{ service.name|snake_case }}_grpc_asyncio_transport_channel(): @@ -1719,6 +1721,8 @@ def test_{{ service.name|snake_case }}_grpc_asyncio_transport_channel(): assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" assert transport._ssl_channel_credentials == None + assert transport._user_provided_channel is True + assert transport._default_host is None {% if service.has_resumable_upload_methods and 'grpc' in opts.transport %} @@ -2565,6 +2569,7 @@ def test_{{ service.name|snake_case }}_base_transport_client_options_object( ) assert transport._client_options is options assert transport._client_options.api_endpoint == initial_endpoint + assert transport._user_provided_channel is False assert transport._default_host == "default.example.com:443" @@ -2594,6 +2599,7 @@ def test_{{ service.name|snake_case }}_base_transport_client_options_dict( ) assert transport._client_options is options assert transport._client_options.get("api_endpoint") == expected_endpoint + assert transport._user_provided_channel is False assert transport._default_host == "default.example.com:443" assert options == initial_options @@ -2602,7 +2608,7 @@ def test_{{ service.name|snake_case }}_base_transport_default_host_ignored_when_ class _CustomChannelTransport(transports.{{ service.name }}Transport): def __init__(self, **kwargs): self._ignore_credentials = True - self._grpc_channel = mock.sentinel.custom_channel + self._user_provided_channel = True super().__init__(**kwargs) options = client_options.ClientOptions() @@ -2612,6 +2618,7 @@ def test_{{ service.name|snake_case }}_base_transport_default_host_ignored_when_ ) assert transport._client_options is options assert transport._client_options.api_endpoint is None + assert transport._user_provided_channel is True assert transport._default_host is None diff --git a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/base.py b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/base.py index 98cf2801fa4f..47a94f1959b4 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/base.py @@ -103,6 +103,8 @@ def __init__( self._scopes = scopes if not hasattr(self, "_ignore_credentials"): self._ignore_credentials: bool = False + if not hasattr(self, "_user_provided_channel"): + self._user_provided_channel: bool = False # If no credentials are provided, then determine the appropriate # defaults. @@ -139,7 +141,7 @@ def __init__( # super().__init__, self._host is not used to create the channel and # cannot be trusted as the default span endpoint. self._default_host: Optional[str] = ( - None if getattr(self, "_grpc_channel", None) is not None else self._host + None if self._user_provided_channel else self._host ) self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/grpc.py index f2a7e6d3af1c..b7e7d2fb3cf3 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/grpc.py @@ -225,6 +225,7 @@ def __init__(self, *, self._ignore_credentials = True # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._user_provided_channel = True self._ssl_channel_credentials = None else: diff --git a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/grpc_asyncio.py index c74319862264..ed98a87ac789 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/grpc_asyncio.py @@ -269,6 +269,7 @@ def __init__(self, *, self._ignore_credentials = True # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._user_provided_channel = True self._ssl_channel_credentials = None else: if api_mtls_endpoint: diff --git a/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py b/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py index 28f2a829b425..b9dd7e19527a 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py +++ b/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py @@ -17808,6 +17808,8 @@ def test_asset_service_grpc_transport_channel(): assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" assert transport._ssl_channel_credentials == None + assert transport._user_provided_channel is True + assert transport._default_host is None def test_asset_service_grpc_asyncio_transport_channel(): @@ -17821,6 +17823,8 @@ def test_asset_service_grpc_asyncio_transport_channel(): assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" assert transport._ssl_channel_credentials == None + assert transport._user_provided_channel is True + assert transport._default_host is None # Remove this test when deprecated arguments (api_mtls_endpoint, client_cert_source) are @@ -18500,6 +18504,7 @@ def test_asset_service_base_transport_client_options_object( ) assert transport._client_options is options assert transport._client_options.api_endpoint == initial_endpoint + assert transport._user_provided_channel is False assert transport._default_host == "default.example.com:443" @@ -18529,6 +18534,7 @@ def test_asset_service_base_transport_client_options_dict( ) assert transport._client_options is options assert transport._client_options.get("api_endpoint") == expected_endpoint + assert transport._user_provided_channel is False assert transport._default_host == "default.example.com:443" assert options == initial_options @@ -18537,7 +18543,7 @@ def test_asset_service_base_transport_default_host_ignored_when_custom_grpc_chan class _CustomChannelTransport(transports.AssetServiceTransport): def __init__(self, **kwargs): self._ignore_credentials = True - self._grpc_channel = mock.sentinel.custom_channel + self._user_provided_channel = True super().__init__(**kwargs) options = client_options.ClientOptions() @@ -18547,6 +18553,7 @@ def __init__(self, **kwargs): ) assert transport._client_options is options assert transport._client_options.api_endpoint is None + assert transport._user_provided_channel is True assert transport._default_host is None diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/base.py b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/base.py index 74346d7c2218..3bec6ad0cc33 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/base.py @@ -100,6 +100,8 @@ def __init__( self._scopes = scopes if not hasattr(self, "_ignore_credentials"): self._ignore_credentials: bool = False + if not hasattr(self, "_user_provided_channel"): + self._user_provided_channel: bool = False # If no credentials are provided, then determine the appropriate # defaults. @@ -136,7 +138,7 @@ def __init__( # super().__init__, self._host is not used to create the channel and # cannot be trusted as the default span endpoint. self._default_host: Optional[str] = ( - None if getattr(self, "_grpc_channel", None) is not None else self._host + None if self._user_provided_channel else self._host ) self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/grpc.py index df22772304fd..8dae63762125 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/grpc.py @@ -230,6 +230,7 @@ def __init__(self, *, self._ignore_credentials = True # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._user_provided_channel = True self._ssl_channel_credentials = None else: diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/grpc_asyncio.py index 45d77f8dacf1..caa5530bb88e 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/grpc_asyncio.py @@ -274,6 +274,7 @@ def __init__(self, *, self._ignore_credentials = True # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._user_provided_channel = True self._ssl_channel_credentials = None else: if api_mtls_endpoint: diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py b/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py index 2134257aac7b..8c7601cf7d18 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py +++ b/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py @@ -4165,6 +4165,8 @@ def test_iam_credentials_grpc_transport_channel(): assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" assert transport._ssl_channel_credentials == None + assert transport._user_provided_channel is True + assert transport._default_host is None def test_iam_credentials_grpc_asyncio_transport_channel(): @@ -4178,6 +4180,8 @@ def test_iam_credentials_grpc_asyncio_transport_channel(): assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" assert transport._ssl_channel_credentials == None + assert transport._user_provided_channel is True + assert transport._default_host is None # Remove this test when deprecated arguments (api_mtls_endpoint, client_cert_source) are @@ -4546,6 +4550,7 @@ def test_iam_credentials_base_transport_client_options_object( ) assert transport._client_options is options assert transport._client_options.api_endpoint == initial_endpoint + assert transport._user_provided_channel is False assert transport._default_host == "default.example.com:443" @@ -4575,6 +4580,7 @@ def test_iam_credentials_base_transport_client_options_dict( ) assert transport._client_options is options assert transport._client_options.get("api_endpoint") == expected_endpoint + assert transport._user_provided_channel is False assert transport._default_host == "default.example.com:443" assert options == initial_options @@ -4583,7 +4589,7 @@ def test_iam_credentials_base_transport_default_host_ignored_when_custom_grpc_ch class _CustomChannelTransport(transports.IAMCredentialsTransport): def __init__(self, **kwargs): self._ignore_credentials = True - self._grpc_channel = mock.sentinel.custom_channel + self._user_provided_channel = True super().__init__(**kwargs) options = client_options.ClientOptions() @@ -4593,6 +4599,7 @@ def __init__(self, **kwargs): ) assert transport._client_options is options assert transport._client_options.api_endpoint is None + assert transport._user_provided_channel is True assert transport._default_host is None diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/base.py b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/base.py index 66344b384807..0643a95575f6 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/base.py @@ -115,6 +115,8 @@ def __init__( self._scopes = scopes if not hasattr(self, "_ignore_credentials"): self._ignore_credentials: bool = False + if not hasattr(self, "_user_provided_channel"): + self._user_provided_channel: bool = False # If no credentials are provided, then determine the appropriate # defaults. @@ -151,7 +153,7 @@ def __init__( # super().__init__, self._host is not used to create the channel and # cannot be trusted as the default span endpoint. self._default_host: Optional[str] = ( - None if getattr(self, "_grpc_channel", None) is not None else self._host + None if self._user_provided_channel else self._host ) self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/grpc.py index 1a4d51772a0b..684cac7b9145 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/grpc.py @@ -239,6 +239,7 @@ def __init__(self, *, self._ignore_credentials = True # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._user_provided_channel = True self._ssl_channel_credentials = None else: diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/grpc_asyncio.py index 6fb79c5eec5a..344511242b78 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/grpc_asyncio.py @@ -283,6 +283,7 @@ def __init__(self, *, self._ignore_credentials = True # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._user_provided_channel = True self._ssl_channel_credentials = None else: if api_mtls_endpoint: diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py b/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py index 8a7296a59148..86fef1613499 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py @@ -31297,6 +31297,8 @@ def test_eventarc_grpc_transport_channel(): assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" assert transport._ssl_channel_credentials == None + assert transport._user_provided_channel is True + assert transport._default_host is None def test_eventarc_grpc_asyncio_transport_channel(): @@ -31310,6 +31312,8 @@ def test_eventarc_grpc_asyncio_transport_channel(): assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" assert transport._ssl_channel_credentials == None + assert transport._user_provided_channel is True + assert transport._default_host is None # Remove this test when deprecated arguments (api_mtls_endpoint, client_cert_source) are @@ -33603,6 +33607,7 @@ def test_eventarc_base_transport_client_options_object( ) assert transport._client_options is options assert transport._client_options.api_endpoint == initial_endpoint + assert transport._user_provided_channel is False assert transport._default_host == "default.example.com:443" @@ -33632,6 +33637,7 @@ def test_eventarc_base_transport_client_options_dict( ) assert transport._client_options is options assert transport._client_options.get("api_endpoint") == expected_endpoint + assert transport._user_provided_channel is False assert transport._default_host == "default.example.com:443" assert options == initial_options @@ -33640,7 +33646,7 @@ def test_eventarc_base_transport_default_host_ignored_when_custom_grpc_channel() class _CustomChannelTransport(transports.EventarcTransport): def __init__(self, **kwargs): self._ignore_credentials = True - self._grpc_channel = mock.sentinel.custom_channel + self._user_provided_channel = True super().__init__(**kwargs) options = client_options.ClientOptions() @@ -33650,6 +33656,7 @@ def __init__(self, **kwargs): ) assert transport._client_options is options assert transport._client_options.api_endpoint is None + assert transport._user_provided_channel is True assert transport._default_host is None diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/base.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/base.py index 62880b00586a..1236732d3cef 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/base.py @@ -106,6 +106,8 @@ def __init__( self._scopes = scopes if not hasattr(self, "_ignore_credentials"): self._ignore_credentials: bool = False + if not hasattr(self, "_user_provided_channel"): + self._user_provided_channel: bool = False # If no credentials are provided, then determine the appropriate # defaults. @@ -142,7 +144,7 @@ def __init__( # super().__init__, self._host is not used to create the channel and # cannot be trusted as the default span endpoint. self._default_host: Optional[str] = ( - None if getattr(self, "_grpc_channel", None) is not None else self._host + None if self._user_provided_channel else self._host ) self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/grpc.py index 2ee41c76666c..e53299a7cf04 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/grpc.py @@ -225,6 +225,7 @@ def __init__(self, *, self._ignore_credentials = True # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._user_provided_channel = True self._ssl_channel_credentials = None else: diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py index c972054052bd..9997dc6d9ef7 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py @@ -269,6 +269,7 @@ def __init__(self, *, self._ignore_credentials = True # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._user_provided_channel = True self._ssl_channel_credentials = None else: if api_mtls_endpoint: diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/base.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/base.py index 76998fcd812c..a49276230595 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/base.py @@ -106,6 +106,8 @@ def __init__( self._scopes = scopes if not hasattr(self, "_ignore_credentials"): self._ignore_credentials: bool = False + if not hasattr(self, "_user_provided_channel"): + self._user_provided_channel: bool = False # If no credentials are provided, then determine the appropriate # defaults. @@ -142,7 +144,7 @@ def __init__( # super().__init__, self._host is not used to create the channel and # cannot be trusted as the default span endpoint. self._default_host: Optional[str] = ( - None if getattr(self, "_grpc_channel", None) is not None else self._host + None if self._user_provided_channel else self._host ) self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/grpc.py index b36b20e3d16a..266d9083b341 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/grpc.py @@ -223,6 +223,7 @@ def __init__(self, *, self._ignore_credentials = True # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._user_provided_channel = True self._ssl_channel_credentials = None else: diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py index 071fe3196e25..77a8d69009b3 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py @@ -267,6 +267,7 @@ def __init__(self, *, self._ignore_credentials = True # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._user_provided_channel = True self._ssl_channel_credentials = None else: if api_mtls_endpoint: diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py index 5fe2ac27c9ba..0e7cf329fac0 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py @@ -106,6 +106,8 @@ def __init__( self._scopes = scopes if not hasattr(self, "_ignore_credentials"): self._ignore_credentials: bool = False + if not hasattr(self, "_user_provided_channel"): + self._user_provided_channel: bool = False # If no credentials are provided, then determine the appropriate # defaults. @@ -142,7 +144,7 @@ def __init__( # super().__init__, self._host is not used to create the channel and # cannot be trusted as the default span endpoint. self._default_host: Optional[str] = ( - None if getattr(self, "_grpc_channel", None) is not None else self._host + None if self._user_provided_channel else self._host ) self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc.py index 351af44bd1a2..aa746fb22171 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc.py @@ -223,6 +223,7 @@ def __init__(self, *, self._ignore_credentials = True # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._user_provided_channel = True self._ssl_channel_credentials = None else: diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py index beaf9710314f..6effe18f939b 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py @@ -267,6 +267,7 @@ def __init__(self, *, self._ignore_credentials = True # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._user_provided_channel = True self._ssl_channel_credentials = None else: if api_mtls_endpoint: diff --git a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py index 8551f5b46051..f2a5fbe88df5 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py @@ -13073,6 +13073,8 @@ def test_config_service_v2_grpc_transport_channel(): assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" assert transport._ssl_channel_credentials == None + assert transport._user_provided_channel is True + assert transport._default_host is None def test_config_service_v2_grpc_asyncio_transport_channel(): @@ -13086,6 +13088,8 @@ def test_config_service_v2_grpc_asyncio_transport_channel(): assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" assert transport._ssl_channel_credentials == None + assert transport._user_provided_channel is True + assert transport._default_host is None # Remove this test when deprecated arguments (api_mtls_endpoint, client_cert_source) are @@ -14097,6 +14101,7 @@ def test_config_service_v2_base_transport_client_options_object( ) assert transport._client_options is options assert transport._client_options.api_endpoint == initial_endpoint + assert transport._user_provided_channel is False assert transport._default_host == "default.example.com:443" @@ -14126,6 +14131,7 @@ def test_config_service_v2_base_transport_client_options_dict( ) assert transport._client_options is options assert transport._client_options.get("api_endpoint") == expected_endpoint + assert transport._user_provided_channel is False assert transport._default_host == "default.example.com:443" assert options == initial_options @@ -14134,7 +14140,7 @@ def test_config_service_v2_base_transport_default_host_ignored_when_custom_grpc_ class _CustomChannelTransport(transports.ConfigServiceV2Transport): def __init__(self, **kwargs): self._ignore_credentials = True - self._grpc_channel = mock.sentinel.custom_channel + self._user_provided_channel = True super().__init__(**kwargs) options = client_options.ClientOptions() @@ -14144,6 +14150,7 @@ def __init__(self, **kwargs): ) assert transport._client_options is options assert transport._client_options.api_endpoint is None + assert transport._user_provided_channel is True assert transport._default_host is None diff --git a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py index 739d4d17ef02..6173c39bb9bf 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py @@ -3667,6 +3667,8 @@ def test_logging_service_v2_grpc_transport_channel(): assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" assert transport._ssl_channel_credentials == None + assert transport._user_provided_channel is True + assert transport._default_host is None def test_logging_service_v2_grpc_asyncio_transport_channel(): @@ -3680,6 +3682,8 @@ def test_logging_service_v2_grpc_asyncio_transport_channel(): assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" assert transport._ssl_channel_credentials == None + assert transport._user_provided_channel is True + assert transport._default_host is None # Remove this test when deprecated arguments (api_mtls_endpoint, client_cert_source) are @@ -4537,6 +4541,7 @@ def test_logging_service_v2_base_transport_client_options_object( ) assert transport._client_options is options assert transport._client_options.api_endpoint == initial_endpoint + assert transport._user_provided_channel is False assert transport._default_host == "default.example.com:443" @@ -4566,6 +4571,7 @@ def test_logging_service_v2_base_transport_client_options_dict( ) assert transport._client_options is options assert transport._client_options.get("api_endpoint") == expected_endpoint + assert transport._user_provided_channel is False assert transport._default_host == "default.example.com:443" assert options == initial_options @@ -4574,7 +4580,7 @@ def test_logging_service_v2_base_transport_default_host_ignored_when_custom_grpc class _CustomChannelTransport(transports.LoggingServiceV2Transport): def __init__(self, **kwargs): self._ignore_credentials = True - self._grpc_channel = mock.sentinel.custom_channel + self._user_provided_channel = True super().__init__(**kwargs) options = client_options.ClientOptions() @@ -4584,6 +4590,7 @@ def __init__(self, **kwargs): ) assert transport._client_options is options assert transport._client_options.api_endpoint is None + assert transport._user_provided_channel is True assert transport._default_host is None diff --git a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py index e1927e1fa019..62b10a0b4b79 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py @@ -3467,6 +3467,8 @@ def test_metrics_service_v2_grpc_transport_channel(): assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" assert transport._ssl_channel_credentials == None + assert transport._user_provided_channel is True + assert transport._default_host is None def test_metrics_service_v2_grpc_asyncio_transport_channel(): @@ -3480,6 +3482,8 @@ def test_metrics_service_v2_grpc_asyncio_transport_channel(): assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" assert transport._ssl_channel_credentials == None + assert transport._user_provided_channel is True + assert transport._default_host is None # Remove this test when deprecated arguments (api_mtls_endpoint, client_cert_source) are @@ -4337,6 +4341,7 @@ def test_metrics_service_v2_base_transport_client_options_object( ) assert transport._client_options is options assert transport._client_options.api_endpoint == initial_endpoint + assert transport._user_provided_channel is False assert transport._default_host == "default.example.com:443" @@ -4366,6 +4371,7 @@ def test_metrics_service_v2_base_transport_client_options_dict( ) assert transport._client_options is options assert transport._client_options.get("api_endpoint") == expected_endpoint + assert transport._user_provided_channel is False assert transport._default_host == "default.example.com:443" assert options == initial_options @@ -4374,7 +4380,7 @@ def test_metrics_service_v2_base_transport_default_host_ignored_when_custom_grpc class _CustomChannelTransport(transports.MetricsServiceV2Transport): def __init__(self, **kwargs): self._ignore_credentials = True - self._grpc_channel = mock.sentinel.custom_channel + self._user_provided_channel = True super().__init__(**kwargs) options = client_options.ClientOptions() @@ -4384,6 +4390,7 @@ def __init__(self, **kwargs): ) assert transport._client_options is options assert transport._client_options.api_endpoint is None + assert transport._user_provided_channel is True assert transport._default_host is None diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/base.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/base.py index 62880b00586a..1236732d3cef 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/base.py @@ -106,6 +106,8 @@ def __init__( self._scopes = scopes if not hasattr(self, "_ignore_credentials"): self._ignore_credentials: bool = False + if not hasattr(self, "_user_provided_channel"): + self._user_provided_channel: bool = False # If no credentials are provided, then determine the appropriate # defaults. @@ -142,7 +144,7 @@ def __init__( # super().__init__, self._host is not used to create the channel and # cannot be trusted as the default span endpoint. self._default_host: Optional[str] = ( - None if getattr(self, "_grpc_channel", None) is not None else self._host + None if self._user_provided_channel else self._host ) self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/grpc.py index 2ee41c76666c..e53299a7cf04 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/grpc.py @@ -225,6 +225,7 @@ def __init__(self, *, self._ignore_credentials = True # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._user_provided_channel = True self._ssl_channel_credentials = None else: diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py index c972054052bd..9997dc6d9ef7 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py @@ -269,6 +269,7 @@ def __init__(self, *, self._ignore_credentials = True # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._user_provided_channel = True self._ssl_channel_credentials = None else: if api_mtls_endpoint: diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/base.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/base.py index 76998fcd812c..a49276230595 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/base.py @@ -106,6 +106,8 @@ def __init__( self._scopes = scopes if not hasattr(self, "_ignore_credentials"): self._ignore_credentials: bool = False + if not hasattr(self, "_user_provided_channel"): + self._user_provided_channel: bool = False # If no credentials are provided, then determine the appropriate # defaults. @@ -142,7 +144,7 @@ def __init__( # super().__init__, self._host is not used to create the channel and # cannot be trusted as the default span endpoint. self._default_host: Optional[str] = ( - None if getattr(self, "_grpc_channel", None) is not None else self._host + None if self._user_provided_channel else self._host ) self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/grpc.py index b36b20e3d16a..266d9083b341 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/grpc.py @@ -223,6 +223,7 @@ def __init__(self, *, self._ignore_credentials = True # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._user_provided_channel = True self._ssl_channel_credentials = None else: diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py index 071fe3196e25..77a8d69009b3 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py @@ -267,6 +267,7 @@ def __init__(self, *, self._ignore_credentials = True # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._user_provided_channel = True self._ssl_channel_credentials = None else: if api_mtls_endpoint: diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py index 5fe2ac27c9ba..0e7cf329fac0 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py @@ -106,6 +106,8 @@ def __init__( self._scopes = scopes if not hasattr(self, "_ignore_credentials"): self._ignore_credentials: bool = False + if not hasattr(self, "_user_provided_channel"): + self._user_provided_channel: bool = False # If no credentials are provided, then determine the appropriate # defaults. @@ -142,7 +144,7 @@ def __init__( # super().__init__, self._host is not used to create the channel and # cannot be trusted as the default span endpoint. self._default_host: Optional[str] = ( - None if getattr(self, "_grpc_channel", None) is not None else self._host + None if self._user_provided_channel else self._host ) self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc.py index 351af44bd1a2..aa746fb22171 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc.py @@ -223,6 +223,7 @@ def __init__(self, *, self._ignore_credentials = True # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._user_provided_channel = True self._ssl_channel_credentials = None else: diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py index beaf9710314f..6effe18f939b 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py @@ -267,6 +267,7 @@ def __init__(self, *, self._ignore_credentials = True # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._user_provided_channel = True self._ssl_channel_credentials = None else: if api_mtls_endpoint: diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py index 94afaa35a7e2..c6052913a6f3 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py @@ -13073,6 +13073,8 @@ def test_config_service_v2_grpc_transport_channel(): assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" assert transport._ssl_channel_credentials == None + assert transport._user_provided_channel is True + assert transport._default_host is None def test_config_service_v2_grpc_asyncio_transport_channel(): @@ -13086,6 +13088,8 @@ def test_config_service_v2_grpc_asyncio_transport_channel(): assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" assert transport._ssl_channel_credentials == None + assert transport._user_provided_channel is True + assert transport._default_host is None # Remove this test when deprecated arguments (api_mtls_endpoint, client_cert_source) are @@ -14097,6 +14101,7 @@ def test_config_service_v2_base_transport_client_options_object( ) assert transport._client_options is options assert transport._client_options.api_endpoint == initial_endpoint + assert transport._user_provided_channel is False assert transport._default_host == "default.example.com:443" @@ -14126,6 +14131,7 @@ def test_config_service_v2_base_transport_client_options_dict( ) assert transport._client_options is options assert transport._client_options.get("api_endpoint") == expected_endpoint + assert transport._user_provided_channel is False assert transport._default_host == "default.example.com:443" assert options == initial_options @@ -14134,7 +14140,7 @@ def test_config_service_v2_base_transport_default_host_ignored_when_custom_grpc_ class _CustomChannelTransport(transports.ConfigServiceV2Transport): def __init__(self, **kwargs): self._ignore_credentials = True - self._grpc_channel = mock.sentinel.custom_channel + self._user_provided_channel = True super().__init__(**kwargs) options = client_options.ClientOptions() @@ -14144,6 +14150,7 @@ def __init__(self, **kwargs): ) assert transport._client_options is options assert transport._client_options.api_endpoint is None + assert transport._user_provided_channel is True assert transport._default_host is None diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py index 739d4d17ef02..6173c39bb9bf 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py @@ -3667,6 +3667,8 @@ def test_logging_service_v2_grpc_transport_channel(): assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" assert transport._ssl_channel_credentials == None + assert transport._user_provided_channel is True + assert transport._default_host is None def test_logging_service_v2_grpc_asyncio_transport_channel(): @@ -3680,6 +3682,8 @@ def test_logging_service_v2_grpc_asyncio_transport_channel(): assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" assert transport._ssl_channel_credentials == None + assert transport._user_provided_channel is True + assert transport._default_host is None # Remove this test when deprecated arguments (api_mtls_endpoint, client_cert_source) are @@ -4537,6 +4541,7 @@ def test_logging_service_v2_base_transport_client_options_object( ) assert transport._client_options is options assert transport._client_options.api_endpoint == initial_endpoint + assert transport._user_provided_channel is False assert transport._default_host == "default.example.com:443" @@ -4566,6 +4571,7 @@ def test_logging_service_v2_base_transport_client_options_dict( ) assert transport._client_options is options assert transport._client_options.get("api_endpoint") == expected_endpoint + assert transport._user_provided_channel is False assert transport._default_host == "default.example.com:443" assert options == initial_options @@ -4574,7 +4580,7 @@ def test_logging_service_v2_base_transport_default_host_ignored_when_custom_grpc class _CustomChannelTransport(transports.LoggingServiceV2Transport): def __init__(self, **kwargs): self._ignore_credentials = True - self._grpc_channel = mock.sentinel.custom_channel + self._user_provided_channel = True super().__init__(**kwargs) options = client_options.ClientOptions() @@ -4584,6 +4590,7 @@ def __init__(self, **kwargs): ) assert transport._client_options is options assert transport._client_options.api_endpoint is None + assert transport._user_provided_channel is True assert transport._default_host is None diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py index fc1ebd4dcc16..5eca8fde6778 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py @@ -3467,6 +3467,8 @@ def test_metrics_service_v2_grpc_transport_channel(): assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" assert transport._ssl_channel_credentials == None + assert transport._user_provided_channel is True + assert transport._default_host is None def test_metrics_service_v2_grpc_asyncio_transport_channel(): @@ -3480,6 +3482,8 @@ def test_metrics_service_v2_grpc_asyncio_transport_channel(): assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" assert transport._ssl_channel_credentials == None + assert transport._user_provided_channel is True + assert transport._default_host is None # Remove this test when deprecated arguments (api_mtls_endpoint, client_cert_source) are @@ -4337,6 +4341,7 @@ def test_metrics_service_v2_base_transport_client_options_object( ) assert transport._client_options is options assert transport._client_options.api_endpoint == initial_endpoint + assert transport._user_provided_channel is False assert transport._default_host == "default.example.com:443" @@ -4366,6 +4371,7 @@ def test_metrics_service_v2_base_transport_client_options_dict( ) assert transport._client_options is options assert transport._client_options.get("api_endpoint") == expected_endpoint + assert transport._user_provided_channel is False assert transport._default_host == "default.example.com:443" assert options == initial_options @@ -4374,7 +4380,7 @@ def test_metrics_service_v2_base_transport_default_host_ignored_when_custom_grpc class _CustomChannelTransport(transports.MetricsServiceV2Transport): def __init__(self, **kwargs): self._ignore_credentials = True - self._grpc_channel = mock.sentinel.custom_channel + self._user_provided_channel = True super().__init__(**kwargs) options = client_options.ClientOptions() @@ -4384,6 +4390,7 @@ def __init__(self, **kwargs): ) assert transport._client_options is options assert transport._client_options.api_endpoint is None + assert transport._user_provided_channel is True assert transport._default_host is None diff --git a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/base.py b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/base.py index c0f91c8c3c84..6daa5eb339c6 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/base.py @@ -103,6 +103,8 @@ def __init__( self._scopes = scopes if not hasattr(self, "_ignore_credentials"): self._ignore_credentials: bool = False + if not hasattr(self, "_user_provided_channel"): + self._user_provided_channel: bool = False # If no credentials are provided, then determine the appropriate # defaults. @@ -139,7 +141,7 @@ def __init__( # super().__init__, self._host is not used to create the channel and # cannot be trusted as the default span endpoint. self._default_host: Optional[str] = ( - None if getattr(self, "_grpc_channel", None) is not None else self._host + None if self._user_provided_channel else self._host ) self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/grpc.py index 219b2278c879..53dc638ad6d3 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/grpc.py @@ -245,6 +245,7 @@ def __init__(self, *, self._ignore_credentials = True # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._user_provided_channel = True self._ssl_channel_credentials = None else: diff --git a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py index c876375cb223..4c95082b021d 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py @@ -289,6 +289,7 @@ def __init__(self, *, self._ignore_credentials = True # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._user_provided_channel = True self._ssl_channel_credentials = None else: if api_mtls_endpoint: diff --git a/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py b/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py index ee17389c0d21..88e200e064a5 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py @@ -11792,6 +11792,8 @@ def test_cloud_redis_grpc_transport_channel(): assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" assert transport._ssl_channel_credentials == None + assert transport._user_provided_channel is True + assert transport._default_host is None def test_cloud_redis_grpc_asyncio_transport_channel(): @@ -11805,6 +11807,8 @@ def test_cloud_redis_grpc_asyncio_transport_channel(): assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" assert transport._ssl_channel_credentials == None + assert transport._user_provided_channel is True + assert transport._default_host is None # Remove this test when deprecated arguments (api_mtls_endpoint, client_cert_source) are @@ -13391,6 +13395,7 @@ def test_cloud_redis_base_transport_client_options_object( ) assert transport._client_options is options assert transport._client_options.api_endpoint == initial_endpoint + assert transport._user_provided_channel is False assert transport._default_host == "default.example.com:443" @@ -13420,6 +13425,7 @@ def test_cloud_redis_base_transport_client_options_dict( ) assert transport._client_options is options assert transport._client_options.get("api_endpoint") == expected_endpoint + assert transport._user_provided_channel is False assert transport._default_host == "default.example.com:443" assert options == initial_options @@ -13428,7 +13434,7 @@ def test_cloud_redis_base_transport_default_host_ignored_when_custom_grpc_channe class _CustomChannelTransport(transports.CloudRedisTransport): def __init__(self, **kwargs): self._ignore_credentials = True - self._grpc_channel = mock.sentinel.custom_channel + self._user_provided_channel = True super().__init__(**kwargs) options = client_options.ClientOptions() @@ -13438,6 +13444,7 @@ def __init__(self, **kwargs): ) assert transport._client_options is options assert transport._client_options.api_endpoint is None + assert transport._user_provided_channel is True assert transport._default_host is None diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/base.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/base.py index 51cc094aab06..8f14810c51c4 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/base.py @@ -103,6 +103,8 @@ def __init__( self._scopes = scopes if not hasattr(self, "_ignore_credentials"): self._ignore_credentials: bool = False + if not hasattr(self, "_user_provided_channel"): + self._user_provided_channel: bool = False # If no credentials are provided, then determine the appropriate # defaults. @@ -139,7 +141,7 @@ def __init__( # super().__init__, self._host is not used to create the channel and # cannot be trusted as the default span endpoint. self._default_host: Optional[str] = ( - None if getattr(self, "_grpc_channel", None) is not None else self._host + None if self._user_provided_channel else self._host ) self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/grpc.py index bdbdd5811d2c..e313f7b527d7 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/grpc.py @@ -245,6 +245,7 @@ def __init__(self, *, self._ignore_credentials = True # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._user_provided_channel = True self._ssl_channel_credentials = None else: diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py index aeb6e7aca426..9d170d41dec2 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py @@ -289,6 +289,7 @@ def __init__(self, *, self._ignore_credentials = True # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._user_provided_channel = True self._ssl_channel_credentials = None else: if api_mtls_endpoint: diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py index 948408ed8a33..e91d07951808 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py @@ -7012,6 +7012,8 @@ def test_cloud_redis_grpc_transport_channel(): assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" assert transport._ssl_channel_credentials == None + assert transport._user_provided_channel is True + assert transport._default_host is None def test_cloud_redis_grpc_asyncio_transport_channel(): @@ -7025,6 +7027,8 @@ def test_cloud_redis_grpc_asyncio_transport_channel(): assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" assert transport._ssl_channel_credentials == None + assert transport._user_provided_channel is True + assert transport._default_host is None # Remove this test when deprecated arguments (api_mtls_endpoint, client_cert_source) are @@ -8611,6 +8615,7 @@ def test_cloud_redis_base_transport_client_options_object( ) assert transport._client_options is options assert transport._client_options.api_endpoint == initial_endpoint + assert transport._user_provided_channel is False assert transport._default_host == "default.example.com:443" @@ -8640,6 +8645,7 @@ def test_cloud_redis_base_transport_client_options_dict( ) assert transport._client_options is options assert transport._client_options.get("api_endpoint") == expected_endpoint + assert transport._user_provided_channel is False assert transport._default_host == "default.example.com:443" assert options == initial_options @@ -8648,7 +8654,7 @@ def test_cloud_redis_base_transport_default_host_ignored_when_custom_grpc_channe class _CustomChannelTransport(transports.CloudRedisTransport): def __init__(self, **kwargs): self._ignore_credentials = True - self._grpc_channel = mock.sentinel.custom_channel + self._user_provided_channel = True super().__init__(**kwargs) options = client_options.ClientOptions() @@ -8658,6 +8664,7 @@ def __init__(self, **kwargs): ) assert transport._client_options is options assert transport._client_options.api_endpoint is None + assert transport._user_provided_channel is True assert transport._default_host is None diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/base.py index 165a960b54dc..d93594895ac0 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/base.py @@ -104,6 +104,8 @@ def __init__( self._scopes = scopes if not hasattr(self, "_ignore_credentials"): self._ignore_credentials: bool = False + if not hasattr(self, "_user_provided_channel"): + self._user_provided_channel: bool = False # If no credentials are provided, then determine the appropriate # defaults. @@ -140,7 +142,7 @@ def __init__( # super().__init__, self._host is not used to create the channel and # cannot be trusted as the default span endpoint. self._default_host: Optional[str] = ( - None if getattr(self, "_grpc_channel", None) is not None else self._host + None if self._user_provided_channel else self._host ) self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/grpc.py index e9f25c269d79..6fed4c1dd2f9 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/grpc.py @@ -234,6 +234,7 @@ def __init__(self, *, self._ignore_credentials = True # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._user_provided_channel = True self._ssl_channel_credentials = None else: diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/grpc_asyncio.py index fbe193800b6a..dd650b16ad33 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/grpc_asyncio.py @@ -278,6 +278,7 @@ def __init__(self, *, self._ignore_credentials = True # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._user_provided_channel = True self._ssl_channel_credentials = None else: if api_mtls_endpoint: diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/base.py index 61439ef3c6f7..3c6d76262f2c 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/base.py @@ -104,6 +104,8 @@ def __init__( self._scopes = scopes if not hasattr(self, "_ignore_credentials"): self._ignore_credentials: bool = False + if not hasattr(self, "_user_provided_channel"): + self._user_provided_channel: bool = False # If no credentials are provided, then determine the appropriate # defaults. @@ -140,7 +142,7 @@ def __init__( # super().__init__, self._host is not used to create the channel and # cannot be trusted as the default span endpoint. self._default_host: Optional[str] = ( - None if getattr(self, "_grpc_channel", None) is not None else self._host + None if self._user_provided_channel else self._host ) self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/grpc.py index 038f440f28ac..1c44b011abfd 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/grpc.py @@ -226,6 +226,7 @@ def __init__(self, *, self._ignore_credentials = True # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._user_provided_channel = True self._ssl_channel_credentials = None else: diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/grpc_asyncio.py index bbc3afe07288..3d1e93a2a8cf 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/grpc_asyncio.py @@ -270,6 +270,7 @@ def __init__(self, *, self._ignore_credentials = True # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._user_provided_channel = True self._ssl_channel_credentials = None else: if api_mtls_endpoint: diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/base.py index 43130ac52102..4626ad60f627 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/base.py @@ -105,6 +105,8 @@ def __init__( self._scopes = scopes if not hasattr(self, "_ignore_credentials"): self._ignore_credentials: bool = False + if not hasattr(self, "_user_provided_channel"): + self._user_provided_channel: bool = False # If no credentials are provided, then determine the appropriate # defaults. @@ -141,7 +143,7 @@ def __init__( # super().__init__, self._host is not used to create the channel and # cannot be trusted as the default span endpoint. self._default_host: Optional[str] = ( - None if getattr(self, "_grpc_channel", None) is not None else self._host + None if self._user_provided_channel else self._host ) self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/grpc.py index 507ea21e57c7..9eabd7fd246d 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/grpc.py @@ -231,6 +231,7 @@ def __init__(self, *, self._ignore_credentials = True # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._user_provided_channel = True self._ssl_channel_credentials = None else: diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/grpc_asyncio.py index fa1acfb38691..db5dca52d7f1 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/grpc_asyncio.py @@ -275,6 +275,7 @@ def __init__(self, *, self._ignore_credentials = True # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._user_provided_channel = True self._ssl_channel_credentials = None else: if api_mtls_endpoint: diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/base.py index 9464d130f3e4..64bbaa97f3ea 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/base.py @@ -104,6 +104,8 @@ def __init__( self._scopes = scopes if not hasattr(self, "_ignore_credentials"): self._ignore_credentials: bool = False + if not hasattr(self, "_user_provided_channel"): + self._user_provided_channel: bool = False # If no credentials are provided, then determine the appropriate # defaults. @@ -141,7 +143,7 @@ def __init__( # super().__init__, self._host is not used to create the channel and # cannot be trusted as the default span endpoint. self._default_host: Optional[str] = ( - None if getattr(self, "_grpc_channel", None) is not None else self._host + None if self._user_provided_channel else self._host ) self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc.py index bb49692b3852..fd81936460f7 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc.py @@ -236,6 +236,7 @@ def __init__(self, *, self._ignore_credentials = True # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._user_provided_channel = True self._ssl_channel_credentials = None else: diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py index 14233a480f93..c49c8aaf5b1b 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py @@ -283,6 +283,7 @@ def __init__(self, *, self._ignore_credentials = True # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._user_provided_channel = True self._ssl_channel_credentials = None else: if api_mtls_endpoint: diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/base.py index 8ab1249004dc..c5bd5a1d904f 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/base.py @@ -105,6 +105,8 @@ def __init__( self._scopes = scopes if not hasattr(self, "_ignore_credentials"): self._ignore_credentials: bool = False + if not hasattr(self, "_user_provided_channel"): + self._user_provided_channel: bool = False # If no credentials are provided, then determine the appropriate # defaults. @@ -141,7 +143,7 @@ def __init__( # super().__init__, self._host is not used to create the channel and # cannot be trusted as the default span endpoint. self._default_host: Optional[str] = ( - None if getattr(self, "_grpc_channel", None) is not None else self._host + None if self._user_provided_channel else self._host ) self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/grpc.py index d0b4bfcdc950..38a445e24e6c 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/grpc.py @@ -229,6 +229,7 @@ def __init__(self, *, self._ignore_credentials = True # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._user_provided_channel = True self._ssl_channel_credentials = None else: diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/grpc_asyncio.py index 37cd7e01265b..a6eefa915155 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/grpc_asyncio.py @@ -273,6 +273,7 @@ def __init__(self, *, self._ignore_credentials = True # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._user_provided_channel = True self._ssl_channel_credentials = None else: if api_mtls_endpoint: diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/base.py index 95c8cc1b89e5..5029de28173b 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/base.py @@ -104,6 +104,8 @@ def __init__( self._scopes = scopes if not hasattr(self, "_ignore_credentials"): self._ignore_credentials: bool = False + if not hasattr(self, "_user_provided_channel"): + self._user_provided_channel: bool = False # If no credentials are provided, then determine the appropriate # defaults. @@ -140,7 +142,7 @@ def __init__( # super().__init__, self._host is not used to create the channel and # cannot be trusted as the default span endpoint. self._default_host: Optional[str] = ( - None if getattr(self, "_grpc_channel", None) is not None else self._host + None if self._user_provided_channel else self._host ) self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/grpc.py index 43ae9163f120..43b9ea5b7c3d 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/grpc.py @@ -232,6 +232,7 @@ def __init__(self, *, self._ignore_credentials = True # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._user_provided_channel = True self._ssl_channel_credentials = None else: diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/grpc_asyncio.py index cdc1d93787ed..0595983ce44c 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/grpc_asyncio.py @@ -276,6 +276,7 @@ def __init__(self, *, self._ignore_credentials = True # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._user_provided_channel = True self._ssl_channel_credentials = None else: if api_mtls_endpoint: diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_echo.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_echo.py index b751f85b5f75..11c8bc0ee587 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_echo.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_echo.py @@ -7890,6 +7890,8 @@ def test_echo_grpc_transport_channel(): assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" assert transport._ssl_channel_credentials == None + assert transport._user_provided_channel is True + assert transport._default_host is None def test_echo_grpc_asyncio_transport_channel(): @@ -7903,6 +7905,8 @@ def test_echo_grpc_asyncio_transport_channel(): assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" assert transport._ssl_channel_credentials == None + assert transport._user_provided_channel is True + assert transport._default_host is None # Remove this test when deprecated arguments (api_mtls_endpoint, client_cert_source) are @@ -9870,6 +9874,7 @@ def test_echo_base_transport_client_options_object( ) assert transport._client_options is options assert transport._client_options.api_endpoint == initial_endpoint + assert transport._user_provided_channel is False assert transport._default_host == "default.example.com:443" @@ -9899,6 +9904,7 @@ def test_echo_base_transport_client_options_dict( ) assert transport._client_options is options assert transport._client_options.get("api_endpoint") == expected_endpoint + assert transport._user_provided_channel is False assert transport._default_host == "default.example.com:443" assert options == initial_options @@ -9907,7 +9913,7 @@ def test_echo_base_transport_default_host_ignored_when_custom_grpc_channel(): class _CustomChannelTransport(transports.EchoTransport): def __init__(self, **kwargs): self._ignore_credentials = True - self._grpc_channel = mock.sentinel.custom_channel + self._user_provided_channel = True super().__init__(**kwargs) options = client_options.ClientOptions() @@ -9917,6 +9923,7 @@ def __init__(self, **kwargs): ) assert transport._client_options is options assert transport._client_options.api_endpoint is None + assert transport._user_provided_channel is True assert transport._default_host is None diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_identity.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_identity.py index 18b3b0ca0843..556c1228c6cb 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_identity.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_identity.py @@ -5014,6 +5014,8 @@ def test_identity_grpc_transport_channel(): assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" assert transport._ssl_channel_credentials == None + assert transport._user_provided_channel is True + assert transport._default_host is None def test_identity_grpc_asyncio_transport_channel(): @@ -5027,6 +5029,8 @@ def test_identity_grpc_asyncio_transport_channel(): assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" assert transport._ssl_channel_credentials == None + assert transport._user_provided_channel is True + assert transport._default_host is None # Remove this test when deprecated arguments (api_mtls_endpoint, client_cert_source) are @@ -6977,6 +6981,7 @@ def test_identity_base_transport_client_options_object( ) assert transport._client_options is options assert transport._client_options.api_endpoint == initial_endpoint + assert transport._user_provided_channel is False assert transport._default_host == "default.example.com:443" @@ -7006,6 +7011,7 @@ def test_identity_base_transport_client_options_dict( ) assert transport._client_options is options assert transport._client_options.get("api_endpoint") == expected_endpoint + assert transport._user_provided_channel is False assert transport._default_host == "default.example.com:443" assert options == initial_options @@ -7014,7 +7020,7 @@ def test_identity_base_transport_default_host_ignored_when_custom_grpc_channel() class _CustomChannelTransport(transports.IdentityTransport): def __init__(self, **kwargs): self._ignore_credentials = True - self._grpc_channel = mock.sentinel.custom_channel + self._user_provided_channel = True super().__init__(**kwargs) options = client_options.ClientOptions() @@ -7024,6 +7030,7 @@ def __init__(self, **kwargs): ) assert transport._client_options is options assert transport._client_options.api_endpoint is None + assert transport._user_provided_channel is True assert transport._default_host is None diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_messaging.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_messaging.py index b5a12bff24f6..0dc1fa615d4d 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_messaging.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_messaging.py @@ -9842,6 +9842,8 @@ def test_messaging_grpc_transport_channel(): assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" assert transport._ssl_channel_credentials == None + assert transport._user_provided_channel is True + assert transport._default_host is None def test_messaging_grpc_asyncio_transport_channel(): @@ -9855,6 +9857,8 @@ def test_messaging_grpc_asyncio_transport_channel(): assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" assert transport._ssl_channel_credentials == None + assert transport._user_provided_channel is True + assert transport._default_host is None # Remove this test when deprecated arguments (api_mtls_endpoint, client_cert_source) are @@ -11877,6 +11881,7 @@ def test_messaging_base_transport_client_options_object( ) assert transport._client_options is options assert transport._client_options.api_endpoint == initial_endpoint + assert transport._user_provided_channel is False assert transport._default_host == "default.example.com:443" @@ -11906,6 +11911,7 @@ def test_messaging_base_transport_client_options_dict( ) assert transport._client_options is options assert transport._client_options.get("api_endpoint") == expected_endpoint + assert transport._user_provided_channel is False assert transport._default_host == "default.example.com:443" assert options == initial_options @@ -11914,7 +11920,7 @@ def test_messaging_base_transport_default_host_ignored_when_custom_grpc_channel( class _CustomChannelTransport(transports.MessagingTransport): def __init__(self, **kwargs): self._ignore_credentials = True - self._grpc_channel = mock.sentinel.custom_channel + self._user_provided_channel = True super().__init__(**kwargs) options = client_options.ClientOptions() @@ -11924,6 +11930,7 @@ def __init__(self, **kwargs): ) assert transport._client_options is options assert transport._client_options.api_endpoint is None + assert transport._user_provided_channel is True assert transport._default_host is None diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py index 51b9ec02e33c..f354e167b850 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py @@ -2287,6 +2287,8 @@ def test_resumable_upload_service_grpc_transport_channel(): assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" assert transport._ssl_channel_credentials == None + assert transport._user_provided_channel is True + assert transport._default_host is None def test_resumable_upload_service_grpc_asyncio_transport_channel(): @@ -2300,6 +2302,8 @@ def test_resumable_upload_service_grpc_asyncio_transport_channel(): assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" assert transport._ssl_channel_credentials == None + assert transport._user_provided_channel is True + assert transport._default_host is None def test_resumable_upload_service_upload_media_grpc_channel_without_credentials_error(): @@ -4570,6 +4574,7 @@ def test_resumable_upload_service_base_transport_client_options_object( ) assert transport._client_options is options assert transport._client_options.api_endpoint == initial_endpoint + assert transport._user_provided_channel is False assert transport._default_host == "default.example.com:443" @@ -4599,6 +4604,7 @@ def test_resumable_upload_service_base_transport_client_options_dict( ) assert transport._client_options is options assert transport._client_options.get("api_endpoint") == expected_endpoint + assert transport._user_provided_channel is False assert transport._default_host == "default.example.com:443" assert options == initial_options @@ -4607,7 +4613,7 @@ def test_resumable_upload_service_base_transport_default_host_ignored_when_custo class _CustomChannelTransport(transports.ResumableUploadServiceTransport): def __init__(self, **kwargs): self._ignore_credentials = True - self._grpc_channel = mock.sentinel.custom_channel + self._user_provided_channel = True super().__init__(**kwargs) options = client_options.ClientOptions() @@ -4617,6 +4623,7 @@ def __init__(self, **kwargs): ) assert transport._client_options is options assert transport._client_options.api_endpoint is None + assert transport._user_provided_channel is True assert transport._default_host is None diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_sequence_service.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_sequence_service.py index 41181e6dd5a4..1b64c7e1e271 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_sequence_service.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_sequence_service.py @@ -5645,6 +5645,8 @@ def test_sequence_service_grpc_transport_channel(): assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" assert transport._ssl_channel_credentials == None + assert transport._user_provided_channel is True + assert transport._default_host is None def test_sequence_service_grpc_asyncio_transport_channel(): @@ -5658,6 +5660,8 @@ def test_sequence_service_grpc_asyncio_transport_channel(): assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" assert transport._ssl_channel_credentials == None + assert transport._user_provided_channel is True + assert transport._default_host is None # Remove this test when deprecated arguments (api_mtls_endpoint, client_cert_source) are @@ -7659,6 +7663,7 @@ def test_sequence_service_base_transport_client_options_object( ) assert transport._client_options is options assert transport._client_options.api_endpoint == initial_endpoint + assert transport._user_provided_channel is False assert transport._default_host == "default.example.com:443" @@ -7688,6 +7693,7 @@ def test_sequence_service_base_transport_client_options_dict( ) assert transport._client_options is options assert transport._client_options.get("api_endpoint") == expected_endpoint + assert transport._user_provided_channel is False assert transport._default_host == "default.example.com:443" assert options == initial_options @@ -7696,7 +7702,7 @@ def test_sequence_service_base_transport_default_host_ignored_when_custom_grpc_c class _CustomChannelTransport(transports.SequenceServiceTransport): def __init__(self, **kwargs): self._ignore_credentials = True - self._grpc_channel = mock.sentinel.custom_channel + self._user_provided_channel = True super().__init__(**kwargs) options = client_options.ClientOptions() @@ -7706,6 +7712,7 @@ def __init__(self, **kwargs): ) assert transport._client_options is options assert transport._client_options.api_endpoint is None + assert transport._user_provided_channel is True assert transport._default_host is None diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_testing.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_testing.py index 299c5d4eba41..e453f8c98db9 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_testing.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_testing.py @@ -5845,6 +5845,8 @@ def test_testing_grpc_transport_channel(): assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" assert transport._ssl_channel_credentials == None + assert transport._user_provided_channel is True + assert transport._default_host is None def test_testing_grpc_asyncio_transport_channel(): @@ -5858,6 +5860,8 @@ def test_testing_grpc_asyncio_transport_channel(): assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" assert transport._ssl_channel_credentials == None + assert transport._user_provided_channel is True + assert transport._default_host is None # Remove this test when deprecated arguments (api_mtls_endpoint, client_cert_source) are @@ -7848,6 +7852,7 @@ def test_testing_base_transport_client_options_object( ) assert transport._client_options is options assert transport._client_options.api_endpoint == initial_endpoint + assert transport._user_provided_channel is False assert transport._default_host == "default.example.com:443" @@ -7877,6 +7882,7 @@ def test_testing_base_transport_client_options_dict( ) assert transport._client_options is options assert transport._client_options.get("api_endpoint") == expected_endpoint + assert transport._user_provided_channel is False assert transport._default_host == "default.example.com:443" assert options == initial_options @@ -7885,7 +7891,7 @@ def test_testing_base_transport_default_host_ignored_when_custom_grpc_channel(): class _CustomChannelTransport(transports.TestingTransport): def __init__(self, **kwargs): self._ignore_credentials = True - self._grpc_channel = mock.sentinel.custom_channel + self._user_provided_channel = True super().__init__(**kwargs) options = client_options.ClientOptions() @@ -7895,6 +7901,7 @@ def __init__(self, **kwargs): ) assert transport._client_options is options assert transport._client_options.api_endpoint is None + assert transport._user_provided_channel is True assert transport._default_host is None diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/base.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/base.py index b75246bbf0ff..e6bd0fca7be7 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/base.py @@ -105,6 +105,8 @@ def __init__( self._scopes = scopes if not hasattr(self, "_ignore_credentials"): self._ignore_credentials: bool = False + if not hasattr(self, "_user_provided_channel"): + self._user_provided_channel: bool = False # If no credentials are provided, then determine the appropriate # defaults. @@ -141,7 +143,7 @@ def __init__( # super().__init__, self._host is not used to create the channel and # cannot be trusted as the default span endpoint. self._default_host: Optional[str] = ( - None if getattr(self, "_grpc_channel", None) is not None else self._host + None if self._user_provided_channel else self._host ) self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/grpc.py index 5f56b630fa1a..aa31ac481fe9 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/grpc.py @@ -231,6 +231,7 @@ def __init__(self, *, self._ignore_credentials = True # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._user_provided_channel = True self._ssl_channel_credentials = None else: diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/grpc_asyncio.py index 9abce3fe8581..d16cf16fd63b 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/grpc_asyncio.py @@ -275,6 +275,7 @@ def __init__(self, *, self._ignore_credentials = True # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._user_provided_channel = True self._ssl_channel_credentials = None else: if api_mtls_endpoint: diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py index b5d7a096f018..522152f1acbc 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py @@ -7083,6 +7083,8 @@ def test_storage_batch_operations_grpc_transport_channel(): assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" assert transport._ssl_channel_credentials == None + assert transport._user_provided_channel is True + assert transport._default_host is None def test_storage_batch_operations_grpc_asyncio_transport_channel(): @@ -7096,6 +7098,8 @@ def test_storage_batch_operations_grpc_asyncio_transport_channel(): assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" assert transport._ssl_channel_credentials == None + assert transport._user_provided_channel is True + assert transport._default_host is None # Remove this test when deprecated arguments (api_mtls_endpoint, client_cert_source) are @@ -8547,6 +8551,7 @@ def test_storage_batch_operations_base_transport_client_options_object( ) assert transport._client_options is options assert transport._client_options.api_endpoint == initial_endpoint + assert transport._user_provided_channel is False assert transport._default_host == "default.example.com:443" @@ -8576,6 +8581,7 @@ def test_storage_batch_operations_base_transport_client_options_dict( ) assert transport._client_options is options assert transport._client_options.get("api_endpoint") == expected_endpoint + assert transport._user_provided_channel is False assert transport._default_host == "default.example.com:443" assert options == initial_options @@ -8584,7 +8590,7 @@ def test_storage_batch_operations_base_transport_default_host_ignored_when_custo class _CustomChannelTransport(transports.StorageBatchOperationsTransport): def __init__(self, **kwargs): self._ignore_credentials = True - self._grpc_channel = mock.sentinel.custom_channel + self._user_provided_channel = True super().__init__(**kwargs) options = client_options.ClientOptions() @@ -8594,6 +8600,7 @@ def __init__(self, **kwargs): ) assert transport._client_options is options assert transport._client_options.api_endpoint is None + assert transport._user_provided_channel is True assert transport._default_host is None