Fix PEP8 issues
[integration/test.git] / csit / libraries / norm_json.py
1 """This module contains single a function for normalizing JSON strings."""
2 # Copyright (c) 2015 Cisco Systems, Inc. and others.  All rights reserved.
3 #
4 # This program and the accompanying materials are made available under the
5 # terms of the Eclipse Public License v1.0 which accompanies this distribution,
6 # and is available at http://www.eclipse.org/legal/epl-v10.html
7
8 import collections as _collections
9 try:
10     import simplejson as _json
11 except ImportError:  # Python2.7 calls it json.
12     import json as _json
13
14
15 __author__ = "Vratko Polak"
16 __copyright__ = "Copyright(c) 2015-2016, Cisco Systems, Inc."
17 __license__ = "Eclipse Public License v1.0"
18 __email__ = "vrpolak@cisco.com"
19
20
21 # Internal details; look down below for Robot Keywords.
22
23
24 class _Hsfl(list):
25     """
26     Hashable sorted frozen list implementation stub.
27
28     Supports only __init__, __repr__ and __hash__ methods.
29     Other list methods are available, but they may break contract.
30     """
31
32     def __init__(self, *args, **kwargs):
33         """Contruct super, sort and compute repr and hash cache values."""
34         sup = super(_Hsfl, self)
35         sup.__init__(*args, **kwargs)
36         sup.sort(key=repr)
37         self.__repr = repr(tuple(self))
38         self.__hash = hash(self.__repr)
39
40     def __repr__(self):
41         """Return cached repr string."""
42         return self.__repr
43
44     def __hash__(self):
45         """Return cached hash."""
46         return self.__hash
47
48
49 class _Hsfod(_collections.OrderedDict):
50     """
51     Hashable sorted (by key) frozen OrderedDict implementation stub.
52
53     Supports only __init__, __repr__ and __hash__ methods.
54     Other OrderedDict methods are available, but they may break contract.
55     """
56
57     def __init__(self, *args, **kwargs):
58         """Put arguments to OrderedDict, sort, pass to super, cache values."""
59         self_unsorted = _collections.OrderedDict(*args, **kwargs)
60         items_sorted = sorted(self_unsorted.items(), key=repr)
61         sup = super(_Hsfod, self)  # possibly something else than OrderedDict
62         sup.__init__(items_sorted)
63         # Repr string is used for sorting, keys are more important than values.
64         self.__repr = '{' + repr(self.keys()) + ':' + repr(self.values()) + '}'
65         self.__hash = hash(self.__repr)
66
67     def __repr__(self):
68         """Return cached repr string."""
69         return self.__repr
70
71     def __hash__(self):
72         """Return cached hash."""
73         return self.__hash
74
75
76 def _hsfl_array(s_and_end, scan_once, **kwargs):
77     """Scan JSON array as usual, but return hsfl instead of list."""
78     values, end = _json.decoder.JSONArray(s_and_end, scan_once, **kwargs)
79     return _Hsfl(values), end
80
81
82 class _Decoder(_json.JSONDecoder):
83     """Private class to act as customized JSON decoder.
84
85     Based on: http://stackoverflow.com/questions/10885238/
86     python-change-list-type-for-json-decoding"""
87
88     def __init__(self, **kwargs):
89         """Initialize decoder with special array implementation."""
90         _json.JSONDecoder.__init__(self, **kwargs)
91         # Use the custom JSONArray
92         self.parse_array = _hsfl_array
93         # Use the python implemenation of the scanner
94         self.scan_once = _json.scanner.py_make_scanner(self)
95
96
97 # Robot Keywords; look above for internal details.
98
99
100 def loads_sorted(text, strict=False):
101     """Return Python object with sorted arrays and dictionary keys."""
102     object_decoded = _json.loads(text, cls=_Decoder, object_hook=_Hsfod)
103     return object_decoded
104
105
106 def dumps_indented(obj, indent=1):
107     """
108     Wrapper for json.dumps with default indentation level. Adds newline.
109
110     The main value is that BuiltIn.Evaluate cannot easily accept Python object
111     as part of its argument.
112     Also, allows to use something different from RequestsLibrary.To_Json
113     """
114     pretty_json = _json.dumps(obj, separators=(',', ': '), indent=indent)
115     return pretty_json + '\n'  # to avoid diff "no newline" warning line
116
117
118 def sort_bits(obj, keys_with_bits=[]):
119     """
120     Rearrange string values of list bits names in alphabetical order.
121
122     This function looks at dict items with known keys.
123     If the value is string, space-separated names are sorted.
124     This function is recursive over dicts and lists.
125     Current implementation performs re-arranging in-place (to save memory),
126     so it is not required to store the return value.
127
128     The intended usage is for composite objects which contain
129     OrderedDict elements. The implementation makes sure that ordering
130     (dictated by keys) is preserved. Support for generic dicts is an added value.
131
132     Sadly, dict (at least in Python 2.7) does not have __updatevalue__(key) method
133     which would guarantee iteritems() is not affected when value is updated.
134     Current "obj[key] = value" implementation is safe for dict and OrderedDict,
135     but it may be not safe for other subclasses of dict.
136
137     TODO: Should this docstring include links to support dict and OrderedDict safety?
138     """
139     if isinstance(obj, dict):
140         for key, value in obj.iteritems():
141             # Unicode is not str and vice versa, isinstance has to check for both.
142             # Luckily, "in" recognizes equivalent strings in different encodings.
143             # Type "bytes" is added for Python 3 compatibility.
144             if key in keys_with_bits and isinstance(value, (unicode, str, bytes)):
145                 obj[key] = " ".join(sorted(value.split(" ")))
146             else:
147                 sort_bits(value, keys_with_bits)
148     # A string is not a list, so there is no risk of recursion over characters.
149     elif isinstance(obj, list):
150         for item in obj:
151             sort_bits(item, keys_with_bits)
152     return obj
153
154
155 def normalize_json_text(text, strict=False, indent=1, keys_with_bits=[]):
156     """
157     Attempt to return sorted indented JSON string.
158
159     If parse error happens:
160     If strict is true, raise the exception.
161     If strict is not true, return original text with error message.
162     If keys_with_bits is non-empty, run sort_bits on intermediate Python object.
163     """
164     try:
165         object_decoded = loads_sorted(text)
166     except ValueError as err:
167         if strict:
168             raise err
169         else:
170             return str(err) + '\n' + text
171     if keys_with_bits:
172         sort_bits(object_decoded, keys_with_bits)
173     pretty_json = dumps_indented(object_decoded, indent=indent)
174     return pretty_json