Skip to content

Commit c6fa94f

Browse files
authored
Merge pull request #225 from seperman/dev
Fixed Delta serialization when None type is present.
2 parents b971eac + 81afa4b commit c6fa94f

13 files changed

+201
-40
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
DeepDiff Change log
22

3+
- v5-2-2: Fixed Delta serialization when None type is present.
34
- 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.
45
- v5-0-2: Bug Fix NoneType in ignore type groups https://github.com/seperman/deepdiff/issues/207
56
- v5-0-1: Bug fix to not apply format to non numbers.

README.md

+13-13
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# DeepDiff v 5.2.1
1+
# DeepDiff v 5.2.2
22

33
![Downloads](https://img.shields.io/pypi/dm/deepdiff.svg?style=flat)
44
![Python Versions](https://img.shields.io/pypi/pyversions/deepdiff.svg?style=flat)
@@ -18,7 +18,7 @@ Tested on Python 3.6+ and PyPy3.
1818

1919
**NOTE: The last version of DeepDiff to work on Python 3.5 was DeepDiff 5-0-2**
2020

21-
- [Documentation](https://zepworks.com/deepdiff/5.2.1/)
21+
- [Documentation](https://zepworks.com/deepdiff/5.2.2/)
2222

2323

2424
## Installation
@@ -54,13 +54,13 @@ Note: if you want to use DeepDiff via commandline, make sure to run `pip install
5454

5555
DeepDiff gets the difference of 2 objects.
5656

57-
> - Please take a look at the [DeepDiff docs](https://zepworks.com/deepdiff/5.2.1/diff.html)
58-
> - The full documentation of all modules can be found on <https://zepworks.com/deepdiff/5.2.1/>
57+
> - Please take a look at the [DeepDiff docs](https://zepworks.com/deepdiff/5.2.2/diff.html)
58+
> - The full documentation of all modules can be found on <https://zepworks.com/deepdiff/5.2.2/>
5959
> - Tutorials and posts about DeepDiff can be found on <https://zepworks.com/tags/deepdiff/>
6060
6161
## A few Examples
6262

63-
> Note: This is just a brief overview of what DeepDiff can do. Please visit <https://zepworks.com/deepdiff/5.2.1/> for full documentation.
63+
> Note: This is just a brief overview of what DeepDiff can do. Please visit <https://zepworks.com/deepdiff/5.2.2/> for full documentation.
6464
6565
### List difference ignoring order or duplicates
6666

@@ -264,8 +264,8 @@ Example:
264264
```
265265

266266

267-
> - Please take a look at the [DeepDiff docs](https://zepworks.com/deepdiff/5.2.1/diff.html)
268-
> - The full documentation can be found on <https://zepworks.com/deepdiff/5.2.1/>
267+
> - Please take a look at the [DeepDiff docs](https://zepworks.com/deepdiff/5.2.2/diff.html)
268+
> - The full documentation can be found on <https://zepworks.com/deepdiff/5.2.2/>
269269
270270

271271
# Deep Search
@@ -297,17 +297,17 @@ And you can pass all the same kwargs as DeepSearch to grep too:
297297
{'matched_paths': {"root['somewhere']": 'around'}, 'matched_values': {"root['long']": 'somewhere'}}
298298
```
299299

300-
> - Please take a look at the [DeepSearch docs](https://zepworks.com/deepdiff/5.2.1/dsearch.html)
301-
> - The full documentation can be found on <https://zepworks.com/deepdiff/5.2.1/>
300+
> - Please take a look at the [DeepSearch docs](https://zepworks.com/deepdiff/5.2.2/dsearch.html)
301+
> - The full documentation can be found on <https://zepworks.com/deepdiff/5.2.2/>
302302
303303
# Deep Hash
304304
(New in v4-0-0)
305305

306306
DeepHash is designed to give you hash of ANY python object based on its contents even if the object is not considered hashable!
307307
DeepHash is supposed to be deterministic in order to make sure 2 objects that contain the same data, produce the same hash.
308308

309-
> - Please take a look at the [DeepHash docs](https://zepworks.com/deepdiff/5.2.1/deephash.html)
310-
> - The full documentation can be found on <https://zepworks.com/deepdiff/5.2.1/>
309+
> - Please take a look at the [DeepHash docs](https://zepworks.com/deepdiff/5.2.2/deephash.html)
310+
> - The full documentation can be found on <https://zepworks.com/deepdiff/5.2.2/>
311311
312312
Let's say you have a dictionary object.
313313

@@ -355,8 +355,8 @@ Which you can write as:
355355
At first it might seem weird why DeepHash(obj)[obj] but remember that DeepHash(obj) is a dictionary of hashes of all other objects that obj contains too.
356356

357357

358-
> - Please take a look at the [DeepHash docs](https://zepworks.com/deepdiff/5.2.1/deephash.html)
359-
> - The full documentation can be found on <https://zepworks.com/deepdiff/5.2.1/>
358+
> - Please take a look at the [DeepHash docs](https://zepworks.com/deepdiff/5.2.2/deephash.html)
359+
> - The full documentation can be found on <https://zepworks.com/deepdiff/5.2.2/>
360360
361361

362362
# Using DeepDiff in unit tests

deepdiff/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""This module offers the DeepDiff, DeepSearch, grep, Delta and DeepHash classes."""
22
# flake8: noqa
3-
__version__ = '5.2.1'
3+
__version__ = '5.2.2'
44
import logging
55

66
if __name__ == '__main__':

deepdiff/commands.py

+40-11
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ def cli():
4848
@click.option('--significant-digits', required=False, default=None, type=int, show_default=True)
4949
@click.option('--truncate-datetime', required=False, type=click.Choice(['second', 'minute', 'hour', 'day'], case_sensitive=True), show_default=True, default=None)
5050
@click.option('--verbose-level', required=False, default=1, type=click.IntRange(0, 2), show_default=True)
51+
@click.option('--debug', is_flag=True, show_default=False)
5152
def diff(
5253
*args, **kwargs
5354
):
@@ -59,6 +60,7 @@ def diff(
5960
6061
T1 and T2 are the path to the files to be compared with each other.
6162
"""
63+
debug = kwargs.pop('debug')
6264
kwargs['ignore_private_variables'] = not kwargs.pop('include_private_variables')
6365
kwargs['progress_logger'] = logger.info if kwargs['progress_logger'] == 'info' else logger.error
6466
create_patch = kwargs.pop('create_patch')
@@ -71,7 +73,10 @@ def diff(
7173
try:
7274
kwargs[name] = load_path_content(t_path, file_type=t_extension)
7375
except Exception as e: # pragma: no cover.
74-
sys.exit(str(f"Error when loading {name}: {e}")) # pragma: no cover.
76+
if debug: # pragma: no cover.
77+
raise # pragma: no cover.
78+
else: # pragma: no cover.
79+
sys.exit(str(f"Error when loading {name}: {e}")) # pragma: no cover.
7580

7681
# if (t1_extension != t2_extension):
7782
if t1_extension in {'csv', 'tsv'}:
@@ -92,7 +97,10 @@ def diff(
9297
try:
9398
delta = Delta(diff)
9499
except Exception as e: # pragma: no cover.
95-
sys.exit(f"Error when loading the patch (aka delta): {e}") # pragma: no cover.
100+
if debug: # pragma: no cover.
101+
raise # pragma: no cover.
102+
else: # pragma: no cover.
103+
sys.exit(f"Error when loading the patch (aka delta): {e}") # pragma: no cover.
96104

97105
# printing into stdout
98106
sys.stdout.buffer.write(delta.dumps())
@@ -105,8 +113,9 @@ def diff(
105113
@click.argument('delta_path', type=click.Path(exists=True, resolve_path=True))
106114
@click.option('--backup', '-b', is_flag=True, show_default=True)
107115
@click.option('--raise-errors', is_flag=True, show_default=True)
116+
@click.option('--debug', is_flag=True, show_default=False)
108117
def patch(
109-
path, delta_path, backup, raise_errors
118+
path, delta_path, backup, raise_errors, debug
110119
):
111120
"""
112121
Deep Patch Commandline
@@ -123,7 +132,10 @@ def patch(
123132
try:
124133
delta = Delta(delta_path=delta_path, raise_errors=raise_errors)
125134
except Exception as e: # pragma: no cover.
126-
sys.exit(str(f"Error when loading the patch (aka delta) {delta_path}: {e}")) # pragma: no cover.
135+
if debug: # pragma: no cover.
136+
raise # pragma: no cover.
137+
else: # pragma: no cover.
138+
sys.exit(str(f"Error when loading the patch (aka delta) {delta_path}: {e}")) # pragma: no cover.
127139

128140
extension = path.split('.')[-1]
129141

@@ -137,7 +149,10 @@ def patch(
137149
try:
138150
save_content_to_path(result, path, file_type=extension, keep_backup=backup)
139151
except Exception as e: # pragma: no cover.
140-
sys.exit(str(f"Error when saving {path}: {e}")) # pragma: no cover.
152+
if debug: # pragma: no cover.
153+
raise # pragma: no cover.
154+
else: # pragma: no cover.
155+
sys.exit(str(f"Error when saving {path}: {e}")) # pragma: no cover.
141156

142157

143158
@cli.command()
@@ -148,7 +163,8 @@ def patch(
148163
@click.option('--exclude-paths', required=False, type=str, show_default=False, multiple=True)
149164
@click.option('--exclude-regex-paths', required=False, type=str, show_default=False, multiple=True)
150165
@click.option('--verbose-level', required=False, default=1, type=click.IntRange(0, 2), show_default=True)
151-
def grep(item, path, **kwargs):
166+
@click.option('--debug', is_flag=True, show_default=False)
167+
def grep(item, path, debug, **kwargs):
152168
"""
153169
Deep Grep Commandline
154170
@@ -162,19 +178,26 @@ def grep(item, path, **kwargs):
162178
try:
163179
content = load_path_content(path)
164180
except Exception as e: # pragma: no cover.
165-
sys.exit(str(f"Error when loading {path}: {e}")) # pragma: no cover.
181+
if debug: # pragma: no cover.
182+
raise # pragma: no cover.
183+
else: # pragma: no cover.
184+
sys.exit(str(f"Error when loading {path}: {e}")) # pragma: no cover.
166185

167186
try:
168187
result = DeepSearch(content, item, **kwargs)
169188
except Exception as e: # pragma: no cover.
170-
sys.exit(str(f"Error when running deep search on {path}: {e}")) # pragma: no cover.
189+
if debug: # pragma: no cover.
190+
raise # pragma: no cover.
191+
else: # pragma: no cover.
192+
sys.exit(str(f"Error when running deep search on {path}: {e}")) # pragma: no cover.
171193
pprint(result, indent=2)
172194

173195

174196
@cli.command()
175197
@click.argument('path_inside', required=True, type=str)
176198
@click.argument('path', type=click.Path(exists=True, resolve_path=True))
177-
def extract(path_inside, path):
199+
@click.option('--debug', is_flag=True, show_default=False)
200+
def extract(path_inside, path, debug):
178201
"""
179202
Deep Extract Commandline
180203
@@ -185,10 +208,16 @@ def extract(path_inside, path):
185208
try:
186209
content = load_path_content(path)
187210
except Exception as e: # pragma: no cover.
188-
sys.exit(str(f"Error when loading {path}: {e}")) # pragma: no cover.
211+
if debug: # pragma: no cover.
212+
raise # pragma: no cover.
213+
else: # pragma: no cover.
214+
sys.exit(str(f"Error when loading {path}: {e}")) # pragma: no cover.
189215

190216
try:
191217
result = deep_extract(content, path_inside)
192218
except Exception as e: # pragma: no cover.
193-
sys.exit(str(f"Error when running deep search on {path}: {e}")) # pragma: no cover.
219+
if debug: # pragma: no cover.
220+
raise # pragma: no cover.
221+
else: # pragma: no cover.
222+
sys.exit(str(f"Error when running deep search on {path}: {e}")) # pragma: no cover.
194223
pprint(result, indent=2)

deepdiff/delta.py

+18-4
Original file line numberDiff line numberDiff line change
@@ -69,24 +69,29 @@ def __init__(
6969
serializer=pickle_dump,
7070
verify_symmetry=False,
7171
):
72+
if 'safe_to_import' not in set(deserializer.__code__.co_varnames):
73+
def _deserializer(obj, safe_to_import=None):
74+
return deserializer(obj)
75+
else:
76+
_deserializer = deserializer
7277

7378
if diff is not None:
7479
if isinstance(diff, DeepDiff):
7580
self.diff = diff._to_delta_dict(directed=not verify_symmetry)
7681
elif isinstance(diff, Mapping):
7782
self.diff = diff
7883
elif isinstance(diff, strings):
79-
self.diff = deserializer(diff, safe_to_import=safe_to_import)
84+
self.diff = _deserializer(diff, safe_to_import=safe_to_import)
8085
elif delta_path:
8186
with open(delta_path, 'rb') as the_file:
8287
content = the_file.read()
83-
self.diff = deserializer(content, safe_to_import=safe_to_import)
88+
self.diff = _deserializer(content, safe_to_import=safe_to_import)
8489
elif delta_file:
8590
try:
8691
content = delta_file.read()
8792
except UnicodeDecodeError as e:
8893
raise ValueError(BINIARY_MODE_NEEDED_MSG.format(e)) from None
89-
self.diff = deserializer(content, safe_to_import=safe_to_import)
94+
self.diff = _deserializer(content, safe_to_import=safe_to_import)
9095
else:
9196
raise ValueError(DELTA_AT_LEAST_ONE_ARG_NEEDED)
9297

@@ -512,7 +517,16 @@ def dump(self, file):
512517
"""
513518
Dump into file object
514519
"""
515-
file.write(self.dumps())
520+
# Small optimization: Our internal pickle serializer can just take a file object
521+
# and directly write to it. However if a user defined serializer is passed
522+
# we want to make it compatible with the expectation that self.serializer(self.diff)
523+
# will give the user the serialization and then it can be written to
524+
# a file object when using the dump(file) function.
525+
param_names_of_serializer = set(self.serializer.__code__.co_varnames)
526+
if 'file_obj' in param_names_of_serializer:
527+
self.serializer(self.diff, file_obj=file)
528+
else:
529+
file.write(self.dumps())
516530

517531
def dumps(self):
518532
"""

deepdiff/serialization.py

+35-4
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ class UnsupportedFormatErr(TypeError):
3939
pass
4040

4141

42+
NONE_TYPE = type(None)
43+
4244
CSV_HEADER_MAX_CHUNK_SIZE = 2048 # The chunk needs to be big enough that covers a couple of rows of data.
4345

4446

@@ -254,10 +256,40 @@ def find_class(self, module, name):
254256
# Forbid everything else.
255257
raise ForbiddenModule(FORBIDDEN_MODULE_MSG.format(module_dot_class)) from None
256258

259+
def persistent_load(self, persistent_id):
260+
if persistent_id == "<<NoneType>>":
261+
return type(None)
262+
263+
264+
class _RestrictedPickler(pickle.Pickler):
265+
def persistent_id(self, obj):
266+
if obj is NONE_TYPE: # NOQA
267+
return "<<NoneType>>"
268+
return None
269+
257270

258-
def pickle_dump(obj):
271+
def pickle_dump(obj, file_obj=None):
272+
"""
273+
**pickle_dump**
274+
Dumps the obj into pickled content.
275+
276+
**Parameters**
277+
278+
obj : Any python object
279+
280+
file_obj : (Optional) A file object to dump the contents into
281+
282+
**Returns**
283+
284+
If file_obj is passed the return value will be None. It will write the object's pickle contents into the file.
285+
However if no file_obj is passed, then it will return the pickle serialization of the obj in the form of bytes.
286+
"""
287+
file_obj_passed = bool(file_obj)
288+
file_obj = file_obj or io.BytesIO()
259289
# We expect at least python 3.5 so protocol 4 is good.
260-
return pickle.dumps(obj, protocol=4, fix_imports=False)
290+
_RestrictedPickler(file_obj, protocol=4, fix_imports=False).dump(obj)
291+
if not file_obj_passed:
292+
return file_obj.getvalue()
261293

262294

263295
def pickle_load(content, safe_to_import=None):
@@ -406,8 +438,7 @@ def _save_content(content, path, file_type, keep_backup=True):
406438
content = toml.dump(content, the_file)
407439
elif file_type == 'pickle':
408440
with open(path, 'wb') as the_file:
409-
content = pickle_dump(content)
410-
the_file.write(content)
441+
content = pickle_dump(content, file_obj=the_file)
411442
elif file_type in {'csv', 'tsv'}:
412443
if clevercsv is None: # pragma: no cover.
413444
raise ImportError('CleverCSV needs to be installed.') # pragma: no cover.

docs/changelog.rst

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Changelog
55

66
DeepDiff Changelog
77

8+
- v5-2-2: Fixed Delta serialization when None type is present.
89
- 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.
910
- v5-0-2: Bug Fix NoneType in ignore type groups https://github.com/seperman/deepdiff/issues/207
1011
- v5-0-1: Bug fix to not apply format to non numbers.

docs/conf.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,9 @@
6060
# built documents.
6161
#
6262
# The short X.Y version.
63-
version = '5.2.1'
63+
version = '5.2.2'
6464
# The full version, including alpha/beta/rc tags.
65-
release = '5.2.1'
65+
release = '5.2.2'
6666

6767
load_dotenv(override=True)
6868
DOC_VERSION = os.environ.get('DOC_VERSION', version)

0 commit comments

Comments
 (0)