Skip to content

Commit abdf5ec

Browse files
feat: add autogenerated snippets (#845)
This PR targets iteration 1 and 2 specified in the [Snippet Gen Design](go/snippet-gen-design): Full canonical coverage of simple requests, paginated, LRO, server streaming, and Bidi streaming with empty request objects. Snippet generation is hidden behind a new option `autogen-snippets`. After discussion with folks on different language teams on snippetgen, I decided using "golden" snippet files would be easier than following the unit testing strategy used to check the library surface. I also believe goldens will be be easier for review for other Python DPEs. Other notes: - I've commented out the existing metadata generation code and tests. The new metadata format is still under discussion. - Async samples are excluded as the existing samplegen infrastructure was written pre-async. I will add the async samples in the next PR. Co-authored-by: Dov Shlachter <dovs@google.com>
1 parent 358a702 commit abdf5ec

29 files changed

+1217
-466
lines changed

.github/snippet-bot.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# https://github.com/googleapis/repo-automation-bots/tree/master/packages/snippet-bot
2+
ignoreFiles:
3+
- "**/*.py"

.github/workflows/tests.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,26 @@ jobs:
265265
run: python -m pip install nox
266266
- name: Typecheck the generated output.
267267
run: nox -s showcase_mypy${{ matrix.variant }}
268+
snippetgen:
269+
runs-on: ubuntu-latest
270+
steps:
271+
- name: Cancel Previous Runs
272+
uses: styfle/cancel-workflow-action@0.7.0
273+
with:
274+
access_token: ${{ github.token }}
275+
- uses: actions/checkout@v2
276+
- name: Set up Python 3.8
277+
uses: actions/setup-python@v2
278+
with:
279+
python-version: 3.8
280+
- name: Install system dependencies.
281+
run: |
282+
sudo apt-get update
283+
sudo apt-get install -y curl pandoc unzip gcc
284+
- name: Install nox.
285+
run: python -m pip install nox
286+
- name: Check autogenerated snippets.
287+
run: nox -s snippetgen
268288
unit:
269289
strategy:
270290
matrix:

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,7 @@ pylintrc.test
6565

6666
# pyenv
6767
.python-version
68+
69+
# Test dependencies and output
70+
api-common-protos
71+
tests/snippetgen/.test_output

gapic/generator/generator.py

Lines changed: 62 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414

1515
import jinja2
1616
import yaml
17+
import itertools
1718
import re
1819
import os
20+
import typing
1921
from typing import Any, DefaultDict, Dict, Mapping
2022
from hashlib import sha256
2123
from collections import OrderedDict, defaultdict
@@ -107,12 +109,12 @@ def get_response(
107109
template_name, api_schema=api_schema, opts=opts)
108110
)
109111

110-
sample_output = self._generate_samples_and_manifest(
111-
api_schema,
112-
self._env.get_template(sample_templates[0]),
113-
) if sample_templates else {}
114-
115-
output_files.update(sample_output)
112+
if sample_templates:
113+
sample_output = self._generate_samples_and_manifest(
114+
api_schema, self._env.get_template(sample_templates[0]),
115+
opts=opts,
116+
)
117+
output_files.update(sample_output)
116118

117119
# Return the CodeGeneratorResponse output.
118120
res = CodeGeneratorResponse(
@@ -121,12 +123,13 @@ def get_response(
121123
return res
122124

123125
def _generate_samples_and_manifest(
124-
self, api_schema: api.API, sample_template: jinja2.Template,
125-
) -> Dict[str, CodeGeneratorResponse.File]:
126+
self, api_schema: api.API, sample_template: jinja2.Template, *, opts: Options) -> Dict:
126127
"""Generate samples and samplegen manifest for the API.
127128
128129
Arguments:
129130
api_schema (api.API): The schema for the API to which the samples belong.
131+
sample_template (jinja2.Template): The template to use to generate samples.
132+
opts (Options): Additional generator options.
130133
131134
Returns:
132135
Dict[str, CodeGeneratorResponse.File]: A dict mapping filepath to rendered file.
@@ -137,56 +140,50 @@ def _generate_samples_and_manifest(
137140
id_to_hash_to_spec: DefaultDict[str,
138141
Dict[str, Any]] = defaultdict(dict)
139142

140-
STANDALONE_TYPE = "standalone"
141-
for config_fpath in self._sample_configs:
142-
with open(config_fpath) as f:
143-
configs = yaml.safe_load_all(f.read())
144-
145-
spec_generator = (
146-
spec
147-
for cfg in configs
148-
if is_valid_sample_cfg(cfg)
149-
for spec in cfg.get("samples", [])
150-
# If unspecified, assume a sample config describes a standalone.
151-
# If sample_types are specified, standalone samples must be
152-
# explicitly enabled.
153-
if STANDALONE_TYPE in spec.get("sample_type", [STANDALONE_TYPE])
154-
)
143+
# Autogenerated sample specs
144+
autogen_specs: typing.List[typing.Dict[str, Any]] = []
145+
if opts.autogen_snippets:
146+
autogen_specs = list(
147+
samplegen.generate_sample_specs(api_schema, opts=opts))
148+
149+
# Also process any handwritten sample specs
150+
handwritten_specs = samplegen.parse_handwritten_specs(
151+
self._sample_configs)
152+
153+
sample_specs = autogen_specs + list(handwritten_specs)
154+
155+
for spec in sample_specs:
156+
# Every sample requires an ID. This may be provided
157+
# by a samplegen config author.
158+
# If no ID is provided, fall back to the region tag.
159+
#
160+
# Ideally the sample author should pick a descriptive, unique ID,
161+
# but this may be impractical and can be error-prone.
162+
spec_hash = sha256(str(spec).encode("utf8")).hexdigest()[:8]
163+
sample_id = spec.get("id") or spec.get("region_tag") or spec_hash
164+
spec["id"] = sample_id
155165

156-
for spec in spec_generator:
157-
# Every sample requires an ID, preferably provided by the
158-
# samplegen config author.
159-
# If no ID is provided, fall back to the region tag.
160-
# If there's no region tag, generate a unique ID.
161-
#
162-
# Ideally the sample author should pick a descriptive, unique ID,
163-
# but this may be impractical and can be error-prone.
164-
spec_hash = sha256(str(spec).encode("utf8")).hexdigest()[:8]
165-
sample_id = spec.get("id") or spec.get(
166-
"region_tag") or spec_hash
167-
spec["id"] = sample_id
168-
169-
hash_to_spec = id_to_hash_to_spec[sample_id]
170-
if spec_hash in hash_to_spec:
171-
raise DuplicateSample(
172-
f"Duplicate samplegen spec found: {spec}")
173-
174-
hash_to_spec[spec_hash] = spec
175-
176-
out_dir = "samples"
166+
hash_to_spec = id_to_hash_to_spec[sample_id]
167+
168+
if spec_hash in hash_to_spec:
169+
raise DuplicateSample(
170+
f"Duplicate samplegen spec found: {spec}")
171+
172+
hash_to_spec[spec_hash] = spec
173+
174+
out_dir = "samples/generated_samples"
177175
fpath_to_spec_and_rendered = {}
178176
for hash_to_spec in id_to_hash_to_spec.values():
179177
for spec_hash, spec in hash_to_spec.items():
180178
id_is_unique = len(hash_to_spec) == 1
181-
# The ID is used to generate the file name and by sample tester
182-
# to link filenames to invoked samples. It must be globally unique.
179+
# The ID is used to generate the file name. It must be globally unique.
183180
if not id_is_unique:
184181
spec["id"] += f"_{spec_hash}"
185182

186183
sample = samplegen.generate_sample(
187184
spec, api_schema, sample_template,)
188185

189-
fpath = spec["id"] + ".py"
186+
fpath = utils.to_snake_case(spec["id"]) + ".py"
190187
fpath_to_spec_and_rendered[os.path.join(out_dir, fpath)] = (
191188
spec,
192189
sample,
@@ -199,20 +196,24 @@ def _generate_samples_and_manifest(
199196
for fname, (_, sample) in fpath_to_spec_and_rendered.items()
200197
}
201198

202-
# Only generate a manifest if we generated samples.
203-
if output_files:
204-
manifest_fname, manifest_doc = manifest.generate(
205-
(
206-
(fname, spec)
207-
for fname, (spec, _) in fpath_to_spec_and_rendered.items()
208-
),
209-
api_schema,
210-
)
211-
212-
manifest_fname = os.path.join(out_dir, manifest_fname)
213-
output_files[manifest_fname] = CodeGeneratorResponse.File(
214-
content=manifest_doc.render(), name=manifest_fname
215-
)
199+
# TODO(busunkim): Re-enable manifest generation once metadata
200+
# format has been formalized.
201+
# https://docs.google.com/document/d/1ghBam8vMj3xdoe4xfXhzVcOAIwrkbTpkMLgKc9RPD9k/edit#heading=h.sakzausv6hue
202+
#
203+
# if output_files:
204+
205+
# manifest_fname, manifest_doc = manifest.generate(
206+
# (
207+
# (fname, spec)
208+
# for fname, (spec, _) in fpath_to_spec_and_rendered.items()
209+
# ),
210+
# api_schema,
211+
# )
212+
213+
# manifest_fname = os.path.join(out_dir, manifest_fname)
214+
# output_files[manifest_fname] = CodeGeneratorResponse.File(
215+
# content=manifest_doc.render(), name=manifest_fname
216+
# )
216217

217218
return output_files
218219

0 commit comments

Comments
 (0)