From c6dfd95df1ccaa66ebb78153dc9a52c1828d5352 Mon Sep 17 00:00:00 2001 From: hsolbrig Date: Wed, 28 Apr 2021 14:24:44 -0500 Subject: [PATCH] Fix issue #11, issue #12 Partial fix for issue #13 Update: Issue 13 fixed as best we're going to make it --- jsonasobj/_jsonobj.py | 39 +++++++++++++++++++------------------- tests/test_issue11.py | 2 +- tests/test_issue12.py | 40 +++++++++++++++++++++++++++++++++++++++ tests/test_issue13.py | 44 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 105 insertions(+), 20 deletions(-) create mode 100644 tests/test_issue12.py create mode 100644 tests/test_issue13.py diff --git a/jsonasobj/_jsonobj.py b/jsonasobj/_jsonobj.py index dfe11d8..fbc9cb8 100644 --- a/jsonasobj/_jsonobj.py +++ b/jsonasobj/_jsonobj.py @@ -19,8 +19,11 @@ class JsonObj(ExtendedNamespace): identifier is represented as a first-class member of the objects. JSON identifiers that begin with "_" are disallowed in this implementation. """ - def __new__(cls, list_or_dict: Optional[Union[List, Dict]] = None, *, - _if_missing: Callable[["JsonObj", str], Tuple[bool, Any]] = None, **kwargs): + # Set this class variable to False if recursive construction is absolutely necessare (see: test_issue13.py for + # details + _idempotent = True + + def __new__(cls, *args, _if_missing: Callable[["JsonObj", str], Tuple[bool, Any]] = None, **kwargs): """ Construct a JsonObj from set of keyword/value pairs :param list_or_dict: A list or dictionary that can be used to construct the object @@ -30,18 +33,15 @@ def __new__(cls, list_or_dict: Optional[Union[List, Dict]] = None, *, :param kwargs: A dictionary as an alternative constructor. """ # This makes JsonObj idempotent - if isinstance(list_or_dict, JsonObj): - if not kwargs and (not _if_missing or _if_missing == list_or_dict._if_missing): - return list_or_dict - else: - obj = super(ExtendedNamespace, cls).__new__(cls) - obj.__init__(as_json_obj(list_or_dict), _if_missing=_if_missing, **kwargs) - else: - obj = super(ExtendedNamespace, cls).__new__(cls) + if cls._idempotent and args and isinstance(args[0], JsonObj): + # If we're being called with a single argument + if not kwargs and not args[1:] and\ + (not _if_missing or _if_missing == args[0]._if_missing) and cls == type(args[0]): + return args[0] + obj = super(ExtendedNamespace, cls).__new__(cls) return obj - def __init__(self, list_or_dict: Optional[Union[List, Dict]] = None, *, - _if_missing: Callable[["JsonObj", str], Tuple[bool, Any]] = None, **kwargs): + def __init__(self, *args, _if_missing: Callable[["JsonObj", str], Tuple[bool, Any]] = None, **kwargs): """ Construct a JsonObj from set of keyword/value pairs :param list_or_dict: A list or dictionary that can be used to construct the object @@ -50,22 +50,23 @@ def __init__(self, list_or_dict: Optional[Union[List, Dict]] = None, *, processing proceeds. :param kwargs: A dictionary as an alternative constructor. """ - if isinstance(list_or_dict, JsonObj): + if args and isinstance(args[0], JsonObj) and not kwargs and not args[1:] and type(self)._idempotent and \ + (not _if_missing or _if_missing == args[0]._if_missing) and type(self) == type(args[0]): return if _if_missing and _if_missing != self._if_missing: self._if_missing = _if_missing - if list_or_dict is not None: + if args: if kwargs: raise TypeError("Constructor can't have both a single item and a dict") - if isinstance(list_or_dict, JsonObj): + if isinstance(args[0], JsonObj): pass - elif isinstance(list_or_dict, dict): - self._init_from_dict(list_or_dict) - elif isinstance(list_or_dict, list): + elif isinstance(args[0], dict): + self._init_from_dict(args[0]) + elif isinstance(args[0], list): ExtendedNamespace.__init__(self, _root=[JsonObj(e) if isinstance(e, (dict, list)) else - e for e in list_or_dict]) + e for e in args[0]]) else: raise TypeError("JSON Object can only be a list or dictionary") else: diff --git a/tests/test_issue11.py b/tests/test_issue11.py index 02c2d1e..676ac93 100644 --- a/tests/test_issue11.py +++ b/tests/test_issue11.py @@ -77,7 +77,7 @@ def test_idempotent(self): """ JsonObj should be idempotent """ o = JsonObj({"a": 42}) self.assertEqual(id(o), id(JsonObj(o))) - self.assertNotEqual(id(o), id(JsonObj(o, _if_missing=1))) + self.assertNotEqual(id(o), id(JsonObj(o, _if_missing=lambda x: (True, None)))) if __name__ == '__main__': diff --git a/tests/test_issue12.py b/tests/test_issue12.py new file mode 100644 index 0000000..009c4d1 --- /dev/null +++ b/tests/test_issue12.py @@ -0,0 +1,40 @@ +import json +import unittest + +from jsonasobj import JsonObj, as_json + + +class ShapeAssociation(JsonObj): + def __init__(self, + nodeSelector, shapeLabel, + status=None, reason=None, + appinfo=None) -> None: + + self.nodeSelector = nodeSelector + self.shapeLabel = shapeLabel + self.status = status if status is not None else "C", + self.reason = reason + self.appinfo = appinfo + super().__init__() + + +expected = { + "nodeSelector": "http://example.org/people/42", + "shapeLabel": "http://example.org/model/Person", + "status": [ + "C" + ], + "reason": "cause", + "appinfo": None +} + + +class PositionalTestCase(unittest.TestCase): + def test_positional(self): + """ jsonasobj has to support positional constructors """ + s = ShapeAssociation("http://example.org/people/42", 'http://example.org/model/Person', reason='cause') + self.assertEqual(expected, json.loads(as_json(s))) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_issue13.py b/tests/test_issue13.py new file mode 100644 index 0000000..9469635 --- /dev/null +++ b/tests/test_issue13.py @@ -0,0 +1,44 @@ +import unittest +from typing import Optional + +from jsonasobj import JsonObj, as_json + + +class MyObj(JsonObj): + def __init__(self, a: JsonObj): + super().__init__() + self.a = a + + +class RecursiveObject(JsonObj): + def __init__(self, parent: Optional["RecursiveObject"] = None): + super().__init__() + self.parent = parent + +class RecursiveObject2(JsonObj): + _idempotent = False + def __init__(self, parent: Optional["RecursiveObject"] = None): + super().__init__() + self.parent = parent + + +class DangerousConstructor(unittest.TestCase): + def test_unintended_recursion(self): + o1 = JsonObj(x=1) + o2 = MyObj(o1) + self.assertNotEqual(id(o1), id(o2)) + + + def test_deliberate_recursion(self): + grandfather = RecursiveObject() + father = RecursiveObject(grandfather) + me = RecursiveObject(father) + self.assertEqual(id(me), id(me.parent)) + + grandfather = RecursiveObject2() + father = RecursiveObject2(grandfather) + me = RecursiveObject2(father) + self.assertNotEqual(id(me), id(me.parent)) + +if __name__ == '__main__': + unittest.main()