Compare commits

...

128 Commits

Author SHA1 Message Date
QMK Bot a1276f8d58 New release: 0.0.47 → 0.0.48 2021-05-10 19:40:17 +00:00
Zach White 3555e34ae9 New release: 0.0.46 → 0.0.47 2021-05-10 12:39:08 -07:00
Zach White b36cf144e3 Pass color and unicode settings down to qmk doctor
fixes #50
2021-05-10 12:28:37 -07:00
Zach White fb05b69adc update README 2021-05-10 12:24:05 -07:00
Zach White 4283c258d9 remove the module checking code in favor of qmk_firmware doing that directly 2021-05-10 12:22:44 -07:00
Zach White b5e8aa4cac New release: 0.0.45 → 0.0.46 2021-05-08 21:04:06 -07:00
Zach White 1c6d81fc3b add hid and pyusb to the requirements 2021-05-08 21:03:31 -07:00
Erovia 0ffae74377 Merge pull request #49 from zvecr/env_command 2021-05-08 13:00:46 +02:00
zvecr cbe3ad0248 Expose is_qmk_firmware status 2021-04-08 23:41:12 +01:00
zvecr 97689b53d7 Add cli.config.general 2021-04-05 21:29:56 +01:00
zvecr 054e071787 Add env subcommand 2021-04-05 21:01:29 +01:00
QMK Bot 78e0fda2f0 New release: 0.0.44 → 0.0.45 2021-03-28 23:43:02 +00:00
Zach White 9c6123483b housekeeping 2021-03-28 16:23:15 -07:00
QMK Bot e851c8fcb5 New release: 0.0.43 → 0.0.44 2021-03-28 01:51:59 +00:00
QMK Bot 92168e1ef0 New release: 0.0.42 → 0.0.43 2021-03-28 01:41:34 +00:00
Zach White fd744ac24c Setup an action to publish a release 2021-03-27 18:38:47 -07:00
Zach White 840c791cc8 don't use sys.executable when running qmk doctor 2021-03-27 17:02:04 -07:00
Zach White 8f5b8e83c0 New release: 0.0.41 → 0.0.42 2021-03-27 13:03:22 -07:00
Zach White 1da98510d3 make the qmk_firmware check more robust 2021-03-27 11:34:36 -07:00
Zach White 6ec242b133 New release: 0.0.40 → 0.0.41 2021-03-27 09:14:16 -07:00
Zach White 6433cbc0f4 this script uses bashisms 2021-03-27 09:13:59 -07:00
Zach White aedad7e602 use the correct name 2021-03-27 09:04:51 -07:00
Zach White 504291bbf5 handle the case where qmk_firmware exists but isn't a clone 2021-03-27 09:04:51 -07:00
Zach White c6e736adc7 make qmk clone more robust 2021-03-27 09:04:51 -07:00
Zach White fe42ab3534 General improvements
* refactor to make the code clearer in some places
* don't call bin/qmk anymore
* ask the user if they want to install python modules when they're missing
2021-03-27 08:34:34 -07:00
Ryan d805fe2b6e Merge pull request #42 from qmk/msys-no-setuptools
[MSYS2] Remove explicit setuptools upgrade from build action
2021-03-08 19:46:50 +11:00
fauxpark 5321a121fc Remove explicit setuptools upgrade from build action 2021-03-08 06:20:24 +11:00
Zach White 3c31d41bf3 Update tested python versions 2021-03-07 11:18:08 -08:00
Zach White c6977fa851 fix the python_requires var 2021-03-07 11:07:36 -08:00
Zach White 3b6c0fd058 write a quickstart 2021-03-02 07:36:49 -08:00
Zach White 216f5d44c1 New release: 0.0.39 → 0.0.40 2021-03-02 07:32:52 -08:00
Zach White 0bf0b5a503 indicate a minimum version for jsonschema 2021-03-02 06:50:21 -08:00
Joel Challis 4b11e04672 Merge pull request #39 from qmk/remove_coc
Delete CODE_OF_CONDUCT.md
2021-02-28 01:39:42 +00:00
Zach White aba043abdc New release: 0.0.38 → 0.0.39 2021-01-11 19:58:09 -08:00
Zach White efd9f68243 add updated reqs from qmk_firmware:develop 2021-01-11 19:57:58 -08:00
Zach White e3e0856f96 New release: 0.0.37 → 0.0.38 2021-01-11 09:53:21 -08:00
Zach White 670a07ff0b replace dash with underscore in module import name 2021-01-11 09:37:32 -08:00
Joel Challis c4c76e3534 Delete CODE_OF_CONDUCT.md 2021-01-10 01:37:50 +00:00
Zach White 9ad519d4a2 Switch to the new way off adding a path.
Per <https://github.blog/changelog/2020-10-01-github-actions-deprecating-set-env-and-add-path-commands/>
2020-12-02 10:44:07 -08:00
Zach White 655979cef5 New release: 0.0.36 → 0.0.37 2020-12-02 10:34:46 -08:00
Zach White 6b0d5fe85b update the release script 2020-12-02 10:34:35 -08:00
fauxpark d163222123 Path-ify QMK_HOME 2020-10-20 10:38:36 -07:00
skullY 5342ab3e10 check if requirements are installed before importing qmk_firmware 2020-10-19 09:30:01 -07:00
skullY 21ee8ce228 New release: 0.0.35 → 0.0.36 2020-10-07 12:19:16 -07:00
skullY 111ebe2a34 add pygments to requirements 2020-10-07 12:08:45 -07:00
skullY 9fdee68e7f Fix qmk setup on windows 2020-10-07 12:06:48 -07:00
skullY b36c53a249 Enhance qmk setup to add upstream 2020-10-07 12:06:02 -07:00
fauxpark 07c38a36d5 Change MSYS2 terminal warning to an error 2020-10-07 12:05:37 -07:00
Zach White 7605ab1007 Switch to installed milc (#31) 2020-10-06 18:42:40 -07:00
fauxpark 315e863b21 Setup/clone: show default argument values more consistently 2020-09-23 20:10:14 -07:00
fauxpark 8fc7dccd13 CLI workflow: fix MSYS2 job 2020-09-23 07:45:29 -07:00
fauxpark 8a531f7b7b Fix CLI not producing any return codes 2020-09-14 08:31:23 -07:00
skullY 12c415f09c Add a warning for when people have (probably) misconfigured QMK_HOME 2020-07-19 16:16:21 -07:00
Zach White 5a777c5de9 New release: 0.0.34 → 0.0.35 2020-05-27 20:58:33 -07:00
Zach White c51c898562 use cli.echo, not cli.args.echo. 2020-05-27 20:51:28 -07:00
Erovia 08ea23b087 Fix possibility of recursive execution 2020-05-26 21:43:15 +02:00
Erovia 0105405d83 Checking for the 'quantum' directory is better for long term 2020-05-10 17:41:30 +02:00
Erovia 92c57e2a46 Fix possibility of recursive execution 2020-05-10 17:30:30 +02:00
skullY 30c89ec8e0 New release: 0.0.33 → 0.0.34 2020-04-29 10:39:57 -07:00
Zach White a7a7bd9f08 improve the qmk setup flow (#20)
* improve the qmk setup flow

* Apply suggestions from code review

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

* Update qmk_cli/subcommands/setup.py

* explicity set encoding to utf-8

* polishing

Co-authored-by: Erovia <Erovia@users.noreply.github.com>
2020-04-29 10:38:56 -07:00
skullY 5229ce14e3 New release: 0.0.32 → 0.0.33 2020-04-15 13:53:35 -07:00
Erovia 8fa960f25e Merge pull request #19 from qmk/fix_store_true_false 2020-04-15 22:49:17 +02:00
skullY 1481dfe157 Make arg_only apply only to the subcommand it is part of 2020-04-15 12:07:15 -07:00
skullY ac39b9bb0f Fix setting config values for store_true and store_false 2020-04-15 09:30:49 -07:00
skullY 44a446308f New release: 0.0.31 → 0.0.32 2020-04-11 10:59:02 -07:00
Erovia 90bdf52416 Merge pull request #18 from Erovia/cli/win_env_check
Add warning message for MSYS users
2020-04-11 19:56:04 +02:00
Erovia 191980052e Add warning message for MSYS users 2020-04-11 19:44:56 +02:00
skullydazed 5fbdd95c4b Fix indentation 2020-04-11 10:31:54 -07:00
skullydazed 1002360e20 Update qmk_cli/script_qmk.py
Co-Authored-By: Erovia <Erovia@users.noreply.github.com>
2020-04-11 10:31:54 -07:00
Joel Challis a03aa1cbba Update qmk_cli/subcommands/setup.py
Co-Authored-By: skullydazed <skullydazed@users.noreply.github.com>
2020-04-11 10:31:54 -07:00
zvecr a0b692d718 Handle failed import with exception instead 2020-04-11 10:31:54 -07:00
zvecr ab7d0c6e46 Protect against empty QMK_HOME folder 2020-04-11 10:31:54 -07:00
skullY 73c903b8b6 Add --no flag 2020-04-11 09:57:31 -07:00
skullydazed 8f2185af32 Update qmk_cli/subcommands/setup.py 2020-04-11 09:57:31 -07:00
Erovia bae2f1f1ee Fix argument passing to doctor subcommand
When 'qmk setup' is started with the '-y' argument, this argument should be specified for 'qmk doctor' as well.
2020-04-11 09:57:31 -07:00
Zach White 120ce557f1 New release: 0.0.30 → 0.0.31 2020-03-26 07:51:42 -07:00
Erovia a15b0d6460 Fix WinError 193 on MSYS2 2020-03-26 07:51:17 -07:00
skullY f0dafaf068 New release: 0.0.29 → 0.0.30 2020-03-10 11:37:44 -07:00
skullY 3414bdcaa3 add nose2 to requirements 2020-03-10 11:37:31 -07:00
Mikkel Jeppesen c8567d8098 Made bade in the readme nicer 2020-02-25 14:33:37 -08:00
skullY dc3ca88deb New release: 0.0.28 → 0.0.29 2020-02-21 12:03:55 -08:00
skullY a4e9c34387 Add a warning when run on old python versions 2020-02-21 12:03:46 -08:00
Mikkel Jeppesen 742e1ce0e1 Added CI badge to readme 2020-02-21 11:13:59 -08:00
Mikkel Jeppesen 39df759f57 Switched to python 3.6 as minimum version, and remove paths condition on PR trigger 2020-02-21 11:13:59 -08:00
Mikkel Jeppesen b694c4284d removed debug echo 2020-02-21 11:13:59 -08:00
Mikkel Jeppesen e184710036 Fixed script path, checking env var 2020-02-21 11:13:59 -08:00
Mikkel Jeppesen d67addde2d moved msys script to root 2020-02-21 11:13:59 -08:00
Mikkel Jeppesen 29541559d0 Update .github/workflows/cli_setup.yml
Co-Authored-By: skullydazed <skullydazed@users.noreply.github.com>
2020-02-21 11:13:59 -08:00
Mikkel Jeppesen db0b8cb9a0 Added workflow to test qmk cli setup on OSX, Linux and MSYS2 2020-02-21 11:13:59 -08:00
skullY 6a0e62a078 Remove notes for things that are done 2020-02-21 11:13:41 -08:00
skullY bbc5ffd6c9 New release: 0.0.27 → 0.0.28 2020-02-13 19:09:10 -08:00
Zach White e0adba4e68 Prevent release from erroring on first run 2020-02-13 18:50:00 -08:00
Zach White 56bcb12ed6 Add support for determining where a config option came from 2020-02-13 18:49:13 -08:00
Mikkel Jeppesen 897fe121e2 Replace rename with replace 2020-02-13 18:48:02 -08:00
skullY 1a723e488f New release: 0.0.26 → 0.0.27 2020-01-26 11:40:14 -08:00
skullY bf502d26fa sync milc with qmk_firmware 2020-01-26 11:40:00 -08:00
skullY ebea7f7ea0 New release: 0.0.25 → 0.0.26 2019-11-28 08:42:55 -08:00
skullY 49b288786a Add support for hidden subcommands 2019-11-28 08:42:49 -08:00
skullY e75b4d4ed1 New release: 0.0.24 → 0.0.25 2019-11-18 22:33:19 -08:00
skullY 7f56cfd781 Add flake8 to requirements 2019-11-18 22:33:14 -08:00
skullY b04bf7a4da New release: 0.0.23 → 0.0.24 2019-11-15 23:25:13 -08:00
skullY fb260f29e1 Fix commands with no arguments 2019-11-15 23:15:07 -08:00
skullY e8e98ca15a New release: 0.0.22 → 0.0.23 2019-11-15 22:21:33 -08:00
skullY 4d1302696e Make folding arguments into config operate more correctly. 2019-11-15 22:21:26 -08:00
skullY 4d78df2cbc New release: 0.0.21 → 0.0.22 2019-11-12 16:54:28 -08:00
skullY 4a45081412 filter None from config files 2019-11-12 16:53:32 -08:00
skullY 9cab9ea610 New release: 0.0.20 → 0.0.21 2019-11-11 13:04:13 -08:00
skullY 177fa55bce fix pytest in qmkfm/base_container 2019-11-11 13:03:39 -08:00
skullY 0783864629 New release: 0.0.19 → 0.0.20 2019-11-11 11:56:35 -08:00
skullY 8ec66dd599 Improve --version 2019-11-11 11:56:20 -08:00
skullY 790536eca4 New release: 0.0.18 → 0.0.19 2019-11-11 09:17:31 -08:00
skullY 6f1ec49a9a Add support for specifying user.qmk_home in config 2019-11-11 09:15:24 -08:00
skullY c7f37a0e1b MILC improvements-
* Remove the shadow argparser
* Make it easier to reason about arguments and how they're translated into the config tree
* Populate self.config during init to support setting user.qmk_home for the global CLI
* Remove the short argument -c so that we can unambiguously determine the config file location without doing full argument processing
* Remove the --save-config option as it's a little confusing anyway
* Use Pathlib for path manipulation
2019-11-11 09:14:24 -08:00
skullY 5001c627ef New release: 0.0.17 → 0.0.18 2019-11-10 11:11:14 -08:00
Erovia 490c94e282 Fix command line variable access for baseurl and branch 2019-11-10 11:10:49 -08:00
skullY 212e185b62 New release: 0.0.16 → 0.0.17 2019-10-23 22:54:53 -07:00
zvecr b7d0a65bdf Fix No such file or directory: 'C:\\dev\\null' 2019-10-23 22:51:53 -07:00
skullY 221d42fec1 sync milc.py with qmk_firmware 2019-10-23 22:47:50 -07:00
skullY f3e83794c9 New release: 0.0.15 → 0.0.16 2019-10-12 13:18:31 -07:00
zvecr 73628cc6b0 Fix in_qmk_firmware for Python 3.5 2019-10-12 13:15:15 -07:00
skullY e077319a16 New release: 0.0.14 → 0.0.15 2019-10-08 07:23:09 -07:00
skullY 5b0ba9a25d Document release requirements 2019-10-08 07:22:56 -07:00
skullY bd4e9bf9cf Add hjson to requirements 2019-10-08 07:20:26 -07:00
Nikita Titov b2974b4219 read README with utf-8 and close file after reading 2019-10-04 07:45:02 -07:00
skullY be0dd95e46 New release: 0.0.13 → 0.0.14 2019-09-25 12:00:35 -07:00
skullY 30031a8cd7 Fix qmk clone 2019-09-25 12:00:32 -07:00
skullY 94adba52c6 New release: 0.0.12 → 0.0.13 2019-09-25 09:54:23 -07:00
skullY 38aefc9716 Fix the environment 2019-09-25 09:54:16 -07:00
17 changed files with 425 additions and 898 deletions
+71
View File
@@ -0,0 +1,71 @@
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.7, 3.8, 3.9]
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: |
python3 -m pip install wheel
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Install QMK CLI from source
run: |
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
- name: Run qmk setup -y
run: qmk setup -y
test_cli_win:
runs-on: windows-latest
env:
QMK_HOME: $HOME/qmk_firmware
steps:
- uses: actions/checkout@v1
- name: (MSYS2) Setup and install dependencies
uses: msys2/setup-msys2@v2
with:
update: true
install: git mingw-w64-x86_64-toolchain mingw-w64-x86_64-python-pip
- name: (MSYS2) Install Python dependencies
shell: msys2 {0}
run: |
python3 -m pip install wheel
- name: (MSYS2) Install QMK CLI from source
shell: msys2 {0}
run: |
python3 -m pip install -r requirements.txt
python3 setup.py sdist bdist_wheel
cd ..
python3 -m pip install --force-reinstall --no-index --no-deps --find-links qmk_cli/dist qmk
- name: (MSYS2) Run qmk setup -y
shell: msys2 {0}
run: qmk setup -y
+47
View File
@@ -0,0 +1,47 @@
# This workflow will upload a Python Package using Twine when a release is created
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
name: Upload Python Package
on:
workflow_dispatch:
inputs:
version_part:
description: 'Which part of the version to increment (patch, minor, major)'
required: true
default: 'patch'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.7'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel
pip install -r requirements-dev.txt
- name: Bump version
run: |
git config --local user.email "hello@qmk.fm"
git config --local user.name "QMK Bot"
bumpversion ${{ github.event.inputs.version_part }}
- name: Push changes
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: master
tags: true
- name: Build and publish
env:
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
run: |
python setup.py sdist bdist_wheel
twine upload dist/*
-15
View File
@@ -1,15 +0,0 @@
# Code Of Conduct
QMK strives to be an inclusive, tolerant, and welcoming community. We encourage participation from anyone regardless of age, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, political belief, race, religion, or sexual identity and orientation.
> “A gentle word turns away wrath, but a harsh word stirs up anger."
Our users, contributors, and collaborators are expected to treat each other with kindness and respect, to assume good intentions, and to gently correct, where possible, rather than react with escalation. While our goal is to be as accurate as possible, kindness and understanding are more valuable than correctness. Some examples of behavior we will not tolerate include, but is not limited to:
* The use of sexualized language or imagery
* Unwelcome advances, sexual or otherwise
* Insults or derogatory comments, or personal or political attacks
* Publishing others private information without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
If someone is violating this Code of Conduct you may email hello@qmk.fm to bring your concern to the Members. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident.
+11 -4
View File
@@ -1,4 +1,5 @@
# QMK CLI
[![CLI Setup](https://github.com/qmk/qmk_cli/workflows/CLI%20Setup/badge.svg)](https://github.com/qmk/qmk_cli/actions?query=workflow%3A%22CLI+Setup%22)
A program to help users work with [QMK Firmware](https://qmk.fm/).
@@ -8,10 +9,16 @@ A program to help users work with [QMK Firmware](https://qmk.fm/).
* Use `qmk clone` to pull down anyone's `qmk_firmware` fork
* Setup your build environment with `qmk setup`
* Check that your environment is correctly setup with `qmk doctor`
* Dispatches to `qmk_firmware/bin/qmk` for additional functionality:
* `qmk compile-json`
* More to come
* Integrates with qmk_firmware for additional functionality:
* `qmk compile`
* `qmk console`
* `qmk flash`
* `qmk lint`
* ...and many more!
# Quickstart
To be written.
* `python3 -m pip install qmk`
* `qmk setup`
Full documentation: <https://docs.qmk.fm/#/tutorial>
-742
View File
@@ -1,742 +0,0 @@
#!/usr/bin/env python3
# coding=utf-8
"""MILC - A CLI Framework
PYTHON_ARGCOMPLETE_OK
MILC is an opinionated framework for writing CLI apps. It optimizes for the
most common unix tool pattern- small tools that are run from the command
line but generally do not feature any user interaction while they run.
For more details see the MILC documentation:
<https://github.com/clueboard/milc/tree/master/docs>
"""
from __future__ import division, print_function, unicode_literals
import argparse
import logging
import os
import re
import shlex
import sys
from decimal import Decimal
from tempfile import NamedTemporaryFile
from time import sleep
try:
from ConfigParser import RawConfigParser
except ImportError:
from configparser import RawConfigParser
try:
import thread
import threading
except ImportError:
thread = None
import argcomplete
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
EMOJI_LOGLEVELS = {
'CRITICAL': '{bg_red}{fg_white}¬_¬{style_reset_all}',
'ERROR': '{fg_red}{style_reset_all}',
'WARNING': '{fg_yellow}{style_reset_all}',
'INFO': '{fg_blue}{style_reset_all}',
'DEBUG': '{fg_cyan}{style_reset_all}',
'NOTSET': '{style_reset_all}¯\\_(o_o)_/¯'
}
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:
# https://stackoverflow.com/a/45448194
ansi_regex = r'\x1b(' \
r'(\[\??\d+[hl])|' \
r'([=<>a-kzNM78])|' \
r'([\(\)][a-b0-2])|' \
r'(\[\d{0,2}[ma-dgkjqi])|' \
r'(\[\d+;\d+[hfy]?)|' \
r'(\[;?[hf])|' \
r'(#[3-68])|' \
r'([01356]n)|' \
r'(O[mlnp-z]?)|' \
r'(/Z)|' \
r'(\d+)|' \
r'(\[\?\d;\d0c)|' \
r'(\d;\dR))'
ansi_escape = re.compile(ansi_regex, flags=re.IGNORECASE)
ansi_styles = (
('fg', colorama.ansi.AnsiFore()),
('bg', colorama.ansi.AnsiBack()),
('style', colorama.ansi.AnsiStyle()),
)
ansi_colors = {}
for prefix, obj in ansi_styles:
for color in [x for x in obj.__dict__ if not x.startswith('_')]:
ansi_colors[prefix + '_' + color.lower()] = getattr(obj, color)
def format_ansi(text):
"""Return a copy of text with certain strings replaced with ansi.
"""
# Avoid .format() so we don't have to worry about the log content
for color in ansi_colors:
text = text.replace('{%s}' % color, ansi_colors[color])
return text + ansi_colors['style_reset_all']
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 on UTF capable terminals.
"""
def format(self, record):
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)
class Configuration(object):
"""Represents the running configuration.
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)
def __iter__(self):
return self._config.__iter__()
def __len__(self):
return self._config.__len__()
def __repr__(self):
return self._config.__repr__()
def keys(self):
return self._config.keys()
def items(self):
return self._config.items()
def values(self):
return self._config.values()
def __init__(self, *args, **kwargs):
self._config = {}
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] = ConfigurationSection(self)
return self._config[key]
def __setitem__(self, key, value):
self.__dict__[key] = value
self._config[key] = value
def __delitem__(self, key):
if key in self.__dict__ and key[0] != '_':
del self.__dict__[key]
if key in self._config:
del self._config[key]
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 value, pulling from the `default` section as a fallback.
"""
if key in self._config:
return self._config[key]
elif key in self.parent.default:
return self.parent.default[key]
return None
def handle_store_boolean(self, *args, **kwargs):
"""Does the add_argument for action='store_boolean'.
"""
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']
for flag in args:
if flag[:2] == '--':
disabled_args = ('--no-' + flag[2:],)
break
self.add_argument(*args, **kwargs)
self.add_argument(*disabled_args, **disabled_kwargs)
return (args, kwargs, disabled_args, disabled_kwargs)
class SubparserWrapper(object):
"""Wrap subparsers so we can populate the normal and the shadow parser.
"""
def __init__(self, cli, submodule, subparser):
self.cli = cli
self.submodule = submodule
self.subparser = subparser
for attr in dir(subparser):
if not hasattr(self, attr):
setattr(self, attr, getattr(subparser, attr))
def completer(self, completer):
"""Add an arpcomplete completer to this subcommand.
"""
self.subparser.completer = completer
def add_argument(self, *args, **kwargs):
if 'action' in kwargs and kwargs['action'] == 'store_boolean':
return handle_store_boolean(self, *args, **kwargs)
self.cli.acquire_lock()
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)
self.cli.release_lock()
class MILC(object):
"""MILC - An Opinionated Batteries Included Framework
"""
def __init__(self):
"""Initialize the MILC object.
"""
# Setup a lock for thread safety
self._lock = threading.RLock() if thread else None
# Define some basic info
self.acquire_lock()
self._description = None
self._entrypoint = None
self._inside_context_manager = False
self.ansi = ansi_colors
self.arg_only = []
self.config = Configuration()
self.config_file = None
self.version = os.environ.get('QMK_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.initialize_argparse()
self.initialize_logging()
@property
def description(self):
return self._description
@description.setter
def description(self, value):
self._description = self._arg_parser.description = self._arg_defaults.description = value
def echo(self, text, *args, **kwargs):
"""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
strings.
If *args or **kwargs are passed they will be used to %-format the strings.
"""
if args and kwargs:
raise RuntimeError('You can only specify *args or **kwargs, not both!')
args = args or kwargs
text = format_ansi(text)
print(text % args)
def initialize_argparse(self):
"""Prepare to process arguments from sys.argv.
"""
kwargs = {
'fromfile_prefix_chars': '@',
'conflict_handler': 'resolve',
}
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
self.print_help = self._arg_parser.print_help
self.release_lock()
def completer(self, completer):
"""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.
"""
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] == '-':
kwargs['dest'] = 'general_' + self.get_argument_name(*args, **kwargs)
if 'add_dest' in kwargs:
del kwargs['add_dest']
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.release_lock()
def initialize_logging(self):
"""Prepare the defaults for the logging infrastructure.
"""
self.acquire_lock()
self.log_file = None
self.log_file_mode = 'a'
self.log_file_handler = None
self.log_print = True
self.log_print_to = sys.stderr
self.log_print_level = logging.INFO
self.log_file_level = logging.DEBUG
self.log_level = logging.INFO
self.log = logging.getLogger(self.__class__.__name__)
self.log.setLevel(logging.DEBUG)
logging.root.setLevel(logging.DEBUG)
self.release_lock()
self.add_argument('-V', '--version', version=self.version, action='version', help='Display the version and exit')
self.add_argument('-v', '--verbose', action='store_true', help='Make the logging more verbose')
self.add_argument('--datetime-fmt', default='%Y-%m-%d %H:%M:%S', help='Format string for datetimes')
self.add_argument('--log-fmt', default='%(levelname)s %(message)s', help='Format string for printed log output')
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')
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()
def acquire_lock(self):
"""Acquire the MILC lock for exclusive access to properties.
"""
if self._lock:
self._lock.acquire()
def release_lock(self):
"""Release the MILC lock.
"""
if self._lock:
self._lock.release()
def find_config_file(self):
"""Locate the config file.
"""
if self.config_file:
return self.config_file
if self.args and self.args.general_config_file:
return self.args.general_config_file
return os.path.join(user_config_dir(appname='qmk', appauthor='QMK'), '%s.ini' % self.prog_name)
def get_argument_name(self, *args, **kwargs):
"""Takes argparse arguments and returns the dest name.
"""
try:
return self._arg_parser._get_optional_kwargs(*args, **kwargs)['dest']
except ValueError:
return self._arg_parser._get_positional_kwargs(*args, **kwargs)['dest']
def argument(self, *args, **kwargs):
"""Decorator to call self.add_argument or self.<subcommand>.add_argument.
"""
if self._inside_context_manager:
raise RuntimeError('You must run this before the with statement!')
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:
self.add_argument(*args, **kwargs)
elif handler.__name__ in self.subcommands:
self.subcommands[handler.__name__].add_argument(*args, **kwargs)
else:
raise RuntimeError('Decorated function is not entrypoint or subcommand!')
return handler
return argument_function
def arg_passed(self, arg):
"""Returns True if arg was passed on the command line.
"""
return self.args_passed[arg] in (None, False)
def parse_args(self):
"""Parse the CLI args.
"""
if self.args:
self.log.debug('Warning: Arguments have already been parsed, ignoring duplicate attempt!')
return
argcomplete.autocomplete(self._arg_parser)
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.
"""
self.acquire_lock()
self.config_file = self.find_config_file()
if self.config_file and os.path.exists(self.config_file):
config = RawConfigParser(self.config)
config.read(self.config_file)
# Iterate over the config file options and write them into self.config
for section in config.sections():
for option in config.options(section):
value = config.get(section, option)
# Coerce values into useful datatypes
if value.lower() in ['1', 'yes', 'true', 'on']:
value = True
elif value.lower() in ['0', 'no', 'false', 'none', 'off']:
value = False
elif value.replace('.', '').isdigit():
if '.' in value:
value = Decimal(value)
else:
value = int(value)
self.config[section][option] = value
# Fold the CLI args into self.config
for argument in vars(self.args):
if argument in ('subparsers', 'entrypoint'):
continue
if '_' in argument:
section, option = argument.split('_', 1)
else:
section = self._entrypoint.__name__
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()
def save_config(self):
"""Save the current configuration to the config file.
"""
self.log.debug("Saving config file to '%s'", self.config_file)
if not self.config_file:
self.log.warning('%s.config_file file not set, not saving config!', self.__class__.__name__)
return
self.acquire_lock()
config = RawConfigParser()
config_dir = os.path.dirname(self.config_file)
for section_name, section in self.config._config.items():
config.add_section(section_name)
for option_name, value in section.items():
if section_name == 'general':
if option_name in ['save_config']:
continue
config.set(section_name, option_name, str(value))
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)
# Move the new config file into place atomically
if os.path.getsize(tmpfile.name) > 0:
os.rename(tmpfile.name, self.config_file)
else:
self.log.warning('Config file saving failed, not replacing %s with %s.', self.config_file, tmpfile.name)
self.release_lock()
cli.log.info('Wrote configuration to %s', shlex.quote(self.config_file))
def __call__(self):
"""Execute the entrypoint function.
"""
if not self._inside_context_manager:
# If they didn't use the context manager use it ourselves
with self:
return self.__call__()
if not self._entrypoint:
raise RuntimeError('No entrypoint provided!')
return self._entrypoint(self)
def entrypoint(self, description):
"""Set the entrypoint for when no subcommand is provided.
"""
if self._inside_context_manager:
raise RuntimeError('You must run this before cli()!')
self.acquire_lock()
self.description = description
self.release_lock()
def entrypoint_func(handler):
self.acquire_lock()
self._entrypoint = handler
self.release_lock()
return handler
return entrypoint_func
def add_subcommand(self, handler, description, name=None, **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()
if not name:
name = handler.__name__
self.acquire_lock()
kwargs['help'] = description
self.subcommands_default[name] = self._subparsers_default.add_parser(name, **kwargs)
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):
"""Decorator to register a subcommand.
"""
def subcommand_function(handler):
return self.add_subcommand(handler, description, **kwargs)
return subcommand_function
def setup_logging(self):
"""Called by __enter__() to setup the logging configuration.
"""
if len(logging.root.handlers) != 0:
# MILC is the only thing that should have root log handlers
logging.root.handlers = []
self.acquire_lock()
if self.config['general']['verbose']:
self.log_print_level = logging.DEBUG
self.log_file = self.config['general']['log_file'] or self.log_file
self.log_file_format = self.config['general']['log_file_fmt']
self.log_file_format = ANSIStrippingFormatter(self.config['general']['log_file_fmt'], self.config['general']['datetime_fmt'])
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)
else:
self.log_format = ANSIStrippingFormatter(self.args.general_log_fmt, self.config.general.datetime_fmt)
if self.log_file:
self.log_file_handler = logging.FileHandler(self.log_file, self.log_file_mode)
self.log_file_handler.setLevel(self.log_file_level)
self.log_file_handler.setFormatter(self.log_file_format)
logging.root.addHandler(self.log_file_handler)
if self.log_print:
self.log_print_handler = logging.StreamHandler(self.log_print_to)
self.log_print_handler.setLevel(self.log_print_level)
self.log_print_handler.setFormatter(self.log_format)
logging.root.addHandler(self.log_print_handler)
self.release_lock()
def __enter__(self):
if self._inside_context_manager:
self.log.debug('Warning: context manager was entered again. This usually means that self.__call__() was called before the with statement. You probably do not want to do that.')
return
self.acquire_lock()
self._inside_context_manager = True
self.release_lock()
colorama.init()
self.parse_args()
self.read_config()
self.setup_logging()
if 'save_config' in self.config.general and self.config.general.save_config:
self.save_config()
exit(0)
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.acquire_lock()
self._inside_context_manager = False
self.release_lock()
if exc_type is not None and not isinstance(SystemExit(), exc_type):
print(exc_type)
logging.exception(exc_val)
exit(255)
cli = MILC()
if __name__ == '__main__':
@cli.argument('-c', '--comma', help='comma in output', default=True, action='store_boolean')
@cli.entrypoint('My useful CLI tool with subcommands.')
def main(cli):
comma = ',' if cli.config.general.comma else ''
cli.log.info('{bg_green}{fg_red}Hello%s World!', comma)
@cli.argument('-n', '--name', help='Name to greet', default='World')
@cli.subcommand('Description of hello subcommand here.')
def hello(cli):
comma = ',' if cli.config.general.comma else ''
cli.log.info('{fg_blue}Hello%s %s!', comma, cli.config.hello.name)
def goodbye(cli):
comma = ',' if cli.config.general.comma else ''
cli.log.info('{bg_red}Goodbye%s %s!', comma, cli.config.goodbye.name)
@cli.argument('-n', '--name', help='Name to greet', default='World')
@cli.subcommand('Think a bit before greeting the user.')
def thinking(cli):
comma = ',' if cli.config.general.comma else ''
spinner = cli.spinner(text='Just a moment...', spinner='earth')
spinner.start()
sleep(2)
spinner.stop()
with cli.spinner(text='Almost there!', spinner='moon'):
sleep(2)
cli.log.info('{fg_cyan}Hello%s %s!', comma, cli.config.thinking.name)
@cli.subcommand('Show off our ANSI colors.')
def pride(cli):
cli.echo('{bg_red} ')
cli.echo('{bg_lightred_ex} ')
cli.echo('{bg_lightyellow_ex} ')
cli.echo('{bg_green} ')
cli.echo('{bg_blue} ')
cli.echo('{bg_magenta} ')
# You can register subcommands using decorators as seen above, or using functions like like this:
cli.add_subcommand(goodbye, 'This will show up in --help output.')
cli.goodbye.add_argument('-n', '--name', help='Name to bid farewell to', default='World')
cli() # Automatically picks between main(), hello() and goodbye()
+1 -1
View File
@@ -1,3 +1,3 @@
"""A program to help you work with qmk_firmware."""
__version__ = '0.0.12'
__version__ = '0.0.48'
+17 -9
View File
@@ -9,24 +9,32 @@ default_fork = 'qmk/' + default_repo
default_branch = 'master'
def clone(url, destination, branch):
def git_clone(url, destination, branch):
git_clone = [
'git',
'clone',
'--recurse-submodules',
'--branch=' + branch,
url,
destination,
str(destination),
]
cli.log.debug('Git clone command: %s', git_clone)
with subprocess.Popen(git_clone, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, bufsize=1, universal_newlines=True) as p:
for line in p.stdout:
print(line, end='')
try:
with subprocess.Popen(git_clone, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, bufsize=1, universal_newlines=True, encoding='utf-8') as p:
for line in p.stdout:
print(line, end='')
if p.returncode != 0:
except Exception as e:
git_cmd = ' '.join([s.replace(' ', r'\ ') for s in git_clone])
cli.log.error("Could not run '%s': %s: %s", git_cmd, e.__class__.__name__, e)
return False
if p.returncode == 0:
cli.log.info('Successfully cloned %s to %s!', url, destination)
return True
else:
cli.log.error('git clone exited %d', p.returncode)
return False
else:
cli.log.info('Successfully cloned %s to %s!', cli.args.fork, cli.args.destination)
return True
+52 -27
View File
@@ -1,35 +1,60 @@
"""Useful helper functions.
"""
import os
from functools import lru_cache
from importlib.util import find_spec
from pathlib import Path
from milc import cli
def question(question, boolean=True, default=''):
"""Asks the user to answer a question.
This keeps re-asking until it gets acceptible input.
def is_qmk_firmware(qmk_firmware):
"""Returns True if the given Path() is a qmk_firmware clone.
"""
if cli.args.yes:
return True
paths = [
qmk_firmware,
qmk_firmware / 'quantum',
qmk_firmware / 'requirements.txt',
qmk_firmware / 'requirements-dev.txt',
qmk_firmware / 'lib/python/qmk/cli/doctor.py'
]
if default and default.lower() == 'y':
answer_key = 'Y/n'
elif default and default.lower() == 'n':
answer_key = 'y/N'
else:
answer_key = 'y/n'
prompt = '*** %s [%s] ' % (question, answer_key)
while True:
answer = input(prompt)
if answer == '' and default.lower() == 'y':
answer = 'y'
elif answer == '' and default.lower() == 'n':
answer = 'n'
if answer.lower() in ['y', 'yes']:
return True
elif answer.lower() in ['n', 'no']:
for path in paths:
if not path.exists():
return False
else:
cli.args.echo('Invalid answer!')
return True
@lru_cache(maxsize=2)
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.
"""
if in_qmk_firmware():
return in_qmk_firmware()
if cli.config.user.qmk_home:
return Path(cli.config.user.qmk_home).expanduser().resolve()
if 'QMK_HOME' in os.environ:
path = Path(os.environ['QMK_HOME']).expanduser()
if path.exists():
return path.resolve()
return path
return Path.home() / 'qmk_firmware'
def in_qmk_firmware():
"""Returns the path to the qmk_firmware we are currently in, or None if we are not inside qmk_firmware.
"""
cur_dir = Path.cwd()
while len(cur_dir.parents) > 0:
if is_qmk_firmware(cur_dir):
return cur_dir
# Move up a directory before the next iteration
cur_dir = cur_dir / '..'
cur_dir = cur_dir.resolve()
+63 -48
View File
@@ -1,19 +1,20 @@
#!/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.
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`)
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 shlex
import subprocess
import sys
from functools import lru_cache
from pathlib import Path
from platform import platform
from traceback import print_exc
import milc
import milc.subcommand.config # noqa
from milc.questions import yesno
from .helpers import find_qmk_firmware, is_qmk_firmware
milc.EMOJI_LOGLEVELS['INFO'] = '{fg_blue}Ψ{style_reset_all}'
@@ -25,58 +26,72 @@ def qmk_main(cli):
cli.print_help()
@lru_cache(maxsize=2)
def in_qmk_firmware():
"""Returns the path to the qmk_firmware we are currently in, or None if we are not inside qmk_firmware.
def run_cmd(*command):
"""Run a command in a subshell.
"""
cur_dir = Path.cwd()
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, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if 'windows' in milc.cli.platform.lower():
safecmd = map(shlex.quote, command)
safecmd = ' '.join(safecmd)
command = [os.environ['SHELL'], '-c', safecmd]
if result.returncode == 0:
return cur_dir
# Move up a directory before the next iteration
cur_dir = cur_dir / '..'
cur_dir = cur_dir.resolve()
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 'QMK_HOME' in os.environ:
return Path(os.environ['QMK_HOME'])
return Path.home() / 'qmk_firmware'
return subprocess.run(command)
# Python setuptools entrypoint
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.
"""
# Warn if they use an outdated python version
if sys.version_info < (3, 7):
print('Warning: Your Python version is out of date! Some subcommands may not work!')
print('Please upgrade to Python 3.7 or later.')
if 'windows' in platform().lower():
msystem = os.environ.get('MSYSTEM', '')
if 'mingw64' not in sys.executable or 'MINGW64' not in msystem:
print('ERROR: It seems you are not using the MINGW64 terminal.')
print('Please close this terminal and open a new MSYS2 MinGW 64-bit terminal.')
print('Python: %s, MSYSTEM: %s' % (sys.executable, msystem))
exit(1)
# Environment setup
import qmk_cli
milc.cli.version = qmk_cli.__version__
qmk_firmware = find_qmk_firmware()
os.environ['QMK_HOME'] = str(qmk_firmware)
os.environ['ORIG_CWD'] = os.getcwd()
# Import the subcommand modules
import qmk_cli.subcommands
if qmk_firmware.exists():
sys.path.append(str(qmk_firmware / 'lib' / 'python'))
import qmk.cli
# Check out and initialize the qmk_firmware environment
if is_qmk_firmware(qmk_firmware):
# All subcommands are run relative to the qmk_firmware root to make it easier to use the right copy of qmk_firmware.
os.chdir(str(qmk_firmware))
sys.path.append(str(qmk_firmware / 'lib/python'))
try:
import qmk.cli # noqa
except ImportError as e:
if qmk_firmware.name != 'qmk_firmware':
print('Warning: %s does not end in "qmk_firmware". Do you need to set QMK_HOME to "%s/qmk_firmware"?' % (qmk_firmware, qmk_firmware))
print('Error: %s: %s', (e.__class__.__name__, e))
print_exc()
sys.exit(1)
# Call the entrypoint
milc.cli()
return_code = milc.cli()
if return_code is False:
exit(1)
elif return_code is not True and isinstance(return_code, int):
if return_code < 0 or return_code > 255:
milc.cli.log.error('Invalid return_code: %d', return_code)
exit(255)
exit(return_code)
exit(0)
+3 -2
View File
@@ -2,5 +2,6 @@
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
from . import clone # noqa
from . import env # noqa
from . import setup # noqa
+9 -7
View File
@@ -4,19 +4,22 @@ import os
from pathlib import Path
from milc import cli
from qmk_cli.git import clone
from qmk_cli.git import 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.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.clone.baseurl, cli.args.fork))
@@ -24,5 +27,4 @@ def clone(cli):
cli.log.error('Destination already exists: %s', cli.args.destination)
exit(1)
success = clone(git_url, cli.args.destination, cli.config.clone.branch)
exit(0 if success else 1)
return git_clone(git_url, cli.args.destination, cli.config.clone.branch)
+30
View File
@@ -0,0 +1,30 @@
"""Prints environment information.
"""
import os
from pathlib import Path
from milc import cli
from qmk_cli.helpers import is_qmk_firmware
@cli.argument('var', default=None, nargs='?', help='Optional variable to query')
@cli.subcommand('Prints environment information.')
def env(cli):
home = os.environ.get('QMK_HOME', "")
data = {
'QMK_HOME': home,
'QMK_FIRMWARE': home if is_qmk_firmware(Path(home)) else ""
}
# Now munge the current cli config
for key, val in cli.config.general.items():
converted_key = 'QMK_' + key.upper()
data[converted_key] = val
if cli.args.var:
# dump out requested arg
print(data[cli.args.var])
else:
# dump out everything
for key, val in data.items():
print(f'{key}="{val}"')
+82 -27
View File
@@ -1,48 +1,103 @@
"""Setup qmk_firmware on your computer.
"""
import os
import shlex
import subprocess
import sys
from pathlib import Path
from milc import cli
from qmk_cli.git import clone
from qmk_cli.helpers import question
from milc.questions import yesno
from qmk_cli.git import git_clone
from qmk_cli.helpers import is_qmk_firmware
default_base = 'https://github.com'
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('--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')
def git_upstream(destination):
"""Add the qmk/qmk_firmware upstream to a qmk_firmware clone.
"""
git_url = '/'.join((cli.config.setup.baseurl, default_fork))
git_cmd = [
'git',
'-C',
destination,
'remote',
'add',
'upstream',
git_url,
]
with subprocess.Popen(git_cmd, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, bufsize=1, universal_newlines=True, encoding='utf-8') as p:
for line in p.stdout:
print(line, end='')
if p.returncode == 0:
cli.log.info('Added %s as remote upstream.', git_url)
return True
else:
cli.log.error('%s exited %d', ' '.join(git_cmd), p.returncode)
return False
@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=default_base, help='The URL all git operations start from. Default: %s' % default_base)
@cli.argument('-b', '--branch', default=default_branch, help='The branch to clone. Default: %s' % default_branch)
@cli.argument('-H', '--home', default=Path(os.environ['QMK_HOME']), type=Path, help='The location for QMK Firmware. Default: %s' % os.environ['QMK_HOME'])
@cli.argument('fork', default=default_fork, nargs='?', help='The qmk_firmware fork to clone. Default: %s' % default_fork)
@cli.subcommand('Setup your computer for qmk_firmware.')
def setup(cli):
qmk_firmware = Path(cli.args.destination)
"""Guide the user through setting up their QMK environment.
"""
clone_prompt = 'Would you like to clone {fg_cyan}%s{fg_reset} to {fg_cyan}%s{fg_reset}?' % (cli.args.fork, shlex.quote(str(cli.args.home)))
home_prompt = 'Would you like to set {fg_cyan}%s{fg_reset} as your QMK home?' % (shlex.quote(str(cli.args.home)),)
# Sanity checks
if cli.args.yes and cli.args.no:
cli.log.error("Can't use both --yes and --no at the same time.")
exit(1)
# Check on qmk_firmware, and if it doesn't exist offer to check it out.
if qmk_firmware.exists():
cli.log.info('Found qmk_firmware at %s.', str(qmk_firmware))
if is_qmk_firmware(cli.args.home):
cli.log.info('Found qmk_firmware at %s.', str(cli.args.home))
elif cli.args.home.exists():
path_str = str(cli.args.home)
if cli.args.home.name != 'qmk_firmware':
cli.log.warning('Warning: %s does not end in "qmk_firmware". Did you mean to use "--home %s/qmk_firmware"?' % (path_str, path_str))
cli.log.error("Path '%s' exists but is not a qmk_firmware clone!", path_str)
exit(1)
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)
cli.log.error('Could not find qmk_firmware!')
if yesno(clone_prompt):
git_url = '/'.join((cli.config.setup.baseurl, cli.args.fork))
# 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 = subprocess.run([sys.executable, qmk_bin, 'doctor'])
if doctor.returncode != 0:
cli.log.error('Your build environment is not setup completely.')
if git_clone(git_url, cli.args.home, cli.config.setup.branch):
git_upstream(cli.args.home)
else:
exit(1)
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!')
# Offer to set `user.qmk_home` for them.
if str(cli.args.home) != os.environ['QMK_HOME'] and yesno(home_prompt):
cli.config['user']['qmk_home'] = str(cli.args.home.absolute())
cli.write_config_option('user', 'qmk_home')
# Run `qmk doctor` to check the rest of the environment out
color = '--color' if cli.config.general.color else '--no-color'
unicode = '--unicode' if cli.config.general.unicode else '--no-unicode'
doctor_command = [sys.argv[0], color, unicode, 'doctor']
if cli.args.no:
doctor_command.append('--no')
if cli.args.yes:
doctor_command.append('--yes')
cli.run(doctor_command, capture_output=False)
+17 -6
View File
@@ -1,5 +1,8 @@
#!/bin/sh
#!/bin/bash
# Increment the verison number and release a new version of qmk_cli.
#
# Required packages: pip3 install bumpversion twine
set -e
set -x
@@ -9,13 +12,21 @@ TWINE_USERNAME=$PYPI_USERNAME
export FLIT_USERNAME TWINE_USERNAME
rm dist/*
if ! bumpversion -h &> /dev/null; then
echo "Missing bumpversion! Please run: pip3 install -U -r requirements-dev.txt"
exit 1
fi
if ! twine -h &> /dev/null; then
echo "Missing twine! Please run: pip3 install -U -r requirements-dev.txt"
exit 1
fi
# Bump the version, tag, and push
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
# Build and upload
python3 setup.py sdist bdist_wheel
twine upload dist/qmk-*
+2
View File
@@ -0,0 +1,2 @@
bumpversion
twine
+2 -3
View File
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.0.12
current_version = 0.0.48
commit = True
tag = True
tag_name = {new_version}
@@ -23,7 +23,7 @@ dist-name = qmk
license_file = LICENSE
module = qmk_cli
home-page = https://github.com/qmk/qmk_cli
requires-python = >=3.5
requires-python = >=3.7
[entry_points]
qmk = qmk_cli.script_qmk:main
@@ -80,4 +80,3 @@ split_penalty_for_added_line_split = 30
split_penalty_import_names = 0
split_penalty_logical_operator = 300
use_tabs = False
+18 -7
View File
@@ -6,11 +6,13 @@ 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()],
'console_scripts': ['%s = %s' % i for i in setup_cfg['entry_points'].items()],
},
license='MIT License',
url=metadata['home-page'],
@@ -19,10 +21,10 @@ if __name__ == "__main__":
author_email=metadata['author-email'],
maintainer=metadata['author'],
maintainer_email=metadata['author-email'],
long_description=open('README.md').read(),
long_description=long_description,
long_description_content_type="text/markdown",
packages=find_packages(),
py_modules = ['milc'],
py_modules=['qmk_cli'],
classifiers=[
'Development Status :: 3 - Alpha',
'Environment :: Console',
@@ -36,11 +38,20 @@ if __name__ == "__main__":
'Topic :: Software Development',
'Topic :: Utilities',
],
requires_python=metadata['requires-python'],
python_requires=metadata['requires-python'],
install_requires=[
#"milc", #FIXME(skullydazed): Included in the repo for now.
"appdirs",
"argcomplete",
"colorama"
],
"colorama",
"dotty-dict",
"flake8",
"hid",
"hjson",
"jsonschema>=3",
"milc>=1.3.0",
"nose2",
"pygments",
"pyusb",
"yapf"
]
)