pax_global_header 0000666 0000000 0000000 00000000064 15162412645 0014520 g ustar 00root root 0000000 0000000 52 comment=41c726586cfef55de0c8474923d2734f1529d223
qlustered-deepdiff-41c7265/ 0000775 0000000 0000000 00000000000 15162412645 0015551 5 ustar 00root root 0000000 0000000 qlustered-deepdiff-41c7265/.bumpversion.cfg 0000664 0000000 0000000 00000000451 15162412645 0020661 0 ustar 00root root 0000000 0000000 [bumpversion]
current_version = 9.0.0
commit = False
tag = False
tag_name = {new_version}
[bumpversion:file:pyproject.toml]
[bumpversion:file:README.md]
[bumpversion:file:CITATION.cff]
[bumpversion:file:docs/index.rst]
[bumpversion:file:docs/conf.py]
[bumpversion:file:deepdiff/__init__.py]
qlustered-deepdiff-41c7265/.coveragerc 0000664 0000000 0000000 00000000121 15162412645 0017664 0 ustar 00root root 0000000 0000000 [report]
omit =
*/python?.?/*
*/site-packages/nose/*
*__init__*
qlustered-deepdiff-41c7265/.direnvrc.example 0000664 0000000 0000000 00000000632 15162412645 0021021 0 ustar 00root root 0000000 0000000 function load_venv () {
ACTUAL_VENV_PATH="$HOME/.venvs/$1"
if [ -d "$ACTUAL_VENV_PATH" ] && [ -f "$ACTUAL_VENV_PATH/bin/activate" ]; then
echo "direnv: Activating $ACTUAL_VENV_PATH..."
source "$ACTUAL_VENV_PATH/bin/activate"
export UV_PROJECT_ENVIRONMENT="$ACTUAL_VENV_PATH"
else
echo "direnv: Virtual environment at $ACTUAL_VENV_PATH not found or is incomplete."
fi
}
qlustered-deepdiff-41c7265/.envrc.example 0000664 0000000 0000000 00000000017 15162412645 0020317 0 ustar 00root root 0000000 0000000 load_venv deep
qlustered-deepdiff-41c7265/.github/ 0000775 0000000 0000000 00000000000 15162412645 0017111 5 ustar 00root root 0000000 0000000 qlustered-deepdiff-41c7265/.github/FUNDING.yml 0000664 0000000 0000000 00000000043 15162412645 0020723 0 ustar 00root root 0000000 0000000 github: [seperman]
ko_fi: seperman
qlustered-deepdiff-41c7265/.github/ISSUE_TEMPLATE/ 0000775 0000000 0000000 00000000000 15162412645 0021274 5 ustar 00root root 0000000 0000000 qlustered-deepdiff-41c7265/.github/ISSUE_TEMPLATE/bug_report.md 0000664 0000000 0000000 00000001342 15162412645 0023766 0 ustar 00root root 0000000 0000000 ---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
Please checkout the [F.A.Q](https://zepworks.com/deepdiff/current/faq.html) page before creating a bug ticket to make sure it is not already addressed.
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior
**Expected behavior**
A clear and concise description of what you expected to happen.
**OS, DeepDiff version and Python version (please complete the following information):**
- OS: [e.g. Ubuntu]
- Version [e.g. 20LTS]
- Python Version [e.g. 3.10.12]
- DeepDiff Version [e.g. 5.8.0]
**Additional context**
Add any other context about the problem here.
qlustered-deepdiff-41c7265/.github/ISSUE_TEMPLATE/feature_request.md 0000664 0000000 0000000 00000001123 15162412645 0025016 0 ustar 00root root 0000000 0000000 ---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
qlustered-deepdiff-41c7265/.github/workflows/ 0000775 0000000 0000000 00000000000 15162412645 0021146 5 ustar 00root root 0000000 0000000 qlustered-deepdiff-41c7265/.github/workflows/main.yaml 0000664 0000000 0000000 00000003300 15162412645 0022752 0 ustar 00root root 0000000 0000000 name: CI
on:
push: { branches: [master, dev] }
pull_request: { branches: [master, dev] }
jobs:
build:
runs-on: ubuntu-latest
env:
DEFAULT_PYTHON: '3.14'
strategy:
matrix:
python-version: ['3.10','3.11','3.12','3.13','3.14']
architecture: ['x64']
steps:
- uses: actions/checkout@v3
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
architecture: ${{ matrix.architecture }}
cache: pip
cache-dependency-path: pyproject.toml
- name: Install nox
run: pip install nox==2025.5.1
- name: Lint with flake8
if: ${{ matrix.python-version == '3.14' }}
run: |
nox -s flake8 -- deepdiff --count --select=E9,F63,F7,F82 --show-source --statistics
nox -s flake8 -- deepdiff --count --exit-zero --max-complexity=26 --max-line-length=250 --statistics
- name: Test with pytest (no coverage)
if: ${{ matrix.python-version != '3.14' }}
run: |
nox -s pytest-${{ matrix.python-version }} -- --benchmark-disable tests/
- name: Test with pytest (+ coverage)
if: ${{ matrix.python-version == '3.14' }}
run: |
nox -s pytest-${{ matrix.python-version }} -- \
--benchmark-disable \
--cov-report=xml \
--cov=deepdiff \
tests/ --runslow
- name: Upload coverage
if: ${{ matrix.python-version == '3.14' }}
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: coverage.xml
env_vars: OS,PYTHON
fail_ci_if_error: true
qlustered-deepdiff-41c7265/.gitignore 0000664 0000000 0000000 00000001540 15162412645 0017541 0 ustar 00root root 0000000 0000000 # Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
.pytest_cache/
# C extensions
*.so
# Distribution / packaging
.Python
env/
.venv
build/
develop-eggs/
dist/
downloads/
eggs/
lib/
lib64/
parts/
sdist/
var/
no_upload/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.cache
nosetests.xml
coverage.xml
# Translations
*.mo
*.pot
# Django stuff:
*.log
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# OS-specific spam
.DS_Store
# Editor / IDE files
*.swp
.idea/
.~lock*
.python-version*
temp*
# env file
.env
pyrightconfig.json
# direnv file
.envrc
qlustered-deepdiff-41c7265/AGENTS.md 0000777 0000000 0000000 00000000000 15162412645 0020326 2CLAUDE.md ustar 00root root 0000000 0000000 qlustered-deepdiff-41c7265/AUTHORS.md 0000664 0000000 0000000 00000020160 15162412645 0017217 0 ustar 00root root 0000000 0000000 # Authors
Authors in order of the timeline of their contributions:
- [Sep Dehpour (Seperman)](http://www.zepworks.com)
- [Victor Hahn Castell](http://hahncastell.de) for the tree view and major contributions:
- [nfvs](https://github.com/nfvs) for Travis-CI setup script.
- [brbsix](https://github.com/brbsix) for initial Py3 porting.
- [WangFenjin](https://github.com/WangFenjin) for unicode support.
- [timoilya](https://github.com/timoilya) for comparing list of sets when ignoring order.
- [Bernhard10](https://github.com/Bernhard10) for significant digits comparison.
- [b-jazz](https://github.com/b-jazz) for PEP257 cleanup, Standardize on full names, fixing line endings.
- [finnhughes](https://github.com/finnhughes) for fixing __slots__
- [moloney](https://github.com/moloney) for Unicode vs. Bytes default
- [serv-inc](https://github.com/serv-inc) for adding help(deepdiff)
- [movermeyer](https://github.com/movermeyer) for updating docs
- [maxrothman](https://github.com/maxrothman) for search in inherited class attributes
- [maxrothman](https://github.com/maxrothman) for search for types/objects
- [MartyHub](https://github.com/MartyHub) for exclude regex paths
- [sreecodeslayer](https://github.com/sreecodeslayer) for DeepSearch match_string
- Brian Maissy [brianmaissy](https://github.com/) for weakref fix, enum tests
- Bartosz Borowik [boba-2](https://github.com/boba-2) for Exclude types fix when ignoring order
- Brian Maissy [brianmaissy](https://github.com/brianmaissy) for fixing classes which inherit from classes with slots didn't have all of their slots compared
- Juan Soler [Soleronline](https://github.com/Soleronline) for adding ignore_type_number
- [mthaddon](https://github.com/mthaddon) for adding timedelta diffing support
- [Necrophagos](https://github.com/Necrophagos) for Hashing of the number 1 vs. True
- [gaal-dev](https://github.com/gaal-dev) for adding exclude_obj_callback
- Ivan Piskunov [van-ess0](https://github.com/van-ess0) for deprecation warning enhancement.
- Michał Karaś [MKaras93](https://github.com/MKaras93) for the pretty view
- Christian Kothe [chkothe](https://github.com/chkothe) for the basic support for diffing numpy arrays
- [Timothy](https://github.com/timson) for truncate_datetime
- [d0b3rm4n](https://github.com/d0b3rm4n) for bugfix to not apply format to non numbers.
- [MyrikLD](https://github.com/MyrikLD) for Bug Fix NoneType in ignore type groups
- Stian Jensen [stianjensen](https://github.com/stianjensen) for improving ignoring of NoneType in diff
- Florian Klien [flowolf](https://github.com/flowolf) for adding math_epsilon
- Tim Klein [timjklein36](https://github.com/timjklein36) for retaining the order of multiple dictionary items added via Delta.
- Wilhelm Schürmann [wbsch](https://github.com/wbsch) for fixing the typo with yml files.
- [lyz-code](https://github.com/lyz-code) for adding support for regular expressions in DeepSearch and strict_checking feature in DeepSearch.
- [dtorres-sf](https://github.com/dtorres-sf) for adding the option for custom compare function
- Tony Wang [Tony-Wang](https://github.com/Tony-Wang) for bugfix: verbose_level==0 should disable values_changes.
- Sun Ao [eggachecat](https://github.com/eggachecat) for adding custom operators.
- Sun Ao [eggachecat](https://github.com/eggachecat) for adding ignore_order_func.
- [SlavaSkvortsov](https://github.com/SlavaSkvortsov) for fixing unprocessed key error.
- HÃ¥vard Thom [havardthom](https://github.com/havardthom) for adding UUID support.
- Dhanvantari Tilak [Dhanvantari](https://github.com/Dhanvantari) for Bug-Fix: `TypeError in _get_numbers_distance() when ignore_order = True`.
- Yael Mintz [yaelmi3](https://github.com/yaelmi3) for detailed pretty print when verbose_level=2.
- Mikhail Khviyuzov [mskhviyu](https://github.com/mskhviyu) for Exclude obj callback strict.
- [dtorres-sf](https://github.com/dtorres-sf) for the fix for diffing using iterable_compare_func with nested objects.
- [Enric Pou](https://github.com/epou) for bug fix of ValueError when using Decimal 0.x
- [Uwe Fladrich](https://github.com/uwefladrich) for fixing bug when diff'ing non-sequence iterables
- [Michal Ozery-Flato](https://github.com/michalozeryflato) for setting equal_nan=ignore_nan_inequality in the call for np.array_equal
- [martin-kokos](https://github.com/martin-kokos) for using Pytest's tmp_path fixture instead of /tmp/
- HÃ¥vard Thom [havardthom](https://github.com/havardthom) for adding include_obj_callback and include_obj_callback_strict.
- [Noam Gottlieb](https://github.com/noamgot) for fixing a corner case where numpy's `np.float32` nans are not ignored when using `ignore_nan_equality`.
- [maggelus](https://github.com/maggelus) for the bugfix deephash for paths.
- [maggelus](https://github.com/maggelus) for the bugfix deephash compiled regex.
- [martin-kokos](https://github.com/martin-kokos) for fixing the tests dependent on toml.
- [kor4ik](https://github.com/kor4ik) for the bugfix for `include_paths` for nested dictionaries.
- [martin-kokos](https://github.com/martin-kokos) for using tomli and tomli-w for dealing with tomli files.
- [Alex Sauer-Budge](https://github.com/amsb) for the bugfix for `datetime.date`.
- [William Jamieson](https://github.com/WilliamJamieson) for [NumPy 2.0 compatibility](https://github.com/seperman/deepdiff/pull/422)
- [Leo Sin](https://github.com/leoslf) for Supporting Python 3.12 in the build process
- [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
- [dtorres-sf](https://github.com/dtorres-sf) for the fix for moving nested tables when using iterable_compare_func.
- [Jim Cipar](https://github.com/jcipar) for the fix recursion depth limit when hashing numpy.datetime64
- [Enji Cooper](https://github.com/ngie-eign) for converting legacy setuptools use to pyproject.toml
- [Diogo Correia](https://github.com/diogotcorreia) for reporting security vulnerability in Delta and DeepDiff that could allow remote code execution.
- [am-periphery](https://github.com/am-periphery) for reporting CVE-2026-33155: denial-of-service via crafted pickle payloads triggering massive memory allocation.
- [echan5](https://github.com/echan5) for adding callable `group_by` support.
- [yannrouillard](https://github.com/yannrouillard) for fixing colored view display when all list items are removed.
- [tpvasconcelos](https://github.com/tpvasconcelos) for fixing `__slots__` handling for objects with `__getattr__`.
- [devin13cox](https://github.com/devin13cox) for always using t1 path for reporting.
- [vitalis89](https://github.com/vitalis89) for fixing `ignore_keys` issue in `detailed__dict__`.
- [ljames8](https://github.com/ljames8) for fixing logarithmic similarity type hint.
- [srini047](https://github.com/srini047) for fixing README typo.
- [Nagato-Yuzuru](https://github.com/Nagato-Yuzuru) for colored view tests.
- [akshat62](https://github.com/akshat62) for adding Fraction numeric support.
qlustered-deepdiff-41c7265/CHANGELOG.md 0000664 0000000 0000000 00000042265 15162412645 0017373 0 ustar 00root root 0000000 0000000 # DeepDiff Change log
- v9-0-0
- migration note:
- `to_dict()` and `to_json()` now accept a `verbose_level` parameter and always return a usable text-view dict. When the original view is `'tree'`, they default to `verbose_level=2` for full detail. The old `view_override` parameter is removed. To get the previous results, you will need to pass the explicit verbose_level to `to_json` and `to_dict` if you are using the tree view.
- Dropping support for Python 3.9
- Support for python 3.14
- Added support for callable `group_by` thanks to [echan5](https://github.com/echan5)
- Added `FlatDeltaDict` TypedDict for `to_flat_dicts` return type
- Fixed colored view display when all list items are removed thanks to [yannrouillard](https://github.com/yannrouillard)
- Fixed `hasattr()` swallowing `AttributeError` in `__slots__` handling for objects with `__getattr__` thanks to [tpvasconcelos](https://github.com/tpvasconcelos)
- Fixed `ignore_order=True` missing int-vs-float type changes
- Always use t1 path for reporting thanks to [devin13cox](https://github.com/devin13cox)
- Fixed `_convert_oversized_ints` failing on NamedTuples
- Fixed orjson `TypeError` for integers exceeding 64-bit range
- Fixed parameter bug in `to_flat_dicts` where `include_action_in_path` and `report_type_changes` were not being passed through
- Fixed `ignore_keys` issue in `detailed__dict__` thanks to [vitalis89](https://github.com/vitalis89)
- Fixed logarithmic similarity type hint thanks to [ljames8](https://github.com/ljames8)
- Added `Fraction` numeric support thanks to [akshat62](https://github.com/akshat62)
- v8-6-2
- Security fix (CVE-2026-33155): Prevent denial-of-service via crafted pickle payloads that trigger massive memory allocation through the REDUCE opcode. Size-sensitive callables like `bytes()` and `bytearray()` are now wrapped to reject allocations exceeding 128 MB.
- v8-6-1
- Patched security vulnerability in the Delta class which was vulnerable to class pollution via its constructor, and when combined with a gadget available in DeltaDiff itself, it could lead to Denial of Service and Remote Code Execution (via insecure Pickle deserialization).
- v8-6-0
- Added Colored View thanks to @mauvilsa
- Added support for applying deltas to NamedTuple thanks to @paulsc
- Fixed test_delta.py with Python 3.14 thanks to @Romain-Geissler-1A
- Added python property serialization to json
- Added ip address serialization
- Switched to UV from pip
- Added Claude.md
- Added uuid hashing thanks to @akshat62
- Added `ignore_uuid_types` flag to DeepDiff to avoid type reports when comparing UUID and string.
- Added comprehensive type hints across the codebase (multiple commits for better type safety)
- Added support for memoryview serialization
- Added support for bytes serialization (non-UTF8 compatible)
- Fixed bug where group_by with numbers would leak type info into group path reports
- Fixed bug in `_get_clean_to_keys_mapping` without explicit significant digits
- Added support for python dict key serialization
- Enhanced support for IP address serialization with safe module imports
- Added development tooling improvements (pyright config, .envrc example)
- Updated documentation and development instructions
- v8-5-0
- Updating deprecated pydantic calls
- Switching to pyproject.toml
- Fix for moving nested tables when using iterable_compare_func.
- Fix recursion depth limit when hashing numpy.datetime64
- Moving from legacy setuptools use to pyproject.toml
- v8-4-2
- fixes the type hints for the base
- fixes summarize so if json dumps fails, we can still get a repr of the results
- adds ipaddress support
- v8-4-1
- Adding BaseOperatorPlus base class for custom operators
- default_timezone can be passed now to set your default timezone to something other than UTC.
- New summarization algorithm that produces valid json
- Better type hint support
- Breaking change in DeepHash where we raise Exception instead of logging if we can't hash a value.
- Added the log_stacktrace parameter to DeepDiff. When True, it will log the stacktrace along with the error.
- v8-3-0
- Fixed some static typing issues
- Added the summarize module for better repr of nested values
- v8-2-0
- Small optimizations so we don't load functions that are not needed
- Updated the minimum version of Orderly-set
- Normalize all datetimes into UTC. Assume timezone naive datetimes are UTC.
- 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 #504
- 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
- Fixes 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.
- v8-0-0
- With the introduction of `threshold_to_diff_deeper`, the values returned are different than in previous versions of DeepDiff. You can still get the older values by setting `threshold_to_diff_deeper=0`. However to signify that enough has changed in this release that the users need to update the parameters passed to DeepDiff, we will be doing a major version update.
- [x] `use_enum_value=True` makes it so when diffing enum, we use the enum's value. It makes it so comparing an enum to a string or any other value is not reported as a type change.
- [x] `threshold_to_diff_deeper=float` is a number between 0 and 1. When comparing dictionaries that have a small intersection of keys, we will report the dictionary as a `new_value` instead of reporting individual keys changed. If you set it to zero, you get the same results as DeepDiff 7.0.1 and earlier, which means this feature is disabled. The new default is 0.33 which means if less that one third of keys between dictionaries intersect, report it as a new object.
- [x] Deprecated `ordered-set` and switched to `orderly-set`. The `ordered-set` package was not being maintained anymore and starting Python 3.6, there were better options for sets that ordered. I forked one of the new implementations, modified it, and published it as `orderly-set`.
- [x] Added `use_log_scale:bool` and `log_scale_similarity_threshold:float`. They can be used to ignore small changes in numbers by comparing their differences in logarithmic space. This is different than ignoring the difference based on significant digits.
- [x] json serialization of reversed lists.
- [x] Fix for iterable moved items when `iterable_compare_func` is used.
- [x] Pandas and Polars support
- v7-0-1
- Fixes the translation between Difflib opcodes and Delta flat rows.
- v7-0-0
- When verbose=2, return `new_path` when the `path` and `new_path` are different (for example when ignore_order=True and the index of items have changed).
- Dropping support for Python 3.7
- Introducing serialize to flat rows for delta objects.
- fixes the issue with hashing `datetime.date` objects where it treated them as numbers instead of dates (fixes #445).
- upgrading orjson to the latest version
- Fix for bug when diffing two lists with ignore_order and providing compare_func
- Fixes "Wrong diff on list of strings" #438
- Supporting Python 3.12 in the build process by [Leo Sin](https://github.com/leoslf)
- Fixes "Instantiating a Delta with a flat_dict_list unexpectedly mutates the flat_dict_list" #457 by [sf-tcalhoun](https://github.com/sf-tcalhoun)
- Fixes "Error on Delta With None Key and Removed Item from List" #441
- Fixes "Error when comparing two nested dicts with 2 added fields" #450
- Fixes "Error when subtracting Delta from a dictionary" #443
- v6-7-1
- Support for subtracting delta objects when iterable_compare_func is used.
- Better handling of force adding a delta to an object.
- Fix for [`Can't compare dicts with both single and double quotes in keys`](https://github.com/seperman/deepdiff/issues/430)
- Updated docs for Inconsistent Behavior with math_epsilon and ignore_order = True
- v6-7-0
- Delta can be subtracted from other objects now.
- verify_symmetry is deprecated. Use bidirectional instead.
- always_include_values flag in Delta can be enabled to include values in the delta for every change.
- Fix for Delta.__add__ breaks with esoteric dict keys.
- You can load a delta from the list of flat dictionaries.
- v6-6-1
- Fix for [DeepDiff raises decimal exception when using significant digits](https://github.com/seperman/deepdiff/issues/426)
- Introducing group_by_sort_key
- Adding group_by 2D. For example `group_by=['last_name', 'zip_code']`
- v6-6-0
- Numpy 2.0 support
- Adding [Delta.to_flat_dicts](https://zepworks.com/deepdiff/current/serialization.html#delta-serialize-to-flat-dictionaries)
- v6-5-0
- Adding [`parse_path`](https://github.com/seperman/deepdiff/pull/419)
- v6-4-1
- Bugfix: Keep Numpy Optional
- v6-4-0
- [Add Ignore List Order Option to DeepHash](https://github.com/seperman/deepdiff/pull/403) by
[Bobby Morck](https://github.com/bmorck)
- [pyyaml to 6.0.1 to fix cython build problems](https://github.com/seperman/deepdiff/pull/406) by [Robert Bo Davis](https://github.com/robert-bo-davis)
- [Precompiled regex simple diff](https://github.com/seperman/deepdiff/pull/413) by [cohml](https://github.com/cohml)
- New flag: `zip_ordered_iterables` for forcing iterable items to be compared one by one.
- v6-3-1
- Bugfix deephash for paths by [maggelus](https://github.com/maggelus)
- Bugfix deephash compiled regex [maggelus](https://github.com/maggelus)
- Fix tests dependent on toml by [martin-kokos](https://github.com/martin-kokos)
- Bugfix for `include_paths` for nested dictionaries by [kor4ik](https://github.com/kor4ik)
- Use tomli and tomli-w for dealing with tomli files by [martin-kokos](https://github.com/martin-kokos)
- Bugfix for `datetime.date` by [Alex Sauer-Budge](https://github.com/amsb)
- v6-3-0
- `PrefixOrSuffixOperator`: This operator will skip strings that are suffix or prefix of each other.
- `include_obj_callback` and `include_obj_callback_strict` are added by [HÃ¥vard Thom](https://github.com/havardthom).
- Fixed a corner case where numpy's `np.float32` nans are not ignored when using `ignore_nan_equality` by [Noam Gottlieb](https://github.com/noamgot)
- `orjson` becomes optional again.
- Fix for `ignore_type_in_groups` with numeric values so it does not report number changes when the number types are different.
- v6-2-3
- Switching to Orjson for serialization to improve the performance.
- Setting `equal_nan=ignore_nan_inequality` in the call for `np.array_equal`
- Using Pytest's tmp_path fixture instead of `/tmp/`
- v6-2-2
- Enum test fix for python 3.11
- Adding support for dateutils rrules
- v6-2-1
- Removed the print statements.
- v6-2-0
- Major improvement in the diff report for lists when items are all hashable and the order of items is important.
- v6-1-0
- DeepDiff.affected_paths can be used to get the list of all paths where a change, addition, or deletion was reported for.
- DeepDiff.affected_root_keys can be used to get the list of all paths where a change, addition, or deletion was reported for.
- Bugfix: ValueError when using Decimal 0.x #339 by [Enric Pou](https://github.com/epou)
- Serialization of UUID
- v6-0-0
- [Exclude obj callback strict](https://github.com/seperman/deepdiff/pull/320/files) parameter is added to DeepDiff by Mikhail Khviyuzov [mskhviyu](https://github.com/mskhviyu).
- A fix for diffing using `iterable_compare_func` with nested objects by [dtorres-sf](https://github.com/dtorres-sf) who originally contributed this feature.
- v5-7-0:
- https://github.com/seperman/deepdiff/pull/284 Bug-Fix: TypeError in _get_numbers_distance() when ignore_order = True by @Dhanvantari
- https://github.com/seperman/deepdiff/pull/280 Add support for UUIDs by @havardthom
- Major bug in delta when it comes to iterable items added or removed is investigated by @uwefladrich and resolved by @seperman
- v5-6-0: Adding custom operators, and ignore_order_func. Bugfix: verbose_level==0 should disable values_changes. Bugfix: unprocessed key error.
- v5-5-0: adding iterable_compare_func for DeepDiff, adding output_format of list for path() in tree view.
- v5-4-0: adding strict_checking for numbers in DeepSearch.
- v5-3-0: add support for regular expressions in DeepSearch.
- v5-2-3: Retaining the order of multiple dictionary items added via Delta. Fixed the typo with yml files in deep cli. Fixing Grep RecursionError where using non UTF-8 character. Allowing kwargs to be passed to to_json method.
- v5-2-2: Fixed Delta serialization when None type is present.
- v5-2-0: Removed Murmur3 as the preferred hashing method. Using SHA256 by default now. Added commandline for deepdiff. Added group_by. Added math_epsilon. Improved ignoring of NoneType.
- v5-0-2: Bug Fix NoneType in ignore type groups https://github.com/seperman/deepdiff/issues/207
- v5-0-1: Bug fix to not apply format to non numbers.
- v5-0-0: Introducing the Delta object, Improving Numpy support, Fixing tuples comparison when ignore_order=True, Dramatically improving the results when ignore_order=True by running in passes, Introducing pretty print view, deep_distance, purge, progress logging, cache and truncate_datetime.
- v4-3-3: Adds support for datetime.time
- v4-3-2: Deprecation Warning Enhancement
- v4-3-1: Fixing the issue with exclude_path and hash calculations when dictionaries were inside iterables. https://github.com/seperman/deepdiff/issues/174
- v4-3-0: adding exclude_obj_callback
- v4-2-0: .json property is finally removed. Fix for Py3.10. Dropping support for EOL Python 3.4. Ignoring private keys when calculating hashes. For example __init__ is not a part of hash calculation anymore. Fix for #166 Problem with comparing lists, with an boolean as element.
- v4-0-9: Fixing the bug for hashing custom unhashable objects
- v4-0-8: Adding ignore_nan_inequality for float('nan')
- v4-0-7: Hashing of the number 1 vs. True
- v4-0-6: found a tiny bug in Python formatting of numbers in scientific notation. Added a workaround.
- v4-0-5: Fixing number diffing. Adding number_format_notation and number_to_string_func.
- v4-0-4: Adding ignore_string_case and ignore_type_subclasses
- v4-0-3: Adding versionbump tool for release
- v4-0-2: Fixing installation issue where rst files are missing.
- v4-0-1: Fixing installation Tarball missing requirements.txt . DeepDiff v4+ should not show up as pip installable for Py2. Making Murmur3 installation optional.
- v4-0-0: Ending Python 2 support, Adding more functionalities and documentation for DeepHash. Switching to Pytest for testing. Switching to Murmur3 128bit for hashing. Fixing classes which inherit from classes with slots didn't have all of their slots compared. Renaming ContentHash to DeepHash. Adding exclude by path and regex path to DeepHash. Adding ignore_type_in_groups. Adding match_string to DeepSearch. Adding Timedelta object diffing.
- v3-5-0: Exclude regex path
- v3-3-0: Searching for objects and class attributes
- v3-2-2: Adding help(deepdiff)
- v3-2-1: Fixing hash of None
- v3-2-0: Adding grep for search: object | grep(item)
- v3-1-3: Unicode vs. Bytes default fix
- v3-1-2: NotPresent Fix when item is added or removed.
- v3-1-1: Bug fix when item value is None (#58)
- v3-1-0: Serialization to/from json
- v3-0-0: Introducing Tree View
- v2-5-3: Bug fix on logging for content hash.
- v2-5-2: Bug fixes on content hash.
- v2-5-0: Adding ContentHash module to fix ignore_order once and for all.
- v2-1-0: Adding Deep Search. Now you can search for item in an object.
- v2-0-0: Exclusion patterns better coverage. Updating docs.
- v1-8-0: Exclusion patterns.
- v1-7-0: Deep Set comparison.
- v1-6-0: Unifying key names. i.e newvalue is new_value now. For backward compatibility, newvalue still works.
- v1-5-0: Fixing ignore order containers with unordered items. Adding significant digits when comparing decimals. Changes property is deprecated.
- v1-1-0: Changing Set, Dictionary and Object Attribute Add/Removal to be reported as Set instead of List. Adding Pypy compatibility.
- v1-0-2: Checking for ImmutableMapping type instead of dict
- v1-0-1: Better ignore order support
- v1-0-0: Restructuring output to make it more useful. This is NOT backward compatible.
- v0-6-1: Fixing iterables with unhashable when order is ignored
- v0-6-0: Adding unicode support
- v0-5-9: Adding decimal support
- v0-5-8: Adding ignore order of unhashables support
- v0-5-7: Adding ignore order support
- v0-5-6: Adding slots support
- v0-5-5: Adding loop detection
qlustered-deepdiff-41c7265/CITATION.cff 0000664 0000000 0000000 00000000433 15162412645 0017443 0 ustar 00root root 0000000 0000000 cff-version: 1.2.0
message: "If you use this software, please cite it as below."
authors:
- family-names: "Dehpour"
given-names: "Sep"
orcid: "https://orcid.org/0009-0009-5828-4345"
title: "DeepDiff"
version: 9.0.0
date-released: 2026
url: "https://github.com/seperman/deepdiff"
qlustered-deepdiff-41c7265/CLAUDE.md 0000664 0000000 0000000 00000007276 15162412645 0017044 0 ustar 00root root 0000000 0000000 # CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
DeepDiff is a Python library for deep comparison, searching, and hashing of Python objects. It provides:
- **DeepDiff**: Deep difference detection between objects
- **DeepSearch**: Search for objects within other objects
- **DeepHash**: Content-based hashing for any object
- **Delta**: Git-like diff objects that can be applied to other objects
- **CLI**: Command-line interface via `deep` command
## Development Commands
### Setup
```bash
# Install with all development dependencies
uv pip install -e ".[cli,coverage,dev,docs,static,test]"
# OR using uv (recommended)
uv sync --all-extras
```
**Virtual Environment**: Activate with `source ~/.venvs/deep/bin/activate` before running tests or Python commands
### Testing
```bash
# Run tests with coverage
source ~/.venvs/deep/bin/activate && pytest --cov=deepdiff --cov-report term-missing
# Run tests including slow ones
source ~/.venvs/deep/bin/activate && pytest --cov=deepdiff --runslow
# Run single test file
source ~/.venvs/deep/bin/activate && pytest tests/test_diff_text.py
# Run tests across multiple Python versions. No need to use this unless getting ready for creating a new build
source ~/.venvs/deep/bin/activate && nox -s pytest
```
### **Type Checking with Pyright:**
Always use this pattern for type checking:
```bash
source ~/.venvs/deep/bin/activate && pyright {file_path}
```
Examples:
- `source ~/.venvs/deep/bin/activate && pyright deepdiff/diff.py` - Type check specific file
- `source ~/.venvs/deep/bin/activate && pyright deepdiff/` - Type check entire module
- `source ~/.venvs/deep/bin/activate && pyright .` - Type check entire repo
### Common Pitfalls to Avoid
1. **Forgetting Virtual Environment**: ALWAYS activate venv before ANY Python command:
```bash
source ~/.venvs/deep/bin/activate
```
2. **Running pytest without venv**: This will cause import errors. Always use:
```bash
source ~/.venvs/deep/bin/activate && pytest
```
3. **Running module commands without venv**: Commands like `capi run`, `cettings shell`, etc. all require venv to be activated first
4. **Using `pip` instead of `uv pip`**: This project uses `uv` for package management. Always use `uv pip` instead of `pip`.
### Slow quality checks only to run before creating a build
```bash
# Linting (max line length: 120)
nox -s flake8
# Type checking
nox -s mypy
# Run all quality checks
nox
```
## Architecture
### Core Structure
- **deepdiff/diff.py**: Main DeepDiff implementation (most complex component)
- **deepdiff/deephash.py**: DeepHash functionality
- **deepdiff/base.py**: Shared base classes and functionality
- **deepdiff/model.py**: Core data structures and result objects
- **deepdiff/helper.py**: Utility functions and type definitions
- **deepdiff/delta.py**: Delta objects for applying changes
### Key Patterns
- **Inheritance**: `Base` class provides common functionality with mixins
- **Result Objects**: Different result formats (`ResultDict`, `TreeResult`, `TextResult`)
- **Path Navigation**: Consistent path notation for nested object access
- **Performance**: LRU caching and numpy array optimization
### Testing
- Located in `/tests/` directory
- Organized by functionality (e.g., `test_diff_text.py`, `test_hash.py`)
- Aims for ~100% test coverage
- Uses pytest with comprehensive fixtures
## Development Notes
- **Python Support**: 3.10+ and PyPy3
- **Main Branch**: `master` (PRs typically go to `dev` branch)
- **Build System**: Modern `pyproject.toml` with `flit_core`
- **Dependencies**: Core dependency is `orderly-set>=5.4.1,<6`
- **CLI Tool**: Available as `deep` command after installation with `[cli]` extra
qlustered-deepdiff-41c7265/LICENSE 0000664 0000000 0000000 00000002166 15162412645 0016563 0 ustar 00root root 0000000 0000000 The MIT License (MIT)
Copyright (c) 2014 - 2026 Sep Dehpour (Seperman) and contributors
getqluster.com
zepworks.com
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
qlustered-deepdiff-41c7265/MANIFEST.in 0000664 0000000 0000000 00000000712 15162412645 0017307 0 ustar 00root root 0000000 0000000 include LICENSE
include AUTHORS
include CHANGELOG
include *.rst
include deepdiff/*.rst
include *.txt
include *.sh
include pytest.ini
include *.py
exclude uv.lock
recursive-include docs/ *.rst
recursive-include docs/ *.png
recursive-include tests *.csv
recursive-include tests *.json
recursive-include tests *.pickle
recursive-include tests *.py
recursive-include tests *.toml
recursive-include tests *.yaml
global-exclude __pycache__
global-exclude *.py[co]
qlustered-deepdiff-41c7265/README.md 0000664 0000000 0000000 00000012007 15162412645 0017030 0 ustar 00root root 0000000 0000000 # DeepDiff v 9.0.0



[](https://github.com/seperman/deepdiff/actions)
[](https://codecov.io/gh/seperman/deepdiff)
**DeepDiff is now part of [Qluster](/qluster).**
*If you're building workflows around data validation and correction, [Qluster](/qluster) gives your team a structured way to manage rules, review failures, approve fixes, and reuse decisions—without building the entire system from scratch.*
## Modules
- [DeepDiff](https://zepworks.com/deepdiff/current/diff.html): Deep Difference of dictionaries, iterables, strings, and ANY other object.
- [DeepSearch](https://zepworks.com/deepdiff/current/dsearch.html): Search for objects within other objects.
- [DeepHash](https://zepworks.com/deepdiff/current/deephash.html): Hash any object based on their content.
- [Delta](https://zepworks.com/deepdiff/current/delta.html): Store the difference of objects and apply them to other objects.
- [Extract](https://zepworks.com/deepdiff/current/extract.html): Extract an item from a nested Python object using its path.
- [commandline](https://zepworks.com/deepdiff/current/commandline.html): Use DeepDiff from commandline.
Tested on Python 3.10+ and PyPy3.
- **[Documentation](https://zepworks.com/deepdiff/9.0.0/)**
## What is new?
Please check the [ChangeLog](CHANGELOG.md) file for the detailed information.
DeepDiff 9-0-0
- migration note:
- `to_dict()` and `to_json()` now accept a `verbose_level` parameter and always return a usable text-view dict. When the original view is `'tree'`, they default to `verbose_level=2` for full detail. The old `view_override` parameter is removed. To get the previous results, you will need to pass the explicit verbose_level to `to_json` and `to_dict` if you are using the tree view.
- Dropping support for Python 3.9
- Support for python 3.14
- Added support for callable `group_by` thanks to @echan5
- Added `FlatDeltaDict` TypedDict for `to_flat_dicts` return type
- Fixed colored view display when all list items are removed thanks to @yannrouillard
- Fixed `hasattr()` swallowing `AttributeError` in `__slots__` handling for objects with `__getattr__` thanks to @tpvasconcelos
- Fixed `ignore_order=True` missing int-vs-float type changes
- Always use t1 path for reporting thanks to @devin13cox
- Fixed `_convert_oversized_ints` failing on NamedTuples
- Fixed orjson `TypeError` for integers exceeding 64-bit range
- Fixed parameter bug in `to_flat_dicts` where `include_action_in_path` and `report_type_changes` were not being passed through
- Fixed `ignore_keys` issue in `detailed__dict__` thanks to @vitalis89
- Fixed logarithmic similarity type hint thanks to @ljames8
- Added `Fraction` numeric support thanks to @akshat62
## Installation
### Install from PyPi:
`pip install deepdiff`
If you want to use DeepDiff from commandline:
`pip install "deepdiff[cli]"`
If you want to improve the performance of DeepDiff with certain functionalities such as improved json serialization:
`pip install "deepdiff[optimize]"`
Install optional packages:
- [yaml](https://pypi.org/project/PyYAML/)
- [tomli](https://pypi.org/project/tomli/) (python 3.10 and older) and [tomli-w](https://pypi.org/project/tomli-w/) for writing
- [clevercsv](https://pypi.org/project/clevercsv/) for more robust CSV parsing
- [orjson](https://pypi.org/project/orjson/) for speed and memory optimized parsing
- [pydantic](https://pypi.org/project/pydantic/)
# Documentation
# ChangeLog
Please take a look at the [CHANGELOG](CHANGELOG.md) file.
# Survey
:mega: **Please fill out our [fast 5-question survey](https://forms.gle/E6qXexcgjoKnSzjB8)** so that we can learn how & why you use DeepDiff, and what improvements we should make. Thank you! :dancers:
# Local dev
1. Clone the repo
2. Switch to the dev branch
3. Create your own branch
4. Install dependencies
- Method 1: Use [`uv`](https://github.com/astral-sh/uv) to install the dependencies: `uv sync --all-extras`.
- Method 2: Use pip: `pip install -e ".[cli,coverage,dev,docs,static,test]"`
5. Build `uv build`
# Contribute
1. Please make your PR against the dev branch
2. Please make sure that your PR has tests. Since DeepDiff is used in many sensitive data driven projects, we strive to maintain around 100% test coverage on the code.
Please run `pytest --cov=deepdiff --runslow` to see the coverage report. Note that the `--runslow` flag will run some slow tests too. In most cases you only want to run the fast tests which so you won't add the `--runslow` flag.
Or to see a more user friendly version, please run: `pytest --cov=deepdiff --cov-report term-missing --runslow`.
Thank you!
# Authors
Please take a look at the [AUTHORS](AUTHORS.md) file.
qlustered-deepdiff-41c7265/conftest.py 0000664 0000000 0000000 00000007224 15162412645 0017755 0 ustar 00root root 0000000 0000000 import sys
import os
import json
import pytest
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), 'tests')))
FIXTURES_DIR = os.path.join(os.path.dirname(__file__), 'tests/fixtures/')
def pytest_addoption(parser):
parser.addoption(
"--runslow", action="store_true", default=False, help="run slow tests"
)
def pytest_configure(config):
config.addinivalue_line("markers", "slow: mark test as slow to run")
def pytest_collection_modifyitems(config, items):
if config.getoption("--runslow"):
# --runslow given in cli: do not skip slow tests
return
skip_slow = pytest.mark.skip(reason="need --runslow option to run")
for item in items:
if "slow" in item.keywords:
item.add_marker(skip_slow)
@pytest.fixture(scope='class')
def nested_a_t1():
with open(os.path.join(FIXTURES_DIR, 'nested_a_t1.json')) as the_file:
return json.load(the_file)
@pytest.fixture(scope='class')
def nested_a_t2():
with open(os.path.join(FIXTURES_DIR, 'nested_a_t2.json')) as the_file:
return json.load(the_file)
@pytest.fixture(scope='class')
def nested_a_result():
with open(os.path.join(FIXTURES_DIR, 'nested_a_result.json')) as the_file:
return json.load(the_file)
@pytest.fixture(scope='function')
def compounds():
with open(os.path.join(FIXTURES_DIR, 'compounds.json')) as the_file:
return json.load(the_file)
@pytest.fixture(scope='class')
def nested_a_affected_paths():
return {
'root[0][0][2][0][1]', 'root[0][1][1][1][5]', 'root[0][2][1]',
'root[1][1][2][0][1]', 'root[1][2][0]', 'root[1][2][0][1][5]',
'root[1][0][2][2][3]', 'root[0][0][1][0][0]', 'root[0][1][0][2][3]',
'root[0][3][0][2][3]', 'root[0][3][1][0][2]', 'root[1][1][1][0][0]',
'root[1][0][1][2][1]', 'root[1][0][2][1][2]', 'root[1][3][0][2][3]',
'root[1][3][1][0][2]', 'root[1][2][0][2]', 'root[1][0][2][0][1]',
'root[0][3][2][0][1]', 'root[0][3][2][1][0]', 'root[1][3][1][1]',
'root[1][2][1][1][0]', 'root[1][2][1][0]', 'root[1][0][0][0][2]',
'root[1][3][2][1][0]', 'root[1][0][0][1][1]', 'root[0][1][2][0]',
'root[0][1][2][1][0]', 'root[0][2][0][1][2]', 'root[1][3][0][1]',
'root[0][3][1][1]', 'root[1][2][0][0][2]', 'root[1][3][2][0][1]',
'root[1][0][1][0]', 'root[1][2][0][0][0]', 'root[1][0][0][0][1]',
'root[1][3][2][2][2]', 'root[0][1][1][2][1]', 'root[0][1][1][2][2]',
'root[0][2][0][0][2]', 'root[0][2][0][0][3]', 'root[0][3][1][2][1]',
'root[0][3][1][2][2]', 'root[1][2][1][2][3]', 'root[1][0][0][1][2]',
'root[1][0][0][2][1]', 'root[1][3][1][2][1]', 'root[1][3][1][2][2]'
}
@pytest.fixture(scope='class')
def nested_b_t1():
with open(os.path.join(FIXTURES_DIR, 'nested_b_t1.json')) as the_file:
return json.load(the_file)
@pytest.fixture(scope='class')
def nested_b_t2():
with open(os.path.join(FIXTURES_DIR, 'nested_b_t2.json')) as the_file:
return json.load(the_file)
@pytest.fixture(scope='class')
def nested_b_result():
with open(os.path.join(FIXTURES_DIR, 'nested_b_result.json')) as the_file:
return json.load(the_file)
@pytest.fixture(scope='class')
def compare_func_t1():
with open(os.path.join(FIXTURES_DIR, 'compare_func_t1.json')) as the_file:
return json.load(the_file)
@pytest.fixture(scope='class')
def compare_func_t2():
with open(os.path.join(FIXTURES_DIR, 'compare_func_t2.json')) as the_file:
return json.load(the_file)
@pytest.fixture(scope='class')
def compare_func_result1():
with open(os.path.join(FIXTURES_DIR, 'compare_func_result1.json')) as the_file:
return json.load(the_file)
qlustered-deepdiff-41c7265/deepdiff/ 0000775 0000000 0000000 00000000000 15162412645 0017317 5 ustar 00root root 0000000 0000000 qlustered-deepdiff-41c7265/deepdiff/__init__.py 0000664 0000000 0000000 00000000736 15162412645 0021436 0 ustar 00root root 0000000 0000000 """This module offers the DeepDiff, DeepSearch, grep, Delta and DeepHash classes."""
# flake8: noqa
__version__ = '9.0.0'
import logging
if __name__ == '__main__':
logging.basicConfig(format='%(asctime)s %(levelname)8s %(message)s')
from .diff import DeepDiff as DeepDiff
from .search import DeepSearch as DeepSearch, grep as grep
from .deephash import DeepHash as DeepHash
from .delta import Delta as Delta
from .path import extract as extract, parse_path as parse_path
qlustered-deepdiff-41c7265/deepdiff/anyset.py 0000664 0000000 0000000 00000003673 15162412645 0021205 0 ustar 00root root 0000000 0000000 from deepdiff.deephash import DeepHash
from deepdiff.helper import dict_, SetOrdered
class AnySet:
"""
Any object can be in this set whether hashable or not.
Note that the current implementation has memory leak and keeps
traces of objects in itself even after popping.
However one the AnySet object is deleted, all those traces will be gone too.
"""
def __init__(self, items=None):
self._set = SetOrdered()
self._hashes = dict_()
self._hash_to_objects = dict_()
if items:
for item in items:
self.add(item)
def add(self, item):
try:
self._set.add(item)
except TypeError:
hashes_obj = DeepHash(item, hashes=self._hashes)
hash_ = hashes_obj[item]
if hash_ not in self._hash_to_objects:
self._hash_to_objects[hash_] = item
def __contains__(self, item):
try:
result = item in self._set
except TypeError:
hashes_obj = DeepHash(item, hashes=self._hashes)
hash_ = hashes_obj[item]
result = hash_ in self._hash_to_objects
return result
def pop(self):
if self._set:
return self._set.pop()
else:
return self._hash_to_objects.pop(next(iter(self._hash_to_objects)))
def __eq__(self, other):
set_part, hashes_to_objs_part = other
return (self._set == set_part and self._hash_to_objects == hashes_to_objs_part)
__req__ = __eq__
def __repr__(self):
return "< AnySet {}, {} >".format(self._set, self._hash_to_objects)
__str__ = __repr__
def __len__(self):
return len(self._set) + len(self._hash_to_objects)
def __iter__(self):
for item in self._set:
yield item
for item in self._hash_to_objects.values():
yield item
def __bool__(self):
return bool(self._set or self._hash_to_objects)
qlustered-deepdiff-41c7265/deepdiff/base.py 0000664 0000000 0000000 00000005265 15162412645 0020613 0 ustar 00root root 0000000 0000000 import uuid
from typing import List, Optional, Union, Tuple, Any, Type
from deepdiff.helper import strings, numbers, SetOrdered
DEFAULT_SIGNIFICANT_DIGITS_WHEN_IGNORE_NUMERIC_TYPES = 12
TYPE_STABILIZATION_MSG = 'Unable to stabilize the Numpy array {} due to {}. Please set ignore_order=False.'
class Base:
numbers = numbers
strings = strings
def get_significant_digits(self, significant_digits: Optional[int], ignore_numeric_type_changes: bool) -> Optional[int]:
if significant_digits is not None and significant_digits < 0:
raise ValueError(
"significant_digits must be None or a non-negative integer")
if significant_digits is None:
if ignore_numeric_type_changes:
significant_digits = DEFAULT_SIGNIFICANT_DIGITS_WHEN_IGNORE_NUMERIC_TYPES
return significant_digits
def get_ignore_types_in_groups(self,
ignore_type_in_groups: Optional[Union[List[Any], Tuple[Any, ...]]],
ignore_string_type_changes: bool,
ignore_numeric_type_changes: bool,
ignore_type_subclasses: bool,
ignore_uuid_types: bool = False) -> List[Union[SetOrdered, Tuple[Type[Any], ...]]]:
if ignore_type_in_groups:
if isinstance(ignore_type_in_groups[0], type):
ignore_type_in_groups = [ignore_type_in_groups]
else:
ignore_type_in_groups = []
result = []
for item_group in ignore_type_in_groups:
new_item_group = SetOrdered()
for item in item_group:
item = type(item) if item is None or not isinstance(item, type) else item
new_item_group.add(item)
result.append(new_item_group)
ignore_type_in_groups = result
if ignore_string_type_changes and self.strings not in ignore_type_in_groups:
ignore_type_in_groups.append(SetOrdered(self.strings))
if ignore_numeric_type_changes and self.numbers not in ignore_type_in_groups:
ignore_type_in_groups.append(SetOrdered(self.numbers))
if ignore_uuid_types:
# Create a group containing both UUID and str types
uuid_str_group = SetOrdered([uuid.UUID, str])
if uuid_str_group not in ignore_type_in_groups:
ignore_type_in_groups.append(uuid_str_group)
if not ignore_type_subclasses:
# is_instance method needs tuples. When we look for subclasses, we need them to be tuples
ignore_type_in_groups = list(map(tuple, ignore_type_in_groups))
return ignore_type_in_groups
qlustered-deepdiff-41c7265/deepdiff/colored_view.py 0000664 0000000 0000000 00000013575 15162412645 0022365 0 ustar 00root root 0000000 0000000 import json
import os
from ast import literal_eval
from importlib.util import find_spec
from typing import Any, Dict
from deepdiff.model import TextResult, TreeResult
if os.name == "nt" and find_spec("colorama"):
import colorama
colorama.init()
# ANSI color codes
RED = '\033[31m'
GREEN = '\033[32m'
RESET = '\033[0m'
class ColoredView:
"""A view that shows JSON with color-coded differences."""
def __init__(self, t2: Any, tree_result: TreeResult, compact: bool = False):
self.t2 = t2
self.tree = tree_result
self.compact = compact
self.diff_paths = self._collect_diff_paths()
def _collect_diff_paths(self) -> Dict[str, str]:
"""Collect all paths that have differences and their types."""
text_result = TextResult(tree_results=self.tree, verbose_level=2)
diff_paths = {}
for diff_type, items in text_result.items():
if not items:
continue
try:
iter(items)
except TypeError:
continue
for path, item in items.items():
if diff_type in ("values_changed", "type_changes"):
changed_path = item.get("new_path") or path
diff_paths[changed_path] = ("changed", item["old_value"], item["new_value"])
elif diff_type in ("dictionary_item_added", "iterable_item_added", "set_item_added"):
diff_paths[path] = ("added", None, item)
elif diff_type in ("dictionary_item_removed", "iterable_item_removed", "set_item_removed"):
diff_paths[path] = ("removed", item, None)
return diff_paths
def _format_value(self, value: Any) -> str:
"""Format a value for display."""
if isinstance(value, bool):
return 'true' if value else 'false'
elif isinstance(value, str):
return f'"{value}"'
elif isinstance(value, (dict, list, tuple)):
return json.dumps(value)
else:
return str(value)
def _get_path_removed(self, path: str) -> dict:
"""Get all removed items for a given path."""
removed = {}
for key, value in self.diff_paths.items():
if value[0] == 'removed' and key.startswith(path + "["):
key_suffix = key[len(path):]
if key_suffix.count("[") == 1 and key_suffix.endswith("]"):
removed[literal_eval(key_suffix[1:-1])] = value[1]
return removed
def _has_differences(self, path_prefix: str) -> bool:
"""Check if a path prefix has any differences under it."""
return any(diff_path.startswith(path_prefix + "[") for diff_path in self.diff_paths)
def _colorize_json(self, obj: Any, path: str = 'root', indent: int = 0) -> str:
"""Recursively colorize JSON based on differences, with pretty-printing."""
INDENT = ' '
current_indent = INDENT * indent
next_indent = INDENT * (indent + 1)
if path in self.diff_paths and path not in self._colorize_skip_paths:
diff_type, old, new = self.diff_paths[path]
if diff_type == 'changed':
return f"{RED}{self._format_value(old)}{RESET} -> {GREEN}{self._format_value(new)}{RESET}"
elif diff_type == 'added':
return f"{GREEN}{self._format_value(new)}{RESET}"
elif diff_type == 'removed':
return f"{RED}{self._format_value(old)}{RESET}"
if isinstance(obj, (dict, list)) and self.compact and not self._has_differences(path):
return '{...}' if isinstance(obj, dict) else '[...]'
if isinstance(obj, dict):
if not obj:
return '{}'
items = []
for key, value in obj.items():
new_path = f"{path}['{key}']" if isinstance(key, str) else f"{path}[{key}]"
if new_path in self.diff_paths and self.diff_paths[new_path][0] == 'added':
# Colorize both key and value for added fields
items.append(f'{next_indent}{GREEN}"{key}": {self._colorize_json(value, new_path, indent + 1)}{RESET}')
else:
items.append(f'{next_indent}"{key}": {self._colorize_json(value, new_path, indent + 1)}')
for key, value in self._get_path_removed(path).items():
new_path = f"{path}['{key}']" if isinstance(key, str) else f"{path}[{key}]"
items.append(f'{next_indent}{RED}"{key}": {self._colorize_json(value, new_path, indent + 1)}{RESET}')
return '{\n' + ',\n'.join(items) + f'\n{current_indent}' + '}'
elif isinstance(obj, (list, tuple)):
removed_map = self._get_path_removed(path)
if not obj and not removed_map:
return '[]'
for index in removed_map:
self._colorize_skip_paths.add(f"{path}[{index}]")
items = []
remove_index = 0
for index, value in enumerate(obj):
while remove_index == next(iter(removed_map), None):
items.append(f'{next_indent}{RED}{self._format_value(removed_map.pop(remove_index))}{RESET}')
remove_index += 1
items.append(f'{next_indent}{self._colorize_json(value, f"{path}[{index}]", indent + 1)}')
remove_index += 1
for value in removed_map.values():
items.append(f'{next_indent}{RED}{self._format_value(value)}{RESET}')
return '[\n' + ',\n'.join(items) + f'\n{current_indent}' + ']'
else:
return self._format_value(obj)
def __str__(self) -> str:
"""Return the colorized, pretty-printed JSON string."""
self._colorize_skip_paths = set()
return self._colorize_json(self.t2)
def __iter__(self):
"""Make the view iterable by yielding the tree results."""
yield from self.tree.items()
qlustered-deepdiff-41c7265/deepdiff/commands.py 0000664 0000000 0000000 00000024223 15162412645 0021475 0 ustar 00root root 0000000 0000000 import click
import sys
from decimal import Decimal
from pprint import pprint
from deepdiff.diff import (
DeepDiff,
CUTOFF_DISTANCE_FOR_PAIRS_DEFAULT,
CUTOFF_INTERSECTION_FOR_PAIRS_DEFAULT,
logger
)
from deepdiff import Delta, DeepSearch, extract as deep_extract
from deepdiff.serialization import load_path_content, save_content_to_path
try:
import orjson
except ImportError:
orjson = None
@click.group()
def cli():
"""A simple command line tool."""
pass # pragma: no cover.
@cli.command()
@click.argument('t1', type=click.Path(exists=True, resolve_path=True))
@click.argument('t2', type=click.Path(exists=True, resolve_path=True))
@click.option('--cutoff-distance-for-pairs', required=False, default=CUTOFF_DISTANCE_FOR_PAIRS_DEFAULT, type=float, show_default=True)
@click.option('--cutoff-intersection-for-pairs', required=False, default=CUTOFF_INTERSECTION_FOR_PAIRS_DEFAULT, type=float, show_default=True)
@click.option('--cache-size', required=False, default=0, type=int, show_default=True)
@click.option('--cache-tuning-sample-size', required=False, default=0, type=int, show_default=True)
@click.option('--cache-purge-level', required=False, default=1, type=click.IntRange(0, 2), show_default=True)
@click.option('--create-patch', is_flag=True, show_default=True)
@click.option('--exclude-paths', required=False, type=str, show_default=False, multiple=True)
@click.option('--exclude-regex-paths', required=False, type=str, show_default=False, multiple=True)
@click.option('--math-epsilon', required=False, type=Decimal, show_default=False)
@click.option('--get-deep-distance', is_flag=True, show_default=True)
@click.option('--group-by', required=False, type=str, show_default=False, multiple=False)
@click.option('--ignore-order', is_flag=True, show_default=True)
@click.option('--ignore-string-type-changes', is_flag=True, show_default=True)
@click.option('--ignore-numeric-type-changes', is_flag=True, show_default=True)
@click.option('--ignore-type-subclasses', is_flag=True, show_default=True)
@click.option('--ignore-string-case', is_flag=True, show_default=True)
@click.option('--ignore-nan-inequality', is_flag=True, show_default=True)
@click.option('--include-private-variables', is_flag=True, show_default=True)
@click.option('--log-frequency-in-sec', required=False, default=0, type=int, show_default=True)
@click.option('--max-passes', required=False, default=10000000, type=int, show_default=True)
@click.option('--max_diffs', required=False, default=None, type=int, show_default=True)
@click.option('--threshold-to-diff-deeper', required=False, default=0.33, type=float, show_default=False)
@click.option('--number-format-notation', required=False, type=click.Choice(['f', 'e'], case_sensitive=True), show_default=True, default="f")
@click.option('--progress-logger', required=False, type=click.Choice(['info', 'error'], case_sensitive=True), show_default=True, default="info")
@click.option('--report-repetition', is_flag=True, show_default=True)
@click.option('--significant-digits', required=False, default=None, type=int, show_default=True)
@click.option('--truncate-datetime', required=False, type=click.Choice(['second', 'minute', 'hour', 'day'], case_sensitive=True), show_default=True, default=None)
@click.option('--verbose-level', required=False, default=1, type=click.IntRange(0, 2), show_default=True)
@click.option('--view', required=False, type=click.Choice(['tree', 'colored', 'colored_compact'], case_sensitive=True), show_default=True, default='tree')
@click.option('--debug', is_flag=True, show_default=False)
def diff(
*args, **kwargs
):
"""
Deep Diff Commandline
Deep Difference of content in files.
It can read csv, tsv, json, yaml, and toml files.
T1 and T2 are the path to the files to be compared with each other.
"""
debug = kwargs.pop('debug')
kwargs['ignore_private_variables'] = not kwargs.pop('include_private_variables')
kwargs['progress_logger'] = logger.info if kwargs['progress_logger'] == 'info' else logger.error
create_patch = kwargs.pop('create_patch')
t1_path = kwargs.pop("t1")
t2_path = kwargs.pop("t2")
t1_extension = t1_path.split('.')[-1]
t2_extension = t2_path.split('.')[-1]
if "view" in kwargs and kwargs["view"] is None:
kwargs.pop("view")
for name, t_path, t_extension in [('t1', t1_path, t1_extension), ('t2', t2_path, t2_extension)]:
try:
kwargs[name] = load_path_content(t_path, file_type=t_extension)
except Exception as e: # pragma: no cover.
if debug: # pragma: no cover.
raise # pragma: no cover.
else: # pragma: no cover.
sys.exit(str(f"Error when loading {name}: {e}")) # pragma: no cover.
# if (t1_extension != t2_extension):
if t1_extension in {'csv', 'tsv'}:
kwargs['t1'] = [dict(i) for i in kwargs['t1']]
if t2_extension in {'csv', 'tsv'}:
kwargs['t2'] = [dict(i) for i in kwargs['t2']]
if create_patch:
# Disabling logging progress since it will leak into stdout
kwargs['log_frequency_in_sec'] = 0
try:
diff = DeepDiff(**kwargs)
except Exception as e: # pragma: no cover. No need to test this.
sys.exit(str(e)) # pragma: no cover. No need to test this.
if create_patch:
try:
delta = Delta(diff)
except Exception as e: # pragma: no cover.
if debug: # pragma: no cover.
raise # pragma: no cover.
else: # pragma: no cover.
sys.exit(f"Error when loading the patch (aka delta): {e}") # pragma: no cover.
# printing into stdout
sys.stdout.buffer.write(delta.dumps())
else:
try:
if kwargs["view"] in {'colored', 'colored_compact'}:
print(diff)
else:
print(diff.to_json(indent=2))
except Exception:
pprint(diff, indent=2)
@cli.command()
@click.argument('path', type=click.Path(exists=True, resolve_path=True))
@click.argument('delta_path', type=click.Path(exists=True, resolve_path=True))
@click.option('--backup', '-b', is_flag=True, show_default=True)
@click.option('--raise-errors', is_flag=True, show_default=True)
@click.option('--debug', is_flag=True, show_default=False)
def patch(
path, delta_path, backup, raise_errors, debug
):
"""
Deep Patch Commandline
Patches a file based on the information in a delta file.
The delta file can be created by the deep diff command and
passing the --create-patch argument.
Deep Patch is similar to Linux's patch command.
The difference is that it is made for patching data.
It can read csv, tsv, json, yaml, and toml files.
"""
try:
delta = Delta(delta_path=delta_path, raise_errors=raise_errors)
except Exception as e: # pragma: no cover.
if debug: # pragma: no cover.
raise # pragma: no cover.
else: # pragma: no cover.
sys.exit(str(f"Error when loading the patch (aka delta) {delta_path}: {e}")) # pragma: no cover.
extension = path.split('.')[-1]
try:
content = load_path_content(path, file_type=extension)
except Exception as e: # pragma: no cover.
sys.exit(str(f"Error when loading {path}: {e}")) # pragma: no cover.
result = delta + content
try:
save_content_to_path(result, path, file_type=extension, keep_backup=backup)
except Exception as e: # pragma: no cover.
if debug: # pragma: no cover.
raise # pragma: no cover.
else: # pragma: no cover.
sys.exit(str(f"Error when saving {path}: {e}")) # pragma: no cover.
@cli.command()
@click.argument('item', required=True, type=str)
@click.argument('path', type=click.Path(exists=True, resolve_path=True))
@click.option('--ignore-case', '-i', is_flag=True, show_default=True)
@click.option('--exact-match', is_flag=True, show_default=True)
@click.option('--exclude-paths', required=False, type=str, show_default=False, multiple=True)
@click.option('--exclude-regex-paths', required=False, type=str, show_default=False, multiple=True)
@click.option('--verbose-level', required=False, default=1, type=click.IntRange(0, 2), show_default=True)
@click.option('--debug', is_flag=True, show_default=False)
def grep(item, path, debug, **kwargs):
"""
Deep Grep Commandline
Grep through the contents of a file and find the path to the item.
It can read csv, tsv, json, yaml, and toml files.
"""
kwargs['case_sensitive'] = not kwargs.pop('ignore_case')
kwargs['match_string'] = kwargs.pop('exact_match')
try:
content = load_path_content(path)
except Exception as e: # pragma: no cover.
if debug: # pragma: no cover.
raise # pragma: no cover.
else: # pragma: no cover.
sys.exit(str(f"Error when loading {path}: {e}")) # pragma: no cover.
try:
result = DeepSearch(content, item, **kwargs)
except Exception as e: # pragma: no cover.
if debug: # pragma: no cover.
raise # pragma: no cover.
else: # pragma: no cover.
sys.exit(str(f"Error when running deep search on {path}: {e}")) # pragma: no cover.
pprint(result, indent=2)
@cli.command()
@click.argument('path_inside', required=True, type=str)
@click.argument('path', type=click.Path(exists=True, resolve_path=True))
@click.option('--debug', is_flag=True, show_default=False)
def extract(path_inside, path, debug):
"""
Deep Extract Commandline
Extract an item from a file based on the path that is passed.
It can read csv, tsv, json, yaml, and toml files.
"""
try:
content = load_path_content(path)
except Exception as e: # pragma: no cover.
if debug: # pragma: no cover.
raise # pragma: no cover.
else: # pragma: no cover.
sys.exit(str(f"Error when loading {path}: {e}")) # pragma: no cover.
try:
result = deep_extract(content, path_inside)
except Exception as e: # pragma: no cover.
if debug: # pragma: no cover.
raise # pragma: no cover.
else: # pragma: no cover.
sys.exit(str(f"Error when running deep search on {path}: {e}")) # pragma: no cover.
pprint(result, indent=2)
qlustered-deepdiff-41c7265/deepdiff/deephash.py 0000664 0000000 0000000 00000074627 15162412645 0021472 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
import logging
import datetime
import uuid
from typing import Union, Optional, Any, List, TYPE_CHECKING, Dict, Tuple, Set, Callable, Generator
from collections.abc import Iterable, MutableMapping
from collections import defaultdict
from hashlib import sha1, sha256
from pathlib import Path
from enum import Enum
import re
from deepdiff.helper import (strings, numbers, only_numbers, times, unprocessed, not_hashed, add_to_frozen_set,
convert_item_or_items_into_set_else_none, get_doc, ipranges,
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,
get_truncate_datetime, dict_, add_root_to_paths, PydanticBaseModel)
from deepdiff.base import Base
if TYPE_CHECKING:
from pytz.tzinfo import BaseTzInfo
import numpy as np
# Type aliases for better readability
HashableType = Union[str, int, float, bytes, bool, tuple, frozenset, type(None)]
HashResult = Union[str, Any] # Can be string hash or unprocessed marker
HashTuple = Tuple[HashResult, int] # (hash_result, count)
HashesDict = Dict[Any, Union[HashTuple, List[Any]]] # Special case for UNPROCESSED_KEY
PathType = Union[str, List[str], Set[str]]
RegexType = Union[str, re.Pattern[str], List[Union[str, re.Pattern[str]]]]
NumberToStringFunc = Callable[..., str] # More flexible for different number_to_string implementations
try:
import pandas
except ImportError:
pandas = False # type: ignore
try:
import polars
except ImportError:
polars = False # type: ignore
try:
import numpy as np
booleanTypes: Tuple[type, ...] = (bool, np.bool_) # type: ignore
except ImportError:
booleanTypes = (bool,) # type: ignore
logger: logging.Logger = logging.getLogger(__name__)
UNPROCESSED_KEY: object = object()
EMPTY_FROZENSET: frozenset = frozenset()
INDEX_VS_ATTRIBUTE: Tuple[str, str] = ('[%s]', '.%s')
HASH_LOOKUP_ERR_MSG: str = '{} is not one of the hashed items.'
def sha256hex(obj: Union[str, bytes]) -> str:
"""Use Sha256 as a cryptographic hash."""
if isinstance(obj, str):
obj = obj.encode('utf-8')
return sha256(obj).hexdigest()
def sha1hex(obj: Union[str, bytes]) -> str:
"""Use Sha1 as a cryptographic hash."""
if isinstance(obj, str):
obj = obj.encode('utf-8')
return sha1(obj).hexdigest()
default_hasher: Callable[[Union[str, bytes]], str] = sha256hex
def combine_hashes_lists(items: List[List[str]], prefix: Union[str, bytes]) -> str:
"""
Combines lists of hashes into one hash
This can be optimized in future.
It needs to work with both murmur3 hashes (int) and sha256 (str)
Although murmur3 is not used anymore.
"""
if isinstance(prefix, bytes):
prefix = prefix.decode('utf-8')
hashes_bytes = b''
for item in items:
# In order to make sure the order of hashes in each item does not affect the hash
# we resort them.
hashes_bytes += (''.join(map(str, sorted(item))) + '--').encode('utf-8')
return prefix + str(default_hasher(hashes_bytes))
class BoolObj(Enum):
TRUE = 1
FALSE = 0
def prepare_string_for_hashing(
obj: Union[str, bytes, memoryview],
ignore_string_type_changes: bool = False,
ignore_string_case: bool = False,
encodings: Optional[List[str]] = None,
ignore_encoding_errors: bool = False,
) -> str:
"""
Clean type conversions
"""
original_type = obj.__class__.__name__
# https://docs.python.org/3/library/codecs.html#codecs.decode
errors_mode = 'ignore' if ignore_encoding_errors else 'strict'
if isinstance(obj, memoryview):
obj = obj.tobytes()
if isinstance(obj, bytes):
err = None
encodings = ['utf-8'] if encodings is None else encodings
encoded = False
for encoding in encodings:
try:
obj = obj.decode(encoding, errors=errors_mode)
encoded = True
break
except UnicodeDecodeError as er:
err = er
if not encoded and err is not None:
obj_decoded = obj.decode('utf-8', errors='ignore') # type: ignore
start = max(err.start - 20, 0)
start_prefix = ''
if start > 0:
start_prefix = '...'
end = err.end + 20
end_suffix = '...'
if end >= len(obj):
end = len(obj)
end_suffix = ''
raise UnicodeDecodeError(
err.encoding,
err.object,
err.start,
err.end,
f"{err.reason} in '{start_prefix}{obj_decoded[start:end]}{end_suffix}'. Please either pass ignore_encoding_errors=True or pass the encoding via encodings=['utf-8', '...']."
) from None
if not ignore_string_type_changes:
obj = KEY_TO_VAL_STR.format(original_type, obj)
if ignore_string_case:
obj = obj.lower()
return str(obj)
doc = get_doc('deephash_doc.rst')
class DeepHash(Base):
__doc__ = doc
# Class attributes
hashes: Dict[Any, Any]
exclude_types_tuple: Tuple[type, ...]
ignore_repetition: bool
exclude_paths: Optional[Set[str]]
include_paths: Optional[Set[str]]
exclude_regex_paths: Optional[List[re.Pattern[str]]]
hasher: Callable[[Union[str, bytes]], str]
use_enum_value: bool
default_timezone: Union[datetime.timezone, "BaseTzInfo"]
significant_digits: Optional[int]
truncate_datetime: Optional[str]
number_format_notation: str
ignore_type_in_groups: Any
ignore_string_type_changes: bool
ignore_numeric_type_changes: bool
ignore_string_case: bool
exclude_obj_callback: Optional[Callable[[Any, str], bool]]
apply_hash: bool
type_check_func: Callable[[type, Any], bool]
number_to_string: Any
ignore_private_variables: bool
encodings: Optional[List[str]]
ignore_encoding_errors: bool
ignore_iterable_order: bool
custom_operators: Optional[List[Any]]
def __init__(self,
obj: Any,
*,
apply_hash: bool = True,
custom_operators: Optional[List[Any]] = None,
default_timezone: Union[datetime.timezone, "BaseTzInfo"] = datetime.timezone.utc,
encodings: Optional[List[str]] = None,
exclude_obj_callback: Optional[Callable[[Any, str], bool]] = None,
exclude_paths: Optional[PathType] = None,
exclude_regex_paths: Optional[RegexType] = None,
exclude_types: Optional[Union[List[type], Set[type], Tuple[type, ...]]] = None,
hasher: Optional[Callable[[Union[str, bytes]], str]] = None,
hashes: Optional[Union[Dict[Any, Any], "DeepHash"]] = None,
ignore_encoding_errors: bool = False,
ignore_iterable_order: bool = True,
ignore_numeric_type_changes: bool = False,
ignore_private_variables: bool = True,
ignore_repetition: bool = True,
ignore_string_case: bool = False,
ignore_string_type_changes: bool = False,
ignore_type_in_groups: Any = None,
ignore_type_subclasses: bool = False,
ignore_uuid_types: bool = False,
include_paths: Optional[PathType] = None,
number_format_notation: str = "f",
number_to_string_func: Optional[NumberToStringFunc] = None,
parent: str = "root",
significant_digits: Optional[int] = None,
truncate_datetime: Optional[str] = None,
use_enum_value: bool = False,
**kwargs) -> None:
if kwargs:
raise ValueError(
("The following parameter(s) are not valid: %s\n"
"The valid parameters are obj, hashes, exclude_types, significant_digits, truncate_datetime,"
"exclude_paths, include_paths, exclude_regex_paths, hasher, ignore_repetition, "
"number_format_notation, apply_hash, ignore_type_in_groups, ignore_string_type_changes, "
"ignore_numeric_type_changes, ignore_type_subclasses, ignore_string_case, ignore_uuid_types, "
"number_to_string_func, ignore_private_variables, parent, use_enum_value, default_timezone "
"encodings, ignore_encoding_errors") % ', '.join(kwargs.keys()))
if isinstance(hashes, MutableMapping):
self.hashes = hashes
elif isinstance(hashes, DeepHash):
self.hashes = hashes.hashes
else:
self.hashes = dict_()
exclude_types = set() if exclude_types is None else set(exclude_types)
self.exclude_types_tuple = tuple(exclude_types) # we need tuple for checking isinstance
self.ignore_repetition = ignore_repetition
self.exclude_paths = add_root_to_paths(convert_item_or_items_into_set_else_none(exclude_paths))
self.include_paths = add_root_to_paths(convert_item_or_items_into_set_else_none(include_paths))
self.exclude_regex_paths = convert_item_or_items_into_compiled_regexes_else_none(exclude_regex_paths)
self.hasher = default_hasher if hasher is None else hasher
self.hashes[UNPROCESSED_KEY] = [] # type: ignore
self.use_enum_value = use_enum_value
self.default_timezone = default_timezone
self.significant_digits = self.get_significant_digits(significant_digits, ignore_numeric_type_changes)
self.truncate_datetime = get_truncate_datetime(truncate_datetime)
self.number_format_notation = number_format_notation
self.ignore_type_in_groups = self.get_ignore_types_in_groups(
ignore_type_in_groups=ignore_type_in_groups,
ignore_string_type_changes=ignore_string_type_changes,
ignore_numeric_type_changes=ignore_numeric_type_changes,
ignore_type_subclasses=ignore_type_subclasses,
ignore_uuid_types=ignore_uuid_types,
)
self.ignore_string_type_changes = ignore_string_type_changes
self.ignore_numeric_type_changes = ignore_numeric_type_changes
self.ignore_string_case = ignore_string_case
self.exclude_obj_callback = exclude_obj_callback
# makes the hash return constant size result if true
# the only time it should be set to False is when
# testing the individual hash functions for different types of objects.
self.apply_hash = apply_hash
self.type_check_func = type_in_type_group if ignore_type_subclasses else type_is_subclass_of_type_group
# self.type_check_func = type_is_subclass_of_type_group if ignore_type_subclasses else type_in_type_group
self.number_to_string = number_to_string_func or number_to_string
self.ignore_private_variables = ignore_private_variables
self.encodings = encodings
self.ignore_encoding_errors = ignore_encoding_errors
self.ignore_iterable_order = ignore_iterable_order
self.custom_operators = custom_operators
self._hash(obj, parent=parent, parents_ids=frozenset({get_id(obj)}))
if self.hashes[UNPROCESSED_KEY]:
logger.warning("Can not hash the following items: {}.".format(self.hashes[UNPROCESSED_KEY]))
else:
del self.hashes[UNPROCESSED_KEY]
sha256hex: Callable[[Union[str, bytes]], str] = sha256hex
sha1hex: Callable[[Union[str, bytes]], str] = sha1hex
def __getitem__(self, obj: Any, extract_index: Optional[int] = 0) -> Any:
return self._getitem(self.hashes, obj, extract_index=extract_index,
use_enum_value=self.use_enum_value,
ignore_numeric_type_changes=self.ignore_numeric_type_changes)
@staticmethod
def _get_slots_dict(obj: Any) -> Dict[str, Any]:
"""Get a dict of initialized slot attributes.
Uses object.__getattribute__ to check each slot directly, bypassing
__getattr__. For uninitialized slots on classes that define __getattr__,
falls back to getattr — letting it raise if the object is truly broken.
"""
result = {}
has_getattr = hasattr(type(obj), '__getattr__')
for slot in obj.__slots__:
try:
result[slot] = object.__getattribute__(obj, slot)
except AttributeError:
if has_getattr:
# The slot isn't initialized, but the class defines __getattr__.
# Try the normal getattr to let __getattr__ provide a value or
# raise — if it raises, we propagate to fail the strategy.
result[slot] = getattr(obj, slot)
return result
@staticmethod
def _getitem(hashes: Dict[Any, Any], obj: Any, extract_index: Optional[int] = 0,
use_enum_value: bool = False, ignore_numeric_type_changes: bool = False) -> Any:
"""
extract_index is zero for hash and 1 for count and None to get them both.
To keep it backward compatible, we only get the hash by default so it is set to zero by default.
"""
key = obj
if obj is True:
key = BoolObj.TRUE
elif obj is False:
key = BoolObj.FALSE
elif use_enum_value and isinstance(obj, Enum):
key = obj.value
key = DeepHash._make_hash_key_for_lookup(key, ignore_numeric_type_changes=ignore_numeric_type_changes)
result_n_count: Tuple[Any, int] = (None, 0) # type: ignore
try:
result_n_count = hashes[key]
except (TypeError, KeyError):
key = get_id(obj)
try:
result_n_count = hashes[key]
except KeyError:
raise KeyError(HASH_LOOKUP_ERR_MSG.format(obj)) from None
if obj is UNPROCESSED_KEY:
extract_index = None
return result_n_count if extract_index is None else result_n_count[extract_index]
def __contains__(self, obj: Any) -> bool:
key = self._make_hash_key(obj)
result = False
try:
result = key in self.hashes
except (TypeError, KeyError):
result = False
if not result:
result = get_id(obj) in self.hashes
return result
def get(self, key: Any, default: Any = None, extract_index: Optional[int] = 0) -> Any:
"""
Get method for the hashes dictionary.
It can extract the hash for a given key that is already calculated when extract_index=0
or the count of items that went to building the object when extract_index=1.
"""
return self.get_key(self.hashes, key, default=default, extract_index=extract_index,
ignore_numeric_type_changes=self.ignore_numeric_type_changes)
@staticmethod
def get_key(hashes: Dict[Any, Any], key: Any, default: Any = None, extract_index: Optional[int] = 0,
use_enum_value: bool = False, ignore_numeric_type_changes: bool = False) -> Any:
"""
get_key method for the hashes dictionary.
It can extract the hash for a given key that is already calculated when extract_index=0
or the count of items that went to building the object when extract_index=1.
"""
try:
result = DeepHash._getitem(hashes, key, extract_index=extract_index,
use_enum_value=use_enum_value,
ignore_numeric_type_changes=ignore_numeric_type_changes)
except KeyError:
result = default
return result
@staticmethod
def _unwrap_hash_key(key: Any) -> Any:
"""Unwrap a (type, value) hash key back to the original value for public API."""
if isinstance(key, tuple) and len(key) == 2 and isinstance(key[0], type) and isinstance(key[1], only_numbers):
return key[1]
return key
def _get_objects_to_hashes_dict(self, extract_index: Optional[int] = 0) -> Dict[Any, Any]:
"""
A dictionary containing only the objects to hashes,
or a dictionary of objects to the count of items that went to build them.
extract_index=0 for hashes and extract_index=1 for counts.
"""
result = dict_()
for key, value in self.hashes.items():
key = self._unwrap_hash_key(key)
if key is UNPROCESSED_KEY:
result[key] = value
else:
result[key] = value[extract_index]
return result
def __eq__(self, other: Any) -> bool:
if isinstance(other, DeepHash):
return self.hashes == other.hashes
else:
# We only care about the hashes
return self._get_objects_to_hashes_dict() == other
__req__ = __eq__
def __repr__(self) -> str:
"""
Hide the counts since it will be confusing to see them when they are hidden everywhere else.
"""
from deepdiff.summarize import summarize
return summarize(self._get_objects_to_hashes_dict(extract_index=0), max_length=500)
def __str__(self) -> str:
return str(self._get_objects_to_hashes_dict(extract_index=0))
def __bool__(self) -> bool:
return bool(self.hashes)
def keys(self) -> Any:
return [self._unwrap_hash_key(k) for k in self.hashes.keys()]
def values(self) -> Generator[Any, None, None]:
return (i[0] for i in self.hashes.values()) # Just grab the item and not its count
def items(self) -> Generator[Tuple[Any, Any], None, None]:
return ((self._unwrap_hash_key(i), v[0]) for i, v in self.hashes.items())
def _prep_obj(self, obj: Any, parent: str, parents_ids: frozenset = EMPTY_FROZENSET, is_namedtuple: bool = False, is_pydantic_object: bool = False) -> HashTuple:
"""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__)
if hasattr(obj, "__slots__"):
obj_to_dict_strategies.append(lambda o: DeepHash._get_slots_dict(o))
else:
import inspect
obj_to_dict_strategies.append(lambda o: dict(inspect.getmembers(o, lambda m: not inspect.isroutine(m))))
for get_dict in obj_to_dict_strategies:
try:
d = get_dict(obj)
break
except AttributeError:
pass
else:
self.hashes[UNPROCESSED_KEY].append(obj) # type: ignore
return (unprocessed, 0)
obj = d
result, counts = self._prep_dict(obj, parent=parent, parents_ids=parents_ids,
print_as_attribute=True, original_type=original_type)
result = "nt{}".format(result) if is_namedtuple else "obj{}".format(result)
return result, counts
def _skip_this(self, obj: Any, parent: str) -> bool:
skip = False
if self.exclude_paths and parent in self.exclude_paths:
skip = True
if self.include_paths and parent != 'root':
if parent not in self.include_paths:
skip = True
for prefix in self.include_paths:
if parent.startswith(prefix):
skip = False
break
elif self.exclude_regex_paths and any(
[exclude_regex_path.search(parent) for exclude_regex_path in self.exclude_regex_paths]): # type: ignore
skip = True
elif self.exclude_types_tuple and isinstance(obj, self.exclude_types_tuple):
skip = True
elif self.exclude_obj_callback and self.exclude_obj_callback(obj, parent):
skip = True
return skip
def _prep_dict(self, obj: Union[Dict[Any, Any], MutableMapping], parent: str, parents_ids: frozenset = EMPTY_FROZENSET, print_as_attribute: bool = False, original_type: Optional[type] = None) -> HashTuple:
result = []
counts = 1
key_text = "%s{}".format(INDEX_VS_ATTRIBUTE[print_as_attribute])
for key, item in obj.items():
counts += 1
# ignore private variables
if self.ignore_private_variables and isinstance(key, str) and key.startswith('__'):
continue
key_formatted = "'%s'" % key if not print_as_attribute and isinstance(key, strings) else key
key_in_report = key_text % (parent, key_formatted)
key_hash, _ = self._hash(key, parent=key_in_report, parents_ids=parents_ids)
if not key_hash:
continue
item_id = get_id(item)
if (parents_ids and item_id in parents_ids) or self._skip_this(item, parent=key_in_report):
continue
parents_ids_added = add_to_frozen_set(parents_ids, item_id)
hashed, count = self._hash(item, parent=key_in_report, parents_ids=parents_ids_added)
hashed = KEY_TO_VAL_STR.format(key_hash, hashed)
result.append(hashed)
counts += count
result.sort()
result = ';'.join(result)
if print_as_attribute:
type_ = original_type or type(obj)
type_str = type_.__name__
for type_group in self.ignore_type_in_groups:
if self.type_check_func(type_, type_group):
type_str = ','.join(map(lambda x: x.__name__, type_group))
break
else:
type_str = 'dict'
return "{}:{{{}}}".format(type_str, result), counts
def _prep_iterable(self, obj: Iterable[Any], parent: str, parents_ids: frozenset = EMPTY_FROZENSET) -> HashTuple:
counts = 1
result = defaultdict(int)
for i, item in enumerate(obj):
new_parent = "{}[{}]".format(parent, i)
if self._skip_this(item, parent=new_parent):
continue
item_id = get_id(item)
if parents_ids and item_id in parents_ids:
continue
parents_ids_added = add_to_frozen_set(parents_ids, item_id)
hashed, count = self._hash(item, parent=new_parent, parents_ids=parents_ids_added)
# counting repetitions
result[hashed] += 1
counts += count
if self.ignore_repetition:
result = list(result.keys())
else:
result = [
'{}|{}'.format(i, v) for i, v in result.items()
]
result = map(str, result) # making sure the result items are string so join command works.
if self.ignore_iterable_order:
result = sorted(result)
result = ','.join(result)
result = KEY_TO_VAL_STR.format(type(obj).__name__, result)
return result, counts
def _prep_bool(self, obj: bool) -> BoolObj:
return BoolObj.TRUE if obj else BoolObj.FALSE
def _prep_path(self, obj: Path) -> str:
type_ = obj.__class__.__name__
return KEY_TO_VAL_STR.format(type_, obj)
def _prep_number(self, obj: Union[int, float, complex]) -> str:
type_ = "number" if self.ignore_numeric_type_changes else obj.__class__.__name__
if self.significant_digits is not None:
obj = self.number_to_string(obj, significant_digits=self.significant_digits,
number_format_notation=self.number_format_notation) # type: ignore
return KEY_TO_VAL_STR.format(type_, obj)
def _prep_ipranges(self, obj) -> str:
type_ = 'iprange'
obj = str(obj)
return KEY_TO_VAL_STR.format(type_, obj)
def _prep_datetime(self, obj: datetime.datetime) -> str:
type_ = 'datetime'
obj = datetime_normalize(self.truncate_datetime, obj, default_timezone=self.default_timezone)
return KEY_TO_VAL_STR.format(type_, obj)
def _prep_date(self, obj: datetime.date) -> str:
type_ = 'datetime' # yes still datetime but it doesn't need normalization
return KEY_TO_VAL_STR.format(type_, obj)
def _prep_tuple(self, obj: tuple, parent: str, parents_ids: frozenset) -> HashTuple:
# Checking to see if it has _fields. Which probably means it is a named
# tuple.
try:
obj._asdict # type: ignore
# It must be a normal tuple
except AttributeError:
result, counts = self._prep_iterable(obj=obj, parent=parent, parents_ids=parents_ids)
# We assume it is a namedtuple then
else:
result, counts = self._prep_obj(obj, parent, parents_ids=parents_ids, is_namedtuple=True)
return result, counts
def _make_hash_key(self, obj: Any) -> Any:
"""
Create a key for the hashes dict that distinguishes numeric types.
In Python, 1 == 1.0 and hash(1) == hash(1.0), so int and float values
collide as dict keys. When ignore_numeric_type_changes is False, we wrap
numeric objects as (type, value) tuples so that each type gets its own
cache entry and its own hash.
"""
if not self.ignore_numeric_type_changes and isinstance(obj, only_numbers):
return (type(obj), obj)
return obj
@staticmethod
def _make_hash_key_for_lookup(obj: Any, ignore_numeric_type_changes: bool = False) -> Any:
"""Static version of _make_hash_key for use in static accessor methods."""
if not ignore_numeric_type_changes and isinstance(obj, only_numbers):
return (type(obj), obj)
return obj
def _hash(self, obj: Any, parent: str, parents_ids: frozenset = EMPTY_FROZENSET) -> HashTuple:
"""The main hash method"""
counts = 1
if self.custom_operators is not None:
for operator in self.custom_operators:
func = getattr(operator, 'normalize_value_for_hashing', None)
if func is None:
raise NotImplementedError(f"{operator.__class__.__name__} needs to define a normalize_value_for_hashing method to be compatible with ignore_order=True or iterable_compare_func.".format(operator))
else:
obj = func(parent, obj)
if isinstance(obj, booleanTypes):
obj = self._prep_bool(obj)
result = None
elif self.use_enum_value and isinstance(obj, Enum):
obj = obj.value
else:
result = not_hashed
hash_key = self._make_hash_key(obj)
try:
result, counts = self.hashes[hash_key]
except (TypeError, KeyError):
pass
else:
return result, counts
if self._skip_this(obj, parent):
return None, 0
elif obj is None:
result = 'NONE'
elif isinstance(obj, strings):
result = prepare_string_for_hashing(
obj,
ignore_string_type_changes=self.ignore_string_type_changes,
ignore_string_case=self.ignore_string_case,
encodings=self.encodings,
ignore_encoding_errors=self.ignore_encoding_errors,
)
elif isinstance(obj, Path):
result = self._prep_path(obj)
elif isinstance(obj, times):
result = self._prep_datetime(obj) # type: ignore
elif isinstance(obj, datetime.date):
result = self._prep_date(obj)
elif isinstance(obj, numbers): # type: ignore
result = self._prep_number(obj)
elif isinstance(obj, ipranges):
result = self._prep_ipranges(obj)
elif isinstance(obj, uuid.UUID):
# Handle UUID objects (including uuid6.UUID) by using their integer value
result = str(obj.int)
elif isinstance(obj, MutableMapping):
result, counts = self._prep_dict(obj=obj, parent=parent, parents_ids=parents_ids)
elif isinstance(obj, tuple):
result, counts = self._prep_tuple(obj=obj, parent=parent, parents_ids=parents_ids)
elif (pandas and isinstance(obj, pandas.DataFrame)): # type: ignore
def gen(): # type: ignore
yield ('dtype', obj.dtypes) # type: ignore
yield ('index', obj.index) # type: ignore
yield from obj.items() # type: ignore # which contains (column name, series tuples)
result, counts = self._prep_iterable(obj=gen(), parent=parent, parents_ids=parents_ids)
elif (polars and isinstance(obj, polars.DataFrame)): # type: ignore
def gen():
yield from obj.columns # type: ignore
yield from list(obj.schema.items()) # type: ignore
yield from obj.rows() # type: ignore
result, counts = self._prep_iterable(obj=gen(), parent=parent, parents_ids=parents_ids)
elif isinstance(obj, Iterable):
result, counts = self._prep_iterable(obj=obj, parent=parent, parents_ids=parents_ids)
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)
if result is not_hashed: # pragma: no cover
self.hashes[UNPROCESSED_KEY].append(obj) # type: ignore
elif result is unprocessed:
pass
elif self.apply_hash:
if isinstance(obj, strings):
result_cleaned = result
else:
result_cleaned = prepare_string_for_hashing(
str(result), ignore_string_type_changes=self.ignore_string_type_changes,
ignore_string_case=self.ignore_string_case)
result = self.hasher(result_cleaned)
# It is important to keep the hash of all objects.
# The hashes will be later used for comparing the objects.
# Object to hash when possible otherwise ObjectID to hash
try:
self.hashes[hash_key] = (result, counts)
except TypeError:
obj_id = get_id(obj)
self.hashes[obj_id] = (result, counts)
return result, counts
if __name__ == "__main__": # pragma: no cover
import doctest
doctest.testmod()
qlustered-deepdiff-41c7265/deepdiff/delta.py 0000664 0000000 0000000 00000163273 15162412645 0020776 0 ustar 00root root 0000000 0000000 import copy
import logging
from typing import List, Dict, IO, Callable, Set, Union, Optional, Any, cast
from functools import partial, cmp_to_key
from collections.abc import Mapping
from copy import deepcopy
from deepdiff import DeepDiff
from deepdiff.serialization import pickle_load, pickle_dump
from deepdiff.helper import (
strings, numbers,
np_ndarray, np_array_factory, numpy_dtypes, get_doc,
not_found, numpy_dtype_string_to_type, dict_,
Opcode, FlatDeltaRow, FlatDeltaDict, UnkownValueCode, FlatDataAction,
OPCODE_TAG_TO_FLAT_DATA_ACTION,
FLAT_DATA_ACTION_TO_OPCODE_TAG,
SetOrdered,
)
from deepdiff.path import (
_path_to_elements, _get_nested_obj, _get_nested_obj_and_force,
GET, GETATTR, check_elem, parse_path, stringify_path,
)
from deepdiff.anyset import AnySet
from deepdiff.summarize import summarize
logger = logging.getLogger(__name__)
VERIFICATION_MSG = 'Expected the old value for {} to be {} but it is {}. Error found on: {}. You may want to set force=True, especially if this delta is created by passing flat_rows_list or flat_dict_list'
ELEM_NOT_FOUND_TO_ADD_MSG = 'Key or index of {} is not found for {} for setting operation.'
TYPE_CHANGE_FAIL_MSG = 'Unable to do the type change for {} from to type {} due to {}'
VERIFY_BIDIRECTIONAL_MSG = ('You have applied the delta to an object that has '
'different values than the original object the delta was made from.')
FAIL_TO_REMOVE_ITEM_IGNORE_ORDER_MSG = 'Failed to remove index[{}] on {}. It was expected to be {} but got {}'
DELTA_NUMPY_OPERATOR_OVERRIDE_MSG = (
'A numpy ndarray is most likely being added to a delta. '
'Due to Numpy override the + operator, you can only do: delta + ndarray '
'and NOT ndarray + delta')
BINIARY_MODE_NEEDED_MSG = "Please open the file in the binary mode and pass to Delta by passing 'b' in open(..., 'b'): {}"
DELTA_AT_LEAST_ONE_ARG_NEEDED = 'At least one of the diff, delta_path or delta_file arguments need to be passed.'
INVALID_ACTION_WHEN_CALLING_GET_ELEM = 'invalid action of {} when calling _get_elem_and_compare_to_old_value'
INVALID_ACTION_WHEN_CALLING_SIMPLE_SET_ELEM = 'invalid action of {} when calling _simple_set_elem_value'
INVALID_ACTION_WHEN_CALLING_SIMPLE_DELETE_ELEM = 'invalid action of {} when calling _simple_set_elem_value'
UNABLE_TO_GET_ITEM_MSG = 'Unable to get the item at {}: {}'
UNABLE_TO_GET_PATH_MSG = 'Unable to get the item at {}'
INDEXES_NOT_FOUND_WHEN_IGNORE_ORDER = 'Delta added to an incompatible object. Unable to add the following items at the specific indexes. {}'
NUMPY_TO_LIST = 'NUMPY_TO_LIST'
NOT_VALID_NUMPY_TYPE = "{} is not a valid numpy type."
doc = get_doc('delta.rst')
class DeltaError(ValueError):
"""
Delta specific errors
"""
pass
class DeltaNumpyOperatorOverrideError(ValueError):
"""
Delta Numpy Operator Override Error
"""
pass
class Delta:
__doc__ = doc
def __init__(
self,
diff: Union[DeepDiff, Mapping, str, bytes, None]=None,
delta_path: Optional[str]=None,
delta_file: Optional[IO]=None,
delta_diff: Optional[dict]=None,
flat_dict_list: Optional[List[Dict]]=None,
flat_rows_list: Optional[List[FlatDeltaRow]]=None,
deserializer: Callable=pickle_load,
log_errors: bool=True,
mutate: bool=False,
raise_errors: bool=False,
safe_to_import: Optional[Set[str]]=None,
serializer: Callable=pickle_dump,
verify_symmetry: Optional[bool]=None,
bidirectional: bool=False,
always_include_values: bool=False,
iterable_compare_func_was_used: Optional[bool]=None,
force: bool=False,
fill: Any=not_found,
):
# for pickle deserializer:
if hasattr(deserializer, '__code__') and 'safe_to_import' in set(deserializer.__code__.co_varnames):
_deserializer = deserializer
else:
def _deserializer(obj, safe_to_import=None):
result = deserializer(obj)
if result.get('_iterable_opcodes'):
_iterable_opcodes = {}
for path, op_codes in result['_iterable_opcodes'].items():
_iterable_opcodes[path] = []
for op_code in op_codes:
_iterable_opcodes[path].append(
Opcode(
**op_code
)
)
result['_iterable_opcodes'] = _iterable_opcodes
return result
self._reversed_diff = None
if verify_symmetry is not None:
logger.warning(
"DeepDiff Deprecation: use bidirectional instead of verify_symmetry parameter."
)
bidirectional = verify_symmetry
self.bidirectional = bidirectional
if bidirectional:
self.always_include_values = True # We need to include the values in bidirectional deltas
else:
self.always_include_values = always_include_values
if diff is not None:
if isinstance(diff, DeepDiff):
self.diff = diff._to_delta_dict(directed=not bidirectional, always_include_values=self.always_include_values)
elif isinstance(diff, Mapping):
self.diff = diff
elif isinstance(diff, strings):
self.diff = _deserializer(diff, safe_to_import=safe_to_import)
elif delta_path:
with open(delta_path, 'rb') as the_file:
content = the_file.read()
self.diff = _deserializer(content, safe_to_import=safe_to_import)
elif delta_diff:
self.diff = delta_diff
elif delta_file:
try:
content = delta_file.read()
except UnicodeDecodeError as e:
raise ValueError(BINIARY_MODE_NEEDED_MSG.format(e)) from None
self.diff = _deserializer(content, safe_to_import=safe_to_import)
elif flat_dict_list:
# Use copy to preserve original value of flat_dict_list in calling module
self.diff = self._from_flat_dicts(copy.deepcopy(flat_dict_list))
elif flat_rows_list:
self.diff = self._from_flat_rows(copy.deepcopy(flat_rows_list))
else:
raise ValueError(DELTA_AT_LEAST_ONE_ARG_NEEDED)
self.mutate = mutate
self.raise_errors = raise_errors
self.log_errors = log_errors
self._numpy_paths = self.diff.get('_numpy_paths', False)
# When we create the delta from a list of flat dictionaries, details such as iterable_compare_func_was_used get lost.
# That's why we allow iterable_compare_func_was_used to be explicitly set.
self._iterable_compare_func_was_used = self.diff.get('_iterable_compare_func_was_used', iterable_compare_func_was_used)
self.serializer = serializer
self.deserializer = deserializer
self.force = force
self.fill = fill
if force:
self.get_nested_obj = _get_nested_obj_and_force
else:
self.get_nested_obj = _get_nested_obj
self.reset()
def __repr__(self):
return "".format(summarize(self.diff, max_length=100))
def reset(self):
self.post_process_paths_to_convert = dict_()
def __add__(self, other):
if isinstance(other, numbers) and self._numpy_paths: # type: ignore
raise DeltaNumpyOperatorOverrideError(DELTA_NUMPY_OPERATOR_OVERRIDE_MSG)
if self.mutate:
self.root = other
else:
self.root = deepcopy(other)
self._do_pre_process()
self._do_values_changed()
self._do_set_item_added()
self._do_set_item_removed()
self._do_type_changes()
# NOTE: the remove iterable action needs to happen BEFORE
# all the other iterables to match the reverse of order of operations in DeepDiff
self._do_iterable_opcodes()
self._do_iterable_item_removed()
self._do_iterable_item_added()
self._do_ignore_order()
self._do_dictionary_item_added()
self._do_dictionary_item_removed()
self._do_attribute_added()
self._do_attribute_removed()
self._do_post_process()
other = self.root
# removing the reference to other
del self.root
self.reset()
return other
__radd__ = __add__
def __rsub__(self, other):
if self._reversed_diff is None:
self._reversed_diff = self._get_reverse_diff()
self.diff, self._reversed_diff = self._reversed_diff, self.diff
result = self.__add__(other)
self.diff, self._reversed_diff = self._reversed_diff, self.diff
return result
def _raise_or_log(self, msg, level='error'):
if self.log_errors:
getattr(logger, level)(msg)
if self.raise_errors:
raise DeltaError(msg)
def _do_verify_changes(self, path, expected_old_value, current_old_value):
if self.bidirectional and expected_old_value != current_old_value:
if isinstance(path, str):
path_str = path
else:
path_str = stringify_path(path, root_element=('', GETATTR))
self._raise_or_log(VERIFICATION_MSG.format(
path_str, expected_old_value, current_old_value, VERIFY_BIDIRECTIONAL_MSG))
def _get_elem_and_compare_to_old_value(
self,
obj,
path_for_err_reporting,
expected_old_value,
elem=None,
action=None,
forced_old_value=None,
next_element=None,
):
try:
check_elem(elem)
except ValueError as error:
self._raise_or_log(UNABLE_TO_GET_ITEM_MSG.format(path_for_err_reporting, error))
return not_found
# if forced_old_value is not None:
try:
if action == GET:
current_old_value = obj[elem]
elif action == GETATTR:
current_old_value = getattr(obj, elem) # type: ignore
else:
raise DeltaError(INVALID_ACTION_WHEN_CALLING_GET_ELEM.format(action))
except (KeyError, IndexError, AttributeError, TypeError) as e:
if self.force:
if forced_old_value is None:
if next_element is None or isinstance(next_element, str):
_forced_old_value = {}
else:
_forced_old_value = []
else:
_forced_old_value = forced_old_value
if action == GET:
if isinstance(obj, list):
if isinstance(elem, int) and elem < len(obj):
obj[elem] = _forced_old_value
else:
obj.append(_forced_old_value)
else:
obj[elem] = _forced_old_value
elif action == GETATTR:
setattr(obj, elem, _forced_old_value) # type: ignore
return _forced_old_value
current_old_value = not_found
if isinstance(path_for_err_reporting, (list, tuple)):
path_for_err_reporting = '.'.join([i[0] for i in path_for_err_reporting])
if self.bidirectional:
self._raise_or_log(VERIFICATION_MSG.format(
path_for_err_reporting,
expected_old_value, current_old_value, e))
else:
self._raise_or_log(UNABLE_TO_GET_PATH_MSG.format(
path_for_err_reporting))
return current_old_value
def _simple_set_elem_value(self, obj, path_for_err_reporting, elem=None, value=None, action=None):
"""
Set the element value directly on an object
"""
try:
if action == GET:
try:
obj[elem] = value
except IndexError:
if elem == len(obj):
obj.append(value)
elif self.fill is not not_found and elem > len(obj):
while len(obj) < elem:
if callable(self.fill):
obj.append(self.fill(obj, value, path_for_err_reporting))
else:
obj.append(self.fill)
obj.append(value)
else:
self._raise_or_log(ELEM_NOT_FOUND_TO_ADD_MSG.format(elem, path_for_err_reporting))
elif action == GETATTR:
setattr(obj, elem, value) # type: ignore
else:
raise DeltaError(INVALID_ACTION_WHEN_CALLING_SIMPLE_SET_ELEM.format(action))
except (KeyError, IndexError, AttributeError, TypeError) as e:
self._raise_or_log('Failed to set {} due to {}'.format(path_for_err_reporting, e))
def _coerce_obj(self, parent, obj, path, parent_to_obj_elem,
parent_to_obj_action, elements, to_type, from_type):
"""
Coerce obj and mark it in post_process_paths_to_convert for later to be converted back.
Also reassign it to its parent to replace the old object.
"""
self.post_process_paths_to_convert[elements[:-1]] = {'old_type': to_type, 'new_type': from_type}
# If this function is going to ever be used to convert numpy arrays, uncomment these lines:
# if from_type is np_ndarray:
# obj = obj.tolist()
# else:
obj = to_type(obj)
if parent:
# Making sure that the object is re-instated inside the parent especially if it was immutable
# and we had to turn it into a mutable one. In such cases the object has a new id.
self._simple_set_elem_value(obj=parent, path_for_err_reporting=path, elem=parent_to_obj_elem,
value=obj, action=parent_to_obj_action)
return obj
def _set_new_value(self, parent, parent_to_obj_elem, parent_to_obj_action,
obj, elements, path, elem, action, new_value):
"""
Set the element value on an object and if necessary convert the object to the proper mutable type
"""
if isinstance(obj, tuple):
# Check if it's a NamedTuple and use _replace() to generate a new copy with the change
if hasattr(obj, '_fields') and hasattr(obj, '_replace'):
if action == GETATTR:
obj = obj._replace(**{elem: new_value})
if parent:
self._simple_set_elem_value(obj=parent, path_for_err_reporting=path,
elem=parent_to_obj_elem, value=obj,
action=parent_to_obj_action)
return
else:
# Regular tuple - convert this object back to a tuple later
obj = self._coerce_obj(
parent, obj, path, parent_to_obj_elem,
parent_to_obj_action, elements,
to_type=list, from_type=tuple)
if elem != 0 and self.force and isinstance(obj, list) and len(obj) == 0:
# it must have been a dictionary
obj = {}
self._simple_set_elem_value(obj=parent, path_for_err_reporting=path, elem=parent_to_obj_elem,
value=obj, action=parent_to_obj_action)
self._simple_set_elem_value(obj=obj, path_for_err_reporting=path, elem=elem,
value=new_value, action=action)
def _simple_delete_elem(self, obj, path_for_err_reporting, elem=None, action=None):
"""
Delete the element directly on an object
"""
try:
if action == GET:
del obj[elem]
elif action == GETATTR:
del obj.__dict__[elem]
else:
raise DeltaError(INVALID_ACTION_WHEN_CALLING_SIMPLE_DELETE_ELEM.format(action))
except (KeyError, IndexError, AttributeError) as e:
self._raise_or_log('Failed to set {} due to {}'.format(path_for_err_reporting, e))
def _del_elem(self, parent, parent_to_obj_elem, parent_to_obj_action,
obj, elements, path, elem, action):
"""
Delete the element value on an object and if necessary convert the object to the proper mutable type
"""
obj_is_new = False
if isinstance(obj, tuple):
# convert this object back to a tuple later
self.post_process_paths_to_convert[elements[:-1]] = {'old_type': list, 'new_type': tuple}
obj = list(obj)
obj_is_new = True
self._simple_delete_elem(obj=obj, path_for_err_reporting=path, elem=elem, action=action)
if obj_is_new and parent:
# Making sure that the object is re-instated inside the parent especially if it was immutable
# and we had to turn it into a mutable one. In such cases the object has a new id.
self._simple_set_elem_value(obj=parent, path_for_err_reporting=path, elem=parent_to_obj_elem,
value=obj, action=parent_to_obj_action)
def _do_iterable_item_added(self):
iterable_item_added = self.diff.get('iterable_item_added', {})
iterable_item_moved = self.diff.get('iterable_item_moved')
# First we need to create a placeholder for moved items.
# This will then get replaced below after we go through added items.
# Without this items can get double added because moved store the new_value and does not need item_added replayed
if iterable_item_moved:
added_dict = {v["new_path"]: None for k, v in iterable_item_moved.items()}
iterable_item_added.update(added_dict)
if iterable_item_added:
self._do_item_added(iterable_item_added, insert=True)
if iterable_item_moved:
added_dict = {v["new_path"]: v["value"] for k, v in iterable_item_moved.items()}
self._do_item_added(added_dict, insert=False)
def _do_dictionary_item_added(self):
dictionary_item_added = self.diff.get('dictionary_item_added')
if dictionary_item_added:
self._do_item_added(dictionary_item_added, sort=False)
def _do_attribute_added(self):
attribute_added = self.diff.get('attribute_added')
if attribute_added:
self._do_item_added(attribute_added)
@staticmethod
def _sort_key_for_item_added(path_and_value):
elements = _path_to_elements(path_and_value[0])
# Example elements: [(4.3, 'GET'), ('b', 'GETATTR'), ('a3', 'GET')]
# We only care about the values in the elements not how to get the values.
return [i[0] for i in elements]
@staticmethod
def _sort_comparison(left, right):
"""
We use sort comparison instead of _sort_key_for_item_added when we run into comparing element types that can not
be compared with each other, such as None to None. Or integer to string.
"""
# Example elements: [(4.3, 'GET'), ('b', 'GETATTR'), ('a3', 'GET')]
# We only care about the values in the elements not how to get the values.
left_path = [i[0] for i in _path_to_elements(left[0], root_element=None)]
right_path = [i[0] for i in _path_to_elements(right[0], root_element=None)]
try:
if left_path < right_path:
return -1
elif left_path > right_path:
return 1
else:
return 0
except TypeError:
if len(left_path) > len(right_path):
left_path = left_path[:len(right_path)]
elif len(right_path) > len(left_path):
right_path = right_path[:len(left_path)]
for l_elem, r_elem in zip(left_path, right_path):
if type(l_elem) != type(r_elem) or type(l_elem) in None:
l_elem = str(l_elem)
r_elem = str(r_elem)
try:
if l_elem < r_elem:
return -1
elif l_elem > r_elem:
return 1
except TypeError:
continue
return 0
def _do_item_added(self, items, sort=True, insert=False):
if sort:
# sorting items by their path so that the items with smaller index
# are applied first (unless `sort` is `False` so that order of
# added items is retained, e.g. for dicts).
try:
items = sorted(items.items(), key=self._sort_key_for_item_added)
except TypeError:
items = sorted(items.items(), key=cmp_to_key(self._sort_comparison))
else:
items = items.items()
for path, new_value in items:
elem_and_details = self._get_elements_and_details(path)
if elem_and_details:
elements, parent, parent_to_obj_elem, parent_to_obj_action, obj, elem, action = elem_and_details
else:
continue # pragma: no cover. Due to cPython peephole optimizer, this line doesn't get covered. https://github.com/nedbat/coveragepy/issues/198
# Insert is only true for iterables, make sure it is a valid index.
if(insert and elem < len(obj)): # type: ignore
obj.insert(elem, None) # type: ignore
self._set_new_value(parent, parent_to_obj_elem, parent_to_obj_action,
obj, elements, path, elem, action, new_value)
def _do_values_changed(self):
values_changed = self.diff.get('values_changed')
if values_changed:
self._do_values_or_type_changed(values_changed)
def _do_type_changes(self):
type_changes = self.diff.get('type_changes')
if type_changes:
self._do_values_or_type_changed(type_changes, is_type_change=True)
def _do_post_process(self):
if self.post_process_paths_to_convert:
# Example: We had converted some object to be mutable and now we are converting them back to be immutable.
# We don't need to check the change because it is not really a change that was part of the original diff.
self._do_values_or_type_changed(self.post_process_paths_to_convert, is_type_change=True, verify_changes=False)
def _do_pre_process(self):
if self._numpy_paths and ('iterable_item_added' in self.diff or 'iterable_item_removed' in self.diff):
preprocess_paths = dict_()
for path, type_ in self._numpy_paths.items(): # type: ignore
preprocess_paths[path] = {'old_type': np_ndarray, 'new_type': list}
try:
type_ = numpy_dtype_string_to_type(type_)
except Exception as e:
self._raise_or_log(NOT_VALID_NUMPY_TYPE.format(e))
continue # pragma: no cover. Due to cPython peephole optimizer, this line doesn't get covered. https://github.com/nedbat/coveragepy/issues/198
self.post_process_paths_to_convert[path] = {'old_type': list, 'new_type': type_}
if preprocess_paths:
self._do_values_or_type_changed(preprocess_paths, is_type_change=True)
def _get_elements_and_details(self, path):
try:
elements = _path_to_elements(path)
if len(elements) > 1:
elements_subset = elements[:-2]
if len(elements_subset) != len(elements):
next_element = elements[-2][0]
next2_element = elements[-1][0]
else:
next_element = None
parent = self.get_nested_obj(obj=self, elements=elements_subset, next_element=next_element)
parent_to_obj_elem, parent_to_obj_action = elements[-2]
obj = self._get_elem_and_compare_to_old_value(
obj=parent, path_for_err_reporting=path, expected_old_value=None,
elem=parent_to_obj_elem, action=parent_to_obj_action, next_element=next2_element) # type: ignore
else:
# parent = self
# obj = self.root
# parent_to_obj_elem = 'root'
# parent_to_obj_action = GETATTR
parent = parent_to_obj_elem = parent_to_obj_action = None
obj = self
# obj = self.get_nested_obj(obj=self, elements=elements[:-1])
elem, action = elements[-1] # type: ignore
check_elem(elem)
except Exception as e:
self._raise_or_log(UNABLE_TO_GET_ITEM_MSG.format(path, e))
return None
else:
if obj is not_found:
return None
return elements, parent, parent_to_obj_elem, parent_to_obj_action, obj, elem, action
def _do_values_or_type_changed(self, changes, is_type_change=False, verify_changes=True):
compare_func_was_used = self.diff.get('_iterable_compare_func_was_used', False)
for path, value in changes.items():
# When iterable_compare_func is used, DiffLevel.path() inverts use_t2 for
# moved items (see model.py DiffLevel.path). This means dict keys here are
# actually t2 paths and new_path holds the t1 path. Apply at t1 so we
# don't access indices that don't exist yet or modify the wrong item.
apply_path = value['new_path'] if (compare_func_was_used and value.get('new_path')) else path
elem_and_details = self._get_elements_and_details(apply_path)
if elem_and_details:
elements, parent, parent_to_obj_elem, parent_to_obj_action, obj, elem, action = elem_and_details
else:
continue # pragma: no cover. Due to cPython peephole optimizer, this line doesn't get covered. https://github.com/nedbat/coveragepy/issues/198
expected_old_value = value.get('old_value', not_found)
current_old_value = self._get_elem_and_compare_to_old_value(
obj=obj, path_for_err_reporting=path, expected_old_value=expected_old_value, elem=elem, action=action)
if current_old_value is not_found:
continue # pragma: no cover. I have not been able to write a test for this case. But we should still check for it.
# With type change if we could have originally converted the type from old_value
# to new_value just by applying the class of the new_value, then we might not include the new_value
# in the delta dictionary. That is defined in Model.DeltaResult._from_tree_type_changes
if is_type_change and 'new_value' not in value:
try:
new_type = value['new_type']
# in case of Numpy we pass the ndarray plus the dtype in a tuple
if new_type in numpy_dtypes:
new_value = np_array_factory(current_old_value, new_type)
else:
new_value = new_type(current_old_value)
except Exception as e:
self._raise_or_log(TYPE_CHANGE_FAIL_MSG.format(obj[elem], value.get('new_type', 'unknown'), e)) # type: ignore
continue
else:
new_value = value['new_value']
self._set_new_value(parent, parent_to_obj_elem, parent_to_obj_action,
obj, elements, path, elem, action, new_value)
if verify_changes:
self._do_verify_changes(path, expected_old_value, current_old_value)
def _do_item_removed(self, items):
"""
Handle removing items.
"""
# Sorting the iterable_item_removed in reverse order based on the paths.
# So that we delete a bigger index before a smaller index
try:
sorted_item = sorted(items.items(), key=self._sort_key_for_item_added, reverse=True)
except TypeError:
sorted_item = sorted(items.items(), key=cmp_to_key(self._sort_comparison), reverse=True)
for path, expected_old_value in sorted_item:
elem_and_details = self._get_elements_and_details(path)
if elem_and_details:
elements, parent, parent_to_obj_elem, parent_to_obj_action, obj, elem, action = elem_and_details
else:
continue # pragma: no cover. Due to cPython peephole optimizer, this line doesn't get covered. https://github.com/nedbat/coveragepy/issues/198
look_for_expected_old_value = False
current_old_value = not_found
try:
if action == GET:
current_old_value = obj[elem] # type: ignore
elif action == GETATTR:
current_old_value = getattr(obj, elem)
look_for_expected_old_value = current_old_value != expected_old_value
except (KeyError, IndexError, AttributeError, TypeError):
look_for_expected_old_value = True
if look_for_expected_old_value and isinstance(obj, list) and not self._iterable_compare_func_was_used:
# It may return None if it doesn't find it
elem = self._find_closest_iterable_element_for_index(obj, elem, expected_old_value)
if elem is not None:
current_old_value = expected_old_value
if current_old_value is not_found or elem is None:
continue
self._del_elem(parent, parent_to_obj_elem, parent_to_obj_action,
obj, elements, path, elem, action)
self._do_verify_changes(path, expected_old_value, current_old_value)
def _find_closest_iterable_element_for_index(self, obj, elem, expected_old_value):
closest_elem = None
closest_distance = float('inf')
for index, value in enumerate(obj):
dist = abs(index - elem)
if dist > closest_distance:
break
if value == expected_old_value and dist < closest_distance:
closest_elem = index
closest_distance = dist
return closest_elem
def _do_iterable_opcodes(self):
_iterable_opcodes = self.diff.get('_iterable_opcodes', {})
if _iterable_opcodes:
for path, opcodes in _iterable_opcodes.items():
transformed = []
# elements = _path_to_elements(path)
elem_and_details = self._get_elements_and_details(path)
if elem_and_details:
elements, parent, parent_to_obj_elem, parent_to_obj_action, obj, elem, action = elem_and_details
if parent is None:
parent = self
obj = self.root
parent_to_obj_elem = 'root'
parent_to_obj_action = GETATTR
else:
continue # pragma: no cover. Due to cPython peephole optimizer, this line doesn't get covered. https://github.com/nedbat/coveragepy/issues/198
# import pytest; pytest.set_trace()
obj = self.get_nested_obj(obj=self, elements=elements)
is_obj_tuple = isinstance(obj, tuple)
for opcode in opcodes:
if opcode.tag == 'replace':
# Replace items in list a[i1:i2] with b[j1:j2]
transformed.extend(opcode.new_values)
elif opcode.tag == 'delete':
# Delete items from list a[i1:i2], so we do nothing here
continue
elif opcode.tag == 'insert':
# Insert items from list b[j1:j2] into the new list
transformed.extend(opcode.new_values)
elif opcode.tag == 'equal':
# Items are the same in both lists, so we add them to the result
transformed.extend(obj[opcode.t1_from_index:opcode.t1_to_index]) # type: ignore
if is_obj_tuple:
obj = tuple(obj) # type: ignore
# Making sure that the object is re-instated inside the parent especially if it was immutable
# and we had to turn it into a mutable one. In such cases the object has a new id.
self._simple_set_elem_value(obj=parent, path_for_err_reporting=path, elem=parent_to_obj_elem,
value=obj, action=parent_to_obj_action)
else:
obj[:] = transformed # type: ignore
# obj = self.get_nested_obj(obj=self, elements=elements)
# for
def _do_iterable_item_removed(self):
iterable_item_removed = self.diff.get('iterable_item_removed', {})
iterable_item_moved = self.diff.get('iterable_item_moved')
if iterable_item_moved:
# These will get added back during items_added
removed_dict = {k: v["value"] for k, v in iterable_item_moved.items()}
iterable_item_removed.update(removed_dict)
if iterable_item_removed:
self._do_item_removed(iterable_item_removed)
def _do_dictionary_item_removed(self):
dictionary_item_removed = self.diff.get('dictionary_item_removed')
if dictionary_item_removed:
self._do_item_removed(dictionary_item_removed)
def _do_attribute_removed(self):
attribute_removed = self.diff.get('attribute_removed')
if attribute_removed:
self._do_item_removed(attribute_removed)
def _do_set_item_added(self):
items = self.diff.get('set_item_added')
if items:
self._do_set_or_frozenset_item(items, func='union')
def _do_set_item_removed(self):
items = self.diff.get('set_item_removed')
if items:
self._do_set_or_frozenset_item(items, func='difference')
def _do_set_or_frozenset_item(self, items, func):
for path, value in items.items():
elements = _path_to_elements(path)
parent = self.get_nested_obj(obj=self, elements=elements[:-1])
elem, action = elements[-1]
obj = self._get_elem_and_compare_to_old_value(
parent, path_for_err_reporting=path, expected_old_value=None, elem=elem, action=action, forced_old_value=set())
new_value = getattr(obj, func)(value)
if hasattr(parent, '_fields') and hasattr(parent, '_replace'):
# Handle parent NamedTuple by creating a new instance with _replace(). Will not work with nested objects.
new_parent = parent._replace(**{elem: new_value})
self.root = new_parent
else:
self._simple_set_elem_value(parent, path_for_err_reporting=path, elem=elem, value=new_value, action=action)
def _do_ignore_order_get_old(self, obj, remove_indexes_per_path, fixed_indexes_values, path_for_err_reporting):
"""
A generator that gets the old values in an iterable when the order was supposed to be ignored.
"""
old_obj_index = -1
max_len = len(obj) - 1
while old_obj_index < max_len:
old_obj_index += 1
current_old_obj = obj[old_obj_index]
if current_old_obj in fixed_indexes_values:
continue
if old_obj_index in remove_indexes_per_path:
expected_obj_to_delete = remove_indexes_per_path.pop(old_obj_index)
if current_old_obj == expected_obj_to_delete:
continue
else:
self._raise_or_log(FAIL_TO_REMOVE_ITEM_IGNORE_ORDER_MSG.format(
old_obj_index, path_for_err_reporting, expected_obj_to_delete, current_old_obj))
yield current_old_obj
def _do_ignore_order(self):
"""
't1': [5, 1, 1, 1, 6],
't2': [7, 1, 1, 1, 8],
'iterable_items_added_at_indexes': {
'root': {
0: 7,
4: 8
}
},
'iterable_items_removed_at_indexes': {
'root': {
4: 6,
0: 5
}
}
"""
fixed_indexes = self.diff.get('iterable_items_added_at_indexes', dict_())
remove_indexes = self.diff.get('iterable_items_removed_at_indexes', dict_())
paths = SetOrdered(fixed_indexes.keys()) | SetOrdered(remove_indexes.keys())
for path in paths: # type: ignore
# In the case of ignore_order reports, we are pointing to the container object.
# Thus we add a [0] to the elements so we can get the required objects and discard what we don't need.
elem_and_details = self._get_elements_and_details("{}[0]".format(path))
if elem_and_details:
_, parent, parent_to_obj_elem, parent_to_obj_action, obj, _, _ = elem_and_details
else:
continue # pragma: no cover. Due to cPython peephole optimizer, this line doesn't get covered. https://github.com/nedbat/coveragepy/issues/198
# copying both these dictionaries since we don't want to mutate them.
fixed_indexes_per_path = fixed_indexes.get(path, dict_()).copy()
remove_indexes_per_path = remove_indexes.get(path, dict_()).copy()
fixed_indexes_values = AnySet(fixed_indexes_per_path.values())
new_obj = []
# Numpy's NdArray does not like the bool function.
if isinstance(obj, np_ndarray):
there_are_old_items = obj.size > 0
else:
there_are_old_items = bool(obj)
old_item_gen = self._do_ignore_order_get_old(
obj, remove_indexes_per_path, fixed_indexes_values, path_for_err_reporting=path)
while there_are_old_items or fixed_indexes_per_path:
new_obj_index = len(new_obj)
if new_obj_index in fixed_indexes_per_path:
new_item = fixed_indexes_per_path.pop(new_obj_index)
new_obj.append(new_item)
elif there_are_old_items:
try:
new_item = next(old_item_gen)
except StopIteration:
there_are_old_items = False
else:
new_obj.append(new_item)
else:
# pop a random item from the fixed_indexes_per_path dictionary
self._raise_or_log(INDEXES_NOT_FOUND_WHEN_IGNORE_ORDER.format(fixed_indexes_per_path))
new_item = fixed_indexes_per_path.pop(next(iter(fixed_indexes_per_path)))
new_obj.append(new_item)
if isinstance(obj, tuple):
new_obj = tuple(new_obj)
# Making sure that the object is re-instated inside the parent especially if it was immutable
# and we had to turn it into a mutable one. In such cases the object has a new id.
self._simple_set_elem_value(obj=parent, path_for_err_reporting=path, elem=parent_to_obj_elem,
value=new_obj, action=parent_to_obj_action)
def _get_reverse_diff(self):
if not self.bidirectional:
raise ValueError('Please recreate the delta with bidirectional=True')
SIMPLE_ACTION_TO_REVERSE = {
'iterable_item_added': 'iterable_item_removed',
'iterable_items_added_at_indexes': 'iterable_items_removed_at_indexes',
'attribute_added': 'attribute_removed',
'set_item_added': 'set_item_removed',
'dictionary_item_added': 'dictionary_item_removed',
}
# Adding the reverse of the dictionary
for key in list(SIMPLE_ACTION_TO_REVERSE.keys()):
SIMPLE_ACTION_TO_REVERSE[SIMPLE_ACTION_TO_REVERSE[key]] = key
r_diff = {}
for action, info in self.diff.items():
reverse_action = SIMPLE_ACTION_TO_REVERSE.get(action)
if reverse_action:
r_diff[reverse_action] = info
elif action == 'values_changed':
r_diff[action] = {}
for path, path_info in info.items():
reverse_path = path_info['new_path'] if path_info.get('new_path') else path
r_diff[action][reverse_path] = {
'new_value': path_info['old_value'], 'old_value': path_info['new_value']
}
elif action == 'type_changes':
r_diff[action] = {}
for path, path_info in info.items():
reverse_path = path_info['new_path'] if path_info.get('new_path') else path
r_diff[action][reverse_path] = {
'old_type': path_info['new_type'], 'new_type': path_info['old_type'],
}
if 'new_value' in path_info:
r_diff[action][reverse_path]['old_value'] = path_info['new_value']
if 'old_value' in path_info:
r_diff[action][reverse_path]['new_value'] = path_info['old_value']
elif action == 'iterable_item_moved':
r_diff[action] = {}
for path, path_info in info.items():
old_path = path_info['new_path']
r_diff[action][old_path] = {
'new_path': path, 'value': path_info['value'],
}
elif action == '_iterable_opcodes':
r_diff[action] = {}
for path, op_codes in info.items():
r_diff[action][path] = []
for op_code in op_codes:
tag = op_code.tag
tag = {'delete': 'insert', 'insert': 'delete'}.get(tag, tag)
new_op_code = Opcode(
tag=tag,
t1_from_index=op_code.t2_from_index,
t1_to_index=op_code.t2_to_index,
t2_from_index=op_code.t1_from_index,
t2_to_index=op_code.t1_to_index,
new_values=op_code.old_values,
old_values=op_code.new_values,
)
r_diff[action][path].append(new_op_code)
return r_diff
def dump(self, file):
"""
Dump into file object
"""
# Small optimization: Our internal pickle serializer can just take a file object
# and directly write to it. However if a user defined serializer is passed
# we want to make it compatible with the expectation that self.serializer(self.diff)
# will give the user the serialization and then it can be written to
# a file object when using the dump(file) function.
param_names_of_serializer = set(self.serializer.__code__.co_varnames)
if 'file_obj' in param_names_of_serializer:
self.serializer(self.diff, file_obj=file)
else:
file.write(self.dumps())
def dumps(self):
"""
Return the serialized representation of the object as a bytes object, instead of writing it to a file.
"""
return self.serializer(self.diff)
def to_dict(self):
return dict(self.diff)
def _flatten_iterable_opcodes(self, _parse_path):
"""
Converts op_codes to FlatDeltaRows
"""
result = []
for path, op_codes in self.diff['_iterable_opcodes'].items():
for op_code in op_codes:
result.append(
FlatDeltaRow(
path=_parse_path(path),
action=OPCODE_TAG_TO_FLAT_DATA_ACTION[op_code.tag],
value=op_code.new_values,
old_value=op_code.old_values,
type=type(op_code.new_values),
old_type=type(op_code.old_values),
new_path=None,
t1_from_index=op_code.t1_from_index,
t1_to_index=op_code.t1_to_index,
t2_from_index=op_code.t2_from_index,
t2_to_index=op_code.t2_to_index,
)
)
return result
@staticmethod
def _get_flat_row(action, info, _parse_path, keys_and_funcs, report_type_changes=True):
for path, details in info.items():
row = {'path': _parse_path(path), 'action': action}
for key, new_key, func in keys_and_funcs:
if key in details:
if func:
row[new_key] = func(details[key])
else:
row[new_key] = details[key]
if report_type_changes:
if 'value' in row and 'type' not in row:
row['type'] = type(row['value'])
if 'old_value' in row and 'old_type' not in row:
row['old_type'] = type(row['old_value'])
yield FlatDeltaRow(**row)
@staticmethod
def _from_flat_rows(flat_rows_list: List[FlatDeltaRow]):
flat_dict_list = (i._asdict() for i in flat_rows_list)
return Delta._from_flat_dicts(flat_dict_list)
@staticmethod
def _from_flat_dicts(flat_dict_list):
"""
Create the delta's diff object from the flat_dict_list
"""
result = {}
FLATTENING_NEW_ACTION_MAP = {
'unordered_iterable_item_added': 'iterable_items_added_at_indexes',
'unordered_iterable_item_removed': 'iterable_items_removed_at_indexes',
}
for flat_dict in flat_dict_list:
index = None
action = flat_dict.get("action")
path = flat_dict.get("path")
value = flat_dict.get('value')
new_path = flat_dict.get('new_path')
old_value = flat_dict.get('old_value', UnkownValueCode)
if not action:
raise ValueError("Flat dict need to include the 'action'.")
if path is None:
raise ValueError("Flat dict need to include the 'path'.")
if action in FLATTENING_NEW_ACTION_MAP:
action = FLATTENING_NEW_ACTION_MAP[action]
index = path.pop()
if action in {
FlatDataAction.attribute_added,
FlatDataAction.attribute_removed,
}:
root_element = ('root', GETATTR)
else:
root_element = ('root', GET)
if isinstance(path, str):
path_str = path
else:
path_str = stringify_path(path, root_element=root_element) # We need the string path
if new_path and new_path != path:
new_path = stringify_path(new_path, root_element=root_element)
else:
new_path = None
if action not in result:
result[action] = {}
if action in {
'iterable_items_added_at_indexes',
'iterable_items_removed_at_indexes',
}:
if path_str not in result[action]:
result[action][path_str] = {}
result[action][path_str][index] = value
elif action in {
FlatDataAction.set_item_added,
FlatDataAction.set_item_removed
}:
if path_str not in result[action]:
result[action][path_str] = set()
result[action][path_str].add(value)
elif action in {
FlatDataAction.dictionary_item_added,
FlatDataAction.dictionary_item_removed,
FlatDataAction.attribute_removed,
FlatDataAction.attribute_added,
FlatDataAction.iterable_item_added,
FlatDataAction.iterable_item_removed,
}:
result[action][path_str] = value
elif action == 'values_changed':
if old_value == UnkownValueCode:
result[action][path_str] = {'new_value': value}
else:
result[action][path_str] = {'new_value': value, 'old_value': old_value}
elif action == 'type_changes':
type_ = flat_dict.get('type', UnkownValueCode)
old_type = flat_dict.get('old_type', UnkownValueCode)
result[action][path_str] = {'new_value': value}
for elem, elem_value in [
('new_type', type_),
('old_type', old_type),
('old_value', old_value),
]:
if elem_value != UnkownValueCode:
result[action][path_str][elem] = elem_value
elif action == FlatDataAction.iterable_item_moved:
result[action][path_str] = {'value': value}
elif action in {
FlatDataAction.iterable_items_inserted,
FlatDataAction.iterable_items_deleted,
FlatDataAction.iterable_items_replaced,
FlatDataAction.iterable_items_equal,
}:
if '_iterable_opcodes' not in result:
result['_iterable_opcodes'] = {}
if path_str not in result['_iterable_opcodes']:
result['_iterable_opcodes'][path_str] = []
result['_iterable_opcodes'][path_str].append(
Opcode(
tag=FLAT_DATA_ACTION_TO_OPCODE_TAG[action], # type: ignore
t1_from_index=flat_dict.get('t1_from_index'),
t1_to_index=flat_dict.get('t1_to_index'),
t2_from_index=flat_dict.get('t2_from_index'),
t2_to_index=flat_dict.get('t2_to_index'),
new_values=flat_dict.get('value'),
old_values=flat_dict.get('old_value'),
)
)
if new_path:
result[action][path_str]['new_path'] = new_path
return result
def to_flat_dicts(self, include_action_in_path=False, report_type_changes=True) -> List[FlatDeltaDict]:
"""
Returns a flat list of actions that is easily machine readable.
For example:
{'iterable_item_added': {'root[3]': 5, 'root[2]': 3}}
Becomes:
[
{'path': [3], 'value': 5, 'action': 'iterable_item_added'},
{'path': [2], 'value': 3, 'action': 'iterable_item_added'},
]
**Parameters**
include_action_in_path : Boolean, default=False
When False, we translate DeepDiff's paths like root[3].attribute1 into a [3, 'attribute1'].
When True, we include the action to retrieve the item in the path: [(3, 'GET'), ('attribute1', 'GETATTR')]
Note that the "action" here is the different than the action reported by to_flat_dicts. The action here is just about the "path" output.
report_type_changes : Boolean, default=True
If False, we don't report the type change. Instead we report the value change.
Example:
t1 = {"a": None}
t2 = {"a": 1}
dump = Delta(DeepDiff(t1, t2)).dumps()
delta = Delta(dump)
assert t2 == delta + t1
flat_result = delta.to_flat_dicts()
flat_expected = [{'path': ['a'], 'action': 'type_changes', 'value': 1, 'new_type': int, 'old_type': type(None)}]
assert flat_expected == flat_result
flat_result2 = delta.to_flat_dicts(report_type_changes=False)
flat_expected2 = [{'path': ['a'], 'action': 'values_changed', 'value': 1}]
**List of actions**
Here are the list of actions that the flat dictionary can return.
iterable_item_added
iterable_item_removed
iterable_item_moved
values_changed
type_changes
set_item_added
set_item_removed
dictionary_item_added
dictionary_item_removed
attribute_added
attribute_removed
"""
return cast(List[FlatDeltaDict], [
i._asdict() for i in self.to_flat_rows(include_action_in_path=include_action_in_path, report_type_changes=report_type_changes)
])
def to_flat_rows(self, include_action_in_path=False, report_type_changes=True) -> List[FlatDeltaRow]:
"""
Just like to_flat_dicts but returns FlatDeltaRow Named Tuples
"""
result = []
if include_action_in_path:
_parse_path = partial(parse_path, include_actions=True)
else:
_parse_path = parse_path
if report_type_changes:
keys_and_funcs = [
('value', 'value', None),
('new_value', 'value', None),
('old_value', 'old_value', None),
('new_type', 'type', None),
('old_type', 'old_type', None),
('new_path', 'new_path', _parse_path),
]
else:
if not self.always_include_values:
raise ValueError(
"When converting to flat dictionaries, if report_type_changes=False and there are type changes, "
"you must set the always_include_values=True at the delta object creation. Otherwise there is nothing to include."
)
keys_and_funcs = [
('value', 'value', None),
('new_value', 'value', None),
('old_value', 'old_value', None),
('new_path', 'new_path', _parse_path),
]
FLATTENING_NEW_ACTION_MAP = {
'iterable_items_added_at_indexes': 'unordered_iterable_item_added',
'iterable_items_removed_at_indexes': 'unordered_iterable_item_removed',
}
for action, info in self.diff.items():
if action == '_iterable_opcodes':
result.extend(self._flatten_iterable_opcodes(_parse_path=_parse_path))
continue
if action.startswith('_'):
continue
if action in FLATTENING_NEW_ACTION_MAP:
new_action = FLATTENING_NEW_ACTION_MAP[action]
for path, index_to_value in info.items():
path = _parse_path(path)
for index, value in index_to_value.items():
path2 = path.copy()
if include_action_in_path:
path2.append((index, 'GET')) # type: ignore
else:
path2.append(index)
if report_type_changes:
row = FlatDeltaRow(path=path2, value=value, action=new_action, type=type(value)) # type: ignore
else:
row = FlatDeltaRow(path=path2, value=value, action=new_action) # type: ignore
result.append(row)
elif action in {'set_item_added', 'set_item_removed'}:
for path, values in info.items():
path = _parse_path(path)
for value in values:
if report_type_changes:
row = FlatDeltaRow(path=path, value=value, action=action, type=type(value))
else:
row = FlatDeltaRow(path=path, value=value, action=action)
result.append(row)
elif action == 'dictionary_item_added':
for path, value in info.items():
path = _parse_path(path)
if isinstance(value, dict) and len(value) == 1:
new_key = next(iter(value))
path.append(new_key)
value = value[new_key]
elif isinstance(value, (list, tuple)) and len(value) == 1:
value = value[0]
path.append(0) # type: ignore
action = 'iterable_item_added'
elif isinstance(value, set) and len(value) == 1:
value = value.pop()
action = 'set_item_added'
if report_type_changes:
row = FlatDeltaRow(path=path, value=value, action=action, type=type(value)) # type: ignore
else:
row = FlatDeltaRow(path=path, value=value, action=action) # type: ignore
result.append(row)
elif action in {
'dictionary_item_removed', 'iterable_item_added',
'iterable_item_removed', 'attribute_removed', 'attribute_added'
}:
for path, value in info.items():
path = _parse_path(path)
if report_type_changes:
row = FlatDeltaRow(path=path, value=value, action=action, type=type(value))
else:
row = FlatDeltaRow(path=path, value=value, action=action)
result.append(row)
elif action == 'type_changes':
if not report_type_changes:
action = 'values_changed'
for row in self._get_flat_row(
action=action,
info=info,
_parse_path=_parse_path,
keys_and_funcs=keys_and_funcs,
report_type_changes=report_type_changes,
):
result.append(row)
else:
for row in self._get_flat_row(
action=action,
info=info,
_parse_path=_parse_path,
keys_and_funcs=keys_and_funcs,
report_type_changes=report_type_changes,
):
result.append(row)
return result
if __name__ == "__main__": # pragma: no cover
import doctest
doctest.testmod()
qlustered-deepdiff-41c7265/deepdiff/diff.py 0000775 0000000 0000000 00000274767 15162412645 0020633 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
# In order to run the docstrings:
# python3 -m deepdiff.diff
# You might need to run it many times since dictionaries come in different orders
# every time you run the docstrings.
# However the docstring expects it in a specific order in order to pass!
import difflib
import logging
import types
import datetime
import uuid
from enum import Enum
from copy import deepcopy
from math import isclose as is_close
from typing import List, Dict, Callable, Union, Any, Pattern, Tuple, Optional, Set, FrozenSet, TYPE_CHECKING, Protocol, Literal
from collections.abc import Mapping, Iterable, Sequence
from collections import defaultdict
from inspect import getmembers
from itertools import zip_longest
from functools import lru_cache
from deepdiff.helper import (strings, bytes_type, numbers, uuids, ListItemRemovedOrAdded, notpresent,
IndexedHash, unprocessed, add_to_frozen_set, basic_types,
convert_item_or_items_into_set_else_none, get_type,
convert_item_or_items_into_compiled_regexes_else_none,
type_is_subclass_of_type_group, type_in_type_group, get_doc,
number_to_string, datetime_normalize, KEY_TO_VAL_STR, booleans,
np_ndarray, np_floating, get_numpy_ndarray_rows, RepeatedTimer,
TEXT_VIEW, TREE_VIEW, DELTA_VIEW, COLORED_VIEW, COLORED_COMPACT_VIEW,
detailed__dict__, add_root_to_paths,
np, get_truncate_datetime, dict_, CannotCompare, ENUM_INCLUDE_KEYS,
PydanticBaseModel, Opcode, SetOrdered, ipranges)
from deepdiff.serialization import SerializationMixin
from deepdiff.distance import DistanceMixin, logarithmic_similarity
from deepdiff.model import (
RemapDict, ResultDict, TextResult, TreeResult, DiffLevel,
DictRelationship, AttributeRelationship, REPORT_KEYS,
SubscriptableIterableRelationship, NonSubscriptableIterableRelationship,
SetRelationship, NumpyArrayRelationship, CUSTOM_FIELD,
FORCE_DEFAULT,
)
from deepdiff.deephash import DeepHash, combine_hashes_lists
from deepdiff.base import Base
from deepdiff.lfucache import LFUCache, DummyLFU
from deepdiff.colored_view import ColoredView
if TYPE_CHECKING:
from pytz.tzinfo import BaseTzInfo
logger = logging.getLogger(__name__)
MAX_PASSES_REACHED_MSG = (
'DeepDiff has reached the max number of passes of {}. '
'You can possibly get more accurate results by increasing the max_passes parameter.')
MAX_DIFFS_REACHED_MSG = (
'DeepDiff has reached the max number of diffs of {}. '
'You can possibly get more accurate results by increasing the max_diffs parameter.')
notpresent_indexed = IndexedHash(indexes=[0], item=notpresent)
doc = get_doc('diff_doc.rst')
PROGRESS_MSG = "DeepDiff {} seconds in progress. Pass #{}, Diff #{}"
def _report_progress(_stats: Dict[str, Any], progress_logger: Callable[[str], None], duration: float) -> None:
"""
Report the progress every few seconds.
"""
progress_logger(PROGRESS_MSG.format(duration, _stats[PASSES_COUNT], _stats[DIFF_COUNT]))
DISTANCE_CACHE_HIT_COUNT = 'DISTANCE CACHE HIT COUNT'
DIFF_COUNT = 'DIFF COUNT'
PASSES_COUNT = 'PASSES COUNT'
MAX_PASS_LIMIT_REACHED = 'MAX PASS LIMIT REACHED'
MAX_DIFF_LIMIT_REACHED = 'MAX DIFF LIMIT REACHED'
DISTANCE_CACHE_ENABLED = 'DISTANCE CACHE ENABLED'
PREVIOUS_DIFF_COUNT = 'PREVIOUS DIFF COUNT'
PREVIOUS_DISTANCE_CACHE_HIT_COUNT = 'PREVIOUS DISTANCE CACHE HIT COUNT'
CANT_FIND_NUMPY_MSG = 'Unable to import numpy. This must be a bug in DeepDiff since a numpy array is detected.'
INVALID_VIEW_MSG = "view parameter must be one of 'text', 'tree', 'delta', 'colored' or 'colored_compact'. But {} was passed."
CUTOFF_RANGE_ERROR_MSG = 'cutoff_distance_for_pairs needs to be a positive float max 1.'
VERBOSE_LEVEL_RANGE_MSG = 'verbose_level should be 0, 1, or 2.'
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
# What is the threshold to calculate pairs of items between 2 iterables.
# For example 2 iterables that have nothing in common, do not need their pairs to be calculated.
CUTOFF_INTERSECTION_FOR_PAIRS_DEFAULT = 0.7
DEEPHASH_PARAM_KEYS = (
'exclude_types',
'exclude_paths',
'include_paths',
'exclude_regex_paths',
'hasher',
'significant_digits',
'number_format_notation',
'ignore_string_type_changes',
'ignore_numeric_type_changes',
'ignore_uuid_types',
'use_enum_value',
'ignore_type_in_groups',
'ignore_type_subclasses',
'ignore_string_case',
'exclude_obj_callback',
'ignore_private_variables',
'encodings',
'ignore_encoding_errors',
'default_timezone',
'custom_operators',
)
class DeepDiffProtocol(Protocol):
t1: Any
t2: Any
cutoff_distance_for_pairs: float
use_log_scale: bool
log_scale_similarity_threshold: float
view: str
math_epsilon: Optional[float]
class DeepDiff(ResultDict, SerializationMixin, DistanceMixin, DeepDiffProtocol, Base):
__doc__ = doc
CACHE_AUTO_ADJUST_THRESHOLD = 0.25
def __init__(self,
t1: Any,
t2: Any,
_original_type: Optional[Any]=None,
cache_purge_level: int=1,
cache_size: int=0,
cache_tuning_sample_size: int=0,
custom_operators: Optional[List[Any]] =None,
cutoff_distance_for_pairs: float=CUTOFF_DISTANCE_FOR_PAIRS_DEFAULT,
cutoff_intersection_for_pairs: float=CUTOFF_INTERSECTION_FOR_PAIRS_DEFAULT,
default_timezone:Union[datetime.timezone, "BaseTzInfo"]=datetime.timezone.utc,
encodings: Optional[List[str]]=None,
exclude_obj_callback: Optional[Callable]=None,
exclude_obj_callback_strict: Optional[Callable]=None,
exclude_paths: Union[str, List[str], Set[str], FrozenSet[str], None]=None,
exclude_regex_paths: Union[str, List[str], Pattern[str], List[Pattern[str]], None]=None,
exclude_types: Optional[List[type]]=None,
get_deep_distance: bool=False,
group_by: Union[str, Tuple[str, str], Callable, None]=None,
group_by_sort_key: Union[str, Callable, None]=None,
hasher: Optional[Callable]=None,
hashes: Optional[Dict[Any, Any]]=None,
ignore_encoding_errors: bool=False,
ignore_nan_inequality: bool=False,
ignore_numeric_type_changes: bool=False,
ignore_order: bool=False,
ignore_order_func: Optional[Callable]=None,
ignore_private_variables: bool=True,
ignore_string_case: bool=False,
ignore_string_type_changes: bool=False,
ignore_type_in_groups: Optional[List[Tuple[Any, ...]]]=None,
ignore_type_subclasses: bool=False,
ignore_uuid_types: bool=False,
include_obj_callback: Optional[Callable]=None,
include_obj_callback_strict: Optional[Callable]=None,
include_paths: Union[str, List[str], None]=None,
iterable_compare_func: Optional[Callable]=None,
log_frequency_in_sec: int=0,
log_scale_similarity_threshold: float=0.1,
log_stacktrace: bool=False,
math_epsilon: Optional[float]=None,
max_diffs: Optional[int]=None,
max_passes: int=10000000,
number_format_notation: Literal["f", "e"]="f",
number_to_string_func: Optional[Callable]=None,
progress_logger: Callable[[str], None]=logger.info,
report_repetition: bool=False,
significant_digits: Optional[int]=None,
threshold_to_diff_deeper: float = 0.33,
truncate_datetime: Optional[str]=None,
use_enum_value: bool=False,
use_log_scale: bool=False,
verbose_level: int=1,
view: str=TEXT_VIEW,
zip_ordered_iterables: bool=False,
_parameters: Optional[Dict[str, Any]]=None,
_shared_parameters: Optional[Dict[str, Any]]=None,
**kwargs):
super().__init__()
if kwargs:
raise ValueError((
"The following parameter(s) are not valid: %s\n"
"The valid parameters are ignore_order, report_repetition, significant_digits, "
"number_format_notation, exclude_paths, include_paths, exclude_types, exclude_regex_paths, ignore_type_in_groups, "
"ignore_string_type_changes, ignore_numeric_type_changes, ignore_type_subclasses, ignore_uuid_types, truncate_datetime, "
"ignore_private_variables, ignore_nan_inequality, number_to_string_func, verbose_level, "
"view, hasher, hashes, max_passes, max_diffs, zip_ordered_iterables, "
"cutoff_distance_for_pairs, cutoff_intersection_for_pairs, log_frequency_in_sec, cache_size, "
"cache_tuning_sample_size, get_deep_distance, group_by, group_by_sort_key, cache_purge_level, log_stacktrace,"
"math_epsilon, iterable_compare_func, use_enum_value, _original_type, threshold_to_diff_deeper, default_timezone "
"ignore_order_func, custom_operators, encodings, ignore_encoding_errors, use_log_scale, log_scale_similarity_threshold "
"_parameters and _shared_parameters.") % ', '.join(kwargs.keys()))
if _parameters:
self.__dict__.update(_parameters)
else:
self.custom_operators = custom_operators or []
self.ignore_order = ignore_order
self.ignore_order_func = ignore_order_func
ignore_type_in_groups = ignore_type_in_groups or []
if numbers == ignore_type_in_groups or numbers in ignore_type_in_groups:
ignore_numeric_type_changes = True
self.ignore_numeric_type_changes = ignore_numeric_type_changes
if strings == ignore_type_in_groups or strings in ignore_type_in_groups:
ignore_string_type_changes = True
# Handle ignore_uuid_types - check if uuid+str group is already in ignore_type_in_groups
uuid_str_group = (uuids[0], str)
if uuid_str_group == ignore_type_in_groups or uuid_str_group in ignore_type_in_groups:
ignore_uuid_types = True
self.ignore_uuid_types = ignore_uuid_types
self.use_enum_value = use_enum_value
self.log_scale_similarity_threshold = log_scale_similarity_threshold
self.use_log_scale = use_log_scale
self.default_timezone = default_timezone
self.log_stacktrace = log_stacktrace
self.threshold_to_diff_deeper = threshold_to_diff_deeper
self.ignore_string_type_changes = ignore_string_type_changes
self.ignore_type_in_groups = self.get_ignore_types_in_groups(
ignore_type_in_groups=ignore_type_in_groups,
ignore_string_type_changes=ignore_string_type_changes,
ignore_numeric_type_changes=ignore_numeric_type_changes,
ignore_type_subclasses=ignore_type_subclasses,
ignore_uuid_types=ignore_uuid_types)
self.report_repetition = report_repetition
self.exclude_paths = add_root_to_paths(convert_item_or_items_into_set_else_none(exclude_paths))
self.include_paths = add_root_to_paths(convert_item_or_items_into_set_else_none(include_paths))
self.exclude_regex_paths = convert_item_or_items_into_compiled_regexes_else_none(exclude_regex_paths)
self.exclude_types = set(exclude_types) if exclude_types else None
self.exclude_types_tuple = tuple(exclude_types) if exclude_types else None # we need tuple for checking isinstance
self.ignore_type_subclasses = ignore_type_subclasses
self.type_check_func = type_in_type_group if ignore_type_subclasses else type_is_subclass_of_type_group
self.ignore_string_case = ignore_string_case
self.exclude_obj_callback = exclude_obj_callback
self.exclude_obj_callback_strict = exclude_obj_callback_strict
self.include_obj_callback = include_obj_callback
self.include_obj_callback_strict = include_obj_callback_strict
self.number_to_string = number_to_string_func or number_to_string
self.iterable_compare_func = iterable_compare_func
self.zip_ordered_iterables = zip_ordered_iterables
self.ignore_private_variables = ignore_private_variables
self.ignore_nan_inequality = ignore_nan_inequality
self.hasher = hasher
self.cache_tuning_sample_size = cache_tuning_sample_size
self.group_by = group_by
if callable(group_by_sort_key):
self.group_by_sort_key = group_by_sort_key
elif group_by_sort_key:
def _group_by_sort_key(x):
return x[group_by_sort_key]
self.group_by_sort_key = _group_by_sort_key
else:
self.group_by_sort_key = None
self.encodings = encodings
self.ignore_encoding_errors = ignore_encoding_errors
self.significant_digits = self.get_significant_digits(significant_digits, ignore_numeric_type_changes)
self.math_epsilon = math_epsilon
if self.math_epsilon is not None and self.ignore_order:
logger.warning("math_epsilon in conjunction with ignore_order=True is only used for flat object comparisons. Custom math_epsilon will not have an effect when comparing nested objects.")
self.truncate_datetime = get_truncate_datetime(truncate_datetime)
self.number_format_notation = number_format_notation
if verbose_level in {0, 1, 2}:
self.verbose_level = verbose_level
else:
raise ValueError(VERBOSE_LEVEL_RANGE_MSG)
if cache_purge_level not in {0, 1, 2}:
raise ValueError(PURGE_LEVEL_RANGE_MSG)
self.view = view
# Setting up the cache for dynamic programming. One dictionary per instance of root of DeepDiff running.
self.max_passes = max_passes
self.max_diffs = max_diffs
self.cutoff_distance_for_pairs = float(cutoff_distance_for_pairs)
self.cutoff_intersection_for_pairs = float(cutoff_intersection_for_pairs)
if self.cutoff_distance_for_pairs < 0 or self.cutoff_distance_for_pairs > 1:
raise ValueError(CUTOFF_RANGE_ERROR_MSG)
# _Parameters are the clean _parameters to initialize DeepDiff with so we avoid all the above
# cleaning functionalities when running DeepDiff recursively.
# However DeepHash has its own set of _parameters that are slightly different than DeepDIff.
# DeepDiff _parameters are transformed to DeepHash _parameters via _get_deephash_params method.
self.progress_logger = progress_logger
self.cache_size = cache_size
_parameters = self.__dict__.copy()
_parameters['group_by'] = None # overwriting since these parameters will be passed on to other passes.
if log_stacktrace:
self.log_err = logger.exception
else:
self.log_err = logger.error
# Non-Root
if _shared_parameters:
self.is_root = False
self._shared_parameters = _shared_parameters
self.__dict__.update(_shared_parameters)
# We are in some pass other than root
progress_timer = None
# Root
else:
self.is_root = True
# Caching the DeepDiff results for dynamic programming
self._distance_cache = LFUCache(cache_size) if cache_size else DummyLFU()
self._stats = {
PASSES_COUNT: 0,
DIFF_COUNT: 0,
DISTANCE_CACHE_HIT_COUNT: 0,
PREVIOUS_DIFF_COUNT: 0,
PREVIOUS_DISTANCE_CACHE_HIT_COUNT: 0,
MAX_PASS_LIMIT_REACHED: False,
MAX_DIFF_LIMIT_REACHED: False,
DISTANCE_CACHE_ENABLED: bool(cache_size),
}
self.hashes = dict_() if hashes is None else hashes
self._numpy_paths = dict_() # if _numpy_paths is None else _numpy_paths
self.group_by_keys = set() # Track keys that originated from group_by operations
self._shared_parameters = {
'hashes': self.hashes,
'_stats': self._stats,
'_distance_cache': self._distance_cache,
'group_by_keys': self.group_by_keys,
'_numpy_paths': self._numpy_paths,
_ENABLE_CACHE_EVERY_X_DIFF: self.cache_tuning_sample_size * 10,
}
if log_frequency_in_sec:
# Creating a progress log reporter that runs in a separate thread every log_frequency_in_sec seconds.
progress_timer = RepeatedTimer(log_frequency_in_sec, _report_progress, self._stats, progress_logger)
else:
progress_timer = None
self._parameters = _parameters
self.deephash_parameters = self._get_deephash_params()
self.tree = TreeResult()
self._iterable_opcodes = {}
if group_by and self.is_root:
try:
original_t1 = t1
t1 = self._group_iterable_to_dict(t1, group_by, item_name='t1')
except (KeyError, ValueError):
pass
else:
try:
t2 = self._group_iterable_to_dict(t2, group_by, item_name='t2')
except (KeyError, ValueError):
t1 = original_t1
self.t1 = t1
self.t2 = t2
try:
root = DiffLevel(t1, t2, verbose_level=self.verbose_level)
# _original_type is only used to pass the original type of the data. Currently only used for numpy arrays.
# The reason is that we convert the numpy array to python list and then later for distance calculations
# we convert only the the last dimension of it into numpy arrays.
self._diff(root, parents_ids=frozenset({id(t1)}), _original_type=_original_type)
if get_deep_distance and view in {TEXT_VIEW, TREE_VIEW}:
self.tree['deep_distance'] = self._get_rough_distance()
self.tree.remove_empty_keys()
view_results = self._get_view_results(self.view)
if isinstance(view_results, ColoredView):
self.update(view_results.tree)
self._colored_view = view_results
else:
self.update(view_results)
finally:
if self.is_root:
if cache_purge_level:
del self._distance_cache
del self.hashes
del self._shared_parameters
del self._parameters
for key in (PREVIOUS_DIFF_COUNT, PREVIOUS_DISTANCE_CACHE_HIT_COUNT,
DISTANCE_CACHE_ENABLED):
del self._stats[key]
if progress_timer:
duration = progress_timer.stop()
self._stats['DURATION SEC'] = duration
logger.info('stats {}'.format(self.get_stats()))
if cache_purge_level == 2:
self.__dict__.clear()
def _get_deephash_params(self):
result = {key: self._parameters[key] for key in DEEPHASH_PARAM_KEYS}
result['ignore_repetition'] = not self.report_repetition
result['number_to_string_func'] = self.number_to_string
return result
def _report_result(self, report_type, change_level, local_tree=None):
"""
Add a detected change to the reference-style result dictionary.
report_type will be added to level.
(We'll create the text-style report from there later.)
:param report_type: A well defined string key describing the type of change.
Examples: "set_item_added", "values_changed"
:param change_level: A DiffLevel object describing the objects in question in their
before-change and after-change object structure.
:local_tree: None
"""
if not self._skip_this(change_level):
change_level.report_type = report_type
tree = self.tree if local_tree is None else local_tree
tree[report_type].add(change_level)
def custom_report_result(self, report_type, level, extra_info=None):
"""
Add a detected change to the reference-style result dictionary.
report_type will be added to level.
(We'll create the text-style report from there later.)
:param report_type: A well defined string key describing the type of change.
Examples: "set_item_added", "values_changed"
:param parent: A DiffLevel object describing the objects in question in their
before-change and after-change object structure.
:param extra_info: A dict that describe this result
:rtype: None
"""
if not self._skip_this(level):
level.report_type = report_type
level.additional[CUSTOM_FIELD] = extra_info
self.tree[report_type].add(level)
@staticmethod
def _dict_from_slots(object: Any) -> Dict[str, Any]:
def unmangle(attribute: str) -> str:
if attribute.startswith('__') and attribute != '__weakref__':
return '_{type}{attribute}'.format(
type=type(object).__name__,
attribute=attribute
)
return attribute
all_slots = []
if isinstance(object, type):
mro = object.__mro__ # pragma: no cover. I have not been able to write a test for this case. But we still check for it.
else:
mro = object.__class__.__mro__
for type_in_mro in mro:
slots = getattr(type_in_mro, '__slots__', None)
if slots:
if isinstance(slots, strings):
all_slots.append(slots)
else:
all_slots.extend(slots)
return {i: getattr(object, key) for i in all_slots if hasattr(object, key := unmangle(i))}
def _diff_enum(self, level: Any, parents_ids: FrozenSet[int]=frozenset(), local_tree: Optional[Any]=None) -> None:
t1 = detailed__dict__(level.t1, include_keys=ENUM_INCLUDE_KEYS)
t2 = detailed__dict__(level.t2, include_keys=ENUM_INCLUDE_KEYS)
self._diff_dict(
level,
parents_ids,
print_as_attribute=True,
override=True,
override_t1=t1,
override_t2=t2,
local_tree=local_tree,
)
def _diff_obj(self, level: Any, parents_ids: FrozenSet[int]=frozenset(), is_namedtuple: bool=False, local_tree: Optional[Any]=None, is_pydantic_object: bool=False) -> None:
"""Difference of 2 objects"""
processing_error = False
t1: Optional[Dict[str, Any]] = None
t2: Optional[Dict[str, Any]] = None
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)
elif all('__slots__' in dir(t) for t in level):
t1 = self._dict_from_slots(level.t1)
t2 = self._dict_from_slots(level.t2)
else:
t1 = {k: v for k, v in getmembers(level.t1) if not callable(v)}
t2 = {k: v for k, v in getmembers(level.t2) if not callable(v)}
except AttributeError:
processing_error = True
if processing_error is True or t1 is None or t2 is None:
self._report_result('unprocessed', level, local_tree=local_tree)
return
self._diff_dict(
level,
parents_ids,
print_as_attribute=True,
override=True,
override_t1=t1,
override_t2=t2,
local_tree=local_tree,
)
def _skip_this(self, level: Any) -> bool:
"""
Check whether this comparison should be skipped because one of the objects to compare meets exclusion criteria.
:rtype: bool
"""
level_path = level.path()
skip = False
if self.exclude_paths and level_path in self.exclude_paths:
skip = True
if self.include_paths and level_path != 'root':
if level_path not in self.include_paths:
skip = True
for prefix in self.include_paths:
if prefix in level_path or level_path in prefix:
skip = False
break
elif self.exclude_regex_paths and any(
[exclude_regex_path.search(level_path) for exclude_regex_path in self.exclude_regex_paths]):
skip = True
elif self.exclude_types_tuple and \
(isinstance(level.t1, self.exclude_types_tuple) or isinstance(level.t2, self.exclude_types_tuple)):
skip = True
elif self.exclude_obj_callback and \
(self.exclude_obj_callback(level.t1, level_path) or self.exclude_obj_callback(level.t2, level_path)):
skip = True
elif self.exclude_obj_callback_strict and \
(self.exclude_obj_callback_strict(level.t1, level_path) and
self.exclude_obj_callback_strict(level.t2, level_path)):
skip = True
elif self.include_obj_callback and level_path != 'root':
skip = True
if (self.include_obj_callback(level.t1, level_path) or self.include_obj_callback(level.t2, level_path)):
skip = False
elif self.include_obj_callback_strict and level_path != 'root':
skip = True
if (self.include_obj_callback_strict(level.t1, level_path) and
self.include_obj_callback_strict(level.t2, level_path)):
skip = False
return skip
def _skip_this_key(self, level: Any, key: Any) -> bool:
# 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: Any, level: Any) -> Dict[Any, Any]:
"""
Get a dictionary of cleaned value of keys to the keys themselves.
This is mainly used to transform the keys when the type changes of keys should be ignored.
TODO: needs also some key conversion for groups of types other than the built-in strings and numbers.
"""
result = dict_()
for key in keys:
if self.ignore_string_type_changes and isinstance(key, bytes):
clean_key = key.decode('utf-8')
elif self.ignore_string_type_changes and isinstance(key, memoryview):
clean_key = key.tobytes().decode('utf-8')
elif self.use_enum_value and isinstance(key, Enum):
clean_key = key.value
elif isinstance(key, numbers):
# Skip type prefixing for keys that originated from group_by operations
if hasattr(self, 'group_by_keys') and key in self.group_by_keys:
if self.significant_digits is None:
clean_key = key
else:
clean_key = self.number_to_string(key, significant_digits=self.significant_digits,
number_format_notation=self.number_format_notation) # type: ignore # type: ignore
else:
type_ = "number" if self.ignore_numeric_type_changes else key.__class__.__name__
if self.significant_digits is None:
clean_key = key
else:
clean_key = self.number_to_string(key, significant_digits=self.significant_digits,
number_format_notation=self.number_format_notation) # type: ignore # type: ignore
clean_key = KEY_TO_VAL_STR.format(type_, clean_key)
else:
clean_key = key
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'
'or ignore_numeric_type_changes are set to be true.').format(
key, result[clean_key], level.path()))
else:
result[clean_key] = key
return result
def _diff_dict(
self,
level: Any,
parents_ids: FrozenSet[int]=frozenset([]),
print_as_attribute: bool=False,
override: bool=False,
override_t1: Optional[Any]=None,
override_t2: Optional[Any]=None,
local_tree: Optional[Any]=None,
) -> None:
"""Difference of 2 dictionaries"""
if override:
# for special stuff like custom objects and named tuples we receive preprocessed t1 and t2
# but must not spoil the chain (=level) with it
t1 = override_t1
t2 = override_t2
else:
t1 = level.t1
t2 = level.t2
if print_as_attribute:
item_added_key = "attribute_added"
item_removed_key = "attribute_removed"
rel_class = AttributeRelationship
else:
item_added_key = "dictionary_item_added"
item_removed_key = "dictionary_item_removed"
rel_class = DictRelationship
if self.ignore_private_variables:
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([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)
t1_keys = SetOrdered(t1_clean_to_keys.keys())
t2_keys = SetOrdered(t2_clean_to_keys.keys())
else:
t1_clean_to_keys = t2_clean_to_keys = None
t_keys_intersect = 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 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
for key in t_keys_added:
if self._count_diff() is StopIteration:
return
key = t2_clean_to_keys[key] if t2_clean_to_keys else key
change_level = level.branch_deeper(
notpresent,
t2[key],
child_relationship_class=rel_class,
child_relationship_param=key,
child_relationship_param2=key,
)
self._report_result(item_added_key, change_level, local_tree=local_tree)
for key in t_keys_removed:
if self._count_diff() is StopIteration:
return # pragma: no cover. This is already covered for addition.
key = t1_clean_to_keys[key] if t1_clean_to_keys else key
change_level = level.branch_deeper(
t1[key],
notpresent,
child_relationship_class=rel_class,
child_relationship_param=key,
child_relationship_param2=key,
)
self._report_result(item_removed_key, change_level, local_tree=local_tree)
for key in t_keys_intersect: # key present in both dicts - need to compare values
if self._count_diff() is StopIteration:
return # pragma: no cover. This is already covered for addition.
key1 = t1_clean_to_keys[key] if t1_clean_to_keys else key
key2 = t2_clean_to_keys[key] if t2_clean_to_keys else key
item_id = id(t1[key1])
if parents_ids and item_id in parents_ids:
continue
parents_ids_added = add_to_frozen_set(parents_ids, item_id)
# Go one level deeper
next_level = level.branch_deeper(
t1[key1],
t2[key2],
child_relationship_class=rel_class,
child_relationship_param=key,
child_relationship_param2=key,
)
self._diff(next_level, parents_ids_added, local_tree=local_tree)
def _diff_set(self, level: Any, local_tree: Optional[Any]=None) -> None:
"""Difference of sets"""
t1_hashtable = self._create_hashtable(level, 't1')
t2_hashtable = self._create_hashtable(level, 't2')
t1_hashes = set(t1_hashtable.keys())
t2_hashes = set(t2_hashtable.keys())
hashes_added = t2_hashes - t1_hashes
hashes_removed = t1_hashes - t2_hashes
items_added = [t2_hashtable[i].item for i in hashes_added]
items_removed = [t1_hashtable[i].item for i in hashes_removed]
for item in items_added:
if self._count_diff() is StopIteration:
return # pragma: no cover. This is already covered for addition.
change_level = level.branch_deeper(
notpresent, item, child_relationship_class=SetRelationship)
self._report_result('set_item_added', change_level, local_tree=local_tree)
for item in items_removed:
if self._count_diff() is StopIteration:
return # pragma: no cover. This is already covered for addition.
change_level = level.branch_deeper(
item, notpresent, child_relationship_class=SetRelationship)
self._report_result('set_item_removed', change_level, local_tree=local_tree)
@staticmethod
def _iterables_subscriptable(t1: Any, t2: Any) -> bool:
try:
if getattr(t1, '__getitem__') and getattr(t2, '__getitem__'):
return True
else: # pragma: no cover
return False # should never happen
except AttributeError:
return False
def _diff_iterable(self, level: Any, parents_ids: FrozenSet[int]=frozenset(), _original_type: Optional[type]=None, local_tree: Optional[Any]=None) -> None:
"""Difference of iterables"""
if (self.ignore_order_func and self.ignore_order_func(level)) or self.ignore_order:
self._diff_iterable_with_deephash(level, parents_ids, _original_type=_original_type, local_tree=local_tree)
else:
self._diff_iterable_in_order(level, parents_ids, _original_type=_original_type, local_tree=local_tree)
def _compare_in_order(
self, level,
t1_from_index=None, t1_to_index=None,
t2_from_index=None, t2_to_index=None
) -> List[Tuple[Tuple[int, int], Tuple[Any, Any]]]:
"""
Default compare if `iterable_compare_func` is not provided.
This will compare in sequence order.
"""
if t1_from_index is None:
return [((i, i), (x, y)) for i, (x, y) in enumerate(
zip_longest(
level.t1, level.t2, fillvalue=ListItemRemovedOrAdded))]
else:
t1_chunk = level.t1[t1_from_index:t1_to_index]
t2_chunk = level.t2[t2_from_index:t2_to_index]
return [((i + t1_from_index, i + t2_from_index), (x, y)) for i, (x, y) in enumerate(
zip_longest(
t1_chunk, t2_chunk, fillvalue=ListItemRemovedOrAdded))]
def _get_matching_pairs(
self, level,
t1_from_index=None, t1_to_index=None,
t2_from_index=None, t2_to_index=None
) -> List[Tuple[Tuple[int, int], Tuple[Any, Any]]]:
"""
Given a level get matching pairs. This returns list of two tuples in the form:
[
(t1 index, t2 index), (t1 item, t2 item)
]
This will compare using the passed in `iterable_compare_func` if available.
Default it to compare in order
"""
if self.iterable_compare_func is None:
# Match in order if there is no compare function provided
return self._compare_in_order(
level,
t1_from_index=t1_from_index, t1_to_index=t1_to_index,
t2_from_index=t2_from_index, t2_to_index=t2_to_index,
)
try:
matches = []
y_matched = set()
y_index_matched = set()
for i, x in enumerate(level.t1):
x_found = False
for j, y in enumerate(level.t2):
if(j in y_index_matched):
# This ensures a one-to-one relationship of matches from t1 to t2.
# If y this index in t2 has already been matched to another x
# it cannot have another match, so just continue.
continue
if(self.iterable_compare_func(x, y, level)):
deep_hash = DeepHash(y,
hashes=self.hashes,
apply_hash=True,
**self.deephash_parameters,
)
y_index_matched.add(j)
y_matched.add(deep_hash[y])
matches.append(((i, j), (x, y)))
x_found = True
break
if(not x_found):
matches.append(((i, -1), (x, ListItemRemovedOrAdded)))
for j, y in enumerate(level.t2):
deep_hash = DeepHash(y,
hashes=self.hashes,
apply_hash=True,
**self.deephash_parameters,
)
if(deep_hash[y] not in y_matched):
matches.append(((-1, j), (ListItemRemovedOrAdded, y)))
return matches
except CannotCompare:
return self._compare_in_order(
level,
t1_from_index=t1_from_index, t1_to_index=t1_to_index,
t2_from_index=t2_from_index, t2_to_index=t2_to_index
)
def _diff_iterable_in_order(self, level, parents_ids=frozenset(), _original_type=None, local_tree=None):
# We're handling both subscriptable and non-subscriptable iterables. Which one is it?
subscriptable = self._iterables_subscriptable(level.t1, level.t2)
if subscriptable:
child_relationship_class = SubscriptableIterableRelationship
else:
child_relationship_class = NonSubscriptableIterableRelationship
if (
not self.zip_ordered_iterables
and isinstance(level.t1, Sequence)
and isinstance(level.t2, Sequence)
and self._all_values_basic_hashable(level.t1)
and self._all_values_basic_hashable(level.t2)
and self.iterable_compare_func is None
):
local_tree_pass = TreeResult()
opcodes_with_values = self._diff_ordered_iterable_by_difflib(
level,
parents_ids=parents_ids,
_original_type=_original_type,
child_relationship_class=child_relationship_class,
local_tree=local_tree_pass,
)
# Sometimes DeepDiff's old iterable diff does a better job than DeepDiff
if len(local_tree_pass) > 1:
local_tree_pass2 = TreeResult()
self._diff_by_forming_pairs_and_comparing_one_by_one(
level,
parents_ids=parents_ids,
_original_type=_original_type,
child_relationship_class=child_relationship_class,
local_tree=local_tree_pass2,
)
if len(local_tree_pass) >= len(local_tree_pass2):
local_tree_pass = local_tree_pass2
else:
self._iterable_opcodes[level.path(force=FORCE_DEFAULT)] = opcodes_with_values
for report_type, levels in local_tree_pass.items():
if levels:
self.tree[report_type] |= levels
else:
self._diff_by_forming_pairs_and_comparing_one_by_one(
level,
parents_ids=parents_ids,
_original_type=_original_type,
child_relationship_class=child_relationship_class,
local_tree=local_tree,
)
def _all_values_basic_hashable(self, iterable: Iterable[Any]) -> bool:
"""
Are all items basic hashable types?
Or there are custom types too?
"""
# We don't want to exhaust a generator
if isinstance(iterable, types.GeneratorType):
return False
for item in iterable:
if not isinstance(item, basic_types):
return False
return True
def _diff_by_forming_pairs_and_comparing_one_by_one(
self, level, local_tree, parents_ids=frozenset(),
_original_type=None, child_relationship_class=None,
t1_from_index=None, t1_to_index=None,
t2_from_index=None, t2_to_index=None,
):
for (i, j), (x, y) in self._get_matching_pairs(
level,
t1_from_index=t1_from_index, t1_to_index=t1_to_index,
t2_from_index=t2_from_index, t2_to_index=t2_to_index
):
if self._count_diff() is StopIteration:
return # pragma: no cover. This is already covered for addition.
reference_param1 = i
reference_param2 = j
if y is ListItemRemovedOrAdded: # item removed completely
change_level = level.branch_deeper(
x,
notpresent,
child_relationship_class=child_relationship_class,
child_relationship_param=reference_param1,
child_relationship_param2=reference_param2,
)
self._report_result('iterable_item_removed', change_level, local_tree=local_tree)
elif x is ListItemRemovedOrAdded: # new item added
change_level = level.branch_deeper(
notpresent,
y,
child_relationship_class=child_relationship_class,
child_relationship_param=reference_param1,
child_relationship_param2=reference_param2,
)
self._report_result('iterable_item_added', change_level, local_tree=local_tree)
else: # check if item value has changed
if (i != j and ((x == y) or self.iterable_compare_func)):
# Item moved
change_level = level.branch_deeper(
x,
y,
child_relationship_class=child_relationship_class,
child_relationship_param=reference_param1,
child_relationship_param2=reference_param2
)
self._report_result('iterable_item_moved', change_level, local_tree=local_tree)
if self.iterable_compare_func:
# Mark additional context denoting that we have moved an item.
# This will allow for correctly setting paths relative to t2 when using an iterable_compare_func
level.additional["moved"] = True
else:
continue
item_id = id(x)
if parents_ids and item_id in parents_ids:
continue
parents_ids_added = add_to_frozen_set(parents_ids, item_id)
# Go one level deeper
next_level = level.branch_deeper(
x,
y,
child_relationship_class=child_relationship_class,
child_relationship_param=reference_param1,
child_relationship_param2=reference_param2
)
self._diff(next_level, parents_ids_added, local_tree=local_tree)
def _diff_ordered_iterable_by_difflib(
self, level, local_tree, parents_ids=frozenset(), _original_type=None, child_relationship_class=None,
):
seq = difflib.SequenceMatcher(isjunk=None, a=level.t1, b=level.t2, autojunk=False)
opcodes = seq.get_opcodes()
opcodes_with_values = []
# TODO: this logic should be revisted so we detect reverse operations
# like when a replacement happens at index X and a reverse replacement happens at index Y
# in those cases we have a "iterable_item_moved" operation.
for tag, t1_from_index, t1_to_index, t2_from_index, t2_to_index in opcodes:
if tag == 'equal':
opcodes_with_values.append(Opcode(
tag, t1_from_index, t1_to_index, t2_from_index, t2_to_index,
))
continue
# print('{:7} t1[{}:{}] --> t2[{}:{}] {!r:>8} --> {!r}'.format(
# tag, t1_from_index, t1_to_index, t2_from_index, t2_to_index, level.t1[t1_from_index:t1_to_index], level.t2[t2_from_index:t2_to_index]))
opcodes_with_values.append(Opcode(
tag, t1_from_index, t1_to_index, t2_from_index, t2_to_index,
old_values = level.t1[t1_from_index: t1_to_index],
new_values = level.t2[t2_from_index: t2_to_index],
))
if tag == 'replace':
self._diff_by_forming_pairs_and_comparing_one_by_one(
level, local_tree=local_tree, parents_ids=parents_ids,
_original_type=_original_type, child_relationship_class=child_relationship_class,
t1_from_index=t1_from_index, t1_to_index=t1_to_index,
t2_from_index=t2_from_index, t2_to_index=t2_to_index,
)
elif tag == 'delete':
for index, x in enumerate(level.t1[t1_from_index:t1_to_index]):
change_level = level.branch_deeper(
x,
notpresent,
child_relationship_class=child_relationship_class,
child_relationship_param=index + t1_from_index,
child_relationship_param2=index + t1_from_index,
)
self._report_result('iterable_item_removed', change_level, local_tree=local_tree)
elif tag == 'insert':
for index, y in enumerate(level.t2[t2_from_index:t2_to_index]):
change_level = level.branch_deeper(
notpresent,
y,
child_relationship_class=child_relationship_class,
child_relationship_param=index + t2_from_index,
child_relationship_param2=index + t2_from_index,
)
self._report_result('iterable_item_added', change_level, local_tree=local_tree)
return opcodes_with_values
def _diff_str(self, level, local_tree=None):
"""Compare strings"""
if self.ignore_string_case:
level.t1 = level.t1.lower()
level.t2 = level.t2.lower()
if type(level.t1) == type(level.t2) and level.t1 == level.t2: # NOQA
return
# do we add a diff for convenience?
do_diff = True
t1_str = level.t1
t2_str = level.t2
if isinstance(level.t1, memoryview):
try:
t1_str = level.t1.tobytes().decode('ascii')
except UnicodeDecodeError:
do_diff = False
elif isinstance(level.t1, bytes_type):
try:
t1_str = level.t1.decode('ascii')
except UnicodeDecodeError:
do_diff = False
if isinstance(level.t2, memoryview):
try:
t2_str = level.t2.tobytes().decode('ascii')
except UnicodeDecodeError:
do_diff = False
elif isinstance(level.t2, bytes_type):
try:
t2_str = level.t2.decode('ascii')
except UnicodeDecodeError:
do_diff = False
if isinstance(level.t1, Enum):
t1_str = level.t1.value
if isinstance(level.t2, Enum):
t2_str = level.t2.value
if t1_str == t2_str:
return
if do_diff:
if '\n' in t1_str or isinstance(t2_str, str) and '\n' in t2_str:
diff = difflib.unified_diff(
t1_str.splitlines(), t2_str.splitlines(), lineterm='')
diff = list(diff)
if diff:
level.additional['diff'] = '\n'.join(diff)
self._report_result('values_changed', level, local_tree=local_tree)
def _diff_tuple(self, level, parents_ids, local_tree=None):
# Checking to see if it has _fields. Which probably means it is a named
# tuple.
try:
level.t1._asdict
# It must be a normal tuple
except AttributeError:
self._diff_iterable(level, parents_ids, local_tree=local_tree)
# We assume it is a namedtuple then
else:
self._diff_obj(level, parents_ids, is_namedtuple=True, local_tree=local_tree)
def _add_hash(self, hashes, item_hash, item, i):
if item_hash in hashes:
hashes[item_hash].indexes.append(i)
else:
hashes[item_hash] = IndexedHash(indexes=[i], item=item)
def _create_hashtable(self, level, t):
"""Create hashtable of {item_hash: (indexes, item)}"""
obj = getattr(level, t)
local_hashes = dict_()
for (i, item) in enumerate(obj):
try:
parent = "{}[{}]".format(level.path(), i)
# Note: in the DeepDiff we only calculate the hash of items when we have to.
# So self.hashes does not include hashes of all objects in t1 and t2.
# It only includes the ones needed when comparing iterables.
# The self.hashes dictionary gets shared between different runs of DeepHash
# So that any object that is already calculated to have a hash is not re-calculated.
deep_hash = DeepHash(
item,
hashes=self.hashes,
parent=parent,
apply_hash=True,
**self.deephash_parameters,
)
except UnicodeDecodeError as err:
err.reason = f"Can not produce a hash for {level.path()}: {err.reason}"
raise
except NotImplementedError:
raise
# except Exception as e: # pragma: no cover
# logger.error("Can not produce a hash for %s."
# "Not counting this object.\n %s" %
# (level.path(), e))
else:
try:
item_hash = deep_hash[item]
except KeyError:
pass
else:
if item_hash is unprocessed: # pragma: no cover
self.log_err("Item %s was not processed while hashing "
"thus not counting this object." %
level.path())
else:
self._add_hash(hashes=local_hashes, item_hash=item_hash, item=item, i=i)
# Also we hash the iterables themselves too so that we can later create cache keys from those hashes.
DeepHash(
obj,
hashes=self.hashes,
parent=level.path(),
apply_hash=True,
**self.deephash_parameters,
)
return local_hashes
@staticmethod
@lru_cache(maxsize=2028)
def _get_distance_cache_key(added_hash, removed_hash):
key1, key2 = (added_hash, removed_hash) if added_hash > removed_hash else (removed_hash, added_hash)
if isinstance(key1, int):
# If the hash function produces integers we convert them to hex values.
# This was used when the default hash function was Murmur3 128bit which produces integers.
key1 = hex(key1).encode('utf-8')
key2 = hex(key2).encode('utf-8')
elif isinstance(key1, str):
key1 = key1.encode('utf-8')
key2 = key2.encode('utf-8')
return key1 + b'--' + key2 + b'dc'
def _get_rough_distance_of_hashed_objs(
self, added_hash, removed_hash, added_hash_obj, removed_hash_obj, _original_type=None):
# We need the rough distance between the 2 objects to see if they qualify to be pairs or not
_distance = cache_key = None
if self._stats[DISTANCE_CACHE_ENABLED]:
cache_key = self._get_distance_cache_key(added_hash, removed_hash)
if cache_key in self._distance_cache:
self._stats[DISTANCE_CACHE_HIT_COUNT] += 1
_distance = self._distance_cache.get(cache_key)
if _distance is None:
# We can only cache the rough distance and not the actual diff result for reuse.
# The reason is that we have modified the parameters explicitly so they are different and can't
# be used for diff reporting
diff = DeepDiff(
removed_hash_obj.item, added_hash_obj.item,
_parameters=self._parameters,
_shared_parameters=self._shared_parameters,
view=DELTA_VIEW,
_original_type=_original_type,
iterable_compare_func=self.iterable_compare_func,
)
_distance = diff._get_rough_distance()
if cache_key and self._stats[DISTANCE_CACHE_ENABLED]:
self._distance_cache.set(cache_key, value=_distance)
return _distance
def _get_most_in_common_pairs_in_iterables(
self, hashes_added, hashes_removed, t1_hashtable, t2_hashtable, parents_ids, _original_type):
"""
Get the closest pairs between items that are removed and items that are added.
returns a dictionary of hashes that are closest to each other.
The dictionary is going to be symmetrical so any key will be a value too and otherwise.
Note that due to the current reporting structure in DeepDiff, we don't compare an item that
was added to an item that is in both t1 and t2.
For example
[{1, 2}, {4, 5, 6}]
[{1, 2}, {1, 2, 3}]
is only compared between {4, 5, 6} and {1, 2, 3} even though technically {1, 2, 3} is
just one item different than {1, 2}
Perhaps in future we can have a report key that is item duplicated and modified instead of just added.
"""
cache_key = None
if self._stats[DISTANCE_CACHE_ENABLED]:
cache_key = combine_hashes_lists(items=[hashes_added, hashes_removed], prefix='pairs_cache')
if cache_key in self._distance_cache:
return self._distance_cache.get(cache_key).copy()
# A dictionary of hashes to distances and each distance to an ordered set of hashes.
# It tells us about the distance of each object from other objects.
# And the objects with the same distances are grouped together in an ordered set.
# It also includes a "max" key that is just the value of the biggest current distance in the
# most_in_common_pairs dictionary.
def defaultdict_orderedset():
return defaultdict(SetOrdered)
most_in_common_pairs = defaultdict(defaultdict_orderedset)
pairs = dict_()
pre_calced_distances = None
if hashes_added and hashes_removed and np and len(hashes_added) > 1 and len(hashes_removed) > 1:
# pre-calculates distances ONLY for 1D arrays whether an _original_type
# was explicitly passed or a homogeneous array is detected.
# Numpy is needed for this optimization.
pre_calced_distances = self._precalculate_numpy_arrays_distance(
hashes_added, hashes_removed, t1_hashtable, t2_hashtable, _original_type)
if hashes_added and hashes_removed \
and self.iterable_compare_func \
and len(hashes_added) > 0 and len(hashes_removed) > 0:
pre_calced_distances = self._precalculate_distance_by_custom_compare_func(
hashes_added, hashes_removed, t1_hashtable, t2_hashtable, _original_type)
for added_hash in hashes_added:
for removed_hash in hashes_removed:
added_hash_obj = t2_hashtable[added_hash]
removed_hash_obj = t1_hashtable[removed_hash]
# Loop is detected
if id(removed_hash_obj.item) in parents_ids:
continue
_distance = None
if pre_calced_distances:
_distance = pre_calced_distances.get("{}--{}".format(added_hash, removed_hash))
if _distance is None:
_distance = self._get_rough_distance_of_hashed_objs(
added_hash, removed_hash, added_hash_obj, removed_hash_obj, _original_type)
# Left for future debugging
# print(f'{Fore.RED}distance of {added_hash_obj.item} and {removed_hash_obj.item}: {_distance}{Style.RESET_ALL}')
# Discard potential pairs that are too far.
if _distance >= self.cutoff_distance_for_pairs:
continue
pairs_of_item = most_in_common_pairs[added_hash]
pairs_of_item[_distance].add(removed_hash)
used_to_hashes = set()
distances_to_from_hashes = defaultdict(SetOrdered)
for from_hash, distances_to_to_hashes in most_in_common_pairs.items():
# del distances_to_to_hashes['max']
for dist in distances_to_to_hashes:
distances_to_from_hashes[dist].add(from_hash)
for dist in sorted(distances_to_from_hashes.keys()):
from_hashes = distances_to_from_hashes[dist]
while from_hashes:
from_hash = from_hashes.pop()
if from_hash not in used_to_hashes:
to_hashes = most_in_common_pairs[from_hash][dist]
while to_hashes:
to_hash = to_hashes.pop()
if to_hash not in used_to_hashes:
used_to_hashes.add(from_hash)
used_to_hashes.add(to_hash)
# Left for future debugging:
# print(f'{bcolors.FAIL}Adding {t2_hashtable[from_hash].item} as a pairs of {t1_hashtable[to_hash].item} with distance of {dist}{bcolors.ENDC}')
pairs[from_hash] = to_hash
inverse_pairs = {v: k for k, v in pairs.items()}
pairs.update(inverse_pairs)
if cache_key and self._stats[DISTANCE_CACHE_ENABLED]:
self._distance_cache.set(cache_key, value=pairs)
return pairs.copy()
def _diff_iterable_with_deephash(self, level, parents_ids, _original_type=None, local_tree=None):
"""Diff of hashable or unhashable iterables. Only used when ignoring the order."""
full_t1_hashtable = self._create_hashtable(level, 't1')
full_t2_hashtable = self._create_hashtable(level, 't2')
t1_hashes = SetOrdered(full_t1_hashtable.keys())
t2_hashes = SetOrdered(full_t2_hashtable.keys())
hashes_added = t2_hashes - t1_hashes
hashes_removed = t1_hashes - t2_hashes
# Deciding whether to calculate pairs or not.
if (len(hashes_added) + len(hashes_removed)) / (len(full_t1_hashtable) + len(full_t2_hashtable) + 1) > self.cutoff_intersection_for_pairs:
get_pairs = False
else:
get_pairs = True
# reduce the size of hashtables
if self.report_repetition:
t1_hashtable = full_t1_hashtable
t2_hashtable = full_t2_hashtable
else:
t1_hashtable = {k: v for k, v in full_t1_hashtable.items() if k in hashes_removed}
t2_hashtable = {k: v for k, v in full_t2_hashtable.items() if k in hashes_added}
if self._stats[PASSES_COUNT] < self.max_passes and get_pairs:
self._stats[PASSES_COUNT] += 1
pairs = self._get_most_in_common_pairs_in_iterables(
hashes_added, hashes_removed, t1_hashtable, t2_hashtable, parents_ids, _original_type)
elif get_pairs:
if not self._stats[MAX_PASS_LIMIT_REACHED]:
self._stats[MAX_PASS_LIMIT_REACHED] = True
logger.warning(MAX_PASSES_REACHED_MSG.format(self.max_passes))
pairs = dict_()
else:
pairs = dict_()
def get_other_pair(hash_value, in_t1=True):
"""
Gets the other paired indexed hash item to the hash_value in the pairs dictionary
in_t1: are we looking for the other pair in t1 or t2?
"""
if in_t1:
hashtable = t1_hashtable
the_other_hashes = hashes_removed
else:
hashtable = t2_hashtable
the_other_hashes = hashes_added
other = pairs.pop(hash_value, notpresent)
if other is notpresent:
other = notpresent_indexed
else:
# The pairs are symmetrical.
# removing the other direction of pair
# so it does not get used.
del pairs[other]
the_other_hashes.remove(other)
other = hashtable[other]
return other
if self.report_repetition:
for hash_value in hashes_added:
if self._count_diff() is StopIteration:
return # pragma: no cover. This is already covered for addition (when report_repetition=False).
other = get_other_pair(hash_value)
item_id = id(other.item)
indexes = t2_hashtable[hash_value].indexes if other.item is notpresent else other.indexes
# When we report repetitions, we want the child_relationship_param2 only if there is no repetition.
# Because when there is a repetition, we report it in a different way (iterable_items_added_at_indexes for example).
# When there is no repetition, we want child_relationship_param2 so that we report the "new_path" correctly.
if len(t2_hashtable[hash_value].indexes) == 1:
index2 = t2_hashtable[hash_value].indexes[0]
else:
index2 = None
for i in indexes:
change_level = level.branch_deeper(
other.item,
t2_hashtable[hash_value].item,
child_relationship_class=SubscriptableIterableRelationship,
child_relationship_param=i,
child_relationship_param2=index2,
)
if other.item is notpresent:
self._report_result('iterable_item_added', change_level, local_tree=local_tree)
else:
parents_ids_added = add_to_frozen_set(parents_ids, item_id)
self._diff(change_level, parents_ids_added, local_tree=local_tree)
for hash_value in hashes_removed:
if self._count_diff() is StopIteration:
return # pragma: no cover. This is already covered for addition.
other = get_other_pair(hash_value, in_t1=False)
item_id = id(other.item)
# When we report repetitions, we want the child_relationship_param2 only if there is no repetition.
# Because when there is a repetition, we report it in a different way (iterable_items_added_at_indexes for example).
# When there is no repetition, we want child_relationship_param2 so that we report the "new_path" correctly.
if other.item is notpresent or len(other.indexes > 1):
index2 = None
else:
index2 = other.indexes[0]
for i in t1_hashtable[hash_value].indexes:
change_level = level.branch_deeper(
t1_hashtable[hash_value].item,
other.item,
child_relationship_class=SubscriptableIterableRelationship,
child_relationship_param=i,
child_relationship_param2=index2,
)
if other.item is notpresent:
self._report_result('iterable_item_removed', change_level, local_tree=local_tree)
else:
# I was not able to make a test case for the following 2 lines since the cases end up
# getting resolved above in the hashes_added calcs. However I am leaving these 2 lines
# in case things change in future.
parents_ids_added = add_to_frozen_set(parents_ids, item_id) # pragma: no cover.
self._diff(change_level, parents_ids_added, local_tree=local_tree) # pragma: no cover.
items_intersect = t2_hashes.intersection(t1_hashes)
for hash_value in items_intersect:
t1_indexes = t1_hashtable[hash_value].indexes
t2_indexes = t2_hashtable[hash_value].indexes
t1_indexes_len = len(t1_indexes)
t2_indexes_len = len(t2_indexes)
if t1_indexes_len != t2_indexes_len: # this is a repetition change!
# create "change" entry, keep current level untouched to handle further changes
repetition_change_level = level.branch_deeper(
t1_hashtable[hash_value].item,
t2_hashtable[hash_value].item, # nb: those are equal!
child_relationship_class=SubscriptableIterableRelationship,
child_relationship_param=t1_hashtable[hash_value]
.indexes[0])
repetition_change_level.additional['repetition'] = RemapDict(
old_repeat=t1_indexes_len,
new_repeat=t2_indexes_len,
old_indexes=t1_indexes,
new_indexes=t2_indexes)
self._report_result('repetition_change',
repetition_change_level, local_tree=local_tree)
else:
for hash_value in hashes_added:
if self._count_diff() is StopIteration:
return
other = get_other_pair(hash_value)
item_id = id(other.item)
index = t2_hashtable[hash_value].indexes[0] if other.item is notpresent else other.indexes[0]
index2 = t2_hashtable[hash_value].indexes[0]
change_level = level.branch_deeper(
other.item,
t2_hashtable[hash_value].item,
child_relationship_class=SubscriptableIterableRelationship,
child_relationship_param=index,
child_relationship_param2=index2,
)
if other.item is notpresent:
self._report_result('iterable_item_added', change_level, local_tree=local_tree)
else:
parents_ids_added = add_to_frozen_set(parents_ids, item_id)
self._diff(change_level, parents_ids_added, local_tree=local_tree)
for hash_value in hashes_removed:
if self._count_diff() is StopIteration:
return # pragma: no cover. This is already covered for addition.
other = get_other_pair(hash_value, in_t1=False)
item_id = id(other.item)
index = t1_hashtable[hash_value].indexes[0]
index2 = t1_hashtable[hash_value].indexes[0] if other.item is notpresent else other.indexes[0]
change_level = level.branch_deeper(
t1_hashtable[hash_value].item,
other.item,
child_relationship_class=SubscriptableIterableRelationship,
child_relationship_param=index,
child_relationship_param2=index2,
)
if other.item is notpresent:
self._report_result('iterable_item_removed', change_level, local_tree=local_tree)
else:
# Just like the case when report_repetition = True, these lines never run currently.
# However they will stay here in case things change in future.
parents_ids_added = add_to_frozen_set(parents_ids, item_id) # pragma: no cover.
self._diff(change_level, parents_ids_added, local_tree=local_tree) # pragma: no cover.
def _diff_booleans(self, level, local_tree=None):
if level.t1 != level.t2:
self._report_result('values_changed', level, local_tree=local_tree)
def _diff_numbers(self, level, local_tree=None, report_type_change=True):
"""Diff Numbers"""
if report_type_change:
t1_type = "number" if self.ignore_numeric_type_changes else level.t1.__class__.__name__
t2_type = "number" if self.ignore_numeric_type_changes else level.t2.__class__.__name__
else:
t1_type = t2_type = ''
if self.use_log_scale:
if not logarithmic_similarity(level.t1, level.t2, threshold=self.log_scale_similarity_threshold):
self._report_result('values_changed', level, local_tree=local_tree)
elif self.math_epsilon is not None:
if not is_close(level.t1, level.t2, abs_tol=self.math_epsilon):
self._report_result('values_changed', level, local_tree=local_tree)
elif self.significant_digits is None:
if level.t1 != level.t2:
self._report_result('values_changed', level, local_tree=local_tree)
else:
# Bernhard10: I use string formatting for comparison, to be consistent with usecases where
# data is read from files that were previously written from python and
# to be consistent with on-screen representation of numbers.
# Other options would be abs(t1-t2)<10**-self.significant_digits
# or math.is_close (python3.5+)
# Note that abs(3.25-3.251) = 0.0009999999999998899 < 0.001
# Note also that "{:.3f}".format(1.1135) = 1.113, but "{:.3f}".format(1.11351) = 1.114
# For Decimals, format seems to round 2.5 to 2 and 3.5 to 4 (to closest even number)
t1_s = self.number_to_string(level.t1,
significant_digits=self.significant_digits,
number_format_notation=self.number_format_notation) # type: ignore
t2_s = self.number_to_string(level.t2,
significant_digits=self.significant_digits,
number_format_notation=self.number_format_notation) # type: ignore
t1_s = KEY_TO_VAL_STR.format(t1_type, t1_s)
t2_s = KEY_TO_VAL_STR.format(t2_type, t2_s)
if t1_s != t2_s:
self._report_result('values_changed', level, local_tree=local_tree)
def _diff_ipranges(self, level, local_tree=None):
"""Diff IP ranges"""
if str(level.t1) != str(level.t2):
self._report_result('values_changed', level, local_tree=local_tree)
def _diff_datetime(self, level, local_tree=None):
"""Diff DateTimes"""
level.t1 = datetime_normalize(self.truncate_datetime, level.t1, default_timezone=self.default_timezone)
level.t2 = datetime_normalize(self.truncate_datetime, level.t2, default_timezone=self.default_timezone)
if level.t1 != level.t2:
self._report_result('values_changed', level, local_tree=local_tree)
def _diff_time(self, level, local_tree=None):
"""Diff DateTimes"""
if self.truncate_datetime:
level.t1 = datetime_normalize(self.truncate_datetime, level.t1, default_timezone=self.default_timezone)
level.t2 = datetime_normalize(self.truncate_datetime, level.t2, default_timezone=self.default_timezone)
if level.t1 != level.t2:
self._report_result('values_changed', level, local_tree=local_tree)
def _diff_uuids(self, level, local_tree=None):
"""Diff UUIDs"""
if level.t1.int != level.t2.int:
self._report_result('values_changed', level, local_tree=local_tree)
def _diff_numpy_array(self, level, parents_ids=frozenset(), local_tree=None):
"""Diff numpy arrays"""
if level.path() not in self._numpy_paths:
self._numpy_paths[level.path()] = get_type(level.t2).__name__
if np is None:
# This line should never be run. If it is ever called means the type check detected a numpy array
# which means numpy module needs to be available. So np can't be None.
raise ImportError(CANT_FIND_NUMPY_MSG) # pragma: no cover
if (self.ignore_order_func and not self.ignore_order_func(level)) or not self.ignore_order:
# fast checks
if self.significant_digits is None:
if np.array_equal(level.t1, level.t2, equal_nan=self.ignore_nan_inequality):
return # all good
else:
try:
np.testing.assert_almost_equal(level.t1, level.t2, decimal=self.significant_digits)
except TypeError:
np.array_equal(level.t1, level.t2, equal_nan=self.ignore_nan_inequality)
except AssertionError:
pass # do detailed checking below
else:
return # all good
# compare array meta-data
_original_type = level.t1.dtype
if level.t1.shape != level.t2.shape:
# arrays are converted to python lists so that certain features of DeepDiff can apply on them easier.
# They will be converted back to Numpy at their final dimension.
level.t1 = level.t1.tolist()
level.t2 = level.t2.tolist()
self._diff_iterable(level, parents_ids, _original_type=_original_type, local_tree=local_tree)
else:
# metadata same -- the difference is in the content
shape = level.t1.shape
dimensions = len(shape)
if dimensions == 1:
self._diff_iterable(level, parents_ids, _original_type=_original_type, local_tree=local_tree)
elif (self.ignore_order_func and self.ignore_order_func(level)) or self.ignore_order:
# arrays are converted to python lists so that certain features of DeepDiff can apply on them easier.
# They will be converted back to Numpy at their final dimension.
level.t1 = level.t1.tolist()
level.t2 = level.t2.tolist()
self._diff_iterable_with_deephash(level, parents_ids, _original_type=_original_type, local_tree=local_tree)
else:
for (t1_path, t1_row), (t2_path, t2_row) in zip(
get_numpy_ndarray_rows(level.t1, shape),
get_numpy_ndarray_rows(level.t2, shape)):
new_level = level.branch_deeper(
t1_row,
t2_row,
child_relationship_class=NumpyArrayRelationship,
child_relationship_param=t1_path,
child_relationship_param2=t2_path,
)
self._diff_iterable_in_order(new_level, parents_ids, _original_type=_original_type, local_tree=local_tree)
def _diff_types(self, level, local_tree=None):
"""Diff types"""
level.report_type = 'type_changes'
self._report_result('type_changes', level, local_tree=local_tree)
def _count_diff(self):
if (self.max_diffs is not None and self._stats[DIFF_COUNT] > self.max_diffs):
if not self._stats[MAX_DIFF_LIMIT_REACHED]:
self._stats[MAX_DIFF_LIMIT_REACHED] = True
logger.warning(MAX_DIFFS_REACHED_MSG.format(self.max_diffs))
return StopIteration
self._stats[DIFF_COUNT] += 1
if self.cache_size and self.cache_tuning_sample_size:
self._auto_tune_cache()
def _auto_tune_cache(self):
take_sample = (self._stats[DIFF_COUNT] % self.cache_tuning_sample_size == 0)
if self.cache_tuning_sample_size:
if self._stats[DISTANCE_CACHE_ENABLED]:
if take_sample:
self._auto_off_cache()
# Turn on the cache once in a while
elif self._stats[DIFF_COUNT] % self._shared_parameters[_ENABLE_CACHE_EVERY_X_DIFF] == 0:
self.progress_logger('Re-enabling the distance and level caches.')
# decreasing the sampling frequency
self._shared_parameters[_ENABLE_CACHE_EVERY_X_DIFF] *= 10
self._stats[DISTANCE_CACHE_ENABLED] = True
if take_sample:
for key in (PREVIOUS_DIFF_COUNT, PREVIOUS_DISTANCE_CACHE_HIT_COUNT):
self._stats[key] = self._stats[key[9:]]
def _auto_off_cache(self):
"""
Auto adjust the cache based on the usage
"""
if self._stats[DISTANCE_CACHE_ENABLED]:
angle = (self._stats[DISTANCE_CACHE_HIT_COUNT] - self._stats['PREVIOUS {}'.format(DISTANCE_CACHE_HIT_COUNT)]) / (self._stats[DIFF_COUNT] - self._stats[PREVIOUS_DIFF_COUNT])
if angle < self.CACHE_AUTO_ADJUST_THRESHOLD:
self._stats[DISTANCE_CACHE_ENABLED] = False
self.progress_logger('Due to minimal cache hits, {} is disabled.'.format('distance cache'))
def _use_custom_operator(self, level):
"""
For each level we check all custom operators.
If any one of them was a match for the level, we run the diff of the operator.
If the operator returned True, the operator must have decided these objects should not
be compared anymore. It might have already reported their results.
In that case the report will appear in the final results of this diff.
Otherwise basically the 2 objects in the level are being omitted from the results.
"""
for operator in self.custom_operators:
if operator.match(level):
prevent_default = operator.give_up_diffing(level=level, diff_instance=self)
if prevent_default:
return True
return False
def _diff(self, level, parents_ids=frozenset(), _original_type=None, local_tree=None):
"""
The main diff method
**parameters**
level: the tree level or tree node
parents_ids: the ids of all the parent objects in the tree from the current node.
_original_type: If the objects had an original type that was different than what currently exists in the level.t1 and t2
"""
if self._count_diff() is StopIteration:
return
if self._use_custom_operator(level):
return
if level.t1 is level.t2:
return
if self._skip_this(level):
return
report_type_change = True
if get_type(level.t1) != get_type(level.t2):
for type_group in self.ignore_type_in_groups:
if self.type_check_func(level.t1, type_group) and self.type_check_func(level.t2, type_group):
report_type_change = False
break
if self.use_enum_value and isinstance(level.t1, Enum):
level.t1 = level.t1.value
report_type_change = False
if self.use_enum_value and isinstance(level.t2, Enum):
level.t2 = level.t2.value
report_type_change = False
if report_type_change:
self._diff_types(level, local_tree=local_tree)
return
# This is an edge case where t1=None or t2=None and None is in the ignore type group.
if level.t1 is None or level.t2 is None:
self._report_result('values_changed', level, local_tree=local_tree)
return
if self.ignore_nan_inequality and isinstance(level.t1, (float, np_floating)) and str(level.t1) == str(level.t2) == 'nan':
return
if isinstance(level.t1, booleans):
self._diff_booleans(level, local_tree=local_tree)
elif isinstance(level.t1, strings):
# Special handling when comparing string with UUID and ignore_uuid_types is True
if self.ignore_uuid_types and isinstance(level.t2, uuids):
try:
# Convert string to UUID for comparison
t1_uuid = uuid.UUID(level.t1)
if t1_uuid.int != level.t2.int:
self._report_result('values_changed', level, local_tree=local_tree)
except (ValueError, AttributeError):
# If string is not a valid UUID, report as changed
self._report_result('values_changed', level, local_tree=local_tree)
else:
self._diff_str(level, local_tree=local_tree)
elif isinstance(level.t1, datetime.datetime):
self._diff_datetime(level, local_tree=local_tree)
elif isinstance(level.t1, ipranges):
self._diff_ipranges(level, local_tree=local_tree)
elif isinstance(level.t1, (datetime.date, datetime.timedelta, datetime.time)):
self._diff_time(level, local_tree=local_tree)
elif isinstance(level.t1, uuids):
# Special handling when comparing UUID with string and ignore_uuid_types is True
if self.ignore_uuid_types and isinstance(level.t2, str):
try:
# Convert string to UUID for comparison
t2_uuid = uuid.UUID(level.t2)
if level.t1.int != t2_uuid.int:
self._report_result('values_changed', level, local_tree=local_tree)
except (ValueError, AttributeError):
# If string is not a valid UUID, report as changed
self._report_result('values_changed', level, local_tree=local_tree)
else:
self._diff_uuids(level, local_tree=local_tree)
elif isinstance(level.t1, numbers):
self._diff_numbers(level, local_tree=local_tree, report_type_change=report_type_change)
elif isinstance(level.t1, Mapping):
self._diff_dict(level, parents_ids, local_tree=local_tree)
elif isinstance(level.t1, tuple):
self._diff_tuple(level, parents_ids, local_tree=local_tree)
elif isinstance(level.t1, (set, frozenset, SetOrdered)):
self._diff_set(level, local_tree=local_tree)
elif isinstance(level.t1, np_ndarray):
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, is_pydantic_object=True)
elif isinstance(level.t1, Iterable):
self._diff_iterable(level, parents_ids, _original_type=_original_type, local_tree=local_tree)
elif isinstance(level.t1, Enum):
self._diff_enum(level, parents_ids, local_tree=local_tree)
else:
self._diff_obj(level, parents_ids)
def _get_view_results(self, view, verbose_level=None):
"""
Get the results based on the view
"""
result = self.tree
if not self.report_repetition: # and self.is_root:
result.mutual_add_removes_to_become_value_changes()
if view == TREE_VIEW:
pass
elif view == TEXT_VIEW:
effective_verbose_level = verbose_level if verbose_level is not None else self.verbose_level
result = TextResult(tree_results=self.tree, verbose_level=effective_verbose_level)
result.remove_empty_keys()
elif view == DELTA_VIEW:
result = self._to_delta_dict(report_repetition_required=False)
elif view == COLORED_VIEW:
result = ColoredView(t2=self.t2, tree_result=self.tree, compact=False)
elif view == COLORED_COMPACT_VIEW:
result = ColoredView(t2=self.t2, tree_result=self.tree, compact=True)
else:
raise ValueError(INVALID_VIEW_MSG.format(view))
return result
@staticmethod
def _get_key_for_group_by(row, group_by, item_name):
"""
Get the key value to group a row by, using the specified group_by parameter.
Example
>>> row = {'first': 'John', 'middle': 'Joe', 'last': 'Smith'}
>>> DeepDiff._get_key_for_group_by(row, 'first', 't1')
'John'
>>> nested_row = {'id': 123, 'demographics': {'names': {'first': 'John', 'middle': 'Joe', 'last': 'Smith'}}}
>>> group_by = lambda x: x['demographics']['names']['first']
>>> DeepDiff._get_key_for_group_by(nested_row, group_by, 't1')
'John'
Args:
row (dict): The dictionary (row) to extract the group by key from.
group_by (str or callable): The key name or function to call to get to the key value to group by.
item_name (str): The name of the item, used for error messages.
Returns:
str: The key value to group by.
Raises:
KeyError: If the specified key is not found in the row.
"""
try:
if callable(group_by):
return group_by(row)
return row.pop(group_by)
except KeyError:
logger.error("Unable to group {} by {}. The key is missing in {}".format(item_name, group_by, row))
raise
def _group_iterable_to_dict(self, item, group_by, item_name):
"""
Convert a list of dictionaries into a dictionary of dictionaries
where the key is the value of the group_by key in each dictionary.
"""
group_by_level2 = None
if isinstance(group_by, (list, tuple)):
group_by_level1 = group_by[0]
if len(group_by) > 1:
group_by_level2 = group_by[1]
else:
group_by_level1 = group_by
if isinstance(item, Iterable) and not isinstance(item, Mapping):
result = {}
item_copy = deepcopy(item)
for row in item_copy:
if isinstance(row, Mapping):
key1 = self._get_key_for_group_by(row, group_by_level1, item_name)
# Track keys created by group_by to avoid type prefixing later
if hasattr(self, 'group_by_keys'):
self.group_by_keys.add(key1)
if group_by_level2:
key2 = self._get_key_for_group_by(row, group_by_level2, item_name)
# Track level 2 keys as well
if hasattr(self, 'group_by_keys'):
self.group_by_keys.add(key2)
if key1 not in result:
result[key1] = {}
if self.group_by_sort_key:
if key2 not in result[key1]:
result[key1][key2] = []
result_key1_key2 = result[key1][key2]
if row not in result_key1_key2:
result_key1_key2.append(row)
else:
result[key1][key2] = row
else:
if self.group_by_sort_key:
if key1 not in result:
result[key1] = []
if row not in result[key1]:
result[key1].append(row)
else:
result[key1] = row
else:
msg = "Unable to group {} by {} since the item {} is not a dictionary.".format(item_name, group_by_level1, row)
logger.error(msg)
raise ValueError(msg)
if self.group_by_sort_key:
if group_by_level2:
for key1, row1 in result.items():
for key2, row in row1.items():
row.sort(key=self.group_by_sort_key)
else:
for key, row in result.items():
row.sort(key=self.group_by_sort_key)
return result
msg = "Unable to group {} by {}".format(item_name, group_by)
logger.error(msg)
raise ValueError(msg)
def get_stats(self):
"""
Get some stats on internals of the DeepDiff run.
"""
return self._stats
@property
def affected_paths(self):
"""
Get the list of paths that were affected.
Whether a value was changed or they were added or removed.
Example
>>> from pprint import pprint
>>> t1 = {1: 1, 2: 2, 3: [3], 4: 4}
>>> t2 = {1: 1, 2: 4, 3: [3, 4], 5: 5, 6: 6}
>>> ddiff = DeepDiff(t1, t2)
>>> pprint(ddiff, indent=4)
{ 'dictionary_item_added': ['root[5]', 'root[6]'],
'dictionary_item_removed': ['root[4]'],
'iterable_item_added': {'root[3][1]': 4},
'values_changed': {'root[2]': {'new_value': 4, 'old_value': 2}}}
>>> sorted(ddiff.affected_paths)
['root[2]', 'root[3][1]', 'root[4]', 'root[5]', 'root[6]']
>>> sorted(ddiff.affected_root_keys)
[2, 3, 4, 5, 6]
"""
result = SetOrdered()
for key in REPORT_KEYS:
value = self.get(key)
if value:
if isinstance(value, SetOrdered):
result |= value
else:
result |= SetOrdered(value.keys())
return result
@property
def affected_root_keys(self):
"""
Get the list of root keys that were affected.
Whether a value was changed or they were added or removed.
Example
>>> from pprint import pprint
>>> t1 = {1: 1, 2: 2, 3: [3], 4: 4}
>>> t2 = {1: 1, 2: 4, 3: [3, 4], 5: 5, 6: 6}
>>> ddiff = DeepDiff(t1, t2)
>>> pprint(ddiff, indent=4)
{ 'dictionary_item_added': ['root[5]', 'root[6]'],
'dictionary_item_removed': ['root[4]'],
'iterable_item_added': {'root[3][1]': 4},
'values_changed': {'root[2]': {'new_value': 4, 'old_value': 2}}}
>>> sorted(ddiff.affected_paths)
['root[2]', 'root[3][1]', 'root[4]', 'root[5]', 'root[6]']
>>> sorted(ddiff.affected_root_keys)
[2, 3, 4, 5, 6]
"""
result = SetOrdered()
for key in REPORT_KEYS:
value = self.tree.get(key)
if value:
if isinstance(value, SetOrdered):
values_list = value
else:
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
def __str__(self):
if hasattr(self, '_colored_view') and self.view in {COLORED_VIEW, COLORED_COMPACT_VIEW}:
return str(self._colored_view)
return super().__str__()
if __name__ == "__main__": # pragma: no cover
import doctest
doctest.testmod()
qlustered-deepdiff-41c7265/deepdiff/distance.py 0000664 0000000 0000000 00000033614 15162412645 0021472 0 ustar 00root root 0000000 0000000 import math
import datetime
from typing import TYPE_CHECKING, Callable, Protocol, Any, Union, Optional
from deepdiff.deephash import DeepHash
from deepdiff.helper import (
DELTA_VIEW, numbers, strings, add_to_frozen_set, not_found, only_numbers, np, np_float64, time_to_seconds,
cartesian_product_numpy, np_ndarray, np_array_factory, get_homogeneous_numpy_compatible_type_of_seq, dict_,
CannotCompare, NumberType)
from collections.abc import Mapping, Iterable
if TYPE_CHECKING:
from deepdiff.diff import DeepDiffProtocol
class DistanceProtocol(DeepDiffProtocol, Protocol):
hashes: dict
deephash_parameters: dict
ignore_numeric_type_changes: bool
iterable_compare_func: Optional[Callable]
math_epsilon: Optional[float]
cutoff_distance_for_pairs: float
def __get_item_rough_length(self, item, parent:str="root") -> float:
...
def _to_delta_dict(
self,
directed: bool = True,
report_repetition_required: bool = True,
always_include_values: bool = False,
) -> dict:
...
def __calculate_item_deephash(self, item: Any) -> None:
...
DISTANCE_CALCS_NEEDS_CACHE = "Distance calculation can not happen once the cache is purged. Try with _cache='keep'"
class DistanceMixin:
def _get_rough_distance(self: "DistanceProtocol"):
"""
Gives a numeric value for the distance of t1 and t2 based on how many operations are needed to convert
one to the other.
This is a similar concept to the Levenshtein Edit Distance but for the structured data and it is designed
to be between 0 and 1.
A distance of zero means the objects are equal and a distance of 1 is very far.
Note: The distance calculation formula is subject to change in future. Use the distance results only as a
way of comparing the distances of pairs of items with other pairs rather than an absolute distance
such as the one provided by Levenshtein edit distance.
Info: The current algorithm is based on the number of operations that are needed to convert t1 to t2 divided
by the number of items that make up t1 and t2.
"""
_distance = get_numeric_types_distance(
self.t1, self.t2, max_=self.cutoff_distance_for_pairs, use_log_scale=self.use_log_scale, log_scale_similarity_threshold=self.log_scale_similarity_threshold)
if _distance is not not_found:
return _distance
item = self if self.view == DELTA_VIEW else self._to_delta_dict(report_repetition_required=False)
diff_length = _get_item_length(item)
if diff_length == 0:
return 0
t1_len = self.__get_item_rough_length(self.t1)
t2_len = self.__get_item_rough_length(self.t2)
return diff_length / (t1_len + t2_len)
def __get_item_rough_length(self: "DistanceProtocol", item, parent='root'):
"""
Get the rough length of an item.
It is used as a part of calculating the rough distance between objects.
**parameters**
item: The item to calculate the rough length for
parent: It is only used for DeepHash reporting purposes. Not really useful here.
"""
if not hasattr(self, 'hashes'):
raise RuntimeError(DISTANCE_CALCS_NEEDS_CACHE)
length = DeepHash.get_key(self.hashes, key=item, default=None, extract_index=1,
ignore_numeric_type_changes=self.ignore_numeric_type_changes)
if length is None:
self.__calculate_item_deephash(item)
length = DeepHash.get_key(self.hashes, key=item, default=None, extract_index=1,
ignore_numeric_type_changes=self.ignore_numeric_type_changes)
return length
def __calculate_item_deephash(self: "DistanceProtocol", item: Any) -> None:
DeepHash(
item,
hashes=self.hashes,
parent='root',
apply_hash=True,
**self.deephash_parameters,
)
def _precalculate_distance_by_custom_compare_func(
self: "DistanceProtocol", hashes_added, hashes_removed, t1_hashtable, t2_hashtable, _original_type):
pre_calced_distances = dict_()
for added_hash in hashes_added:
for removed_hash in hashes_removed:
try:
is_close_distance = self.iterable_compare_func(t2_hashtable[added_hash].item, t1_hashtable[removed_hash].item)
except CannotCompare:
pass
else:
if is_close_distance:
# an arbitrary small distance if math_epsilon is not defined
distance = self.math_epsilon or 0.000001
else:
distance = 1
pre_calced_distances["{}--{}".format(added_hash, removed_hash)] = distance
return pre_calced_distances
def _precalculate_numpy_arrays_distance(
self: "DistanceProtocol", hashes_added, hashes_removed, t1_hashtable, t2_hashtable, _original_type):
# We only want to deal with 1D arrays.
if isinstance(t2_hashtable[next(iter(hashes_added))].item, (np_ndarray, list)):
return
pre_calced_distances = dict_()
added = [t2_hashtable[k].item for k in hashes_added]
removed = [t1_hashtable[k].item for k in hashes_removed]
if _original_type is None:
added_numpy_compatible_type = get_homogeneous_numpy_compatible_type_of_seq(added)
removed_numpy_compatible_type = get_homogeneous_numpy_compatible_type_of_seq(removed)
if added_numpy_compatible_type and added_numpy_compatible_type == removed_numpy_compatible_type:
_original_type = added_numpy_compatible_type
if _original_type is None:
return
added = np_array_factory(added, dtype=_original_type)
removed = np_array_factory(removed, dtype=_original_type)
pairs = cartesian_product_numpy(added, removed)
pairs_transposed = pairs.T
distances = _get_numpy_array_distance(
pairs_transposed[0], pairs_transposed[1],
max_=self.cutoff_distance_for_pairs,
use_log_scale=self.use_log_scale,
log_scale_similarity_threshold=self.log_scale_similarity_threshold,
)
i = 0
for added_hash in hashes_added:
for removed_hash in hashes_removed:
pre_calced_distances["{}--{}".format(added_hash, removed_hash)] = distances[i]
i += 1
return pre_calced_distances
def _get_item_length(item, parents_ids=frozenset([])):
"""
Get the number of operations in a diff object.
It is designed mainly for the delta view output
but can be used with other dictionary types of view outputs too.
"""
length = 0
if isinstance(item, Mapping):
for key, subitem in item.items():
# dedupe the repetition report so the number of times items have shown up does not affect the distance.
if key in {'iterable_items_added_at_indexes', 'iterable_items_removed_at_indexes'}:
new_subitem = dict_()
for path_, indexes_to_items in subitem.items():
used_value_ids = set()
new_indexes_to_items = dict_()
for k, v in indexes_to_items.items():
v_id = id(v)
if v_id not in used_value_ids:
used_value_ids.add(v_id)
new_indexes_to_items[k] = v
new_subitem[path_] = new_indexes_to_items
subitem = new_subitem
# internal keys such as _numpy_paths should not count towards the distance.
# old_type and old_value are metadata about the previous state, not additional operations.
if isinstance(key, strings) and (key.startswith('_') or key == 'deep_distance' or key == 'new_path'
or key == 'old_type' or key == 'old_value'):
continue
item_id = id(subitem)
if parents_ids and item_id in parents_ids:
continue
parents_ids_added = add_to_frozen_set(parents_ids, item_id)
length += _get_item_length(subitem, parents_ids_added)
elif isinstance(item, numbers):
length = 1
elif isinstance(item, strings):
length = 1
elif isinstance(item, Iterable):
for subitem in item:
item_id = id(subitem)
if parents_ids and item_id in parents_ids:
continue
parents_ids_added = add_to_frozen_set(parents_ids, item_id)
length += _get_item_length(subitem, parents_ids_added)
elif isinstance(item, type): # it is a class
length = 1
else:
if hasattr(item, '__dict__'):
for subitem in item.__dict__:
item_id = id(subitem)
parents_ids_added = add_to_frozen_set(parents_ids, item_id)
length += _get_item_length(subitem, parents_ids_added)
return length
def _get_numbers_distance(num1, num2, max_=1, use_log_scale=False, log_scale_similarity_threshold=0.1):
"""
Get the distance of 2 numbers. The output is a number between 0 to the max.
The reason is the
When max is returned means the 2 numbers are really far, and 0 means they are equal.
"""
if num1 == num2:
return 0
if use_log_scale:
distance = logarithmic_distance(num1, num2)
if distance < 0:
return 0
return distance
if not isinstance(num1, float):
num1 = float(num1)
if not isinstance(num2, float):
num2 = float(num2)
# Since we have a default cutoff of 0.3 distance when
# getting the pairs of items during the ingore_order=True
# calculations, we need to make the divisor of comparison very big
# so that any 2 numbers can be chosen as pairs.
divisor = (num1 + num2) / max_
if divisor == 0:
return max_
try:
return min(max_, abs((num1 - num2) / divisor))
except Exception: # pragma: no cover. I don't think this line will ever run but doesn't hurt to leave it.
return max_ # pragma: no cover
def _numpy_div(a, b, replace_inf_with=1):
max_array = np.full(shape=a.shape, fill_value=replace_inf_with, dtype=np_float64)
result = np.divide(a, b, out=max_array, where=b != 0, dtype=np_float64)
# wherever 2 numbers are the same, make sure the distance is zero. This is mainly for 0 divided by zero.
result[a == b] = 0
return result
# To deal with numbers close to zero
MATH_LOG_OFFSET = 1e-10
def numpy_apply_log_keep_sign(array, offset=MATH_LOG_OFFSET):
# Calculate the absolute value and add the offset
abs_plus_offset = np.abs(array) + offset
# Calculate the logarithm
log_values = np.log(abs_plus_offset)
# Apply the original signs to the log values
signed_log_values = np.copysign(log_values, array)
return signed_log_values
def logarithmic_similarity(a: NumberType, b: NumberType, threshold: float=0.1) -> bool:
"""
A threshold of 0.1 translates to about 10.5% difference.
A threshold of 0.5 translates to about 65% difference.
A threshold of 0.05 translates to about 5.1% difference.
"""
return logarithmic_distance(a, b) < threshold
def logarithmic_distance(a: NumberType, b: NumberType) -> float:
# Apply logarithm to the absolute values and consider the sign
a = float(a)
b = float(b)
log_a = math.copysign(math.log(abs(a) + MATH_LOG_OFFSET), a)
log_b = math.copysign(math.log(abs(b) + MATH_LOG_OFFSET), b)
return abs(log_a - log_b)
def _get_numpy_array_distance(num1, num2, max_=1, use_log_scale=False, log_scale_similarity_threshold=0.1):
"""
Get the distance of 2 numbers. The output is a number between 0 to the max.
The reason is the
When max is returned means the 2 numbers are really far, and 0 means they are equal.
"""
# Since we have a default cutoff of 0.3 distance when
# getting the pairs of items during the ingore_order=True
# calculations, we need to make the divisor of comparison very big
# so that any 2 numbers can be chosen as pairs.
if use_log_scale:
num1 = numpy_apply_log_keep_sign(num1)
num2 = numpy_apply_log_keep_sign(num2)
divisor = (num1 + num2) / max_
result = _numpy_div((num1 - num2), divisor, replace_inf_with=max_)
distance_array = np.clip(np.absolute(result), 0, max_)
if use_log_scale:
distance_array[distance_array < log_scale_similarity_threshold] = 0
return distance_array
def _get_datetime_distance(date1, date2, max_, use_log_scale, log_scale_similarity_threshold):
return _get_numbers_distance(date1.timestamp(), date2.timestamp(), max_)
def _get_date_distance(date1, date2, max_, use_log_scale, log_scale_similarity_threshold):
return _get_numbers_distance(date1.toordinal(), date2.toordinal(), max_)
def _get_timedelta_distance(timedelta1, timedelta2, max_, use_log_scale, log_scale_similarity_threshold):
return _get_numbers_distance(timedelta1.total_seconds(), timedelta2.total_seconds(), max_)
def _get_time_distance(time1, time2, max_, use_log_scale, log_scale_similarity_threshold):
return _get_numbers_distance(time_to_seconds(time1), time_to_seconds(time2), max_)
TYPES_TO_DIST_FUNC = [
(only_numbers, _get_numbers_distance),
(datetime.datetime, _get_datetime_distance),
(datetime.date, _get_date_distance),
(datetime.timedelta, _get_timedelta_distance),
(datetime.time, _get_time_distance),
]
def get_numeric_types_distance(num1, num2, max_, use_log_scale=False, log_scale_similarity_threshold=0.1):
for type_, func in TYPES_TO_DIST_FUNC:
if isinstance(num1, type_) and isinstance(num2, type_):
return func(num1, num2, max_, use_log_scale, log_scale_similarity_threshold)
return not_found
qlustered-deepdiff-41c7265/deepdiff/docstrings/ 0000775 0000000 0000000 00000000000 15162412645 0021476 5 ustar 00root root 0000000 0000000 qlustered-deepdiff-41c7265/deepdiff/docstrings/authors.rst 0000664 0000000 0000000 00000021656 15162412645 0023727 0 ustar 00root root 0000000 0000000 :doc:`/index`
Authors
=======
Authors in order of the timeline of their contributions:
- `Sep Dehpour (Seperman)`_
- `Victor Hahn Castell`_ for the tree view and major contributions:
- `nfvs`_ for Travis-CI setup script.
- `brbsix`_ for initial Py3 porting.
- `WangFenjin`_ for unicode support.
- `timoilya`_ for comparing list of sets when ignoring order.
- `Bernhard10`_ for significant digits comparison.
- `b-jazz`_ for PEP257 cleanup, Standardize on full names, fixing line
endings.
- `finnhughes`_ for fixing **slots**
- `moloney`_ for Unicode vs. Bytes default
- `serv-inc`_ for adding help(deepdiff)
- `movermeyer`_ for updating docs
- `maxrothman`_ for search in inherited class attributes
- `maxrothman`_ for search for types/objects
- `MartyHub`_ for exclude regex paths
- `sreecodeslayer`_ for DeepSearch match_string
- Brian Maissy `brianmaissy`_ for weakref fix, enum tests
- Bartosz Borowik `boba-2`_ for Exclude types fix when ignoring order
- Brian Maissy `brianmaissy `__ for
fixing classes which inherit from classes with slots didn’t have all
of their slots compared
- Juan Soler `Soleronline`_ for adding ignore_type_number
- `mthaddon`_ for adding timedelta diffing support
- `Necrophagos`_ for Hashing of the number 1 vs. True
- `gaal-dev`_ for adding exclude_obj_callback
- Ivan Piskunov `van-ess0`_ for deprecation warning enhancement.
- Michał Karaś `MKaras93`_ for the pretty view
- Christian Kothe `chkothe`_ for the basic support for diffing numpy
arrays
- `Timothy`_ for truncate_datetime
- `d0b3rm4n`_ for bugfix to not apply format to non numbers.
- `MyrikLD`_ for Bug Fix NoneType in ignore type groups
- Stian Jensen `stianjensen`_ for improving ignoring of NoneType in
diff
- Florian Klien `flowolf`_ for adding math_epsilon
- Tim Klein `timjklein36`_ for retaining the order of multiple
dictionary items added via Delta.
- Wilhelm Schürmann\ `wbsch`_ for fixing the typo with yml files.
- `lyz-code`_ for adding support for regular expressions in DeepSearch
and strict_checking feature in DeepSearch.
- `dtorres-sf`_ for adding the option for custom compare function
- Tony Wang `Tony-Wang`_ for bugfix: verbose_level==0 should disable
values_changes.
- Sun Ao `eggachecat`_ for adding custom operators.
- Sun Ao `eggachecat`_ for adding ignore_order_func.
- `SlavaSkvortsov`_ for fixing unprocessed key error.
- HÃ¥vard Thom `havardthom`_ for adding UUID support.
- Dhanvantari Tilak `Dhanvantari`_ for Bug-Fix:
``TypeError in _get_numbers_distance() when ignore_order = True``.
- Yael Mintz `yaelmi3`_ for detailed pretty print when verbose_level=2.
- Mikhail Khviyuzov `mskhviyu`_ for Exclude obj callback strict.
- `dtorres-sf`_ for the fix for diffing using iterable_compare_func with nested objects.
- `Enric Pou `__ for bug fix of ValueError
when using Decimal 0.x
- `Uwe Fladrich `__ for fixing bug when diff'ing non-sequence iterables
- `Michal Ozery-Flato `__ for
setting equal_nan=ignore_nan_inequality in the call for
np.array_equal
- `martin-kokos `__ for using Pytest’s
tmp_path fixture instead of /tmp/
- HÃ¥vard Thom `havardthom `__ for adding
include_obj_callback and include_obj_callback_strict.
- `Noam Gottlieb `__ for fixing a corner
case where numpy’s ``np.float32`` nans are not ignored when using
``ignore_nan_equality``.
- `maggelus `__ for the bugfix deephash
for paths.
- `maggelus `__ for the bugfix deephash
compiled regex.
- `martin-kokos `__ for fixing the
tests dependent on toml.
- `kor4ik `__ for the bugfix for
``include_paths`` for nested dictionaries.
- `martin-kokos `__ for using tomli
and tomli-w for dealing with tomli files.
- `Alex Sauer-Budge `__ for the bugfix for
``datetime.date``.
- `William Jamieson `__ for `NumPy 2.0 compatibility `__
- `Leo Sin `__ for Supporting Python 3.12 in
the build process
- `sf-tcalhoun `__ for fixing
“Instantiating a Delta with a flat_dict_list unexpectedly mutates the
flat_dict_list�
- `dtorres-sf `__ for fixing iterable
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
- `dtorres-sf `__ for the fix for moving
nested tables when using iterable_compare_func.
- `Jim Cipar `__ for the fix recursion depth
limit when hashing numpy.datetime64
- `Enji Cooper `__ for converting legacy
setuptools use to pyproject.toml
- `Diogo Correia `__ for reporting security vulnerability in Delta and DeepDiff that could allow remote code execution.
- `am-periphery `__ for reporting CVE-2026-33155: denial-of-service via crafted pickle payloads triggering massive memory allocation.
- `echan5 `__ for adding callable ``group_by`` support.
- `yannrouillard `__ for fixing colored view display when all list items are removed.
- `tpvasconcelos `__ for fixing ``__slots__`` handling for objects with ``__getattr__``.
- `devin13cox `__ for always using t1 path for reporting.
- `vitalis89 `__ for fixing ``ignore_keys`` issue in ``detailed__dict__``.
- `ljames8 `__ for fixing logarithmic similarity type hint.
- `srini047 `__ for fixing README typo.
- `Nagato-Yuzuru `__ for colored view tests.
- `akshat62 `__ for adding Fraction numeric support.
.. _Sep Dehpour (Seperman): http://www.zepworks.com
.. _Victor Hahn Castell: http://hahncastell.de
.. _nfvs: https://github.com/nfvs
.. _brbsix: https://github.com/brbsix
.. _WangFenjin: https://github.com/WangFenjin
.. _timoilya: https://github.com/timoilya
.. _Bernhard10: https://github.com/Bernhard10
.. _b-jazz: https://github.com/b-jazz
.. _finnhughes: https://github.com/finnhughes
.. _moloney: https://github.com/moloney
.. _serv-inc: https://github.com/serv-inc
.. _movermeyer: https://github.com/movermeyer
.. _maxrothman: https://github.com/maxrothman
.. _MartyHub: https://github.com/MartyHub
.. _sreecodeslayer: https://github.com/sreecodeslayer
.. _brianmaissy: https://github.com/
.. _boba-2: https://github.com/boba-2
.. _Soleronline: https://github.com/Soleronline
.. _mthaddon: https://github.com/mthaddon
.. _Necrophagos: https://github.com/Necrophagos
.. _gaal-dev: https://github.com/gaal-dev
.. _van-ess0: https://github.com/van-ess0
.. _MKaras93: https://github.com/MKaras93
.. _chkothe: https://github.com/chkothe
.. _Timothy: https://github.com/timson
.. _d0b3rm4n: https://github.com/d0b3rm4n
.. _MyrikLD: https://github.com/MyrikLD
.. _stianjensen: https://github.com/stianjensen
.. _flowolf: https://github.com/flowolf
.. _timjklein36: https://github.com/timjklein36
.. _wbsch: https://github.com/wbsch
.. _lyz-code: https://github.com/lyz-code
.. _dtorres-sf: https://github.com/dtorres-sf
.. _Tony-Wang: https://github.com/Tony-Wang
.. _eggachecat: https://github.com/eggachecat
.. _SlavaSkvortsov: https://github.com/SlavaSkvortsov
.. _havardthom: https://github.com/havardthom
.. _Dhanvantari: https://github.com/Dhanvantari
.. _yaelmi3: https://github.com/yaelmi3
.. _mskhviyu: https://github.com/mskhviyu
Thank you for contributing to DeepDiff!
Back to :doc:`/index`
qlustered-deepdiff-41c7265/deepdiff/docstrings/basics.rst 0000664 0000000 0000000 00000034421 15162412645 0023500 0 ustar 00root root 0000000 0000000 :doc:`/index`
Basics
======
Importing
>>> from deepdiff import DeepDiff
>>> from pprint import pprint
Same object returns empty
>>> t1 = {1:1, 2:2, 3:3}
>>> t2 = t1
>>> print(DeepDiff(t1, t2))
{}
Type of an item has changed
>>> t1 = {1:1, 2:2, 3:3}
>>> t2 = {1:1, 2:"2", 3:3}
>>> pprint(DeepDiff(t1, t2), indent=2)
{ 'type_changes': { 'root[2]': { 'new_type': ,
'new_value': '2',
'old_type': ,
'old_value': 2}}}
Value of an item has changed
>>> t1 = {1:1, 2:2, 3:3}
>>> t2 = {1:1, 2:4, 3:3}
>>> pprint(DeepDiff(t1, t2, verbose_level=0), indent=2)
{'values_changed': {'root[2]': {'new_value': 4, 'old_value': 2}}}
Item added and/or removed
>>> t1 = {1:1, 3:3, 4:4}
>>> t2 = {1:1, 3:3, 5:5, 6:6}
>>> ddiff = DeepDiff(t1, t2)
>>> pprint (ddiff)
{'dictionary_item_added': [root[5], root[6]],
'dictionary_item_removed': [root[4]]}
Set verbose level to 2 in order to see the added or removed items with their values
>>> t1 = {1:1, 3:3, 4:4}
>>> t2 = {1:1, 3:3, 5:5, 6:6}
>>> ddiff = DeepDiff(t1, t2, verbose_level=2)
>>> pprint(ddiff, indent=2)
{ 'dictionary_item_added': {'root[5]': 5, 'root[6]': 6},
'dictionary_item_removed': {'root[4]': 4}}
Set verbose level to 2 includes new_path when the path has changed for a report between t1 and t2:
>>> t1 = [1, 3]
>>> t2 = [3, 2]
>>>
>>>
>>> diff = DeepDiff(t1, t2, ignore_order=True, verbose_level=2)
>>> pprint(diff)
{'values_changed': {'root[0]': {'new_path': 'root[1]',
'new_value': 2,
'old_value': 1}}}
String difference
>>> t1 = {1:1, 2:2, 3:3, 4:{"a":"hello", "b":"world"}}
>>> t2 = {1:1, 2:4, 3:3, 4:{"a":"hello", "b":"world!"}}
>>> ddiff = DeepDiff(t1, t2)
>>> pprint (ddiff, indent = 2)
{ 'values_changed': { 'root[2]': {'new_value': 4, 'old_value': 2},
"root[4]['b']": { 'new_value': 'world!',
'old_value': 'world'}}}
String difference 2
>>> t1 = {1:1, 2:2, 3:3, 4:{"a":"hello", "b":"world!\nGoodbye!\n1\n2\nEnd"}}
>>> t2 = {1:1, 2:2, 3:3, 4:{"a":"hello", "b":"world\n1\n2\nEnd"}}
>>> ddiff = DeepDiff(t1, t2)
>>> pprint (ddiff, indent = 2)
{ 'values_changed': { "root[4]['b']": { 'diff': '--- \n'
'+++ \n'
'@@ -1,5 +1,4 @@\n'
'-world!\n'
'-Goodbye!\n'
'+world\n'
' 1\n'
' 2\n'
' End',
'new_value': 'world\n1\n2\nEnd',
'old_value': 'world!\n'
'Goodbye!\n'
'1\n'
'2\n'
'End'}}}
>>>
>>> print (ddiff['values_changed']["root[4]['b']"]["diff"])
---
+++
@@ -1,5 +1,4 @@
-world!
-Goodbye!
+world
1
2
End
List difference
>>> t1 = {1:1, 2:2, 3:3, 4:{"a":"hello", "b":[1, 2, 3, 4]}}
>>> t2 = {1:1, 2:2, 3:3, 4:{"a":"hello", "b":[1, 2]}}
>>> ddiff = DeepDiff(t1, t2)
>>> pprint (ddiff, indent = 2)
{'iterable_item_removed': {"root[4]['b'][2]": 3, "root[4]['b'][3]": 4}}
List that contains dictionary:
>>> t1 = {1:1, 2:2, 3:3, 4:{"a":"hello", "b":[1, 2, {1:1, 2:2}]}}
>>> t2 = {1:1, 2:2, 3:3, 4:{"a":"hello", "b":[1, 2, {1:3}]}}
>>> ddiff = DeepDiff(t1, t2)
>>> pprint (ddiff, indent = 2)
{ 'dictionary_item_removed': [root[4]['b'][2][2]],
'values_changed': {"root[4]['b'][2][1]": {'new_value': 3, 'old_value': 1}}}
Sets:
>>> t1 = {1, 2, 8}
>>> t2 = {1, 2, 3, 5}
>>> ddiff = DeepDiff(t1, t2)
>>> pprint(ddiff)
{'set_item_added': [root[3], root[5]], 'set_item_removed': [root[8]]}
Named Tuples:
>>> from collections import namedtuple
>>> Point = namedtuple('Point', ['x', 'y'])
>>> t1 = Point(x=11, y=22)
>>> t2 = Point(x=11, y=23)
>>> pprint (DeepDiff(t1, t2))
{'values_changed': {'root.y': {'new_value': 23, 'old_value': 22}}}
Custom objects:
>>> class ClassA(object):
... a = 1
... def __init__(self, b):
... self.b = b
...
>>> t1 = ClassA(1)
>>> t2 = ClassA(2)
>>>
>>> pprint(DeepDiff(t1, t2))
{'values_changed': {'root.b': {'new_value': 2, 'old_value': 1}}}
Object attribute added:
>>> t2.c = "new attribute"
>>> pprint(DeepDiff(t1, t2))
{'attribute_added': [root.c],
'values_changed': {'root.b': {'new_value': 2, 'old_value': 1}}}
Datetime
DeepDiff converts all datetimes into UTC. If a datetime is timezone naive, we assume it is in UTC too.
That is different than what Python does. Python assumes your timezone naive datetime is in your local timezone.
>>> from deepdiff import DeepDiff
>>> from datetime import datetime, timezone
>>> d1 = datetime(2020, 8, 31, 13, 14, 1)
>>> d2 = datetime(2020, 8, 31, 13, 14, 1, tzinfo=timezone.utc)
>>> d1 == d2
False
>>> DeepDiff(d1, d2)
{}
.. note::
All the examples above use the default :ref:`text_view_label`.
If you want traversing functionality in the results, use the :ref:`tree_view_label`.
You just need to set view='tree' to get it in tree form.
.. _group_by_label:
Group By
--------
group_by can be used when dealing with the list of dictionaries. It converts them from lists to a single dictionary with the key defined by group_by. The common use case is when reading data from a flat CSV, and the primary key is one of the columns in the CSV. We want to use the primary key instead of the CSV row number to group the rows. The group_by can do 2D group_by by passing a list of 2 keys. It is also possible to have a callable group_by, which can be used to access keys in more nested data structures.
For example:
>>> [
... {'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody'},
... {'id': 'BB', 'name': 'James', 'last_name': 'Blue'},
... {'id': 'CC', 'name': 'Mike', 'last_name': 'Apple'},
... ]
Becomes:
>>> t1 = {
... 'AA': {'name': 'Joe', 'last_name': 'Nobody'},
... 'BB': {'name': 'James', 'last_name': 'Blue'},
... 'CC': {'name': 'Mike', 'last_name': 'Apple'},
... }
With that in mind, let's take a look at the following:
>>> from deepdiff import DeepDiff
>>> t1 = [
... {'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody'},
... {'id': 'BB', 'name': 'James', 'last_name': 'Blue'},
... {'id': 'CC', 'name': 'Mike', 'last_name': 'Apple'},
... ]
>>>
>>> t2 = [
... {'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody'},
... {'id': 'BB', 'name': 'James', 'last_name': 'Brown'},
... {'id': 'CC', 'name': 'Mike', 'last_name': 'Apple'},
... ]
>>>
>>> DeepDiff(t1, t2)
{'values_changed': {"root[1]['last_name']": {'new_value': 'Brown', 'old_value': 'Blue'}}}
Now we use group_by='id':
>>> DeepDiff(t1, t2, group_by='id')
{'values_changed': {"root['BB']['last_name']": {'new_value': 'Brown', 'old_value': 'Blue'}}}
.. note::
group_by actually changes the structure of the t1 and t2. You can see this by using the tree view:
>>> diff = DeepDiff(t1, t2, group_by='id', view='tree')
>>> diff
{'values_changed': []}
>>> diff['values_changed'][0]
>>> diff['values_changed'][0].up
>>> diff['values_changed'][0].up.up
>>> diff['values_changed'][0].up.up.t1
{'AA': {'name': 'Joe', 'last_name': 'Nobody'}, 'BB': {'name': 'James', 'last_name': 'Blue'}, 'CC': {'name': 'Mike', 'last_name': 'Apple'}}
2D Example:
>>> from pprint import pprint
>>> from deepdiff import DeepDiff
>>>
>>> t1 = [
... {'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody'},
... {'id': 'BB', 'name': 'James', 'last_name': 'Blue'},
... {'id': 'BB', 'name': 'Jimmy', 'last_name': 'Red'},
... {'id': 'CC', 'name': 'Mike', 'last_name': 'Apple'},
... ]
>>>
>>> t2 = [
... {'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody'},
... {'id': 'BB', 'name': 'James', 'last_name': 'Brown'},
... {'id': 'CC', 'name': 'Mike', 'last_name': 'Apple'},
... ]
>>>
>>> diff = DeepDiff(t1, t2, group_by=['id', 'name'])
>>> pprint(diff)
{'dictionary_item_removed': [root['BB']['Jimmy']],
'values_changed': {"root['BB']['James']['last_name']": {'new_value': 'Brown',
'old_value': 'Blue'}}}
Callable group_by Example:
>>> from deepdiff import DeepDiff
>>>
>>> t1 = [
... {'id': 'AA', 'demographics': {'names': {'first': 'Joe', 'middle': 'John', 'last': 'Nobody'}}},
... {'id': 'BB', 'demographics': {'names': {'first': 'James', 'middle': 'Joyce', 'last': 'Blue'}}},
... {'id': 'CC', 'demographics': {'names': {'first': 'Mike', 'middle': 'Mark', 'last': 'Apple'}}},
... ]
>>>
>>> t2 = [
... {'id': 'AA', 'demographics': {'names': {'first': 'Joe', 'middle': 'John', 'last': 'Nobody'}}},
... {'id': 'BB', 'demographics': {'names': {'first': 'James', 'middle': 'Joyce', 'last': 'Brown'}}},
... {'id': 'CC', 'demographics': {'names': {'first': 'Mike', 'middle': 'Charles', 'last': 'Apple'}}},
... ]
>>>
>>> diff = DeepDiff(t1, t2, group_by=lambda x: x['demographics']['names']['first'])
>>> pprint(diff)
{'values_changed': {"root['James']['demographics']['names']['last']": {'new_value': 'Brown',
'old_value': 'Blue'},
"root['Mike']['demographics']['names']['middle']": {'new_value': 'Charles',
'old_value': 'Mark'}}}
.. _group_by_sort_key_label:
Group By - Sort Key
-------------------
group_by_sort_key is used to define how dictionaries are sorted if multiple ones fall under one group. When this parameter is used, group_by converts the lists of dictionaries into a dictionary of keys to lists of dictionaries. Then, group_by_sort_key is used to sort between the list.
For example, there are duplicate id values. If we only use group_by='id', one of the dictionaries with id of 'BB' will overwrite the other. However, if we also set group_by_sort_key='name', we keep both dictionaries with the id of 'BB'.
Example:
>>> [{'id': 'AA', 'int_id': 2, 'last_name': 'Nobody', 'name': 'Joe'},
... {'id': 'BB', 'int_id': 20, 'last_name': 'Blue', 'name': 'James'},
... {'id': 'BB', 'int_id': 3, 'last_name': 'Red', 'name': 'Jimmy'},
... {'id': 'CC', 'int_id': 4, 'last_name': 'Apple', 'name': 'Mike'}]
Becomes:
>>> {'AA': [{'int_id': 2, 'last_name': 'Nobody', 'name': 'Joe'}],
... 'BB': [{'int_id': 20, 'last_name': 'Blue', 'name': 'James'},
... {'int_id': 3, 'last_name': 'Red', 'name': 'Jimmy'}],
... 'CC': [{'int_id': 4, 'last_name': 'Apple', 'name': 'Mike'}]}
Example of using group_by_sort_key
>>> t1 = [
... {'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody', 'int_id': 2},
... {'id': 'BB', 'name': 'James', 'last_name': 'Blue', 'int_id': 20},
... {'id': 'BB', 'name': 'Jimmy', 'last_name': 'Red', 'int_id': 3},
... {'id': 'CC', 'name': 'Mike', 'last_name': 'Apple', 'int_id': 4},
... ]
>>>
>>> t2 = [
... {'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody', 'int_id': 2},
... {'id': 'BB', 'name': 'James', 'last_name': 'Brown', 'int_id': 20},
... {'id': 'CC', 'name': 'Mike', 'last_name': 'Apple', 'int_id': 4},
... ]
>>>
>>> diff = DeepDiff(t1, t2, group_by='id', group_by_sort_key='name')
>>>
>>> pprint(diff)
{'iterable_item_removed': {"root['BB'][1]": {'int_id': 3,
'last_name': 'Red',
'name': 'Jimmy'}},
'values_changed': {"root['BB'][0]['last_name']": {'new_value': 'Brown',
'old_value': 'Blue'}}}
.. _default_timezone_label:
Default Time Zone
-----------------
default_timezone defines the default timezone. If a datetime is timezone naive, which means it doesn't have a timezone, we assume the datetime is in this timezone. Also any datetime that has a timezone will be converted to this timezone so the datetimes can be compared properly all in the same timezone. Note that Python's default behavior assumes the default timezone is your local timezone. DeepDiff's default is UTC, not your local time zone.
Note that if we change the default_timezone, the output timezone changes accordingly
>>> from deepdiff import DeepDiff
>>> import pytz
>>> from datetime import date, datetime, time, timezone
>>> dt_utc = datetime(2025, 2, 3, 12, 0, 0, tzinfo=pytz.utc) # UTC timezone
>>> dt_utc2 = datetime(2025, 2, 3, 11, 0, 0, tzinfo=pytz.utc) # UTC timezone
>>> dt_ny = dt_utc.astimezone(pytz.timezone('America/New_York'))
>>> dt_ny2 = dt_utc2.astimezone(pytz.timezone('America/New_York'))
>>> diff = DeepDiff(dt_ny, dt_ny2)
>>> diff
{'values_changed': {'root': {'new_value': datetime.datetime(2025, 2, 3, 11, 0, tzinfo=datetime.timezone.utc), 'old_value': datetime.datetime(2025, 2, 3, 12, 0, tzinfo=datetime.timezone.utc)}}}
>>> diff2 = DeepDiff(dt_ny, dt_ny2, default_timezone=pytz.timezone('America/New_York'))
>>> diff2
{'values_changed': {'root': {'new_value': datetime.datetime(2025, 2, 3, 6, 0, tzinfo=), 'old_value': datetime.datetime(2025, 2, 3, 7, 0, tzinfo=)}}}
Back to :doc:`/index`
qlustered-deepdiff-41c7265/deepdiff/docstrings/changelog.rst 0000664 0000000 0000000 00000043333 15162412645 0024165 0 ustar 00root root 0000000 0000000 :doc:`/index`
Changelog
=========
DeepDiff Changelog
- v9-0-0
- migration note:
- `to_dict()` and `to_json()` now accept a `verbose_level` parameter and always return a usable text-view dict. When the original view is `'tree'`, they default to `verbose_level=2` for full detail. The old `view_override` parameter is removed. To get the previous results, you will need to pass the explicit verbose_level to `to_json` and `to_dict` if you are using the tree view.
- Dropping support for Python 3.9
- Support for python 3.14
- Added support for callable ``group_by`` thanks to `echan5 `__
- Added ``FlatDeltaDict`` TypedDict for ``to_flat_dicts`` return type
- Fixed colored view display when all list items are removed thanks to `yannrouillard `__
- Fixed ``hasattr()`` swallowing ``AttributeError`` in ``__slots__`` handling for objects with ``__getattr__`` thanks to `tpvasconcelos `__
- Fixed ``ignore_order=True`` missing int-vs-float type changes
- Always use t1 path for reporting thanks to `devin13cox `__
- Fixed ``_convert_oversized_ints`` failing on NamedTuples
- Fixed orjson ``TypeError`` for integers exceeding 64-bit range
- Fixed parameter bug in ``to_flat_dicts`` where ``include_action_in_path`` and ``report_type_changes`` were not being passed through
- Fixed ``ignore_keys`` issue in ``detailed__dict__`` thanks to `vitalis89 `__
- Fixed logarithmic similarity type hint thanks to `ljames8 `__
- Added ``Fraction`` numeric support thanks to `akshat62 `__
- v8-6-2
- Security fix (CVE-2026-33155): Prevent denial-of-service via crafted pickle payloads that trigger massive memory allocation through the REDUCE opcode. Size-sensitive callables like ``bytes()`` and ``bytearray()`` are now wrapped to reject allocations exceeding 128 MB.
- v8-6-1
- Patched security vulnerability in the Delta class which was vulnerable to class pollution via its constructor, and when combined with a gadget available in DeltaDiff itself, it could lead to Denial of Service and Remote Code Execution (via insecure Pickle deserialization).
- v8-6-0
- Added Colored View thanks to @mauvilsa
- Added support for applying deltas to NamedTuple thanks to @paulsc
- Fixed test_delta.py with Python 3.14 thanks to @Romain-Geissler-1A
- Added python property serialization to json
- Added ip address serialization
- Switched to UV from pip
- Added Claude.md
- Added uuid hashing thanks to @akshat62
- Added ``ignore_uuid_types`` flag to DeepDiff to avoid type reports
when comparing UUID and string.
- Added comprehensive type hints across the codebase (multiple commits
for better type safety)
- Added support for memoryview serialization
- Added support for bytes serialization (non-UTF8 compatible)
- Fixed bug where group_by with numbers would leak type info into group
path reports
- Fixed bug in ``_get_clean_to_keys_mapping`` without explicit
significant digits
- Added support for python dict key serialization
- Enhanced support for IP address serialization with safe module imports
- Added development tooling improvements (pyright config, .envrc
example)
- Updated documentation and development instructions
- v8-5-0
- Updating deprecated pydantic calls
- Switching to pyproject.toml
- Fix for moving nested tables when using iterable_compare_func. by
- Fix recursion depth limit when hashing numpy.datetime64
- Moving from legacy setuptools use to pyproject.toml
- v8-4-2
- fixes the type hints for the base
- fixes summarize so if json dumps fails, we can still get a repr of the results
- adds ipaddress support
- v8-4-1
- Adding BaseOperatorPlus base class for custom operators
- default_timezone can be passed now to set your default timezone to something other than UTC.
- New summarization algorithm that produces valid json
- Better type hint support
- Breaking change in DeepHash where we raise Exception instead of logging if we can't hash a value.
- Added the log_stacktrace parameter to DeepDiff. When True, it will log the stacktrace along with the error.
- v8-3-0
- Fixed some static typing issues
- Added the summarize module for better repr of nested values
- v8-2-0
- Small optimizations so we don't load functions that are not needed
- Updated the minimum version of Orderly-set
- Normalize all datetimes into UTC. Assume timezone naive datetimes are UTC.
- 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
- Fixes issue where the key deep_distance is not returned when both
compared items are equal
- Fixes exclude_paths fails to work in certain cases
- exclude_paths fails to work
- Fixes to_json() method chokes on standard json.dumps() kwargs such as
sort_keys
- to_dict() method chokes on standard json.dumps() kwargs
- Fixes accessing the affected_root_keys property on the diff object
returned by DeepDiff fails when one of the dicts is empty
- Fixes accessing the affected_root_keys property on the
diff object returned by DeepDiff fails when one of the dicts is empty
- v8-0-1
- Bugfix. Numpy should be optional.
- v8-0-0
- With the introduction of `threshold_to_diff_deeper`, the values returned are different than in previous versions of DeepDiff. You can still get the older values by setting `threshold_to_diff_deeper=0`. However to signify that enough has changed in this release that the users need to update the parameters passed to DeepDiff, we will be doing a major version update.
- `use_enum_value=True` makes it so when diffing enum, we use the enum's value. It makes it so comparing an enum to a string or any other value is not reported as a type change.
- `threshold_to_diff_deeper=float` is a number between 0 and 1. When comparing dictionaries that have a small intersection of keys, we will report the dictionary as a `new_value` instead of reporting individual keys changed. If you set it to zero, you get the same results as DeepDiff 7.0.1 and earlier, which means this feature is disabled. The new default is 0.33 which means if less that one third of keys between dictionaries intersect, report it as a new object.
- Deprecated `ordered-set` and switched to `orderly-set`. The `ordered-set` package was not being maintained anymore and starting Python 3.6, there were better options for sets that ordered. I forked one of the new implementations, modified it, and published it as `orderly-set`.
- Added `use_log_scale:bool` and `log_scale_similarity_threshold:float`. They can be used to ignore small changes in numbers by comparing their differences in logarithmic space. This is different than ignoring the difference based on significant digits.
- json serialization of reversed lists.
- Fix for iterable moved items when `iterable_compare_func` is used.
- Pandas and Polars support
- v7-0-1
- Fixes the translation between Difflib opcodes and Delta flat rows.
- v7-0-0
- When verbose=2, return ``new_path`` when the ``path`` and
``new_path`` are different (for example when ignore_order=True and
the index of items have changed).
- Dropping support for Python 3.7
- Introducing serialize to flat rows for delta objects.
- fixes the issue with hashing ``datetime.date`` objects where it
treated them as numbers instead of dates (fixes #445).
- upgrading orjson to the latest version
- Fix for bug when diffing two lists with ignore_order and providing
compare_func
- Fixes “Wrong diff on list of strings� #438
- Supporting Python 3.12 in the build process by `Leo
Sin `__
- Fixes “Instantiating a Delta with a flat_dict_list unexpectedly
mutates the flat_dict_list� #457 by
`sf-tcalhoun `__
- Fixes “Error on Delta With None Key and Removed Item from List�
#441
- Fixes “Error when comparing two nested dicts with 2 added fields�
#450
- Fixes “Error when subtracting Delta from a dictionary� #443
- v6-7-1
- Support for subtracting delta objects when iterable_compare_func
is used.
- Better handling of force adding a delta to an object.
- Fix for
```Can't compare dicts with both single and double quotes in keys`` `__
- Updated docs for Inconsistent Behavior with math_epsilon and
ignore_order = True
- v6-7-0
- Delta can be subtracted from other objects now.
- verify_symmetry is deprecated. Use bidirectional instead.
- always_include_values flag in Delta can be enabled to include
values in the delta for every change.
- Fix for Delta.\__add\_\_ breaks with esoteric dict keys.
- v6-6-1
- Fix for `DeepDiff raises decimal exception when using significant
digits `__
- Introducing group_by_sort_key
- Adding group_by 2D. For example
``group_by=['last_name', 'zip_code']``
- v6-6-0
- Numpy 2.0 support
- Adding
`Delta.to_flat_dicts `__
- v6-5-0
- Adding
```parse_path`` `__
- v6-4-1
- Bugfix: Keep Numpy Optional
- v6-4-0
- `Add Ignore List Order Option to
DeepHash `__ by
`Bobby Morck `__
- `pyyaml to 6.0.1 to fix cython build
problems `__ by
`Robert Bo Davis `__
- `Precompiled regex simple
diff `__ by
`cohml `__
- New flag: ``zip_ordered_iterables`` for forcing iterable items to
be compared one by one.
- v6-3-1
- Bugfix deephash for paths by
`maggelus `__
- Bugfix deephash compiled regex
`maggelus `__
- Fix tests dependent on toml by
`martin-kokos `__
- Bugfix for ``include_paths`` for nested dictionaries by
`kor4ik `__
- Use tomli and tomli-w for dealing with tomli files by
`martin-kokos `__
- Bugfix for ``datetime.date`` by `Alex
Sauer-Budge `__
- v6-3-0
- ``PrefixOrSuffixOperator``: This operator will skip strings that
are suffix or prefix of each other.
- ``include_obj_callback`` and ``include_obj_callback_strict`` are
added by `HÃ¥vard Thom `__.
- Fixed a corner case where numpy’s ``np.float32`` nans are not
ignored when using ``ignore_nan_equality`` by `Noam
Gottlieb `__
- ``orjson`` becomes optional again.
- Fix for ``ignore_type_in_groups`` with numeric values so it does
not report number changes when the number types are different.
- v6-2-3
- Switching to Orjson for serialization to improve the performance.
- Setting ``equal_nan=ignore_nan_inequality`` in the call for
``np.array_equal``
- Using Pytest’s tmp_path fixture instead of ``/tmp/``
- v6-2-2
- Enum test fix for python 3.11
- Adding support for dateutils rrules
- v6-2-1
- Removed the print statements.
- v6-2-0
- Major improvement in the diff report for lists when items are all
hashable and the order of items is important.
- v6-1-0
- DeepDiff.affected_paths can be used to get the list of all paths
where a change, addition, or deletion was reported for.
- DeepDiff.affected_root_keys can be used to get the list of all
paths where a change, addition, or deletion was reported for.
- Bugfix: ValueError when using Decimal 0.x #339 by `Enric
Pou `__
- Serialization of UUID
- v6-0-0
- `Exclude obj callback
strict `__
parameter is added to DeepDiff by Mikhail Khviyuzov
`mskhviyu `__.
- A fix for diffing using ``iterable_compare_func`` with nested
objects by `dtorres-sf `__ who
originally contributed this feature.
- v5-7-0:
- https://github.com/seperman/deepdiff/pull/284 Bug-Fix: TypeError
in \_get_numbers_distance() when ignore_order = True by
@Dhanvantari
- https://github.com/seperman/deepdiff/pull/280 Add support for
UUIDs by @havardthom
- Major bug in delta when it comes to iterable items added or
removed is investigated by @uwefladrich and resolved by @seperman
- v5-6-0: Adding custom operators, and ignore_order_func. Bugfix: verbose_level==0 should disable values_changes. Bugfix: unprocessed key error.
- v5-5-0: adding iterable_compare_func for DeepDiff, adding output_format of list for path() in tree view.
- v5-4-0: adding strict_checking for numbers in DeepSearch.
- v5-3-0: add support for regular expressions in DeepSearch.
- v5-2-3: Retaining the order of multiple dictionary items added via Delta. Fixed the typo with yml files in deep cli. Fixing Grep RecursionError where using non UTF-8 character. Allowing kwargs to be passed to to_json method.
- v5-2-2: Fixed Delta serialization when None type is present.
- v5-2-0: Removed Murmur3 as the preferred hashing method. Using SHA256 by default now. Added commandline for deepdiff. Added group_by. Added math_epsilon. Improved ignoring of NoneType.
- v5-0-2: Bug Fix NoneType in ignore type groups https://github.com/seperman/deepdiff/issues/207
- v5-0-1: Bug fix to not apply format to non numbers.
- v5-0-0: Introducing the Delta object, Improving Numpy support, Fixing tuples comparison when ignore_order=True, Dramatically improving the results when ignore_order=True by running in passes, Introducing pretty print view, deep_distance, purge, progress logging, cache and truncate_datetime.
- v4-3-3: Adds support for datetime.time
- v4-3-2: Deprecation Warning Enhancement
- v4-3-1: Fixing the issue with exclude_path and hash calculations when dictionaries were inside iterables. https://github.com/seperman/deepdiff/issues/174
- v4-3-0: adding exclude_obj_callback
- v4-2-0: .json property is finally removed. Fix for Py3.10. Dropping support for EOL Python 3.4. Ignoring private keys when calculating hashes. For example __init__ is not a part of hash calculation anymore. Fix for #166 Problem with comparing lists, with an boolean as element.
- v4-1-0: .json property is finally removed.
- v4-0-9: Fixing the bug for hashing custom unhashable objects
- v4-0-8: Adding ignore_nan_inequality for float('nan')
- v4-0-7: Hashing of the number 1 vs. True
- v4-0-6: found a tiny bug in Python formatting of numbers in scientific notation. Added a workaround.
- v4-0-5: Fixing number diffing. Adding number_format_notation and number_to_string_func.
- v4-0-4: Adding ignore_string_case and ignore_type_subclasses
- v4-0-3: Adding versionbump tool for release
- v4-0-2: Fixing installation issue where rst files are missing.
- v4-0-1: Fixing installation Tarball missing requirements.txt . DeepDiff v4+ should not show up as pip installable for Py2. Making Murmur3 installation optional.
- v4-0-0: Ending Python 2 support, Adding more functionalities and documentation for DeepHash. Switching to Pytest for testing. Switching to Murmur3 128bit for hashing. Fixing classes which inherit from classes with slots didn't have all of their slots compared. Renaming ContentHash to DeepHash. Adding exclude by path and regex path to DeepHash. Adding ignore_type_in_groups. Adding match_string to DeepSearch. Adding Timedelta object diffing.
- v3-5-0: Exclude regex path
- v3-3-0: Searching for objects and class attributes
- v3-2-2: Adding help(deepdiff)
- v3-2-1: Fixing hash of None
- v3-2-0: Adding grep for search: object | grep(item)
- v3-1-3: Unicode vs. Bytes default fix
- v3-1-2: NotPresent Fix when item is added or removed.
- v3-1-1: Bug fix when item value is None (#58)
- v3-1-0: Serialization to/from json
- v3-0-0: Introducing Tree View
- v2-5-3: Bug fix on logging for content hash.
- v2-5-2: Bug fixes on content hash.
- v2-5-0: Adding ContentHash module to fix ignore_order once and for all.
- v2-1-0: Adding Deep Search. Now you can search for item in an object.
- v2-0-0: Exclusion patterns better coverage. Updating docs.
- v1-8-0: Exclusion patterns.
- v1-7-0: Deep Set comparison.
- v1-6-0: Unifying key names. i.e newvalue is new_value now. For backward compatibility, newvalue still works.
- v1-5-0: Fixing ignore order containers with unordered items. Adding significant digits when comparing decimals. Changes property is deprecated.
- v1-1-0: Changing Set, Dictionary and Object Attribute Add/Removal to be reported as Set instead of List. Adding Pypy compatibility.
- v1-0-2: Checking for ImmutableMapping type instead of dict
- v1-0-1: Better ignore order support
- v1-0-0: Restructuring output to make it more useful. This is NOT backward compatible.
- v0-6-1: Fixing iterables with unhashable when order is ignored
- v0-6-0: Adding unicode support
- v0-5-9: Adding decimal support
- v0-5-8: Adding ignore order for unhashables support
- v0-5-7: Adding ignore order support
- v0-5-6: Adding slots support
- v0-5-5: Adding loop detection
Back to :doc:`/index`
qlustered-deepdiff-41c7265/deepdiff/docstrings/colored_view.rst 0000664 0000000 0000000 00000005766 15162412645 0024727 0 ustar 00root root 0000000 0000000 .. _colored_view_label:
Colored View
============
The `ColoredView` feature in `deepdiff` provides a human-readable, color-coded JSON output of the
differences between two objects. This feature is particularly useful for visualizing changes in a
clear and intuitive manner.
- **Color-Coded Differences:**
- **Added Elements:** Shown in green.
- **Removed Elements:** Shown in red.
- **Changed Elements:** The old value is shown in red, and the new value is shown in green.
Usage
-----
To use the `ColoredView`, simply pass the `COLORED_VIEW` option to the `DeepDiff` function:
.. code-block:: python
from deepdiff import DeepDiff
from deepdiff.helper import COLORED_VIEW
t1 = {"name": "John", "age": 30, "scores": [1, 2, 3], "address": {"city": "New York", "zip": "10001"}}
t2 = {"name": "John", "age": 31, "scores": [1, 2, 4], "address": {"city": "Boston", "zip": "10001"}, "new": "value"}
diff = DeepDiff(t1, t2, view=COLORED_VIEW)
print(diff)
Or from command line:
.. code-block:: bash
deep diff --view colored t1.json t2.json
The output will look something like this:
.. raw:: html
Colored Compact View
--------------------
For a more concise output, especially with deeply nested objects where many parts are unchanged,
the `ColoredView` with the compact option can be used. This view is similar but collapses
unchanged nested dictionaries to `{...}` and unchanged lists/tuples to `[...]`. To use the compact
option do:
.. code-block:: python
from deepdiff import DeepDiff
from deepdiff.helper import COLORED_COMPACT_VIEW
t1 = {"name": "John", "age": 30, "scores": [1, 2, 3], "address": {"city": "New York", "zip": "10001"}}
t2 = {"name": "John", "age": 31, "scores": [1, 2, 4], "address": {"city": "New York", "zip": "10001"}, "new": "value"}
diff = DeepDiff(t1, t2, view=COLORED_COMPACT_VIEW)
print(diff)
Or from command line:
.. code-block:: bash
deep diff --view colored_compact t1.json t2.json
The output will look something like this:
.. raw:: html