Skip to content

Commit

Permalink
issue #48, functions: improve Python defined function error handling
Browse files Browse the repository at this point in the history
When exceptions happen in a Python defined function, the execution of
the Rule is interrupted. The error and traceback are logged on
`stdwrn` routers and the error message is forwarded back to CLIPS.

A new set of API `error_state` and `clear_error_state()` are provided
to check if errors during rule execution happened.

Signed-off-by: Matteo Cafasso <noxdafox@gmail.com>
  • Loading branch information
noxdafox committed Nov 3, 2021
1 parent de42246 commit 2ee4d97
Show file tree
Hide file tree
Showing 2 changed files with 49 additions and 2 deletions.
32 changes: 31 additions & 1 deletion clips/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"""

import traceback
from typing import Union

import clips

Expand Down Expand Up @@ -347,6 +348,31 @@ class Functions:
def __init__(self, env: ffi.CData):
self._env = env

@property
def error_state(self) -> Union[None, CLIPSError]:
"""Get the CLIPS environment error state.
Equivalent to the CLIPS (get-error) function.
"""
value = clips.values.clips_udf_value(self._env)

lib.GetErrorFunction(self._env, ffi.NULL, value)
state = clips.values.python_value(self._env, value)

if isinstance(state, clips.Symbol):
return None
else:
return CLIPSError(self._env, message=state)

def clear_error_state(self):
"""Clear the CLIPS environment error state.
Equivalent to the CLIPS (clear-error) function.
"""
lib.ClearErrorValue(self._env)

def call(self, function: str, *arguments) -> type:
"""Call the CLIPS function with the given arguments."""
value = clips.values.clips_value(self._env)
Expand Down Expand Up @@ -439,7 +465,11 @@ def python_function(env: ffi.CData, context: ffi.CData, output: ffi.CData):
try:
ret = environment_data(env, 'user_functions')[funcname](*arguments)
except Exception as error:
clips.values.clips_udf_value(env, traceback.format_exc(), value)
message = "[PYCODEFUN1] %r" % error
string = "\n".join((message, traceback.format_exc()))

lib.WriteString(env, 'stdwrn'.encode(), string.encode())
clips.values.clips_udf_value(env, message, value)
lib.SetErrorValue(env, value.header)
lib.UDFThrowError(context)
else:
Expand Down
19 changes: 18 additions & 1 deletion test/environment_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
import unittest
from tempfile import mkstemp

from clips import CLIPSError
from clips import Environment, Symbol, LoggingRouter, ImpliedFact, InstanceName


DEFRULE_FACT = """
(defrule fact-rule
?fact <- (test-fact)
Expand Down Expand Up @@ -38,6 +38,10 @@ def python_types():
return None, True, False


def python_error():
raise Exception("BOOM!")


class TempFile:
"""Cross-platform temporary file."""
name = None
Expand All @@ -60,6 +64,7 @@ def setUp(self):
self.env.define_function(python_function)
self.env.define_function(python_function,
name='python-function-renamed')
self.env.define_function(python_error)
self.env.define_function(python_types)
self.env.define_function(self.python_method)
self.env.define_function(self.python_fact_method)
Expand Down Expand Up @@ -93,6 +98,18 @@ def test_eval_python_function(self):
ret = self.env.eval('(python_types)')
self.assertEqual(ret, expected)

def test_eval_python_error(self):
"""Errors in Python functions are correctly set."""
self.assertIsNone(self.env.error_state)

with self.assertRaises(CLIPSError):
self.env.eval('(python_error)')
self.assertEqual(str(self.env.error_state),
"[PYCODEFUN1] Exception('BOOM!')")

self.env.clear_error_state()
self.assertIsNone(self.env.error_state)

def test_eval_python_method(self):
"""Python method is evaluated correctly."""
expected = [0, 1.1, "2", Symbol('three')]
Expand Down

0 comments on commit 2ee4d97

Please sign in to comment.