json_encoders.py (7836B)
1 """Class that pretty-prints QMK info.json files. 2 """ 3 import json 4 from decimal import Decimal 5 6 newline = '\n' 7 8 9 class QMKJSONEncoder(json.JSONEncoder): 10 """Base class for all QMK JSON encoders. 11 """ 12 container_types = (list, tuple, dict) 13 indentation_char = " " 14 15 def __init__(self, *args, **kwargs): 16 super().__init__(*args, **kwargs) 17 self.indentation_level = 0 18 19 if not self.indent: 20 self.indent = 4 21 22 def encode_decimal(self, obj): 23 """Encode a decimal object. 24 """ 25 if obj == int(obj): # I can't believe Decimal objects don't have .is_integer() 26 return int(obj) 27 28 return float(obj) 29 30 def encode_dict(self, obj, path): 31 """Encode a dict-like object. 32 """ 33 if obj: 34 self.indentation_level += 1 35 36 items = sorted(obj.items(), key=self.sort_dict) if self.sort_keys else obj.items() 37 output = [self.indent_str + f"{json.dumps(key)}: {self.encode(value, path + [key])}" for key, value in items] 38 39 self.indentation_level -= 1 40 41 return "{\n" + ",\n".join(output) + "\n" + self.indent_str + "}" 42 else: 43 return "{}" 44 45 def encode_dict_single_line(self, obj, path): 46 """Encode a dict-like object onto a single line. 47 """ 48 return "{" + ", ".join(f"{json.dumps(key)}: {self.encode(value, path + [key])}" for key, value in sorted(obj.items(), key=self.sort_layout)) + "}" 49 50 def encode_list(self, obj, path): 51 """Encode a list-like object. 52 """ 53 if self.primitives_only(obj): 54 return "[" + ", ".join(self.encode(value, path + [index]) for index, value in enumerate(obj)) + "]" 55 56 else: 57 self.indentation_level += 1 58 59 if path[-1] in ('layout', 'rotary'): 60 # These are part of a LED layout or encoder config, put them on a single line 61 output = [self.indent_str + self.encode_dict_single_line(value, path + [index]) for index, value in enumerate(obj)] 62 else: 63 output = [self.indent_str + self.encode(value, path + [index]) for index, value in enumerate(obj)] 64 65 self.indentation_level -= 1 66 67 return "[\n" + ",\n".join(output) + "\n" + self.indent_str + "]" 68 69 def encode(self, obj, path=[]): 70 """Encode JSON objects for QMK. 71 """ 72 if isinstance(obj, Decimal): 73 return self.encode_decimal(obj) 74 75 elif isinstance(obj, (list, tuple)): 76 return self.encode_list(obj, path) 77 78 elif isinstance(obj, dict): 79 return self.encode_dict(obj, path) 80 81 else: 82 return super().encode(obj) 83 84 def primitives_only(self, obj): 85 """Returns true if the object doesn't have any container type objects (list, tuple, dict). 86 """ 87 if isinstance(obj, dict): 88 obj = obj.values() 89 90 return not any(isinstance(element, self.container_types) for element in obj) 91 92 @property 93 def indent_str(self): 94 return self.indentation_char * (self.indentation_level * self.indent) 95 96 97 class InfoJSONEncoder(QMKJSONEncoder): 98 """Custom encoder to make info.json's a little nicer to work with. 99 """ 100 def sort_layout(self, item): 101 """Sorts the hashes in a nice way. 102 """ 103 key = item[0] 104 105 if key == 'label': 106 return '00label' 107 108 elif key == 'matrix': 109 return '01matrix' 110 111 elif key == 'x': 112 return '02x' 113 114 elif key == 'y': 115 return '03y' 116 117 elif key == 'w': 118 return '04w' 119 120 elif key == 'h': 121 return '05h' 122 123 elif key == 'flags': 124 return '06flags' 125 126 return key 127 128 def sort_dict(self, item): 129 """Forces layout to the back of the sort order. 130 """ 131 key = item[0] 132 133 if self.indentation_level == 1: 134 if key == 'manufacturer': 135 return '10manufacturer' 136 137 elif key == 'keyboard_name': 138 return '11keyboard_name' 139 140 elif key == 'maintainer': 141 return '12maintainer' 142 143 elif key == 'community_layouts': 144 return '97community_layouts' 145 146 elif key == 'layout_aliases': 147 return '98layout_aliases' 148 149 elif key == 'layouts': 150 return '99layouts' 151 152 else: 153 return '50' + str(key) 154 155 return key 156 157 158 class KeymapJSONEncoder(QMKJSONEncoder): 159 """Custom encoder to make keymap.json's a little nicer to work with. 160 """ 161 def encode_list(self, obj, path): 162 """Encode a list-like object. 163 """ 164 if self.indentation_level == 2: 165 indent_level = self.indentation_level + 1 166 # We have a list of keycodes 167 layer = [[]] 168 169 for key in obj: 170 if key == 'JSON_NEWLINE': 171 layer.append([]) 172 else: 173 if isinstance(key, dict): 174 # We have a macro 175 176 # TODO: Add proper support for nicely formatting keymap.json macros 177 layer[-1].append(f'{self.encode(key)}') 178 else: 179 layer[-1].append(f'"{key}"') 180 181 layer = [f"{self.indent_str * indent_level}{', '.join(row)}" for row in layer] 182 183 return f"{self.indent_str}[\n{newline.join(layer)}\n{self.indent_str * self.indentation_level}]" 184 185 elif self.primitives_only(obj): 186 return "[" + ", ".join(self.encode(element) for element in obj) + "]" 187 188 else: 189 self.indentation_level += 1 190 output = [self.indent_str + self.encode(element) for element in obj] 191 self.indentation_level -= 1 192 193 return "[\n" + ",\n".join(output) + "\n" + self.indent_str + "]" 194 195 def sort_dict(self, item): 196 """Sorts the hashes in a nice way. 197 """ 198 key = item[0] 199 200 if self.indentation_level == 1: 201 if key == 'version': 202 return '00version' 203 204 elif key == 'author': 205 return '01author' 206 207 elif key == 'notes': 208 return '02notes' 209 210 elif key == 'layers': 211 return '98layers' 212 213 elif key == 'documentation': 214 return '99documentation' 215 216 else: 217 return '50' + str(key) 218 219 return key 220 221 222 class UserspaceJSONEncoder(QMKJSONEncoder): 223 """Custom encoder to make userspace qmk.json's a little nicer to work with. 224 """ 225 def sort_dict(self, item): 226 """Sorts the hashes in a nice way. 227 """ 228 key = item[0] 229 230 if self.indentation_level == 1: 231 if key == 'userspace_version': 232 return '00userspace_version' 233 234 if key == 'build_targets': 235 return '01build_targets' 236 237 return key 238 239 240 class CommunityModuleJSONEncoder(QMKJSONEncoder): 241 """Custom encoder to make qmk_module.json's a little nicer to work with. 242 """ 243 def sort_dict(self, item): 244 """Sorts the hashes in a nice way. 245 """ 246 key = item[0] 247 248 if self.indentation_level == 1: 249 if key == 'module_name': 250 return '00module_name' 251 if key == 'maintainer': 252 return '01maintainer' 253 if key == 'license': 254 return '02license' 255 if key == 'url': 256 return '03url' 257 if key == 'features': 258 return '04features' 259 if key == 'keycodes': 260 return '05keycodes' 261 elif self.indentation_level == 3: # keycodes 262 if key == 'key': 263 return '00key' 264 if key == 'aliases': 265 return '01aliases' 266 267 return key