Merge remote-tracking branch 'origin/master' into develop
This commit is contained in:
		
						commit
						a747ef966b
					
				
					 15 changed files with 319 additions and 107 deletions
				
			
		|  | @ -131,6 +131,16 @@ Check your environment and report problems only: | |||
| 
 | ||||
|     qmk doctor -n | ||||
| 
 | ||||
| ## `qmk format-json` | ||||
| 
 | ||||
| Formats a JSON file in a (mostly) human-friendly way. Will usually correctly detect the format of the JSON (info.json or keymap.json) but you can override this with `--format` if neccesary. | ||||
| 
 | ||||
| **Usage**: | ||||
| 
 | ||||
| ``` | ||||
| qmk format-json [-f FORMAT] <json_file> | ||||
| ``` | ||||
| 
 | ||||
| ## `qmk info` | ||||
| 
 | ||||
| Displays information about keyboards and keymaps in QMK. You can use this to get information about a keyboard, show the layouts, display the underlying key matrix, or to pretty-print JSON keymaps. | ||||
|  |  | |||
|  | @ -16,6 +16,7 @@ from . import docs | |||
| from . import doctor | ||||
| from . import fileformat | ||||
| from . import flash | ||||
| from . import format | ||||
| from . import generate | ||||
| from . import hello | ||||
| from . import info | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ from milc import cli | |||
| 
 | ||||
| import qmk.keymap | ||||
| import qmk.path | ||||
| from qmk.info_json_encoder import InfoJSONEncoder | ||||
| from qmk.json_encoders import InfoJSONEncoder | ||||
| from qmk.keyboard import keyboard_folder | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										1
									
								
								lib/python/qmk/cli/format/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								lib/python/qmk/cli/format/__init__.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | |||
| from . import json | ||||
							
								
								
									
										66
									
								
								lib/python/qmk/cli/format/json.py
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										66
									
								
								lib/python/qmk/cli/format/json.py
									
										
									
									
									
										Executable file
									
								
							|  | @ -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)) | ||||
|  | @ -8,7 +8,7 @@ from milc import cli | |||
| 
 | ||||
| from qmk.datetime import current_datetime | ||||
| from qmk.info import info_json | ||||
| from qmk.info_json_encoder import InfoJSONEncoder | ||||
| from qmk.json_encoders import InfoJSONEncoder | ||||
| from qmk.json_schema import json_load | ||||
| from qmk.keyboard import list_keyboards | ||||
| 
 | ||||
|  |  | |||
|  | @ -9,7 +9,7 @@ from milc import cli | |||
| 
 | ||||
| from qmk.decorators import automagic_keyboard, automagic_keymap | ||||
| from qmk.info import info_json | ||||
| from qmk.info_json_encoder import InfoJSONEncoder | ||||
| from qmk.json_encoders import InfoJSONEncoder | ||||
| from qmk.json_schema import load_jsonschema | ||||
| from qmk.keyboard import keyboard_folder | ||||
| from qmk.path import is_keyboard | ||||
|  |  | |||
|  | @ -7,7 +7,7 @@ import platform | |||
| 
 | ||||
| from milc import cli | ||||
| 
 | ||||
| from qmk.info_json_encoder import InfoJSONEncoder | ||||
| from qmk.json_encoders import InfoJSONEncoder | ||||
| from qmk.constants import COL_LETTERS, ROW_LETTERS | ||||
| from qmk.decorators import automagic_keyboard, automagic_keymap | ||||
| from qmk.keyboard import keyboard_folder, render_layouts, render_layout | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ from milc import cli | |||
| from kle2xy import KLE2xy | ||||
| 
 | ||||
| from qmk.converter import kle2qmk | ||||
| from qmk.info_json_encoder import InfoJSONEncoder | ||||
| from qmk.json_encoders import InfoJSONEncoder | ||||
| 
 | ||||
| 
 | ||||
| @cli.argument('filename', help='The KLE raw txt to convert') | ||||
|  |  | |||
|  | @ -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) | ||||
							
								
								
									
										192
									
								
								lib/python/qmk/json_encoders.py
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										192
									
								
								lib/python/qmk/json_encoders.py
									
										
									
									
									
										Executable file
									
								
							|  | @ -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 | ||||
							
								
								
									
										13
									
								
								lib/python/qmk/tests/minimal_info.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								lib/python/qmk/tests/minimal_info.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -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] } | ||||
|             ] | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										7
									
								
								lib/python/qmk/tests/minimal_keymap.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								lib/python/qmk/tests/minimal_keymap.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -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"]] | ||||
| } | ||||
|  | @ -259,3 +259,27 @@ def test_generate_layouts(): | |||
|     result = check_subcommand('generate-layouts', '-kb', 'handwired/pytest/basic') | ||||
|     check_returncode(result) | ||||
|     assert '#define LAYOUT_custom(k0A) {' in result.stdout | ||||
| 
 | ||||
| 
 | ||||
| def test_format_json_keyboard(): | ||||
|     result = check_subcommand('format-json', '--format', 'keyboard', 'lib/python/qmk/tests/minimal_info.json') | ||||
|     check_returncode(result) | ||||
|     assert result.stdout == '{\n    "keyboard_name": "tester",\n    "maintainer": "qmk",\n    "height": 5,\n    "width": 15,\n    "layouts": {\n        "LAYOUT": {\n            "layout": [\n                { "label": "KC_A", "matrix": [0, 0], "x": 0, "y": 0 }\n            ]\n        }\n    }\n}\n' | ||||
| 
 | ||||
| 
 | ||||
| def test_format_json_keymap(): | ||||
|     result = check_subcommand('format-json', '--format', 'keymap', 'lib/python/qmk/tests/minimal_keymap.json') | ||||
|     check_returncode(result) | ||||
|     assert result.stdout == '{\n    "version": 1,\n    "keyboard": "handwired/pytest/basic",\n    "keymap": "test",\n    "layout": "LAYOUT_ortho_1x1",\n    "layers": [\n                [\n                        "KC_A"\n                ]\n    ]\n}\n' | ||||
| 
 | ||||
| 
 | ||||
| def test_format_json_keyboard_auto(): | ||||
|     result = check_subcommand('format-json', '--format', 'auto', 'lib/python/qmk/tests/minimal_info.json') | ||||
|     check_returncode(result) | ||||
|     assert result.stdout == '{\n    "keyboard_name": "tester",\n    "maintainer": "qmk",\n    "height": 5,\n    "width": 15,\n    "layouts": {\n        "LAYOUT": {\n            "layout": [\n                { "label": "KC_A", "matrix": [0, 0], "x": 0, "y": 0 }\n            ]\n        }\n    }\n}\n' | ||||
| 
 | ||||
| 
 | ||||
| def test_format_json_keymap_auto(): | ||||
|     result = check_subcommand('format-json', '--format', 'auto', 'lib/python/qmk/tests/minimal_keymap.json') | ||||
|     check_returncode(result) | ||||
|     assert result.stdout == '{\n    "keyboard": "handwired/pytest/basic",\n    "keymap": "test",\n    "layers": [\n        ["KC_A"]\n    ],\n    "layout": "LAYOUT_ortho_1x1",\n    "version": 1\n}\n' | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 QMK Bot
						QMK Bot