Compare commits

...

84 Commits

Author SHA1 Message Date
Zach White 5a777c5de9 New release: 0.0.34 → 0.0.35 2020-05-27 20:58:33 -07:00
Zach White c51c898562 use cli.echo, not cli.args.echo. 2020-05-27 20:51:28 -07:00
Erovia 08ea23b087 Fix possibility of recursive execution 2020-05-26 21:43:15 +02:00
Erovia 0105405d83 Checking for the 'quantum' directory is better for long term 2020-05-10 17:41:30 +02:00
Erovia 92c57e2a46 Fix possibility of recursive execution 2020-05-10 17:30:30 +02:00
skullY 30c89ec8e0 New release: 0.0.33 → 0.0.34 2020-04-29 10:39:57 -07:00
Zach White a7a7bd9f08 improve the qmk setup flow (#20)
* improve the qmk setup flow

* Apply suggestions from code review

Co-Authored-By: Erovia <Erovia@users.noreply.github.com>

* Update qmk_cli/subcommands/setup.py

* explicity set encoding to utf-8

* polishing

Co-authored-by: Erovia <Erovia@users.noreply.github.com>
2020-04-29 10:38:56 -07:00
skullY 5229ce14e3 New release: 0.0.32 → 0.0.33 2020-04-15 13:53:35 -07:00
Erovia 8fa960f25e Merge pull request #19 from qmk/fix_store_true_false 2020-04-15 22:49:17 +02:00
skullY 1481dfe157 Make arg_only apply only to the subcommand it is part of 2020-04-15 12:07:15 -07:00
skullY ac39b9bb0f Fix setting config values for store_true and store_false 2020-04-15 09:30:49 -07:00
skullY 44a446308f New release: 0.0.31 → 0.0.32 2020-04-11 10:59:02 -07:00
Erovia 90bdf52416 Merge pull request #18 from Erovia/cli/win_env_check
Add warning message for MSYS users
2020-04-11 19:56:04 +02:00
Erovia 191980052e Add warning message for MSYS users 2020-04-11 19:44:56 +02:00
skullydazed 5fbdd95c4b Fix indentation 2020-04-11 10:31:54 -07:00
skullydazed 1002360e20 Update qmk_cli/script_qmk.py
Co-Authored-By: Erovia <Erovia@users.noreply.github.com>
2020-04-11 10:31:54 -07:00
Joel Challis a03aa1cbba Update qmk_cli/subcommands/setup.py
Co-Authored-By: skullydazed <skullydazed@users.noreply.github.com>
2020-04-11 10:31:54 -07:00
zvecr a0b692d718 Handle failed import with exception instead 2020-04-11 10:31:54 -07:00
zvecr ab7d0c6e46 Protect against empty QMK_HOME folder 2020-04-11 10:31:54 -07:00
skullY 73c903b8b6 Add --no flag 2020-04-11 09:57:31 -07:00
skullydazed 8f2185af32 Update qmk_cli/subcommands/setup.py 2020-04-11 09:57:31 -07:00
Erovia bae2f1f1ee Fix argument passing to doctor subcommand
When 'qmk setup' is started with the '-y' argument, this argument should be specified for 'qmk doctor' as well.
2020-04-11 09:57:31 -07:00
Zach White 120ce557f1 New release: 0.0.30 → 0.0.31 2020-03-26 07:51:42 -07:00
Erovia a15b0d6460 Fix WinError 193 on MSYS2 2020-03-26 07:51:17 -07:00
skullY f0dafaf068 New release: 0.0.29 → 0.0.30 2020-03-10 11:37:44 -07:00
skullY 3414bdcaa3 add nose2 to requirements 2020-03-10 11:37:31 -07:00
Mikkel Jeppesen c8567d8098 Made bade in the readme nicer 2020-02-25 14:33:37 -08:00
skullY dc3ca88deb New release: 0.0.28 → 0.0.29 2020-02-21 12:03:55 -08:00
skullY a4e9c34387 Add a warning when run on old python versions 2020-02-21 12:03:46 -08:00
Mikkel Jeppesen 742e1ce0e1 Added CI badge to readme 2020-02-21 11:13:59 -08:00
Mikkel Jeppesen 39df759f57 Switched to python 3.6 as minimum version, and remove paths condition on PR trigger 2020-02-21 11:13:59 -08:00
Mikkel Jeppesen b694c4284d removed debug echo 2020-02-21 11:13:59 -08:00
Mikkel Jeppesen e184710036 Fixed script path, checking env var 2020-02-21 11:13:59 -08:00
Mikkel Jeppesen d67addde2d moved msys script to root 2020-02-21 11:13:59 -08:00
Mikkel Jeppesen 29541559d0 Update .github/workflows/cli_setup.yml
Co-Authored-By: skullydazed <skullydazed@users.noreply.github.com>
2020-02-21 11:13:59 -08:00
Mikkel Jeppesen db0b8cb9a0 Added workflow to test qmk cli setup on OSX, Linux and MSYS2 2020-02-21 11:13:59 -08:00
skullY 6a0e62a078 Remove notes for things that are done 2020-02-21 11:13:41 -08:00
skullY bbc5ffd6c9 New release: 0.0.27 → 0.0.28 2020-02-13 19:09:10 -08:00
Zach White e0adba4e68 Prevent release from erroring on first run 2020-02-13 18:50:00 -08:00
Zach White 56bcb12ed6 Add support for determining where a config option came from 2020-02-13 18:49:13 -08:00
Mikkel Jeppesen 897fe121e2 Replace rename with replace 2020-02-13 18:48:02 -08:00
skullY 1a723e488f New release: 0.0.26 → 0.0.27 2020-01-26 11:40:14 -08:00
skullY bf502d26fa sync milc with qmk_firmware 2020-01-26 11:40:00 -08:00
skullY ebea7f7ea0 New release: 0.0.25 → 0.0.26 2019-11-28 08:42:55 -08:00
skullY 49b288786a Add support for hidden subcommands 2019-11-28 08:42:49 -08:00
skullY e75b4d4ed1 New release: 0.0.24 → 0.0.25 2019-11-18 22:33:19 -08:00
skullY 7f56cfd781 Add flake8 to requirements 2019-11-18 22:33:14 -08:00
skullY b04bf7a4da New release: 0.0.23 → 0.0.24 2019-11-15 23:25:13 -08:00
skullY fb260f29e1 Fix commands with no arguments 2019-11-15 23:15:07 -08:00
skullY e8e98ca15a New release: 0.0.22 → 0.0.23 2019-11-15 22:21:33 -08:00
skullY 4d1302696e Make folding arguments into config operate more correctly. 2019-11-15 22:21:26 -08:00
skullY 4d78df2cbc New release: 0.0.21 → 0.0.22 2019-11-12 16:54:28 -08:00
skullY 4a45081412 filter None from config files 2019-11-12 16:53:32 -08:00
skullY 9cab9ea610 New release: 0.0.20 → 0.0.21 2019-11-11 13:04:13 -08:00
skullY 177fa55bce fix pytest in qmkfm/base_container 2019-11-11 13:03:39 -08:00
skullY 0783864629 New release: 0.0.19 → 0.0.20 2019-11-11 11:56:35 -08:00
skullY 8ec66dd599 Improve --version 2019-11-11 11:56:20 -08:00
skullY 790536eca4 New release: 0.0.18 → 0.0.19 2019-11-11 09:17:31 -08:00
skullY 6f1ec49a9a Add support for specifying user.qmk_home in config 2019-11-11 09:15:24 -08:00
skullY c7f37a0e1b MILC improvements-
* Remove the shadow argparser
* Make it easier to reason about arguments and how they're translated into the config tree
* Populate self.config during init to support setting user.qmk_home for the global CLI
* Remove the short argument -c so that we can unambiguously determine the config file location without doing full argument processing
* Remove the --save-config option as it's a little confusing anyway
* Use Pathlib for path manipulation
2019-11-11 09:14:24 -08:00
skullY 5001c627ef New release: 0.0.17 → 0.0.18 2019-11-10 11:11:14 -08:00
Erovia 490c94e282 Fix command line variable access for baseurl and branch 2019-11-10 11:10:49 -08:00
skullY 212e185b62 New release: 0.0.16 → 0.0.17 2019-10-23 22:54:53 -07:00
zvecr b7d0a65bdf Fix No such file or directory: 'C:\\dev\\null' 2019-10-23 22:51:53 -07:00
skullY 221d42fec1 sync milc.py with qmk_firmware 2019-10-23 22:47:50 -07:00
skullY f3e83794c9 New release: 0.0.15 → 0.0.16 2019-10-12 13:18:31 -07:00
zvecr 73628cc6b0 Fix in_qmk_firmware for Python 3.5 2019-10-12 13:15:15 -07:00
skullY e077319a16 New release: 0.0.14 → 0.0.15 2019-10-08 07:23:09 -07:00
skullY 5b0ba9a25d Document release requirements 2019-10-08 07:22:56 -07:00
skullY bd4e9bf9cf Add hjson to requirements 2019-10-08 07:20:26 -07:00
Nikita Titov b2974b4219 read README with utf-8 and close file after reading 2019-10-04 07:45:02 -07:00
skullY be0dd95e46 New release: 0.0.13 → 0.0.14 2019-09-25 12:00:35 -07:00
skullY 30031a8cd7 Fix qmk clone 2019-09-25 12:00:32 -07:00
skullY 94adba52c6 New release: 0.0.12 → 0.0.13 2019-09-25 09:54:23 -07:00
skullY 38aefc9716 Fix the environment 2019-09-25 09:54:16 -07:00
skullY 081fa0c8a6 New release: 0.0.11 → 0.0.12 2019-09-22 14:56:52 -07:00
skullY 8a03e2ad46 Add milc to setup.py 2019-09-22 14:56:44 -07:00
skullY c8d966afa7 New release: 0.0.10 → 0.0.11 2019-09-22 14:43:32 -07:00
skullY 1ea8f02927 Rework how we import subcommands 2019-09-09 10:30:10 -07:00
skullY 3683f86b4d update milc 2019-09-09 10:30:01 -07:00
skullY 1d622467e4 Fix logging so certain modules don't output unwanted debug messages 2019-09-08 21:21:04 -07:00
skullY a5c8efddb4 Rework how the CLI calls subcommands 2019-09-08 19:58:56 -07:00
skullY 8d75fd3dca Sync milc with qmk_firmware 2019-09-08 19:56:54 -07:00
skullY 72c46efc74 change the order of qmk doctor checks 2019-07-15 13:09:30 -07:00
16 changed files with 454 additions and 374 deletions
+68
View File
@@ -0,0 +1,68 @@
name: CLI Setup
on:
push:
branches:
- master
pull_request:
jobs:
test_cli_linux_macos:
runs-on: ${{ matrix.os }}
env:
QMK_HOME: ~/qmk_firmware
strategy:
matrix:
os: [macos-latest, ubuntu-latest]
python-version: [3.6, 3.7, 3.8]
steps:
- uses: actions/checkout@v1
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Install python dependencies
run: |
python -m pip install wheel
echo "::add-path::$HOME/.local/bin"
- name: Install QMK CLI from source
run: |
python3 -m pip install -r requirements.txt
python setup.py sdist bdist_wheel
cd ..
python3 -m pip install --force-reinstall --no-index --no-deps --prefix=~/.local --find-links qmk_cli/dist qmk
- name: Run qmk setup -y
run: qmk setup -y
test_cli_win:
runs-on: windows-latest
strategy:
matrix:
python-version: [3.6, 3.8]
steps:
- uses: actions/checkout@v1
- name: Set up MSYS2
uses: qmk/setup-msys2@v1
with:
update: true
- name: (MSYS2) Install git and python3
run: msys2do pacman -S git python3-pip --noconfirm
- name: (MSYS2) Upgrade pip and setuptools and install wheel
run: |
msys2do python3 -m pip install --upgrade pip setuptools
msys2do python3 -m pip install wheel
- name: (MSYS2) Install QMK CLI from source
run: msys2do ./setup_msys.sh $(cygpath "${{ github.workspace }}")
- name: (MSYS2) Run qmk setup -y
run: msys2do qmk setup -y
+1 -1
View File
@@ -1,5 +1,5 @@
# QMK CLI # QMK CLI
[![CLI Setup](https://github.com/qmk/qmk_cli/workflows/CLI%20Setup/badge.svg)](https://github.com/qmk/qmk_cli/actions?query=workflow%3A%22CLI+Setup%22)
A program to help users work with [QMK Firmware](https://qmk.fm/). A program to help users work with [QMK Firmware](https://qmk.fm/).
# Features # Features
+227 -138
View File
@@ -17,8 +17,10 @@ import argparse
import logging import logging
import os import os
import re import re
import shlex
import sys import sys
from decimal import Decimal from decimal import Decimal
from pathlib import Path
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from time import sleep from time import sleep
@@ -35,6 +37,10 @@ except ImportError:
import argcomplete import argcomplete
import colorama import colorama
from appdirs import user_config_dir
# Disable logging until we can configure it how the user wants
logging.basicConfig(stream=os.devnull)
# Log Level Representations # Log Level Representations
EMOJI_LOGLEVELS = { EMOJI_LOGLEVELS = {
@@ -47,6 +53,7 @@ EMOJI_LOGLEVELS = {
} }
EMOJI_LOGLEVELS['FATAL'] = EMOJI_LOGLEVELS['CRITICAL'] EMOJI_LOGLEVELS['FATAL'] = EMOJI_LOGLEVELS['CRITICAL']
EMOJI_LOGLEVELS['WARN'] = EMOJI_LOGLEVELS['WARNING'] EMOJI_LOGLEVELS['WARN'] = EMOJI_LOGLEVELS['WARNING']
UNICODE_SUPPORT = sys.stdout.encoding.lower().startswith('utf')
# ANSI Color setup # ANSI Color setup
# Regex was gratefully borrowed from kfir on stackoverflow: # Regex was gratefully borrowed from kfir on stackoverflow:
@@ -90,25 +97,23 @@ def format_ansi(text):
class ANSIFormatter(logging.Formatter): class ANSIFormatter(logging.Formatter):
"""A log formatter that inserts ANSI color. """A log formatter that inserts ANSI color.
""" """
def format(self, record): def format(self, record):
msg = super(ANSIFormatter, self).format(record) msg = super(ANSIFormatter, self).format(record)
return format_ansi(msg) return format_ansi(msg)
class ANSIEmojiLoglevelFormatter(ANSIFormatter): class ANSIEmojiLoglevelFormatter(ANSIFormatter):
"""A log formatter that makes the loglevel an emoji. """A log formatter that makes the loglevel an emoji on UTF capable terminals.
""" """
def format(self, record): def format(self, record):
record.levelname = EMOJI_LOGLEVELS[record.levelname].format(**ansi_colors) if UNICODE_SUPPORT:
record.levelname = EMOJI_LOGLEVELS[record.levelname].format(**ansi_colors)
return super(ANSIEmojiLoglevelFormatter, self).format(record) return super(ANSIEmojiLoglevelFormatter, self).format(record)
class ANSIStrippingFormatter(ANSIFormatter): class ANSIStrippingFormatter(ANSIFormatter):
"""A log formatter that strips ANSI. """A log formatter that strips ANSI.
""" """
def format(self, record): def format(self, record):
msg = super(ANSIStrippingFormatter, self).format(record) msg = super(ANSIStrippingFormatter, self).format(record)
return ansi_escape.sub('', msg) return ansi_escape.sub('', msg)
@@ -120,7 +125,6 @@ class Configuration(object):
This class never raises IndexError, instead it will return None if a This class never raises IndexError, instead it will return None if a
section or option does not yet exist. section or option does not yet exist.
""" """
def __contains__(self, key): def __contains__(self, key):
return self._config.__contains__(key) return self._config.__contains__(key)
@@ -144,13 +148,15 @@ class Configuration(object):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self._config = {} self._config = {}
self.default_container = ConfigurationOption
def __getattr__(self, key):
return self.__getitem__(key)
def __getitem__(self, key): def __getitem__(self, key):
"""Returns a config section, creating it if it doesn't exist yet. """Returns a config section, creating it if it doesn't exist yet.
""" """
if key not in self._config: if key not in self._config:
self.__dict__[key] = self._config[key] = ConfigurationOption() self.__dict__[key] = self._config[key] = ConfigurationSection(self)
return self._config[key] return self._config[key]
@@ -161,30 +167,44 @@ class Configuration(object):
def __delitem__(self, key): def __delitem__(self, key):
if key in self.__dict__ and key[0] != '_': if key in self.__dict__ and key[0] != '_':
del self.__dict__[key] del self.__dict__[key]
del self._config[key] if key in self._config:
del self._config[key]
class ConfigurationOption(Configuration): class ConfigurationSection(Configuration):
def __init__(self, *args, **kwargs): def __init__(self, parent, *args, **kwargs):
super(ConfigurationOption, self).__init__(*args, **kwargs) super(ConfigurationSection, self).__init__(*args, **kwargs)
self.default_container = dict self.parent = parent
def __getitem__(self, key): def __getitem__(self, key):
"""Returns a config section, creating it if it doesn't exist yet. """Returns a config value, pulling from the `user` section as a fallback.
This is called when the attribute is accessed either via the get method or through [ ] index.
""" """
if key not in self._config: if key in self._config and self._config.get(key) is not None:
self.__dict__[key] = self._config[key] = None return self._config[key]
return self._config[key] elif key in self.parent.user:
return self.parent.user[key]
return None
def __getattr__(self, key):
"""Returns the config value from the `user` section.
This is called when the attribute is accessed via dot notation but does not exists.
"""
if key in self.parent.user:
return self.parent.user[key]
return None
def handle_store_boolean(self, *args, **kwargs): def handle_store_boolean(self, *args, **kwargs):
"""Does the add_argument for action='store_boolean'. """Does the add_argument for action='store_boolean'.
""" """
kwargs['add_dest'] = False
disabled_args = None disabled_args = None
disabled_kwargs = kwargs.copy() disabled_kwargs = kwargs.copy()
disabled_kwargs['action'] = 'store_false' disabled_kwargs['action'] = 'store_false'
disabled_kwargs['dest'] = self.get_argument_name(*args, **kwargs)
disabled_kwargs['help'] = 'Disable ' + kwargs['help'] disabled_kwargs['help'] = 'Disable ' + kwargs['help']
kwargs['action'] = 'store_true' kwargs['action'] = 'store_true'
kwargs['help'] = 'Enable ' + kwargs['help'] kwargs['help'] = 'Enable ' + kwargs['help']
@@ -201,9 +221,8 @@ def handle_store_boolean(self, *args, **kwargs):
class SubparserWrapper(object): class SubparserWrapper(object):
"""Wrap subparsers so we can populate the normal and the shadow parser. """Wrap subparsers so we can track what options the user passed.
""" """
def __init__(self, cli, submodule, subparser): def __init__(self, cli, submodule, subparser):
self.cli = cli self.cli = cli
self.submodule = submodule self.submodule = submodule
@@ -219,48 +238,64 @@ class SubparserWrapper(object):
self.subparser.completer = completer self.subparser.completer = completer
def add_argument(self, *args, **kwargs): def add_argument(self, *args, **kwargs):
if kwargs.get('add_dest', True): """Add an argument for this subcommand.
kwargs['dest'] = self.submodule + '_' + self.cli.get_argument_name(*args, **kwargs)
if 'add_dest' in kwargs:
del kwargs['add_dest']
if 'action' in kwargs and kwargs['action'] == 'store_boolean': This also stores the default for the argument in `self.cli.default_arguments`.
"""
if kwargs.get('action') == 'store_boolean':
# Store boolean will call us again with the enable/disable flag arguments
return handle_store_boolean(self, *args, **kwargs) return handle_store_boolean(self, *args, **kwargs)
self.cli.acquire_lock() self.cli.acquire_lock()
argument_name = self.cli.get_argument_name(*args, **kwargs)
self.subparser.add_argument(*args, **kwargs) self.subparser.add_argument(*args, **kwargs)
if 'default' in kwargs: if kwargs.get('action') == 'store_false':
del kwargs['default'] self.cli._config_store_false.append(argument_name)
if 'action' in kwargs and kwargs['action'] == 'store_false':
kwargs['action'] == 'store_true' if kwargs.get('action') == 'store_true':
self.cli.subcommands_default[self.submodule].add_argument(*args, **kwargs) self.cli._config_store_true.append(argument_name)
if self.submodule not in self.cli.default_arguments:
self.cli.default_arguments[self.submodule] = {}
self.cli.default_arguments[self.submodule][argument_name] = kwargs.get('default')
self.cli.release_lock() self.cli.release_lock()
class MILC(object): class MILC(object):
"""MILC - An Opinionated Batteries Included Framework """MILC - An Opinionated Batteries Included Framework
""" """
def __init__(self): def __init__(self):
"""Initialize the MILC object. """Initialize the MILC object.
version
The version string to associate with your CLI program
""" """
# Setup a lock for thread safety # Setup a lock for thread safety
self._lock = threading.RLock() if thread else None self._lock = threading.RLock() if thread else None
# Define some basic info # Define some basic info
self.acquire_lock() self.acquire_lock()
self._config_store_true = []
self._config_store_false = []
self._description = None self._description = None
self._entrypoint = None self._entrypoint = None
self._inside_context_manager = False self._inside_context_manager = False
self.ansi = ansi_colors self.ansi = ansi_colors
self.config = Configuration() self.arg_only = {}
self.config = self.config_source = None
self.config_file = None self.config_file = None
self.prog_name = sys.argv[0][:-3] if sys.argv[0].endswith('.py') else sys.argv[0] self.default_arguments = {}
self.version = os.environ.get('QMK_VERSION', 'unknown') self.version = 'unknown'
self.release_lock() self.release_lock()
# Figure out our program name
self.prog_name = sys.argv[0][:-3] if sys.argv[0].endswith('.py') else sys.argv[0]
self.prog_name = self.prog_name.split('/')[-1]
# Initialize all the things # Initialize all the things
self.initialize_config()
self.initialize_argparse() self.initialize_argparse()
self.initialize_logging() self.initialize_logging()
@@ -270,10 +305,10 @@ class MILC(object):
@description.setter @description.setter
def description(self, value): def description(self, value):
self._description = self._arg_parser.description = self._arg_defaults.description = value self._description = self._arg_parser.description = value
def echo(self, text, *args, **kwargs): def echo(self, text, *args, **kwargs):
"""Print colorized text to stdout, as long as stdout is a tty. """Print colorized text to stdout.
ANSI color strings (such as {fg-blue}) will be converted into ANSI ANSI color strings (such as {fg-blue}) will be converted into ANSI
escape sequences, and the ANSI reset sequence will be added to all escape sequences, and the ANSI reset sequence will be added to all
@@ -284,11 +319,10 @@ class MILC(object):
if args and kwargs: if args and kwargs:
raise RuntimeError('You can only specify *args or **kwargs, not both!') raise RuntimeError('You can only specify *args or **kwargs, not both!')
if sys.stdout.isatty(): args = args or kwargs
args = args or kwargs text = format_ansi(text)
text = format_ansi(text)
print(text % args) print(text % args)
def initialize_argparse(self): def initialize_argparse(self):
"""Prepare to process arguments from sys.argv. """Prepare to process arguments from sys.argv.
@@ -300,12 +334,9 @@ class MILC(object):
self.acquire_lock() self.acquire_lock()
self.subcommands = {} self.subcommands = {}
self.subcommands_default = {}
self._subparsers = None self._subparsers = None
self._subparsers_default = None
self.argwarn = argcomplete.warn self.argwarn = argcomplete.warn
self.args = None self.args = None
self._arg_defaults = argparse.ArgumentParser(**kwargs)
self._arg_parser = argparse.ArgumentParser(**kwargs) self._arg_parser = argparse.ArgumentParser(**kwargs)
self.set_defaults = self._arg_parser.set_defaults self.set_defaults = self._arg_parser.set_defaults
self.print_usage = self._arg_parser.print_usage self.print_usage = self._arg_parser.print_usage
@@ -313,30 +344,23 @@ class MILC(object):
self.release_lock() self.release_lock()
def completer(self, completer): def completer(self, completer):
"""Add an arpcomplete completer to this subcommand. """Add an argcomplete completer to this subcommand.
""" """
self._arg_parser.completer = completer self._arg_parser.completer = completer
def add_argument(self, *args, **kwargs): def add_argument(self, *args, **kwargs):
"""Wrapper to add arguments to both the main and the shadow argparser. """Wrapper to add arguments and track whether they were passed on the command line.
""" """
if kwargs.get('add_dest', True) and args[0][0] == '-':
kwargs['dest'] = 'general_' + self.get_argument_name(*args, **kwargs)
if 'add_dest' in kwargs:
del kwargs['add_dest']
if 'action' in kwargs and kwargs['action'] == 'store_boolean': if 'action' in kwargs and kwargs['action'] == 'store_boolean':
return handle_store_boolean(self, *args, **kwargs) return handle_store_boolean(self, *args, **kwargs)
self.acquire_lock() self.acquire_lock()
self._arg_parser.add_argument(*args, **kwargs)
# Populate the shadow parser self._arg_parser.add_argument(*args, **kwargs)
if 'default' in kwargs: if 'general' not in self.default_arguments:
del kwargs['default'] self.default_arguments['general'] = {}
if 'action' in kwargs and kwargs['action'] == 'store_false': self.default_arguments['general'][self.get_argument_name(*args, **kwargs)] = kwargs.get('default')
kwargs['action'] == 'store_true'
self._arg_defaults.add_argument(*args, **kwargs)
self.release_lock() self.release_lock()
def initialize_logging(self): def initialize_logging(self):
@@ -363,15 +387,14 @@ class MILC(object):
self.add_argument('--log-file-fmt', default='[%(levelname)s] [%(asctime)s] [file:%(pathname)s] [line:%(lineno)d] %(message)s', help='Format string for log file.') self.add_argument('--log-file-fmt', default='[%(levelname)s] [%(asctime)s] [file:%(pathname)s] [line:%(lineno)d] %(message)s', help='Format string for log file.')
self.add_argument('--log-file', help='File to write log messages to') self.add_argument('--log-file', help='File to write log messages to')
self.add_argument('--color', action='store_boolean', default=True, help='color in output') self.add_argument('--color', action='store_boolean', default=True, help='color in output')
self.add_argument('-c', '--config-file', help='The config file to read and/or write') self.add_argument('--config-file', help='The location for the configuration file')
self.add_argument('--save-config', action='store_true', help='Save the running configuration to the config file') self.arg_only['config_file'] = ['general']
def add_subparsers(self, title='Sub-commands', **kwargs): def add_subparsers(self, title='Sub-commands', **kwargs):
if self._inside_context_manager: if self._inside_context_manager:
raise RuntimeError('You must run this before the with statement!') raise RuntimeError('You must run this before the with statement!')
self.acquire_lock() self.acquire_lock()
self._subparsers_default = self._arg_defaults.add_subparsers(title=title, dest='subparsers', **kwargs)
self._subparsers = self._arg_parser.add_subparsers(title=title, dest='subparsers', **kwargs) self._subparsers = self._arg_parser.add_subparsers(title=title, dest='subparsers', **kwargs)
self.release_lock() self.release_lock()
@@ -393,10 +416,12 @@ class MILC(object):
if self.config_file: if self.config_file:
return self.config_file return self.config_file
if self.args and self.args.general_config_file: if '--config-file' in sys.argv:
return self.args.general_config_file return Path(sys.argv[sys.argv.index('--config-file') + 1]).expanduser().resolve()
return os.path.abspath(os.path.expanduser('~/.%s.ini' % self.prog_name)) filedir = user_config_dir(appname='qmk', appauthor='QMK')
filename = '%s.ini' % self.prog_name
return Path(filedir) / filename
def get_argument_name(self, *args, **kwargs): def get_argument_name(self, *args, **kwargs):
"""Takes argparse arguments and returns the dest name. """Takes argparse arguments and returns the dest name.
@@ -413,11 +438,20 @@ class MILC(object):
raise RuntimeError('You must run this before the with statement!') raise RuntimeError('You must run this before the with statement!')
def argument_function(handler): def argument_function(handler):
subcommand_name = handler.__name__.replace("_", "-")
if kwargs.get('arg_only'):
arg_name = self.get_argument_name(*args, **kwargs)
if arg_name not in self.arg_only:
self.arg_only[arg_name] = []
self.arg_only[arg_name].append(subcommand_name)
del kwargs['arg_only']
if handler is self._entrypoint: if handler is self._entrypoint:
self.add_argument(*args, **kwargs) self.add_argument(*args, **kwargs)
elif handler.__name__ in self.subcommands: elif subcommand_name in self.subcommands:
self.subcommands[handler.__name__].add_argument(*args, **kwargs) self.subcommands[subcommand_name].add_argument(*args, **kwargs)
else: else:
raise RuntimeError('Decorated function is not entrypoint or subcommand!') raise RuntimeError('Decorated function is not entrypoint or subcommand!')
@@ -429,7 +463,7 @@ class MILC(object):
def arg_passed(self, arg): def arg_passed(self, arg):
"""Returns True if arg was passed on the command line. """Returns True if arg was passed on the command line.
""" """
return self.args_passed[arg] in (None, False) return self.default_arguments.get(arg) != self.args[arg]
def parse_args(self): def parse_args(self):
"""Parse the CLI args. """Parse the CLI args.
@@ -442,91 +476,155 @@ class MILC(object):
self.acquire_lock() self.acquire_lock()
self.args = self._arg_parser.parse_args() self.args = self._arg_parser.parse_args()
self.args_passed = self._arg_defaults.parse_args()
if 'entrypoint' in self.args: if 'entrypoint' in self.args:
self._entrypoint = self.args.entrypoint self._entrypoint = self.args.entrypoint
if self.args.general_config_file:
self.config_file = self.args.general_config_file
self.release_lock() self.release_lock()
def read_config(self): def read_config_file(self, config_file):
"""Parse the configuration file and determine the runtime configuration. """Read in the configuration file and return Configuration objects for it and the config_source.
""" """
self.acquire_lock() config = Configuration()
self.config_file = self.find_config_file() config_source = Configuration()
if self.config_file and os.path.exists(self.config_file): if config_file.exists():
config = RawConfigParser(self.config) raw_config = RawConfigParser()
config.read(self.config_file) raw_config.read(str(config_file))
# Iterate over the config file options and write them into self.config # Iterate over the config file options and write them into config
for section in config.sections(): for section in raw_config.sections():
for option in config.options(section): for option in raw_config.options(section):
value = config.get(section, option) value = raw_config.get(section, option)
# Coerce values into useful datatypes # Coerce values into useful datatypes
if value.lower() in ['1', 'yes', 'true', 'on']: if value.lower() in ['1', 'yes', 'true', 'on']:
value = True value = True
elif value.lower() in ['0', 'no', 'false', 'none', 'off']: elif value.lower() in ['0', 'no', 'false', 'off']:
value = False value = False
elif value.lower() in ['none']:
continue
elif value.replace('.', '').isdigit(): elif value.replace('.', '').isdigit():
if '.' in value: if '.' in value:
value = Decimal(value) value = Decimal(value)
else: else:
value = int(value) value = int(value)
self.config[section][option] = value config[section][option] = value
config_source[section][option] = 'config_file'
# Fold the CLI args into self.config return config, config_source
def initialize_config(self):
"""Read in the configuration file and store it in self.config.
"""
self.acquire_lock()
self.config_file = self.find_config_file()
self.config, self.config_source = self.read_config_file(self.config_file)
self.release_lock()
def merge_args_into_config(self):
"""Merge CLI arguments into self.config to create the runtime configuration.
"""
self.acquire_lock()
for argument in vars(self.args): for argument in vars(self.args):
if argument in ('subparsers', 'entrypoint'): if argument in ('subparsers', 'entrypoint'):
continue continue
if '_' not in argument: # Find the argument's section
continue # Underscores in command's names are converted to dashes during initialization.
# TODO(Erovia) Find a better solution
entrypoint_name = self._entrypoint.__name__.replace("_", "-")
if entrypoint_name in self.default_arguments and argument in self.default_arguments[entrypoint_name]:
argument_found = True
section = self._entrypoint.__name__
if argument in self.default_arguments['general']:
argument_found = True
section = 'general'
section, option = argument.split('_', 1) if not argument_found:
if hasattr(self.args_passed, argument): raise RuntimeError('Could not find argument in `self.default_arguments`. This should be impossible!')
self.config[section][option] = getattr(self.args, argument) exit(1)
else:
if option not in self.config[section]: if argument not in self.arg_only or section not in self.arg_only[argument]:
self.config[section][option] = getattr(self.args, argument) # Determine the arg value and source
arg_value = getattr(self.args, argument)
if argument in self._config_store_true and arg_value:
passed_on_cmdline = True
elif argument in self._config_store_false and not arg_value:
passed_on_cmdline = True
elif arg_value is not None:
passed_on_cmdline = True
else:
passed_on_cmdline = False
# Merge this argument into self.config
if passed_on_cmdline and (argument in self.default_arguments['general'] or argument in self.default_arguments[entrypoint_name] or argument not in self.config[entrypoint_name]):
self.config[section][argument] = arg_value
self.config_source[section][argument] = 'argument'
self.release_lock() self.release_lock()
def _save_config_file(self, config):
"""Write config to disk.
"""
# Generate a sanitized version of our running configuration
sane_config = RawConfigParser()
for section_name, section in config._config.items():
sane_config.add_section(section_name)
for option_name, value in section.items():
if section_name == 'general':
if option_name in ['config_file']:
continue
if value is not None:
sane_config.set(section_name, option_name, str(value))
config_dir = self.config_file.parent
if not config_dir.exists():
config_dir.mkdir(parents=True, exist_ok=True)
# Write the config file atomically.
self.acquire_lock()
with NamedTemporaryFile(mode='w', dir=str(config_dir), delete=False) as tmpfile:
sane_config.write(tmpfile)
if os.path.getsize(tmpfile.name) > 0:
os.replace(tmpfile.name, str(self.config_file))
else:
self.log.warning('Config file saving failed, not replacing %s with %s.', str(self.config_file), tmpfile.name)
self.release_lock()
def write_config_option(self, section, option):
"""Save a single config option to the config file.
"""
if not self.config_file:
self.log.warning('%s.config_file not set, not saving config!', self.__class__.__name__)
return
config, config_source = self.read_config_file(self.config_file)
if section in config and option in config[section] and config[section][option] is None:
del(config[section][option])
else:
config[section][option] = str(self.config[section][option])
self._save_config_file(config)
# Housekeeping
self.log.info('Wrote configuration to %s', shlex.quote(str(self.config_file)))
def save_config(self): def save_config(self):
"""Save the current configuration to the config file. """Save the current configuration to the config file.
""" """
self.log.debug("Saving config file to '%s'", self.config_file) self.log.debug("Saving config file to '%s'", str(self.config_file))
if not self.config_file: if not self.config_file:
self.log.warning('%s.config_file file not set, not saving config!', self.__class__.__name__) self.log.warning('%s.config_file file not set, not saving config!', self.__class__.__name__)
return return
self.acquire_lock() # Write config to disk
self._save_config_file(self.config)
config = RawConfigParser() self.log.info('Wrote configuration to %s', shlex.quote(str(self.config_file)))
for section_name, section in self.config._config.items():
config.add_section(section_name)
for option_name, value in section.items():
if section_name == 'general':
if option_name in ['save_config']:
continue
config.set(section_name, option_name, str(value))
with NamedTemporaryFile(mode='w', dir=os.path.dirname(self.config_file), delete=False) as tmpfile:
config.write(tmpfile)
# Move the new config file into place atomically
if os.path.getsize(tmpfile.name) > 0:
os.rename(tmpfile.name, self.config_file)
else:
self.log.warning('Config file saving failed, not replacing %s with %s.', self.config_file, tmpfile.name)
self.release_lock()
def __call__(self): def __call__(self):
"""Execute the entrypoint function. """Execute the entrypoint function.
@@ -534,8 +632,7 @@ class MILC(object):
if not self._inside_context_manager: if not self._inside_context_manager:
# If they didn't use the context manager use it ourselves # If they didn't use the context manager use it ourselves
with self: with self:
self.__call__() return self.__call__()
return
if not self._entrypoint: if not self._entrypoint:
raise RuntimeError('No entrypoint provided!') raise RuntimeError('No entrypoint provided!')
@@ -561,41 +658,37 @@ class MILC(object):
return entrypoint_func return entrypoint_func
def add_subcommand(self, handler, description, name=None, **kwargs): def add_subcommand(self, handler, description, name=None, hidden=False, **kwargs):
"""Register a subcommand. """Register a subcommand.
If name is not provided we use `handler.__name__`. If name is not provided we use `handler.__name__`.
""" """
if self._inside_context_manager: if self._inside_context_manager:
raise RuntimeError('You must run this before the with statement!') raise RuntimeError('You must run this before the with statement!')
if self._subparsers is None: if self._subparsers is None:
self.add_subparsers() self.add_subparsers(metavar="")
if not name: if not name:
name = handler.__name__ name = handler.__name__.replace("_", "-")
self.acquire_lock() self.acquire_lock()
kwargs['help'] = description if not hidden:
self.subcommands_default[name] = self._subparsers_default.add_parser(name, **kwargs) self._subparsers.metavar = "{%s,%s}" % (self._subparsers.metavar[1:-1], name) if self._subparsers.metavar else "{%s%s}" % (self._subparsers.metavar[1:-1], name)
kwargs['help'] = description
self.subcommands[name] = SubparserWrapper(self, name, self._subparsers.add_parser(name, **kwargs)) self.subcommands[name] = SubparserWrapper(self, name, self._subparsers.add_parser(name, **kwargs))
self.subcommands[name].set_defaults(entrypoint=handler) self.subcommands[name].set_defaults(entrypoint=handler)
if name not in self.__dict__:
self.__dict__[name] = self.subcommands[name]
else:
self.log.debug("Could not add subcommand '%s' to attributes, key already exists!", name)
self.release_lock() self.release_lock()
return handler return handler
def subcommand(self, description, **kwargs): def subcommand(self, description, hidden=False, **kwargs):
"""Decorator to register a subcommand. """Decorator to register a subcommand.
""" """
def subcommand_function(handler): def subcommand_function(handler):
return self.add_subcommand(handler, description, **kwargs) return self.add_subcommand(handler, description, hidden=hidden, **kwargs)
return subcommand_function return subcommand_function
@@ -603,8 +696,8 @@ class MILC(object):
"""Called by __enter__() to setup the logging configuration. """Called by __enter__() to setup the logging configuration.
""" """
if len(logging.root.handlers) != 0: if len(logging.root.handlers) != 0:
# This is not a design decision. This is what I'm doing for now until I can examine and think about this situation in more detail. # MILC is the only thing that should have root log handlers
raise RuntimeError('MILC should be the only system installing root log handlers!') logging.root.handlers = []
self.acquire_lock() self.acquire_lock()
@@ -617,9 +710,9 @@ class MILC(object):
self.log_format = self.config['general']['log_fmt'] self.log_format = self.config['general']['log_fmt']
if self.config.general.color: if self.config.general.color:
self.log_format = ANSIEmojiLoglevelFormatter(self.args.general_log_fmt, self.config.general.datetime_fmt) self.log_format = ANSIEmojiLoglevelFormatter(self.args.log_fmt, self.config.general.datetime_fmt)
else: else:
self.log_format = ANSIStrippingFormatter(self.args.general_log_fmt, self.config.general.datetime_fmt) self.log_format = ANSIStrippingFormatter(self.args.log_fmt, self.config.general.datetime_fmt)
if self.log_file: if self.log_file:
self.log_file_handler = logging.FileHandler(self.log_file, self.log_file_mode) self.log_file_handler = logging.FileHandler(self.log_file, self.log_file_mode)
@@ -646,12 +739,9 @@ class MILC(object):
colorama.init() colorama.init()
self.parse_args() self.parse_args()
self.read_config() self.merge_args_into_config()
self.setup_logging() self.setup_logging()
if self.config.general.save_config:
self.save_config()
return self return self
def __exit__(self, exc_type, exc_val, exc_tb): def __exit__(self, exc_type, exc_val, exc_tb):
@@ -713,4 +803,3 @@ if __name__ == '__main__':
cli.goodbye.add_argument('-n', '--name', help='Name to bid farewell to', default='World') cli.goodbye.add_argument('-n', '--name', help='Name to bid farewell to', default='World')
cli() # Automatically picks between main(), hello() and goodbye() cli() # Automatically picks between main(), hello() and goodbye()
print(sorted(ansi_colors.keys()))
+1 -1
View File
@@ -1,3 +1,3 @@
"""A program to help you work with qmk_firmware.""" """A program to help you work with qmk_firmware."""
__version__ = '0.0.10' __version__ = '0.0.35'
-96
View File
@@ -1,96 +0,0 @@
"""QMK Python Doctor
Check up for QMK environment.
"""
import shutil
import platform
import os
from pathlib import Path
from qmk_cli.milc import cli
def check_qmk_firmware():
"""Make sure qmk_firmware and the qmk cli are there.
"""
qmk_firmware = Path(os.environ['QMK_HOME'])
qmk_cli = qmk_firmware / 'bin' / 'qmk'
if qmk_firmware.exists() and not qmk_cli.exists():
cli.log.error("{fg_red}Can't find %s/bin/qmk! You need to `git pull`.", os.environ['QMK_HOME'])
return False
elif not qmk_firmware.exists():
cli.log.error("{fg_red}Can't find the qmk_firmware checkout! %s does not exist!", os.environ['QMK_HOME'])
return False
cli.log.info('Found qmk_firmware checkout in {fg_cyan}%s', str(qmk_firmware))
return True
def check_vital_programs():
"""Make sure the software we need has been installed.
TODO(unclaimed):
* [ ] Run the binaries to make sure they work
* [ ] Compile a trivial program with each compiler
"""
ok = True
binaries = ['dfu-programmer', 'avrdude', 'dfu-util', 'avr-gcc', 'arm-none-eabi-gcc']
for binary in binaries:
res = shutil.which(binary)
if res is None:
cli.log.error("{fg_red}QMK can't find %s in your path", binary)
ok = False
if ok:
cli.log.info("All necessary software is installed.")
return ok
def check_platform_tests():
"""Dispatch to platform specific tests.
"""
OS = platform.system()
if OS == "Darwin":
cli.log.info("Detected {fg_cyan}macOS")
return check_mac_os()
elif OS == "Linux":
cli.log.info("Detected {fg_cyan}linux")
return check_linux()
else:
cli.log.info("Assuming {fg_cyan}Windows")
return check_windows()
def check_mac_os():
"""Run macOS specific tests.
There aren't any yet.
"""
return True
def check_linux():
"""Run Linux specific tests.
TODO(unclaimed):
* [ ] Check for udev entries on linux
"""
test = 'systemctl list-unit-files | grep enabled | grep -i ModemManager'
if os.system(test) == 0:
cli.log.warn("{bg_yellow}Detected modem manager. Please disable it if you are using Pro Micros")
def check_windows():
"""Run Windows specific tests.
TODO(unclaimed):
* [ ] Check out the driver situation
"""
return True
+6 -6
View File
@@ -2,14 +2,14 @@
""" """
import subprocess import subprocess
from qmk_cli.milc import cli from milc import cli
default_repo = 'qmk_firmware' default_repo = 'qmk_firmware'
default_fork = 'qmk/' + default_repo default_fork = 'qmk/' + default_repo
default_branch = 'master' default_branch = 'master'
def clone(url, destination, branch): def git_clone(url, destination, branch):
git_clone = [ git_clone = [
'git', 'git',
'clone', 'clone',
@@ -20,13 +20,13 @@ def clone(url, destination, branch):
] ]
cli.log.debug('Git clone command: %s', git_clone) cli.log.debug('Git clone command: %s', git_clone)
with subprocess.Popen(git_clone, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, bufsize=1, universal_newlines=True) as p: with subprocess.Popen(git_clone, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, bufsize=1, universal_newlines=True, encoding='utf-8') as p:
for line in p.stdout: for line in p.stdout:
print(line, end='') print(line, end='')
if p.returncode != 0: if p.returncode != 0:
cli.log.error('git clone exited %d', p.returncode) cli.log.error('git clone exited %d', p.returncode)
return False
else: else:
cli.log.info('Successfully cloned %s to %s!', cli.args.fork, cli.args.destination) cli.log.info('Successfully cloned %s to %s!', url, destination)
return True
return p.returncode
+5 -4
View File
@@ -1,6 +1,6 @@
"""Useful helper functions. """Useful helper functions.
""" """
from qmk_cli.milc import cli from milc import cli, format_ansi
def question(question, boolean=True, default=''): def question(question, boolean=True, default=''):
@@ -8,7 +8,7 @@ def question(question, boolean=True, default=''):
This keeps re-asking until it gets acceptible input. This keeps re-asking until it gets acceptible input.
""" """
if cli.args.general_yes: if cli.args.yes:
return True return True
if default and default.lower() == 'y': if default and default.lower() == 'y':
@@ -18,10 +18,11 @@ def question(question, boolean=True, default=''):
else: else:
answer_key = 'y/n' answer_key = 'y/n'
prompt = '*** %s [%s] ' % (question, answer_key) prompt = format_ansi('\n*** %s [%s] ' % (question, answer_key))
while True: while True:
answer = input(prompt) answer = input(prompt)
print()
if answer == '' and default.lower() == 'y': if answer == '' and default.lower() == 'y':
answer = 'y' answer = 'y'
elif answer == '' and default.lower() == 'n': elif answer == '' and default.lower() == 'n':
@@ -32,4 +33,4 @@ def question(question, boolean=True, default=''):
elif answer.lower() in ['n', 'no']: elif answer.lower() in ['n', 'no']:
return False return False
else: else:
cli.args.echo('Invalid answer!') cli.echo('Invalid answer!')
+56 -53
View File
@@ -1,15 +1,26 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""CLI wrapper for running QMK commands. """CLI wrapper for running QMK commands.
This program can be run from anywhere, with or without a qmk_firmware checkout. It provides a small set of subcommands for working with QMK and otherwise dispatches to the `qmk_firmware/bin/qmk` script for the repo you are currently in, or your default repo if you are not currently in a qmk_firmware checkout. This program can be run from anywhere, with or without a qmk_firmware repository. If a qmk_firmware repository can be located we will use that to augment our available subcommands.
""" """
import argparse import argparse
import os import os
import subprocess import subprocess
import sys import sys
from functools import lru_cache from functools import lru_cache
from importlib import import_module
from pathlib import Path from pathlib import Path
import platform
import milc
milc.EMOJI_LOGLEVELS['INFO'] = '{fg_blue}Ψ{style_reset_all}'
@milc.cli.entrypoint('CLI wrapper for running QMK commands.')
def qmk_main(cli):
"""The function that gets run when there's no subcommand.
"""
cli.print_help()
@lru_cache(maxsize=2) @lru_cache(maxsize=2)
@@ -19,9 +30,14 @@ def in_qmk_firmware():
cur_dir = Path.cwd() cur_dir = Path.cwd()
while len(cur_dir.parents) > 0: while len(cur_dir.parents) > 0:
found_bin = cur_dir / 'bin' / 'qmk' found_bin = cur_dir / 'bin' / 'qmk'
if found_bin.is_file(): # An additional check for something that exists in the root of qmk_firmware,
command = [found_bin, '--version'] # but not in the script's install directory, to avoid recursive execution,
result = subprocess.run(command) # if started from install directory.
# e.g.: cd ~/.local/bin && ./qmk
found_quantum = cur_dir / 'quantum'
if found_bin.is_file() and found_quantum.is_dir():
command = [sys.executable, found_bin.as_posix()]
result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if result.returncode == 0: if result.returncode == 0:
return cur_dir return cur_dir
@@ -35,67 +51,54 @@ def find_qmk_firmware():
"""Look for qmk_firmware in the usual places. """Look for qmk_firmware in the usual places.
This function returns the path to qmk_firmware, or the default location if one does not exist. This function returns the path to qmk_firmware, or the default location if one does not exist.
FIXME(skullydazed): add config file support
""" """
if in_qmk_firmware(): if in_qmk_firmware():
return in_qmk_firmware() return in_qmk_firmware()
if milc.cli.config.user.qmk_home:
return Path(milc.cli.config.user.qmk_home).expanduser().resolve()
if 'QMK_HOME' in os.environ: if 'QMK_HOME' in os.environ:
return Path(os.environ['QMK_HOME']) path = Path(os.environ['QMK_HOME']).expanduser()
if path.exists():
return path.resolve()
return path
return Path.home() / 'qmk_firmware' return Path.home() / 'qmk_firmware'
def parse_args():
"""Process arguments outside milc.
"""
parser = argparse.ArgumentParser(description='CLI wrapper for running QMK commands.')
parser.add_argument('-H', '--home', help='Path to the qmk_firmware directory.')
parser.add_argument('subcommand', help='Subcommand to run')
parser.add_argument('subcommand_args', nargs=argparse.REMAINDER, help='Arguments to pass to the subcommand.')
args = parser.parse_args()
if args.home:
os.environ['QMK_HOME'] = args.home
return (args.subcommand, args.subcommand_args)
def main(): def main():
"""Dispatch the CLI subcommand to the proper place. """Setup the environment before dispatching to the entrypoint.
We first check to see if the subcommand was provided by the global `qmk`. If it was we import that module and hand control over to the entrypoint.
All other subcommands are dispatched to the local `qmk`, either the one we are currently in or whatever the user's default qmk_firmware is.
""" """
subcommand, subcommand_args = parse_args() # Warn if they use an outdated python version
subcommand_module = 'qmk_cli.subcommands.' + subcommand if sys.version_info < (3, 6):
sys.argv = ['qmk-'+subcommand] + subcommand_args print('Warning: Your Python version is out of date! Some subcommands may not work!')
print('Please upgrade to Python 3.6 or later.')
if 'windows' in platform.platform().lower():
if 'mingw64' not in sys.executable or 'mingw64' not in os.environ.get('MSYSTEM_PREFIX', ''):
print('Warning: It seems you are not using the MINGW64 terminal.')
print('While the MSYS one can work, too, we recommend/support you start "MSYS2 MinGW 64-bit".\n')
# Environment setup
import qmk_cli
milc.cli.version = qmk_cli.__version__
qmk_firmware = find_qmk_firmware() qmk_firmware = find_qmk_firmware()
qmk_bin = qmk_firmware / 'bin' / 'qmk'
os.environ['QMK_HOME'] = str(qmk_firmware) os.environ['QMK_HOME'] = str(qmk_firmware)
os.environ['ORIG_CWD'] = os.getcwd()
try: # Import the subcommand modules
# Attempt to import the subcommand from qmk_cli first import qmk_cli.subcommands
import qmk_cli.milc
import_module(subcommand_module)
qmk_cli.milc.cli()
except ImportError as e: if qmk_firmware.exists():
# Check to make sure there's not a bad import statement in qmk_cli os.chdir(str(qmk_firmware))
if e.name != subcommand_module: sys.path.append(str(qmk_firmware / 'lib' / 'python'))
raise try:
import qmk.cli
except ImportError:
print('Error: %s is too old or not set up correctly!' % qmk_firmware)
print('Please update it or remove it completely before continuing.')
sys.exit(1)
# Dispatch to the underlying `qmk_firmware/bin/qmk` # Call the entrypoint
if qmk_bin.is_file() and os.access(str(qmk_bin), os.X_OK): milc.cli()
argv = ['python3', str(qmk_bin), subcommand] + subcommand_args
os.execvp('python3', argv)
# Tell the user we can't continue
print('*** Could not locate qmk_firmware directory!')
exit(255)
if __name__ == '__main__':
main()
+6
View File
@@ -0,0 +1,6 @@
"""QMK CLI Subcommands
We list each subcommand here explicitly because all the reliable ways of searching for modules are slow and delay startup.
"""
from . import clone
from . import setup
+13 -11
View File
@@ -3,26 +3,28 @@
import os import os
from pathlib import Path from pathlib import Path
from qmk_cli.milc import cli from milc import cli
from qmk.cli.git import clone from qmk_cli.git import git_clone
default_repo = 'qmk_firmware' default_repo = 'qmk_firmware'
default_fork = 'qmk/' + default_repo default_fork = 'qmk/' + default_repo
default_branch = 'master' default_branch = 'master'
@cli.argument('--baseurl', default='https://github.com', help='The URL all git operations start from.') @cli.argument('--baseurl', default='https://github.com', help='The URL all git operations start from (Default: https://github.com)')
@cli.argument('-b', '--branch', default=default_branch, help='The branch to clone.') @cli.argument('-b', '--branch', default=default_branch, help='The branch to clone (Default: %s)' % default_branch)
@cli.argument('destination', default=os.environ['QMK_HOME'], nargs='?', help='The directory to clone to.') @cli.argument('destination', default=None, nargs='?', help='The directory to clone to (Default: Current Directory)')
@cli.argument('fork', default=default_fork, nargs='?', help='The qmk_firmware fork to clone') @cli.argument('fork', default=default_fork, nargs='?', help='The qmk_firmware fork to clone (Default: %s)' % default_fork)
@cli.entrypoint('Clone a qmk_firmware fork.') @cli.subcommand('Clone a qmk_firmware fork.')
def main(cli): def clone(cli):
if not cli.args.destination:
cli.args.destination = os.path.join(os.environ['ORIG_CWD'], default_fork.split('/')[-1])
qmk_firmware = Path(cli.args.destination) qmk_firmware = Path(cli.args.destination)
git_url = '/'.join((cli.config.general.baseurl, cli.args.fork)) git_url = '/'.join((cli.config.clone.baseurl, cli.args.fork))
if qmk_firmware.exists(): if qmk_firmware.exists():
cli.log.error('Destination already exists: %s', cli.args.destination) cli.log.error('Destination already exists: %s', cli.args.destination)
exit(1) exit(1)
success = clone(git_url, cli.args.destination, cli.config.general.branch) return git_clone(git_url, cli.args.destination, cli.config.clone.branch)
exit(0 if success else 1)
-29
View File
@@ -1,29 +0,0 @@
"""QMK Python Doctor
Check up for QMK environment.
"""
import qmk_cli.doctor
from qmk_cli.milc import cli
@cli.entrypoint('Basic QMK environment checks')
def main(cli):
"""Basic QMK environment checks.
This is currently very simple, it just checks that all the expected binaries are on your system.
"""
cli.log.info('QMK Doctor is checking your environment')
funcs = (
qmk_cli.doctor.check_qmk_firmware,
qmk_cli.doctor.check_vital_programs,
qmk_cli.doctor.check_platform_tests,
)
ok = True
for func in funcs:
if not func():
ok = False
if ok:
cli.log.info('{fg_green}QMK is ready to go')
+41 -30
View File
@@ -1,51 +1,62 @@
"""Setup qmk_firmware on your computer. """Setup qmk_firmware on your computer.
""" """
import os import os
import shlex
import subprocess import subprocess
import sys
from pathlib import Path from pathlib import Path
from qmk_cli.doctor import check_vital_programs from milc import cli
from qmk_cli.git import clone from qmk_cli.git import git_clone
from qmk_cli.helpers import question from qmk_cli.helpers import question
from qmk_cli.milc import cli
default_repo = 'qmk_firmware' default_repo = 'qmk_firmware'
default_fork = 'qmk/' + default_repo default_fork = 'qmk/' + default_repo
default_branch = 'master' default_branch = 'master'
@cli.argument('-y', '--yes', action='store_true', help='Answer yes to all questions') @cli.argument('-n', '--no', arg_only=True, action='store_true', help='Answer no to all questions')
@cli.argument('-y', '--yes', arg_only=True, action='store_true', help='Answer yes to all questions')
@cli.argument('--baseurl', default='https://github.com', help='The URL all git operations start from') @cli.argument('--baseurl', default='https://github.com', help='The URL all git operations start from')
@cli.argument('-b', '--branch', default=default_branch, help='The branch to clone') @cli.argument('-b', '--branch', default=default_branch, help='The branch to clone')
@cli.argument('destination', default=os.environ['QMK_HOME'], nargs='?', help='The directory to clone to') @cli.argument('-H', '--home', default=Path(os.environ['QMK_HOME']), type=Path, help='The location for QMK Firmware. Defaults to %s' % (os.environ['QMK_HOME'],))
@cli.argument('fork', default=default_fork, nargs='?', help='The qmk_firmware fork to clone') @cli.argument('fork', default=default_fork, nargs='?', help='The qmk_firmware fork to clone')
@cli.entrypoint('Setup your computer for qmk_firmware.') @cli.subcommand('Setup your computer for qmk_firmware.')
def main(cli): def setup(cli):
setup_successful = False """Guide the user through setting up their QMK environment.
qmk_firmware = Path(cli.args.destination) """
clone_prompt = 'Would you like to clone {fg_cyan}%s{fg_reset} to {fg_cyan}%s{fg_reset}?' % (cli.args.fork, shlex.quote(str(cli.args.home)))
home_prompt = 'Would you like to set {fg_cyan}%s{fg_reset} as your QMK home?' % (shlex.quote(str(cli.args.home)),)
# Sanity checks
if cli.args.yes and cli.args.no:
cli.log.error("Can't use both --yes and --no at the same time.")
exit(1)
# Check on qmk_firmware, and if it doesn't exist offer to check it out. # Check on qmk_firmware, and if it doesn't exist offer to check it out.
if qmk_firmware.exists(): if (cli.args.home / 'Makefile').exists():
cli.log.info('Found qmk_firmware at %s.', str(qmk_firmware)) cli.log.info('Found qmk_firmware at %s.', str(cli.args.home))
else: else:
cli.log.error('qmk_firmware not found!') cli.log.error('Could not find qmk_firmware!')
if question('Would you like to clone %s?' % cli.args.fork): if question(clone_prompt):
git_url = '/'.join((cli.config.general.baseurl, cli.args.fork)) git_url = '/'.join((cli.config.setup.baseurl, cli.args.fork))
clone(git_url, cli.args.destination, cli.config.general.branch) result = git_clone(git_url, cli.args.home, cli.config.setup.branch)
if result != 0:
exit(1)
# Check if the build environment is setup, and if not offer to set it up # Offer to set `user.qmk_home` for them.
if check_vital_programs(): if cli.args.home != os.environ['QMK_HOME'] and question(home_prompt):
cli.log.info('Your build environment is ready!') cli.config['user']['qmk_home'] = str(cli.args.home.absolute())
else: cli.write_config_option('user', 'qmk_home')
cli.log.error('Your build environment is not setup completely.')
if qmk_firmware.exists() and question('Would you like to run util/qmk_install?'):
curdir = os.getcwd()
os.chdir(str(qmk_firmware))
process = subprocess.run(['util/qmk_install.sh'])
os.chdir(curdir)
if process.returncode == 0:
setup_successful = True
# fin # Run `qmk_firmware/bin/qmk doctor` to check the rest of the environment out
if setup_successful: qmk_bin = cli.args.home / 'bin/qmk'
cli.log.info('QMK setup complete!') doctor_cmd = [sys.executable, qmk_bin, 'doctor']
if cli.args.yes:
doctor_cmd.append('--yes')
if cli.args.no:
doctor_cmd.append('--no')
subprocess.run(doctor_cmd)
+4 -1
View File
@@ -1,5 +1,8 @@
#!/bin/sh #!/bin/sh
# Increment the verison number and release a new version of qmk_cli. # Increment the verison number and release a new version of qmk_cli.
#
# Required packages: pip3 install bumpversion twine
set -e set -e
set -x set -x
@@ -9,7 +12,7 @@ TWINE_USERNAME=$PYPI_USERNAME
export FLIT_USERNAME TWINE_USERNAME export FLIT_USERNAME TWINE_USERNAME
rm dist/* rm -f dist/*
bumpversion patch bumpversion patch
git push origin master --tags git push origin master --tags
+1 -1
View File
@@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 0.0.10 current_version = 0.0.35
commit = True commit = True
tag = True tag = True
tag_name = {new_version} tag_name = {new_version}
+10 -3
View File
@@ -6,11 +6,13 @@ setup_cfg.read('setup.cfg')
metadata = setup_cfg['metadata'] metadata = setup_cfg['metadata']
if __name__ == "__main__": if __name__ == "__main__":
with open('README.md', encoding='utf-8') as readme_file:
long_description=readme_file.read()
setup( setup(
name=metadata['dist-name'], name=metadata['dist-name'],
description='A program to help users work with QMK Firmware.', description='A program to help users work with QMK Firmware.',
entry_points={ entry_points={
'console_scripts': ['%s = %s' % l for l in setup_cfg['entry_points'].items()] 'console_scripts': ['%s = %s' % l for l in setup_cfg['entry_points'].items()],
}, },
license='MIT License', license='MIT License',
url=metadata['home-page'], url=metadata['home-page'],
@@ -19,9 +21,10 @@ if __name__ == "__main__":
author_email=metadata['author-email'], author_email=metadata['author-email'],
maintainer=metadata['author'], maintainer=metadata['author'],
maintainer_email=metadata['author-email'], maintainer_email=metadata['author-email'],
long_description=open('README.md').read(), long_description=long_description,
long_description_content_type="text/markdown", long_description_content_type="text/markdown",
packages=find_packages(), packages=find_packages(),
py_modules = ['milc'],
classifiers=[ classifiers=[
'Development Status :: 3 - Alpha', 'Development Status :: 3 - Alpha',
'Environment :: Console', 'Environment :: Console',
@@ -38,8 +41,12 @@ if __name__ == "__main__":
requires_python=metadata['requires-python'], requires_python=metadata['requires-python'],
install_requires=[ install_requires=[
#"milc", #FIXME(skullydazed): Included in the repo for now. #"milc", #FIXME(skullydazed): Included in the repo for now.
"appdirs",
"argcomplete", "argcomplete",
"colorama", "colorama",
#"halo" "flake8",
"hjson",
"nose2",
"yapf"
], ],
) )
+15
View File
@@ -0,0 +1,15 @@
#!/bin/bash
cp -r $1 ~/qmk_cli
export PATH=~/.local/bin:$PATH
echo "PATH=$PATH" >> ~/.bashrc
export QMK_HOME=~/qmk_firmware
export CLI_DIR=~/qmk_cli
cd $CLI_DIR
python3 -m pip install -r requirements.txt
python3 setup.py sdist bdist_wheel
cd ~
python3 -m pip install --force-reinstall --no-index --no-deps --prefix=~/.local --find-links qmk_cli/dist qmk