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