core tests - very readable error for unpredicted return values

Previously:
E       TypeError: Object of type Prophecy is not JSON serializable

Now:
E           TypeError: Failed to serialize call: '{'name': 'duplicate_ifc_objects', 'args': ([<Prophecy for 'bonsai.core.tool.Ifc'>],), 'kwargs': {}}'.
E           Possibly due to unpredicted return value for some call.
E           See the list of the recent calls without return values:
E           - {'name': 'get_object', 'args': ('annotation',), 'kwargs': {}}
E           - {'name': 'run', 'args': ('group.assign_group',), 'kwargs': {'group': 'new_group', 'products': ['new_drawing']}}
E           - {'name': 'run', 'args': ('group.edit_group',), 'kwargs': {'group': 'new_group', 'attributes': {'Name': 'unique_name', 'ObjectType': 'DRAWING'}}}
E           - {'name': 'run', 'args': ('group.unassign_group',), 'kwargs': {'group': 'group', 'products': ['new_drawing']}}
This commit is contained in:
Andrej730
2025-04-25 12:14:53 +05:00
parent e2c2234199
commit 330799c799
+42 -2
View File
@@ -241,6 +241,14 @@ def voider():
prophet.verify()
def flatten(iterable):
for item in iterable:
if isinstance(item, (list, tuple)):
yield from flatten(item)
else:
yield item
Call = TypedDict("Call", {"name": str, "args": tuple[Any, ...], "kwargs": dict[str, Any]})
Prediction = TypedDict("Prediction", {"type": Literal["SHOULD_BE_CALLED"], "number": Optional[int], "call": Call})
@@ -270,6 +278,13 @@ class Prophecy:
self.return_values: dict[str, Any] = {}
self.should_call: Optional[Call] = None
@staticmethod
def serialize(call: Call) -> str:
return json.dumps(call, sort_keys=True)
def __repr__(self) -> str:
return f"<Prophecy for '{self.subject.__original_qualname__}'>"
def __getattr__(self, attr: str):
if not hasattr(self.subject, attr):
raise AttributeError(f"Interface '{self.subject.__original_qualname__}' has no attribute '{attr}'.")
@@ -280,7 +295,31 @@ class Prophecy:
call: Call = {"name": attr, "args": args, "kwargs": kwargs}
# Ensure that signature is valid
getattr(self.subject, attr)(*args, **kwargs)
key = json.dumps(call, sort_keys=True)
try:
key = self.serialize(call)
except TypeError as e:
# Serialization error will occur if unpredicted return value
# will be used as an argument to other call
msg = f"Failed to serialize call: '{call}'."
values = list(flatten(args)) + list(flatten(kwargs.values()))
prophecy = next((value for value in values if isinstance(value, Prophecy)), None)
if prophecy is None:
msg += "\nCouldn't find related Prophecy."
raise TypeError(msg) from e
msg += "\nPossibly due to unpredicted return value for some call."
calls_strs: list[str] = []
for call in reversed(prophecy.calls):
call_str = self.serialize(call)
if call_str in prophecy.return_values:
continue
calls_strs.append(f"- {call}")
if calls_strs:
msg += "\nSee the list of the recent calls without return values:\n"
msg += "\n".join(calls_strs)
raise TypeError(msg) from e
self.calls.append(call)
if key in self.return_values:
return self.return_values[key]
@@ -296,7 +335,8 @@ class Prophecy:
def will_return(self, value: Any) -> Self:
"""Remember a return value for the last predicted call."""
key = json.dumps(self.should_call, sort_keys=True)
assert self.should_call
key = self.serialize(self.should_call)
self.return_values[key] = value
return self