1# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
2
3# Copyright (C) 2003-2017 Nominum, Inc.
4#
5# Permission to use, copy, modify, and distribute this software and its
6# documentation for any purpose with or without fee is hereby granted,
7# provided that the above copyright notice and this permission notice
8# appear in all copies.
9#
10# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
11# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
13# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
16# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17
18"""Common DNS Exceptions.
19
20Dnspython modules may also define their own exceptions, which will
21always be subclasses of ``DNSException``.
22"""
23
24class DNSException(Exception):
25    """Abstract base class shared by all dnspython exceptions.
26
27    It supports two basic modes of operation:
28
29    a) Old/compatible mode is used if ``__init__`` was called with
30    empty *kwargs*.  In compatible mode all *args* are passed
31    to the standard Python Exception class as before and all *args* are
32    printed by the standard ``__str__`` implementation.  Class variable
33    ``msg`` (or doc string if ``msg`` is ``None``) is returned from ``str()``
34    if *args* is empty.
35
36    b) New/parametrized mode is used if ``__init__`` was called with
37    non-empty *kwargs*.
38    In the new mode *args* must be empty and all kwargs must match
39    those set in class variable ``supp_kwargs``. All kwargs are stored inside
40    ``self.kwargs`` and used in a new ``__str__`` implementation to construct
41    a formatted message based on the ``fmt`` class variable, a ``string``.
42
43    In the simplest case it is enough to override the ``supp_kwargs``
44    and ``fmt`` class variables to get nice parametrized messages.
45    """
46
47    msg = None  # non-parametrized message
48    supp_kwargs = set()  # accepted parameters for _fmt_kwargs (sanity check)
49    fmt = None  # message parametrized with results from _fmt_kwargs
50
51    def __init__(self, *args, **kwargs):
52        self._check_params(*args, **kwargs)
53        if kwargs:
54            self.kwargs = self._check_kwargs(**kwargs)
55            self.msg = str(self)
56        else:
57            self.kwargs = dict()  # defined but empty for old mode exceptions
58        if self.msg is None:
59            # doc string is better implicit message than empty string
60            self.msg = self.__doc__
61        if args:
62            super(DNSException, self).__init__(*args)
63        else:
64            super(DNSException, self).__init__(self.msg)
65
66    def _check_params(self, *args, **kwargs):
67        """Old exceptions supported only args and not kwargs.
68
69        For sanity we do not allow to mix old and new behavior."""
70        if args or kwargs:
71            assert bool(args) != bool(kwargs), \
72                'keyword arguments are mutually exclusive with positional args'
73
74    def _check_kwargs(self, **kwargs):
75        if kwargs:
76            assert set(kwargs.keys()) == self.supp_kwargs, \
77                'following set of keyword args is required: %s' % (
78                    self.supp_kwargs)
79        return kwargs
80
81    def _fmt_kwargs(self, **kwargs):
82        """Format kwargs before printing them.
83
84        Resulting dictionary has to have keys necessary for str.format call
85        on fmt class variable.
86        """
87        fmtargs = {}
88        for kw, data in kwargs.items():
89            if isinstance(data, (list, set)):
90                # convert list of <someobj> to list of str(<someobj>)
91                fmtargs[kw] = list(map(str, data))
92                if len(fmtargs[kw]) == 1:
93                    # remove list brackets [] from single-item lists
94                    fmtargs[kw] = fmtargs[kw].pop()
95            else:
96                fmtargs[kw] = data
97        return fmtargs
98
99    def __str__(self):
100        if self.kwargs and self.fmt:
101            # provide custom message constructed from keyword arguments
102            fmtargs = self._fmt_kwargs(**self.kwargs)
103            return self.fmt.format(**fmtargs)
104        else:
105            # print *args directly in the same way as old DNSException
106            return super(DNSException, self).__str__()
107
108
109class FormError(DNSException):
110    """DNS message is malformed."""
111
112
113class SyntaxError(DNSException):
114    """Text input is malformed."""
115
116
117class UnexpectedEnd(SyntaxError):
118    """Text input ended unexpectedly."""
119
120
121class TooBig(DNSException):
122    """The DNS message is too big."""
123
124
125class Timeout(DNSException):
126    """The DNS operation timed out."""
127    supp_kwargs = {'timeout'}
128    fmt = "The DNS operation timed out after {timeout} seconds"
129