diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 1d7584c1..4bbcd755 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] architecture: ["x64"] steps: - uses: actions/checkout@v2 @@ -44,34 +44,34 @@ jobs: ${{ runner.os }}-pip- ${{ runner.os }}- - name: Upgrade setuptools - if: matrix.python-version == 3.12 + if: matrix.python-version => 3.12 run: | - # workaround for 3.12, SEE: https://github.com/pypa/setuptools/issues/3661#issuecomment-1813845177 + # workaround for 3.13, SEE: https://github.com/pypa/setuptools/issues/3661#issuecomment-1813845177 pip install --upgrade setuptools - name: Install dependencies - if: matrix.python-version != 3.8 + if: matrix.python-version > 3.9 run: pip install -r requirements-dev.txt - name: Install dependencies - if: matrix.python-version == 3.8 + if: matrix.python-version <= 3.9 run: pip install -r requirements-dev3.8.txt - name: Lint with flake8 - if: matrix.python-version == 3.12 + if: matrix.python-version == 3.13 run: | # stop the build if there are Python syntax errors or undefined names flake8 deepdiff --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 deepdiff --count --exit-zero --max-complexity=26 --max-line-lengt=250 --statistics - name: Test with pytest and get the coverage - if: matrix.python-version == 3.12 + if: matrix.python-version == 3.13 run: | pytest --benchmark-disable --cov-report=xml --cov=deepdiff tests/ --runslow - name: Test with pytest and no coverage report - if: matrix.python-version != 3.12 + if: matrix.python-version != 3.13 run: | pytest --benchmark-disable - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 - if: matrix.python-version == 3.12 + if: matrix.python-version == 3.13 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} with: diff --git a/AUTHORS.md b/AUTHORS.md index cd3db130..1f8fe5c9 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -63,3 +63,12 @@ Authors in order of the timeline of their contributions: - [sf-tcalhoun](https://github.com/sf-tcalhoun) for fixing "Instantiating a Delta with a flat_dict_list unexpectedly mutates the flat_dict_list" - [dtorres-sf](https://github.com/dtorres-sf) for fixing iterable moved items when iterable_compare_func is used. - [Florian Finkernagel](https://github.com/TyberiusPrime) for pandas and polars support. +- Mathis Chenuet [artemisart](https://github.com/artemisart) for fixing slots classes comparison and PR review. +- Sherjeel Shabih [sherjeelshabih](https://github.com/sherjeelshabih) for fixing the issue where the key deep_distance is not returned when both compared items are equal #510 +- [Aaron D. Marasco](https://github.com/AaronDMarasco) for adding `prefix` option to `pretty()` +- [Juergen Skrotzky](https://github.com/Jorgen-VikingGod) for adding empty `py.typed` +- [Mate Valko](https://github.com/vmatt) for fixing the issue so we lower only if clean_key is instance of str via #504 +- [jlaba](https://github.com/jlaba) for fixing #493 include_paths, when only certain keys are included via #499 +- [Doron Behar](https://github.com/doronbehar) for fixing DeepHash for numpy booleans via #496 +- [Aaron D. Marasco](https://github.com/AaronDMarasco) for adding print() options which allows a user-defined string (or callback function) to prefix every output when using the pretty() call. +- [David Hotham](https://github.com/dimbleby) for relaxing orderly-set dependency via #486 diff --git a/CHANGELOG.md b/CHANGELOG.md index 95cd2c74..9273ca59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,25 @@ # DeepDiff Change log +- v8-1-0 + - Removing deprecated lines from setup.py + - Added `prefix` option to `pretty()` + - Fixes hashing of numpy boolean values. + - Fixes __slots__ comparison when the attribute doesn't exist. + - Relaxing orderly-set reqs + - Added Python 3.13 support + - Only lower if clean_key is instance of str + - Only lower if clean_key is instance of str #504 + - Fixes issue where the key deep_distance is not returned when both compared items are equal + - Fixes issue where the key deep_distance is not returned when both compared items are equal #510 + - Fixes exclude_paths fails to work in certain cases + - exclude_paths fails to work #509 + - Fixes to_json() method chokes on standard json.dumps() kwargs such as sort_keys + - to_dict() method chokes on standard json.dumps() kwargs #490 + - Fixes accessing the affected_root_keys property on the diff object returned by DeepDiff fails when one of the dicts is empty + - In version 8.0.1, accessing the affected_root_keys property on the diff object returned by DeepDiff fails when one of the dicts is empty #508 + + - v8-0-1 - Bugfix. Numpy should be optional. diff --git a/README.md b/README.md index 22d86dc2..5636f17e 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,25 @@ Tested on Python 3.8+ and PyPy3. Please check the [ChangeLog](CHANGELOG.md) file for the detailed information. +DeepDiff 8-1-0 + +- Removing deprecated lines from setup.py +- Added `prefix` option to `pretty()` +- Fixes hashing of numpy boolean values. +- Fixes __slots__ comparison when the attribute doesn't exist. +- Relaxing orderly-set reqs +- Added Python 3.13 support +- Only lower if clean_key is instance of str +- Only lower if clean_key is instance of str #504 +- Fixes issue where the key deep_distance is not returned when both compared items are equal +- Fixes issue where the key deep_distance is not returned when both compared items are equal #510 +- Fixes exclude_paths fails to work in certain cases +- exclude_paths fails to work #509 +- Fixes to_json() method chokes on standard json.dumps() kwargs such as sort_keys +- to_dict() method chokes on standard json.dumps() kwargs #490 +- Fixes accessing the affected_root_keys property on the diff object returned by DeepDiff fails when one of the dicts is empty +- In version 8.0.1, accessing the affected_root_keys property on the diff object returned by DeepDiff fails when one of the dicts is empty #508 + DeepDiff 8-0-1 - Bugfix. Numpy should be optional. diff --git a/deepdiff/deephash.py b/deepdiff/deephash.py index 32fee9c3..1f293bd4 100644 --- a/deepdiff/deephash.py +++ b/deepdiff/deephash.py @@ -12,7 +12,7 @@ convert_item_or_items_into_compiled_regexes_else_none, get_id, type_is_subclass_of_type_group, type_in_type_group, number_to_string, datetime_normalize, KEY_TO_VAL_STR, short_repr, - get_truncate_datetime, dict_, add_root_to_paths) + get_truncate_datetime, dict_, add_root_to_paths, PydanticBaseModel) from deepdiff.base import Base try: @@ -24,6 +24,11 @@ import polars except ImportError: polars = False +try: + import numpy as np + booleanTypes = (bool, np.bool_) +except ImportError: + booleanTypes = bool logger = logging.getLogger(__name__) @@ -326,13 +331,15 @@ def values(self): def items(self): return ((i, v[0]) for i, v in self.hashes.items()) - def _prep_obj(self, obj, parent, parents_ids=EMPTY_FROZENSET, is_namedtuple=False): + def _prep_obj(self, obj, parent, parents_ids=EMPTY_FROZENSET, is_namedtuple=False, is_pydantic_object=False): """prepping objects""" original_type = type(obj) if not isinstance(obj, type) else obj obj_to_dict_strategies = [] if is_namedtuple: obj_to_dict_strategies.append(lambda o: o._asdict()) + elif is_pydantic_object: + obj_to_dict_strategies.append(lambda o: {k: v for (k, v) in o.__dict__.items() if v !="model_fields_set"}) else: obj_to_dict_strategies.append(lambda o: o.__dict__) @@ -492,7 +499,7 @@ def _hash(self, obj, parent, parents_ids=EMPTY_FROZENSET): """The main hash method""" counts = 1 - if isinstance(obj, bool): + if isinstance(obj, booleanTypes): obj = self._prep_bool(obj) result = None elif self.use_enum_value and isinstance(obj, Enum): @@ -557,6 +564,8 @@ def gen(): elif obj == BoolObj.TRUE or obj == BoolObj.FALSE: result = 'bool:true' if obj is BoolObj.TRUE else 'bool:false' + elif isinstance(obj, PydanticBaseModel): + result, counts = self._prep_obj(obj=obj, parent=parent, parents_ids=parents_ids, is_pydantic_object=True) else: result, counts = self._prep_obj(obj=obj, parent=parent, parents_ids=parents_ids) diff --git a/deepdiff/diff.py b/deepdiff/diff.py index 4dfec50c..a6fe06ba 100755 --- a/deepdiff/diff.py +++ b/deepdiff/diff.py @@ -80,6 +80,9 @@ def _report_progress(_stats, progress_logger, duration): PURGE_LEVEL_RANGE_MSG = 'cache_purge_level should be 0, 1, or 2.' _ENABLE_CACHE_EVERY_X_DIFF = '_ENABLE_CACHE_EVERY_X_DIFF' +model_fields_set = frozenset(["model_fields_set"]) + + # What is the threshold to consider 2 items to be pairs. Only used when ignore_order = True. CUTOFF_DISTANCE_FOR_PAIRS_DEFAULT = 0.3 @@ -421,7 +424,7 @@ def unmangle(attribute): else: all_slots.extend(slots) - return {i: getattr(object, unmangle(i)) for i in all_slots} + return {i: getattr(object, key) for i in all_slots if hasattr(object, key := unmangle(i))} def _diff_enum(self, level, parents_ids=frozenset(), local_tree=None): t1 = detailed__dict__(level.t1, include_keys=ENUM_INCLUDE_KEYS) @@ -437,13 +440,16 @@ def _diff_enum(self, level, parents_ids=frozenset(), local_tree=None): local_tree=local_tree, ) - def _diff_obj(self, level, parents_ids=frozenset(), is_namedtuple=False, local_tree=None): + def _diff_obj(self, level, parents_ids=frozenset(), is_namedtuple=False, local_tree=None, is_pydantic_object=False): """Difference of 2 objects""" processing_error = False try: if is_namedtuple: t1 = level.t1._asdict() t2 = level.t2._asdict() + elif is_pydantic_object: + t1 = detailed__dict__(level.t1, ignore_private_variables=self.ignore_private_variables, ignore_keys=model_fields_set) + t2 = detailed__dict__(level.t2, ignore_private_variables=self.ignore_private_variables, ignore_keys=model_fields_set) elif all('__dict__' in dir(t) for t in level): t1 = detailed__dict__(level.t1, ignore_private_variables=self.ignore_private_variables) t2 = detailed__dict__(level.t2, ignore_private_variables=self.ignore_private_variables) @@ -510,6 +516,32 @@ def _skip_this(self, level): return skip + def _skip_this_key(self, level, key): + # if include_paths is not set, than treet every path as included + if self.include_paths is None: + return False + if "{}['{}']".format(level.path(), key) in self.include_paths: + return False + if level.path() in self.include_paths: + # matches e.g. level+key root['foo']['bar']['veg'] include_paths ["root['foo']['bar']"] + return False + for prefix in self.include_paths: + if "{}['{}']".format(level.path(), key) in prefix: + # matches as long the prefix is longer than this object key + # eg.: level+key root['foo']['bar'] matches prefix root['foo']['bar'] from include paths + # level+key root['foo'] matches prefix root['foo']['bar'] from include_paths + # level+key root['foo']['bar'] DOES NOT match root['foo'] from include_paths This needs to be handled afterwards + return False + # check if a higher level is included as a whole (=without any sublevels specified) + # matches e.g. level+key root['foo']['bar']['veg'] include_paths ["root['foo']"] + # but does not match, if it is level+key root['foo']['bar']['veg'] include_paths ["root['foo']['bar']['fruits']"] + up = level.up + while up is not None: + if up.path() in self.include_paths: + return False + up = up.up + return True + def _get_clean_to_keys_mapping(self, keys, level): """ Get a dictionary of cleaned value of keys to the keys themselves. @@ -530,7 +562,7 @@ def _get_clean_to_keys_mapping(self, keys, level): clean_key = KEY_TO_VAL_STR.format(type_, clean_key) else: clean_key = key - if self.ignore_string_case: + if self.ignore_string_case and isinstance(clean_key, str): clean_key = clean_key.lower() if clean_key in result: logger.warning(('{} and {} in {} become the same key when ignore_numeric_type_changes' @@ -570,11 +602,11 @@ def _diff_dict( rel_class = DictRelationship if self.ignore_private_variables: - t1_keys = SetOrdered([key for key in t1 if not(isinstance(key, str) and key.startswith('__'))]) - t2_keys = SetOrdered([key for key in t2 if not(isinstance(key, str) and key.startswith('__'))]) + t1_keys = SetOrdered([key for key in t1 if not(isinstance(key, str) and key.startswith('__')) and not self._skip_this_key(level, key)]) + t2_keys = SetOrdered([key for key in t2 if not(isinstance(key, str) and key.startswith('__')) and not self._skip_this_key(level, key)]) else: - t1_keys = SetOrdered(t1.keys()) - t2_keys = SetOrdered(t2.keys()) + t1_keys = SetOrdered([key for key in t1 if not self._skip_this_key(level, key)]) + t2_keys = SetOrdered([key for key in t2 if not self._skip_this_key(level, key)]) if self.ignore_string_type_changes or self.ignore_numeric_type_changes or self.ignore_string_case: t1_clean_to_keys = self._get_clean_to_keys_mapping(keys=t1_keys, level=level) t2_clean_to_keys = self._get_clean_to_keys_mapping(keys=t2_keys, level=level) @@ -584,11 +616,17 @@ def _diff_dict( t1_clean_to_keys = t2_clean_to_keys = None t_keys_intersect = t2_keys & t1_keys - t_keys_union = t2_keys | t1_keys t_keys_added = t2_keys - t_keys_intersect t_keys_removed = t1_keys - t_keys_intersect + if self.threshold_to_diff_deeper: - if len(t_keys_union) > 1 and len(t_keys_intersect) / len(t_keys_union) < self.threshold_to_diff_deeper: + if self.exclude_paths: + t_keys_union = {f"{level.path()}[{repr(key)}]" for key in (t2_keys | t1_keys)} + t_keys_union -= self.exclude_paths + t_keys_union_len = len(t_keys_union) + else: + t_keys_union_len = len(t2_keys | t1_keys) + if t_keys_union_len > 1 and len(t_keys_intersect) / t_keys_union_len < self.threshold_to_diff_deeper: self._report_result('values_changed', level, local_tree=local_tree) return @@ -1652,7 +1690,7 @@ def _diff(self, level, parents_ids=frozenset(), _original_type=None, local_tree= self._diff_numpy_array(level, parents_ids, local_tree=local_tree) elif isinstance(level.t1, PydanticBaseModel): - self._diff_obj(level, parents_ids, local_tree=local_tree) + self._diff_obj(level, parents_ids, local_tree=local_tree, is_pydantic_object=True) elif isinstance(level.t1, Iterable): self._diff_iterable(level, parents_ids, _original_type=_original_type, local_tree=local_tree) @@ -1808,9 +1846,13 @@ def affected_root_keys(self): value = self.tree.get(key) if value: if isinstance(value, SetOrdered): - result |= SetOrdered([i.get_root_key() for i in value]) + values_list = value else: - result |= SetOrdered([i.get_root_key() for i in value.keys()]) + values_list = value.keys() + for item in values_list: + root_key = item.get_root_key() + if root_key is not notpresent: + result.add(root_key) return result diff --git a/deepdiff/model.py b/deepdiff/model.py index 2373195a..f5e5a4d3 100644 --- a/deepdiff/model.py +++ b/deepdiff/model.py @@ -41,7 +41,7 @@ def remove_empty_keys(self): Remove empty keys from this object. Should always be called after the result is final. :return: """ - empty_keys = [k for k, v in self.items() if not v] + empty_keys = [k for k, v in self.items() if not isinstance(v, (int)) and not v] for k in empty_keys: del self[k] @@ -88,7 +88,13 @@ def __getitem__(self, item): return self.get(item) def __len__(self): - return sum([len(i) for i in self.values() if isinstance(i, SetOrdered)]) + length = 0 + for value in self.values(): + if isinstance(value, SetOrdered): + length += len(value) + elif isinstance(value, int): + length += 1 + return length class TextResult(ResultDict): @@ -659,7 +665,9 @@ def get_root_key(self, use_t2=False): else: next_rel = root_level.t1_child_rel or root_level.t2_child_rel # next relationship object to get a formatted param from - return next_rel.param + if next_rel: + return next_rel.param + return notpresent def path(self, root="root", force=None, get_parent_too=False, use_t2=False, output_format='str'): """ diff --git a/deepdiff/py.typed b/deepdiff/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/deepdiff/serialization.py b/deepdiff/serialization.py index 5b4075e2..41197425 100644 --- a/deepdiff/serialization.py +++ b/deepdiff/serialization.py @@ -44,6 +44,7 @@ from copy import deepcopy, copy from functools import partial from collections.abc import Mapping +from typing import Callable, Optional, Union from deepdiff.helper import ( strings, get_type, @@ -179,7 +180,7 @@ def from_json_pickle(cls, value): else: logger.error('jsonpickle library needs to be installed in order to run from_json_pickle') # pragma: no cover. Json pickle is getting deprecated. - def to_json(self, default_mapping=None, **kwargs): + def to_json(self, default_mapping: Optional[dict]=None, force_use_builtin_json=False, **kwargs): """ Dump json of the text view. **Parameters** @@ -190,6 +191,11 @@ def to_json(self, default_mapping=None, **kwargs): If you have a certain object type that the json serializer can not serialize it, please pass the appropriate type conversion through this dictionary. + force_use_builtin_json: Boolean, default = False + When True, we use Python's builtin Json library for serialization, + even if Orjson is installed. + + kwargs: Any other kwargs you pass will be passed on to Python's json.dumps() **Example** @@ -212,7 +218,12 @@ def to_json(self, default_mapping=None, **kwargs): '{"type_changes": {"root": {"old_type": "A", "new_type": "B", "old_value": "obj A", "new_value": "obj B"}}}' """ dic = self.to_dict(view_override=TEXT_VIEW) - return json_dumps(dic, default_mapping=default_mapping, **kwargs) + return json_dumps( + dic, + default_mapping=default_mapping, + force_use_builtin_json=force_use_builtin_json, + **kwargs, + ) def to_dict(self, view_override=None): """ @@ -296,11 +307,13 @@ def _to_delta_dict(self, directed=True, report_repetition_required=True, always_ return deepcopy(dict(result)) - def pretty(self): + def pretty(self, prefix: Optional[Union[str, Callable]]=None): """ The pretty human readable string output for the diff object regardless of what view was used to generate the diff. + prefix can be a callable or a string or None. + Example: >>> t1={1,2,4} >>> t2={2,3} @@ -310,12 +323,16 @@ def pretty(self): Item root[1] removed from set. """ result = [] + if prefix is None: + prefix = '' keys = sorted(self.tree.keys()) # sorting keys to guarantee constant order across python versions. for key in keys: for item_key in self.tree[key]: result += [pretty_print_diff(item_key)] - return '\n'.join(result) + if callable(prefix): + return "\n".join(f"{prefix(diff=self)}{r}" for r in result) + return "\n".join(f"{prefix}{r}" for r in result) class _RestrictedUnpickler(pickle.Unpickler): @@ -633,14 +650,26 @@ def object_hook(self, obj): return obj -def json_dumps(item, default_mapping=None, **kwargs): +def json_dumps(item, default_mapping=None, force_use_builtin_json: bool=False, **kwargs): """ Dump json with extra details that are not normally json serializable + + parameters + ---------- + + force_use_builtin_json: Boolean, default = False + When True, we use Python's builtin Json library for serialization, + even if Orjson is installed. """ - if orjson: + if orjson and not force_use_builtin_json: indent = kwargs.pop('indent', None) if indent: kwargs['option'] = orjson.OPT_INDENT_2 + if 'sort_keys' in kwargs: + raise TypeError( + "orjson does not accept the sort_keys parameter. " + "If you need to pass sort_keys, set force_use_builtin_json=True " + "to use Python's built-in json library instead of orjson.") return orjson.dumps( item, default=json_convertor_default(default_mapping=default_mapping), diff --git a/docs/authors.rst b/docs/authors.rst index 1ca60aea..1226d62f 100644 --- a/docs/authors.rst +++ b/docs/authors.rst @@ -93,6 +93,24 @@ Authors in order of the timeline of their contributions: moved items when iterable_compare_func is used. - `Florian Finkernagel `__ for pandas and polars support. +- Mathis Chenuet `artemisart `__ for + fixing slots classes comparison and PR review. +- Sherjeel Shabih `sherjeelshabih `__ + for fixing the issue where the key deep_distance is not returned when + both compared items are equal #510 +- `Juergen Skrotzky `__ for adding + empty ``py.typed`` +- `Mate Valko `__ for fixing the issue so we + lower only if clean_key is instance of str via #504 +- `jlaba `__ for fixing #493 include_paths, + when only certain keys are included via #499 +- `Doron Behar `__ for fixing DeepHash + for numpy booleans via #496 +- `Aaron D. Marasco `__ for adding + print() options which allows a user-defined string (or callback + function) to prefix every output when using the pretty() call. +- `David Hotham `__ for relaxing + orderly-set dependency via #486 .. _Sep Dehpour (Seperman): http://www.zepworks.com .. _Victor Hahn Castell: http://hahncastell.de diff --git a/docs/index.rst b/docs/index.rst index dcaafefe..bccdc8db 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -31,6 +31,32 @@ The DeepDiff library includes the following modules: What Is New *********** +DeepDiff v8-1-0 + + - Removing deprecated lines from setup.py + - Added ``prefix`` option to ``pretty()`` + - Fixes hashing of numpy boolean values. + - Fixes **slots** comparison when the attribute doesn’t exist. + - Relaxing orderly-set reqs + - Added Python 3.13 support + - Only lower if clean_key is instance of str + - Only lower if clean_key is instance of str #504 + - Fixes issue where the key deep_distance is not returned when both + compared items are equal + - Fixes issue where the key deep_distance is not returned when both + compared items are equal #510 + - Fixes exclude_paths fails to work in certain cases + - exclude_paths fails to work #509 + - Fixes to_json() method chokes on standard json.dumps() kwargs such + as sort_keys + - to_dict() method chokes on standard json.dumps() kwargs #490 + - Fixes accessing the affected_root_keys property on the diff object + returned by DeepDiff fails when one of the dicts is empty + - In version 8.0.1, accessing the affected_root_keys property on the + diff object returned by DeepDiff fails when one of the dicts is + empty #508 + + DeepDiff 8-0-1 - Bugfix. Numpy should be optional. diff --git a/docs/view.rst b/docs/view.rst index f50fc9f1..6343590f 100644 --- a/docs/view.rst +++ b/docs/view.rst @@ -299,6 +299,29 @@ Use the pretty method for human readable output. This is regardless of what view Item root[4] removed from set. Item root[1] removed from set. +The pretty method has an optional parameter ``prefix`` that allows a prefix string before every output line (*e.g.* for logging): + >>> from deepdiff import DeepDiff + >>> t1={1,2,4} + >>> t2={2,3} + >>> print(DeepDiff(t1, t2).pretty(prefix='Diff: ')) + Diff: Item root[3] added to set. + Diff: Item root[4] removed from set. + Diff: Item root[1] removed from set. + +The ``prefix`` may also be a callable function. This function must accept ``**kwargs``; as of this version, the only parameter is ``diff`` but the signature allows for future expansion. +The ``diff`` given will be the ``DeepDiff`` that ``pretty`` was called on; this allows interesting capabilities such as: + >>> from deepdiff import DeepDiff + >>> t1={1,2,4} + >>> t2={2,3} + >>> def callback(**kwargs): + ... """Helper function using a hidden variable on the diff that tracks which count prints next""" + ... kwargs['diff']._diff_count = 1 + getattr(kwargs['diff'], '_diff_count', 0) + ... return f"Diff #{kwargs['diff']._diff_count}: " + ... + >>> print(DeepDiff(t1, t2).pretty(prefix=callback)) + Diff #1: Item root[3] added to set. + Diff #2: Item root[4] removed from set. + Diff #3: Item root[1] removed from set. Text view vs. Tree view vs. vs. pretty() method diff --git a/requirements-cli.txt b/requirements-cli.txt index 0ba0c7e6..5f1275e8 100644 --- a/requirements-cli.txt +++ b/requirements-cli.txt @@ -1,2 +1,2 @@ click==8.1.7 -pyyaml==6.0.1 +pyyaml==6.0.2 diff --git a/requirements-dev.txt b/requirements-dev.txt index 5241e2bf..fce48a55 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,23 +1,23 @@ -r requirements.txt -r requirements-cli.txt bump2version==1.0.1 -jsonpickle==3.2.1 -coverage==7.5.3 +jsonpickle==4.0.0 +coverage==7.6.9 ipdb==0.13.13 -numpy==2.0.0 -pytest==8.2.2 -pytest-cov==5.0.0 +numpy==2.1.3 +pytest==8.3.4 +pytest-cov==6.0.0 python-dotenv==1.0.1 -Sphinx==6.2.1 # We use the html style that is not supported in Sphinx 7 anymore. +Sphinx==6.2.1 # We use the html style that is not supported in Sphinx 7 anymore. sphinx-sitemap==2.6.0 -sphinxemoji==0.2.0 -flake8==7.1.0 +sphinxemoji==0.3.1 +flake8==7.1.1 python-dateutil==2.9.0.post0 -orjson==3.10.5 -wheel==0.43.0 -tomli==2.0.1 -tomli-w==1.0.0 -pydantic==2.7.4 -pytest-benchmark==4.0.0 -pandas==2.2.2 -polars==1.0.0 +orjson==3.10.12 +wheel==0.45.1 +tomli==2.2.1 +tomli-w==1.1.0 +pydantic==2.10.3 +pytest-benchmark==5.1.0 +pandas==2.2.3 +polars==1.16.0 diff --git a/requirements-dev3.8.txt b/requirements-dev3.8.txt index 532e1413..b39b7fe4 100644 --- a/requirements-dev3.8.txt +++ b/requirements-dev3.8.txt @@ -14,7 +14,7 @@ sphinx-sitemap==2.6.0 sphinxemoji==0.2.0 flake8==7.1.0 python-dateutil==2.9.0.post0 -orjson==3.10.5 +orjson==3.10.12 wheel==0.43.0 tomli==2.0.1 tomli-w==1.0.0 diff --git a/requirements.txt b/requirements.txt index 28bbd74e..8270bf8e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -orderly-set==5.2.2 +orderly-set>=5.2.3,<6 diff --git a/setup.py b/setup.py index 7db28b65..5ae81bfb 100755 --- a/setup.py +++ b/setup.py @@ -36,10 +36,9 @@ def get_reqs(filename): author_email='sep@zepworks.com', license='MIT', packages=['deepdiff'], + package_data={"deepdiff": ["py.typed"]}, zip_safe=True, - test_suite="tests", include_package_data=True, - tests_require=['mock'], long_description=long_description, long_description_content_type='text/markdown', install_requires=reqs, diff --git a/tests/test_command.py b/tests/test_command.py index bc97e011..fa671cc8 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -12,7 +12,7 @@ class TestCommands: @pytest.mark.parametrize('name1, name2, expected_in_stdout, expected_exit_code', [ ('t1.json', 't2.json', """dictionary_item_added": [\n "root[0][\'key3\']""", 0), - ('t1_corrupt.json', 't2.json', "Expecting property name enclosed in double quotes", 1), + ('t1_corrupt.json', 't2.json', "Error when loading t1:", 1), ('t1.json', 't2_json.csv', '"old_value": "value2"', 0), ('t2_json.csv', 't1.json', '"old_value": "value3"', 0), ('t1.csv', 't2.csv', '"new_value": "James"', 0), @@ -23,6 +23,7 @@ class TestCommands: def test_diff_command(self, name1, name2, expected_in_stdout, expected_exit_code): t1 = os.path.join(FIXTURES_DIR, name1) t2 = os.path.join(FIXTURES_DIR, name2) + runner = CliRunner() result = runner.invoke(diff, [t1, t2]) assert result.exit_code == expected_exit_code, f"test_diff_command failed for {name1}, {name2}" diff --git a/tests/test_delta.py b/tests/test_delta.py index 81a05784..fe328b6c 100644 --- a/tests/test_delta.py +++ b/tests/test_delta.py @@ -595,6 +595,7 @@ def compare_func(item1, item2, level=None): delta = Delta(flat_rows_list=flat_rows_list, always_include_values=True, bidirectional=True, raise_errors=True) + flat_rows_list_again = delta.to_flat_rows() # if the flat_rows_list is (unexpectedly) mutated, it will be missing the list index number on the path value. old_mutated_list_missing_indexes_on_path = [FlatDeltaRow(path=['individualNames'], value={'firstName': 'Johnny', @@ -620,6 +621,7 @@ def compare_func(item1, item2, level=None): # Verify that our fix in the delta constructor worked... assert flat_rows_list != old_mutated_list_missing_indexes_on_path assert flat_rows_list == preserved_flat_dict_list + assert flat_rows_list == flat_rows_list_again picklalbe_obj_without_item = PicklableClass(11) @@ -874,6 +876,13 @@ def compare_func(item1, item2, level=None): 'to_delta_kwargs': {'directed': True}, 'expected_delta_dict': {'values_changed': {'root["a\'][\'b\'][\'c"]': {'new_value': 2}}} }, + 'delta_case21_empty_list_add': { + 't1': {'car_model': [], 'car_model_version_id': 0}, + 't2': {'car_model': ['Super Duty F-250'], 'car_model_version_id': 1}, + 'deepdiff_kwargs': {}, + 'to_delta_kwargs': {'directed': True}, + 'expected_delta_dict': {'iterable_item_added': {"root['car_model'][0]": 'Super Duty F-250'}, 'values_changed': {"root['car_model_version_id']": {'new_value': 1}}}, + }, } @@ -2469,6 +2478,33 @@ def test_delta_flat_rows(self): delta2 = Delta(flat_rows_list=flat_rows, bidirectional=True, force=True) assert t1 + delta2 == t2 + def test_delta_bool(self): + flat_rows_list = [FlatDeltaRow(path=['dollar_to_cent'], action='values_changed', value=False, old_value=True, type=bool, old_type=bool)] + value = {'dollar_to_cent': False} + delta = Delta(flat_rows_list=flat_rows_list, bidirectional=True, force=True) + assert {'dollar_to_cent': True} == value - delta + + def test_detla_add_to_empty_iterable_and_flatten(self): + t1 = {'models': [], 'version_id': 0} + t2 = {'models': ['Super Duty F-250'], 'version_id': 1} + t3 = {'models': ['Super Duty F-250', 'Focus'], 'version_id': 1} + diff = DeepDiff(t1, t2, verbose_level=2) + delta = Delta(diff, bidirectional=True) + assert t1 + delta == t2 + flat_rows = delta.to_flat_rows() + delta2 = Delta(flat_rows_list=flat_rows, bidirectional=True) # , force=True + assert t1 + delta2 == t2 + assert t2 - delta2 == t1 + + diff3 = DeepDiff(t2, t3) + delta3 = Delta(diff3, bidirectional=True) + flat_dicts3 = delta3.to_flat_dicts() + + delta3_again = Delta(flat_dict_list=flat_dicts3, bidirectional=True) + assert t2 + delta3_again == t3 + assert t3 - delta3_again == t2 + + def test_flat_dict_and_deeply_nested_dict(self): beforeImage = [ { diff --git a/tests/test_diff_include_paths b/tests/test_diff_include_paths deleted file mode 100644 index 9dace5cd..00000000 --- a/tests/test_diff_include_paths +++ /dev/null @@ -1,81 +0,0 @@ -import pytest -from deepdiff import DeepDiff - -t1 = { - "foo": { - "bar": { - "veg": "potato", - "fruit": "apple" - } - }, - "ingredients": [ - { - "lunch": [ - "bread", - "cheese" - ] - }, - { - "dinner": [ - "soup", - "meat" - ] - } - ] -} -t2 = { - "foo": { - "bar": { - "veg": "potato", - "fruit": "peach" - } - }, - "ingredients": [ - { - "lunch": [ - "bread", - "cheese" - ] - }, - { - "dinner": [ - "soup", - "meat" - ] - } - ] -} - - -class TestDeepDiffIncludePaths: - - @staticmethod - def deep_diff(dict1, dict2, include_paths): - diff = DeepDiff(dict1, dict2, include_paths=include_paths) - print(diff) - return diff - - def test_include_paths_root_neg(self): - expected = {'values_changed': {"root['foo']['bar']['fruit']": {'new_value': 'peach', 'old_value': 'apple'}}} - actual = self.deep_diff(t1, t2, 'foo') - assert expected == actual - - def test_include_paths_root_pos(self): - expected = {} - actual = self.deep_diff(t1, t2, 'ingredients') - assert expected == actual - - def test_include_paths_nest00_neg(self): - expected = {'values_changed': {"root['foo']['bar']['fruit']": {'new_value': 'peach', 'old_value': 'apple'}}} - actual = self.deep_diff(t1, t2, "root['foo']['bar']") - assert expected == actual - - def test_include_paths_nest01_neg(self): - expected = {'values_changed': {"root['foo']['bar']['fruit']": {'new_value': 'peach', 'old_value': 'apple'}}} - actual = self.deep_diff(t1, t2, "root['foo']['bar']['fruit']") - assert expected == actual - - def test_include_paths_nest_pos(self): - expected = {} - actual = self.deep_diff(t1, t2, "root['foo']['bar']['veg']") - assert expected == actual diff --git a/tests/test_diff_include_paths.py b/tests/test_diff_include_paths.py new file mode 100644 index 00000000..8e6c2464 --- /dev/null +++ b/tests/test_diff_include_paths.py @@ -0,0 +1,282 @@ +import pytest +from deepdiff import DeepDiff + +t1 = { + "foo": { + "bar": { + "veg": "potato", + "fruit": "apple" + } + }, + "ingredients": [ + { + "lunch": [ + "bread", + "cheese" + ] + }, + { + "dinner": [ + "soup", + "meat" + ] + } + ] +} +t2 = { + "foo": { + "bar": { + "veg": "potato", + "fruit": "peach" + } + }, + "ingredients": [ + { + "lunch": [ + "bread", + "cheese" + ] + }, + { + "dinner": [ + "soup", + "meat" + ] + } + ] +} + + +class TestDeepDiffIncludePaths: + + @staticmethod + def deep_diff(dict1, dict2, include_paths): + diff = DeepDiff(dict1, dict2, include_paths=include_paths) + print(diff) + return diff + + def test_include_paths_root_neg(self): + expected = {'values_changed': {"root['foo']['bar']['fruit']": {'new_value': 'peach', 'old_value': 'apple'}}} + actual = self.deep_diff(t1, t2, 'foo') + assert expected == actual + + def test_include_paths_root_pos(self): + expected = {} + actual = self.deep_diff(t1, t2, 'ingredients') + assert expected == actual + + def test_include_paths_nest00_neg(self): + expected = {'values_changed': {"root['foo']['bar']['fruit']": {'new_value': 'peach', 'old_value': 'apple'}}} + actual = self.deep_diff(t1, t2, "root['foo']['bar']") + assert expected == actual + + def test_include_paths_nest01_neg(self): + expected = {'values_changed': {"root['foo']['bar']['fruit']": {'new_value': 'peach', 'old_value': 'apple'}}} + actual = self.deep_diff(t1, t2, "root['foo']['bar']['fruit']") + assert expected == actual + + def test_include_paths_nest_pos(self): + expected = {} + actual = self.deep_diff(t1, t2, "root['foo']['bar']['veg']") + assert expected == actual + + @pytest.mark.parametrize( + "test_num, data", + [ + ( + 1, # test_num + { + "old": { + 'name': 'Testname Old', + 'desciption': 'Desc Old', + 'sub_path': { + 'name': 'Testname Subpath old', + 'desciption': 'Desc Subpath old', + }, + }, + "new": { + 'name': 'Testname New', + 'desciption': 'Desc New', + 'new_attribute': 'New Value', + 'sub_path': { + 'name': 'Testname Subpath old', + 'desciption': 'Desc Subpath old', + }, + }, + "include_paths": "root['sub_path']", + "expected_result1": {'dictionary_item_added': ["root['new_attribute']"], 'values_changed': {"root['name']": {'new_value': 'Testname New', 'old_value': 'Testname Old'}, "root['desciption']": {'new_value': 'Desc New', 'old_value': 'Desc Old'}}}, + "expected_result2": {}, + }, + ), + ( + 2, # test_num + { + "old": { + 'name': 'Testname Old', + 'desciption': 'Desc Old', + 'sub_path': { + 'name': 'Testname Subpath old', + 'desciption': 'Desc Subpath old', + }, + }, + "new": { + 'name': 'Testname New', + 'desciption': 'Desc New', + 'new_attribute': 'New Value', + 'sub_path': { + 'name': 'Testname Subpath New', + 'desciption': 'Desc Subpath old', + }, + }, + "include_paths": "root['sub_path']", + "expected_result1": {'dictionary_item_added': ["root['new_attribute']"], 'values_changed': {"root['name']": {'new_value': 'Testname New', 'old_value': 'Testname Old'}, "root['desciption']": {'new_value': 'Desc New', 'old_value': 'Desc Old'}, "root['sub_path']['name']": {'new_value': 'Testname Subpath New', 'old_value': 'Testname Subpath old'}}}, + "expected_result2": {"values_changed": {"root['sub_path']['name']": {"old_value": "Testname Subpath old", "new_value": "Testname Subpath New"}}}, + }, + ), + ( + 3, # test_num + { + "old": { + 'name': 'Testname Old', + 'desciption': 'Desc Old', + 'sub_path': { + 'name': 'Testname Subpath old', + 'desciption': 'Desc Subpath old', + 'old_attr': 'old attr value', + }, + }, + "new": { + 'name': 'Testname New', + 'desciption': 'Desc New', + 'new_attribute': 'New Value', + 'sub_path': { + 'name': 'Testname Subpath old', + 'desciption': 'Desc Subpath New', + 'new_sub_path_attr': 'new sub path attr value', + }, + }, + "include_paths": "root['sub_path']['name']", + "expected_result1": {'dictionary_item_added': ["root['new_attribute']", "root['sub_path']['new_sub_path_attr']"], 'dictionary_item_removed': ["root['sub_path']['old_attr']"], 'values_changed': {"root['name']": {'new_value': 'Testname New', 'old_value': 'Testname Old'}, "root['desciption']": {'new_value': 'Desc New', 'old_value': 'Desc Old'}, "root['sub_path']['desciption']": {'new_value': 'Desc Subpath New', 'old_value': 'Desc Subpath old'}}}, + "expected_result2": {}, + }, + ), + ( + 4, # test_num + { + "old": { + 'name': 'Testname old', + 'desciption': 'Desc old', + 'new_attribute': 'old Value', + 'sub_path': { + 'name': 'Testname', + 'removed_attr': 'revemod attr value', + }, + }, + "new": { + 'name': 'Testname new', + 'desciption': 'Desc new', + 'new_attribute': 'new Value', + 'sub_path': { + 'added_attr': 'Added Attr Value', + 'name': 'Testname', + }, + }, + "include_paths": "root['sub_path']['name']", + "expected_result1": {'dictionary_item_added': ["root['sub_path']['added_attr']"], 'dictionary_item_removed': ["root['sub_path']['removed_attr']"], 'values_changed': {"root['name']": {'new_value': 'Testname new', 'old_value': 'Testname old'}, "root['desciption']": {'new_value': 'Desc new', 'old_value': 'Desc old'}, "root['new_attribute']": {'new_value': 'new Value', 'old_value': 'old Value'}}}, + "expected_result2": {}, + }, + ), + ( + 5, # test_num + { + "old": { + 'name': 'Testname', + 'removed_attr': 'revemod attr value', + }, + "new": { + 'added_attr': 'Added Attr Value', + 'name': 'Testname', + }, + "include_paths": "root['name']", + "expected_result1": {'dictionary_item_added': ["root['added_attr']"], 'dictionary_item_removed': ["root['removed_attr']"]}, + "expected_result2": {}, + }, + ), + ( + 6, # test_num + { + "old": { + 'name': 'Testname', + 'removed_attr': 'revemod attr value', + 'removed_attr_2': 'revemod attr value', + }, + "new": { + 'added_attr': 'Added Attr Value', + 'name': 'Testname', + }, + "include_paths": "root['name']", + "expected_result1": {'values_changed': {'root': {'new_value': {'added_attr': 'Added Attr Value', 'name': 'Testname'}, 'old_value': {'name': 'Testname', 'removed_attr': 'revemod attr value', 'removed_attr_2': 'revemod attr value'}}}}, + "expected_result2": {}, + }, + ), + ( + 7, # test_num + { + "old": { + 'name': 'Testname old', + 'desciption': 'Desc old', + 'new_attribute': 'old Value', + 'sub_path': { + 'name': 'Testname', + 'removed_attr': 'revemod attr value', + 'removed_attr_2': 'blu', + }, + }, + "new": { + 'name': 'Testname new', + 'desciption': 'Desc new', + 'new_attribute': 'new Value', + 'sub_path': { + 'added_attr': 'Added Attr Value', + 'name': 'Testname', + }, + }, + "include_paths": "root['sub_path']['name']", + "expected_result1": {'values_changed': {"root['name']": {'new_value': 'Testname new', 'old_value': 'Testname old'}, "root['desciption']": {'new_value': 'Desc new', 'old_value': 'Desc old'}, "root['new_attribute']": {'new_value': 'new Value', 'old_value': 'old Value'}, "root['sub_path']": {'new_value': {'added_attr': 'Added Attr Value', 'name': 'Testname'}, 'old_value': {'name': 'Testname', 'removed_attr': 'revemod attr value', 'removed_attr_2': 'blu'}}}}, + "expected_result2": {}, + }, + ), + ( + 8, # test_num + { + "old": [{ + 'name': 'Testname old', + 'desciption': 'Desc old', + 'new_attribute': 'old Value', + 'sub_path': { + 'name': 'Testname', + 'removed_attr': 'revemod attr value', + 'removed_attr_2': 'blu', + }, + }], + "new": [{ + 'name': 'Testname new', + 'desciption': 'Desc new', + 'new_attribute': 'new Value', + 'sub_path': { + 'added_attr': 'Added Attr Value', + 'name': 'New Testname', + }, + }], + "include_paths": "root[0]['sub_path']['name']", + "expected_result1": {'values_changed': {"root[0]['name']": {'new_value': 'Testname new', 'old_value': 'Testname old'}, "root[0]['desciption']": {'new_value': 'Desc new', 'old_value': 'Desc old'}, "root[0]['new_attribute']": {'new_value': 'new Value', 'old_value': 'old Value'}, "root[0]['sub_path']": {'new_value': {'added_attr': 'Added Attr Value', 'name': 'New Testname'}, 'old_value': {'name': 'Testname', 'removed_attr': 'revemod attr value', 'removed_attr_2': 'blu'}}}}, + "expected_result2": {'values_changed': {"root[0]['sub_path']['name']": {'new_value': 'New Testname', 'old_value': 'Testname'}}}, + }, + ), + ] + ) + def test_diff_include_paths_root(self, test_num, data): + diff1 = DeepDiff(data["old"], data["new"]) + diff2 = DeepDiff(data["old"], data["new"], include_paths=data["include_paths"]) + assert data['expected_result1'] == diff1, f"test_diff_include_paths_root test_num #{test_num} failed." + assert data['expected_result2'] == diff2, f"test_diff_include_paths_root test_num #{test_num} failed." diff --git a/tests/test_diff_text.py b/tests/test_diff_text.py index ec6f66b4..63df30a2 100755 --- a/tests/test_diff_text.py +++ b/tests/test_diff_text.py @@ -807,6 +807,24 @@ class ClassB: result = {'attribute_removed': ['root.y']} assert result == ddiff + def test_custom_objects_slot_in_group_change(self): + class ClassA: + __slots__ = ('x', 'y') + + def __init__(self, x, y): + self.x = x + self.y = y + + class ClassB(ClassA): + pass + + t1 = ClassA(1, 1) + t2 = ClassB(1, 1) + ddiff = DeepDiff(t1, t2, ignore_type_in_groups=[(ClassA, ClassB)]) + result = {} + assert result == ddiff + + def test_custom_class_changes_none_when_ignore_type(self): ddiff1 = DeepDiff({'a': None}, {'a': 1}, ignore_type_subclasses=True, ignore_type_in_groups=[(int, float)]) result = { @@ -1533,6 +1551,10 @@ def test_skip_path2_reverse(self): ddiff = DeepDiff(t2, t1, exclude_paths={"root['ingredients']"}) assert {} == ddiff + def test_exclude_path_when_prefix_of_exclude_path_matches1(self): + diff = DeepDiff({}, {'foo': '', 'bar': ''}, exclude_paths=['foo', 'bar']) + assert not diff + def test_include_path3(self): t1 = { "for life": "vegan", @@ -1570,6 +1592,59 @@ def test_include_path4_nested(self): } } == ddiff + def test_include_path5(self): + diff = DeepDiff( + { + 'name': 'Testname', + 'code': 'bla', + 'noneCode': 'blu', + }, { + 'uid': '12345', + 'name': 'Testname2', + }, + ) + + diff2 = DeepDiff( + { + 'name': 'Testname', + 'code': 'bla', + 'noneCode': 'blu', + }, { + 'uid': '12345', + 'name': 'Testname2', + }, + include_paths = "root['name']" + ) + expected = {'values_changed': {'root': {'new_value': {'uid': '12345', 'name': 'Testname2'}, 'old_value': {'name': 'Testname', 'code': 'bla', 'noneCode': 'blu'}}}} + expected2 = {'values_changed': {"root['name']": {'new_value': 'Testname2', 'old_value': 'Testname'}}} + + assert expected == diff + assert expected2 == diff2 + + def test_include_path6(self): + t1 = [1, 2, 3, [4, 5, {6: 7}]] + t2 = [1, 2, 3, [4, 5, {6: 1000}]] + diff = DeepDiff( + t1, + t2, + ) + + diff2 = DeepDiff( + t1, + t2, + include_paths = "root[3]" + ) + + diff3 = DeepDiff( + t1, + t2, + include_paths = "root[4]" + ) + expected = {'values_changed': {'root[3][2][6]': {'new_value': 1000, 'old_value': 7}}} + assert expected == diff + assert diff == diff2 + assert not diff3 + def test_skip_path4(self): t1 = { "for life": "vegan", @@ -1713,7 +1788,7 @@ def __str__(self): t2 = Bad() ddiff = DeepDiff(t1, t2) - result = {'unprocessed': ['root: Bad Object and Bad Object']} + result = {} assert result == ddiff def test_dict_none_item_removed(self): @@ -2147,3 +2222,32 @@ class MyDataClass: diff = DeepDiff(t1, t2, exclude_regex_paths=["any"]) assert {'values_changed': {'root[MyDataClass(val=2,val2=4)]': {'new_value': 10, 'old_value': 20}}} == diff + + + def test_group_by_with_none_key_and_ignore_case(self): + """Test that group_by works with None keys when ignore_string_case is True""" + dict1 = [{'txt_field': 'FULL_NONE', 'group_id': None}, {'txt_field': 'FULL', 'group_id': 'a'}] + dict2 = [{'txt_field': 'PARTIAL_NONE', 'group_id': None}, {'txt_field': 'PARTIAL', 'group_id': 'a'}] + + diff = DeepDiff( + dict1, + dict2, + ignore_order=True, + group_by='group_id', + ignore_string_case=True + ) + + expected = {'values_changed': {"root[None]['txt_field']": + {'new_value': 'partial_none', 'old_value': 'full_none'}, + "root['a']['txt_field']": + {'new_value': 'partial', 'old_value': 'full'} + } + } + assert expected == diff + + def test_affected_root_keys_when_dict_empty(self): + diff = DeepDiff({}, {1:1, 2:2}, threshold_to_diff_deeper=0) + assert [1, 2] == diff.affected_root_keys + + diff2 = DeepDiff({}, {1:1, 2:2}) + assert [] == diff2.affected_root_keys diff --git a/tests/test_hash.py b/tests/test_hash.py index 52637577..22a86e24 100755 --- a/tests/test_hash.py +++ b/tests/test_hash.py @@ -187,6 +187,12 @@ def test_re(self): a_hash = DeepHash(a)[a] assert not( a_hash is unprocessed) + # https://github.com/seperman/deepdiff/issues/494 + def test_numpy_bool(self): + a = {'b': np.array([True], dtype='bool')} + a_hash = DeepHash(a)[a] + assert not( a_hash is unprocessed) + class TestDeepHashPrep: """DeepHashPrep Tests covering object serialization.""" diff --git a/tests/test_serialization.py b/tests/test_serialization.py index facda246..3c506834 100644 --- a/tests/test_serialization.py +++ b/tests/test_serialization.py @@ -47,6 +47,14 @@ def test_serialization_text(self): jsoned = ddiff.to_json() assert "world" in jsoned + def test_serialization_text_force_builtin_json(self): + ddiff = DeepDiff(t1, t2) + with pytest.raises(TypeError) as excinfo: + jsoned = ddiff.to_json(sort_keys=True) + assert str(excinfo.value).startswith("orjson does not accept the sort_keys parameter.") + jsoned = ddiff.to_json(sort_keys=True, force_use_builtin_json=True) + assert "world" in jsoned + def test_deserialization(self): ddiff = DeepDiff(t1, t2) jsoned = ddiff.to_json_pickle() @@ -330,6 +338,49 @@ def test_pretty_form_method(self, expected, verbose_level): result = ddiff.pretty() assert result == expected + @pytest.mark.parametrize("expected, verbose_level", + ( + ('\t\tItem root[5] added to dictionary.' + '\n\t\tItem root[3] removed from dictionary.' + '\n\t\tType of root[2] changed from int to str and value changed from 2 to "b".' + '\n\t\tValue of root[4] changed from 4 to 5.', 0), + ('\t\tItem root[5] (5) added to dictionary.' + '\n\t\tItem root[3] (3) removed from dictionary.' + '\n\t\tType of root[2] changed from int to str and value changed from 2 to "b".' + '\n\t\tValue of root[4] changed from 4 to 5.', 2), + ), ids=("verbose=0", "verbose=2") + ) + def test_pretty_form_method_prefixed_simple(self, expected, verbose_level): + t1 = {2: 2, 3: 3, 4: 4} + t2 = {2: 'b', 4: 5, 5: 5} + ddiff = DeepDiff(t1, t2, verbose_level=verbose_level) + result = ddiff.pretty(prefix="\t\t") + assert result == expected + + @pytest.mark.parametrize("expected, verbose_level", + ( + ('Diff #1: Item root[5] added to dictionary.' + '\nDiff #2: Item root[3] removed from dictionary.' + '\nDiff #3: Type of root[2] changed from int to str and value changed from 2 to "b".' + '\nDiff #4: Value of root[4] changed from 4 to 5.', 0), + ('Diff #1: Item root[5] (5) added to dictionary.' + '\nDiff #2: Item root[3] (3) removed from dictionary.' + '\nDiff #3: Type of root[2] changed from int to str and value changed from 2 to "b".' + '\nDiff #4: Value of root[4] changed from 4 to 5.', 2), + ), ids=("verbose=0", "verbose=2") + ) + def test_pretty_form_method_prefixed_callback(self, expected, verbose_level): + def prefix_callback(**kwargs): + """Helper function using a hidden variable on the diff that tracks which count prints next""" + kwargs['diff']._diff_count = 1 + getattr(kwargs['diff'], '_diff_count', 0) + return f"Diff #{kwargs['diff']._diff_count}: " + + t1 = {2: 2, 3: 3, 4: 4} + t2 = {2: 'b', 4: 5, 5: 5} + ddiff = DeepDiff(t1, t2, verbose_level=verbose_level) + result = ddiff.pretty(prefix=prefix_callback) + assert result == expected + @pytest.mark.parametrize('test_num, value, func_to_convert_back', [ (1, {'10': None}, None), (2, {"type_changes": {"root": {"old_type": None, "new_type": list, "new_value": ["你好", 2, 3, 5]}}}, None),