diff options
Diffstat (limited to 'lib/python')
| -rw-r--r-- | lib/python/qmk/cli/__init__.py | 1 | ||||
| -rw-r--r-- | lib/python/qmk/cli/c2json.py | 2 | ||||
| -rw-r--r-- | lib/python/qmk/cli/format/__init__.py | 1 | ||||
| -rwxr-xr-x | lib/python/qmk/cli/format/json.py | 66 | ||||
| -rwxr-xr-x | lib/python/qmk/cli/generate/api.py | 2 | ||||
| -rwxr-xr-x | lib/python/qmk/cli/generate/info_json.py | 2 | ||||
| -rwxr-xr-x | lib/python/qmk/cli/info.py | 2 | ||||
| -rwxr-xr-x | lib/python/qmk/cli/kle2json.py | 2 | ||||
| -rwxr-xr-x | lib/python/qmk/info_json_encoder.py | 96 | ||||
| -rwxr-xr-x | lib/python/qmk/json_encoders.py | 192 | ||||
| -rw-r--r-- | lib/python/qmk/tests/minimal_info.json | 13 | ||||
| -rw-r--r-- | lib/python/qmk/tests/minimal_keymap.json | 7 | ||||
| -rw-r--r-- | lib/python/qmk/tests/pytest_export.json | 6 | ||||
| -rw-r--r-- | lib/python/qmk/tests/test_cli_commands.py | 24 | 
14 files changed, 309 insertions, 107 deletions
| diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py index a5f1f47679..1349e68a9b 100644 --- a/lib/python/qmk/cli/__init__.py +++ b/lib/python/qmk/cli/__init__.py @@ -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 diff --git a/lib/python/qmk/cli/c2json.py b/lib/python/qmk/cli/c2json.py index a97e212223..1fa833b647 100644 --- a/lib/python/qmk/cli/c2json.py +++ b/lib/python/qmk/cli/c2json.py @@ -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 diff --git a/lib/python/qmk/cli/format/__init__.py b/lib/python/qmk/cli/format/__init__.py new file mode 100644 index 0000000000..741ec778b1 --- /dev/null +++ b/lib/python/qmk/cli/format/__init__.py @@ -0,0 +1 @@ +from . import json diff --git a/lib/python/qmk/cli/format/json.py b/lib/python/qmk/cli/format/json.py new file mode 100755 index 0000000000..1358c70e7a --- /dev/null +++ b/lib/python/qmk/cli/format/json.py @@ -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 + + [email protected]('json_file', arg_only=True, type=normpath, help='JSON file to format') [email protected]('-f', '--format', choices=['auto', 'keyboard', 'keymap'], default='auto', arg_only=True, help='JSON formatter to use (Default: autodetect)') [email protected]('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)) diff --git a/lib/python/qmk/cli/generate/api.py b/lib/python/qmk/cli/generate/api.py index 2905908345..70019428f0 100755 --- a/lib/python/qmk/cli/generate/api.py +++ b/lib/python/qmk/cli/generate/api.py @@ -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 diff --git a/lib/python/qmk/cli/generate/info_json.py b/lib/python/qmk/cli/generate/info_json.py index 6c00ba7d8a..1af7f04392 100755 --- a/lib/python/qmk/cli/generate/info_json.py +++ b/lib/python/qmk/cli/generate/info_json.py @@ -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 diff --git a/lib/python/qmk/cli/info.py b/lib/python/qmk/cli/info.py index 88b65686f5..aac507c1a5 100755 --- a/lib/python/qmk/cli/info.py +++ b/lib/python/qmk/cli/info.py @@ -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 diff --git a/lib/python/qmk/cli/kle2json.py b/lib/python/qmk/cli/kle2json.py index 3bb7443582..91499c9af3 100755 --- a/lib/python/qmk/cli/kle2json.py +++ b/lib/python/qmk/cli/kle2json.py @@ -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') diff --git a/lib/python/qmk/info_json_encoder.py b/lib/python/qmk/info_json_encoder.py deleted file mode 100755 index 60dae7247f..0000000000 --- a/lib/python/qmk/info_json_encoder.py +++ /dev/null @@ -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) diff --git a/lib/python/qmk/json_encoders.py b/lib/python/qmk/json_encoders.py new file mode 100755 index 0000000000..9f3da022b4 --- /dev/null +++ b/lib/python/qmk/json_encoders.py @@ -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 diff --git a/lib/python/qmk/tests/minimal_info.json b/lib/python/qmk/tests/minimal_info.json new file mode 100644 index 0000000000..b91c23bd3d --- /dev/null +++ b/lib/python/qmk/tests/minimal_info.json @@ -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] } +            ] +        } +    } +} diff --git a/lib/python/qmk/tests/minimal_keymap.json b/lib/python/qmk/tests/minimal_keymap.json new file mode 100644 index 0000000000..258f9e8a9a --- /dev/null +++ b/lib/python/qmk/tests/minimal_keymap.json @@ -0,0 +1,7 @@ +{ +    "keyboard": "handwired/pytest/basic", +    "keymap": "test", +    "layers": [["KC_A"]], +    "layout": "LAYOUT_ortho_1x1", +    "version": 1 +} diff --git a/lib/python/qmk/tests/pytest_export.json b/lib/python/qmk/tests/pytest_export.json deleted file mode 100644 index 5fb0d624f8..0000000000 --- a/lib/python/qmk/tests/pytest_export.json +++ /dev/null @@ -1,6 +0,0 @@ -{ -    "keyboard":"handwired/pytest/basic", -    "keymap":"pytest_unittest", -    "layout":"LAYOUT", -    "layers":[["KC_A"]] -} diff --git a/lib/python/qmk/tests/test_cli_commands.py b/lib/python/qmk/tests/test_cli_commands.py index b16777e54b..0a87f60e02 100644 --- a/lib/python/qmk/tests/test_cli_commands.py +++ b/lib/python/qmk/tests/test_cli_commands.py @@ -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' | 
