Skip to content

Commit

Permalink
Fix issue #10
Browse files Browse the repository at this point in the history
A bit of cleanup now that we've moved to the PBR system as well
  • Loading branch information
hsolbrig committed Apr 23, 2021
1 parent 41ee914 commit cbf959c
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 41 deletions.
3 changes: 3 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
CHANGES
=======

* Ignore Pipfile.lock
* Fairly extensive refactoring

v2.0.0
------

Expand Down
2 changes: 2 additions & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ hbreader = "*"

[dev-packages]
yadict-compare = "*"
requests = "*"
pyjsg = "*"
5 changes: 1 addition & 4 deletions jsonasobj/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
from jsonasobj.jsonobj import JsonObj, as_dict, as_list, as_json, as_json_obj, get, items, loads, load, setdefault
from jsonasobj.extendednamespace import ExtendedNamespace

__license__ = 'CC0 1.0 Universal (CC0 1.0) Public Domain Dedication'
__version__ = '1.4.0'

__all__ = ['JsonObj', 'extendednamespace', 'as_dict', 'as_list', 'as_json', 'as_json_obj', 'get', 'items',
__all__ = ['JsonObj', 'ExtendedNamespace', 'as_dict', 'as_list', 'as_json', 'as_json_obj', 'get', 'items',
'load', 'loads', 'setdefault']
51 changes: 16 additions & 35 deletions jsonasobj/jsonobj.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import json
from typing import Union, List, Dict, Tuple, Optional, Callable, Any
from typing import Union, List, Dict, Tuple, Optional, Callable, Any, Iterator
from hbreader import hbread

from .extendednamespace import ExtendedNamespace
Expand All @@ -10,6 +10,9 @@
# Types in the pure JSON representation
JsonTypes = Union[Dict[str, "JsonTypes"], List["JsonTypes"], str, bool, int, float, None]

# Control variables -- note that subclasses can add to this list
hide = ['_if_missing', '_root']


class JsonObj(ExtendedNamespace):
""" A namespace/dictionary representation of a JSON object. Any name in a JSON object that is a valid python
Expand All @@ -29,7 +32,7 @@ def __init__(self, list_or_dict: Optional[Union[List, Dict]] = None, *,
:param kwargs: A dictionary as an alternative constructor.
"""
if _if_missing and _if_missing != self._if_missing:
self._set_hidden('_if_missing', _if_missing)
self._if_missing = _if_missing
if list_or_dict is not None:
if len(kwargs):
raise TypeError("Constructor can't have both a single item and a dict")
Expand All @@ -50,31 +53,11 @@ def _if_missing(obj: "JsonObj", item: str) -> Tuple[bool, Any]:

def _init_from_dict(self, d: Union[dict, "JsonObj"]) -> None:
""" Construct a JsonObj from a dictionary or another JsonObj """
if isinstance(d, dict):
if self._has_underbars(d):
raise ValueError(f"{self._first_underbar(d)}: underscore not allowed")
else:
if not isinstance(d, dict):
d = d._as_dict
ExtendedNamespace.__init__(self, _if_missing=self._if_missing, **{k: JsonObj(**v) if isinstance(v, dict) else v
for k, v in d.items()})

@staticmethod
def _has_underbars(d: dict) -> bool:
""" Return true if anything in dictionary d starts with an underbar """
return any(k.startswith('_') for k in d.keys())

@staticmethod
def _first_underbar(d: dict) -> Optional[str]:
""" Return the first underbar in d, if any """
for k in d.keys():
if k.startswith('_'):
return k
return None

def _set_hidden(self, key, value):
""" Add a hidden value w/ a leading underbar """
super().__setattr__(key, value)

# ===================================================
# JSON Serializer method
# ===================================================
Expand Down Expand Up @@ -103,11 +86,15 @@ def _setdefault(self, k: str, value: Union[Dict, JsonTypes]) -> JsonObjTypes:

def _keys(self) -> List[str]:
""" Return all non-hidden keys """
return [k for k in self.__dict__.keys() if not k.startswith('_')]
for k in self.__dict__.keys():
if k not in hide:
yield k

def _items(self) -> List[Tuple[str, JsonObjTypes]]:
""" Return all non-hidden items """
return [(k, v) for k, v in self.__dict__.items() if not k.startswith('_')]
for k, v in self.__dict__.items():
if k not in hide:
yield k, v

# ===================================================
# Various converters -- use exposed methods in place of underscores
Expand All @@ -134,15 +121,8 @@ def __getattr__(self, item):
return super().__getattribute__(item)

def __setattr__(self, key, value):
if key.startswith('_') and key not in ('_if_missing', '_root'):
raise ValueError(f"{key}: underscore not allowed")
super().__setattr__(key, JsonObj(value) if isinstance(value, dict) else value)

def __setitem__(self, key, value):
if key.startswith('_'):
raise ValueError(f"{key}: underscore not allowed")
super().__setitem__(key, value)

@property
def _as_json(self) -> str:
""" Convert a JsonObj into straight json text
Expand Down Expand Up @@ -234,7 +214,8 @@ def as_json(obj: Union[JsonObj, List], indent: Optional[str] = ' ',
"""
if isinstance(obj, JsonObj) and '_root' in obj:
obj = obj._root
return json.dumps(obj, default=lambda o: JsonObj._default(o, filtr) if filtr else JsonObj._default(o),
return obj._as_json_dumps(indent, filtr=filtr, **kwargs) if isinstance(obj, JsonObj) else \
json.dumps(obj, default=lambda o: JsonObj._default(o, filtr) if filtr else JsonObj._default(o),
indent=indent, **kwargs)


Expand All @@ -257,12 +238,12 @@ def setdefault(obj: JsonObj, k: str, value: Union[Dict, JsonTypes]) -> JsonObjTy
return obj._setdefault(k, value)


def keys(obj: JsonObj) -> List[str]:
def keys(obj: JsonObj) -> Iterator[str]:
""" same as dict keys() without polluting the namespace """
return obj._keys()


def items(obj: JsonObj) -> List[Tuple[str, JsonObjTypes]]:
def items(obj: JsonObj) -> Iterator[Tuple[str, JsonObjTypes]]:
""" Same as dict items() except that the values are JsonObjs instead of vanilla dictionaries
:return:
"""
Expand Down
57 changes: 57 additions & 0 deletions tests/test_issue10.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import unittest

from pyjsg.jsglib import JSGObjectMap, JSGString, JSGPattern, ArrayFactory, JSGContext, Integer
from pyjsg.validate_json import JSGPython

from json import loads as jsonloads
from jsonasobj import as_json

# Taken from
class HEX(JSGString):
pattern = JSGPattern(r'[0-9]|[A-F]|[a-f]')


_CONTEXT = JSGContext()


class BuiltinSyntaxTestCase(unittest.TestCase):
""" This tests a somewhat mysterious issue focused around the as_json function """
# From: https://github.com/hsolbrig/pyjsg/blob/master/tests/test_issues/test_builtins_issue.py
def test_1(self):
x = JSGPython('''doc {
v1: @string,
v2: @number,
v3: @int,
v4: @bool,
v5: @null,
v6: @array,
v7: @object
}
obj {a: . , }''')

rslt = x.conforms('''
{ "v1": "This is text!",
"v2": -117.432e+2,
"v3": -100173,
"v4": false,
"v5": null,
"v6": [12, "text", null],
"v7": {"q": "life", "a": 42}
}''')
self.assertTrue(rslt.success)

def test_basic_map(self):
""" Test sometimes failing pyjsg use case for jsonasobj """
# From: https://github.com/hsolbrig/pyjsg/blob/master/tests/test_jsglib/test_objectmap.py#L15
class IntObjectMap(JSGObjectMap):
_name_filter = HEX
_value_type = ArrayFactory('', _CONTEXT, Integer, 0, None)

def __init__(self,
**_kwargs):
super().__init__(_CONTEXT, **_kwargs)

x = IntObjectMap()
x.E = [1,2,3]
self.assertTrue(x._is_valid())
self.assertEqual(as_json(x), as_json(jsonloads('{"E":[1,2,3]}')))
25 changes: 25 additions & 0 deletions tests/test_issue9.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import unittest
from copy import deepcopy
from dataclasses import dataclass

from jsonasobj import JsonObj


class YAMLRoot(JsonObj):
pass


@dataclass
class Test(YAMLRoot):
v: int


class MissingConstructorTestCase(unittest.TestCase):
def test_missing_constructor(self):
""" Test for reasonable behavior when the constructor wasn't invoked """
z = Test(1)
deepcopy(z) # Invokes test for local __deepcopy__ in z


if __name__ == '__main__':
unittest.main()
5 changes: 3 additions & 2 deletions tests/test_refactoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
class Root(JsonObj):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._set_hidden('_initialized', True)
self._initialized = True
self.__post_init__()

def __post_init__(self):
# leave a mark saying we've been here
self._set_hidden('_post_initialized', True)
self._post_initialized = True


class A(Root):
Expand All @@ -35,6 +35,7 @@ def test_list_or_dict(self):
o = JsonObj(JsonObj(a=1, b={"@x": 17}))
self.assertEqual("JsonObj(a=1, b=JsonObj(**{'@x': 17}))", str(o))

@unittest.skip("Underscore testing turns out to be far to invasive -- checks have been removed")
def test_no_underscores(self):
""" Make sure that underscore values cannot be constructed or added afterwards """
with self.assertRaises(ValueError) as e:
Expand Down

0 comments on commit cbf959c

Please sign in to comment.