Show More
This diff has been collapsed as it changes many lines, (520 lines changed) Show them Hide them | |||||
@@ -0,0 +1,520 b'' | |||||
|
1 | # encoding: utf-8 | |||
|
2 | """ | |||
|
3 | A base class for a configurable application. | |||
|
4 | ||||
|
5 | Authors: | |||
|
6 | ||||
|
7 | * Brian Granger | |||
|
8 | * Min RK | |||
|
9 | """ | |||
|
10 | ||||
|
11 | #----------------------------------------------------------------------------- | |||
|
12 | # Copyright (C) 2008-2011 The IPython Development Team | |||
|
13 | # | |||
|
14 | # Distributed under the terms of the BSD License. The full license is in | |||
|
15 | # the file COPYING, distributed as part of this software. | |||
|
16 | #----------------------------------------------------------------------------- | |||
|
17 | ||||
|
18 | #----------------------------------------------------------------------------- | |||
|
19 | # Imports | |||
|
20 | #----------------------------------------------------------------------------- | |||
|
21 | ||||
|
22 | import logging | |||
|
23 | import os | |||
|
24 | import re | |||
|
25 | import sys | |||
|
26 | from copy import deepcopy | |||
|
27 | from collections import defaultdict | |||
|
28 | ||||
|
29 | from IPython.external.decorator import decorator | |||
|
30 | ||||
|
31 | from IPython.config.configurable import SingletonConfigurable | |||
|
32 | from IPython.config.loader import ( | |||
|
33 | KVArgParseConfigLoader, PyFileConfigLoader, Config, ArgumentError, ConfigFileNotFound, | |||
|
34 | ) | |||
|
35 | ||||
|
36 | from IPython.utils.traitlets import ( | |||
|
37 | Unicode, List, Enum, Dict, Instance, TraitError | |||
|
38 | ) | |||
|
39 | from IPython.utils.importstring import import_item | |||
|
40 | from IPython.utils.text import indent, wrap_paragraphs, dedent | |||
|
41 | ||||
|
42 | #----------------------------------------------------------------------------- | |||
|
43 | # function for re-wrapping a helpstring | |||
|
44 | #----------------------------------------------------------------------------- | |||
|
45 | ||||
|
46 | #----------------------------------------------------------------------------- | |||
|
47 | # Descriptions for the various sections | |||
|
48 | #----------------------------------------------------------------------------- | |||
|
49 | ||||
|
50 | # merge flags&aliases into options | |||
|
51 | option_description = """ | |||
|
52 | Arguments that take values are actually convenience aliases to full | |||
|
53 | Configurables, whose aliases are listed on the help line. For more information | |||
|
54 | on full configurables, see '--help-all'. | |||
|
55 | """.strip() # trim newlines of front and back | |||
|
56 | ||||
|
57 | keyvalue_description = """ | |||
|
58 | Parameters are set from command-line arguments of the form: | |||
|
59 | `--Class.trait=value`. | |||
|
60 | This line is evaluated in Python, so simple expressions are allowed, e.g.:: | |||
|
61 | `--C.a='range(3)'` For setting C.a=[0,1,2]. | |||
|
62 | """.strip() # trim newlines of front and back | |||
|
63 | ||||
|
64 | subcommand_description = """ | |||
|
65 | Subcommands are launched as `{app} cmd [args]`. For information on using | |||
|
66 | subcommand 'cmd', do: `{app} cmd -h`. | |||
|
67 | """.strip().format(app=os.path.basename(sys.argv[0])) | |||
|
68 | # get running program name | |||
|
69 | ||||
|
70 | #----------------------------------------------------------------------------- | |||
|
71 | # Application class | |||
|
72 | #----------------------------------------------------------------------------- | |||
|
73 | ||||
|
74 | @decorator | |||
|
75 | def catch_config_error(method, app, *args, **kwargs): | |||
|
76 | """Method decorator for catching invalid config (Trait/ArgumentErrors) during init. | |||
|
77 | ||||
|
78 | On a TraitError (generally caused by bad config), this will print the trait's | |||
|
79 | message, and exit the app. | |||
|
80 | ||||
|
81 | For use on init methods, to prevent invoking excepthook on invalid input. | |||
|
82 | """ | |||
|
83 | try: | |||
|
84 | return method(app, *args, **kwargs) | |||
|
85 | except (TraitError, ArgumentError) as e: | |||
|
86 | app.print_description() | |||
|
87 | app.print_help() | |||
|
88 | app.print_examples() | |||
|
89 | app.log.fatal("Bad config encountered during initialization:") | |||
|
90 | app.log.fatal(str(e)) | |||
|
91 | app.log.debug("Config at the time: %s", app.config) | |||
|
92 | app.exit(1) | |||
|
93 | ||||
|
94 | ||||
|
95 | class ApplicationError(Exception): | |||
|
96 | pass | |||
|
97 | ||||
|
98 | ||||
|
99 | class Application(SingletonConfigurable): | |||
|
100 | """A singleton application with full configuration support.""" | |||
|
101 | ||||
|
102 | # The name of the application, will usually match the name of the command | |||
|
103 | # line application | |||
|
104 | name = Unicode(u'application') | |||
|
105 | ||||
|
106 | # The description of the application that is printed at the beginning | |||
|
107 | # of the help. | |||
|
108 | description = Unicode(u'This is an application.') | |||
|
109 | # default section descriptions | |||
|
110 | option_description = Unicode(option_description) | |||
|
111 | keyvalue_description = Unicode(keyvalue_description) | |||
|
112 | subcommand_description = Unicode(subcommand_description) | |||
|
113 | ||||
|
114 | # The usage and example string that goes at the end of the help string. | |||
|
115 | examples = Unicode() | |||
|
116 | ||||
|
117 | # A sequence of Configurable subclasses whose config=True attributes will | |||
|
118 | # be exposed at the command line. | |||
|
119 | classes = List([]) | |||
|
120 | ||||
|
121 | # The version string of this application. | |||
|
122 | version = Unicode(u'0.0') | |||
|
123 | ||||
|
124 | # The log level for the application | |||
|
125 | log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'), | |||
|
126 | default_value=logging.WARN, | |||
|
127 | config=True, | |||
|
128 | help="Set the log level by value or name.") | |||
|
129 | def _log_level_changed(self, name, old, new): | |||
|
130 | """Adjust the log level when log_level is set.""" | |||
|
131 | if isinstance(new, basestring): | |||
|
132 | new = getattr(logging, new) | |||
|
133 | self.log_level = new | |||
|
134 | self.log.setLevel(new) | |||
|
135 | ||||
|
136 | log_format = Unicode("[%(name)s] %(message)s", config=True, | |||
|
137 | help="The Logging format template", | |||
|
138 | ) | |||
|
139 | log = Instance(logging.Logger) | |||
|
140 | def _log_default(self): | |||
|
141 | """Start logging for this application. | |||
|
142 | ||||
|
143 | The default is to log to stdout using a StreaHandler. The log level | |||
|
144 | starts at loggin.WARN, but this can be adjusted by setting the | |||
|
145 | ``log_level`` attribute. | |||
|
146 | """ | |||
|
147 | log = logging.getLogger(self.__class__.__name__) | |||
|
148 | log.setLevel(self.log_level) | |||
|
149 | if sys.executable.endswith('pythonw.exe'): | |||
|
150 | # this should really go to a file, but file-logging is only | |||
|
151 | # hooked up in parallel applications | |||
|
152 | _log_handler = logging.StreamHandler(open(os.devnull, 'w')) | |||
|
153 | else: | |||
|
154 | _log_handler = logging.StreamHandler() | |||
|
155 | _log_formatter = logging.Formatter(self.log_format) | |||
|
156 | _log_handler.setFormatter(_log_formatter) | |||
|
157 | log.addHandler(_log_handler) | |||
|
158 | return log | |||
|
159 | ||||
|
160 | # the alias map for configurables | |||
|
161 | aliases = Dict({'log-level' : 'Application.log_level'}) | |||
|
162 | ||||
|
163 | # flags for loading Configurables or store_const style flags | |||
|
164 | # flags are loaded from this dict by '--key' flags | |||
|
165 | # this must be a dict of two-tuples, the first element being the Config/dict | |||
|
166 | # and the second being the help string for the flag | |||
|
167 | flags = Dict() | |||
|
168 | def _flags_changed(self, name, old, new): | |||
|
169 | """ensure flags dict is valid""" | |||
|
170 | for key,value in new.iteritems(): | |||
|
171 | assert len(value) == 2, "Bad flag: %r:%s"%(key,value) | |||
|
172 | assert isinstance(value[0], (dict, Config)), "Bad flag: %r:%s"%(key,value) | |||
|
173 | assert isinstance(value[1], basestring), "Bad flag: %r:%s"%(key,value) | |||
|
174 | ||||
|
175 | ||||
|
176 | # subcommands for launching other applications | |||
|
177 | # if this is not empty, this will be a parent Application | |||
|
178 | # this must be a dict of two-tuples, | |||
|
179 | # the first element being the application class/import string | |||
|
180 | # and the second being the help string for the subcommand | |||
|
181 | subcommands = Dict() | |||
|
182 | # parse_command_line will initialize a subapp, if requested | |||
|
183 | subapp = Instance('IPython.config.application.Application', allow_none=True) | |||
|
184 | ||||
|
185 | # extra command-line arguments that don't set config values | |||
|
186 | extra_args = List(Unicode) | |||
|
187 | ||||
|
188 | ||||
|
189 | def __init__(self, **kwargs): | |||
|
190 | SingletonConfigurable.__init__(self, **kwargs) | |||
|
191 | # Ensure my class is in self.classes, so my attributes appear in command line | |||
|
192 | # options and config files. | |||
|
193 | if self.__class__ not in self.classes: | |||
|
194 | self.classes.insert(0, self.__class__) | |||
|
195 | ||||
|
196 | def _config_changed(self, name, old, new): | |||
|
197 | SingletonConfigurable._config_changed(self, name, old, new) | |||
|
198 | self.log.debug('Config changed:') | |||
|
199 | self.log.debug(repr(new)) | |||
|
200 | ||||
|
201 | @catch_config_error | |||
|
202 | def initialize(self, argv=None): | |||
|
203 | """Do the basic steps to configure me. | |||
|
204 | ||||
|
205 | Override in subclasses. | |||
|
206 | """ | |||
|
207 | self.parse_command_line(argv) | |||
|
208 | ||||
|
209 | ||||
|
210 | def start(self): | |||
|
211 | """Start the app mainloop. | |||
|
212 | ||||
|
213 | Override in subclasses. | |||
|
214 | """ | |||
|
215 | if self.subapp is not None: | |||
|
216 | return self.subapp.start() | |||
|
217 | ||||
|
218 | def print_alias_help(self): | |||
|
219 | """Print the alias part of the help.""" | |||
|
220 | if not self.aliases: | |||
|
221 | return | |||
|
222 | ||||
|
223 | lines = [] | |||
|
224 | classdict = {} | |||
|
225 | for cls in self.classes: | |||
|
226 | # include all parents (up to, but excluding Configurable) in available names | |||
|
227 | for c in cls.mro()[:-3]: | |||
|
228 | classdict[c.__name__] = c | |||
|
229 | ||||
|
230 | for alias, longname in self.aliases.iteritems(): | |||
|
231 | classname, traitname = longname.split('.',1) | |||
|
232 | cls = classdict[classname] | |||
|
233 | ||||
|
234 | trait = cls.class_traits(config=True)[traitname] | |||
|
235 | help = cls.class_get_trait_help(trait).splitlines() | |||
|
236 | # reformat first line | |||
|
237 | help[0] = help[0].replace(longname, alias) + ' (%s)'%longname | |||
|
238 | if len(alias) == 1: | |||
|
239 | help[0] = help[0].replace('--%s='%alias, '-%s '%alias) | |||
|
240 | lines.extend(help) | |||
|
241 | # lines.append('') | |||
|
242 | print os.linesep.join(lines) | |||
|
243 | ||||
|
244 | def print_flag_help(self): | |||
|
245 | """Print the flag part of the help.""" | |||
|
246 | if not self.flags: | |||
|
247 | return | |||
|
248 | ||||
|
249 | lines = [] | |||
|
250 | for m, (cfg,help) in self.flags.iteritems(): | |||
|
251 | prefix = '--' if len(m) > 1 else '-' | |||
|
252 | lines.append(prefix+m) | |||
|
253 | lines.append(indent(dedent(help.strip()))) | |||
|
254 | # lines.append('') | |||
|
255 | print os.linesep.join(lines) | |||
|
256 | ||||
|
257 | def print_options(self): | |||
|
258 | if not self.flags and not self.aliases: | |||
|
259 | return | |||
|
260 | lines = ['Options'] | |||
|
261 | lines.append('-'*len(lines[0])) | |||
|
262 | lines.append('') | |||
|
263 | for p in wrap_paragraphs(self.option_description): | |||
|
264 | lines.append(p) | |||
|
265 | lines.append('') | |||
|
266 | print os.linesep.join(lines) | |||
|
267 | self.print_flag_help() | |||
|
268 | self.print_alias_help() | |||
|
269 | ||||
|
270 | ||||
|
271 | def print_subcommands(self): | |||
|
272 | """Print the subcommand part of the help.""" | |||
|
273 | if not self.subcommands: | |||
|
274 | return | |||
|
275 | ||||
|
276 | lines = ["Subcommands"] | |||
|
277 | lines.append('-'*len(lines[0])) | |||
|
278 | lines.append('') | |||
|
279 | for p in wrap_paragraphs(self.subcommand_description): | |||
|
280 | lines.append(p) | |||
|
281 | lines.append('') | |||
|
282 | for subc, (cls, help) in self.subcommands.iteritems(): | |||
|
283 | lines.append(subc) | |||
|
284 | if help: | |||
|
285 | lines.append(indent(dedent(help.strip()))) | |||
|
286 | lines.append('') | |||
|
287 | print os.linesep.join(lines) | |||
|
288 | ||||
|
289 | def print_help(self, classes=False): | |||
|
290 | """Print the help for each Configurable class in self.classes. | |||
|
291 | ||||
|
292 | If classes=False (the default), only flags and aliases are printed. | |||
|
293 | """ | |||
|
294 | self.print_subcommands() | |||
|
295 | self.print_options() | |||
|
296 | ||||
|
297 | if classes: | |||
|
298 | if self.classes: | |||
|
299 | print "Class parameters" | |||
|
300 | print "----------------" | |||
|
301 | ||||
|
302 | for p in wrap_paragraphs(self.keyvalue_description): | |||
|
303 | print p | |||
|
304 | ||||
|
305 | ||||
|
306 | for cls in self.classes: | |||
|
307 | cls.class_print_help() | |||
|
308 | ||||
|
309 | else: | |||
|
310 | print "To see all available configurables, use `--help-all`" | |||
|
311 | ||||
|
312 | ||||
|
313 | def print_description(self): | |||
|
314 | """Print the application description.""" | |||
|
315 | for p in wrap_paragraphs(self.description): | |||
|
316 | print p | |||
|
317 | ||||
|
318 | ||||
|
319 | def print_examples(self): | |||
|
320 | """Print usage and examples. | |||
|
321 | ||||
|
322 | This usage string goes at the end of the command line help string | |||
|
323 | and should contain examples of the application's usage. | |||
|
324 | """ | |||
|
325 | if self.examples: | |||
|
326 | print "Examples" | |||
|
327 | print "--------" | |||
|
328 | ||||
|
329 | print indent(dedent(self.examples.strip())) | |||
|
330 | ||||
|
331 | ||||
|
332 | def print_version(self): | |||
|
333 | """Print the version string.""" | |||
|
334 | print self.version | |||
|
335 | ||||
|
336 | def update_config(self, config): | |||
|
337 | """Fire the traits events when the config is updated.""" | |||
|
338 | # Save a copy of the current config. | |||
|
339 | newconfig = deepcopy(self.config) | |||
|
340 | # Merge the new config into the current one. | |||
|
341 | newconfig._merge(config) | |||
|
342 | # Save the combined config as self.config, which triggers the traits | |||
|
343 | # events. | |||
|
344 | self.config = newconfig | |||
|
345 | ||||
|
346 | @catch_config_error | |||
|
347 | def initialize_subcommand(self, subc, argv=None): | |||
|
348 | """Initialize a subcommand with argv.""" | |||
|
349 | subapp,help = self.subcommands.get(subc) | |||
|
350 | ||||
|
351 | if isinstance(subapp, basestring): | |||
|
352 | subapp = import_item(subapp) | |||
|
353 | ||||
|
354 | # clear existing instances | |||
|
355 | self.__class__.clear_instance() | |||
|
356 | # instantiate | |||
|
357 | self.subapp = subapp.instance() | |||
|
358 | # and initialize subapp | |||
|
359 | self.subapp.initialize(argv) | |||
|
360 | ||||
|
361 | def flatten_flags(self): | |||
|
362 | """flatten flags and aliases, so cl-args override as expected. | |||
|
363 | ||||
|
364 | This prevents issues such as an alias pointing to InteractiveShell, | |||
|
365 | but a config file setting the same trait in TerminalInteraciveShell | |||
|
366 | getting inappropriate priority over the command-line arg. | |||
|
367 | ||||
|
368 | Only aliases with exactly one descendent in the class list | |||
|
369 | will be promoted. | |||
|
370 | ||||
|
371 | """ | |||
|
372 | # build a tree of classes in our list that inherit from a particular | |||
|
373 | # it will be a dict by parent classname of classes in our list | |||
|
374 | # that are descendents | |||
|
375 | mro_tree = defaultdict(list) | |||
|
376 | for cls in self.classes: | |||
|
377 | clsname = cls.__name__ | |||
|
378 | for parent in cls.mro()[1:-3]: | |||
|
379 | # exclude cls itself and Configurable,HasTraits,object | |||
|
380 | mro_tree[parent.__name__].append(clsname) | |||
|
381 | # flatten aliases, which have the form: | |||
|
382 | # { 'alias' : 'Class.trait' } | |||
|
383 | aliases = {} | |||
|
384 | for alias, cls_trait in self.aliases.iteritems(): | |||
|
385 | cls,trait = cls_trait.split('.',1) | |||
|
386 | children = mro_tree[cls] | |||
|
387 | if len(children) == 1: | |||
|
388 | # exactly one descendent, promote alias | |||
|
389 | cls = children[0] | |||
|
390 | aliases[alias] = '.'.join([cls,trait]) | |||
|
391 | ||||
|
392 | # flatten flags, which are of the form: | |||
|
393 | # { 'key' : ({'Cls' : {'trait' : value}}, 'help')} | |||
|
394 | flags = {} | |||
|
395 | for key, (flagdict, help) in self.flags.iteritems(): | |||
|
396 | newflag = {} | |||
|
397 | for cls, subdict in flagdict.iteritems(): | |||
|
398 | children = mro_tree[cls] | |||
|
399 | # exactly one descendent, promote flag section | |||
|
400 | if len(children) == 1: | |||
|
401 | cls = children[0] | |||
|
402 | newflag[cls] = subdict | |||
|
403 | flags[key] = (newflag, help) | |||
|
404 | return flags, aliases | |||
|
405 | ||||
|
406 | @catch_config_error | |||
|
407 | def parse_command_line(self, argv=None): | |||
|
408 | """Parse the command line arguments.""" | |||
|
409 | argv = sys.argv[1:] if argv is None else argv | |||
|
410 | ||||
|
411 | if argv and argv[0] == 'help': | |||
|
412 | # turn `ipython help notebook` into `ipython notebook -h` | |||
|
413 | argv = argv[1:] + ['-h'] | |||
|
414 | ||||
|
415 | if self.subcommands and len(argv) > 0: | |||
|
416 | # we have subcommands, and one may have been specified | |||
|
417 | subc, subargv = argv[0], argv[1:] | |||
|
418 | if re.match(r'^\w(\-?\w)*$', subc) and subc in self.subcommands: | |||
|
419 | # it's a subcommand, and *not* a flag or class parameter | |||
|
420 | return self.initialize_subcommand(subc, subargv) | |||
|
421 | ||||
|
422 | # Arguments after a '--' argument are for the script IPython may be | |||
|
423 | # about to run, not IPython iteslf. For arguments parsed here (help and | |||
|
424 | # version), we want to only search the arguments up to the first | |||
|
425 | # occurrence of '--', which we're calling interpreted_argv. | |||
|
426 | try: | |||
|
427 | interpreted_argv = argv[:argv.index('--')] | |||
|
428 | except ValueError: | |||
|
429 | interpreted_argv = argv | |||
|
430 | ||||
|
431 | if any(x in interpreted_argv for x in ('-h', '--help-all', '--help')): | |||
|
432 | self.print_description() | |||
|
433 | self.print_help('--help-all' in interpreted_argv) | |||
|
434 | self.print_examples() | |||
|
435 | self.exit(0) | |||
|
436 | ||||
|
437 | if '--version' in interpreted_argv or '-V' in interpreted_argv: | |||
|
438 | self.print_version() | |||
|
439 | self.exit(0) | |||
|
440 | ||||
|
441 | # flatten flags&aliases, so cl-args get appropriate priority: | |||
|
442 | flags,aliases = self.flatten_flags() | |||
|
443 | ||||
|
444 | loader = KVArgParseConfigLoader(argv=argv, aliases=aliases, | |||
|
445 | flags=flags) | |||
|
446 | config = loader.load_config() | |||
|
447 | self.update_config(config) | |||
|
448 | # store unparsed args in extra_args | |||
|
449 | self.extra_args = loader.extra_args | |||
|
450 | ||||
|
451 | @catch_config_error | |||
|
452 | def load_config_file(self, filename, path=None): | |||
|
453 | """Load a .py based config file by filename and path.""" | |||
|
454 | loader = PyFileConfigLoader(filename, path=path) | |||
|
455 | try: | |||
|
456 | config = loader.load_config() | |||
|
457 | except ConfigFileNotFound: | |||
|
458 | # problem finding the file, raise | |||
|
459 | raise | |||
|
460 | except Exception: | |||
|
461 | # try to get the full filename, but it will be empty in the | |||
|
462 | # unlikely event that the error raised before filefind finished | |||
|
463 | filename = loader.full_filename or filename | |||
|
464 | # problem while running the file | |||
|
465 | self.log.error("Exception while loading config file %s", | |||
|
466 | filename, exc_info=True) | |||
|
467 | else: | |||
|
468 | self.log.debug("Loaded config file: %s", loader.full_filename) | |||
|
469 | self.update_config(config) | |||
|
470 | ||||
|
471 | def generate_config_file(self): | |||
|
472 | """generate default config file from Configurables""" | |||
|
473 | lines = ["# Configuration file for %s."%self.name] | |||
|
474 | lines.append('') | |||
|
475 | lines.append('c = get_config()') | |||
|
476 | lines.append('') | |||
|
477 | for cls in self.classes: | |||
|
478 | lines.append(cls.class_config_section()) | |||
|
479 | return '\n'.join(lines) | |||
|
480 | ||||
|
481 | def exit(self, exit_status=0): | |||
|
482 | self.log.debug("Exiting application: %s" % self.name) | |||
|
483 | sys.exit(exit_status) | |||
|
484 | ||||
|
485 | #----------------------------------------------------------------------------- | |||
|
486 | # utility functions, for convenience | |||
|
487 | #----------------------------------------------------------------------------- | |||
|
488 | ||||
|
489 | def boolean_flag(name, configurable, set_help='', unset_help=''): | |||
|
490 | """Helper for building basic --trait, --no-trait flags. | |||
|
491 | ||||
|
492 | Parameters | |||
|
493 | ---------- | |||
|
494 | ||||
|
495 | name : str | |||
|
496 | The name of the flag. | |||
|
497 | configurable : str | |||
|
498 | The 'Class.trait' string of the trait to be set/unset with the flag | |||
|
499 | set_help : unicode | |||
|
500 | help string for --name flag | |||
|
501 | unset_help : unicode | |||
|
502 | help string for --no-name flag | |||
|
503 | ||||
|
504 | Returns | |||
|
505 | ------- | |||
|
506 | ||||
|
507 | cfg : dict | |||
|
508 | A dict with two keys: 'name', and 'no-name', for setting and unsetting | |||
|
509 | the trait, respectively. | |||
|
510 | """ | |||
|
511 | # default helpstrings | |||
|
512 | set_help = set_help or "set %s=True"%configurable | |||
|
513 | unset_help = unset_help or "set %s=False"%configurable | |||
|
514 | ||||
|
515 | cls,trait = configurable.split('.') | |||
|
516 | ||||
|
517 | setter = {cls : {trait : True}} | |||
|
518 | unsetter = {cls : {trait : False}} | |||
|
519 | return {name : (setter, set_help), 'no-'+name : (unsetter, unset_help)} | |||
|
520 |
This diff has been collapsed as it changes many lines, (701 lines changed) Show them Hide them | |||||
@@ -0,0 +1,701 b'' | |||||
|
1 | """A simple configuration system. | |||
|
2 | ||||
|
3 | Inheritance diagram: | |||
|
4 | ||||
|
5 | .. inheritance-diagram:: IPython.config.loader | |||
|
6 | :parts: 3 | |||
|
7 | ||||
|
8 | Authors | |||
|
9 | ------- | |||
|
10 | * Brian Granger | |||
|
11 | * Fernando Perez | |||
|
12 | * Min RK | |||
|
13 | """ | |||
|
14 | ||||
|
15 | #----------------------------------------------------------------------------- | |||
|
16 | # Copyright (C) 2008-2011 The IPython Development Team | |||
|
17 | # | |||
|
18 | # Distributed under the terms of the BSD License. The full license is in | |||
|
19 | # the file COPYING, distributed as part of this software. | |||
|
20 | #----------------------------------------------------------------------------- | |||
|
21 | ||||
|
22 | #----------------------------------------------------------------------------- | |||
|
23 | # Imports | |||
|
24 | #----------------------------------------------------------------------------- | |||
|
25 | ||||
|
26 | import __builtin__ as builtin_mod | |||
|
27 | import os | |||
|
28 | import re | |||
|
29 | import sys | |||
|
30 | ||||
|
31 | from IPython.external import argparse | |||
|
32 | from IPython.utils.path import filefind, get_ipython_dir | |||
|
33 | from IPython.utils import py3compat, text, warn | |||
|
34 | from IPython.utils.encoding import DEFAULT_ENCODING | |||
|
35 | ||||
|
36 | #----------------------------------------------------------------------------- | |||
|
37 | # Exceptions | |||
|
38 | #----------------------------------------------------------------------------- | |||
|
39 | ||||
|
40 | ||||
|
41 | class ConfigError(Exception): | |||
|
42 | pass | |||
|
43 | ||||
|
44 | class ConfigLoaderError(ConfigError): | |||
|
45 | pass | |||
|
46 | ||||
|
47 | class ConfigFileNotFound(ConfigError): | |||
|
48 | pass | |||
|
49 | ||||
|
50 | class ArgumentError(ConfigLoaderError): | |||
|
51 | pass | |||
|
52 | ||||
|
53 | #----------------------------------------------------------------------------- | |||
|
54 | # Argparse fix | |||
|
55 | #----------------------------------------------------------------------------- | |||
|
56 | ||||
|
57 | # Unfortunately argparse by default prints help messages to stderr instead of | |||
|
58 | # stdout. This makes it annoying to capture long help screens at the command | |||
|
59 | # line, since one must know how to pipe stderr, which many users don't know how | |||
|
60 | # to do. So we override the print_help method with one that defaults to | |||
|
61 | # stdout and use our class instead. | |||
|
62 | ||||
|
63 | class ArgumentParser(argparse.ArgumentParser): | |||
|
64 | """Simple argparse subclass that prints help to stdout by default.""" | |||
|
65 | ||||
|
66 | def print_help(self, file=None): | |||
|
67 | if file is None: | |||
|
68 | file = sys.stdout | |||
|
69 | return super(ArgumentParser, self).print_help(file) | |||
|
70 | ||||
|
71 | print_help.__doc__ = argparse.ArgumentParser.print_help.__doc__ | |||
|
72 | ||||
|
73 | #----------------------------------------------------------------------------- | |||
|
74 | # Config class for holding config information | |||
|
75 | #----------------------------------------------------------------------------- | |||
|
76 | ||||
|
77 | ||||
|
78 | class Config(dict): | |||
|
79 | """An attribute based dict that can do smart merges.""" | |||
|
80 | ||||
|
81 | def __init__(self, *args, **kwds): | |||
|
82 | dict.__init__(self, *args, **kwds) | |||
|
83 | # This sets self.__dict__ = self, but it has to be done this way | |||
|
84 | # because we are also overriding __setattr__. | |||
|
85 | dict.__setattr__(self, '__dict__', self) | |||
|
86 | ||||
|
87 | def _merge(self, other): | |||
|
88 | to_update = {} | |||
|
89 | for k, v in other.iteritems(): | |||
|
90 | if k not in self: | |||
|
91 | to_update[k] = v | |||
|
92 | else: # I have this key | |||
|
93 | if isinstance(v, Config): | |||
|
94 | # Recursively merge common sub Configs | |||
|
95 | self[k]._merge(v) | |||
|
96 | else: | |||
|
97 | # Plain updates for non-Configs | |||
|
98 | to_update[k] = v | |||
|
99 | ||||
|
100 | self.update(to_update) | |||
|
101 | ||||
|
102 | def _is_section_key(self, key): | |||
|
103 | if key[0].upper()==key[0] and not key.startswith('_'): | |||
|
104 | return True | |||
|
105 | else: | |||
|
106 | return False | |||
|
107 | ||||
|
108 | def __contains__(self, key): | |||
|
109 | if self._is_section_key(key): | |||
|
110 | return True | |||
|
111 | else: | |||
|
112 | return super(Config, self).__contains__(key) | |||
|
113 | # .has_key is deprecated for dictionaries. | |||
|
114 | has_key = __contains__ | |||
|
115 | ||||
|
116 | def _has_section(self, key): | |||
|
117 | if self._is_section_key(key): | |||
|
118 | if super(Config, self).__contains__(key): | |||
|
119 | return True | |||
|
120 | return False | |||
|
121 | ||||
|
122 | def copy(self): | |||
|
123 | return type(self)(dict.copy(self)) | |||
|
124 | ||||
|
125 | def __copy__(self): | |||
|
126 | return self.copy() | |||
|
127 | ||||
|
128 | def __deepcopy__(self, memo): | |||
|
129 | import copy | |||
|
130 | return type(self)(copy.deepcopy(self.items())) | |||
|
131 | ||||
|
132 | def __getitem__(self, key): | |||
|
133 | # We cannot use directly self._is_section_key, because it triggers | |||
|
134 | # infinite recursion on top of PyPy. Instead, we manually fish the | |||
|
135 | # bound method. | |||
|
136 | is_section_key = self.__class__._is_section_key.__get__(self) | |||
|
137 | ||||
|
138 | # Because we use this for an exec namespace, we need to delegate | |||
|
139 | # the lookup of names in __builtin__ to itself. This means | |||
|
140 | # that you can't have section or attribute names that are | |||
|
141 | # builtins. | |||
|
142 | try: | |||
|
143 | return getattr(builtin_mod, key) | |||
|
144 | except AttributeError: | |||
|
145 | pass | |||
|
146 | if is_section_key(key): | |||
|
147 | try: | |||
|
148 | return dict.__getitem__(self, key) | |||
|
149 | except KeyError: | |||
|
150 | c = Config() | |||
|
151 | dict.__setitem__(self, key, c) | |||
|
152 | return c | |||
|
153 | else: | |||
|
154 | return dict.__getitem__(self, key) | |||
|
155 | ||||
|
156 | def __setitem__(self, key, value): | |||
|
157 | # Don't allow names in __builtin__ to be modified. | |||
|
158 | if hasattr(builtin_mod, key): | |||
|
159 | raise ConfigError('Config variable names cannot have the same name ' | |||
|
160 | 'as a Python builtin: %s' % key) | |||
|
161 | if self._is_section_key(key): | |||
|
162 | if not isinstance(value, Config): | |||
|
163 | raise ValueError('values whose keys begin with an uppercase ' | |||
|
164 | 'char must be Config instances: %r, %r' % (key, value)) | |||
|
165 | else: | |||
|
166 | dict.__setitem__(self, key, value) | |||
|
167 | ||||
|
168 | def __getattr__(self, key): | |||
|
169 | try: | |||
|
170 | return self.__getitem__(key) | |||
|
171 | except KeyError as e: | |||
|
172 | raise AttributeError(e) | |||
|
173 | ||||
|
174 | def __setattr__(self, key, value): | |||
|
175 | try: | |||
|
176 | self.__setitem__(key, value) | |||
|
177 | except KeyError as e: | |||
|
178 | raise AttributeError(e) | |||
|
179 | ||||
|
180 | def __delattr__(self, key): | |||
|
181 | try: | |||
|
182 | dict.__delitem__(self, key) | |||
|
183 | except KeyError as e: | |||
|
184 | raise AttributeError(e) | |||
|
185 | ||||
|
186 | ||||
|
187 | #----------------------------------------------------------------------------- | |||
|
188 | # Config loading classes | |||
|
189 | #----------------------------------------------------------------------------- | |||
|
190 | ||||
|
191 | ||||
|
192 | class ConfigLoader(object): | |||
|
193 | """A object for loading configurations from just about anywhere. | |||
|
194 | ||||
|
195 | The resulting configuration is packaged as a :class:`Struct`. | |||
|
196 | ||||
|
197 | Notes | |||
|
198 | ----- | |||
|
199 | A :class:`ConfigLoader` does one thing: load a config from a source | |||
|
200 | (file, command line arguments) and returns the data as a :class:`Struct`. | |||
|
201 | There are lots of things that :class:`ConfigLoader` does not do. It does | |||
|
202 | not implement complex logic for finding config files. It does not handle | |||
|
203 | default values or merge multiple configs. These things need to be | |||
|
204 | handled elsewhere. | |||
|
205 | """ | |||
|
206 | ||||
|
207 | def __init__(self): | |||
|
208 | """A base class for config loaders. | |||
|
209 | ||||
|
210 | Examples | |||
|
211 | -------- | |||
|
212 | ||||
|
213 | >>> cl = ConfigLoader() | |||
|
214 | >>> config = cl.load_config() | |||
|
215 | >>> config | |||
|
216 | {} | |||
|
217 | """ | |||
|
218 | self.clear() | |||
|
219 | ||||
|
220 | def clear(self): | |||
|
221 | self.config = Config() | |||
|
222 | ||||
|
223 | def load_config(self): | |||
|
224 | """Load a config from somewhere, return a :class:`Config` instance. | |||
|
225 | ||||
|
226 | Usually, this will cause self.config to be set and then returned. | |||
|
227 | However, in most cases, :meth:`ConfigLoader.clear` should be called | |||
|
228 | to erase any previous state. | |||
|
229 | """ | |||
|
230 | self.clear() | |||
|
231 | return self.config | |||
|
232 | ||||
|
233 | ||||
|
234 | class FileConfigLoader(ConfigLoader): | |||
|
235 | """A base class for file based configurations. | |||
|
236 | ||||
|
237 | As we add more file based config loaders, the common logic should go | |||
|
238 | here. | |||
|
239 | """ | |||
|
240 | pass | |||
|
241 | ||||
|
242 | ||||
|
243 | class PyFileConfigLoader(FileConfigLoader): | |||
|
244 | """A config loader for pure python files. | |||
|
245 | ||||
|
246 | This calls execfile on a plain python file and looks for attributes | |||
|
247 | that are all caps. These attribute are added to the config Struct. | |||
|
248 | """ | |||
|
249 | ||||
|
250 | def __init__(self, filename, path=None): | |||
|
251 | """Build a config loader for a filename and path. | |||
|
252 | ||||
|
253 | Parameters | |||
|
254 | ---------- | |||
|
255 | filename : str | |||
|
256 | The file name of the config file. | |||
|
257 | path : str, list, tuple | |||
|
258 | The path to search for the config file on, or a sequence of | |||
|
259 | paths to try in order. | |||
|
260 | """ | |||
|
261 | super(PyFileConfigLoader, self).__init__() | |||
|
262 | self.filename = filename | |||
|
263 | self.path = path | |||
|
264 | self.full_filename = '' | |||
|
265 | self.data = None | |||
|
266 | ||||
|
267 | def load_config(self): | |||
|
268 | """Load the config from a file and return it as a Struct.""" | |||
|
269 | self.clear() | |||
|
270 | try: | |||
|
271 | self._find_file() | |||
|
272 | except IOError as e: | |||
|
273 | raise ConfigFileNotFound(str(e)) | |||
|
274 | self._read_file_as_dict() | |||
|
275 | self._convert_to_config() | |||
|
276 | return self.config | |||
|
277 | ||||
|
278 | def _find_file(self): | |||
|
279 | """Try to find the file by searching the paths.""" | |||
|
280 | self.full_filename = filefind(self.filename, self.path) | |||
|
281 | ||||
|
282 | def _read_file_as_dict(self): | |||
|
283 | """Load the config file into self.config, with recursive loading.""" | |||
|
284 | # This closure is made available in the namespace that is used | |||
|
285 | # to exec the config file. It allows users to call | |||
|
286 | # load_subconfig('myconfig.py') to load config files recursively. | |||
|
287 | # It needs to be a closure because it has references to self.path | |||
|
288 | # and self.config. The sub-config is loaded with the same path | |||
|
289 | # as the parent, but it uses an empty config which is then merged | |||
|
290 | # with the parents. | |||
|
291 | ||||
|
292 | # If a profile is specified, the config file will be loaded | |||
|
293 | # from that profile | |||
|
294 | ||||
|
295 | def load_subconfig(fname, profile=None): | |||
|
296 | # import here to prevent circular imports | |||
|
297 | from IPython.core.profiledir import ProfileDir, ProfileDirError | |||
|
298 | if profile is not None: | |||
|
299 | try: | |||
|
300 | profile_dir = ProfileDir.find_profile_dir_by_name( | |||
|
301 | get_ipython_dir(), | |||
|
302 | profile, | |||
|
303 | ) | |||
|
304 | except ProfileDirError: | |||
|
305 | return | |||
|
306 | path = profile_dir.location | |||
|
307 | else: | |||
|
308 | path = self.path | |||
|
309 | loader = PyFileConfigLoader(fname, path) | |||
|
310 | try: | |||
|
311 | sub_config = loader.load_config() | |||
|
312 | except ConfigFileNotFound: | |||
|
313 | # Pass silently if the sub config is not there. This happens | |||
|
314 | # when a user s using a profile, but not the default config. | |||
|
315 | pass | |||
|
316 | else: | |||
|
317 | self.config._merge(sub_config) | |||
|
318 | ||||
|
319 | # Again, this needs to be a closure and should be used in config | |||
|
320 | # files to get the config being loaded. | |||
|
321 | def get_config(): | |||
|
322 | return self.config | |||
|
323 | ||||
|
324 | namespace = dict(load_subconfig=load_subconfig, get_config=get_config) | |||
|
325 | fs_encoding = sys.getfilesystemencoding() or 'ascii' | |||
|
326 | conf_filename = self.full_filename.encode(fs_encoding) | |||
|
327 | py3compat.execfile(conf_filename, namespace) | |||
|
328 | ||||
|
329 | def _convert_to_config(self): | |||
|
330 | if self.data is None: | |||
|
331 | ConfigLoaderError('self.data does not exist') | |||
|
332 | ||||
|
333 | ||||
|
334 | class CommandLineConfigLoader(ConfigLoader): | |||
|
335 | """A config loader for command line arguments. | |||
|
336 | ||||
|
337 | As we add more command line based loaders, the common logic should go | |||
|
338 | here. | |||
|
339 | """ | |||
|
340 | ||||
|
341 | def _exec_config_str(self, lhs, rhs): | |||
|
342 | """execute self.config.<lhs> = <rhs> | |||
|
343 | ||||
|
344 | * expands ~ with expanduser | |||
|
345 | * tries to assign with raw eval, otherwise assigns with just the string, | |||
|
346 | allowing `--C.a=foobar` and `--C.a="foobar"` to be equivalent. *Not* | |||
|
347 | equivalent are `--C.a=4` and `--C.a='4'`. | |||
|
348 | """ | |||
|
349 | rhs = os.path.expanduser(rhs) | |||
|
350 | try: | |||
|
351 | # Try to see if regular Python syntax will work. This | |||
|
352 | # won't handle strings as the quote marks are removed | |||
|
353 | # by the system shell. | |||
|
354 | value = eval(rhs) | |||
|
355 | except (NameError, SyntaxError): | |||
|
356 | # This case happens if the rhs is a string. | |||
|
357 | value = rhs | |||
|
358 | ||||
|
359 | exec u'self.config.%s = value' % lhs | |||
|
360 | ||||
|
361 | def _load_flag(self, cfg): | |||
|
362 | """update self.config from a flag, which can be a dict or Config""" | |||
|
363 | if isinstance(cfg, (dict, Config)): | |||
|
364 | # don't clobber whole config sections, update | |||
|
365 | # each section from config: | |||
|
366 | for sec,c in cfg.iteritems(): | |||
|
367 | self.config[sec].update(c) | |||
|
368 | else: | |||
|
369 | raise TypeError("Invalid flag: %r" % cfg) | |||
|
370 | ||||
|
371 | # raw --identifier=value pattern | |||
|
372 | # but *also* accept '-' as wordsep, for aliases | |||
|
373 | # accepts: --foo=a | |||
|
374 | # --Class.trait=value | |||
|
375 | # --alias-name=value | |||
|
376 | # rejects: -foo=value | |||
|
377 | # --foo | |||
|
378 | # --Class.trait | |||
|
379 | kv_pattern = re.compile(r'\-\-[A-Za-z][\w\-]*(\.[\w\-]+)*\=.*') | |||
|
380 | ||||
|
381 | # just flags, no assignments, with two *or one* leading '-' | |||
|
382 | # accepts: --foo | |||
|
383 | # -foo-bar-again | |||
|
384 | # rejects: --anything=anything | |||
|
385 | # --two.word | |||
|
386 | ||||
|
387 | flag_pattern = re.compile(r'\-\-?\w+[\-\w]*$') | |||
|
388 | ||||
|
389 | class KeyValueConfigLoader(CommandLineConfigLoader): | |||
|
390 | """A config loader that loads key value pairs from the command line. | |||
|
391 | ||||
|
392 | This allows command line options to be gives in the following form:: | |||
|
393 | ||||
|
394 | ipython --profile="foo" --InteractiveShell.autocall=False | |||
|
395 | """ | |||
|
396 | ||||
|
397 | def __init__(self, argv=None, aliases=None, flags=None): | |||
|
398 | """Create a key value pair config loader. | |||
|
399 | ||||
|
400 | Parameters | |||
|
401 | ---------- | |||
|
402 | argv : list | |||
|
403 | A list that has the form of sys.argv[1:] which has unicode | |||
|
404 | elements of the form u"key=value". If this is None (default), | |||
|
405 | then sys.argv[1:] will be used. | |||
|
406 | aliases : dict | |||
|
407 | A dict of aliases for configurable traits. | |||
|
408 | Keys are the short aliases, Values are the resolved trait. | |||
|
409 | Of the form: `{'alias' : 'Configurable.trait'}` | |||
|
410 | flags : dict | |||
|
411 | A dict of flags, keyed by str name. Vaues can be Config objects, | |||
|
412 | dicts, or "key=value" strings. If Config or dict, when the flag | |||
|
413 | is triggered, The flag is loaded as `self.config.update(m)`. | |||
|
414 | ||||
|
415 | Returns | |||
|
416 | ------- | |||
|
417 | config : Config | |||
|
418 | The resulting Config object. | |||
|
419 | ||||
|
420 | Examples | |||
|
421 | -------- | |||
|
422 | ||||
|
423 | >>> from IPython.config.loader import KeyValueConfigLoader | |||
|
424 | >>> cl = KeyValueConfigLoader() | |||
|
425 | >>> d = cl.load_config(["--A.name='brian'","--B.number=0"]) | |||
|
426 | >>> sorted(d.items()) | |||
|
427 | [('A', {'name': 'brian'}), ('B', {'number': 0})] | |||
|
428 | """ | |||
|
429 | self.clear() | |||
|
430 | if argv is None: | |||
|
431 | argv = sys.argv[1:] | |||
|
432 | self.argv = argv | |||
|
433 | self.aliases = aliases or {} | |||
|
434 | self.flags = flags or {} | |||
|
435 | ||||
|
436 | ||||
|
437 | def clear(self): | |||
|
438 | super(KeyValueConfigLoader, self).clear() | |||
|
439 | self.extra_args = [] | |||
|
440 | ||||
|
441 | ||||
|
442 | def _decode_argv(self, argv, enc=None): | |||
|
443 | """decode argv if bytes, using stin.encoding, falling back on default enc""" | |||
|
444 | uargv = [] | |||
|
445 | if enc is None: | |||
|
446 | enc = DEFAULT_ENCODING | |||
|
447 | for arg in argv: | |||
|
448 | if not isinstance(arg, unicode): | |||
|
449 | # only decode if not already decoded | |||
|
450 | arg = arg.decode(enc) | |||
|
451 | uargv.append(arg) | |||
|
452 | return uargv | |||
|
453 | ||||
|
454 | ||||
|
455 | def load_config(self, argv=None, aliases=None, flags=None): | |||
|
456 | """Parse the configuration and generate the Config object. | |||
|
457 | ||||
|
458 | After loading, any arguments that are not key-value or | |||
|
459 | flags will be stored in self.extra_args - a list of | |||
|
460 | unparsed command-line arguments. This is used for | |||
|
461 | arguments such as input files or subcommands. | |||
|
462 | ||||
|
463 | Parameters | |||
|
464 | ---------- | |||
|
465 | argv : list, optional | |||
|
466 | A list that has the form of sys.argv[1:] which has unicode | |||
|
467 | elements of the form u"key=value". If this is None (default), | |||
|
468 | then self.argv will be used. | |||
|
469 | aliases : dict | |||
|
470 | A dict of aliases for configurable traits. | |||
|
471 | Keys are the short aliases, Values are the resolved trait. | |||
|
472 | Of the form: `{'alias' : 'Configurable.trait'}` | |||
|
473 | flags : dict | |||
|
474 | A dict of flags, keyed by str name. Values can be Config objects | |||
|
475 | or dicts. When the flag is triggered, The config is loaded as | |||
|
476 | `self.config.update(cfg)`. | |||
|
477 | """ | |||
|
478 | from IPython.config.configurable import Configurable | |||
|
479 | ||||
|
480 | self.clear() | |||
|
481 | if argv is None: | |||
|
482 | argv = self.argv | |||
|
483 | if aliases is None: | |||
|
484 | aliases = self.aliases | |||
|
485 | if flags is None: | |||
|
486 | flags = self.flags | |||
|
487 | ||||
|
488 | # ensure argv is a list of unicode strings: | |||
|
489 | uargv = self._decode_argv(argv) | |||
|
490 | for idx,raw in enumerate(uargv): | |||
|
491 | # strip leading '-' | |||
|
492 | item = raw.lstrip('-') | |||
|
493 | ||||
|
494 | if raw == '--': | |||
|
495 | # don't parse arguments after '--' | |||
|
496 | # this is useful for relaying arguments to scripts, e.g. | |||
|
497 | # ipython -i foo.py --pylab=qt -- args after '--' go-to-foo.py | |||
|
498 | self.extra_args.extend(uargv[idx+1:]) | |||
|
499 | break | |||
|
500 | ||||
|
501 | if kv_pattern.match(raw): | |||
|
502 | lhs,rhs = item.split('=',1) | |||
|
503 | # Substitute longnames for aliases. | |||
|
504 | if lhs in aliases: | |||
|
505 | lhs = aliases[lhs] | |||
|
506 | if '.' not in lhs: | |||
|
507 | # probably a mistyped alias, but not technically illegal | |||
|
508 | warn.warn("Unrecognized alias: '%s', it will probably have no effect."%lhs) | |||
|
509 | try: | |||
|
510 | self._exec_config_str(lhs, rhs) | |||
|
511 | except Exception: | |||
|
512 | raise ArgumentError("Invalid argument: '%s'" % raw) | |||
|
513 | ||||
|
514 | elif flag_pattern.match(raw): | |||
|
515 | if item in flags: | |||
|
516 | cfg,help = flags[item] | |||
|
517 | self._load_flag(cfg) | |||
|
518 | else: | |||
|
519 | raise ArgumentError("Unrecognized flag: '%s'"%raw) | |||
|
520 | elif raw.startswith('-'): | |||
|
521 | kv = '--'+item | |||
|
522 | if kv_pattern.match(kv): | |||
|
523 | raise ArgumentError("Invalid argument: '%s', did you mean '%s'?"%(raw, kv)) | |||
|
524 | else: | |||
|
525 | raise ArgumentError("Invalid argument: '%s'"%raw) | |||
|
526 | else: | |||
|
527 | # keep all args that aren't valid in a list, | |||
|
528 | # in case our parent knows what to do with them. | |||
|
529 | self.extra_args.append(item) | |||
|
530 | return self.config | |||
|
531 | ||||
|
532 | class ArgParseConfigLoader(CommandLineConfigLoader): | |||
|
533 | """A loader that uses the argparse module to load from the command line.""" | |||
|
534 | ||||
|
535 | def __init__(self, argv=None, aliases=None, flags=None, *parser_args, **parser_kw): | |||
|
536 | """Create a config loader for use with argparse. | |||
|
537 | ||||
|
538 | Parameters | |||
|
539 | ---------- | |||
|
540 | ||||
|
541 | argv : optional, list | |||
|
542 | If given, used to read command-line arguments from, otherwise | |||
|
543 | sys.argv[1:] is used. | |||
|
544 | ||||
|
545 | parser_args : tuple | |||
|
546 | A tuple of positional arguments that will be passed to the | |||
|
547 | constructor of :class:`argparse.ArgumentParser`. | |||
|
548 | ||||
|
549 | parser_kw : dict | |||
|
550 | A tuple of keyword arguments that will be passed to the | |||
|
551 | constructor of :class:`argparse.ArgumentParser`. | |||
|
552 | ||||
|
553 | Returns | |||
|
554 | ------- | |||
|
555 | config : Config | |||
|
556 | The resulting Config object. | |||
|
557 | """ | |||
|
558 | super(CommandLineConfigLoader, self).__init__() | |||
|
559 | self.clear() | |||
|
560 | if argv is None: | |||
|
561 | argv = sys.argv[1:] | |||
|
562 | self.argv = argv | |||
|
563 | self.aliases = aliases or {} | |||
|
564 | self.flags = flags or {} | |||
|
565 | ||||
|
566 | self.parser_args = parser_args | |||
|
567 | self.version = parser_kw.pop("version", None) | |||
|
568 | kwargs = dict(argument_default=argparse.SUPPRESS) | |||
|
569 | kwargs.update(parser_kw) | |||
|
570 | self.parser_kw = kwargs | |||
|
571 | ||||
|
572 | def load_config(self, argv=None, aliases=None, flags=None): | |||
|
573 | """Parse command line arguments and return as a Config object. | |||
|
574 | ||||
|
575 | Parameters | |||
|
576 | ---------- | |||
|
577 | ||||
|
578 | args : optional, list | |||
|
579 | If given, a list with the structure of sys.argv[1:] to parse | |||
|
580 | arguments from. If not given, the instance's self.argv attribute | |||
|
581 | (given at construction time) is used.""" | |||
|
582 | self.clear() | |||
|
583 | if argv is None: | |||
|
584 | argv = self.argv | |||
|
585 | if aliases is None: | |||
|
586 | aliases = self.aliases | |||
|
587 | if flags is None: | |||
|
588 | flags = self.flags | |||
|
589 | self._create_parser(aliases, flags) | |||
|
590 | self._parse_args(argv) | |||
|
591 | self._convert_to_config() | |||
|
592 | return self.config | |||
|
593 | ||||
|
594 | def get_extra_args(self): | |||
|
595 | if hasattr(self, 'extra_args'): | |||
|
596 | return self.extra_args | |||
|
597 | else: | |||
|
598 | return [] | |||
|
599 | ||||
|
600 | def _create_parser(self, aliases=None, flags=None): | |||
|
601 | self.parser = ArgumentParser(*self.parser_args, **self.parser_kw) | |||
|
602 | self._add_arguments(aliases, flags) | |||
|
603 | ||||
|
604 | def _add_arguments(self, aliases=None, flags=None): | |||
|
605 | raise NotImplementedError("subclasses must implement _add_arguments") | |||
|
606 | ||||
|
607 | def _parse_args(self, args): | |||
|
608 | """self.parser->self.parsed_data""" | |||
|
609 | # decode sys.argv to support unicode command-line options | |||
|
610 | enc = DEFAULT_ENCODING | |||
|
611 | uargs = [py3compat.cast_unicode(a, enc) for a in args] | |||
|
612 | self.parsed_data, self.extra_args = self.parser.parse_known_args(uargs) | |||
|
613 | ||||
|
614 | def _convert_to_config(self): | |||
|
615 | """self.parsed_data->self.config""" | |||
|
616 | for k, v in vars(self.parsed_data).iteritems(): | |||
|
617 | exec "self.config.%s = v"%k in locals(), globals() | |||
|
618 | ||||
|
619 | class KVArgParseConfigLoader(ArgParseConfigLoader): | |||
|
620 | """A config loader that loads aliases and flags with argparse, | |||
|
621 | but will use KVLoader for the rest. This allows better parsing | |||
|
622 | of common args, such as `ipython -c 'print 5'`, but still gets | |||
|
623 | arbitrary config with `ipython --InteractiveShell.use_readline=False`""" | |||
|
624 | ||||
|
625 | def _add_arguments(self, aliases=None, flags=None): | |||
|
626 | self.alias_flags = {} | |||
|
627 | # print aliases, flags | |||
|
628 | if aliases is None: | |||
|
629 | aliases = self.aliases | |||
|
630 | if flags is None: | |||
|
631 | flags = self.flags | |||
|
632 | paa = self.parser.add_argument | |||
|
633 | for key,value in aliases.iteritems(): | |||
|
634 | if key in flags: | |||
|
635 | # flags | |||
|
636 | nargs = '?' | |||
|
637 | else: | |||
|
638 | nargs = None | |||
|
639 | if len(key) is 1: | |||
|
640 | paa('-'+key, '--'+key, type=unicode, dest=value, nargs=nargs) | |||
|
641 | else: | |||
|
642 | paa('--'+key, type=unicode, dest=value, nargs=nargs) | |||
|
643 | for key, (value, help) in flags.iteritems(): | |||
|
644 | if key in self.aliases: | |||
|
645 | # | |||
|
646 | self.alias_flags[self.aliases[key]] = value | |||
|
647 | continue | |||
|
648 | if len(key) is 1: | |||
|
649 | paa('-'+key, '--'+key, action='append_const', dest='_flags', const=value) | |||
|
650 | else: | |||
|
651 | paa('--'+key, action='append_const', dest='_flags', const=value) | |||
|
652 | ||||
|
653 | def _convert_to_config(self): | |||
|
654 | """self.parsed_data->self.config, parse unrecognized extra args via KVLoader.""" | |||
|
655 | # remove subconfigs list from namespace before transforming the Namespace | |||
|
656 | if '_flags' in self.parsed_data: | |||
|
657 | subcs = self.parsed_data._flags | |||
|
658 | del self.parsed_data._flags | |||
|
659 | else: | |||
|
660 | subcs = [] | |||
|
661 | ||||
|
662 | for k, v in vars(self.parsed_data).iteritems(): | |||
|
663 | if v is None: | |||
|
664 | # it was a flag that shares the name of an alias | |||
|
665 | subcs.append(self.alias_flags[k]) | |||
|
666 | else: | |||
|
667 | # eval the KV assignment | |||
|
668 | self._exec_config_str(k, v) | |||
|
669 | ||||
|
670 | for subc in subcs: | |||
|
671 | self._load_flag(subc) | |||
|
672 | ||||
|
673 | if self.extra_args: | |||
|
674 | sub_parser = KeyValueConfigLoader() | |||
|
675 | sub_parser.load_config(self.extra_args) | |||
|
676 | self.config._merge(sub_parser.config) | |||
|
677 | self.extra_args = sub_parser.extra_args | |||
|
678 | ||||
|
679 | ||||
|
680 | def load_pyconfig_files(config_files, path): | |||
|
681 | """Load multiple Python config files, merging each of them in turn. | |||
|
682 | ||||
|
683 | Parameters | |||
|
684 | ========== | |||
|
685 | config_files : list of str | |||
|
686 | List of config files names to load and merge into the config. | |||
|
687 | path : unicode | |||
|
688 | The full path to the location of the config files. | |||
|
689 | """ | |||
|
690 | config = Config() | |||
|
691 | for cf in config_files: | |||
|
692 | loader = PyFileConfigLoader(cf, path=path) | |||
|
693 | try: | |||
|
694 | next_config = loader.load_config() | |||
|
695 | except ConfigFileNotFound: | |||
|
696 | pass | |||
|
697 | except: | |||
|
698 | raise | |||
|
699 | else: | |||
|
700 | config._merge(next_config) | |||
|
701 | return config |
General Comments 0
You need to be logged in to leave comments.
Login now