diff --git a/libyang/context.py b/libyang/context.py index f9bd5a5..2a80c93 100644 --- a/libyang/context.py +++ b/libyang/context.py @@ -16,7 +16,15 @@ validation_flags, ) from .schema import Module, SNode, schema_in_format -from .util import DataType, IOType, LibyangError, c2str, data_load, str2c +from .util import ( + DataType, + IOType, + LibyangError, + LibyangErrorItem, + c2str, + data_load, + str2c, +) # ------------------------------------------------------------------------------------- @@ -284,23 +292,35 @@ def __exit__(self, *args, **kwargs): self.destroy() def error(self, msg: str, *args) -> LibyangError: - msg %= args + if args: + msg = msg % args + + parts = [msg] + errors = [] if self.cdata: err = lib.ly_err_first(self.cdata) while err: - if err.msg: - msg += ": %s" % c2str(err.msg) - if err.data_path: - msg += ": Data path: %s" % c2str(err.data_path) - if err.schema_path: - msg += ": Schema path: %s" % c2str(err.schema_path) - if err.line != 0: - msg += " (line %u)" % err.line + m = c2str(err.msg) if err.msg else None + dp = c2str(err.data_path) if err.data_path else None + sp = c2str(err.schema_path) if err.schema_path else None + ln = int(err.line) if err.line else None + parts.extend( + tmpl.format(val) + for val, tmpl in [ + (m, ": {}"), + (dp, ": Data path: {}"), + (sp, ": Schema path: {}"), + (ln, " (line {})"), + ] + if val is not None + ) + errors.append(LibyangErrorItem(m, dp, sp, ln)) err = err.next lib.ly_err_clean(self.cdata, ffi.NULL) - return LibyangError(msg) + msg = "".join(parts) + return LibyangError(msg, errors=errors) def parse_module( self, diff --git a/libyang/util.py b/libyang/util.py index c380ae5..6e86a69 100644 --- a/libyang/util.py +++ b/libyang/util.py @@ -2,16 +2,34 @@ # Copyright (c) 2021 RACOM s.r.o. # SPDX-License-Identifier: MIT +from dataclasses import dataclass import enum -from typing import Optional +from typing import Iterable, Optional import warnings from _libyang import ffi, lib +# ------------------------------------------------------------------------------------- +@dataclass(frozen=True) +class LibyangErrorItem: + msg: Optional[str] + data_path: Optional[str] + schema_path: Optional[str] + line: Optional[int] + + # ------------------------------------------------------------------------------------- class LibyangError(Exception): - pass + def __init__( + self, message: str, *args, errors: Optional[Iterable[LibyangErrorItem]] = None + ): + super().__init__(message, *args) + self.message = message + self.errors = tuple(errors or ()) + + def __str__(self): + return self.message # ------------------------------------------------------------------------------------- diff --git a/tests/test_data.py b/tests/test_data.py index 1479eb9..9cd3959 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -289,6 +289,18 @@ def test_data_parse_config_xml_multi_error(self): "Data path: /yolo-system:conf/url[proto='https'] (line 7)", ) + first = cm.exception.errors[0] + self.assertEqual(first.msg, 'Invalid boolean value "abcd".') + self.assertEqual( + first.data_path, "/yolo-system:conf/url[proto='https']/enabled" + ) + self.assertEqual(first.line, 6) + + second = cm.exception.errors[1] + self.assertEqual(second.msg, 'List instance is missing its key "host".') + self.assertEqual(second.data_path, "/yolo-system:conf/url[proto='https']") + self.assertEqual(second.line, 7) + XML_STATE = """ foo