1"""A base class for objects that are configurable.""" 2 3# Copyright (c) IPython Development Team. 4# Distributed under the terms of the Modified BSD License. 5 6 7from copy import deepcopy 8import logging 9import warnings 10 11from .loader import Config, LazyConfigValue, DeferredConfig, _is_section_key 12from traitlets.traitlets import ( 13 Any, 14 HasTraits, 15 Instance, 16 Container, 17 Dict, 18 observe, 19 observe_compat, 20 default, 21 validate, 22) 23from traitlets.utils.text import indent, wrap_paragraphs 24from textwrap import dedent 25 26 27 28 29#----------------------------------------------------------------------------- 30# Helper classes for Configurables 31#----------------------------------------------------------------------------- 32 33 34class ConfigurableError(Exception): 35 pass 36 37 38class MultipleInstanceError(ConfigurableError): 39 pass 40 41#----------------------------------------------------------------------------- 42# Configurable implementation 43#----------------------------------------------------------------------------- 44 45class Configurable(HasTraits): 46 47 config = Instance(Config, (), {}) 48 parent = Instance('traitlets.config.configurable.Configurable', allow_none=True) 49 50 def __init__(self, **kwargs): 51 """Create a configurable given a config config. 52 53 Parameters 54 ---------- 55 config : Config 56 If this is empty, default values are used. If config is a 57 :class:`Config` instance, it will be used to configure the 58 instance. 59 parent : Configurable instance, optional 60 The parent Configurable instance of this object. 61 62 Notes 63 ----- 64 Subclasses of Configurable must call the :meth:`__init__` method of 65 :class:`Configurable` *before* doing anything else and using 66 :func:`super`:: 67 68 class MyConfigurable(Configurable): 69 def __init__(self, config=None): 70 super(MyConfigurable, self).__init__(config=config) 71 # Then any other code you need to finish initialization. 72 73 This ensures that instances will be configured properly. 74 """ 75 parent = kwargs.pop('parent', None) 76 if parent is not None: 77 # config is implied from parent 78 if kwargs.get('config', None) is None: 79 kwargs['config'] = parent.config 80 self.parent = parent 81 82 config = kwargs.pop('config', None) 83 84 # load kwarg traits, other than config 85 super(Configurable, self).__init__(**kwargs) 86 87 # record traits set by config 88 config_override_names = set() 89 def notice_config_override(change): 90 """Record traits set by both config and kwargs. 91 92 They will need to be overridden again after loading config. 93 """ 94 if change.name in kwargs: 95 config_override_names.add(change.name) 96 self.observe(notice_config_override) 97 98 # load config 99 if config is not None: 100 # We used to deepcopy, but for now we are trying to just save 101 # by reference. This *could* have side effects as all components 102 # will share config. In fact, I did find such a side effect in 103 # _config_changed below. If a config attribute value was a mutable type 104 # all instances of a component were getting the same copy, effectively 105 # making that a class attribute. 106 # self.config = deepcopy(config) 107 self.config = config 108 else: 109 # allow _config_default to return something 110 self._load_config(self.config) 111 self.unobserve(notice_config_override) 112 113 for name in config_override_names: 114 setattr(self, name, kwargs[name]) 115 116 117 #------------------------------------------------------------------------- 118 # Static trait notifiations 119 #------------------------------------------------------------------------- 120 121 @classmethod 122 def section_names(cls): 123 """return section names as a list""" 124 return [c.__name__ for c in reversed(cls.__mro__) if 125 issubclass(c, Configurable) and issubclass(cls, c) 126 ] 127 128 def _find_my_config(self, cfg): 129 """extract my config from a global Config object 130 131 will construct a Config object of only the config values that apply to me 132 based on my mro(), as well as those of my parent(s) if they exist. 133 134 If I am Bar and my parent is Foo, and their parent is Tim, 135 this will return merge following config sections, in this order:: 136 137 [Bar, Foo.Bar, Tim.Foo.Bar] 138 139 With the last item being the highest priority. 140 """ 141 cfgs = [cfg] 142 if self.parent: 143 cfgs.append(self.parent._find_my_config(cfg)) 144 my_config = Config() 145 for c in cfgs: 146 for sname in self.section_names(): 147 # Don't do a blind getattr as that would cause the config to 148 # dynamically create the section with name Class.__name__. 149 if c._has_section(sname): 150 my_config.merge(c[sname]) 151 return my_config 152 153 def _load_config(self, cfg, section_names=None, traits=None): 154 """load traits from a Config object""" 155 156 if traits is None: 157 traits = self.traits(config=True) 158 if section_names is None: 159 section_names = self.section_names() 160 161 my_config = self._find_my_config(cfg) 162 163 # hold trait notifications until after all config has been loaded 164 with self.hold_trait_notifications(): 165 for name, config_value in my_config.items(): 166 if name in traits: 167 if isinstance(config_value, LazyConfigValue): 168 # ConfigValue is a wrapper for using append / update on containers 169 # without having to copy the initial value 170 initial = getattr(self, name) 171 config_value = config_value.get_value(initial) 172 elif isinstance(config_value, DeferredConfig): 173 # DeferredConfig tends to come from CLI/environment variables 174 config_value = config_value.get_value(traits[name]) 175 # We have to do a deepcopy here if we don't deepcopy the entire 176 # config object. If we don't, a mutable config_value will be 177 # shared by all instances, effectively making it a class attribute. 178 setattr(self, name, deepcopy(config_value)) 179 elif not _is_section_key(name) and not isinstance(config_value, Config): 180 from difflib import get_close_matches 181 if isinstance(self, LoggingConfigurable): 182 warn = self.log.warning 183 else: 184 warn = lambda msg: warnings.warn(msg, stacklevel=9) 185 matches = get_close_matches(name, traits) 186 msg = "Config option `{option}` not recognized by `{klass}`.".format( 187 option=name, klass=self.__class__.__name__) 188 189 if len(matches) == 1: 190 msg += " Did you mean `{matches}`?".format(matches=matches[0]) 191 elif len(matches) >= 1: 192 msg +=" Did you mean one of: `{matches}`?".format(matches=', '.join(sorted(matches))) 193 warn(msg) 194 195 @observe('config') 196 @observe_compat 197 def _config_changed(self, change): 198 """Update all the class traits having ``config=True`` in metadata. 199 200 For any class trait with a ``config`` metadata attribute that is 201 ``True``, we update the trait with the value of the corresponding 202 config entry. 203 """ 204 # Get all traits with a config metadata entry that is True 205 traits = self.traits(config=True) 206 207 # We auto-load config section for this class as well as any parent 208 # classes that are Configurable subclasses. This starts with Configurable 209 # and works down the mro loading the config for each section. 210 section_names = self.section_names() 211 self._load_config(change.new, traits=traits, section_names=section_names) 212 213 def update_config(self, config): 214 """Update config and load the new values""" 215 # traitlets prior to 4.2 created a copy of self.config in order to trigger change events. 216 # Some projects (IPython < 5) relied upon one side effect of this, 217 # that self.config prior to update_config was not modified in-place. 218 # For backward-compatibility, we must ensure that self.config 219 # is a new object and not modified in-place, 220 # but config consumers should not rely on this behavior. 221 self.config = deepcopy(self.config) 222 # load config 223 self._load_config(config) 224 # merge it into self.config 225 self.config.merge(config) 226 # TODO: trigger change event if/when dict-update change events take place 227 # DO NOT trigger full trait-change 228 229 @classmethod 230 def class_get_help(cls, inst=None): 231 """Get the help string for this class in ReST format. 232 233 If `inst` is given, it's current trait values will be used in place of 234 class defaults. 235 """ 236 assert inst is None or isinstance(inst, cls) 237 final_help = [] 238 base_classes = ', '.join(p.__name__ for p in cls.__bases__) 239 final_help.append('%s(%s) options' % (cls.__name__, base_classes)) 240 final_help.append(len(final_help[0])*'-') 241 for k, v in sorted(cls.class_traits(config=True).items()): 242 help = cls.class_get_trait_help(v, inst) 243 final_help.append(help) 244 return '\n'.join(final_help) 245 246 @classmethod 247 def class_get_trait_help(cls, trait, inst=None, helptext=None): 248 """Get the helptext string for a single trait. 249 250 :param inst: 251 If given, it's current trait values will be used in place of 252 the class default. 253 :param helptext: 254 If not given, uses the `help` attribute of the current trait. 255 """ 256 assert inst is None or isinstance(inst, cls) 257 lines = [] 258 header = "--%s.%s" % (cls.__name__, trait.name) 259 if isinstance(trait, (Container, Dict)): 260 multiplicity = trait.metadata.get('multiplicity', 'append') 261 if isinstance(trait, Dict): 262 sample_value = '<key-1>=<value-1>' 263 else: 264 sample_value = '<%s-item-1>' % trait.__class__.__name__.lower() 265 if multiplicity == 'append': 266 header = "%s=%s..." % (header, sample_value) 267 else: 268 header = "%s %s..." % (header, sample_value) 269 else: 270 header = '%s=<%s>' % (header, trait.__class__.__name__) 271 #header = "--%s.%s=<%s>" % (cls.__name__, trait.name, trait.__class__.__name__) 272 lines.append(header) 273 274 if helptext is None: 275 helptext = trait.help 276 if helptext != '': 277 helptext = '\n'.join(wrap_paragraphs(helptext, 76)) 278 lines.append(indent(helptext)) 279 280 if 'Enum' in trait.__class__.__name__: 281 # include Enum choices 282 lines.append(indent('Choices: %s' % trait.info())) 283 284 if inst is not None: 285 lines.append(indent("Current: %r" % (getattr(inst, trait.name),))) 286 else: 287 try: 288 dvr = trait.default_value_repr() 289 except Exception: 290 dvr = None # ignore defaults we can't construct 291 if dvr is not None: 292 if len(dvr) > 64: 293 dvr = dvr[:61] + "..." 294 lines.append(indent("Default: %s" % dvr)) 295 296 return '\n'.join(lines) 297 298 @classmethod 299 def class_print_help(cls, inst=None): 300 """Get the help string for a single trait and print it.""" 301 print(cls.class_get_help(inst)) 302 303 @classmethod 304 def _defining_class(cls, trait, classes): 305 """Get the class that defines a trait 306 307 For reducing redundant help output in config files. 308 Returns the current class if: 309 - the trait is defined on this class, or 310 - the class where it is defined would not be in the config file 311 312 Parameters 313 ---------- 314 trait : Trait 315 The trait to look for 316 classes : list 317 The list of other classes to consider for redundancy. 318 Will return `cls` even if it is not defined on `cls` 319 if the defining class is not in `classes`. 320 """ 321 defining_cls = cls 322 for parent in cls.mro(): 323 if issubclass(parent, Configurable) and \ 324 parent in classes and \ 325 parent.class_own_traits(config=True).get(trait.name, None) is trait: 326 defining_cls = parent 327 return defining_cls 328 329 @classmethod 330 def class_config_section(cls, classes=None): 331 """Get the config section for this class. 332 333 Parameters 334 ---------- 335 classes : list, optional 336 The list of other classes in the config file. 337 Used to reduce redundant information. 338 """ 339 def c(s): 340 """return a commented, wrapped block.""" 341 s = '\n\n'.join(wrap_paragraphs(s, 78)) 342 343 return '## ' + s.replace('\n', '\n# ') 344 345 # section header 346 breaker = '#' + '-' * 78 347 parent_classes = ', '.join( 348 p.__name__ for p in cls.__bases__ 349 if issubclass(p, Configurable) 350 ) 351 352 s = "# %s(%s) configuration" % (cls.__name__, parent_classes) 353 lines = [breaker, s, breaker] 354 # get the description trait 355 desc = cls.class_traits().get('description') 356 if desc: 357 desc = desc.default_value 358 if not desc: 359 # no description from trait, use __doc__ 360 desc = getattr(cls, '__doc__', '') 361 if desc: 362 lines.append(c(desc)) 363 lines.append('') 364 365 for name, trait in sorted(cls.class_traits(config=True).items()): 366 default_repr = trait.default_value_repr() 367 368 if classes: 369 defining_class = cls._defining_class(trait, classes) 370 else: 371 defining_class = cls 372 if defining_class is cls: 373 # cls owns the trait, show full help 374 if trait.help: 375 lines.append(c(trait.help)) 376 if 'Enum' in type(trait).__name__: 377 # include Enum choices 378 lines.append('# Choices: %s' % trait.info()) 379 lines.append('# Default: %s' % default_repr) 380 else: 381 # Trait appears multiple times and isn't defined here. 382 # Truncate help to first line + "See also Original.trait" 383 if trait.help: 384 lines.append(c(trait.help.split('\n', 1)[0])) 385 lines.append('# See also: %s.%s' % (defining_class.__name__, name)) 386 387 lines.append('# c.%s.%s = %s' % (cls.__name__, name, default_repr)) 388 lines.append('') 389 return '\n'.join(lines) 390 391 @classmethod 392 def class_config_rst_doc(cls): 393 """Generate rST documentation for this class' config options. 394 395 Excludes traits defined on parent classes. 396 """ 397 lines = [] 398 classname = cls.__name__ 399 for k, trait in sorted(cls.class_traits(config=True).items()): 400 ttype = trait.__class__.__name__ 401 402 termline = classname + '.' + trait.name 403 404 # Choices or type 405 if 'Enum' in ttype: 406 # include Enum choices 407 termline += ' : ' + trait.info_rst() 408 else: 409 termline += ' : ' + ttype 410 lines.append(termline) 411 412 # Default value 413 try: 414 dvr = trait.default_value_repr() 415 except Exception: 416 dvr = None # ignore defaults we can't construct 417 if dvr is not None: 418 if len(dvr) > 64: 419 dvr = dvr[:61]+'...' 420 # Double up backslashes, so they get to the rendered docs 421 dvr = dvr.replace("\\n", "\\\\n") 422 lines.append(indent("Default: ``%s``" % dvr)) 423 lines.append("") 424 425 help = trait.help or 'No description' 426 lines.append(indent(dedent(help))) 427 428 # Blank line 429 lines.append('') 430 431 return '\n'.join(lines) 432 433 434 435class LoggingConfigurable(Configurable): 436 """A parent class for Configurables that log. 437 438 Subclasses have a log trait, and the default behavior 439 is to get the logger from the currently running Application. 440 """ 441 442 log = Any(help="Logger or LoggerAdapter instance") 443 444 @validate("log") 445 def _validate_log(self, proposal): 446 if not isinstance(proposal.value, (logging.Logger, logging.LoggerAdapter)): 447 # warn about unsupported type, but be lenient to allow for duck typing 448 warnings.warn( 449 f"{self.__class__.__name__}.log should be a Logger or LoggerAdapter," 450 f" got {proposal.value}." 451 ) 452 return proposal.value 453 454 @default("log") 455 def _log_default(self): 456 if isinstance(self.parent, LoggingConfigurable): 457 return self.parent.log 458 from traitlets import log 459 return log.get_logger() 460 461 def _get_log_handler(self): 462 """Return the default Handler 463 464 Returns None if none can be found 465 """ 466 logger = self.log 467 if isinstance(logger, logging.LoggerAdapter): 468 logger = logger.logger 469 if not getattr(logger, "handlers", None): 470 # no handlers attribute or empty handlers list 471 return None 472 return logger.handlers[0] 473 474 475class SingletonConfigurable(LoggingConfigurable): 476 """A configurable that only allows one instance. 477 478 This class is for classes that should only have one instance of itself 479 or *any* subclass. To create and retrieve such a class use the 480 :meth:`SingletonConfigurable.instance` method. 481 """ 482 483 _instance = None 484 485 @classmethod 486 def _walk_mro(cls): 487 """Walk the cls.mro() for parent classes that are also singletons 488 489 For use in instance() 490 """ 491 492 for subclass in cls.mro(): 493 if issubclass(cls, subclass) and \ 494 issubclass(subclass, SingletonConfigurable) and \ 495 subclass != SingletonConfigurable: 496 yield subclass 497 498 @classmethod 499 def clear_instance(cls): 500 """unset _instance for this class and singleton parents. 501 """ 502 if not cls.initialized(): 503 return 504 for subclass in cls._walk_mro(): 505 if isinstance(subclass._instance, cls): 506 # only clear instances that are instances 507 # of the calling class 508 subclass._instance = None 509 510 @classmethod 511 def instance(cls, *args, **kwargs): 512 """Returns a global instance of this class. 513 514 This method create a new instance if none have previously been created 515 and returns a previously created instance is one already exists. 516 517 The arguments and keyword arguments passed to this method are passed 518 on to the :meth:`__init__` method of the class upon instantiation. 519 520 Examples 521 -------- 522 Create a singleton class using instance, and retrieve it:: 523 524 >>> from traitlets.config.configurable import SingletonConfigurable 525 >>> class Foo(SingletonConfigurable): pass 526 >>> foo = Foo.instance() 527 >>> foo == Foo.instance() 528 True 529 530 Create a subclass that is retrived using the base class instance:: 531 532 >>> class Bar(SingletonConfigurable): pass 533 >>> class Bam(Bar): pass 534 >>> bam = Bam.instance() 535 >>> bam == Bar.instance() 536 True 537 """ 538 # Create and save the instance 539 if cls._instance is None: 540 inst = cls(*args, **kwargs) 541 # Now make sure that the instance will also be returned by 542 # parent classes' _instance attribute. 543 for subclass in cls._walk_mro(): 544 subclass._instance = inst 545 546 if isinstance(cls._instance, cls): 547 return cls._instance 548 else: 549 raise MultipleInstanceError( 550 "An incompatible sibling of '%s' is already instanciated" 551 " as singleton: %s" % (cls.__name__, type(cls._instance).__name__) 552 ) 553 554 @classmethod 555 def initialized(cls): 556 """Has an instance been created?""" 557 return hasattr(cls, "_instance") and cls._instance is not None 558 559 560 561