diff --git a/langfuse/_utils/serializer.py b/langfuse/_utils/serializer.py index 3d795d13e..3c4fad58b 100644 --- a/langfuse/_utils/serializer.py +++ b/langfuse/_utils/serializer.py @@ -164,10 +164,26 @@ def _default_inner(self, obj: Any) -> Any: return [self.default(item) for item in obj] if hasattr(obj, "__slots__"): - return { - slot: self.default(getattr(obj, slot, None)) - for slot in obj.__slots__ - } + obj_id = id(obj) + + if obj_id in self.seen: + # Break on circular references + return type(obj).__name__ + else: + self.seen.add(obj_id) + try: + slots = ( + (obj.__slots__,) + if isinstance(obj.__slots__, str) + else obj.__slots__ + ) + return { + slot: self.default(getattr(obj, slot, None)) + for slot in slots + } + finally: + self.seen.remove(obj_id) + elif hasattr(obj, "__dict__"): obj_id = id(obj) @@ -176,8 +192,10 @@ def _default_inner(self, obj: Any) -> Any: return type(obj).__name__ else: self.seen.add(obj_id) - result = {k: self.default(v) for k, v in vars(obj).items()} - self.seen.remove(obj_id) + try: + result = {k: self.default(v) for k, v in vars(obj).items()} + finally: + self.seen.remove(obj_id) return result diff --git a/tests/unit/test_serializer.py b/tests/unit/test_serializer.py index b26990b12..362997048 100644 --- a/tests/unit/test_serializer.py +++ b/tests/unit/test_serializer.py @@ -368,6 +368,29 @@ def __init__(self): assert json.loads(serializer.encode(obj)) == {"field": "value"} +def test_slots_single_string(): + class SingleSlotClass: + __slots__ = "query" + + def __init__(self): + self.query = "SELECT 1" + + obj = SingleSlotClass() + serializer = EventSerializer() + assert json.loads(serializer.encode(obj)) == {"query": "SELECT 1"} + + +def test_slots_circular_reference(): + class SlotNode: + __slots__ = ("next",) + + node = SlotNode() + node.next = node + + serializer = EventSerializer() + assert json.loads(serializer.encode(node)) == {"next": "SlotNode"} + + def test_deeply_nested_object_does_not_hang(): class Inner: def __init__(self):