1# -*- coding: utf-8 -*- #
2# Copyright 2018 Google LLC. All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#    http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15"""Classes to define how concept args are added to argparse.
16
17A PresentationSpec is used to define how a concept spec is presented in an
18individual command, such as its help text. ResourcePresentationSpecs are
19used for resource specs.
20"""
21
22from __future__ import absolute_import
23from __future__ import division
24from __future__ import unicode_literals
25
26from googlecloudsdk.calliope.concepts import util
27from googlecloudsdk.command_lib.util.concepts import info_holders
28
29
30class PresentationSpec(object):
31  """Class that defines how concept arguments are presented in a command.
32
33  Attributes:
34    name: str, the name of the main arg for the concept. Can be positional or
35      flag style (UPPER_SNAKE_CASE or --lower-train-case).
36    concept_spec: googlecloudsdk.calliope.concepts.ConceptSpec, The spec that
37      specifies the concept.
38    group_help: str, the help text for the entire arg group.
39    prefixes: bool, whether to use prefixes before the attribute flags, such as
40      `--myresource-project`.
41    required: bool, whether the anchor argument should be required. If True, the
42      command will fail at argparse time if the anchor argument isn't given.
43    plural: bool, True if the resource will be parsed as a list, False
44      otherwise.
45    group: the parser or subparser for a Calliope command that the resource
46      arguments should be added to. If not provided, will be added to the main
47      parser.
48    attribute_to_args_map: {str: str}, dict of attribute names to names of
49      associated arguments.
50  """
51
52  def __init__(self, name, concept_spec, group_help, prefixes=False,
53               required=False, flag_name_overrides=None, plural=False,
54               group=None):
55    """Initializes a ResourcePresentationSpec.
56
57    Args:
58      name: str, the name of the main arg for the concept.
59      concept_spec: googlecloudsdk.calliope.concepts.ConceptSpec, The spec that
60        specifies the concept.
61      group_help: str, the help text for the entire arg group.
62      prefixes: bool, whether to use prefixes before the attribute flags, such
63        as `--myresource-project`. This will match the "name" (in flag format).
64      required: bool, whether the anchor argument should be required.
65      flag_name_overrides: {str: str}, dict of attribute names to the desired
66        flag name. To remove a flag altogether, use '' as its rename value.
67      plural: bool, True if the resource will be parsed as a list, False
68        otherwise.
69      group: the parser or subparser for a Calliope command that the resource
70        arguments should be added to. If not provided, will be added to the main
71        parser.
72    """
73    self.name = name
74    self._concept_spec = concept_spec
75    self.group_help = group_help
76    self.prefixes = prefixes
77    self.required = required
78    self.plural = plural
79    self.group = group
80    self._attribute_to_args_map = self._GetAttributeToArgsMap(
81        flag_name_overrides)
82
83  @property
84  def concept_spec(self):
85    """The ConceptSpec associated with the PresentationSpec.
86
87    Returns:
88      (googlecloudsdk.calliope.concepts.ConceptSpec) the concept spec.
89    """
90    return self._concept_spec
91
92  @property
93  def attribute_to_args_map(self):
94    """The map of attribute names to associated args.
95
96    Returns:
97      {str: str}, the map.
98    """
99    return self._attribute_to_args_map
100
101  def _GenerateInfo(self, fallthroughs_map):
102    """Generate a ConceptInfo object for the ConceptParser.
103
104    Must be overridden in subclasses.
105
106    Args:
107      fallthroughs_map: {str: [googlecloudsdk.calliope.concepts.deps.
108        _FallthroughBase]}, dict keyed by attribute name to lists of
109        fallthroughs.
110
111    Returns:
112      info_holders.ConceptInfo, the ConceptInfo object.
113    """
114    raise NotImplementedError
115
116  def _GetAttributeToArgsMap(self, flag_name_overrides):
117    """Generate a map of attributes to primary arg names.
118
119    Must be overridden in subclasses.
120
121    Args:
122      flag_name_overrides: {str: str}, the dict of flags to overridden names.
123
124    Returns:
125      {str: str}, dict from attribute names to arg names.
126    """
127    raise NotImplementedError
128
129
130class ResourcePresentationSpec(PresentationSpec):
131  """Class that specifies how resource arguments are presented in a command."""
132
133  def _ValidateFlagNameOverrides(self, flag_name_overrides):
134    if not flag_name_overrides:
135      return
136    for attribute_name in flag_name_overrides.keys():
137      for attribute in self.concept_spec.attributes:
138        if attribute.name == attribute_name:
139          break
140      else:
141        raise ValueError(
142            'Attempting to override the name for an attribute not present in '
143            'the concept: [{}]. Available attributes: [{}]'.format(
144                attribute_name,
145                ', '.join([attribute.name
146                           for attribute in self.concept_spec.attributes])))
147
148  def _GetAttributeToArgsMap(self, flag_name_overrides):
149    self._ValidateFlagNameOverrides(flag_name_overrides)
150    # Create a rename map for the attributes to their flags.
151    attribute_to_args_map = {}
152    for i, attribute in enumerate(self._concept_spec.attributes):
153      is_anchor = i == len(self._concept_spec.attributes) - 1
154      name = self.GetFlagName(
155          attribute.name, self.name, flag_name_overrides, self.prefixes,
156          is_anchor=is_anchor)
157      if name:
158        attribute_to_args_map[attribute.name] = name
159    return attribute_to_args_map
160
161  @staticmethod
162  def GetFlagName(attribute_name, presentation_name, flag_name_overrides=None,
163                  prefixes=False, is_anchor=False):
164    """Gets the flag name for a given attribute name.
165
166    Returns a flag name for an attribute, adding prefixes as necessary or using
167    overrides if an override map is provided.
168
169    Args:
170      attribute_name: str, the name of the attribute to base the flag name on.
171      presentation_name: str, the anchor argument name of the resource the
172        attribute belongs to (e.g. '--foo').
173      flag_name_overrides: {str: str}, a dict of attribute names to exact string
174        of the flag name to use for the attribute. None if no overrides.
175      prefixes: bool, whether to use the resource name as a prefix for the flag.
176      is_anchor: bool, True if this it he anchor flag, False otherwise.
177
178    Returns:
179      (str) the name of the flag.
180    """
181    flag_name_overrides = flag_name_overrides or {}
182    if attribute_name in flag_name_overrides:
183      return flag_name_overrides.get(attribute_name)
184    if attribute_name == 'project':
185      return ''
186    if is_anchor:
187      return presentation_name
188    prefix = util.PREFIX
189    if prefixes:
190      if presentation_name.startswith(util.PREFIX):
191        prefix += presentation_name[len(util.PREFIX):] + '-'
192      else:
193        prefix += presentation_name.lower().replace('_', '-') + '-'
194    return prefix + attribute_name
195
196  def _GenerateInfo(self, fallthroughs_map):
197    """Gets the ResourceInfo object for the ConceptParser.
198
199    Args:
200      fallthroughs_map: {str: [googlecloudsdk.calliope.concepts.deps.
201        _FallthroughBase]}, dict keyed by attribute name to lists of
202        fallthroughs.
203
204    Returns:
205      info_holders.ResourceInfo, the ResourceInfo object.
206    """
207    return info_holders.ResourceInfo(
208        self.name,
209        self.concept_spec,
210        self.group_help,
211        self.attribute_to_args_map,
212        fallthroughs_map,
213        required=self.required,
214        plural=self.plural,
215        group=self.group)
216
217  def __eq__(self, other):
218    if not isinstance(other, type(self)):
219      return False
220    return (self.name == other.name and
221            self.concept_spec == other.concept_spec and
222            self.group_help == other.group_help and
223            self.prefixes == other.prefixes and
224            self.plural == other.plural and
225            self.required == other.required and
226            self.group == other.group)
227
228
229# Currently no other type of multitype concepts have been implemented.
230class MultitypeResourcePresentationSpec(PresentationSpec):
231  """A resource-specific presentation spec."""
232
233  def _GetAttributeToArgsMap(self, flag_name_overrides):
234    # Create a rename map for the attributes to their flags.
235    attribute_to_args_map = {}
236    leaf_anchors = [a for a in self._concept_spec.attributes
237                    if self._concept_spec.IsLeafAnchor(a)]
238    for attribute in self._concept_spec.attributes:
239      is_anchor = [attribute] == leaf_anchors
240      name = self.GetFlagName(
241          attribute.name, self.name, flag_name_overrides=flag_name_overrides,
242          prefixes=self.prefixes, is_anchor=is_anchor)
243      if name:
244        attribute_to_args_map[attribute.name] = name
245    return attribute_to_args_map
246
247  @staticmethod
248  def GetFlagName(attribute_name, presentation_name, flag_name_overrides=None,
249                  prefixes=False, is_anchor=False):
250    """Gets the flag name for a given attribute name.
251
252    Returns a flag name for an attribute, adding prefixes as necessary or using
253    overrides if an override map is provided.
254
255    Args:
256      attribute_name: str, the name of the attribute to base the flag name on.
257      presentation_name: str, the anchor argument name of the resource the
258        attribute belongs to (e.g. '--foo').
259      flag_name_overrides: {str: str}, a dict of attribute names to exact string
260        of the flag name to use for the attribute. None if no overrides.
261      prefixes: bool, whether to use the resource name as a prefix for the flag.
262      is_anchor: bool, True if this is the anchor flag, False otherwise.
263
264    Returns:
265      (str) the name of the flag.
266    """
267    flag_name_overrides = flag_name_overrides or {}
268    if attribute_name in flag_name_overrides:
269      return flag_name_overrides.get(attribute_name)
270    if is_anchor:
271      return presentation_name
272    if attribute_name == 'project':
273      return ''
274
275    if prefixes:
276      return util.FlagNameFormat('-'.join([presentation_name, attribute_name]))
277    return util.FlagNameFormat(attribute_name)
278
279  def _GenerateInfo(self, fallthroughs_map):
280    """Gets the MultitypeResourceInfo object for the ConceptParser.
281
282    Args:
283      fallthroughs_map: {str: [googlecloudsdk.calliope.concepts.deps.
284        _FallthroughBase]}, dict keyed by attribute name to lists of
285        fallthroughs.
286
287    Returns:
288      info_holders.MultitypeResourceInfo, the ResourceInfo object.
289    """
290    return info_holders.MultitypeResourceInfo(
291        self.name,
292        self.concept_spec,
293        self.group_help,
294        self.attribute_to_args_map,
295        fallthroughs_map,
296        required=self.required,
297        plural=self.plural,
298        group=self.group)
299
300  def __eq__(self, other):
301    if not isinstance(other, type(self)):
302      return False
303    return (self.name == other.name and
304            self.concept_spec == other.concept_spec and
305            self.group_help == other.group_help and
306            self.prefixes == other.prefixes and
307            self.plural == other.plural and
308            self.required == other.required and
309            self.group == other.group)
310