Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 48 additions & 2 deletions src/lob_hlpr/hlpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,53 @@ def lob_print(log_path: str, *args, **kwargs):

@staticmethod
def ascleandict(dclass, remove_false=False):
"""Convert a dataclass to a dictionary and remove None values."""
return asdict(
"""Convert a dataclass to a dictionary and remove None values.

Largely generated by AI...

Args:
dclass: The dataclass instance to convert.
remove_false: If True, also remove boolean fields that are False.

Returns:
dict: The cleaned dictionary without empty values.
"""

def clean_value(v):
"""Recursively clean nested structures."""
if isinstance(v, dict):
cleaned = {
k: clean_value(val)
for (k, val) in v.items()
if (val is not None)
and not (isinstance(val, list) and len(val) == 0)
and not (isinstance(val, dict) and len(val) == 0)
and not (remove_false and (isinstance(val, bool) and val is False))
}
# Keep removing empty dicts/lists until nothing changes
while True:
filtered = {
k: v
for (k, v) in cleaned.items()
if not (isinstance(v, dict) and len(v) == 0)
and not (isinstance(v, list) and len(v) == 0)
}
if len(filtered) == len(cleaned):
break
cleaned = filtered
return cleaned
elif isinstance(v, list):
cleaned_items = [clean_value(item) for item in v]
# Filter out empty dicts and lists from the result
return [
item
for item in cleaned_items
if not (isinstance(item, dict) and len(item) == 0)
and not (isinstance(item, list) and len(item) == 0)
]
return v

result = asdict(
dclass,
dict_factory=lambda x: {
k: v
Expand All @@ -178,6 +223,7 @@ def ascleandict(dclass, remove_false=False):
and not (remove_false and (isinstance(v, bool) and v is False))
},
)
return clean_value(result)

@staticmethod
def unix_timestamp() -> int:
Expand Down
55 changes: 54 additions & 1 deletion tests/test_lob_hlpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,14 @@ def test_log_print_passes(tmp_path, capsys):
def test_as_clean_dict_passes():
"""Test as_clean_dict function with valid inputs."""

@dataclass
class MoreNestedTestClass:
listkey: list | None = None
dictkey: dict | None = None

@dataclass
class NestedTestClass:
nested_key: str | None = None
more_nested: MoreNestedTestClass | None = None

@dataclass
class TestClass:
Expand All @@ -184,10 +189,58 @@ class TestClass:
dclass.dictkey = {}
dclass.boolkey = False
dclass.nested = NestedTestClass()
dclass.nested.more_nested = MoreNestedTestClass(dictkey={}, listkey=[])
assert hlp.ascleandict(dclass) == {"intkey": 0, "strkey": "", "boolkey": False}
assert "nested" not in hlp.ascleandict(dclass)

assert hlp.ascleandict(dclass, remove_false=True) == {"intkey": 0, "strkey": ""}

@dataclass
class DataWithList:
items: list
name: str

data2 = DataWithList(
items=[{"value": 1, "empty_dict": {}}, {"value": 2, "none_val": None}, [], {}],
name="test",
)
result2 = hlp.ascleandict(data2)
assert result2 == {"name": "test", "items": [{"value": 1}, {"value": 2}]}


def test_ascleandict_nested_cleanup_multiple_passes():
"""Test that ascleandict removes cascading empty nested structures."""

@dataclass
class DeepNested:
value: str | None = None

@dataclass
class MidLevel:
deep: DeepNested | None = None
data: dict | None = None

@dataclass
class TopLevel:
mid: MidLevel | None = None
name: str = "test"

# Create data where cleaning cascades:
# - DeepNested.value = None gets removed
# - MidLevel.deep becomes {} and gets removed
# - MidLevel.data is already {} and gets removed
# - MidLevel itself might become {} and gets removed
data = TopLevel(
name="test",
mid=MidLevel(
deep=DeepNested(value=None), # Becomes {}
data={"inner": None}, # Becomes {}
),
)
result = hlp.ascleandict(data)
# The while loop should run multiple times, executing line 190
assert result == {"name": "test"}


def test_unix_timestamp():
"""Test unix_timestamp function."""
Expand Down