Browse Source

Add decorators for determining keyboard and keymap based on current directory (#8191)

* Use pathlib everywhere we can

* Improvements based on @erovia's feedback

* rework qmk compile and qmk flash to use pathlib

* style

* Remove the subcommand_name argument from find_keyboard_keymap()

* add experimental decorators

* Create decorators for finding keyboard and keymap based on current directory.

Decorators were inspired by @Erovia's brilliant work on the proof of concept.
skullydazed 5 years ago
parent
commit
f81b0e35a6

+ 5 - 0
lib/python/qmk/cli/__init__.py

@@ -2,6 +2,8 @@
 
 
 We list each subcommand here explicitly because all the reliable ways of searching for modules are slow and delay startup.
 We list each subcommand here explicitly because all the reliable ways of searching for modules are slow and delay startup.
 """
 """
+from milc import cli
+
 from . import cformat
 from . import cformat
 from . import compile
 from . import compile
 from . import config
 from . import config
@@ -16,3 +18,6 @@ from . import kle2json
 from . import new
 from . import new
 from . import pyformat
 from . import pyformat
 from . import pytest
 from . import pytest
+
+if not hasattr(cli, 'config_source'):
+    cli.log.warning("Your QMK CLI is out of date. Please upgrade with `pip3 install --upgrade qmk` or by using your package manager.")

+ 25 - 12
lib/python/qmk/cli/compile.py

@@ -8,13 +8,17 @@ from argparse import FileType
 from milc import cli
 from milc import cli
 
 
 import qmk.path
 import qmk.path
-from qmk.commands import compile_configurator_json, create_make_command, find_keyboard_keymap, parse_configurator_json
+from qmk.decorators import automagic_keyboard, automagic_keymap
+from qmk.commands import compile_configurator_json, create_make_command, parse_configurator_json
 
 
 
 
 @cli.argument('filename', nargs='?', arg_only=True, type=FileType('r'), help='The configurator export to compile')
 @cli.argument('filename', nargs='?', arg_only=True, type=FileType('r'), help='The configurator export to compile')
 @cli.argument('-kb', '--keyboard', help='The keyboard to build a firmware for. Ignored when a configurator export is supplied.')
 @cli.argument('-kb', '--keyboard', help='The keyboard to build a firmware for. Ignored when a configurator export is supplied.')
 @cli.argument('-km', '--keymap', help='The keymap to build a firmware for. Ignored when a configurator export is supplied.')
 @cli.argument('-km', '--keymap', help='The keymap to build a firmware for. Ignored when a configurator export is supplied.')
+@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Don't actually build, just show the make command to be run.")
 @cli.subcommand('Compile a QMK Firmware.')
 @cli.subcommand('Compile a QMK Firmware.')
+@automagic_keyboard
+@automagic_keymap
 def compile(cli):
 def compile(cli):
     """Compile a QMK Firmware.
     """Compile a QMK Firmware.
 
 
@@ -22,8 +26,10 @@ def compile(cli):
 
 
     If a keyboard and keymap are provided this command will build a firmware based on that.
     If a keyboard and keymap are provided this command will build a firmware based on that.
     """
     """
+    command = None
+
     if cli.args.filename:
     if cli.args.filename:
-        # If a configurator JSON was provided skip straight to compiling it
+        # If a configurator JSON was provided generate a keymap and compile it
         # FIXME(skullydazed): add code to check and warn if the keymap already exists when compiling a json keymap.
         # FIXME(skullydazed): add code to check and warn if the keymap already exists when compiling a json keymap.
         user_keymap = parse_configurator_json(cli.args.filename)
         user_keymap = parse_configurator_json(cli.args.filename)
         keymap_path = qmk.path.keymap(user_keymap['keyboard'])
         keymap_path = qmk.path.keymap(user_keymap['keyboard'])
@@ -32,16 +38,23 @@ def compile(cli):
         cli.log.info('Wrote keymap to {fg_cyan}%s/%s/keymap.c', keymap_path, user_keymap['keymap'])
         cli.log.info('Wrote keymap to {fg_cyan}%s/%s/keymap.c', keymap_path, user_keymap['keymap'])
 
 
     else:
     else:
-        # Perform the action the user specified
-        user_keyboard, user_keymap = find_keyboard_keymap()
-        if user_keyboard and user_keymap:
+        if cli.config.compile.keyboard and cli.config.compile.keymap:
             # Generate the make command for a specific keyboard/keymap.
             # Generate the make command for a specific keyboard/keymap.
-            command = create_make_command(user_keyboard, user_keymap)
+            command = create_make_command(cli.config.compile.keyboard, cli.config.compile.keymap)
+
+        elif not cli.config.compile.keyboard:
+            cli.log.error('Could not determine keyboard!')
+        elif not cli.config.compile.keymap:
+            cli.log.error('Could not determine keymap!')
 
 
-        else:
-            cli.log.error('You must supply a configurator export, both `--keyboard` and `--keymap`, or be in a directory for a keyboard or keymap.')
-            cli.echo('usage: qmk compile [-h] [-b] [-kb KEYBOARD] [-km KEYMAP] [filename]')
-            return False
+    # Compile the firmware, if we're able to
+    if command:
+        cli.log.info('Compiling keymap with {fg_cyan}%s', ' '.join(command))
+        if not cli.args.dry_run:
+            cli.echo('\n')
+            subprocess.run(command)
 
 
-    cli.log.info('Compiling keymap with {fg_cyan}%s\n\n', ' '.join(command))
-    subprocess.run(command)
+    else:
+        cli.log.error('You must supply a configurator export, both `--keyboard` and `--keymap`, or be in a directory for a keyboard or keymap.')
+        cli.echo('usage: qmk compile [-h] [-b] [-kb KEYBOARD] [-km KEYMAP] [filename]')
+        return False

+ 30 - 17
lib/python/qmk/cli/flash.py

@@ -6,9 +6,11 @@ A bootloader must be specified.
 import subprocess
 import subprocess
 from argparse import FileType
 from argparse import FileType
 
 
-import qmk.path
 from milc import cli
 from milc import cli
-from qmk.commands import compile_configurator_json, create_make_command, find_keyboard_keymap, parse_configurator_json
+
+import qmk.path
+from qmk.decorators import automagic_keyboard, automagic_keymap
+from qmk.commands import compile_configurator_json, create_make_command, parse_configurator_json
 
 
 
 
 def print_bootloader_help():
 def print_bootloader_help():
@@ -28,12 +30,15 @@ def print_bootloader_help():
     cli.echo('For more info, visit https://docs.qmk.fm/#/flashing')
     cli.echo('For more info, visit https://docs.qmk.fm/#/flashing')
 
 
 
 
-@cli.argument('-bl', '--bootloader', default='flash', help='The flash command, corresponding to qmk\'s make options of bootloaders.')
 @cli.argument('filename', nargs='?', arg_only=True, type=FileType('r'), help='The configurator export JSON to compile.')
 @cli.argument('filename', nargs='?', arg_only=True, type=FileType('r'), help='The configurator export JSON to compile.')
+@cli.argument('-b', '--bootloaders', action='store_true', help='List the available bootloaders.')
+@cli.argument('-bl', '--bootloader', default='flash', help='The flash command, corresponding to qmk\'s make options of bootloaders.')
 @cli.argument('-km', '--keymap', help='The keymap to build a firmware for. Use this if you dont have a configurator file. Ignored when a configurator file is supplied.')
 @cli.argument('-km', '--keymap', help='The keymap to build a firmware for. Use this if you dont have a configurator file. Ignored when a configurator file is supplied.')
 @cli.argument('-kb', '--keyboard', help='The keyboard to build a firmware for. Use this if you dont have a configurator file. Ignored when a configurator file is supplied.')
 @cli.argument('-kb', '--keyboard', help='The keyboard to build a firmware for. Use this if you dont have a configurator file. Ignored when a configurator file is supplied.')
-@cli.argument('-b', '--bootloaders', action='store_true', help='List the available bootloaders.')
+@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Don't actually build, just show the make command to be run.")
 @cli.subcommand('QMK Flash.')
 @cli.subcommand('QMK Flash.')
+@automagic_keyboard
+@automagic_keymap
 def flash(cli):
 def flash(cli):
     """Compile and or flash QMK Firmware or keyboard/layout
     """Compile and or flash QMK Firmware or keyboard/layout
 
 
@@ -42,12 +47,13 @@ def flash(cli):
 
 
     If no file is supplied, keymap and keyboard are expected.
     If no file is supplied, keymap and keyboard are expected.
 
 
-    If bootloader is omitted, the one according to the rules.mk will be used.
-
+    If bootloader is omitted the make system will use the configured bootloader for that keyboard.
     """
     """
+    command = ''
+
     if cli.args.bootloaders:
     if cli.args.bootloaders:
         # Provide usage and list bootloaders
         # Provide usage and list bootloaders
-        cli.echo('usage: qmk flash [-h] [-b] [-kb KEYBOARD] [-km KEYMAP] [-bl BOOTLOADER] [filename]')
+        cli.echo('usage: qmk flash [-h] [-b] [-n] [-kb KEYBOARD] [-km KEYMAP] [-bl BOOTLOADER] [filename]')
         print_bootloader_help()
         print_bootloader_help()
         return False
         return False
 
 
@@ -60,16 +66,23 @@ def flash(cli):
         cli.log.info('Wrote keymap to {fg_cyan}%s/%s/keymap.c', keymap_path, user_keymap['keymap'])
         cli.log.info('Wrote keymap to {fg_cyan}%s/%s/keymap.c', keymap_path, user_keymap['keymap'])
 
 
     else:
     else:
-        # Perform the action the user specified
-        user_keyboard, user_keymap = find_keyboard_keymap()
-        if user_keyboard and user_keymap:
+        if cli.config.flash.keyboard and cli.config.flash.keymap:
             # Generate the make command for a specific keyboard/keymap.
             # Generate the make command for a specific keyboard/keymap.
-            command = create_make_command(user_keyboard, user_keymap, cli.args.bootloader)
+            command = create_make_command(cli.config.flash.keyboard, cli.config.flash.keymap, cli.args.bootloader)
 
 
-        else:
-            cli.log.error('You must supply a configurator export or both `--keyboard` and `--keymap`.')
-            cli.echo('usage: qmk flash [-h] [-b] [-kb KEYBOARD] [-km KEYMAP] [-bl BOOTLOADER] [filename]')
-            return False
+        elif not cli.config.flash.keyboard:
+            cli.log.error('Could not determine keyboard!')
+        elif not cli.config.flash.keymap:
+            cli.log.error('Could not determine keymap!')
 
 
-    cli.log.info('Flashing keymap with {fg_cyan}%s\n\n', ' '.join(command))
-    subprocess.run(command)
+    # Compile the firmware, if we're able to
+    if command:
+        cli.log.info('Compiling keymap with {fg_cyan}%s', ' '.join(command))
+        if not cli.args.dry_run:
+            cli.echo('\n')
+            subprocess.run(command)
+
+    else:
+        cli.log.error('You must supply a configurator export, both `--keyboard` and `--keymap`, or be in a directory for a keyboard or keymap.')
+        cli.echo('usage: qmk flash [-h] [-b] [-n] [-kb KEYBOARD] [-km KEYMAP] [-bl BOOTLOADER] [filename]')
+        return False

+ 3 - 0
lib/python/qmk/cli/list/keymaps.py

@@ -1,12 +1,15 @@
 """List the keymaps for a specific keyboard
 """List the keymaps for a specific keyboard
 """
 """
 from milc import cli
 from milc import cli
+
 import qmk.keymap
 import qmk.keymap
+from qmk.decorators import automagic_keyboard
 from qmk.errors import NoSuchKeyboardError
 from qmk.errors import NoSuchKeyboardError
 
 
 
 
 @cli.argument("-kb", "--keyboard", help="Specify keyboard name. Example: 1upkeyboards/1up60hse")
 @cli.argument("-kb", "--keyboard", help="Specify keyboard name. Example: 1upkeyboards/1up60hse")
 @cli.subcommand("List the keymaps for a specific keyboard")
 @cli.subcommand("List the keymaps for a specific keyboard")
+@automagic_keyboard
 def list_keymaps(cli):
 def list_keymaps(cli):
     """List the keymaps for a specific keyboard
     """List the keymaps for a specific keyboard
     """
     """

+ 3 - 0
lib/python/qmk/cli/new/keymap.py

@@ -4,12 +4,15 @@ import shutil
 from pathlib import Path
 from pathlib import Path
 
 
 import qmk.path
 import qmk.path
+from qmk.decorators import automagic_keyboard, automagic_keymap
 from milc import cli
 from milc import cli
 
 
 
 
 @cli.argument('-kb', '--keyboard', help='Specify keyboard name. Example: 1upkeyboards/1up60hse')
 @cli.argument('-kb', '--keyboard', help='Specify keyboard name. Example: 1upkeyboards/1up60hse')
 @cli.argument('-km', '--keymap', help='Specify the name for the new keymap directory')
 @cli.argument('-km', '--keymap', help='Specify the name for the new keymap directory')
 @cli.subcommand('Creates a new keymap for the keyboard of your choosing')
 @cli.subcommand('Creates a new keymap for the keyboard of your choosing')
+@automagic_keyboard
+@automagic_keymap
 def new_keymap(cli):
 def new_keymap(cli):
     """Creates a new keymap for the keyboard of your choosing.
     """Creates a new keymap for the keyboard of your choosing.
     """
     """

+ 0 - 69
lib/python/qmk/commands.py

@@ -1,12 +1,8 @@
 """Helper functions for commands.
 """Helper functions for commands.
 """
 """
 import json
 import json
-from pathlib import Path
-
-from milc import cli
 
 
 import qmk.keymap
 import qmk.keymap
-from qmk.path import is_keyboard, is_keymap_dir, under_qmk_firmware
 
 
 
 
 def create_make_command(keyboard, keymap, target=None):
 def create_make_command(keyboard, keymap, target=None):
@@ -59,71 +55,6 @@ def compile_configurator_json(user_keymap, bootloader=None):
     return create_make_command(user_keymap['keyboard'], user_keymap['keymap'], bootloader)
     return create_make_command(user_keymap['keyboard'], user_keymap['keymap'], bootloader)
 
 
 
 
-def find_keyboard_keymap():
-    """Returns `(keyboard_name, keymap_name)` based on the user's current environment.
-
-    This determines the keyboard and keymap name using the following precedence order:
-
-        * Command line flags (--keyboard and --keymap)
-        * Current working directory
-            * `keyboards/<keyboard_name>`
-            * `keyboards/<keyboard_name>/keymaps/<keymap_name>`
-            * `layouts/**/<keymap_name>`
-            * `users/<keymap_name>`
-        * Configuration
-            * cli.config.<subcommand>.keyboard
-            * cli.config.<subcommand>.keymap
-    """
-    # Check to make sure their copy of MILC supports config_source
-    if not hasattr(cli, 'config_source'):
-        cli.log.error("Your QMK CLI is out of date. Please upgrade using pip3 or your package manager.")
-        exit(1)
-
-    # State variables
-    relative_cwd = under_qmk_firmware()
-    keyboard_name = ""
-    keymap_name = ""
-
-    # If the keyboard or keymap are passed as arguments use that in preference to anything else
-    if cli.config_source[cli._entrypoint.__name__]['keyboard'] == 'argument':
-        keyboard_name = cli.config[cli._entrypoint.__name__]['keyboard']
-    if cli.config_source[cli._entrypoint.__name__]['keymap'] == 'argument':
-        keymap_name = cli.config[cli._entrypoint.__name__]['keymap']
-
-    if not keyboard_name or not keymap_name:
-        # If we don't have a keyboard_name and keymap_name from arguments try to derive one or both
-        if relative_cwd and relative_cwd.parts and relative_cwd.parts[0] == 'keyboards':
-            # Try to determine the keyboard and/or keymap name
-            current_path = Path('/'.join(relative_cwd.parts[1:]))
-
-            if current_path.parts[-2] == 'keymaps':
-                if not keymap_name:
-                    keymap_name = current_path.parts[-1]
-                if not keyboard_name:
-                    keyboard_name = '/'.join(current_path.parts[:-2])
-            elif not keyboard_name and is_keyboard(current_path):
-                keyboard_name = str(current_path)
-
-        elif relative_cwd and relative_cwd.parts and relative_cwd.parts[0] == 'layouts':
-            # Try to determine the keymap name from the community layout
-            if is_keymap_dir(relative_cwd) and not keymap_name:
-                keymap_name = relative_cwd.name
-
-        elif relative_cwd and relative_cwd.parts and relative_cwd.parts[0] == 'users':
-            # Try to determine the keymap name based on which userspace they're in
-            if not keymap_name and len(relative_cwd.parts) > 1:
-                keymap_name = relative_cwd.parts[1]
-
-    # If we still don't have a keyboard and keymap check the config
-    if not keyboard_name and cli.config[cli._entrypoint.__name__]['keyboard']:
-        keyboard_name = cli.config[cli._entrypoint.__name__]['keyboard']
-
-    if not keymap_name and cli.config[cli._entrypoint.__name__]['keymap']:
-        keymap_name = cli.config[cli._entrypoint.__name__]['keymap']
-
-    return (keyboard_name, keymap_name)
-
-
 def parse_configurator_json(configurator_file):
 def parse_configurator_json(configurator_file):
     """Open and parse a configurator json export
     """Open and parse a configurator json export
     """
     """

+ 85 - 0
lib/python/qmk/decorators.py

@@ -0,0 +1,85 @@
+"""Helpful decorators that subcommands can use.
+"""
+import functools
+from pathlib import Path
+
+from milc import cli
+
+from qmk.path import is_keyboard, is_keymap_dir, under_qmk_firmware
+
+
+def automagic_keyboard(func):
+    """Sets `cli.config.<subcommand>.keyboard` based on environment.
+
+    This will rewrite cli.config.<subcommand>.keyboard if the user did not pass `--keyboard` and the directory they are currently in is a keyboard or keymap directory.
+    """
+    @functools.wraps(func)
+    def wrapper(*args, **kwargs):
+        # Check to make sure their copy of MILC supports config_source
+        if not hasattr(cli, 'config_source'):
+            cli.log.error("This subcommand requires a newer version of the QMK CLI. Please upgrade using `pip3 install --upgrade qmk` or your package manager.")
+            exit(1)
+
+        # Ensure that `--keyboard` was not passed and CWD is under `qmk_firmware/keyboards`
+        if cli.config_source[cli._entrypoint.__name__]['keyboard'] != 'argument':
+            relative_cwd = under_qmk_firmware()
+
+            if relative_cwd and len(relative_cwd.parts) > 1 and relative_cwd.parts[0] == 'keyboards':
+                # Attempt to extract the keyboard name from the current directory
+                current_path = Path('/'.join(relative_cwd.parts[1:]))
+
+                if 'keymaps' in current_path.parts:
+                    # Strip current_path of anything after `keymaps`
+                    keymap_index = len(current_path.parts) - current_path.parts.index('keymaps') - 1
+                    current_path = current_path.parents[keymap_index]
+
+                if is_keyboard(current_path):
+                    cli.config[cli._entrypoint.__name__]['keyboard'] = str(current_path)
+                    cli.config_source[cli._entrypoint.__name__]['keyboard'] = 'keyboard_directory'
+
+        return func(*args, **kwargs)
+
+    return wrapper
+
+
+def automagic_keymap(func):
+    """Sets `cli.config.<subcommand>.keymap` based on environment.
+
+    This will rewrite cli.config.<subcommand>.keymap if the user did not pass `--keymap` and the directory they are currently in is a keymap, layout, or user directory.
+    """
+    @functools.wraps(func)
+    def wrapper(*args, **kwargs):
+        # Check to make sure their copy of MILC supports config_source
+        if not hasattr(cli, 'config_source'):
+            cli.log.error("This subcommand requires a newer version of the QMK CLI. Please upgrade using `pip3 install --upgrade qmk` or your package manager.")
+            exit(1)
+
+        # Ensure that `--keymap` was not passed and that we're under `qmk_firmware`
+        if cli.config_source[cli._entrypoint.__name__]['keymap'] != 'argument':
+            relative_cwd = under_qmk_firmware()
+
+            if relative_cwd and len(relative_cwd.parts) > 1:
+                # If we're in `qmk_firmware/keyboards` and `keymaps` is in our path, try to find the keyboard name.
+                if relative_cwd.parts[0] == 'keyboards' and 'keymaps' in relative_cwd.parts:
+                    current_path = Path('/'.join(relative_cwd.parts[1:]))  # Strip 'keyboards' from the front
+
+                    if 'keymaps' in current_path.parts and current_path.name != 'keymaps':
+                        while current_path.parent.name != 'keymaps':
+                            current_path = current_path.parent
+                        cli.config[cli._entrypoint.__name__]['keymap'] = current_path.name
+                        cli.config_source[cli._entrypoint.__name__]['keyboard'] = 'keymap_directory'
+
+                # If we're in `qmk_firmware/layouts` guess the name from the community keymap they're in
+                elif relative_cwd.parts[0] == 'layouts' and is_keymap_dir(relative_cwd):
+                    cli.config[cli._entrypoint.__name__]['keymap'] = relative_cwd.name
+                    cli.config_source[cli._entrypoint.__name__]['keyboard'] = 'layouts_directory'
+
+                # If we're in `qmk_firmware/users` guess the name from the userspace they're in
+                elif relative_cwd.parts[0] == 'users':
+                    # Guess the keymap name based on which userspace they're in
+                    cli.config[cli._entrypoint.__name__]['keymap'] = relative_cwd.parts[1]
+                    cli.config_source[cli._entrypoint.__name__]['keyboard'] = 'users_directory'
+
+        return func(*args, **kwargs)
+
+    return wrapper

+ 1 - 1
lib/python/qmk/path.py

@@ -65,7 +65,7 @@ def normpath(path):
     path = Path(path)
     path = Path(path)
 
 
     if path.is_absolute():
     if path.is_absolute():
-        return Path(path)
+        return path
 
 
     return Path(os.environ['ORIG_CWD']) / path
     return Path(os.environ['ORIG_CWD']) / path