Source code for gapic.generator.generator

# Copyright 2018 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from collections import OrderedDict
from typing import Dict, Mapping
import os
import re

import jinja2

from google.protobuf.compiler.plugin_pb2 import CodeGeneratorResponse

from gapic import utils
from gapic.generator import formatter
from gapic.generator import options
from gapic.samplegen import samplegen
from gapic.schema import api


[docs]class Generator: """A protoc code generator for client libraries. This class provides an interface for getting a :class:`~.plugin_pb2.CodeGeneratorResponse` for an :class:`~api.API` schema object (which it does through rendering templates). Args: opts (~.options.Options): An options instance. templates (str): Optional. Path to the templates to be rendered. If this is not provided, the templates included with this application are used. """ def __init__(self, opts: options.Options) -> None: # Create the jinja environment with which to render templates. self._env = jinja2.Environment( loader=jinja2.FileSystemLoader(searchpath=opts.templates), undefined=jinja2.StrictUndefined, extensions=["jinja2.ext.do"], ) # Add filters which templates require. self._env.filters['rst'] = utils.rst self._env.filters['snake_case'] = utils.to_snake_case self._env.filters['sort_lines'] = utils.sort_lines self._env.filters['wrap'] = utils.wrap self._env.filters['coerce_response_name'] = samplegen.coerce_response_name
[docs] def get_response(self, api_schema: api.API) -> CodeGeneratorResponse: """Return a :class:`~.CodeGeneratorResponse` for this library. This is a complete response to be written to (usually) stdout, and thus read by ``protoc``. Args: api_schema (~api.API): An API schema object. Returns: ~.CodeGeneratorResponse: A response describing appropriate files and contents. See ``plugin.proto``. """ output_files: Dict[str, CodeGeneratorResponse.File] = OrderedDict() # TODO: handle sample_templates specially, generate samples. sample_templates, client_templates = utils.partition( lambda fname: os.path.basename(fname) == samplegen.TEMPLATE_NAME, self._env.loader.list_templates()) # Iterate over each template and add the appropriate output files # based on that template. for template_name in client_templates: # Sanity check: Skip "private" templates. filename = template_name.split('/')[-1] if filename.startswith('_') and filename != '__init__.py.j2': continue # Append to the output files dictionary. output_files.update(self._render_template(template_name, api_schema=api_schema, )) # Return the CodeGeneratorResponse output. return CodeGeneratorResponse(file=[i for i in output_files.values()])
def _render_template( self, template_name: str, *, api_schema: api.API, ) -> Dict[str, CodeGeneratorResponse.File]: """Render the requested templates. Args: template_name (str): The template to be rendered. It is expected that these come from :class:`jinja2.FileSystemLoader`, and they should be able to be sent to the :meth:`jinja2.Environment.get_template` method. api_schema (~.api.API): An API schema object. Returns: Sequence[~.CodeGeneratorResponse.File]: A sequence of File objects for inclusion in the final response. """ answer: Dict[str, CodeGeneratorResponse.File] = OrderedDict() skip_subpackages = False # Sanity check: Rendering per service and per proto would be a # combinatorial explosion and is almost certainly not what anyone # ever wants. Error colorfully on it. if '$service' in template_name and '$proto' in template_name: raise ValueError('Template files may live under a $proto or ' '$service directory, but not both.') # If this template should be rendered for subpackages, process it # for all subpackages and set the strict flag (restricting what # services and protos we pull from for the remainder of the method). if '$sub' in template_name: for subpackage in api_schema.subpackages.values(): answer.update(self._render_template(template_name, api_schema=subpackage, )) skip_subpackages = True # If this template should be rendered once per proto, iterate over # all protos to be rendered if '$proto' in template_name: for proto in api_schema.protos.values(): if (skip_subpackages and proto.meta.address.subpackage != api_schema.subpackage_view): continue answer.update(self._get_file(template_name, api_schema=api_schema, proto=proto )) return answer # If this template should be rendered once per service, iterate # over all services to be rendered. if '$service' in template_name: for service in api_schema.services.values(): if (skip_subpackages and service.meta.address.subpackage != api_schema.subpackage_view): continue answer.update(self._get_file(template_name, api_schema=api_schema, service=service, )) return answer # This file is not iterating over anything else; return back # the one applicable file. answer.update(self._get_file(template_name, api_schema=api_schema)) return answer def _get_file(self, template_name: str, *, api_schema=api.API, **context: Mapping): """Render a template to a protobuf plugin File object.""" # Determine the target filename. fn = self._get_filename(template_name, api_schema=api_schema, context=context, ) # Render the file contents. cgr_file = CodeGeneratorResponse.File( content=formatter.fix_whitespace( self._env.get_template(template_name).render( api=api_schema, **context ), ), name=fn, ) # Sanity check: Do not render empty files. if (utils.empty(cgr_file.content) and not fn.endswith(('py.typed', '__init__.py'))): return {} # Return the filename and content in a length-1 dictionary # (because we track output files overall in a dictionary). return {fn: cgr_file} def _get_filename( self, template_name: str, *, api_schema: api.API, context: dict = None, ) -> str: """Return the appropriate output filename for this template. This entails running the template name through a series of replacements to replace the "filename variables" (``$name``, ``$service``, etc.). Additionally, any of these variables may be substituted with an empty value, and we should do the right thing in this case. (The exception to this is ``$service``, which is guaranteed to be set if it is needed.) Args: template_name (str): The filename of the template, from the filesystem, relative to ``templates/``. api_schema (~.api.API): An API schema object. context (Mapping): Additional context being sent to the template. Returns: str: The appropriate output filename. """ filename = template_name[:-len('.j2')] # Replace the $namespace variable. filename = filename.replace( '$namespace', os.path.sep.join([i.lower() for i in api_schema.naming.namespace]), ).lstrip(os.path.sep) # Replace the $name, $version, and $sub variables. filename = filename.replace('$name_$version', api_schema.naming.versioned_module_name) filename = filename.replace('$version', api_schema.naming.version) filename = filename.replace('$name', api_schema.naming.module_name) filename = filename.replace('$sub', '/'.join(api_schema.subpackage_view)) # Replace the $service variable if applicable. if context and 'service' in context: filename = filename.replace( '$service', context['service'].module_name, ) # Replace the $proto variable if appliable. # In the cases of protos, we also honor subpackages. if context and 'proto' in context: filename = filename.replace( '$proto', context['proto'].module_name, ) # Paths may have empty path segments if components are empty # (e.g. no $version); handle this. filename = re.sub(r'/+', '/', filename) # Done, return the filename. return filename
__all__ = ( 'Generator', )