diff --git a/deepmerge/strategy/type_conflict.py b/deepmerge/strategy/type_conflict.py index 1140368..941452b 100644 --- a/deepmerge/strategy/type_conflict.py +++ b/deepmerge/strategy/type_conflict.py @@ -1,5 +1,6 @@ from __future__ import annotations +from collections.abc import Sized from typing import Any, TypeVar import deepmerge.merger @@ -30,5 +31,14 @@ def strategy_use_existing( def strategy_override_if_not_empty( config: deepmerge.merger.Merger, path: list, base: T1, nxt: T2 ) -> T1 | T2: - """overrides the new object over the old object only if the new object is not empty or null""" - return nxt if nxt else base + """overrides the new object over the old object only if the new object is not empty or null. + + ``None`` is treated as null. Sized objects (``dict``, ``list``, ``set``, ``str``, …) are + treated as empty when ``len(nxt) == 0``. All other values — including falsy primitives such + as ``0``, ``0.0``, and ``False`` — are considered non-empty and will override *base*. + """ + if nxt is None: + return base + if isinstance(nxt, Sized) and len(nxt) == 0: + return base + return nxt diff --git a/deepmerge/tests/strategy/test_type_conflict.py b/deepmerge/tests/strategy/test_type_conflict.py index ae8f576..f6ccf93 100644 --- a/deepmerge/tests/strategy/test_type_conflict.py +++ b/deepmerge/tests/strategy/test_type_conflict.py @@ -1,11 +1,15 @@ from typing import Dict +import pytest + from deepmerge.strategy.type_conflict import TypeConflictStrategies EMPTY_DICT: Dict = {} CONTENT_AS_LIST = [{"key": "val"}] +BASE = "base" + def test_merge_if_not_empty(): strategy = TypeConflictStrategies.strategy_override_if_not_empty( @@ -20,3 +24,42 @@ def test_merge_if_not_empty(): strategy = TypeConflictStrategies.strategy_override_if_not_empty({}, [], CONTENT_AS_LIST, None) assert strategy == CONTENT_AS_LIST + + +@pytest.mark.parametrize( + "nxt, expected", + [ + (0, 0), + (False, False), + (0.0, 0.0), + (0j, 0j), + ("0", "0"), + ([0], [0]), + (None, BASE), + ("", BASE), + ([], BASE), + ({}, BASE), + (set(), BASE), + ((), BASE), + ], + ids=[ + "zero int", + "false", + "zero float", + "zero complex", + "zero string", + "list holding zero", + "none", + "empty string", + "empty list", + "empty dict", + "empty set", + "empty tuple", + ], +) +def test_merge_if_not_empty_falsy(nxt, expected): + """Only null and empty sized values keep base; falsy primitives override it.""" + strategy = TypeConflictStrategies.strategy_override_if_not_empty({}, [], BASE, nxt) + assert strategy == expected + # 0 == False, so compare types too + assert type(strategy) is type(expected)