782 lines
26 KiB
Python
782 lines
26 KiB
Python
# -*- coding: utf-8 -*- #
|
|
# Copyright 2017 Google LLC. All Rights Reserved.
|
|
#
|
|
# 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
|
|
#
|
|
# http://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.
|
|
|
|
"""Helpers to load commands from the filesystem."""
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import unicode_literals
|
|
|
|
import abc
|
|
import importlib
|
|
import os
|
|
import re
|
|
|
|
from googlecloudsdk.calliope import base
|
|
from googlecloudsdk.calliope import command_release_tracks
|
|
from googlecloudsdk.core import exceptions
|
|
from googlecloudsdk.core.util import pkg_resources
|
|
from ruamel import yaml
|
|
import six
|
|
|
|
PARTIALS_ATTRIBUTE = '_PARTIALS_'
|
|
PARTIALS_DIR = '_partials'
|
|
|
|
|
|
class CommandLoadFailure(Exception):
|
|
"""An exception for when a command or group module cannot be imported."""
|
|
|
|
def __init__(self, command, root_exception):
|
|
self.command = command
|
|
self.root_exception = root_exception
|
|
super(CommandLoadFailure, self).__init__(
|
|
'Problem loading {command}: {issue}.'.format(
|
|
command=command, issue=six.text_type(root_exception)
|
|
)
|
|
)
|
|
|
|
|
|
class LayoutException(Exception):
|
|
"""An exception for when a command or group .py file has the wrong types."""
|
|
|
|
|
|
class ReleaseTrackNotImplementedException(Exception):
|
|
"""An exception for when a command or group does not support a release track."""
|
|
|
|
|
|
class YamlCommandTranslator(six.with_metaclass(abc.ABCMeta, object)):
|
|
"""An interface to implement when registering a custom command loader."""
|
|
|
|
@abc.abstractmethod
|
|
def Translate(self, path, command_data):
|
|
"""Translates a yaml command into a calliope command.
|
|
|
|
Args:
|
|
path: [str], A list of group names that got us down to this command group
|
|
with respect to the CLI itself. This path should be used for things
|
|
like error reporting when a specific element in the tree needs to be
|
|
referenced.
|
|
command_data: dict, The parsed contents of the command spec from the yaml
|
|
file that corresponds to the release track being loaded.
|
|
|
|
Returns:
|
|
calliope.base.Command, A command class (not instance) that
|
|
implements the spec.
|
|
"""
|
|
pass
|
|
|
|
|
|
def FindSubElements(impl_paths, path):
|
|
"""Find all the sub groups and commands under this group.
|
|
|
|
Args:
|
|
impl_paths: [str], A list of file paths to the command implementation for
|
|
this group.
|
|
path: [str], A list of group names that got us down to this command group
|
|
with respect to the CLI itself. This path should be used for things like
|
|
error reporting when a specific element in the tree needs to be
|
|
referenced.
|
|
|
|
Raises:
|
|
CommandLoadFailure: If the command is invalid and cannot be loaded.
|
|
LayoutException: if there is a command or group with an illegal name.
|
|
|
|
Returns:
|
|
({str: [str]}, {str: [str]), A tuple of groups and commands found where each
|
|
item is a mapping from name to a list of paths that implement that command
|
|
or group. There can be multiple paths because a command or group could be
|
|
implemented in both python and yaml (for different release tracks).
|
|
"""
|
|
if len(impl_paths) > 1:
|
|
raise CommandLoadFailure(
|
|
'.'.join(path),
|
|
Exception('Command groups cannot be implemented in yaml'),
|
|
)
|
|
impl_path = impl_paths[0]
|
|
groups, commands = pkg_resources.ListPackage(
|
|
impl_path, extra_extensions=['.yaml']
|
|
)
|
|
return (
|
|
_GenerateElementInfo(impl_path, groups),
|
|
_GenerateElementInfo(impl_path, commands),
|
|
)
|
|
|
|
|
|
def _GenerateElementInfo(impl_path, names):
|
|
"""Generates the data a group needs to load sub elements.
|
|
|
|
Args:
|
|
impl_path: The file path to the command implementation for this group.
|
|
names: [str], The names of the sub groups or commands found in the group.
|
|
|
|
Raises:
|
|
LayoutException: if there is a command or group with an illegal name.
|
|
|
|
Returns:
|
|
{str: [str], A mapping from name to a list of paths that implement that
|
|
command or group. There can be multiple paths because a command or group
|
|
could be implemented in both python and yaml (for different release tracks).
|
|
"""
|
|
elements = {}
|
|
for name in names:
|
|
if re.search('[A-Z]', name):
|
|
raise LayoutException(
|
|
'Commands and groups cannot have capital letters: {0}.'.format(name)
|
|
)
|
|
cli_name = name[:-5] if name.endswith('.yaml') else name
|
|
sub_path = os.path.join(impl_path, name)
|
|
|
|
existing = elements.setdefault(cli_name, [])
|
|
existing.append(sub_path)
|
|
return elements
|
|
|
|
|
|
def LoadCommonType(
|
|
impl_paths,
|
|
path,
|
|
release_track,
|
|
construction_id,
|
|
is_command,
|
|
yaml_command_translator=None,
|
|
):
|
|
"""Loads a calliope command or group from a file.
|
|
|
|
Args:
|
|
impl_paths: [str], A list of file paths to the command implementation for
|
|
this group or command.
|
|
path: [str], A list of group names that got us down to this command group
|
|
with respect to the CLI itself. This path should be used for things like
|
|
error reporting when a specific element in the tree needs to be
|
|
referenced.
|
|
release_track: ReleaseTrack, The release track that we should load.
|
|
construction_id: str, A unique identifier for the CLILoader that is being
|
|
constructed.
|
|
is_command: bool, True if we are loading a command, False to load a group.
|
|
yaml_command_translator: YamlCommandTranslator, An instance of a translator
|
|
to use to load the yaml data.
|
|
|
|
Raises:
|
|
CommandLoadFailure: If the command is invalid and cannot be loaded.
|
|
|
|
Returns:
|
|
The base._Common class for the command or group.
|
|
"""
|
|
implementations = _GetAllImplementations(
|
|
impl_paths, path, construction_id, is_command, yaml_command_translator
|
|
)
|
|
return _ExtractReleaseTrackImplementation(
|
|
impl_paths[0], release_track, implementations
|
|
)()
|
|
|
|
|
|
def Cache(func):
|
|
cached_results = {}
|
|
|
|
def ReturnCachedOrCallFunc(*args):
|
|
try:
|
|
return cached_results[args]
|
|
except KeyError:
|
|
result = func(*args)
|
|
cached_results[args] = result
|
|
return result
|
|
|
|
return ReturnCachedOrCallFunc
|
|
|
|
|
|
@Cache
|
|
def _SafeLoadYamlFile(path):
|
|
return yaml.safe_load(pkg_resources.GetResourceFromFile(path))
|
|
|
|
|
|
@Cache
|
|
def _CustomLoadYamlFile(path):
|
|
return CreateYamlLoader(path).load(pkg_resources.GetResourceFromFile(path))
|
|
|
|
|
|
def _GetAllImplementations(
|
|
impl_paths, path, construction_id, is_command, yaml_command_translator
|
|
):
|
|
"""Gets all the release track command implementations.
|
|
|
|
Can load both python and yaml modules.
|
|
|
|
Args:
|
|
impl_paths: [str], A list of file paths to the command implementation for
|
|
this group or command.
|
|
path: [str], A list of group names that got us down to this command group
|
|
with respect to the CLI itself. This path should be used for things like
|
|
error reporting when a specific element in the tree needs to be
|
|
referenced.
|
|
construction_id: str, A unique identifier for the CLILoader that is being
|
|
constructed.
|
|
is_command: bool, True if we are loading a command, False to load a group.
|
|
yaml_command_translator: YamlCommandTranslator, An instance of a translator
|
|
to use to load the yaml data.
|
|
|
|
Raises:
|
|
CommandLoadFailure: If the command is invalid and cannot be loaded.
|
|
|
|
Returns:
|
|
[(func->base._Common, [base.ReleaseTrack])], A list of tuples that can be
|
|
passed to _ExtractReleaseTrackImplementation. Each item in this list
|
|
represents a command implementation. The first element is a function that
|
|
returns the implementation, and the second element is a list of release
|
|
tracks it is valid for.
|
|
"""
|
|
implementations = []
|
|
for impl_file in impl_paths:
|
|
if impl_file.endswith('.yaml'):
|
|
if not is_command:
|
|
raise CommandLoadFailure(
|
|
'.'.join(path),
|
|
Exception('Command groups cannot be implemented in yaml'),
|
|
)
|
|
if _IsCommandWithPartials(impl_file, path):
|
|
data = _LoadCommandWithPartials(impl_file, path)
|
|
else:
|
|
data = _CustomLoadYamlFile(impl_file)
|
|
implementations.extend(
|
|
(_ImplementationsFromYaml(path, data, yaml_command_translator))
|
|
)
|
|
else:
|
|
module = _GetModuleFromPath(impl_file, path, construction_id)
|
|
implementations.extend(
|
|
_ImplementationsFromModule(
|
|
module.__file__,
|
|
list(module.__dict__.values()),
|
|
is_command=is_command,
|
|
)
|
|
)
|
|
return implementations
|
|
|
|
|
|
def _IsCommandWithPartials(impl_file, path):
|
|
"""Checks if the YAML file is a command with partials.
|
|
|
|
Args:
|
|
impl_file: file path to the main YAML command implementation.
|
|
path: [str], A list of group names that got us down to this command group
|
|
with respect to the CLI itself. This path should be used for things like
|
|
error reporting when a specific element in the tree needs to be
|
|
referenced.
|
|
|
|
Raises:
|
|
CommandLoadFailure: If the command is invalid and should not be loaded.
|
|
|
|
Returns:
|
|
Whether or not it is a valid command with partials to load.
|
|
"""
|
|
found_partial_token = False
|
|
with pkg_resources.GetFileTextReaderByLine(impl_file) as file:
|
|
for line in file:
|
|
line = line.strip()
|
|
if not line or line.startswith('#'):
|
|
continue
|
|
if line == f'{PARTIALS_ATTRIBUTE}: true':
|
|
found_partial_token = True
|
|
elif found_partial_token:
|
|
raise CommandLoadFailure(
|
|
'.'.join(path),
|
|
Exception(
|
|
f'Command with {PARTIALS_ATTRIBUTE} attribute cannot have'
|
|
' extra content'
|
|
),
|
|
)
|
|
else:
|
|
break
|
|
|
|
return found_partial_token
|
|
|
|
|
|
def _LoadCommandWithPartials(impl_file, path):
|
|
"""Loads all YAML partials for a command with partials based on conventions.
|
|
|
|
Partial files are loaded using _CustomLoadYamlFile as normal YAML commands.
|
|
|
|
Conventions:
|
|
- Partials should be placed in subfolder `_partials`.
|
|
- File names of partials should match the main command name and follow this
|
|
format: _[command_name]_[version|release_track].yaml
|
|
- Release tracks should not be duplicatd across all partials.
|
|
|
|
Args:
|
|
impl_file: file path to the main YAML command implementation.
|
|
path: [str], A list of group names that got us down to this command group
|
|
with respect to the CLI itself. This path should be used for things like
|
|
error reporting when a specific element in the tree needs to be
|
|
referenced.
|
|
|
|
Returns:
|
|
List with data loaded from partial YAML files for the main command.
|
|
"""
|
|
file_name = os.path.basename(impl_file)
|
|
command_name = file_name[:-5] # strip .yaml
|
|
partials_dir = os.path.join(os.path.dirname(impl_file), PARTIALS_DIR)
|
|
partial_files = pkg_resources.GetFilesFromDirectory(
|
|
partials_dir, f'_{command_name}_*.yaml'
|
|
)
|
|
|
|
command_data_list = []
|
|
command_path = re.escape(os.path.join(partials_dir, f'_{command_name}'))
|
|
for partial_file in partial_files:
|
|
if re.match(fr'{command_path}_(alpha|beta|ga)\.yaml', partial_file):
|
|
command_data_list.extend(_CustomLoadYamlFile(partial_file))
|
|
|
|
_ValidateCommandWithPartials(command_data_list, path)
|
|
return command_data_list
|
|
|
|
|
|
def _ValidateCommandWithPartials(command_data_list, path):
|
|
"""Validates that the command with partials do not have duplicated tracks.
|
|
|
|
Args:
|
|
command_data_list: List with data loaded from all YAML partials.
|
|
path: [str], A list of group names that got us down to this command group
|
|
with respect to the CLI itself. This path should be used for things like
|
|
error reporting when a specific element in the tree needs to be
|
|
referenced.
|
|
|
|
Raises:
|
|
CommandLoadFailure: If the command is invalid and should not be loaded.
|
|
"""
|
|
release_tracks = set()
|
|
for command_data in command_data_list:
|
|
for release_track in command_data['release_tracks']:
|
|
if release_track in release_tracks:
|
|
raise CommandLoadFailure(
|
|
'.'.join(path),
|
|
Exception(
|
|
'Command with partials cannot have duplicated release tracks.'
|
|
f' Found multiple [{release_track}s]'
|
|
),
|
|
)
|
|
else:
|
|
release_tracks.add(release_track)
|
|
|
|
|
|
def CreateYamlLoader(impl_path):
|
|
"""Creates a custom yaml loader that handles includes from common data.
|
|
|
|
Args:
|
|
impl_path: str, The path to the file we are loading data from.
|
|
|
|
Returns:
|
|
yaml.Loader, A yaml loader to use.
|
|
"""
|
|
common_file_path = os.path.join(os.path.dirname(impl_path), '__init__.yaml')
|
|
common_data = None
|
|
try:
|
|
common_data = _SafeLoadYamlFile(common_file_path)
|
|
except IOError:
|
|
pass
|
|
|
|
class Constructor(yaml.Constructor):
|
|
"""A custom yaml constructor.
|
|
|
|
It adds 2 different import capabilities. Assuming __init__.yaml has the
|
|
contents:
|
|
|
|
foo:
|
|
a: b
|
|
c: d
|
|
|
|
baz:
|
|
- e: f
|
|
- g: h
|
|
|
|
The first uses a custom constructor to insert data into your current file,
|
|
so:
|
|
|
|
bar: !COMMON foo.a
|
|
|
|
results in:
|
|
|
|
bar: b
|
|
|
|
The second mechanism overrides construct_mapping and construct_sequence to
|
|
post process the data and replace the merge macro with keys from the other
|
|
file. We can't use the custom constructor for this as well because the
|
|
merge key type in yaml is processed before custom constructors which makes
|
|
importing and merging not possible. So:
|
|
|
|
bar:
|
|
_COMMON_: foo
|
|
i: j
|
|
|
|
results in:
|
|
|
|
bar:
|
|
a: b
|
|
c: d
|
|
i: j
|
|
|
|
This can also be used to merge list contexts, so:
|
|
|
|
bar:
|
|
- _COMMON_baz
|
|
- i: j
|
|
|
|
results in:
|
|
|
|
bar:
|
|
- e: f
|
|
- g: h
|
|
- i: j
|
|
|
|
You may also use the !REF and _REF_ directives in the same way. Instead of
|
|
pulling from the common file, they can pull from an arbitrary yaml file
|
|
somewhere in the googlecloudsdk tree. The syntax looks like:
|
|
|
|
bar: !REF googlecloudsdk.foo.bar:a.b.c
|
|
|
|
This will load googlecloudsdk/foo/bar.yaml and from that file return the
|
|
a.b.c nested attribute.
|
|
"""
|
|
|
|
INCLUDE_COMMON_MACRO = '!COMMON'
|
|
MERGE_COMMON_MACRO = '_COMMON_'
|
|
INCLUDE_REF_MACRO = '!REF'
|
|
MERGE_REF_MACRO = '_REF_'
|
|
|
|
def construct_mapping(self, *args, **kwargs):
|
|
data = super(Constructor, self).construct_mapping(*args, **kwargs)
|
|
data = self._ConstructMappingHelper(
|
|
Constructor.MERGE_COMMON_MACRO, self._GetCommonData, data
|
|
)
|
|
return self._ConstructMappingHelper(
|
|
Constructor.MERGE_REF_MACRO, self._GetRefData, data
|
|
)
|
|
|
|
def _ConstructMappingHelper(self, macro, source_func, data):
|
|
attribute_path = data.pop(macro, None)
|
|
if not attribute_path:
|
|
return data
|
|
|
|
modified_data = {}
|
|
for path in attribute_path.split(','):
|
|
modified_data.update(source_func(path))
|
|
# Add the explicit data last so it can override the imports.
|
|
modified_data.update(data)
|
|
return modified_data
|
|
|
|
def construct_sequence(self, *args, **kwargs):
|
|
data = super(Constructor, self).construct_sequence(*args, **kwargs)
|
|
data = self._ConstructSequenceHelper(
|
|
Constructor.MERGE_COMMON_MACRO, self._GetCommonData, data
|
|
)
|
|
return self._ConstructSequenceHelper(
|
|
Constructor.MERGE_REF_MACRO, self._GetRefData, data
|
|
)
|
|
|
|
def _ConstructSequenceHelper(self, macro, source_func, data):
|
|
new_list = []
|
|
for i in data:
|
|
if isinstance(i, six.string_types) and i.startswith(macro):
|
|
attribute_path = i[len(macro) :]
|
|
for path in attribute_path.split(','):
|
|
new_list.extend(source_func(path))
|
|
else:
|
|
new_list.append(i)
|
|
return new_list
|
|
|
|
def IncludeCommon(self, node):
|
|
attribute_path = self.construct_scalar(node)
|
|
return self._GetCommonData(attribute_path)
|
|
|
|
def IncludeRef(self, node):
|
|
attribute_path = self.construct_scalar(node)
|
|
return self._GetRefData(attribute_path)
|
|
|
|
def _GetCommonData(self, attribute_path):
|
|
if not common_data:
|
|
raise LayoutException(
|
|
'Command [{}] references [common command] data but it does not '
|
|
'exist.'.format(impl_path)
|
|
)
|
|
return self._GetAttribute(common_data, attribute_path, 'common command')
|
|
|
|
def _GetRefData(self, path):
|
|
"""Loads the YAML data from the given reference.
|
|
|
|
A YAML reference must refer to a YAML file and an attribute within that
|
|
file to extract.
|
|
|
|
Args:
|
|
path: str, The path of the YAML file to import. It must be in the form
|
|
of package.module:attribute.attribute, where the module path is
|
|
separated from the sub attributes within the YAML by a ':'.
|
|
|
|
Raises:
|
|
LayoutException: If the given module or attribute cannot be loaded.
|
|
|
|
Returns:
|
|
The referenced YAML data.
|
|
"""
|
|
parts = path.split(':')
|
|
if len(parts) != 2:
|
|
raise LayoutException(
|
|
'Invalid Yaml reference: [{}]. References must be in the format: '
|
|
'path(.path)+:attribute(.attribute)*'.format(path)
|
|
)
|
|
path_segments = parts[0].split('.')
|
|
try:
|
|
root_module = importlib.import_module(path_segments[0])
|
|
yaml_path = (
|
|
os.path.join(
|
|
os.path.dirname(root_module.__file__), *path_segments[1:]
|
|
)
|
|
+ '.yaml'
|
|
)
|
|
data = _SafeLoadYamlFile(yaml_path)
|
|
except (ImportError, IOError) as e:
|
|
raise LayoutException(
|
|
'Failed to load Yaml reference file [{}]: {}'.format(parts[0], e)
|
|
)
|
|
|
|
return self._GetAttribute(data, parts[1], yaml_path)
|
|
|
|
def _GetAttribute(self, data, attribute_path, location):
|
|
value = data
|
|
for attribute in attribute_path.split('.'):
|
|
value = value.get(attribute, None)
|
|
if not value:
|
|
raise LayoutException(
|
|
'Command [{}] references [{}] data attribute [{}] in '
|
|
'path [{}] but it does not exist.'.format(
|
|
impl_path, location, attribute, attribute_path
|
|
)
|
|
)
|
|
return value
|
|
|
|
loader = yaml.YAML()
|
|
loader.Constructor = Constructor
|
|
loader.constructor.add_constructor(
|
|
Constructor.INCLUDE_COMMON_MACRO, Constructor.IncludeCommon
|
|
)
|
|
loader.constructor.add_constructor(
|
|
Constructor.INCLUDE_REF_MACRO, Constructor.IncludeRef
|
|
)
|
|
return loader
|
|
|
|
|
|
def _GetModuleFromPath(impl_file, path, construction_id):
|
|
"""Import the module and dig into it to return the namespace we are after.
|
|
|
|
Import the module relative to the top level directory. Then return the
|
|
actual module corresponding to the last bit of the path.
|
|
|
|
Args:
|
|
impl_file: str, The path to the file this was loaded from (for error
|
|
reporting).
|
|
path: [str], A list of group names that got us down to this command group
|
|
with respect to the CLI itself. This path should be used for things like
|
|
error reporting when a specific element in the tree needs to be
|
|
referenced.
|
|
construction_id: str, A unique identifier for the CLILoader that is being
|
|
constructed.
|
|
|
|
Returns:
|
|
The imported module.
|
|
"""
|
|
# Make sure this module name never collides with any real module name.
|
|
# Use the CLI naming path, so values are always unique.
|
|
name_to_give = '__calliope__command__.{construction_id}.{name}'.format(
|
|
construction_id=construction_id, name='.'.join(path).replace('-', '_')
|
|
)
|
|
try:
|
|
return pkg_resources.GetModuleFromPath(name_to_give, impl_file)
|
|
# pylint:disable=broad-except, We really do want to catch everything here,
|
|
# because if any exceptions make it through for any single command or group
|
|
# file, the whole CLI will not work. Instead, just log whatever it is.
|
|
except Exception as e:
|
|
exceptions.reraise(CommandLoadFailure('.'.join(path), e))
|
|
|
|
|
|
def _ImplementationsFromModule(mod_file, module_attributes, is_command):
|
|
"""Gets all the release track command implementations from the module.
|
|
|
|
Args:
|
|
mod_file: str, The __file__ attribute of the module resulting from importing
|
|
the file containing a command.
|
|
module_attributes: The __dict__.values() of the module.
|
|
is_command: bool, True if we are loading a command, False to load a group.
|
|
|
|
Raises:
|
|
LayoutException: If there is not exactly one type inheriting CommonBase.
|
|
|
|
Returns:
|
|
[(func->base._Common, [base.ReleaseTrack])], A list of tuples that can be
|
|
passed to _ExtractReleaseTrackImplementation. Each item in this list
|
|
represents a command implementation. The first element is a function that
|
|
returns the implementation, and the second element is a list of release
|
|
tracks it is valid for.
|
|
"""
|
|
commands = []
|
|
groups = []
|
|
|
|
# Collect all the registered groups and commands.
|
|
for command_or_group in module_attributes:
|
|
if getattr(command_or_group, 'IS_COMMAND', False):
|
|
commands.append(command_or_group)
|
|
elif getattr(command_or_group, 'IS_COMMAND_GROUP', False):
|
|
groups.append(command_or_group)
|
|
|
|
if is_command:
|
|
if groups:
|
|
# Ensure that there are no groups if we are expecting a command.
|
|
raise LayoutException(
|
|
'You cannot define groups [{0}] in a command file: [{1}]'.format(
|
|
', '.join([g.__name__ for g in groups]), mod_file
|
|
)
|
|
)
|
|
if not commands:
|
|
# Make sure we found a command.
|
|
raise LayoutException(
|
|
'No commands defined in file: [{0}]'.format(mod_file)
|
|
)
|
|
commands_or_groups = commands
|
|
else:
|
|
# Ensure that there are no commands if we are expecting a group.
|
|
if commands:
|
|
raise LayoutException(
|
|
'You cannot define commands [{0}] in a command group file: [{1}]'
|
|
.format(', '.join([c.__name__ for c in commands]), mod_file)
|
|
)
|
|
if not groups:
|
|
# Make sure we found a group.
|
|
raise LayoutException(
|
|
'No command groups defined in file: [{0}]'.format(mod_file)
|
|
)
|
|
commands_or_groups = groups
|
|
|
|
# pylint:disable=undefined-loop-variable, Linter is just wrong here.
|
|
# We need to use a default param on the lambda so that it captures the value
|
|
# of the variable at the time in the loop or else the closure will just have
|
|
# the last value that was iterated on.
|
|
return [(lambda c=c: c, c.ValidReleaseTracks()) for c in commands_or_groups]
|
|
|
|
|
|
def _ImplementationsFromYaml(path, data, yaml_command_translator):
|
|
"""Gets all the release track command implementations from the yaml file.
|
|
|
|
Args:
|
|
path: [str], A list of group names that got us down to this command group
|
|
with respect to the CLI itself. This path should be used for things like
|
|
error reporting when a specific element in the tree needs to be
|
|
referenced.
|
|
data: dict, The loaded yaml data.
|
|
yaml_command_translator: YamlCommandTranslator, An instance of a translator
|
|
to use to load the yaml data.
|
|
|
|
Raises:
|
|
CommandLoadFailure: If the command is invalid and cannot be loaded.
|
|
|
|
Returns:
|
|
[(func->base._Common, [base.ReleaseTrack])], A list of tuples that can be
|
|
passed to _ExtractReleaseTrackImplementation. Each item in this list
|
|
represents a command implementation. The first element is a function that
|
|
returns the implementation, and the second element is a list of release
|
|
tracks it is valid for.
|
|
"""
|
|
if not yaml_command_translator:
|
|
raise CommandLoadFailure(
|
|
'.'.join(path),
|
|
Exception('No yaml command translator has been registered'),
|
|
)
|
|
|
|
# pylint:disable=undefined-loop-variable, Linter is just wrong here.
|
|
# We need to use a default param on the lambda so that it captures the value
|
|
# of the variable at the time in the loop or else the closure will just have
|
|
# the last value that was iterated on.
|
|
implementations = [
|
|
(
|
|
lambda i=i: yaml_command_translator.Translate(path, i),
|
|
{base.ReleaseTrack.FromId(t) for t in i.get('release_tracks', [])},
|
|
)
|
|
for i in command_release_tracks.SeparateDeclarativeCommandTracks(data)
|
|
]
|
|
return implementations
|
|
|
|
|
|
def _ExtractReleaseTrackImplementation(
|
|
impl_file, expected_track, implementations
|
|
):
|
|
"""Validates and extracts the correct implementation of the command or group.
|
|
|
|
Args:
|
|
impl_file: str, The path to the file this was loaded from (for error
|
|
reporting).
|
|
expected_track: base.ReleaseTrack, The release track we are trying to load.
|
|
implementations: [(func->base._Common, [base.ReleaseTrack])], A list of
|
|
tuples where each item in this list represents a command implementation.
|
|
The first element is a function that returns the implementation, and the
|
|
second element is a list of release tracks it is valid for.
|
|
|
|
Raises:
|
|
LayoutException: If there is not exactly one type inheriting
|
|
CommonBase.
|
|
ReleaseTrackNotImplementedException: If there is no command or group
|
|
implementation for the request release track.
|
|
|
|
Returns:
|
|
object, The single implementation that matches the expected release track.
|
|
"""
|
|
# We found a single thing, if it's valid for this track, return it.
|
|
if len(implementations) == 1:
|
|
impl, valid_tracks = implementations[0]
|
|
# If there is a single thing defined, and it does not declare any valid
|
|
# tracks, just assume it is enabled for all tracks that it's parent is.
|
|
if not valid_tracks or expected_track in valid_tracks:
|
|
return impl
|
|
raise ReleaseTrackNotImplementedException(
|
|
'No implementation for release track [{0}] for element: [{1}]'.format(
|
|
expected_track.id, impl_file
|
|
)
|
|
)
|
|
|
|
# There was more than one thing found, make sure there are no conflicts.
|
|
implemented_release_tracks = set()
|
|
for impl, valid_tracks in implementations:
|
|
# When there are multiple definitions, they need to explicitly register
|
|
# their track to keep things sane.
|
|
if not valid_tracks:
|
|
raise LayoutException(
|
|
'Multiple implementations defined for element: [{0}]. Each must '
|
|
'explicitly declare valid release tracks.'.format(impl_file)
|
|
)
|
|
# Make sure no two classes define the same track.
|
|
duplicates = implemented_release_tracks & valid_tracks
|
|
if duplicates:
|
|
raise LayoutException(
|
|
'Multiple definitions for release tracks [{0}] for element: [{1}]'
|
|
.format(', '.join([six.text_type(d) for d in duplicates]), impl_file)
|
|
)
|
|
implemented_release_tracks |= valid_tracks
|
|
|
|
valid_commands_or_groups = [
|
|
impl
|
|
for impl, valid_tracks in implementations
|
|
if expected_track in valid_tracks
|
|
]
|
|
# We know there is at most 1 because of the above check.
|
|
if len(valid_commands_or_groups) != 1:
|
|
raise ReleaseTrackNotImplementedException(
|
|
'No implementation for release track [{0}] for element: [{1}]'.format(
|
|
expected_track.id, impl_file
|
|
)
|
|
)
|
|
|
|
return valid_commands_or_groups[0]
|