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
16"""Classes that manage concepts and dependencies."""
17
18from __future__ import absolute_import
19from __future__ import division
20from __future__ import unicode_literals
21
22import functools
23
24from googlecloudsdk.calliope.concepts import deps as deps_lib
25from googlecloudsdk.command_lib.concepts import base
26from googlecloudsdk.command_lib.concepts import exceptions
27from googlecloudsdk.command_lib.concepts import names
28
29import six
30
31
32def GetPresentationNames(nodes):
33  return (child.GetPresentationName() for child in nodes)
34
35
36class DependencyManager(object):
37  """Holds dependency info for a single overall concept and creates views.
38
39  Attributes:
40    node: the DependencyNode at the root of the dependency tree for this
41      concept.
42  """
43
44  def __init__(self, node):
45    self.node = node
46
47  def ParseConcept(self, parsed_args):
48    """Parse the concept recursively by building the dependencies in a DFS.
49
50    Args are formatted in the same way as usage_text.py:GetArgsUsage, except
51    concepts in a concept group are not sorted. Concepts are displayed in the
52    order they were added to the group.
53
54    Args:
55      parsed_args: the raw parsed argparse namespace.
56
57    Raises:
58      googlecloudsdk.command_lib.concepts.exceptions.Error: if parsing fails.
59
60    Returns:
61      the parsed top-level concept.
62    """
63
64    def _ParseConcept(node):
65      """Recursive parsing."""
66      if not node.is_group:
67        fallthroughs = []
68        if node.arg_name:
69          fallthroughs.append(deps_lib.ArgFallthrough(node.arg_name))
70        fallthroughs += node.fallthroughs
71        return node.concept.Parse(
72            DependencyViewFromValue(
73                functools.partial(
74                    deps_lib.GetFromFallthroughs, fallthroughs, parsed_args),
75                marshalled_dependencies=node.dependencies))
76
77      # TODO(b/120132521) Replace and eliminate argparse extensions
78      also_optional = []  # The optional concepts that were not specified.
79      have_optional = []  # The specified optional (not required) concepts.
80      have_required = []  # The specified required concepts.
81      need_required = []  # The required concepts that must be specified.
82      namespace = {}
83      for name, child in six.iteritems(node.dependencies):
84        result = None
85        try:
86          result = _ParseConcept(child)
87          if result:
88            if child.concept.required:
89              have_required.append(child.concept)
90            else:
91              have_optional.append(child.concept)
92          else:
93            also_optional.append(child.concept)
94        except exceptions.MissingRequiredArgumentError:
95          need_required.append(child.concept)
96        namespace[name] = result
97
98      if need_required:
99        missing = ' '.join(GetPresentationNames(need_required))
100        if have_optional or have_required:
101          specified_parts = []
102          if have_required:
103            specified_parts.append(' '.join(
104                GetPresentationNames(have_required)))
105          if have_required and have_optional:
106            specified_parts.append(':')
107          if have_optional:
108            specified_parts.append(' '.join(
109                GetPresentationNames(have_optional)))
110
111          specified = ' '.join(specified_parts)
112          if have_required and have_optional:
113            if node.concept.required:
114              specified = '({})'.format(specified)
115            else:
116              specified = '[{}]'.format(specified)
117          raise exceptions.ModalGroupError(
118              node.concept.GetPresentationName(), specified, missing)
119
120      count = len(have_required) + len(have_optional)
121      if node.concept.mutex:
122        specified = ' | '.join(
123            GetPresentationNames(node.concept.concepts))
124        if node.concept.required:
125          specified = '({specified})'.format(specified=specified)
126          if count != 1:
127            raise exceptions.RequiredMutexGroupError(
128                node.concept.GetPresentationName(), specified)
129        else:
130          if count > 1:
131            raise exceptions.OptionalMutexGroupError(
132                node.concept.GetPresentationName(), specified)
133
134      return node.concept.Parse(DependencyView(namespace))
135
136    return _ParseConcept(self.node)
137
138
139class DependencyView(object):
140  """Simple namespace used by concept.Parse for concept groups."""
141
142  def __init__(self, values_dict):
143    for key, value in six.iteritems(values_dict):
144      setattr(self, names.ConvertToNamespaceName(key), value)
145
146
147class DependencyViewFromValue(object):
148  """Simple namespace for single value."""
149
150  def __init__(self, value_getter, marshalled_dependencies=None):
151    self._value_getter = value_getter
152    self._marshalled_dependencies = marshalled_dependencies
153
154  @property
155  def value(self):
156    """Lazy value getter.
157
158    Returns:
159      the value of the attribute, from its fallthroughs.
160
161    Raises:
162      deps_lib.AttributeNotFoundError: if the value cannot be found.
163    """
164    try:
165      return self._value_getter()
166    except TypeError:
167      return self._value_getter
168
169  @property
170  def marshalled_dependencies(self):
171    """Returns the marshalled dependencies or None if not marshalled."""
172    return self._marshalled_dependencies
173
174
175class DependencyNode(object):
176  """A node of a dependency tree.
177
178  Attributes:
179    name: the name that will be used to look up the dependency from higher
180      in the tree. Corresponds to the "key" of the attribute.
181    concept: the concept of the attribute.
182    dependencies: {str: DependencyNode}, a map from dependency names to
183      sub-dependency trees.
184    arg_name: str, the argument name of the attribute.
185    fallthroughs: [deps_lib._Fallthrough], the list of fallthroughs for the
186      dependency.
187    marshalled: [base.Concept], the list of concepts marshalled by concept.
188      The marshalled dependencies are generated here, but concept handles the
189      parsing.
190  """
191
192  def __init__(self, name, is_group, concept=None, dependencies=None,
193               arg_name=None, fallthroughs=None):
194    self.name = name
195    self.is_group = is_group
196    self.concept = concept
197    self.dependencies = dependencies
198    self.arg_name = arg_name
199    self.fallthroughs = fallthroughs or []
200
201  @classmethod
202  def FromAttribute(cls, attribute):
203    """Builds the dependency tree from the attribute."""
204    kwargs = {
205        'concept': attribute.concept,
206    }
207    marshal = attribute.concept.Marshal()
208    if marshal:
209      attributes = [concept.Attribute() for concept in marshal]
210    elif not isinstance(attribute, base.Attribute):
211      attributes = attribute.attributes
212    else:
213      attributes = None
214    if isinstance(attribute, base.Attribute) and (marshal or not attributes):
215      kwargs['arg_name'] = attribute.arg_name
216      kwargs['fallthroughs'] = attribute.fallthroughs
217    if attributes:
218      kwargs['dependencies'] = {a.concept.key: DependencyNode.FromAttribute(a)
219                                for a in attributes}
220    return DependencyNode(attribute.concept.key,
221                          not isinstance(attribute, base.Attribute), **kwargs)
222