Skip to content
This repository was archived by the owner on Mar 26, 2026. It is now read-only.
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ from collections import OrderedDict
import os
import re
from typing import Callable, Dict, Mapping, MutableMapping, MutableSequence, Optional, {% if service.any_server_streaming %}Iterable, {% endif %}{% if service.any_client_streaming %}Iterator, {% endif %}Sequence, Tuple, Type, Union, cast
{% if api.all_method_settings.values()|map(attribute="auto_populated_fields")|list %}
Comment thread
parthea marked this conversation as resolved.
Outdated
import uuid
{% endif %}
{% if service.any_deprecated %}
import warnings
{% endif %}
Expand Down Expand Up @@ -473,6 +476,27 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta):
)
{% endif %}

{#
Automatically populate UUID4 fields according to AIP-4235
(https://google.aip.dev/client-libraries/4235) if the
field satisfies either of:
Comment thread
vchudnov-g marked this conversation as resolved.
Outdated
- The field supports explicit presence and has not been set by the user.
- The field doesn't support explicit presence, and its value is the empty
string (i.e. the default value).
#}
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
{% if method_settings is not none %}
{% for auto_populated_field in method_settings.auto_populated_fields %}
{% if method.input.fields[auto_populated_field].proto3_optional %}
if '{{ auto_populated_field }}' not in request:
{% else %}
if not request.{{ auto_populated_field }}:
{% endif %}
request.{{ auto_populated_field }} = str(uuid.uuid4())
{% endfor %}
{% endif %}{# if method_settings is not none #}
{% endwith %}{# method_settings #}

# Send the request.
{%+ if not method.void %}response = {% endif %}rpc(
{% if not method.client_streaming %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
{% block content %}

import os
{% if api.all_method_settings.values()|map(attribute="auto_populated_fields")|list %}
import re
{% endif %}
# try/except added for compatibility with python < 3.8
try:
from unittest import mock
Expand Down Expand Up @@ -521,6 +524,18 @@ def test_{{ method_name }}(request_type, transport: str = 'grpc'):
# Everything is optional in proto3 as far as the runtime is concerned,
# and we are mocking out the actual API, so just send an empty request.
request = request_type()

{# Set UUID4 fields so that they are not automatically popoulated. #}
Comment thread
vchudnov-g marked this conversation as resolved.
Outdated
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
{% if method_settings is not none %}
{% for auto_populated_field in method_settings.auto_populated_fields %}
if isinstance(request, dict):
request['{{ auto_populated_field }}'] = "str_value"
Comment thread
vchudnov-g marked this conversation as resolved.
Outdated
else:
request.{{ auto_populated_field }} = "str_value"
{% endfor %}
{% endif %}{# if method_settings is not none #}
{% endwith %}{# method_settings #}
{% if method.client_streaming %}
requests = [request]
{% endif %}
Expand Down Expand Up @@ -568,7 +583,15 @@ def test_{{ method_name }}(request_type, transport: str = 'grpc'):
{% if method.client_streaming %}
assert next(args[0]) == request
{% else %}
assert args[0] == {{ method.input.ident }}()
request = {{ method.input.ident }}()
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
{% if method_settings is not none %}
{% for auto_populated_field in method_settings.auto_populated_fields %}
request.{{ auto_populated_field }} = "str_value"
{% endfor %}
{% endif %}{# if method_settings is not none #}
{% endwith %}{# method_settings #}
assert args[0] == request
{% endif %}

# Establish that the response is the type that we expect.
Expand Down Expand Up @@ -629,6 +652,16 @@ def test_{{ method_name }}_empty_call():
{% if method.client_streaming %}
assert next(args[0]) == request
{% else %}
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
{% if method_settings is not none %}
{% for auto_populated_field in method_settings.auto_populated_fields %}
# Ensure that the uuid4 field is set according to AIP 4235
assert re.match(r"[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}", args[0].{{ auto_populated_field }})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have this check in multiple places, so I suggest having this in a macro. Even if we have to have duplicate macros for the ads vs non-ads templates (we can define the Ads macro in this file, IIUC)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 70f0c64

# clear UUID field so that the check below succeeds
args[0].{{ auto_populated_field }} = None
{% endfor %}
{% endif %}{# if method_settings is not none #}
{% endwith %}{# method_settings #}
assert args[0] == {{ method.input.ident }}()
{% endif %}
{% endif %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@
)
{% endif %} {# method.explicit_routing #}

{{ auto_populate_uuid4_fields(api, method) }}

# Validate the universe domain.
self._validate_universe_domain()

Expand Down Expand Up @@ -265,3 +267,26 @@

{% macro define_extended_operation_subclass(extended_operation) %}
{% endmacro %}

{% macro auto_populate_uuid4_fields(api, method) %}
Comment thread
parthea marked this conversation as resolved.
{#
Automatically populate UUID4 fields according to AIP-4235
(https://google.aip.dev/client-libraries/4235) if the
field satisfies either of:
Comment thread
parthea marked this conversation as resolved.
Outdated
- The field supports explicit presence and has not been set by the user.
- The field doesn't support explicit presence, and its value is the empty
string (i.e. the default value).
#}
Comment thread
vchudnov-g marked this conversation as resolved.
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
{% if method_settings is not none %}
{% for auto_populated_field in method_settings.auto_populated_fields %}
{% if method.input.fields[auto_populated_field].proto3_optional %}
if '{{ auto_populated_field }}' not in request:
{% else %}
if not request.{{ auto_populated_field }}:
{% endif %}
request.{{ auto_populated_field }} = str(uuid.uuid4())
{% endfor %}
{% endif %}{# if method_settings is not none #}
{% endwith %}{# method_settings #}
{% endmacro %}
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
{% extends "_base.py.j2" %}

{% block content %}
{% import "%namespace/%name_%version/%sub/services/%service/_client_macros.j2" as macros %}

from collections import OrderedDict
import functools
import re
from typing import Dict, Mapping, MutableMapping, MutableSequence, Optional, {% if service.any_server_streaming %}AsyncIterable, Awaitable, {% endif %}{% if service.any_client_streaming %}AsyncIterator, {% endif %}Sequence, Tuple, Type, Union
{% if api.all_method_settings.values()|map(attribute="auto_populated_fields")|list %}
import uuid
{% endif %}
{% if service.any_deprecated %}
import warnings
{% endif %}
Expand Down Expand Up @@ -386,6 +390,8 @@ class {{ service.async_client_name }}:
)
{% endif %}

{{ macros.auto_populate_uuid4_fields(api, method) }}

# Validate the universe domain.
self._client._validate_universe_domain()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import functools
import os
import re
from typing import Dict, Mapping, MutableMapping, MutableSequence, Optional, {% if service.any_server_streaming %}Iterable, {% endif %}{% if service.any_client_streaming %}Iterator, {% endif %}Sequence, Tuple, Type, Union, cast
{% if api.all_method_settings.values()|map(attribute="auto_populated_fields")|list %}
import uuid
{% endif %}
import warnings

{% set package_path = api.naming.module_namespace|join('.') + "." + api.naming.versioned_module_name %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
{% import "tests/unit/gapic/%name_%version/%sub/test_macros.j2" as test_macros %}

import os
{% if api.all_method_settings.values()|map(attribute="auto_populated_fields")|list %}
import re
{% endif %}
# try/except added for compatibility with python < 3.8
try:
from unittest import mock
Expand Down Expand Up @@ -849,10 +852,10 @@ def test_{{ service.client_name|snake_case }}_create_channel_credentials_file(cl

{% for method in service.methods.values() if 'grpc' in opts.transport %}{# method_name #}
{% if method.extended_lro %}
{{ test_macros.grpc_required_tests(method, service, full_extended_lro=True) }}
{{ test_macros.grpc_required_tests(method, service, api, full_extended_lro=True) }}

{% endif %}
{{ test_macros.grpc_required_tests(method, service) }}
{{ test_macros.grpc_required_tests(method, service, api) }}
{% endfor %} {# method in methods for grpc #}

{% for method in service.methods.values() if 'rest' in opts.transport %}
Expand Down
110 changes: 107 additions & 3 deletions gapic/templates/tests/unit/gapic/%name_%version/%sub/test_macros.j2
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% macro grpc_required_tests(method, service, full_extended_lro=False) %}
{% macro grpc_required_tests(method, service, api, full_extended_lro=False) %}
{% with method_name = method.safe_name|snake_case + "_unary" if method.extended_lro and not full_extended_lro else method.safe_name|snake_case, method_output = method.extended_lro.operation_type if method.extended_lro and not full_extended_lro else method.output %}
@pytest.mark.parametrize("request_type", [
{{ method.input.ident }},
Expand All @@ -13,6 +13,17 @@ def test_{{ method_name }}(request_type, transport: str = 'grpc'):
# Everything is optional in proto3 as far as the runtime is concerned,
# and we are mocking out the actual API, so just send an empty request.
request = request_type()
{# Set UUID4 fields so that they are not automatically popoulated. #}
Comment thread
vchudnov-g marked this conversation as resolved.
Outdated
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
{% if method_settings is not none %}
{% for auto_populated_field in method_settings.auto_populated_fields %}
if isinstance(request, dict):
request['{{ auto_populated_field }}'] = "str_value"
Comment thread
vchudnov-g marked this conversation as resolved.
Outdated
else:
request.{{ auto_populated_field }} = "str_value"
{% endfor %}
{% endif %}{# if method_settings is not none #}
{% endwith %}{# method_settings #}
{% if method.client_streaming %}
requests = [request]
{% endif %}
Expand Down Expand Up @@ -58,7 +69,15 @@ def test_{{ method_name }}(request_type, transport: str = 'grpc'):
{% if method.client_streaming %}
assert next(args[0]) == request
{% else %}
assert args[0] == {{ method.input.ident }}()
request = {{ method.input.ident }}()
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
{% if method_settings is not none %}
{% for auto_populated_field in method_settings.auto_populated_fields %}
request.{{ auto_populated_field }} = "str_value"
{% endfor %}
{% endif %}{# if method_settings is not none #}
{% endwith %}{# method_settings #}
assert args[0] == request
{% endif %}

# Establish that the response is the type that we expect.
Expand Down Expand Up @@ -119,11 +138,77 @@ def test_{{ method_name }}_empty_call():
{% if method.client_streaming %}
assert next(args[0]) == request
{% else %}
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
{% if method_settings is not none %}
{% for auto_populated_field in method_settings.auto_populated_fields %}
# Ensure that the uuid4 field is set according to AIP 4235
assert re.match(r"[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}", args[0].{{ auto_populated_field }})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto previous comment: We have this check in multiple places, so I suggest having this in a macro.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 70f0c64

# clear UUID field so that the check below succeeds
args[0].{{ auto_populated_field }} = None
{% endfor %}
{% endif %}{# if method_settings is not none #}
{% endwith %}{# method_settings #}
assert args[0] == {{ method.input.ident }}()
{% endif %}
{% endif %}

{% if not full_extended_lro %}
{% if not method.client_streaming %}
@pytest.mark.asyncio
async def test_{{ method_name }}_empty_call_async():
# This test is a coverage failsafe to make sure that totally empty calls,
# i.e. request == None and no flattened fields passed, work.
client = {{ service.async_client_name }}(
credentials=ga_credentials.AnonymousCredentials(),
transport='grpc_asyncio',
)

# Mock the actual call within the gRPC stub, and fake the request.
with mock.patch.object(
type(client.transport.{{ method.transport_safe_name|snake_case }}),
'__call__') as call:
# Designate an appropriate return value for the call.
{% if method.void %}
call.return_value = grpc_helpers_async.FakeUnaryUnaryCall(None)
{% elif method.lro %}
call.return_value = grpc_helpers_async.FakeUnaryUnaryCall(
operations_pb2.Operation(name='operations/spam')
)
{% elif not method.client_streaming and method.server_streaming %}
call.return_value = mock.Mock(aio.UnaryStreamCall, autospec=True)
call.return_value.read = mock.AsyncMock(side_effect=[{{ method.output.ident }}()])
{% elif method.client_streaming and method.server_streaming %}
call.return_value = mock.Mock(aio.StreamStreamCall, autospec=True)
call.return_value.read = mock.AsyncMock(side_effect=[{{ method.output.ident }}()])
{% else %}
call.return_value ={{ '' }}
Comment thread
vchudnov-g marked this conversation as resolved.
Outdated
{%- if not method.client_streaming and not method.server_streaming -%}
grpc_helpers_async.FakeUnaryUnaryCall
{%- else -%}
grpc_helpers_async.FakeStreamUnaryCall
{%- endif -%}({{ method.output.ident }}(
{% for field in method.output.fields.values() | rejectattr('message') %}{% if not field.oneof or field.proto3_optional %}
{{ field.name }}={{ field.mock_value }},
{% endif %}
{% endfor %}
))
{% endif %}
response = await client.{{ method_name }}()
call.assert_called()
_, args, _ = call.mock_calls[0]
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
{% if method_settings is not none %}
{% for auto_populated_field in method_settings.auto_populated_fields %}
# Ensure that the uuid4 field is set according to AIP 4235
assert re.match(r"[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}", args[0].{{ auto_populated_field }})
# clear UUID field so that the check below succeeds
args[0].{{ auto_populated_field }} = None
{% endfor %}
{% endif %}{# if method_settings is not none #}
{% endwith %}{# method_settings #}
assert args[0] == {{ method.input.ident }}()
{% endif %}

@pytest.mark.asyncio
async def test_{{ method_name }}_async(transport: str = 'grpc_asyncio', request_type={{ method.input.ident }}):
client = {{ service.async_client_name }}(
Expand All @@ -134,6 +219,17 @@ async def test_{{ method_name }}_async(transport: str = 'grpc_asyncio', request_
# Everything is optional in proto3 as far as the runtime is concerned,
# and we are mocking out the actual API, so just send an empty request.
request = request_type()
{# Set UUID4 fields so that they are not automatically popoulated. #}
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
{% if method_settings is not none %}
{% for auto_populated_field in method_settings.auto_populated_fields %}
if isinstance(request, dict):
request['{{ auto_populated_field }}'] = "str_value"
else:
request.{{ auto_populated_field }} = "str_value"
{% endfor %}
{% endif %}{# if method_settings is not none #}
{% endwith %}{# method_settings #}
{% if method.client_streaming %}
requests = [request]
{% endif %}
Expand Down Expand Up @@ -182,7 +278,15 @@ async def test_{{ method_name }}_async(transport: str = 'grpc_asyncio', request_
{% if method.client_streaming %}
assert next(args[0]) == request
{% else %}
assert args[0] == {{ method.input.ident }}()
request = {{ method.input.ident }}()
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
{% if method_settings is not none %}
{% for auto_populated_field in method_settings.auto_populated_fields %}
request.{{ auto_populated_field }} = "str_value"
{% endfor %}
{% endif %}{# if method_settings is not none #}
{% endwith %}{# method_settings #}
assert args[0] == request
{% endif %}

# Establish that the response is the type that we expect.
Expand Down
Loading