From ad65d4c7b4eece3b5f197932536e18a3c9b9f26e Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Tue, 27 Oct 2026 11:12:41 -0400 Subject: [PATCH 1/4] 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 81e79e2a7916415daea174a9cfbda73bfc2b4502 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Fri, 30 Oct 2026 10:37:05 -0400 Subject: [PATCH 2/4] 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 8ac1e81836f8391c7db9912890f0f13d745d631f Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Fri, 30 Oct 2026 12:42:43 -0400 Subject: [PATCH 3/4] 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 10a0662f6d1dd3159429b438bc0a79fdf152813d Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Fri, 30 Oct 2026 17:55:10 -0400 Subject: [PATCH 4/4] refactor(observability): attach _host via internal _TransportClientOptions subclass --- .../services/%service/transports/base.py.j2 | 50 +++++++++++------ .../%name_%version/%sub/test_%service.py.j2 | 56 ++++++++++++------- .../services/asset_service/transports/base.py | 50 +++++++++++------ .../unit/gapic/asset_v1/test_asset_service.py | 56 ++++++++++++------- .../iam_credentials/transports/base.py | 50 +++++++++++------ .../credentials_v1/test_iam_credentials.py | 56 ++++++++++++------- .../services/eventarc/transports/base.py | 50 +++++++++++------ .../unit/gapic/eventarc_v1/test_eventarc.py | 56 ++++++++++++------- .../config_service_v2/transports/base.py | 50 +++++++++++------ .../logging_service_v2/transports/base.py | 50 +++++++++++------ .../metrics_service_v2/transports/base.py | 50 +++++++++++------ .../logging_v2/test_config_service_v2.py | 56 ++++++++++++------- .../logging_v2/test_logging_service_v2.py | 56 ++++++++++++------- .../logging_v2/test_metrics_service_v2.py | 56 ++++++++++++------- .../config_service_v2/transports/base.py | 50 +++++++++++------ .../logging_service_v2/transports/base.py | 50 +++++++++++------ .../metrics_service_v2/transports/base.py | 50 +++++++++++------ .../logging_v2/test_config_service_v2.py | 56 ++++++++++++------- .../logging_v2/test_logging_service_v2.py | 56 ++++++++++++------- .../logging_v2/test_metrics_service_v2.py | 56 ++++++++++++------- .../services/cloud_redis/transports/base.py | 50 +++++++++++------ .../unit/gapic/redis_v1/test_cloud_redis.py | 56 ++++++++++++------- .../services/cloud_redis/transports/base.py | 50 +++++++++++------ .../unit/gapic/redis_v1/test_cloud_redis.py | 56 ++++++++++++------- .../services/echo/transports/base.py | 50 +++++++++++------ .../services/identity/transports/base.py | 50 +++++++++++------ .../services/messaging/transports/base.py | 50 +++++++++++------ .../transports/base.py | 50 +++++++++++------ .../sequence_service/transports/base.py | 50 +++++++++++------ .../services/testing/transports/base.py | 50 +++++++++++------ .../unit/gapic/showcase_v1beta1/test_echo.py | 56 ++++++++++++------- .../gapic/showcase_v1beta1/test_identity.py | 56 ++++++++++++------- .../gapic/showcase_v1beta1/test_messaging.py | 56 ++++++++++++------- .../test_resumable_upload_service.py | 56 ++++++++++++------- .../showcase_v1beta1/test_sequence_service.py | 56 ++++++++++++------- .../gapic/showcase_v1beta1/test_testing.py | 56 ++++++++++++------- .../transports/base.py | 50 +++++++++++------ .../test_storage_batch_operations.py | 56 ++++++++++++------- .../tests/system/test_span_compliance.py | 18 ++++++ .../google/api_core/_observability.py | 6 +- .../tests/unit/test_observability.py | 52 +++++++++++++++++ 41 files changed, 1385 insertions(+), 705 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 9546b58ca1fb..3ce984d3dca7 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,7 +3,6 @@ {% 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 %} @@ -66,6 +65,22 @@ DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo(gapic_version=package_vers DEFAULT_CLIENT_INFO.protobuf_runtime_version = google.protobuf.__version__ +class _TransportClientOptions(client_options_lib.ClientOptions): + """Internal ClientOptions subclass that attaches the resolved transport host + as a separate ``_host`` attribute without mutating ``api_endpoint``.""" + + def __init__( + self, + base_options: Optional[client_options_lib.ClientOptions] = None, + *, + _host: Optional[str] = None, + ) -> None: + super().__init__() + if base_options is not None: + self.__dict__.update(base_options.__dict__) + self._host = _host + + class {{ service.name }}Transport(abc.ABC): """Abstract transport class for {{ service.name }}.""" @@ -164,24 +179,23 @@ 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) + if isinstance(client_options, dict): + client_options = client_options_lib.from_dict(client_options) + + # Attach the resolved transport host on an internal ClientOptions subclass + # so OpenTelemetry spans can fall back to _host without mutating api_endpoint. + # Skip when _ignore_credentials is True (e.g., when a pre-constructed gRPC + # channel is supplied and self._host does not reflect the channel target). + if not self._ignore_credentials and ( + client_options is not None + or ( + _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 + client_options = _TransportClientOptions( + client_options, _host=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 dd69c549fbac..a56da1e894b5 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 @@ -2501,14 +2501,14 @@ def test_{{ service.name|snake_case }}_base_transport_wrap_method_modern_api_cor @pytest.mark.parametrize( - "otel_enabled,expected_endpoint", + "otel_enabled,expected_host", [ 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 + monkeypatch, otel_enabled, expected_host ): fake_observability = mock.MagicMock() fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled @@ -2519,22 +2519,19 @@ def test_{{ service.name|snake_case }}_base_transport_client_options_none( credentials=ga_credentials.AnonymousCredentials(), client_options=None, ) - assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + assert getattr(transport._client_options, "api_endpoint", None) is None + assert getattr(transport._client_options, "_host", None) == expected_host @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 +2539,30 @@ 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 isinstance(transport._client_options, client_options.ClientOptions) + assert transport._client_options.api_endpoint == initial_endpoint + assert transport._client_options._host == "default.example.com:443" assert options.api_endpoint == initial_endpoint + assert not hasattr(options, "_host") @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 +2570,28 @@ 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 isinstance(transport._client_options, client_options.ClientOptions) + assert transport._client_options.api_endpoint == expected_endpoint + assert transport._client_options._host == "default.example.com:443" assert options == initial_options +def test_{{ service.name|snake_case }}_base_transport_client_options_ignored_when_ignore_credentials(): + class _CustomChannelTransport(transports.{{ service.name }}Transport): + def __init__(self, **kwargs): + self._ignore_credentials = True + 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 not hasattr(transport._client_options, "_host") + + 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 a40ac40c6553..1c983a7b6c9c 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,7 +14,6 @@ # 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 @@ -45,6 +44,22 @@ DEFAULT_CLIENT_INFO.protobuf_runtime_version = google.protobuf.__version__ +class _TransportClientOptions(client_options_lib.ClientOptions): + """Internal ClientOptions subclass that attaches the resolved transport host + as a separate ``_host`` attribute without mutating ``api_endpoint``.""" + + def __init__( + self, + base_options: Optional[client_options_lib.ClientOptions] = None, + *, + _host: Optional[str] = None, + ) -> None: + super().__init__() + if base_options is not None: + self.__dict__.update(base_options.__dict__) + self._host = _host + + class AssetServiceTransport(abc.ABC): """Abstract transport class for AssetService.""" @@ -135,24 +150,23 @@ 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) + if isinstance(client_options, dict): + client_options = client_options_lib.from_dict(client_options) + + # Attach the resolved transport host on an internal ClientOptions subclass + # so OpenTelemetry spans can fall back to _host without mutating api_endpoint. + # Skip when _ignore_credentials is True (e.g., when a pre-constructed gRPC + # channel is supplied and self._host does not reflect the channel target). + if not self._ignore_credentials and ( + client_options is not None + or ( + _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 + client_options = _TransportClientOptions( + client_options, _host=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 a31872a61b88..9d2c8e0ce996 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 @@ -18436,14 +18436,14 @@ def test_asset_service_base_transport_wrap_method_modern_api_core(monkeypatch): @pytest.mark.parametrize( - "otel_enabled,expected_endpoint", + "otel_enabled,expected_host", [ 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 + monkeypatch, otel_enabled, expected_host ): fake_observability = mock.MagicMock() fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled @@ -18454,22 +18454,19 @@ def test_asset_service_base_transport_client_options_none( credentials=ga_credentials.AnonymousCredentials(), client_options=None, ) - assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + assert getattr(transport._client_options, "api_endpoint", None) is None + assert getattr(transport._client_options, "_host", None) == expected_host @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 +18474,30 @@ 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 isinstance(transport._client_options, client_options.ClientOptions) + assert transport._client_options.api_endpoint == initial_endpoint + assert transport._client_options._host == "default.example.com:443" assert options.api_endpoint == initial_endpoint + assert not hasattr(options, "_host") @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 +18505,28 @@ def test_asset_service_base_transport_client_options_dict( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options == expected_options + assert isinstance(transport._client_options, client_options.ClientOptions) + assert transport._client_options.api_endpoint == expected_endpoint + assert transport._client_options._host == "default.example.com:443" assert options == initial_options +def test_asset_service_base_transport_client_options_ignored_when_ignore_credentials(): + class _CustomChannelTransport(transports.AssetServiceTransport): + def __init__(self, **kwargs): + self._ignore_credentials = True + 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 not hasattr(transport._client_options, "_host") + + 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 39d1b8bf9763..b01ed15fa856 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,7 +14,6 @@ # 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 @@ -42,6 +41,22 @@ DEFAULT_CLIENT_INFO.protobuf_runtime_version = google.protobuf.__version__ +class _TransportClientOptions(client_options_lib.ClientOptions): + """Internal ClientOptions subclass that attaches the resolved transport host + as a separate ``_host`` attribute without mutating ``api_endpoint``.""" + + def __init__( + self, + base_options: Optional[client_options_lib.ClientOptions] = None, + *, + _host: Optional[str] = None, + ) -> None: + super().__init__() + if base_options is not None: + self.__dict__.update(base_options.__dict__) + self._host = _host + + class IAMCredentialsTransport(abc.ABC): """Abstract transport class for IAMCredentials.""" @@ -132,24 +147,23 @@ 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) + if isinstance(client_options, dict): + client_options = client_options_lib.from_dict(client_options) + + # Attach the resolved transport host on an internal ClientOptions subclass + # so OpenTelemetry spans can fall back to _host without mutating api_endpoint. + # Skip when _ignore_credentials is True (e.g., when a pre-constructed gRPC + # channel is supplied and self._host does not reflect the channel target). + if not self._ignore_credentials and ( + client_options is not None + or ( + _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 + client_options = _TransportClientOptions( + client_options, _host=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 d3af824d005c..335044510b6f 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 @@ -4482,14 +4482,14 @@ def test_iam_credentials_base_transport_wrap_method_modern_api_core(monkeypatch) @pytest.mark.parametrize( - "otel_enabled,expected_endpoint", + "otel_enabled,expected_host", [ 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 + monkeypatch, otel_enabled, expected_host ): fake_observability = mock.MagicMock() fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled @@ -4500,22 +4500,19 @@ def test_iam_credentials_base_transport_client_options_none( credentials=ga_credentials.AnonymousCredentials(), client_options=None, ) - assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + assert getattr(transport._client_options, "api_endpoint", None) is None + assert getattr(transport._client_options, "_host", None) == expected_host @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 +4520,30 @@ 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 isinstance(transport._client_options, client_options.ClientOptions) + assert transport._client_options.api_endpoint == initial_endpoint + assert transport._client_options._host == "default.example.com:443" assert options.api_endpoint == initial_endpoint + assert not hasattr(options, "_host") @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 +4551,28 @@ def test_iam_credentials_base_transport_client_options_dict( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options == expected_options + assert isinstance(transport._client_options, client_options.ClientOptions) + assert transport._client_options.api_endpoint == expected_endpoint + assert transport._client_options._host == "default.example.com:443" assert options == initial_options +def test_iam_credentials_base_transport_client_options_ignored_when_ignore_credentials(): + class _CustomChannelTransport(transports.IAMCredentialsTransport): + def __init__(self, **kwargs): + self._ignore_credentials = True + 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 not hasattr(transport._client_options, "_host") + + 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 e4a5b3ecae76..47083faecb65 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,7 +14,6 @@ # 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 @@ -57,6 +56,22 @@ DEFAULT_CLIENT_INFO.protobuf_runtime_version = google.protobuf.__version__ +class _TransportClientOptions(client_options_lib.ClientOptions): + """Internal ClientOptions subclass that attaches the resolved transport host + as a separate ``_host`` attribute without mutating ``api_endpoint``.""" + + def __init__( + self, + base_options: Optional[client_options_lib.ClientOptions] = None, + *, + _host: Optional[str] = None, + ) -> None: + super().__init__() + if base_options is not None: + self.__dict__.update(base_options.__dict__) + self._host = _host + + class EventarcTransport(abc.ABC): """Abstract transport class for Eventarc.""" @@ -147,24 +162,23 @@ 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) + if isinstance(client_options, dict): + client_options = client_options_lib.from_dict(client_options) + + # Attach the resolved transport host on an internal ClientOptions subclass + # so OpenTelemetry spans can fall back to _host without mutating api_endpoint. + # Skip when _ignore_credentials is True (e.g., when a pre-constructed gRPC + # channel is supplied and self._host does not reflect the channel target). + if not self._ignore_credentials and ( + client_options is not None + or ( + _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 + client_options = _TransportClientOptions( + client_options, _host=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 556b3265ab3b..5813de209667 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 @@ -33539,14 +33539,14 @@ def test_eventarc_base_transport_wrap_method_modern_api_core(monkeypatch): @pytest.mark.parametrize( - "otel_enabled,expected_endpoint", + "otel_enabled,expected_host", [ 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 + monkeypatch, otel_enabled, expected_host ): fake_observability = mock.MagicMock() fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled @@ -33557,22 +33557,19 @@ def test_eventarc_base_transport_client_options_none( credentials=ga_credentials.AnonymousCredentials(), client_options=None, ) - assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + assert getattr(transport._client_options, "api_endpoint", None) is None + assert getattr(transport._client_options, "_host", None) == expected_host @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 +33577,30 @@ def test_eventarc_base_transport_client_options_object( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options.api_endpoint == expected_endpoint + assert isinstance(transport._client_options, client_options.ClientOptions) + assert transport._client_options.api_endpoint == initial_endpoint + assert transport._client_options._host == "default.example.com:443" assert options.api_endpoint == initial_endpoint + assert not hasattr(options, "_host") @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 +33608,28 @@ def test_eventarc_base_transport_client_options_dict( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options == expected_options + assert isinstance(transport._client_options, client_options.ClientOptions) + assert transport._client_options.api_endpoint == expected_endpoint + assert transport._client_options._host == "default.example.com:443" assert options == initial_options +def test_eventarc_base_transport_client_options_ignored_when_ignore_credentials(): + class _CustomChannelTransport(transports.EventarcTransport): + def __init__(self, **kwargs): + self._ignore_credentials = True + 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 not hasattr(transport._client_options, "_host") + + 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 cb1732a8c974..39e779508c30 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,7 +14,6 @@ # 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 @@ -45,6 +44,22 @@ DEFAULT_CLIENT_INFO.protobuf_runtime_version = google.protobuf.__version__ +class _TransportClientOptions(client_options_lib.ClientOptions): + """Internal ClientOptions subclass that attaches the resolved transport host + as a separate ``_host`` attribute without mutating ``api_endpoint``.""" + + def __init__( + self, + base_options: Optional[client_options_lib.ClientOptions] = None, + *, + _host: Optional[str] = None, + ) -> None: + super().__init__() + if base_options is not None: + self.__dict__.update(base_options.__dict__) + self._host = _host + + class ConfigServiceV2Transport(abc.ABC): """Abstract transport class for ConfigServiceV2.""" @@ -138,24 +153,23 @@ 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) + if isinstance(client_options, dict): + client_options = client_options_lib.from_dict(client_options) + + # Attach the resolved transport host on an internal ClientOptions subclass + # so OpenTelemetry spans can fall back to _host without mutating api_endpoint. + # Skip when _ignore_credentials is True (e.g., when a pre-constructed gRPC + # channel is supplied and self._host does not reflect the channel target). + if not self._ignore_credentials and ( + client_options is not None + or ( + _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 + client_options = _TransportClientOptions( + client_options, _host=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 c0de6b00c70b..34c0dab36412 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,7 +14,6 @@ # 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 @@ -44,6 +43,22 @@ DEFAULT_CLIENT_INFO.protobuf_runtime_version = google.protobuf.__version__ +class _TransportClientOptions(client_options_lib.ClientOptions): + """Internal ClientOptions subclass that attaches the resolved transport host + as a separate ``_host`` attribute without mutating ``api_endpoint``.""" + + def __init__( + self, + base_options: Optional[client_options_lib.ClientOptions] = None, + *, + _host: Optional[str] = None, + ) -> None: + super().__init__() + if base_options is not None: + self.__dict__.update(base_options.__dict__) + self._host = _host + + class LoggingServiceV2Transport(abc.ABC): """Abstract transport class for LoggingServiceV2.""" @@ -138,24 +153,23 @@ 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) + if isinstance(client_options, dict): + client_options = client_options_lib.from_dict(client_options) + + # Attach the resolved transport host on an internal ClientOptions subclass + # so OpenTelemetry spans can fall back to _host without mutating api_endpoint. + # Skip when _ignore_credentials is True (e.g., when a pre-constructed gRPC + # channel is supplied and self._host does not reflect the channel target). + if not self._ignore_credentials and ( + client_options is not None + or ( + _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 + client_options = _TransportClientOptions( + client_options, _host=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 630294a64d2e..75129f39de39 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,7 +14,6 @@ # 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 @@ -44,6 +43,22 @@ DEFAULT_CLIENT_INFO.protobuf_runtime_version = google.protobuf.__version__ +class _TransportClientOptions(client_options_lib.ClientOptions): + """Internal ClientOptions subclass that attaches the resolved transport host + as a separate ``_host`` attribute without mutating ``api_endpoint``.""" + + def __init__( + self, + base_options: Optional[client_options_lib.ClientOptions] = None, + *, + _host: Optional[str] = None, + ) -> None: + super().__init__() + if base_options is not None: + self.__dict__.update(base_options.__dict__) + self._host = _host + + class MetricsServiceV2Transport(abc.ABC): """Abstract transport class for MetricsServiceV2.""" @@ -138,24 +153,23 @@ 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) + if isinstance(client_options, dict): + client_options = client_options_lib.from_dict(client_options) + + # Attach the resolved transport host on an internal ClientOptions subclass + # so OpenTelemetry spans can fall back to _host without mutating api_endpoint. + # Skip when _ignore_credentials is True (e.g., when a pre-constructed gRPC + # channel is supplied and self._host does not reflect the channel target). + if not self._ignore_credentials and ( + client_options is not None + or ( + _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 + client_options = _TransportClientOptions( + client_options, _host=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 790030306491..33f3bb9ec412 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 @@ -14033,14 +14033,14 @@ def test_config_service_v2_base_transport_wrap_method_modern_api_core(monkeypatc @pytest.mark.parametrize( - "otel_enabled,expected_endpoint", + "otel_enabled,expected_host", [ 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 + monkeypatch, otel_enabled, expected_host ): fake_observability = mock.MagicMock() fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled @@ -14051,22 +14051,19 @@ def test_config_service_v2_base_transport_client_options_none( credentials=ga_credentials.AnonymousCredentials(), client_options=None, ) - assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + assert getattr(transport._client_options, "api_endpoint", None) is None + assert getattr(transport._client_options, "_host", None) == expected_host @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 +14071,30 @@ 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 isinstance(transport._client_options, client_options.ClientOptions) + assert transport._client_options.api_endpoint == initial_endpoint + assert transport._client_options._host == "default.example.com:443" assert options.api_endpoint == initial_endpoint + assert not hasattr(options, "_host") @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 +14102,28 @@ def test_config_service_v2_base_transport_client_options_dict( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options == expected_options + assert isinstance(transport._client_options, client_options.ClientOptions) + assert transport._client_options.api_endpoint == expected_endpoint + assert transport._client_options._host == "default.example.com:443" assert options == initial_options +def test_config_service_v2_base_transport_client_options_ignored_when_ignore_credentials(): + class _CustomChannelTransport(transports.ConfigServiceV2Transport): + def __init__(self, **kwargs): + self._ignore_credentials = True + 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 not hasattr(transport._client_options, "_host") + + 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 a123afd9d2c2..4e8db85de8cc 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 @@ -4473,14 +4473,14 @@ def test_logging_service_v2_base_transport_wrap_method_modern_api_core(monkeypat @pytest.mark.parametrize( - "otel_enabled,expected_endpoint", + "otel_enabled,expected_host", [ 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 + monkeypatch, otel_enabled, expected_host ): fake_observability = mock.MagicMock() fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled @@ -4491,22 +4491,19 @@ def test_logging_service_v2_base_transport_client_options_none( credentials=ga_credentials.AnonymousCredentials(), client_options=None, ) - assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + assert getattr(transport._client_options, "api_endpoint", None) is None + assert getattr(transport._client_options, "_host", None) == expected_host @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 +4511,30 @@ 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 isinstance(transport._client_options, client_options.ClientOptions) + assert transport._client_options.api_endpoint == initial_endpoint + assert transport._client_options._host == "default.example.com:443" assert options.api_endpoint == initial_endpoint + assert not hasattr(options, "_host") @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 +4542,28 @@ def test_logging_service_v2_base_transport_client_options_dict( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options == expected_options + assert isinstance(transport._client_options, client_options.ClientOptions) + assert transport._client_options.api_endpoint == expected_endpoint + assert transport._client_options._host == "default.example.com:443" assert options == initial_options +def test_logging_service_v2_base_transport_client_options_ignored_when_ignore_credentials(): + class _CustomChannelTransport(transports.LoggingServiceV2Transport): + def __init__(self, **kwargs): + self._ignore_credentials = True + 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 not hasattr(transport._client_options, "_host") + + 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 5eee17c5025f..e1f2519e74b9 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 @@ -4273,14 +4273,14 @@ def test_metrics_service_v2_base_transport_wrap_method_modern_api_core(monkeypat @pytest.mark.parametrize( - "otel_enabled,expected_endpoint", + "otel_enabled,expected_host", [ 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 + monkeypatch, otel_enabled, expected_host ): fake_observability = mock.MagicMock() fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled @@ -4291,22 +4291,19 @@ def test_metrics_service_v2_base_transport_client_options_none( credentials=ga_credentials.AnonymousCredentials(), client_options=None, ) - assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + assert getattr(transport._client_options, "api_endpoint", None) is None + assert getattr(transport._client_options, "_host", None) == expected_host @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 +4311,30 @@ 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 isinstance(transport._client_options, client_options.ClientOptions) + assert transport._client_options.api_endpoint == initial_endpoint + assert transport._client_options._host == "default.example.com:443" assert options.api_endpoint == initial_endpoint + assert not hasattr(options, "_host") @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 +4342,28 @@ def test_metrics_service_v2_base_transport_client_options_dict( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options == expected_options + assert isinstance(transport._client_options, client_options.ClientOptions) + assert transport._client_options.api_endpoint == expected_endpoint + assert transport._client_options._host == "default.example.com:443" assert options == initial_options +def test_metrics_service_v2_base_transport_client_options_ignored_when_ignore_credentials(): + class _CustomChannelTransport(transports.MetricsServiceV2Transport): + def __init__(self, **kwargs): + self._ignore_credentials = True + 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 not hasattr(transport._client_options, "_host") + + 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 cb1732a8c974..39e779508c30 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,7 +14,6 @@ # 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 @@ -45,6 +44,22 @@ DEFAULT_CLIENT_INFO.protobuf_runtime_version = google.protobuf.__version__ +class _TransportClientOptions(client_options_lib.ClientOptions): + """Internal ClientOptions subclass that attaches the resolved transport host + as a separate ``_host`` attribute without mutating ``api_endpoint``.""" + + def __init__( + self, + base_options: Optional[client_options_lib.ClientOptions] = None, + *, + _host: Optional[str] = None, + ) -> None: + super().__init__() + if base_options is not None: + self.__dict__.update(base_options.__dict__) + self._host = _host + + class ConfigServiceV2Transport(abc.ABC): """Abstract transport class for ConfigServiceV2.""" @@ -138,24 +153,23 @@ 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) + if isinstance(client_options, dict): + client_options = client_options_lib.from_dict(client_options) + + # Attach the resolved transport host on an internal ClientOptions subclass + # so OpenTelemetry spans can fall back to _host without mutating api_endpoint. + # Skip when _ignore_credentials is True (e.g., when a pre-constructed gRPC + # channel is supplied and self._host does not reflect the channel target). + if not self._ignore_credentials and ( + client_options is not None + or ( + _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 + client_options = _TransportClientOptions( + client_options, _host=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 c0de6b00c70b..34c0dab36412 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,7 +14,6 @@ # 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 @@ -44,6 +43,22 @@ DEFAULT_CLIENT_INFO.protobuf_runtime_version = google.protobuf.__version__ +class _TransportClientOptions(client_options_lib.ClientOptions): + """Internal ClientOptions subclass that attaches the resolved transport host + as a separate ``_host`` attribute without mutating ``api_endpoint``.""" + + def __init__( + self, + base_options: Optional[client_options_lib.ClientOptions] = None, + *, + _host: Optional[str] = None, + ) -> None: + super().__init__() + if base_options is not None: + self.__dict__.update(base_options.__dict__) + self._host = _host + + class LoggingServiceV2Transport(abc.ABC): """Abstract transport class for LoggingServiceV2.""" @@ -138,24 +153,23 @@ 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) + if isinstance(client_options, dict): + client_options = client_options_lib.from_dict(client_options) + + # Attach the resolved transport host on an internal ClientOptions subclass + # so OpenTelemetry spans can fall back to _host without mutating api_endpoint. + # Skip when _ignore_credentials is True (e.g., when a pre-constructed gRPC + # channel is supplied and self._host does not reflect the channel target). + if not self._ignore_credentials and ( + client_options is not None + or ( + _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 + client_options = _TransportClientOptions( + client_options, _host=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 630294a64d2e..75129f39de39 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,7 +14,6 @@ # 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 @@ -44,6 +43,22 @@ DEFAULT_CLIENT_INFO.protobuf_runtime_version = google.protobuf.__version__ +class _TransportClientOptions(client_options_lib.ClientOptions): + """Internal ClientOptions subclass that attaches the resolved transport host + as a separate ``_host`` attribute without mutating ``api_endpoint``.""" + + def __init__( + self, + base_options: Optional[client_options_lib.ClientOptions] = None, + *, + _host: Optional[str] = None, + ) -> None: + super().__init__() + if base_options is not None: + self.__dict__.update(base_options.__dict__) + self._host = _host + + class MetricsServiceV2Transport(abc.ABC): """Abstract transport class for MetricsServiceV2.""" @@ -138,24 +153,23 @@ 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) + if isinstance(client_options, dict): + client_options = client_options_lib.from_dict(client_options) + + # Attach the resolved transport host on an internal ClientOptions subclass + # so OpenTelemetry spans can fall back to _host without mutating api_endpoint. + # Skip when _ignore_credentials is True (e.g., when a pre-constructed gRPC + # channel is supplied and self._host does not reflect the channel target). + if not self._ignore_credentials and ( + client_options is not None + or ( + _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 + client_options = _TransportClientOptions( + client_options, _host=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 1bbf2dd15340..b3e9f0f93138 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 @@ -14033,14 +14033,14 @@ def test_config_service_v2_base_transport_wrap_method_modern_api_core(monkeypatc @pytest.mark.parametrize( - "otel_enabled,expected_endpoint", + "otel_enabled,expected_host", [ 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 + monkeypatch, otel_enabled, expected_host ): fake_observability = mock.MagicMock() fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled @@ -14051,22 +14051,19 @@ def test_config_service_v2_base_transport_client_options_none( credentials=ga_credentials.AnonymousCredentials(), client_options=None, ) - assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + assert getattr(transport._client_options, "api_endpoint", None) is None + assert getattr(transport._client_options, "_host", None) == expected_host @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 +14071,30 @@ 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 isinstance(transport._client_options, client_options.ClientOptions) + assert transport._client_options.api_endpoint == initial_endpoint + assert transport._client_options._host == "default.example.com:443" assert options.api_endpoint == initial_endpoint + assert not hasattr(options, "_host") @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 +14102,28 @@ def test_config_service_v2_base_transport_client_options_dict( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options == expected_options + assert isinstance(transport._client_options, client_options.ClientOptions) + assert transport._client_options.api_endpoint == expected_endpoint + assert transport._client_options._host == "default.example.com:443" assert options == initial_options +def test_config_service_v2_base_transport_client_options_ignored_when_ignore_credentials(): + class _CustomChannelTransport(transports.ConfigServiceV2Transport): + def __init__(self, **kwargs): + self._ignore_credentials = True + 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 not hasattr(transport._client_options, "_host") + + 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 a123afd9d2c2..4e8db85de8cc 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 @@ -4473,14 +4473,14 @@ def test_logging_service_v2_base_transport_wrap_method_modern_api_core(monkeypat @pytest.mark.parametrize( - "otel_enabled,expected_endpoint", + "otel_enabled,expected_host", [ 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 + monkeypatch, otel_enabled, expected_host ): fake_observability = mock.MagicMock() fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled @@ -4491,22 +4491,19 @@ def test_logging_service_v2_base_transport_client_options_none( credentials=ga_credentials.AnonymousCredentials(), client_options=None, ) - assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + assert getattr(transport._client_options, "api_endpoint", None) is None + assert getattr(transport._client_options, "_host", None) == expected_host @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 +4511,30 @@ 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 isinstance(transport._client_options, client_options.ClientOptions) + assert transport._client_options.api_endpoint == initial_endpoint + assert transport._client_options._host == "default.example.com:443" assert options.api_endpoint == initial_endpoint + assert not hasattr(options, "_host") @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 +4542,28 @@ def test_logging_service_v2_base_transport_client_options_dict( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options == expected_options + assert isinstance(transport._client_options, client_options.ClientOptions) + assert transport._client_options.api_endpoint == expected_endpoint + assert transport._client_options._host == "default.example.com:443" assert options == initial_options +def test_logging_service_v2_base_transport_client_options_ignored_when_ignore_credentials(): + class _CustomChannelTransport(transports.LoggingServiceV2Transport): + def __init__(self, **kwargs): + self._ignore_credentials = True + 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 not hasattr(transport._client_options, "_host") + + 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 5430c5a16ea9..ab376d187ebd 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 @@ -4273,14 +4273,14 @@ def test_metrics_service_v2_base_transport_wrap_method_modern_api_core(monkeypat @pytest.mark.parametrize( - "otel_enabled,expected_endpoint", + "otel_enabled,expected_host", [ 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 + monkeypatch, otel_enabled, expected_host ): fake_observability = mock.MagicMock() fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled @@ -4291,22 +4291,19 @@ def test_metrics_service_v2_base_transport_client_options_none( credentials=ga_credentials.AnonymousCredentials(), client_options=None, ) - assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + assert getattr(transport._client_options, "api_endpoint", None) is None + assert getattr(transport._client_options, "_host", None) == expected_host @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 +4311,30 @@ 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 isinstance(transport._client_options, client_options.ClientOptions) + assert transport._client_options.api_endpoint == initial_endpoint + assert transport._client_options._host == "default.example.com:443" assert options.api_endpoint == initial_endpoint + assert not hasattr(options, "_host") @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 +4342,28 @@ def test_metrics_service_v2_base_transport_client_options_dict( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options == expected_options + assert isinstance(transport._client_options, client_options.ClientOptions) + assert transport._client_options.api_endpoint == expected_endpoint + assert transport._client_options._host == "default.example.com:443" assert options == initial_options +def test_metrics_service_v2_base_transport_client_options_ignored_when_ignore_credentials(): + class _CustomChannelTransport(transports.MetricsServiceV2Transport): + def __init__(self, **kwargs): + self._ignore_credentials = True + 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 not hasattr(transport._client_options, "_host") + + 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 6630c8470486..3da74b7acd7b 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,7 +14,6 @@ # 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 @@ -45,6 +44,22 @@ DEFAULT_CLIENT_INFO.protobuf_runtime_version = google.protobuf.__version__ +class _TransportClientOptions(client_options_lib.ClientOptions): + """Internal ClientOptions subclass that attaches the resolved transport host + as a separate ``_host`` attribute without mutating ``api_endpoint``.""" + + def __init__( + self, + base_options: Optional[client_options_lib.ClientOptions] = None, + *, + _host: Optional[str] = None, + ) -> None: + super().__init__() + if base_options is not None: + self.__dict__.update(base_options.__dict__) + self._host = _host + + class CloudRedisTransport(abc.ABC): """Abstract transport class for CloudRedis.""" @@ -135,24 +150,23 @@ 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) + if isinstance(client_options, dict): + client_options = client_options_lib.from_dict(client_options) + + # Attach the resolved transport host on an internal ClientOptions subclass + # so OpenTelemetry spans can fall back to _host without mutating api_endpoint. + # Skip when _ignore_credentials is True (e.g., when a pre-constructed gRPC + # channel is supplied and self._host does not reflect the channel target). + if not self._ignore_credentials and ( + client_options is not None + or ( + _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 + client_options = _TransportClientOptions( + client_options, _host=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 eaf5fc2b0a60..876aeac3b5fc 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 @@ -13327,14 +13327,14 @@ def test_cloud_redis_base_transport_wrap_method_modern_api_core(monkeypatch): @pytest.mark.parametrize( - "otel_enabled,expected_endpoint", + "otel_enabled,expected_host", [ 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 + monkeypatch, otel_enabled, expected_host ): fake_observability = mock.MagicMock() fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled @@ -13345,22 +13345,19 @@ def test_cloud_redis_base_transport_client_options_none( credentials=ga_credentials.AnonymousCredentials(), client_options=None, ) - assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + assert getattr(transport._client_options, "api_endpoint", None) is None + assert getattr(transport._client_options, "_host", None) == expected_host @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 +13365,30 @@ 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 isinstance(transport._client_options, client_options.ClientOptions) + assert transport._client_options.api_endpoint == initial_endpoint + assert transport._client_options._host == "default.example.com:443" assert options.api_endpoint == initial_endpoint + assert not hasattr(options, "_host") @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 +13396,28 @@ def test_cloud_redis_base_transport_client_options_dict( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options == expected_options + assert isinstance(transport._client_options, client_options.ClientOptions) + assert transport._client_options.api_endpoint == expected_endpoint + assert transport._client_options._host == "default.example.com:443" assert options == initial_options +def test_cloud_redis_base_transport_client_options_ignored_when_ignore_credentials(): + class _CustomChannelTransport(transports.CloudRedisTransport): + def __init__(self, **kwargs): + self._ignore_credentials = True + 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 not hasattr(transport._client_options, "_host") + + 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 2f0e15493d99..36189e3ab34c 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,7 +14,6 @@ # 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 @@ -45,6 +44,22 @@ DEFAULT_CLIENT_INFO.protobuf_runtime_version = google.protobuf.__version__ +class _TransportClientOptions(client_options_lib.ClientOptions): + """Internal ClientOptions subclass that attaches the resolved transport host + as a separate ``_host`` attribute without mutating ``api_endpoint``.""" + + def __init__( + self, + base_options: Optional[client_options_lib.ClientOptions] = None, + *, + _host: Optional[str] = None, + ) -> None: + super().__init__() + if base_options is not None: + self.__dict__.update(base_options.__dict__) + self._host = _host + + class CloudRedisTransport(abc.ABC): """Abstract transport class for CloudRedis.""" @@ -135,24 +150,23 @@ 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) + if isinstance(client_options, dict): + client_options = client_options_lib.from_dict(client_options) + + # Attach the resolved transport host on an internal ClientOptions subclass + # so OpenTelemetry spans can fall back to _host without mutating api_endpoint. + # Skip when _ignore_credentials is True (e.g., when a pre-constructed gRPC + # channel is supplied and self._host does not reflect the channel target). + if not self._ignore_credentials and ( + client_options is not None + or ( + _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 + client_options = _TransportClientOptions( + client_options, _host=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 ad69829602f9..6bfd827cf357 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 @@ -8547,14 +8547,14 @@ def test_cloud_redis_base_transport_wrap_method_modern_api_core(monkeypatch): @pytest.mark.parametrize( - "otel_enabled,expected_endpoint", + "otel_enabled,expected_host", [ 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 + monkeypatch, otel_enabled, expected_host ): fake_observability = mock.MagicMock() fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled @@ -8565,22 +8565,19 @@ def test_cloud_redis_base_transport_client_options_none( credentials=ga_credentials.AnonymousCredentials(), client_options=None, ) - assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + assert getattr(transport._client_options, "api_endpoint", None) is None + assert getattr(transport._client_options, "_host", None) == expected_host @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 +8585,30 @@ 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 isinstance(transport._client_options, client_options.ClientOptions) + assert transport._client_options.api_endpoint == initial_endpoint + assert transport._client_options._host == "default.example.com:443" assert options.api_endpoint == initial_endpoint + assert not hasattr(options, "_host") @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 +8616,28 @@ def test_cloud_redis_base_transport_client_options_dict( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options == expected_options + assert isinstance(transport._client_options, client_options.ClientOptions) + assert transport._client_options.api_endpoint == expected_endpoint + assert transport._client_options._host == "default.example.com:443" assert options == initial_options +def test_cloud_redis_base_transport_client_options_ignored_when_ignore_credentials(): + class _CustomChannelTransport(transports.CloudRedisTransport): + def __init__(self, **kwargs): + self._ignore_credentials = True + 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 not hasattr(transport._client_options, "_host") + + 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 7982b81e3f98..b20a002caac8 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,7 +14,6 @@ # 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 @@ -47,6 +46,22 @@ DEFAULT_CLIENT_INFO.protobuf_runtime_version = google.protobuf.__version__ +class _TransportClientOptions(client_options_lib.ClientOptions): + """Internal ClientOptions subclass that attaches the resolved transport host + as a separate ``_host`` attribute without mutating ``api_endpoint``.""" + + def __init__( + self, + base_options: Optional[client_options_lib.ClientOptions] = None, + *, + _host: Optional[str] = None, + ) -> None: + super().__init__() + if base_options is not None: + self.__dict__.update(base_options.__dict__) + self._host = _host + + class EchoTransport(abc.ABC): """Abstract transport class for Echo.""" @@ -136,24 +151,23 @@ 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) + if isinstance(client_options, dict): + client_options = client_options_lib.from_dict(client_options) + + # Attach the resolved transport host on an internal ClientOptions subclass + # so OpenTelemetry spans can fall back to _host without mutating api_endpoint. + # Skip when _ignore_credentials is True (e.g., when a pre-constructed gRPC + # channel is supplied and self._host does not reflect the channel target). + if not self._ignore_credentials and ( + client_options is not None + or ( + _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 + client_options = _TransportClientOptions( + client_options, _host=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 ee98130046f1..8c43821e3efe 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,7 +14,6 @@ # 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 @@ -47,6 +46,22 @@ DEFAULT_CLIENT_INFO.protobuf_runtime_version = google.protobuf.__version__ +class _TransportClientOptions(client_options_lib.ClientOptions): + """Internal ClientOptions subclass that attaches the resolved transport host + as a separate ``_host`` attribute without mutating ``api_endpoint``.""" + + def __init__( + self, + base_options: Optional[client_options_lib.ClientOptions] = None, + *, + _host: Optional[str] = None, + ) -> None: + super().__init__() + if base_options is not None: + self.__dict__.update(base_options.__dict__) + self._host = _host + + class IdentityTransport(abc.ABC): """Abstract transport class for Identity.""" @@ -136,24 +151,23 @@ 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) + if isinstance(client_options, dict): + client_options = client_options_lib.from_dict(client_options) + + # Attach the resolved transport host on an internal ClientOptions subclass + # so OpenTelemetry spans can fall back to _host without mutating api_endpoint. + # Skip when _ignore_credentials is True (e.g., when a pre-constructed gRPC + # channel is supplied and self._host does not reflect the channel target). + if not self._ignore_credentials and ( + client_options is not None + or ( + _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 + client_options = _TransportClientOptions( + client_options, _host=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 b1c947a35f80..6eed3807b57d 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,7 +14,6 @@ # 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 @@ -48,6 +47,22 @@ DEFAULT_CLIENT_INFO.protobuf_runtime_version = google.protobuf.__version__ +class _TransportClientOptions(client_options_lib.ClientOptions): + """Internal ClientOptions subclass that attaches the resolved transport host + as a separate ``_host`` attribute without mutating ``api_endpoint``.""" + + def __init__( + self, + base_options: Optional[client_options_lib.ClientOptions] = None, + *, + _host: Optional[str] = None, + ) -> None: + super().__init__() + if base_options is not None: + self.__dict__.update(base_options.__dict__) + self._host = _host + + class MessagingTransport(abc.ABC): """Abstract transport class for Messaging.""" @@ -137,24 +152,23 @@ 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) + if isinstance(client_options, dict): + client_options = client_options_lib.from_dict(client_options) + + # Attach the resolved transport host on an internal ClientOptions subclass + # so OpenTelemetry spans can fall back to _host without mutating api_endpoint. + # Skip when _ignore_credentials is True (e.g., when a pre-constructed gRPC + # channel is supplied and self._host does not reflect the channel target). + if not self._ignore_credentials and ( + client_options is not None + or ( + _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 + client_options = _TransportClientOptions( + client_options, _host=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 22b2609ecfcf..0afaf052f740 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,7 +14,6 @@ # 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 @@ -47,6 +46,22 @@ DEFAULT_CLIENT_INFO.protobuf_runtime_version = google.protobuf.__version__ +class _TransportClientOptions(client_options_lib.ClientOptions): + """Internal ClientOptions subclass that attaches the resolved transport host + as a separate ``_host`` attribute without mutating ``api_endpoint``.""" + + def __init__( + self, + base_options: Optional[client_options_lib.ClientOptions] = None, + *, + _host: Optional[str] = None, + ) -> None: + super().__init__() + if base_options is not None: + self.__dict__.update(base_options.__dict__) + self._host = _host + + class ResumableUploadServiceTransport(abc.ABC): """Abstract transport class for ResumableUploadService.""" @@ -137,24 +152,23 @@ 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) + if isinstance(client_options, dict): + client_options = client_options_lib.from_dict(client_options) + + # Attach the resolved transport host on an internal ClientOptions subclass + # so OpenTelemetry spans can fall back to _host without mutating api_endpoint. + # Skip when _ignore_credentials is True (e.g., when a pre-constructed gRPC + # channel is supplied and self._host does not reflect the channel target). + if not self._ignore_credentials and ( + client_options is not None + or ( + _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 + client_options = _TransportClientOptions( + client_options, _host=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 22d317b32796..fff8f7a70e06 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,7 +14,6 @@ # 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 @@ -48,6 +47,22 @@ DEFAULT_CLIENT_INFO.protobuf_runtime_version = google.protobuf.__version__ +class _TransportClientOptions(client_options_lib.ClientOptions): + """Internal ClientOptions subclass that attaches the resolved transport host + as a separate ``_host`` attribute without mutating ``api_endpoint``.""" + + def __init__( + self, + base_options: Optional[client_options_lib.ClientOptions] = None, + *, + _host: Optional[str] = None, + ) -> None: + super().__init__() + if base_options is not None: + self.__dict__.update(base_options.__dict__) + self._host = _host + + class SequenceServiceTransport(abc.ABC): """Abstract transport class for SequenceService.""" @@ -137,24 +152,23 @@ 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) + if isinstance(client_options, dict): + client_options = client_options_lib.from_dict(client_options) + + # Attach the resolved transport host on an internal ClientOptions subclass + # so OpenTelemetry spans can fall back to _host without mutating api_endpoint. + # Skip when _ignore_credentials is True (e.g., when a pre-constructed gRPC + # channel is supplied and self._host does not reflect the channel target). + if not self._ignore_credentials and ( + client_options is not None + or ( + _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 + client_options = _TransportClientOptions( + client_options, _host=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 e6431cf277c5..fd44ffb5a593 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,7 +14,6 @@ # 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 @@ -47,6 +46,22 @@ DEFAULT_CLIENT_INFO.protobuf_runtime_version = google.protobuf.__version__ +class _TransportClientOptions(client_options_lib.ClientOptions): + """Internal ClientOptions subclass that attaches the resolved transport host + as a separate ``_host`` attribute without mutating ``api_endpoint``.""" + + def __init__( + self, + base_options: Optional[client_options_lib.ClientOptions] = None, + *, + _host: Optional[str] = None, + ) -> None: + super().__init__() + if base_options is not None: + self.__dict__.update(base_options.__dict__) + self._host = _host + + class TestingTransport(abc.ABC): """Abstract transport class for Testing.""" @@ -136,24 +151,23 @@ 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) + if isinstance(client_options, dict): + client_options = client_options_lib.from_dict(client_options) + + # Attach the resolved transport host on an internal ClientOptions subclass + # so OpenTelemetry spans can fall back to _host without mutating api_endpoint. + # Skip when _ignore_credentials is True (e.g., when a pre-constructed gRPC + # channel is supplied and self._host does not reflect the channel target). + if not self._ignore_credentials and ( + client_options is not None + or ( + _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 + client_options = _TransportClientOptions( + client_options, _host=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 d28b4688187d..183be032c2eb 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 @@ -9806,14 +9806,14 @@ def test_echo_base_transport_wrap_method_modern_api_core(monkeypatch): @pytest.mark.parametrize( - "otel_enabled,expected_endpoint", + "otel_enabled,expected_host", [ 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 + monkeypatch, otel_enabled, expected_host ): fake_observability = mock.MagicMock() fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled @@ -9824,22 +9824,19 @@ def test_echo_base_transport_client_options_none( credentials=ga_credentials.AnonymousCredentials(), client_options=None, ) - assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + assert getattr(transport._client_options, "api_endpoint", None) is None + assert getattr(transport._client_options, "_host", None) == expected_host @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 +9844,30 @@ def test_echo_base_transport_client_options_object( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options.api_endpoint == expected_endpoint + assert isinstance(transport._client_options, client_options.ClientOptions) + assert transport._client_options.api_endpoint == initial_endpoint + assert transport._client_options._host == "default.example.com:443" assert options.api_endpoint == initial_endpoint + assert not hasattr(options, "_host") @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 +9875,28 @@ def test_echo_base_transport_client_options_dict( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options == expected_options + assert isinstance(transport._client_options, client_options.ClientOptions) + assert transport._client_options.api_endpoint == expected_endpoint + assert transport._client_options._host == "default.example.com:443" assert options == initial_options +def test_echo_base_transport_client_options_ignored_when_ignore_credentials(): + class _CustomChannelTransport(transports.EchoTransport): + def __init__(self, **kwargs): + self._ignore_credentials = True + 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 not hasattr(transport._client_options, "_host") + + 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 bbe163a03bb1..1c84b8be9ede 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 @@ -6913,14 +6913,14 @@ def test_identity_base_transport_wrap_method_modern_api_core(monkeypatch): @pytest.mark.parametrize( - "otel_enabled,expected_endpoint", + "otel_enabled,expected_host", [ 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 + monkeypatch, otel_enabled, expected_host ): fake_observability = mock.MagicMock() fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled @@ -6931,22 +6931,19 @@ def test_identity_base_transport_client_options_none( credentials=ga_credentials.AnonymousCredentials(), client_options=None, ) - assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + assert getattr(transport._client_options, "api_endpoint", None) is None + assert getattr(transport._client_options, "_host", None) == expected_host @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 +6951,30 @@ def test_identity_base_transport_client_options_object( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options.api_endpoint == expected_endpoint + assert isinstance(transport._client_options, client_options.ClientOptions) + assert transport._client_options.api_endpoint == initial_endpoint + assert transport._client_options._host == "default.example.com:443" assert options.api_endpoint == initial_endpoint + assert not hasattr(options, "_host") @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 +6982,28 @@ def test_identity_base_transport_client_options_dict( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options == expected_options + assert isinstance(transport._client_options, client_options.ClientOptions) + assert transport._client_options.api_endpoint == expected_endpoint + assert transport._client_options._host == "default.example.com:443" assert options == initial_options +def test_identity_base_transport_client_options_ignored_when_ignore_credentials(): + class _CustomChannelTransport(transports.IdentityTransport): + def __init__(self, **kwargs): + self._ignore_credentials = True + 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 not hasattr(transport._client_options, "_host") + + 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 fbbbcac1f4a4..319ff39361ef 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 @@ -11813,14 +11813,14 @@ def test_messaging_base_transport_wrap_method_modern_api_core(monkeypatch): @pytest.mark.parametrize( - "otel_enabled,expected_endpoint", + "otel_enabled,expected_host", [ 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 + monkeypatch, otel_enabled, expected_host ): fake_observability = mock.MagicMock() fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled @@ -11831,22 +11831,19 @@ def test_messaging_base_transport_client_options_none( credentials=ga_credentials.AnonymousCredentials(), client_options=None, ) - assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + assert getattr(transport._client_options, "api_endpoint", None) is None + assert getattr(transport._client_options, "_host", None) == expected_host @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 +11851,30 @@ def test_messaging_base_transport_client_options_object( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options.api_endpoint == expected_endpoint + assert isinstance(transport._client_options, client_options.ClientOptions) + assert transport._client_options.api_endpoint == initial_endpoint + assert transport._client_options._host == "default.example.com:443" assert options.api_endpoint == initial_endpoint + assert not hasattr(options, "_host") @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 +11882,28 @@ def test_messaging_base_transport_client_options_dict( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options == expected_options + assert isinstance(transport._client_options, client_options.ClientOptions) + assert transport._client_options.api_endpoint == expected_endpoint + assert transport._client_options._host == "default.example.com:443" assert options == initial_options +def test_messaging_base_transport_client_options_ignored_when_ignore_credentials(): + class _CustomChannelTransport(transports.MessagingTransport): + def __init__(self, **kwargs): + self._ignore_credentials = True + 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 not hasattr(transport._client_options, "_host") + + 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 8e17b5ccb41e..97fb6781b76b 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 @@ -4506,14 +4506,14 @@ def test_resumable_upload_service_base_transport_wrap_method_modern_api_core(mon @pytest.mark.parametrize( - "otel_enabled,expected_endpoint", + "otel_enabled,expected_host", [ 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 + monkeypatch, otel_enabled, expected_host ): fake_observability = mock.MagicMock() fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled @@ -4524,22 +4524,19 @@ def test_resumable_upload_service_base_transport_client_options_none( credentials=ga_credentials.AnonymousCredentials(), client_options=None, ) - assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + assert getattr(transport._client_options, "api_endpoint", None) is None + assert getattr(transport._client_options, "_host", None) == expected_host @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 +4544,30 @@ 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 isinstance(transport._client_options, client_options.ClientOptions) + assert transport._client_options.api_endpoint == initial_endpoint + assert transport._client_options._host == "default.example.com:443" assert options.api_endpoint == initial_endpoint + assert not hasattr(options, "_host") @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 +4575,28 @@ def test_resumable_upload_service_base_transport_client_options_dict( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options == expected_options + assert isinstance(transport._client_options, client_options.ClientOptions) + assert transport._client_options.api_endpoint == expected_endpoint + assert transport._client_options._host == "default.example.com:443" assert options == initial_options +def test_resumable_upload_service_base_transport_client_options_ignored_when_ignore_credentials(): + class _CustomChannelTransport(transports.ResumableUploadServiceTransport): + def __init__(self, **kwargs): + self._ignore_credentials = True + 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 not hasattr(transport._client_options, "_host") + + 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 79303aefe964..1371e23c9217 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 @@ -7595,14 +7595,14 @@ def test_sequence_service_base_transport_wrap_method_modern_api_core(monkeypatch @pytest.mark.parametrize( - "otel_enabled,expected_endpoint", + "otel_enabled,expected_host", [ 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 + monkeypatch, otel_enabled, expected_host ): fake_observability = mock.MagicMock() fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled @@ -7613,22 +7613,19 @@ def test_sequence_service_base_transport_client_options_none( credentials=ga_credentials.AnonymousCredentials(), client_options=None, ) - assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + assert getattr(transport._client_options, "api_endpoint", None) is None + assert getattr(transport._client_options, "_host", None) == expected_host @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 +7633,30 @@ 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 isinstance(transport._client_options, client_options.ClientOptions) + assert transport._client_options.api_endpoint == initial_endpoint + assert transport._client_options._host == "default.example.com:443" assert options.api_endpoint == initial_endpoint + assert not hasattr(options, "_host") @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 +7664,28 @@ def test_sequence_service_base_transport_client_options_dict( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options == expected_options + assert isinstance(transport._client_options, client_options.ClientOptions) + assert transport._client_options.api_endpoint == expected_endpoint + assert transport._client_options._host == "default.example.com:443" assert options == initial_options +def test_sequence_service_base_transport_client_options_ignored_when_ignore_credentials(): + class _CustomChannelTransport(transports.SequenceServiceTransport): + def __init__(self, **kwargs): + self._ignore_credentials = True + 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 not hasattr(transport._client_options, "_host") + + 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 7bc47a2be1b5..209e9a760a7e 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 @@ -7784,14 +7784,14 @@ def test_testing_base_transport_wrap_method_modern_api_core(monkeypatch): @pytest.mark.parametrize( - "otel_enabled,expected_endpoint", + "otel_enabled,expected_host", [ 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 + monkeypatch, otel_enabled, expected_host ): fake_observability = mock.MagicMock() fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled @@ -7802,22 +7802,19 @@ def test_testing_base_transport_client_options_none( credentials=ga_credentials.AnonymousCredentials(), client_options=None, ) - assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + assert getattr(transport._client_options, "api_endpoint", None) is None + assert getattr(transport._client_options, "_host", None) == expected_host @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 +7822,30 @@ def test_testing_base_transport_client_options_object( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options.api_endpoint == expected_endpoint + assert isinstance(transport._client_options, client_options.ClientOptions) + assert transport._client_options.api_endpoint == initial_endpoint + assert transport._client_options._host == "default.example.com:443" assert options.api_endpoint == initial_endpoint + assert not hasattr(options, "_host") @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 +7853,28 @@ def test_testing_base_transport_client_options_dict( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options == expected_options + assert isinstance(transport._client_options, client_options.ClientOptions) + assert transport._client_options.api_endpoint == expected_endpoint + assert transport._client_options._host == "default.example.com:443" assert options == initial_options +def test_testing_base_transport_client_options_ignored_when_ignore_credentials(): + class _CustomChannelTransport(transports.TestingTransport): + def __init__(self, **kwargs): + self._ignore_credentials = True + 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 not hasattr(transport._client_options, "_host") + + 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 eb72570fd60e..7410f62d990a 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,7 +14,6 @@ # 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 @@ -47,6 +46,22 @@ DEFAULT_CLIENT_INFO.protobuf_runtime_version = google.protobuf.__version__ +class _TransportClientOptions(client_options_lib.ClientOptions): + """Internal ClientOptions subclass that attaches the resolved transport host + as a separate ``_host`` attribute without mutating ``api_endpoint``.""" + + def __init__( + self, + base_options: Optional[client_options_lib.ClientOptions] = None, + *, + _host: Optional[str] = None, + ) -> None: + super().__init__() + if base_options is not None: + self.__dict__.update(base_options.__dict__) + self._host = _host + + class StorageBatchOperationsTransport(abc.ABC): """Abstract transport class for StorageBatchOperations.""" @@ -137,24 +152,23 @@ 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) + if isinstance(client_options, dict): + client_options = client_options_lib.from_dict(client_options) + + # Attach the resolved transport host on an internal ClientOptions subclass + # so OpenTelemetry spans can fall back to _host without mutating api_endpoint. + # Skip when _ignore_credentials is True (e.g., when a pre-constructed gRPC + # channel is supplied and self._host does not reflect the channel target). + if not self._ignore_credentials and ( + client_options is not None + or ( + _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 + client_options = _TransportClientOptions( + client_options, _host=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 bf810d75835e..ccd43107ddfc 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 @@ -8483,14 +8483,14 @@ def test_storage_batch_operations_base_transport_wrap_method_modern_api_core(mon @pytest.mark.parametrize( - "otel_enabled,expected_endpoint", + "otel_enabled,expected_host", [ 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 + monkeypatch, otel_enabled, expected_host ): fake_observability = mock.MagicMock() fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled @@ -8501,22 +8501,19 @@ def test_storage_batch_operations_base_transport_client_options_none( credentials=ga_credentials.AnonymousCredentials(), client_options=None, ) - assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + assert getattr(transport._client_options, "api_endpoint", None) is None + assert getattr(transport._client_options, "_host", None) == expected_host @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 +8521,30 @@ 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 isinstance(transport._client_options, client_options.ClientOptions) + assert transport._client_options.api_endpoint == initial_endpoint + assert transport._client_options._host == "default.example.com:443" assert options.api_endpoint == initial_endpoint + assert not hasattr(options, "_host") @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 +8552,28 @@ def test_storage_batch_operations_base_transport_client_options_dict( credentials=ga_credentials.AnonymousCredentials(), client_options=options, ) - assert transport._client_options == expected_options + assert isinstance(transport._client_options, client_options.ClientOptions) + assert transport._client_options.api_endpoint == expected_endpoint + assert transport._client_options._host == "default.example.com:443" assert options == initial_options +def test_storage_batch_operations_base_transport_client_options_ignored_when_ignore_credentials(): + class _CustomChannelTransport(transports.StorageBatchOperationsTransport): + def __init__(self, **kwargs): + self._ignore_credentials = True + 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 not hasattr(transport._client_options, "_host") + + 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"): diff --git a/packages/gapic-generator/tests/system/test_span_compliance.py b/packages/gapic-generator/tests/system/test_span_compliance.py index cc1a87a66a5a..18443ebc86c2 100644 --- a/packages/gapic-generator/tests/system/test_span_compliance.py +++ b/packages/gapic-generator/tests/system/test_span_compliance.py @@ -201,6 +201,20 @@ 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._ignore_credentials = False``) while still creating an insecure + channel to the local Showcase server. + """ + import grpc + + return grpc.insecure_channel(host) + + def construct_observability_client( client_class, transport: str, @@ -227,11 +241,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..68aac8da6927 100644 --- a/packages/google-api-core/google/api_core/_observability.py +++ b/packages/google-api-core/google/api_core/_observability.py @@ -92,10 +92,12 @@ def _extract_endpoint_attributes( universe_domain = None if isinstance(client_options, dict): - endpoint = client_options.get("api_endpoint") + endpoint = client_options.get("api_endpoint") or client_options.get("_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 getattr( + client_options, "_host", None + ) universe_domain = getattr(client_options, "universe_domain", None) attrs["url.domain"] = universe_domain or "googleapis.com" diff --git a/packages/google-api-core/tests/unit/test_observability.py b/packages/google-api-core/tests/unit/test_observability.py index 1f18428102b8..51fca263b9c6 100644 --- a/packages/google-api-core/tests/unit/test_observability.py +++ b/packages/google-api-core/tests/unit/test_observability.py @@ -379,6 +379,58 @@ def test_extract_endpoint_attributes(client_options, expected_attrs): assert _observability._extract_endpoint_attributes(client_options) == expected_attrs +@pytest.mark.parametrize( + "client_options,expected_attrs", + [ + pytest.param( + {"_host": "localhost:7469"}, + { + "server.address": "localhost", + "server.port": 7469, + "url.domain": "googleapis.com", + }, + id="dict_host_fallback", + ), + pytest.param( + {"api_endpoint": "custom.example.com:8443", "_host": "localhost:7469"}, + { + "server.address": "custom.example.com", + "server.port": 8443, + "url.domain": "googleapis.com", + }, + id="dict_api_endpoint_overrides_host", + ), + pytest.param( + types.SimpleNamespace( + api_endpoint=None, _host="localhost:7469", universe_domain=None + ), + { + "server.address": "localhost", + "server.port": 7469, + "url.domain": "googleapis.com", + }, + id="object_host_fallback", + ), + pytest.param( + types.SimpleNamespace( + api_endpoint="custom.example.com:8443", + _host="localhost:7469", + universe_domain="custom.domain", + ), + { + "server.address": "custom.example.com", + "server.port": 8443, + "url.domain": "custom.domain", + }, + id="object_api_endpoint_overrides_host", + ), + ], +) +def test_extract_endpoint_attributes_host_fallback(client_options, expected_attrs): + """Proves that _extract_endpoint_attributes falls back to _host when api_endpoint is not set.""" + assert _observability._extract_endpoint_attributes(client_options) == 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.