From 5f5ba1ca72f14f30d0f583dc1e9e3bbaf3309944 Mon Sep 17 00:00:00 2001 From: Gianluca Ficarelli Date: Thu, 11 Apr 2024 17:44:58 +0200 Subject: [PATCH] In blueetl_core.utils.is_subfilter(), add the strict parameter --- CHANGELOG.rst | 8 +++++++ src/blueetl_core/utils.py | 25 ++++++++++++++++++---- tests/test_utils.py | 45 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 73 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index dee1b9f..eee3150 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,14 @@ Changelog ========= +Version 0.2.3 +------------- + +Improvements +~~~~~~~~~~~~ + +- In ``blueetl_core.utils.is_subfilter()``, add the ``strict`` parameter. + Version 0.2.2 ------------- diff --git a/src/blueetl_core/utils.py b/src/blueetl_core/utils.py index 5c848bc..904c561 100644 --- a/src/blueetl_core/utils.py +++ b/src/blueetl_core/utils.py @@ -132,20 +132,32 @@ def compare(obj: Union[pd.Series, pd.Index], value: Any) -> np.ndarray: return np.asarray(obj == value) -def is_subfilter(left: dict, right: dict) -> bool: +def is_subfilter(left: dict, right: dict, strict: bool = False) -> bool: """Return True if ``left`` is a subfilter of ``right``, False otherwise. - ``left`` is a subfilter of ``right`` if it's equal or more specific. + Args: + left: left filter dict. + right: right filter dict. + strict: if False, ``left`` is a subfilter of ``right`` if it's equal or more specific; + if True, ``left`` is a subfilter of ``right`` only if it's more specific. Examples: >>> print(is_subfilter({}, {})) True + >>> print(is_subfilter({}, {}, strict=True)) + False >>> print(is_subfilter({}, {"key": 1})) False >>> print(is_subfilter({"key": 1}, {})) True >>> print(is_subfilter({"key": 1}, {"key": 1})) True + >>> print(is_subfilter({"key": 1}, {"key": 1}, strict=True)) + False + >>> print(is_subfilter({"key": 1}, {"key": [1]})) + True + >>> print(is_subfilter({"key": 1}, {"key": [1]}, strict=True)) + False >>> print(is_subfilter({"key": 1}, {"key": [1, 2]})) True >>> print(is_subfilter({"key": 1}, {"key": {"isin": [1, 2]}})) @@ -178,7 +190,7 @@ def _to_dict(obj) -> dict: return {"isin": [obj]} def _is_subdict(d1: dict, d2: dict) -> bool: - """Return True if d1 is a subdict of d2.""" + """Return True if d1 is a subdict of d2, or d1 and d2 are equal.""" # mapping operator -> operation operators = { "ne": operator.eq, @@ -201,14 +213,19 @@ def _is_subdict(d1: dict, d2: dict) -> bool: L.debug("unmatched keys: %s", sorted(unmatched_keys)) return len(unmatched_keys) == 0 + # keys present in left, but missing or different in right + difference = set(left) for key in right: if key not in left: return False dict_left = _to_dict(left[key]) dict_right = _to_dict(right[key]) + if strict and dict_left == dict_right: + difference.remove(key) + continue if not _is_subdict(dict_left, dict_right): return False - return True + return not strict or len(difference) > 0 def smart_concat(iterable, *, keys=None, copy=False, skip_empty=True, **kwargs): diff --git a/tests/test_utils.py b/tests/test_utils.py index f04e9a6..68ac8c7 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -125,6 +125,7 @@ def test_compare_empty_filter(): ({}, {"key": 1}, False), ({"key": 1}, {}, True), ({"key": 1}, {"key": 1}, True), + ({"key": 1}, {"key": [1]}, True), ({"key": 1}, {"key": [1, 2]}, True), ({"key": 1}, {"key": {"isin": [1, 2]}}, True), ({"key": 1}, {"key": 2}, False), @@ -155,10 +156,52 @@ def test_compare_empty_filter(): ], ) def test_is_subfilter(left, right, expected): - result = test_module.is_subfilter(left, right) + result = test_module.is_subfilter(left, right, strict=False) assert result == expected +@pytest.mark.parametrize( + "left, right, expected", + [ + ({}, {}, False), + ({}, {"key": 1}, False), + ({"key": 1}, {}, True), + ({"key": 1}, {"key": 1}, False), + ({"key": 1}, {"key": [1]}, False), + ({"key": 1}, {"key": [1, 2]}, True), + ({"key": 1}, {"key": {"isin": [1, 2]}}, True), + ({"key": 1}, {"key": 2}, False), + ({"key": 1}, {"key": [2, 3]}, False), + ({"key": 1}, {"key": {"isin": [2, 3]}}, False), + ({"key1": 1, "key2": 2}, {"key1": 1}, True), + ({"key1": 1}, {"key1": 1, "key2": 2}, False), + ({"key": {"isin": [1, 2]}}, {"key": 1}, False), + ({"key": {"ne": 3}}, {"key": {"ne": 3}}, False), + ({"key": {"ne": 3}}, {"key": {"ne": 4}}, False), + ({"key": {"gt": 3}}, {"key": {"gt": 2}}, True), + ({"key": {"gt": 3}}, {"key": {"gt": 3}}, False), + ({"key": {"gt": 3}}, {"key": {"gt": 4}}, False), + ({"key": {"ge": 3}}, {"key": {"ge": 2}}, True), + ({"key": {"ge": 3}}, {"key": {"ge": 3}}, False), + ({"key": {"ge": 3}}, {"key": {"ge": 4}}, False), + ({"key": {"lt": 3}}, {"key": {"lt": 2}}, False), + ({"key": {"lt": 3}}, {"key": {"lt": 3}}, False), + ({"key": {"lt": 3}}, {"key": {"lt": 4}}, True), + ({"key": {"le": 3}}, {"key": {"le": 2}}, False), + ({"key": {"le": 3}}, {"key": {"le": 3}}, False), + ({"key": {"le": 3}}, {"key": {"le": 4}}, True), + ({"key": {"le": 3, "ge": 1}}, {"key": {"le": 4}}, True), + ({"key": {"le": 3, "ge": 1}}, {"key": {"le": 4, "ge": 0}}, True), + ({"key": 1}, {"key": {"eq": 1}}, False), + ({"key": 1}, {"key": {"eq": 1, "isin": [1, 2]}}, False), + ({"key": 1}, {"key": {"eq": 1, "isin": [2, 3]}}, False), + ], +) +def test_is_subfilter_strict(left, right, expected): + result = test_module.is_subfilter(left, right, strict=True) + assert result is expected + + def test_smart_concat_series(series1): obj1 = series1.copy() + 1 obj2 = series1.copy() + 2