Compare commits

...

93 Commits

Author SHA1 Message Date
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
skullY fdc42051d5 New release: 0.0.9 → 0.0.10 2019-07-15 11:28:10 -07:00
skullY a4859aa15c Remove old dists before building 2019-07-15 11:28:00 -07:00
skullY 7ce14c0f74 New release: 0.0.8 → 0.0.9 2019-07-15 11:26:51 -07:00
skullY 94831732c6 Set the twine username 2019-07-15 11:26:43 -07:00
skullY 819cbea904 Remove the link from description. 2019-07-15 11:26:27 -07:00
skullY 65bcd35588 New release: 0.0.7 → 0.0.8 2019-07-15 11:23:04 -07:00
skullY ebe8db89d7 Fix description so markdown is rendered again 2019-07-15 11:22:58 -07:00
skullY 9c6db3844f New release: 0.0.6 → 0.0.7 2019-07-15 11:08:19 -07:00
skullY fb372d11ec rip out toml 2019-07-15 11:08:14 -07:00
skullY f23614d245 New release: 0.0.5 → 0.0.6 2019-07-15 10:45:18 -07:00
skullY 427e6addda Switch packaging from flit to setuptools 2019-07-15 10:45:06 -07:00
skullY 67698a52e0 flake8 config and cleanup 2019-07-01 13:11:42 -07:00
skullY c54aaa9c80 New release: 0.0.4 → 0.0.5 2019-07-01 12:25:30 -07:00
skullY 6681535ae1 document the release process 2019-07-01 12:24:25 -07:00
skullY fb365b6850 Cleanup qmk doctor 2019-07-01 01:47:57 -07:00
skullY 7ff0f36338 Update the readme so people know what they're looking at. 2019-07-01 01:41:12 -07:00
18 changed files with 523 additions and 783 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
+6 -1
View File
@@ -1 +1,6 @@
include *.md LICENSE
include *.md
include *.txt
include LICENSE
include qmk
include release
recursive-include qmk_cli *.py
+2 -2
View File
@@ -1,6 +1,6 @@
# QMK CLI
A program to help users work with QMK
[![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/).
# Features
+176 -114
View File
@@ -17,8 +17,10 @@ import argparse
import logging
import os
import re
import shlex
import sys
from decimal import Decimal
from pathlib import Path
from tempfile import NamedTemporaryFile
from time import sleep
@@ -35,6 +37,10 @@ except ImportError:
import argcomplete
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
EMOJI_LOGLEVELS = {
@@ -47,6 +53,7 @@ EMOJI_LOGLEVELS = {
}
EMOJI_LOGLEVELS['FATAL'] = EMOJI_LOGLEVELS['CRITICAL']
EMOJI_LOGLEVELS['WARN'] = EMOJI_LOGLEVELS['WARNING']
UNICODE_SUPPORT = sys.stdout.encoding.lower().startswith('utf')
# ANSI Color setup
# Regex was gratefully borrowed from kfir on stackoverflow:
@@ -90,25 +97,23 @@ def format_ansi(text):
class ANSIFormatter(logging.Formatter):
"""A log formatter that inserts ANSI color.
"""
def format(self, record):
msg = super(ANSIFormatter, self).format(record)
return format_ansi(msg)
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):
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)
class ANSIStrippingFormatter(ANSIFormatter):
"""A log formatter that strips ANSI.
"""
def format(self, record):
msg = super(ANSIStrippingFormatter, self).format(record)
return ansi_escape.sub('', msg)
@@ -120,7 +125,6 @@ class Configuration(object):
This class never raises IndexError, instead it will return None if a
section or option does not yet exist.
"""
def __contains__(self, key):
return self._config.__contains__(key)
@@ -144,13 +148,15 @@ class Configuration(object):
def __init__(self, *args, **kwargs):
self._config = {}
self.default_container = ConfigurationOption
def __getattr__(self, key):
return self.__getitem__(key)
def __getitem__(self, key):
"""Returns a config section, creating it if it doesn't exist yet.
"""
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]
@@ -161,30 +167,44 @@ class Configuration(object):
def __delitem__(self, key):
if key in self.__dict__ and key[0] != '_':
del self.__dict__[key]
del self._config[key]
if key in self._config:
del self._config[key]
class ConfigurationOption(Configuration):
def __init__(self, *args, **kwargs):
super(ConfigurationOption, self).__init__(*args, **kwargs)
self.default_container = dict
class ConfigurationSection(Configuration):
def __init__(self, parent, *args, **kwargs):
super(ConfigurationSection, self).__init__(*args, **kwargs)
self.parent = parent
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:
self.__dict__[key] = self._config[key] = None
if key in self._config and self._config.get(key) is not 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):
"""Does the add_argument for action='store_boolean'.
"""
kwargs['add_dest'] = False
disabled_args = None
disabled_kwargs = kwargs.copy()
disabled_kwargs['action'] = 'store_false'
disabled_kwargs['dest'] = self.get_argument_name(*args, **kwargs)
disabled_kwargs['help'] = 'Disable ' + kwargs['help']
kwargs['action'] = 'store_true'
kwargs['help'] = 'Enable ' + kwargs['help']
@@ -201,9 +221,8 @@ def handle_store_boolean(self, *args, **kwargs):
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):
self.cli = cli
self.submodule = submodule
@@ -219,48 +238,64 @@ class SubparserWrapper(object):
self.subparser.completer = completer
def add_argument(self, *args, **kwargs):
if kwargs.get('add_dest', True):
kwargs['dest'] = self.submodule + '_' + self.cli.get_argument_name(*args, **kwargs)
if 'add_dest' in kwargs:
del kwargs['add_dest']
"""Add an argument for this subcommand.
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)
self.cli.acquire_lock()
argument_name = self.cli.get_argument_name(*args, **kwargs)
self.subparser.add_argument(*args, **kwargs)
if 'default' in kwargs:
del kwargs['default']
if 'action' in kwargs and kwargs['action'] == 'store_false':
kwargs['action'] == 'store_true'
self.cli.subcommands_default[self.submodule].add_argument(*args, **kwargs)
if kwargs.get('action') == 'store_false':
self.cli._config_store_false.append(argument_name)
if kwargs.get('action') == 'store_true':
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()
class MILC(object):
"""MILC - An Opinionated Batteries Included Framework
"""
def __init__(self):
"""Initialize the MILC object.
version
The version string to associate with your CLI program
"""
# Setup a lock for thread safety
self._lock = threading.RLock() if thread else None
# Define some basic info
self.acquire_lock()
self._config_store_true = []
self._config_store_false = []
self._description = None
self._entrypoint = None
self._inside_context_manager = False
self.ansi = ansi_colors
self.config = Configuration()
self.arg_only = {}
self.config = self.config_source = None
self.config_file = None
self.prog_name = sys.argv[0][:-3] if sys.argv[0].endswith('.py') else sys.argv[0]
self.version = os.environ.get('QMK_VERSION', 'unknown')
self.default_arguments = {}
self.version = 'unknown'
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
self.read_config_file()
self.initialize_argparse()
self.initialize_logging()
@@ -270,10 +305,10 @@ class MILC(object):
@description.setter
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):
"""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
escape sequences, and the ANSI reset sequence will be added to all
@@ -284,11 +319,10 @@ class MILC(object):
if args and kwargs:
raise RuntimeError('You can only specify *args or **kwargs, not both!')
if sys.stdout.isatty():
args = args or kwargs
text = format_ansi(text)
args = args or kwargs
text = format_ansi(text)
print(text % args)
print(text % args)
def initialize_argparse(self):
"""Prepare to process arguments from sys.argv.
@@ -300,12 +334,9 @@ class MILC(object):
self.acquire_lock()
self.subcommands = {}
self.subcommands_default = {}
self._subparsers = None
self._subparsers_default = None
self.argwarn = argcomplete.warn
self.args = None
self._arg_defaults = argparse.ArgumentParser(**kwargs)
self._arg_parser = argparse.ArgumentParser(**kwargs)
self.set_defaults = self._arg_parser.set_defaults
self.print_usage = self._arg_parser.print_usage
@@ -313,30 +344,23 @@ class MILC(object):
self.release_lock()
def completer(self, completer):
"""Add an arpcomplete completer to this subcommand.
"""Add an argcomplete completer to this subcommand.
"""
self._arg_parser.completer = completer
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':
return handle_store_boolean(self, *args, **kwargs)
self.acquire_lock()
self._arg_parser.add_argument(*args, **kwargs)
# Populate the shadow parser
if 'default' in kwargs:
del kwargs['default']
if 'action' in kwargs and kwargs['action'] == 'store_false':
kwargs['action'] == 'store_true'
self._arg_defaults.add_argument(*args, **kwargs)
self._arg_parser.add_argument(*args, **kwargs)
if 'general' not in self.default_arguments:
self.default_arguments['general'] = {}
self.default_arguments['general'][self.get_argument_name(*args, **kwargs)] = kwargs.get('default')
self.release_lock()
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', help='File to write log messages to')
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('--save-config', action='store_true', help='Save the running configuration to the config file')
self.add_argument('--config-file', help='The location for the configuration file')
self.arg_only['config_file'] = ['general']
def add_subparsers(self, title='Sub-commands', **kwargs):
if self._inside_context_manager:
raise RuntimeError('You must run this before the with statement!')
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.release_lock()
@@ -393,10 +416,12 @@ class MILC(object):
if self.config_file:
return self.config_file
if self.args and self.args.general_config_file:
return self.args.general_config_file
if '--config-file' in sys.argv:
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):
"""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!')
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:
self.add_argument(*args, **kwargs)
elif handler.__name__ in self.subcommands:
self.subcommands[handler.__name__].add_argument(*args, **kwargs)
elif subcommand_name in self.subcommands:
self.subcommands[subcommand_name].add_argument(*args, **kwargs)
else:
raise RuntimeError('Decorated function is not entrypoint or subcommand!')
@@ -429,7 +463,7 @@ class MILC(object):
def arg_passed(self, arg):
"""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):
"""Parse the CLI args.
@@ -442,25 +476,23 @@ class MILC(object):
self.acquire_lock()
self.args = self._arg_parser.parse_args()
self.args_passed = self._arg_defaults.parse_args()
if 'entrypoint' in self.args:
self._entrypoint = self.args.entrypoint
if self.args.general_config_file:
self.config_file = self.args.general_config_file
self.release_lock()
def read_config(self):
"""Parse the configuration file and determine the runtime configuration.
def read_config_file(self):
"""Read in the configuration file and store it in self.config.
"""
self.acquire_lock()
self.config = Configuration()
self.config_source = Configuration()
self.config_file = self.find_config_file()
if self.config_file and os.path.exists(self.config_file):
if self.config_file and self.config_file.exists():
config = RawConfigParser(self.config)
config.read(self.config_file)
config.read(str(self.config_file))
# Iterate over the config file options and write them into self.config
for section in config.sections():
@@ -470,8 +502,10 @@ class MILC(object):
# Coerce values into useful datatypes
if value.lower() in ['1', 'yes', 'true', 'on']:
value = True
elif value.lower() in ['0', 'no', 'false', 'none', 'off']:
elif value.lower() in ['0', 'no', 'false', 'off']:
value = False
elif value.lower() in ['none']:
continue
elif value.replace('.', '').isdigit():
if '.' in value:
value = Decimal(value)
@@ -479,28 +513,56 @@ class MILC(object):
value = int(value)
self.config[section][option] = value
self.config_source[section][option] = 'config_file'
# Fold the CLI args into self.config
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):
if argument in ('subparsers', 'entrypoint'):
continue
if '_' not in argument:
continue
# Find the argument's section
# 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 hasattr(self.args_passed, argument):
self.config[section][option] = getattr(self.args, argument)
else:
if option not in self.config[section]:
self.config[section][option] = getattr(self.args, argument)
if not argument_found:
raise RuntimeError('Could not find argument in `self.default_arguments`. This should be impossible!')
exit(1)
if argument not in self.arg_only or section not in self.arg_only[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()
def save_config(self):
"""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:
self.log.warning('%s.config_file file not set, not saving config!', self.__class__.__name__)
@@ -508,25 +570,34 @@ class MILC(object):
self.acquire_lock()
# Generate a sanitized version of our running configuration
config = RawConfigParser()
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']:
if option_name in ['config_file']:
continue
config.set(section_name, option_name, str(value))
if value is not None:
config.set(section_name, option_name, str(value))
with NamedTemporaryFile(mode='w', dir=os.path.dirname(self.config_file), delete=False) as tmpfile:
# Write out the config file
config_dir = self.config_file.parent
if not config_dir.exists():
config_dir.mkdir(parents=True, exist_ok=True)
with NamedTemporaryFile(mode='w', dir=str(config_dir), 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)
os.replace(tmpfile.name, str(self.config_file))
else:
self.log.warning('Config file saving failed, not replacing %s with %s.', self.config_file, tmpfile.name)
self.log.warning('Config file saving failed, not replacing %s with %s.', str(self.config_file), tmpfile.name)
# Housekeeping
self.release_lock()
cli.log.info('Wrote configuration to %s', shlex.quote(str(self.config_file)))
def __call__(self):
"""Execute the entrypoint function.
@@ -534,8 +605,7 @@ class MILC(object):
if not self._inside_context_manager:
# If they didn't use the context manager use it ourselves
with self:
self.__call__()
return
return self.__call__()
if not self._entrypoint:
raise RuntimeError('No entrypoint provided!')
@@ -561,41 +631,37 @@ class MILC(object):
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.
If name is not provided we use `handler.__name__`.
"""
if self._inside_context_manager:
raise RuntimeError('You must run this before the with statement!')
if self._subparsers is None:
self.add_subparsers()
self.add_subparsers(metavar="")
if not name:
name = handler.__name__
name = handler.__name__.replace("_", "-")
self.acquire_lock()
kwargs['help'] = description
self.subcommands_default[name] = self._subparsers_default.add_parser(name, **kwargs)
if not hidden:
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].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()
return handler
def subcommand(self, description, **kwargs):
def subcommand(self, description, hidden=False, **kwargs):
"""Decorator to register a subcommand.
"""
def subcommand_function(handler):
return self.add_subcommand(handler, description, **kwargs)
return self.add_subcommand(handler, description, hidden=hidden, **kwargs)
return subcommand_function
@@ -603,8 +669,8 @@ class MILC(object):
"""Called by __enter__() to setup the logging configuration.
"""
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.
raise RuntimeError('MILC should be the only system installing root log handlers!')
# MILC is the only thing that should have root log handlers
logging.root.handlers = []
self.acquire_lock()
@@ -617,9 +683,9 @@ class MILC(object):
self.log_format = self.config['general']['log_fmt']
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:
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:
self.log_file_handler = logging.FileHandler(self.log_file, self.log_file_mode)
@@ -646,12 +712,9 @@ class MILC(object):
colorama.init()
self.parse_args()
self.read_config()
self.merge_args_into_config()
self.setup_logging()
if self.config.general.save_config:
self.save_config()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
@@ -713,4 +776,3 @@ if __name__ == '__main__':
cli.goodbye.add_argument('-n', '--name', help='Name to bid farewell to', default='World')
cli() # Automatically picks between main(), hello() and goodbye()
print(sorted(ansi_colors.keys()))
-34
View File
@@ -1,34 +0,0 @@
[build-system]
requires = ["flit"]
build-backend = "flit.buildapi"
[tool.flit.metadata]
dist-name = "qmk"
module = "qmk_cli"
author = "skullydazed"
author-email = "skullydazed@gmail.com"
description-file = "README.md"
home-page = "https://github.com/qmk/qmk_cli"
classifiers = [
'Development Status :: 3 - Alpha',
'Environment :: Console',
'Intended Audience :: Developers',
'Intended Audience :: System Administrators',
'Intended Audience :: End Users/Desktop',
'License :: OSI Approved :: MIT License',
'Natural Language :: English',
'Programming Language :: Python :: 3 :: Only',
'Topic :: Scientific/Engineering',
'Topic :: Software Development',
'Topic :: Utilities',
]
requires-python = ">=3.5"
requires = [
#"milc", #FIXME(skullydazed): Included in the repo for now.
"argcomplete",
"colorama",
#"halo"
]
[tool.flit.scripts]
qmk = "qmk_cli.script_qmk:main"
+1 -1
View File
@@ -1,3 +1,3 @@
"""A program to help you work with qmk_firmware."""
__version__ = '0.0.3'
__version__ = '0.0.33'
-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
+1 -1
View File
@@ -2,7 +2,7 @@
"""
import subprocess
from qmk_cli.milc import cli
from milc import cli
default_repo = 'qmk_firmware'
default_fork = 'qmk/' + default_repo
+2 -2
View File
@@ -1,6 +1,6 @@
"""Useful helper functions.
"""
from qmk_cli.milc import cli
from milc import cli
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.
"""
if cli.args.general_yes:
if cli.args.yes:
return True
if default and default.lower() == 'y':
+50 -52
View File
@@ -1,15 +1,26 @@
#!/usr/bin/env python3
"""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 os
import subprocess
import sys
from functools import lru_cache
from importlib import import_module
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)
@@ -20,8 +31,8 @@ def in_qmk_firmware():
while len(cur_dir.parents) > 0:
found_bin = cur_dir / 'bin' / 'qmk'
if found_bin.is_file():
command = [found_bin, '--version']
result = subprocess.run(command)
command = [sys.executable, found_bin.as_posix()]
result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if result.returncode == 0:
return cur_dir
@@ -35,67 +46,54 @@ def find_qmk_firmware():
"""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.
FIXME(skullydazed): add config file support
"""
if 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:
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'
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():
"""Dispatch the CLI subcommand to the proper place.
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.
"""Setup the environment before dispatching to the entrypoint.
"""
subcommand, subcommand_args = parse_args()
subcommand_module = 'qmk_cli.subcommands.' + subcommand
sys.argv = ['qmk-'+subcommand] + subcommand_args
# Warn if they use an outdated python version
if sys.version_info < (3, 6):
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_bin = qmk_firmware / 'bin' / 'qmk'
os.environ['QMK_HOME'] = str(qmk_firmware)
os.environ['ORIG_CWD'] = os.getcwd()
try:
# Attempt to import the subcommand from qmk_cli first
import qmk_cli.milc
import_module(subcommand_module)
qmk_cli.milc.cli()
# Import the subcommand modules
import qmk_cli.subcommands
except ImportError as e:
# Check to make sure there's not a bad import statement in qmk_cli
if e.name != subcommand_module:
raise
if qmk_firmware.exists():
os.chdir(str(qmk_firmware))
sys.path.append(str(qmk_firmware / 'lib' / 'python'))
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`
if qmk_bin.is_file() and os.access(str(qmk_bin), os.X_OK):
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()
# Call the entrypoint
milc.cli()
+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 -10
View File
@@ -3,26 +3,29 @@
import os
from pathlib import Path
from qmk_cli.milc import cli
from qmk.cli.git import clone
from milc import cli
from qmk_cli.git import clone as git_clone
default_repo = 'qmk_firmware'
default_fork = 'qmk/' + default_repo
default_branch = 'master'
@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('destination', default=os.environ['QMK_HOME'], nargs='?', help='The directory to clone to.')
@cli.argument('fork', default=default_fork, nargs='?', help='The qmk_firmware fork to clone')
@cli.entrypoint('Clone a qmk_firmware fork.')
def main(cli):
@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 (Default: %s)' % default_branch)
@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 (Default: %s)' % default_fork)
@cli.subcommand('Clone a qmk_firmware fork.')
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)
git_url = '/'.join((cli.config.general.baseurl, cli.args.fork))
git_url = '/'.join((cli.config.clone.baseurl, cli.args.fork))
if qmk_firmware.exists():
cli.log.error('Destination already exists: %s', cli.args.destination)
exit(1)
success = clone(git_url, cli.args.destination, cli.config.general.branch)
success = git_clone(git_url, cli.args.destination, cli.config.clone.branch)
exit(0 if success else 1)
-119
View File
@@ -1,119 +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
@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 = (
check_qmk_firmware,
check_vital_programs,
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')
+32 -25
View File
@@ -2,51 +2,58 @@
"""
import os
import subprocess
import sys
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.helpers import question
from qmk_cli.milc import cli
default_repo = 'qmk_firmware'
default_fork = 'qmk/' + default_repo
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('-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('fork', default=default_fork, nargs='?', help='The qmk_firmware fork to clone')
@cli.entrypoint('Setup your computer for qmk_firmware.')
def main(cli):
setup_successful = False
@cli.subcommand('Setup your computer for qmk_firmware.')
def setup(cli):
qmk_firmware = Path(cli.args.destination)
# 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.
if qmk_firmware.exists():
if (qmk_firmware / 'Makefile').exists():
cli.log.info('Found qmk_firmware at %s.', str(qmk_firmware))
else:
cli.log.error('qmk_firmware not found!')
if question('Would you like to clone %s?' % cli.args.fork):
git_url = '/'.join((cli.config.general.baseurl, cli.args.fork))
clone(git_url, cli.args.destination, cli.config.general.branch)
git_url = '/'.join((cli.config.setup.baseurl, cli.args.fork))
clone(git_url, cli.args.destination, cli.config.setup.branch)
# Check if the build environment is setup, and if not offer to set it up
if check_vital_programs():
cli.log.info('Your build environment is ready!')
else:
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
# Run `qmk_firmware/bin/qmk doctor` to check the rest of the environment out
if qmk_firmware.exists():
qmk_bin = qmk_firmware / 'bin' / 'qmk'
doctor_cmd = [sys.executable, str(qmk_bin), 'doctor']
if cli.args.yes:
doctor_cmd.append('--yes')
if cli.args.no:
doctor_cmd.append('--no')
doctor = subprocess.run(doctor_cmd)
if doctor.returncode != 0:
cli.log.error('Your build environment is not setup completely.')
# fin
if setup_successful:
cli.log.info('QMK setup complete!')
if 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:
cli.log.info('QMK setup complete!')
Executable
+24
View File
@@ -0,0 +1,24 @@
#!/bin/sh
# Increment the verison number and release a new version of qmk_cli.
#
# Required packages: pip3 install bumpversion twine
set -e
set -x
PYPI_USERNAME=${PYPI_USERNAME:=skully}
FLIT_USERNAME=$PYPI_USERNAME
TWINE_USERNAME=$PYPI_USERNAME
export FLIT_USERNAME TWINE_USERNAME
rm -f dist/*
bumpversion patch
git push origin master --tags
# For now we don't use flit
#flit publish
# Use setuptools until flit works with homebrew
python3 setup.py sdist bdist_wheel
twine upload dist/qmk-*
+75 -326
View File
@@ -1,334 +1,83 @@
[bumpversion]
current_version = 0.0.33
commit = True
tag = True
tag_name = {new_version}
message = New release: {current_version} → {new_version}
[bumpversion:file:qmk_cli/__init__.py]
[bumpversion:file:setup.cfg]
[bdist_wheel]
universal = 1
[flake8]
ignore = E501,E226
[metadata]
author = skullydazed
author-email = skullydazed@gmail.com
description-file = README.md
dist-name = qmk
license_file = LICENSE
module = qmk_cli
home-page = https://github.com/qmk/qmk_cli
requires-python = >=3.5
[entry_points]
qmk = qmk_cli.script_qmk:main
[yapf]
# Align closing bracket with visual indentation.
align_closing_bracket_with_visual_indent=True
# Allow dictionary keys to exist on multiple lines. For example:
#
# x = {
# ('this is the first element of a tuple',
# 'this is the second element of a tuple'):
# value,
# }
allow_multiline_dictionary_keys=False
# Allow lambdas to be formatted on more than one line.
allow_multiline_lambdas=False
# Allow splitting before a default / named assignment in an argument list.
allow_split_before_default_or_named_assigns=True
# Allow splits before the dictionary value.
allow_split_before_dict_value=True
# Let spacing indicate operator precedence. For example:
#
# a = 1 * 2 + 3 / 4
# b = 1 / 2 - 3 * 4
# c = (1 + 2) * (3 - 4)
# d = (1 - 2) / (3 + 4)
# e = 1 * 2 - 3
# f = 1 + 2 + 3 + 4
#
# will be formatted as follows to indicate precedence:
#
# a = 1*2 + 3/4
# b = 1/2 - 3*4
# c = (1+2) * (3-4)
# d = (1-2) / (3+4)
# e = 1*2 - 3
# f = 1 + 2 + 3 + 4
#
arithmetic_precedence_indication=True
# Number of blank lines surrounding top-level function and class
# definitions.
blank_lines_around_top_level_definition=2
# Insert a blank line before a class-level docstring.
blank_line_before_class_docstring=False
# Insert a blank line before a module docstring.
blank_line_before_module_docstring=False
# Insert a blank line before a 'def' or 'class' immediately nested
# within another 'def' or 'class'. For example:
#
# class Foo:
# # <------ this blank line
# def method():
# ...
blank_line_before_nested_class_or_def=False
# Do not split consecutive brackets. Only relevant when
# dedent_closing_brackets is set. For example:
#
# call_func_that_takes_a_dict(
# {
# 'key1': 'value1',
# 'key2': 'value2',
# }
# )
#
# would reformat to:
#
# call_func_that_takes_a_dict({
# 'key1': 'value1',
# 'key2': 'value2',
# })
coalesce_brackets=True
# The column limit.
column_limit=256
# The style for continuation alignment. Possible values are:
#
# - SPACE: Use spaces for continuation alignment. This is default behavior.
# - FIXED: Use fixed number (CONTINUATION_INDENT_WIDTH) of columns
# (ie: CONTINUATION_INDENT_WIDTH/INDENT_WIDTH tabs) for continuation
# alignment.
# - VALIGN-RIGHT: Vertically align continuation lines with indent
# characters. Slightly right (one more indent character) if cannot
# vertically align continuation lines with indent characters.
#
# For options FIXED, and VALIGN-RIGHT are only available when USE_TABS is
# enabled.
continuation_align_style=SPACE
# Indent width used for line continuations.
continuation_indent_width=4
# Put closing brackets on a separate line, dedented, if the bracketed
# expression can't fit in a single line. Applies to all kinds of brackets,
# including function definitions and calls. For example:
#
# config = {
# 'key1': 'value1',
# 'key2': 'value2',
# } # <--- this bracket is dedented and on a separate line
#
# time_series = self.remote_client.query_entity_counters(
# entity='dev3246.region1',
# key='dns.query_latency_tcp',
# transform=Transformation.AVERAGE(window=timedelta(seconds=60)),
# start_ts=now()-timedelta(days=3),
# end_ts=now(),
# ) # <--- this bracket is dedented and on a separate line
dedent_closing_brackets=True
# Disable the heuristic which places each list element on a separate line
# if the list is comma-terminated.
disable_ending_comma_heuristic=False
# Place each dictionary entry onto its own line.
each_dict_entry_on_separate_line=True
# The regex for an i18n comment. The presence of this comment stops
# reformatting of that line, because the comments are required to be
# next to the string they translate.
i18n_comment=
# The i18n function call names. The presence of this function stops
# reformattting on that line, because the string it has cannot be moved
# away from the i18n comment.
i18n_function_call=
# Indent blank lines.
indent_blank_lines=False
# Indent the dictionary value if it cannot fit on the same line as the
# dictionary key. For example:
#
# config = {
# 'key1':
# 'value1',
# 'key2': value1 +
# value2,
# }
indent_dictionary_value=True
# The number of columns to use for indentation.
indent_width=4
# Join short lines into one line. E.g., single line 'if' statements.
join_multiple_lines=False
# Do not include spaces around selected binary operators. For example:
#
# 1 + 2 * 3 - 4 / 5
#
# will be formatted as follows when configured with "*,/":
#
# 1 + 2*3 - 4/5
no_spaces_around_selected_binary_operators=
# Use spaces around default or named assigns.
spaces_around_default_or_named_assign=False
# Use spaces around the power operator.
spaces_around_power_operator=False
# The number of spaces required before a trailing comment.
# This can be a single value (representing the number of spaces
# before each trailing comment) or list of values (representing
# alignment column values; trailing comments within a block will
# be aligned to the first column value that is greater than the maximum
# line length within the block). For example:
#
# With spaces_before_comment=5:
#
# 1 + 1 # Adding values
#
# will be formatted as:
#
# 1 + 1 # Adding values <-- 5 spaces between the end of the statement and comment
#
# With spaces_before_comment=15, 20:
#
# 1 + 1 # Adding values
# two + two # More adding
#
# longer_statement # This is a longer statement
# short # This is a shorter statement
#
# a_very_long_statement_that_extends_beyond_the_final_column # Comment
# short # This is a shorter statement
#
# will be formatted as:
#
# 1 + 1 # Adding values <-- end of line comments in block aligned to col 15
# two + two # More adding
#
# longer_statement # This is a longer statement <-- end of line comments in block aligned to col 20
# short # This is a shorter statement
#
# a_very_long_statement_that_extends_beyond_the_final_column # Comment <-- the end of line comments are aligned based on the line length
# short # This is a shorter statement
#
spaces_before_comment=2
# Insert a space between the ending comma and closing bracket of a list,
# etc.
space_between_ending_comma_and_closing_bracket=False
# Split before arguments
split_all_comma_separated_values=False
# Split before arguments if the argument list is terminated by a
# comma.
split_arguments_when_comma_terminated=True
# Set to True to prefer splitting before '+', '-', '*', '/', '//', or '@'
# rather than after.
split_before_arithmetic_operator=False
# Set to True to prefer splitting before '&', '|' or '^' rather than
# after.
split_before_bitwise_operator=True
# Split before the closing bracket if a list or dict literal doesn't fit on
# a single line.
split_before_closing_bracket=True
# Split before a dictionary or set generator (comp_for). For example, note
# the split before the 'for':
#
# foo = {
# variable: 'Hello world, have a nice day!'
# for variable in bar if variable != 42
# }
split_before_dict_set_generator=True
# Split before the '.' if we need to split a longer expression:
#
# foo = ('This is a really long string: {}, {}, {}, {}'.format(a, b, c, d))
#
# would reformat to something like:
#
# foo = ('This is a really long string: {}, {}, {}, {}'
# .format(a, b, c, d))
split_before_dot=False
# Split after the opening paren which surrounds an expression if it doesn't
# fit on a single line.
split_before_expression_after_opening_paren=False
# If an argument / parameter list is going to be split, then split before
# the first argument.
split_before_first_argument=False
# Set to True to prefer splitting before 'and' or 'or' rather than
# after.
split_before_logical_operator=False
# Split named assignments onto individual lines.
split_before_named_assigns=True
# Set to True to split list comprehensions and generators that have
# non-trivial expressions and multiple clauses before each of these
# clauses. For example:
#
# result = [
# a_long_var + 100 for a_long_var in xrange(1000)
# if a_long_var % 10]
#
# would reformat to something like:
#
# result = [
# a_long_var + 100
# for a_long_var in xrange(1000)
# if a_long_var % 10]
split_complex_comprehension=True
# The penalty for splitting right after the opening bracket.
split_penalty_after_opening_bracket=300
# The penalty for splitting the line after a unary operator.
split_penalty_after_unary_operator=10000
# The penalty of splitting the line around the '+', '-', '*', '/', '//',
# ``%``, and '@' operators.
split_penalty_arithmetic_operator=300
# The penalty for splitting right before an if expression.
split_penalty_before_if_expr=0
# The penalty of splitting the line around the '&', '|', and '^'
# operators.
split_penalty_bitwise_operator=300
# The penalty for splitting a list comprehension or generator
# expression.
split_penalty_comprehension=80
# The penalty for characters over the column limit.
split_penalty_excess_character=7000
# The penalty incurred by adding a line split to the unwrapped line. The
# more line splits added the higher the penalty.
split_penalty_for_added_line_split=30
# The penalty of splitting a list of "import as" names. For example:
#
# from a_very_long_or_indented_module_name_yada_yad import (long_argument_1,
# long_argument_2,
# long_argument_3)
#
# would reformat to something like:
#
# from a_very_long_or_indented_module_name_yada_yad import (
# long_argument_1, long_argument_2, long_argument_3)
split_penalty_import_names=0
# The penalty of splitting the line around the 'and' and 'or'
# operators.
split_penalty_logical_operator=300
# Use the Tab character for indentation.
use_tabs=False
align_closing_bracket_with_visual_indent = True
allow_multiline_dictionary_keys = False
allow_multiline_lambdas = False
allow_split_before_default_or_named_assigns = True
allow_split_before_dict_value = True
arithmetic_precedence_indication = True
blank_lines_around_top_level_definition = 2
blank_line_before_class_docstring = False
blank_line_before_module_docstring = False
blank_line_before_nested_class_or_def = False
coalesce_brackets = True
column_limit = 256
continuation_align_style = SPACE
continuation_indent_width = 4
dedent_closing_brackets = True
disable_ending_comma_heuristic = False
each_dict_entry_on_separate_line = True
i18n_comment =
i18n_function_call =
indent_blank_lines = False
indent_dictionary_value = True
indent_width = 4
join_multiple_lines = False
no_spaces_around_selected_binary_operators =
spaces_around_default_or_named_assign = False
spaces_around_power_operator = False
spaces_before_comment = 2
space_between_ending_comma_and_closing_bracket = False
split_all_comma_separated_values = False
split_arguments_when_comma_terminated = True
split_before_arithmetic_operator = False
split_before_bitwise_operator = True
split_before_closing_bracket = True
split_before_dict_set_generator = True
split_before_dot = False
split_before_expression_after_opening_paren = False
split_before_first_argument = False
split_before_logical_operator = False
split_before_named_assigns = True
split_complex_comprehension = True
split_penalty_after_opening_bracket = 300
split_penalty_after_unary_operator = 10000
split_penalty_arithmetic_operator = 300
split_penalty_before_if_expr = 0
split_penalty_bitwise_operator = 300
split_penalty_comprehension = 80
split_penalty_excess_character = 7000
split_penalty_for_added_line_split = 30
split_penalty_import_names = 0
split_penalty_logical_operator = 300
use_tabs = False
+52
View File
@@ -0,0 +1,52 @@
from configparser import ConfigParser
from setuptools import setup, find_packages
setup_cfg = ConfigParser()
setup_cfg.read('setup.cfg')
metadata = setup_cfg['metadata']
if __name__ == "__main__":
with open('README.md', encoding='utf-8') as readme_file:
long_description=readme_file.read()
setup(
name=metadata['dist-name'],
description='A program to help users work with QMK Firmware.',
entry_points={
'console_scripts': ['%s = %s' % l for l in setup_cfg['entry_points'].items()],
},
license='MIT License',
url=metadata['home-page'],
version=setup_cfg['bumpversion']['current_version'],
author=metadata['author'],
author_email=metadata['author-email'],
maintainer=metadata['author'],
maintainer_email=metadata['author-email'],
long_description=long_description,
long_description_content_type="text/markdown",
packages=find_packages(),
py_modules = ['milc'],
classifiers=[
'Development Status :: 3 - Alpha',
'Environment :: Console',
'Intended Audience :: Developers',
'Intended Audience :: System Administrators',
'Intended Audience :: End Users/Desktop',
'License :: OSI Approved :: MIT License',
'Natural Language :: English',
'Programming Language :: Python :: 3 :: Only',
'Topic :: Scientific/Engineering',
'Topic :: Software Development',
'Topic :: Utilities',
],
requires_python=metadata['requires-python'],
install_requires=[
#"milc", #FIXME(skullydazed): Included in the repo for now.
"appdirs",
"argcomplete",
"colorama",
"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