Skip to content

Commit 7f93132

Browse files
authored
Merge pull request #545 from seperman/dev
8.5.0
2 parents 724938d + ed5469c commit 7f93132

File tree

11 files changed

+381
-79
lines changed

11 files changed

+381
-79
lines changed

.github/workflows/main.yaml

+51-52
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,60 @@
1-
name: Unit Tests
1+
name: CI
22

33
on:
4-
push:
5-
branches: [ "master", "dev" ]
6-
pull_request:
7-
branches: [ "master", "dev" ]
4+
push: { branches: [master, dev] }
5+
pull_request: { branches: [master, dev] }
86

97
jobs:
108
build:
11-
env:
12-
DEFAULT_PYTHON: 3.12
139
runs-on: ubuntu-latest
10+
env:
11+
DEFAULT_PYTHON: '3.12'
1412
strategy:
1513
matrix:
16-
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
17-
architecture: ["x64"]
14+
python-version: ['3.9','3.10','3.11','3.12','3.13']
15+
architecture: ['x64']
16+
1817
steps:
19-
- uses: actions/checkout@v2
20-
- name: Setup Python ${{ matrix.python-version }} on ${{ matrix.architecture }}
21-
uses: actions/setup-python@v2
22-
with:
23-
python-version: ${{ matrix.python-version }}
24-
architecture: ${{ matrix.architecture }}
25-
- name: Cache pip
26-
env:
27-
PYO3_USE_ABI3_FORWARD_COMPATIBILITY: "1"
28-
uses: actions/cache@v4
29-
with:
30-
restore-keys: |
31-
${{ runner.os }}-
32-
- name: Upgrade setuptools
33-
if: matrix.python-version >= 3.12
34-
run: |
35-
# workaround for 3.12, SEE: https://github.com/pypa/setuptools/issues/3661#issuecomment-1813845177
36-
pip install --upgrade setuptools
37-
- name: Lint with flake8
38-
if: matrix.python-version == ${{ env.DEFAULT_PYTHON }}
39-
run: |
40-
# stop the build if there are Python syntax errors or undefined names
41-
nox -e flake8 -- deepdiff --count --select=E9,F63,F7,F82 --show-source --statistics
42-
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
43-
nox -e flake8 -- deepdiff --count --exit-zero --max-complexity=26 --max-line-length=250 --statistics
44-
- name: Test with pytest and get the coverage
45-
if: matrix.python-version == ${{ env.DEFAULT_PYTHON }}
46-
run: |
47-
nox -e pytest -s -- --benchmark-disable --cov-report=xml --cov=deepdiff tests/ --runslow
48-
- name: Test with pytest and no coverage report
49-
if: matrix.python-version != ${{ env.DEFAULT_PYTHON }}
50-
run: |
51-
nox -e pytest -s -- --benchmark-disable tests/
52-
- name: Upload coverage to Codecov
53-
uses: codecov/codecov-action@v4
54-
if: matrix.python-version == ${{ env.DEFAULT_PYTHON }}
55-
env:
56-
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
57-
with:
58-
file: ./coverage.xml
59-
token: ${{ secrets.CODECOV_TOKEN }}
60-
env_vars: OS,PYTHON
61-
fail_ci_if_error: true
18+
- uses: actions/checkout@v3
19+
20+
- name: Setup Python
21+
uses: actions/setup-python@v4
22+
with:
23+
python-version: ${{ matrix.python-version }}
24+
architecture: ${{ matrix.architecture }}
25+
cache: pip
26+
cache-dependency-path: pyproject.toml
27+
28+
- name: Install nox
29+
run: pip install nox==2025.5.1
30+
- name: Upgrade setuptools & wheel (for all venvs)
31+
run: pip install --upgrade setuptools wheel
32+
33+
- name: Lint with flake8
34+
if: ${{ matrix.python-version == '3.12' }}
35+
run: |
36+
nox -s flake8 -- deepdiff --count --select=E9,F63,F7,F82 --show-source --statistics
37+
nox -s flake8 -- deepdiff --count --exit-zero --max-complexity=26 --max-line-length=250 --statistics
38+
39+
- name: Test with pytest (no coverage)
40+
if: ${{ matrix.python-version != '3.12' }}
41+
run: |
42+
nox -s pytest-${{ matrix.python-version }} -- --benchmark-disable tests/
43+
44+
- name: Test with pytest (+ coverage)
45+
if: ${{ matrix.python-version == '3.12' }}
46+
run: |
47+
nox -s pytest-${{ matrix.python-version }} -- \
48+
--benchmark-disable \
49+
--cov-report=xml \
50+
--cov=deepdiff \
51+
tests/ --runslow
52+
53+
- name: Upload coverage
54+
if: ${{ matrix.python-version == '3.12' }}
55+
uses: codecov/codecov-action@v4
56+
with:
57+
token: ${{ secrets.CODECOV_TOKEN }}
58+
file: coverage.xml
59+
env_vars: OS,PYTHON
60+
fail_ci_if_error: true

deepdiff/diff.py

+4-5
Original file line numberDiff line numberDiff line change
@@ -952,11 +952,10 @@ def _diff_by_forming_pairs_and_comparing_one_by_one(
952952
self._report_result('iterable_item_moved', change_level, local_tree=local_tree)
953953

954954
if self.iterable_compare_func:
955-
# Intentionally setting j as the first child relationship param in cases of a moved item.
956-
# If the item was moved using an iterable_compare_func then we want to make sure that the index
957-
# is relative to t2.
958-
reference_param1 = j
959-
reference_param2 = i
955+
# Mark additional context denoting that we have moved an item.
956+
# This will allow for correctly setting paths relative to t2 when using an iterable_compare_func
957+
level.additional["moved"] = True
958+
960959
else:
961960
continue
962961

deepdiff/helper.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ def __repr__(self):
5858
np_complex128 = np_type # pragma: no cover.
5959
np_cdouble = np_type # pragma: no cover.
6060
np_complexfloating = np_type # pragma: no cover.
61+
np_datetime64 = np_type # pragma: no cover.
6162
else:
6263
np_array_factory = np.array
6364
np_ndarray = np.ndarray
@@ -80,6 +81,7 @@ def __repr__(self):
8081
np_complex128 = np.complex128
8182
np_cdouble = np.cdouble # np.complex_ is an alias for np.cdouble and is being removed by NumPy 2.0
8283
np_complexfloating = np.complexfloating
84+
np_datetime64 = np.datetime64
8385

8486
numpy_numbers = (
8587
np_int8, np_int16, np_int32, np_int64, np_uint8,
@@ -93,6 +95,7 @@ def __repr__(self):
9395

9496
numpy_dtypes = set(numpy_numbers)
9597
numpy_dtypes.add(np_bool_) # type: ignore
98+
numpy_dtypes.add(np_datetime64) # type: ignore
9699

97100
numpy_dtype_str_to_type = {
98101
item.__name__: item for item in numpy_dtypes
@@ -184,10 +187,10 @@ def get_semvar_as_integer(version):
184187
bytes_type = bytes
185188
only_complex_number = (complex,) + numpy_complex_numbers
186189
only_numbers = (int, float, complex, Decimal) + numpy_numbers
187-
datetimes = (datetime.datetime, datetime.date, datetime.timedelta, datetime.time)
190+
datetimes = (datetime.datetime, datetime.date, datetime.timedelta, datetime.time, np_datetime64)
188191
ipranges = (ipaddress.IPv4Interface, ipaddress.IPv6Interface, ipaddress.IPv4Network, ipaddress.IPv6Network)
189192
uuids = (uuid.UUID, )
190-
times = (datetime.datetime, datetime.time)
193+
times = (datetime.datetime, datetime.time,np_datetime64)
191194
numbers: Tuple = only_numbers + datetimes
192195
booleans = (bool, np_bool_)
193196

@@ -733,13 +736,17 @@ def detailed__dict__(obj, ignore_private_variables=True, ignore_keys=frozenset()
733736
ignore_private_variables and key.startswith('__') and not key.startswith(private_var_prefix)
734737
):
735738
del result[key]
739+
if isinstance(obj, PydanticBaseModel):
740+
getter = lambda x, y: getattr(type(x), y)
741+
else:
742+
getter = getattr
736743
for key in dir(obj):
737744
if key not in result and key not in ignore_keys and (
738745
not ignore_private_variables or (
739746
ignore_private_variables and not key.startswith('__') and not key.startswith(private_var_prefix)
740747
)
741748
):
742-
value = getattr(obj, key)
749+
value = getter(obj, key)
743750
if not callable(value):
744751
result[key] = value
745752
return result

deepdiff/model.py

+20-7
Original file line numberDiff line numberDiff line change
@@ -221,10 +221,11 @@ def _from_tree_value_changed(self, tree):
221221

222222
def _from_tree_iterable_item_moved(self, tree):
223223
if 'iterable_item_moved' in tree and self.verbose_level > 1:
224+
224225
for change in tree['iterable_item_moved']:
225-
the_changed = {'new_path': change.path(use_t2=True), 'value': change.t2}
226+
the_changed = {'new_path': change.path(use_t2=True, reporting_move=True), 'value': change.t2}
226227
self['iterable_item_moved'][change.path(
227-
force=FORCE_DEFAULT)] = the_changed
228+
force=FORCE_DEFAULT, use_t2=False, reporting_move=True)] = the_changed
228229

229230
def _from_tree_unprocessed(self, tree):
230231
if 'unprocessed' in tree:
@@ -428,11 +429,11 @@ def _from_tree_iterable_item_moved(self, tree):
428429
if 'iterable_item_moved' in tree:
429430
for change in tree['iterable_item_moved']:
430431
if (
431-
change.up.path(force=FORCE_DEFAULT) not in self["_iterable_opcodes"]
432+
change.up.path(force=FORCE_DEFAULT, reporting_move=True) not in self["_iterable_opcodes"]
432433
):
433-
the_changed = {'new_path': change.path(use_t2=True), 'value': change.t2}
434+
the_changed = {'new_path': change.path(use_t2=True, reporting_move=True), 'value': change.t2}
434435
self['iterable_item_moved'][change.path(
435-
force=FORCE_DEFAULT)] = the_changed
436+
force=FORCE_DEFAULT, reporting_move=True)] = the_changed
436437

437438

438439
class DiffLevel:
@@ -673,7 +674,7 @@ def get_root_key(self, use_t2=False):
673674
return next_rel.param
674675
return notpresent
675676

676-
def path(self, root="root", force=None, get_parent_too=False, use_t2=False, output_format='str'):
677+
def path(self, root="root", force=None, get_parent_too=False, use_t2=False, output_format='str', reporting_move=False):
677678
"""
678679
A python syntax string describing how to descend to this level, assuming the top level object is called root.
679680
Returns None if the path is not representable as a string.
@@ -699,6 +700,9 @@ def path(self, root="root", force=None, get_parent_too=False, use_t2=False, outp
699700
:param output_format: The format of the output. The options are 'str' which is the default and produces a
700701
string representation of the path or 'list' to produce a list of keys and attributes
701702
that produce the path.
703+
704+
:param reporting_move: This should be set to true if and only if we are reporting on iterable_item_moved.
705+
All other cases should leave this set to False.
702706
"""
703707
# TODO: We could optimize this by building on top of self.up's path if it is cached there
704708
cache_key = "{}{}{}{}".format(force, get_parent_too, use_t2, output_format)
@@ -720,7 +724,16 @@ def path(self, root="root", force=None, get_parent_too=False, use_t2=False, outp
720724
# traverse all levels of this relationship
721725
while level and level is not self:
722726
# get this level's relationship object
723-
if use_t2:
727+
if level.additional.get("moved") and not reporting_move:
728+
# To ensure we can properly replay items such as values_changed in items that may have moved, we
729+
# need to make sure that all paths are reported relative to t2 if a level has reported a move.
730+
# If we are reporting a move, the path is already correct and does not need to be swapped.
731+
# Additional context of "moved" is only ever set if using iterable_compare_func and a move has taken place.
732+
level_use_t2 = not use_t2
733+
else:
734+
level_use_t2 = use_t2
735+
736+
if level_use_t2:
724737
next_rel = level.t2_child_rel or level.t1_child_rel
725738
else:
726739
next_rel = level.t1_child_rel or level.t2_child_rel # next relationship object to get a formatted param from

deepdiff/operator.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import re
2-
from typing import Any, Optional, List
2+
from typing import Any, Optional, List, TYPE_CHECKING
33
from abc import ABCMeta, abstractmethod
44
from deepdiff.helper import convert_item_or_items_into_compiled_regexes_else_none
55

6+
if TYPE_CHECKING:
7+
from deepdiff import DeepDiff
68

79

810
class BaseOperatorPlus(metaclass=ABCMeta):
@@ -16,7 +18,7 @@ def match(self, level) -> bool:
1618
pass
1719

1820
@abstractmethod
19-
def give_up_diffing(self, level, diff_instance: float) -> bool:
21+
def give_up_diffing(self, level, diff_instance: "DeepDiff") -> bool:
2022
"""
2123
Given a level which includes t1 and t2 in the tree view, and the "distance" between l1 and l2.
2224
do we consider t1 and t2 to be equal or not. The distance is a number between zero to one and is calculated by DeepDiff to measure how similar objects are.

deepdiff/serialization.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -615,7 +615,7 @@ def _serialize_tuple(value):
615615
}
616616

617617
if PydanticBaseModel is not pydantic_base_model_type:
618-
JSON_CONVERTOR[PydanticBaseModel] = lambda x: x.dict()
618+
JSON_CONVERTOR[PydanticBaseModel] = lambda x: x.model_dump()
619619

620620

621621
def json_convertor_default(default_mapping=None):

docs/custom.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ Base Operator Plus
202202
pass
203203
204204
@abstractmethod
205-
def give_up_diffing(self, level, diff_instance: float) -> bool:
205+
def give_up_diffing(self, level, diff_instance: "DeepDiff") -> bool:
206206
"""
207207
Given a level which includes t1 and t2 in the tree view, and the "distance" between l1 and l2.
208208
do we consider t1 and t2 to be equal or not. The distance is a number between zero to one and is calculated by DeepDiff to measure how similar objects are.

pyproject.toml

+5-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ build-backend = "flit_core.buildapi"
66
name = "deepdiff"
77
version = "8.4.2"
88
dependencies = [
9-
"orderly-set>=5.3.0,<6",
9+
"orderly-set>=5.4.1,<6",
1010
]
1111
requires-python = ">=3.9"
1212
authors = [
@@ -54,6 +54,7 @@ dev = [
5454
"tomli-w~=1.2.0",
5555
"pandas~=2.2.0",
5656
"polars~=1.21.0",
57+
"nox==2025.5.1",
5758
]
5859
docs = [
5960
# We use the html style that is not supported in Sphinx 7 anymore.
@@ -73,6 +74,9 @@ test = [
7374
"pytest-cov~=6.0.0",
7475
"python-dotenv~=1.0.0",
7576
]
77+
optimize = [
78+
"orjson",
79+
]
7680

7781
[project.scripts]
7882
deep = "deepdiff.commands:cli"

0 commit comments

Comments
 (0)