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