1"""
2Validation errors, and some surrounding helpers.
3"""
4from collections import defaultdict, deque
5from pprint import pformat
6from textwrap import dedent, indent
7import itertools
8
9import attr
10
11from jsonschema import _utils
12
13WEAK_MATCHES = frozenset(["anyOf", "oneOf"])
14STRONG_MATCHES = frozenset()
15
16_unset = _utils.Unset()
17
18
19class _Error(Exception):
20    def __init__(
21        self,
22        message,
23        validator=_unset,
24        path=(),
25        cause=None,
26        context=(),
27        validator_value=_unset,
28        instance=_unset,
29        schema=_unset,
30        schema_path=(),
31        parent=None,
32    ):
33        super(_Error, self).__init__(
34            message,
35            validator,
36            path,
37            cause,
38            context,
39            validator_value,
40            instance,
41            schema,
42            schema_path,
43            parent,
44        )
45        self.message = message
46        self.path = self.relative_path = deque(path)
47        self.schema_path = self.relative_schema_path = deque(schema_path)
48        self.context = list(context)
49        self.cause = self.__cause__ = cause
50        self.validator = validator
51        self.validator_value = validator_value
52        self.instance = instance
53        self.schema = schema
54        self.parent = parent
55
56        for error in context:
57            error.parent = self
58
59    def __repr__(self):
60        return f"<{self.__class__.__name__}: {self.message!r}>"
61
62    def __str__(self):
63        essential_for_verbose = (
64            self.validator, self.validator_value, self.instance, self.schema,
65        )
66        if any(m is _unset for m in essential_for_verbose):
67            return self.message
68
69        schema_path = _utils.format_as_index(
70            container=self._word_for_schema_in_error_message,
71            indices=list(self.relative_schema_path)[:-1],
72        )
73        instance_path = _utils.format_as_index(
74            container=self._word_for_instance_in_error_message,
75            indices=self.relative_path,
76        )
77        prefix = 16 * " "
78
79        return dedent(
80            f"""\
81            {self.message}
82
83            Failed validating {self.validator!r} in {schema_path}:
84                {indent(pformat(self.schema, width=72), prefix).lstrip()}
85
86            On {instance_path}:
87                {indent(pformat(self.instance, width=72), prefix).lstrip()}
88            """.rstrip(),
89        )
90
91    @classmethod
92    def create_from(cls, other):
93        return cls(**other._contents())
94
95    @property
96    def absolute_path(self):
97        parent = self.parent
98        if parent is None:
99            return self.relative_path
100
101        path = deque(self.relative_path)
102        path.extendleft(reversed(parent.absolute_path))
103        return path
104
105    @property
106    def absolute_schema_path(self):
107        parent = self.parent
108        if parent is None:
109            return self.relative_schema_path
110
111        path = deque(self.relative_schema_path)
112        path.extendleft(reversed(parent.absolute_schema_path))
113        return path
114
115    @property
116    def json_path(self):
117        path = "$"
118        for elem in self.absolute_path:
119            if isinstance(elem, int):
120                path += "[" + str(elem) + "]"
121            else:
122                path += "." + elem
123        return path
124
125    def _set(self, **kwargs):
126        for k, v in kwargs.items():
127            if getattr(self, k) is _unset:
128                setattr(self, k, v)
129
130    def _contents(self):
131        attrs = (
132            "message", "cause", "context", "validator", "validator_value",
133            "path", "schema_path", "instance", "schema", "parent",
134        )
135        return dict((attr, getattr(self, attr)) for attr in attrs)
136
137
138class ValidationError(_Error):
139    """
140    An instance was invalid under a provided schema.
141    """
142
143    _word_for_schema_in_error_message = "schema"
144    _word_for_instance_in_error_message = "instance"
145
146
147class SchemaError(_Error):
148    """
149    A schema was invalid under its corresponding metaschema.
150    """
151
152    _word_for_schema_in_error_message = "metaschema"
153    _word_for_instance_in_error_message = "schema"
154
155
156@attr.s(hash=True)
157class RefResolutionError(Exception):
158    """
159    A ref could not be resolved.
160    """
161
162    _cause = attr.ib()
163
164    def __str__(self):
165        return str(self._cause)
166
167
168class UndefinedTypeCheck(Exception):
169    """
170    A type checker was asked to check a type it did not have registered.
171    """
172
173    def __init__(self, type):
174        self.type = type
175
176    def __str__(self):
177        return f"Type {self.type!r} is unknown to this type checker"
178
179
180class UnknownType(Exception):
181    """
182    A validator was asked to validate an instance against an unknown type.
183    """
184
185    def __init__(self, type, instance, schema):
186        self.type = type
187        self.instance = instance
188        self.schema = schema
189
190    def __str__(self):
191        prefix = 16 * " "
192
193        return dedent(
194            f"""\
195            Unknown type {self.type!r} for validator with schema:
196                {indent(pformat(self.schema, width=72), prefix).lstrip()}
197
198            While checking instance:
199                {indent(pformat(self.instance, width=72), prefix).lstrip()}
200            """.rstrip(),
201        )
202
203
204class FormatError(Exception):
205    """
206    Validating a format failed.
207    """
208
209    def __init__(self, message, cause=None):
210        super(FormatError, self).__init__(message, cause)
211        self.message = message
212        self.cause = self.__cause__ = cause
213
214    def __str__(self):
215        return self.message
216
217
218class ErrorTree(object):
219    """
220    ErrorTrees make it easier to check which validations failed.
221    """
222
223    _instance = _unset
224
225    def __init__(self, errors=()):
226        self.errors = {}
227        self._contents = defaultdict(self.__class__)
228
229        for error in errors:
230            container = self
231            for element in error.path:
232                container = container[element]
233            container.errors[error.validator] = error
234
235            container._instance = error.instance
236
237    def __contains__(self, index):
238        """
239        Check whether ``instance[index]`` has any errors.
240        """
241
242        return index in self._contents
243
244    def __getitem__(self, index):
245        """
246        Retrieve the child tree one level down at the given ``index``.
247
248        If the index is not in the instance that this tree corresponds
249        to and is not known by this tree, whatever error would be raised
250        by ``instance.__getitem__`` will be propagated (usually this is
251        some subclass of `LookupError`.
252        """
253
254        if self._instance is not _unset and index not in self:
255            self._instance[index]
256        return self._contents[index]
257
258    def __setitem__(self, index, value):
259        """
260        Add an error to the tree at the given ``index``.
261        """
262        self._contents[index] = value
263
264    def __iter__(self):
265        """
266        Iterate (non-recursively) over the indices in the instance with errors.
267        """
268
269        return iter(self._contents)
270
271    def __len__(self):
272        """
273        Return the `total_errors`.
274        """
275        return self.total_errors
276
277    def __repr__(self):
278        return f"<{self.__class__.__name__} ({len(self)} total errors)>"
279
280    @property
281    def total_errors(self):
282        """
283        The total number of errors in the entire tree, including children.
284        """
285
286        child_errors = sum(len(tree) for _, tree in self._contents.items())
287        return len(self.errors) + child_errors
288
289
290def by_relevance(weak=WEAK_MATCHES, strong=STRONG_MATCHES):
291    """
292    Create a key function that can be used to sort errors by relevance.
293
294    Arguments:
295        weak (set):
296            a collection of validator names to consider to be "weak".
297            If there are two errors at the same level of the instance
298            and one is in the set of weak validator names, the other
299            error will take priority. By default, :validator:`anyOf` and
300            :validator:`oneOf` are considered weak validators and will
301            be superseded by other same-level validation errors.
302
303        strong (set):
304            a collection of validator names to consider to be "strong"
305    """
306    def relevance(error):
307        validator = error.validator
308        return -len(error.path), validator not in weak, validator in strong
309    return relevance
310
311
312relevance = by_relevance()
313
314
315def best_match(errors, key=relevance):
316    """
317    Try to find an error that appears to be the best match among given errors.
318
319    In general, errors that are higher up in the instance (i.e. for which
320    `ValidationError.path` is shorter) are considered better matches,
321    since they indicate "more" is wrong with the instance.
322
323    If the resulting match is either :validator:`oneOf` or :validator:`anyOf`,
324    the *opposite* assumption is made -- i.e. the deepest error is picked,
325    since these validators only need to match once, and any other errors may
326    not be relevant.
327
328    Arguments:
329        errors (collections.abc.Iterable):
330
331            the errors to select from. Do not provide a mixture of
332            errors from different validation attempts (i.e. from
333            different instances or schemas), since it won't produce
334            sensical output.
335
336        key (collections.abc.Callable):
337
338            the key to use when sorting errors. See `relevance` and
339            transitively `by_relevance` for more details (the default is
340            to sort with the defaults of that function). Changing the
341            default is only useful if you want to change the function
342            that rates errors but still want the error context descent
343            done by this function.
344
345    Returns:
346        the best matching error, or ``None`` if the iterable was empty
347
348    .. note::
349
350        This function is a heuristic. Its return value may change for a given
351        set of inputs from version to version if better heuristics are added.
352    """
353    errors = iter(errors)
354    best = next(errors, None)
355    if best is None:
356        return
357    best = max(itertools.chain([best], errors), key=key)
358
359    while best.context:
360        best = min(best.context, key=key)
361    return best
362