~ruther/qmk_firmware

f81b0e35a6a25a9a6e633dc65a4900bed2458cfb — skullydazed 5 years ago 5e98eaa
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.
M lib/python/qmk/cli/__init__.py => lib/python/qmk/cli/__init__.py +5 -0
@@ 2,6 2,8 @@

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 compile
from . import config


@@ 16,3 18,6 @@ from . import kle2json
from . import new
from . import pyformat
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.")

M lib/python/qmk/cli/compile.py => lib/python/qmk/cli/compile.py +25 -12
@@ 8,13 8,17 @@ from argparse import FileType
from milc import cli

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('-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('-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.')
@automagic_keyboard
@automagic_keymap
def compile(cli):
    """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.
    """
    command = None

    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.
        user_keymap = parse_configurator_json(cli.args.filename)
        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'])

    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.
            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

M lib/python/qmk/cli/flash.py => lib/python/qmk/cli/flash.py +30 -17
@@ 6,9 6,11 @@ A bootloader must be specified.
import subprocess
from argparse import FileType

import qmk.path
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():


@@ 28,12 30,15 @@ def print_bootloader_help():
    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('-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('-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.')
@automagic_keyboard
@automagic_keymap
def flash(cli):
    """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 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:
        # 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()
        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'])

    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.
            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

M lib/python/qmk/cli/list/keymaps.py => lib/python/qmk/cli/list/keymaps.py +3 -0
@@ 1,12 1,15 @@
"""List the keymaps for a specific keyboard
"""
from milc import cli

import qmk.keymap
from qmk.decorators import automagic_keyboard
from qmk.errors import NoSuchKeyboardError


@cli.argument("-kb", "--keyboard", help="Specify keyboard name. Example: 1upkeyboards/1up60hse")
@cli.subcommand("List the keymaps for a specific keyboard")
@automagic_keyboard
def list_keymaps(cli):
    """List the keymaps for a specific keyboard
    """

M lib/python/qmk/cli/new/keymap.py => lib/python/qmk/cli/new/keymap.py +3 -0
@@ 4,12 4,15 @@ import shutil
from pathlib import Path

import qmk.path
from qmk.decorators import automagic_keyboard, automagic_keymap
from milc import cli


@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.subcommand('Creates a new keymap for the keyboard of your choosing')
@automagic_keyboard
@automagic_keymap
def new_keymap(cli):
    """Creates a new keymap for the keyboard of your choosing.
    """

M lib/python/qmk/commands.py => lib/python/qmk/commands.py +0 -69
@@ 1,12 1,8 @@
"""Helper functions for commands.
"""
import json
from pathlib import Path

from milc import cli

import qmk.keymap
from qmk.path import is_keyboard, is_keymap_dir, under_qmk_firmware


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)


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):
    """Open and parse a configurator json export
    """

A lib/python/qmk/decorators.py => lib/python/qmk/decorators.py +85 -0
@@ 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

M lib/python/qmk/path.py => lib/python/qmk/path.py +1 -1
@@ 65,7 65,7 @@ def normpath(path):
    path = Path(path)

    if path.is_absolute():
        return Path(path)
        return path

    return Path(os.environ['ORIG_CWD']) / path