From 962bc8d9dd413690dbeadeaac971a5389697210f Mon Sep 17 00:00:00 2001 From: Zach White Date: Sat, 9 Jan 2021 13:34:14 -0800 Subject: [PATCH] Use the schema to eliminate custom code (#11108) * use the schema to eliminate custom code * Update docs/reference_info_json.md Co-authored-by: Ryan * make flake8 happy * bugfix * do not overwrite make vars from json Co-authored-by: Ryan --- data/schemas/keyboard.jsonschema | 2 +- docs/reference_info_json.md | 1 + lib/python/qmk/cli/generate/info_json.py | 48 ++++++++++++------- lib/python/qmk/cli/generate/layouts.py | 4 ++ lib/python/qmk/cli/generate/rules_mk.py | 10 ++-- lib/python/qmk/constants.py | 2 +- lib/python/qmk/info.py | 60 ++++++++++++------------ setup.cfg | 2 + 8 files changed, 75 insertions(+), 54 deletions(-) diff --git a/data/schemas/keyboard.jsonschema b/data/schemas/keyboard.jsonschema index e13771e92a39317a2b77f0c273c80aa71a405b3a..f76c7fd1895652478f1fa699329ea322ab47ab6f 100644 --- a/data/schemas/keyboard.jsonschema +++ b/data/schemas/keyboard.jsonschema @@ -25,7 +25,7 @@ }, "processor": { "type": "string", - "enum": ["MK20DX128", "MK20DX256", "MKL26Z64", "STM32F042", "STM32F072", "STM32F103", "STM32F303", "STM32F401", "STM32F411", "at90usb1286", "at90usb646", "atmega16u2", "atmega328p", "atmega32a", "atmega32u2", "atmega32u4", "attiny85", "cortex-m4"] + "enum": ["MK20DX128", "MK20DX256", "MKL26Z64", "STM32F042", "STM32F072", "STM32F103", "STM32F303", "STM32F401", "STM32F411", "at90usb1286", "at90usb646", "atmega16u2", "atmega328p", "atmega32a", "atmega32u2", "atmega32u4", "attiny85", "cortex-m4", "unknown"] }, "bootloader": { "type": "string", diff --git a/docs/reference_info_json.md b/docs/reference_info_json.md index 47506bc92dc5daeadd31173b416642b6dd281ef7..c9864ea2de700f9eac30495ac4880237409a9955 100644 --- a/docs/reference_info_json.md +++ b/docs/reference_info_json.md @@ -106,6 +106,7 @@ Example: ["A7", "B1"], [null, "B2"] ] + } } ``` diff --git a/lib/python/qmk/cli/generate/info_json.py b/lib/python/qmk/cli/generate/info_json.py index fba4b1c01400e074e2158380a8414a54233c8984..f3fc54ddcf1261541e4c7c690b21ec401a73e37a 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. """ import json +from jsonschema import Draft7Validator, validators from milc import cli -from qmk.info_json_encoder import InfoJSONEncoder from qmk.decorators import automagic_keyboard, automagic_keymap -from qmk.info import info_json +from qmk.info import info_json, _jsonschema +from qmk.info_json_encoder import InfoJSONEncoder from qmk.path import is_keyboard +def pruning_validator(validator_class): + """Extends Draft7Validator to remove properties that aren't specified in the schema. + """ + validate_properties = validator_class.VALIDATORS["properties"] + + def remove_additional_properties(validator, properties, instance, schema): + for prop in list(instance.keys()): + if prop not in properties: + del instance[prop] + + for error in validate_properties(validator, properties, instance, schema): + yield error + + return validators.extend(validator_class, {"properties": remove_additional_properties}) + + +def strip_info_json(kb_info_json): + """Remove the API-only properties from the info.json. + """ + pruning_draft_7_validator = pruning_validator(Draft7Validator) + schema = _jsonschema('keyboard') + validator = pruning_draft_7_validator(schema).validate + + return validator(kb_info_json) + + @cli.argument('-kb', '--keyboard', help='Keyboard to show info for.') @cli.argument('-km', '--keymap', help='Show the layers for a JSON keymap too.') @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): """ # Determine our keyboard(s) if not cli.config.generate_info_json.keyboard: - cli.log.error('Missing paramater: --keyboard') + cli.log.error('Missing parameter: --keyboard') cli.subcommands['info'].print_help() return False @@ -32,18 +59,7 @@ def generate_info_json(cli): # Build the info.json file kb_info_json = info_json(cli.config.generate_info_json.keyboard) - pared_down_json = {} - - for key in ('manufacturer', 'maintainer', 'usb', 'keyboard_name', 'width', 'height', 'debounce', 'diode_direction', 'features', 'community_layouts', 'layout_aliases', 'matrix_pins', 'rgblight', 'url'): - if key in kb_info_json: - pared_down_json[key] = kb_info_json[key] - - pared_down_json['layouts'] = {} - if 'layouts' in kb_info_json: - for layout_name, layout in kb_info_json['layouts'].items(): - pared_down_json['layouts'][layout_name] = {} - pared_down_json['layouts'][layout_name]['key_count'] = layout.get('key_count', len(layout['layout'])) - pared_down_json['layouts'][layout_name]['layout'] = layout['layout'] + strip_info_json(kb_info_json) # Display the results - print(json.dumps(pared_down_json, indent=2, cls=InfoJSONEncoder)) + 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 273870e15c4742a098d14537405cedc29283af85..b7baae0651d9438676ef41e44ef0ab872a3f6f7a 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): if kb_info_json['layouts'][layout_name]['c_macro']: continue + if 'matrix' not in kb_info_json['layouts'][layout_name]['layout'][0]: + cli.log.debug('%s/%s: No matrix data!', cli.config.generate_layouts.keyboard, layout_name) + continue + layout_keys = [] layout_matrix = [['KC_NO' for i in range(col_num)] for i in range(row_num)] diff --git a/lib/python/qmk/cli/generate/rules_mk.py b/lib/python/qmk/cli/generate/rules_mk.py index 2a7e9185692fd2294e3baecb7302eef70524abbb..0fdccb40489ab55f2d5bee5682841014f69dc713 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): # Bring in settings for info_key, rule_key in info_to_rules.items(): - rules_mk_lines.append(f'{rule_key} := {kb_info_json[info_key]}') + rules_mk_lines.append(f'{rule_key} ?= {kb_info_json[info_key]}') # Find features that should be enabled if 'features' in kb_info_json: for feature, enabled in kb_info_json['features'].items(): if feature == 'bootmagic_lite' and enabled: - rules_mk_lines.append('BOOTMAGIC_ENABLE := lite') + rules_mk_lines.append('BOOTMAGIC_ENABLE ?= lite') else: feature = feature.upper() enabled = 'yes' if enabled else 'no' - rules_mk_lines.append(f'{feature}_ENABLE := {enabled}') + rules_mk_lines.append(f'{feature}_ENABLE ?= {enabled}') # Set the LED driver if 'led_matrix' in kb_info_json and 'driver' in kb_info_json['led_matrix']: driver = kb_info_json['led_matrix']['driver'] - rules_mk_lines.append(f'LED_MATRIX_DRIVER = {driver}') + rules_mk_lines.append(f'LED_MATRIX_DRIVER ?= {driver}') # Add community layouts if 'community_layouts' in kb_info_json: - rules_mk_lines.append(f'LAYOUTS = {" ".join(kb_info_json["community_layouts"])}') + rules_mk_lines.append(f'LAYOUTS ?= {" ".join(kb_info_json["community_layouts"])}') # Show the results rules_mk = '\n'.join(rules_mk_lines) + '\n' diff --git a/lib/python/qmk/constants.py b/lib/python/qmk/constants.py index 675832c5068af140ab74f7c993464329bcbb5c83..6a643070fd62e887b3a0525ce03ad2a954d1bda3 100644 --- a/lib/python/qmk/constants.py +++ b/lib/python/qmk/constants.py @@ -26,5 +26,5 @@ ROW_LETTERS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnop' LED_INDICATORS = { 'caps_lock': 'LED_CAPS_LOCK_PIN', 'num_lock': 'LED_NUM_LOCK_PIN', - 'scrol_lock': 'LED_SCROLL_LOCK_PIN' + 'scrol_lock': 'LED_SCROLL_LOCK_PIN', } diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py index 28c281a4bca9aa222362aa79f19132e692167876..0ea5136a977835efaed56fe0e4aad12ecdc353da 100644 --- a/lib/python/qmk/info.py +++ b/lib/python/qmk/info.py @@ -1,6 +1,7 @@ """Functions that help us generate and use info.json files. """ import json +from collections.abc import Mapping from glob import glob from pathlib import Path @@ -140,6 +141,8 @@ def _json_load(json_file): def _jsonschema(schema_name): """Read a jsonschema file from disk. + + FIXME(skullydazed/anyone): Refactor to make this a public function. """ schema_path = Path(f'data/schemas/{schema_name}.jsonschema') @@ -638,49 +641,44 @@ def unknown_processor_rules(info_data, rules): return info_data +def deep_update(origdict, newdict): + """Update a dictionary in place, recursing to do a deep copy. + """ + for key, value in newdict.items(): + if isinstance(value, Mapping): + origdict[key] = deep_update(origdict.get(key, {}), value) + + else: + origdict[key] = value + + return origdict + + def merge_info_jsons(keyboard, info_data): """Return a merged copy of all the info.json files for a keyboard. """ for info_file in find_info_json(keyboard): # Load and validate the JSON data + new_info_data = _json_load(info_file) + + if not isinstance(new_info_data, dict): + _log_error(info_data, "Invalid file %s, root object should be a dictionary." % (str(info_file),)) + continue + try: - new_info_data = _json_load(info_file) keyboard_validate(new_info_data) - except jsonschema.ValidationError as e: json_path = '.'.join([str(p) for p in e.absolute_path]) - cli.log.error('Invalid info.json data: %s: %s: %s', info_file, json_path, e.message) + cli.log.error('Not including data from file: %s', info_file) + cli.log.error('\t%s: %s', json_path, e.message) continue - if not isinstance(new_info_data, dict): - _log_error(info_data, "Invalid file %s, root object should be a dictionary." % (str(info_file),)) - continue - - # Copy whitelisted keys into `info_data` - for key in ('debounce', 'diode_direction', 'indicators', 'keyboard_name', 'manufacturer', 'identifier', 'url', 'maintainer', 'processor', 'bootloader', 'width', 'height'): - if key in new_info_data: - info_data[key] = new_info_data[key] - - # Deep merge certain keys - # 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. - for key in ('features', 'layout_aliases', 'led_matrix', 'matrix_pins', 'rgblight', 'usb'): - if key in new_info_data: - if key not in info_data: - info_data[key] = {} - - info_data[key].update(new_info_data[key]) - - # Merge the layouts - if 'community_layouts' in new_info_data: - if 'community_layouts' in info_data: - for layout in new_info_data['community_layouts']: - if layout not in info_data['community_layouts']: - info_data['community_layouts'].append(layout) - else: - info_data['community_layouts'] = new_info_data['community_layouts'] + # Mark the layouts as coming from json + for layout in new_info_data.get('layouts', {}).values(): + layout['c_macro'] = False - if 'layouts' in new_info_data: - _merge_layouts(info_data, new_info_data) + # Update info_data with the new data + deep_update(info_data, new_info_data) return info_data diff --git a/setup.cfg b/setup.cfg index 5ef2f9ba0954f9c16b4b13b84d5404149d87b274..baa6a039676517d424c3b6a57f6e936f1ddcd8c1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,6 +3,8 @@ ignore = # QMK is ok with long lines. E501 + # Conflicts with our yapf config + E231 per_file_ignores = **/__init__.py:F401