feat: Add new gcloud commands, API clients, and third-party libraries across various services.

This commit is contained in:
2026-01-01 20:26:35 +01:00
parent 5e23cbece0
commit a19e592eb7
25221 changed files with 8324611 additions and 0 deletions

View File

@@ -0,0 +1,586 @@
# -*- coding: utf-8 -*- #
# Copyright 2018 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.
"""completers for resource library."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import collections
from apitools.base.protorpclite import messages
from googlecloudsdk.api_lib.util import resource as resource_lib # pylint: disable=unused-import
from googlecloudsdk.command_lib.util import completers
from googlecloudsdk.command_lib.util.apis import arg_utils
from googlecloudsdk.command_lib.util.apis import registry
from googlecloudsdk.command_lib.util.concepts import resource_parameter_info
from googlecloudsdk.core import exceptions
from googlecloudsdk.core import log
from googlecloudsdk.core import properties
from googlecloudsdk.core import resources
import six
DEFAULT_ID_FIELD = 'name'
_PROJECTS_COLLECTION = 'cloudresourcemanager.projects'
_PROJECT_ID_FIELD = 'projectId'
class Error(exceptions.Error):
"""Base error class for this module."""
class ParentTranslator(object):
"""Translates parent collections for completers.
Attributes:
collection: str, the collection name.
param_translation: {str: str}, lookup from the params of the child
collection to the params of the special parent collection. If None,
then the collections match and translate methods are a no-op.
"""
def __init__(self, collection, param_translation=None):
self.collection = collection
self.param_translation = param_translation or {}
def ToChildParams(self, params):
"""Translate from original parent params to params that match the child."""
if self.param_translation:
for orig_param, new_param in six.iteritems(self.param_translation):
params[orig_param] = params.get(new_param)
del params[new_param]
return params
def MessageResourceMap(self, message, ref):
"""Get dict for translating parent params into the given message type."""
message_resource_map = {}
# Parse resource with any params in the translator that are needed for the
# request.
for orig_param, special_param in six.iteritems(self.param_translation):
try:
message.field_by_name(orig_param)
# The field is not found, meaning that the original param isn't in the
# message.
except KeyError:
continue
message_resource_map[orig_param] = getattr(ref, special_param, None)
return message_resource_map
def Parse(self, parent_params, parameter_info, aggregations_dict):
"""Parse the parent resource from parameter info and aggregations.
Args:
parent_params: [str], a list of params in the current collection's parent
collection.
parameter_info: the runtime ResourceParameterInfo object.
aggregations_dict: {str: str}, a dict of params to values that are
being aggregated from earlier updates.
Returns:
resources.Resource | None, the parsed parent reference or None if there
is not enough information to parse.
"""
param_values = {
self.param_translation.get(p, p): parameter_info.GetValue(p)
for p in parent_params}
for p, value in six.iteritems(aggregations_dict):
translated_name = self.param_translation.get(p, p)
if value and not param_values.get(translated_name, None):
param_values[translated_name] = value
try:
return resources.Resource(
resources.REGISTRY,
collection_info=resources.REGISTRY.GetCollectionInfo(self.collection),
subcollection='',
param_values=param_values,
endpoint_url=None)
# Not all completion list calls may need to have a parent, so even if we
# can't parse a parent, we log the error and attempt to send an update call
# without one. (Any error returned by the API will be raised.)
except resources.Error as e:
log.info(six.text_type(e).rstrip())
return None
# A map from parent params (in original resource parser order, joined with '.')
# to special collections. If the original params are different from the special
# collection, the param_translator is used to translate back and forth between
# the original params and the special collection.
_PARENT_TRANSLATORS = {
'projectsId': ParentTranslator(_PROJECTS_COLLECTION,
{'projectsId': _PROJECT_ID_FIELD}),
'projectId': ParentTranslator(_PROJECTS_COLLECTION)}
class CollectionConfig(collections.namedtuple(
'CollectionConfig',
[
# static params are used to build the List request when updating
# the cache (equivalent to completion_request_params in AttributeConfig
# objects)
'static_params',
# Configures the ID field that is used to parse the results of a List
# request when updating the cache. Equivalent to completion_id_field
# in AttributeConfig objects.
'id_field',
# Configures the param name for the completer.
'param_name']
)):
"""Stores data about special collections for configuring completion."""
# This maps special collections to configuration for CompleterInfo objects
# rather than using configuration from the parent resource's collection.
# Currently only covers projects.
_SPECIAL_COLLECTIONS_MAP = {
_PROJECTS_COLLECTION: CollectionConfig({'filter': 'lifecycleState:ACTIVE'},
_PROJECT_ID_FIELD,
_PROJECT_ID_FIELD)}
class ResourceArgumentCompleter(completers.ResourceCompleter):
"""A completer for an argument that's part of a resource argument."""
def __init__(self, resource_spec, collection_info, method,
static_params=None, id_field=None, param=None, **kwargs):
"""Initializes."""
self.resource_spec = resource_spec
self._method = method
self._static_params = static_params or {}
self.id_field = id_field or DEFAULT_ID_FIELD
collection_name = collection_info.full_name
api_version = collection_info.api_version
super(ResourceArgumentCompleter, self).__init__(
collection=collection_name,
api_version=api_version,
param=param,
parse_all=True,
**kwargs)
@property
def method(self):
"""Gets the list method for the collection.
Returns:
googlecloudsdk.command_lib.util.apis.registry.APIMethod, the method.
"""
return self._method
def _ParentParams(self):
"""Get the parent params of the collection."""
return self.collection_info.GetParams('')[:-1]
def _GetUpdaters(self):
"""Helper function to build dict of updaters."""
# Find the attribute that matches the final param of the collection for this
# completer.
final_param = self.collection_info.GetParams('')[-1]
for i, attribute in enumerate(self.resource_spec.attributes):
if self.resource_spec.ParamName(attribute.name) == final_param:
attribute_idx = i
break
else:
attribute_idx = 0
updaters = {}
for i, attribute in enumerate(
self.resource_spec.attributes[:attribute_idx]):
completer = CompleterForAttribute(self.resource_spec, attribute.name)
if completer:
updaters[self.resource_spec.ParamName(attribute.name)] = (completer,
True)
else:
updaters[self.resource_spec.ParamName(attribute.name)] = (None,
False)
return updaters
def ParameterInfo(self, parsed_args, argument):
"""Builds a ResourceParameterInfo object.
Args:
parsed_args: the namespace.
argument: unused.
Returns:
ResourceParameterInfo, the parameter info for runtime information.
"""
resource_info = parsed_args.CONCEPTS.ArgNameToConceptInfo(argument.dest)
updaters = self._GetUpdaters()
return resource_parameter_info.ResourceParameterInfo(
resource_info, parsed_args, argument, updaters=updaters,
collection=self.collection)
def ValidateAttributeSources(self, aggregations):
"""Validates that parent attributes values exitst before making request."""
parameters_needing_resolution = set([p.name for p in self.parameters[:-1]])
resolved_parameters = set([a.name for a in aggregations])
# attributes can also be resolved by completers
for attribute in self.resource_spec.attributes:
if CompleterForAttribute(self.resource_spec, attribute.name):
resolved_parameters.add(
self.resource_spec.attribute_to_params_map[attribute.name])
return parameters_needing_resolution.issubset(resolved_parameters)
def Update(self, parameter_info, aggregations):
if self.method is None:
return None
if not self.ValidateAttributeSources(aggregations):
return None
log.info(
'Cache query parameters={} aggregations={}'
'resource info={}'.format(
[(p, parameter_info.GetValue(p))
for p in self.collection_info.GetParams('')],
[(p.name, p.value) for p in aggregations],
parameter_info.resource_info.attribute_to_args_map))
parent_translator = self._GetParentTranslator(parameter_info, aggregations)
try:
query = self.BuildListQuery(parameter_info, aggregations,
parent_translator=parent_translator)
except Exception as e: # pylint: disable=broad-except
if properties.VALUES.core.print_completion_tracebacks.GetBool():
raise
log.info(six.text_type(e).rstrip())
raise Error('Could not build query to list completions: {} {}'.format(
type(e), six.text_type(e).rstrip()))
try:
response = self.method.Call(query)
response_collection = self.method.collection
items = [self._ParseResponse(r, response_collection,
parameter_info=parameter_info,
aggregations=aggregations,
parent_translator=parent_translator)
for r in response]
log.info('cache items={}'.format(
[i.RelativeName() for i in items]))
except Exception as e: # pylint: disable=broad-except
if properties.VALUES.core.print_completion_tracebacks.GetBool():
raise
log.info(six.text_type(e).rstrip())
# Give user more information if they hit an apitools validation error,
# which probably means that they haven't provided enough information
# for us to complete.
if isinstance(e, messages.ValidationError):
raise Error('Update query failed, may not have enough information to '
'list existing resources: {} {}'.format(
type(e), six.text_type(e).rstrip()))
raise Error('Update query [{}]: {} {}'.format(
query, type(e), six.text_type(e).rstrip()))
return [self.StringToRow(item.RelativeName()) for item in items]
def _ParseResponse(self, response, response_collection,
parameter_info=None, aggregations=None,
parent_translator=None):
"""Gets a resource ref from a single item in a list response."""
param_values = self._GetParamValuesFromParent(
parameter_info, aggregations=aggregations,
parent_translator=parent_translator)
param_names = response_collection.detailed_params
for param in param_names:
val = getattr(response, param, None)
if val is not None:
param_values[param] = val
line = getattr(response, self.id_field, '')
return resources.REGISTRY.Parse(
line, collection=response_collection.full_name, params=param_values)
def _GetParamValuesFromParent(self, parameter_info, aggregations=None,
parent_translator=None):
parent_ref = self.GetParent(parameter_info, aggregations=aggregations,
parent_translator=parent_translator)
if not parent_ref:
return {}
params = parent_ref.AsDict()
if parent_translator:
return parent_translator.ToChildParams(params)
return params
def _GetAggregationsValuesDict(self, aggregations):
"""Build a {str: str} dict of name to value for aggregations."""
aggregations_dict = {}
aggregations = [] if aggregations is None else aggregations
for aggregation in aggregations:
if aggregation.value:
aggregations_dict[aggregation.name] = aggregation.value
return aggregations_dict
def BuildListQuery(self, parameter_info, aggregations=None,
parent_translator=None):
"""Builds a list request to list values for the given argument.
Args:
parameter_info: the runtime ResourceParameterInfo object.
aggregations: a list of _RuntimeParameter objects.
parent_translator: a ParentTranslator object if needed.
Returns:
The apitools request.
"""
method = self.method
if method is None:
return None
message = method.GetRequestType()()
for field, value in six.iteritems(self._static_params):
arg_utils.SetFieldInMessage(message, field, value)
parent = self.GetParent(parameter_info, aggregations=aggregations,
parent_translator=parent_translator)
if not parent:
return message
message_resource_map = {}
if parent_translator:
message_resource_map = parent_translator.MessageResourceMap(
message, parent)
arg_utils.ParseResourceIntoMessage(
parent, method, message,
message_resource_map=message_resource_map, is_primary_resource=True)
return message
def _GetParentTranslator(self, parameter_info, aggregations=None):
"""Get a special parent translator if needed and available."""
aggregations_dict = self._GetAggregationsValuesDict(aggregations)
param_values = self._GetRawParamValuesForParent(
parameter_info, aggregations_dict=aggregations_dict)
try:
self._ParseDefaultParent(param_values)
# If there's no error, we don't need a translator.
return None
except resources.ParentCollectionResolutionException:
# Check the parent params against the _PARENT_TRANSLATORS dict, using the
# parent params (joined by '.' in original resource parser order) as a
# key.
key = '.'.join(self._ParentParams())
if key in _PARENT_TRANSLATORS:
return _PARENT_TRANSLATORS.get(key)
# Errors will be raised and logged later when actually parsing the parent.
except resources.Error:
return None
def _GetRawParamValuesForParent(self, parameter_info, aggregations_dict=None):
"""Get raw param values for the resource in prep for parsing parent."""
param_values = {p: parameter_info.GetValue(p) for p in self._ParentParams()}
for name, value in six.iteritems(aggregations_dict or {}):
if value and not param_values.get(name, None):
param_values[name] = value
final_param = self.collection_info.GetParams('')[-1]
if param_values.get(final_param, None) is None:
param_values[final_param] = 'fake' # Stripped when we get the parent.
return param_values
def _ParseDefaultParent(self, param_values):
"""Parse the parent for a resource using default collection."""
resource = resources.Resource(
resources.REGISTRY,
collection_info=self.collection_info,
subcollection='',
param_values=param_values,
endpoint_url=None)
return resource.Parent()
def GetParent(self, parameter_info, aggregations=None,
parent_translator=None):
"""Gets the parent reference of the parsed parameters.
Args:
parameter_info: the runtime ResourceParameterInfo object.
aggregations: a list of _RuntimeParameter objects.
parent_translator: a ParentTranslator for translating to a special
parent collection, if needed.
Returns:
googlecloudsdk.core.resources.Resource | None, the parent resource or None
if no parent was found.
"""
aggregations_dict = self._GetAggregationsValuesDict(aggregations)
param_values = self._GetRawParamValuesForParent(
parameter_info, aggregations_dict=aggregations_dict)
try:
if not parent_translator:
return self._ParseDefaultParent(param_values)
return parent_translator.Parse(self._ParentParams(), parameter_info,
aggregations_dict)
except resources.ParentCollectionResolutionException as e:
# We don't know the parent collection.
log.info(six.text_type(e).rstrip())
return None
# No resource could be parsed.
except resources.Error as e:
log.info(six.text_type(e).rstrip())
return None
def __eq__(self, other):
"""Overrides."""
# Not using type(self) because the class is created programmatically.
if not isinstance(other, ResourceArgumentCompleter):
return False
return (self.resource_spec == other.resource_spec and
self.collection == other.collection and
self.method == other.method)
def _MatchCollection(resource_spec, attribute):
"""Gets the collection for an attribute in a resource."""
resource_collection_info = resource_spec._collection_info # pylint: disable=protected-access
resource_collection = registry.APICollection(
resource_collection_info)
if resource_collection is None:
return None
if attribute == resource_spec.attributes[-1]:
return resource_collection.name
attribute_idx = resource_spec.attributes.index(attribute)
api_name = resource_collection_info.api_name
resource_collections = registry.GetAPICollections(
api_name,
resource_collection_info.api_version)
params = resource_collection.detailed_params[:attribute_idx + 1]
for c in resource_collections:
if c.detailed_params == params:
return c.name
def _GetCompleterCollectionInfo(resource_spec, attribute):
"""Gets collection info for an attribute in a resource."""
api_version = None
collection = _MatchCollection(resource_spec, attribute)
if collection:
# pylint: disable=protected-access
full_collection_name = (
resource_spec._collection_info.api_name + '.' + collection)
api_version = resource_spec._collection_info.api_version
# The CloudResourceManager projects collection can be used for "synthetic"
# project resources that don't have their own method.
elif attribute.name == 'project':
full_collection_name = 'cloudresourcemanager.projects'
else:
return None
return resources.REGISTRY.GetCollectionInfo(full_collection_name,
api_version=api_version)
class CompleterInfo(object):
"""Holds data that can be used to instantiate a resource completer."""
def __init__(self, static_params=None, id_field=None, collection_info=None,
method=None, param_name=None):
self.static_params = static_params
self.id_field = id_field
self.collection_info = collection_info
self.method = method
self.param_name = param_name
@classmethod
def FromResource(cls, resource_spec, attribute_name):
"""Gets the method, param_name, and other configuration for a completer.
Args:
resource_spec: concepts.ResourceSpec, the overall resource.
attribute_name: str, the name of the attribute whose argument will use
this completer.
Raises:
AttributeError: if the attribute doesn't belong to the resource.
Returns:
CompleterInfo, the instantiated object.
"""
for a in resource_spec.attributes:
if a.name == attribute_name:
attribute = a
break
else:
raise AttributeError(
'Attribute [{}] not found in resource.'.format(attribute_name))
param_name = resource_spec.ParamName(attribute_name)
static_params = attribute.completion_request_params
id_field = attribute.completion_id_field
collection_info = _GetCompleterCollectionInfo(resource_spec, attribute)
if collection_info.full_name in _SPECIAL_COLLECTIONS_MAP:
special_info = _SPECIAL_COLLECTIONS_MAP.get(collection_info.full_name)
method = registry.GetMethod(collection_info.full_name, 'list')
static_params = special_info.static_params
id_field = special_info.id_field
param_name = special_info.param_name
if not collection_info:
return CompleterInfo(static_params, id_field, None, None, param_name)
# If there is no appropriate list method for the collection, we can't auto-
# create a completer.
try:
method = registry.GetMethod(
collection_info.full_name, 'list',
api_version=collection_info.api_version)
except registry.UnknownMethodError:
if (collection_info.full_name != _PROJECTS_COLLECTION
and collection_info.full_name.split('.')[-1] == 'projects'):
# The CloudResourceManager projects methods can be used for "synthetic"
# project resources that don't have their own method.
# This is a bit of a hack, so if any resource arguments come up for
# which this doesn't work, a toggle should be added to the
# ResourceSpec class to disable this.
# Does not use param_name from the special collections map because
# the collection exists with the current params, it's just the list
# method that we're borrowing.
special_info = _SPECIAL_COLLECTIONS_MAP.get(_PROJECTS_COLLECTION)
method = registry.GetMethod(_PROJECTS_COLLECTION, 'list')
static_params = special_info.static_params
id_field = special_info.id_field
else:
method = None
except registry.Error:
method = None
return CompleterInfo(static_params, id_field, collection_info, method,
param_name)
def GetMethod(self):
"""Get the APIMethod for an attribute in a resource."""
return self.method
def CompleterForAttribute(resource_spec, attribute_name):
"""Gets a resource argument completer for a specific attribute."""
class Completer(ResourceArgumentCompleter):
"""A specific completer for this attribute and resource."""
def __init__(self, resource_spec=resource_spec,
attribute_name=attribute_name, **kwargs):
completer_info = CompleterInfo.FromResource(resource_spec, attribute_name)
super(Completer, self).__init__(
resource_spec,
completer_info.collection_info,
completer_info.method,
static_params=completer_info.static_params,
id_field=completer_info.id_field,
param=completer_info.param_name,
**kwargs)
@classmethod
def validate(cls):
"""Checks whether the completer is valid (has a list method)."""
return bool(
CompleterInfo.FromResource(resource_spec, attribute_name).GetMethod())
if not Completer.validate():
return None
return Completer

View File

@@ -0,0 +1,290 @@
# -*- 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.
"""ConceptParsers manage the adding of all concept arguments to argparse parser.
The ConceptParser is created with a list of all resources needed for the
command, and they should be added all at once during calliope's Args method.
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope.concepts import deps
from googlecloudsdk.calliope.concepts import handlers
from googlecloudsdk.calliope.concepts import util
from googlecloudsdk.command_lib.util.concepts import presentation_specs
import six
class ConceptParser(object):
"""Class that handles adding concept specs to argparse."""
def __init__(self, specs, command_level_fallthroughs=None):
"""Initializes a concept holder.
Args:
specs: [presentation_specs.PresentationSpec], a list of the
specs for concepts to be added to the parser.
command_level_fallthroughs: {str: str}, a map of attributes to argument
fallthroughs for those attributes. The format of the key should be FOO.a
(the resource presentation name is "FOO" and the attribute name is "a").
The format of the value should either be "BAR.b" (where the argument
depended upon is the main argument generated for attribute "b" of
the resource presentation spec that is named "BAR"), or "--baz", where
"--baz" is a non-resource argument that is added separately to the
parser.
Raises:
ValueError: if two presentation specs have the same name or two specs
contain positional arguments.
"""
self._specs = {}
self._all_args = []
for spec in specs:
self._AddSpec(spec)
self._command_level_fallthroughs = self._ValidateAndFormatFallthroughsMap(
command_level_fallthroughs or {})
@classmethod
def ForResource(cls, name, resource_spec, group_help, required=False,
hidden=False, flag_name_overrides=None,
plural=False, prefixes=False,
group=None, command_level_fallthroughs=None):
"""Constructs a ConceptParser for a single resource argument.
Automatically sets prefixes to False.
Args:
name: str, the name of the main arg for the resource.
resource_spec: googlecloudsdk.calliope.concepts.ResourceSpec, The spec
that specifies the resource.
group_help: str, the help text for the entire arg group.
required: bool, whether the main argument should be required for the
command.
hidden: bool, whether or not the resource is hidden.
flag_name_overrides: {str: str}, dict of attribute names to the desired
flag name. To remove a flag altogether, use '' as its rename value.
plural: bool, True if the resource will be parsed as a list, False
otherwise.
prefixes: bool, True if flag names will be prefixed with the resource
name, False otherwise. Should be False for all typical use cases.
group: the parser or subparser for a Calliope command that the resource
arguments should be added to. If not provided, will be added to the main
parser.
command_level_fallthroughs: a map of attribute names to lists of command-
specific fallthroughs. These will be prioritized over the default
fallthroughs for the attribute.
Returns:
(googlecloudsdk.calliope.concepts.concept_parsers.ConceptParser) The fully
initialized ConceptParser.
"""
presentation_spec = presentation_specs.ResourcePresentationSpec(
name,
resource_spec,
group_help,
required=required,
flag_name_overrides=flag_name_overrides or {},
plural=plural,
prefixes=prefixes,
group=group,
hidden=hidden)
fallthroughs_map = {}
UpdateFallthroughsMap(fallthroughs_map, name, command_level_fallthroughs)
for attribute_name, fallthroughs in six.iteritems(
command_level_fallthroughs or {}):
key = '{}.{}'.format(presentation_spec.name, attribute_name)
fallthroughs_map[key] = fallthroughs
return cls([presentation_spec], fallthroughs_map)
def _ArgNameMatches(self, name, other_name):
"""Checks if two argument names match in the namespace.
RESOURCE_ARG and --resource-arg will match with each other, as well as exact
matches.
Args:
name: the first argument name.
other_name: the second argument name.
Returns:
(bool) True if the names match.
"""
if util.NormalizeFormat(name) == util.NormalizeFormat(other_name):
return True
return False
def _AddSpec(self, presentation_spec):
"""Adds a given presentation spec to the concept holder's spec registry.
Args:
presentation_spec: PresentationSpec, the spec to be added.
Raises:
ValueError: if two presentation specs have the same name, if two
presentation specs are both positional, or if two args are going to
overlap.
"""
# Check for duplicate spec names.
for spec_name in self._specs:
if self._ArgNameMatches(spec_name, presentation_spec.name):
raise ValueError('Attempted to add two concepts with the same name: '
'[{}, {}]'.format(spec_name, presentation_spec.name))
if (util.IsPositional(spec_name) and
util.IsPositional(presentation_spec.name)):
raise ValueError('Attempted to add multiple concepts with positional '
'arguments: [{}, {}]'.format(spec_name,
presentation_spec.name))
# Also check for duplicate argument names.
for a, arg_name in six.iteritems(presentation_spec.attribute_to_args_map):
del a # Unused.
name = util.NormalizeFormat(arg_name)
if name in self._all_args:
raise ValueError('Attempted to add a duplicate argument name: [{}]'
.format(arg_name))
self._all_args.append(name)
self._specs[presentation_spec.name] = presentation_spec
def _ValidateAndFormatFallthroughsMap(self, command_level_fallthroughs):
"""Validate formatting of fallthroughs and build map keyed to spec name."""
spec_map = {}
for key, fallthroughs_list in six.iteritems(command_level_fallthroughs):
keys = key.split('.')
if len(keys) != 2:
raise ValueError('invalid fallthrough key: [{}]. Must be in format '
'"FOO.a" where FOO is the presentation spec name and '
'a is the attribute name.'.format(key))
spec_name, attribute_name = keys
self._ValidateSpecAndAttributeExist('key', spec_name, attribute_name)
for fallthrough_string in fallthroughs_list:
values = fallthrough_string.split('.')
if len(values) not in [1, 2]:
raise ValueError('invalid fallthrough value: [{}]. Must be in the '
'form BAR.b or --baz'.format(fallthrough_string))
if len(values) == 2:
value_spec_name, value_attribute_name = values
self._ValidateSpecAndAttributeExist('value',
value_spec_name,
value_attribute_name)
spec_map.setdefault(spec_name, {})[attribute_name] = fallthroughs_list
return spec_map
def _ValidateSpecAndAttributeExist(self, location, spec_name, attribute_name):
"""Raises if a formatted string refers to non-existent spec or attribute."""
if spec_name not in self.specs:
raise ValueError('invalid fallthrough {}: [{}]. Spec name is not '
'present in the presentation specs. Available names: '
'[{}]'.format(
location,
'{}.{}'.format(spec_name, attribute_name),
', '.join(sorted(list(self.specs.keys())))))
spec = self.specs.get(spec_name)
if attribute_name not in [
attribute.name for attribute in spec.concept_spec.attributes]:
raise ValueError('invalid fallthrough {}: [{}]. spec named [{}] has no '
'attribute named [{}]'.format(
location,
'{}.{}'.format(spec_name, attribute_name),
spec_name,
attribute_name))
@property
def specs(self):
return self._specs
def AddToParser(self, parser):
"""Adds attribute args for all presentation specs to argparse.
Args:
parser: the parser for a Calliope command.
"""
runtime_handler = parser.data.concept_handler
if not runtime_handler:
runtime_handler = handlers.RuntimeHandler()
parser.add_concepts(runtime_handler)
for spec_name, spec in six.iteritems(self._specs):
concept_info = self.GetInfo(spec_name)
concept_info.AddToParser(parser)
runtime_handler.AddConcept(
util.NormalizeFormat(spec_name),
concept_info,
required=spec.required)
def GetExampleArgString(self):
"""Returns a command line example arg string for the concept."""
examples = []
for spec_name in self._specs:
info = self.GetInfo(spec_name)
args = info.GetExampleArgList()
if args:
examples.extend(args)
def _PositionalsFirst(arg):
prefix = 'Z' if arg.startswith('--') else 'A'
return prefix + arg
return ' '.join(sorted(examples, key=_PositionalsFirst))
def _MakeFallthrough(self, fallthrough_string):
"""Make an ArgFallthrough from a formatted string."""
values = fallthrough_string.split('.')
if len(values) == 1:
arg_name = values
return deps.ArgFallthrough(values[0])
elif len(values) == 2:
spec_name, attribute_name = values
spec = self.specs.get(spec_name)
arg_name = spec.attribute_to_args_map.get(attribute_name, None)
if not arg_name:
raise ValueError(
'Invalid fallthrough value [{}]: No argument associated with '
'attribute [{}] in concept argument named [{}]'.format(
fallthrough_string,
attribute_name,
spec_name))
return deps.ArgFallthrough(arg_name)
else:
# Defensive only, should be validated earlier
raise ValueError('bad fallthrough string [{}]'.format(fallthrough_string))
def GetInfo(self, presentation_spec_name):
"""Build ConceptInfo object for the spec with the given name."""
if presentation_spec_name not in self.specs:
raise ValueError('Presentation spec with name [{}] has not been added '
'to the concept parser, cannot generate info.'.format(
presentation_spec_name))
presentation_spec = self.specs[presentation_spec_name]
fallthroughs_map = {}
for attribute in presentation_spec.concept_spec.attributes:
fallthrough_strings = self._command_level_fallthroughs.get(
presentation_spec.name, {}).get(attribute.name, [])
fallthroughs = [self._MakeFallthrough(fallthrough_string)
for fallthrough_string in fallthrough_strings]
fallthroughs_map[attribute.name] = fallthroughs + attribute.fallthroughs
return presentation_spec._GenerateInfo(fallthroughs_map) # pylint: disable=protected-access
def UpdateFallthroughsMap(fallthroughs_map, resource_arg_name,
command_level_fallthroughs):
"""Helper to add a single resource's command level fallthroughs."""
for attribute_name, fallthroughs in six.iteritems(
command_level_fallthroughs or {}):
key = '{}.{}'.format(resource_arg_name, attribute_name)
fallthroughs_map[key] = fallthroughs

View File

@@ -0,0 +1,471 @@
# -*- coding: utf-8 -*- #
# Copyright 2018 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.
"""Classes for runtime handling of concept arguments."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import abc
from googlecloudsdk.calliope import arg_parsers
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope.concepts import deps as deps_lib
from googlecloudsdk.calliope.concepts import util
from googlecloudsdk.command_lib.util.concepts import completers
from googlecloudsdk.core.util import text
import six
from six.moves import filter # pylint: disable=redefined-builtin
ANCHOR_HELP = ('ID of the {resource} or fully qualified identifier for the '
'{resource}.')
PLURAL_ANCHOR_HELP = ('IDs of the {resource} or fully qualified identifiers '
'for the {resource}.')
class ConceptInfo(six.with_metaclass(abc.ABCMeta, object)):
"""Holds information for a concept argument.
The ConceptInfo object is responsible for holding information about the
dependencies of a concept, and building a Deps object when it is time for
lazy parsing of the concept.
Attributes:
concept_spec: The concept spec underlying the concept handler.
attribute_to_args_map: A map of attributes to the names of their associated
flags.
fallthroughs_map: A map of attributes to non-argument fallthroughs.
"""
@property
def concept_spec(self):
"""The concept spec associated with this info class."""
raise NotImplementedError
@property
def fallthroughs_map(self):
"""A map of attribute names to non-primary fallthroughs."""
raise NotImplementedError
@abc.abstractmethod
def GetHints(self, attribute_name):
"""Get a list of string hints for how to specify a concept's attribute.
Args:
attribute_name: str, the name of the attribute to get hints for.
Returns:
[str], a list of string hints.
"""
def GetGroupHelp(self):
"""Get the group help for the group defined by the presentation spec.
Must be overridden in subclasses.
Returns:
(str) the help text.
"""
raise NotImplementedError
def GetAttributeArgs(self):
"""Generate args to add to the argument group.
Must be overridden in subclasses.
Yields:
(calliope.base.Argument), all arguments corresponding to concept
attributes.
"""
raise NotImplementedError
def AddToParser(self, parser):
"""Adds all attribute args for the concept to argparse.
Must be overridden in subclasses.
Args:
parser: the parser for the Calliope command.
"""
raise NotImplementedError
@abc.abstractmethod
def Parse(self, parsed_args=None):
"""Lazy parsing function to parse concept.
Args:
parsed_args: the argparse namespace from the runtime handler.
Returns:
the parsed concept.
"""
def ClearCache(self):
"""Clear cache if it exists. Override where needed."""
pass
class ResourceInfo(ConceptInfo):
"""Holds information for a resource argument."""
def __init__(self,
presentation_name,
concept_spec,
group_help,
attribute_to_args_map,
fallthroughs_map,
required=False,
plural=False,
group=None,
hidden=False):
"""Initializes the ResourceInfo.
Args:
presentation_name: str, the name of the anchor argument of the
presentation spec.
concept_spec: googlecloudsdk.calliope.concepts.ConceptSpec, The underlying
concept spec.
group_help: str, the group help for the argument group.
attribute_to_args_map: {str: str}, A map of attribute names to the names
of their associated flags.
fallthroughs_map: {str: [deps_lib.Fallthrough]} A map of attribute names
to non-argument fallthroughs.
required: bool, False if resource parsing is allowed to return no
resource, otherwise True.
plural: bool, True if multiple resources can be parsed, False otherwise.
group: an argparse argument group parser to which the resource arg group
should be added, if any.
hidden: bool, True, if the resource should be hidden.
"""
super(ResourceInfo, self).__init__()
self.presentation_name = presentation_name
self._concept_spec = concept_spec
self._fallthroughs_map = fallthroughs_map
self.attribute_to_args_map = attribute_to_args_map
self.plural = plural
self.group_help = group_help
self.allow_empty = not required
self.group = group
self.hidden = hidden
self._result = None
self._result_computed = False
self.sentinel = 0
@property
def concept_spec(self):
return self._concept_spec
@property
def resource_spec(self):
return self.concept_spec
@property
def fallthroughs_map(self):
return self._fallthroughs_map
@property
def title(self):
"""The title of the arg group for the spec, in all caps with spaces."""
name = self.concept_spec.name
name = name[0].upper() + name[1:]
return name.replace('_', ' ').replace('-', ' ')
def _IsAnchor(self, attribute):
return self.concept_spec.IsAnchor(attribute)
def BuildFullFallthroughsMap(self):
return self.concept_spec.BuildFullFallthroughsMap(
self.attribute_to_args_map,
self.fallthroughs_map)
def GetHints(self, attribute_name):
"""Gets a list of string hints for how to set an attribute.
Given the attribute name, gets a list of hints corresponding to the
attribute's fallthroughs.
Args:
attribute_name: str, the name of the attribute.
Returns:
A list of hints for its fallthroughs, including its primary arg if any.
"""
fallthroughs = self.BuildFullFallthroughsMap().get(attribute_name, [])
return deps_lib.GetHints(fallthroughs)
def GetGroupHelp(self):
"""Build group help for the argument group."""
if len(list(filter(bool, list(self.attribute_to_args_map.values())))) == 1:
generic_help = 'This represents a Cloud resource.'
else:
generic_help = ('The arguments in this group can be used to specify the '
'attributes of this resource.')
description = ['{} resource - {} {}'.format(
self.title,
self.group_help,
generic_help)]
skip_flags = [
attribute.name for attribute in self.resource_spec.attributes
if not self.attribute_to_args_map.get(attribute.name)]
if skip_flags:
description.append('(NOTE) Some attributes are not given arguments in '
'this group but can be set in other ways.')
for attr_name in skip_flags:
hints = [
'\n* {}'.format(hint) for hint in self.GetHints(attr_name)]
if not hints:
# This may be an error, but existence of fallthroughs should not be
# enforced here.
continue
hint = '\n\nTo set the `{}` attribute:{}.'.format(
attr_name, ';'.join(hints)
)
description.append(hint)
return ' '.join(description)
@property
def args_required(self):
"""True if the resource is required and any arguments have no fallthroughs.
If fallthroughs can ever be configured in the ResourceInfo object,
a more robust solution will be needed, e.g. a GetFallthroughsForAttribute
method.
Returns:
bool, whether the argument group should be required.
"""
if self.allow_empty:
return False
anchor = self.resource_spec.anchor
if (self.attribute_to_args_map.get(anchor.name, None)
and not self.fallthroughs_map.get(anchor.name, [])):
return True
return False
def _GetHelpTextForAttribute(self, attribute):
"""Helper to get the help text for the attribute arg."""
if self._IsAnchor(attribute):
help_text = ANCHOR_HELP if not self.plural else PLURAL_ANCHOR_HELP
else:
help_text = attribute.help_text
expansion_name = text.Pluralize(
2 if self.plural else 1,
self.resource_spec.name,
plural=getattr(self.resource_spec, 'plural_name', None))
hints = [
'\n* {}'.format(hint) for hint in self.GetHints(attribute.name)]
if hints:
hint = '\n\nTo set the `{}` attribute:{}.'.format(
attribute.name, ';'.join(hints)
)
help_text += hint
return help_text.format(resource=expansion_name)
def _IsRequiredArg(self, attribute):
# An argument cannot be required if it's hidden
return not self.hidden and (
self._IsAnchor(attribute) and
not self.fallthroughs_map.get(attribute.name, []))
def _IsPluralArg(self, attribute):
return self._IsAnchor(attribute) and self.plural
def _KwargsForAttribute(self, name, attribute):
"""Constructs the kwargs for adding an attribute to argparse."""
# Argument is modal if it's the anchor, unless there are fallthroughs.
# If fallthroughs can ever be configured in the ResourceInfo object,
# a more robust solution will be needed, e.g. a GetFallthroughsForAttribute
# method.
required = self._IsRequiredArg(attribute)
final_help_text = self._GetHelpTextForAttribute(attribute)
plural = self._IsPluralArg(attribute)
if attribute.completer:
completer = attribute.completer
elif not self.resource_spec.disable_auto_completers:
completer = completers.CompleterForAttribute(
self.resource_spec,
attribute.name)
else:
completer = None
kwargs_dict = {
'help': final_help_text,
'type': attribute.value_type,
'completer': completer,
'hidden': self.hidden
}
if util.IsPositional(name):
if plural and required:
kwargs_dict.update({'nargs': '+'})
# The following should not usually happen because anchor args are
# required.
elif plural and not required:
kwargs_dict.update({'nargs': '*'})
elif not required:
kwargs_dict.update({'nargs': '?'})
else:
kwargs_dict.update({'metavar': util.MetavarFormat(name)})
if required:
kwargs_dict.update({'required': True})
if plural:
kwargs_dict.update({'type': arg_parsers.ArgList()})
return kwargs_dict
def _GetAttributeArg(self, attribute):
"""Creates argument for a specific attribute."""
name = self.attribute_to_args_map.get(attribute.name, None)
# Return None for any false value.
if not name:
return None
return base.Argument(
name,
**self._KwargsForAttribute(name, attribute))
def GetAttributeArgs(self):
"""Generate args to add to the argument group."""
args = []
for attribute in self.resource_spec.attributes:
arg = self._GetAttributeArg(attribute)
if arg:
args.append(arg)
return args
def AddToParser(self, parser):
"""Adds all attributes of the concept to argparse.
Creates a group to hold all the attributes and adds an argument for each
attribute. If the presentation spec is required, then the anchor attribute
argument will be required.
Args:
parser: the parser for the Calliope command.
"""
args = self.GetAttributeArgs()
if not args:
# Don't create the group if there are not going to be any args generated.
return
# If this spec is supposed to be added to a subgroup, that overrides the
# provided parser.
parser = self.group or parser
hidden = any(x.IsHidden() for x in args)
resource_group = parser.add_group(
help=self.GetGroupHelp(), required=self.args_required, hidden=hidden)
for arg in args:
arg.AddToParser(resource_group)
def GetExampleArgList(self):
"""Returns a list of command line example arg strings for the concept."""
args = self.GetAttributeArgs()
examples = []
for arg in args:
if arg.name.startswith('--'):
example = '{}=my-{}'.format(arg.name, arg.name[2:])
else:
example = 'my-{}'.format(arg.name.lower())
examples.append(example)
return examples
def Parse(self, parsed_args=None):
"""Lazy, cached parsing function for resource.
Args:
parsed_args: the parsed Namespace.
Returns:
the initialized resource or a list of initialized resources if the
resource argument was pluralized.
"""
if not self._result_computed:
result = self.concept_spec.Parse(self.attribute_to_args_map,
self.fallthroughs_map,
parsed_args=parsed_args,
plural=self.plural,
allow_empty=self.allow_empty)
self._result_computed = True
self._result = result
return self._result
def ClearCache(self):
self._result = None
self._result_computed = False
class MultitypeResourceInfo(ResourceInfo):
"""ResourceInfo object specifically for multitype resources."""
def _IsAnchor(self, attribute):
"""Returns true if the attribute is an anchor."""
return self.concept_spec.IsAnchor(attribute)
def _GetAnchors(self):
return [a for a in self.concept_spec.attributes if self._IsAnchor(a)]
def _IsRequiredArg(self, attribute):
"""Returns True if the attribute arg should be required."""
anchors = self._GetAnchors()
return anchors == [attribute] and not self.fallthroughs_map.get(
attribute.name, [])
def _IsPluralArg(self, attribute):
return self.concept_spec.Pluralize(attribute, plural=self.plural)
@property
def args_required(self):
"""True if resource is required & has a single anchor with no fallthroughs.
Returns:
bool, whether the argument group should be required.
"""
if self.allow_empty:
return False
anchors = self._GetAnchors()
if len(anchors) != 1:
return False
anchor = anchors[0]
if self.fallthroughs_map.get(anchor.name, []):
return False
# There's only one anchor and it's got no fallthroughs.
return True
def GetGroupHelp(self):
base_text = super(MultitypeResourceInfo, self).GetGroupHelp()
all_types = [
type_.name for type_ in self.concept_spec.type_enum] # pylint: disable=protected-access
return base_text + (' This resource can be one of the following types: '
'[{}].'.format(', '.join(all_types)))
def _GetHelpTextForAttribute(self, attribute):
base_text = super(MultitypeResourceInfo, self)._GetHelpTextForAttribute(
attribute)
# pylint: disable=protected-access
relevant_types = sorted([
type_.name for type_ in self.concept_spec._attribute_to_types_map.get(
attribute.name)])
# Don't add anything extra if this attribute is used in all types.
all_types = [
type_.name for type_ in self.concept_spec.type_enum]
if len(set(relevant_types)) == len(all_types):
return base_text
# pylint: disable=protected-access
return base_text + (
' Must be specified for resource of type {}.'
.format(' or '.join(['[{}]'.format(t) for t in relevant_types])))

View File

@@ -0,0 +1,332 @@
# -*- coding: utf-8 -*- #
# Copyright 2018 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.
"""Classes to define how concept args are added to argparse.
A PresentationSpec is used to define how a concept spec is presented in an
individual command, such as its help text. ResourcePresentationSpecs are
used for resource specs.
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope.concepts import util
from googlecloudsdk.command_lib.util.concepts import info_holders
class PresentationSpec(object):
"""Class that defines how concept arguments are presented in a command.
Attributes:
name: str, the name of the main arg for the concept. Can be positional or
flag style (UPPER_SNAKE_CASE or --lower-train-case).
concept_spec: googlecloudsdk.calliope.concepts.ConceptSpec, The spec that
specifies the concept.
group_help: str, the help text for the entire arg group.
prefixes: bool, whether to use prefixes before the attribute flags, such as
`--myresource-project`.
required: bool, whether the anchor argument should be required. If True, the
command will fail at argparse time if the anchor argument isn't given.
plural: bool, True if the resource will be parsed as a list, False
otherwise.
group: the parser or subparser for a Calliope command that the resource
arguments should be added to. If not provided, will be added to the main
parser.
attribute_to_args_map: {str: str}, dict of attribute names to names of
associated arguments.
hidden: bool, True if the arguments should be hidden.
"""
def __init__(self,
name,
concept_spec,
group_help,
prefixes=False,
required=False,
flag_name_overrides=None,
plural=False,
group=None,
hidden=False):
"""Initializes a ResourcePresentationSpec.
Args:
name: str, the name of the main arg for the concept.
concept_spec: googlecloudsdk.calliope.concepts.ConceptSpec, The spec that
specifies the concept.
group_help: str, the help text for the entire arg group.
prefixes: bool, whether to use prefixes before the attribute flags, such
as `--myresource-project`. This will match the "name" (in flag format).
required: bool, whether the anchor argument should be required.
flag_name_overrides: {str: str}, dict of attribute names to the desired
flag name. To remove a flag altogether, use '' as its rename value.
plural: bool, True if the resource will be parsed as a list, False
otherwise.
group: the parser or subparser for a Calliope command that the resource
arguments should be added to. If not provided, will be added to the main
parser.
hidden: bool, True if the arguments should be hidden.
"""
self.name = name
self._concept_spec = concept_spec
self.group_help = group_help
self.prefixes = prefixes
self.required = required
self.plural = plural
self.group = group
self._attribute_to_args_map = self._GetAttributeToArgsMap(
flag_name_overrides)
self.hidden = hidden
@property
def concept_spec(self):
"""The ConceptSpec associated with the PresentationSpec.
Returns:
(googlecloudsdk.calliope.concepts.ConceptSpec) the concept spec.
"""
return self._concept_spec
@property
def attribute_to_args_map(self):
"""The map of attribute names to associated args.
Returns:
{str: str}, the map.
"""
return self._attribute_to_args_map
def _GenerateInfo(self, fallthroughs_map):
"""Generate a ConceptInfo object for the ConceptParser.
Must be overridden in subclasses.
Args:
fallthroughs_map: {str: [googlecloudsdk.calliope.concepts.deps.
_FallthroughBase]}, dict keyed by attribute name to lists of
fallthroughs.
Returns:
info_holders.ConceptInfo, the ConceptInfo object.
"""
raise NotImplementedError
def _GetAttributeToArgsMap(self, flag_name_overrides):
"""Generate a map of attributes to primary arg names.
Must be overridden in subclasses.
Args:
flag_name_overrides: {str: str}, the dict of flags to overridden names.
Returns:
{str: str}, dict from attribute names to arg names.
"""
raise NotImplementedError
class ResourcePresentationSpec(PresentationSpec):
"""Class that specifies how resource arguments are presented in a command."""
def _ValidateFlagNameOverrides(self, flag_name_overrides):
if not flag_name_overrides:
return
for attribute_name in flag_name_overrides.keys():
for attribute in self.concept_spec.attributes:
if attribute.name == attribute_name:
break
else:
raise ValueError(
'Attempting to override the name for an attribute not present in '
'the concept: [{}]. Available attributes: [{}]'.format(
attribute_name,
', '.join([attribute.name
for attribute in self.concept_spec.attributes])))
def _GetAttributeToArgsMap(self, flag_name_overrides):
self._ValidateFlagNameOverrides(flag_name_overrides)
# Create a rename map for the attributes to their flags.
attribute_to_args_map = {}
for i, attribute in enumerate(self._concept_spec.attributes):
is_anchor = i == len(self._concept_spec.attributes) - 1
name = self.GetFlagName(
attribute.name, self.name, flag_name_overrides, self.prefixes,
is_anchor=is_anchor)
if name:
attribute_to_args_map[attribute.name] = name
return attribute_to_args_map
@staticmethod
def GetFlagName(attribute_name, presentation_name, flag_name_overrides=None,
prefixes=False, is_anchor=False):
"""Gets the flag name for a given attribute name.
Returns a flag name for an attribute, adding prefixes as necessary or using
overrides if an override map is provided.
Args:
attribute_name: str, the name of the attribute to base the flag name on.
presentation_name: str, the anchor argument name of the resource the
attribute belongs to (e.g. '--foo').
flag_name_overrides: {str: str}, a dict of attribute names to exact string
of the flag name to use for the attribute. None if no overrides.
prefixes: bool, whether to use the resource name as a prefix for the flag.
is_anchor: bool, True if this it he anchor flag, False otherwise.
Returns:
(str) the name of the flag.
"""
flag_name_overrides = flag_name_overrides or {}
if attribute_name in flag_name_overrides:
return flag_name_overrides.get(attribute_name)
if attribute_name == 'project':
return ''
if is_anchor:
return presentation_name
prefix = util.PREFIX
if prefixes:
if presentation_name.startswith(util.PREFIX):
prefix += presentation_name[len(util.PREFIX):] + '-'
else:
prefix += presentation_name.lower().replace('_', '-') + '-'
return prefix + attribute_name
def _GenerateInfo(self, fallthroughs_map):
"""Gets the ResourceInfo object for the ConceptParser.
Args:
fallthroughs_map: {str: [googlecloudsdk.calliope.concepts.deps.
_FallthroughBase]}, dict keyed by attribute name to lists of
fallthroughs.
Returns:
info_holders.ResourceInfo, the ResourceInfo object.
"""
return info_holders.ResourceInfo(
self.name,
self.concept_spec,
self.group_help,
self.attribute_to_args_map,
fallthroughs_map,
required=self.required,
plural=self.plural,
group=self.group,
hidden=self.hidden)
def __eq__(self, other):
if not isinstance(other, type(self)):
return False
return (self.name == other.name and
self.concept_spec == other.concept_spec and
self.group_help == other.group_help and
self.prefixes == other.prefixes and self.plural == other.plural and
self.required == other.required and self.group == other.group and
self.hidden == other.hidden)
class InvalidPresentationSpecError(Exception):
"""Error for invalid presentation spec."""
# Currently no other type of multitype concepts have been implemented.
class MultitypeResourcePresentationSpec(PresentationSpec):
"""A resource-specific presentation spec."""
def _GetAttributeToArgsMap(self, flag_name_overrides):
# Create a rename map for the attributes to their flags.
attribute_to_args_map = {}
leaf_anchors = [a for a in self._concept_spec.attributes
if self._concept_spec.IsLeafAnchor(a)]
if len(leaf_anchors) > 1 and util.IsPositional(self.name):
anchor_names = ', '.join(a.name for a in leaf_anchors)
raise InvalidPresentationSpecError(
f'Multitype resource has anchors [{anchor_names}] and positional '
f'name [{self.name}]. Multitype resource can only be non-positional '
'or have a single anchor. Update multitype collections or change '
f'the presentation name to {util.FlagNameFormat(self.name)}.')
for attribute in self._concept_spec.attributes:
is_anchor = [attribute] == leaf_anchors
name = self.GetFlagName(
attribute.name, self.name, flag_name_overrides=flag_name_overrides,
prefixes=self.prefixes, is_anchor=is_anchor)
if name:
attribute_to_args_map[attribute.name] = name
return attribute_to_args_map
@staticmethod
def GetFlagName(attribute_name, presentation_name, flag_name_overrides=None,
prefixes=False, is_anchor=False):
"""Gets the flag name for a given attribute name.
Returns a flag name for an attribute, adding prefixes as necessary or using
overrides if an override map is provided.
Args:
attribute_name: str, the name of the attribute to base the flag name on.
presentation_name: str, the anchor argument name of the resource the
attribute belongs to (e.g. '--foo').
flag_name_overrides: {str: str}, a dict of attribute names to exact string
of the flag name to use for the attribute. None if no overrides.
prefixes: bool, whether to use the resource name as a prefix for the flag.
is_anchor: bool, True if this is the anchor flag, False otherwise.
Returns:
(str) the name of the flag.
"""
flag_name_overrides = flag_name_overrides or {}
if attribute_name in flag_name_overrides:
return flag_name_overrides.get(attribute_name)
if is_anchor:
return presentation_name
if attribute_name == 'project':
return ''
if prefixes:
return util.FlagNameFormat('-'.join([presentation_name, attribute_name]))
else:
return util.FlagNameFormat(attribute_name)
def _GenerateInfo(self, fallthroughs_map):
"""Gets the MultitypeResourceInfo object for the ConceptParser.
Args:
fallthroughs_map: {str: [googlecloudsdk.calliope.concepts.deps.
_FallthroughBase]}, dict keyed by attribute name to lists of
fallthroughs.
Returns:
info_holders.MultitypeResourceInfo, the ResourceInfo object.
"""
return info_holders.MultitypeResourceInfo(
self.name,
self.concept_spec,
self.group_help,
self.attribute_to_args_map,
fallthroughs_map,
required=self.required,
plural=self.plural,
group=self.group)
def __eq__(self, other):
if not isinstance(other, type(self)):
return False
return (self.name == other.name and
self.concept_spec == other.concept_spec and
self.group_help == other.group_help and
self.prefixes == other.prefixes and self.plural == other.plural and
self.required == other.required and self.group == other.group and
self.hidden == other.hidden)

View File

@@ -0,0 +1,126 @@
# -*- coding: utf-8 -*- #
# Copyright 2018 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.
"""Parameter info lib for resource completers."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope.concepts import deps
from googlecloudsdk.calliope.concepts import util
from googlecloudsdk.command_lib.util import parameter_info_lib
from googlecloudsdk.core import properties
class ResourceParameterInfo(parameter_info_lib.ParameterInfoByConvention):
"""Gets parameter info for resource arguments."""
def __init__(self, resource_info, parsed_args, argument, **kwargs):
"""Initializes."""
self.resource_info = resource_info
super(ResourceParameterInfo, self).__init__(
parsed_args,
argument,
**kwargs)
def GetValue(self, parameter_name, check_properties=True):
"""Returns the program state value for parameter_name.
Args:
parameter_name: The parameter name.
check_properties: bool, whether to check the properties (unused).
Returns:
The program state value for parameter_name.
"""
del check_properties # Unused.
attribute_name = (
self.resource_info.resource_spec.AttributeName(parameter_name))
current = properties.VALUES.core.disable_prompts.GetBool()
# TODO(b/73073941): Come up with a better way to temporarily disable
# prompts. This prevents arbitrary fallthroughs with prompting from
# being run during completion.
properties.VALUES.core.disable_prompts.Set(True)
try:
return deps.Get(
attribute_name,
self.resource_info.BuildFullFallthroughsMap(),
parsed_args=self.parsed_args) if attribute_name else None
except deps.AttributeNotFoundError:
return None
finally:
properties.VALUES.core.disable_prompts.Set(current)
def _AttributeName(self, parameter_name):
"""Helper function to get the corresponding attribute for a parameter."""
return self.resource_info.resource_spec.AttributeName(parameter_name)
def GetDest(self, parameter_name, prefix=None):
"""Returns the argument parser dest name for parameter_name with prefix.
Args:
parameter_name: The resource parameter name.
prefix: The prefix name for parameter_name if not None.
Returns:
The argument parser dest name for parameter_name.
"""
del prefix # Unused.
attribute_name = self._AttributeName(parameter_name)
flag_name = self.resource_info.attribute_to_args_map.get(attribute_name,
None)
if not flag_name:
return None
return util.NamespaceFormat(flag_name)
def GetFlag(self, parameter_name, parameter_value=None,
check_properties=True, for_update=False):
"""Returns the command line flag for parameter.
If the flag is already present in program values, returns None.
If the user needs to specify it, returns a string in the form
'--flag-name=value'. If the flag is boolean and True, returns '--flag-name'.
Args:
parameter_name: The parameter name.
parameter_value: The parameter value if not None. Otherwise
GetValue() is used to get the value.
check_properties: Check property values if parsed_args don't help.
for_update: Return flag for a cache update command.
Returns:
The command line flag for the parameter, or None.
"""
del for_update
attribute_name = self._AttributeName(parameter_name)
flag_name = self.resource_info.attribute_to_args_map.get(
attribute_name, None)
if not flag_name:
# Project attributes are typically elided in favor of the global --project
# flag. If the project flag is brought under the concept argument umbrella
# this can be removed.
if attribute_name == 'project':
flag_name = '--project'
else:
return None
program_value = self.GetValue(parameter_name)
if parameter_value != program_value:
if parameter_value is None:
parameter_value = program_value
if parameter_value:
if parameter_value is True:
return flag_name
return '{name}={value}'.format(name=flag_name, value=parameter_value)
return None