From 7329c2d02d38f40a23d38f789de34057fd2acd42 Mon Sep 17 00:00:00 2001 From: Cody Bender <50554676+cfbender@users.noreply.github.com> Date: Tue, 12 Nov 2019 21:55:41 -0700 Subject: Add cli convert subcommand, from raw KLE to JSON (#6898) * Add initial pass at KLE convert * Add cli log on convert * Move kle2xy, add absolute filepath arg support * Add overwrite flag, and context sensitive conversion * Update docs/cli.md * Fix converter.py typo * Add convert unit test * Rename to kle2qmk * Rename subcommand * Rename subcommand to kle2json * Change tests to cover rename * Rename in __init__.py * Update CLI docs with new subcommand name * Fix from suggestions in PR #6898 * Help with cases of case sensitivity * Update cli.md * Use angle brackets to indicate required option * Make the output text more accurate --- lib/python/kle2xy.py | 155 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 lib/python/kle2xy.py (limited to 'lib/python/kle2xy.py') diff --git a/lib/python/kle2xy.py b/lib/python/kle2xy.py new file mode 100644 index 0000000000..ea16a4b5ee --- /dev/null +++ b/lib/python/kle2xy.py @@ -0,0 +1,155 @@ +""" Original code from https://github.com/skullydazed/kle2xy +""" + +import hjson +from decimal import Decimal + +class KLE2xy(list): + """Abstract interface for interacting with a KLE layout. + """ + def __init__(self, layout=None, name='', invert_y=True): + super(KLE2xy, self).__init__() + + self.name = name + self.invert_y = invert_y + self.key_width = Decimal('19.05') + self.key_skel = { + 'decal': False, + 'border_color': 'none', + 'keycap_profile': '', + 'keycap_color': 'grey', + 'label_color': 'black', + 'label_size': 3, + 'label_style': 4, + 'width': Decimal('1'), 'height': Decimal('1'), + 'x': Decimal('0'), 'y': Decimal('0') + } + self.rows = Decimal(0) + self.columns = Decimal(0) + + if layout: + self.parse_layout(layout) + + @property + def width(self): + """Returns the width of the keyboard plate. + """ + return (Decimal(self.columns) * self.key_width) + self.key_width/2 + + @property + def height(self): + """Returns the height of the keyboard plate. + """ + return (self.rows * self.key_width) + self.key_width/2 + + @property + def size(self): + """Returns the size of the keyboard plate. + """ + return (self.width, self.height) + + def attrs(self, properties): + """Parse the keyboard properties dictionary. + """ + # FIXME: Store more than just the keyboard name. + if 'name' in properties: + self.name = properties['name'] + + def parse_layout(self, layout): + # Wrap this in a dictionary so hjson will parse KLE raw data + layout = '{"layout": [' + layout + ']}' + layout = hjson.loads(layout)['layout'] + + # Initialize our state machine + current_key = self.key_skel.copy() + current_row = Decimal(0) + current_col = Decimal(0) + current_x = 0 + current_y = self.key_width / 2 + + if isinstance(layout[0], dict): + self.attrs(layout[0]) + layout = layout[1:] + + for row_num, row in enumerate(layout): + self.append([]) + + # Process the current row + for key in row: + if isinstance(key, dict): + if 'w' in key and key['w'] != Decimal(1): + current_key['width'] = Decimal(key['w']) + if 'w2' in key and 'h2' in key and key['w2'] == 1.5 and key['h2'] == 1: + # FIXME: ISO Key uses these params: {x:0.25,w:1.25,h:2,w2:1.5,h2:1,x2:-0.25} + current_key['isoenter'] = True + if 'h' in key and key['h'] != Decimal(1): + current_key['height'] = Decimal(key['h']) + if 'a' in key: + current_key['label_style'] = self.key_skel['label_style'] = int(key['a']) + if current_key['label_style'] < 0: + current_key['label_style'] = 0 + elif current_key['label_style'] > 9: + current_key['label_style'] = 9 + if 'f' in key: + font_size = int(key['f']) + if font_size > 9: + font_size = 9 + elif font_size < 1: + font_size = 1 + current_key['label_size'] = self.key_skel['label_size'] = font_size + if 'p' in key: + current_key['keycap_profile'] = self.key_skel['keycap_profile'] = key['p'] + if 'c' in key: + current_key['keycap_color'] = self.key_skel['keycap_color'] = key['c'] + if 't' in key: + # FIXME: Need to do better validation, plus figure out how to support multiple colors + if '\n' in key['t']: + key['t'] = key['t'].split('\n')[0] + if key['t'] == "0": + key['t'] = "#000000" + current_key['label_color'] = self.key_skel['label_color'] = key['t'] + if 'x' in key: + current_col += Decimal(key['x']) + current_x += Decimal(key['x']) * self.key_width + if 'y' in key: + current_row += Decimal(key['y']) + current_y += Decimal(key['y']) * self.key_width + if 'd' in key: + current_key['decal'] = True + + else: + current_key['name'] = key + current_key['row'] = current_row + current_key['column'] = current_col + + # Determine the X center + x_center = (current_key['width'] * self.key_width) / 2 + current_x += x_center + current_key['x'] = current_x + current_x += x_center + + # Determine the Y center + y_center = (current_key['height'] * self.key_width) / 2 + y_offset = y_center - (self.key_width / 2) + current_key['y'] = (current_y + y_offset) + + # Tend to our row/col count + current_col += current_key['width'] + if current_col > self.columns: + self.columns = current_col + + # Invert the y-axis if neccesary + if self.invert_y: + current_key['y'] = -current_key['y'] + + # Store this key + self[-1].append(current_key) + current_key = self.key_skel.copy() + + # Move to the next row + current_x = 0 + current_y += self.key_width + current_col = Decimal(0) + current_row += Decimal(1) + if current_row > self.rows: + self.rows = Decimal(current_row) -- cgit v1.2.3 From a4c2a9b083d82d9e7d7fe3a68c0d51ae2280495f Mon Sep 17 00:00:00 2001 From: QMK Bot Date: Wed, 13 Nov 2019 05:24:56 +0000 Subject: format code according to conventions [skip ci] --- lib/python/kle2xy.py | 17 ++++------------- lib/python/qmk/cli/kle2json.py | 10 ++++------ lib/python/qmk/tests/test_cli_commands.py | 2 ++ 3 files changed, 10 insertions(+), 19 deletions(-) (limited to 'lib/python/kle2xy.py') diff --git a/lib/python/kle2xy.py b/lib/python/kle2xy.py index ea16a4b5ee..9291443190 100644 --- a/lib/python/kle2xy.py +++ b/lib/python/kle2xy.py @@ -4,6 +4,7 @@ import hjson from decimal import Decimal + class KLE2xy(list): """Abstract interface for interacting with a KLE layout. """ @@ -13,17 +14,7 @@ class KLE2xy(list): self.name = name self.invert_y = invert_y self.key_width = Decimal('19.05') - self.key_skel = { - 'decal': False, - 'border_color': 'none', - 'keycap_profile': '', - 'keycap_color': 'grey', - 'label_color': 'black', - 'label_size': 3, - 'label_style': 4, - 'width': Decimal('1'), 'height': Decimal('1'), - 'x': Decimal('0'), 'y': Decimal('0') - } + self.key_skel = {'decal': False, 'border_color': 'none', 'keycap_profile': '', 'keycap_color': 'grey', 'label_color': 'black', 'label_size': 3, 'label_style': 4, 'width': Decimal('1'), 'height': Decimal('1'), 'x': Decimal('0'), 'y': Decimal('0')} self.rows = Decimal(0) self.columns = Decimal(0) @@ -34,13 +25,13 @@ class KLE2xy(list): def width(self): """Returns the width of the keyboard plate. """ - return (Decimal(self.columns) * self.key_width) + self.key_width/2 + return (Decimal(self.columns) * self.key_width) + self.key_width / 2 @property def height(self): """Returns the height of the keyboard plate. """ - return (self.rows * self.key_width) + self.key_width/2 + return (self.rows * self.key_width) + self.key_width / 2 @property def size(self): diff --git a/lib/python/qmk/cli/kle2json.py b/lib/python/qmk/cli/kle2json.py index 22eb515dff..5a4e97e3ab 100755 --- a/lib/python/qmk/cli/kle2json.py +++ b/lib/python/qmk/cli/kle2json.py @@ -31,7 +31,7 @@ class CustomJSONEncoder(json.JSONEncoder): @cli.subcommand('Convert a KLE layout to a Configurator JSON') def kle2json(cli): """Convert a KLE layout to QMK's layout format. - """ # If filename is a path + """ # If filename is a path if cli.args.filename.startswith("/") or cli.args.filename.startswith("./"): file_path = Path(cli.args.filename) # Otherwise assume it is a file name @@ -45,7 +45,7 @@ def kle2json(cli): # Check if info.json exists, allow overwrite with force if Path(out_path, "info.json").exists() and not cli.args.force: cli.log.error('File {fg_cyan}%s/info.json{style_reset_all} already exists, use -f or --force to overwrite.', str(out_path)) - return False; + return False try: # Convert KLE raw to x/y coordinates (using kle2xy package from skullydazed) kle = KLE2xy(raw_code) @@ -65,11 +65,9 @@ def kle2json(cli): }}, ) # Initialize keyboard with json encoded from ordered dict - keyboard = json.dumps(keyboard, indent=4, separators=( - ', ', ': '), sort_keys=False, cls=CustomJSONEncoder) + keyboard = json.dumps(keyboard, indent=4, separators=(', ', ': '), sort_keys=False, cls=CustomJSONEncoder) # Initialize layout with kle2qmk from converter module - layout = json.dumps(kle2qmk(kle), separators=( - ', ', ':'), cls=CustomJSONEncoder) + layout = json.dumps(kle2qmk(kle), separators=(', ', ':'), cls=CustomJSONEncoder) # Replace layout in keyboard json keyboard = keyboard.replace('"LAYOUT_JSON_HERE"', layout) # Write our info.json diff --git a/lib/python/qmk/tests/test_cli_commands.py b/lib/python/qmk/tests/test_cli_commands.py index d91af992a8..85d4d91af1 100644 --- a/lib/python/qmk/tests/test_cli_commands.py +++ b/lib/python/qmk/tests/test_cli_commands.py @@ -19,9 +19,11 @@ def test_config(): assert result.returncode == 0 assert 'general.color' in result.stdout + def test_kle2json(): assert check_subcommand('kle2json', 'kle.txt', '-f').returncode == 0 + def test_doctor(): result = check_subcommand('doctor') assert result.returncode == 0 -- cgit v1.2.3 From f7bdc54c697ff24bec1bd0781666ac05401bafb2 Mon Sep 17 00:00:00 2001 From: skullydazed Date: Wed, 20 Nov 2019 14:54:18 -0800 Subject: Add flake8 to our test suite and fix all errors (#7379) * Add flake8 to our test suite and fix all errors * Add some documentation --- bin/qmk | 4 +- docs/cli_development.md | 32 ++++++++++++ lib/python/kle2xy.py | 2 +- lib/python/qmk/cli/compile.py | 3 -- lib/python/qmk/cli/config.py | 94 ++++++++++++++++++++++-------------- lib/python/qmk/cli/doctor.py | 2 - lib/python/qmk/cli/flash.py | 10 +--- lib/python/qmk/cli/json/keymap.py | 1 - lib/python/qmk/cli/kle2json.py | 4 +- lib/python/qmk/cli/list/keyboards.py | 26 +++++----- lib/python/qmk/cli/pytest.py | 16 +++--- lib/python/qmk/keymap.py | 4 -- lib/python/qmk/path.py | 1 - requirements.txt | 2 + setup.cfg | 9 ++++ 15 files changed, 125 insertions(+), 85 deletions(-) (limited to 'lib/python/kle2xy.py') diff --git a/bin/qmk b/bin/qmk index 5da8673ba0..4d5b3d884f 100755 --- a/bin/qmk +++ b/bin/qmk @@ -41,7 +41,7 @@ else: os.environ['QMK_VERSION'] = 'nogit-' + strftime('%Y-%m-%d-%H:%M:%S') + '-dirty' # Setup the CLI -import milc +import milc # noqa milc.EMOJI_LOGLEVELS['INFO'] = '{fg_blue}Ψ{style_reset_all}' @@ -61,7 +61,7 @@ def main(): os.chdir(qmk_dir) # Import the subcommands - import qmk.cli + import qmk.cli # noqa # Execute return_code = milc.cli() diff --git a/docs/cli_development.md b/docs/cli_development.md index f5c7ad139a..cc8c59d067 100644 --- a/docs/cli_development.md +++ b/docs/cli_development.md @@ -173,3 +173,35 @@ You will only be able to access these arguments using `cli.args`. For example: ``` cli.log.info('Reading from %s and writing to %s', cli.args.filename, cli.args.output) ``` + +# Testing, and Linting, and Formatting (oh my!) + +We use nose2, flake8, and yapf to test, lint, and format code. You can use the `pytest` and `pyformat` subcommands to run these tests: + +### Testing and Linting + + qmk pytest + +### Formatting + + qmk pyformat + +## Formatting Details + +We use [yapf](https://github.com/google/yapf) to automatically format code. Our configuration is in the `[yapf]` section of `setup.cfg`. + +?> Tip- Many editors can use yapf as a plugin to automatically format code as you type. + +## Testing Details + +Our tests can be found in `lib/python/qmk/tests/`. You will find both unit and integration tests in this directory. We hope you will write both unit and integration tests for your code, but if you do not please favor integration tests. + +If your PR does not include a comprehensive set of tests please add comments like this to your code so that other people know where they can help: + + # TODO(unassigned/): Write tests + +We use [nose2](https://nose2.readthedocs.io/en/latest/getting_started.html) to run our tests. You can refer to the nose2 documentation for more details on what you can do in your test functions. + +## Linting Details + +We use flake8 to lint our code. Your code should pass flake8 before you open a PR. This will be checked when you run `qmk pytest` and by CI when you submit a PR. diff --git a/lib/python/kle2xy.py b/lib/python/kle2xy.py index 9291443190..bff1d025b7 100644 --- a/lib/python/kle2xy.py +++ b/lib/python/kle2xy.py @@ -46,7 +46,7 @@ class KLE2xy(list): if 'name' in properties: self.name = properties['name'] - def parse_layout(self, layout): + def parse_layout(self, layout): # noqa FIXME(skullydazed): flake8 says this has a complexity of 25, it should be refactored. # Wrap this in a dictionary so hjson will parse KLE raw data layout = '{"layout": [' + layout + ']}' layout = hjson.loads(layout)['layout'] diff --git a/lib/python/qmk/cli/compile.py b/lib/python/qmk/cli/compile.py index 5c29800096..234ffb12ca 100755 --- a/lib/python/qmk/cli/compile.py +++ b/lib/python/qmk/cli/compile.py @@ -2,9 +2,6 @@ You can compile a keymap already in the repo or using a QMK Configurator export. """ -import json -import os -import sys import subprocess from argparse import FileType diff --git a/lib/python/qmk/cli/config.py b/lib/python/qmk/cli/config.py index c4ee20cba5..e17d8bb9ba 100644 --- a/lib/python/qmk/cli/config.py +++ b/lib/python/qmk/cli/config.py @@ -1,8 +1,5 @@ """Read and write configuration settings """ -import os -import subprocess - from milc import cli @@ -12,6 +9,54 @@ def print_config(section, key): cli.echo('%s.%s{fg_cyan}={fg_reset}%s', section, key, cli.config[section][key]) +def show_config(): + """Print the current configuration to stdout. + """ + for section in cli.config: + for key in cli.config[section]: + print_config(section, key) + + +def parse_config_token(config_token): + """Split a user-supplied configuration-token into its components. + """ + section = option = value = None + + if '=' in config_token and '.' not in config_token: + cli.log.error('Invalid configuration token, the key must be of the form
.