diff options
-rw-r--r-- | data/schemas/keyboard.jsonschema | 2 | ||||
-rw-r--r-- | docs/reference_info_json.md | 1 | ||||
-rwxr-xr-x | lib/python/qmk/cli/generate/info_json.py | 48 | ||||
-rwxr-xr-x | lib/python/qmk/cli/generate/layouts.py | 4 | ||||
-rwxr-xr-x | lib/python/qmk/cli/generate/rules_mk.py | 10 | ||||
-rw-r--r-- | lib/python/qmk/constants.py | 2 | ||||
-rw-r--r-- | lib/python/qmk/info.py | 60 | ||||
-rw-r--r-- | setup.cfg | 2 |
8 files changed, 75 insertions, 54 deletions
diff --git a/data/schemas/keyboard.jsonschema b/data/schemas/keyboard.jsonschema index e13771e92..f76c7fd18 100644 --- a/data/schemas/keyboard.jsonschema +++ b/data/schemas/keyboard.jsonschema | |||
@@ -25,7 +25,7 @@ | |||
25 | }, | 25 | }, |
26 | "processor": { | 26 | "processor": { |
27 | "type": "string", | 27 | "type": "string", |
28 | "enum": ["MK20DX128", "MK20DX256", "MKL26Z64", "STM32F042", "STM32F072", "STM32F103", "STM32F303", "STM32F401", "STM32F411", "at90usb1286", "at90usb646", "atmega16u2", "atmega328p", "atmega32a", "atmega32u2", "atmega32u4", "attiny85", "cortex-m4"] | 28 | "enum": ["MK20DX128", "MK20DX256", "MKL26Z64", "STM32F042", "STM32F072", "STM32F103", "STM32F303", "STM32F401", "STM32F411", "at90usb1286", "at90usb646", "atmega16u2", "atmega328p", "atmega32a", "atmega32u2", "atmega32u4", "attiny85", "cortex-m4", "unknown"] |
29 | }, | 29 | }, |
30 | "bootloader": { | 30 | "bootloader": { |
31 | "type": "string", | 31 | "type": "string", |
diff --git a/docs/reference_info_json.md b/docs/reference_info_json.md index 47506bc92..c9864ea2d 100644 --- a/docs/reference_info_json.md +++ b/docs/reference_info_json.md | |||
@@ -106,6 +106,7 @@ Example: | |||
106 | ["A7", "B1"], | 106 | ["A7", "B1"], |
107 | [null, "B2"] | 107 | [null, "B2"] |
108 | ] | 108 | ] |
109 | } | ||
109 | } | 110 | } |
110 | ``` | 111 | ``` |
111 | 112 | ||
diff --git a/lib/python/qmk/cli/generate/info_json.py b/lib/python/qmk/cli/generate/info_json.py index fba4b1c01..f3fc54ddc 100755 --- a/lib/python/qmk/cli/generate/info_json.py +++ b/lib/python/qmk/cli/generate/info_json.py | |||
@@ -4,14 +4,41 @@ Compile an info.json for a particular keyboard and pretty-print it. | |||
4 | """ | 4 | """ |
5 | import json | 5 | import json |
6 | 6 | ||
7 | from jsonschema import Draft7Validator, validators | ||
7 | from milc import cli | 8 | from milc import cli |
8 | 9 | ||
9 | from qmk.info_json_encoder import InfoJSONEncoder | ||
10 | from qmk.decorators import automagic_keyboard, automagic_keymap | 10 | from qmk.decorators import automagic_keyboard, automagic_keymap |
11 | from qmk.info import info_json | 11 | from qmk.info import info_json, _jsonschema |
12 | from qmk.info_json_encoder import InfoJSONEncoder | ||
12 | from qmk.path import is_keyboard | 13 | from qmk.path import is_keyboard |
13 | 14 | ||
14 | 15 | ||
16 | def pruning_validator(validator_class): | ||
17 | """Extends Draft7Validator to remove properties that aren't specified in the schema. | ||
18 | """ | ||
19 | validate_properties = validator_class.VALIDATORS["properties"] | ||
20 | |||
21 | def remove_additional_properties(validator, properties, instance, schema): | ||
22 | for prop in list(instance.keys()): | ||
23 | if prop not in properties: | ||
24 | del instance[prop] | ||
25 | |||
26 | for error in validate_properties(validator, properties, instance, schema): | ||
27 | yield error | ||
28 | |||
29 | return validators.extend(validator_class, {"properties": remove_additional_properties}) | ||
30 | |||
31 | |||
32 | def strip_info_json(kb_info_json): | ||
33 | """Remove the API-only properties from the info.json. | ||
34 | """ | ||
35 | pruning_draft_7_validator = pruning_validator(Draft7Validator) | ||
36 | schema = _jsonschema('keyboard') | ||
37 | validator = pruning_draft_7_validator(schema).validate | ||
38 | |||
39 | return validator(kb_info_json) | ||
40 | |||
41 | |||
15 | @cli.argument('-kb', '--keyboard', help='Keyboard to show info for.') | 42 | @cli.argument('-kb', '--keyboard', help='Keyboard to show info for.') |
16 | @cli.argument('-km', '--keymap', help='Show the layers for a JSON keymap too.') | 43 | @cli.argument('-km', '--keymap', help='Show the layers for a JSON keymap too.') |
17 | @cli.subcommand('Generate an info.json file for a keyboard.', hidden=False if cli.config.user.developer else True) | 44 | @cli.subcommand('Generate an info.json file for a keyboard.', hidden=False if cli.config.user.developer else True) |
@@ -22,7 +49,7 @@ def generate_info_json(cli): | |||
22 | """ | 49 | """ |
23 | # Determine our keyboard(s) | 50 | # Determine our keyboard(s) |
24 | if not cli.config.generate_info_json.keyboard: | 51 | if not cli.config.generate_info_json.keyboard: |
25 | cli.log.error('Missing paramater: --keyboard') | 52 | cli.log.error('Missing parameter: --keyboard') |
26 | cli.subcommands['info'].print_help() | 53 | cli.subcommands['info'].print_help() |
27 | return False | 54 | return False |
28 | 55 | ||
@@ -32,18 +59,7 @@ def generate_info_json(cli): | |||
32 | 59 | ||
33 | # Build the info.json file | 60 | # Build the info.json file |
34 | kb_info_json = info_json(cli.config.generate_info_json.keyboard) | 61 | kb_info_json = info_json(cli.config.generate_info_json.keyboard) |
35 | pared_down_json = {} | 62 | strip_info_json(kb_info_json) |
36 | |||
37 | for key in ('manufacturer', 'maintainer', 'usb', 'keyboard_name', 'width', 'height', 'debounce', 'diode_direction', 'features', 'community_layouts', 'layout_aliases', 'matrix_pins', 'rgblight', 'url'): | ||
38 | if key in kb_info_json: | ||
39 | pared_down_json[key] = kb_info_json[key] | ||
40 | |||
41 | pared_down_json['layouts'] = {} | ||
42 | if 'layouts' in kb_info_json: | ||
43 | for layout_name, layout in kb_info_json['layouts'].items(): | ||
44 | pared_down_json['layouts'][layout_name] = {} | ||
45 | pared_down_json['layouts'][layout_name]['key_count'] = layout.get('key_count', len(layout['layout'])) | ||
46 | pared_down_json['layouts'][layout_name]['layout'] = layout['layout'] | ||
47 | 63 | ||
48 | # Display the results | 64 | # Display the results |
49 | print(json.dumps(pared_down_json, indent=2, cls=InfoJSONEncoder)) | 65 | print(json.dumps(kb_info_json, indent=2, cls=InfoJSONEncoder)) |
diff --git a/lib/python/qmk/cli/generate/layouts.py b/lib/python/qmk/cli/generate/layouts.py index 273870e15..b7baae065 100755 --- a/lib/python/qmk/cli/generate/layouts.py +++ b/lib/python/qmk/cli/generate/layouts.py | |||
@@ -54,6 +54,10 @@ def generate_layouts(cli): | |||
54 | if kb_info_json['layouts'][layout_name]['c_macro']: | 54 | if kb_info_json['layouts'][layout_name]['c_macro']: |
55 | continue | 55 | continue |
56 | 56 | ||
57 | if 'matrix' not in kb_info_json['layouts'][layout_name]['layout'][0]: | ||
58 | cli.log.debug('%s/%s: No matrix data!', cli.config.generate_layouts.keyboard, layout_name) | ||
59 | continue | ||
60 | |||
57 | layout_keys = [] | 61 | layout_keys = [] |
58 | layout_matrix = [['KC_NO' for i in range(col_num)] for i in range(row_num)] | 62 | layout_matrix = [['KC_NO' for i in range(col_num)] for i in range(row_num)] |
59 | 63 | ||
diff --git a/lib/python/qmk/cli/generate/rules_mk.py b/lib/python/qmk/cli/generate/rules_mk.py index 2a7e91856..0fdccb404 100755 --- a/lib/python/qmk/cli/generate/rules_mk.py +++ b/lib/python/qmk/cli/generate/rules_mk.py | |||
@@ -37,26 +37,26 @@ def generate_rules_mk(cli): | |||
37 | 37 | ||
38 | # Bring in settings | 38 | # Bring in settings |
39 | for info_key, rule_key in info_to_rules.items(): | 39 | for info_key, rule_key in info_to_rules.items(): |
40 | rules_mk_lines.append(f'{rule_key} := {kb_info_json[info_key]}') | 40 | rules_mk_lines.append(f'{rule_key} ?= {kb_info_json[info_key]}') |
41 | 41 | ||
42 | # Find features that should be enabled | 42 | # Find features that should be enabled |
43 | if 'features' in kb_info_json: | 43 | if 'features' in kb_info_json: |
44 | for feature, enabled in kb_info_json['features'].items(): | 44 | for feature, enabled in kb_info_json['features'].items(): |
45 | if feature == 'bootmagic_lite' and enabled: | 45 | if feature == 'bootmagic_lite' and enabled: |
46 | rules_mk_lines.append('BOOTMAGIC_ENABLE := lite') | 46 | rules_mk_lines.append('BOOTMAGIC_ENABLE ?= lite') |
47 | else: | 47 | else: |
48 | feature = feature.upper() | 48 | feature = feature.upper() |
49 | enabled = 'yes' if enabled else 'no' | 49 | enabled = 'yes' if enabled else 'no' |
50 | rules_mk_lines.append(f'{feature}_ENABLE := {enabled}') | 50 | rules_mk_lines.append(f'{feature}_ENABLE ?= {enabled}') |
51 | 51 | ||
52 | # Set the LED driver | 52 | # Set the LED driver |
53 | if 'led_matrix' in kb_info_json and 'driver' in kb_info_json['led_matrix']: | 53 | if 'led_matrix' in kb_info_json and 'driver' in kb_info_json['led_matrix']: |
54 | driver = kb_info_json['led_matrix']['driver'] | 54 | driver = kb_info_json['led_matrix']['driver'] |
55 | rules_mk_lines.append(f'LED_MATRIX_DRIVER = {driver}') | 55 | rules_mk_lines.append(f'LED_MATRIX_DRIVER ?= {driver}') |
56 | 56 | ||
57 | # Add community layouts | 57 | # Add community layouts |
58 | if 'community_layouts' in kb_info_json: | 58 | if 'community_layouts' in kb_info_json: |
59 | rules_mk_lines.append(f'LAYOUTS = {" ".join(kb_info_json["community_layouts"])}') | 59 | rules_mk_lines.append(f'LAYOUTS ?= {" ".join(kb_info_json["community_layouts"])}') |
60 | 60 | ||
61 | # Show the results | 61 | # Show the results |
62 | rules_mk = '\n'.join(rules_mk_lines) + '\n' | 62 | rules_mk = '\n'.join(rules_mk_lines) + '\n' |
diff --git a/lib/python/qmk/constants.py b/lib/python/qmk/constants.py index 675832c50..6a643070f 100644 --- a/lib/python/qmk/constants.py +++ b/lib/python/qmk/constants.py | |||
@@ -26,5 +26,5 @@ ROW_LETTERS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnop' | |||
26 | LED_INDICATORS = { | 26 | LED_INDICATORS = { |
27 | 'caps_lock': 'LED_CAPS_LOCK_PIN', | 27 | 'caps_lock': 'LED_CAPS_LOCK_PIN', |
28 | 'num_lock': 'LED_NUM_LOCK_PIN', | 28 | 'num_lock': 'LED_NUM_LOCK_PIN', |
29 | 'scrol_lock': 'LED_SCROLL_LOCK_PIN' | 29 | 'scrol_lock': 'LED_SCROLL_LOCK_PIN', |
30 | } | 30 | } |
diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py index 28c281a4b..0ea5136a9 100644 --- a/lib/python/qmk/info.py +++ b/lib/python/qmk/info.py | |||
@@ -1,6 +1,7 @@ | |||
1 | """Functions that help us generate and use info.json files. | 1 | """Functions that help us generate and use info.json files. |
2 | """ | 2 | """ |
3 | import json | 3 | import json |
4 | from collections.abc import Mapping | ||
4 | from glob import glob | 5 | from glob import glob |
5 | from pathlib import Path | 6 | from pathlib import Path |
6 | 7 | ||
@@ -140,6 +141,8 @@ def _json_load(json_file): | |||
140 | 141 | ||
141 | def _jsonschema(schema_name): | 142 | def _jsonschema(schema_name): |
142 | """Read a jsonschema file from disk. | 143 | """Read a jsonschema file from disk. |
144 | |||
145 | FIXME(skullydazed/anyone): Refactor to make this a public function. | ||
143 | """ | 146 | """ |
144 | schema_path = Path(f'data/schemas/{schema_name}.jsonschema') | 147 | schema_path = Path(f'data/schemas/{schema_name}.jsonschema') |
145 | 148 | ||
@@ -638,49 +641,44 @@ def unknown_processor_rules(info_data, rules): | |||
638 | return info_data | 641 | return info_data |
639 | 642 | ||
640 | 643 | ||
644 | def deep_update(origdict, newdict): | ||
645 | """Update a dictionary in place, recursing to do a deep copy. | ||
646 | """ | ||
647 | for key, value in newdict.items(): | ||
648 | if isinstance(value, Mapping): | ||
649 | origdict[key] = deep_update(origdict.get(key, {}), value) | ||
650 | |||
651 | else: | ||
652 | origdict[key] = value | ||
653 | |||
654 | return origdict | ||
655 | |||
656 | |||
641 | def merge_info_jsons(keyboard, info_data): | 657 | def merge_info_jsons(keyboard, info_data): |
642 | """Return a merged copy of all the info.json files for a keyboard. | 658 | """Return a merged copy of all the info.json files for a keyboard. |
643 | """ | 659 | """ |
644 | for info_file in find_info_json(keyboard): | 660 | for info_file in find_info_json(keyboard): |
645 | # Load and validate the JSON data | 661 | # Load and validate the JSON data |
662 | new_info_data = _json_load(info_file) | ||
663 | |||
664 | if not isinstance(new_info_data, dict): | ||
665 | _log_error(info_data, "Invalid file %s, root object should be a dictionary." % (str(info_file),)) | ||
666 | continue | ||
667 | |||
646 | try: | 668 | try: |
647 | new_info_data = _json_load(info_file) | ||
648 | keyboard_validate(new_info_data) | 669 | keyboard_validate(new_info_data) |
649 | |||
650 | except jsonschema.ValidationError as e: | 670 | except jsonschema.ValidationError as e: |
651 | json_path = '.'.join([str(p) for p in e.absolute_path]) | 671 | json_path = '.'.join([str(p) for p in e.absolute_path]) |
652 | cli.log.error('Invalid info.json data: %s: %s: %s', info_file, json_path, e.message) | 672 | cli.log.error('Not including data from file: %s', info_file) |
673 | cli.log.error('\t%s: %s', json_path, e.message) | ||
653 | continue | 674 | continue |
654 | 675 | ||
655 | if not isinstance(new_info_data, dict): | 676 | # Mark the layouts as coming from json |
656 | _log_error(info_data, "Invalid file %s, root object should be a dictionary." % (str(info_file),)) | 677 | for layout in new_info_data.get('layouts', {}).values(): |
657 | continue | 678 | layout['c_macro'] = False |
658 | |||
659 | # Copy whitelisted keys into `info_data` | ||
660 | for key in ('debounce', 'diode_direction', 'indicators', 'keyboard_name', 'manufacturer', 'identifier', 'url', 'maintainer', 'processor', 'bootloader', 'width', 'height'): | ||
661 | if key in new_info_data: | ||
662 | info_data[key] = new_info_data[key] | ||
663 | |||
664 | # Deep merge certain keys | ||
665 | # FIXME(skullydazed/anyone): this should be generalized more so that we can inteligently merge more than one level deep. It would be nice if we could filter on valid keys too. That may have to wait for a future where we use openapi or something. | ||
666 | for key in ('features', 'layout_aliases', 'led_matrix', 'matrix_pins', 'rgblight', 'usb'): | ||
667 | if key in new_info_data: | ||
668 | if key not in info_data: | ||
669 | info_data[key] = {} | ||
670 | |||
671 | info_data[key].update(new_info_data[key]) | ||
672 | |||
673 | # Merge the layouts | ||
674 | if 'community_layouts' in new_info_data: | ||
675 | if 'community_layouts' in info_data: | ||
676 | for layout in new_info_data['community_layouts']: | ||
677 | if layout not in info_data['community_layouts']: | ||
678 | info_data['community_layouts'].append(layout) | ||
679 | else: | ||
680 | info_data['community_layouts'] = new_info_data['community_layouts'] | ||
681 | 679 | ||
682 | if 'layouts' in new_info_data: | 680 | # Update info_data with the new data |
683 | _merge_layouts(info_data, new_info_data) | 681 | deep_update(info_data, new_info_data) |
684 | 682 | ||
685 | return info_data | 683 | return info_data |
686 | 684 | ||
@@ -3,6 +3,8 @@ | |||
3 | ignore = | 3 | ignore = |
4 | # QMK is ok with long lines. | 4 | # QMK is ok with long lines. |
5 | E501 | 5 | E501 |
6 | # Conflicts with our yapf config | ||
7 | E231 | ||
6 | per_file_ignores = | 8 | per_file_ignores = |
7 | **/__init__.py:F401 | 9 | **/__init__.py:F401 |
8 | 10 | ||