##// END OF EJS Templates
make subcommands optional
MinRK -
Show More
@@ -1,368 +1,362 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """
2 """
3 A base class for a configurable application.
3 A base class for a configurable application.
4
4
5 Authors:
5 Authors:
6
6
7 * Brian Granger
7 * Brian Granger
8 """
8 """
9
9
10 #-----------------------------------------------------------------------------
10 #-----------------------------------------------------------------------------
11 # Copyright (C) 2008-2011 The IPython Development Team
11 # Copyright (C) 2008-2011 The IPython Development Team
12 #
12 #
13 # Distributed under the terms of the BSD License. The full license is in
13 # Distributed under the terms of the BSD License. The full license is in
14 # the file COPYING, distributed as part of this software.
14 # the file COPYING, distributed as part of this software.
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16
16
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18 # Imports
18 # Imports
19 #-----------------------------------------------------------------------------
19 #-----------------------------------------------------------------------------
20
20
21 from copy import deepcopy
21 from copy import deepcopy
22 import logging
22 import logging
23 import re
23 import sys
24 import sys
24
25
25 from IPython.config.configurable import SingletonConfigurable
26 from IPython.config.configurable import SingletonConfigurable
26 from IPython.config.loader import (
27 from IPython.config.loader import (
27 KeyValueConfigLoader, PyFileConfigLoader, Config, ArgumentError
28 KeyValueConfigLoader, PyFileConfigLoader, Config, ArgumentError
28 )
29 )
29
30
30 from IPython.utils.traitlets import (
31 from IPython.utils.traitlets import (
31 Unicode, List, Int, Enum, Dict, Instance
32 Unicode, List, Int, Enum, Dict, Instance
32 )
33 )
33 from IPython.utils.importstring import import_item
34 from IPython.utils.importstring import import_item
34 from IPython.utils.text import indent
35 from IPython.utils.text import indent
35
36
36 #-----------------------------------------------------------------------------
37 #-----------------------------------------------------------------------------
37 # Descriptions for the various sections
38 # Descriptions for the various sections
38 #-----------------------------------------------------------------------------
39 #-----------------------------------------------------------------------------
39
40
40 flag_description = """
41 flag_description = """
41 Flags are command-line arguments passed as '--<flag>'.
42 Flags are command-line arguments passed as '--<flag>'.
42 These take no parameters, unlike regular key-value arguments.
43 These take no parameters, unlike regular key-value arguments.
43 They are typically used for setting boolean flags, or enabling
44 They are typically used for setting boolean flags, or enabling
44 modes that involve setting multiple options together.
45 modes that involve setting multiple options together.
45 """.strip() # trim newlines of front and back
46 """.strip() # trim newlines of front and back
46
47
47 alias_description = """
48 alias_description = """
48 These are commonly set parameters, given abbreviated aliases for convenience.
49 These are commonly set parameters, given abbreviated aliases for convenience.
49 They are set in the same `name=value` way as class parameters, where
50 They are set in the same `name=value` way as class parameters, where
50 <name> is replaced by the real parameter for which it is an alias.
51 <name> is replaced by the real parameter for which it is an alias.
51 """.strip() # trim newlines of front and back
52 """.strip() # trim newlines of front and back
52
53
53 keyvalue_description = """
54 keyvalue_description = """
54 Parameters are set from command-line arguments of the form:
55 Parameters are set from command-line arguments of the form:
55 `Class.trait=value`. Parameters will *never* be prefixed with '-'.
56 `Class.trait=value`. Parameters will *never* be prefixed with '-'.
56 This line is evaluated in Python, so simple expressions are allowed, e.g.
57 This line is evaluated in Python, so simple expressions are allowed, e.g.
57 `C.a='range(3)'` For setting C.a=[0,1,2]
58 `C.a='range(3)'` For setting C.a=[0,1,2]
58 """.strip() # trim newlines of front and back
59 """.strip() # trim newlines of front and back
59
60
60 #-----------------------------------------------------------------------------
61 #-----------------------------------------------------------------------------
61 # Application class
62 # Application class
62 #-----------------------------------------------------------------------------
63 #-----------------------------------------------------------------------------
63
64
64
65
65 class ApplicationError(Exception):
66 class ApplicationError(Exception):
66 pass
67 pass
67
68
68
69
69 class Application(SingletonConfigurable):
70 class Application(SingletonConfigurable):
70 """A singleton application with full configuration support."""
71 """A singleton application with full configuration support."""
71
72
72 # The name of the application, will usually match the name of the command
73 # The name of the application, will usually match the name of the command
73 # line application
74 # line application
74 name = Unicode(u'application')
75 name = Unicode(u'application')
75
76
76 # The description of the application that is printed at the beginning
77 # The description of the application that is printed at the beginning
77 # of the help.
78 # of the help.
78 description = Unicode(u'This is an application.')
79 description = Unicode(u'This is an application.')
79 # default section descriptions
80 # default section descriptions
80 flag_description = Unicode(flag_description)
81 flag_description = Unicode(flag_description)
81 alias_description = Unicode(alias_description)
82 alias_description = Unicode(alias_description)
82 keyvalue_description = Unicode(keyvalue_description)
83 keyvalue_description = Unicode(keyvalue_description)
83
84
84
85
85 # A sequence of Configurable subclasses whose config=True attributes will
86 # A sequence of Configurable subclasses whose config=True attributes will
86 # be exposed at the command line.
87 # be exposed at the command line.
87 classes = List([])
88 classes = List([])
88
89
89 # The version string of this application.
90 # The version string of this application.
90 version = Unicode(u'0.0')
91 version = Unicode(u'0.0')
91
92
92 # The log level for the application
93 # The log level for the application
93 log_level = Enum((0,10,20,30,40,50), default_value=logging.WARN,
94 log_level = Enum((0,10,20,30,40,50), default_value=logging.WARN,
94 config=True,
95 config=True,
95 help="Set the log level.")
96 help="Set the log level.")
96
97
97 # the alias map for configurables
98 # the alias map for configurables
98 aliases = Dict(dict(log_level='Application.log_level'))
99 aliases = Dict(dict(log_level='Application.log_level'))
99
100
100 # flags for loading Configurables or store_const style flags
101 # flags for loading Configurables or store_const style flags
101 # flags are loaded from this dict by '--key' flags
102 # flags are loaded from this dict by '--key' flags
102 # this must be a dict of two-tuples, the first element being the Config/dict
103 # this must be a dict of two-tuples, the first element being the Config/dict
103 # and the second being the help string for the flag
104 # and the second being the help string for the flag
104 flags = Dict()
105 flags = Dict()
105
106
106 # subcommands for launching other applications
107 # subcommands for launching other applications
107 # if this is not empty, this will be a parent Application
108 # if this is not empty, this will be a parent Application
108 # this must be a dict of two-tuples, the first element being the application class/import string
109 # this must be a dict of two-tuples, the first element being the application class/import string
109 # and the second being the help string for the subcommand
110 # and the second being the help string for the subcommand
110 subcommands = Dict()
111 subcommands = Dict()
111 # parse_command_line will initialize a subapp, if requested
112 # parse_command_line will initialize a subapp, if requested
112 subapp = Instance('IPython.config.application.Application', allow_none=True)
113 subapp = Instance('IPython.config.application.Application', allow_none=True)
113
114
114
115
115 def __init__(self, **kwargs):
116 def __init__(self, **kwargs):
116 SingletonConfigurable.__init__(self, **kwargs)
117 SingletonConfigurable.__init__(self, **kwargs)
117 # Add my class to self.classes so my attributes appear in command line
118 # Add my class to self.classes so my attributes appear in command line
118 # options.
119 # options.
119 self.classes.insert(0, self.__class__)
120 self.classes.insert(0, self.__class__)
120
121
121 # ensure self.flags dict is valid
122 # ensure self.flags dict is valid
122 for key,value in self.flags.iteritems():
123 for key,value in self.flags.iteritems():
123 assert len(value) == 2, "Bad flag: %r:%s"%(key,value)
124 assert len(value) == 2, "Bad flag: %r:%s"%(key,value)
124 assert isinstance(value[0], (dict, Config)), "Bad flag: %r:%s"%(key,value)
125 assert isinstance(value[0], (dict, Config)), "Bad flag: %r:%s"%(key,value)
125 assert isinstance(value[1], basestring), "Bad flag: %r:%s"%(key,value)
126 assert isinstance(value[1], basestring), "Bad flag: %r:%s"%(key,value)
126 self.init_logging()
127 self.init_logging()
127
128
128 def _config_changed(self, name, old, new):
129 def _config_changed(self, name, old, new):
129 SingletonConfigurable._config_changed(self, name, old, new)
130 SingletonConfigurable._config_changed(self, name, old, new)
130 self.log.debug('Config changed:')
131 self.log.debug('Config changed:')
131 self.log.debug(repr(new))
132 self.log.debug(repr(new))
132
133
133 def init_logging(self):
134 def init_logging(self):
134 """Start logging for this application.
135 """Start logging for this application.
135
136
136 The default is to log to stdout using a StreaHandler. The log level
137 The default is to log to stdout using a StreaHandler. The log level
137 starts at loggin.WARN, but this can be adjusted by setting the
138 starts at loggin.WARN, but this can be adjusted by setting the
138 ``log_level`` attribute.
139 ``log_level`` attribute.
139 """
140 """
140 self.log = logging.getLogger(self.__class__.__name__)
141 self.log = logging.getLogger(self.__class__.__name__)
141 self.log.setLevel(self.log_level)
142 self.log.setLevel(self.log_level)
142 self._log_handler = logging.StreamHandler()
143 self._log_handler = logging.StreamHandler()
143 self._log_formatter = logging.Formatter("[%(name)s] %(message)s")
144 self._log_formatter = logging.Formatter("[%(name)s] %(message)s")
144 self._log_handler.setFormatter(self._log_formatter)
145 self._log_handler.setFormatter(self._log_formatter)
145 self.log.addHandler(self._log_handler)
146 self.log.addHandler(self._log_handler)
146
147
147 def initialize(self, argv=None):
148 def initialize(self, argv=None):
148 """Do the basic steps to configure me.
149 """Do the basic steps to configure me.
149
150
150 Override in subclasses.
151 Override in subclasses.
151 """
152 """
152 self.parse_command_line(argv)
153 self.parse_command_line(argv)
153
154
154
155
155 def start(self):
156 def start(self):
156 """Start the app mainloop.
157 """Start the app mainloop.
157
158
158 Override in subclasses.
159 Override in subclasses.
159 """
160 """
160 if self.subapp is not None:
161 if self.subapp is not None:
161 return self.subapp.start()
162 return self.subapp.start()
162
163
163 def _log_level_changed(self, name, old, new):
164 def _log_level_changed(self, name, old, new):
164 """Adjust the log level when log_level is set."""
165 """Adjust the log level when log_level is set."""
165 self.log.setLevel(new)
166 self.log.setLevel(new)
166
167
167 def print_alias_help(self):
168 def print_alias_help(self):
168 """print the alias part of the help"""
169 """print the alias part of the help"""
169 if not self.aliases:
170 if not self.aliases:
170 return
171 return
171
172
172 lines = ['Aliases']
173 lines = ['Aliases']
173 lines.append('-'*len(lines[0]))
174 lines.append('-'*len(lines[0]))
174 lines.append(self.alias_description)
175 lines.append(self.alias_description)
175 lines.append('')
176 lines.append('')
176
177
177 classdict = {}
178 classdict = {}
178 for cls in self.classes:
179 for cls in self.classes:
179 # include all parents in available names
180 # include all parents (up to, but excluding Configurable) in available names
180 for c in cls.mro():
181 for c in cls.mro()[:-3]:
181 classdict[c.__name__] = c
182 classdict[c.__name__] = c
182
183
183 for alias, longname in self.aliases.iteritems():
184 for alias, longname in self.aliases.iteritems():
184 classname, traitname = longname.split('.',1)
185 classname, traitname = longname.split('.',1)
185 cls = classdict[classname]
186 cls = classdict[classname]
186
187
187 trait = cls.class_traits(config=True)[traitname]
188 trait = cls.class_traits(config=True)[traitname]
188 help = cls.class_get_trait_help(trait)
189 help = cls.class_get_trait_help(trait)
189 help = help.replace(longname, "%s (%s)"%(alias, longname), 1)
190 help = help.replace(longname, "%s (%s)"%(alias, longname), 1)
190 lines.append(help)
191 lines.append(help)
191 lines.append('')
192 lines.append('')
192 print '\n'.join(lines)
193 print '\n'.join(lines)
193
194
194 def print_flag_help(self):
195 def print_flag_help(self):
195 """print the flag part of the help"""
196 """print the flag part of the help"""
196 if not self.flags:
197 if not self.flags:
197 return
198 return
198
199
199 lines = ['Flags']
200 lines = ['Flags']
200 lines.append('-'*len(lines[0]))
201 lines.append('-'*len(lines[0]))
201 lines.append(self.flag_description)
202 lines.append(self.flag_description)
202 lines.append('')
203 lines.append('')
203
204
204 for m, (cfg,help) in self.flags.iteritems():
205 for m, (cfg,help) in self.flags.iteritems():
205 lines.append('--'+m)
206 lines.append('--'+m)
206 lines.append(indent(help, flatten=True))
207 lines.append(indent(help, flatten=True))
207 lines.append('')
208 lines.append('')
208 print '\n'.join(lines)
209 print '\n'.join(lines)
209
210
210 def print_subcommands(self):
211 def print_subcommands(self):
211 """print the subcommand part of the help"""
212 """print the subcommand part of the help"""
212 if not self.subcommands:
213 if not self.subcommands:
213 return
214 return
214
215
215 lines = ["Subcommands"]
216 lines = ["Subcommands"]
216 lines.append('-'*len(lines[0]))
217 lines.append('-'*len(lines[0]))
217 for subc, (cls,help) in self.subcommands.iteritems():
218 for subc, (cls,help) in self.subcommands.iteritems():
218 lines.append("%s : %s"%(subc, cls))
219 lines.append("%s : %s"%(subc, cls))
219 if help:
220 if help:
220 lines.append(indent(help, flatten=True))
221 lines.append(indent(help, flatten=True))
221 lines.append('')
222 lines.append('')
222 print '\n'.join(lines)
223 print '\n'.join(lines)
223
224
224 def print_help(self, classes=False):
225 def print_help(self, classes=False):
225 """Print the help for each Configurable class in self.classes.
226 """Print the help for each Configurable class in self.classes.
226
227
227 If classes=False (the default), only flags and aliases are printed
228 If classes=False (the default), only flags and aliases are printed
228 """
229 """
230 self.print_subcommands()
229 self.print_flag_help()
231 self.print_flag_help()
230 self.print_alias_help()
232 self.print_alias_help()
231
233
232 if classes:
234 if classes:
233 if self.classes:
235 if self.classes:
234 print "Class parameters"
236 print "Class parameters"
235 print "----------------"
237 print "----------------"
236 print self.keyvalue_description
238 print self.keyvalue_description
237 print
239 print
238
240
239 for cls in self.classes:
241 for cls in self.classes:
240 cls.class_print_help()
242 cls.class_print_help()
241 print
243 print
242 else:
244 else:
243 print "To see all available configurables, use `--help-all`"
245 print "To see all available configurables, use `--help-all`"
244 print
246 print
245
247
246 def print_description(self):
248 def print_description(self):
247 """Print the application description."""
249 """Print the application description."""
248 print self.description
250 print self.description
249 print
251 print
250
252
251 def print_version(self):
253 def print_version(self):
252 """Print the version string."""
254 """Print the version string."""
253 print self.version
255 print self.version
254
256
255 def update_config(self, config):
257 def update_config(self, config):
256 """Fire the traits events when the config is updated."""
258 """Fire the traits events when the config is updated."""
257 # Save a copy of the current config.
259 # Save a copy of the current config.
258 newconfig = deepcopy(self.config)
260 newconfig = deepcopy(self.config)
259 # Merge the new config into the current one.
261 # Merge the new config into the current one.
260 newconfig._merge(config)
262 newconfig._merge(config)
261 # Save the combined config as self.config, which triggers the traits
263 # Save the combined config as self.config, which triggers the traits
262 # events.
264 # events.
263 self.config = newconfig
265 self.config = newconfig
264
266
265 def initialize_subcommand(self, subc, argv=None):
267 def initialize_subcommand(self, subc, argv=None):
266 """Initialize a subcommand with argv"""
268 """Initialize a subcommand with argv"""
267 if '-h' in subc:
268 # requested help
269 self.print_description()
270 self.print_subcommands()
271 self.exit(0)
272 subapp,help = self.subcommands.get(subc, (None,None))
269 subapp,help = self.subcommands.get(subc, (None,None))
273 if subapp is None:
270 if subapp is None:
274 self.print_description()
271 self.print_description()
275 print "No such subcommand: %r"%subc
272 print "No such subcommand: %r"%subc
276 print
273 print
277 self.print_subcommands()
274 self.print_subcommands()
278 self.exit(1)
275 self.exit(1)
279
276
280 if isinstance(subapp, basestring):
277 if isinstance(subapp, basestring):
281 subapp = import_item(subapp)
278 subapp = import_item(subapp)
282
279
283 # instantiate
280 # instantiate
284 self.subapp = subapp()
281 self.subapp = subapp()
285 # and initialize subapp
282 # and initialize subapp
286 self.subapp.initialize(argv)
283 self.subapp.initialize(argv)
287
284
288 def parse_command_line(self, argv=None):
285 def parse_command_line(self, argv=None):
289 """Parse the command line arguments."""
286 """Parse the command line arguments."""
290 argv = sys.argv[1:] if argv is None else argv
287 argv = sys.argv[1:] if argv is None else argv
291
288
292 if self.subcommands:
289 if self.subcommands and len(argv) > 0:
293 # we have subcommands
290 # we have subcommands, and one may have been specified
294 if len(argv) == 0:
291 subc, subargv = argv[0], argv[1:]
295 # none specified
292 if re.match(r'^\w(\-?\w)*$', subc):
296 self.print_description()
293 # it's a subcommand, and *not* a flag or class parameter
297 self.print_subcommands()
294 return self.initialize_subcommand(subc, subargv)
298 self.exit(1)
299
300 return self.initialize_subcommand(argv[0], argv[1:])
301
295
302 if '-h' in argv or '--help' in argv or '--help-all' in argv:
296 if '-h' in argv or '--help' in argv or '--help-all' in argv:
303 self.print_description()
297 self.print_description()
304 self.print_help('--help-all' in argv)
298 self.print_help('--help-all' in argv)
305 self.exit(0)
299 self.exit(0)
306
300
307 if '--version' in argv:
301 if '--version' in argv:
308 self.print_version()
302 self.print_version()
309 self.exit(0)
303 self.exit(0)
310
304
311 loader = KeyValueConfigLoader(argv=argv, aliases=self.aliases,
305 loader = KeyValueConfigLoader(argv=argv, aliases=self.aliases,
312 flags=self.flags)
306 flags=self.flags)
313 try:
307 try:
314 config = loader.load_config()
308 config = loader.load_config()
315 except ArgumentError as e:
309 except ArgumentError as e:
316 self.log.fatal(str(e))
310 self.log.fatal(str(e))
317 self.print_description()
311 self.print_description()
318 self.print_help()
312 self.print_help()
319 self.exit(1)
313 self.exit(1)
320 self.update_config(config)
314 self.update_config(config)
321
315
322 def load_config_file(self, filename, path=None):
316 def load_config_file(self, filename, path=None):
323 """Load a .py based config file by filename and path."""
317 """Load a .py based config file by filename and path."""
324 loader = PyFileConfigLoader(filename, path=path)
318 loader = PyFileConfigLoader(filename, path=path)
325 config = loader.load_config()
319 config = loader.load_config()
326 self.update_config(config)
320 self.update_config(config)
327
321
328 def exit(self, exit_status=0):
322 def exit(self, exit_status=0):
329 self.log.debug("Exiting application: %s" % self.name)
323 self.log.debug("Exiting application: %s" % self.name)
330 sys.exit(exit_status)
324 sys.exit(exit_status)
331
325
332 #-----------------------------------------------------------------------------
326 #-----------------------------------------------------------------------------
333 # utility functions, for convenience
327 # utility functions, for convenience
334 #-----------------------------------------------------------------------------
328 #-----------------------------------------------------------------------------
335
329
336 def boolean_flag(name, configurable, set_help='', unset_help=''):
330 def boolean_flag(name, configurable, set_help='', unset_help=''):
337 """helper for building basic --trait, --no-trait flags
331 """helper for building basic --trait, --no-trait flags
338
332
339 Parameters
333 Parameters
340 ----------
334 ----------
341
335
342 name : str
336 name : str
343 The name of the flag.
337 The name of the flag.
344 configurable : str
338 configurable : str
345 The 'Class.trait' string of the trait to be set/unset with the flag
339 The 'Class.trait' string of the trait to be set/unset with the flag
346 set_help : unicode
340 set_help : unicode
347 help string for --name flag
341 help string for --name flag
348 unset_help : unicode
342 unset_help : unicode
349 help string for --no-name flag
343 help string for --no-name flag
350
344
351 Returns
345 Returns
352 -------
346 -------
353
347
354 cfg : dict
348 cfg : dict
355 A dict with two keys: 'name', and 'no-name', for setting and unsetting
349 A dict with two keys: 'name', and 'no-name', for setting and unsetting
356 the trait, respectively.
350 the trait, respectively.
357 """
351 """
358 # default helpstrings
352 # default helpstrings
359 set_help = set_help or "set %s=True"%configurable
353 set_help = set_help or "set %s=True"%configurable
360 unset_help = unset_help or "set %s=False"%configurable
354 unset_help = unset_help or "set %s=False"%configurable
361
355
362 cls,trait = configurable.split('.')
356 cls,trait = configurable.split('.')
363
357
364 setter = Config()
358 setter = Config()
365 setter[cls][trait] = True
359 setter[cls][trait] = True
366 unsetter = Config()
360 unsetter = Config()
367 unsetter[cls][trait] = False
361 unsetter[cls][trait] = False
368 return {name : (setter, set_help), 'no-'+name : (unsetter, unset_help)}
362 return {name : (setter, set_help), 'no-'+name : (unsetter, unset_help)}
General Comments 0
You need to be logged in to leave comments. Login now