Compare commits

...

23 Commits

Author SHA1 Message Date
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
16 changed files with 275 additions and 695 deletions
+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
+1 -1
View File
@@ -1,6 +1,6 @@
# QMK CLI # QMK CLI
A program to help users work with QMK A program to help users work with [QMK Firmware](https://qmk.fm/).
# Features # Features
+71 -45
View File
@@ -17,6 +17,7 @@ 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 tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
@@ -35,6 +36,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(filename='/dev/null')
# Log Level Representations # Log Level Representations
EMOJI_LOGLEVELS = { EMOJI_LOGLEVELS = {
@@ -47,6 +52,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:
@@ -97,11 +103,12 @@ class ANSIFormatter(logging.Formatter):
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)
@@ -144,13 +151,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 +170,34 @@ 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 `default` section as a fallback.
""" """
if key not in self._config: if key in self._config:
self.__dict__[key] = self._config[key] = None return self._config[key]
return self._config[key] elif key in self.parent.default:
return self.parent.default[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']
@@ -219,11 +232,6 @@ 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):
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': if 'action' in kwargs and kwargs['action'] == 'store_boolean':
return handle_store_boolean(self, *args, **kwargs) return handle_store_boolean(self, *args, **kwargs)
@@ -254,12 +262,16 @@ class MILC(object):
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.arg_only = []
self.config = Configuration() self.config = Configuration()
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.version = os.environ.get('QMK_VERSION', 'unknown') self.version = os.environ.get('QMK_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_argparse() self.initialize_argparse()
self.initialize_logging() self.initialize_logging()
@@ -273,7 +285,7 @@ class MILC(object):
self._description = self._arg_parser.description = self._arg_defaults.description = value self._description = self._arg_parser.description = self._arg_defaults.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 +296,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.
@@ -313,21 +324,21 @@ 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 to both the main and the shadow argparser.
""" """
if 'action' in kwargs and kwargs['action'] == 'store_boolean':
return handle_store_boolean(self, *args, **kwargs)
if kwargs.get('add_dest', True) and args[0][0] == '-': if kwargs.get('add_dest', True) and args[0][0] == '-':
kwargs['dest'] = 'general_' + self.get_argument_name(*args, **kwargs) kwargs['dest'] = 'general_' + self.get_argument_name(*args, **kwargs)
if 'add_dest' in kwargs: if 'add_dest' in kwargs:
del kwargs['add_dest'] del kwargs['add_dest']
if 'action' in kwargs and kwargs['action'] == 'store_boolean':
return handle_store_boolean(self, *args, **kwargs)
self.acquire_lock() self.acquire_lock()
self._arg_parser.add_argument(*args, **kwargs) self._arg_parser.add_argument(*args, **kwargs)
@@ -396,7 +407,7 @@ class MILC(object):
if self.args and self.args.general_config_file: if self.args and self.args.general_config_file:
return self.args.general_config_file return self.args.general_config_file
return os.path.abspath(os.path.expanduser('~/.%s.ini' % self.prog_name)) return os.path.join(user_config_dir(appname='qmk', appauthor='QMK'), '%s.ini' % self.prog_name)
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,6 +424,11 @@ 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):
if 'arg_only' in kwargs and kwargs['arg_only']:
arg_name = self.get_argument_name(*args, **kwargs)
self.arg_only.append(arg_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)
@@ -485,15 +501,20 @@ class MILC(object):
if argument in ('subparsers', 'entrypoint'): if argument in ('subparsers', 'entrypoint'):
continue continue
if '_' not in argument: if '_' in argument:
continue section, option = argument.split('_', 1)
section, option = argument.split('_', 1)
if hasattr(self.args_passed, argument):
self.config[section][option] = getattr(self.args, argument)
else: else:
if option not in self.config[section]: section = self._entrypoint.__name__
self.config[section][option] = getattr(self.args, argument) option = argument
if option not in self.arg_only:
if hasattr(self.args_passed, argument):
arg_value = getattr(self.args, argument)
if arg_value:
self.config[section][option] = arg_value
else:
if option not in self.config[section]:
self.config[section][option] = getattr(self.args, argument)
self.release_lock() self.release_lock()
@@ -509,6 +530,8 @@ class MILC(object):
self.acquire_lock() self.acquire_lock()
config = RawConfigParser() config = RawConfigParser()
config_dir = os.path.dirname(self.config_file)
for section_name, section in self.config._config.items(): for section_name, section in self.config._config.items():
config.add_section(section_name) config.add_section(section_name)
for option_name, value in section.items(): for option_name, value in section.items():
@@ -517,7 +540,10 @@ class MILC(object):
continue continue
config.set(section_name, option_name, str(value)) config.set(section_name, option_name, str(value))
with NamedTemporaryFile(mode='w', dir=os.path.dirname(self.config_file), delete=False) as tmpfile: if not os.path.exists(config_dir):
os.makedirs(config_dir)
with NamedTemporaryFile(mode='w', dir=config_dir, delete=False) as tmpfile:
config.write(tmpfile) config.write(tmpfile)
# Move the new config file into place atomically # Move the new config file into place atomically
@@ -527,6 +553,7 @@ class MILC(object):
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.', self.config_file, tmpfile.name)
self.release_lock() self.release_lock()
cli.log.info('Wrote configuration to %s', shlex.quote(self.config_file))
def __call__(self): def __call__(self):
"""Execute the entrypoint function. """Execute the entrypoint function.
@@ -534,8 +561,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!')
@@ -603,8 +629,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()
@@ -649,8 +675,9 @@ class MILC(object):
self.read_config() self.read_config()
self.setup_logging() self.setup_logging()
if self.config.general.save_config: if 'save_config' in self.config.general and self.config.general.save_config:
self.save_config() self.save_config()
exit(0)
return self return self
@@ -713,4 +740,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()))
-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.""" """A program to help you work with qmk_firmware."""
__version__ = '0.0.3' __version__ = '0.0.11'
-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 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
+2 -2
View File
@@ -1,6 +1,6 @@
"""Useful helper functions. """Useful helper functions.
""" """
from qmk_cli.milc import cli from milc import cli
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':
+23 -42
View File
@@ -2,15 +2,28 @@
"""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 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.
FIXME(skullydazed/anyone): --help shows underscores where we want dashes in subcommands (EG json_keymap instead of json-keymap)
TODO(skullydazed/anyone): Need a way to filter some subcommands from --help (EG `qmk 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 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)
def in_qmk_firmware(): def in_qmk_firmware():
@@ -21,7 +34,7 @@ def in_qmk_firmware():
found_bin = cur_dir / 'bin' / 'qmk' found_bin = cur_dir / 'bin' / 'qmk'
if found_bin.is_file(): if found_bin.is_file():
command = [found_bin, '--version'] command = [found_bin, '--version']
result = subprocess.run(command) result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if result.returncode == 0: if result.returncode == 0:
return cur_dir return cur_dir
@@ -47,21 +60,6 @@ def find_qmk_firmware():
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. """Dispatch the CLI subcommand to the proper place.
@@ -69,33 +67,16 @@ def main():
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. 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() # Environment setup
subcommand_module = 'qmk_cli.subcommands.' + subcommand
sys.argv = ['qmk-'+subcommand] + subcommand_args
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)
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 sys.path.append(str(qmk_firmware / 'lib' / 'python'))
if e.name != subcommand_module: import qmk.cli
raise
# 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
+6 -6
View File
@@ -3,8 +3,8 @@
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 clone
default_repo = 'qmk_firmware' default_repo = 'qmk_firmware'
default_fork = 'qmk/' + default_repo default_fork = 'qmk/' + default_repo
@@ -15,14 +15,14 @@ default_branch = 'master'
@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('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.argument('fork', default=default_fork, nargs='?', help='The qmk_firmware fork to clone')
@cli.entrypoint('Clone a qmk_firmware fork.') @cli.subcommand('Clone a qmk_firmware fork.')
def main(cli): def clone(cli):
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) success = clone(git_url, cli.args.destination, cli.config.clone.branch)
exit(0 if success else 1) 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')
+17 -21
View File
@@ -2,12 +2,12 @@
""" """
import os import os
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 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
@@ -19,9 +19,8 @@ default_branch = 'master'
@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('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.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
qmk_firmware = Path(cli.args.destination) qmk_firmware = Path(cli.args.destination)
# 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.
@@ -33,20 +32,17 @@ def main(cli):
git_url = '/'.join((cli.config.general.baseurl, cli.args.fork)) git_url = '/'.join((cli.config.general.baseurl, cli.args.fork))
clone(git_url, cli.args.destination, cli.config.general.branch) clone(git_url, cli.args.destination, cli.config.general.branch)
# Check if the build environment is setup, and if not offer to set it up # Run `qmk_firmware/bin/qmk doctor` to check the rest of the environment out
if check_vital_programs(): if qmk_firmware.exists():
cli.log.info('Your build environment is ready!') qmk_bin = qmk_firmware / 'bin' / 'qmk'
else: doctor = subprocess.run([sys.executable, qmk_bin, 'doctor'])
cli.log.error('Your build environment is not setup completely.') if doctor.returncode != 0:
if qmk_firmware.exists() and question('Would you like to run util/qmk_install?'): cli.log.error('Your build environment is not setup completely.')
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
if question('Would you like to run util/qmk_install?'):
# fin curdir = os.getcwd()
if setup_successful: os.chdir(str(qmk_firmware))
cli.log.info('QMK setup complete!') process = subprocess.run(['util/qmk_install.sh'])
os.chdir(curdir)
if process.returncode == 0:
cli.log.info('QMK setup complete!')
Executable
+21
View File
@@ -0,0 +1,21 @@
#!/bin/sh
# Increment the verison number and release a new version of qmk_cli.
set -e
set -x
PYPI_USERNAME=${PYPI_USERNAME:=skully}
FLIT_USERNAME=$PYPI_USERNAME
TWINE_USERNAME=$PYPI_USERNAME
export FLIT_USERNAME TWINE_USERNAME
rm 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.11
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] [bdist_wheel]
universal = 1 universal = 1
[flake8]
ignore = E501,E226
[metadata] [metadata]
author = skullydazed
author-email = skullydazed@gmail.com
description-file = README.md
dist-name = qmk
license_file = LICENSE 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] [yapf]
# Align closing bracket with visual indentation. align_closing_bracket_with_visual_indent = True
align_closing_bracket_with_visual_indent=True allow_multiline_dictionary_keys = False
allow_multiline_lambdas = False
# Allow dictionary keys to exist on multiple lines. For example: allow_split_before_default_or_named_assigns = True
# allow_split_before_dict_value = True
# x = { arithmetic_precedence_indication = True
# ('this is the first element of a tuple', blank_lines_around_top_level_definition = 2
# 'this is the second element of a tuple'): blank_line_before_class_docstring = False
# value, blank_line_before_module_docstring = False
# } blank_line_before_nested_class_or_def = False
allow_multiline_dictionary_keys=False coalesce_brackets = True
column_limit = 256
# Allow lambdas to be formatted on more than one line. continuation_align_style = SPACE
allow_multiline_lambdas=False continuation_indent_width = 4
dedent_closing_brackets = True
# Allow splitting before a default / named assignment in an argument list. disable_ending_comma_heuristic = False
allow_split_before_default_or_named_assigns=True each_dict_entry_on_separate_line = True
i18n_comment =
# Allow splits before the dictionary value. i18n_function_call =
allow_split_before_dict_value=True indent_blank_lines = False
indent_dictionary_value = True
# Let spacing indicate operator precedence. For example: indent_width = 4
# join_multiple_lines = False
# a = 1 * 2 + 3 / 4 no_spaces_around_selected_binary_operators =
# b = 1 / 2 - 3 * 4 spaces_around_default_or_named_assign = False
# c = (1 + 2) * (3 - 4) spaces_around_power_operator = False
# d = (1 - 2) / (3 + 4) spaces_before_comment = 2
# e = 1 * 2 - 3 space_between_ending_comma_and_closing_bracket = False
# f = 1 + 2 + 3 + 4 split_all_comma_separated_values = False
# split_arguments_when_comma_terminated = True
# will be formatted as follows to indicate precedence: split_before_arithmetic_operator = False
# split_before_bitwise_operator = True
# a = 1*2 + 3/4 split_before_closing_bracket = True
# b = 1/2 - 3*4 split_before_dict_set_generator = True
# c = (1+2) * (3-4) split_before_dot = False
# d = (1-2) / (3+4) split_before_expression_after_opening_paren = False
# e = 1*2 - 3 split_before_first_argument = False
# f = 1 + 2 + 3 + 4 split_before_logical_operator = False
# split_before_named_assigns = True
arithmetic_precedence_indication=True split_complex_comprehension = True
split_penalty_after_opening_bracket = 300
# Number of blank lines surrounding top-level function and class split_penalty_after_unary_operator = 10000
# definitions. split_penalty_arithmetic_operator = 300
blank_lines_around_top_level_definition=2 split_penalty_before_if_expr = 0
split_penalty_bitwise_operator = 300
# Insert a blank line before a class-level docstring. split_penalty_comprehension = 80
blank_line_before_class_docstring=False split_penalty_excess_character = 7000
split_penalty_for_added_line_split = 30
# Insert a blank line before a module docstring. split_penalty_import_names = 0
blank_line_before_module_docstring=False split_penalty_logical_operator = 300
use_tabs = 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
+45
View File
@@ -0,0 +1,45 @@
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__":
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=open('README.md').read(),
long_description_content_type="text/markdown",
packages=find_packages(),
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"
],
)