# -*- 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 that manage concepts and dependencies.""" from __future__ import absolute_import from __future__ import division from __future__ import unicode_literals import functools from googlecloudsdk.calliope.concepts import deps as deps_lib from googlecloudsdk.command_lib.concepts import base from googlecloudsdk.command_lib.concepts import exceptions from googlecloudsdk.command_lib.concepts import names import six def GetPresentationNames(nodes): return (child.GetPresentationName() for child in nodes) class DependencyManager(object): """Holds dependency info for a single overall concept and creates views. Attributes: node: the DependencyNode at the root of the dependency tree for this concept. """ def __init__(self, node): self.node = node def ParseConcept(self, parsed_args): """Parse the concept recursively by building the dependencies in a DFS. Args are formatted in the same way as usage_text.py:GetArgsUsage, except concepts in a concept group are not sorted. Concepts are displayed in the order they were added to the group. Args: parsed_args: the raw parsed argparse namespace. Raises: googlecloudsdk.command_lib.concepts.exceptions.Error: if parsing fails. Returns: the parsed top-level concept. """ def _ParseConcept(node): """Recursive parsing.""" if not node.is_group: fallthroughs = [] if node.arg_name: fallthroughs.append(deps_lib.ArgFallthrough(node.arg_name)) fallthroughs += node.fallthroughs return node.concept.Parse( DependencyViewFromValue( functools.partial( deps_lib.GetFromFallthroughs, fallthroughs, parsed_args), marshalled_dependencies=node.dependencies)) # TODO(b/120132521) Replace and eliminate argparse extensions also_optional = [] # The optional concepts that were not specified. have_optional = [] # The specified optional (not required) concepts. have_required = [] # The specified required concepts. need_required = [] # The required concepts that must be specified. namespace = {} for name, child in six.iteritems(node.dependencies): result = None try: result = _ParseConcept(child) if result: if child.concept.required: have_required.append(child.concept) else: have_optional.append(child.concept) else: also_optional.append(child.concept) except exceptions.MissingRequiredArgumentError: need_required.append(child.concept) namespace[name] = result if need_required: missing = ' '.join(GetPresentationNames(need_required)) if have_optional or have_required: specified_parts = [] if have_required: specified_parts.append(' '.join( GetPresentationNames(have_required))) if have_required and have_optional: specified_parts.append(':') if have_optional: specified_parts.append(' '.join( GetPresentationNames(have_optional))) specified = ' '.join(specified_parts) if have_required and have_optional: if node.concept.required: specified = '({})'.format(specified) else: specified = '[{}]'.format(specified) raise exceptions.ModalGroupError( node.concept.GetPresentationName(), specified, missing) count = len(have_required) + len(have_optional) if node.concept.mutex: specified = ' | '.join( GetPresentationNames(node.concept.concepts)) if node.concept.required: specified = '({specified})'.format(specified=specified) if count != 1: raise exceptions.RequiredMutexGroupError( node.concept.GetPresentationName(), specified) else: if count > 1: raise exceptions.OptionalMutexGroupError( node.concept.GetPresentationName(), specified) return node.concept.Parse(DependencyView(namespace)) return _ParseConcept(self.node) class DependencyView(object): """Simple namespace used by concept.Parse for concept groups.""" def __init__(self, values_dict): for key, value in six.iteritems(values_dict): setattr(self, names.ConvertToNamespaceName(key), value) class DependencyViewFromValue(object): """Simple namespace for single value.""" def __init__(self, value_getter, marshalled_dependencies=None): self._value_getter = value_getter self._marshalled_dependencies = marshalled_dependencies @property def value(self): """Lazy value getter. Returns: the value of the attribute, from its fallthroughs. Raises: deps_lib.AttributeNotFoundError: if the value cannot be found. """ try: return self._value_getter() except TypeError: return self._value_getter @property def marshalled_dependencies(self): """Returns the marshalled dependencies or None if not marshalled.""" return self._marshalled_dependencies class DependencyNode(object): """A node of a dependency tree. Attributes: name: the name that will be used to look up the dependency from higher in the tree. Corresponds to the "key" of the attribute. concept: the concept of the attribute. dependencies: {str: DependencyNode}, a map from dependency names to sub-dependency trees. arg_name: str, the argument name of the attribute. fallthroughs: [deps_lib._Fallthrough], the list of fallthroughs for the dependency. marshalled: [base.Concept], the list of concepts marshalled by concept. The marshalled dependencies are generated here, but concept handles the parsing. """ def __init__(self, name, is_group, concept=None, dependencies=None, arg_name=None, fallthroughs=None): self.name = name self.is_group = is_group self.concept = concept self.dependencies = dependencies self.arg_name = arg_name self.fallthroughs = fallthroughs or [] @classmethod def FromAttribute(cls, attribute): """Builds the dependency tree from the attribute.""" kwargs = { 'concept': attribute.concept, } marshal = attribute.concept.Marshal() if marshal: attributes = [concept.Attribute() for concept in marshal] elif not isinstance(attribute, base.Attribute): attributes = attribute.attributes else: attributes = None if isinstance(attribute, base.Attribute) and (marshal or not attributes): kwargs['arg_name'] = attribute.arg_name kwargs['fallthroughs'] = attribute.fallthroughs if attributes: kwargs['dependencies'] = {a.concept.key: DependencyNode.FromAttribute(a) for a in attributes} return DependencyNode(attribute.concept.key, not isinstance(attribute, base.Attribute), **kwargs)