1# Copyright 2014 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Application context management for the cr tool.
6
7Contains all the support code to enable the shared context used by the cr tool.
8This includes the configuration variables and command line handling.
9"""
10
11import argparse
12import os
13import cr
14
15class _DumpVisitor(cr.visitor.ExportVisitor):
16  """A visitor that prints all variables in a config hierarchy."""
17
18  def __init__(self, with_source):
19    super(_DumpVisitor, self).__init__({})
20    self.to_dump = {}
21    self.with_source = with_source
22
23  def StartNode(self):
24    if self.with_source:
25      self._DumpNow()
26    super(_DumpVisitor, self).StartNode()
27
28  def EndNode(self):
29    if self.with_source or not self.stack:
30      self._DumpNow()
31    super(_DumpVisitor, self).EndNode()
32    if not self.stack:
33      self._DumpNow()
34
35  def Visit(self, key, value):
36    super(_DumpVisitor, self).Visit(key, value)
37    if key in self.store:
38      str_value = str(self.store[key])
39      if str_value != str(os.environ.get(key, None)):
40        self.to_dump[key] = str_value
41
42  def _DumpNow(self):
43    if self.to_dump:
44      if self.with_source:
45        print 'From', self.Where()
46      for key in sorted(self.to_dump.keys()):
47        print '  ', key, '=', self.to_dump[key]
48      self.to_dump = {}
49
50
51class _ShowHelp(argparse.Action):
52  """An argparse action to print the help text.
53
54  This is like the built in help text printing action, except it knows to do
55  nothing when we are just doing the early speculative parse of the args.
56  """
57
58  def __call__(self, parser, namespace, values, option_string=None):
59    if cr.context.speculative:
60      return
61    command = cr.Command.GetActivePlugin()
62    if command:
63      command.parser.print_help()
64    else:
65      parser.print_help()
66    exit(1)
67
68
69class _ArgumentParser(argparse.ArgumentParser):
70  """An extension of an ArgumentParser to enable speculative parsing.
71
72  It supports doing an early parse that never produces errors or output, to do
73  early collection of arguments that may affect what other arguments are
74  allowed.
75  """
76
77  def error(self, message):
78    if cr.context.speculative:
79      return
80    super(_ArgumentParser, self).error(message)
81
82  def parse_args(self):
83    if cr.context.speculative:
84      result = self.parse_known_args()
85      if result:
86        return result[0]
87      return None
88    return super(_ArgumentParser, self).parse_args()
89
90  def parse_known_args(self, args=None, namespace=None):
91    result = super(_ArgumentParser, self).parse_known_args(args, namespace)
92    if result is None:
93      return namespace, None
94    return result
95
96
97# The context stack
98_stack = []
99
100
101class _ContextData:
102  pass
103
104
105class Context(cr.config.Config):
106  """The base context holder for the cr system.
107
108  This holds the common context shared throughout cr.
109  Mostly this is stored in the Config structure of variables.
110  """
111
112  def __init__(self, name='Context'):
113    super(Context, self).__init__(name)
114    self._data = _ContextData()
115
116  def CreateData(self, description='', epilog=''):
117    self._data.args = None
118    self._data.arguments = cr.config.Config('ARGS')
119    self._data.derived = cr.config.Config('DERIVED')
120    self.AddChildren(*cr.config.GLOBALS)
121    self.AddChildren(
122        cr.config.Config('ENVIRONMENT', literal=True, export=True).Set(
123            {k: self.ParseValue(v) for k, v in os.environ.items()}),
124        self._data.arguments,
125        self._data.derived,
126    )
127    # Build the command line argument parser
128    self._data.parser = _ArgumentParser(add_help=False, description=description,
129                                        epilog=epilog)
130    self._data.subparsers = self.parser.add_subparsers()
131    # Add the global arguments
132    self.AddCommonArguments(self._data.parser)
133    self._data.gclient = {}
134
135  @property
136  def data(self):
137    return self._data
138
139  def __enter__(self):
140    """ To support using 'with cr.base.context.Create():'"""
141    _stack.append(self)
142    cr.context = self
143    return self
144
145  def __exit__(self, *_):
146    _stack.pop()
147    if _stack:
148      cr.context = _stack[-1]
149    return False
150
151  def AddSubParser(self, source):
152    parser = source.AddArguments(self._data.subparsers)
153
154  @classmethod
155  def AddCommonArguments(cls, parser):
156    """Adds the command line arguments common to all commands in cr."""
157    parser.add_argument(
158        '-h', '--help',
159        action=_ShowHelp, nargs=0,
160        help='show the help message and exit.'
161    )
162    parser.add_argument(
163        '--dry-run', dest='CR_DRY_RUN',
164        action='store_true', default=None,
165        help="""
166          Don't execute commands, just print them. Implies verbose.
167          Overrides CR_DRY_RUN
168          """
169    )
170    parser.add_argument(
171        '-v', '--verbose', dest='CR_VERBOSE',
172        action='count', default=None,
173        help="""
174          Print information about commands being performed.
175          Repeating multiple times increases the verbosity level.
176          Overrides CR_VERBOSE
177          """
178    )
179
180  @property
181  def args(self):
182    return self._data.args
183
184  @property
185  def arguments(self):
186    return self._data.arguments
187
188  @property
189  def speculative(self):
190    return self._data.speculative
191
192  @property
193  def derived(self):
194    return self._data.derived
195
196  @property
197  def parser(self):
198    return self._data.parser
199
200  @property
201  def remains(self):
202    remains = getattr(self._data.args, '_remains', None)
203    if remains and remains[0] == '--':
204      remains = remains[1:]
205    return remains
206
207  @property
208  def verbose(self):
209    if self.autocompleting:
210      return False
211    return self.Find('CR_VERBOSE') or self.dry_run
212
213  @property
214  def dry_run(self):
215    if self.autocompleting:
216      return True
217    return self.Find('CR_DRY_RUN')
218
219  @property
220  def autocompleting(self):
221    return 'COMP_WORD' in os.environ
222
223  @property
224  def gclient(self):
225    if not self._data.gclient:
226      self._data.gclient = cr.base.client.ReadGClient()
227    return self._data.gclient
228
229  def ParseArgs(self, speculative=False):
230    cr.plugin.DynamicChoices.only_active = not speculative
231    self._data.speculative = speculative
232    self._data.args = self._data.parser.parse_args()
233    self._data.arguments.Wipe()
234    if self._data.args:
235      self._data.arguments.Set(
236          {k: v for k, v in vars(self._data.args).items() if v is not None})
237
238  def DumpValues(self, with_source):
239    _DumpVisitor(with_source).VisitNode(self)
240
241
242def Create(description='', epilog=''):
243  context = Context()
244  context.CreateData(description=description, epilog=epilog)
245  return context
246