spiff-arena/SpiffWorkflow/exceptions.py

132 lines
4.9 KiB
Python

# -*- coding: utf-8 -*-
# Copyright (C) 2007 Samuel Abels
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
import re
from SpiffWorkflow.util import levenshtein
class SpiffWorkflowException(Exception):
"""
Base class for all SpiffWorkflow-generated exceptions.
"""
def __init__(self, msg):
super().__init__(msg)
self.notes = []
def add_note(self, note):
"""add_note is a python 3.11 feature, this can be removed when we
stop supporting versions prior to 3.11"""
self.notes.append(note)
def __str__(self):
return super().__str__() + ". " + ". ".join(self.notes)
class WorkflowException(SpiffWorkflowException):
"""
Base class for all SpiffWorkflow-generated exceptions.
"""
def __init__(self, message, task_spec=None):
"""
Standard exception class.
:param task_spec: the task spec that threw the exception
:type task_spec: TaskSpec
:param error: a human-readable error message
:type error: string
"""
super().__init__(str(message))
# Points to the TaskSpec that generated the exception.
self.task_spec = task_spec
@staticmethod
def get_task_trace(task):
task_trace = [f"{task.task_spec.description} ({task.workflow.spec.file})"]
workflow = task.workflow
while workflow != workflow.outer_workflow:
caller = workflow.name
workflow = workflow.outer_workflow
task_trace.append(f"{workflow.spec.task_specs[caller].description} ({workflow.spec.file})")
return task_trace
@staticmethod
def did_you_mean_from_name_error(name_exception, options):
"""Returns a string along the lines of 'did you mean 'dog'? Given
a name_error, and a set of possible things that could have been called,
or an empty string if no valid suggestions come up. """
if isinstance(name_exception, NameError):
def_match = re.match("name '(.+)' is not defined", str(name_exception))
if def_match:
bad_variable = re.match("name '(.+)' is not defined",
str(name_exception)).group(1)
most_similar = levenshtein.most_similar(bad_variable,
options, 3)
error_msg = ""
if len(most_similar) == 1:
error_msg += f' Did you mean \'{most_similar[0]}\'?'
if len(most_similar) > 1:
error_msg += f' Did you mean one of \'{most_similar}\'?'
return error_msg
class WorkflowTaskException(WorkflowException):
"""WorkflowException that provides task_trace information."""
def __init__(self, error_msg, task=None, exception=None,
line_number=None, offset=None, error_line=None):
"""
Exception initialization.
:param task: the task that threw the exception
:type task: Task
:param error_msg: a human readable error message
:type error_msg: str
:param exception: an exception to wrap, if any
:type exception: Exception
"""
self.task = task
self.line_number = line_number
self.offset = offset
self.error_line = error_line
if exception:
self.error_type = exception.__class__.__name__
else:
self.error_type = "unknown"
super().__init__(error_msg, task_spec=task.task_spec)
if isinstance(exception, SyntaxError) and not line_number:
# Line number and offset can be recovered directly from syntax errors,
# otherwise they must be passed in.
self.line_number = exception.lineno
self.offset = exception.offset
elif isinstance(exception, NameError):
self.add_note(self.did_you_mean_from_name_error(exception, list(task.data.keys())))
# If encountered in a sub-workflow, this traces back up the stack,
# so we can tell how we got to this particular task, no matter how
# deeply nested in sub-workflows it is. Takes the form of:
# task-description (file-name)
self.task_trace = self.get_task_trace(task)
class StorageException(SpiffWorkflowException):
pass