From 12659360c6e3d8d80a8d2e631971452fd4560d79 Mon Sep 17 00:00:00 2001 From: Kevin Weiss Date: Fri, 10 Apr 2026 09:14:29 +0200 Subject: [PATCH 1/2] fix: AI tries to fix ascleandict again There were recursion errors... possibly from the user side but it would be good to fix them. --- src/lob_hlpr/hlpr.py | 78 +++++++++++++++++++++++++++----------------- 1 file changed, 48 insertions(+), 30 deletions(-) diff --git a/src/lob_hlpr/hlpr.py b/src/lob_hlpr/hlpr.py index 16b8692..2fb976b 100644 --- a/src/lob_hlpr/hlpr.py +++ b/src/lob_hlpr/hlpr.py @@ -213,36 +213,54 @@ def _keep(v) -> bool: return False return True - def _convert(obj: object) -> Any: - if is_dataclass(obj) and not isinstance(obj, type): - result = {} - for f in fields(obj): - converted = _convert(getattr(obj, f.name)) - if _keep(converted): - result[f.name] = converted - return result - if isinstance(obj, dict): - result = {} - for k, v in obj.items(): - converted = _convert(v) - if _keep(converted): - key = ( - str(k) - if json_serializable and not isinstance(k, str) - else k - ) - result[key] = converted - return result - if isinstance(obj, (list, tuple)): - items = [_convert(item) for item in obj] - items = [item for item in items if _keep(item)] - return tuple(items) if isinstance(obj, tuple) else items - if json_serializable: - try: - json.dumps(obj) - except (TypeError, OverflowError): - return str(obj) - return obj + def _convert(obj: object, _seen: set | None = None) -> Any: + is_container = isinstance(obj, (dict, list)) or ( + is_dataclass(obj) and not isinstance(obj, type) + ) + if is_container: + if _seen is None: + _seen = set() + obj_id = id(obj) + if obj_id in _seen: + logging.getLogger(__name__).warning( + "ascleandict: circular reference detected in %s, skipping", + type(obj).__name__, + ) + return f"" + _seen.add(obj_id) + try: + if is_dataclass(obj) and not isinstance(obj, type): + result = {} + for f in fields(obj): + converted = _convert(getattr(obj, f.name), _seen) + if _keep(converted): + result[f.name] = converted + return result + if isinstance(obj, dict): + result = {} + for k, v in obj.items(): + converted = _convert(v, _seen) + if _keep(converted): + key = ( + str(k) + if json_serializable and not isinstance(k, str) + else k + ) + result[key] = converted + return result + if isinstance(obj, (list, tuple)): + items = [_convert(item, _seen) for item in obj] + items = [item for item in items if _keep(item)] + return tuple(items) if isinstance(obj, tuple) else items + if json_serializable: + try: + json.dumps(obj) + except (TypeError, OverflowError): + return str(obj) + return obj + finally: + if is_container: + _seen.discard(obj_id) return _convert(dclass) From e379803f0d16411b65eb17d8d4233fef074114b5 Mon Sep 17 00:00:00 2001 From: Kevin Weiss Date: Fri, 10 Apr 2026 09:27:32 +0200 Subject: [PATCH 2/2] test: Check recursive dataclasses still export stuff --- tests/test_lob_hlpr.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/test_lob_hlpr.py b/tests/test_lob_hlpr.py index a978ae9..769489a 100644 --- a/tests/test_lob_hlpr.py +++ b/tests/test_lob_hlpr.py @@ -415,6 +415,37 @@ class TopLevel: assert result == {"name": "test"} +def test_ascleandict_circular_reference_in_list(): + """Test that ascleandict handles a circular list reference without crashing.""" + + @dataclass + class Node: + items: list + + circular_list: list = [] + circular_list.append(circular_list) # List references itself + node = Node(items=circular_list) + + result = hlp.ascleandict(node) + # The circular entry is replaced with a sentinel string, not empty, so kept + assert result["items"] == [""] + + +def test_ascleandict_circular_reference_in_dict(): + """Test that ascleandict handles a circular dict reference without crashing.""" + + @dataclass + class Container: + data: dict + + circular_dict: dict = {} + circular_dict["self"] = circular_dict # Dict references itself + container = Container(data=circular_dict) + + result = hlp.ascleandict(container) + assert result["data"]["self"] == "" + + def test_unix_timestamp(): """Test unix_timestamp function.""" timestamp = hlp.unix_timestamp()