summaryrefslogtreecommitdiff
path: root/distixlib/structurederror.py
blob: 8170a0bab5382105e60b5be20b8923f7c81417a9 (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
# Copyright 2013-2014  Lars Wirzenius
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# =*= License: GPL-3+ =*=


import hashlib


class StructuredError(Exception):

    '''A structured error exception.

    Structured errors get a set of keyword arguments to the
    initialiser, and use them to fill in templates in a message string
    provided by the exception class. In addition, each structured
    exception has a unique ID, computed from the class name, which can
    be used, for example, as a user-visible error code. The ID is
    prepended to the message. The ID could also be used to look up
    translations, though that is not currently implemented. The ID
    will also make translated log files more greppable.

    The format string uses syntax according to the str.format
    specification (not the old % interpolation), in order to easy
    eventual migration to Python 3.

    To use structured error exceptions, subclass this class for each
    error condition, and add a msg attribute to the subclass. There is
    no such attribute in this base class, to prevent you from using it
    directly.

    Example:

        class NoLasagnaError(StructuredError):

            msg = 'Lunch is {lunch} instead of lasagna.'

    That's all the subclass needs to do. The caller must then provide
    the right keyword arguments: these are verified when the ``str``
    is called on the exception object. If the caller has not provided
    all the required arguments, an unformatted error message is
    provided, plus a note of what arguments are missing, and all the
    arguments that were provided. It's not an error to provide extra
    keyword arguments.

    '''

    msg = 'BUG: StructuredError SUBCLASS DOES NOT DEFINE msg ATTRIBUTE'

    def __init__(self, **kwargs):
        Exception.__init__(self)
        self.kwargs = kwargs

    @property
    def id(self):
        '''A semi-unique ID for this exception class.

        The ID is computed from the name of the class and the module
        it is in, using a checksum. There is no guarantee of
        uniqueness, but the likelihood of collisions is low.

        The ID is of the form RabcdeX, where abcde is five hexadecimal
        digits. The R prefix and the X suffix are there to make it
        easier to grep for the error code in large log files: they
        reduce the number of accidental hits compared to grepping just
        for the hexadecimal digits. R and X were chosen because they're
        pretty.

        '''

        summer = hashlib.md5()
        summer.update(self.__class__.__name__)
        summer.update(self.__class__.__module__)
        hash_value = summer.hexdigest()[:5]
        return 'R{0}X'.format(hash_value.upper())

    def _format_msg(self, template):
        try:
            formatted_msg = template.format(**self.kwargs)
        except KeyError as e:
            # If there were any errors in the formatting of the message,
            # report them here. We do NOT want replace the actual error
            # message, because that would hide information from the user.
            # We do want to know there was an error, though.
            formatted_msg = '{0} (PROGRAMMING ERROR: {1} {2})'.format(
                template, repr(e), repr(self.kwargs))

        return '{0}: {1}'.format(self.id, formatted_msg)

    def __str__(self):
        return self._format_msg(self.msg)