Skip to content

Commit 1dcacd0

Browse files
authored
Re-add original flattening semantics (#256)
Not a strict readition: flattened fields can be non-primitives.
1 parent 29fd85d commit 1dcacd0

File tree

10 files changed

+141
-155
lines changed

10 files changed

+141
-155
lines changed

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

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -545,26 +545,14 @@ def field_headers(self) -> Sequence[str]:
545545
@utils.cached_property
546546
def flattened_fields(self) -> Mapping[str, Field]:
547547
"""Return the signature defined for this method."""
548-
answer: Dict[str, Field] = collections.OrderedDict()
549548
signatures = self.options.Extensions[client_pb2.method_signature]
550549

551-
# Iterate over each signature and add the appropriate fields.
552-
for sig in signatures:
553-
# Get all of the individual fields.
554-
fields = collections.OrderedDict([
555-
(f.strip(), self.input.get_field(*f.strip().split('.')))
556-
for f in sig.split(',')
557-
])
558-
559-
# Sanity check: If any fields contain a message, we ignore the
560-
# entire signature.
561-
if any([i.message for i in fields.values()]):
562-
continue
563-
564-
# Add the fields to the answer.
565-
answer.update(fields)
550+
answer: Dict[str, Field] = collections.OrderedDict(
551+
(f.strip(), self.input.get_field(*f.strip().split('.')))
552+
for sig in signatures
553+
for f in sig.split(',')
554+
)
566555

567-
# Done; return the flattened fields
568556
return answer
569557

570558
@utils.cached_property

packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -126,14 +126,11 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta):
126126

127127
{% for method in service.methods.values() -%}
128128
def {{ method.name|snake_case }}(self,
129-
{% for field in method.legacy_flattened_fields.values() -%}
130-
{% if field.required -%}
131-
{{ field.name }}: {{ field.ident }},
132-
{% else -%}
129+
request: {{ method.input.ident }} = None,
130+
*,
131+
{% for field in method.flattened_fields.values() -%}
133132
{{ field.name }}: {{ field.ident }} = None,
134-
{% endif -%}
135133
{% endfor -%}
136-
*,
137134
retry: retries.Retry = gapic_v1.method.DEFAULT,
138135
timeout: float = None,
139136
metadata: Sequence[Tuple[str, str]] = (),
@@ -164,13 +161,20 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta):
164161
{%- endif %}
165162
"""
166163
# Create or coerce a protobuf request object.
167-
{% if method.legacy_flattened_fields -%}
164+
{% if method.flattened_fields -%}
165+
# Sanity check: If we got a request object, we should *not* have
166+
# gotten any keyword arguments that map to the request.
167+
if request is not None and any([{{ method.flattened_fields.values()|join(', ', attribute='name') }}]):
168+
raise ValueError('If the `request` argument is set, then none of '
169+
'the individual field arguments should be set.')
170+
168171
# If we have keyword arguments corresponding to fields on the
169172
# request, apply these.
170173
{% endif -%}
171-
request = {{ method.input.ident }}()
172-
{%- for field in method.legacy_flattened_fields.values() %}
173-
request.{{ field.name }} = {{ field.name }}
174+
request = {{ method.input.ident }}(request)
175+
{%- for key, field in method.flattened_fields.items() %}
176+
if {{ field.name }} is not None:
177+
request.{{ key }} = {{ field.name }}
174178
{%- endfor %}
175179

176180
# Wrap the RPC method; this adds retry and timeout information,

packages/gapic-generator/gapic/templates/tests/unit/%name_%version/%sub/test_%service.py.j2

Lines changed: 76 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ def test_{{ method.name|snake_case }}(transport: str = 'grpc'):
7171
transport=transport,
7272
)
7373

74+
# Everything is optional in proto3 as far as the runtime is concerned,
75+
# and we are mocking out the actual API, so just send an empty request.
76+
request = {{ method.input.ident }}()
77+
7478
# Mock the actual call within the gRPC stub, and fake the request.
7579
with mock.patch.object(
7680
type(client._transport.{{ method.name|snake_case }}),
@@ -89,18 +93,12 @@ def test_{{ method.name|snake_case }}(transport: str = 'grpc'):
8993
{%- endfor %}
9094
)
9195
{% endif -%}
92-
# Everything is optional in proto3 as far as the runtime is concerned,
93-
# and we are mocking out the actual API, so just send an empty request.
94-
response = client.{{ method.name|snake_case }}(
95-
{% for field in method.legacy_flattened_fields.values() -%}
96-
{{ field.name }}=None,
97-
{% endfor -%}
98-
)
99-
96+
response = client.{{ method.name|snake_case }}(request)
97+
10098
# Establish that the underlying gRPC stub method was called.
10199
assert len(call.mock_calls) == 1
102100
_, args, _ = call.mock_calls[0]
103-
assert args[0] == {{ method.input.ident }}()
101+
assert args[0] == request
104102

105103
# Establish that the response is the type that we expect.
106104
{% if method.void -%}
@@ -121,29 +119,26 @@ def test_{{ method.name|snake_case }}(transport: str = 'grpc'):
121119
def test_{{ method.name|snake_case }}_field_headers():
122120
client = {{ service.client_name }}(
123121
credentials=credentials.AnonymousCredentials(),
122+
)
123+
124+
# Any value that is part of the HTTP/1.1 URI should be sent as
125+
# a field header. Set these to a non-empty value.
126+
request = {{ method.input.ident }}(
127+
{%- for field_header in method.field_headers %}
128+
{{ field_header }}='{{ field_header }}/value',
129+
{%- endfor %}
124130
)
125131

126132
# Mock the actual call within the gRPC stub, and fake the request.
127133
with mock.patch.object(
128134
type(client._transport.{{ method.name|snake_case }}),
129135
'__call__') as call:
130136
call.return_value = {{ method.output.ident }}()
131-
# Any value that is part of the HTTP/1.1 URI should be sent as
132-
# a field header. Set these to a non-empty value.
133-
response = client.{{ method.name|snake_case }}(
134-
{%- for field_header in method.field_headers %}
135-
{{ field_header }}='{{ field_header }}/value',
136-
{%- endfor %}
137-
)
138-
137+
response = client.{{ method.name|snake_case }}(request)
138+
139139
# Establish that the underlying gRPC stub method was called.
140140
assert len(call.mock_calls) == 1
141141
_, args, _ = call.mock_calls[0]
142-
request = {{ method.input.ident }}(
143-
{%- for field_header in method.field_headers %}
144-
{{ field_header }}='{{ field_header }}/value',
145-
{%- endfor %}
146-
)
147142
assert args[0] == request
148143

149144
# Establish that the field header was sent.
@@ -155,63 +150,60 @@ def test_{{ method.name|snake_case }}_field_headers():
155150
{%- if not loop.last %}&{% endif -%}
156151
{%- endfor %}',
157152
) in kw['metadata']
158-
{% endif %} {#- method.field_headers #}
159-
160-
{# NOTE: the backwards compatibility requirements of legacy flattening make these tests no longer relevant #}
161-
{# If the legacy flattening is ever abandoned, these tests or something like them will be needed again. #}
162-
{# #}
163-
{# {% if method.flattened_fields %} #}
164-
{# def test_{{ method.name|snake_case }}_flattened(): #}
165-
{# client = {{ service.client_name }}( #}
166-
{# credentials=credentials.AnonymousCredentials(), #}
167-
{# ) #}
168-
169-
{# # Mock the actual call within the gRPC stub, and fake the request. #}
170-
{# with mock.patch.object( #}
171-
{# type(client._transport.{{ method.name|snake_case }}), #}
172-
{# '__call__') as call: #}
173-
{# # Designate an appropriate return value for the call. #}
174-
{# {% if method.void -%} #}
175-
{# call.return_value = None #}
176-
{# {% elif method.lro -%} #}
177-
{# call.return_value = operations_pb2.Operation(name='operations/op') #}
178-
{# {% elif method.server_streaming -%} #}
179-
{# call.return_value = iter([{{ method.output.ident }}()]) #}
180-
{# {% else -%} #}
181-
{# call.return_value = {{ method.output.ident }}() #}
182-
{# {% endif %} #}
183-
{# # Call the method with a truthy value for each flattened field, #}
184-
{# # using the keyword arguments to the method. #}
185-
{# response = client.{{ method.name|snake_case }}( #}
186-
{# {%- for key, field in method.flattened_fields.items() %} #}
187-
{# {{ field.name }}={{ field.mock_value }}, #}
188-
{# {%- endfor %} #}
189-
{# ) #}
190-
191-
{# # Establish that the underlying call was made with the expected #}
192-
{# # request object values. #}
193-
{# assert len(call.mock_calls) == 1 #}
194-
{# _, args, _ = call.mock_calls[0] #}
195-
{# {% for key, field in method.flattened_fields.items() -%} #}
196-
{# assert args[0].{{ key }} == {{ field.mock_value }} #}
197-
{# {% endfor %} #}
198-
199-
200-
{# def test_{{ method.name|snake_case }}_flattened_error(): #}
201-
{# client = {{ service.client_name }}( #}
202-
{# credentials=credentials.AnonymousCredentials(), #}
203-
{# ) #}
204-
205-
{# # Attempting to call a method with both a request object and flattened #}
206-
{# # fields is an error. #}
207-
{# with pytest.raises(ValueError): #}
208-
{# client.{{ method.name|snake_case }}( #}
209-
{# {{ method.input.ident }}(), #}
210-
{# {%- for key, field in method.flattened_fields.items() %} #}
211-
{# {{ field.name }}={{ field.mock_value }}, #}
212-
{# {%- endfor %} #}
213-
{# ) #}
214-
{# {% endif %} {\#- method.flattened_fields #\} #}
153+
{% endif %}
154+
155+
{% if method.flattened_fields %}
156+
def test_{{ method.name|snake_case }}_flattened():
157+
client = {{ service.client_name }}(
158+
credentials=credentials.AnonymousCredentials(),
159+
)
160+
161+
# Mock the actual call within the gRPC stub, and fake the request.
162+
with mock.patch.object(
163+
type(client._transport.{{ method.name|snake_case }}),
164+
'__call__') as call:
165+
# Designate an appropriate return value for the call.
166+
{% if method.void -%}
167+
call.return_value = None
168+
{% elif method.lro -%}
169+
call.return_value = operations_pb2.Operation(name='operations/op')
170+
{% elif method.server_streaming -%}
171+
call.return_value = iter([{{ method.output.ident }}()])
172+
{% else -%}
173+
call.return_value = {{ method.output.ident }}()
174+
{% endif %}
175+
# Call the method with a truthy value for each flattened field,
176+
# using the keyword arguments to the method.
177+
response = client.{{ method.name|snake_case }}(
178+
{%- for field in method.flattened_fields.values() %}
179+
{{ field.name }}={{ field.mock_value }},
180+
{%- endfor %}
181+
)
182+
183+
# Establish that the underlying call was made with the expected
184+
# request object values.
185+
assert len(call.mock_calls) == 1
186+
_, args, _ = call.mock_calls[0]
187+
{% for key, field in method.flattened_fields.items() -%}
188+
assert args[0].{{ key }} == {{ field.mock_value }}
189+
{% endfor %}
190+
191+
192+
def test_{{ method.name|snake_case }}_flattened_error():
193+
client = {{ service.client_name }}(
194+
credentials=credentials.AnonymousCredentials(),
195+
)
196+
197+
# Attempting to call a method with both a request object and flattened
198+
# fields is an error.
199+
with pytest.raises(ValueError):
200+
client.{{ method.name|snake_case }}(
201+
{{ method.input.ident }}(),
202+
{%- for field in method.flattened_fields.values() %}
203+
{{ field.name }}={{ field.mock_value }},
204+
{%- endfor %}
205+
)
206+
{% endif %}
215207

216208

217209
{% if method.paged_result_field %}
@@ -252,7 +244,9 @@ def test_{{ method.name|snake_case }}_pager():
252244
),
253245
RuntimeError,
254246
)
255-
results = [i for i in client.{{ method.name|snake_case }}({% for field in method.legacy_flattened_fields.values() if field.required -%}None,{% endfor -%})]
247+
results = [i for i in client.{{ method.name|snake_case }}(
248+
request={},
249+
)]
256250
assert len(results) == 6
257251
assert all([isinstance(i, {{ method.paged_result_field.message.ident }})
258252
for i in results])
@@ -294,7 +288,7 @@ def test_{{ method.name|snake_case }}_pages():
294288
),
295289
RuntimeError,
296290
)
297-
pages = list(client.{{ method.name|snake_case }}({% for field in method.legacy_flattened_fields.values() if field.required -%}None,{% endfor -%}).pages)
291+
pages = list(client.{{ method.name|snake_case }}(request={}).pages)
298292
for page, token in zip(pages, ['abc','def','ghi', '']):
299293
assert page.raw_page.next_page_token == token
300294
{% elif method.lro and "next_page_token" in method.lro.response_type.fields.keys() %}
@@ -352,7 +346,7 @@ def test_{{ service.name|snake_case }}_base_transport():
352346
)
353347
for method in methods:
354348
with pytest.raises(NotImplementedError):
355-
getattr(transport, method)()
349+
getattr(transport, method)(request=object())
356350

357351
{% if service.has_lro -%}
358352
# Additionally, the LRO client (a property) should

packages/gapic-generator/tests/system/test_grpc_lro.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@
1818

1919

2020
def test_lro(echo):
21-
future = echo.wait(
22-
end_time=datetime.now(tz=timezone.utc) + timedelta(seconds=1),
23-
success={
21+
future = echo.wait({
22+
'end_time': datetime.now(tz=timezone.utc) + timedelta(seconds=1),
23+
'success': {
2424
'content': 'The hail in Wales falls mainly on the snails...eventually.'
25-
}
25+
}}
2626
)
2727
response = future.result()
2828
assert isinstance(response, showcase_v1beta1.WaitResponse)

packages/gapic-generator/tests/system/test_grpc_streams.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515

1616
def test_unary_stream(echo):
1717
content = 'The hail in Wales falls mainly on the snails.'
18-
responses = echo.expand(
19-
content=content,
20-
)
18+
responses = echo.expand({
19+
'content': content,
20+
})
2121

2222
# Consume the response and ensure it matches what we expect.
2323
# with pytest.raises(exceptions.NotFound) as exc:

packages/gapic-generator/tests/system/test_grpc_unary.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,28 +20,28 @@
2020
from google import showcase
2121

2222

23-
def test_unary(echo):
24-
content = 'The hail in Wales falls mainly on the snails.'
25-
response = echo.echo(
26-
content=content,
27-
)
28-
assert response.content == content
23+
def test_unary_with_request_object(echo):
24+
response = echo.echo(showcase.EchoRequest(
25+
content='The hail in Wales falls mainly on the snails.',
26+
))
27+
assert response.content == 'The hail in Wales falls mainly on the snails.'
2928

3029

31-
def test_unary_positional(echo):
32-
content = 'The hail in Wales falls mainly on the snails.'
33-
response = echo.echo(content,)
34-
assert response.content == content
30+
def test_unary_with_dict(echo):
31+
response = echo.echo({
32+
'content': 'The hail in Wales falls mainly on the snails.',
33+
})
34+
assert response.content == 'The hail in Wales falls mainly on the snails.'
3535

3636

3737
def test_unary_error(echo):
3838
message = 'Bad things! Bad things!'
3939
with pytest.raises(exceptions.InvalidArgument) as exc:
40-
echo.echo(
41-
error={
40+
echo.echo({
41+
'error': {
4242
'code': code_pb2.Code.Value('INVALID_ARGUMENT'),
4343
'message': message,
4444
},
45-
)
45+
})
4646
assert exc.value.code == 400
4747
assert exc.value.message == message

packages/gapic-generator/tests/system/test_pagination.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,21 @@
1717

1818
def test_pagination(echo):
1919
text = 'The hail in Wales falls mainly on the snails.'
20-
results = [i for i in echo.paged_expand(
21-
content=text,
22-
page_size=3,
23-
)]
20+
results = [i for i in echo.paged_expand({
21+
'content': text,
22+
'page_size': 3,
23+
})]
2424
assert len(results) == 9
2525
assert results == [showcase.EchoResponse(content=i)
2626
for i in text.split(' ')]
2727

2828

2929
def test_pagination_pages(echo):
3030
text = "The hail in Wales falls mainly on the snails."
31-
page_results = list(echo.paged_expand(
32-
content=text,
33-
page_size=3,
34-
).pages)
31+
page_results = list(echo.paged_expand({
32+
'content': text,
33+
'page_size': 3,
34+
}).pages)
3535

3636
assert len(page_results) == 3
3737
assert not page_results[-1].next_page_token

0 commit comments

Comments
 (0)