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