configurable.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  1. # encoding: utf-8
  2. """A base class for objects that are configurable."""
  3. # Copyright (c) IPython Development Team.
  4. # Distributed under the terms of the Modified BSD License.
  5. from __future__ import print_function, absolute_import
  6. from copy import deepcopy
  7. import warnings
  8. from .loader import Config, LazyConfigValue, _is_section_key
  9. from traitlets.traitlets import HasTraits, Instance, observe, observe_compat, default
  10. from ipython_genutils.text import indent, dedent, wrap_paragraphs
  11. #-----------------------------------------------------------------------------
  12. # Helper classes for Configurables
  13. #-----------------------------------------------------------------------------
  14. class ConfigurableError(Exception):
  15. pass
  16. class MultipleInstanceError(ConfigurableError):
  17. pass
  18. #-----------------------------------------------------------------------------
  19. # Configurable implementation
  20. #-----------------------------------------------------------------------------
  21. class Configurable(HasTraits):
  22. config = Instance(Config, (), {})
  23. parent = Instance('traitlets.config.configurable.Configurable', allow_none=True)
  24. def __init__(self, **kwargs):
  25. """Create a configurable given a config config.
  26. Parameters
  27. ----------
  28. config : Config
  29. If this is empty, default values are used. If config is a
  30. :class:`Config` instance, it will be used to configure the
  31. instance.
  32. parent : Configurable instance, optional
  33. The parent Configurable instance of this object.
  34. Notes
  35. -----
  36. Subclasses of Configurable must call the :meth:`__init__` method of
  37. :class:`Configurable` *before* doing anything else and using
  38. :func:`super`::
  39. class MyConfigurable(Configurable):
  40. def __init__(self, config=None):
  41. super(MyConfigurable, self).__init__(config=config)
  42. # Then any other code you need to finish initialization.
  43. This ensures that instances will be configured properly.
  44. """
  45. parent = kwargs.pop('parent', None)
  46. if parent is not None:
  47. # config is implied from parent
  48. if kwargs.get('config', None) is None:
  49. kwargs['config'] = parent.config
  50. self.parent = parent
  51. config = kwargs.pop('config', None)
  52. # load kwarg traits, other than config
  53. super(Configurable, self).__init__(**kwargs)
  54. # load config
  55. if config is not None:
  56. # We used to deepcopy, but for now we are trying to just save
  57. # by reference. This *could* have side effects as all components
  58. # will share config. In fact, I did find such a side effect in
  59. # _config_changed below. If a config attribute value was a mutable type
  60. # all instances of a component were getting the same copy, effectively
  61. # making that a class attribute.
  62. # self.config = deepcopy(config)
  63. self.config = config
  64. else:
  65. # allow _config_default to return something
  66. self._load_config(self.config)
  67. # Ensure explicit kwargs are applied after loading config.
  68. # This is usually redundant, but ensures config doesn't override
  69. # explicitly assigned values.
  70. for key, value in kwargs.items():
  71. setattr(self, key, value)
  72. #-------------------------------------------------------------------------
  73. # Static trait notifiations
  74. #-------------------------------------------------------------------------
  75. @classmethod
  76. def section_names(cls):
  77. """return section names as a list"""
  78. return [c.__name__ for c in reversed(cls.__mro__) if
  79. issubclass(c, Configurable) and issubclass(cls, c)
  80. ]
  81. def _find_my_config(self, cfg):
  82. """extract my config from a global Config object
  83. will construct a Config object of only the config values that apply to me
  84. based on my mro(), as well as those of my parent(s) if they exist.
  85. If I am Bar and my parent is Foo, and their parent is Tim,
  86. this will return merge following config sections, in this order::
  87. [Bar, Foo.bar, Tim.Foo.Bar]
  88. With the last item being the highest priority.
  89. """
  90. cfgs = [cfg]
  91. if self.parent:
  92. cfgs.append(self.parent._find_my_config(cfg))
  93. my_config = Config()
  94. for c in cfgs:
  95. for sname in self.section_names():
  96. # Don't do a blind getattr as that would cause the config to
  97. # dynamically create the section with name Class.__name__.
  98. if c._has_section(sname):
  99. my_config.merge(c[sname])
  100. return my_config
  101. def _load_config(self, cfg, section_names=None, traits=None):
  102. """load traits from a Config object"""
  103. if traits is None:
  104. traits = self.traits(config=True)
  105. if section_names is None:
  106. section_names = self.section_names()
  107. my_config = self._find_my_config(cfg)
  108. # hold trait notifications until after all config has been loaded
  109. with self.hold_trait_notifications():
  110. for name, config_value in my_config.items():
  111. if name in traits:
  112. if isinstance(config_value, LazyConfigValue):
  113. # ConfigValue is a wrapper for using append / update on containers
  114. # without having to copy the initial value
  115. initial = getattr(self, name)
  116. config_value = config_value.get_value(initial)
  117. # We have to do a deepcopy here if we don't deepcopy the entire
  118. # config object. If we don't, a mutable config_value will be
  119. # shared by all instances, effectively making it a class attribute.
  120. setattr(self, name, deepcopy(config_value))
  121. elif not _is_section_key(name) and not isinstance(config_value, Config):
  122. from difflib import get_close_matches
  123. if isinstance(self, LoggingConfigurable):
  124. warn = self.log.warning
  125. else:
  126. warn = lambda msg: warnings.warn(msg, stacklevel=9)
  127. matches = get_close_matches(name, traits)
  128. msg = u"Config option `{option}` not recognized by `{klass}`.".format(
  129. option=name, klass=self.__class__.__name__)
  130. if len(matches) == 1:
  131. msg += u" Did you mean `{matches}`?".format(matches=matches[0])
  132. elif len(matches) >= 1:
  133. msg +=" Did you mean one of: `{matches}`?".format(matches=', '.join(sorted(matches)))
  134. warn(msg)
  135. @observe('config')
  136. @observe_compat
  137. def _config_changed(self, change):
  138. """Update all the class traits having ``config=True`` in metadata.
  139. For any class trait with a ``config`` metadata attribute that is
  140. ``True``, we update the trait with the value of the corresponding
  141. config entry.
  142. """
  143. # Get all traits with a config metadata entry that is True
  144. traits = self.traits(config=True)
  145. # We auto-load config section for this class as well as any parent
  146. # classes that are Configurable subclasses. This starts with Configurable
  147. # and works down the mro loading the config for each section.
  148. section_names = self.section_names()
  149. self._load_config(change.new, traits=traits, section_names=section_names)
  150. def update_config(self, config):
  151. """Update config and load the new values"""
  152. # traitlets prior to 4.2 created a copy of self.config in order to trigger change events.
  153. # Some projects (IPython < 5) relied upon one side effect of this,
  154. # that self.config prior to update_config was not modified in-place.
  155. # For backward-compatibility, we must ensure that self.config
  156. # is a new object and not modified in-place,
  157. # but config consumers should not rely on this behavior.
  158. self.config = deepcopy(self.config)
  159. # load config
  160. self._load_config(config)
  161. # merge it into self.config
  162. self.config.merge(config)
  163. # TODO: trigger change event if/when dict-update change events take place
  164. # DO NOT trigger full trait-change
  165. @classmethod
  166. def class_get_help(cls, inst=None):
  167. """Get the help string for this class in ReST format.
  168. If `inst` is given, it's current trait values will be used in place of
  169. class defaults.
  170. """
  171. assert inst is None or isinstance(inst, cls)
  172. final_help = []
  173. final_help.append(u'%s options' % cls.__name__)
  174. final_help.append(len(final_help[0])*u'-')
  175. for k, v in sorted(cls.class_traits(config=True).items()):
  176. help = cls.class_get_trait_help(v, inst)
  177. final_help.append(help)
  178. return '\n'.join(final_help)
  179. @classmethod
  180. def class_get_trait_help(cls, trait, inst=None):
  181. """Get the help string for a single trait.
  182. If `inst` is given, it's current trait values will be used in place of
  183. the class default.
  184. """
  185. assert inst is None or isinstance(inst, cls)
  186. lines = []
  187. header = "--%s.%s=<%s>" % (cls.__name__, trait.name, trait.__class__.__name__)
  188. lines.append(header)
  189. if inst is not None:
  190. lines.append(indent('Current: %r' % getattr(inst, trait.name), 4))
  191. else:
  192. try:
  193. dvr = trait.default_value_repr()
  194. except Exception:
  195. dvr = None # ignore defaults we can't construct
  196. if dvr is not None:
  197. if len(dvr) > 64:
  198. dvr = dvr[:61]+'...'
  199. lines.append(indent('Default: %s' % dvr, 4))
  200. if 'Enum' in trait.__class__.__name__:
  201. # include Enum choices
  202. lines.append(indent('Choices: %r' % (trait.values,)))
  203. help = trait.help
  204. if help != '':
  205. help = '\n'.join(wrap_paragraphs(help, 76))
  206. lines.append(indent(help, 4))
  207. return '\n'.join(lines)
  208. @classmethod
  209. def class_print_help(cls, inst=None):
  210. """Get the help string for a single trait and print it."""
  211. print(cls.class_get_help(inst))
  212. @classmethod
  213. def class_config_section(cls):
  214. """Get the config class config section"""
  215. def c(s):
  216. """return a commented, wrapped block."""
  217. s = '\n\n'.join(wrap_paragraphs(s, 78))
  218. return '## ' + s.replace('\n', '\n# ')
  219. # section header
  220. breaker = '#' + '-'*78
  221. parent_classes = ','.join(p.__name__ for p in cls.__bases__)
  222. s = "# %s(%s) configuration" % (cls.__name__, parent_classes)
  223. lines = [breaker, s, breaker, '']
  224. # get the description trait
  225. desc = cls.class_traits().get('description')
  226. if desc:
  227. desc = desc.default_value
  228. if not desc:
  229. # no description from trait, use __doc__
  230. desc = getattr(cls, '__doc__', '')
  231. if desc:
  232. lines.append(c(desc))
  233. lines.append('')
  234. for name, trait in sorted(cls.class_own_traits(config=True).items()):
  235. lines.append(c(trait.help))
  236. lines.append('#c.%s.%s = %s' % (cls.__name__, name, trait.default_value_repr()))
  237. lines.append('')
  238. return '\n'.join(lines)
  239. @classmethod
  240. def class_config_rst_doc(cls):
  241. """Generate rST documentation for this class' config options.
  242. Excludes traits defined on parent classes.
  243. """
  244. lines = []
  245. classname = cls.__name__
  246. for k, trait in sorted(cls.class_own_traits(config=True).items()):
  247. ttype = trait.__class__.__name__
  248. termline = classname + '.' + trait.name
  249. # Choices or type
  250. if 'Enum' in ttype:
  251. # include Enum choices
  252. termline += ' : ' + '|'.join(repr(x) for x in trait.values)
  253. else:
  254. termline += ' : ' + ttype
  255. lines.append(termline)
  256. # Default value
  257. try:
  258. dvr = trait.default_value_repr()
  259. except Exception:
  260. dvr = None # ignore defaults we can't construct
  261. if dvr is not None:
  262. if len(dvr) > 64:
  263. dvr = dvr[:61]+'...'
  264. # Double up backslashes, so they get to the rendered docs
  265. dvr = dvr.replace('\\n', '\\\\n')
  266. lines.append(' Default: ``%s``' % dvr)
  267. lines.append('')
  268. help = trait.help or 'No description'
  269. lines.append(indent(dedent(help), 4))
  270. # Blank line
  271. lines.append('')
  272. return '\n'.join(lines)
  273. class LoggingConfigurable(Configurable):
  274. """A parent class for Configurables that log.
  275. Subclasses have a log trait, and the default behavior
  276. is to get the logger from the currently running Application.
  277. """
  278. log = Instance('logging.Logger')
  279. @default('log')
  280. def _log_default(self):
  281. from traitlets import log
  282. return log.get_logger()
  283. class SingletonConfigurable(LoggingConfigurable):
  284. """A configurable that only allows one instance.
  285. This class is for classes that should only have one instance of itself
  286. or *any* subclass. To create and retrieve such a class use the
  287. :meth:`SingletonConfigurable.instance` method.
  288. """
  289. _instance = None
  290. @classmethod
  291. def _walk_mro(cls):
  292. """Walk the cls.mro() for parent classes that are also singletons
  293. For use in instance()
  294. """
  295. for subclass in cls.mro():
  296. if issubclass(cls, subclass) and \
  297. issubclass(subclass, SingletonConfigurable) and \
  298. subclass != SingletonConfigurable:
  299. yield subclass
  300. @classmethod
  301. def clear_instance(cls):
  302. """unset _instance for this class and singleton parents.
  303. """
  304. if not cls.initialized():
  305. return
  306. for subclass in cls._walk_mro():
  307. if isinstance(subclass._instance, cls):
  308. # only clear instances that are instances
  309. # of the calling class
  310. subclass._instance = None
  311. @classmethod
  312. def instance(cls, *args, **kwargs):
  313. """Returns a global instance of this class.
  314. This method create a new instance if none have previously been created
  315. and returns a previously created instance is one already exists.
  316. The arguments and keyword arguments passed to this method are passed
  317. on to the :meth:`__init__` method of the class upon instantiation.
  318. Examples
  319. --------
  320. Create a singleton class using instance, and retrieve it::
  321. >>> from traitlets.config.configurable import SingletonConfigurable
  322. >>> class Foo(SingletonConfigurable): pass
  323. >>> foo = Foo.instance()
  324. >>> foo == Foo.instance()
  325. True
  326. Create a subclass that is retrived using the base class instance::
  327. >>> class Bar(SingletonConfigurable): pass
  328. >>> class Bam(Bar): pass
  329. >>> bam = Bam.instance()
  330. >>> bam == Bar.instance()
  331. True
  332. """
  333. # Create and save the instance
  334. if cls._instance is None:
  335. inst = cls(*args, **kwargs)
  336. # Now make sure that the instance will also be returned by
  337. # parent classes' _instance attribute.
  338. for subclass in cls._walk_mro():
  339. subclass._instance = inst
  340. if isinstance(cls._instance, cls):
  341. return cls._instance
  342. else:
  343. raise MultipleInstanceError(
  344. 'Multiple incompatible subclass instances of '
  345. '%s are being created.' % cls.__name__
  346. )
  347. @classmethod
  348. def initialized(cls):
  349. """Has an instance been created?"""
  350. return hasattr(cls, "_instance") and cls._instance is not None