# Source code for fontTools.misc.loggingTools

    import sys
    import logging
    import timeit
    from functools import wraps
    from collections.abc import Mapping, Callable
    import warnings
    from logging import PercentStyle

    # default logging level used by Timer class
    TIME_LEVEL = logging.DEBUG

    # per-level format strings used by the default formatter
    # (the level name is not printed for INFO and DEBUG messages)
    DEFAULT_FORMATS = {
        "*": "%(levelname)s: %(message)s",
        "INFO": "%(message)s",
        "DEBUG": "%(message)s",
    }

    class LevelFormatter(logging.Formatter):
        """Log formatter with level-specific formatting.

        Formatter class which optionally takes a dict of logging levels to
        format strings, allowing to customise the log records appearance for
        specific levels.

        Attributes:
                fmt: A dictionary mapping logging levels to format strings.
                        The ``*`` key identifies the default format string.
                datefmt: As per py:class:`logging.Formatter`
                style: As per py:class:`logging.Formatter`

        >>> import sys
        >>> handler = logging.StreamHandler(sys.stdout)
        >>> formatter = LevelFormatter(
        ...     fmt={
        ...         '*':     '[%(levelname)s] %(message)s',
        ...         'DEBUG': '%(name)s [%(levelname)s] %(message)s',
        ...         'INFO':  '%(message)s',
        ...     })
        >>> handler.setFormatter(formatter)
        >>> log = logging.getLogger('test')
        >>> log.setLevel(logging.DEBUG)
        >>> log.addHandler(handler)
        >>> log.debug('this uses a custom format string')
        test [DEBUG] this uses a custom format string
        >>> log.info('this also uses a custom format string')
        this also uses a custom format string
        >>> log.warning("this one uses the default format string")
        [WARNING] this one uses the default format string
        """

        def __init__(self, fmt=None, datefmt=None, style="%"):
            if style != "%":
                raise ValueError(
                    "only '%' percent style is supported in both python 2 and 3"
                )
            if fmt is None:
                fmt = DEFAULT_FORMATS
            if isinstance(fmt, str):
                default_format = fmt
                custom_formats = {}
            elif isinstance(fmt, Mapping):
                custom_formats = dict(fmt)
                default_format = custom_formats.pop("*", None)
            else:
                raise TypeError("fmt must be a str or a dict of str: %r" % fmt)
            super(LevelFormatter, self).__init__(default_format, datefmt)
            self.default_format = self._fmt
            self.custom_formats = {}
            for level, fmt in custom_formats.items():
                level = logging._checkLevel(level)
                self.custom_formats[level] = fmt

        def format(self, record):
            if self.custom_formats:
                fmt = self.custom_formats.get(record.levelno, self.default_format)
                if self._fmt != fmt:
                    self._fmt = fmt
                    # for python >= 3.2, _style needs to be set if _fmt changes
                    if PercentStyle:
                        self._style = PercentStyle(fmt)
            return super(LevelFormatter, self).format(record)

    def configLogger(**kwargs):
        """A more sophisticated logging system configuation manager.

        This is more or less the same as :py:func:`logging.basicConfig`,
        with some additional options and defaults.

        The default behaviour is to create a ``StreamHandler`` which writes to
        sys.stderr, set a formatter using the ``DEFAULT_FORMATS`` strings, and add
        the handler to the top-level library logger ("fontTools").

        A number of optional keyword arguments may be specified, which can alter
        the default behaviour.

        Args:

                logger: Specifies the logger name or a Logger instance to be
                        configured. (Defaults to "fontTools" logger). Unlike ``basicConfig``,
                        this function can be called multiple times to reconfigure a logger.
                        If the logger or any of its children already exists before the call is
                        made, they will be reset before the new configuration is applied.
                filename: Specifies that a ``FileHandler`` be created, using the
                        specified filename, rather than a ``StreamHandler``.
                filemode: Specifies the mode to open the file, if filename is
                        specified. (If filemode is unspecified, it defaults to ``a``).
                format: Use the specified format string for the handler. This
                        argument also accepts a dictionary of format strings keyed by
                        level name, to allow customising the records appearance for
                        specific levels. The special ``'*'`` key is for 'any other' level.
                datefmt: Use the specified date/time format.
                level: Set the logger level to the specified level.
                stream: Use the specified stream to initialize the StreamHandler. Note
                        that this argument is incompatible with ``filename`` - if both
                        are present, ``stream`` is ignored.
                handlers: If specified, this should be an iterable of already created
                        handlers, which will be added to the logger. Any handler in the
                        list which does not have a formatter assigned will be assigned the
                        formatter created in this function.
                filters: If specified, this should be an iterable of already created
                        filters. If the ``handlers`` do not already have filters assigned,
                        these filters will be added to them.
                propagate: All loggers have a ``propagate`` attribute which determines
                        whether to continue searching for handlers up the logging hierarchy.
                        If not provided, the "propagate" attribute will be set to ``False``.
        """
        # using kwargs to enforce keyword-only arguments in py2.
        handlers = kwargs.pop("handlers", None)
        if handlers is None:
            if "stream" in kwargs and "filename" in kwargs:
                raise ValueError(
                    "'stream' and 'filename' should not be " "specified together"
                )
        else:
            if "stream" in kwargs or "filename" in kwargs:
                raise ValueError(
                    "'stream' or 'filename' should not be "
                    "specified together with 'handlers'"
                )
        if handlers is None:
            filename = kwargs.pop("filename", None)
            mode = kwargs.pop("filemode", "a")
            if filename:
                h = logging.FileHandler(filename, mode)
            else:
                stream = kwargs.pop("stream", None)
                h = logging.StreamHandler(stream)
            handlers = [h]
        # By default, the top-level library logger is configured.
        logger = kwargs.pop("logger", "fontTools")
        if not logger or isinstance(logger, str):
            # empty "" or None means the 'root' logger
            logger = logging.getLogger(logger)
        # before (re)configuring, reset named logger and its children (if exist)
        _resetExistingLoggers(parent=logger.name)
        # use DEFAULT_FORMATS if 'format' is None
        fs = kwargs.pop("format", None)
        dfs = kwargs.pop("datefmt", None)
        # XXX: '%' is the only format style supported on both py2 and 3
        style = kwargs.pop("style", "%")
        fmt = LevelFormatter(fs, dfs, style)
        filters = kwargs.pop("filters", [])
        for h in handlers:
            if h.formatter is None:
                h.setFormatter(fmt)
            if not h.filters:
                for f in filters:
                    h.addFilter(f)
            logger.addHandler(h)
        if logger.name != "root":
            # stop searching up the hierarchy for handlers
            logger.propagate = kwargs.pop("propagate", False)
        # set a custom severity level
        level = kwargs.pop("level", None)
        if level is not None:
            logger.setLevel(level)
        if kwargs:
            keys = ", ".join(kwargs.keys())
            raise ValueError("Unrecognised argument(s): %s" % keys)

    def _resetExistingLoggers(parent="root"):
        """Reset the logger named 'parent' and all its children to their initial
        state, if they already exist in the current configuration.
        """
        root = logging.root
        # get sorted list of all existing loggers
        existing = sorted(root.manager.loggerDict.keys())
        if parent == "root":
            # all the existing loggers are children of 'root'
            loggers_to_reset = [parent] + existing
        elif parent not in existing:
            # nothing to do
            return
        elif parent in existing:
            loggers_to_reset = [parent]
            # collect children, starting with the entry after parent name
            i = existing.index(parent) + 1
            prefixed = parent + "."
            pflen = len(prefixed)
            num_existing = len(existing)
            while i < num_existing:
                if existing[i][:pflen] == prefixed:
                    loggers_to_reset.append(existing[i])
                i += 1
        for name in loggers_to_reset:
            if name == "root":
                root.setLevel(logging.WARNING)
                for h in root.handlers[:]:
                    root.removeHandler(h)
                for f in root.filters[:]:
                    root.removeFilters(f)
                root.disabled = False
            else:
                logger = root.manager.loggerDict[name]
                logger.level = logging.NOTSET
                logger.handlers = []
                logger.filters = []
                logger.propagate = True
                logger.disabled = False

    class Timer(object):
        """Keeps track of overall time and split/lap times.

        >>> import time
        >>> timer = Timer()
        >>> time.sleep(0.01)
        >>> print("First lap:", timer.split())
        First lap: ...
        >>> time.sleep(0.02)
        >>> print("Second lap:", timer.split())
        Second lap: ...
        >>> print("Overall time:", timer.time())
        Overall time: ...

        Can be used as a context manager inside with-statements.

        >>> with Timer() as t:
        ...     time.sleep(0.01)
        >>> print("%0.3f seconds" % t.elapsed)
        0... seconds

        If initialised with a logger, it can log the elapsed time automatically
        upon exiting the with-statement.

        >>> import logging
        >>> log = logging.getLogger("my-fancy-timer-logger")
        >>> configLogger(logger=log, level="DEBUG", format="%(message)s", stream=sys.stdout)
        >>> with Timer(log, 'do something'):
        ...     time.sleep(0.01)
        Took ... to do something

        The same Timer instance, holding a reference to a logger, can be reused
        in multiple with-statements, optionally with different messages or levels.

        >>> timer = Timer(log)
        >>> with timer():
        ...     time.sleep(0.01)
        elapsed time: ...s
        >>> with timer('redo it', level=logging.INFO):
        ...     time.sleep(0.02)
        Took ... to redo it

        It can also be used as a function decorator to log the time elapsed to run
        the decorated function.

        >>> @timer()
        ... def test1():
        ...    time.sleep(0.01)
        >>> @timer('run test 2', level=logging.INFO)
        ... def test2():
        ...    time.sleep(0.02)
        >>> test1()
        Took ... to run 'test1'
        >>> test2()
        Took ... to run test 2
        """

        # timeit.default_timer choses the most accurate clock for each platform
        _time: Callable[[], float] = staticmethod(timeit.default_timer)
        default_msg = "elapsed time: %(time).3fs"
        default_format = "Took %(time).3fs to %(msg)s"

        def __init__(self, logger=None, msg=None, level=None, start=None):
            self.reset(start)
            if logger is None:
                for arg in ("msg", "level"):
                    if locals().get(arg) is not None:
                        raise ValueError("'%s' can't be specified without a 'logger'" % arg)
            self.logger = logger
            self.level = level if level is not None else TIME_LEVEL
            self.msg = msg

        def reset(self, start=None):
            """Reset timer to 'start_time' or the current time."""
            if start is None:
                self.start = self._time()
            else:
                self.start = start
            self.last = self.start
            self.elapsed = 0.0

        def time(self):
            """Return the overall time (in seconds) since the timer started."""
            return self._time() - self.start

        def split(self):
            """Split and return the lap time (in seconds) in between splits."""
            current = self._time()
            self.elapsed = current - self.last
            self.last = current
            return self.elapsed

        def formatTime(self, msg, time):
            """Format 'time' value in 'msg' and return formatted string.
            If 'msg' contains a '%(time)' format string, try to use that.
            Otherwise, use the predefined 'default_format'.
            If 'msg' is empty or None, fall back to 'default_msg'.
            """
            if not msg:
                msg = self.default_msg
            if msg.find("%(time)") < 0:
                msg = self.default_format % {"msg": msg, "time": time}
            else:
                try:
                    msg = msg % {"time": time}
                except (KeyError, ValueError):
                    pass  # skip if the format string is malformed
            return msg

        def __enter__(self):
            """Start a new lap"""
            self.last = self._time()
            self.elapsed = 0.0
            return self

        def __exit__(self, exc_type, exc_value, traceback):
            """End the current lap. If timer has a logger, log the time elapsed,
            using the format string in self.msg (or the default one).
            """
            time = self.split()
            if self.logger is None or exc_type:
                # if there's no logger attached, or if any exception occurred in
                # the with-statement, exit without logging the time
                return
            message = self.formatTime(self.msg, time)
            # Allow log handlers to see the individual parts to facilitate things
            # like a server accumulating aggregate stats.
            msg_parts = {"msg": self.msg, "time": time}
            self.logger.log(self.level, message, msg_parts)

        def __call__(self, func_or_msg=None, **kwargs):
            """If the first argument is a function, return a decorator which runs
            the wrapped function inside Timer's context manager.
            Otherwise, treat the first argument as a 'msg' string and return an updated
            Timer instance, referencing the same logger.
            A 'level' keyword can also be passed to override self.level.
            """
            if isinstance(func_or_msg, Callable):
                func = func_or_msg
                # use the function name when no explicit 'msg' is provided
                if not self.msg:
                    self.msg = "run '%s'" % func.__name__

                @wraps(func)
                def wrapper(*args, **kwds):
                    with self:
                        return func(*args, **kwds)

                return wrapper
            else:
                msg = func_or_msg or kwargs.get("msg")
                level = kwargs.get("level", self.level)
                return self.__class__(self.logger, msg, level)

        def __float__(self):
            return self.elapsed

        def __int__(self):
            return int(self.elapsed)

        def __str__(self):
            return "%.3f" % self.elapsed

    class ChannelsFilter(logging.Filter):
        """Provides a hierarchical filter for log entries based on channel names.

        Filters out records emitted from a list of enabled channel names,
        including their children. It works the same as the ``logging.Filter``
        class, but allows the user to specify multiple channel names.

        >>> import sys
        >>> handler = logging.StreamHandler(sys.stdout)
        >>> handler.setFormatter(logging.Formatter("%(message)s"))
        >>> filter = ChannelsFilter("A.B", "C.D")
        >>> handler.addFilter(filter)
        >>> root = logging.getLogger()
        >>> root.addHandler(handler)
        >>> root.setLevel(level=logging.DEBUG)
        >>> logging.getLogger('A.B').debug('this record passes through')
        this record passes through
        >>> logging.getLogger('A.B.C').debug('records from children also pass')
        records from children also pass
        >>> logging.getLogger('C.D').debug('this one as well')
        this one as well
        >>> logging.getLogger('A.B.').debug('also this one')
        also this one
        >>> logging.getLogger('A.F').debug('but this one does not!')
        >>> logging.getLogger('C.DE').debug('neither this one!')
        """

        def __init__(self, *names):
            self.names = names
            self.num = len(names)
            self.lengths = {n: len(n) for n in names}

        def filter(self, record):
            if self.num == 0:
                return True
            for name in self.names:
                nlen = self.lengths[name]
                if name == record.name:
                    return True
                elif record.name.find(name, 0, nlen) == 0 and record.name[nlen] == ".":
                    return True
            return False

    class CapturingLogHandler(logging.Handler):
        def __init__(self, logger, level):
            super(CapturingLogHandler, self).__init__(level=level)
            self.records = []
            if isinstance(logger, str):
                self.logger = logging.getLogger(logger)
            else:
                self.logger = logger

        def __enter__(self):
            self.original_disabled = self.logger.disabled
            self.original_level = self.logger.level
            self.original_propagate = self.logger.propagate

            self.logger.addHandler(self)
            self.logger.setLevel(self.level)
            self.logger.disabled = False
            self.logger.propagate = False

            return self

        def __exit__(self, type, value, traceback):
            self.logger.removeHandler(self)
            self.logger.setLevel(self.original_level)
            self.logger.disabled = self.original_disabled
            self.logger.propagate = self.original_propagate

            return self

        def emit(self, record):
            self.records.append(record)

        def assertRegex(self, regexp, msg=None):
            import re

            pattern = re.compile(regexp)
            for r in self.records:
                if pattern.search(r.getMessage()):
                    return True
            if msg is None:
                msg = "Pattern '%s' not found in logger records" % regexp
            assert 0, msg

    class LogMixin(object):
        """Mixin class that adds logging functionality to another class.

        You can define a new class that subclasses from ``LogMixin`` as well as
        other base classes through multiple inheritance.
        All instances of that class will have a ``log`` property that returns
        a ``logging.Logger`` named after their respective ``<module>.<class>``.

        For example:

        >>> class BaseClass(object):
        ...     pass
        >>> class MyClass(LogMixin, BaseClass):
        ...     pass
        >>> a = MyClass()
        >>> isinstance(a.log, logging.Logger)
        True
        >>> print(a.log.name)
        fontTools.misc.loggingTools.MyClass
        >>> class AnotherClass(MyClass):
        ...     pass
        >>> b = AnotherClass()
        >>> isinstance(b.log, logging.Logger)
        True
        >>> print(b.log.name)
        fontTools.misc.loggingTools.AnotherClass
        """

        @property
        def log(self):
            if not hasattr(self, "_log"):
                name = ".".join((self.__class__.__module__, self.__class__.__name__))
                self._log = logging.getLogger(name)
            return self._log

    def deprecateArgument(name, msg, category=UserWarning):
        """Raise a warning about deprecated function argument 'name'."""
        warnings.warn("%r is deprecated; %s" % (name, msg), category=category, stacklevel=3)

    def deprecateFunction(msg, category=UserWarning):
        """Decorator to raise a warning when a deprecated function is called."""

        def decorator(func):
            @wraps(func)
            def wrapper(*args, **kwargs):
                warnings.warn(
                    "%r is deprecated; %s" % (func.__name__, msg),
                    category=category,
                    stacklevel=2,
                )
                return func(*args, **kwargs)

            return wrapper

        return decorator

    if __name__ == "__main__":
        import doctest

        sys.exit(doctest.testmod(optionflags=doctest.ELLIPSIS).failed)