"""
smarter.common.utils.dict
==========================
Module providing dictionary utility functions for the Smarter framework.
This module includes helper functions for:
- Recursively sorting dictionaries by key for deterministic output.
- Checking if all keys and values in one dictionary are present in another (deep containment).
- Recursively verifying if a dictionary or list is a subset of another.
These utilities are useful for testing, serialization, comparison, and data validation tasks.
Functions
---------
- recursive_sort_dict(d): Recursively sorts a dictionary by its keys.
- dict_is_contained_in(dict1, dict2): Checks if all keys and values in dict1 are present in dict2, recursively.
- dict_is_subset(small, big): Recursively checks that all items in the dictionary or list 'small' exist in 'big'.
Example
-------
.. code-block:: python
from smarter.common.utils import recursive_sort_dict, dict_is_contained_in, dict_is_subset
d = {"b": 2, "a": {"d": 4, "c": 3}}
sorted_d = recursive_sort_dict(d)
model = {"name": "Alice", "profile": {"age": 30}}
test = {"name": "Alice", "profile": {"age": 30}, "extra": "value"}
result = dict_is_contained_in(model, test)
big = {"roles": ["admin", "user"]}
small = {"roles": ["admin"]}
result = dict_is_subset(small, big)
"""
from smarter.lib import logging
logger = logging.getLogger(__name__)
logger_prefix = logging.formatted_text(__name__)
[docs]
def recursive_sort_dict(d):
"""
Recursively sorts a dictionary by its keys.
:param d: The input dictionary to be sorted. Nested dictionaries are also sorted recursively.
:type d: dict
:return: A new dictionary with all keys sorted in ascending order. If a value is itself a dictionary, it is also sorted recursively.
:rtype: dict
**Example usage:**
.. code-block:: python
from smarter.common.utils import recursive_sort_dict
data = {
"b": 2,
"a": {
"d": 4,
"c": 3
}
}
sorted_data = recursive_sort_dict(data)
print(sorted_data)
# Output: {'a': {'c': 3, 'd': 4}, 'b': 2}
"""
logger.debug("%s.recursive_sort_dict()", logger_prefix)
return {k: recursive_sort_dict(v) if isinstance(v, dict) else v for k, v in sorted(d.items())}
[docs]
def dict_is_contained_in(dict1, dict2):
"""
Checks whether all keys and values in ``dict1`` are present in ``dict2``, recursively.
:param dict1: The dictionary whose keys and values are to be checked for containment.
:type dict1: dict
:param dict2: The dictionary in which to check for the presence of keys and values from ``dict1``.
:type dict2: dict
:return: Returns ``True`` if every key in ``dict1`` exists in ``dict2`` and the corresponding values match (including nested dictionaries). Returns ``False`` otherwise.
:rtype: bool
**Example usage:**
.. code-block:: python
from smarter.common.utils import dict_is_contained_in
model = {
"name": "Alice",
"profile": {
"age": 30,
"city": "Wonderland"
}
}
test = {
"name": "Alice",
"profile": {
"age": 30,
"city": "Wonderland"
},
"extra": "value"
}
result = dict_is_contained_in(model, test)
print(result) # True
# Example with missing key
test_missing = {
"name": "Alice"
}
result = dict_is_contained_in(model, test_missing)
print(result) # False
"""
logger.debug("%s.dict_is_contained_in()", logger_prefix)
for key, value in dict1.items():
if key not in dict2:
return False
if isinstance(value, dict):
if not dict_is_contained_in(value, dict2[key]):
return False
else:
if dict2[key] != value:
return False
return True
[docs]
def dict_is_subset(small, big) -> bool:
"""
Recursively checks that all items in the dictionary ``small`` exist in the dictionary ``big``.
:param small: The dictionary (or list) whose items should be checked for existence in ``big``.
:type small: dict or list
:param big: The dictionary (or list) in which to check for the presence of items from ``small``.
:type big: dict or list
:return: Returns ``True`` if every item in ``small`` exists in ``big`` (including nested dictionaries and lists). Returns ``False`` otherwise.
:rtype: bool
.. note::
- For dictionaries, all keys and their corresponding values must exist in ``big``.
- For lists, all elements in ``small`` must be present in ``big``; order does not matter.
- Nested dictionaries and lists are checked recursively.
**Example usage:**
.. code-block:: python
from smarter.common.utils import dict_is_subset
big = {
"name": "Alice",
"profile": {
"age": 30,
"city": "Wonderland"
},
"roles": ["admin", "user"]
}
small = {
"profile": {
"age": 30
},
"roles": ["admin"]
}
result = dict_is_subset(small, big)
print(result) # True
# Example with missing value
small_missing = {
"profile": {
"age": 31
}
}
result = dict_is_subset(small_missing, big)
print(result) # False
"""
logger.debug("%s.dict_is_subset()", logger_prefix)
if isinstance(small, dict) and isinstance(big, dict):
for k, v in small.items():
if k not in big:
return False
if not dict_is_subset(v, big[k]):
return False
return True
elif isinstance(small, list) and isinstance(big, list):
# Check that all items in 'small' are in 'big' (order does NOT matter)
for sv in small:
if isinstance(sv, dict):
if not any(dict_is_subset(sv, bv) for bv in big if isinstance(bv, dict)):
return False
else:
if sv not in big:
return False
return True
else:
return small == big
__all__ = [
"dict_is_contained_in",
"dict_is_subset",
"recursive_sort_dict",
]