# 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 re
from dataclasses import dataclass
from pathlib import Path
from typing import Optional, Tuple
[docs]@dataclass
class Location:
"""Representation of a location within a file
Attributes:
file (Optional[Path]): Path to the file
line (int): Line within the file (default: ``0``)
columns (Tuple[int,int]): Columns on the line, from/to (default:
``(0, 0)``)
"""
file: Optional[Path]
lines: Tuple[int, int] = (0, 0)
columns: Tuple[int, int] = (0, 0)
[docs]class ConfigurationError(Exception):
"""Exception raised during the configuration of MiniZinc
Attributes:
message (str): Explanation of the error
"""
message: str
[docs]class MiniZincError(Exception):
"""Exception raised for errors caused by a MiniZinc Driver
Attributes:
location (Optional[Location]): File location of the error
message (str): Explanation of the error
"""
location: Optional[Location]
message: str
def __init__(self, location: Optional[Location] = None, message: str = ""):
super().__init__(message)
self.location = location
class MiniZincWarning(Warning):
"""Warning created for warnings originating from a MiniZinc Driver"""
[docs]class EvaluationError(MiniZincError):
"""Exception raised for errors due to an error during instance evaluation by
the MiniZinc Driver"""
pass
[docs]class AssertionError(EvaluationError):
"""Exception raised for MiniZinc assertions that failed during instance
evaluation"""
pass
[docs]class TypeError(MiniZincError):
"""Exception raised for type errors found in an MiniZinc Instance"""
pass
class IncludeError(MiniZincError):
"""Exception raised for type errors found in an MiniZinc Instance"""
pass
class CyclicIncludeError(MiniZincError):
"""Exception raised for type errors found in an MiniZinc Instance"""
pass
[docs]class SyntaxError(MiniZincError):
"""Exception raised for syntax errors found in an MiniZinc Instance"""
pass
[docs]def parse_error(error_txt: bytes) -> MiniZincError:
"""Parse error from bytes array (raw string)
Parse error scans the output from a MiniZinc driver to generate the
appropriate MiniZincError. It will make the distinction between different
kinds of errors as found by MiniZinc and tries to parse the relevant
information to the error. The different kinds of errors are represented by
different sub-classes of MiniZincError.
Args:
error_txt (bytes): raw string containing a MiniZinc error. Generally
this should be the error stream of a driver.
Returns:
An error generated from the string
"""
error = MiniZincError
if b"MiniZinc: evaluation error:" in error_txt:
error = EvaluationError
if b"Assertion failed:" in error_txt:
error = AssertionError
elif b"MiniZinc: type error:" in error_txt:
error = TypeError
elif b"Error: syntax error" in error_txt:
error = SyntaxError
location = None
match = re.search(rb"([^\s]+):(\d+)(.(\d+)-(\d+))?:\s", error_txt)
if match:
columns = (0, 0)
if match[3]:
columns = (int(match[4].decode()), int(match[5].decode()))
lines = (int(match[2].decode()), int(match[2].decode()))
location = Location(Path(match[1].decode()), lines, columns)
message = error_txt.decode().strip()
if not message:
message = (
"MiniZinc stopped with a non-zero exit code, but did not output an "
"error message. "
)
elif location is not None and location.file is not None and location.file.exists():
with location.file.open() as f:
for _ in range(location.lines[0] - 2):
f.readline()
message += "\nFile fragment:\n"
for nr in range(max(1, location.lines[0] - 1), location.lines[1] + 2):
line = f.readline()
if line == "":
break
message += f"{nr}: {line.rstrip()}\n"
diff = location.columns[1] - location.columns[0]
if (
(location.lines[0] == location.lines[1])
and nr == location.lines[0]
and diff > 0
):
message += (
" " * (len(str(nr)) + 2 + location.columns[0] - 1)
+ "^" * (diff + 1)
+ "\n"
)
return error(location, message)
def error_from_stream_obj(obj):
"""Convert object from JSON stream into MiniZinc Python error
Args:
obj (Dict): Parsed JSON object from the ``--json-stream``
mode of MiniZinc.
Returns:
An error generated from the object
"""
assert obj["type"] == "error"
error = MiniZincError
if obj["what"] == "syntax error":
error = SyntaxError
elif obj["what"] == "type error":
error = TypeError
elif obj["what"] == "include error":
error = IncludeError
elif obj["what"] == "cyclic include error":
error = CyclicIncludeError
elif obj["what"] == "evaluation error":
error = EvaluationError
elif obj["what"] == "assertion failed":
error = AssertionError
location = None
if "location" in obj:
location = Location(
obj["location"]["filename"],
(obj["location"]["firstLine"], obj["location"]["lastLine"]),
(obj["location"]["firstColumn"], obj["location"]["lastColumn"]),
)
# TODO: Process stack information
message = (
"MiniZinc stopped with a non-zero exit code, but did not output an "
"error message. "
)
if "message" in obj:
message = obj["message"]
elif "cycle" in obj:
message = " includes ".join(obj["cycle"])
return error(location, message)