-
Couldn't load subscription status.
- Fork 6
handle None comparison in deep_merge_dict #494
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
WalkthroughUpdated deep_merge_dict in infrahub_sdk/utils.py to adjust merge semantics, especially for None handling and equality checks. Tests extended to cover merging dicts where one side has None values to ensure expected outcomes. No public API signatures changed. Changes
Sequence Diagram(s)Estimated code review effort🎯 2 (Simple) | ⏱️ ~8 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
Codecov Report❌ Patch coverage is
@@ Coverage Diff @@
## stable #494 +/- ##
==========================================
- Coverage 75.83% 75.76% -0.08%
==========================================
Files 100 100
Lines 8870 8825 -45
Branches 1740 1728 -12
==========================================
- Hits 6727 6686 -41
+ Misses 1667 1664 -3
+ Partials 476 475 -1
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 1 file with indirect coverage changes 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (6)
infrahub_sdk/utils.py (5)
151-153: List merge is correct but O(n^2); consider a fast path for empty lists.Current membership checks make this quadratic. A small readability/perf win: early-return when either side is empty.
Apply this diff to short-circuit obvious cases:
- elif isinstance(a_val, list) and isinstance(b_val, list): + elif isinstance(a_val, list) and isinstance(b_val, list): # Merge lists # Cannot use compare_list because list of dicts won't work (dict not hashable) - dicta[key] = [i for i in a_val if i not in b_val] + b_val + if not a_val: + dicta[key] = b_val + elif not b_val: + dicta[key] = a_val + else: + dicta[key] = [i for i in a_val if i not in b_val] + b_val
154-157: None-aware merge resolves the “plain vs property” conflict (issue #7022).
- If a_val is None and b_val is not None → take b_val.
- If b_val is None → keep a_val.
- Equal scalars → no-op.
This exactly handles cases like {"name": None} + {"name": {"value": None}} → {"name": {"value": None}}.Consider documenting this behavior in the function docstring for future readers.
139-141: Document the new None-handling semantics in the docstring.Clarify merge rules to prevent regressions and ease maintenance.
Apply this diff to enhance the docstring:
def deep_merge_dict(dicta: dict, dictb: dict, path: list | None = None) -> dict: - """Deep Merge Dictionary B into Dictionary A. - Code is inspired by https://stackoverflow.com/a/7205107 - """ + """Deep merge Dictionary B into Dictionary A (mutates A). + - Dicts are merged recursively. + - Lists are merged by appending unique items from A that are not in B, preserving B's order. + - None-handling: + * If A has None and B has non-None, take B's value. + * If B has None and A has non-None, keep A's value. + * If values are equal, keep A. + - For differing non-None, non-mergeable scalars, a ValueError is raised (“Conflict at <path>”). + Code is inspired by https://stackoverflow.com/a/7205107 + """
144-147: Minor readability improvement: iterate items to avoid repeated dict lookups.Using items() removes double indexing and keeps b_val scoped consistently.
Apply this diff:
- for key in dictb: - b_val = dictb[key] + for key, b_val in dictb.items(): if key in dicta: - a_val = dicta[key] + a_val = dicta[key]
160-161: Use the local b_val for consistency and a tiny perf win.Avoids re-indexing dictb.
Apply this diff:
- else: - dicta[key] = dictb[key] + else: + dicta[key] = b_valtests/unit/sdk/test_utils.py (1)
99-105: Good coverage for scalar None-vs-non-None; add nested dict vs None to reflect the reported issue.To directly exercise the “name” vs “name__value” case that motivated this PR, add tests where one side is a dict and the other is None.
Apply this diff within the same test or as separate assertions:
assert deep_merge_dict(d, e) == {"keyA": [10, 20, 30], "keyB": "foo"} assert deep_merge_dict(f, g) == {"keyA": "foo", "keyB": "bar"} + + # None vs dict handling (motivating scenario) + h = {"name": None} + i = {"name": {"value": None}} + assert deep_merge_dict(h, i) == {"name": {"value": None}} + # Reverse direction should preserve the non-None dict + assert deep_merge_dict(i, h) == {"name": {"value": None}}Alternatively, you can add a dedicated test function:
def test_deep_merge_dict_none_vs_dict(): assert deep_merge_dict({"name": None}, {"name": {"value": None}}) == {"name": {"value": None}} assert deep_merge_dict({"name": {"value": None}}, {"name": None}) == {"name": {"value": None}}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
infrahub_sdk/utils.py(1 hunks)tests/unit/sdk/test_utils.py(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
tests/unit/sdk/test_utils.py (1)
infrahub_sdk/utils.py (1)
deep_merge_dict(138-162)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
- GitHub Check: unit-tests (3.9)
- GitHub Check: unit-tests (3.13)
- GitHub Check: unit-tests (3.12)
- GitHub Check: unit-tests (3.10)
- GitHub Check: unit-tests (3.11)
- GitHub Check: integration-tests-latest-infrahub
- GitHub Check: Cloudflare Pages
🔇 Additional comments (1)
infrahub_sdk/utils.py (1)
148-150: Recursion on dict values is correct and preserves in-place merge semantics.Using a_val/b_val locals improves readability and avoids repeated lookups. This aligns with the desired deep-merge behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. I'm wondering if there's a point to have deep_merge_dict() in the SDK, to me that feels wrong as it's only used within Infrahub. Not really related to this PR though.
part of fixing opsmill/infrahub#7022
handles the situation when a
Noneis compared to a non-None. for example{"name": None}and{"name": {"value": None}}should be merged as{"name": {"value": None}}Summary by CodeRabbit
Bug Fixes
Tests