├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.rst ├── jsane ├── __init__.py ├── traversable.py └── wrapper.py ├── setup.cfg ├── setup.py ├── tests └── test_jsane.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .cache/ 3 | .eggs/ 4 | .tox/ 5 | .hypothesis/ 6 | build/ 7 | dist/ 8 | *.egg-info/ 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.4" 5 | - "3.5" 6 | - "3.6" 7 | - "pypy" 8 | install: # No dependencies. 9 | script: python setup.py test 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Maintainer guidelines 2 | ===================== 3 | 4 | Hello, and thank you for choosing to contribute to this repository. The short 5 | information below will help everyone to stop calling me a fascist: 6 | 7 | 1. Always test your changes and write tests for them. The tests run 8 | automatically and will tell you if there's a problem on any of the 9 | supported platforms/versions. 10 | 2. When committing, categorize what you did by following [Angular's Git Commit 11 | Guidelines](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#commit). 12 | Basically, prepend one of feat/fix/docs/style/refactor/perf/text/chore to the 13 | commit subject, like so: `fix: Fix foo in bar.`. Capitalizing the first 14 | letter and trailing periods are encouraged. 15 | 3. Don't be an asshole. 16 | 17 | That's pretty much it, thanks for your contribution! 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Stavros Korokithakis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | JSane 2 | ===== 3 | 4 | .. image:: https://travis-ci.org/skorokithakis/jsane.svg?branch=master 5 | :target: https://travis-ci.org/skorokithakis/jsane 6 | 7 | JSane is a JSON "parser" that makes attribute accesses easier. 8 | 9 | Three-line intro 10 | ---------------- 11 | 12 | :: 13 | 14 | >>> import jsane 15 | >>> j = jsane.loads('{"foo": {"bar": {"baz": ["well", "hello", "there"]}}}') 16 | >>> j.foo.bar.baz[1].r() 17 | 'hello' 18 | 19 | 20 | Motivation 21 | ---------- 22 | 23 | Picture the scene. You're a jet-setting developer who is obsessed with going to 24 | the gym. One day, a world-class jewel thief kidnaps you and asks you to hack 25 | into the super-secure bank server in thirty seconds, while an ultramodel is 26 | performing oral sex on you. You hurriedly trace the protocol on the wire, only 27 | to discover, to your dismay, that it uses JSON. Nested JSON, with levels and 28 | levels of keys. 29 | 30 | It's hopeless! You'll never type all those brackets and quotation marks in time! 31 | Suddenly, a flash of a memory races through your mind, like some cliche from 32 | a badly-written README. You launch the shell and type two words:: 33 | 34 | import jsane 35 | 36 | `The day is saved`_. 37 | 38 | 39 | Motivation (non-Hollywood version) 40 | ---------------------------------- 41 | 42 | Are you frustrated with having to traverse your nested JSON key by key? 43 | 44 | :: 45 | 46 | root = my_json.get("root") 47 | if root is None: 48 | return None 49 | 50 | key1 = root.get("key1") 51 | if key1 is None: 52 | return None 53 | 54 | key2 = key1.get("key2") 55 | if key2 is None: 56 | return None 57 | 58 | 59 | 60 | Is your code ruined by pesky all-catching ``except`` blocks? 61 | 62 | :: 63 | 64 | try: 65 | my_json["root"]["key1"]["key2"]["key3"] 66 | except: 67 | return None 68 | 69 | Are you tired of typing all the braces and quotes all the time? 70 | 71 | :: 72 | 73 | my_json["root"]["key1"[""]][]"]']'"}}""] 74 | 75 | Now there's JSane! 76 | 77 | 78 | Motivation (non-infomercial version) 79 | ------------------------------------ 80 | 81 | Okay seriously, ``this["thing"]["is"]["no"]["fun"]``. JSane lets you 82 | ``traverse.json.like.this.r()``. That's it. 83 | 84 | 85 | Usage 86 | ----- 87 | 88 | Using JSane is simple, at least. It's pretty much a copy of the builtin 89 | ``json`` module. 90 | 91 | First of all, install it with ``pip`` or ``easy_install``:: 92 | 93 | pip install jsane 94 | 95 | Here's an example of its usage:: 96 | 97 | >>> import jsane 98 | 99 | >>> j = jsane.loads('{"some": {"json": [1, 2, 3]}}') 100 | >>> j.some.json[2].r() 101 | 3 102 | 103 | You can also load an existing object:: 104 | >>> j = jsane.from_object({"hi": "there"}) 105 | >>> j.hi 106 | 107 | 108 | If the object contains any data types that aren't valid in JSON (like 109 | functions), it still should work, but you're on your own. 110 | 111 | Due to Python being a sensible language, there's a limit to the amount of 112 | crap you can pull with it, so JSane actually returns a ``Traversable`` object on 113 | accesses:: 114 | 115 | >>> j = jsane.loads('{"foo": {"bar": {"baz": "yes!"}}}') 116 | >>> type(j.foo) 117 | 118 | 119 | If you want your real object back at the end of the wild attribute ride, call 120 | ``.r()``:: 121 | 122 | >>> j.foo.bar.r() 123 | {'baz': 'yes!'} 124 | 125 | Likewise, if you prefer, you can call the object as a function with no 126 | arguments:: 127 | 128 | >>> j.foo.bar() 129 | {'baz': 'yes!'} 130 | 131 | If an attribute, item or index along the way does not exist, you'll get an 132 | exception. You can get rid of that by specifying a default:: 133 | 134 | >>> import jsane 135 | 136 | >>> j = jsane.loads('{"some": "json"}') 137 | >>> j.this.path.doesnt.exist.r() 138 | Traceback (most recent call last): 139 | ... 140 | jsane.traversable.JSaneException: "Key does not exist: 'this'" 141 | >>> j.haha_sucka_this_doesnt_exist_either.r(default="💩") 142 | '💩' 143 | 144 | "But how do I access a key called ``__call__``, ``r``, or ``_obj`` where you 145 | store the wrapped object?!", I hear you ask. Worry not, object keys are still 146 | accessible with indexing:: 147 | 148 | >>> j = jsane.loads('{"r": {"__call__": {"_obj": 5}}}') 149 | >>> j["r"]["__call__"]["_obj"].r() 150 | 5 151 | 152 | For convenience, you can access values specifically as numbers:: 153 | 154 | >>> import jsane 155 | 156 | >>> j = jsane.loads(''' 157 | ... { 158 | ... "numbers": { 159 | ... "one": 1, 160 | ... "two": "2" 161 | ... }, 162 | ... "letters": "XYZ" 163 | ... } 164 | ... ''') 165 | >>> +j.numbers.one 166 | 1 167 | >>> +j.letter, +j.numbers.two # Things that aren't numbers are nan 168 | (nan, nan) 169 | >>> +j.numbers 170 | nan 171 | >>> +j.what # Things that don't exist are also nan. 172 | nan 173 | 174 | (NaN is not representable in JSON, so this should be enough for most use cases. 175 | Testing for NaN is also easy with the standard library ``math.isnan()`` 176 | function.) 177 | 178 | Likewise for strings, calling ``str()`` on a Traversable object is a simple 179 | shortcut:: 180 | 181 | >>> str(j.letters) 182 | 'XYZ' 183 | >>> str(j.numbers) 184 | "{'one': 1, 'two': '2'}" 185 | >>> str(j.numbers.one) 186 | '1' 187 | 188 | In the same fashion, ``int()`` and ``float()`` are also shortcuts, but unlike 189 | ``str()`` (and consistent with their behavior elsewhere in Python) they do not 190 | infallibly return objects of their respective type (that is, they may raise a 191 | ValueError instead). 192 | 193 | That's about it. No guarantees of stability before version 1, as always. Semver 194 | giveth, and semver taketh away. 195 | 196 | Help needed/welcome/etc, mostly with designing the API. Also, if you find this 197 | library useless, let me know. 198 | 199 | 200 | License 201 | ------- 202 | 203 | BSD. Or MIT. Whatever's in the LICENSE file. I forget. It's permissive, though, 204 | so relax. 205 | 206 | 207 | Self-promotion 208 | -------------- 209 | 210 | It's me, Stavros. 211 | 212 | 213 | FAQ 214 | --- 215 | 216 | * Do you find it ironic that the README for JSane is insane? 217 | 218 | No. 219 | 220 | * Is this library awesome? 221 | 222 | Yes. 223 | 224 | * All my JSON data uses 'r' and '_obj' as keys! 225 | 226 | Come on, man. :( 227 | 228 | .. _The day is saved: https://www.youtube.com/watch?v=mWqGJ613M5Y 229 | -------------------------------------------------------------------------------- /jsane/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | from .traversable import JSaneException 3 | from .wrapper import load, loads, dump, dumps, from_dict, from_object, new 4 | 5 | __version__ = '0.1.2' 6 | -------------------------------------------------------------------------------- /jsane/traversable.py: -------------------------------------------------------------------------------- 1 | from numbers import Number 2 | 3 | 4 | class JSaneException(KeyError): 5 | pass 6 | 7 | 8 | class Empty(object): 9 | def __init__(self, key_name=""): 10 | self._key_name = key_name 11 | 12 | def __getattr__(self, _): 13 | return self # Empty object returned should reflect the 1st failed key 14 | __getitem__ = __getattr__ 15 | 16 | def __setattr__(self, key, value): 17 | if key == '_key_name': 18 | return object.__setattr__(self, '_key_name', value) 19 | raise JSaneException("There is nothing here!") 20 | 21 | def __delattr__(self, key): 22 | raise JSaneException( 23 | "Key does not exist: {}".format(repr(self._key_name)) 24 | ) 25 | __delitem__ = __delattr__ 26 | 27 | def __eq__(self, other): 28 | return False 29 | 30 | def __repr__(self): 31 | return "".format( 32 | repr(self._key_name) 33 | ) 34 | 35 | def r(self, **kwargs): 36 | """ 37 | Resolve the object. 38 | 39 | This returns default (if present) or fails on an Empty. 40 | """ 41 | # by using kwargs we ensure that usage of positional arguments, as if 42 | # this object were another kind of function, will fail-fast and raise 43 | # a TypeError 44 | if 'default' in kwargs: 45 | default = kwargs.pop('default') 46 | if kwargs: 47 | raise TypeError( 48 | "Unexpected argument: {}".format(repr(next(iter(kwargs)))) 49 | ) 50 | return default 51 | else: 52 | raise JSaneException( 53 | "Key does not exist: {}".format(repr(self._key_name)) 54 | ) 55 | __call__ = r 56 | 57 | def __pos__(self): 58 | return float('nan') 59 | 60 | def __str__(self): 61 | raise JSaneException( 62 | "Key does not exist: {}".format(repr(self._key_name)) 63 | ) 64 | __int__ = __float__ = __str__ 65 | 66 | def __contains__(self, _): 67 | return False 68 | 69 | def __dir__(self): 70 | raise JSaneException( 71 | "Key does not exist: {}".format(repr(self._key_name)) 72 | ) 73 | 74 | 75 | class Traversable(object): 76 | def __init__(self, obj): 77 | self._obj = obj 78 | 79 | def __getattr__(self, key): 80 | try: 81 | return Traversable(self._obj[key]) 82 | except (KeyError, AttributeError, IndexError, TypeError): 83 | return Empty(key) 84 | __getitem__ = __getattr__ 85 | 86 | def __setattr__(self, key, value): 87 | if key == '_obj': 88 | object.__setattr__(self, '_obj', value) 89 | return 90 | if isinstance(value, Traversable): 91 | value = value._obj 92 | # may cause TypeError; allow this to fall through 93 | self._obj[key] = value 94 | 95 | def __setitem__(self, key, value): 96 | if isinstance(value, Traversable): 97 | value = value._obj 98 | # may cause TypeError; allow this to fall through 99 | self._obj[key] = value 100 | 101 | def __delattr__(self, key): 102 | try: 103 | del self._obj[key] 104 | except KeyError: 105 | raise JSaneException("Key does not exist: {}".format(repr(key))) 106 | __delitem__ = __delattr__ 107 | 108 | def __eq__(self, other): 109 | """ 110 | Compare to other objects very reluctantly. 111 | 112 | Only succeed when both objects are Traversable objects, and 113 | fail otherwise (which defaults to returning False) to ensure 114 | that the developer doesn't forget too easily what kind of 115 | object they're actually using. 116 | """ 117 | if isinstance(other, Traversable): 118 | return self._obj == other._obj 119 | else: 120 | return NotImplemented 121 | 122 | def __repr__(self): 123 | return "".format(repr(self._obj)) 124 | 125 | def r(self, **kwargs): 126 | """ 127 | Resolve the object. 128 | 129 | This will always succeed, since, if a lookup fails, an Empty 130 | instance will be returned farther upstream. 131 | """ 132 | # by using kwargs we ensure that usage of positional arguments, as if 133 | # this object were another kind of function, will fail-fast and raise 134 | # a TypeError 135 | kwargs.pop('default', None) 136 | if kwargs: 137 | raise TypeError( 138 | "Unexpected argument: {}".format(repr(next(iter(kwargs)))) 139 | ) 140 | return self._obj 141 | __call__ = r 142 | 143 | def __pos__(self): 144 | """ 145 | Resolve the object as a number ONLY if it is a number. 146 | 147 | Missing keys or non-numeric type will yield nan. Numeric 148 | strings are not considered to be numbers under this 149 | determination. 150 | """ 151 | if isinstance(self._obj, Number): 152 | return self._obj 153 | else: 154 | return float('nan') 155 | 156 | def __str__(self): 157 | """ 158 | Resolve the object and turn it into a string if it is not already. 159 | 160 | Essentially, str(traversable) is syntactic sugar for 161 | str(traversable()). 162 | """ 163 | return str(self._obj) 164 | 165 | def __int__(self): 166 | """ 167 | Resolve the object as an int if possible. 168 | 169 | Essentially, int(traversable) is syntactic sugar for 170 | int(traversable()). 171 | """ 172 | return int(self._obj) 173 | 174 | def __float__(self): 175 | """ 176 | Resolve the object as a float if possible. 177 | 178 | Essentially, float(traversable) is syntactic sugar for 179 | float(traversable()). 180 | """ 181 | return float(self._obj) 182 | 183 | def __contains__(self, key): 184 | """ 185 | Test for containment if the wrapped object is a list or dict. 186 | 187 | Do not always check for containment whenever it supports the 188 | operator, as this could result in some very sneaky bugs if a 189 | key in the JSON is a string instead of a nested JSON object. 190 | """ 191 | if isinstance(self._obj, (dict, list)): 192 | return key in self._obj 193 | else: 194 | return False 195 | 196 | def __dir__(self): 197 | """ 198 | Return the attributes of this object. 199 | 200 | Includes keys of the internal object if it's a dictionary. 201 | Tremendously helpful for advanced interactive shell. 202 | """ 203 | keys = dir(super(Traversable, self)) 204 | if isinstance(self._obj, dict): 205 | keys += sorted(str(k) for k in self._obj) 206 | return keys 207 | -------------------------------------------------------------------------------- /jsane/wrapper.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from .traversable import Traversable 4 | 5 | 6 | def load(*args, **kwargs): 7 | j = json.load(*args, **kwargs) 8 | return Traversable(j) 9 | 10 | 11 | def loads(*args, **kwargs): 12 | j = json.loads(*args, **kwargs) 13 | return Traversable(j) 14 | 15 | 16 | def dump(obj, *args, **kwargs): 17 | if isinstance(obj, Traversable): 18 | obj = obj._obj 19 | return json.dump(obj, *args, **kwargs) 20 | 21 | 22 | def dumps(obj, *args, **kwargs): 23 | if isinstance(obj, Traversable): 24 | obj = obj._obj 25 | return json.dumps(obj, *args, **kwargs) 26 | 27 | 28 | def from_dict(jdict): 29 | """ 30 | Return a JSane Traversable object from a dict. 31 | """ 32 | return Traversable(jdict) 33 | 34 | 35 | def from_object(obj): 36 | """ 37 | Return a JSane Traversable object from any object (e.g. a list). 38 | """ 39 | return Traversable(obj) 40 | 41 | 42 | def new(kind=dict): 43 | return Traversable(kind()) 44 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [semantic_release] 2 | version_variable = jsane/__init__.py:__version__ 3 | 4 | [aliases] 5 | test=pytest 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | from jsane import __version__ 5 | assert sys.version >= '2.7', ("Requires Python v2.7 or above, get with the " 6 | "times, grandpa.") 7 | from setuptools import setup 8 | 9 | classifiers = [ 10 | "License :: OSI Approved :: MIT License", 11 | "Programming Language :: Python", 12 | "Programming Language :: Python :: 2.7", 13 | "Programming Language :: Python :: 3.4", 14 | "Programming Language :: Python :: 3.5", 15 | "Programming Language :: Python :: 3.6", 16 | "Topic :: Software Development :: Libraries :: Python Modules", 17 | ] 18 | 19 | install_requires = [] 20 | setup_requires = ['pytest-runner'] 21 | tests_require = ['pep8', 'pytest'] + install_requires 22 | 23 | setup( 24 | name="jsane", 25 | version=__version__, 26 | author="Stavros Korokithakis", 27 | author_email="hi@stavros.io", 28 | url="https://github.com/skorokithakis/jsane/", 29 | description="A saner way to parse JSON.", 30 | long_description=open("README.rst").read(), 31 | license="MIT", 32 | classifiers=classifiers, 33 | packages=["jsane"], 34 | setup_requires=setup_requires, 35 | tests_require=tests_require, 36 | install_requires=install_requires, 37 | test_suite='jsane.tests', 38 | ) 39 | -------------------------------------------------------------------------------- /tests/test_jsane.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import sys 4 | import pytest 5 | import pep8 6 | 7 | sys.path.insert(0, os.path.abspath(__file__ + "/../..")) 8 | 9 | from jsane import loads, dumps, JSaneException, from_dict, from_object, new 10 | from jsane.traversable import Traversable 11 | 12 | 13 | class TestClass: 14 | @pytest.fixture(autouse=True) 15 | def create_data(self): 16 | self.json1 = """ 17 | { 18 | "r": "yo", 19 | "key_1": "value_1", 20 | "key_2": { 21 | "key_21": [ 22 | [2100, 2101], 23 | [2110, 2111] 24 | ], 25 | "key_22": ["l1", "l2"], 26 | "key_23": {"key_231":"v"}, 27 | "key_24": { 28 | "key_241": 502, 29 | "key_242": [ 30 | [5, 0], 31 | [7, 0] 32 | ], 33 | "key_243": { 34 | "key_2431": [0, 0], 35 | "key_2432": 504, 36 | "key_2433": [ 37 | [11451, 0], 38 | [11452, 0] 39 | ] 40 | }, 41 | "key_244": { 42 | "key_2441": { 43 | "key_24411": { 44 | "key_244111": "v_24411", 45 | "key_244112": [[5549, 0]] 46 | }, 47 | "key_24412": "v_24412" 48 | }, 49 | "key_2442": ["ll1", "ll2"] 50 | } 51 | } 52 | }, 53 | "numeric_string": "115", 54 | "list": [1, 1, 2, 3, 5, 8] 55 | } 56 | """ 57 | self.dict1 = {"foo": "bar"} 58 | 59 | def test_wrapper(self): 60 | assert loads(dumps(self.dict1)).r() == self.dict1 61 | assert json.dumps(self.dict1) == dumps(self.dict1) 62 | assert self.dict1["foo"] == from_dict(self.dict1).foo.r() 63 | assert loads(dumps(self.dict1)), Traversable(self.dict1) 64 | 65 | def test_access(self): 66 | j = loads(self.json1) 67 | assert j.key_1() == "value_1" 68 | assert j["r"]() == "yo" 69 | assert j.key_2.key_21[1][1]() == 2111 70 | assert j.key_1.r() == "value_1" 71 | assert j["r"].r() == "yo" 72 | assert j.key_2.key_21[1][1].r() == 2111 73 | 74 | def test_exception(self): 75 | j = loads(self.json1) 76 | with pytest.raises(JSaneException): 77 | j.key_2.nonexistent[0]() 78 | with pytest.raises(JSaneException): 79 | j.key_2.key_21[7]() 80 | with pytest.raises(JSaneException): 81 | j.key_2.nonexistent[0].r() 82 | with pytest.raises(JSaneException): 83 | j.key_2.key_21[7].r() 84 | with pytest.raises(JSaneException): 85 | j.key_1.key_2.r() 86 | with pytest.raises(IndexError): 87 | j.key_2.key_24.key_244.key_2442[0].r()[7] 88 | with pytest.raises(JSaneException): 89 | j.key_2.key_24.key_244.key_2442[0][7].r() 90 | 91 | def test_default(self): 92 | j = loads(self.json1) 93 | assert j.key_1.key_2(default=None) is None 94 | assert j.key_2.nonexistent[0](default="default") == "default" 95 | assert j.key_2.key_21[7](default="default") == "default" 96 | assert j.key_1.key_2.r(default=None) is None 97 | assert j.key_2.nonexistent[0].r(default="default") == "default" 98 | assert j.key_2.key_21[7].r(default="default") == "default" 99 | with pytest.raises(IndexError): 100 | j.key_2.key_24.key_244.key_2442[0].r(default="default")[7] 101 | 102 | def test_resolution(self): 103 | j = loads(self.json1) 104 | assert j.key_2.key_21[0].r() == [2100, 2101] 105 | assert j.key_2.key_21[0].r() == [2100, 2101] 106 | assert j.key_2.key_24.key_244.key_2442[0].r()[0] == "l" 107 | assert j.key_2.key_21[0]() == [2100, 2101] 108 | assert j.key_2.key_21[0]() == [2100, 2101] 109 | assert j.key_2.key_24.key_244.key_2442[0]()[0] == "l" 110 | 111 | def test_numeric_resolution(self): 112 | j = loads(self.json1) 113 | assert +j.key_2.key_24.key_241 == 502 114 | assert +j.key_2.key_24.key_242[1][0] == 7 115 | assert +j.key_1 != +j.key_1 # inequality to oneself is the NaN test 116 | assert +j.nonexistent != +j.nonexistent 117 | assert +j.numeric_string != +j.numeric_string 118 | 119 | def test_easy_casting(self): 120 | j = loads(self.json1) 121 | assert str(j.key_2.key_21[0]) == "[2100, 2101]" 122 | assert str(j.numeric_string) == "115" 123 | assert int(j.numeric_string) == 115 124 | assert float(j.numeric_string) == 115.0 125 | assert type(float(j.numeric_string)) is float 126 | assert float(j.key_2.key_24.key_241) == 502 127 | assert type(float(j.key_2.key_24.key_241)) is float 128 | with pytest.raises(ValueError): 129 | int(j.key_1) 130 | with pytest.raises(ValueError): 131 | float(j.key_1) 132 | 133 | def test_contains(self): 134 | j = loads(self.json1) 135 | assert "key_1" in j 136 | assert "v" not in j.key_1 # do not pass 'in' operator to strings 137 | assert "v" in j.key_1.r() 138 | assert "key_22" in j.key_2 139 | assert "l1" in j.key_2.key_22 # do pass 'in' operator to lists 140 | assert "nonexistent" not in j 141 | 142 | def test_dir(self): 143 | j = loads(self.json1) 144 | assert "numeric_string" in dir(j) 145 | assert "key_22" in dir(j.key_2) 146 | with pytest.raises(JSaneException): 147 | dir(j.nonexistent) 148 | 149 | def test_new(self): 150 | assert new()() == {} 151 | assert new(list)() == [] 152 | 153 | def test_setting(self): 154 | j = loads(self.json1) 155 | assert "nonexistent" not in j 156 | j.nonexistent = 5 157 | assert j.nonexistent() == 5 158 | del j.nonexistent 159 | assert "nonexistent" not in j 160 | j.list = [5] 161 | assert j.list[0]() == 5 162 | j.list[0] = "six" 163 | assert j.list[0]() == "six" 164 | 165 | def test_deleting(self): 166 | j = loads(self.json1) 167 | assert "r" in j 168 | del j.r 169 | assert "r" not in j 170 | assert j.list() == [1, 1, 2, 3, 5, 8] 171 | del j.list[1:-1] 172 | assert j.list() == [1, 8] 173 | 174 | def test_equality_behavior(self): 175 | i = loads('{"five": 5}') 176 | f = loads('{"five": 5.0}') 177 | assert i == f # comparisons succeed between Traversable objects 178 | assert i.five == f.five 179 | assert i != {"five": 5} # comparisons always return False otherwise 180 | assert i.five != 5 181 | assert i() == {"five": 5} 182 | assert i.five() == 5 # once the value is out, comparison succeeds 183 | 184 | def test_pep8(self): 185 | pep8style = pep8.StyleGuide([['statistics', True], 186 | ['show-sources', True], 187 | ['repeat', True], 188 | ['ignore', "E501"], 189 | ['paths', [os.path.dirname( 190 | os.path.abspath(__file__))]]], 191 | parse_argv=False) 192 | report = pep8style.check_files() 193 | assert report.total_errors == 0 194 | 195 | def test_obj(self): 196 | obj = [1, 2, 3, {"foo": "bar"}] 197 | j = from_object(obj) 198 | assert j[0].r() == 1 199 | for x, y in zip(j, obj): 200 | assert x.r() == y 201 | assert j[3].foo.r() == "bar" 202 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27, py34, py35, pypy 3 | 4 | [testenv] 5 | setenv = 6 | PYTHONPATH = {toxinidir}:{toxinidir}/jsane 7 | deps = 8 | pep8 9 | pytest 10 | commands = pytest --doctest-glob=README.rst 11 | 12 | [testenv:py27] 13 | basepython = python2.7 14 | commands = pytest # Do not run doctests for py2.7 15 | 16 | [testenv:py34] 17 | basepython = python3.4 18 | 19 | [testenv:py35] 20 | basepython = python3.5 21 | 22 | [testenv:py36] 23 | basepython = python3.6 24 | 25 | [testenv:pypy] 26 | basepython = pypy 27 | --------------------------------------------------------------------------------