-
Notifications
You must be signed in to change notification settings - Fork 50
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WIP Added support for tuples, namedtuples (both from collections and typing), sets, frozensets and OrderedDict's in MSONable, MontyEncoder and MontyDecoder #100
base: master
Are you sure you want to change the base?
Changes from 7 commits
0755e76
f259b94
92246a7
680461d
9a57714
41f4463
d5527b9
3f3c029
6866658
b67e6e6
e233d4c
465467d
3ed73cc
7e823a9
d982c3b
04e0513
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,10 +7,13 @@ | |
import datetime | ||
from bson.objectid import ObjectId | ||
from enum import Enum | ||
from collections import namedtuple | ||
from collections import OrderedDict | ||
|
||
from . import __version__ as tests_version | ||
from monty.json import MSONable, MontyEncoder, MontyDecoder, jsanitize | ||
from monty.json import _load_redirect | ||
from monty.collections import is_namedtuple | ||
|
||
test_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "test_files") | ||
|
||
|
@@ -188,6 +191,44 @@ def test_enum_serialization(self): | |
self.assertEqual(e_new.name, e.name) | ||
self.assertEqual(e_new.value, e.value) | ||
|
||
def test_tuple_serialization(self): | ||
a = GoodMSONClass(a=1, b=tuple(), c=1) | ||
afromdict = GoodMSONClass.from_dict(a.as_dict()) | ||
assert type(a.b) is tuple | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think isinstance is a better approach. This applies to all instances where you use type(X) is or == something. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. isinstance checks that the instance is a tuple or a subclass of tuple. I think it is more safe to use type(a.b) is tuple as it would prevent someone (a weirdo btw ;-) to change the MSONable as_dict and/or from_dict to return a subclass of tuple when deserializing. What do you think ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree. But this is a test, not the actual code. So here, it suffices to make sure it is an instance of tuple. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the user serializes a tuple, he expects to get back exactly a tuple when deserializing. Using isinstance in the test allows to change that. I could modify the MSONable from_dict to return a subclass of a tuple instead of a real tuple. |
||
assert type(afromdict.b) is tuple | ||
assert a.b == afromdict.b | ||
a = GoodMSONClass(a=1, b=[1, tuple([2, tuple([1, 2, 3, 4])])], c=1) | ||
afromdict = GoodMSONClass.from_dict(a.as_dict()) | ||
assert type(a.b) is list | ||
assert type(afromdict.b) is list | ||
assert afromdict.b[0] == 1 | ||
assert type(afromdict.b[1]) is tuple | ||
assert afromdict.b[1][0] == 2 | ||
assert type(afromdict.b[1][1]) is tuple | ||
assert afromdict.b[1][1] == (1, 2, 3, 4) | ||
|
||
def test_namedtuple_serialization(self): | ||
a = namedtuple('A', ['x', 'y', 'zzz']) | ||
b = GoodMSONClass(a=1, b=a(1, 2, 3), c=1) | ||
assert is_namedtuple(b.b) is True | ||
assert b.b._fields == ('x', 'y', 'zzz') | ||
bfromdict = GoodMSONClass.from_dict(b.as_dict()) | ||
assert is_namedtuple(bfromdict.b) is True | ||
assert bfromdict.b._fields == ('x', 'y', 'zzz') | ||
assert bfromdict.b == b.b | ||
assert bfromdict.b.x == b.b.x | ||
assert bfromdict.b.y == b.b.y | ||
assert bfromdict.b.zzz == b.b.zzz | ||
|
||
def test_OrderedDict_serialization(self): | ||
od = OrderedDict([('val1', 1), ('val2', 2)]) | ||
od['val3'] = '3' | ||
a = GoodMSONClass(a=1, b=od, c=1) | ||
assert list(a.b.keys()) == ['val1', 'val2', 'val3'] | ||
afromdict = GoodMSONClass.from_dict(a.as_dict()) | ||
assert type(afromdict.b) is OrderedDict | ||
assert list(afromdict.b.keys()) == ['val1', 'val2', 'val3'] | ||
|
||
|
||
class JsonTest(unittest.TestCase): | ||
|
||
|
@@ -303,6 +344,38 @@ def test_redirect_settings_file(self): | |
data = _load_redirect(os.path.join(test_dir, "test_settings.yaml")) | ||
self.assertEqual(data, {'old_module': {'old_class': {'@class': 'new_class', '@module': 'new_module'}}}) | ||
|
||
def test_complex_enc_dec(self): | ||
a = tuple([0, 2, 4]) | ||
a_jsonstr = json.dumps(a, cls=MontyEncoder) | ||
a_from_jsonstr = json.loads(a_jsonstr, cls=MontyDecoder) | ||
assert type(a_from_jsonstr) is tuple | ||
|
||
nt = namedtuple('A', ['x', 'y', 'zzz']) | ||
nt2 = namedtuple('ABC', ['ab', 'cd', 'ef']) | ||
od = OrderedDict([('val1', 1), ('val2', GoodMSONClass(a=a, b=nt2(1, 2, 3), c=1))]) | ||
od['val3'] = '3' | ||
|
||
obj = nt(x=a, y=od, zzz=[1, 2, 3]) | ||
obj_jsonstr = json.dumps(obj, cls=MontyEncoder) | ||
obj_from_jsonstr = json.loads(obj_jsonstr, cls=MontyDecoder) | ||
|
||
assert is_namedtuple(obj_from_jsonstr) is True | ||
assert obj_from_jsonstr.__class__.__name__ == 'A' | ||
assert type(obj_from_jsonstr.x) is tuple | ||
assert obj_from_jsonstr.x == (0, 2, 4) | ||
assert type(obj_from_jsonstr.y) is OrderedDict | ||
assert list(obj_from_jsonstr.y.keys()) == ['val1', 'val2', 'val3'] | ||
assert obj_from_jsonstr.y['val1'] == 1 | ||
assert type(obj_from_jsonstr.y['val2']) is GoodMSONClass | ||
assert type(obj_from_jsonstr.y['val2'].a) is tuple | ||
assert obj_from_jsonstr.y['val2'].a == (0, 2, 4) | ||
assert is_namedtuple(obj_from_jsonstr.y['val2'].b) is True | ||
assert obj_from_jsonstr.y['val2'].b.__class__.__name__ == 'ABC' | ||
assert obj_from_jsonstr.y['val2'].b.ab == 1 | ||
assert obj_from_jsonstr.y['val2'].b.cd == 2 | ||
assert obj_from_jsonstr.y['val2'].b.ef == 3 | ||
assert obj_from_jsonstr.y['val3'] == '3' | ||
|
||
|
||
if __name__ == "__main__": | ||
unittest.main() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think you need "is True". The assert tests whether it is True.
Similarly, 3 lines later you just need "assert not is_named_tuple..."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is actually not exactly equivalent. The "is True" also checks that the returned value is a bool and not for example an empty list vs a non empty list. If is_namedtuple(a1) were to return, e.g. ["hello"] (or to return [] in the "False" case), "assert is_namedtuple(a1)" would also pass while the API would have changed. I would keep the "is True" if that's ok for you. Same for 3 lines after (and also in quite a lot of other places).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I know it is not exactly equivalent, but we need to think of typical use cases. When people use that method, they are not going to check that it is a boolean type. They will merely check that it is a "True-like" value. E.g., if is_namedtuple(a1). That's the Pythonic way of doing this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Of course, users will use it this way. My point is that this method is supposed (from its docstring/name) to return a bool indicating whether it is a namedtuple or not. Why should we allow any new development/change to return something else (e.g. the string "Yes it is a namedtuple" for the True case and "" for the False case) ? This unit test is just preventing that. Anyway if you prefer to remove the "is True", I can do it. For me the pythonic way applies for the code itself, but the test code can deviate from that for the reason above.