Skip to content

Commit c6df511

Browse files
authored
Merge pull request #24 from muellermichel/master
Compatibility with non-JSON objects, html encode feature, better empty object behaviour, cleaned up the code some more
2 parents e02e96a + 465a50a commit c6df511

File tree

3 files changed

+210
-75
lines changed

3 files changed

+210
-75
lines changed

README.rst

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,18 @@ Live Demo
4242
List of valid arguments
4343
-----------------------
4444

45-
``json2html.convert`` - The module's ``convert`` method accepts three different types of arguments being passed.
45+
``json2html.convert`` - The module's ``convert`` method accepts the following arguments:
4646

4747
===================== ================
4848
Argument Description
4949
--------------------- ----------------
50-
`json` a valid JSON
50+
`json` a valid JSON; This can either be a string in valid JSON format or a python object that is either dict-like or list-like at the top level.
5151
--------------------- ----------------
52-
`table_attributes` `id="info-table"`/`class="bootstrap-class"`/`data-*` attributes can be applied to the generated table
52+
`table_attributes` e.g. pass `id="info-table"` or `class="bootstrap-class"`/`data-*` to apply these attributes to the generated table
5353
--------------------- ----------------
54-
`clubbing` turn clubbing of list with same keys of a dict / Array of objects with same key
54+
`clubbing` turn on[default]/off clubbing of list with same keys of a dict / Array of objects with same key
55+
--------------------- ----------------
56+
`encode` turn on/off[default] encoding of result to escaped html, compatible with any browser
5557
===================== ================
5658

5759
Installation
@@ -212,6 +214,8 @@ Contributors
212214
* Now supports JSON Lists (at top level), including clubbing.
213215
* Now supports empty inputs and positional arguments for convert.
214216
* Python 3 support ; Added integration tests for Python 2.6, 3.4 and 3.5 such that support doesn't break.
217+
* Can now also do the proper encoding for you (disabled by default to not break backwards compatibility).
218+
* Can now handle non-JSON objects on a best-effort principle.
215219

216220
2. Daniel Lekic: [@lekic](https://github.com/lekic)
217221
* Fixed issue with one-item lists not rendering correctly.

json2html/jsonconv.py

Lines changed: 64 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -34,29 +34,66 @@
3434
text_types = [str]
3535

3636
class Json2Html:
37-
def convert(self, json="", table_attributes='border="1"', clubbing=True):
37+
def convert(self, json="", table_attributes='border="1"', clubbing=True, encode=False):
3838
"""
3939
Convert JSON to HTML Table format
4040
"""
41-
4241
# table attributes such as class, id, data-attr-*, etc.
4342
# eg: table_attributes = 'class = "table table-bordered sortable"'
4443
self.table_init_markup = "<table %s>" % table_attributes
4544
self.clubbing = clubbing
46-
45+
json_input = None
4746
if not json:
4847
json_input = {}
4948
elif type(json) in text_types:
5049
json_input = json_parser.loads(json, object_pairs_hook=OrderedDict)
5150
else:
5251
json_input = json
52+
converted = self.convert_json_node(json_input)
53+
if encode:
54+
return converted.encode('ascii', 'xmlcharrefreplace')
55+
return converted
56+
57+
def column_headers_from_list_of_dicts(self, json_input):
58+
"""
59+
This method is required to implement clubbing.
60+
It tries to come up with column headers for your input
61+
"""
62+
if not json_input \
63+
or not hasattr(json_input, '__getitem__') \
64+
or not hasattr(json_input[0], 'keys'):
65+
return None
66+
column_headers = json_input[0].keys()
67+
for entry in json_input:
68+
if not hasattr(entry, 'keys') \
69+
or not hasattr(entry, '__iter__') \
70+
or len(entry.keys()) != len(column_headers):
71+
return None
72+
for header in column_headers:
73+
if header not in entry:
74+
return None
75+
return column_headers
5376

54-
if isinstance(json_input, list):
77+
def convert_json_node(self, json_input):
78+
"""
79+
Dispatch JSON input according to the outermost type and process it
80+
to generate the super awesome HTML format.
81+
We try to adhere to duck typing such that users can just pass all kinds
82+
of funky objects to json2html that *behave* like dicts and lists and other
83+
basic JSON types.
84+
"""
85+
if type(json_input) in text_types:
86+
return text(json_input)
87+
if hasattr(json_input, 'items'):
88+
return self.convert_object(json_input)
89+
if hasattr(json_input, '__iter__') and hasattr(json_input, '__getitem__'):
5590
return self.convert_list(json_input)
56-
return self.convert_json(json_input)
91+
return text(json_input)
5792

58-
def column_headers_from_list_of_dicts(self, json_input):
93+
def convert_list(self, list_input):
5994
"""
95+
Iterate over the JSON list and process it
96+
to generate either an HTML table or a HTML list, depending on what's inside.
6097
If suppose some key has array of objects and all the keys are same,
6198
instead of creating a new row for each such entry,
6299
club such values, thus it makes more sense and more readable table.
@@ -79,44 +116,8 @@ def column_headers_from_list_of_dicts(self, json_input):
79116
80117
@contributed by: @muellermichel
81118
"""
82-
if not json_input or not isinstance(json_input[0], dict):
83-
return None
84-
85-
column_headers = json_input[0].keys()
86-
for entry in json_input:
87-
if not isinstance(entry, dict) or (len(entry.keys()) != len(column_headers)):
88-
return None
89-
for header in column_headers:
90-
if header not in entry:
91-
return None
92-
return column_headers
93-
94-
def convert_json(self, json_input):
95-
"""
96-
Iterate over the JSON input and process it
97-
to generate the super awesome HTML format
98-
"""
99-
if type(json_input) in text_types + [int, float]:
100-
return text(json_input)
101-
if isinstance(json_input, list) and len(json_input) == 0:
102-
return ''
103-
if isinstance(json_input, list):
104-
list_markup = '<ul><li>'
105-
list_markup += '</li><li>'.join([self.convert_json(child) for child in json_input])
106-
list_markup += '</li></ul>'
107-
return list_markup
108-
if isinstance(json_input, dict):
109-
return self.convert_object(json_input)
110-
111-
# safety: don't do recursion over anything that we don't know about
112-
# - iteritems() will most probably fail
113-
return ''
114-
115-
def convert_list(self, list_input):
116-
"""
117-
Iterate over the JSON list and process it
118-
to generate either an HTML table or a HTML list, depending on what's inside.
119-
"""
119+
if not list_input:
120+
return ""
120121
converted_output = ""
121122
column_headers = None
122123
if self.clubbing:
@@ -126,35 +127,35 @@ def convert_list(self, list_input):
126127
converted_output += '<tr><th>' + '</th><th>'.join(column_headers) + '</th></tr>'
127128
for list_entry in list_input:
128129
converted_output += '<tr><td>'
129-
converted_output += '</td><td>'.join([self.convert_json(list_entry[column_header]) for column_header in
130+
converted_output += '</td><td>'.join([self.convert_json_node(list_entry[column_header]) for column_header in
130131
column_headers])
131132
converted_output += '</td></tr>'
132133
converted_output += '</table>'
133-
else:
134-
converted_output += self.convert_json(list_input)
135-
return converted_output
134+
return converted_output
136135

137-
def convert_cell_content(self, cell_input):
138-
"""
139-
Wrap content in <td> markup
140-
"""
141-
return '<td>' + self.convert_json(cell_input) + '</td>'
136+
#so you don't want or need clubbing eh? This makes @muellermichel very sad... ;(
137+
#alright, let's fall back to a basic list here...
138+
converted_output = '<ul><li>'
139+
converted_output += '</li><li>'.join([self.convert_json_node(child) for child in list_input])
140+
converted_output += '</li></ul>'
141+
return converted_output
142142

143143
def convert_object(self, json_input):
144144
"""
145145
Iterate over the JSON object and process it
146146
to generate the super awesome HTML Table format
147147
"""
148-
converted_output = self.table_init_markup
149-
for k, v in json_input.items():
150-
converted_output += '<tr><th>' + self.convert_json(k) + '</th>'
151-
if v is None:
152-
v = text("")
153-
if isinstance(v, list):
154-
converted_output += self.convert_cell_content(self.convert_list(v)) + "</tr>"
155-
else:
156-
converted_output += self.convert_cell_content(v) + "</tr>"
157-
converted_output += '</table>'
148+
if not json_input:
149+
return "" #avoid empty tables
150+
converted_output = self.table_init_markup + "<tr>"
151+
converted_output += "</tr><tr>".join([
152+
"<th>%s</th><td>%s</td>" %(
153+
self.convert_json_node(k),
154+
self.convert_json_node(v)
155+
)
156+
for k, v in json_input.items()
157+
])
158+
converted_output += '</tr></table>'
158159
return converted_output
159160

160161
json2html = Json2Html()

test/run_tests.py

Lines changed: 138 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# -*- coding: utf-8 -*-
12
import os, sys, re
23

34
lib_path = os.path.abspath(os.path.join('..'))
@@ -37,15 +38,15 @@ def tearDown(self):
3738
pass
3839

3940
def test_empty_json(self, *args, **kwargs):
40-
self.assertTrue(
41+
self.assertEqual(
4142
json2html.convert(json = ""),
4243
""
4344
)
44-
self.assertTrue(
45+
self.assertEqual(
4546
json2html.convert(json = []),
4647
""
4748
)
48-
self.assertTrue(
49+
self.assertEqual(
4950
json2html.convert(json = {}),
5051
""
5152
)
@@ -55,22 +56,151 @@ def test_invalid_json_exception(self, *args, **kwargs):
5556
_json = "{'name'}"
5657
with self.assertRaises(ValueError) as context:
5758
json2html.convert(json = _json)
58-
5959
self.assertIn('Expecting property name', str(context.exception))
6060

61+
def test_funky_objects(self):
62+
class objecty_class1(object):
63+
pass
64+
class objecty_class2(object):
65+
def __repr__(self):
66+
return u"blübidö"
67+
class objecty_class3:
68+
pass
69+
class objecty_class4:
70+
def __repr__(self):
71+
return u"blübidöbidü"
72+
objecty_the_funky_object1 = objecty_class1()
73+
objecty_the_funky_object2 = objecty_class2()
74+
objecty_the_funky_object3 = objecty_class3()
75+
objecty_the_funky_object4 = objecty_class4()
76+
funky_non_json_object = (
77+
{"blib":(u"blüb", u"ـث‎"), u"ـث‎":1E-3},
78+
"blub",
79+
{
80+
1: objecty_the_funky_object1,
81+
2: objecty_the_funky_object2,
82+
3: objecty_the_funky_object3,
83+
4: objecty_the_funky_object4,
84+
},
85+
tuple([
86+
objecty_the_funky_object1,
87+
objecty_the_funky_object2,
88+
objecty_the_funky_object3,
89+
objecty_the_funky_object4
90+
])
91+
)
92+
converted = json2html.convert(funky_non_json_object)
93+
self.assertTrue(u"ـث‎" in converted)
94+
self.assertTrue(u"blüb" in converted)
95+
self.assertTrue(u"blübidö" in converted)
96+
self.assertTrue(u"blübidöbidü" in converted)
97+
self.assertTrue(u"blübidöbidü" in converted)
98+
self.assertTrue(u"objecty_class1" in converted)
99+
self.assertTrue(u"objecty_class3" in converted)
100+
101+
def test_dictlike_objects(self):
102+
class binary_dict(object):
103+
def __init__(self, one, two):
104+
self.one = one
105+
self.two = two
106+
107+
def __getitem__(self, key):
108+
if key not in self.keys():
109+
raise KeyError()
110+
if key == "one":
111+
return self.one
112+
return self.two
113+
114+
def __iter__(self):
115+
yield self.one
116+
yield self.two
117+
raise StopIteration()
118+
119+
def __contains__(self, key):
120+
return key in self.keys()
121+
122+
def keys(self):
123+
return ("one", "two")
124+
125+
def items(self):
126+
return [(k, self[k]) for k in self.keys()]
127+
128+
#single object
129+
self.assertEqual(
130+
json2html.convert(binary_dict([1, 2], u"blübi")),
131+
u'<table border="1"><tr><th>one</th><td><ul><li>1</li><li>2</li></ul></td></tr><tr><th>two</th><td>blübi</td></tr></table>'
132+
)
133+
#clubbed with single element
134+
self.assertEqual(
135+
json2html.convert([binary_dict([1, 2], u"blübi")]),
136+
u'<table border="1"><tr><th>one</th><th>two</th></tr><tr><td><ul><li>1</li><li>2</li></ul></td><td>blübi</td></tr></table>'
137+
)
138+
#clubbed with two elements
139+
self.assertEqual(
140+
json2html.convert([
141+
binary_dict([1, 2], u"blübi"),
142+
binary_dict("foo", "bar")
143+
]),
144+
u'<table border="1"><tr><th>one</th><th>two</th></tr><tr><td><ul><li>1</li><li>2</li></ul></td><td>blübi</td></tr><tr><td>foo</td><td>bar</td></tr></table>'
145+
)
146+
#not clubbed, second element has different keys
147+
self.assertEqual(
148+
json2html.convert([
149+
binary_dict([1, 2], u"blübi"),
150+
{"three":3}
151+
]),
152+
u'<ul><li><table border="1"><tr><th>one</th><td><ul><li>1</li><li>2</li></ul></td></tr><tr><th>two</th><td>blübi</td></tr></table></li><li><table border="1"><tr><th>three</th><td>3</td></tr></table></li></ul>'
153+
)
154+
155+
def test_listlike_objects(self):
156+
class binary_tuple(object):
157+
def __init__(self, one, two):
158+
self.one = one
159+
self.two = two
160+
161+
def __getitem__(self, key):
162+
if key == 0:
163+
return self.one
164+
if key == 1:
165+
return self.two
166+
raise KeyError()
167+
168+
def __iter__(self):
169+
yield self.one
170+
yield self.two
171+
return
172+
173+
#single object
174+
self.assertEqual(
175+
json2html.convert(binary_tuple([1, 2], u"blübi")),
176+
u'<ul><li><ul><li>1</li><li>2</li></ul></li><li>blübi</li></ul>'
177+
)
178+
179+
def test_bool(self):
180+
self.assertEqual(
181+
json2html.convert(True),
182+
u'True'
183+
)
184+
185+
def test_none(self):
186+
self.assertEqual(
187+
json2html.convert(None),
188+
u''
189+
)
190+
61191
def test_all(self):
62192
for test_definition in self.test_json:
63193
_json = test_definition['json']
64194
_clubbing = "no_clubbing" not in test_definition['filename']
65195
print("testing %s" %(test_definition['filename']))
66196
self.assertEqual(
67-
json2html.convert(json = _json, clubbing=_clubbing),
68-
test_definition['output']
197+
json2html.convert(json = _json, clubbing=_clubbing, encode=True),
198+
test_definition['output'].encode('ascii', 'xmlcharrefreplace')
69199
)
70200
#testing whether we can call convert with a positional args instead of keyword arg
71201
self.assertEqual(
72-
json2html.convert(_json, clubbing=_clubbing),
73-
test_definition['output']
202+
json2html.convert(_json, clubbing=_clubbing, encode=True),
203+
test_definition['output'].encode('ascii', 'xmlcharrefreplace')
74204
)
75205

76206

0 commit comments

Comments
 (0)