summaryrefslogtreecommitdiffstats
path: root/python/mozbuild/mozpack/errors.py
blob: eeb8632fbb244363fa4c5460a8cc14f5f007c408 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

import sys
from contextlib import contextmanager


class ErrorMessage(Exception):
    """Exception type raised from errors.error() and errors.fatal()"""


class AccumulatedErrors(Exception):
    """Exception type raised from errors.accumulate()"""


class ErrorCollector(object):
    """
    Error handling/logging class. A global instance, errors, is provided for
    convenience.

    Warnings, errors and fatal errors may be logged by calls to the following
    functions:
        errors.warn(message)
        errors.error(message)
        errors.fatal(message)

    Warnings only send the message on the logging output, while errors and
    fatal errors send the message and throw an ErrorMessage exception. The
    exception, however, may be deferred. See further below.

    Errors may be ignored by calling:
        errors.ignore_errors()

    After calling that function, only fatal errors throw an exception.

    The warnings, errors or fatal errors messages may be augmented with context
    information when a context is provided. Context is defined by a pair
    (filename, linenumber), and may be set with errors.context() used as a
    context manager:
        with errors.context(filename, linenumber):
            errors.warn(message)

    Arbitrary nesting is supported, both for errors.context calls:
        with errors.context(filename1, linenumber1):
            errors.warn(message)
            with errors.context(filename2, linenumber2):
                errors.warn(message)

    as well as for function calls:
        def func():
            errors.warn(message)
        with errors.context(filename, linenumber):
            func()

    Errors and fatal errors can have their exception thrown at a later time,
    allowing for several different errors to be reported at once before
    throwing. This is achieved with errors.accumulate() as a context manager:
        with errors.accumulate():
            if test1:
                errors.error(message1)
            if test2:
                errors.error(message2)

    In such cases, a single AccumulatedErrors exception is thrown, but doesn't
    contain information about the exceptions. The logged messages do.
    """

    out = sys.stderr
    WARN = 1
    ERROR = 2
    FATAL = 3
    _level = ERROR
    _context = []
    _count = None

    def ignore_errors(self, ignore=True):
        if ignore:
            self._level = self.FATAL
        else:
            self._level = self.ERROR

    def _full_message(self, level, msg):
        if level >= self._level:
            level = "error"
        else:
            level = "warning"
        if self._context:
            file, line = self._context[-1]
            return "%s: %s:%d: %s" % (level, file, line, msg)
        return "%s: %s" % (level, msg)

    def _handle(self, level, msg):
        msg = self._full_message(level, msg)
        if level >= self._level:
            if self._count is None:
                raise ErrorMessage(msg)
            self._count += 1
        print(msg, file=self.out)

    def fatal(self, msg):
        self._handle(self.FATAL, msg)

    def error(self, msg):
        self._handle(self.ERROR, msg)

    def warn(self, msg):
        self._handle(self.WARN, msg)

    def get_context(self):
        if self._context:
            return self._context[-1]

    @contextmanager
    def context(self, file, line):
        if file and line:
            self._context.append((file, line))
        yield
        if file and line:
            self._context.pop()

    @contextmanager
    def accumulate(self):
        assert self._count is None
        self._count = 0
        yield
        count = self._count
        self._count = None
        if count:
            raise AccumulatedErrors()

    @property
    def count(self):
        # _count can be None.
        return self._count if self._count else 0


errors = ErrorCollector()