Skip to content

Commit

Permalink
Use the schema to eliminate custom code (#11108)
Browse files Browse the repository at this point in the history
* use the schema to eliminate custom code

* Update docs/reference_info_json.md

Co-authored-by: Ryan <[email protected]>

* make flake8 happy

* bugfix

* do not overwrite make vars from json

Co-authored-by: Ryan <[email protected]>
  • Loading branch information
skullydazed and fauxpark authored Jan 9, 2021
1 parent c550047 commit 962bc8d
Show file tree
Hide file tree
Showing 8 changed files with 75 additions and 54 deletions.
2 changes: 1 addition & 1 deletion data/schemas/keyboard.jsonschema
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions docs/reference_info_json.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ Example:
["A7", "B1"],
[null, "B2"]
]
}
}
```

Expand Down
48 changes: 32 additions & 16 deletions lib/python/qmk/cli/generate/info_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,41 @@
"""
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)
Expand All @@ -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

Expand All @@ -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))
4 changes: 4 additions & 0 deletions lib/python/qmk/cli/generate/layouts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)]

Expand Down
10 changes: 5 additions & 5 deletions lib/python/qmk/cli/generate/rules_mk.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
2 changes: 1 addition & 1 deletion lib/python/qmk/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,5 @@
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',
}
60 changes: 29 additions & 31 deletions lib/python/qmk/info.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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')

Expand Down Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
ignore =
# QMK is ok with long lines.
E501
# Conflicts with our yapf config
E231
per_file_ignores =
**/__init__.py:F401

Expand Down

0 comments on commit 962bc8d

Please sign in to comment.