Skip to content

Commit

Permalink
Fix issue #11, issue #12
Browse files Browse the repository at this point in the history
Partial fix for issue #13
Update: Issue 13 fixed as best we're going to make it
  • Loading branch information
hsolbrig committed Apr 28, 2021
1 parent e817f88 commit c6dfd95
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 20 deletions.
39 changes: 20 additions & 19 deletions jsonasobj/_jsonobj.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion tests/test_issue11.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__':
Expand Down
40 changes: 40 additions & 0 deletions tests/test_issue12.py
Original file line number Diff line number Diff line change
@@ -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()
44 changes: 44 additions & 0 deletions tests/test_issue13.py
Original file line number Diff line number Diff line change
@@ -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()

0 comments on commit c6dfd95

Please sign in to comment.