##// END OF EJS Templates
Merge pull request #8030 from minrk/no-eval-cli...
Thomas Kluyver -
r20685:240e700e merge
parent child Browse files
Show More
@@ -1,844 +1,845 b''
1 1 # encoding: utf-8
2 2 """A simple configuration system."""
3 3
4 4 # Copyright (c) IPython Development Team.
5 5 # Distributed under the terms of the Modified BSD License.
6 6
7 7 import argparse
8 8 import copy
9 9 import logging
10 10 import os
11 11 import re
12 12 import sys
13 13 import json
14 from ast import literal_eval
14 15
15 16 from IPython.utils.path import filefind, get_ipython_dir
16 17 from IPython.utils import py3compat
17 18 from IPython.utils.encoding import DEFAULT_ENCODING
18 19 from IPython.utils.py3compat import unicode_type, iteritems
19 20 from IPython.utils.traitlets import HasTraits, List, Any
20 21
21 22 #-----------------------------------------------------------------------------
22 23 # Exceptions
23 24 #-----------------------------------------------------------------------------
24 25
25 26
26 27 class ConfigError(Exception):
27 28 pass
28 29
29 30 class ConfigLoaderError(ConfigError):
30 31 pass
31 32
32 33 class ConfigFileNotFound(ConfigError):
33 34 pass
34 35
35 36 class ArgumentError(ConfigLoaderError):
36 37 pass
37 38
38 39 #-----------------------------------------------------------------------------
39 40 # Argparse fix
40 41 #-----------------------------------------------------------------------------
41 42
42 43 # Unfortunately argparse by default prints help messages to stderr instead of
43 44 # stdout. This makes it annoying to capture long help screens at the command
44 45 # line, since one must know how to pipe stderr, which many users don't know how
45 46 # to do. So we override the print_help method with one that defaults to
46 47 # stdout and use our class instead.
47 48
48 49 class ArgumentParser(argparse.ArgumentParser):
49 50 """Simple argparse subclass that prints help to stdout by default."""
50 51
51 52 def print_help(self, file=None):
52 53 if file is None:
53 54 file = sys.stdout
54 55 return super(ArgumentParser, self).print_help(file)
55 56
56 57 print_help.__doc__ = argparse.ArgumentParser.print_help.__doc__
57 58
58 59 #-----------------------------------------------------------------------------
59 60 # Config class for holding config information
60 61 #-----------------------------------------------------------------------------
61 62
62 63 class LazyConfigValue(HasTraits):
63 64 """Proxy object for exposing methods on configurable containers
64 65
65 66 Exposes:
66 67
67 68 - append, extend, insert on lists
68 69 - update on dicts
69 70 - update, add on sets
70 71 """
71 72
72 73 _value = None
73 74
74 75 # list methods
75 76 _extend = List()
76 77 _prepend = List()
77 78
78 79 def append(self, obj):
79 80 self._extend.append(obj)
80 81
81 82 def extend(self, other):
82 83 self._extend.extend(other)
83 84
84 85 def prepend(self, other):
85 86 """like list.extend, but for the front"""
86 87 self._prepend[:0] = other
87 88
88 89 _inserts = List()
89 90 def insert(self, index, other):
90 91 if not isinstance(index, int):
91 92 raise TypeError("An integer is required")
92 93 self._inserts.append((index, other))
93 94
94 95 # dict methods
95 96 # update is used for both dict and set
96 97 _update = Any()
97 98 def update(self, other):
98 99 if self._update is None:
99 100 if isinstance(other, dict):
100 101 self._update = {}
101 102 else:
102 103 self._update = set()
103 104 self._update.update(other)
104 105
105 106 # set methods
106 107 def add(self, obj):
107 108 self.update({obj})
108 109
109 110 def get_value(self, initial):
110 111 """construct the value from the initial one
111 112
112 113 after applying any insert / extend / update changes
113 114 """
114 115 if self._value is not None:
115 116 return self._value
116 117 value = copy.deepcopy(initial)
117 118 if isinstance(value, list):
118 119 for idx, obj in self._inserts:
119 120 value.insert(idx, obj)
120 121 value[:0] = self._prepend
121 122 value.extend(self._extend)
122 123
123 124 elif isinstance(value, dict):
124 125 if self._update:
125 126 value.update(self._update)
126 127 elif isinstance(value, set):
127 128 if self._update:
128 129 value.update(self._update)
129 130 self._value = value
130 131 return value
131 132
132 133 def to_dict(self):
133 134 """return JSONable dict form of my data
134 135
135 136 Currently update as dict or set, extend, prepend as lists, and inserts as list of tuples.
136 137 """
137 138 d = {}
138 139 if self._update:
139 140 d['update'] = self._update
140 141 if self._extend:
141 142 d['extend'] = self._extend
142 143 if self._prepend:
143 144 d['prepend'] = self._prepend
144 145 elif self._inserts:
145 146 d['inserts'] = self._inserts
146 147 return d
147 148
148 149
149 150 def _is_section_key(key):
150 151 """Is a Config key a section name (does it start with a capital)?"""
151 152 if key and key[0].upper()==key[0] and not key.startswith('_'):
152 153 return True
153 154 else:
154 155 return False
155 156
156 157
157 158 class Config(dict):
158 159 """An attribute based dict that can do smart merges."""
159 160
160 161 def __init__(self, *args, **kwds):
161 162 dict.__init__(self, *args, **kwds)
162 163 self._ensure_subconfig()
163 164
164 165 def _ensure_subconfig(self):
165 166 """ensure that sub-dicts that should be Config objects are
166 167
167 168 casts dicts that are under section keys to Config objects,
168 169 which is necessary for constructing Config objects from dict literals.
169 170 """
170 171 for key in self:
171 172 obj = self[key]
172 173 if _is_section_key(key) \
173 174 and isinstance(obj, dict) \
174 175 and not isinstance(obj, Config):
175 176 setattr(self, key, Config(obj))
176 177
177 178 def _merge(self, other):
178 179 """deprecated alias, use Config.merge()"""
179 180 self.merge(other)
180 181
181 182 def merge(self, other):
182 183 """merge another config object into this one"""
183 184 to_update = {}
184 185 for k, v in iteritems(other):
185 186 if k not in self:
186 187 to_update[k] = copy.deepcopy(v)
187 188 else: # I have this key
188 189 if isinstance(v, Config) and isinstance(self[k], Config):
189 190 # Recursively merge common sub Configs
190 191 self[k].merge(v)
191 192 else:
192 193 # Plain updates for non-Configs
193 194 to_update[k] = copy.deepcopy(v)
194 195
195 196 self.update(to_update)
196 197
197 198 def collisions(self, other):
198 199 """Check for collisions between two config objects.
199 200
200 201 Returns a dict of the form {"Class": {"trait": "collision message"}}`,
201 202 indicating which values have been ignored.
202 203
203 204 An empty dict indicates no collisions.
204 205 """
205 206 collisions = {}
206 207 for section in self:
207 208 if section not in other:
208 209 continue
209 210 mine = self[section]
210 211 theirs = other[section]
211 212 for key in mine:
212 213 if key in theirs and mine[key] != theirs[key]:
213 214 collisions.setdefault(section, {})
214 215 collisions[section][key] = "%r ignored, using %r" % (mine[key], theirs[key])
215 216 return collisions
216 217
217 218 def __contains__(self, key):
218 219 # allow nested contains of the form `"Section.key" in config`
219 220 if '.' in key:
220 221 first, remainder = key.split('.', 1)
221 222 if first not in self:
222 223 return False
223 224 return remainder in self[first]
224 225
225 226 return super(Config, self).__contains__(key)
226 227
227 228 # .has_key is deprecated for dictionaries.
228 229 has_key = __contains__
229 230
230 231 def _has_section(self, key):
231 232 return _is_section_key(key) and key in self
232 233
233 234 def copy(self):
234 235 return type(self)(dict.copy(self))
235 236
236 237 def __copy__(self):
237 238 return self.copy()
238 239
239 240 def __deepcopy__(self, memo):
240 241 import copy
241 242 return type(self)(copy.deepcopy(list(self.items())))
242 243
243 244 def __getitem__(self, key):
244 245 try:
245 246 return dict.__getitem__(self, key)
246 247 except KeyError:
247 248 if _is_section_key(key):
248 249 c = Config()
249 250 dict.__setitem__(self, key, c)
250 251 return c
251 252 elif not key.startswith('_'):
252 253 # undefined, create lazy value, used for container methods
253 254 v = LazyConfigValue()
254 255 dict.__setitem__(self, key, v)
255 256 return v
256 257 else:
257 258 raise KeyError
258 259
259 260 def __setitem__(self, key, value):
260 261 if _is_section_key(key):
261 262 if not isinstance(value, Config):
262 263 raise ValueError('values whose keys begin with an uppercase '
263 264 'char must be Config instances: %r, %r' % (key, value))
264 265 dict.__setitem__(self, key, value)
265 266
266 267 def __getattr__(self, key):
267 268 if key.startswith('__'):
268 269 return dict.__getattr__(self, key)
269 270 try:
270 271 return self.__getitem__(key)
271 272 except KeyError as e:
272 273 raise AttributeError(e)
273 274
274 275 def __setattr__(self, key, value):
275 276 if key.startswith('__'):
276 277 return dict.__setattr__(self, key, value)
277 278 try:
278 279 self.__setitem__(key, value)
279 280 except KeyError as e:
280 281 raise AttributeError(e)
281 282
282 283 def __delattr__(self, key):
283 284 if key.startswith('__'):
284 285 return dict.__delattr__(self, key)
285 286 try:
286 287 dict.__delitem__(self, key)
287 288 except KeyError as e:
288 289 raise AttributeError(e)
289 290
290 291
291 292 #-----------------------------------------------------------------------------
292 293 # Config loading classes
293 294 #-----------------------------------------------------------------------------
294 295
295 296
296 297 class ConfigLoader(object):
297 298 """A object for loading configurations from just about anywhere.
298 299
299 300 The resulting configuration is packaged as a :class:`Config`.
300 301
301 302 Notes
302 303 -----
303 304 A :class:`ConfigLoader` does one thing: load a config from a source
304 305 (file, command line arguments) and returns the data as a :class:`Config` object.
305 306 There are lots of things that :class:`ConfigLoader` does not do. It does
306 307 not implement complex logic for finding config files. It does not handle
307 308 default values or merge multiple configs. These things need to be
308 309 handled elsewhere.
309 310 """
310 311
311 312 def _log_default(self):
312 313 from IPython.utils.log import get_logger
313 314 return get_logger()
314 315
315 316 def __init__(self, log=None):
316 317 """A base class for config loaders.
317 318
318 319 log : instance of :class:`logging.Logger` to use.
319 320 By default loger of :meth:`IPython.config.application.Application.instance()`
320 321 will be used
321 322
322 323 Examples
323 324 --------
324 325
325 326 >>> cl = ConfigLoader()
326 327 >>> config = cl.load_config()
327 328 >>> config
328 329 {}
329 330 """
330 331 self.clear()
331 332 if log is None:
332 333 self.log = self._log_default()
333 334 self.log.debug('Using default logger')
334 335 else:
335 336 self.log = log
336 337
337 338 def clear(self):
338 339 self.config = Config()
339 340
340 341 def load_config(self):
341 342 """Load a config from somewhere, return a :class:`Config` instance.
342 343
343 344 Usually, this will cause self.config to be set and then returned.
344 345 However, in most cases, :meth:`ConfigLoader.clear` should be called
345 346 to erase any previous state.
346 347 """
347 348 self.clear()
348 349 return self.config
349 350
350 351
351 352 class FileConfigLoader(ConfigLoader):
352 353 """A base class for file based configurations.
353 354
354 355 As we add more file based config loaders, the common logic should go
355 356 here.
356 357 """
357 358
358 359 def __init__(self, filename, path=None, **kw):
359 360 """Build a config loader for a filename and path.
360 361
361 362 Parameters
362 363 ----------
363 364 filename : str
364 365 The file name of the config file.
365 366 path : str, list, tuple
366 367 The path to search for the config file on, or a sequence of
367 368 paths to try in order.
368 369 """
369 370 super(FileConfigLoader, self).__init__(**kw)
370 371 self.filename = filename
371 372 self.path = path
372 373 self.full_filename = ''
373 374
374 375 def _find_file(self):
375 376 """Try to find the file by searching the paths."""
376 377 self.full_filename = filefind(self.filename, self.path)
377 378
378 379 class JSONFileConfigLoader(FileConfigLoader):
379 380 """A JSON file loader for config"""
380 381
381 382 def load_config(self):
382 383 """Load the config from a file and return it as a Config object."""
383 384 self.clear()
384 385 try:
385 386 self._find_file()
386 387 except IOError as e:
387 388 raise ConfigFileNotFound(str(e))
388 389 dct = self._read_file_as_dict()
389 390 self.config = self._convert_to_config(dct)
390 391 return self.config
391 392
392 393 def _read_file_as_dict(self):
393 394 with open(self.full_filename) as f:
394 395 return json.load(f)
395 396
396 397 def _convert_to_config(self, dictionary):
397 398 if 'version' in dictionary:
398 399 version = dictionary.pop('version')
399 400 else:
400 401 version = 1
401 402 self.log.warn("Unrecognized JSON config file version, assuming version {}".format(version))
402 403
403 404 if version == 1:
404 405 return Config(dictionary)
405 406 else:
406 407 raise ValueError('Unknown version of JSON config file: {version}'.format(version=version))
407 408
408 409
409 410 class PyFileConfigLoader(FileConfigLoader):
410 411 """A config loader for pure python files.
411 412
412 413 This is responsible for locating a Python config file by filename and
413 414 path, then executing it to construct a Config object.
414 415 """
415 416
416 417 def load_config(self):
417 418 """Load the config from a file and return it as a Config object."""
418 419 self.clear()
419 420 try:
420 421 self._find_file()
421 422 except IOError as e:
422 423 raise ConfigFileNotFound(str(e))
423 424 self._read_file_as_dict()
424 425 return self.config
425 426
426 427
427 428 def _read_file_as_dict(self):
428 429 """Load the config file into self.config, with recursive loading."""
429 430 # This closure is made available in the namespace that is used
430 431 # to exec the config file. It allows users to call
431 432 # load_subconfig('myconfig.py') to load config files recursively.
432 433 # It needs to be a closure because it has references to self.path
433 434 # and self.config. The sub-config is loaded with the same path
434 435 # as the parent, but it uses an empty config which is then merged
435 436 # with the parents.
436 437
437 438 # If a profile is specified, the config file will be loaded
438 439 # from that profile
439 440
440 441 def load_subconfig(fname, profile=None):
441 442 # import here to prevent circular imports
442 443 from IPython.core.profiledir import ProfileDir, ProfileDirError
443 444 if profile is not None:
444 445 try:
445 446 profile_dir = ProfileDir.find_profile_dir_by_name(
446 447 get_ipython_dir(),
447 448 profile,
448 449 )
449 450 except ProfileDirError:
450 451 return
451 452 path = profile_dir.location
452 453 else:
453 454 path = self.path
454 455 loader = PyFileConfigLoader(fname, path)
455 456 try:
456 457 sub_config = loader.load_config()
457 458 except ConfigFileNotFound:
458 459 # Pass silently if the sub config is not there. This happens
459 460 # when a user s using a profile, but not the default config.
460 461 pass
461 462 else:
462 463 self.config.merge(sub_config)
463 464
464 465 # Again, this needs to be a closure and should be used in config
465 466 # files to get the config being loaded.
466 467 def get_config():
467 468 return self.config
468 469
469 470 namespace = dict(
470 471 load_subconfig=load_subconfig,
471 472 get_config=get_config,
472 473 __file__=self.full_filename,
473 474 )
474 475 fs_encoding = sys.getfilesystemencoding() or 'ascii'
475 476 conf_filename = self.full_filename.encode(fs_encoding)
476 477 py3compat.execfile(conf_filename, namespace)
477 478
478 479
479 480 class CommandLineConfigLoader(ConfigLoader):
480 481 """A config loader for command line arguments.
481 482
482 483 As we add more command line based loaders, the common logic should go
483 484 here.
484 485 """
485 486
486 487 def _exec_config_str(self, lhs, rhs):
487 488 """execute self.config.<lhs> = <rhs>
488 489
489 490 * expands ~ with expanduser
490 * tries to assign with raw eval, otherwise assigns with just the string,
491 * tries to assign with literal_eval, otherwise assigns with just the string,
491 492 allowing `--C.a=foobar` and `--C.a="foobar"` to be equivalent. *Not*
492 493 equivalent are `--C.a=4` and `--C.a='4'`.
493 494 """
494 495 rhs = os.path.expanduser(rhs)
495 496 try:
496 497 # Try to see if regular Python syntax will work. This
497 498 # won't handle strings as the quote marks are removed
498 499 # by the system shell.
499 value = eval(rhs)
500 except (NameError, SyntaxError):
500 value = literal_eval(rhs)
501 except (NameError, SyntaxError, ValueError):
501 502 # This case happens if the rhs is a string.
502 503 value = rhs
503 504
504 505 exec(u'self.config.%s = value' % lhs)
505 506
506 507 def _load_flag(self, cfg):
507 508 """update self.config from a flag, which can be a dict or Config"""
508 509 if isinstance(cfg, (dict, Config)):
509 510 # don't clobber whole config sections, update
510 511 # each section from config:
511 512 for sec,c in iteritems(cfg):
512 513 self.config[sec].update(c)
513 514 else:
514 515 raise TypeError("Invalid flag: %r" % cfg)
515 516
516 517 # raw --identifier=value pattern
517 518 # but *also* accept '-' as wordsep, for aliases
518 519 # accepts: --foo=a
519 520 # --Class.trait=value
520 521 # --alias-name=value
521 522 # rejects: -foo=value
522 523 # --foo
523 524 # --Class.trait
524 525 kv_pattern = re.compile(r'\-\-[A-Za-z][\w\-]*(\.[\w\-]+)*\=.*')
525 526
526 527 # just flags, no assignments, with two *or one* leading '-'
527 528 # accepts: --foo
528 529 # -foo-bar-again
529 530 # rejects: --anything=anything
530 531 # --two.word
531 532
532 533 flag_pattern = re.compile(r'\-\-?\w+[\-\w]*$')
533 534
534 535 class KeyValueConfigLoader(CommandLineConfigLoader):
535 536 """A config loader that loads key value pairs from the command line.
536 537
537 538 This allows command line options to be gives in the following form::
538 539
539 540 ipython --profile="foo" --InteractiveShell.autocall=False
540 541 """
541 542
542 543 def __init__(self, argv=None, aliases=None, flags=None, **kw):
543 544 """Create a key value pair config loader.
544 545
545 546 Parameters
546 547 ----------
547 548 argv : list
548 549 A list that has the form of sys.argv[1:] which has unicode
549 550 elements of the form u"key=value". If this is None (default),
550 551 then sys.argv[1:] will be used.
551 552 aliases : dict
552 553 A dict of aliases for configurable traits.
553 554 Keys are the short aliases, Values are the resolved trait.
554 555 Of the form: `{'alias' : 'Configurable.trait'}`
555 556 flags : dict
556 557 A dict of flags, keyed by str name. Vaues can be Config objects,
557 558 dicts, or "key=value" strings. If Config or dict, when the flag
558 559 is triggered, The flag is loaded as `self.config.update(m)`.
559 560
560 561 Returns
561 562 -------
562 563 config : Config
563 564 The resulting Config object.
564 565
565 566 Examples
566 567 --------
567 568
568 569 >>> from IPython.config.loader import KeyValueConfigLoader
569 570 >>> cl = KeyValueConfigLoader()
570 571 >>> d = cl.load_config(["--A.name='brian'","--B.number=0"])
571 572 >>> sorted(d.items())
572 573 [('A', {'name': 'brian'}), ('B', {'number': 0})]
573 574 """
574 575 super(KeyValueConfigLoader, self).__init__(**kw)
575 576 if argv is None:
576 577 argv = sys.argv[1:]
577 578 self.argv = argv
578 579 self.aliases = aliases or {}
579 580 self.flags = flags or {}
580 581
581 582
582 583 def clear(self):
583 584 super(KeyValueConfigLoader, self).clear()
584 585 self.extra_args = []
585 586
586 587
587 588 def _decode_argv(self, argv, enc=None):
588 589 """decode argv if bytes, using stdin.encoding, falling back on default enc"""
589 590 uargv = []
590 591 if enc is None:
591 592 enc = DEFAULT_ENCODING
592 593 for arg in argv:
593 594 if not isinstance(arg, unicode_type):
594 595 # only decode if not already decoded
595 596 arg = arg.decode(enc)
596 597 uargv.append(arg)
597 598 return uargv
598 599
599 600
600 601 def load_config(self, argv=None, aliases=None, flags=None):
601 602 """Parse the configuration and generate the Config object.
602 603
603 604 After loading, any arguments that are not key-value or
604 605 flags will be stored in self.extra_args - a list of
605 606 unparsed command-line arguments. This is used for
606 607 arguments such as input files or subcommands.
607 608
608 609 Parameters
609 610 ----------
610 611 argv : list, optional
611 612 A list that has the form of sys.argv[1:] which has unicode
612 613 elements of the form u"key=value". If this is None (default),
613 614 then self.argv will be used.
614 615 aliases : dict
615 616 A dict of aliases for configurable traits.
616 617 Keys are the short aliases, Values are the resolved trait.
617 618 Of the form: `{'alias' : 'Configurable.trait'}`
618 619 flags : dict
619 620 A dict of flags, keyed by str name. Values can be Config objects
620 621 or dicts. When the flag is triggered, The config is loaded as
621 622 `self.config.update(cfg)`.
622 623 """
623 624 self.clear()
624 625 if argv is None:
625 626 argv = self.argv
626 627 if aliases is None:
627 628 aliases = self.aliases
628 629 if flags is None:
629 630 flags = self.flags
630 631
631 632 # ensure argv is a list of unicode strings:
632 633 uargv = self._decode_argv(argv)
633 634 for idx,raw in enumerate(uargv):
634 635 # strip leading '-'
635 636 item = raw.lstrip('-')
636 637
637 638 if raw == '--':
638 639 # don't parse arguments after '--'
639 640 # this is useful for relaying arguments to scripts, e.g.
640 641 # ipython -i foo.py --matplotlib=qt -- args after '--' go-to-foo.py
641 642 self.extra_args.extend(uargv[idx+1:])
642 643 break
643 644
644 645 if kv_pattern.match(raw):
645 646 lhs,rhs = item.split('=',1)
646 647 # Substitute longnames for aliases.
647 648 if lhs in aliases:
648 649 lhs = aliases[lhs]
649 650 if '.' not in lhs:
650 651 # probably a mistyped alias, but not technically illegal
651 652 self.log.warn("Unrecognized alias: '%s', it will probably have no effect.", raw)
652 653 try:
653 654 self._exec_config_str(lhs, rhs)
654 655 except Exception:
655 656 raise ArgumentError("Invalid argument: '%s'" % raw)
656 657
657 658 elif flag_pattern.match(raw):
658 659 if item in flags:
659 660 cfg,help = flags[item]
660 661 self._load_flag(cfg)
661 662 else:
662 663 raise ArgumentError("Unrecognized flag: '%s'"%raw)
663 664 elif raw.startswith('-'):
664 665 kv = '--'+item
665 666 if kv_pattern.match(kv):
666 667 raise ArgumentError("Invalid argument: '%s', did you mean '%s'?"%(raw, kv))
667 668 else:
668 669 raise ArgumentError("Invalid argument: '%s'"%raw)
669 670 else:
670 671 # keep all args that aren't valid in a list,
671 672 # in case our parent knows what to do with them.
672 673 self.extra_args.append(item)
673 674 return self.config
674 675
675 676 class ArgParseConfigLoader(CommandLineConfigLoader):
676 677 """A loader that uses the argparse module to load from the command line."""
677 678
678 679 def __init__(self, argv=None, aliases=None, flags=None, log=None, *parser_args, **parser_kw):
679 680 """Create a config loader for use with argparse.
680 681
681 682 Parameters
682 683 ----------
683 684
684 685 argv : optional, list
685 686 If given, used to read command-line arguments from, otherwise
686 687 sys.argv[1:] is used.
687 688
688 689 parser_args : tuple
689 690 A tuple of positional arguments that will be passed to the
690 691 constructor of :class:`argparse.ArgumentParser`.
691 692
692 693 parser_kw : dict
693 694 A tuple of keyword arguments that will be passed to the
694 695 constructor of :class:`argparse.ArgumentParser`.
695 696
696 697 Returns
697 698 -------
698 699 config : Config
699 700 The resulting Config object.
700 701 """
701 702 super(CommandLineConfigLoader, self).__init__(log=log)
702 703 self.clear()
703 704 if argv is None:
704 705 argv = sys.argv[1:]
705 706 self.argv = argv
706 707 self.aliases = aliases or {}
707 708 self.flags = flags or {}
708 709
709 710 self.parser_args = parser_args
710 711 self.version = parser_kw.pop("version", None)
711 712 kwargs = dict(argument_default=argparse.SUPPRESS)
712 713 kwargs.update(parser_kw)
713 714 self.parser_kw = kwargs
714 715
715 716 def load_config(self, argv=None, aliases=None, flags=None):
716 717 """Parse command line arguments and return as a Config object.
717 718
718 719 Parameters
719 720 ----------
720 721
721 722 args : optional, list
722 723 If given, a list with the structure of sys.argv[1:] to parse
723 724 arguments from. If not given, the instance's self.argv attribute
724 725 (given at construction time) is used."""
725 726 self.clear()
726 727 if argv is None:
727 728 argv = self.argv
728 729 if aliases is None:
729 730 aliases = self.aliases
730 731 if flags is None:
731 732 flags = self.flags
732 733 self._create_parser(aliases, flags)
733 734 self._parse_args(argv)
734 735 self._convert_to_config()
735 736 return self.config
736 737
737 738 def get_extra_args(self):
738 739 if hasattr(self, 'extra_args'):
739 740 return self.extra_args
740 741 else:
741 742 return []
742 743
743 744 def _create_parser(self, aliases=None, flags=None):
744 745 self.parser = ArgumentParser(*self.parser_args, **self.parser_kw)
745 746 self._add_arguments(aliases, flags)
746 747
747 748 def _add_arguments(self, aliases=None, flags=None):
748 749 raise NotImplementedError("subclasses must implement _add_arguments")
749 750
750 751 def _parse_args(self, args):
751 752 """self.parser->self.parsed_data"""
752 753 # decode sys.argv to support unicode command-line options
753 754 enc = DEFAULT_ENCODING
754 755 uargs = [py3compat.cast_unicode(a, enc) for a in args]
755 756 self.parsed_data, self.extra_args = self.parser.parse_known_args(uargs)
756 757
757 758 def _convert_to_config(self):
758 759 """self.parsed_data->self.config"""
759 760 for k, v in iteritems(vars(self.parsed_data)):
760 761 exec("self.config.%s = v"%k, locals(), globals())
761 762
762 763 class KVArgParseConfigLoader(ArgParseConfigLoader):
763 764 """A config loader that loads aliases and flags with argparse,
764 765 but will use KVLoader for the rest. This allows better parsing
765 766 of common args, such as `ipython -c 'print 5'`, but still gets
766 767 arbitrary config with `ipython --InteractiveShell.use_readline=False`"""
767 768
768 769 def _add_arguments(self, aliases=None, flags=None):
769 770 self.alias_flags = {}
770 771 # print aliases, flags
771 772 if aliases is None:
772 773 aliases = self.aliases
773 774 if flags is None:
774 775 flags = self.flags
775 776 paa = self.parser.add_argument
776 777 for key,value in iteritems(aliases):
777 778 if key in flags:
778 779 # flags
779 780 nargs = '?'
780 781 else:
781 782 nargs = None
782 783 if len(key) is 1:
783 784 paa('-'+key, '--'+key, type=unicode_type, dest=value, nargs=nargs)
784 785 else:
785 786 paa('--'+key, type=unicode_type, dest=value, nargs=nargs)
786 787 for key, (value, help) in iteritems(flags):
787 788 if key in self.aliases:
788 789 #
789 790 self.alias_flags[self.aliases[key]] = value
790 791 continue
791 792 if len(key) is 1:
792 793 paa('-'+key, '--'+key, action='append_const', dest='_flags', const=value)
793 794 else:
794 795 paa('--'+key, action='append_const', dest='_flags', const=value)
795 796
796 797 def _convert_to_config(self):
797 798 """self.parsed_data->self.config, parse unrecognized extra args via KVLoader."""
798 799 # remove subconfigs list from namespace before transforming the Namespace
799 800 if '_flags' in self.parsed_data:
800 801 subcs = self.parsed_data._flags
801 802 del self.parsed_data._flags
802 803 else:
803 804 subcs = []
804 805
805 806 for k, v in iteritems(vars(self.parsed_data)):
806 807 if v is None:
807 808 # it was a flag that shares the name of an alias
808 809 subcs.append(self.alias_flags[k])
809 810 else:
810 811 # eval the KV assignment
811 812 self._exec_config_str(k, v)
812 813
813 814 for subc in subcs:
814 815 self._load_flag(subc)
815 816
816 817 if self.extra_args:
817 818 sub_parser = KeyValueConfigLoader(log=self.log)
818 819 sub_parser.load_config(self.extra_args)
819 820 self.config.merge(sub_parser.config)
820 821 self.extra_args = sub_parser.extra_args
821 822
822 823
823 824 def load_pyconfig_files(config_files, path):
824 825 """Load multiple Python config files, merging each of them in turn.
825 826
826 827 Parameters
827 828 ==========
828 829 config_files : list of str
829 830 List of config files names to load and merge into the config.
830 831 path : unicode
831 832 The full path to the location of the config files.
832 833 """
833 834 config = Config()
834 835 for cf in config_files:
835 836 loader = PyFileConfigLoader(cf, path=path)
836 837 try:
837 838 next_config = loader.load_config()
838 839 except ConfigFileNotFound:
839 840 pass
840 841 except:
841 842 raise
842 843 else:
843 844 config.merge(next_config)
844 845 return config
@@ -1,404 +1,413 b''
1 1 # encoding: utf-8
2 2 """Tests for IPython.config.loader"""
3 3
4 4 # Copyright (c) IPython Development Team.
5 5 # Distributed under the terms of the Modified BSD License.
6 6
7 7 import os
8 8 import pickle
9 9 import sys
10 10
11 11 from tempfile import mkstemp
12 12 from unittest import TestCase
13 13
14 14 from nose import SkipTest
15 15 import nose.tools as nt
16 16
17 17
18 18
19 19 from IPython.config.loader import (
20 20 Config,
21 21 LazyConfigValue,
22 22 PyFileConfigLoader,
23 23 JSONFileConfigLoader,
24 24 KeyValueConfigLoader,
25 25 ArgParseConfigLoader,
26 26 KVArgParseConfigLoader,
27 27 ConfigError,
28 28 )
29 29
30 30
31 31 pyfile = """
32 32 c = get_config()
33 33 c.a=10
34 34 c.b=20
35 35 c.Foo.Bar.value=10
36 c.Foo.Bam.value=list(range(10)) # list() is just so it's the same on Python 3
36 c.Foo.Bam.value=list(range(10))
37 37 c.D.C.value='hi there'
38 38 """
39 39
40 40 json1file = """
41 41 {
42 42 "version": 1,
43 43 "a": 10,
44 44 "b": 20,
45 45 "Foo": {
46 46 "Bam": {
47 47 "value": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
48 48 },
49 49 "Bar": {
50 50 "value": 10
51 51 }
52 52 },
53 53 "D": {
54 54 "C": {
55 55 "value": "hi there"
56 56 }
57 57 }
58 58 }
59 59 """
60 60
61 61 # should not load
62 62 json2file = """
63 63 {
64 64 "version": 2
65 65 }
66 66 """
67 67
68 68 import logging
69 69 log = logging.getLogger('devnull')
70 70 log.setLevel(0)
71 71
72 72 class TestFileCL(TestCase):
73 73
74 74 def _check_conf(self, config):
75 75 self.assertEqual(config.a, 10)
76 76 self.assertEqual(config.b, 20)
77 77 self.assertEqual(config.Foo.Bar.value, 10)
78 78 self.assertEqual(config.Foo.Bam.value, list(range(10)))
79 79 self.assertEqual(config.D.C.value, 'hi there')
80 80
81 81 def test_python(self):
82 82 fd, fname = mkstemp('.py')
83 83 f = os.fdopen(fd, 'w')
84 84 f.write(pyfile)
85 85 f.close()
86 86 # Unlink the file
87 87 cl = PyFileConfigLoader(fname, log=log)
88 88 config = cl.load_config()
89 89 self._check_conf(config)
90 90
91 91 def test_json(self):
92 92 fd, fname = mkstemp('.json')
93 93 f = os.fdopen(fd, 'w')
94 94 f.write(json1file)
95 95 f.close()
96 96 # Unlink the file
97 97 cl = JSONFileConfigLoader(fname, log=log)
98 98 config = cl.load_config()
99 99 self._check_conf(config)
100 100
101 101 def test_collision(self):
102 102 a = Config()
103 103 b = Config()
104 104 self.assertEqual(a.collisions(b), {})
105 105 a.A.trait1 = 1
106 106 b.A.trait2 = 2
107 107 self.assertEqual(a.collisions(b), {})
108 108 b.A.trait1 = 1
109 109 self.assertEqual(a.collisions(b), {})
110 110 b.A.trait1 = 0
111 111 self.assertEqual(a.collisions(b), {
112 112 'A': {
113 113 'trait1': "1 ignored, using 0",
114 114 }
115 115 })
116 116 self.assertEqual(b.collisions(a), {
117 117 'A': {
118 118 'trait1': "0 ignored, using 1",
119 119 }
120 120 })
121 121 a.A.trait2 = 3
122 122 self.assertEqual(b.collisions(a), {
123 123 'A': {
124 124 'trait1': "0 ignored, using 1",
125 125 'trait2': "2 ignored, using 3",
126 126 }
127 127 })
128 128
129 129 def test_v2raise(self):
130 130 fd, fname = mkstemp('.json')
131 131 f = os.fdopen(fd, 'w')
132 132 f.write(json2file)
133 133 f.close()
134 134 # Unlink the file
135 135 cl = JSONFileConfigLoader(fname, log=log)
136 136 with nt.assert_raises(ValueError):
137 137 cl.load_config()
138 138
139 139
140 140 class MyLoader1(ArgParseConfigLoader):
141 141 def _add_arguments(self, aliases=None, flags=None):
142 142 p = self.parser
143 143 p.add_argument('-f', '--foo', dest='Global.foo', type=str)
144 144 p.add_argument('-b', dest='MyClass.bar', type=int)
145 145 p.add_argument('-n', dest='n', action='store_true')
146 146 p.add_argument('Global.bam', type=str)
147 147
148 148 class MyLoader2(ArgParseConfigLoader):
149 149 def _add_arguments(self, aliases=None, flags=None):
150 150 subparsers = self.parser.add_subparsers(dest='subparser_name')
151 151 subparser1 = subparsers.add_parser('1')
152 152 subparser1.add_argument('-x',dest='Global.x')
153 153 subparser2 = subparsers.add_parser('2')
154 154 subparser2.add_argument('y')
155 155
156 156 class TestArgParseCL(TestCase):
157 157
158 158 def test_basic(self):
159 159 cl = MyLoader1()
160 160 config = cl.load_config('-f hi -b 10 -n wow'.split())
161 161 self.assertEqual(config.Global.foo, 'hi')
162 162 self.assertEqual(config.MyClass.bar, 10)
163 163 self.assertEqual(config.n, True)
164 164 self.assertEqual(config.Global.bam, 'wow')
165 165 config = cl.load_config(['wow'])
166 166 self.assertEqual(list(config.keys()), ['Global'])
167 167 self.assertEqual(list(config.Global.keys()), ['bam'])
168 168 self.assertEqual(config.Global.bam, 'wow')
169 169
170 170 def test_add_arguments(self):
171 171 cl = MyLoader2()
172 172 config = cl.load_config('2 frobble'.split())
173 173 self.assertEqual(config.subparser_name, '2')
174 174 self.assertEqual(config.y, 'frobble')
175 175 config = cl.load_config('1 -x frobble'.split())
176 176 self.assertEqual(config.subparser_name, '1')
177 177 self.assertEqual(config.Global.x, 'frobble')
178 178
179 179 def test_argv(self):
180 180 cl = MyLoader1(argv='-f hi -b 10 -n wow'.split())
181 181 config = cl.load_config()
182 182 self.assertEqual(config.Global.foo, 'hi')
183 183 self.assertEqual(config.MyClass.bar, 10)
184 184 self.assertEqual(config.n, True)
185 185 self.assertEqual(config.Global.bam, 'wow')
186 186
187 187
188 188 class TestKeyValueCL(TestCase):
189 189 klass = KeyValueConfigLoader
190 190
191 def test_eval(self):
192 cl = self.klass(log=log)
193 config = cl.load_config('--Class.str_trait=all --Class.int_trait=5 --Class.list_trait=["hello",5]'.split())
194 self.assertEqual(config.Class.str_trait, 'all')
195 self.assertEqual(config.Class.int_trait, 5)
196 self.assertEqual(config.Class.list_trait, ["hello", 5])
197
191 198 def test_basic(self):
192 199 cl = self.klass(log=log)
193 argv = ['--'+s.strip('c.') for s in pyfile.split('\n')[2:-1]]
200 argv = [ '--' + s[2:] for s in pyfile.split('\n') if s.startswith('c.') ]
201 print(argv)
194 202 config = cl.load_config(argv)
195 203 self.assertEqual(config.a, 10)
196 204 self.assertEqual(config.b, 20)
197 205 self.assertEqual(config.Foo.Bar.value, 10)
198 self.assertEqual(config.Foo.Bam.value, list(range(10)))
206 # non-literal expressions are not evaluated
207 self.assertEqual(config.Foo.Bam.value, 'list(range(10))')
199 208 self.assertEqual(config.D.C.value, 'hi there')
200 209
201 210 def test_expanduser(self):
202 211 cl = self.klass(log=log)
203 212 argv = ['--a=~/1/2/3', '--b=~', '--c=~/', '--d="~/"']
204 213 config = cl.load_config(argv)
205 214 self.assertEqual(config.a, os.path.expanduser('~/1/2/3'))
206 215 self.assertEqual(config.b, os.path.expanduser('~'))
207 216 self.assertEqual(config.c, os.path.expanduser('~/'))
208 217 self.assertEqual(config.d, '~/')
209 218
210 219 def test_extra_args(self):
211 220 cl = self.klass(log=log)
212 221 config = cl.load_config(['--a=5', 'b', '--c=10', 'd'])
213 222 self.assertEqual(cl.extra_args, ['b', 'd'])
214 223 self.assertEqual(config.a, 5)
215 224 self.assertEqual(config.c, 10)
216 225 config = cl.load_config(['--', '--a=5', '--c=10'])
217 226 self.assertEqual(cl.extra_args, ['--a=5', '--c=10'])
218 227
219 228 def test_unicode_args(self):
220 229 cl = self.klass(log=log)
221 230 argv = [u'--a=épsîlön']
222 231 config = cl.load_config(argv)
223 232 self.assertEqual(config.a, u'épsîlön')
224 233
225 234 def test_unicode_bytes_args(self):
226 235 uarg = u'--a=é'
227 236 try:
228 237 barg = uarg.encode(sys.stdin.encoding)
229 238 except (TypeError, UnicodeEncodeError):
230 239 raise SkipTest("sys.stdin.encoding can't handle 'é'")
231 240
232 241 cl = self.klass(log=log)
233 242 config = cl.load_config([barg])
234 243 self.assertEqual(config.a, u'é')
235 244
236 245 def test_unicode_alias(self):
237 246 cl = self.klass(log=log)
238 247 argv = [u'--a=épsîlön']
239 248 config = cl.load_config(argv, aliases=dict(a='A.a'))
240 249 self.assertEqual(config.A.a, u'épsîlön')
241 250
242 251
243 252 class TestArgParseKVCL(TestKeyValueCL):
244 253 klass = KVArgParseConfigLoader
245 254
246 255 def test_expanduser2(self):
247 256 cl = self.klass(log=log)
248 257 argv = ['-a', '~/1/2/3', '--b', "'~/1/2/3'"]
249 258 config = cl.load_config(argv, aliases=dict(a='A.a', b='A.b'))
250 259 self.assertEqual(config.A.a, os.path.expanduser('~/1/2/3'))
251 260 self.assertEqual(config.A.b, '~/1/2/3')
252 261
253 262 def test_eval(self):
254 263 cl = self.klass(log=log)
255 264 argv = ['-c', 'a=5']
256 265 config = cl.load_config(argv, aliases=dict(c='A.c'))
257 266 self.assertEqual(config.A.c, u"a=5")
258 267
259 268
260 269 class TestConfig(TestCase):
261 270
262 271 def test_setget(self):
263 272 c = Config()
264 273 c.a = 10
265 274 self.assertEqual(c.a, 10)
266 275 self.assertEqual('b' in c, False)
267 276
268 277 def test_auto_section(self):
269 278 c = Config()
270 279 self.assertNotIn('A', c)
271 280 assert not c._has_section('A')
272 281 A = c.A
273 282 A.foo = 'hi there'
274 283 self.assertIn('A', c)
275 284 assert c._has_section('A')
276 285 self.assertEqual(c.A.foo, 'hi there')
277 286 del c.A
278 287 self.assertEqual(c.A, Config())
279 288
280 289 def test_merge_doesnt_exist(self):
281 290 c1 = Config()
282 291 c2 = Config()
283 292 c2.bar = 10
284 293 c2.Foo.bar = 10
285 294 c1.merge(c2)
286 295 self.assertEqual(c1.Foo.bar, 10)
287 296 self.assertEqual(c1.bar, 10)
288 297 c2.Bar.bar = 10
289 298 c1.merge(c2)
290 299 self.assertEqual(c1.Bar.bar, 10)
291 300
292 301 def test_merge_exists(self):
293 302 c1 = Config()
294 303 c2 = Config()
295 304 c1.Foo.bar = 10
296 305 c1.Foo.bam = 30
297 306 c2.Foo.bar = 20
298 307 c2.Foo.wow = 40
299 308 c1.merge(c2)
300 309 self.assertEqual(c1.Foo.bam, 30)
301 310 self.assertEqual(c1.Foo.bar, 20)
302 311 self.assertEqual(c1.Foo.wow, 40)
303 312 c2.Foo.Bam.bam = 10
304 313 c1.merge(c2)
305 314 self.assertEqual(c1.Foo.Bam.bam, 10)
306 315
307 316 def test_deepcopy(self):
308 317 c1 = Config()
309 318 c1.Foo.bar = 10
310 319 c1.Foo.bam = 30
311 320 c1.a = 'asdf'
312 321 c1.b = range(10)
313 322 import copy
314 323 c2 = copy.deepcopy(c1)
315 324 self.assertEqual(c1, c2)
316 325 self.assertTrue(c1 is not c2)
317 326 self.assertTrue(c1.Foo is not c2.Foo)
318 327
319 328 def test_builtin(self):
320 329 c1 = Config()
321 330 c1.format = "json"
322 331
323 332 def test_fromdict(self):
324 333 c1 = Config({'Foo' : {'bar' : 1}})
325 334 self.assertEqual(c1.Foo.__class__, Config)
326 335 self.assertEqual(c1.Foo.bar, 1)
327 336
328 337 def test_fromdictmerge(self):
329 338 c1 = Config()
330 339 c2 = Config({'Foo' : {'bar' : 1}})
331 340 c1.merge(c2)
332 341 self.assertEqual(c1.Foo.__class__, Config)
333 342 self.assertEqual(c1.Foo.bar, 1)
334 343
335 344 def test_fromdictmerge2(self):
336 345 c1 = Config({'Foo' : {'baz' : 2}})
337 346 c2 = Config({'Foo' : {'bar' : 1}})
338 347 c1.merge(c2)
339 348 self.assertEqual(c1.Foo.__class__, Config)
340 349 self.assertEqual(c1.Foo.bar, 1)
341 350 self.assertEqual(c1.Foo.baz, 2)
342 351 self.assertNotIn('baz', c2.Foo)
343 352
344 353 def test_contains(self):
345 354 c1 = Config({'Foo' : {'baz' : 2}})
346 355 c2 = Config({'Foo' : {'bar' : 1}})
347 356 self.assertIn('Foo', c1)
348 357 self.assertIn('Foo.baz', c1)
349 358 self.assertIn('Foo.bar', c2)
350 359 self.assertNotIn('Foo.bar', c1)
351 360
352 361 def test_pickle_config(self):
353 362 cfg = Config()
354 363 cfg.Foo.bar = 1
355 364 pcfg = pickle.dumps(cfg)
356 365 cfg2 = pickle.loads(pcfg)
357 366 self.assertEqual(cfg2, cfg)
358 367
359 368 def test_getattr_section(self):
360 369 cfg = Config()
361 370 self.assertNotIn('Foo', cfg)
362 371 Foo = cfg.Foo
363 372 assert isinstance(Foo, Config)
364 373 self.assertIn('Foo', cfg)
365 374
366 375 def test_getitem_section(self):
367 376 cfg = Config()
368 377 self.assertNotIn('Foo', cfg)
369 378 Foo = cfg['Foo']
370 379 assert isinstance(Foo, Config)
371 380 self.assertIn('Foo', cfg)
372 381
373 382 def test_getattr_not_section(self):
374 383 cfg = Config()
375 384 self.assertNotIn('foo', cfg)
376 385 foo = cfg.foo
377 386 assert isinstance(foo, LazyConfigValue)
378 387 self.assertIn('foo', cfg)
379 388
380 389 def test_getattr_private_missing(self):
381 390 cfg = Config()
382 391 self.assertNotIn('_repr_html_', cfg)
383 392 with self.assertRaises(AttributeError):
384 393 _ = cfg._repr_html_
385 394 self.assertNotIn('_repr_html_', cfg)
386 395 self.assertEqual(len(cfg), 0)
387 396
388 397 def test_getitem_not_section(self):
389 398 cfg = Config()
390 399 self.assertNotIn('foo', cfg)
391 400 foo = cfg['foo']
392 401 assert isinstance(foo, LazyConfigValue)
393 402 self.assertIn('foo', cfg)
394 403
395 404 def test_merge_copies(self):
396 405 c = Config()
397 406 c2 = Config()
398 407 c2.Foo.trait = []
399 408 c.merge(c2)
400 409 c2.Foo.trait.append(1)
401 410 self.assertIsNot(c.Foo, c2.Foo)
402 411 self.assertEqual(c.Foo.trait, [])
403 412 self.assertEqual(c2.Foo.trait, [1])
404 413
General Comments 0
You need to be logged in to leave comments. Login now