Skip to content

Commit d52b2da

Browse files
authored
Merge pull request #380 from seperman/dev
6.3.1
2 parents 75e1edd + 8951d92 commit d52b2da

17 files changed

+427
-42
lines changed

.github/workflows/main.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,23 +38,23 @@ jobs:
3838
if: matrix.python-version != 3.7
3939
run: pip install -r requirements-dev.txt
4040
- name: Lint with flake8
41-
if: matrix.python-version == 3.10
41+
if: matrix.python-version == 3.11
4242
run: |
4343
# stop the build if there are Python syntax errors or undefined names
4444
flake8 deepdiff --count --select=E9,F63,F7,F82 --show-source --statistics
4545
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
4646
flake8 deepdiff --count --exit-zero --max-complexity=26 --max-line-lengt=250 --statistics
4747
- name: Test with pytest and get the coverage
48-
if: matrix.python-version == 3.10
48+
if: matrix.python-version == 3.11
4949
run: |
5050
pytest --cov-report=xml --cov=deepdiff tests/ --runslow
5151
- name: Test with pytest and no coverage report
52-
if: matrix.python-version != 3.10
52+
if: matrix.python-version != 3.11
5353
run: |
5454
pytest
5555
- name: Upload coverage to Codecov
56-
uses: codecov/codecov-action@v1
57-
if: matrix.python-version == 3.10
56+
uses: codecov/codecov-action@v3
57+
if: matrix.python-version == 3.11
5858
with:
5959
file: ./coverage.xml
6060
env_vars: OS,PYTHON

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ If you want to improve the performance of DeepDiff with certain functionalities
5050

5151
`pip install "deepdiff[optimize]"`
5252

53+
Install optional packages:
54+
- [yaml](https://pypi.org/project/PyYAML/)
55+
- [tomli](https://pypi.org/project/tomli/) (python 3.10 and older) and [tomli-w](https://pypi.org/project/tomli-w/) for writing
56+
- [clevercsv](https://pypi.org/project/clevercsv/) for more rubust CSV parsing
57+
- [orjson](https://pypi.org/project/orjson/) for speed and memory optimized parsing
58+
- [pydantic](https://pypi.org/project/pydantic/)
59+
60+
5361
# Documentation
5462

5563
<https://zepworks.com/deepdiff/current/>

deepdiff/deephash.py

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
#!/usr/bin/env python
2+
import inspect
23
import logging
34
from collections.abc import Iterable, MutableMapping
45
from collections import defaultdict
56
from hashlib import sha1, sha256
7+
from pathlib import Path
68
from enum import Enum
79
from deepdiff.helper import (strings, numbers, times, unprocessed, not_hashed, add_to_frozen_set,
810
convert_item_or_items_into_set_else_none, get_doc,
@@ -308,17 +310,28 @@ def items(self):
308310
def _prep_obj(self, obj, parent, parents_ids=EMPTY_FROZENSET, is_namedtuple=False):
309311
"""prepping objects"""
310312
original_type = type(obj) if not isinstance(obj, type) else obj
311-
try:
312-
if is_namedtuple:
313-
obj = obj._asdict()
314-
else:
315-
obj = obj.__dict__
316-
except AttributeError:
313+
314+
obj_to_dict_strategies = []
315+
if is_namedtuple:
316+
obj_to_dict_strategies.append(lambda o: o._asdict())
317+
else:
318+
obj_to_dict_strategies.append(lambda o: o.__dict__)
319+
320+
if hasattr(obj, "__slots__"):
321+
obj_to_dict_strategies.append(lambda o: {i: getattr(o, i) for i in o.__slots__})
322+
else:
323+
obj_to_dict_strategies.append(lambda o: dict(inspect.getmembers(o, lambda m: not inspect.isroutine(m))))
324+
325+
for get_dict in obj_to_dict_strategies:
317326
try:
318-
obj = {i: getattr(obj, i) for i in obj.__slots__}
327+
d = get_dict(obj)
328+
break
319329
except AttributeError:
320-
self.hashes[UNPROCESSED_KEY].append(obj)
321-
return (unprocessed, 0)
330+
pass
331+
else:
332+
self.hashes[UNPROCESSED_KEY].append(obj)
333+
return (unprocessed, 0)
334+
obj = d
322335

323336
result, counts = self._prep_dict(obj, parent=parent, parents_ids=parents_ids,
324337
print_as_attribute=True, original_type=original_type)
@@ -420,6 +433,12 @@ def _prep_iterable(self, obj, parent, parents_ids=EMPTY_FROZENSET):
420433
def _prep_bool(self, obj):
421434
return BoolObj.TRUE if obj else BoolObj.FALSE
422435

436+
437+
def _prep_path(self, obj):
438+
type_ = obj.__class__.__name__
439+
return KEY_TO_VAL_STR.format(type_, obj)
440+
441+
423442
def _prep_number(self, obj):
424443
type_ = "number" if self.ignore_numeric_type_changes else obj.__class__.__name__
425444
if self.significant_digits is not None:
@@ -476,6 +495,9 @@ def _hash(self, obj, parent, parents_ids=EMPTY_FROZENSET):
476495
ignore_encoding_errors=self.ignore_encoding_errors,
477496
)
478497

498+
elif isinstance(obj, Path):
499+
result = self._prep_path(obj)
500+
479501
elif isinstance(obj, times):
480502
result = self._prep_datetime(obj)
481503

deepdiff/delta.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
np_ndarray, np_array_factory, numpy_dtypes, get_doc,
1010
not_found, numpy_dtype_string_to_type, dict_,
1111
)
12-
from deepdiff.path import _path_to_elements, _get_nested_obj, GET, GETATTR
12+
from deepdiff.path import _path_to_elements, _get_nested_obj, _get_nested_obj_and_force, GET, GETATTR
1313
from deepdiff.anyset import AnySet
1414

1515

@@ -70,6 +70,7 @@ def __init__(
7070
safe_to_import=None,
7171
serializer=pickle_dump,
7272
verify_symmetry=False,
73+
force=False,
7374
):
7475
if hasattr(deserializer, '__code__') and 'safe_to_import' in set(deserializer.__code__.co_varnames):
7576
_deserializer = deserializer
@@ -104,6 +105,11 @@ def _deserializer(obj, safe_to_import=None):
104105
self._numpy_paths = self.diff.pop('_numpy_paths', False)
105106
self.serializer = serializer
106107
self.deserializer = deserializer
108+
self.force = force
109+
if force:
110+
self.get_nested_obj = _get_nested_obj_and_force
111+
else:
112+
self.get_nested_obj = _get_nested_obj
107113
self.reset()
108114

109115
def __repr__(self):
@@ -162,7 +168,14 @@ def _get_elem_and_compare_to_old_value(self, obj, path_for_err_reporting, expect
162168
current_old_value = getattr(obj, elem)
163169
else:
164170
raise DeltaError(INVALID_ACTION_WHEN_CALLING_GET_ELEM.format(action))
165-
except (KeyError, IndexError, AttributeError, IndexError, TypeError) as e:
171+
except (KeyError, IndexError, AttributeError, TypeError) as e:
172+
if self.force:
173+
forced_old_value = {}
174+
if action == GET:
175+
obj[elem] = forced_old_value
176+
elif action == GETATTR:
177+
setattr(obj, elem, forced_old_value)
178+
return forced_old_value
166179
current_old_value = not_found
167180
if isinstance(path_for_err_reporting, (list, tuple)):
168181
path_for_err_reporting = '.'.join([i[0] for i in path_for_err_reporting])
@@ -351,14 +364,14 @@ def _get_elements_and_details(self, path):
351364
try:
352365
elements = _path_to_elements(path)
353366
if len(elements) > 1:
354-
parent = _get_nested_obj(obj=self, elements=elements[:-2])
367+
parent = self.get_nested_obj(obj=self, elements=elements[:-2])
355368
parent_to_obj_elem, parent_to_obj_action = elements[-2]
356369
obj = self._get_elem_and_compare_to_old_value(
357370
obj=parent, path_for_err_reporting=path, expected_old_value=None,
358371
elem=parent_to_obj_elem, action=parent_to_obj_action)
359372
else:
360373
parent = parent_to_obj_elem = parent_to_obj_action = None
361-
obj = _get_nested_obj(obj=self, elements=elements[:-1])
374+
obj = self.get_nested_obj(obj=self, elements=elements[:-1])
362375
elem, action = elements[-1]
363376
except Exception as e:
364377
self._raise_or_log(UNABLE_TO_GET_ITEM_MSG.format(path, e))
@@ -458,7 +471,7 @@ def _do_set_item_removed(self):
458471
def _do_set_or_frozenset_item(self, items, func):
459472
for path, value in items.items():
460473
elements = _path_to_elements(path)
461-
parent = _get_nested_obj(obj=self, elements=elements[:-1])
474+
parent = self.get_nested_obj(obj=self, elements=elements[:-1])
462475
elem, action = elements[-1]
463476
obj = self._get_elem_and_compare_to_old_value(
464477
parent, path_for_err_reporting=path, expected_old_value=None, elem=elem, action=action)

deepdiff/diff.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,16 @@
1515
from collections import defaultdict
1616
from itertools import zip_longest
1717
from ordered_set import OrderedSet
18-
from deepdiff.helper import (strings, bytes_type, numbers, uuids, times, ListItemRemovedOrAdded, notpresent,
18+
from deepdiff.helper import (strings, bytes_type, numbers, uuids, datetimes, ListItemRemovedOrAdded, notpresent,
1919
IndexedHash, unprocessed, add_to_frozen_set, basic_types,
2020
convert_item_or_items_into_set_else_none, get_type,
2121
convert_item_or_items_into_compiled_regexes_else_none,
2222
type_is_subclass_of_type_group, type_in_type_group, get_doc,
2323
number_to_string, datetime_normalize, KEY_TO_VAL_STR, booleans,
2424
np_ndarray, np_floating, get_numpy_ndarray_rows, OrderedSetPlus, RepeatedTimer,
2525
TEXT_VIEW, TREE_VIEW, DELTA_VIEW, detailed__dict__, add_root_to_paths,
26-
np, get_truncate_datetime, dict_, CannotCompare, ENUM_INCLUDE_KEYS)
26+
np, get_truncate_datetime, dict_, CannotCompare, ENUM_INCLUDE_KEYS,
27+
PydanticBaseModel, )
2728
from deepdiff.serialization import SerializationMixin
2829
from deepdiff.distance import DistanceMixin
2930
from deepdiff.model import (
@@ -452,7 +453,7 @@ def _skip_this(self, level):
452453
if level_path not in self.include_paths:
453454
skip = True
454455
for prefix in self.include_paths:
455-
if level_path.startswith(prefix):
456+
if prefix in level_path or level_path in prefix:
456457
skip = False
457458
break
458459
elif self.exclude_regex_paths and any(
@@ -1529,7 +1530,7 @@ def _diff(self, level, parents_ids=frozenset(), _original_type=None, local_tree=
15291530
if isinstance(level.t1, strings):
15301531
self._diff_str(level, local_tree=local_tree)
15311532

1532-
elif isinstance(level.t1, times):
1533+
elif isinstance(level.t1, datetimes):
15331534
self._diff_datetimes(level, local_tree=local_tree)
15341535

15351536
elif isinstance(level.t1, uuids):
@@ -1550,6 +1551,9 @@ def _diff(self, level, parents_ids=frozenset(), _original_type=None, local_tree=
15501551
elif isinstance(level.t1, np_ndarray):
15511552
self._diff_numpy_array(level, parents_ids, local_tree=local_tree)
15521553

1554+
elif isinstance(level.t1, PydanticBaseModel):
1555+
self._diff_obj(level, parents_ids, local_tree=local_tree)
1556+
15531557
elif isinstance(level.t1, Iterable):
15541558
self._diff_iterable(level, parents_ids, _original_type=_original_type, local_tree=local_tree)
15551559

deepdiff/helper.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ class np_type:
1919
pass
2020

2121

22+
class pydantic_base_model_type:
23+
pass
24+
25+
2226
try:
2327
import numpy as np
2428
except ImportError: # pragma: no cover. The case without Numpy is tested locally only.
@@ -84,6 +88,12 @@ class np_type:
8488
item.__name__: item for item in numpy_dtypes
8589
}
8690

91+
try:
92+
from pydantic.main import BaseModel as PydanticBaseModel
93+
except ImportError:
94+
PydanticBaseModel = pydantic_base_model_type
95+
96+
8797
logger = logging.getLogger(__name__)
8898

8999
py_major_version = sys.version_info.major

deepdiff/path.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,24 @@ def _get_nested_obj(obj, elements):
111111
return obj
112112

113113

114+
def _get_nested_obj_and_force(obj, elements):
115+
for (elem, action) in elements:
116+
if action == GET:
117+
try:
118+
obj = obj[elem]
119+
except KeyError:
120+
obj[elem] = {}
121+
obj = obj[elem]
122+
except IndexError:
123+
if isinstance(obj, list) and isinstance(elem, int) and elem >= len(obj):
124+
obj.extend([None] * (elem - len(obj)))
125+
obj.append({})
126+
obj = obj[-1]
127+
elif action == GETATTR:
128+
obj = getattr(obj, elem)
129+
return obj
130+
131+
114132
def extract(obj, path):
115133
"""
116134
Get the item from obj based on path.

deepdiff/serialization.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,16 @@
1616
except ImportError: # pragma: no cover.
1717
yaml = None # pragma: no cover.
1818
try:
19-
import toml
19+
if sys.version_info >= (3, 11):
20+
import tomllib as tomli
21+
else:
22+
import tomli
23+
except ImportError: # pragma: no cover.
24+
tomli = None # pragma: no cover.
25+
try:
26+
import tomli_w
2027
except ImportError: # pragma: no cover.
21-
toml = None # pragma: no cover.
28+
tomli_w = None # pragma: no cover.
2229
try:
2330
import clevercsv
2431
csv = None
@@ -430,10 +437,10 @@ def load_path_content(path, file_type=None):
430437
with open(path, 'r') as the_file:
431438
content = yaml.safe_load(the_file)
432439
elif file_type == 'toml':
433-
if toml is None: # pragma: no cover.
434-
raise ImportError('Toml needs to be installed.') # pragma: no cover.
435-
with open(path, 'r') as the_file:
436-
content = toml.load(the_file)
440+
if tomli is None: # pragma: no cover.
441+
raise ImportError('On python<=3.10 tomli needs to be installed.') # pragma: no cover.
442+
with open(path, 'rb') as the_file:
443+
content = tomli.load(the_file)
437444
elif file_type == 'pickle':
438445
with open(path, 'rb') as the_file:
439446
content = the_file.read()
@@ -495,10 +502,10 @@ def _save_content(content, path, file_type, keep_backup=True):
495502
with open(path, 'w') as the_file:
496503
content = yaml.safe_dump(content, stream=the_file)
497504
elif file_type == 'toml':
498-
if toml is None: # pragma: no cover.
499-
raise ImportError('Toml needs to be installed.') # pragma: no cover.
500-
with open(path, 'w') as the_file:
501-
content = toml.dump(content, the_file)
505+
if tomli_w is None: # pragma: no cover.
506+
raise ImportError('Tomli-w needs to be installed.') # pragma: no cover.
507+
with open(path, 'wb') as the_file:
508+
content = tomli_w.dump(content, the_file)
502509
elif file_type == 'pickle':
503510
with open(path, 'wb') as the_file:
504511
content = pickle_dump(content, file_obj=the_file)

docs/delta.rst

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,3 +416,50 @@ Expected the old value for root[0] to be 1 but it is 3. Error found on: while ch
416416
[2]
417417

418418
And if you had set raise_errors=True, then it would have raised the error in addition to logging it.
419+
420+
421+
.. _delta_force_label:
422+
423+
Delta Force
424+
-----------
425+
426+
force : Boolean, default=False
427+
force is used to force apply a delta to objects that have a very different structure.
428+
429+
430+
>>> from deepdiff import DeepDiff, Delta
431+
>>> t1 = {
432+
... 'x': {
433+
... 'y': [1, 2, 3]
434+
... },
435+
... 'q': {
436+
... 'r': 'abc',
437+
... }
438+
... }
439+
>>>
440+
>>> t2 = {
441+
... 'x': {
442+
... 'y': [1, 2, 3, 4]
443+
... },
444+
... 'q': {
445+
... 'r': 'abc',
446+
... 't': 0.5,
447+
... }
448+
... }
449+
>>>
450+
>>> diff = DeepDiff(t1, t2)
451+
>>> diff
452+
{'dictionary_item_added': [root['q']['t']], 'iterable_item_added': {"root['x']['y'][3]": 4}}
453+
>>> delta = Delta(diff)
454+
>>> {} + delta
455+
Unable to get the item at root['x']['y'][3]: 'x'
456+
Unable to get the item at root['q']['t']
457+
{}
458+
459+
# Once we set the force to be True
460+
461+
>>> delta = Delta(diff, force=True)
462+
>>> {} + delta
463+
{'x': {'y': {3: 4}}, 'q': {'t': 0.5}}
464+
465+
Notice that the force attribute does not know the original object at ['x']['y'] was supposed to be a list, so it assumes it was a dictionary.

requirements-dev-3.7.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,8 @@ pytest==7.1.2
88
python-dotenv==0.20.0
99
python-dateutil==2.8.2
1010
wheel==0.38.1
11+
tomli==2.0.0
12+
tomli-w==1.0.0
13+
pydantic==1.10.8
14+
python_dateutil==2.8.2
15+
tomli_w==1.0.0

0 commit comments

Comments
 (0)