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.

import collections
import os
import re
from typing import Any, Iterable, Mapping, Sequence, Tuple

import jinja2

from google.protobuf.compiler.plugin_pb2 import CodeGeneratorResponse

from gapic import utils
from gapic.generator import formatter
from gapic.generator import loader
from gapic.schema import api


[docs]class Generator: """A protoc code generator for client libraries. This class receives a :class:`~.api.API`, a representation of the API schema, and provides an interface for getting a :class:`~.plugin_pb2.CodeGeneratorResponse` (which it does through rendering templates). Args: api_schema (~.API): An API schema object, which is sent to every template as the ``api`` variable. 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, api_schema: api.API, *, templates: str = None) -> None: self._api = api_schema # If explicit templates were not provided, use our default. if not templates: templates = os.path.join( os.path.realpath(os.path.dirname(__file__)), '..', 'templates', ) # Create the jinja environment with which to render templates. self._env = jinja2.Environment( loader=loader.TemplateLoader(searchpath=templates), undefined=jinja2.StrictUndefined, ) # Add filters which templates require. self._env.filters['rst'] = utils.rst self._env.filters['snake_case'] = utils.to_snake_case self._env.filters['wrap'] = utils.wrap
[docs] def get_response(self) -> CodeGeneratorResponse: """Return a :class:`~.CodeGeneratorResponse` for this library. This is a complete response to be written to (usually) stdout, and thus read by ``protoc``. Returns: ~.CodeGeneratorResponse: A response describing appropriate files and contents. See ``plugin.proto``. """ output_files = collections.OrderedDict() # Some templates are rendered once per API client library. # These are generally boilerplate packaging and metadata files. output_files.update( self._render_templates(self._env.loader.api_templates), ) # Some templates are rendered once per proto (and API may have # one or more protos). for proto in self._api.protos.values(): if not proto.file_to_generate: continue output_files.update(self._render_templates( self._env.loader.proto_templates, additional_context={'proto': proto}, )) # Some templates are rendered once per service (an API may have # one or more services). for service in self._api.services.values(): output_files.update(self._render_templates( self._env.loader.service_templates, additional_context={'service': service}, )) # Return the CodeGeneratorResponse output. return CodeGeneratorResponse(file=[i for i in output_files.values()])
def _render_templates( self, templates: Iterable[str], *, additional_context: Mapping[str, Any] = None, ) -> Sequence[CodeGeneratorResponse.File]: """Render the requested templates. Args: templates (Iterable[str]): The set of templates to be rendered. It is expected that these come from the methods on :class:`~.loader.TemplateLoader`, and they should be able to be set to the :meth:`jinja2.Environment.get_template` method. additional_context (Mapping[str, Any]): Additional variables to be sent to the templates. The ``api`` variable is always available. Returns: Sequence[~.CodeGeneratorResponse.File]: A sequence of File objects for inclusion in the final response. """ answer = collections.OrderedDict() context = additional_context or {} # Iterate over the provided templates and generate a File object # for each. for template_name in templates: for fn in self._get_filenames(template_name, context=context): # Generate the File object. answer[fn] = CodeGeneratorResponse.File( content=formatter.fix_whitespace( self._env.get_template(template_name).render( api=self._api, **context ), ), name=fn, ) # Done; return the File objects based on these templates. return answer def _get_filenames( self, template_name: str, *, context: dict = None, ) -> Tuple[str]: """Return the appropriate output filenames 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/``. context (Mapping): Additional context being sent to the template. Returns: Tuple[str]: The appropriate output filenames. """ filename = template_name[:-len('.j2')] # Special case: If the filename is `$namespace/__init__.py`, we # need this exact file to be part of every individual directory # in the namespace tree. Handle this special case. # # For more information, see: # https://packaging.python.org/guides/packaging-namespace-packages/ if filename == os.path.join('$namespace', '__init__.py'): return tuple([ os.path.sep.join(i.split('.') + ['__init__.py']) for i in self._api.naming.namespace_packages ]) # Replace the $namespace variable. filename = filename.replace( '$namespace', os.path.sep.join([i.lower() for i in self._api.naming.namespace]), ).lstrip(os.path.sep) # Replace the $name and $version variables. filename = filename.replace('$name_$version', self._api.naming.versioned_module_name) filename = filename.replace('$version', self._api.naming.version) filename = filename.replace('$name', self._api.naming.module_name) # 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, ).replace( '$sub', '/'.join(context['proto'].meta.address.subpackage), ) # 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', )