core: improve None value processing in merge_dicts() (#17462)

- **Description:** fix `None` and `0` merging in `merge_dicts()`, add
tests.
```python
from langchain_core.utils._merge import merge_dicts
assert merge_dicts({"a": None}, {"a": 0}) == {"a": 0}
```

---------

Co-authored-by: Sergey Kozlov <sergey.kozlov@ludditelabs.io>
This commit is contained in:
Sergey Kozlov 2024-02-13 22:48:02 +06:00 committed by GitHub
parent e5472b5eb8
commit db6f266d97
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 77 additions and 5 deletions

View File

@ -19,11 +19,9 @@ def merge_dicts(left: Dict[str, Any], right: Dict[str, Any]) -> Dict[str, Any]:
for k, v in right.items(): for k, v in right.items():
if k not in merged: if k not in merged:
merged[k] = v merged[k] = v
elif merged[k] is None and v: elif v is not None and merged[k] is None:
merged[k] = v merged[k] = v
elif v is None: elif v is None or merged[k] == v:
continue
elif merged[k] == v:
continue continue
elif type(merged[k]) != type(v): elif type(merged[k]) != type(v):
raise TypeError( raise TypeError(

View File

@ -1,9 +1,12 @@
from typing import Dict, Optional, Tuple, Type import re
from contextlib import AbstractContextManager, nullcontext
from typing import Dict, Optional, Tuple, Type, Union
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
from langchain_core.utils import check_package_version from langchain_core.utils import check_package_version
from langchain_core.utils._merge import merge_dicts
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -28,3 +31,74 @@ def test_check_package_version(
else: else:
with pytest.raises(expected[0], match=expected[1]): with pytest.raises(expected[0], match=expected[1]):
check_package_version(package, **check_kwargs) check_package_version(package, **check_kwargs)
@pytest.mark.parametrize(
("left", "right", "expected"),
(
# Merge `None` and `1`.
({"a": None}, {"a": 1}, {"a": 1}),
# Merge `1` and `None`.
({"a": 1}, {"a": None}, {"a": 1}),
# Merge `None` and a value.
({"a": None}, {"a": 0}, {"a": 0}),
({"a": None}, {"a": "txt"}, {"a": "txt"}),
# Merge equal values.
({"a": 1}, {"a": 1}, {"a": 1}),
({"a": 1.5}, {"a": 1.5}, {"a": 1.5}),
({"a": True}, {"a": True}, {"a": True}),
({"a": False}, {"a": False}, {"a": False}),
({"a": "txt"}, {"a": "txt"}, {"a": "txt"}),
({"a": [1, 2]}, {"a": [1, 2]}, {"a": [1, 2]}),
({"a": {"b": "txt"}}, {"a": {"b": "txt"}}, {"a": {"b": "txt"}}),
# Merge strings.
({"a": "one"}, {"a": "two"}, {"a": "onetwo"}),
# Merge dicts.
({"a": {"b": 1}}, {"a": {"c": 2}}, {"a": {"b": 1, "c": 2}}),
(
{"function_call": {"arguments": None}},
{"function_call": {"arguments": "{\n"}},
{"function_call": {"arguments": "{\n"}},
),
# Merge lists.
({"a": [1, 2]}, {"a": [3]}, {"a": [1, 2, 3]}),
({"a": 1, "b": 2}, {"a": 1}, {"a": 1, "b": 2}),
({"a": 1, "b": 2}, {"c": None}, {"a": 1, "b": 2, "c": None}),
#
# Invalid inputs.
#
(
{"a": 1},
{"a": "1"},
pytest.raises(
TypeError,
match=re.escape(
'additional_kwargs["a"] already exists in this message, '
"but with a different type."
),
),
),
(
{"a": (1, 2)},
{"a": (3,)},
pytest.raises(
TypeError,
match=(
"Additional kwargs key a already exists in left dict and value "
"has unsupported type .+tuple.+."
),
),
),
),
)
def test_merge_dicts(
left: dict, right: dict, expected: Union[dict, AbstractContextManager]
) -> None:
if isinstance(expected, AbstractContextManager):
err = expected
else:
err = nullcontext()
with err:
actual = merge_dicts(left, right)
assert actual == expected