Compare commits
262 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ebfc4e5812 | |||
| 33eddecd6e | |||
| ee779cec4d | |||
| df7bd253d4 | |||
| 8c6339493d | |||
| b87b18e9d4 | |||
| e7b88acea2 | |||
| 45c8ad6709 | |||
| 2116dd9582 | |||
| 05d1c36f49 | |||
| 5e57fdefe0 | |||
| af2ef5d90f | |||
| 1f6bb7dab0 | |||
| 2eb3279945 | |||
| 8fd7af98d9 | |||
| 240ccd781a | |||
| af9e8063f6 | |||
| 3054dad82b | |||
| 39a271132b | |||
| c19c21449e | |||
| f245cb7899 | |||
| 0b5866159a | |||
| 0dd2280781 | |||
| c049da0c6f | |||
| 7b69bd6f11 | |||
| c594e5df09 | |||
| 915bb40473 | |||
| af1403fc41 | |||
| 13e5f5c855 | |||
| f393ad0053 | |||
| 947e49a15d | |||
| cd3ed07cd0 | |||
| 558e248e20 | |||
| cd1e98e9d0 | |||
| 8c2ae297ed | |||
| 7be310f364 | |||
| 2316a34ce9 | |||
| 36c23e3de6 | |||
| b6bc144e9e | |||
| 93e629608e | |||
| cacd0fabbc | |||
| d9f9b5a27d | |||
| 4d4892f58a | |||
| ffe20fb63e | |||
| b384f871e8 | |||
| f2b8522c89 | |||
| 75dcb0e475 | |||
| 1e98a2e459 | |||
| c53fb88479 | |||
| fed1e39611 | |||
| ed0d5699ff | |||
| b1542e4a62 | |||
| 9c7c371419 | |||
| ae45bdeb02 | |||
| d20af71118 | |||
| 8f85a8856f | |||
| 87c356773f | |||
| 27a97e4ebf | |||
| 53d8dab86f | |||
| 86a3decd0a | |||
| d3917b10e7 | |||
| 86b3b74b21 | |||
| bb385f7786 | |||
| e07ad4c9ee | |||
| 1e03b45986 | |||
| 7a1b556b25 | |||
| d7911ffaf7 | |||
| daa7f54dd0 | |||
| 753136ade4 | |||
| 579763620d | |||
| cbc22adb76 | |||
| 82bb65bd52 | |||
| 671e553239 | |||
| fa4bcc034d | |||
| 0a048da8a4 | |||
| 7808a1166c | |||
| b9b2ebbe13 | |||
| b4b53e80eb | |||
| 2212fdabf7 | |||
| 1e8a174e0c | |||
| 6868d5ba6f | |||
| e203006537 | |||
| 1c72e1e564 | |||
| aecd02e850 | |||
| 5019970d01 | |||
| 446547df04 | |||
| dea98e71b8 | |||
| ea62fa1cec | |||
| 32129ee5a2 | |||
| 6638c13c36 | |||
| 98ab6a52f6 | |||
| f9d3020616 | |||
| 1c98cf7053 | |||
| 6b98cb311f | |||
| 8a4bf2d437 | |||
| db645b7db8 | |||
| 3f4c491202 | |||
| 39d3adddff | |||
| d557058db7 | |||
| 3126bdb90a | |||
| d2f73683f8 | |||
| 83978d4c68 | |||
| bb00940631 | |||
| 29bcd3aa01 | |||
| 42481f2db3 | |||
| 928d22d370 | |||
| 169e12a4c6 | |||
| 26ad0cd2b9 | |||
| 63fc8ff722 | |||
| bb912d3473 | |||
| 7cb41c2daa | |||
| c7af299b48 | |||
| c0698e953b | |||
| 54bf8328a1 | |||
| ace9daea3e | |||
| 255990f45b | |||
| 1742e84da5 | |||
| 2b4021eaa2 | |||
| 670ead38ad | |||
| c12c4a6e0d | |||
| bd5e3b5cb4 | |||
| 82fede52ac | |||
| 76fa43cf78 | |||
| 7ec12a8b9d | |||
| b714ea60c3 | |||
| 1797e83de8 | |||
| 593ae652e6 | |||
| 963818be15 | |||
| 47b977f9b2 | |||
| 8c0f655483 | |||
| e0aacc9dc7 | |||
| 24472967eb | |||
| 1a2e43aa58 | |||
| ffed902922 | |||
| 325868514b | |||
| dbfdd2dec5 | |||
| de7f8f6b70 | |||
| 552fe67430 | |||
| de28654c6d | |||
| 02c190894a | |||
| 7df9a61f89 | |||
| dad8b3c15a | |||
| e24448ed18 | |||
| 9befc84294 | |||
| bb489e8ec2 | |||
| f208f0228c | |||
| e4e8f987e1 | |||
| 9f74f1ca9c | |||
| bb1a9e43cf | |||
| 501182017e | |||
| 710dc3e951 | |||
| 12242d94c2 | |||
| 19336ff9ee | |||
| 8e1de74918 | |||
| 137ed60056 | |||
| d320265936 | |||
| 97346a44bc | |||
| 1e0b08250d | |||
| 6a928e9322 | |||
| 5315431058 | |||
| aa11928c03 | |||
| 369c3e150a | |||
| c4e82e0740 | |||
| 95152b6f7f | |||
| 3bd1a79bfa | |||
| 42fcfc3930 | |||
| 60f2bb743d | |||
| f8902bd258 | |||
| f7125127f1 | |||
| f8fe363999 | |||
| bbd667e30c | |||
| 32506bf1bb | |||
| 45a231af0c | |||
| 1f7ad8a73e | |||
| 2faca28631 | |||
| d99eed2ed0 | |||
| a5a481e322 | |||
| 21daea7028 | |||
| 51e80c2f52 | |||
| 567ef9637e | |||
| e8818c9d23 | |||
| 2710813c51 | |||
| fb69e6d50a | |||
| 135d9f63a1 | |||
| 695cd7eb4d | |||
| 61ca025afd | |||
| 2bd1630621 | |||
| 34517d01b7 | |||
| 0a4d66e13a | |||
| fe0a3af05f | |||
| 4508b8548f | |||
| 2ea90a0893 | |||
| 9fccc990be | |||
| 6d99eacdf8 | |||
| dbd731585a | |||
| 9d480bd287 | |||
| 709f416e52 | |||
| bcdac9a80e | |||
| 4772e68828 | |||
| 24741c7d2b | |||
| b756421528 | |||
| 3d96bcdc14 | |||
| 6e6b6bcde6 | |||
| 1df06eed72 | |||
| 60a56b6976 | |||
| 1e3605feb3 | |||
| 48ddcefea1 | |||
| 2f6f38cc33 | |||
| c73ae7e97c | |||
| 0475193870 | |||
| 8d9b3b2880 | |||
| 0dbe3dc448 | |||
| c41fd5e940 | |||
| b70736a876 | |||
| 69b3b890d2 | |||
| 8b30a9766d | |||
| 982349a450 | |||
| 4d161c9ec4 | |||
| 047eb00594 | |||
| 558251860a | |||
| b685fe5d3f | |||
| e447fb4252 | |||
| 066a865084 | |||
| a1276f8d58 | |||
| 3555e34ae9 | |||
| b36cf144e3 | |||
| fb05b69adc | |||
| 4283c258d9 | |||
| b5e8aa4cac | |||
| 1c6d81fc3b | |||
| 0ffae74377 | |||
| cbe3ad0248 | |||
| 97689b53d7 | |||
| 054e071787 | |||
| 78e0fda2f0 | |||
| 9c6123483b | |||
| e851c8fcb5 | |||
| 92168e1ef0 | |||
| fd744ac24c | |||
| 840c791cc8 | |||
| 8f5b8e83c0 | |||
| 1da98510d3 | |||
| 6ec242b133 | |||
| 6433cbc0f4 | |||
| aedad7e602 | |||
| 504291bbf5 | |||
| c6e736adc7 | |||
| fe42ab3534 | |||
| d805fe2b6e | |||
| 5321a121fc | |||
| 3c31d41bf3 | |||
| c6977fa851 | |||
| 3b6c0fd058 | |||
| 216f5d44c1 | |||
| 0bf0b5a503 | |||
| 4b11e04672 | |||
| aba043abdc | |||
| efd9f68243 | |||
| e3e0856f96 | |||
| 670a07ff0b | |||
| c4c76e3534 | |||
| 9ad519d4a2 |
@@ -0,0 +1,12 @@
|
||||
[bumpversion]
|
||||
current_version = 1.2.0
|
||||
commit = True
|
||||
tag = True
|
||||
tag_name = {new_version}
|
||||
message = New release: {current_version} → {new_version}
|
||||
|
||||
[bumpversion:file:.bumpversion.cfg]
|
||||
|
||||
[bumpversion:file:qmk_cli/__init__.py]
|
||||
|
||||
[bumpversion:file:pyproject.toml]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions" # See documentation for possible values
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
reviewers:
|
||||
- "qmk/collaborators"
|
||||
- package-ecosystem: "pip" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "daily"
|
||||
reviewers:
|
||||
- "qmk/collaborators"
|
||||
@@ -0,0 +1,41 @@
|
||||
name: CLI Lint
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
paths:
|
||||
- 'qmk_cli/**'
|
||||
- 'pyproject.toml'
|
||||
- '.github/workflows/cli_lint.yml'
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-slim
|
||||
|
||||
steps:
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.9'
|
||||
|
||||
- name: Activate virtualenv
|
||||
shell: bash
|
||||
run: |
|
||||
python -m venv .ci_venv
|
||||
source .ci_venv/bin/activate
|
||||
echo PATH=$PATH >> $GITHUB_ENV
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Install dependencies
|
||||
run: python -m pip install --group dev
|
||||
|
||||
- name: Run lint
|
||||
run: ruff check --no-cache --output-format=github
|
||||
|
||||
- name: Run format
|
||||
run: yapf -r --diff qmk_cli test
|
||||
@@ -7,66 +7,119 @@ on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test_cli_base_container:
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/qmk/qmk_base_container
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Install dependencies
|
||||
run: apt-get update && apt-get install -y python3-venv
|
||||
|
||||
- name: Run unit test
|
||||
shell: bash
|
||||
run: |
|
||||
python3 -m venv .ci_venv
|
||||
source .ci_venv/bin/activate
|
||||
python3 -m pip install -r requirements.txt
|
||||
python3 -m pip install --group dev
|
||||
pytest
|
||||
|
||||
- name: Run ci_tests
|
||||
run: ./ci_tests
|
||||
|
||||
test_cli_linux_macos:
|
||||
needs: test_cli_base_container
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
QMK_HOME: ~/qmk_firmware
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest]
|
||||
python-version: [3.6, 3.7, 3.8]
|
||||
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
python3 -m pip install wheel
|
||||
echo "::add-path::$HOME/.local/bin"
|
||||
|
||||
- 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
|
||||
- name: Run ci_tests
|
||||
run: ./ci_tests
|
||||
|
||||
test_cli_win:
|
||||
needs: test_cli_base_container
|
||||
|
||||
runs-on: windows-latest
|
||||
env:
|
||||
QMK_HOME: $HOME/qmk_firmware
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- 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
|
||||
install: git mingw-w64-x86_64-toolchain mingw-w64-x86_64-python-pip mingw-w64-x86_64-python-build mingw-w64-x86_64-python-pillow mingw-w64-x86_64-rust mingw-w64-x86_64-python-halo mingw-w64-x86_64-python-nh3 mingw-w64-x86_64-python-rpds-py mingw-w64-x86_64-python-ruff
|
||||
|
||||
- name: (MSYS2) Upgrade pip and setuptools and install wheel
|
||||
# Upgrade pip due to msys packaging + pypa/build/pull/736 issues
|
||||
- name: (MSYS2) Install Python dependencies
|
||||
shell: msys2 {0}
|
||||
run: |
|
||||
python3 -m pip install --upgrade pip setuptools
|
||||
python3 -m pip install wheel
|
||||
python3 -m pip install --break-system-packages --force-reinstall --upgrade pip
|
||||
|
||||
- name: (MSYS2) Install QMK CLI from source
|
||||
- name: Run ci_tests
|
||||
shell: msys2 {0}
|
||||
run: ./ci_tests
|
||||
|
||||
test_docker_container:
|
||||
needs: test_cli_base_container
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
TEST_TAG: qmkfm/qmk_cli:test
|
||||
|
||||
steps:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.9'
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Install dependencies
|
||||
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
|
||||
python3 -m pip install --upgrade pip
|
||||
pip install setuptools wheel
|
||||
pip install --group dev
|
||||
|
||||
- name: (MSYS2) Run qmk setup -y
|
||||
shell: msys2 {0}
|
||||
run: qmk setup -y
|
||||
- name: Build Python
|
||||
run: |
|
||||
python3 -m build
|
||||
|
||||
- name: Build Test Container
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
load: true
|
||||
tags: ${{ env.TEST_TAG }}
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
docker run --rm -v ${{ github.workspace }}:/qmk_cli -w /qmk_cli ${{ env.TEST_TAG }} /qmk_cli/ci_tests
|
||||
|
||||
- name: Build All Containers
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
target: full
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ master ]
|
||||
schedule:
|
||||
- cron: '22 18 * * 0'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'python' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||
# Learn more:
|
||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v4
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v4
|
||||
@@ -0,0 +1,74 @@
|
||||
name: Rebuild Docker Image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
redeploy:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.9'
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get Previous tag
|
||||
id: previoustag
|
||||
uses: "WyriHaximus/github-action-get-previous-tag@v2"
|
||||
|
||||
- name: Download previous artifact
|
||||
run: |
|
||||
pip download -d "${GITHUB_WORKSPACE}/dist" qmk=="${{ steps.previoustag.outputs.tag }}"
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||
|
||||
- name: Build and Push slim to Docker Hub
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
target: slim
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: |
|
||||
ghcr.io/qmk/qmk_cli:slim
|
||||
qmkfm/qmk_cli:slim
|
||||
ghcr.io/qmk/qmk_cli:${{ steps.previoustag.outputs.tag }}-slim
|
||||
qmkfm/qmk_cli:${{ steps.previoustag.outputs.tag }}-slim
|
||||
|
||||
- name: Build and Push full to Docker Hub
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
target: full
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: |
|
||||
ghcr.io/qmk/qmk_cli:latest
|
||||
qmkfm/qmk_cli:latest
|
||||
ghcr.io/qmk/qmk_cli:${{ github.sha }}
|
||||
qmkfm/qmk_cli:${{ github.sha }}
|
||||
ghcr.io/qmk/qmk_cli:${{ steps.previoustag.outputs.tag }}
|
||||
qmkfm/qmk_cli:${{ steps.previoustag.outputs.tag }}
|
||||
@@ -0,0 +1,101 @@
|
||||
# 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:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.9'
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python3 -m pip install --upgrade pip
|
||||
pip install setuptools wheel
|
||||
pip install --group dev
|
||||
|
||||
- name: Bump version
|
||||
run: |
|
||||
git config --local user.email "hello@qmk.fm"
|
||||
git config --local user.name "QMK Bot"
|
||||
bumpversion --dry-run --list ${{ github.event.inputs.version_part }} | grep new_version= | sed "s/new_version/NEW_CLI_VERSION/" >> "$GITHUB_ENV"
|
||||
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: __token__
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_APITOKEN }}
|
||||
run: |
|
||||
python3 -m build
|
||||
twine upload dist/*
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||
|
||||
- name: Build and Push slim to Docker Hub
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
target: slim
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: |
|
||||
ghcr.io/qmk/qmk_cli:slim
|
||||
qmkfm/qmk_cli:slim
|
||||
ghcr.io/qmk/qmk_cli:${{ env.NEW_CLI_VERSION }}-slim
|
||||
qmkfm/qmk_cli:${{ env.NEW_CLI_VERSION }}-slim
|
||||
|
||||
- name: Build and Push full to Docker Hub
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
target: full
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: |
|
||||
ghcr.io/qmk/qmk_cli:latest
|
||||
qmkfm/qmk_cli:latest
|
||||
ghcr.io/qmk/qmk_cli:${{ github.sha }}
|
||||
qmkfm/qmk_cli:${{ github.sha }}
|
||||
ghcr.io/qmk/qmk_cli:${{ env.NEW_CLI_VERSION }}
|
||||
qmkfm/qmk_cli:${{ env.NEW_CLI_VERSION }}
|
||||
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"python.formatting.provider": "yapf"
|
||||
}
|
||||
@@ -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.
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
FROM ghcr.io/qmk/qmk_base_container:latest AS builder
|
||||
|
||||
# Copy package in
|
||||
ADD dist /tmp/dist
|
||||
|
||||
# Install QMK CLI via bootstrap script
|
||||
RUN mkdir -p /opt/uv/bin \
|
||||
&& export UV_PYTHON_INSTALL_DIR=/opt/uv/bin \
|
||||
&& /bin/bash -c "curl -fsSL https://install.qmk.fm | sh -s -- --confirm --uv-install-dir=/usr/local/bin --uv-tool-dir=/opt/uv/tools --qmk-distrib-dir=/opt/qmk"
|
||||
|
||||
# Do the equivalent of entering the virtual environment
|
||||
ENV PATH=/opt/qmk/bin:/opt/uv/tools/qmk/bin:/usr/local/bin:$PATH \
|
||||
VIRTUAL_ENV=/opt/uv/tools/qmk
|
||||
|
||||
# Install python packages
|
||||
RUN python3 -m pip uninstall -y qmk || true
|
||||
RUN python3 -m pip install --upgrade pip setuptools wheel nose2 && \
|
||||
python3 -m pip install /tmp/dist/qmk-*.whl
|
||||
|
||||
# Allow the uv-installed python to be usable by others
|
||||
RUN chmod -R go=u,go-w /opt/uv /opt/qmk
|
||||
|
||||
# 2nd stage so we don't have /tmp/dist in the final image
|
||||
FROM ghcr.io/qmk/qmk_base_container:latest AS slim
|
||||
|
||||
COPY --from=builder /opt/uv /opt/uv
|
||||
|
||||
# Do the equivalent of entering the virtual environment
|
||||
ENV PATH=/opt/uv/tools/qmk/bin:$PATH \
|
||||
VIRTUAL_ENV=/opt/uv/tools/qmk \
|
||||
QMK_HOME=/qmk_firmware \
|
||||
QMK_USERSPACE=/qmk_userspace
|
||||
|
||||
# 2nd stage so we don't have /tmp/dist in the final image
|
||||
FROM ghcr.io/qmk/qmk_base_container:latest AS full
|
||||
|
||||
COPY --from=builder /opt/uv /opt/uv
|
||||
COPY --from=builder /opt/qmk /opt/qmk
|
||||
|
||||
# Do the equivalent of entering the virtual environment
|
||||
ENV PATH=/opt/qmk/bin:/opt/uv/tools/qmk/bin:$PATH \
|
||||
VIRTUAL_ENV=/opt/uv/tools/qmk \
|
||||
QMK_DISTRIB_DIR=/opt/qmk \
|
||||
QMK_HOME=/qmk_firmware \
|
||||
QMK_USERSPACE=/qmk_userspace
|
||||
|
||||
@@ -1,17 +1,57 @@
|
||||
# QMK CLI
|
||||
[](https://github.com/qmk/qmk_cli/actions?query=workflow%3A%22CLI+Setup%22)
|
||||
A program to help users work with [QMK Firmware](https://qmk.fm/).
|
||||
|
||||
# Features
|
||||
> A program to help users work with [QMK Firmware](https://qmk.fm/).
|
||||
|
||||
[](https://github.com/qmk/qmk_cli/tags)
|
||||
[](https://github.com/qmk/qmk_cli/actions?query=workflow%3ACLI+branch%3Amaster)
|
||||
[](https://discord.gg/qmk)
|
||||
|
||||
## Features
|
||||
|
||||
* Interact with your qmk_firmware tree from any location
|
||||
* Use `qmk clone` to pull down anyone's `qmk_firmware` fork
|
||||
* Setup your build environment with `qmk setup`
|
||||
* Use `qmk console` to get debugging information from your keyboard(s)
|
||||
* 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 info`
|
||||
* `qmk flash`
|
||||
* `qmk lint`
|
||||
* ...and many more!
|
||||
|
||||
# Quickstart
|
||||
## Quickstart
|
||||
|
||||
To be written.
|
||||
Follow our [documentation to get started](https://docs.qmk.fm/newbs_getting_started).
|
||||
|
||||
## Packages
|
||||
|
||||
We provide "install and go" packages for many Operating Systems.
|
||||
|
||||
### macOS
|
||||
|
||||
Using [Homebrew](https://brew.sh):
|
||||
|
||||
brew install qmk/qmk/qmk
|
||||
|
||||
### Windows
|
||||
|
||||
Download our custom MSYS2 installer here: https://msys.qmk.fm/
|
||||
|
||||
## Building
|
||||
|
||||
We follow PEP517, you can install using [build](https://pypi.org/project/build/):
|
||||
|
||||
Setup:
|
||||
|
||||
python3 -m pip install --group dev
|
||||
|
||||
Build:
|
||||
|
||||
python3 -m build
|
||||
|
||||
You can read more about working with PEP517 packages in the [Python Packaging User Guide](https://packaging.python.org/guides/distributing-packages-using-setuptools/#packaging-your-project).
|
||||
|
||||
## Documentation
|
||||
|
||||
Full documentation: <https://docs.qmk.fm/#/tutorial>
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Use this section to tell people about which versions of your project are
|
||||
currently being supported with security updates.
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 0.0.x | :white_check_mark: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
* [Open an Issue](https://github.com/qmk/qmk_cli/issues/new?assignees=&labels=bug&template=bug_report.md&title=%5BBug%5D+)
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
#set -x
|
||||
|
||||
TMPDIR=$(mktemp -d)
|
||||
|
||||
trap 'rm -rf "$TMPDIR"' EXIT
|
||||
|
||||
CI_VENV="$TMPDIR/.ci_venv"
|
||||
|
||||
# Avoid issues with building pillow as we install mingw-w64-x86_64-python-pillow
|
||||
if [[ "$(uname -s)" =~ ^MINGW64_NT.* ]]; then
|
||||
CI_VENV_ARGS="--system-site-packages"
|
||||
fi
|
||||
|
||||
python3 -m venv $CI_VENV_ARGS $CI_VENV
|
||||
source $CI_VENV/bin/activate
|
||||
|
||||
# Install dependencies
|
||||
python3 -m pip install -U pip wheel
|
||||
python3 -m pip install .
|
||||
python3 -m pip install --group dev
|
||||
|
||||
pytest -vv -m system --qmk=real
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=82.0.1"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "qmk"
|
||||
version = "1.2.0"
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
authors = [{name = "skullydazed", email = "skullydazed@gmail.com"}]
|
||||
description = "A program to help users work with QMK Firmware."
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Environment :: Console",
|
||||
"Intended Audience :: Developers",
|
||||
"Intended Audience :: System Administrators",
|
||||
"Intended Audience :: End Users/Desktop",
|
||||
"Natural Language :: English",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Topic :: Scientific/Engineering",
|
||||
"Topic :: Software Development",
|
||||
"Topic :: Utilities",
|
||||
]
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"hid",
|
||||
"milc>=1.9.0",
|
||||
"platformdirs",
|
||||
"pyusb",
|
||||
# qmk_firmware packages
|
||||
"dotty-dict",
|
||||
"hjson",
|
||||
"jsonschema>=4",
|
||||
"pillow",
|
||||
"pygments",
|
||||
"pyserial",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"build",
|
||||
"bumpversion",
|
||||
"pytest",
|
||||
"ruff",
|
||||
"twine",
|
||||
"yapf",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
"Bug Tracker" = "https://github.com/qmk/qmk_cli/issues"
|
||||
Documentation = "https://docs.qmk.fm/cli"
|
||||
Homepage = "https://qmk.fm/"
|
||||
Source = "https://github.com/qmk/qmk_cli/"
|
||||
|
||||
[project.scripts]
|
||||
qmk = "qmk_cli.script_qmk:main"
|
||||
|
||||
[tool.setuptools]
|
||||
include-package-data = false
|
||||
|
||||
[tool.setuptools.packages]
|
||||
find = {namespaces = false}
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py39"
|
||||
|
||||
include = [
|
||||
"qmk_cli/**/*.py",
|
||||
"test/**/*.py",
|
||||
]
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "B", "N", "W", "COM", "C901", "PGH004"]
|
||||
ignore = [
|
||||
"E501", # QMK is ok with long lines.
|
||||
"E231", # Conflicts with our yapf config
|
||||
]
|
||||
|
||||
[tool.ruff.lint.mccabe]
|
||||
max-complexity = 14 # Let's slowly crank this down
|
||||
|
||||
[tool.yapfignore]
|
||||
ignore_patterns = [
|
||||
".direnv/**/*",
|
||||
".env/**/*",
|
||||
]
|
||||
|
||||
[tool.yapf]
|
||||
align_closing_bracket_with_visual_indent = true
|
||||
allow_multiline_dictionary_keys = false
|
||||
allow_multiline_lambdas = false
|
||||
allow_split_before_default_or_named_assigns = true
|
||||
allow_split_before_dict_value = true
|
||||
arithmetic_precedence_indication = true
|
||||
blank_lines_around_top_level_definition = 2
|
||||
blank_line_before_class_docstring = false
|
||||
blank_line_before_module_docstring = false
|
||||
blank_line_before_nested_class_or_def = false
|
||||
coalesce_brackets = true
|
||||
column_limit = 256
|
||||
continuation_align_style = "SPACE"
|
||||
continuation_indent_width = 4
|
||||
dedent_closing_brackets = true
|
||||
disable_ending_comma_heuristic = false
|
||||
each_dict_entry_on_separate_line = true
|
||||
i18n_comment = ""
|
||||
i18n_function_call = ""
|
||||
indent_blank_lines = false
|
||||
indent_dictionary_value = true
|
||||
indent_width = 4
|
||||
join_multiple_lines = false
|
||||
no_spaces_around_selected_binary_operators = ""
|
||||
spaces_around_default_or_named_assign = false
|
||||
spaces_around_power_operator = false
|
||||
spaces_before_comment = 2
|
||||
space_between_ending_comma_and_closing_bracket = false
|
||||
split_all_comma_separated_values = false
|
||||
split_arguments_when_comma_terminated = true
|
||||
split_before_arithmetic_operator = false
|
||||
split_before_bitwise_operator = true
|
||||
split_before_closing_bracket = true
|
||||
split_before_dict_set_generator = true
|
||||
split_before_dot = false
|
||||
split_before_expression_after_opening_paren = false
|
||||
split_before_first_argument = false
|
||||
split_before_logical_operator = false
|
||||
split_before_named_assigns = true
|
||||
split_complex_comprehension = true
|
||||
split_penalty_after_opening_bracket = 300
|
||||
split_penalty_after_unary_operator = 10000
|
||||
split_penalty_arithmetic_operator = 300
|
||||
split_penalty_before_if_expr = 0
|
||||
split_penalty_bitwise_operator = 300
|
||||
split_penalty_comprehension = 80
|
||||
split_penalty_excess_character = 7000
|
||||
split_penalty_for_added_line_split = 30
|
||||
split_penalty_import_names = 0
|
||||
split_penalty_logical_operator = 300
|
||||
use_tabs = false
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
markers = [
|
||||
"integration",
|
||||
"system",
|
||||
]
|
||||
addopts = "-m 'not (integration or system)'"
|
||||
@@ -2,4 +2,5 @@
|
||||
"""Wrapper to call the qmk cli script entrypoint.
|
||||
"""
|
||||
import qmk_cli.script_qmk
|
||||
|
||||
qmk_cli.script_qmk.main()
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
"""A program to help you work with qmk_firmware."""
|
||||
|
||||
__version__ = '0.0.37'
|
||||
__version__ = '1.2.0'
|
||||
|
||||
+69
-17
@@ -1,32 +1,84 @@
|
||||
"""Helpers for working with git.
|
||||
"""
|
||||
"""Helpers for working with git."""
|
||||
|
||||
import subprocess
|
||||
from .helpers import rmtree
|
||||
|
||||
from milc import cli
|
||||
|
||||
default_repo = 'qmk_firmware'
|
||||
default_fork = 'qmk/' + default_repo
|
||||
default_branch = 'master'
|
||||
DEFAULT_UPSTREAM = 'https://github.com/qmk/qmk_firmware'
|
||||
|
||||
|
||||
def git_clone(url, destination, branch):
|
||||
def git_clone(destination, url, branch):
|
||||
"""Add the qmk/qmk_firmware upstream to a qmk_firmware clone."""
|
||||
git_clone = [
|
||||
'git',
|
||||
'clone',
|
||||
'--recurse-submodules',
|
||||
'--branch=' + branch,
|
||||
f'--branch={branch}',
|
||||
url,
|
||||
destination,
|
||||
str(destination),
|
||||
]
|
||||
cli.log.debug('Git clone command: %s', git_clone)
|
||||
cli.log.debug(f'Git clone command: {git_clone}')
|
||||
|
||||
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='')
|
||||
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:
|
||||
cli.log.info('Successfully cloned %s to %s!', url, destination)
|
||||
return True
|
||||
else:
|
||||
cli.log.error('git clone exited %d', p.returncode)
|
||||
except Exception as e:
|
||||
git_cmd = ' '.join([s.replace(' ', r'\ ') for s in git_clone])
|
||||
|
||||
cli.log.error(f"Could not run '{git_cmd}': {e.__class__.__name__}:{e}")
|
||||
return False
|
||||
|
||||
if p.returncode != 0:
|
||||
cli.log.error(f'git clone exited {p.returncode}')
|
||||
return False
|
||||
|
||||
cli.log.info(f'Successfully cloned {url} to {destination}!')
|
||||
return True
|
||||
|
||||
|
||||
def git_set_upstream(destination):
|
||||
"""Add the qmk/qmk_firmware upstream to a qmk_firmware clone."""
|
||||
git_remote = [
|
||||
'git',
|
||||
'-C',
|
||||
str(destination),
|
||||
'remote',
|
||||
'add',
|
||||
'upstream',
|
||||
DEFAULT_UPSTREAM,
|
||||
]
|
||||
cli.log.debug(f'Git remote command: {git_remote}')
|
||||
|
||||
try:
|
||||
with subprocess.Popen(git_remote, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, bufsize=1, universal_newlines=True, encoding='utf-8') as p:
|
||||
for line in p.stdout:
|
||||
print(line, end='')
|
||||
|
||||
except Exception as e:
|
||||
git_cmd = ' '.join([s.replace(' ', r'\ ') for s in git_remote])
|
||||
|
||||
cli.log.error(f"Could not run '{git_cmd}': {e.__class__.__name__}:{e}")
|
||||
return False
|
||||
|
||||
if p.returncode != 0:
|
||||
cli.log.error(f'git remote add exited {p.returncode}')
|
||||
return False
|
||||
|
||||
cli.log.info(f'Added {DEFAULT_UPSTREAM} as remote upstream.')
|
||||
return True
|
||||
|
||||
|
||||
def git_clone_fork(destination, baseurl, fork, branch, force=False):
|
||||
"""Clone fork and configure upstream."""
|
||||
url = '/'.join((baseurl, fork))
|
||||
|
||||
if force:
|
||||
rmtree(destination)
|
||||
|
||||
if not git_clone(destination, url, branch):
|
||||
return False
|
||||
|
||||
return git_set_upstream(destination)
|
||||
|
||||
+91
-36
@@ -1,48 +1,59 @@
|
||||
"""Useful helper functions.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import stat
|
||||
import json
|
||||
import shutil
|
||||
from functools import lru_cache
|
||||
from importlib.util import find_spec
|
||||
from pathlib import Path
|
||||
|
||||
from milc import cli
|
||||
|
||||
|
||||
def find_broken_requirements(requirements):
|
||||
""" Check if the modules in the given requirements.txt are available.
|
||||
|
||||
Args:
|
||||
|
||||
requirements
|
||||
The path to a requirements.txt file
|
||||
|
||||
Returns a list of modules that couldn't be imported
|
||||
def AbsPath(arg): # noqa: N802
|
||||
"""Resolve relative paths to the original working directory.
|
||||
"""
|
||||
with Path(requirements).open() as fd:
|
||||
broken_modules = []
|
||||
arg = Path(arg)
|
||||
if not arg.is_absolute():
|
||||
return (Path(os.environ['ORIG_CWD']) / arg).absolute()
|
||||
|
||||
for line in fd.readlines():
|
||||
line = line.strip().replace('<', '=').replace('>', '=')
|
||||
|
||||
if len(line) == 0 or line[0] == '#' or line.startswith('-r'):
|
||||
continue
|
||||
|
||||
if '#' in line:
|
||||
line = line.split('#')[0]
|
||||
|
||||
module_name = module_import_name = line.split('=')[0] if '=' in line else line
|
||||
|
||||
# Not every module is importable by its own name.
|
||||
if module_name == "pep8-naming":
|
||||
module_import_name = "pep8ext_naming"
|
||||
|
||||
if not find_spec(module_import_name):
|
||||
broken_modules.append(module_name)
|
||||
|
||||
return broken_modules
|
||||
return arg
|
||||
|
||||
|
||||
@lru_cache(maxsize=2)
|
||||
def rmtree(path):
|
||||
"""Safe version of shutil.rmtree which handles errors on Windows where some of the files have their read-only bit set."""
|
||||
def remove_readonly(func, path, _):
|
||||
"""Clear the readonly bit and reattempt the removal."""
|
||||
os.chmod(path, stat.S_IWRITE)
|
||||
func(path)
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
# See https://docs.python.org/3.12/whatsnew/3.12.html#shutil
|
||||
return shutil.rmtree(path, onexc=remove_readonly)
|
||||
else:
|
||||
return shutil.rmtree(path, onerror=remove_readonly)
|
||||
|
||||
|
||||
def is_qmk_firmware(qmk_firmware):
|
||||
"""Returns True if the given Path() is a qmk_firmware clone.
|
||||
"""
|
||||
paths = [
|
||||
qmk_firmware,
|
||||
qmk_firmware / 'quantum',
|
||||
qmk_firmware / 'requirements.txt',
|
||||
qmk_firmware / 'requirements-dev.txt',
|
||||
qmk_firmware / 'lib/python/qmk/cli/__init__.py',
|
||||
]
|
||||
|
||||
for path in paths:
|
||||
if not path.exists():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def find_qmk_firmware():
|
||||
"""Look for qmk_firmware in the usual places.
|
||||
|
||||
@@ -63,15 +74,59 @@ def find_qmk_firmware():
|
||||
return Path.home() / 'qmk_firmware'
|
||||
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
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:
|
||||
found_lib = cur_dir / 'lib/python/qmk/cli/__init__.py'
|
||||
found_quantum = cur_dir / 'quantum'
|
||||
if found_lib.is_file() and found_quantum.is_dir():
|
||||
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()
|
||||
|
||||
|
||||
def is_qmk_userspace(qmk_userspace):
|
||||
"""Returns True if the given Path() is a qmk_userspace clone.
|
||||
"""
|
||||
path = qmk_userspace / 'qmk.json'
|
||||
if not path.exists():
|
||||
return False
|
||||
|
||||
try:
|
||||
return 'userspace_version' in json.loads(path.read_text(encoding="UTF-8"))
|
||||
except json.decoder.JSONDecodeError:
|
||||
return False
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def find_qmk_userspace():
|
||||
"""Look for qmk_userspace in the usual places.
|
||||
"""
|
||||
if in_qmk_userspace():
|
||||
return in_qmk_userspace()
|
||||
|
||||
if 'QMK_USERSPACE' in os.environ:
|
||||
path = Path(os.environ['QMK_USERSPACE']).expanduser()
|
||||
if path.exists():
|
||||
return path.resolve()
|
||||
return path
|
||||
|
||||
if cli.config.user.overlay_dir:
|
||||
return Path(cli.config.user.overlay_dir).expanduser().resolve()
|
||||
|
||||
return Path.home() / 'qmk_userspace'
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def in_qmk_userspace():
|
||||
"""Returns the path to the qmk_userspace we are currently in, or None if we are not inside qmk_userspace.
|
||||
"""
|
||||
cur_dir = Path.cwd()
|
||||
while len(cur_dir.parents) > 0:
|
||||
if is_qmk_userspace(cur_dir):
|
||||
return cur_dir
|
||||
|
||||
# Move up a directory before the next iteration
|
||||
|
||||
+60
-49
@@ -4,18 +4,47 @@
|
||||
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 os
|
||||
from pathlib import Path
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
from platform import platform
|
||||
from traceback import print_exc
|
||||
|
||||
import milc
|
||||
import milc.subcommand.config # noqa
|
||||
import platformdirs
|
||||
|
||||
from .helpers import find_broken_requirements, find_qmk_firmware, in_qmk_firmware
|
||||
from . import __version__
|
||||
from .helpers import find_qmk_firmware, is_qmk_firmware, find_qmk_userspace, is_qmk_userspace
|
||||
|
||||
|
||||
def _get_default_distrib_path():
|
||||
if 'windows' in platform().lower():
|
||||
try:
|
||||
result = milc.cli.run(['cygpath', '-w', '/opt/qmk'])
|
||||
if result.returncode == 0:
|
||||
return result.stdout.strip()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return platformdirs.user_data_dir('qmk')
|
||||
|
||||
|
||||
# Ensure the QMK distribution is on the `$PATH` if present. This must be kept in sync with qmk/qmk_firmware.
|
||||
QMK_DISTRIB_DIR = Path(os.environ.get('QMK_DISTRIB_DIR', _get_default_distrib_path()))
|
||||
if QMK_DISTRIB_DIR.exists():
|
||||
os.environ['PATH'] = str(QMK_DISTRIB_DIR / 'bin') + os.pathsep + os.environ['PATH']
|
||||
|
||||
# Prepend any user-defined path prefix
|
||||
if 'QMK_PATH_PREFIX' in os.environ:
|
||||
os.environ['PATH'] = os.environ['QMK_PATH_PREFIX'] + os.pathsep + os.environ['PATH']
|
||||
|
||||
milc.cli.milc_options(version=__version__)
|
||||
milc.EMOJI_LOGLEVELS['INFO'] = '{fg_blue}Ψ{style_reset_all}'
|
||||
|
||||
# These must happen after the milc.milc_options() call
|
||||
import milc.subcommand.config # noqa: E402, must come after milc.milc_options()
|
||||
|
||||
|
||||
@milc.cli.entrypoint('CLI wrapper for running QMK commands.')
|
||||
def qmk_main(cli):
|
||||
@@ -24,72 +53,54 @@ def qmk_main(cli):
|
||||
cli.print_help()
|
||||
|
||||
|
||||
def run_cmd(*command):
|
||||
"""Run a command in a subshell.
|
||||
"""
|
||||
if 'windows' in milc.cli.platform.lower():
|
||||
safecmd = map(shlex.quote, command)
|
||||
safecmd = ' '.join(safecmd)
|
||||
command = [os.environ['SHELL'], '-c', safecmd]
|
||||
|
||||
return subprocess.run(command)
|
||||
|
||||
|
||||
# Python setuptools entrypoint
|
||||
def main():
|
||||
"""Setup the environment before dispatching to the entrypoint.
|
||||
"""
|
||||
# Warn if they use an outdated python version
|
||||
if sys.version_info < (3, 6):
|
||||
if sys.version_info < (3, 9):
|
||||
print('Warning: Your Python version is out of date! Some subcommands may not work!')
|
||||
print('Please upgrade to Python 3.6 or later.')
|
||||
|
||||
if 'windows' in platform().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)
|
||||
print('Please upgrade to Python 3.9 or later.')
|
||||
|
||||
# Environment setup
|
||||
import qmk_cli
|
||||
milc.cli.version = qmk_cli.__version__
|
||||
qmk_userspace = find_qmk_userspace()
|
||||
qmk_firmware = find_qmk_firmware()
|
||||
if is_qmk_userspace(qmk_userspace):
|
||||
os.environ['QMK_USERSPACE'] = str(qmk_userspace)
|
||||
elif 'QMK_USERSPACE' in os.environ: # Failed to find valid userspace, including what was in the environment if specified -- wipe any environment variable if present as it's clearly invalid.
|
||||
os.environ.pop('QMK_USERSPACE')
|
||||
os.environ['QMK_HOME'] = str(qmk_firmware)
|
||||
os.environ['ORIG_CWD'] = os.getcwd()
|
||||
|
||||
import qmk_cli.subcommands
|
||||
import qmk_cli.subcommands # noqa: F401
|
||||
|
||||
# Check out and initialize the qmk_firmware environment
|
||||
if qmk_firmware.exists():
|
||||
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'))
|
||||
|
||||
# Check to make sure we have all the requirements
|
||||
broken_modules = find_broken_requirements('requirements.txt')
|
||||
broken_dev_modules = find_broken_requirements('requirements-dev.txt') if milc.cli.config.user.developer else []
|
||||
try:
|
||||
import qmk.cli # noqa: F401
|
||||
|
||||
if broken_modules or broken_dev_modules:
|
||||
for module in broken_modules + broken_dev_modules:
|
||||
print('Could not find module %s!' % module)
|
||||
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))
|
||||
|
||||
msg_install = 'Please run `python3 -m pip install -r %s` to install required python dependencies.'
|
||||
if broken_modules:
|
||||
print()
|
||||
print(msg_install % (qmk_firmware / 'requirements.txt',))
|
||||
|
||||
if broken_dev_modules:
|
||||
print()
|
||||
print(msg_install % (qmk_firmware / 'requirements-dev.txt',))
|
||||
print('You can also turn off developer mode: qmk config user.developer=None')
|
||||
|
||||
print()
|
||||
|
||||
else:
|
||||
# Environment looks good, include the qmk_firmware subcommands
|
||||
sys.path.append(str(qmk_firmware / 'lib/python'))
|
||||
|
||||
try:
|
||||
import qmk.cli
|
||||
|
||||
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)
|
||||
print('Error: %s: %s', (e.__class__.__name__, e))
|
||||
print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
# Call the entrypoint
|
||||
return_code = milc.cli()
|
||||
|
||||
@@ -2,5 +2,7 @@
|
||||
|
||||
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: F401
|
||||
from . import console # noqa: F401
|
||||
from . import env # noqa: F401
|
||||
from . import setup # noqa: F401
|
||||
|
||||
@@ -5,26 +5,24 @@ from pathlib import Path
|
||||
|
||||
from milc import cli
|
||||
from qmk_cli.git import git_clone
|
||||
from qmk_cli.helpers import AbsPath
|
||||
|
||||
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 (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.argument('--baseurl', arg_only=True, default='https://github.com', help='The URL all git operations start from (Default: https://github.com)')
|
||||
@cli.argument('-b', '--branch', arg_only=True, default=default_branch, help='The branch to clone. Default: %s' % default_branch)
|
||||
@cli.argument('destination', arg_only=True, default=Path(os.environ['ORIG_CWD']) / default_repo, type=AbsPath, nargs='?', help='The directory to clone to. Default: (current directory)')
|
||||
@cli.argument('fork', arg_only=True, 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])
|
||||
git_url = '/'.join((cli.args.baseurl, cli.args.fork))
|
||||
|
||||
qmk_firmware = Path(cli.args.destination)
|
||||
git_url = '/'.join((cli.config.clone.baseurl, cli.args.fork))
|
||||
|
||||
if qmk_firmware.exists():
|
||||
# Exists (but not an empty dir)
|
||||
if cli.args.destination.exists() and any(cli.args.destination.iterdir()):
|
||||
cli.log.error('Destination already exists: %s', cli.args.destination)
|
||||
exit(1)
|
||||
return False
|
||||
|
||||
return git_clone(git_url, cli.args.destination, cli.config.clone.branch)
|
||||
return git_clone(cli.args.destination, git_url, cli.args.branch)
|
||||
|
||||
@@ -0,0 +1,374 @@
|
||||
"""Acquire debugging information from usb hid devices
|
||||
|
||||
cli implementation of https://www.pjrc.com/teensy/hid_listen.html
|
||||
"""
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from platform import platform
|
||||
from threading import Thread
|
||||
from time import sleep, strftime
|
||||
|
||||
from milc import cli
|
||||
from milc.questions import yesno
|
||||
|
||||
LOG_COLOR = {
|
||||
'next': 0,
|
||||
'colors': [
|
||||
'{fg_blue}',
|
||||
'{fg_cyan}',
|
||||
'{fg_green}',
|
||||
'{fg_magenta}',
|
||||
'{fg_red}',
|
||||
'{fg_yellow}',
|
||||
],
|
||||
}
|
||||
|
||||
KNOWN_BOOTLOADERS = {
|
||||
# VID , PID
|
||||
('03EB', '2045'): 'lufa-ms: LUFA Mass Storage Bootloader',
|
||||
('03EB', '2067'): 'qmk-hid: HID Bootloader',
|
||||
('03EB', '2FEF'): 'atmel-dfu: ATmega16U2',
|
||||
('03EB', '2FF0'): 'atmel-dfu: ATmega32U2',
|
||||
('03EB', '2FF3'): 'atmel-dfu: ATmega16U4',
|
||||
('03EB', '2FF4'): 'atmel-dfu: ATmega32U4',
|
||||
('03EB', '2FF9'): 'atmel-dfu: AT90USB64',
|
||||
('03EB', '2FFA'): 'atmel-dfu: AT90USB162',
|
||||
('03EB', '2FFB'): 'atmel-dfu: AT90USB128',
|
||||
('03EB', '6124'): 'Microchip SAM-BA',
|
||||
('0483', 'DF11'): 'stm32-dfu: STM32 BOOTLOADER',
|
||||
('16C0', '05DC'): 'usbasploader: USBaspLoader',
|
||||
('16C0', '05DF'): 'bootloadhid: HIDBoot',
|
||||
('16C0', '0478'): 'halfkay: Teensy Halfkay',
|
||||
('1B4F', '9203'): 'caterina: Pro Micro 3.3V',
|
||||
('1B4F', '9205'): 'caterina: Pro Micro 5V',
|
||||
('1B4F', '9207'): 'caterina: LilyPadUSB',
|
||||
('1C11', 'B007'): 'kiibohd: Kiibohd DFU Bootloader',
|
||||
('1EAF', '0003'): 'stm32duino: Maple 003',
|
||||
('1FFB', '0101'): 'caterina: Pololu A-Star 32U4 Bootloader',
|
||||
('2341', '0036'): 'caterina: Arduino Leonardo',
|
||||
('2341', '0037'): 'caterina: Arduino Micro',
|
||||
('239A', '000C'): 'caterina: Adafruit Feather 32U4',
|
||||
('239A', '000D'): 'caterina: Adafruit ItsyBitsy 32U4 3v',
|
||||
('239A', '000E'): 'caterina: Adafruit ItsyBitsy 32U4 5v',
|
||||
('2A03', '0036'): 'caterina: Arduino Leonardo',
|
||||
('2A03', '0037'): 'caterina: Arduino Micro',
|
||||
('314B', '0106'): 'apm32-dfu: APM32 DFU ISP Mode',
|
||||
('342D', 'DFA0'): 'wb32-dfu: WB32 Device in DFU Mode',
|
||||
}
|
||||
|
||||
|
||||
def install_deps():
|
||||
"""Install the necessary dependencies for qmk console.
|
||||
"""
|
||||
this_platform = platform().lower()
|
||||
|
||||
if 'darwin' in this_platform or 'macos' in this_platform:
|
||||
command = ['brew', 'install', 'hidapi']
|
||||
elif 'linux' in this_platform:
|
||||
command = ['sudo', 'apt', 'install', '-y', 'libhidapi-hidraw0', 'libusb-dev']
|
||||
elif 'windows' in this_platform:
|
||||
command = ['pacboy', 'sync', '--needed', '--noconfirm', '--disable-download-timeout', 'hidapi:x']
|
||||
else:
|
||||
cli.log.error('Unsupported platform: %s', this_platform)
|
||||
|
||||
if yesno("Would you like to run `%s` to install the necessary package?", ' '.join(command)):
|
||||
cli.run(command, capture_output=False)
|
||||
return True
|
||||
|
||||
|
||||
@lru_cache(maxsize=0)
|
||||
def import_usb_core():
|
||||
"""Attempts to import the usb.core module.
|
||||
"""
|
||||
try:
|
||||
import usb.core
|
||||
return usb.core
|
||||
|
||||
except ImportError as e:
|
||||
cli.log.error('Could not import usb.core: %s', e)
|
||||
|
||||
if install_deps():
|
||||
return import_usb_core()
|
||||
|
||||
raise
|
||||
|
||||
|
||||
def import_hid():
|
||||
"""Attempts to import the hid module.
|
||||
"""
|
||||
import os
|
||||
old_cwd = os.getcwd()
|
||||
try:
|
||||
# macOS library search paths don't include homebrew by default when launched via a `uv`-based deployment.
|
||||
# The `hid` python module does a search and attempts to load a set of known library names, but doesn't qualify the path, nor gives the option to do so.
|
||||
# To work around this, we chdir to the homebrew lib directory before importing hid, then chdir back to where we were.
|
||||
if 'darwin' in platform().lower() or 'macos' in platform().lower():
|
||||
for path in ('/opt/homebrew/lib', '/usr/local/lib'):
|
||||
lib_search = Path(path) / 'libhidapi.dylib'
|
||||
if lib_search.exists():
|
||||
os.chdir(path)
|
||||
break
|
||||
|
||||
import hid
|
||||
return hid
|
||||
|
||||
except ImportError as e:
|
||||
cli.log.error('Could not import hid: %s', e)
|
||||
|
||||
if install_deps():
|
||||
return import_hid()
|
||||
|
||||
raise
|
||||
finally:
|
||||
os.chdir(old_cwd)
|
||||
|
||||
|
||||
class MonitorDevice(object):
|
||||
def __init__(self, hid_device, numeric):
|
||||
self.hid = import_hid()
|
||||
self.hid_device = hid_device
|
||||
self.numeric = numeric
|
||||
self.device = self.hid.Device(path=hid_device['path'])
|
||||
self.current_line = ''
|
||||
|
||||
cli.log.info('Console Connected: %(color)s%(manufacturer_string)s %(product_string)s{style_reset_all} (%(color)s%(vendor_id)04X:%(product_id)04X:%(index)d{style_reset_all})', hid_device)
|
||||
|
||||
def read(self, size, encoding='ascii', timeout=1):
|
||||
"""Read size bytes from the device.
|
||||
"""
|
||||
return self.device.read(size, timeout).decode(encoding)
|
||||
|
||||
def read_line(self):
|
||||
"""Read from the device's console until we get a \n.
|
||||
"""
|
||||
while '\n' not in self.current_line:
|
||||
self.current_line += self.read(32).replace('\x00', '')
|
||||
|
||||
lines = self.current_line.split('\n', 1)
|
||||
self.current_line = lines[1]
|
||||
|
||||
return lines[0]
|
||||
|
||||
def run_forever(self):
|
||||
while True:
|
||||
try:
|
||||
message = {**self.hid_device, 'text': self.read_line()}
|
||||
identifier = (int2hex(message['vendor_id']), int2hex(message['product_id'])) if self.numeric else (message['manufacturer_string'], message['product_string'])
|
||||
message['identifier'] = ':'.join(identifier)
|
||||
message['ts'] = '{style_dim}{fg_green}%s{style_reset_all} ' % (strftime(cli.config.general.datetime_fmt),) if cli.args.timestamp else ''
|
||||
|
||||
cli.echo('%s', '%(ts)s%(color)s%(identifier)s:%(index)d{style_reset_all}: %(text)s' % message)
|
||||
|
||||
except self.hid.HIDException:
|
||||
break
|
||||
|
||||
|
||||
class FindDevices(object):
|
||||
def __init__(self, vid, pid, index, numeric):
|
||||
self.hid = import_hid()
|
||||
self.vid = vid
|
||||
self.pid = pid
|
||||
self.index = index
|
||||
self.numeric = numeric
|
||||
|
||||
def run_forever(self):
|
||||
"""Process messages from our queue in a loop.
|
||||
"""
|
||||
live_devices = {}
|
||||
live_bootloaders = {}
|
||||
|
||||
while True:
|
||||
try:
|
||||
for device in list(live_devices):
|
||||
if not live_devices[device]['thread'].is_alive():
|
||||
cli.log.info('Console Disconnected: %(color)s%(manufacturer_string)s %(product_string)s{style_reset_all} (%(color)s%(vendor_id)04X:%(product_id)04X:%(index)d{style_reset_all})', live_devices[device])
|
||||
del live_devices[device]
|
||||
|
||||
for device in self.find_devices():
|
||||
if device['path'] not in live_devices:
|
||||
device['color'] = LOG_COLOR['colors'][LOG_COLOR['next']]
|
||||
LOG_COLOR['next'] = (LOG_COLOR['next'] + 1) % len(LOG_COLOR['colors'])
|
||||
live_devices[device['path']] = device
|
||||
|
||||
try:
|
||||
monitor = MonitorDevice(device, self.numeric)
|
||||
device['thread'] = Thread(target=monitor.run_forever, daemon=True)
|
||||
|
||||
device['thread'].start()
|
||||
except Exception as e:
|
||||
device['e'] = e
|
||||
device['e_name'] = e.__class__.__name__
|
||||
cli.log.error("Could not connect to %(color)s%(manufacturer_string)s %(product_string)s{style_reset_all} (%(color)s%(vendor_id)04X:%(product_id)04X:%(index)d{style_reset_all}): %(e_name)s: %(e)s", device)
|
||||
if cli.config.general.verbose:
|
||||
cli.log.exception(e)
|
||||
del live_devices[device['path']]
|
||||
|
||||
if cli.args.bootloaders:
|
||||
for device in self.find_bootloaders():
|
||||
if device.address in live_bootloaders:
|
||||
live_bootloaders[device.address]._qmk_found = True
|
||||
else:
|
||||
name = KNOWN_BOOTLOADERS[(int2hex(device.idVendor), int2hex(device.idProduct))]
|
||||
cli.log.info('Bootloader Connected: {style_bright}{fg_magenta}%s', name)
|
||||
device._qmk_found = True
|
||||
live_bootloaders[device.address] = device
|
||||
|
||||
for device in list(live_bootloaders):
|
||||
if live_bootloaders[device]._qmk_found:
|
||||
live_bootloaders[device]._qmk_found = False
|
||||
else:
|
||||
name = KNOWN_BOOTLOADERS[(int2hex(live_bootloaders[device].idVendor), int2hex(live_bootloaders[device].idProduct))]
|
||||
cli.log.info('Bootloader Disconnected: {style_bright}{fg_magenta}%s', name)
|
||||
del live_bootloaders[device]
|
||||
|
||||
sleep(.1)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
break
|
||||
|
||||
def is_bootloader(self, hid_device):
|
||||
"""Returns true if the device in question matches a known bootloader vid/pid.
|
||||
"""
|
||||
return (int2hex(hid_device.idVendor), int2hex(hid_device.idProduct)) in KNOWN_BOOTLOADERS
|
||||
|
||||
def is_console_hid(self, hid_device):
|
||||
"""Returns true when the usage page indicates it's a teensy-style console.
|
||||
"""
|
||||
return hid_device['usage_page'] == 0xFF31 and hid_device['usage'] == 0x0074
|
||||
|
||||
def is_filtered_device(self, hid_device):
|
||||
"""Returns True if the device should be included in the list of available consoles.
|
||||
"""
|
||||
return int2hex(hid_device['vendor_id']) == self.vid and int2hex(hid_device['product_id']) == self.pid
|
||||
|
||||
def find_devices_by_report(self, hid_devices):
|
||||
"""Returns a list of available teensy-style consoles by doing a brute-force search.
|
||||
|
||||
Some versions of linux don't report usage and usage_page. In that case we fallback to reading the report (possibly inaccurately) ourselves.
|
||||
"""
|
||||
devices = []
|
||||
|
||||
for device in hid_devices:
|
||||
path = device['path'].decode('utf-8')
|
||||
|
||||
if path.startswith('/dev/hidraw'):
|
||||
number = path[11:]
|
||||
report = Path(f'/sys/class/hidraw/hidraw{number}/device/report_descriptor')
|
||||
|
||||
if report.exists():
|
||||
rp = report.read_bytes()
|
||||
|
||||
if rp[1] == 0x31 and rp[3] == 0x09:
|
||||
devices.append(device)
|
||||
|
||||
return devices
|
||||
|
||||
def find_bootloaders(self):
|
||||
"""Returns a list of available bootloader devices.
|
||||
"""
|
||||
import usb.core
|
||||
|
||||
return list(filter(self.is_bootloader, usb.core.find(find_all=True)))
|
||||
|
||||
def find_devices(self):
|
||||
"""Returns a list of available teensy-style consoles.
|
||||
"""
|
||||
hid_devices = self.hid.enumerate()
|
||||
devices = list(filter(self.is_console_hid, hid_devices))
|
||||
|
||||
if not devices:
|
||||
devices = self.find_devices_by_report(hid_devices)
|
||||
|
||||
if self.vid and self.pid:
|
||||
devices = list(filter(self.is_filtered_device, devices))
|
||||
|
||||
# Add index numbers
|
||||
device_index = {}
|
||||
for device in devices:
|
||||
id = ':'.join((int2hex(device['vendor_id']), int2hex(device['product_id'])))
|
||||
|
||||
if id not in device_index:
|
||||
device_index[id] = 0
|
||||
|
||||
device_index[id] += 1
|
||||
device['index'] = device_index[id]
|
||||
|
||||
return devices
|
||||
|
||||
|
||||
def int2hex(number):
|
||||
"""Returns a string representation of the number as hex.
|
||||
"""
|
||||
return "%04X" % number
|
||||
|
||||
|
||||
def list_devices(device_finder):
|
||||
"""Show the user a nicely formatted list of devices.
|
||||
"""
|
||||
devices = device_finder.find_devices()
|
||||
|
||||
if devices:
|
||||
cli.log.info('Available devices:')
|
||||
for dev in devices:
|
||||
color = LOG_COLOR['colors'][LOG_COLOR['next']]
|
||||
LOG_COLOR['next'] = (LOG_COLOR['next'] + 1) % len(LOG_COLOR['colors'])
|
||||
cli.log.info("\t%s%s:%s:%d{style_reset_all}\t%s %s", color, int2hex(dev['vendor_id']), int2hex(dev['product_id']), dev['index'], dev['manufacturer_string'], dev['product_string'])
|
||||
|
||||
if cli.args.bootloaders:
|
||||
bootloaders = device_finder.find_bootloaders()
|
||||
|
||||
if bootloaders:
|
||||
cli.log.info('Available Bootloaders:')
|
||||
|
||||
for dev in bootloaders:
|
||||
cli.log.info("\t%s:%s\t%s", int2hex(dev.idVendor), int2hex(dev.idProduct), KNOWN_BOOTLOADERS[(int2hex(dev.idVendor), int2hex(dev.idProduct))])
|
||||
|
||||
|
||||
@cli.argument('--bootloaders', arg_only=True, default=True, action='store_boolean', help='displaying bootloaders.')
|
||||
@cli.argument('-d', '--device', help='Device to select - uses format <pid>:<vid>[:<index>].')
|
||||
@cli.argument('-l', '--list', arg_only=True, action='store_true', help='List available hid_listen devices.')
|
||||
@cli.argument('-n', '--numeric', arg_only=True, action='store_true', help='Show VID/PID instead of names.')
|
||||
@cli.argument('-t', '--timestamp', arg_only=True, action='store_true', help='Print the timestamp for received messages as well.')
|
||||
@cli.argument('-w', '--wait', type=int, default=1, help="How many seconds to wait between checks (Default: 1)")
|
||||
@cli.subcommand('Acquire debugging information from usb hid devices.')
|
||||
def console(cli):
|
||||
"""Acquire debugging information from usb hid devices
|
||||
"""
|
||||
vid = None
|
||||
pid = None
|
||||
index = 1
|
||||
|
||||
if cli.config.console.device:
|
||||
device = cli.config.console.device.split(':')
|
||||
|
||||
if len(device) == 2:
|
||||
vid, pid = device
|
||||
|
||||
elif len(device) == 3:
|
||||
vid, pid, index = device
|
||||
|
||||
if not index.isdigit():
|
||||
cli.log.error('Device index must be a number! Got "%s" instead.', index)
|
||||
exit(1)
|
||||
|
||||
index = int(index)
|
||||
|
||||
if index < 1:
|
||||
cli.log.error('Device index must be greater than 0! Got %s', index)
|
||||
exit(1)
|
||||
|
||||
else:
|
||||
cli.log.error('Invalid format for device, expected "<pid>:<vid>[:<index>]" but got "%s".', cli.config.console.device)
|
||||
cli.print_help()
|
||||
exit(1)
|
||||
|
||||
vid = vid.upper()
|
||||
pid = pid.upper()
|
||||
|
||||
device_finder = FindDevices(vid, pid, index, cli.args.numeric)
|
||||
|
||||
if cli.args.list:
|
||||
return list_devices(device_finder)
|
||||
|
||||
print('Looking for devices...', flush=True)
|
||||
device_finder.run_forever()
|
||||
@@ -0,0 +1,39 @@
|
||||
"""Prints environment information.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from milc import cli
|
||||
from qmk_cli.helpers import is_qmk_firmware, is_qmk_userspace
|
||||
|
||||
|
||||
@cli.argument('var', arg_only=True, default=None, nargs='?', help='Optional variable to query')
|
||||
@cli.subcommand('Prints environment information.')
|
||||
def env(cli):
|
||||
home = os.environ.get('QMK_HOME', "")
|
||||
userspace = os.environ.get('QMK_USERSPACE', "")
|
||||
data = {
|
||||
'QMK_HOME': home,
|
||||
'QMK_FIRMWARE': home if is_qmk_firmware(Path(home)) else "",
|
||||
'QMK_USERSPACE': userspace if is_qmk_userspace(Path(userspace)) else "",
|
||||
'QMK_DISTRIB_DIR': os.environ.get('QMK_DISTRIB_DIR', ""),
|
||||
'QMK_PATH_PREFIX': os.environ.get('QMK_PATH_PREFIX', ""),
|
||||
}
|
||||
|
||||
# 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:
|
||||
if cli.args.var not in data:
|
||||
print(f'Variable "{cli.args.var}" does not exist!', file=sys.stderr)
|
||||
return False
|
||||
|
||||
# dump out requested arg
|
||||
print(data[cli.args.var])
|
||||
else:
|
||||
# dump out everything
|
||||
for key, val in data.items():
|
||||
print(f'{key}="{val}"')
|
||||
@@ -2,54 +2,28 @@
|
||||
"""
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from milc import cli
|
||||
from milc.questions import yesno
|
||||
from qmk_cli.git import git_clone
|
||||
from milc.questions import choice, question, yesno
|
||||
from qmk_cli.git import git_clone_fork
|
||||
from qmk_cli.helpers import AbsPath, is_qmk_firmware
|
||||
|
||||
default_base = 'https://github.com'
|
||||
default_repo = 'qmk_firmware'
|
||||
default_fork = 'qmk/' + default_repo
|
||||
default_branch = 'master'
|
||||
|
||||
|
||||
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
|
||||
DEFAULT_BASE = 'https://github.com'
|
||||
DEFAULT_REPO = 'qmk_firmware'
|
||||
DEFAULT_FORK = 'qmk/' + DEFAULT_REPO
|
||||
DEFAULT_BRANCH = 'master'
|
||||
|
||||
|
||||
@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.argument('--baseurl', arg_only=True, default=DEFAULT_BASE, help='The URL all git operations start from. Default: %s' % DEFAULT_BASE)
|
||||
@cli.argument('-b', '--branch', arg_only=True, default=DEFAULT_BRANCH, help='The branch to clone. Default: %s' % DEFAULT_BRANCH)
|
||||
@cli.argument('-H', '--home', arg_only=True, default=Path(os.environ['QMK_HOME']), type=AbsPath, help='The location for QMK Firmware. Default: %s' % os.environ['QMK_HOME'])
|
||||
@cli.argument('fork', arg_only=True, 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):
|
||||
def setup(cli): # noqa: C901
|
||||
"""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)))
|
||||
@@ -58,34 +32,73 @@ def setup(cli):
|
||||
# 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)
|
||||
return False
|
||||
|
||||
# Check on qmk_firmware, and if it doesn't exist offer to check it out.
|
||||
if (cli.args.home / 'Makefile').exists():
|
||||
# Check on qmk_firmware
|
||||
# If it exists, ask the user what to do with it
|
||||
# If it doesn't exist, offer to check it out
|
||||
if is_qmk_firmware(cli.args.home):
|
||||
cli.log.info('Found qmk_firmware at %s.', str(cli.args.home))
|
||||
found_prompt = "What do you want to do?"
|
||||
found_options = [
|
||||
f"Delete and reclone {cli.args.fork}",
|
||||
"Delete and clone a different fork",
|
||||
"Keep it and continue",
|
||||
]
|
||||
delete_confirm = "WARNING: This will delete your current qmk_firmware directory. Proceed?"
|
||||
|
||||
found_action = choice(found_prompt, options=found_options, default=2)
|
||||
if found_action == f"Delete and reclone {cli.args.fork}":
|
||||
if not yesno(delete_confirm, default=False):
|
||||
return False
|
||||
|
||||
if not git_clone_fork(cli.args.home, cli.args.baseurl, cli.args.fork, cli.args.branch, force=True):
|
||||
return False
|
||||
|
||||
elif found_action == "Delete and clone a different fork":
|
||||
fork_name = question("Enter the name of the fork:", default=cli.args.fork)
|
||||
branch_name = question("Enter the branch name to clone:", default=cli.args.branch)
|
||||
|
||||
if not yesno(delete_confirm, default=False):
|
||||
return False
|
||||
|
||||
if not git_clone_fork(cli.args.home, cli.args.baseurl, fork_name, branch_name, force=True):
|
||||
return False
|
||||
|
||||
# Exists (but not an empty dir)
|
||||
elif cli.args.home.exists() and any(cli.args.home.iterdir()):
|
||||
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)
|
||||
return False
|
||||
|
||||
else:
|
||||
cli.log.error('Could not find qmk_firmware!')
|
||||
if yesno(clone_prompt):
|
||||
git_url = '/'.join((cli.config.setup.baseurl, cli.args.fork))
|
||||
|
||||
if git_clone(git_url, cli.args.home, cli.config.setup.branch):
|
||||
git_upstream(cli.args.home)
|
||||
else:
|
||||
exit(1)
|
||||
if not git_clone_fork(cli.args.home, cli.args.baseurl, cli.args.fork, cli.args.branch):
|
||||
return False
|
||||
else:
|
||||
cli.log.warning('Not cloning qmk_firmware due to user input or --no flag.')
|
||||
|
||||
# Offer to set `user.qmk_home` for them.
|
||||
if cli.args.home != Path(os.environ['QMK_HOME']) and yesno(home_prompt):
|
||||
if str(cli.args.home) != os.environ['QMK_HOME'] and yesno(home_prompt):
|
||||
cli.config['user']['qmk_home'] = str(cli.args.home.absolute())
|
||||
cli.config_source['user']['qmk_home'] = 'config_file'
|
||||
cli.write_config_option('user', 'qmk_home')
|
||||
|
||||
# Run `qmk_firmware/bin/qmk doctor` to check the rest of the environment out
|
||||
qmk_bin = cli.args.home / 'bin/qmk'
|
||||
doctor_cmd = [sys.executable, qmk_bin.as_posix(), 'doctor']
|
||||
# Run `qmk doctor` to check the rest of the environment out
|
||||
if cli.args.home.exists():
|
||||
color = '--color' if cli.config.general.color else '--no-color'
|
||||
unicode = '--unicode' if cli.config.general.unicode else '--no-unicode'
|
||||
doctor_command = [Path(sys.argv[0]).as_posix(), color, unicode, 'doctor']
|
||||
|
||||
if cli.args.yes:
|
||||
doctor_cmd.append('--yes')
|
||||
if cli.args.no:
|
||||
doctor_command.append('-n')
|
||||
|
||||
if cli.args.no:
|
||||
doctor_cmd.append('--no')
|
||||
if cli.args.yes:
|
||||
doctor_command.append('-y')
|
||||
|
||||
subprocess.run(doctor_cmd)
|
||||
cli.run(doctor_command, stdin=None, capture_output=False, cwd=cli.args.home)
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Increment the verison number and release a new version of qmk_cli.
|
||||
#
|
||||
# Required packages: pip3 install bumpversion twine
|
||||
|
||||
set -e
|
||||
set -x
|
||||
|
||||
PYPI_USERNAME=${PYPI_USERNAME:=skully}
|
||||
FLIT_USERNAME=$PYPI_USERNAME
|
||||
TWINE_USERNAME=$PYPI_USERNAME
|
||||
|
||||
export FLIT_USERNAME TWINE_USERNAME
|
||||
|
||||
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
|
||||
python3 setup.py sdist bdist_wheel
|
||||
twine upload dist/qmk-*
|
||||
@@ -1,2 +0,0 @@
|
||||
bumpversion
|
||||
twine
|
||||
@@ -1,82 +0,0 @@
|
||||
[bumpversion]
|
||||
current_version = 0.0.37
|
||||
commit = True
|
||||
tag = True
|
||||
tag_name = {new_version}
|
||||
message = New release: {current_version} → {new_version}
|
||||
|
||||
[bumpversion:file:qmk_cli/__init__.py]
|
||||
|
||||
[bumpversion:file:setup.cfg]
|
||||
|
||||
[bdist_wheel]
|
||||
universal = 1
|
||||
|
||||
[flake8]
|
||||
ignore = E501,E226
|
||||
|
||||
[metadata]
|
||||
author = skullydazed
|
||||
author-email = skullydazed@gmail.com
|
||||
description-file = README.md
|
||||
dist-name = qmk
|
||||
license_file = LICENSE
|
||||
module = qmk_cli
|
||||
home-page = https://github.com/qmk/qmk_cli
|
||||
requires-python = >=3.5
|
||||
|
||||
[entry_points]
|
||||
qmk = qmk_cli.script_qmk:main
|
||||
|
||||
[yapf]
|
||||
align_closing_bracket_with_visual_indent = True
|
||||
allow_multiline_dictionary_keys = False
|
||||
allow_multiline_lambdas = False
|
||||
allow_split_before_default_or_named_assigns = True
|
||||
allow_split_before_dict_value = True
|
||||
arithmetic_precedence_indication = True
|
||||
blank_lines_around_top_level_definition = 2
|
||||
blank_line_before_class_docstring = False
|
||||
blank_line_before_module_docstring = False
|
||||
blank_line_before_nested_class_or_def = False
|
||||
coalesce_brackets = True
|
||||
column_limit = 256
|
||||
continuation_align_style = SPACE
|
||||
continuation_indent_width = 4
|
||||
dedent_closing_brackets = True
|
||||
disable_ending_comma_heuristic = False
|
||||
each_dict_entry_on_separate_line = True
|
||||
i18n_comment =
|
||||
i18n_function_call =
|
||||
indent_blank_lines = False
|
||||
indent_dictionary_value = True
|
||||
indent_width = 4
|
||||
join_multiple_lines = False
|
||||
no_spaces_around_selected_binary_operators =
|
||||
spaces_around_default_or_named_assign = False
|
||||
spaces_around_power_operator = False
|
||||
spaces_before_comment = 2
|
||||
space_between_ending_comma_and_closing_bracket = False
|
||||
split_all_comma_separated_values = False
|
||||
split_arguments_when_comma_terminated = True
|
||||
split_before_arithmetic_operator = False
|
||||
split_before_bitwise_operator = True
|
||||
split_before_closing_bracket = True
|
||||
split_before_dict_set_generator = True
|
||||
split_before_dot = False
|
||||
split_before_expression_after_opening_paren = False
|
||||
split_before_first_argument = False
|
||||
split_before_logical_operator = False
|
||||
split_before_named_assigns = True
|
||||
split_complex_comprehension = True
|
||||
split_penalty_after_opening_bracket = 300
|
||||
split_penalty_after_unary_operator = 10000
|
||||
split_penalty_arithmetic_operator = 300
|
||||
split_penalty_before_if_expr = 0
|
||||
split_penalty_bitwise_operator = 300
|
||||
split_penalty_comprehension = 80
|
||||
split_penalty_excess_character = 7000
|
||||
split_penalty_for_added_line_split = 30
|
||||
split_penalty_import_names = 0
|
||||
split_penalty_logical_operator = 300
|
||||
use_tabs = False
|
||||
@@ -1,53 +0,0 @@
|
||||
from configparser import ConfigParser
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
setup_cfg = ConfigParser()
|
||||
setup_cfg.read('setup.cfg')
|
||||
metadata = setup_cfg['metadata']
|
||||
|
||||
if __name__ == "__main__":
|
||||
with open('README.md', encoding='utf-8') as readme_file:
|
||||
long_description=readme_file.read()
|
||||
setup(
|
||||
name=metadata['dist-name'],
|
||||
description='A program to help users work with QMK Firmware.',
|
||||
entry_points={
|
||||
'console_scripts': ['%s = %s' % l for l in setup_cfg['entry_points'].items()],
|
||||
},
|
||||
license='MIT License',
|
||||
url=metadata['home-page'],
|
||||
version=setup_cfg['bumpversion']['current_version'],
|
||||
author=metadata['author'],
|
||||
author_email=metadata['author-email'],
|
||||
maintainer=metadata['author'],
|
||||
maintainer_email=metadata['author-email'],
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
packages=find_packages(),
|
||||
py_modules = ['milc'],
|
||||
classifiers=[
|
||||
'Development Status :: 3 - Alpha',
|
||||
'Environment :: Console',
|
||||
'Intended Audience :: Developers',
|
||||
'Intended Audience :: System Administrators',
|
||||
'Intended Audience :: End Users/Desktop',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Natural Language :: English',
|
||||
'Programming Language :: Python :: 3 :: Only',
|
||||
'Topic :: Scientific/Engineering',
|
||||
'Topic :: Software Development',
|
||||
'Topic :: Utilities',
|
||||
],
|
||||
requires_python=metadata['requires-python'],
|
||||
install_requires=[
|
||||
"appdirs",
|
||||
"argcomplete",
|
||||
"colorama",
|
||||
"flake8",
|
||||
"hjson",
|
||||
"milc>=1.0.8",
|
||||
"nose2",
|
||||
"pygments",
|
||||
"yapf"
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,39 @@
|
||||
import tempfile
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from unittest.mock import create_autospec, PropertyMock, MagicMock
|
||||
|
||||
from milc import MILCInterface
|
||||
from milc.configuration import Configuration
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption(
|
||||
'--qmk',
|
||||
action='store',
|
||||
default='wrapper',
|
||||
help='options: ./qmk wrapper or real qmk from path',
|
||||
choices=('wrapper', 'real'),
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_directory():
|
||||
"""Similar behavior to the default tmpdir fixture except the folder is cleaned up."""
|
||||
with tempfile.TemporaryDirectory() as tmp_path:
|
||||
yield Path(tmp_path)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_cli():
|
||||
"""Mock cli to pass to cli subcommands,"""
|
||||
ret = create_autospec(MILCInterface)
|
||||
|
||||
# default behavior
|
||||
type(ret).config = Configuration()
|
||||
ret.config.general.unicode = True
|
||||
ret.config.general.color = True
|
||||
type(ret).write_config_option = MagicMock()
|
||||
type(ret.args).var = PropertyMock(return_value=None)
|
||||
|
||||
return ret
|
||||
@@ -0,0 +1,40 @@
|
||||
import pytest
|
||||
from unittest.mock import patch, PropertyMock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def subcommand(temp_directory):
|
||||
tmp = temp_directory.as_posix()
|
||||
with patch.dict('os.environ', {'ORIG_CWD': tmp, 'QMK_HOME': tmp}, clear=True):
|
||||
import qmk_cli.subcommands.clone as clone
|
||||
|
||||
yield clone
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def git_clone(subcommand):
|
||||
with patch.object(subcommand, 'git_clone') as git_clone:
|
||||
yield git_clone
|
||||
|
||||
|
||||
def test_clone(subcommand, mock_cli, git_clone):
|
||||
type(mock_cli.args).baseurl = PropertyMock(return_value='asdf')
|
||||
type(mock_cli.args).fork = PropertyMock(return_value='fork')
|
||||
git_clone.return_value = True
|
||||
|
||||
ret = subcommand.clone(mock_cli)
|
||||
|
||||
git_clone.assert_called_once()
|
||||
assert ret is True
|
||||
|
||||
|
||||
def test_clone_not_empty(subcommand, mock_cli, temp_directory, git_clone):
|
||||
type(mock_cli.args).baseurl = PropertyMock(return_value='asdf')
|
||||
type(mock_cli.args).fork = PropertyMock(return_value='fork')
|
||||
type(mock_cli.args).destination = PropertyMock(return_value=temp_directory)
|
||||
(temp_directory / 'asdf').touch()
|
||||
|
||||
ret = subcommand.clone(mock_cli)
|
||||
|
||||
git_clone.assert_not_called()
|
||||
assert ret is False
|
||||
@@ -0,0 +1,51 @@
|
||||
import pytest
|
||||
from unittest.mock import patch, PropertyMock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def subcommand(temp_directory):
|
||||
tmp = temp_directory.as_posix()
|
||||
with patch.dict('os.environ', {'ORIG_CWD': tmp, 'QMK_HOME': tmp}, clear=True):
|
||||
import qmk_cli.subcommands.env as env
|
||||
|
||||
yield env
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def is_qmk_firmware(subcommand):
|
||||
with patch.object(subcommand, 'is_qmk_firmware') as is_qmk_firmware:
|
||||
is_qmk_firmware.return_value = True
|
||||
|
||||
yield is_qmk_firmware
|
||||
|
||||
|
||||
def test_request_qmk_firmware_exists(subcommand, mock_cli, capsys, temp_directory, is_qmk_firmware):
|
||||
subcommand.env(mock_cli)
|
||||
captured = capsys.readouterr().out
|
||||
assert f'QMK_HOME="{temp_directory}"' in captured
|
||||
assert f'QMK_FIRMWARE="{temp_directory}"' in captured
|
||||
|
||||
|
||||
def test_request_all_variables(subcommand, mock_cli, capsys, temp_directory):
|
||||
subcommand.env(mock_cli)
|
||||
captured = capsys.readouterr().out
|
||||
assert f'QMK_HOME="{temp_directory}"' in captured
|
||||
assert 'QMK_FIRMWARE=""' in captured
|
||||
assert 'QMK_UNICODE="True"' in captured
|
||||
|
||||
|
||||
def test_request_single_variable(subcommand, mock_cli, capsys, temp_directory):
|
||||
type(mock_cli.args).var = PropertyMock(return_value='QMK_HOME')
|
||||
|
||||
subcommand.env(mock_cli)
|
||||
captured = capsys.readouterr().out
|
||||
assert captured == f'{temp_directory}\n'
|
||||
|
||||
|
||||
def test_request_single_variable_no_exits(subcommand, mock_cli, capsys):
|
||||
type(mock_cli.args).var = PropertyMock(return_value='ASDF')
|
||||
|
||||
ret = subcommand.env(mock_cli)
|
||||
captured = capsys.readouterr().err
|
||||
assert captured == 'Variable "ASDF" does not exist!\n'
|
||||
assert ret is False
|
||||
@@ -0,0 +1,221 @@
|
||||
import pytest
|
||||
from unittest.mock import patch, PropertyMock, ANY
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def subcommand(temp_directory):
|
||||
tmp = temp_directory.as_posix()
|
||||
with patch.dict('os.environ', {'ORIG_CWD': tmp, 'QMK_HOME': tmp}, clear=True):
|
||||
import qmk_cli.subcommands.setup as setup
|
||||
|
||||
yield setup
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def is_qmk_firmware(subcommand):
|
||||
with patch.object(subcommand, 'is_qmk_firmware') as is_qmk_firmware:
|
||||
is_qmk_firmware.return_value = True
|
||||
|
||||
yield is_qmk_firmware
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def yesno(subcommand):
|
||||
with patch.object(subcommand, 'yesno') as yesno:
|
||||
yesno.return_value = True
|
||||
|
||||
yield yesno
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def choice(subcommand):
|
||||
with patch.object(subcommand, 'choice') as choice:
|
||||
yield choice
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def question(subcommand):
|
||||
with patch.object(subcommand, 'question') as question:
|
||||
yield question
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def git_clone_fork(subcommand):
|
||||
with patch.object(subcommand, 'git_clone_fork') as git_clone_fork:
|
||||
yield git_clone_fork
|
||||
|
||||
|
||||
def test_setup_both_yes_no_invalid(subcommand, mock_cli):
|
||||
type(mock_cli.args).yes = PropertyMock(return_value=True)
|
||||
type(mock_cli.args).no = PropertyMock(return_value=True)
|
||||
|
||||
ret = subcommand.setup(mock_cli)
|
||||
|
||||
assert ret is False
|
||||
|
||||
|
||||
def test_setup_reclone(subcommand, mock_cli, is_qmk_firmware, temp_directory, yesno, choice, git_clone_fork):
|
||||
type(mock_cli.args).yes = PropertyMock(return_value=False)
|
||||
type(mock_cli.args).no = PropertyMock(return_value=False)
|
||||
type(mock_cli.args).home = PropertyMock(return_value=temp_directory)
|
||||
type(mock_cli.args).fork = 'asdf'
|
||||
choice.return_value = 'Delete and reclone asdf'
|
||||
yesno.return_value = True
|
||||
|
||||
subcommand.setup(mock_cli)
|
||||
|
||||
yesno.assert_called_once()
|
||||
assert 'This will delete your current qmk_firmware directory.' in yesno.call_args.args[0]
|
||||
git_clone_fork.assert_called_once()
|
||||
|
||||
|
||||
def test_setup_reclone_failed(subcommand, mock_cli, is_qmk_firmware, temp_directory, yesno, choice, git_clone_fork):
|
||||
type(mock_cli.args).yes = PropertyMock(return_value=False)
|
||||
type(mock_cli.args).no = PropertyMock(return_value=False)
|
||||
type(mock_cli.args).home = PropertyMock(return_value=temp_directory)
|
||||
type(mock_cli.args).fork = 'asdf'
|
||||
choice.return_value = 'Delete and reclone asdf'
|
||||
yesno.return_value = True
|
||||
git_clone_fork.return_value = False
|
||||
|
||||
ret = subcommand.setup(mock_cli)
|
||||
|
||||
yesno.assert_called_once()
|
||||
assert 'This will delete your current qmk_firmware directory.' in yesno.call_args.args[0]
|
||||
git_clone_fork.assert_called_once()
|
||||
assert ret is False
|
||||
|
||||
|
||||
def test_setup_reclone_no(subcommand, mock_cli, is_qmk_firmware, temp_directory, yesno, choice, git_clone_fork):
|
||||
type(mock_cli.args).yes = PropertyMock(return_value=False)
|
||||
type(mock_cli.args).no = PropertyMock(return_value=False)
|
||||
type(mock_cli.args).home = PropertyMock(return_value=temp_directory)
|
||||
type(mock_cli.args).fork = 'asdf'
|
||||
choice.return_value = 'Delete and reclone asdf'
|
||||
yesno.return_value = False
|
||||
|
||||
ret = subcommand.setup(mock_cli)
|
||||
|
||||
assert ret is False
|
||||
git_clone_fork.assert_not_called()
|
||||
|
||||
|
||||
def test_setup_clone_diff_fork(subcommand, mock_cli, is_qmk_firmware, temp_directory, yesno, choice, question, git_clone_fork):
|
||||
type(mock_cli.args).yes = PropertyMock(return_value=False)
|
||||
type(mock_cli.args).no = PropertyMock(return_value=False)
|
||||
type(mock_cli.args).home = PropertyMock(return_value=temp_directory)
|
||||
choice.return_value = 'Delete and clone a different fork'
|
||||
yesno.return_value = True
|
||||
|
||||
subcommand.setup(mock_cli)
|
||||
|
||||
yesno.assert_called_once()
|
||||
assert 'This will delete your current qmk_firmware directory.' in yesno.call_args.args[0]
|
||||
git_clone_fork.assert_called_once()
|
||||
|
||||
|
||||
def test_setup_clone_diff_fork_failed(subcommand, mock_cli, is_qmk_firmware, temp_directory, yesno, choice, question, git_clone_fork):
|
||||
type(mock_cli.args).yes = PropertyMock(return_value=False)
|
||||
type(mock_cli.args).no = PropertyMock(return_value=False)
|
||||
type(mock_cli.args).home = PropertyMock(return_value=temp_directory)
|
||||
choice.return_value = 'Delete and clone a different fork'
|
||||
yesno.return_value = True
|
||||
git_clone_fork.return_value = False
|
||||
|
||||
ret = subcommand.setup(mock_cli)
|
||||
|
||||
yesno.assert_called_once()
|
||||
assert 'This will delete your current qmk_firmware directory.' in yesno.call_args.args[0]
|
||||
git_clone_fork.assert_called_once()
|
||||
assert ret is False
|
||||
|
||||
|
||||
def test_setup_clone_diff_fork_no(subcommand, mock_cli, is_qmk_firmware, temp_directory, yesno, choice, question, git_clone_fork):
|
||||
type(mock_cli.args).yes = PropertyMock(return_value=False)
|
||||
type(mock_cli.args).no = PropertyMock(return_value=False)
|
||||
type(mock_cli.args).home = PropertyMock(return_value=temp_directory)
|
||||
choice.return_value = 'Delete and clone a different fork'
|
||||
yesno.return_value = False
|
||||
|
||||
ret = subcommand.setup(mock_cli)
|
||||
|
||||
assert ret is False
|
||||
git_clone_fork.assert_not_called()
|
||||
|
||||
|
||||
def test_setup_home_exists_not_empty(subcommand, mock_cli, is_qmk_firmware, temp_directory):
|
||||
type(mock_cli.args).yes = PropertyMock(return_value=False)
|
||||
type(mock_cli.args).no = PropertyMock(return_value=False)
|
||||
type(mock_cli.args).home = PropertyMock(return_value=temp_directory)
|
||||
is_qmk_firmware.return_value = False
|
||||
(temp_directory / 'asdf').touch()
|
||||
|
||||
ret = subcommand.setup(mock_cli)
|
||||
|
||||
assert ret is False
|
||||
|
||||
|
||||
def test_setup_missing(subcommand, mock_cli, is_qmk_firmware, temp_directory, yesno, git_clone_fork):
|
||||
type(mock_cli.args).yes = PropertyMock(return_value=False)
|
||||
type(mock_cli.args).no = PropertyMock(return_value=False)
|
||||
type(mock_cli.args).home = PropertyMock(return_value=temp_directory)
|
||||
is_qmk_firmware.return_value = False
|
||||
yesno.return_value = True
|
||||
|
||||
subcommand.setup(mock_cli)
|
||||
|
||||
yesno.assert_called_once()
|
||||
assert 'Would you like to clone' in yesno.call_args.args[0]
|
||||
git_clone_fork.assert_called_once()
|
||||
|
||||
|
||||
def test_setup_missing_failed(subcommand, mock_cli, is_qmk_firmware, temp_directory, yesno, git_clone_fork):
|
||||
type(mock_cli.args).yes = PropertyMock(return_value=False)
|
||||
type(mock_cli.args).no = PropertyMock(return_value=False)
|
||||
type(mock_cli.args).home = PropertyMock(return_value=temp_directory)
|
||||
is_qmk_firmware.return_value = False
|
||||
yesno.return_value = True
|
||||
git_clone_fork.return_value = False
|
||||
|
||||
ret = subcommand.setup(mock_cli)
|
||||
|
||||
yesno.assert_called_once()
|
||||
assert 'Would you like to clone' in yesno.call_args.args[0]
|
||||
git_clone_fork.assert_called_once()
|
||||
assert ret is False
|
||||
|
||||
|
||||
def test_setup_missing_no(subcommand, mock_cli, is_qmk_firmware, temp_directory, yesno, git_clone_fork):
|
||||
type(mock_cli.args).yes = PropertyMock(return_value=False)
|
||||
type(mock_cli.args).no = PropertyMock(return_value=False)
|
||||
type(mock_cli.args).home = PropertyMock(return_value=temp_directory)
|
||||
is_qmk_firmware.return_value = False
|
||||
yesno.return_value = False
|
||||
|
||||
subcommand.setup(mock_cli)
|
||||
|
||||
yesno.assert_called_once()
|
||||
assert 'Would you like to clone' in yesno.call_args.args[0]
|
||||
git_clone_fork.assert_not_called()
|
||||
|
||||
|
||||
def test_setup_chains_args_to_doctor(subcommand, mock_cli, is_qmk_firmware, yesno, choice, git_clone_fork):
|
||||
type(mock_cli.args).yes = PropertyMock(return_value=True)
|
||||
type(mock_cli.args).no = PropertyMock(return_value=False)
|
||||
|
||||
subcommand.setup(mock_cli)
|
||||
|
||||
mock_cli.run.assert_called_once()
|
||||
assert mock_cli.run.call_args.args[0] == [ANY, '--color', '--unicode', 'doctor', '-y']
|
||||
|
||||
|
||||
def test_setup_chains_args_to_doctor_no(subcommand, mock_cli, is_qmk_firmware, yesno, choice, git_clone_fork):
|
||||
type(mock_cli.args).yes = PropertyMock(return_value=False)
|
||||
type(mock_cli.args).no = PropertyMock(return_value=True)
|
||||
mock_cli.config.general.color = False
|
||||
mock_cli.config.general.unicode = False
|
||||
|
||||
subcommand.setup(mock_cli)
|
||||
|
||||
mock_cli.run.assert_called_once()
|
||||
assert mock_cli.run.call_args.args[0] == [ANY, '--no-color', '--no-unicode', 'doctor', '-n']
|
||||
@@ -0,0 +1,46 @@
|
||||
import pytest
|
||||
import subprocess
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def temp_env(temp_directory, monkeypatch):
|
||||
qmk_firmware = temp_directory / 'qmk_firmware'
|
||||
monkeypatch.setenv("QMK_HOME", qmk_firmware.as_posix())
|
||||
yield qmk_firmware
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def run_cli(request):
|
||||
def run_cli_wrapper(args):
|
||||
cmd = f'{request.config.rootpath}/qmk' if request.config.getoption("--qmk") == 'wrapper' else 'qmk'
|
||||
|
||||
return subprocess.run([cmd, *args], capture_output=True, text=True)
|
||||
|
||||
return run_cli_wrapper
|
||||
|
||||
|
||||
@pytest.mark.system
|
||||
def test_cli_help(run_cli):
|
||||
completed_process = run_cli(['clone', '--help'])
|
||||
|
||||
assert 'usage:' in completed_process.stdout
|
||||
assert completed_process.returncode == 0
|
||||
|
||||
|
||||
@pytest.mark.system
|
||||
def test_cli_setup_no(run_cli):
|
||||
completed_process = run_cli(['setup', '-n'])
|
||||
|
||||
assert 'Could not find qmk_firmware!' in completed_process.stderr
|
||||
assert 'Not cloning qmk_firmware due to user input or --no flag.' in completed_process.stderr
|
||||
assert completed_process.returncode == 0
|
||||
|
||||
|
||||
@pytest.mark.system
|
||||
def test_cli_setup_yes(run_cli):
|
||||
completed_process = run_cli(['setup', '-y'])
|
||||
|
||||
assert 'Successfully cloned https://github.com/qmk/qmk_firmware' in completed_process.stderr
|
||||
assert 'Added https://github.com/qmk/qmk_firmware as remote upstream' in completed_process.stderr
|
||||
assert 'QMK Doctor is checking your environment' in completed_process.stderr
|
||||
assert completed_process.returncode == 0
|
||||
@@ -0,0 +1,117 @@
|
||||
import pytest
|
||||
import subprocess
|
||||
from unittest.mock import patch, PropertyMock
|
||||
|
||||
import qmk_cli.git
|
||||
|
||||
|
||||
@patch("qmk_cli.git.subprocess.Popen", side_effect=Exception("foobar"))
|
||||
@patch("qmk_cli.git.cli.log.error")
|
||||
def test_git_clone_except(mock_log_error, mock_popen):
|
||||
ret = qmk_cli.git.git_clone('/tmp', 'https://github.com/qmk/qmk_firmware', 'master')
|
||||
|
||||
assert "Could not run" in str(mock_log_error.call_args_list)
|
||||
assert "Exception:foobar" in str(mock_log_error.call_args_list)
|
||||
assert ret is False
|
||||
|
||||
|
||||
@patch("qmk_cli.git.subprocess.Popen")
|
||||
def test_git_clone_logs_output(mock_popen, capsys):
|
||||
type(mock_popen().__enter__()).returncode = PropertyMock(return_value=0)
|
||||
type(mock_popen().__enter__()).stdout = PropertyMock(return_value=['asdf'])
|
||||
|
||||
ret = qmk_cli.git.git_clone('/tmp', 'https://github.com/qmk/qmk_firmware', 'master')
|
||||
|
||||
captured = capsys.readouterr().out
|
||||
assert 'asdf' in captured
|
||||
assert ret is True
|
||||
|
||||
|
||||
@patch("qmk_cli.git.subprocess.Popen")
|
||||
def test_git_clone_captures_exit_code(mock_popen):
|
||||
type(mock_popen().__enter__()).returncode = PropertyMock(return_value=1)
|
||||
|
||||
ret = qmk_cli.git.git_clone('/tmp', 'https://github.com/qmk/qmk_firmware', 'master')
|
||||
|
||||
assert ret is False
|
||||
|
||||
|
||||
@patch("qmk_cli.git.subprocess.Popen", side_effect=Exception("foobar"))
|
||||
@patch("qmk_cli.git.cli.log.error")
|
||||
def test_git_set_upstream_except(mock_log_error, mock_popen):
|
||||
ret = qmk_cli.git.git_set_upstream('/tmp')
|
||||
|
||||
assert "Could not run" in str(mock_log_error.call_args_list)
|
||||
assert "Exception:foobar" in str(mock_log_error.call_args_list)
|
||||
assert ret is False
|
||||
|
||||
|
||||
@patch("qmk_cli.git.subprocess.Popen")
|
||||
def test_git_set_upstream_logs_output(mock_popen, capsys):
|
||||
type(mock_popen().__enter__()).returncode = PropertyMock(return_value=0)
|
||||
type(mock_popen().__enter__()).stdout = PropertyMock(return_value=['asdf'])
|
||||
|
||||
ret = qmk_cli.git.git_set_upstream('/tmp')
|
||||
|
||||
captured = capsys.readouterr().out
|
||||
assert 'asdf' in captured
|
||||
assert ret is True
|
||||
|
||||
|
||||
@patch("qmk_cli.git.subprocess.Popen")
|
||||
def test_git_set_upstream_captures_exit_code(mock_popen):
|
||||
type(mock_popen().__enter__()).returncode = PropertyMock(return_value=1)
|
||||
|
||||
ret = qmk_cli.git.git_set_upstream('/tmp')
|
||||
|
||||
assert ret is False
|
||||
|
||||
|
||||
@patch.object(qmk_cli.git, 'git_clone')
|
||||
@patch.object(qmk_cli.git, 'git_set_upstream')
|
||||
def test_git_clone_fork_fail(git_set_upstream, git_clone, temp_directory):
|
||||
git_clone.return_value = False
|
||||
git_set_upstream.return_value = False
|
||||
|
||||
ret = qmk_cli.git.git_clone_fork(temp_directory.as_posix(), 'https://github.com', 'qmk/qmk_firmware', 'master', False)
|
||||
|
||||
assert ret is False
|
||||
|
||||
|
||||
@patch.object(qmk_cli.git, 'git_clone')
|
||||
@patch.object(qmk_cli.git, 'git_set_upstream')
|
||||
def test_git_clone_fork_force(git_set_upstream, git_clone, temp_directory):
|
||||
(temp_directory / 'asdf').touch()
|
||||
|
||||
git_clone.return_value = True
|
||||
git_set_upstream.return_value = True
|
||||
|
||||
ret = qmk_cli.git.git_clone_fork(temp_directory.as_posix(), 'https://github.com', 'qmk/qmk_firmware', 'master', True)
|
||||
|
||||
assert ret is True
|
||||
assert (temp_directory / 'asdf').exists() is False
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_git_clone(temp_directory):
|
||||
qmk_firmware = temp_directory / 'qmk_firmware'
|
||||
ret = qmk_cli.git.git_clone(qmk_firmware.as_posix(), 'https://github.com/qmk/qmk_firmware', 'master')
|
||||
|
||||
from qmk_cli.helpers import is_qmk_firmware
|
||||
|
||||
assert is_qmk_firmware(qmk_firmware) is True
|
||||
assert ret is True
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_git_clone_fork(temp_directory):
|
||||
qmk_firmware = temp_directory / 'qmk_firmware'
|
||||
qmk_firmware.mkdir()
|
||||
(qmk_firmware / 'asdf').touch()
|
||||
ret = qmk_cli.git.git_clone_fork(qmk_firmware.as_posix(), 'https://github.com', 'qmk/qmk_firmware', 'master', True)
|
||||
|
||||
from qmk_cli.helpers import is_qmk_firmware
|
||||
|
||||
assert is_qmk_firmware(qmk_firmware) is True
|
||||
assert 'https://github.com/qmk/qmk_firmware' in subprocess.run(['git', '-C', qmk_firmware.as_posix(), 'remote', 'get-url', 'upstream'], capture_output=True, text=True).stdout
|
||||
assert ret is True
|
||||
@@ -0,0 +1,246 @@
|
||||
import os
|
||||
import stat
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch, PropertyMock
|
||||
|
||||
import qmk_cli.helpers
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_env():
|
||||
cur_dir = os.getcwd()
|
||||
|
||||
yield cur_dir
|
||||
|
||||
os.chdir(cur_dir)
|
||||
qmk_cli.helpers.find_qmk_firmware.cache_clear()
|
||||
qmk_cli.helpers.in_qmk_firmware.cache_clear()
|
||||
qmk_cli.helpers.find_qmk_userspace.cache_clear()
|
||||
qmk_cli.helpers.in_qmk_userspace.cache_clear()
|
||||
|
||||
|
||||
def test_abspath(temp_directory):
|
||||
tmp = temp_directory.absolute()
|
||||
ret = qmk_cli.helpers.AbsPath(tmp)
|
||||
assert ret.samefile(tmp)
|
||||
|
||||
|
||||
def test_abspath_str(temp_directory):
|
||||
tmp = temp_directory.absolute().as_posix()
|
||||
ret = qmk_cli.helpers.AbsPath(tmp)
|
||||
assert ret.samefile(tmp)
|
||||
|
||||
|
||||
def test_abspath_rel(temp_directory):
|
||||
with patch.dict('os.environ', {'ORIG_CWD': temp_directory.as_posix()}, clear=True):
|
||||
tmp = './asdf'
|
||||
ret = qmk_cli.helpers.AbsPath(tmp)
|
||||
assert ret.as_posix() == (temp_directory / 'asdf').as_posix()
|
||||
|
||||
|
||||
def test_rmtree(temp_directory):
|
||||
tmp = temp_directory / 'asdf.txt'
|
||||
tmp.touch()
|
||||
|
||||
qmk_cli.helpers.rmtree(temp_directory)
|
||||
|
||||
assert tmp.exists() is False
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name != "nt", reason="Only triggers issue on windows")
|
||||
def test_rmtree_readonly(temp_directory):
|
||||
tmp = temp_directory / 'asdf.txt'
|
||||
tmp.touch()
|
||||
os.chmod(tmp, stat.S_IREAD)
|
||||
|
||||
qmk_cli.helpers.rmtree(temp_directory)
|
||||
|
||||
assert tmp.exists() is False
|
||||
|
||||
|
||||
def test_is_qmk_firmware(temp_directory):
|
||||
(temp_directory / 'quantum').mkdir(parents=True)
|
||||
(temp_directory / 'lib/python/qmk/cli').mkdir(parents=True)
|
||||
(temp_directory / 'requirements.txt').touch()
|
||||
(temp_directory / 'requirements-dev.txt').touch()
|
||||
(temp_directory / 'lib/python/qmk/cli/__init__.py').touch()
|
||||
|
||||
assert qmk_cli.helpers.is_qmk_firmware(temp_directory) is True
|
||||
|
||||
|
||||
def test_is_qmk_firmware_partially_missing(temp_directory):
|
||||
(temp_directory / 'lib/python/qmk/cli').mkdir(parents=True)
|
||||
(temp_directory / 'lib/python/qmk/cli/__init__.py').touch()
|
||||
|
||||
assert qmk_cli.helpers.is_qmk_firmware(temp_directory) is False
|
||||
|
||||
|
||||
def test_find_qmk_firmware(temp_directory):
|
||||
(temp_directory / 'quantum').mkdir(parents=True)
|
||||
(temp_directory / 'lib/python/qmk/cli').mkdir(parents=True)
|
||||
(temp_directory / 'requirements.txt').touch()
|
||||
(temp_directory / 'requirements-dev.txt').touch()
|
||||
(temp_directory / 'lib/python/qmk/cli/__init__.py').touch()
|
||||
os.chdir(temp_directory)
|
||||
|
||||
assert qmk_cli.helpers.find_qmk_firmware().as_posix() == temp_directory.as_posix()
|
||||
|
||||
|
||||
def test_find_qmk_firmware_env(temp_directory):
|
||||
with patch.dict('os.environ', {'QMK_HOME': temp_directory.as_posix()}, clear=True):
|
||||
assert qmk_cli.helpers.find_qmk_firmware().as_posix() == temp_directory.as_posix()
|
||||
|
||||
|
||||
def test_find_qmk_firmware_env_missing(temp_directory):
|
||||
with patch.dict('os.environ', {'QMK_HOME': (temp_directory / 'asdf').as_posix()}, clear=True):
|
||||
assert qmk_cli.helpers.find_qmk_firmware().as_posix() == (temp_directory / 'asdf').as_posix()
|
||||
|
||||
|
||||
def test_find_qmk_firmware_config(temp_directory):
|
||||
with patch.object(qmk_cli.helpers, 'cli') as cli:
|
||||
type(cli.config.user).qmk_home = PropertyMock(return_value=temp_directory.as_posix())
|
||||
|
||||
assert qmk_cli.helpers.find_qmk_firmware().as_posix() == temp_directory.as_posix()
|
||||
|
||||
|
||||
def test_find_qmk_firmware_config_user():
|
||||
with patch.object(qmk_cli.helpers, 'cli') as cli:
|
||||
type(cli.config.user).qmk_home = PropertyMock(return_value='~/asdf')
|
||||
|
||||
assert qmk_cli.helpers.find_qmk_firmware().as_posix() == (Path.home() / 'asdf').as_posix()
|
||||
|
||||
|
||||
def test_find_qmk_firmware_default(temp_directory):
|
||||
with patch('pathlib.Path.home') as home:
|
||||
home.return_value = temp_directory
|
||||
|
||||
assert qmk_cli.helpers.find_qmk_firmware().as_posix() == (temp_directory / 'qmk_firmware').as_posix()
|
||||
|
||||
|
||||
def test_in_qmk_firmware(temp_directory):
|
||||
(temp_directory / 'quantum').mkdir(parents=True)
|
||||
(temp_directory / 'lib/python/qmk/cli').mkdir(parents=True)
|
||||
(temp_directory / 'requirements.txt').touch()
|
||||
(temp_directory / 'requirements-dev.txt').touch()
|
||||
(temp_directory / 'lib/python/qmk/cli/__init__.py').touch()
|
||||
os.chdir(temp_directory)
|
||||
|
||||
assert qmk_cli.helpers.in_qmk_firmware().as_posix() == temp_directory.as_posix()
|
||||
|
||||
|
||||
def test_in_qmk_firmware_sub(temp_directory):
|
||||
(temp_directory / 'quantum').mkdir(parents=True)
|
||||
(temp_directory / 'lib/python/qmk/cli').mkdir(parents=True)
|
||||
(temp_directory / 'requirements.txt').touch()
|
||||
(temp_directory / 'requirements-dev.txt').touch()
|
||||
(temp_directory / 'lib/python/qmk/cli/__init__.py').touch()
|
||||
os.chdir(temp_directory / 'quantum')
|
||||
|
||||
assert qmk_cli.helpers.in_qmk_firmware().as_posix() == temp_directory.as_posix()
|
||||
|
||||
|
||||
def test_in_qmk_firmware_sub_sub(temp_directory):
|
||||
(temp_directory / 'quantum').mkdir(parents=True)
|
||||
(temp_directory / 'lib/python/qmk/cli').mkdir(parents=True)
|
||||
(temp_directory / 'requirements.txt').touch()
|
||||
(temp_directory / 'requirements-dev.txt').touch()
|
||||
(temp_directory / 'lib/python/qmk/cli/__init__.py').touch()
|
||||
os.chdir(temp_directory / 'lib' / 'python')
|
||||
|
||||
assert qmk_cli.helpers.in_qmk_firmware().as_posix() == temp_directory.as_posix()
|
||||
|
||||
|
||||
def test_in_qmk_firmware_invalid(temp_directory):
|
||||
os.chdir(temp_directory)
|
||||
|
||||
assert qmk_cli.helpers.in_qmk_firmware() is None
|
||||
|
||||
|
||||
def test_is_qmk_userspace(temp_directory):
|
||||
(temp_directory / 'qmk.json').write_text('{"userspace_version": 2}')
|
||||
|
||||
assert qmk_cli.helpers.is_qmk_userspace(temp_directory) is True
|
||||
|
||||
|
||||
def test_is_qmk_userspace_missing(temp_directory):
|
||||
assert qmk_cli.helpers.is_qmk_userspace(temp_directory) is False
|
||||
|
||||
|
||||
def test_is_qmk_userspace_invalid(temp_directory):
|
||||
(temp_directory / 'qmk.json').write_text('asdf')
|
||||
|
||||
assert qmk_cli.helpers.is_qmk_userspace(temp_directory) is False
|
||||
|
||||
|
||||
def test_is_qmk_userspace_empty(temp_directory):
|
||||
(temp_directory / 'qmk.json').touch()
|
||||
|
||||
assert qmk_cli.helpers.is_qmk_userspace(temp_directory) is False
|
||||
|
||||
|
||||
def test_find_qmk_userspace(temp_directory):
|
||||
(temp_directory / 'qmk.json').write_text('{"userspace_version": 2}')
|
||||
os.chdir(temp_directory)
|
||||
|
||||
assert qmk_cli.helpers.find_qmk_userspace().as_posix() == temp_directory.as_posix()
|
||||
|
||||
|
||||
def test_find_qmk_userspace_env(temp_directory):
|
||||
with patch.dict('os.environ', {'QMK_USERSPACE': temp_directory.as_posix()}, clear=True):
|
||||
assert qmk_cli.helpers.find_qmk_userspace().as_posix() == temp_directory.as_posix()
|
||||
|
||||
|
||||
def test_find_qmk_userspace_env_missing(temp_directory):
|
||||
with patch.dict('os.environ', {'QMK_USERSPACE': (temp_directory / 'asdf').as_posix()}, clear=True):
|
||||
assert qmk_cli.helpers.find_qmk_userspace().as_posix() == (temp_directory / 'asdf').as_posix()
|
||||
|
||||
|
||||
def test_find_qmk_userspace_config(temp_directory):
|
||||
with patch.object(qmk_cli.helpers, 'cli') as cli:
|
||||
type(cli.config.user).overlay_dir = PropertyMock(return_value=temp_directory.as_posix())
|
||||
|
||||
assert qmk_cli.helpers.find_qmk_userspace().as_posix() == temp_directory.as_posix()
|
||||
|
||||
|
||||
def test_find_qmk_userspace_config_user():
|
||||
with patch.object(qmk_cli.helpers, 'cli') as cli:
|
||||
type(cli.config.user).overlay_dir = PropertyMock(return_value='~/asdf')
|
||||
|
||||
assert qmk_cli.helpers.find_qmk_userspace().as_posix() == (Path.home() / 'asdf').as_posix()
|
||||
|
||||
|
||||
def test_find_qmk_userspace_default(temp_directory):
|
||||
with patch('pathlib.Path.home') as home:
|
||||
home.return_value = temp_directory
|
||||
|
||||
assert qmk_cli.helpers.find_qmk_userspace().as_posix() == (temp_directory / 'qmk_userspace').as_posix()
|
||||
|
||||
|
||||
def test_in_qmk_userspace(temp_directory):
|
||||
(temp_directory / 'qmk.json').write_text('{"userspace_version": 2}')
|
||||
os.chdir(temp_directory)
|
||||
|
||||
assert qmk_cli.helpers.in_qmk_userspace().as_posix() == temp_directory.as_posix()
|
||||
|
||||
|
||||
def test_in_qmk_userspace_sub(temp_directory):
|
||||
(temp_directory / 'qmk.json').write_text('{"userspace_version": 2}')
|
||||
(temp_directory / 'keymaps' / 'test').mkdir(parents=True)
|
||||
os.chdir(temp_directory / 'keymaps')
|
||||
|
||||
assert qmk_cli.helpers.in_qmk_userspace().as_posix() == temp_directory.as_posix()
|
||||
|
||||
|
||||
def test_in_qmk_userspace_sub_sub(temp_directory):
|
||||
(temp_directory / 'qmk.json').write_text('{"userspace_version": 2}')
|
||||
(temp_directory / 'keymaps' / 'test').mkdir(parents=True)
|
||||
os.chdir(temp_directory / 'keymaps' / 'test')
|
||||
|
||||
assert qmk_cli.helpers.in_qmk_userspace().as_posix() == temp_directory.as_posix()
|
||||
|
||||
|
||||
def test_in_qmk_userspace_invalid(temp_directory):
|
||||
os.chdir(temp_directory)
|
||||
|
||||
assert qmk_cli.helpers.in_qmk_userspace() is None
|
||||
Reference in New Issue
Block a user