From 95fd100ed4ddede30c83d7a52efb4b771ee553f9 Mon Sep 17 00:00:00 2001 From: Nehciy Date: Fri, 2 Oct 2026 16:55:21 +0800 Subject: [PATCH] Skip excluded objects before calling custom operators --- deepdiff/diff.py | 10 ++++++--- tests/test_operators.py | 49 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/deepdiff/diff.py b/deepdiff/diff.py index 4c7a4e5c..af7fa86f 100755 --- a/deepdiff/diff.py +++ b/deepdiff/diff.py @@ -2135,13 +2135,17 @@ def _diff(self, level, parents_ids=frozenset(), _original_type=None, local_tree= if self._count_diff() is StopIteration: return - if self._use_custom_operator(level): + # Custom operators may report differences even for identical objects. + if level.t1 is level.t2 and not self.custom_operators: return - if level.t1 is level.t2: + if self._skip_this(level): return - if self._skip_this(level): + if self._use_custom_operator(level): + return + + if level.t1 is level.t2: return report_type_change = True diff --git a/tests/test_operators.py b/tests/test_operators.py index 98444680..acc74761 100644 --- a/tests/test_operators.py +++ b/tests/test_operators.py @@ -8,6 +8,55 @@ class TestOperators: + @pytest.mark.parametrize('exclude_options', [ + {'exclude_paths': ['b']}, + {'exclude_paths': ["root['b']"]}, + {'exclude_regex_paths': [r"\['b'\]$"]}, + {'exclude_obj_callback': lambda obj, path: path == "root['b']"}, + ]) + @pytest.mark.parametrize('prevent_default', [False, True]) + def test_custom_operator_skips_excluded_paths(self, exclude_options, prevent_default): + matched_paths = [] + compared_paths = [] + + class CustomClass: + def __init__(self, values): + self.values = values + + class ListMatchOperator(BaseOperator): + def match(self, level): + matched_paths.append(level.path()) + return super().match(level) + + def give_up_diffing(self, level, diff_instance): + compared_paths.append(level.path()) + if level.t1.values == level.t2.values: + diff_instance.custom_report_result('equal_lists', level, {}) + return prevent_default + + shared = CustomClass([1, 2, 3]) + t1 = {'a': shared, 'b': CustomClass([1, 2, 3])} + t2 = {'a': shared, 'b': CustomClass([1, 2, 3, 4])} + + result = DeepDiff(t1, t2, custom_operators=[ListMatchOperator(types=[CustomClass])], **exclude_options) + + assert "root['b']" not in matched_paths + assert compared_paths == ["root['a']"] + assert result == {'equal_lists': {"root['a']": {}}} + + @pytest.mark.parametrize('exclude_options', [ + {'exclude_paths': ['root']}, + {'exclude_types': [dict]}, + ]) + def test_custom_operator_skips_excluded_root(self, exclude_options): + class UnexpectedOperator(BaseOperator): + def match(self, level): + raise AssertionError('Excluded objects should not reach custom operators') + + result = DeepDiff({'a': 1}, {'a': 2}, custom_operators=[UnexpectedOperator()], **exclude_options) + + assert result == {} + def test_custom_operators_prevent_default(self): t1 = { "coordinates": [