source: sasmodels/sasmodels/jsonutil.py @ 14de349

core_shell_microgelscostrafo411magnetic_modelrelease_v0.94release_v0.95ticket-1257-vesicle-productticket_1156ticket_1265_superballticket_822_more_unit_tests
Last change on this file since 14de349 was 14de349, checked in by Paul Kienzle <pkienzle@…>, 10 years ago

add autogenerated polydispersity loops

  • Property mode set to 100644
File size: 4.9 KB
Line 
1"""
2Read a relaxed JSON file.
3
4Relaxed JSON allows comments (introduced by // and going to the end
5of the line), optional quotes on key names in dictionaries and optional
6trailing commas at the ends of lists and dictionaries.  It also strips
7the leading characters up to the first '{'.  Multiline strings can be
8formatted using "\n\" at the end of each line.
9
10If the file contains e.g., "var _ = {...}", then it can be edited with
11a JavaScript aware editor such as VJET for eclipse, and it will be easier
12to locate errors and adjust formatting.
13"""
14import re
15import json
16from contextlib import contextmanager
17
18try:
19    from collections import OrderedDict
20except:
21    from ordered_dict import OrderedDict
22
23
24_LEADING_TEXT = re.compile(r'^.*?[{]', re.DOTALL)
25_LINE_CONTINUATION = re.compile(r'\\\s*\n')
26_TRAILING_COMMENT = re.compile(r'(?P<comment>\s*//.*?)\n')
27_MULTILINE_COMMENT = re.compile(r'(?P<comment>/\*.*?\*/)', re.DOTALL)
28_UNQUOTED_FIELDNAME = re.compile(r'(?P<prefix>[,{]\s*)(?P<key>[^\s,{}:"]+)(?P<tail>\s*:)')
29_TRAILING_COMMA = re.compile(r',(?P<tail>\s*[]}])')
30
31def relaxed_load(path, **kw):
32    return relaxed_loads(open(path).read(), **kw)
33
34def relaxed_loads(text, **kw):
35    """
36    Parse and return a relaxed JSON string.
37    """
38    ordered = kw.pop('ordered', False)
39    if ordered:  kw['object_pairs_hook'] = OrderedDict
40    # TODO: need a little state machine that performs the translation so that
41    # TODO: line and column numbers are preserved, and so that we can have
42    # TODO: http:// in a string (instead of it being treated like a comment).
43    #print "== raw text\n", text
44    text = _LINE_CONTINUATION.sub('', text)
45    #print "== joined lines\n", text
46    text = _TRAILING_COMMENT.sub(r'\n', text)
47    text = _MULTILINE_COMMENT.sub(r'', text)
48    #print "== stripped comments\n", text
49    text = _LEADING_TEXT.sub('{', text)
50    #print "== trimmed text\n", text
51    text = _UNQUOTED_FIELDNAME.sub(r'\g<prefix>"\g<key>"\g<tail>', text)
52    #print "== quoted field names\n", text
53    text = _TRAILING_COMMA.sub(r'\g<tail>', text)
54    #print "== processed text\n", text
55    try:
56        obj = json.loads(text, object_hook=decode_dict_as_str, **kw)
57    except ValueError, e:
58        msg = [str(e)]
59        M = re.findall('line ([0-9]*) column ([0-9]*)', msg[0])
60        if M:
61            line,col = int(M[0][0]), int(M[0][1])
62            lines = text.split("\n")
63            if line>=2: msg.append(lines[line-2])
64            if line>=1: msg.append(lines[line-1])
65            msg.append(" "*(col-1) + "^")
66            if line<len(lines): msg.append(lines[line])
67        msg = "\n".join(msg)
68        raise e.__class__(msg)
69    return obj
70
71@contextmanager
72def float_format(formatstr='.15g'):
73    """
74    Allow the float format to be changed for a json encoding action.
75
76    This is a context manager, and should be used for example as::
77
78        >>> with float_format('.2g'):
79        >>>    print json.dumps(sqrt(2))
80        1.41
81    """
82    formatter = json.encoder.FLOAT_REPR
83    json.encoder.FLOAT_REPR = lambda o: format(o, formatstr)
84    yield
85    json.encoder.FLOAT_REPR = formatter
86
87def numpy_encoder(o):
88    """
89    JSON encoder for numpy data.
90
91    To automatically convert numpy data to lists when writing a datastream
92    use json.dumps(object, default=numpy_json).
93    """
94    try:
95        return o.tolist()
96    except AttributeError:
97        raise TypeError
98
99
100def _decode_list(lst):
101    newlist = []
102    for i in lst:
103        if isinstance(i, unicode):
104            i = i.encode('utf-8')
105        elif isinstance(i, list):
106            i = _decode_list(i)
107        newlist.append(i)
108    return newlist
109
110def decode_dict_as_str(dct):
111    newdict = {}
112    for k, v in dct.iteritems():
113        if isinstance(k, unicode):
114            k = k.encode('utf-8')
115        if isinstance(v, unicode):
116            v = v.encode('utf-8')
117        elif isinstance(v, list):
118            v = _decode_list(v)
119        newdict[k] = v
120    return newdict
121
122
123def test():
124    """
125    Verify that the translation from pseudo-JSON to JSON works.
126    """
127    good = """\
128// This is a source definition with no errors
129var entry = {
130field : { // A comment about the field
131  "field" : "te\\
132x\\ 
133t",
134  other$field : 56,
135  },
136/*
137multiline comment
138*/
139secondfield : {
140  content: ["string", "string"], /* a second comment */
141  content: [{name:"good", value:3, URL:"http:\\/\\/my.url.com"},]
142  },
143}
144"""
145    broken = """\
146// This is a source definition with a missing comma
147{
148field : { // A comment about the field
149  field : "te\\   
150x\\ 
151t"
152  other$field : 56,
153  },
154/*
155multiline comment
156*/
157secondfield : {
158  content: ["string", "string"], /* a second comment */
159  content: [{name:"good", value:3},]
160  },
161}
162"""
163    result = relaxed_loads(good)
164    assert result['field']['field'] == "text"
165    assert result['field']['other$field'] == 56
166    assert result['secondfield']['content'][0]['name'] == 'good'
167    try: relaxed_loads(broken)
168    except ValueError, _: pass
169    else: raise Exception("No exception raised in broken")
170
171if __name__ == "__main__":
172    test()
Note: See TracBrowser for help on using the repository browser.