* Add a command to format json files * change to work after rebase * add test for qmk format-json * add documentation for qmk format-json * Update lib/python/qmk/cli/format/json.pypull/12380/head
@ -0,0 +1 @@ | |||
from . import json |
@ -0,0 +1,66 @@ | |||
"""JSON Formatting Script | |||
Spits out a JSON file formatted with one of QMK's formatters. | |||
""" | |||
import json | |||
from jsonschema import ValidationError | |||
from milc import cli | |||
from qmk.info import info_json | |||
from qmk.json_schema import json_load, keyboard_validate | |||
from qmk.json_encoders import InfoJSONEncoder, KeymapJSONEncoder | |||
from qmk.path import normpath | |||
@cli.argument('json_file', arg_only=True, type=normpath, help='JSON file to format') | |||
@cli.argument('-f', '--format', choices=['auto', 'keyboard', 'keymap'], default='auto', arg_only=True, help='JSON formatter to use (Default: autodetect)') | |||
@cli.subcommand('Generate an info.json file for a keyboard.', hidden=False if cli.config.user.developer else True) | |||
def format_json(cli): | |||
"""Format a json file. | |||
""" | |||
json_file = json_load(cli.args.json_file) | |||
if cli.args.format == 'auto': | |||
try: | |||
keyboard_validate(json_file) | |||
json_encoder = InfoJSONEncoder | |||
except ValidationError as e: | |||
cli.log.warning('File %s did not validate as a keyboard:\n\t%s', cli.args.json_file, e) | |||
cli.log.info('Treating %s as a keymap file.', cli.args.json_file) | |||
json_encoder = KeymapJSONEncoder | |||
elif cli.args.format == 'keyboard': | |||
json_encoder = InfoJSONEncoder | |||
elif cli.args.format == 'keymap': | |||
json_encoder = KeymapJSONEncoder | |||
else: | |||
# This should be impossible | |||
cli.log.error('Unknown format: %s', cli.args.format) | |||
return False | |||
if json_encoder == KeymapJSONEncoder and 'layout' in json_file: | |||
# Attempt to format the keycodes. | |||
layout = json_file['layout'] | |||
info_data = info_json(json_file['keyboard']) | |||
if layout in info_data.get('layout_aliases', {}): | |||
layout = json_file['layout'] = info_data['layout_aliases'][layout] | |||
if layout in info_data.get('layouts'): | |||
for layer_num, layer in enumerate(json_file['layers']): | |||
current_layer = [] | |||
last_row = 0 | |||
for keymap_key, info_key in zip(layer, info_data['layouts'][layout]['layout']): | |||
if last_row != info_key['y']: | |||
current_layer.append('JSON_NEWLINE') | |||
last_row = info_key['y'] | |||
current_layer.append(keymap_key) | |||
json_file['layers'][layer_num] = current_layer | |||
# Display the results | |||
print(json.dumps(json_file, cls=json_encoder)) |
@ -1,96 +0,0 @@ | |||
"""Class that pretty-prints QMK info.json files. | |||
""" | |||
import json | |||
from decimal import Decimal | |||
class InfoJSONEncoder(json.JSONEncoder): | |||
"""Custom encoder to make info.json's a little nicer to work with. | |||
""" | |||
container_types = (list, tuple, dict) | |||
indentation_char = " " | |||
def __init__(self, *args, **kwargs): | |||
super().__init__(*args, **kwargs) | |||
self.indentation_level = 0 | |||
if not self.indent: | |||
self.indent = 4 | |||
def encode(self, obj): | |||
"""Encode JSON objects for QMK. | |||
""" | |||
if isinstance(obj, Decimal): | |||
if obj == int(obj): # I can't believe Decimal objects don't have .is_integer() | |||
return int(obj) | |||
return float(obj) | |||
elif isinstance(obj, (list, tuple)): | |||
if self._primitives_only(obj): | |||
return "[" + ", ".join(self.encode(element) for element in obj) + "]" | |||
else: | |||
self.indentation_level += 1 | |||
output = [self.indent_str + self.encode(element) for element in obj] | |||
self.indentation_level -= 1 | |||
return "[\n" + ",\n".join(output) + "\n" + self.indent_str + "]" | |||
elif isinstance(obj, dict): | |||
if obj: | |||
if self.indentation_level == 4: | |||
# These are part of a layout, put them on a single line. | |||
return "{ " + ", ".join(f"{self.encode(key)}: {self.encode(element)}" for key, element in sorted(obj.items())) + " }" | |||
else: | |||
self.indentation_level += 1 | |||
output = [self.indent_str + f"{json.dumps(key)}: {self.encode(value)}" for key, value in sorted(obj.items(), key=self.sort_root_dict)] | |||
self.indentation_level -= 1 | |||
return "{\n" + ",\n".join(output) + "\n" + self.indent_str + "}" | |||
else: | |||
return "{}" | |||
else: | |||
return super().encode(obj) | |||
def _primitives_only(self, obj): | |||
"""Returns true if the object doesn't have any container type objects (list, tuple, dict). | |||
""" | |||
if isinstance(obj, dict): | |||
obj = obj.values() | |||
return not any(isinstance(element, self.container_types) for element in obj) | |||
def sort_root_dict(self, key): | |||
"""Forces layout to the back of the sort order. | |||
""" | |||
key = key[0] | |||
if self.indentation_level == 1: | |||
if key == 'manufacturer': | |||
return '10keyboard_name' | |||
elif key == 'keyboard_name': | |||
return '11keyboard_name' | |||
elif key == 'maintainer': | |||
return '12maintainer' | |||
elif key in ('height', 'width'): | |||
return '40' + str(key) | |||
elif key == 'community_layouts': | |||
return '97community_layouts' | |||
elif key == 'layout_aliases': | |||
return '98layout_aliases' | |||
elif key == 'layouts': | |||
return '99layouts' | |||
else: | |||
return '50' + str(key) | |||
return key | |||
@property | |||
def indent_str(self): | |||
return self.indentation_char * (self.indentation_level * self.indent) |
@ -0,0 +1,192 @@ | |||
"""Class that pretty-prints QMK info.json files. | |||
""" | |||
import json | |||
from decimal import Decimal | |||
newline = '\n' | |||
class QMKJSONEncoder(json.JSONEncoder): | |||
"""Base class for all QMK JSON encoders. | |||
""" | |||
container_types = (list, tuple, dict) | |||
indentation_char = " " | |||
def __init__(self, *args, **kwargs): | |||
super().__init__(*args, **kwargs) | |||
self.indentation_level = 0 | |||
if not self.indent: | |||
self.indent = 4 | |||
def encode_decimal(self, obj): | |||
"""Encode a decimal object. | |||
""" | |||
if obj == int(obj): # I can't believe Decimal objects don't have .is_integer() | |||
return int(obj) | |||
return float(obj) | |||
def encode_list(self, obj): | |||
"""Encode a list-like object. | |||
""" | |||
if self.primitives_only(obj): | |||
return "[" + ", ".join(self.encode(element) for element in obj) + "]" | |||
else: | |||
self.indentation_level += 1 | |||
output = [self.indent_str + self.encode(element) for element in obj] | |||
self.indentation_level -= 1 | |||
return "[\n" + ",\n".join(output) + "\n" + self.indent_str + "]" | |||
def encode(self, obj): | |||
"""Encode keymap.json objects for QMK. | |||
""" | |||
if isinstance(obj, Decimal): | |||
return self.encode_decimal(obj) | |||
elif isinstance(obj, (list, tuple)): | |||
return self.encode_list(obj) | |||
elif isinstance(obj, dict): | |||
return self.encode_dict(obj) | |||
else: | |||
return super().encode(obj) | |||
def primitives_only(self, obj): | |||
"""Returns true if the object doesn't have any container type objects (list, tuple, dict). | |||
""" | |||
if isinstance(obj, dict): | |||
obj = obj.values() | |||
return not any(isinstance(element, self.container_types) for element in obj) | |||
@property | |||
def indent_str(self): | |||
return self.indentation_char * (self.indentation_level * self.indent) | |||
class InfoJSONEncoder(QMKJSONEncoder): | |||
"""Custom encoder to make info.json's a little nicer to work with. | |||
""" | |||
def encode_dict(self, obj): | |||
"""Encode info.json dictionaries. | |||
""" | |||
if obj: | |||
if self.indentation_level == 4: | |||
# These are part of a layout, put them on a single line. | |||
return "{ " + ", ".join(f"{self.encode(key)}: {self.encode(element)}" for key, element in sorted(obj.items())) + " }" | |||
else: | |||
self.indentation_level += 1 | |||
output = [self.indent_str + f"{json.dumps(key)}: {self.encode(value)}" for key, value in sorted(obj.items(), key=self.sort_dict)] | |||
self.indentation_level -= 1 | |||
return "{\n" + ",\n".join(output) + "\n" + self.indent_str + "}" | |||
else: | |||
return "{}" | |||
def sort_dict(self, key): | |||
"""Forces layout to the back of the sort order. | |||
""" | |||
key = key[0] | |||
if self.indentation_level == 1: | |||
if key == 'manufacturer': | |||
return '10keyboard_name' | |||
elif key == 'keyboard_name': | |||
return '11keyboard_name' | |||
elif key == 'maintainer': | |||
return '12maintainer' | |||
elif key in ('height', 'width'): | |||
return '40' + str(key) | |||
elif key == 'community_layouts': | |||
return '97community_layouts' | |||
elif key == 'layout_aliases': | |||
return '98layout_aliases' | |||
elif key == 'layouts': | |||
return '99layouts' | |||
else: | |||
return '50' + str(key) | |||
return key | |||
class KeymapJSONEncoder(QMKJSONEncoder): | |||
"""Custom encoder to make keymap.json's a little nicer to work with. | |||
""" | |||
def encode_dict(self, obj): | |||
"""Encode dictionary objects for keymap.json. | |||
""" | |||
if obj: | |||
self.indentation_level += 1 | |||
output_lines = [f"{self.indent_str}{json.dumps(key)}: {self.encode(value)}" for key, value in sorted(obj.items(), key=self.sort_dict)] | |||
output = ',\n'.join(output_lines) | |||
self.indentation_level -= 1 | |||
return f"{{\n{output}\n{self.indent_str}}}" | |||
else: | |||
return "{}" | |||
def encode_list(self, obj): | |||
"""Encode a list-like object. | |||
""" | |||
if self.indentation_level == 2: | |||
indent_level = self.indentation_level + 1 | |||
# We have a list of keycodes | |||
layer = [[]] | |||
for key in obj: | |||
if key == 'JSON_NEWLINE': | |||
layer.append([]) | |||
else: | |||
layer[-1].append(f'"{key}"') | |||
layer = [f"{self.indent_str*indent_level}{', '.join(row)}" for row in layer] | |||
return f"{self.indent_str}[\n{newline.join(layer)}\n{self.indent_str*self.indentation_level}]" | |||
elif self.primitives_only(obj): | |||
return "[" + ", ".join(self.encode(element) for element in obj) + "]" | |||
else: | |||
self.indentation_level += 1 | |||
output = [self.indent_str + self.encode(element) for element in obj] | |||
self.indentation_level -= 1 | |||
return "[\n" + ",\n".join(output) + "\n" + self.indent_str + "]" | |||
def sort_dict(self, key): | |||
"""Sorts the hashes in a nice way. | |||
""" | |||
key = key[0] | |||
if self.indentation_level == 1: | |||
if key == 'version': | |||
return '00version' | |||
elif key == 'author': | |||
return '01author' | |||
elif key == 'notes': | |||
return '02notes' | |||
elif key == 'layers': | |||
return '98layers' | |||
elif key == 'documentation': | |||
return '99documentation' | |||
else: | |||
return '50' + str(key) | |||
return key |
@ -0,0 +1,13 @@ | |||
{ | |||
"keyboard_name": "tester", | |||
"maintainer": "qmk", | |||
"height": 5, | |||
"width": 15, | |||
"layouts": { | |||
"LAYOUT": { | |||
"layout": [ | |||
{ "label": "KC_A", "x": 0, "y": 0, "matrix": [0, 0] } | |||
] | |||
} | |||
} | |||
} |
@ -0,0 +1,7 @@ | |||
{ | |||
"keyboard": "handwired/pytest/basic", | |||
"keymap": "test", | |||
"layers": [["KC_A"]], | |||
"layout": "LAYOUT_ortho_1x1", | |||
"version": 1 | |||
} |
@ -1,6 +0,0 @@ | |||
{ | |||
"keyboard":"handwired/pytest/basic", | |||
"keymap":"pytest_unittest", | |||
"layout":"LAYOUT", | |||
"layers":[["KC_A"]] | |||
} |