From 0755e766ed8dc9347c2762986d00757bb469ec23 Mon Sep 17 00:00:00 2001 From: davidwaroquiers Date: Tue, 14 Jan 2020 10:39:36 +0100 Subject: [PATCH 01/15] Added is_namedtuple method to test whether an object is a namedtuple in collections.py. Unit test for above method. --- monty/collections.py | 5 +++++ tests/test_collections.py | 25 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/monty/collections.py b/monty/collections.py index 75c71dc6c..90a9aea0d 100644 --- a/monty/collections.py +++ b/monty/collections.py @@ -223,3 +223,8 @@ def dict2namedtuple(*args, **kwargs): d.update(**kwargs) return collections.namedtuple(typename="dict2namedtuple", field_names=list(d.keys()))(**d) + + +def is_namedtuple(obj): + return isinstance(obj, tuple) and hasattr(obj, "_fields") and \ + hasattr(obj, "_fields_defaults") and hasattr(obj, "_asdict") diff --git a/tests/test_collections.py b/tests/test_collections.py index 031688234..59baf3e2f 100644 --- a/tests/test_collections.py +++ b/tests/test_collections.py @@ -1,8 +1,11 @@ import unittest import os +from collections import namedtuple + from monty.collections import frozendict, Namespace, AttrDict, \ FrozenAttrDict, tree +from monty.collections import is_namedtuple test_dir = os.path.join(os.path.dirname(__file__), 'test_files') @@ -49,5 +52,27 @@ def test_tree(self): self.assertEqual(x['a']['b']['c']['d'], 1) +def test_is_namedtuple(): + a_nt = namedtuple('a', ['x', 'y', 'z']) + a1 = a_nt(1, 2, 3) + assert a1 == (1, 2, 3) + assert is_namedtuple(a1) is True + a_t = tuple([1, 2]) + assert a_t == (1, 2) + assert is_namedtuple(a_t) is False + + class SubList(list): + def _fields(self): + return [] + def _fields_defaults(self): + return [] + def _asdict(self): + return {} + + sublist = SubList([3, 2, 1]) + assert sublist == [3, 2, 1] + assert is_namedtuple(sublist) is False + + if __name__ == "__main__": unittest.main() From 92246a7a6ede8a0fd76b6516008c6cc7a2bccbfa Mon Sep 17 00:00:00 2001 From: davidwaroquiers Date: Tue, 14 Jan 2020 11:49:28 +0100 Subject: [PATCH 02/15] Added (partial) support for tuple and namedtuples in json.py. Unit tests for tuple and namedtuple serialization. --- monty/json.py | 23 ++++++++++++++++++++++- tests/test_json.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/monty/json.py b/monty/json.py index 072b466e5..bfbdead31 100644 --- a/monty/json.py +++ b/monty/json.py @@ -9,6 +9,7 @@ from hashlib import sha1 from collections import OrderedDict, defaultdict +from collections import namedtuple from enum import Enum from importlib import import_module @@ -33,6 +34,9 @@ except ImportError: yaml = None # type: ignore +from monty.collections import is_namedtuple + + __version__ = "3.0.0" @@ -123,7 +127,18 @@ def as_dict(self) -> dict: args = getfullargspec(self.__class__.__init__).args def recursive_as_dict(obj): - if isinstance(obj, (list, tuple)): + if is_namedtuple(obj): + return {"namedtuple_as_list": [recursive_as_dict(it) for it in obj], + "fields": obj._fields, + "fields_defaults": obj._fields_defaults, + "typename": obj.__class__.__name__, + "@module": "builtins", + "@class": "namedtuple"} + if isinstance(obj, tuple): + return {"tuple_as_list": [recursive_as_dict(it) for it in obj], + "@module": "builtins", + "@class": "tuple"} + if isinstance(obj, list): return [recursive_as_dict(it) for it in obj] if isinstance(obj, dict): return {kk: recursive_as_dict(vv) for kk, vv in obj.items()} @@ -308,6 +323,12 @@ def process_decoded(self, d): dt = datetime.datetime.strptime(d["string"], "%Y-%m-%d %H:%M:%S") return dt + if modname == "builtins": + if classname == "tuple": + return tuple([self.process_decoded(item) for item in d['tuple_as_list']]) + if classname == "namedtuple": + nt = namedtuple(d['typename'], d['fields'], defaults=d['fields_defaults']) + return nt(*[self.process_decoded(item) for item in d['namedtuple_as_list']]) mod = __import__(modname, globals(), locals(), [classname], 0) if hasattr(mod, classname): diff --git a/tests/test_json.py b/tests/test_json.py index b8ae32fb3..46425dbdf 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -7,10 +7,12 @@ import datetime from bson.objectid import ObjectId from enum import Enum +from collections import namedtuple 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 +190,35 @@ 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 + 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 + class JsonTest(unittest.TestCase): From 680461da1d59d011490ca0b53efba7028ea8ae0d Mon Sep 17 00:00:00 2001 From: davidwaroquiers Date: Tue, 14 Jan 2020 14:20:45 +0100 Subject: [PATCH 03/15] Added support for tuple, namedtuples and OrderedDict in json.py. Unit tests for tuple, namedtuple and OrderedDict serialization. Works with MSONable class as well as with MontyEncoder and MontyDecoder. --- monty/json.py | 66 +++++++++++++++++++++++++++++++--------------- tests/test_json.py | 42 +++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 21 deletions(-) diff --git a/monty/json.py b/monty/json.py index bfbdead31..d8c1906b6 100644 --- a/monty/json.py +++ b/monty/json.py @@ -66,6 +66,35 @@ def _load_redirect(redirect_file): return dict(redirect_dict) +def _recursive_as_dict(obj): + """Recursive function to prepare serialization of objects. + + Takes care of tuples, namedtuples, OrderedDict, objects with an as_dict method. + """ + if is_namedtuple(obj): + return {"namedtuple_as_list": [_recursive_as_dict(it) for it in obj], + "fields": obj._fields, + "fields_defaults": obj._fields_defaults, + "typename": obj.__class__.__name__, + "@module": "builtins", + "@class": "namedtuple"} + if isinstance(obj, tuple): + return {"tuple_as_list": [_recursive_as_dict(it) for it in obj], + "@module": "builtins", + "@class": "tuple"} + if isinstance(obj, OrderedDict): + return {"ordereddict_as_list": [[key, _recursive_as_dict(val)] for key, val in obj.items()], + "@module": "builtins", + "@class": "OrderedDict"} + if isinstance(obj, list): + return [_recursive_as_dict(it) for it in obj] + if isinstance(obj, dict): + return {kk: _recursive_as_dict(vv) for kk, vv in obj.items()} + if hasattr(obj, "as_dict"): + return obj.as_dict() + return obj + + class MSONable: """ This is a mix-in base class specifying an API for msonable objects. MSON @@ -126,26 +155,6 @@ def as_dict(self) -> dict: args = getfullargspec(self.__class__.__init__).args - def recursive_as_dict(obj): - if is_namedtuple(obj): - return {"namedtuple_as_list": [recursive_as_dict(it) for it in obj], - "fields": obj._fields, - "fields_defaults": obj._fields_defaults, - "typename": obj.__class__.__name__, - "@module": "builtins", - "@class": "namedtuple"} - if isinstance(obj, tuple): - return {"tuple_as_list": [recursive_as_dict(it) for it in obj], - "@module": "builtins", - "@class": "tuple"} - if isinstance(obj, list): - return [recursive_as_dict(it) for it in obj] - if isinstance(obj, dict): - return {kk: recursive_as_dict(vv) for kk, vv in obj.items()} - if hasattr(obj, "as_dict"): - return obj.as_dict() - return obj - for c in args: if c != "self": try: @@ -162,7 +171,7 @@ def recursive_as_dict(obj): "a self.kwargs variable to automatically " "determine the dict format. Alternatively, " "you can implement both as_dict and from_dict.") - d[c] = recursive_as_dict(a) + d[c] = _recursive_as_dict(a) if hasattr(self, "kwargs"): # type: ignore d.update(**getattr(self, "kwargs")) # pylint: disable=E1101 @@ -266,6 +275,8 @@ def default(self, o) -> dict: # pylint: disable=E0202 "@class": "ObjectId", "oid": str(o)} + # Is this still useful as we are now calling the _recursive_as_dict + # method (which takes care of as_dict's) before the encoding ? try: d = o.as_dict() if "@module" not in d: @@ -283,6 +294,17 @@ def default(self, o) -> dict: # pylint: disable=E0202 except AttributeError: return json.JSONEncoder.default(self, o) + def encode(self, o): + """MontyEncoder's encode method. + + First, prepares the object by recursively transforming tuples, namedtuples, + object having an as_dict method and others to encodable python objects. + """ + # This cannot go in the default method because default is called as a last resort, + # such that tuples and namedtuples have already been transformed to lists by json's encode method. + o = _recursive_as_dict(o) + return super().encode(o) + class MontyDecoder(json.JSONDecoder): """ @@ -329,6 +351,8 @@ def process_decoded(self, d): if classname == "namedtuple": nt = namedtuple(d['typename'], d['fields'], defaults=d['fields_defaults']) return nt(*[self.process_decoded(item) for item in d['namedtuple_as_list']]) + if classname == "OrderedDict": + return OrderedDict([(key, self.process_decoded(val)) for key, val in d['ordereddict_as_list']]) mod = __import__(modname, globals(), locals(), [classname], 0) if hasattr(mod, classname): diff --git a/tests/test_json.py b/tests/test_json.py index 46425dbdf..f635c0ef7 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -8,6 +8,7 @@ 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 @@ -219,6 +220,15 @@ def test_namedtuple_serialization(self): 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): @@ -334,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() From 9a57714cba85e131e07d4efafa5d0b300a70e56a Mon Sep 17 00:00:00 2001 From: davidwaroquiers Date: Tue, 14 Jan 2020 15:23:03 +0100 Subject: [PATCH 04/15] Fixed namedtuple support for python versions < 3.7, for which default values were not possible. --- monty/collections.py | 4 ++-- monty/json.py | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/monty/collections.py b/monty/collections.py index 90a9aea0d..5e2a7f9cc 100644 --- a/monty/collections.py +++ b/monty/collections.py @@ -226,5 +226,5 @@ def dict2namedtuple(*args, **kwargs): def is_namedtuple(obj): - return isinstance(obj, tuple) and hasattr(obj, "_fields") and \ - hasattr(obj, "_fields_defaults") and hasattr(obj, "_asdict") + """Test if an object is a class generated from collections.namedtuple.""" + return isinstance(obj, tuple) and hasattr(obj, "_fields") and hasattr(obj, "_asdict") diff --git a/monty/json.py b/monty/json.py index d8c1906b6..945fd52d7 100644 --- a/monty/json.py +++ b/monty/json.py @@ -6,6 +6,7 @@ import os import json import datetime +import sys from hashlib import sha1 from collections import OrderedDict, defaultdict @@ -72,6 +73,12 @@ def _recursive_as_dict(obj): Takes care of tuples, namedtuples, OrderedDict, objects with an as_dict method. """ if is_namedtuple(obj): + if sys.version_info < (3, 7): # default values for namedtuples were introduced in python 3.7. + return {"namedtuple_as_list": [_recursive_as_dict(it) for it in obj], + "fields": obj._fields, + "typename": obj.__class__.__name__, + "@module": "builtins", + "@class": "namedtuple"} return {"namedtuple_as_list": [_recursive_as_dict(it) for it in obj], "fields": obj._fields, "fields_defaults": obj._fields_defaults, @@ -349,7 +356,13 @@ def process_decoded(self, d): if classname == "tuple": return tuple([self.process_decoded(item) for item in d['tuple_as_list']]) if classname == "namedtuple": - nt = namedtuple(d['typename'], d['fields'], defaults=d['fields_defaults']) + # default values for collections.namedtuple have been introduced in python 3.7 + # it is probably not essential to deserialize the defaults if the object was serialized with + # python >= 3.7 and deserialized with python < 3.7. + if sys.version_info < (3, 7): + nt = namedtuple(d['typename'], d['fields']) + else: + nt = namedtuple(d['typename'], d['fields'], defaults=d['fields_defaults']) return nt(*[self.process_decoded(item) for item in d['namedtuple_as_list']]) if classname == "OrderedDict": return OrderedDict([(key, self.process_decoded(val)) for key, val in d['ordereddict_as_list']]) From 41f44633110375a27fac775e8a5585620025289e Mon Sep 17 00:00:00 2001 From: davidwaroquiers Date: Tue, 14 Jan 2020 15:28:33 +0100 Subject: [PATCH 05/15] Disable pylint's check on unexpected keyword argument 'defaults'. --- monty/json.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monty/json.py b/monty/json.py index 945fd52d7..190b647a3 100644 --- a/monty/json.py +++ b/monty/json.py @@ -362,7 +362,8 @@ def process_decoded(self, d): if sys.version_info < (3, 7): nt = namedtuple(d['typename'], d['fields']) else: - nt = namedtuple(d['typename'], d['fields'], defaults=d['fields_defaults']) + nt = namedtuple(d['typename'], d['fields'], + defaults=d['fields_defaults']) # pylint: disable=E1123 return nt(*[self.process_decoded(item) for item in d['namedtuple_as_list']]) if classname == "OrderedDict": return OrderedDict([(key, self.process_decoded(val)) for key, val in d['ordereddict_as_list']]) From d5527b9d2921596cb253c469156579984069aba0 Mon Sep 17 00:00:00 2001 From: davidwaroquiers Date: Tue, 14 Jan 2020 15:30:44 +0100 Subject: [PATCH 06/15] Disable pylint's check on unexpected keyword argument 'defaults'. --- monty/json.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monty/json.py b/monty/json.py index 190b647a3..8038c930e 100644 --- a/monty/json.py +++ b/monty/json.py @@ -362,7 +362,7 @@ def process_decoded(self, d): if sys.version_info < (3, 7): nt = namedtuple(d['typename'], d['fields']) else: - nt = namedtuple(d['typename'], d['fields'], + nt = namedtuple(d['typename'], d['fields'], # pylint: disable=E1123 defaults=d['fields_defaults']) # pylint: disable=E1123 return nt(*[self.process_decoded(item) for item in d['namedtuple_as_list']]) if classname == "OrderedDict": From 3f3c029738e18d52cb5a1cad94c6eeffca2d454a Mon Sep 17 00:00:00 2001 From: davidwaroquiers Date: Wed, 15 Jan 2020 11:27:00 +0100 Subject: [PATCH 07/15] Added is_NamedTuple to check whether object is a class generated from typing.NamedTuple. Unit tests for is_NamedTuple. --- monty/collections.py | 10 +++++++++- tests/test_collections.py | 27 +++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/monty/collections.py b/monty/collections.py index 5e2a7f9cc..09744458f 100644 --- a/monty/collections.py +++ b/monty/collections.py @@ -227,4 +227,12 @@ def dict2namedtuple(*args, **kwargs): def is_namedtuple(obj): """Test if an object is a class generated from collections.namedtuple.""" - return isinstance(obj, tuple) and hasattr(obj, "_fields") and hasattr(obj, "_asdict") + return (isinstance(obj, tuple) and hasattr(obj, "_fields") and + hasattr(obj, "_asdict") and (not hasattr(obj, '_field_types'))) + + +def is_NamedTuple(obj): + """Test if an object is a class generated from typing.NamedTuple.""" + return (isinstance(obj, tuple) and hasattr(obj, "_fields") and + hasattr(obj, "_asdict") and hasattr(obj, '_field_types') and + hasattr(obj, '__annotations__')) diff --git a/tests/test_collections.py b/tests/test_collections.py index 59baf3e2f..d51de6bbd 100644 --- a/tests/test_collections.py +++ b/tests/test_collections.py @@ -1,11 +1,14 @@ import unittest import os +import sys from collections import namedtuple +from typing import NamedTuple from monty.collections import frozendict, Namespace, AttrDict, \ FrozenAttrDict, tree from monty.collections import is_namedtuple +from monty.collections import is_NamedTuple test_dir = os.path.join(os.path.dirname(__file__), 'test_files') @@ -53,13 +56,16 @@ def test_tree(self): def test_is_namedtuple(): + # Testing collections.namedtuple a_nt = namedtuple('a', ['x', 'y', 'z']) a1 = a_nt(1, 2, 3) assert a1 == (1, 2, 3) assert is_namedtuple(a1) is True + assert is_NamedTuple(a1) is False a_t = tuple([1, 2]) assert a_t == (1, 2) assert is_namedtuple(a_t) is False + assert is_NamedTuple(a_t) is False class SubList(list): def _fields(self): @@ -72,6 +78,27 @@ def _asdict(self): sublist = SubList([3, 2, 1]) assert sublist == [3, 2, 1] assert is_namedtuple(sublist) is False + assert is_NamedTuple(sublist) is False + + # Testing typing.NamedTuple + A = NamedTuple('A', [('int1', int), ('str1', str)]) + nt = A(3, 'b') + assert is_NamedTuple(nt) is True + assert is_namedtuple(nt) is False + + # Testing typing.NamedTuple with type annotations (for python >= 3.6) + # This will not work for python < 3.6, leading to a SyntaxError hence the + # exec here. + try: + exec('class B(NamedTuple):\n\ + int1: int = 1\n\ + str1: str = \'a\'\n\ + global B') # Make the B class available globally + nt = B(2, 'hello') + assert is_NamedTuple(nt) is True + assert is_namedtuple(nt) is False + except SyntaxError: + assert sys.version_info < (3, 6) # Make sure we get this SyntaxError only in the case of python < 3.6. if __name__ == "__main__": From 68666588997d40e1574eee907e5cca78438de5e9 Mon Sep 17 00:00:00 2001 From: davidwaroquiers Date: Thu, 16 Jan 2020 09:50:55 +0100 Subject: [PATCH 08/15] Added validate_NamedTuple to check whether the items in a NamedTuple have the correct type. Unit tests for validate_NamedTuple. --- monty/collections.py | 11 +++++++++++ tests/test_collections.py | 21 ++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/monty/collections.py b/monty/collections.py index 09744458f..410a8f5ff 100644 --- a/monty/collections.py +++ b/monty/collections.py @@ -236,3 +236,14 @@ def is_NamedTuple(obj): return (isinstance(obj, tuple) and hasattr(obj, "_fields") and hasattr(obj, "_asdict") and hasattr(obj, '_field_types') and hasattr(obj, '__annotations__')) + + +def validate_NamedTuple(obj): + """Validates whether the items in the NamedTuple have the correct type.""" + if not is_NamedTuple(obj): + raise ValueError('Cannot validate object of type "{}".'.format(obj.__class__.__name__)) + for field, field_type in obj._field_types.items(): + value = getattr(obj, field) + if not type(value) is field_type: + return False + return True diff --git a/tests/test_collections.py b/tests/test_collections.py index d51de6bbd..348d5916f 100644 --- a/tests/test_collections.py +++ b/tests/test_collections.py @@ -4,11 +4,13 @@ from collections import namedtuple from typing import NamedTuple +import pytest from monty.collections import frozendict, Namespace, AttrDict, \ FrozenAttrDict, tree from monty.collections import is_namedtuple from monty.collections import is_NamedTuple +from monty.collections import validate_NamedTuple test_dir = os.path.join(os.path.dirname(__file__), 'test_files') @@ -62,10 +64,14 @@ def test_is_namedtuple(): assert a1 == (1, 2, 3) assert is_namedtuple(a1) is True assert is_NamedTuple(a1) is False + with pytest.raises(ValueError, match=r'Cannot validate object of type "a"\.'): + validate_NamedTuple(a1) a_t = tuple([1, 2]) assert a_t == (1, 2) assert is_namedtuple(a_t) is False assert is_NamedTuple(a_t) is False + with pytest.raises(ValueError, match=r'Cannot validate object of type "tuple"\.'): + validate_NamedTuple(a_t) class SubList(list): def _fields(self): @@ -79,12 +85,19 @@ def _asdict(self): assert sublist == [3, 2, 1] assert is_namedtuple(sublist) is False assert is_NamedTuple(sublist) is False + with pytest.raises(ValueError, match=r'Cannot validate object of type "SubList"\.'): + validate_NamedTuple(sublist) # Testing typing.NamedTuple A = NamedTuple('A', [('int1', int), ('str1', str)]) nt = A(3, 'b') assert is_NamedTuple(nt) is True assert is_namedtuple(nt) is False + assert validate_NamedTuple(nt) is True + nt = A(3, 2) + assert validate_NamedTuple(nt) is False + nt = A('a', 'b') + assert validate_NamedTuple(nt) is False # Testing typing.NamedTuple with type annotations (for python >= 3.6) # This will not work for python < 3.6, leading to a SyntaxError hence the @@ -97,8 +110,14 @@ def _asdict(self): nt = B(2, 'hello') assert is_NamedTuple(nt) is True assert is_namedtuple(nt) is False + assert validate_NamedTuple(nt) is True + nt = B('a', 'b') + assert validate_NamedTuple(nt) is False + nt = B(3, 4) + assert validate_NamedTuple(nt) is False except SyntaxError: - assert sys.version_info < (3, 6) # Make sure we get this SyntaxError only in the case of python < 3.6. + # Make sure we get this SyntaxError only in the case of python < 3.6. + assert sys.version_info < (3, 6) if __name__ == "__main__": From b67e6e604680ba4719471f0f4ea6ad54c10d150d Mon Sep 17 00:00:00 2001 From: davidwaroquiers Date: Thu, 16 Jan 2020 10:05:12 +0100 Subject: [PATCH 09/15] Changed validate_NamedTuple to accept subclass of the types as valid field types for the different fields. --- monty/collections.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monty/collections.py b/monty/collections.py index 410a8f5ff..215769684 100644 --- a/monty/collections.py +++ b/monty/collections.py @@ -244,6 +244,6 @@ def validate_NamedTuple(obj): raise ValueError('Cannot validate object of type "{}".'.format(obj.__class__.__name__)) for field, field_type in obj._field_types.items(): value = getattr(obj, field) - if not type(value) is field_type: + if isinstance(value, field_type): return False return True From e233d4c9c519307717bf142098f91e8832e335ab Mon Sep 17 00:00:00 2001 From: davidwaroquiers Date: Thu, 16 Jan 2020 10:13:22 +0100 Subject: [PATCH 10/15] Changed validate_NamedTuple to accept subclass of the types as valid field types for the different fields. --- monty/collections.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monty/collections.py b/monty/collections.py index 215769684..86275c187 100644 --- a/monty/collections.py +++ b/monty/collections.py @@ -244,6 +244,6 @@ def validate_NamedTuple(obj): raise ValueError('Cannot validate object of type "{}".'.format(obj.__class__.__name__)) for field, field_type in obj._field_types.items(): value = getattr(obj, field) - if isinstance(value, field_type): + if not isinstance(value, field_type): return False return True From 465467d70eba851bddfc0667871dbd3c715e3c71 Mon Sep 17 00:00:00 2001 From: davidwaroquiers Date: Thu, 16 Jan 2020 10:31:56 +0100 Subject: [PATCH 11/15] Ignoring pytest import for mypy. --- tests/test_collections.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_collections.py b/tests/test_collections.py index 348d5916f..0a75a4cf2 100644 --- a/tests/test_collections.py +++ b/tests/test_collections.py @@ -4,7 +4,7 @@ from collections import namedtuple from typing import NamedTuple -import pytest +import pytest # type: ignore # Ignore pytest import for mypy from monty.collections import frozendict, Namespace, AttrDict, \ FrozenAttrDict, tree From 3ed73cc9b470fe61d556bf867d022b8fe32b4981 Mon Sep 17 00:00:00 2001 From: davidwaroquiers Date: Thu, 16 Jan 2020 10:45:38 +0100 Subject: [PATCH 12/15] Changed "builtins" to "@builtins" in the serialization of tuples, namedtuples, etc ... in order to avoid potential clashes with the builtins package. --- monty/json.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/monty/json.py b/monty/json.py index 8038c930e..b8b2c95fd 100644 --- a/monty/json.py +++ b/monty/json.py @@ -77,21 +77,21 @@ def _recursive_as_dict(obj): return {"namedtuple_as_list": [_recursive_as_dict(it) for it in obj], "fields": obj._fields, "typename": obj.__class__.__name__, - "@module": "builtins", + "@module": "@builtins", "@class": "namedtuple"} return {"namedtuple_as_list": [_recursive_as_dict(it) for it in obj], "fields": obj._fields, "fields_defaults": obj._fields_defaults, "typename": obj.__class__.__name__, - "@module": "builtins", + "@module": "@builtins", "@class": "namedtuple"} if isinstance(obj, tuple): return {"tuple_as_list": [_recursive_as_dict(it) for it in obj], - "@module": "builtins", + "@module": "@builtins", "@class": "tuple"} if isinstance(obj, OrderedDict): return {"ordereddict_as_list": [[key, _recursive_as_dict(val)] for key, val in obj.items()], - "@module": "builtins", + "@module": "@builtins", "@class": "OrderedDict"} if isinstance(obj, list): return [_recursive_as_dict(it) for it in obj] @@ -352,7 +352,7 @@ def process_decoded(self, d): dt = datetime.datetime.strptime(d["string"], "%Y-%m-%d %H:%M:%S") return dt - if modname == "builtins": + if modname == "@builtins": if classname == "tuple": return tuple([self.process_decoded(item) for item in d['tuple_as_list']]) if classname == "namedtuple": From 7e823a99fa7b78273060c55eaa5c907ceaf56796 Mon Sep 17 00:00:00 2001 From: davidwaroquiers Date: Fri, 17 Jan 2020 12:01:26 +0100 Subject: [PATCH 13/15] Added serialization of typing.NamedTuple's. --- monty/json.py | 96 ++++++++++++++++++++++++++++++++++++------ tests/test_json.py | 103 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 186 insertions(+), 13 deletions(-) diff --git a/monty/json.py b/monty/json.py index b8b2c95fd..82fe40540 100644 --- a/monty/json.py +++ b/monty/json.py @@ -12,6 +12,7 @@ from collections import OrderedDict, defaultdict from collections import namedtuple from enum import Enum +from typing import NamedTuple from importlib import import_module @@ -36,6 +37,7 @@ yaml = None # type: ignore from monty.collections import is_namedtuple +from monty.collections import is_NamedTuple __version__ = "3.0.0" @@ -66,6 +68,55 @@ def _load_redirect(redirect_file): return dict(redirect_dict) +# (Private) helper methods and variables for the serialization of +# types for typing.NamedTuple's. +_typ2name = {typ: typ.__name__ for typ in (bool, int, float, complex, + list, tuple, range, + str, + bytes, bytearray, memoryview, + set, frozenset, + dict)} +_name2typ = {val: key for key, val in _typ2name.items()} +_name2typ['NoneType'] = type(None) + + +def _serialize_type(typ): + """Serialization of types.""" + # Builtin standard types + if typ in _typ2name: + return{"@module": "@builtins", + "@class": "@types", + "type": _typ2name[typ]} + # None/NoneType is a special case + if typ is type(None) or typ is None: + return {"@module": "@builtins", + "@class": "@types", + "type": "NoneType"} + # Other types ("normal" classes) + return {"@module": "@builtins", + "@class": "@types", + "type": "@class", + "type_module": typ.__module__, + "type_class": typ.__name__} + + +def _deserialize_type(d): + """Deserialization of types.""" + if d["type"] in _name2typ: + return _name2typ[d["type"]] + if d["type"] == "@class": + modname = d["type_module"] + classname = d["type_class"] + if classname in MSONable.REDIRECT.get(modname, {}): + modname = MSONable.REDIRECT[modname][classname]["@module"] + classname = MSONable.REDIRECT[modname][classname]["@class"] + mod = __import__(modname, globals(), locals(), [classname], 0) + try: + return getattr(mod, classname) + except AttributeError: + raise ValueError('Could not deserialize type.') + raise ValueError('Could not deserialize type.') + def _recursive_as_dict(obj): """Recursive function to prepare serialization of objects. @@ -73,18 +124,30 @@ def _recursive_as_dict(obj): Takes care of tuples, namedtuples, OrderedDict, objects with an as_dict method. """ if is_namedtuple(obj): - if sys.version_info < (3, 7): # default values for namedtuples were introduced in python 3.7. - return {"namedtuple_as_list": [_recursive_as_dict(it) for it in obj], - "fields": obj._fields, - "typename": obj.__class__.__name__, - "@module": "@builtins", - "@class": "namedtuple"} - return {"namedtuple_as_list": [_recursive_as_dict(it) for it in obj], - "fields": obj._fields, - "fields_defaults": obj._fields_defaults, - "typename": obj.__class__.__name__, - "@module": "@builtins", - "@class": "namedtuple"} + d = {"namedtuple_as_list": [_recursive_as_dict(it) for it in obj], + "fields": obj._fields, + "typename": obj.__class__.__name__, + "@module": "@builtins", + "@class": "collections.namedtuple"} + if sys.version_info >= (3, 7): # default values for collections.namedtuple's were introduced in python 3.7. + d["fields_defaults"] = obj._fields_defaults + return d + if is_NamedTuple(obj): + d = {"NamedTuple_as_list": [_recursive_as_dict(it) for it in obj], + "fields": obj._fields, + "fields_types": [_serialize_type(obj._field_types[field]) for field in obj._fields], + "typename": obj.__class__.__name__, + "doc": obj.__doc__, + "@module": "@builtins", + "@class": "typing.NamedTuple"} + if sys.version_info >= (3, 6): # default values for typing.NamedTuple's were introduced in python 3.6. + try: + d["fields_defaults"] = [(field, _recursive_as_dict(field_default)) for field, field_default in obj._field_defaults.items()] + except AttributeError: + d["fields_defaults"] = [] + return d + # The order of the ifs matter here as namedtuples and NamedTuples are instances (subclasses) of tuples, + # same for OrderedDict which is an instance (subclass) of dict. if isinstance(obj, tuple): return {"tuple_as_list": [_recursive_as_dict(it) for it in obj], "@module": "@builtins", @@ -355,7 +418,7 @@ def process_decoded(self, d): if modname == "@builtins": if classname == "tuple": return tuple([self.process_decoded(item) for item in d['tuple_as_list']]) - if classname == "namedtuple": + if classname == "collections.namedtuple": # default values for collections.namedtuple have been introduced in python 3.7 # it is probably not essential to deserialize the defaults if the object was serialized with # python >= 3.7 and deserialized with python < 3.7. @@ -365,6 +428,13 @@ def process_decoded(self, d): nt = namedtuple(d['typename'], d['fields'], # pylint: disable=E1123 defaults=d['fields_defaults']) # pylint: disable=E1123 return nt(*[self.process_decoded(item) for item in d['namedtuple_as_list']]) + if classname == "typing.NamedTuple": + NT = NamedTuple(d['typename'], [(field, _deserialize_type(field_type)) for field, field_type in zip(d['fields'], d['fields_types'])]) + NT.__doc__ = d['doc'] + # default values for typing.NamedTuple have been introduced in python 3.6 + if sys.version_info >= (3, 6): + NT._field_defaults = OrderedDict([(field, self.process_decoded(default)) for field, default in d['fields_defaults']]) + return NT(*[self.process_decoded(item) for item in d['NamedTuple_as_list']]) if classname == "OrderedDict": return OrderedDict([(key, self.process_decoded(val)) for key, val in d['ordereddict_as_list']]) diff --git a/tests/test_json.py b/tests/test_json.py index f635c0ef7..ac03bcd65 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -5,15 +5,18 @@ import numpy as np import json import datetime +import sys from bson.objectid import ObjectId from enum import Enum from collections import namedtuple from collections import OrderedDict +from typing import NamedTuple 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 +from monty.collections import is_NamedTuple test_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "test_files") @@ -228,6 +231,82 @@ def test_OrderedDict_serialization(self): afromdict = GoodMSONClass.from_dict(a.as_dict()) assert type(afromdict.b) is OrderedDict assert list(afromdict.b.keys()) == ['val1', 'val2', 'val3'] + assert afromdict.b == a.b + + def test_NamedTuple_serialization(self): + # "Old style" typing.NamedTuple definition + A = NamedTuple('MyNamedTuple', [('int1', int), ('str1', str), ('int2', int)]) + A_NT = A(1, 'a', 3) + a = GoodMSONClass(a=1, b=A_NT, c=1) + assert is_NamedTuple(a.b) + afromdict = GoodMSONClass.from_dict(a.as_dict()) + assert is_NamedTuple(afromdict.b) + assert afromdict.b.__class__.__name__ == 'MyNamedTuple' + assert afromdict.b._field_types == a.b._field_types + assert afromdict.b == a.b + + # "New style" typing.NamedTuple definition with class with type annotations (for python >= 3.6) + # This will not work for python < 3.6, leading to a SyntaxError hence the exec here. + try: + exec('class SomeNT(NamedTuple):\n\ + aaa: int\n\ + bbb: float\n\ + ccc: str\n\ + global SomeNT') # Make the SomeNT class available globally + + myNT = SomeNT(1, 2.1, 'a') + + a = GoodMSONClass(a=1, b=myNT, c=1) + assert is_NamedTuple(a.b) + afromdict = GoodMSONClass.from_dict(a.as_dict()) + assert is_NamedTuple(afromdict.b) + assert afromdict.b.__class__.__name__ == 'SomeNT' + assert afromdict.b._field_types == a.b._field_types + assert afromdict.b == a.b + except SyntaxError: + # Make sure we get this SyntaxError only in the case of python < 3.6. + assert sys.version_info < (3, 6) + + try: + exec('class SomeNT(NamedTuple):\n\ + """My NamedTuple docstring."""\n\ + aaa: int\n\ + bbb: float = 1.0\n\ + ccc: str = \'hello\'\n\ + global SomeNT') # Make the SomeNT class available globally + + myNT = SomeNT(1, 2.1, 'a') + + a = GoodMSONClass(a=1, b=myNT, c=1) + assert is_NamedTuple(a.b) + afromdict = GoodMSONClass.from_dict(a.as_dict()) + assert is_NamedTuple(afromdict.b) + assert afromdict.b.__class__.__name__ == 'SomeNT' + assert afromdict.b._field_types == a.b._field_types + assert afromdict.b == a.b + assert afromdict.b.__doc__ == a.b.__doc__ + except SyntaxError: + # Make sure we get this SyntaxError only in the case of python < 3.6. + assert sys.version_info < (3, 6) + + # Testing "normal classes" types in NamedTuple serialization + A = NamedTuple('MyNamedTuple', [('int1', int), + ('gmsoncls', GoodMSONClass), + ('gnestedmsoncls', GoodNestedMSONClass)]) + A_NT = A(1, + GoodMSONClass(a=1, b=2, c=3), + GoodNestedMSONClass(a_list=[3, 4], + b_dict={'a': 2}, + c_list_dict_list=[{'ab': ['a', 'b'], + '34': [3, 4]}])) + a = GoodMSONClass(a=1, b=A_NT, c=1) + afromdict = GoodMSONClass.from_dict(a.as_dict()) + assert afromdict.b._field_types == a.b._field_types + assert a.a == afromdict.a + assert a.b.gmsoncls == afromdict.b.gmsoncls + assert a.b.gnestedmsoncls.a_list == afromdict.b.gnestedmsoncls.a_list + assert a.b.gnestedmsoncls.b_dict == afromdict.b.gnestedmsoncls.b_dict + assert a.b.gnestedmsoncls._c_list_dict_list == afromdict.b.gnestedmsoncls._c_list_dict_list class JsonTest(unittest.TestCase): @@ -355,6 +434,30 @@ def test_complex_enc_dec(self): od = OrderedDict([('val1', 1), ('val2', GoodMSONClass(a=a, b=nt2(1, 2, 3), c=1))]) od['val3'] = '3' + NT = NamedTuple('MyTypingNamedTuple', [('A1', int), ('A2', float), ('B1', str)]) + a_NT = NT(2, 3.1, 'hello') + a_NT_jsonstr = json.dumps(a_NT, cls=MontyEncoder) + a_NT_from_jsonstr = json.loads(a_NT_jsonstr, cls=MontyDecoder) + assert is_NamedTuple(a_NT_from_jsonstr) is True + + try: + exec('class NT_def(NamedTuple):\n\ + """My NamedTuple with defaults."""\n\ + aaa: int\n\ + bbb: float = 1.0\n\ + ccc: str = \'hello\'\n\ + global NT_def') # Make the NT_def class available globally + + myNT = NT_def(1, 2.1) + myNT_jsonstr = json.dumps(myNT, cls=MontyEncoder) + myNT_from_jsonstr = json.loads(myNT_jsonstr, cls=MontyDecoder) + assert is_NamedTuple(myNT_from_jsonstr) + assert myNT_from_jsonstr.__doc__ == 'My NamedTuple with defaults.' + assert myNT_from_jsonstr.ccc == 'hello' + except SyntaxError: + # Make sure we get this SyntaxError only in the case of python < 3.6. + assert sys.version_info < (3, 6) + 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) From d982c3b212ee9822638a734abb063b6a7473654f Mon Sep 17 00:00:00 2001 From: davidwaroquiers Date: Fri, 17 Jan 2020 12:08:44 +0100 Subject: [PATCH 14/15] Added serialization of sets. --- monty/json.py | 6 ++++++ tests/test_json.py | 13 +++++++++++++ 2 files changed, 19 insertions(+) diff --git a/monty/json.py b/monty/json.py index 82fe40540..d015e5517 100644 --- a/monty/json.py +++ b/monty/json.py @@ -148,6 +148,10 @@ def _recursive_as_dict(obj): return d # The order of the ifs matter here as namedtuples and NamedTuples are instances (subclasses) of tuples, # same for OrderedDict which is an instance (subclass) of dict. + if isinstance(obj, set): + return {"set_as_list": [_recursive_as_dict(it) for it in obj], + "@module": "@builtins", + "@class": "set"} if isinstance(obj, tuple): return {"tuple_as_list": [_recursive_as_dict(it) for it in obj], "@module": "@builtins", @@ -418,6 +422,8 @@ def process_decoded(self, d): if modname == "@builtins": if classname == "tuple": return tuple([self.process_decoded(item) for item in d['tuple_as_list']]) + if classname == "set": + return set([self.process_decoded(item) for item in d['set_as_list']]) if classname == "collections.namedtuple": # default values for collections.namedtuple have been introduced in python 3.7 # it is probably not essential to deserialize the defaults if the object was serialized with diff --git a/tests/test_json.py b/tests/test_json.py index ac03bcd65..c541b176b 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -210,6 +210,11 @@ def test_tuple_serialization(self): assert type(afromdict.b[1][1]) is tuple assert afromdict.b[1][1] == (1, 2, 3, 4) + def test_set_serialization(self): + a = GoodMSONClass(a={1, 2, 3}, b={'q', 'r'}, c=[14]) + afromdict = GoodMSONClass.from_dict(a.as_dict()) + assert type(afromdict.a) is set + def test_namedtuple_serialization(self): a = namedtuple('A', ['x', 'y', 'zzz']) b = GoodMSONClass(a=1, b=a(1, 2, 3), c=1) @@ -479,6 +484,14 @@ def test_complex_enc_dec(self): assert obj_from_jsonstr.y['val2'].b.ef == 3 assert obj_from_jsonstr.y['val3'] == '3' + def test_set_json(self): + myset = {1, 2, 3} + myset_jsonstr = json.dumps(myset, cls=MontyEncoder) + myset_from_jsonstr = json.loads(myset_jsonstr, cls=MontyDecoder) + assert type(myset_from_jsonstr) is set + assert myset_from_jsonstr == myset + + if __name__ == "__main__": unittest.main() From 04e0513e6f0904189738c72ddb919dfaec82bb1f Mon Sep 17 00:00:00 2001 From: davidwaroquiers Date: Fri, 17 Jan 2020 12:25:31 +0100 Subject: [PATCH 15/15] Fixed pycodestyle, pylint, mypy, ... --- monty/json.py | 20 +++++++++++++------- tests/test_json.py | 1 - 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/monty/json.py b/monty/json.py index d015e5517..58e0b4b04 100644 --- a/monty/json.py +++ b/monty/json.py @@ -68,6 +68,7 @@ def _load_redirect(redirect_file): return dict(redirect_dict) + # (Private) helper methods and variables for the serialization of # types for typing.NamedTuple's. _typ2name = {typ: typ.__name__ for typ in (bool, int, float, complex, @@ -88,7 +89,7 @@ def _serialize_type(typ): "@class": "@types", "type": _typ2name[typ]} # None/NoneType is a special case - if typ is type(None) or typ is None: + if typ is type(None) or typ is None: # noqa - disable pycodestyle check here return {"@module": "@builtins", "@class": "@types", "type": "NoneType"} @@ -142,7 +143,8 @@ def _recursive_as_dict(obj): "@class": "typing.NamedTuple"} if sys.version_info >= (3, 6): # default values for typing.NamedTuple's were introduced in python 3.6. try: - d["fields_defaults"] = [(field, _recursive_as_dict(field_default)) for field, field_default in obj._field_defaults.items()] + d["fields_defaults"] = [(field, _recursive_as_dict(field_default)) + for field, field_default in obj._field_defaults.items()] except AttributeError: d["fields_defaults"] = [] return d @@ -423,7 +425,7 @@ def process_decoded(self, d): if classname == "tuple": return tuple([self.process_decoded(item) for item in d['tuple_as_list']]) if classname == "set": - return set([self.process_decoded(item) for item in d['set_as_list']]) + return {self.process_decoded(item) for item in d['set_as_list']} if classname == "collections.namedtuple": # default values for collections.namedtuple have been introduced in python 3.7 # it is probably not essential to deserialize the defaults if the object was serialized with @@ -435,14 +437,18 @@ def process_decoded(self, d): defaults=d['fields_defaults']) # pylint: disable=E1123 return nt(*[self.process_decoded(item) for item in d['namedtuple_as_list']]) if classname == "typing.NamedTuple": - NT = NamedTuple(d['typename'], [(field, _deserialize_type(field_type)) for field, field_type in zip(d['fields'], d['fields_types'])]) + NT = NamedTuple(d['typename'], [(field, _deserialize_type(field_type)) + for field, field_type in zip(d['fields'], d['fields_types'])]) NT.__doc__ = d['doc'] # default values for typing.NamedTuple have been introduced in python 3.6 if sys.version_info >= (3, 6): - NT._field_defaults = OrderedDict([(field, self.process_decoded(default)) for field, default in d['fields_defaults']]) - return NT(*[self.process_decoded(item) for item in d['NamedTuple_as_list']]) + NT._field_defaults = OrderedDict([(field, self.process_decoded(default)) + for field, default in d['fields_defaults']]) + return NT(*[self.process_decoded(item) # pylint: disable=E1102 + for item in d['NamedTuple_as_list']]) # pylint: disable=E1102 if classname == "OrderedDict": - return OrderedDict([(key, self.process_decoded(val)) for key, val in d['ordereddict_as_list']]) + return OrderedDict([(key, self.process_decoded(val)) + for key, val in d['ordereddict_as_list']]) mod = __import__(modname, globals(), locals(), [classname], 0) if hasattr(mod, classname): diff --git a/tests/test_json.py b/tests/test_json.py index c541b176b..c94719f16 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -492,6 +492,5 @@ def test_set_json(self): assert myset_from_jsonstr == myset - if __name__ == "__main__": unittest.main()