Skip to content

Commit

Permalink
Merge pull request #581 from idaholab/gotta_have_more_input_lines
Browse files Browse the repository at this point in the history
Adding more input context to all error messages.
  • Loading branch information
MicahGale authored Nov 6, 2024
2 parents 9e77f4f + 6f04bc7 commit de312e6
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 26 deletions.
7 changes: 7 additions & 0 deletions doc/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ MontePy Changelog
0.5 releases
============

#Next Version#
--------------

**Error Handling**

* Added the input file, line number, and the input text to almost all errors raised by ``MCNP_Object`` (:pull:`581`).

0.5.1
--------------

Expand Down
109 changes: 87 additions & 22 deletions montepy/errors.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
# Copyright 2024, Battelle Energy Alliance, LLC All Rights Reserved.

import traceback


class LineOverRunWarning(UserWarning):
"""
Raised when non-comment inputs exceed the allowed line length in an input.
Expand All @@ -25,12 +29,7 @@ def __init__(self, input, message):
path = ""
start_line = 0
lines = ""
self.message = f"""
: {path}, line {start_line}
{message}
the full input:
{lines}"""
self.message = message
super().__init__(self.message)


Expand Down Expand Up @@ -62,29 +61,54 @@ def __init__(self, input, message, error_queue):
line_no = 0
index = 0
base_message = f"The input ended prematurely."
buffer = [f" {path}, line {start_line + line_no -1}", ""]
if input:
for i, line in enumerate(input.input_lines):
if i == line_no - 1:
buffer.append(f" >{start_line + i:5g}| {line}")
if token:
length = len(token.value)
marker = "^" * length
buffer.append(
f"{' '* 10}|{' ' * (index+1)}{marker} not expected here."
)
else:
buffer.append(f" {start_line + i:5g}| {line}")
buffer.append(base_message)
buffer.append(error["message"])
messages.append("\n".join(buffer))
messages.append(
_print_input(
path,
start_line,
error["message"],
line_no,
input,
token,
base_message,
index,
)
)
self.message = "\n".join(messages + [message])
else:
self.message = message

ValueError.__init__(self, self.message)


def _print_input(
path,
start_line,
error_msg,
line_no=0,
input=None,
token=None,
base_message=None,
index=None,
):
buffer = [f" {path}, line {start_line + line_no -1}", ""]
if input:
for i, line in enumerate(input.input_lines):
if i == line_no - 1:
buffer.append(f" >{start_line + i:5g}| {line}")
if token:
length = len(token.value)
marker = "^" * length
buffer.append(
f"{' '* 10}|{' ' * (index+1)}{marker} not expected here."
)
else:
buffer.append(f" {start_line + i:5g}| {line}")
if base_message:
buffer.append(base_message)
buffer.append(error_msg)
return "\n".join(buffer)


class NumberConflictError(Exception):
"""
Raised when there is a conflict in number spaces
Expand Down Expand Up @@ -189,3 +213,44 @@ class LineExpansionWarning(Warning):
def __init__(self, message):
self.message = message
super().__init__(self.message)


def add_line_number_to_exception(error, broken_robot):
"""
Adds additional context to an Exception raised by an :class:`~montepy.mcnp_object.MCNP_Object`.
This will add the line, file name, and the input lines to the error.
:param error: The error that was raised.
:type error: Exception
:param broken_robot: The parent object that had the error raised.
:type broken_robot: MCNP_Object
:raises Exception: ... that's the whole point.
"""
# avoid calling this n times recursively
if hasattr(error, "montepy_handled"):
raise error
error.montepy_handled = True
args = error.args
trace = error.__traceback__
if len(args) > 0:
message = args[0]
else:
message = ""
try:
input_obj = broken_robot._input
assert input_obj is not None
lineno = input_obj.line_number
file = str(input_obj.input_file)
lines = input_obj.input_lines
message = _print_input(file, lineno, message, input=input_obj)
except Exception as e:
try:
message = (
f"{message}\n\nError came from {broken_robot} from an unknown file."
)
except Exception as e2:
message = f"{message}\n\nError came from an object of type {type(broken_robot)} from an unknown file."
args = (message,) + args[1:]
error.args = args
raise error.with_traceback(trace)
2 changes: 1 addition & 1 deletion montepy/input_parser/input_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,4 +180,4 @@ def write(self, to_write):
return self._fh.write(to_write)

def __str__(self):
return self.name
return str(self.name)
64 changes: 62 additions & 2 deletions montepy/mcnp_object.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Copyright 2024, Battelle Energy Alliance, LLC All Rights Reserved.
from abc import ABC, abstractmethod
from abc import ABC, ABCMeta, abstractmethod
import copy
import functools
import itertools as it
from montepy.errors import *
from montepy.constants import (
Expand All @@ -22,7 +23,66 @@
import weakref


class MCNP_Object(ABC):
class _ExceptionContextAdder(ABCMeta):
"""
A metaclass for wrapping all class properties and methods in :func:`~montepy.errors.add_line_number_to_exception`.
"""

@staticmethod
def _wrap_attr_call(func):
"""
Wraps the function, and returns the modified function.
"""

@functools.wraps(func)
def wrapped(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
if len(args) > 0 and isinstance(args[0], MCNP_Object):
self = args[0]
add_line_number_to_exception(e, self)
else:
raise e

if isinstance(func, staticmethod):
return staticmethod(wrapped)
if isinstance(func, classmethod):
return classmethod(wrapped)
return wrapped

def __new__(meta, classname, bases, attributes):
"""
This will replace all properties and callable attributes with
wrapped versions.
"""
new_attrs = {}
for key, value in attributes.items():
if key.startswith("_"):
new_attrs[key] = value
if callable(value):
new_attrs[key] = _ExceptionContextAdder._wrap_attr_call(value)
elif isinstance(value, property):
new_props = {}
for attr_name in {"fget", "fset", "fdel", "doc"}:
try:
assert getattr(value, attr_name)
new_props[attr_name] = _ExceptionContextAdder._wrap_attr_call(
getattr(value, attr_name)
)
except (AttributeError, AssertionError):
new_props[attr_name] = None

new_attrs[key] = property(**new_props)
else:
new_attrs[key] = value
cls = super().__new__(meta, classname, bases, new_attrs)
return cls


class MCNP_Object(ABC, metaclass=_ExceptionContextAdder):
"""
Abstract class for semantic representations of MCNP inputs.
Expand Down
3 changes: 2 additions & 1 deletion montepy/universe.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ class Universe(Numbered_MCNP_Object):
"""

def __init__(self, number):
self._number = self._generate_default_node(int, -1)
if not isinstance(number, int):
raise TypeError("number must be int")
if number < 0:
raise ValueError(f"Universe number must be ≥ 0. {number} given.")
self._number = montepy.input_parser.syntax_node.ValueNode(number, int)
self._number = self._generate_default_node(int, number)

class Parser:
def parse(self, token_gen, input):
Expand Down

0 comments on commit de312e6

Please sign in to comment.