Skip to content

Commit c6dfd95

Browse files
committed
Fix issue #11, issue #12
Partial fix for issue #13 Update: Issue 13 fixed as best we're going to make it
1 parent e817f88 commit c6dfd95

File tree

4 files changed

+105
-20
lines changed

4 files changed

+105
-20
lines changed

jsonasobj/_jsonobj.py

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@ class JsonObj(ExtendedNamespace):
1919
identifier is represented as a first-class member of the objects. JSON identifiers that begin with "_" are
2020
disallowed in this implementation.
2121
"""
22-
def __new__(cls, list_or_dict: Optional[Union[List, Dict]] = None, *,
23-
_if_missing: Callable[["JsonObj", str], Tuple[bool, Any]] = None, **kwargs):
22+
# Set this class variable to False if recursive construction is absolutely necessare (see: test_issue13.py for
23+
# details
24+
_idempotent = True
25+
26+
def __new__(cls, *args, _if_missing: Callable[["JsonObj", str], Tuple[bool, Any]] = None, **kwargs):
2427
""" Construct a JsonObj from set of keyword/value pairs
2528
2629
: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, *,
3033
:param kwargs: A dictionary as an alternative constructor.
3134
"""
3235
# This makes JsonObj idempotent
33-
if isinstance(list_or_dict, JsonObj):
34-
if not kwargs and (not _if_missing or _if_missing == list_or_dict._if_missing):
35-
return list_or_dict
36-
else:
37-
obj = super(ExtendedNamespace, cls).__new__(cls)
38-
obj.__init__(as_json_obj(list_or_dict), _if_missing=_if_missing, **kwargs)
39-
else:
40-
obj = super(ExtendedNamespace, cls).__new__(cls)
36+
if cls._idempotent and args and isinstance(args[0], JsonObj):
37+
# If we're being called with a single argument
38+
if not kwargs and not args[1:] and\
39+
(not _if_missing or _if_missing == args[0]._if_missing) and cls == type(args[0]):
40+
return args[0]
41+
obj = super(ExtendedNamespace, cls).__new__(cls)
4142
return obj
4243

43-
def __init__(self, list_or_dict: Optional[Union[List, Dict]] = None, *,
44-
_if_missing: Callable[["JsonObj", str], Tuple[bool, Any]] = None, **kwargs):
44+
def __init__(self, *args, _if_missing: Callable[["JsonObj", str], Tuple[bool, Any]] = None, **kwargs):
4545
""" Construct a JsonObj from set of keyword/value pairs
4646
4747
: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, *,
5050
processing proceeds.
5151
:param kwargs: A dictionary as an alternative constructor.
5252
"""
53-
if isinstance(list_or_dict, JsonObj):
53+
if args and isinstance(args[0], JsonObj) and not kwargs and not args[1:] and type(self)._idempotent and \
54+
(not _if_missing or _if_missing == args[0]._if_missing) and type(self) == type(args[0]):
5455
return
5556

5657
if _if_missing and _if_missing != self._if_missing:
5758
self._if_missing = _if_missing
58-
if list_or_dict is not None:
59+
if args:
5960
if kwargs:
6061
raise TypeError("Constructor can't have both a single item and a dict")
61-
if isinstance(list_or_dict, JsonObj):
62+
if isinstance(args[0], JsonObj):
6263
pass
63-
elif isinstance(list_or_dict, dict):
64-
self._init_from_dict(list_or_dict)
65-
elif isinstance(list_or_dict, list):
64+
elif isinstance(args[0], dict):
65+
self._init_from_dict(args[0])
66+
elif isinstance(args[0], list):
6667
ExtendedNamespace.__init__(self,
6768
_root=[JsonObj(e) if isinstance(e, (dict, list)) else
68-
e for e in list_or_dict])
69+
e for e in args[0]])
6970
else:
7071
raise TypeError("JSON Object can only be a list or dictionary")
7172
else:

tests/test_issue11.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def test_idempotent(self):
7777
""" JsonObj should be idempotent """
7878
o = JsonObj({"a": 42})
7979
self.assertEqual(id(o), id(JsonObj(o)))
80-
self.assertNotEqual(id(o), id(JsonObj(o, _if_missing=1)))
80+
self.assertNotEqual(id(o), id(JsonObj(o, _if_missing=lambda x: (True, None))))
8181

8282

8383
if __name__ == '__main__':

tests/test_issue12.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import json
2+
import unittest
3+
4+
from jsonasobj import JsonObj, as_json
5+
6+
7+
class ShapeAssociation(JsonObj):
8+
def __init__(self,
9+
nodeSelector, shapeLabel,
10+
status=None, reason=None,
11+
appinfo=None) -> None:
12+
13+
self.nodeSelector = nodeSelector
14+
self.shapeLabel = shapeLabel
15+
self.status = status if status is not None else "C",
16+
self.reason = reason
17+
self.appinfo = appinfo
18+
super().__init__()
19+
20+
21+
expected = {
22+
"nodeSelector": "http://example.org/people/42",
23+
"shapeLabel": "http://example.org/model/Person",
24+
"status": [
25+
"C"
26+
],
27+
"reason": "cause",
28+
"appinfo": None
29+
}
30+
31+
32+
class PositionalTestCase(unittest.TestCase):
33+
def test_positional(self):
34+
""" jsonasobj has to support positional constructors """
35+
s = ShapeAssociation("http://example.org/people/42", 'http://example.org/model/Person', reason='cause')
36+
self.assertEqual(expected, json.loads(as_json(s)))
37+
38+
39+
if __name__ == '__main__':
40+
unittest.main()

tests/test_issue13.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import unittest
2+
from typing import Optional
3+
4+
from jsonasobj import JsonObj, as_json
5+
6+
7+
class MyObj(JsonObj):
8+
def __init__(self, a: JsonObj):
9+
super().__init__()
10+
self.a = a
11+
12+
13+
class RecursiveObject(JsonObj):
14+
def __init__(self, parent: Optional["RecursiveObject"] = None):
15+
super().__init__()
16+
self.parent = parent
17+
18+
class RecursiveObject2(JsonObj):
19+
_idempotent = False
20+
def __init__(self, parent: Optional["RecursiveObject"] = None):
21+
super().__init__()
22+
self.parent = parent
23+
24+
25+
class DangerousConstructor(unittest.TestCase):
26+
def test_unintended_recursion(self):
27+
o1 = JsonObj(x=1)
28+
o2 = MyObj(o1)
29+
self.assertNotEqual(id(o1), id(o2))
30+
31+
32+
def test_deliberate_recursion(self):
33+
grandfather = RecursiveObject()
34+
father = RecursiveObject(grandfather)
35+
me = RecursiveObject(father)
36+
self.assertEqual(id(me), id(me.parent))
37+
38+
grandfather = RecursiveObject2()
39+
father = RecursiveObject2(grandfather)
40+
me = RecursiveObject2(father)
41+
self.assertNotEqual(id(me), id(me.parent))
42+
43+
if __name__ == '__main__':
44+
unittest.main()

0 commit comments

Comments
 (0)