Compare commits
178 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 |
@@ -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]
|
||||||
@@ -7,6 +7,8 @@ updates:
|
|||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "daily"
|
interval: "daily"
|
||||||
|
reviewers:
|
||||||
|
- "qmk/collaborators"
|
||||||
- package-ecosystem: "pip" # See documentation for possible values
|
- package-ecosystem: "pip" # See documentation for possible values
|
||||||
directory: "/" # Location of package manifests
|
directory: "/" # Location of package manifests
|
||||||
schedule:
|
schedule:
|
||||||
|
|||||||
@@ -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,51 +7,119 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
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:
|
test_cli_linux_macos:
|
||||||
|
needs: test_cli_base_container
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
env:
|
|
||||||
QMK_HOME: ~/qmk_firmware
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [macos-latest, ubuntu-latest]
|
os: [macos-latest, ubuntu-latest]
|
||||||
python-version: [3.7, 3.8, 3.9]
|
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2.3.4
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v2.2.2
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
- name: Run ci_tests
|
- name: Run ci_tests
|
||||||
run: ./ci_tests -a
|
run: ./ci_tests
|
||||||
|
|
||||||
test_cli_win:
|
test_cli_win:
|
||||||
|
needs: test_cli_base_container
|
||||||
|
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
env:
|
|
||||||
QMK_HOME: $HOME/qmk_firmware
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2.3.4
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: (MSYS2) Setup and install dependencies
|
- name: (MSYS2) Setup and install dependencies
|
||||||
uses: msys2/setup-msys2@v2
|
uses: msys2/setup-msys2@v2
|
||||||
with:
|
with:
|
||||||
update: true
|
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
|
||||||
|
|
||||||
|
# Upgrade pip due to msys packaging + pypa/build/pull/736 issues
|
||||||
- name: (MSYS2) Install Python dependencies
|
- name: (MSYS2) Install Python dependencies
|
||||||
shell: msys2 {0}
|
shell: msys2 {0}
|
||||||
run: |
|
run: |
|
||||||
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}
|
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: |
|
run: |
|
||||||
python3 -m pip install -r requirements.txt
|
python3 -m pip install --upgrade pip
|
||||||
python3 setup.py sdist bdist_wheel
|
pip install setuptools wheel
|
||||||
cd ..
|
pip install --group dev
|
||||||
python3 -m pip install --force-reinstall --no-index --no-deps --find-links qmk_cli/dist qmk
|
|
||||||
- name: (MSYS2) Run qmk setup -y
|
- name: Build Python
|
||||||
shell: msys2 {0}
|
run: |
|
||||||
run: qmk setup -y
|
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
|
||||||
|
|||||||
@@ -39,11 +39,11 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2.3.4
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v1
|
uses: github/codeql-action/init@v4
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
@@ -54,7 +54,7 @@ jobs:
|
|||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# 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)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v1
|
uses: github/codeql-action/autobuild@v4
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 https://git.io/JvXDl
|
# 📚 https://git.io/JvXDl
|
||||||
@@ -68,4 +68,4 @@ jobs:
|
|||||||
# make release
|
# make release
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v1
|
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 }}
|
||||||
@@ -17,44 +17,85 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2.3.4
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v2.2.2
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: '3.7'
|
python-version: '3.9'
|
||||||
- name: Run ci_tests
|
|
||||||
run: ./ci_tests
|
- 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
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python3 -m pip install --upgrade pip
|
||||||
pip install setuptools wheel
|
pip install setuptools wheel
|
||||||
pip install -r requirements-dev.txt
|
pip install --group dev
|
||||||
|
|
||||||
- name: Bump version
|
- name: Bump version
|
||||||
run: |
|
run: |
|
||||||
git config --local user.email "hello@qmk.fm"
|
git config --local user.email "hello@qmk.fm"
|
||||||
git config --local user.name "QMK Bot"
|
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 }}
|
bumpversion ${{ github.event.inputs.version_part }}
|
||||||
|
|
||||||
- name: Push changes
|
- name: Push changes
|
||||||
uses: ad-m/github-push-action@master
|
uses: ad-m/github-push-action@master
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
branch: master
|
branch: master
|
||||||
tags: true
|
tags: true
|
||||||
|
|
||||||
- name: Build and publish
|
- name: Build and publish
|
||||||
env:
|
env:
|
||||||
TWINE_USERNAME: qmk
|
TWINE_USERNAME: __token__
|
||||||
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
|
TWINE_PASSWORD: ${{ secrets.PYPI_APITOKEN }}
|
||||||
run: |
|
run: |
|
||||||
python setup.py sdist bdist_wheel
|
python3 -m build
|
||||||
twine upload dist/*
|
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
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@v1.10.0
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||||
- name: Build and Push to Docker Hub
|
|
||||||
uses: docker/build-push-action@v2.6.1
|
- name: Build and Push slim to Docker Hub
|
||||||
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
tags: qmkfm/qmk_cli:latest
|
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 }}
|
||||||
|
|
||||||
|
|||||||
+38
-5
@@ -1,13 +1,46 @@
|
|||||||
FROM qmkfm/base_container
|
FROM ghcr.io/qmk/qmk_base_container:latest AS builder
|
||||||
|
|
||||||
# Copy package in
|
# Copy package in
|
||||||
ADD dist /tmp/dist
|
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
|
# Install python packages
|
||||||
RUN python3 -m pip uninstall -y qmk || true
|
RUN python3 -m pip uninstall -y qmk || true
|
||||||
RUN python3 -m pip install --upgrade pip setuptools wheel nose2 && \
|
RUN python3 -m pip install --upgrade pip setuptools wheel nose2 && \
|
||||||
python3 -m pip install /tmp/dist/qmk-*.whl && \
|
python3 -m pip install /tmp/dist/qmk-*.whl
|
||||||
rm -rf /tmp/dist
|
|
||||||
|
# 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
|
||||||
|
|
||||||
# Set the default location for qmk_firmware
|
|
||||||
ENV QMK_HOME /qmk_firmware
|
|
||||||
|
|||||||
@@ -1,24 +1,57 @@
|
|||||||
# QMK CLI
|
# 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/).
|
> A program to help users work with [QMK Firmware](https://qmk.fm/).
|
||||||
|
|
||||||
# Features
|
[](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
|
* Interact with your qmk_firmware tree from any location
|
||||||
* Use `qmk clone` to pull down anyone's `qmk_firmware` fork
|
* Use `qmk clone` to pull down anyone's `qmk_firmware` fork
|
||||||
* Setup your build environment with `qmk setup`
|
* 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`
|
* Check that your environment is correctly setup with `qmk doctor`
|
||||||
* Integrates with qmk_firmware for additional functionality:
|
* Integrates with qmk_firmware for additional functionality:
|
||||||
* `qmk compile`
|
* `qmk compile`
|
||||||
* `qmk console`
|
* `qmk info`
|
||||||
* `qmk flash`
|
* `qmk flash`
|
||||||
* `qmk lint`
|
* `qmk lint`
|
||||||
* ...and many more!
|
* ...and many more!
|
||||||
|
|
||||||
# Quickstart
|
## Quickstart
|
||||||
|
|
||||||
* `python3 -m pip install qmk`
|
Follow our [documentation to get started](https://docs.qmk.fm/newbs_getting_started).
|
||||||
* `qmk setup`
|
|
||||||
|
## 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>
|
Full documentation: <https://docs.qmk.fm/#/tutorial>
|
||||||
|
|||||||
@@ -3,57 +3,23 @@
|
|||||||
set -e
|
set -e
|
||||||
#set -x
|
#set -x
|
||||||
|
|
||||||
ADVANCED=false
|
|
||||||
TMPDIR=$(mktemp -d)
|
TMPDIR=$(mktemp -d)
|
||||||
QMK_HOME="$TMPDIR/qmk_firmware"
|
|
||||||
export QMK_HOME
|
|
||||||
|
|
||||||
for arg in $@; do
|
trap 'rm -rf "$TMPDIR"' EXIT
|
||||||
if [ "$arg" = "-a" ]; then
|
|
||||||
ADVANCED=true
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ $ADVANCED = true ]; then
|
CI_VENV="$TMPDIR/.ci_venv"
|
||||||
echo "*** Running in advanced mode with tmp files in $TMPDIR"
|
|
||||||
else
|
# Avoid issues with building pillow as we install mingw-w64-x86_64-python-pillow
|
||||||
echo "*** Running in basic mode with tmp files in $TMPDIR"
|
if [[ "$(uname -s)" =~ ^MINGW64_NT.* ]]; then
|
||||||
|
CI_VENV_ARGS="--system-site-packages"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Setup our virtualenv
|
python3 -m venv $CI_VENV_ARGS $CI_VENV
|
||||||
if [ -e .ci_venv ]; then
|
source $CI_VENV/bin/activate
|
||||||
rm -rf .ci_venv
|
|
||||||
fi
|
|
||||||
|
|
||||||
python3 -m venv .ci_venv
|
|
||||||
source .ci_venv/bin/activate
|
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
python3 -m pip install -U pip wheel
|
python3 -m pip install -U pip wheel
|
||||||
python3 -m pip install .
|
python3 -m pip install .
|
||||||
python3 -m pip install -r requirements-dev.txt
|
python3 -m pip install --group dev
|
||||||
|
|
||||||
# Ensure that qmk works
|
pytest -vv -m system --qmk=real
|
||||||
echo "*** Testing 'qmk clone -h'"
|
|
||||||
qmk clone -h
|
|
||||||
echo "*** Testing 'qmk config -a'"
|
|
||||||
qmk config -a
|
|
||||||
echo "*** Testing 'qmk setup -n'"
|
|
||||||
qmk setup -n
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "*** Basic tests completed successfully!"
|
|
||||||
|
|
||||||
# Run advanced test if requested
|
|
||||||
if [ $ADVANCED = true ]; then
|
|
||||||
echo
|
|
||||||
echo "*** Testing 'qmk setup -y'"
|
|
||||||
qmk setup -y
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "*** Advanced tests completed successfully!"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
deactivate
|
|
||||||
rm -rf .ci_venv $TMPDIR
|
|
||||||
|
|||||||
+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.
|
"""Wrapper to call the qmk cli script entrypoint.
|
||||||
"""
|
"""
|
||||||
import qmk_cli.script_qmk
|
import qmk_cli.script_qmk
|
||||||
|
|
||||||
qmk_cli.script_qmk.main()
|
qmk_cli.script_qmk.main()
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
"""A program to help you work with qmk_firmware."""
|
"""A program to help you work with qmk_firmware."""
|
||||||
|
|
||||||
__version__ = '0.2.2'
|
__version__ = '1.2.0'
|
||||||
|
|||||||
+59
-15
@@ -1,24 +1,24 @@
|
|||||||
"""Helpers for working with git.
|
"""Helpers for working with git."""
|
||||||
"""
|
|
||||||
import subprocess
|
import subprocess
|
||||||
|
from .helpers import rmtree
|
||||||
|
|
||||||
from milc import cli
|
from milc import cli
|
||||||
|
|
||||||
default_repo = 'qmk_firmware'
|
DEFAULT_UPSTREAM = 'https://github.com/qmk/qmk_firmware'
|
||||||
default_fork = 'qmk/' + default_repo
|
|
||||||
default_branch = 'master'
|
|
||||||
|
|
||||||
|
|
||||||
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 = [
|
||||||
'git',
|
'git',
|
||||||
'clone',
|
'clone',
|
||||||
'--recurse-submodules',
|
'--recurse-submodules',
|
||||||
'--branch=' + branch,
|
f'--branch={branch}',
|
||||||
url,
|
url,
|
||||||
str(destination),
|
str(destination),
|
||||||
]
|
]
|
||||||
cli.log.debug('Git clone command: %s', git_clone)
|
cli.log.debug(f'Git clone command: {git_clone}')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with subprocess.Popen(git_clone, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, bufsize=1, universal_newlines=True, encoding='utf-8') as p:
|
with subprocess.Popen(git_clone, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, bufsize=1, universal_newlines=True, encoding='utf-8') as p:
|
||||||
@@ -28,13 +28,57 @@ def git_clone(url, destination, branch):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
git_cmd = ' '.join([s.replace(' ', r'\ ') for s in git_clone])
|
git_cmd = ' '.join([s.replace(' ', r'\ ') for s in git_clone])
|
||||||
|
|
||||||
cli.log.error("Could not run '%s': %s: %s", git_cmd, e.__class__.__name__, e)
|
cli.log.error(f"Could not run '{git_cmd}': {e.__class__.__name__}:{e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if p.returncode == 0:
|
if p.returncode != 0:
|
||||||
cli.log.info('Successfully cloned %s to %s!', url, destination)
|
cli.log.error(f'git clone exited {p.returncode}')
|
||||||
return True
|
|
||||||
|
|
||||||
else:
|
|
||||||
cli.log.error('git clone exited %d', p.returncode)
|
|
||||||
return False
|
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)
|
||||||
|
|||||||
+77
-3
@@ -1,13 +1,40 @@
|
|||||||
"""Useful helper functions.
|
"""Useful helper functions.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
import stat
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from importlib.util import find_spec
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from milc import cli
|
from milc import cli
|
||||||
|
|
||||||
|
|
||||||
|
def AbsPath(arg): # noqa: N802
|
||||||
|
"""Resolve relative paths to the original working directory.
|
||||||
|
"""
|
||||||
|
arg = Path(arg)
|
||||||
|
if not arg.is_absolute():
|
||||||
|
return (Path(os.environ['ORIG_CWD']) / arg).absolute()
|
||||||
|
|
||||||
|
return arg
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
def is_qmk_firmware(qmk_firmware):
|
||||||
"""Returns True if the given Path() is a qmk_firmware clone.
|
"""Returns True if the given Path() is a qmk_firmware clone.
|
||||||
"""
|
"""
|
||||||
@@ -16,7 +43,7 @@ def is_qmk_firmware(qmk_firmware):
|
|||||||
qmk_firmware / 'quantum',
|
qmk_firmware / 'quantum',
|
||||||
qmk_firmware / 'requirements.txt',
|
qmk_firmware / 'requirements.txt',
|
||||||
qmk_firmware / 'requirements-dev.txt',
|
qmk_firmware / 'requirements-dev.txt',
|
||||||
qmk_firmware / 'lib/python/qmk/cli/__init__.py'
|
qmk_firmware / 'lib/python/qmk/cli/__init__.py',
|
||||||
]
|
]
|
||||||
|
|
||||||
for path in paths:
|
for path in paths:
|
||||||
@@ -26,7 +53,7 @@ def is_qmk_firmware(qmk_firmware):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=2)
|
@lru_cache(maxsize=1)
|
||||||
def find_qmk_firmware():
|
def find_qmk_firmware():
|
||||||
"""Look for qmk_firmware in the usual places.
|
"""Look for qmk_firmware in the usual places.
|
||||||
|
|
||||||
@@ -47,6 +74,7 @@ def find_qmk_firmware():
|
|||||||
return Path.home() / 'qmk_firmware'
|
return Path.home() / 'qmk_firmware'
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
def in_qmk_firmware():
|
def in_qmk_firmware():
|
||||||
"""Returns the path to the qmk_firmware we are currently in, or None if we are not inside qmk_firmware.
|
"""Returns the path to the qmk_firmware we are currently in, or None if we are not inside qmk_firmware.
|
||||||
"""
|
"""
|
||||||
@@ -58,3 +86,49 @@ def in_qmk_firmware():
|
|||||||
# Move up a directory before the next iteration
|
# Move up a directory before the next iteration
|
||||||
cur_dir = cur_dir / '..'
|
cur_dir = cur_dir / '..'
|
||||||
cur_dir = cur_dir.resolve()
|
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
|
||||||
|
cur_dir = cur_dir / '..'
|
||||||
|
cur_dir = cur_dir.resolve()
|
||||||
|
|||||||
+37
-18
@@ -4,6 +4,7 @@
|
|||||||
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.
|
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
|
import os
|
||||||
|
from pathlib import Path
|
||||||
import shlex
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
@@ -11,16 +12,38 @@ from platform import platform
|
|||||||
from traceback import print_exc
|
from traceback import print_exc
|
||||||
|
|
||||||
import milc
|
import milc
|
||||||
|
import platformdirs
|
||||||
|
|
||||||
from . import __version__
|
from . import __version__
|
||||||
from .helpers import find_qmk_firmware, is_qmk_firmware
|
from .helpers import find_qmk_firmware, is_qmk_firmware, find_qmk_userspace, is_qmk_userspace
|
||||||
|
|
||||||
milc.set_metadata(version=__version__)
|
|
||||||
|
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}'
|
milc.EMOJI_LOGLEVELS['INFO'] = '{fg_blue}Ψ{style_reset_all}'
|
||||||
|
|
||||||
# These must happen after the milc.set_metadata() call
|
# These must happen after the milc.milc_options() call
|
||||||
import milc.subcommand.config # noqa, must come after milc.set_metadata()
|
import milc.subcommand.config # noqa: E402, must come after milc.milc_options()
|
||||||
from milc.questions import yesno
|
|
||||||
|
|
||||||
|
|
||||||
@milc.cli.entrypoint('CLI wrapper for running QMK commands.')
|
@milc.cli.entrypoint('CLI wrapper for running QMK commands.')
|
||||||
@@ -46,25 +69,21 @@ def main():
|
|||||||
"""Setup the environment before dispatching to the entrypoint.
|
"""Setup the environment before dispatching to the entrypoint.
|
||||||
"""
|
"""
|
||||||
# Warn if they use an outdated python version
|
# Warn if they use an outdated python version
|
||||||
if sys.version_info < (3, 7):
|
if sys.version_info < (3, 9):
|
||||||
print('Warning: Your Python version is out of date! Some subcommands may not work!')
|
print('Warning: Your Python version is out of date! Some subcommands may not work!')
|
||||||
print('Please upgrade to Python 3.7 or later.')
|
print('Please upgrade to Python 3.9 or later.')
|
||||||
|
|
||||||
if 'windows' in platform().lower():
|
|
||||||
msystem = os.environ.get('MSYSTEM', '')
|
|
||||||
|
|
||||||
if 'mingw64' not in sys.executable or 'MINGW64' not in msystem:
|
|
||||||
print('ERROR: It seems you are not using the MINGW64 terminal.')
|
|
||||||
print('Please close this terminal and open a new MSYS2 MinGW 64-bit terminal.')
|
|
||||||
print('Python: %s, MSYSTEM: %s' % (sys.executable, msystem))
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
# Environment setup
|
# Environment setup
|
||||||
|
qmk_userspace = find_qmk_userspace()
|
||||||
qmk_firmware = find_qmk_firmware()
|
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['QMK_HOME'] = str(qmk_firmware)
|
||||||
os.environ['ORIG_CWD'] = os.getcwd()
|
os.environ['ORIG_CWD'] = os.getcwd()
|
||||||
|
|
||||||
import qmk_cli.subcommands
|
import qmk_cli.subcommands # noqa: F401
|
||||||
|
|
||||||
# Check out and initialize the qmk_firmware environment
|
# Check out and initialize the qmk_firmware environment
|
||||||
if is_qmk_firmware(qmk_firmware):
|
if is_qmk_firmware(qmk_firmware):
|
||||||
@@ -73,7 +92,7 @@ def main():
|
|||||||
sys.path.append(str(qmk_firmware / 'lib/python'))
|
sys.path.append(str(qmk_firmware / 'lib/python'))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import qmk.cli # noqa
|
import qmk.cli # noqa: F401
|
||||||
|
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
if qmk_firmware.name != 'qmk_firmware':
|
if qmk_firmware.name != 'qmk_firmware':
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
We list each subcommand here explicitly because all the reliable ways of searching for modules are slow and delay startup.
|
We list each subcommand here explicitly because all the reliable ways of searching for modules are slow and delay startup.
|
||||||
"""
|
"""
|
||||||
from . import clone # noqa
|
from . import clone # noqa: F401
|
||||||
from . import env # noqa
|
from . import console # noqa: F401
|
||||||
from . import setup # noqa
|
from . import env # noqa: F401
|
||||||
|
from . import setup # noqa: F401
|
||||||
|
|||||||
@@ -5,26 +5,24 @@ from pathlib import Path
|
|||||||
|
|
||||||
from milc import cli
|
from milc import cli
|
||||||
from qmk_cli.git import git_clone
|
from qmk_cli.git import git_clone
|
||||||
|
from qmk_cli.helpers import AbsPath
|
||||||
|
|
||||||
default_repo = 'qmk_firmware'
|
default_repo = 'qmk_firmware'
|
||||||
default_fork = 'qmk/' + default_repo
|
default_fork = 'qmk/' + default_repo
|
||||||
default_branch = 'master'
|
default_branch = 'master'
|
||||||
|
|
||||||
|
|
||||||
@cli.argument('--baseurl', default='https://github.com', help='The URL all git operations start from (Default: https://github.com)')
|
@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', default=default_branch, help='The branch to clone. Default: %s' % default_branch)
|
@cli.argument('-b', '--branch', arg_only=True, 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('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', default=default_fork, nargs='?', help='The qmk_firmware fork to clone. Default: %s' % default_fork)
|
@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.')
|
@cli.subcommand('Clone a qmk_firmware fork.')
|
||||||
def clone(cli):
|
def clone(cli):
|
||||||
if not cli.args.destination:
|
git_url = '/'.join((cli.args.baseurl, cli.args.fork))
|
||||||
cli.args.destination = os.path.join(os.environ['ORIG_CWD'], default_fork.split('/')[-1])
|
|
||||||
|
|
||||||
qmk_firmware = Path(cli.args.destination)
|
# Exists (but not an empty dir)
|
||||||
git_url = '/'.join((cli.config.clone.baseurl, cli.args.fork))
|
if cli.args.destination.exists() and any(cli.args.destination.iterdir()):
|
||||||
|
|
||||||
if qmk_firmware.exists():
|
|
||||||
cli.log.error('Destination already exists: %s', cli.args.destination)
|
cli.log.error('Destination already exists: %s', cli.args.destination)
|
||||||
exit(1)
|
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()
|
||||||
@@ -1,19 +1,24 @@
|
|||||||
"""Prints environment information.
|
"""Prints environment information.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from milc import cli
|
from milc import cli
|
||||||
from qmk_cli.helpers import is_qmk_firmware
|
from qmk_cli.helpers import is_qmk_firmware, is_qmk_userspace
|
||||||
|
|
||||||
|
|
||||||
@cli.argument('var', default=None, nargs='?', help='Optional variable to query')
|
@cli.argument('var', arg_only=True, default=None, nargs='?', help='Optional variable to query')
|
||||||
@cli.subcommand('Prints environment information.')
|
@cli.subcommand('Prints environment information.')
|
||||||
def env(cli):
|
def env(cli):
|
||||||
home = os.environ.get('QMK_HOME', "")
|
home = os.environ.get('QMK_HOME', "")
|
||||||
|
userspace = os.environ.get('QMK_USERSPACE', "")
|
||||||
data = {
|
data = {
|
||||||
'QMK_HOME': home,
|
'QMK_HOME': home,
|
||||||
'QMK_FIRMWARE': home if is_qmk_firmware(Path(home)) else ""
|
'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
|
# Now munge the current cli config
|
||||||
@@ -22,6 +27,10 @@ def env(cli):
|
|||||||
data[converted_key] = val
|
data[converted_key] = val
|
||||||
|
|
||||||
if cli.args.var:
|
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
|
# dump out requested arg
|
||||||
print(data[cli.args.var])
|
print(data[cli.args.var])
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -2,55 +2,28 @@
|
|||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
import shlex
|
import shlex
|
||||||
import subprocess
|
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from milc import cli
|
from milc import cli
|
||||||
from milc.questions import yesno
|
from milc.questions import choice, question, yesno
|
||||||
from qmk_cli.git import git_clone
|
from qmk_cli.git import git_clone_fork
|
||||||
from qmk_cli.helpers import is_qmk_firmware
|
from qmk_cli.helpers import AbsPath, is_qmk_firmware
|
||||||
|
|
||||||
default_base = 'https://github.com'
|
DEFAULT_BASE = 'https://github.com'
|
||||||
default_repo = 'qmk_firmware'
|
DEFAULT_REPO = 'qmk_firmware'
|
||||||
default_fork = 'qmk/' + default_repo
|
DEFAULT_FORK = 'qmk/' + DEFAULT_REPO
|
||||||
default_branch = 'master'
|
DEFAULT_BRANCH = 'master'
|
||||||
|
|
||||||
|
|
||||||
def git_upstream(destination):
|
|
||||||
"""Add the qmk/qmk_firmware upstream to a qmk_firmware clone.
|
|
||||||
"""
|
|
||||||
git_url = '/'.join((cli.config.setup.baseurl, default_fork))
|
|
||||||
git_cmd = [
|
|
||||||
'git',
|
|
||||||
'-C',
|
|
||||||
destination,
|
|
||||||
'remote',
|
|
||||||
'add',
|
|
||||||
'upstream',
|
|
||||||
git_url,
|
|
||||||
]
|
|
||||||
|
|
||||||
with subprocess.Popen(git_cmd, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, bufsize=1, universal_newlines=True, encoding='utf-8') as p:
|
|
||||||
for line in p.stdout:
|
|
||||||
print(line, end='')
|
|
||||||
|
|
||||||
if p.returncode == 0:
|
|
||||||
cli.log.info('Added %s as remote upstream.', git_url)
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
cli.log.error('%s exited %d', ' '.join(git_cmd), p.returncode)
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
@cli.argument('-n', '--no', arg_only=True, action='store_true', help='Answer no to all questions')
|
@cli.argument('-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('-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('--baseurl', arg_only=True, 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('-b', '--branch', arg_only=True, 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('-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', default=default_fork, nargs='?', help='The qmk_firmware fork to clone. Default: %s' % default_fork)
|
@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.')
|
@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.
|
"""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)))
|
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)))
|
||||||
@@ -59,30 +32,54 @@ def setup(cli):
|
|||||||
# Sanity checks
|
# Sanity checks
|
||||||
if cli.args.yes and cli.args.no:
|
if cli.args.yes and cli.args.no:
|
||||||
cli.log.error("Can't use both --yes and --no at the same time.")
|
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.
|
# 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):
|
if is_qmk_firmware(cli.args.home):
|
||||||
cli.log.info('Found qmk_firmware at %s.', str(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?"
|
||||||
|
|
||||||
elif cli.args.home.exists():
|
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)
|
path_str = str(cli.args.home)
|
||||||
|
|
||||||
if cli.args.home.name != 'qmk_firmware':
|
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.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)
|
cli.log.error("Path '%s' exists but is not a qmk_firmware clone!", path_str)
|
||||||
exit(1)
|
return False
|
||||||
|
|
||||||
else:
|
else:
|
||||||
cli.log.error('Could not find qmk_firmware!')
|
cli.log.error('Could not find qmk_firmware!')
|
||||||
if yesno(clone_prompt):
|
if yesno(clone_prompt):
|
||||||
git_url = '/'.join((cli.config.setup.baseurl, cli.args.fork))
|
if not git_clone_fork(cli.args.home, cli.args.baseurl, cli.args.fork, cli.args.branch):
|
||||||
|
return False
|
||||||
if git_clone(git_url, cli.args.home, cli.config.setup.branch):
|
|
||||||
git_upstream(cli.args.home)
|
|
||||||
else:
|
|
||||||
exit(1)
|
|
||||||
else:
|
else:
|
||||||
cli.log.warning('Not cloning qmk_firmware due to user input or --no flag.')
|
cli.log.warning('Not cloning qmk_firmware due to user input or --no flag.')
|
||||||
|
|
||||||
@@ -104,4 +101,4 @@ def setup(cli):
|
|||||||
if cli.args.yes:
|
if cli.args.yes:
|
||||||
doctor_command.append('-y')
|
doctor_command.append('-y')
|
||||||
|
|
||||||
cli.run(doctor_command, stdin=None, capture_output=False)
|
cli.run(doctor_command, stdin=None, capture_output=False, cwd=cli.args.home)
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Increment the verison number and release a new version of qmk_cli.
|
|
||||||
#
|
|
||||||
# Required packages: pip3 install bumpversion twine
|
|
||||||
|
|
||||||
echo "Use the github action instead!"
|
|
||||||
exit 1
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
bleach==3.3.1
|
|
||||||
bump2version==1.0.1
|
|
||||||
bumpversion==0.6.0
|
|
||||||
certifi==2021.5.30
|
|
||||||
cffi==1.14.6
|
|
||||||
chardet==4.0.0
|
|
||||||
colorama==0.4.4
|
|
||||||
cryptography==3.4.7
|
|
||||||
docutils==0.17.1
|
|
||||||
idna==2.10
|
|
||||||
importlib-metadata==4.6.1
|
|
||||||
jeepney==0.7.0
|
|
||||||
keyring==23.0.1
|
|
||||||
packaging==21.0
|
|
||||||
pkginfo==1.7.1
|
|
||||||
pycparser==2.20
|
|
||||||
Pygments==2.9.0
|
|
||||||
pyparsing==2.4.7
|
|
||||||
readme-renderer==29.0
|
|
||||||
requests==2.26.0
|
|
||||||
requests-toolbelt==0.9.1
|
|
||||||
rfc3986==1.5.0
|
|
||||||
SecretStorage==3.3.1
|
|
||||||
six==1.16.0
|
|
||||||
tqdm==4.61.2
|
|
||||||
twine==3.4.1
|
|
||||||
urllib3==1.26.6
|
|
||||||
webencodings==0.5.1
|
|
||||||
zipp==3.5.0
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
[bumpversion]
|
|
||||||
current_version = 0.2.2
|
|
||||||
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.7
|
|
||||||
|
|
||||||
[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,69 +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' % i for i 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=['qmk_cli'],
|
|
||||||
classifiers=[
|
|
||||||
'Development Status :: 3 - Alpha',
|
|
||||||
'Environment :: Console',
|
|
||||||
'Intended Audience :: Developers',
|
|
||||||
'Intended Audience :: System Administrators',
|
|
||||||
'Intended Audience :: End Users/Desktop',
|
|
||||||
'License :: OSI Approved :: MIT License',
|
|
||||||
'Natural Language :: English',
|
|
||||||
'Programming Language :: Python :: 3 :: Only',
|
|
||||||
'Topic :: Scientific/Engineering',
|
|
||||||
'Topic :: Software Development',
|
|
||||||
'Topic :: Utilities',
|
|
||||||
],
|
|
||||||
python_requires=metadata['requires-python'],
|
|
||||||
install_requires=[
|
|
||||||
"appdirs==1.4.4",
|
|
||||||
"argcomplete==1.12.3",
|
|
||||||
"attrs==21.2.0",
|
|
||||||
"colorama==0.4.4",
|
|
||||||
"coverage==5.5",
|
|
||||||
"qmk-dotty-dict==1.3.0.post1",
|
|
||||||
"flake8==3.9.2",
|
|
||||||
"halo==0.0.31",
|
|
||||||
"hid==1.0.4",
|
|
||||||
"hjson==3.0.2",
|
|
||||||
"jsonschema==3.2.0",
|
|
||||||
"log-symbols==0.0.14",
|
|
||||||
"mccabe==0.6.1",
|
|
||||||
"milc==1.4.2",
|
|
||||||
"nose2==0.10.0",
|
|
||||||
"pycodestyle==2.7.0",
|
|
||||||
"pyflakes==2.3.1",
|
|
||||||
"Pygments==2.9.0",
|
|
||||||
"pyrsistent==0.18.0",
|
|
||||||
"pyusb==1.2.1",
|
|
||||||
"setuptools>=45",
|
|
||||||
"six==1.16.0",
|
|
||||||
"spinners==0.0.24",
|
|
||||||
"termcolor==1.1.0",
|
|
||||||
"yapf==0.31.0"
|
|
||||||
]
|
|
||||||
)
|
|
||||||
@@ -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