Skip to content

Commit d3fed59

Browse files
committed
feat(parse_result): add recursive None stripping for nested structures
Signed-off-by: Ntege Daniel <danientege785@gmail.com>
1 parent 47502f7 commit d3fed59

3 files changed

Lines changed: 79 additions & 3 deletions

File tree

tck/handlers/registry.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,14 +81,29 @@ def safe_dispatch(method_name: str, params: Any, request_id: str | int | None) -
8181
return build_json_rpc_error_response(error, request_id)
8282

8383

84+
def _strip_none(obj: Any, nullable_keys: set[str] | None = None) -> Any:
85+
"""Recursively strip None values from nested dicts and lists."""
86+
if isinstance(obj, dict):
87+
return {
88+
k: _strip_none(v)
89+
for k, v in obj.items()
90+
if v is not None or (nullable_keys is not None and k in nullable_keys)
91+
}
92+
if isinstance(obj, list):
93+
return [_strip_none(item) for item in obj]
94+
return obj
95+
96+
8497
def parse_result(result: Any) -> dict:
8598
"""Parse the result from the methods to dict containing non none key:values.
8699
87100
Fields with metadata={"nullable": True} are preserved even when None.
101+
Recursively strips None from nested structures.
88102
"""
89103
nullable_fields: set[str] = set()
90104
for f in dc_fields(result):
91105
if f.metadata.get("nullable"):
92106
nullable_fields.add(f.name)
93107

94-
return {k: v for k, v in asdict(result).items() if v is not None or k in nullable_fields}
108+
raw = asdict(result)
109+
return _strip_none(raw, nullable_keys=nullable_fields)

tck/handlers/token.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,7 @@ def claim_token(params: ClaimTokenParams) -> ClaimTokenResponse:
489489
return ClaimTokenResponse(status=ResponseCode(receipt.status).name)
490490

491491

492-
def _serialize_key(key) -> str:
492+
def _serialize_key(key) -> str | None:
493493
"""Serialize a key to its DER-encoded hex string representation."""
494494
if key is None:
495495
return ""

tests/tck/handlers_test.py

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from __future__ import annotations
44

5-
from dataclasses import dataclass
5+
from dataclasses import dataclass, field
66
from unittest.mock import MagicMock
77

88
import pytest
@@ -57,6 +57,24 @@ class DummyResult:
5757
other: str | None = None
5858

5959

60+
@dataclass
61+
class NestedChild:
62+
"""Nested dataclass for testing recursive None stripping."""
63+
64+
amount: str | None = None
65+
label: str | None = None
66+
67+
68+
@dataclass
69+
class DummyResultWithNested:
70+
"""Dataclass with nested fields for testing recursive parse_result."""
71+
72+
name: str | None = None
73+
child: NestedChild | None = None
74+
children: list[NestedChild] | None = None
75+
nullable_field: str | None = field(metadata={"nullable": True}, default=None)
76+
77+
6078
class TestHandlerRegistration:
6179
def test_handler_registration_via_decorator(self):
6280
"""Test that @rpc_method decorator registers handler."""
@@ -265,3 +283,46 @@ def test_parse_result_dataclass_ignore_none(self):
265283
parsed = parse_result(result)
266284

267285
assert parsed == {"value": 10}, "Expected filtered dataclass result"
286+
287+
def test_parse_result_strips_none_in_nested_dicts(self):
288+
"""None values in nested dataclass dicts should be stripped."""
289+
result = DummyResultWithNested(
290+
name="test",
291+
child=NestedChild(amount="5", label=None),
292+
)
293+
294+
parsed = parse_result(result)
295+
296+
assert parsed == {"name": "test", "child": {"amount": "5"}, "nullable_field": None}
297+
298+
def test_parse_result_strips_none_in_nested_lists(self):
299+
"""None values inside list items should be stripped."""
300+
result = DummyResultWithNested(
301+
name="test",
302+
children=[
303+
NestedChild(amount="1", label=None),
304+
NestedChild(amount="2", label="ok"),
305+
],
306+
)
307+
308+
parsed = parse_result(result)
309+
310+
assert parsed == {
311+
"name": "test",
312+
"children": [
313+
{"amount": "1"},
314+
{"amount": "2", "label": "ok"},
315+
],
316+
"nullable_field": None,
317+
}
318+
319+
def test_parse_result_preserves_nullable_field_when_none(self):
320+
"""Fields marked nullable should be preserved even when None."""
321+
result = DummyResultWithNested(
322+
name="test",
323+
nullable_field=None,
324+
)
325+
326+
parsed = parse_result(result)
327+
328+
assert parsed == {"name": "test", "nullable_field": None}

0 commit comments

Comments
 (0)