Skip to content

Commit a3884d4

Browse files
authored
Initial part of abstracting out python specifics from sample generation (#182)
Initial movement of manifest generation to a new file Lift out python specifics Move some unrelated code to utility file
1 parent 6c3b3cd commit a3884d4

File tree

11 files changed

+227
-124
lines changed

11 files changed

+227
-124
lines changed

packages/gapic-generator/gapic/generator/generator.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@
1919
from typing import (Any, DefaultDict, Dict, Mapping, List)
2020
from hashlib import sha256
2121
from collections import (OrderedDict, defaultdict)
22-
from gapic.samplegen_utils.utils import is_valid_sample_cfg
22+
from gapic.samplegen_utils.utils import (
23+
coerce_response_name, is_valid_sample_cfg)
2324
from gapic.samplegen_utils.types import InvalidConfig
24-
from gapic.samplegen import samplegen
25+
from gapic.samplegen import (manifest, samplegen)
2526
from gapic.generator import options
2627
from gapic.generator import formatter
2728
from gapic.schema import api
@@ -56,7 +57,7 @@ def __init__(self, opts: options.Options) -> None:
5657
self._env.filters['snake_case'] = utils.to_snake_case
5758
self._env.filters['sort_lines'] = utils.sort_lines
5859
self._env.filters['wrap'] = utils.wrap
59-
self._env.filters['coerce_response_name'] = samplegen.coerce_response_name
60+
self._env.filters['coerce_response_name'] = coerce_response_name
6061

6162
self._sample_configs = opts.sample_configs
6263

@@ -76,7 +77,8 @@ def get_response(self, api_schema: api.API) -> CodeGeneratorResponse:
7677
output_files: Dict[str, CodeGeneratorResponse.File] = OrderedDict()
7778

7879
sample_templates, client_templates = utils.partition(
79-
lambda fname: os.path.basename(fname) == samplegen.TEMPLATE_NAME,
80+
lambda fname: os.path.basename(
81+
fname) == samplegen.DEFAULT_TEMPLATE_NAME,
8082
self._env.loader.list_templates())
8183

8284
# Iterate over each template and add the appropriate output files
@@ -138,7 +140,6 @@ def _generate_samples_and_manifest(
138140
spec["id"] = sample_id
139141
id_to_samples[sample_id].append(spec)
140142

141-
# Interpolate the special variables in the sample_out_dir template.
142143
out_dir = "samples"
143144
fpath_to_spec_and_rendered = {}
144145
for samples in id_to_samples.values():
@@ -167,10 +168,9 @@ def _generate_samples_and_manifest(
167168

168169
# Only generate a manifest if we generated samples.
169170
if output_files:
170-
manifest_fname, manifest_doc = samplegen.generate_manifest(
171+
manifest_fname, manifest_doc = manifest.generate(
171172
((fname, spec)
172173
for fname, (spec, _) in fpath_to_spec_and_rendered.items()),
173-
out_dir,
174174
api_schema
175175
)
176176

packages/gapic-generator/gapic/samplegen/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
# limitations under the License.
1414

1515
from gapic.samplegen import samplegen
16+
from gapic.samplegen import manifest
1617

1718
__all__ = (
19+
'manifest',
1820
'samplegen',
1921
)
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# Copyright (C) 2019 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import os
16+
import time
17+
from typing import Tuple
18+
19+
from gapic.samplegen_utils import (types, yaml)
20+
21+
BASE_PATH_KEY = "base_path"
22+
DEFAULT_SAMPLE_DIR = "samples"
23+
24+
# The default environment for executing python samples.
25+
# Custom environments must adhere to the following pattern:
26+
# they must be a yaml.Map with a defined anchor_name field,
27+
# and 'environment', 'base_path', and 'invocation' keys must be present.
28+
# The 'invocation' key must map to an interpolable commandline
29+
# that will invoke the given sample.
30+
PYTHON3_ENVIRONMENT = yaml.Map(
31+
name="python",
32+
anchor_name="python",
33+
elements=[
34+
yaml.KeyVal("environment", "python"),
35+
yaml.KeyVal("bin", "python3"),
36+
yaml.KeyVal(BASE_PATH_KEY, DEFAULT_SAMPLE_DIR),
37+
yaml.KeyVal("invocation", "'{bin} {path} @args'"),
38+
],
39+
)
40+
41+
42+
def generate(
43+
fpaths_and_samples,
44+
api_schema,
45+
*,
46+
environment: yaml.Map = PYTHON3_ENVIRONMENT,
47+
manifest_time: int = None
48+
) -> Tuple[str, yaml.Doc]:
49+
"""Generate a samplegen manifest for use by sampletest
50+
51+
Args:
52+
fpaths_and_samples (Iterable[Tuple[str, Mapping[str, Any]]]):
53+
The file paths and samples to be listed in the manifest
54+
api_schema (~.api.API): An API schema object.
55+
environment (yaml.Map): Optional custom sample execution environment.
56+
Set this if the samples are being generated for
57+
a custom language.
58+
manifest_time (int): Optional. An override for the timestamp in the name of the manifest filename.
59+
Primarily used for testing.
60+
61+
Returns:
62+
Tuple[str, yaml.Doc]: The filename of the manifest and the manifest data as a dictionary.
63+
64+
Raises:
65+
types.InvalidSampleFpath: If any of the paths in fpaths_and_samples do not
66+
begin with the base_path from the environment.
67+
68+
"""
69+
base_path = environment.get(BASE_PATH_KEY, DEFAULT_SAMPLE_DIR)
70+
71+
def transform_path(fpath):
72+
fpath = os.path.normpath(fpath)
73+
if not fpath.startswith(base_path):
74+
raise types.InvalidSampleFpath(
75+
f"Sample fpath does not start with '{base_path}': {fpath}")
76+
77+
return "'{base_path}/%s'" % os.path.relpath(fpath, base_path)
78+
79+
doc = yaml.Doc(
80+
[
81+
yaml.KeyVal("type", "manifest/samples"),
82+
yaml.KeyVal("schema_version", "3"),
83+
environment,
84+
yaml.Collection(
85+
name="samples",
86+
elements=[
87+
[ # type: ignore
88+
# Mypy doesn't correctly intuit the type of the
89+
# "region_tag" conditional expression.
90+
yaml.Alias(environment.anchor_name or ""),
91+
yaml.KeyVal("sample", sample["id"]),
92+
yaml.KeyVal(
93+
"path", transform_path(fpath)
94+
),
95+
(yaml.KeyVal("region_tag", sample["region_tag"])
96+
if "region_tag" in sample else
97+
yaml.Null),
98+
]
99+
for fpath, sample in fpaths_and_samples
100+
],
101+
),
102+
]
103+
)
104+
105+
dt = time.gmtime(manifest_time)
106+
manifest_fname_template = (
107+
"{api}.{version}.{language}."
108+
"{year:04d}{month:02d}{day:02d}."
109+
"{hour:02d}{minute:02d}{second:02d}."
110+
"manifest.yaml"
111+
)
112+
113+
manifest_fname = manifest_fname_template.format(
114+
api=api_schema.naming.name,
115+
version=api_schema.naming.version,
116+
language=environment.name,
117+
year=dt.tm_year,
118+
month=dt.tm_mon,
119+
day=dt.tm_mday,
120+
hour=dt.tm_hour,
121+
minute=dt.tm_min,
122+
second=dt.tm_sec,
123+
)
124+
125+
return manifest_fname, doc

packages/gapic-generator/gapic/samplegen/samplegen.py

Lines changed: 21 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import re
2121
import time
2222

23-
from gapic.samplegen_utils import (types, yaml)
23+
from gapic.samplegen_utils import types
2424
from gapic.schema import (api, wrappers)
2525

2626
from collections import (defaultdict, namedtuple, ChainMap as chainmap)
@@ -55,9 +55,7 @@
5555
)
5656
)
5757

58-
# TODO: configure the base template name so that
59-
# e.g. other languages can use the same machinery.
60-
TEMPLATE_NAME = "sample.py.j2"
58+
DEFAULT_TEMPLATE_NAME = "sample.py.j2"
6159

6260

6361
@dataclasses.dataclass(frozen=True)
@@ -106,20 +104,6 @@ class TransformedRequest:
106104
body: Optional[List[AttributeRequestSetup]]
107105

108106

109-
def coerce_response_name(s: str) -> str:
110-
# In the sample config, the "$resp" keyword is used to refer to the
111-
# item of interest as received by the corresponding calling form.
112-
# For a 'regular', i.e. unary, synchronous, non-long-running method,
113-
# it's the return value; for a server-streaming method, it's the iteration
114-
# variable in the for loop that iterates over the return value, and for
115-
# a long running promise, the user calls result on the method return value to
116-
# resolve the future.
117-
#
118-
# The sample schema uses '$resp' as the special variable,
119-
# but in the samples the 'response' variable is used instead.
120-
return s.replace("$resp", "response")
121-
122-
123107
class Validator:
124108
"""Class that validates a sample.
125109
@@ -160,6 +144,17 @@ def __init__(self, method: wrappers.Method):
160144
}
161145
)
162146

147+
@staticmethod
148+
def preprocess_sample(sample, api_schema):
149+
"""Modify a sample to set default or missing fields.
150+
151+
Args:
152+
sample (Any): A definition for a single sample generated from parsed yaml.
153+
api_schema (api.API): The schema that defines the API to which the sample belongs.
154+
"""
155+
sample["package_name"] = api_schema.naming.warehouse_package_name
156+
sample.setdefault("response", [{"print": ["%s", "$resp"]}])
157+
163158
def var_field(self, var_name: str) -> Optional[wrappers.Field]:
164159
return self.var_defs_.get(var_name)
165160

@@ -661,7 +656,8 @@ def _validate_loop(self, loop):
661656

662657
def generate_sample(sample,
663658
env: jinja2.environment.Environment,
664-
api_schema: api.API) -> str:
659+
api_schema: api.API,
660+
template_name: str = DEFAULT_TEMPLATE_NAME) -> str:
665661
"""Generate a standalone, runnable sample.
666662
667663
Rendering and writing the rendered output is left for the caller.
@@ -671,11 +667,13 @@ def generate_sample(sample,
671667
env (jinja2.environment.Environment): The jinja environment used to generate
672668
the filled template for the sample.
673669
api_schema (api.API): The schema that defines the API to which the sample belongs.
670+
template_name (str): An optional override for the name of the template
671+
used to generate the sample.
674672
675673
Returns:
676674
str: The rendered sample.
677675
"""
678-
sample_template = env.get_template(TEMPLATE_NAME)
676+
sample_template = env.get_template(template_name)
679677

680678
service_name = sample["service"]
681679
service = api_schema.services.get(service_name)
@@ -693,99 +691,17 @@ def generate_sample(sample,
693691
calling_form = types.CallingForm.method_default(rpc)
694692

695693
v = Validator(rpc)
694+
# Tweak some small aspects of the sample to set sane defaults for optional
695+
# fields, add fields that are required for the template, and so forth.
696+
v.preprocess_sample(sample, api_schema)
696697
sample["request"] = v.validate_and_transform_request(calling_form,
697698
sample["request"])
698699
v.validate_response(sample["response"])
699700

700-
sample["package_name"] = api_schema.naming.warehouse_package_name
701-
702701
return sample_template.render(
703702
file_header=FILE_HEADER,
704703
sample=sample,
705704
imports=[],
706705
calling_form=calling_form,
707706
calling_form_enum=types.CallingForm,
708707
)
709-
710-
711-
def generate_manifest(
712-
fpaths_and_samples,
713-
base_path: str,
714-
api_schema,
715-
*,
716-
manifest_time: int = None
717-
) -> Tuple[str, yaml.Doc]:
718-
"""Generate a samplegen manifest for use by sampletest
719-
720-
Args:
721-
fpaths_and_samples (Iterable[Tuple[str, Mapping[str, Any]]]):
722-
The file paths and samples to be listed in the manifest
723-
base_path (str): The base directory where the samples are generated.
724-
api_schema (~.api.API): An API schema object.
725-
manifest_time (int): Optional. An override for the timestamp in the name of the manifest filename.
726-
Primarily used for testing.
727-
728-
Returns:
729-
Tuple[str, yaml.Doc]: The filename of the manifest and the manifest data as a dictionary.
730-
731-
"""
732-
733-
doc = yaml.Doc(
734-
[
735-
yaml.KeyVal("type", "manifest/samples"),
736-
yaml.KeyVal("schema_version", "3"),
737-
# TODO: make the environment configurable to allow other languages
738-
# to use the same basic machinery.
739-
yaml.Map(
740-
name="python",
741-
anchor_name="python",
742-
elements=[
743-
yaml.KeyVal("environment", "python"),
744-
yaml.KeyVal("bin", "python3"),
745-
yaml.KeyVal("base_path", base_path),
746-
yaml.KeyVal("invocation", "'{bin} {path} @args'"),
747-
],
748-
),
749-
yaml.Collection(
750-
name="samples",
751-
elements=[
752-
[ # type: ignore
753-
# Mypy doesn't correctly intuit the type of the
754-
# "region_tag" conditional expression.
755-
yaml.Alias("python"),
756-
yaml.KeyVal("sample", sample["id"]),
757-
yaml.KeyVal("path",
758-
"'{base_path}/%s'" % os.path.relpath(fpath,
759-
base_path)),
760-
(yaml.KeyVal("region_tag", sample["region_tag"])
761-
if "region_tag" in sample else
762-
yaml.Null),
763-
764-
]
765-
for fpath, sample in fpaths_and_samples
766-
],
767-
),
768-
]
769-
)
770-
771-
dt = time.gmtime(manifest_time)
772-
# TODO: allow other language configuration
773-
manifest_fname_template = (
774-
"{api}.{version}.python."
775-
"{year:04d}{month:02d}{day:02d}."
776-
"{hour:02d}{minute:02d}{second:02d}."
777-
"manifest.yaml"
778-
)
779-
780-
manifest_fname = manifest_fname_template.format(
781-
api=api_schema.naming.name,
782-
version=api_schema.naming.version,
783-
year=dt.tm_year,
784-
month=dt.tm_mon,
785-
day=dt.tm_mday,
786-
hour=dt.tm_hour,
787-
minute=dt.tm_min,
788-
second=dt.tm_sec,
789-
)
790-
791-
return manifest_fname, doc

packages/gapic-generator/gapic/samplegen_utils/types.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ class InvalidEnumVariant(SampleError):
7676
pass
7777

7878

79+
class InvalidSampleFpath(SampleError):
80+
pass
81+
82+
7983
class CallingForm(Enum):
8084
Request = auto()
8185
RequestPaged = auto()

0 commit comments

Comments
 (0)