aboutsummaryrefslogtreecommitdiff
path: root/lib/python
diff options
context:
space:
mode:
Diffstat (limited to 'lib/python')
-rw-r--r--lib/python/qmk/cli/__init__.py4
-rwxr-xr-x[-rw-r--r--]lib/python/qmk/cli/cformat.py139
-rwxr-xr-xlib/python/qmk/cli/doctor/__init__.py5
-rw-r--r--lib/python/qmk/cli/doctor/check.py (renamed from lib/python/qmk/os_helpers/__init__.py)22
-rw-r--r--lib/python/qmk/cli/doctor/linux.py (renamed from lib/python/qmk/os_helpers/linux/__init__.py)40
-rw-r--r--lib/python/qmk/cli/doctor/macos.py13
-rwxr-xr-xlib/python/qmk/cli/doctor/main.py (renamed from lib/python/qmk/cli/doctor.py)76
-rw-r--r--lib/python/qmk/cli/doctor/windows.py14
-rwxr-xr-x[-rw-r--r--]lib/python/qmk/cli/fileformat.py24
-rw-r--r--lib/python/qmk/cli/format/c.py137
-rwxr-xr-xlib/python/qmk/cli/format/json.py5
-rwxr-xr-xlib/python/qmk/cli/format/python.py26
-rw-r--r--lib/python/qmk/cli/format/text.py27
-rw-r--r--lib/python/qmk/cli/generate/version_h.py28
-rwxr-xr-xlib/python/qmk/cli/info.py2
-rwxr-xr-xlib/python/qmk/cli/kle2json.py2
-rw-r--r--lib/python/qmk/cli/new/keyboard.py141
-rwxr-xr-xlib/python/qmk/cli/pyformat.py32
-rw-r--r--lib/python/qmk/commands.py142
-rw-r--r--lib/python/qmk/constants.py5
-rw-r--r--lib/python/qmk/info.py57
-rwxr-xr-xlib/python/qmk/json_encoders.py3
-rw-r--r--lib/python/qmk/json_schema.py34
-rw-r--r--lib/python/qmk/tests/minimal_info.json2
-rw-r--r--lib/python/qmk/tests/test_cli_commands.py22
25 files changed, 726 insertions, 276 deletions
diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py
index de71a5d1e..b22f1c0d2 100644
--- a/lib/python/qmk/cli/__init__.py
+++ b/lib/python/qmk/cli/__init__.py
@@ -40,7 +40,10 @@ subcommands = [
40 'qmk.cli.doctor', 40 'qmk.cli.doctor',
41 'qmk.cli.fileformat', 41 'qmk.cli.fileformat',
42 'qmk.cli.flash', 42 'qmk.cli.flash',
43 'qmk.cli.format.c',
43 'qmk.cli.format.json', 44 'qmk.cli.format.json',
45 'qmk.cli.format.python',
46 'qmk.cli.format.text',
44 'qmk.cli.generate.api', 47 'qmk.cli.generate.api',
45 'qmk.cli.generate.config_h', 48 'qmk.cli.generate.config_h',
46 'qmk.cli.generate.dfu_header', 49 'qmk.cli.generate.dfu_header',
@@ -50,6 +53,7 @@ subcommands = [
50 'qmk.cli.generate.layouts', 53 'qmk.cli.generate.layouts',
51 'qmk.cli.generate.rgb_breathe_table', 54 'qmk.cli.generate.rgb_breathe_table',
52 'qmk.cli.generate.rules_mk', 55 'qmk.cli.generate.rules_mk',
56 'qmk.cli.generate.version_h',
53 'qmk.cli.hello', 57 'qmk.cli.hello',
54 'qmk.cli.info', 58 'qmk.cli.info',
55 'qmk.cli.json2c', 59 'qmk.cli.json2c',
diff --git a/lib/python/qmk/cli/cformat.py b/lib/python/qmk/cli/cformat.py
index efeb45967..9d0ecaeba 100644..100755
--- a/lib/python/qmk/cli/cformat.py
+++ b/lib/python/qmk/cli/cformat.py
@@ -1,137 +1,28 @@
1"""Format C code according to QMK's style. 1"""Point people to the new command name.
2""" 2"""
3from os import path 3import sys
4from shutil import which 4from pathlib import Path
5from subprocess import CalledProcessError, DEVNULL, Popen, PIPE
6 5
7from argcomplete.completers import FilesCompleter
8from milc import cli 6from milc import cli
9 7
10from qmk.path import normpath
11from qmk.c_parse import c_source_files
12
13c_file_suffixes = ('c', 'h', 'cpp')
14core_dirs = ('drivers', 'quantum', 'tests', 'tmk_core', 'platforms')
15ignored = ('tmk_core/protocol/usb_hid', 'quantum/template', 'platforms/chibios')
16
17
18def find_clang_format():
19 """Returns the path to clang-format.
20 """
21 for clang_version in range(20, 6, -1):
22 binary = f'clang-format-{clang_version}'
23
24 if which(binary):
25 return binary
26
27 return 'clang-format'
28
29
30def find_diffs(files):
31 """Run clang-format and diff it against a file.
32 """
33 found_diffs = False
34
35 for file in files:
36 cli.log.debug('Checking for changes in %s', file)
37 clang_format = Popen([find_clang_format(), file], stdout=PIPE, stderr=PIPE, universal_newlines=True)
38 diff = cli.run(['diff', '-u', f'--label=a/{file}', f'--label=b/{file}', str(file), '-'], stdin=clang_format.stdout, capture_output=True)
39
40 if diff.returncode != 0:
41 print(diff.stdout)
42 found_diffs = True
43
44 return found_diffs
45
46
47def cformat_run(files):
48 """Spawn clang-format subprocess with proper arguments
49 """
50 # Determine which version of clang-format to use
51 clang_format = [find_clang_format(), '-i']
52
53 try:
54 cli.run([*clang_format, *map(str, files)], check=True, capture_output=False, stdin=DEVNULL)
55 cli.log.info('Successfully formatted the C code.')
56 return True
57
58 except CalledProcessError as e:
59 cli.log.error('Error formatting C code!')
60 cli.log.debug('%s exited with returncode %s', e.cmd, e.returncode)
61 cli.log.debug('STDOUT:')
62 cli.log.debug(e.stdout)
63 cli.log.debug('STDERR:')
64 cli.log.debug(e.stderr)
65 return False
66
67
68def filter_files(files, core_only=False):
69 """Yield only files to be formatted and skip the rest
70 """
71 if core_only:
72 # Filter non-core files
73 for index, file in enumerate(files):
74 # The following statement checks each file to see if the file path is
75 # - in the core directories
76 # - not in the ignored directories
77 if not any(i in str(file) for i in core_dirs) or any(i in str(file) for i in ignored):
78 files[index] = None
79 cli.log.debug("Skipping non-core file %s, as '--core-only' is used.", file)
80
81 for file in files:
82 if file and file.name.split('.')[-1] in c_file_suffixes:
83 yield file
84 else:
85 cli.log.debug('Skipping file %s', file)
86
87 8
88@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Flag only, don't automatically format.") 9@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Flag only, don't automatically format.")
89@cli.argument('-b', '--base-branch', default='origin/master', help='Branch to compare to diffs to.') 10@cli.argument('-b', '--base-branch', default='origin/master', help='Branch to compare to diffs to.')
90@cli.argument('-a', '--all-files', arg_only=True, action='store_true', help='Format all core files.') 11@cli.argument('-a', '--all-files', arg_only=True, action='store_true', help='Format all core files.')
91@cli.argument('--core-only', arg_only=True, action='store_true', help='Format core files only.') 12@cli.argument('--core-only', arg_only=True, action='store_true', help='Format core files only.')
92@cli.argument('files', nargs='*', arg_only=True, type=normpath, completer=FilesCompleter('.c'), help='Filename(s) to format.') 13@cli.argument('files', nargs='*', arg_only=True, help='Filename(s) to format.')
93@cli.subcommand("Format C code according to QMK's style.", hidden=False if cli.config.user.developer else True) 14@cli.subcommand('Pointer to the new command name: qmk format-c.', hidden=True)
94def cformat(cli): 15def cformat(cli):
95 """Format C code according to QMK's style. 16 """Pointer to the new command name: qmk format-c.
96 """ 17 """
97 # Find the list of files to format 18 cli.log.warning('"qmk cformat" has been renamed to "qmk format-c". Please use the new command in the future.')
98 if cli.args.files: 19 argv = [sys.executable, *sys.argv]
99 files = list(filter_files(cli.args.files, cli.args.core_only)) 20 argv[argv.index('cformat')] = 'format-c'
100 21 script_path = Path(argv[1])
101 if not files: 22 script_path_exe = Path(f'{argv[1]}.exe')
102 cli.log.error('No C files in filelist: %s', ', '.join(map(str, cli.args.files)))
103 exit(0)
104
105 if cli.args.all_files:
106 cli.log.warning('Filenames passed with -a, only formatting: %s', ','.join(map(str, files)))
107
108 elif cli.args.all_files:
109 all_files = c_source_files(core_dirs)
110 files = list(filter_files(all_files, True))
111
112 else:
113 git_diff_cmd = ['git', 'diff', '--name-only', cli.args.base_branch, *core_dirs]
114 git_diff = cli.run(git_diff_cmd, stdin=DEVNULL)
115
116 if git_diff.returncode != 0:
117 cli.log.error("Error running %s", git_diff_cmd)
118 print(git_diff.stderr)
119 return git_diff.returncode
120
121 files = []
122
123 for file in git_diff.stdout.strip().split('\n'):
124 if not any([file.startswith(ignore) for ignore in ignored]):
125 if path.exists(file) and file.split('.')[-1] in c_file_suffixes:
126 files.append(file)
127 23
128 # Sanity check 24 if not script_path.exists() and script_path_exe.exists():
129 if not files: 25 # For reasons I don't understand ".exe" is stripped from the script name on windows.
130 cli.log.error('No changed files detected. Use "qmk cformat -a" to format all core files') 26 argv[1] = str(script_path_exe)
131 return False
132 27
133 # Run clang-format on the files we've found 28 return cli.run(argv, capture_output=False).returncode
134 if cli.args.dry_run:
135 return not find_diffs(files)
136 else:
137 return cformat_run(files)
diff --git a/lib/python/qmk/cli/doctor/__init__.py b/lib/python/qmk/cli/doctor/__init__.py
new file mode 100755
index 000000000..272e04202
--- /dev/null
+++ b/lib/python/qmk/cli/doctor/__init__.py
@@ -0,0 +1,5 @@
1"""QMK Doctor
2
3Check out the user's QMK environment and make sure it's ready to compile.
4"""
5from .main import doctor
diff --git a/lib/python/qmk/os_helpers/__init__.py b/lib/python/qmk/cli/doctor/check.py
index 3e98db3c3..0807f4151 100644
--- a/lib/python/qmk/os_helpers/__init__.py
+++ b/lib/python/qmk/cli/doctor/check.py
@@ -1,4 +1,4 @@
1"""OS-agnostic helper functions 1"""Check for specific programs.
2""" 2"""
3from enum import Enum 3from enum import Enum
4import re 4import re
@@ -30,7 +30,7 @@ ESSENTIAL_BINARIES = {
30} 30}
31 31
32 32
33def parse_gcc_version(version): 33def _parse_gcc_version(version):
34 m = re.match(r"(\d+)(?:\.(\d+))?(?:\.(\d+))?", version) 34 m = re.match(r"(\d+)(?:\.(\d+))?(?:\.(\d+))?", version)
35 35
36 return { 36 return {
@@ -40,7 +40,7 @@ def parse_gcc_version(version):
40 } 40 }
41 41
42 42
43def check_arm_gcc_version(): 43def _check_arm_gcc_version():
44 """Returns True if the arm-none-eabi-gcc version is not known to cause problems. 44 """Returns True if the arm-none-eabi-gcc version is not known to cause problems.
45 """ 45 """
46 if 'output' in ESSENTIAL_BINARIES['arm-none-eabi-gcc']: 46 if 'output' in ESSENTIAL_BINARIES['arm-none-eabi-gcc']:
@@ -50,7 +50,7 @@ def check_arm_gcc_version():
50 return CheckStatus.OK # Right now all known arm versions are ok 50 return CheckStatus.OK # Right now all known arm versions are ok
51 51
52 52
53def check_avr_gcc_version(): 53def _check_avr_gcc_version():
54 """Returns True if the avr-gcc version is not known to cause problems. 54 """Returns True if the avr-gcc version is not known to cause problems.
55 """ 55 """
56 rc = CheckStatus.ERROR 56 rc = CheckStatus.ERROR
@@ -60,7 +60,7 @@ def check_avr_gcc_version():
60 cli.log.info('Found avr-gcc version %s', version_number) 60 cli.log.info('Found avr-gcc version %s', version_number)
61 rc = CheckStatus.OK 61 rc = CheckStatus.OK
62 62
63 parsed_version = parse_gcc_version(version_number) 63 parsed_version = _parse_gcc_version(version_number)
64 if parsed_version['major'] > 8: 64 if parsed_version['major'] > 8:
65 cli.log.warning('{fg_yellow}We do not recommend avr-gcc newer than 8. Downgrading to 8.x is recommended.') 65 cli.log.warning('{fg_yellow}We do not recommend avr-gcc newer than 8. Downgrading to 8.x is recommended.')
66 rc = CheckStatus.WARNING 66 rc = CheckStatus.WARNING
@@ -68,7 +68,7 @@ def check_avr_gcc_version():
68 return rc 68 return rc
69 69
70 70
71def check_avrdude_version(): 71def _check_avrdude_version():
72 if 'output' in ESSENTIAL_BINARIES['avrdude']: 72 if 'output' in ESSENTIAL_BINARIES['avrdude']:
73 last_line = ESSENTIAL_BINARIES['avrdude']['output'].split('\n')[-2] 73 last_line = ESSENTIAL_BINARIES['avrdude']['output'].split('\n')[-2]
74 version_number = last_line.split()[2][:-1] 74 version_number = last_line.split()[2][:-1]
@@ -77,7 +77,7 @@ def check_avrdude_version():
77 return CheckStatus.OK 77 return CheckStatus.OK
78 78
79 79
80def check_dfu_util_version(): 80def _check_dfu_util_version():
81 if 'output' in ESSENTIAL_BINARIES['dfu-util']: 81 if 'output' in ESSENTIAL_BINARIES['dfu-util']:
82 first_line = ESSENTIAL_BINARIES['dfu-util']['output'].split('\n')[0] 82 first_line = ESSENTIAL_BINARIES['dfu-util']['output'].split('\n')[0]
83 version_number = first_line.split()[1] 83 version_number = first_line.split()[1]
@@ -86,7 +86,7 @@ def check_dfu_util_version():
86 return CheckStatus.OK 86 return CheckStatus.OK
87 87
88 88
89def check_dfu_programmer_version(): 89def _check_dfu_programmer_version():
90 if 'output' in ESSENTIAL_BINARIES['dfu-programmer']: 90 if 'output' in ESSENTIAL_BINARIES['dfu-programmer']:
91 first_line = ESSENTIAL_BINARIES['dfu-programmer']['output'].split('\n')[0] 91 first_line = ESSENTIAL_BINARIES['dfu-programmer']['output'].split('\n')[0]
92 version_number = first_line.split()[1] 92 version_number = first_line.split()[1]
@@ -111,7 +111,7 @@ def check_binary_versions():
111 """Check the versions of ESSENTIAL_BINARIES 111 """Check the versions of ESSENTIAL_BINARIES
112 """ 112 """
113 versions = [] 113 versions = []
114 for check in (check_arm_gcc_version, check_avr_gcc_version, check_avrdude_version, check_dfu_util_version, check_dfu_programmer_version): 114 for check in (_check_arm_gcc_version, _check_avr_gcc_version, _check_avrdude_version, _check_dfu_util_version, _check_dfu_programmer_version):
115 versions.append(check()) 115 versions.append(check())
116 return versions 116 return versions
117 117
@@ -159,6 +159,6 @@ def check_git_repo():
159 This is a decent enough indicator that the qmk_firmware directory is a 159 This is a decent enough indicator that the qmk_firmware directory is a
160 proper Git repository, rather than a .zip download from GitHub. 160 proper Git repository, rather than a .zip download from GitHub.
161 """ 161 """
162 dot_git_dir = QMK_FIRMWARE / '.git' 162 dot_git = QMK_FIRMWARE / '.git'
163 163
164 return CheckStatus.OK if dot_git_dir.is_dir() else CheckStatus.WARNING 164 return CheckStatus.OK if dot_git.exists() else CheckStatus.WARNING
diff --git a/lib/python/qmk/os_helpers/linux/__init__.py b/lib/python/qmk/cli/doctor/linux.py
index 008654ab0..8ea04cd69 100644
--- a/lib/python/qmk/os_helpers/linux/__init__.py
+++ b/lib/python/qmk/cli/doctor/linux.py
@@ -1,11 +1,13 @@
1"""OS-specific functions for: Linux 1"""OS-specific functions for: Linux
2""" 2"""
3from pathlib import Path 3import platform
4import shutil 4import shutil
5from pathlib import Path
5 6
6from milc import cli 7from milc import cli
8
7from qmk.constants import QMK_FIRMWARE 9from qmk.constants import QMK_FIRMWARE
8from qmk.os_helpers import CheckStatus 10from .check import CheckStatus
9 11
10 12
11def _udev_rule(vid, pid=None, *args): 13def _udev_rule(vid, pid=None, *args):
@@ -39,7 +41,12 @@ def check_udev_rules():
39 """Make sure the udev rules look good. 41 """Make sure the udev rules look good.
40 """ 42 """
41 rc = CheckStatus.OK 43 rc = CheckStatus.OK
42 udev_dir = Path("/etc/udev/rules.d/") 44 udev_dirs = [
45 Path("/usr/lib/udev/rules.d/"),
46 Path("/usr/local/lib/udev/rules.d/"),
47 Path("/run/udev/rules.d/"),
48 Path("/etc/udev/rules.d/"),
49 ]
43 desired_rules = { 50 desired_rules = {
44 'atmel-dfu': { 51 'atmel-dfu': {
45 _udev_rule("03eb", "2fef"), # ATmega16U2 52 _udev_rule("03eb", "2fef"), # ATmega16U2
@@ -88,8 +95,8 @@ def check_udev_rules():
88 'tmk': {_deprecated_udev_rule("feed")} 95 'tmk': {_deprecated_udev_rule("feed")}
89 } 96 }
90 97
91 if udev_dir.exists(): 98 if any(udev_dir.exists() for udev_dir in udev_dirs):
92 udev_rules = [rule_file for rule_file in udev_dir.glob('*.rules')] 99 udev_rules = [rule_file for udev_dir in udev_dirs for rule_file in udev_dir.glob('*.rules')]
93 current_rules = set() 100 current_rules = set()
94 101
95 # Collect all rules from the config files 102 # Collect all rules from the config files
@@ -115,7 +122,8 @@ def check_udev_rules():
115 cli.log.warning("{fg_yellow}Missing or outdated udev rules for '%s' boards. Run 'sudo cp %s/util/udev/50-qmk.rules /etc/udev/rules.d/'.", bootloader, QMK_FIRMWARE) 122 cli.log.warning("{fg_yellow}Missing or outdated udev rules for '%s' boards. Run 'sudo cp %s/util/udev/50-qmk.rules /etc/udev/rules.d/'.", bootloader, QMK_FIRMWARE)
116 123
117 else: 124 else:
118 cli.log.warning("{fg_yellow}'%s' does not exist. Skipping udev rule checking...", udev_dir) 125 cli.log.warning("{fg_yellow}Can't find udev rules, skipping udev rule checking...")
126 cli.log.debug("Checked directories: %s", ', '.join(str(udev_dir) for udev_dir in udev_dirs))
119 127
120 return rc 128 return rc
121 129
@@ -138,3 +146,23 @@ def check_modem_manager():
138 """(TODO): Add check for non-systemd systems 146 """(TODO): Add check for non-systemd systems
139 """ 147 """
140 return False 148 return False
149
150
151def os_test_linux():
152 """Run the Linux specific tests.
153 """
154 # Don't bother with udev on WSL, for now
155 if 'microsoft' in platform.uname().release.lower():
156 cli.log.info("Detected {fg_cyan}Linux (WSL){fg_reset}.")
157
158 # https://github.com/microsoft/WSL/issues/4197
159 if QMK_FIRMWARE.as_posix().startswith("/mnt"):
160 cli.log.warning("I/O performance on /mnt may be extremely slow.")
161 return CheckStatus.WARNING
162
163 return CheckStatus.OK
164 else:
165 cli.log.info("Detected {fg_cyan}Linux{fg_reset}.")
166 from .linux import check_udev_rules
167
168 return check_udev_rules()
diff --git a/lib/python/qmk/cli/doctor/macos.py b/lib/python/qmk/cli/doctor/macos.py
new file mode 100644
index 000000000..00fb27285
--- /dev/null
+++ b/lib/python/qmk/cli/doctor/macos.py
@@ -0,0 +1,13 @@
1import platform
2
3from milc import cli
4
5from .check import CheckStatus
6
7
8def os_test_macos():
9 """Run the Mac specific tests.
10 """
11 cli.log.info("Detected {fg_cyan}macOS %s{fg_reset}.", platform.mac_ver()[0])
12
13 return CheckStatus.OK
diff --git a/lib/python/qmk/cli/doctor.py b/lib/python/qmk/cli/doctor/main.py
index 327bc9cb3..6a31ccdfd 100755
--- a/lib/python/qmk/cli/doctor.py
+++ b/lib/python/qmk/cli/doctor/main.py
@@ -7,9 +7,11 @@ from subprocess import DEVNULL
7 7
8from milc import cli 8from milc import cli
9from milc.questions import yesno 9from milc.questions import yesno
10
10from qmk import submodules 11from qmk import submodules
11from qmk.constants import QMK_FIRMWARE 12from qmk.constants import QMK_FIRMWARE, QMK_FIRMWARE_UPSTREAM
12from qmk.os_helpers import CheckStatus, check_binaries, check_binary_versions, check_submodules, check_git_repo 13from .check import CheckStatus, check_binaries, check_binary_versions, check_submodules
14from qmk.commands import git_check_repo, git_get_branch, git_is_dirty, git_get_remotes, git_check_deviation, in_virtualenv
13 15
14 16
15def os_tests(): 17def os_tests():
@@ -18,51 +20,48 @@ def os_tests():
18 platform_id = platform.platform().lower() 20 platform_id = platform.platform().lower()
19 21
20 if 'darwin' in platform_id or 'macos' in platform_id: 22 if 'darwin' in platform_id or 'macos' in platform_id:
23 from .macos import os_test_macos
21 return os_test_macos() 24 return os_test_macos()
22 elif 'linux' in platform_id: 25 elif 'linux' in platform_id:
26 from .linux import os_test_linux
23 return os_test_linux() 27 return os_test_linux()
24 elif 'windows' in platform_id: 28 elif 'windows' in platform_id:
29 from .windows import os_test_windows
25 return os_test_windows() 30 return os_test_windows()
26 else: 31 else:
27 cli.log.warning('Unsupported OS detected: %s', platform_id) 32 cli.log.warning('Unsupported OS detected: %s', platform_id)
28 return CheckStatus.WARNING 33 return CheckStatus.WARNING
29 34
30 35
31def os_test_linux(): 36def git_tests():
32 """Run the Linux specific tests. 37 """Run Git-related checks
33 """ 38 """
34 # Don't bother with udev on WSL, for now 39 status = CheckStatus.OK
35 if 'microsoft' in platform.uname().release.lower():
36 cli.log.info("Detected {fg_cyan}Linux (WSL){fg_reset}.")
37
38 # https://github.com/microsoft/WSL/issues/4197
39 if QMK_FIRMWARE.as_posix().startswith("/mnt"):
40 cli.log.warning("I/O performance on /mnt may be extremely slow.")
41 return CheckStatus.WARNING
42 40
43 return CheckStatus.OK 41 # Make sure our QMK home is a Git repo
42 git_ok = git_check_repo()
43 if not git_ok:
44 cli.log.warning("{fg_yellow}QMK home does not appear to be a Git repository! (no .git folder)")
45 status = CheckStatus.WARNING
44 else: 46 else:
45 cli.log.info("Detected {fg_cyan}Linux{fg_reset}.") 47 git_branch = git_get_branch()
46 from qmk.os_helpers.linux import check_udev_rules 48 if git_branch:
47 49 cli.log.info('Git branch: %s', git_branch)
48 return check_udev_rules() 50 git_dirty = git_is_dirty()
49 51 if git_dirty:
50 52 cli.log.warning('{fg_yellow}Git has unstashed/uncommitted changes.')
51def os_test_macos(): 53 status = CheckStatus.WARNING
52 """Run the Mac specific tests. 54 git_remotes = git_get_remotes()
53 """ 55 if 'upstream' not in git_remotes.keys() or QMK_FIRMWARE_UPSTREAM not in git_remotes['upstream'].get('url', ''):
54 cli.log.info("Detected {fg_cyan}macOS %s{fg_reset}.", platform.mac_ver()[0]) 56 cli.log.warning('{fg_yellow}The official repository does not seem to be configured as git remote "upstream".')
55 57 status = CheckStatus.WARNING
56 return CheckStatus.OK 58 else:
57 59 git_deviation = git_check_deviation(git_branch)
58 60 if git_branch in ['master', 'develop'] and git_deviation:
59def os_test_windows(): 61 cli.log.warning('{fg_yellow}The local "%s" branch contains commits not found in the upstream branch.', git_branch)
60 """Run the Windows specific tests. 62 status = CheckStatus.WARNING
61 """ 63
62 win32_ver = platform.win32_ver() 64 return status
63 cli.log.info("Detected {fg_cyan}Windows %s (%s){fg_reset}.", win32_ver[0], win32_ver[1])
64
65 return CheckStatus.OK
66 65
67 66
68@cli.argument('-y', '--yes', action='store_true', arg_only=True, help='Answer yes to all questions.') 67@cli.argument('-y', '--yes', action='store_true', arg_only=True, help='Answer yes to all questions.')
@@ -82,12 +81,11 @@ def doctor(cli):
82 81
83 status = os_tests() 82 status = os_tests()
84 83
85 # Make sure our QMK home is a Git repo 84 status = git_tests()
86 git_ok = check_git_repo()
87 85
88 if git_ok == CheckStatus.WARNING: 86 venv = in_virtualenv()
89 cli.log.warning("QMK home does not appear to be a Git repository! (no .git folder)") 87 if venv:
90 status = CheckStatus.WARNING 88 cli.log.info('CLI installed in virtualenv.')
91 89
92 # Make sure the basic CLI tools we need are available and can be executed. 90 # Make sure the basic CLI tools we need are available and can be executed.
93 bin_ok = check_binaries() 91 bin_ok = check_binaries()
diff --git a/lib/python/qmk/cli/doctor/windows.py b/lib/python/qmk/cli/doctor/windows.py
new file mode 100644
index 000000000..381ab36fd
--- /dev/null
+++ b/lib/python/qmk/cli/doctor/windows.py
@@ -0,0 +1,14 @@
1import platform
2
3from milc import cli
4
5from .check import CheckStatus
6
7
8def os_test_windows():
9 """Run the Windows specific tests.
10 """
11 win32_ver = platform.win32_ver()
12 cli.log.info("Detected {fg_cyan}Windows %s (%s){fg_reset}.", win32_ver[0], win32_ver[1])
13
14 return CheckStatus.OK
diff --git a/lib/python/qmk/cli/fileformat.py b/lib/python/qmk/cli/fileformat.py
index 112d8d59d..cee4ba1ac 100644..100755
--- a/lib/python/qmk/cli/fileformat.py
+++ b/lib/python/qmk/cli/fileformat.py
@@ -1,13 +1,23 @@
1"""Format files according to QMK's style. 1"""Point people to the new command name.
2""" 2"""
3from milc import cli 3import sys
4from pathlib import Path
4 5
5import subprocess 6from milc import cli
6 7
7 8
8@cli.subcommand("Format files according to QMK's style.", hidden=True) 9@cli.subcommand('Pointer to the new command name: qmk format-text.', hidden=True)
9def fileformat(cli): 10def fileformat(cli):
10 """Run several general formatting commands. 11 """Pointer to the new command name: qmk format-text.
11 """ 12 """
12 dos2unix = subprocess.run(['bash', '-c', 'git ls-files -z | xargs -0 dos2unix'], stdout=subprocess.DEVNULL) 13 cli.log.warning('"qmk fileformat" has been renamed to "qmk format-text". Please use the new command in the future.')
13 return dos2unix.returncode 14 argv = [sys.executable, *sys.argv]
15 argv[argv.index('fileformat')] = 'format-text'
16 script_path = Path(argv[1])
17 script_path_exe = Path(f'{argv[1]}.exe')
18
19 if not script_path.exists() and script_path_exe.exists():
20 # For reasons I don't understand ".exe" is stripped from the script name on windows.
21 argv[1] = str(script_path_exe)
22
23 return cli.run(argv, capture_output=False).returncode
diff --git a/lib/python/qmk/cli/format/c.py b/lib/python/qmk/cli/format/c.py
new file mode 100644
index 000000000..b7263e19f
--- /dev/null
+++ b/lib/python/qmk/cli/format/c.py
@@ -0,0 +1,137 @@
1"""Format C code according to QMK's style.
2"""
3from os import path
4from shutil import which
5from subprocess import CalledProcessError, DEVNULL, Popen, PIPE
6
7from argcomplete.completers import FilesCompleter
8from milc import cli
9
10from qmk.path import normpath
11from qmk.c_parse import c_source_files
12
13c_file_suffixes = ('c', 'h', 'cpp')
14core_dirs = ('drivers', 'quantum', 'tests', 'tmk_core', 'platforms')
15ignored = ('tmk_core/protocol/usb_hid', 'quantum/template', 'platforms/chibios')
16
17
18def find_clang_format():
19 """Returns the path to clang-format.
20 """
21 for clang_version in range(20, 6, -1):
22 binary = f'clang-format-{clang_version}'
23
24 if which(binary):
25 return binary
26
27 return 'clang-format'
28
29
30def find_diffs(files):
31 """Run clang-format and diff it against a file.
32 """
33 found_diffs = False
34
35 for file in files:
36 cli.log.debug('Checking for changes in %s', file)
37 clang_format = Popen([find_clang_format(), file], stdout=PIPE, stderr=PIPE, universal_newlines=True)
38 diff = cli.run(['diff', '-u', f'--label=a/{file}', f'--label=b/{file}', str(file), '-'], stdin=clang_format.stdout, capture_output=True)
39
40 if diff.returncode != 0:
41 print(diff.stdout)
42 found_diffs = True
43
44 return found_diffs
45
46
47def cformat_run(files):
48 """Spawn clang-format subprocess with proper arguments
49 """
50 # Determine which version of clang-format to use
51 clang_format = [find_clang_format(), '-i']
52
53 try:
54 cli.run([*clang_format, *map(str, files)], check=True, capture_output=False, stdin=DEVNULL)
55 cli.log.info('Successfully formatted the C code.')
56 return True
57
58 except CalledProcessError as e:
59 cli.log.error('Error formatting C code!')
60 cli.log.debug('%s exited with returncode %s', e.cmd, e.returncode)
61 cli.log.debug('STDOUT:')
62 cli.log.debug(e.stdout)
63 cli.log.debug('STDERR:')
64 cli.log.debug(e.stderr)
65 return False
66
67
68def filter_files(files, core_only=False):
69 """Yield only files to be formatted and skip the rest
70 """
71 if core_only:
72 # Filter non-core files
73 for index, file in enumerate(files):
74 # The following statement checks each file to see if the file path is
75 # - in the core directories
76 # - not in the ignored directories
77 if not any(i in str(file) for i in core_dirs) or any(i in str(file) for i in ignored):
78 files[index] = None
79 cli.log.debug("Skipping non-core file %s, as '--core-only' is used.", file)
80
81 for file in files:
82 if file and file.name.split('.')[-1] in c_file_suffixes:
83 yield file
84 else:
85 cli.log.debug('Skipping file %s', file)
86
87
88@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Flag only, don't automatically format.")
89@cli.argument('-b', '--base-branch', default='origin/master', help='Branch to compare to diffs to.')
90@cli.argument('-a', '--all-files', arg_only=True, action='store_true', help='Format all core files.')
91@cli.argument('--core-only', arg_only=True, action='store_true', help='Format core files only.')
92@cli.argument('files', nargs='*', arg_only=True, type=normpath, completer=FilesCompleter('.c'), help='Filename(s) to format.')
93@cli.subcommand("Format C code according to QMK's style.", hidden=False if cli.config.user.developer else True)
94def format_c(cli):
95 """Format C code according to QMK's style.
96 """
97 # Find the list of files to format
98 if cli.args.files:
99 files = list(filter_files(cli.args.files, cli.args.core_only))
100
101 if not files:
102 cli.log.error('No C files in filelist: %s', ', '.join(map(str, cli.args.files)))
103 exit(0)
104
105 if cli.args.all_files:
106 cli.log.warning('Filenames passed with -a, only formatting: %s', ','.join(map(str, files)))
107
108 elif cli.args.all_files:
109 all_files = c_source_files(core_dirs)
110 files = list(filter_files(all_files, True))
111
112 else:
113 git_diff_cmd = ['git', 'diff', '--name-only', cli.args.base_branch, *core_dirs]
114 git_diff = cli.run(git_diff_cmd, stdin=DEVNULL)
115
116 if git_diff.returncode != 0:
117 cli.log.error("Error running %s", git_diff_cmd)
118 print(git_diff.stderr)
119 return git_diff.returncode
120
121 files = []
122
123 for file in git_diff.stdout.strip().split('\n'):
124 if not any([file.startswith(ignore) for ignore in ignored]):
125 if path.exists(file) and file.split('.')[-1] in c_file_suffixes:
126 files.append(file)
127
128 # Sanity check
129 if not files:
130 cli.log.error('No changed files detected. Use "qmk format-c -a" to format all core files')
131 return False
132
133 # Run clang-format on the files we've found
134 if cli.args.dry_run:
135 return not find_diffs(files)
136 else:
137 return cformat_run(files)
diff --git a/lib/python/qmk/cli/format/json.py b/lib/python/qmk/cli/format/json.py
index 1358c70e7..19d504491 100755
--- a/lib/python/qmk/cli/format/json.py
+++ b/lib/python/qmk/cli/format/json.py
@@ -8,7 +8,7 @@ from jsonschema import ValidationError
8from milc import cli 8from milc import cli
9 9
10from qmk.info import info_json 10from qmk.info import info_json
11from qmk.json_schema import json_load, keyboard_validate 11from qmk.json_schema import json_load, validate
12from qmk.json_encoders import InfoJSONEncoder, KeymapJSONEncoder 12from qmk.json_encoders import InfoJSONEncoder, KeymapJSONEncoder
13from qmk.path import normpath 13from qmk.path import normpath
14 14
@@ -23,14 +23,13 @@ def format_json(cli):
23 23
24 if cli.args.format == 'auto': 24 if cli.args.format == 'auto':
25 try: 25 try:
26 keyboard_validate(json_file) 26 validate(json_file, 'qmk.keyboard.v1')
27 json_encoder = InfoJSONEncoder 27 json_encoder = InfoJSONEncoder
28 28
29 except ValidationError as e: 29 except ValidationError as e:
30 cli.log.warning('File %s did not validate as a keyboard:\n\t%s', cli.args.json_file, e) 30 cli.log.warning('File %s did not validate as a keyboard:\n\t%s', cli.args.json_file, e)
31 cli.log.info('Treating %s as a keymap file.', cli.args.json_file) 31 cli.log.info('Treating %s as a keymap file.', cli.args.json_file)
32 json_encoder = KeymapJSONEncoder 32 json_encoder = KeymapJSONEncoder
33
34 elif cli.args.format == 'keyboard': 33 elif cli.args.format == 'keyboard':
35 json_encoder = InfoJSONEncoder 34 json_encoder = InfoJSONEncoder
36 elif cli.args.format == 'keymap': 35 elif cli.args.format == 'keymap':
diff --git a/lib/python/qmk/cli/format/python.py b/lib/python/qmk/cli/format/python.py
new file mode 100755
index 000000000..00612f97e
--- /dev/null
+++ b/lib/python/qmk/cli/format/python.py
@@ -0,0 +1,26 @@
1"""Format python code according to QMK's style.
2"""
3from subprocess import CalledProcessError, DEVNULL
4
5from milc import cli
6
7
8@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Don't actually format.")
9@cli.subcommand("Format python code according to QMK's style.", hidden=False if cli.config.user.developer else True)
10def format_python(cli):
11 """Format python code according to QMK's style.
12 """
13 edit = '--diff' if cli.args.dry_run else '--in-place'
14 yapf_cmd = ['yapf', '-vv', '--recursive', edit, 'bin/qmk', 'lib/python']
15 try:
16 cli.run(yapf_cmd, check=True, capture_output=False, stdin=DEVNULL)
17 cli.log.info('Python code in `bin/qmk` and `lib/python` is correctly formatted.')
18 return True
19
20 except CalledProcessError:
21 if cli.args.dry_run:
22 cli.log.error('Python code in `bin/qmk` and `lib/python` incorrectly formatted!')
23 else:
24 cli.log.error('Error formatting python code!')
25
26 return False
diff --git a/lib/python/qmk/cli/format/text.py b/lib/python/qmk/cli/format/text.py
new file mode 100644
index 000000000..e7e07b729
--- /dev/null
+++ b/lib/python/qmk/cli/format/text.py
@@ -0,0 +1,27 @@
1"""Ensure text files have the proper line endings.
2"""
3from subprocess import CalledProcessError
4
5from milc import cli
6
7
8@cli.subcommand("Ensure text files have the proper line endings.", hidden=True)
9def format_text(cli):
10 """Ensure text files have the proper line endings.
11 """
12 try:
13 file_list_cmd = cli.run(['git', 'ls-files', '-z'], check=True)
14 except CalledProcessError as e:
15 cli.log.error('Could not get file list: %s', e)
16 exit(1)
17 except Exception as e:
18 cli.log.error('Unhandled exception: %s: %s', e.__class__.__name__, e)
19 cli.log.exception(e)
20 exit(1)
21
22 dos2unix = cli.run(['xargs', '-0', 'dos2unix'], stdin=None, input=file_list_cmd.stdout)
23
24 if dos2unix.returncode != 0:
25 print(dos2unix.stderr)
26
27 return dos2unix.returncode
diff --git a/lib/python/qmk/cli/generate/version_h.py b/lib/python/qmk/cli/generate/version_h.py
new file mode 100644
index 000000000..b8e52588c
--- /dev/null
+++ b/lib/python/qmk/cli/generate/version_h.py
@@ -0,0 +1,28 @@
1"""Used by the make system to generate version.h for use in code.
2"""
3from milc import cli
4
5from qmk.commands import create_version_h
6from qmk.path import normpath
7
8
9@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to')
10@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
11@cli.argument('--skip-git', arg_only=True, action='store_true', help='Skip Git operations')
12@cli.argument('--skip-all', arg_only=True, action='store_true', help='Use placeholder values for all defines (implies --skip-git)')
13@cli.subcommand('Used by the make system to generate version.h for use in code', hidden=True)
14def generate_version_h(cli):
15 """Generates the version.h file.
16 """
17 if cli.args.skip_all:
18 cli.args.skip_git = True
19
20 version_h = create_version_h(cli.args.skip_git, cli.args.skip_all)
21
22 if cli.args.output:
23 cli.args.output.write_text(version_h)
24
25 if not cli.args.quiet:
26 cli.log.info('Wrote version.h to %s.', cli.args.output)
27 else:
28 print(version_h)
diff --git a/lib/python/qmk/cli/info.py b/lib/python/qmk/cli/info.py
index 0d08d242c..337b494a9 100755
--- a/lib/python/qmk/cli/info.py
+++ b/lib/python/qmk/cli/info.py
@@ -87,8 +87,6 @@ def print_friendly_output(kb_info_json):
87 cli.echo('{fg_blue}Maintainer{fg_reset}: %s', kb_info_json['maintainer']) 87 cli.echo('{fg_blue}Maintainer{fg_reset}: %s', kb_info_json['maintainer'])
88 cli.echo('{fg_blue}Keyboard Folder{fg_reset}: %s', kb_info_json.get('keyboard_folder', 'Unknown')) 88 cli.echo('{fg_blue}Keyboard Folder{fg_reset}: %s', kb_info_json.get('keyboard_folder', 'Unknown'))
89 cli.echo('{fg_blue}Layouts{fg_reset}: %s', ', '.join(sorted(kb_info_json['layouts'].keys()))) 89 cli.echo('{fg_blue}Layouts{fg_reset}: %s', ', '.join(sorted(kb_info_json['layouts'].keys())))
90 if 'width' in kb_info_json and 'height' in kb_info_json:
91 cli.echo('{fg_blue}Size{fg_reset}: %s x %s' % (kb_info_json['width'], kb_info_json['height']))
92 cli.echo('{fg_blue}Processor{fg_reset}: %s', kb_info_json.get('processor', 'Unknown')) 90 cli.echo('{fg_blue}Processor{fg_reset}: %s', kb_info_json.get('processor', 'Unknown'))
93 cli.echo('{fg_blue}Bootloader{fg_reset}: %s', kb_info_json.get('bootloader', 'Unknown')) 91 cli.echo('{fg_blue}Bootloader{fg_reset}: %s', kb_info_json.get('bootloader', 'Unknown'))
94 if 'layout_aliases' in kb_info_json: 92 if 'layout_aliases' in kb_info_json:
diff --git a/lib/python/qmk/cli/kle2json.py b/lib/python/qmk/cli/kle2json.py
index acb75ef4f..bbfddf426 100755
--- a/lib/python/qmk/cli/kle2json.py
+++ b/lib/python/qmk/cli/kle2json.py
@@ -44,8 +44,6 @@ def kle2json(cli):
44 'keyboard_name': kle.name, 44 'keyboard_name': kle.name,
45 'url': '', 45 'url': '',
46 'maintainer': 'qmk', 46 'maintainer': 'qmk',
47 'width': kle.columns,
48 'height': kle.rows,
49 'layouts': { 47 'layouts': {
50 'LAYOUT': { 48 'LAYOUT': {
51 'layout': kle2qmk(kle) 49 'layout': kle2qmk(kle)
diff --git a/lib/python/qmk/cli/new/keyboard.py b/lib/python/qmk/cli/new/keyboard.py
index ae4445ca4..9e4232679 100644
--- a/lib/python/qmk/cli/new/keyboard.py
+++ b/lib/python/qmk/cli/new/keyboard.py
@@ -1,11 +1,142 @@
1"""This script automates the creation of keyboards. 1"""This script automates the creation of new keyboard directories using a starter template.
2""" 2"""
3from datetime import date
4import fileinput
5from pathlib import Path
6import re
7import shutil
8
9from qmk.commands import git_get_username
10import qmk.path
3from milc import cli 11from milc import cli
12from milc.questions import choice, question
13
14KEYBOARD_TYPES = ['avr', 'ps2avrgb']
15
16
17def keyboard_name(name):
18 """Callable for argparse validation.
19 """
20 if not validate_keyboard_name(name):
21 raise ValueError
22 return name
4 23
5 24
6@cli.subcommand('Creates a new keyboard') 25def validate_keyboard_name(name):
26 """Returns True if the given keyboard name contains only lowercase a-z, 0-9 and underscore characters.
27 """
28 regex = re.compile(r'^[a-z0-9][a-z0-9/_]+$')
29 return bool(regex.match(name))
30
31
32@cli.argument('-kb', '--keyboard', help='Specify the name for the new keyboard directory', arg_only=True, type=keyboard_name)
33@cli.argument('-t', '--type', help='Specify the keyboard type', arg_only=True, choices=KEYBOARD_TYPES)
34@cli.argument('-u', '--username', help='Specify your username (default from Git config)', arg_only=True)
35@cli.subcommand('Creates a new keyboard directory')
7def new_keyboard(cli): 36def new_keyboard(cli):
8 """Creates a new keyboard 37 """Creates a new keyboard.
9 """ 38 """
10 # TODO: replace this bodge to the existing script 39 cli.log.info('{style_bright}Generating a new QMK keyboard directory{style_normal}')
11 cli.run(['util/new_keyboard.sh'], stdin=None, capture_output=False) 40 cli.echo('')
41
42 # Get keyboard name
43 new_keyboard_name = None
44 while not new_keyboard_name:
45 new_keyboard_name = cli.args.keyboard if cli.args.keyboard else question('Keyboard Name:')
46 if not validate_keyboard_name(new_keyboard_name):
47 cli.log.error('Keyboard names must contain only {fg_cyan}lowercase a-z{fg_reset}, {fg_cyan}0-9{fg_reset}, and {fg_cyan}_{fg_reset}! Please choose a different name.')
48
49 # Exit if passed by arg
50 if cli.args.keyboard:
51 return False
52
53 new_keyboard_name = None
54 continue
55
56 keyboard_path = qmk.path.keyboard(new_keyboard_name)
57 if keyboard_path.exists():
58 cli.log.error(f'Keyboard {{fg_cyan}}{new_keyboard_name}{{fg_reset}} already exists! Please choose a different name.')
59
60 # Exit if passed by arg
61 if cli.args.keyboard:
62 return False
63
64 new_keyboard_name = None
65
66 # Get keyboard type
67 keyboard_type = cli.args.type if cli.args.type else choice('Keyboard Type:', KEYBOARD_TYPES, default=0)
68
69 # Get username
70 user_name = None
71 while not user_name:
72 user_name = question('Your Name:', default=find_user_name())
73
74 if not user_name:
75 cli.log.error('You didn\'t provide a username, and we couldn\'t find one set in your QMK or Git configs. Please try again.')
76
77 # Exit if passed by arg
78 if cli.args.username:
79 return False
80
81 # Copy all the files
82 copy_templates(keyboard_type, keyboard_path)
83
84 # Replace all the placeholders
85 keyboard_basename = keyboard_path.name
86 replacements = [
87 ('%YEAR%', str(date.today().year)),
88 ('%KEYBOARD%', keyboard_basename),
89 ('%YOUR_NAME%', user_name),
90 ]
91 filenames = [
92 keyboard_path / 'config.h',
93 keyboard_path / 'info.json',
94 keyboard_path / 'readme.md',
95 keyboard_path / f'{keyboard_basename}.c',
96 keyboard_path / f'{keyboard_basename}.h',
97 keyboard_path / 'keymaps/default/readme.md',
98 keyboard_path / 'keymaps/default/keymap.c',
99 ]
100 replace_placeholders(replacements, filenames)
101
102 cli.echo('')
103 cli.log.info(f'{{fg_green}}Created a new keyboard called {{fg_cyan}}{new_keyboard_name}{{fg_green}}.{{fg_reset}}')
104 cli.log.info(f'To start working on things, `cd` into {{fg_cyan}}{keyboard_path}{{fg_reset}},')
105 cli.log.info('or open the directory in your preferred text editor.')
106
107
108def find_user_name():
109 if cli.args.username:
110 return cli.args.username
111 elif cli.config.user.name:
112 return cli.config.user.name
113 else:
114 return git_get_username()
115
116
117def copy_templates(keyboard_type, keyboard_path):
118 """Copies the template files from quantum/template to the new keyboard directory.
119 """
120 template_base_path = Path('quantum/template')
121 keyboard_basename = keyboard_path.name
122
123 cli.log.info('Copying base template files...')
124 shutil.copytree(template_base_path / 'base', keyboard_path)
125
126 cli.log.info(f'Copying {{fg_cyan}}{keyboard_type}{{fg_reset}} template files...')
127 shutil.copytree(template_base_path / keyboard_type, keyboard_path, dirs_exist_ok=True)
128
129 cli.log.info(f'Renaming {{fg_cyan}}keyboard.[ch]{{fg_reset}} to {{fg_cyan}}{keyboard_basename}.[ch]{{fg_reset}}...')
130 shutil.move(keyboard_path / 'keyboard.c', keyboard_path / f'{keyboard_basename}.c')
131 shutil.move(keyboard_path / 'keyboard.h', keyboard_path / f'{keyboard_basename}.h')
132
133
134def replace_placeholders(replacements, filenames):
135 """Replaces the given placeholders in each template file.
136 """
137 for replacement in replacements:
138 cli.log.info(f'Replacing {{fg_cyan}}{replacement[0]}{{fg_reset}} with {{fg_cyan}}{replacement[1]}{{fg_reset}}...')
139
140 with fileinput.input(files=filenames, inplace=True) as file:
141 for line in file:
142 print(line.replace(replacement[0], replacement[1]), end='')
diff --git a/lib/python/qmk/cli/pyformat.py b/lib/python/qmk/cli/pyformat.py
index abe5f6de1..c624f74ae 100755
--- a/lib/python/qmk/cli/pyformat.py
+++ b/lib/python/qmk/cli/pyformat.py
@@ -1,26 +1,24 @@
1"""Format python code according to QMK's style. 1"""Point people to the new command name.
2""" 2"""
3from subprocess import CalledProcessError, DEVNULL 3import sys
4from pathlib import Path
4 5
5from milc import cli 6from milc import cli
6 7
7 8
8@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Flag only, don't automatically format.") 9@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Don't actually format.")
9@cli.subcommand("Format python code according to QMK's style.", hidden=False if cli.config.user.developer else True) 10@cli.subcommand('Pointer to the new command name: qmk format-python.', hidden=False if cli.config.user.developer else True)
10def pyformat(cli): 11def pyformat(cli):
11 """Format python code according to QMK's style. 12 """Pointer to the new command name: qmk format-python.
12 """ 13 """
13 edit = '--diff' if cli.args.dry_run else '--in-place' 14 cli.log.warning('"qmk pyformat" has been renamed to "qmk format-python". Please use the new command in the future.')
14 yapf_cmd = ['yapf', '-vv', '--recursive', edit, 'bin/qmk', 'lib/python'] 15 argv = [sys.executable, *sys.argv]
15 try: 16 argv[argv.index('pyformat')] = 'format-python'
16 cli.run(yapf_cmd, check=True, capture_output=False, stdin=DEVNULL) 17 script_path = Path(argv[1])
17 cli.log.info('Python code in `bin/qmk` and `lib/python` is correctly formatted.') 18 script_path_exe = Path(f'{argv[1]}.exe')
18 return True
19 19
20 except CalledProcessError: 20 if not script_path.exists() and script_path_exe.exists():
21 if cli.args.dry_run: 21 # For reasons I don't understand ".exe" is stripped from the script name on windows.
22 cli.log.error('Python code in `bin/qmk` and `lib/python` incorrectly formatted!') 22 argv[1] = str(script_path_exe)
23 else:
24 cli.log.error('Error formatting python code!')
25 23
26 return False 24 return cli.run(argv, capture_output=False).returncode
diff --git a/lib/python/qmk/commands.py b/lib/python/qmk/commands.py
index 3a35c1103..8c66228b2 100644
--- a/lib/python/qmk/commands.py
+++ b/lib/python/qmk/commands.py
@@ -2,6 +2,7 @@
2""" 2"""
3import json 3import json
4import os 4import os
5import sys
5import shutil 6import shutil
6from pathlib import Path 7from pathlib import Path
7from subprocess import DEVNULL 8from subprocess import DEVNULL
@@ -10,7 +11,7 @@ from time import strftime
10from milc import cli 11from milc import cli
11 12
12import qmk.keymap 13import qmk.keymap
13from qmk.constants import KEYBOARD_OUTPUT_PREFIX 14from qmk.constants import QMK_FIRMWARE, KEYBOARD_OUTPUT_PREFIX
14from qmk.json_schema import json_load 15from qmk.json_schema import json_load
15 16
16time_fmt = '%Y-%m-%d-%H:%M:%S' 17time_fmt = '%Y-%m-%d-%H:%M:%S'
@@ -86,11 +87,17 @@ def create_make_command(keyboard, keymap, target=None, parallel=1, **env_vars):
86 return create_make_target(':'.join(make_args), parallel, **env_vars) 87 return create_make_target(':'.join(make_args), parallel, **env_vars)
87 88
88 89
89def get_git_version(repo_dir='.', check_dir='.'): 90def get_git_version(current_time, repo_dir='.', check_dir='.'):
90 """Returns the current git version for a repo, or the current time. 91 """Returns the current git version for a repo, or the current time.
91 """ 92 """
92 git_describe_cmd = ['git', 'describe', '--abbrev=6', '--dirty', '--always', '--tags'] 93 git_describe_cmd = ['git', 'describe', '--abbrev=6', '--dirty', '--always', '--tags']
93 94
95 if repo_dir != '.':
96 repo_dir = Path('lib') / repo_dir
97
98 if check_dir != '.':
99 check_dir = repo_dir / check_dir
100
94 if Path(check_dir).exists(): 101 if Path(check_dir).exists():
95 git_describe = cli.run(git_describe_cmd, stdin=DEVNULL, cwd=repo_dir) 102 git_describe = cli.run(git_describe_cmd, stdin=DEVNULL, cwd=repo_dir)
96 103
@@ -100,23 +107,40 @@ def get_git_version(repo_dir='.', check_dir='.'):
100 else: 107 else:
101 cli.log.warn(f'"{" ".join(git_describe_cmd)}" returned error code {git_describe.returncode}') 108 cli.log.warn(f'"{" ".join(git_describe_cmd)}" returned error code {git_describe.returncode}')
102 print(git_describe.stderr) 109 print(git_describe.stderr)
103 return strftime(time_fmt) 110 return current_time
104 111
105 return strftime(time_fmt) 112 return current_time
106 113
107 114
108def write_version_h(git_version, build_date, chibios_version, chibios_contrib_version): 115def create_version_h(skip_git=False, skip_all=False):
109 """Generate and write quantum/version.h 116 """Generate version.h contents
110 """ 117 """
111 version_h = [ 118 if skip_all:
112 f'#define QMK_VERSION "{git_version}"', 119 current_time = "1970-01-01-00:00:00"
113 f'#define QMK_BUILDDATE "{build_date}"', 120 else:
114 f'#define CHIBIOS_VERSION "{chibios_version}"', 121 current_time = strftime(time_fmt)
115 f'#define CHIBIOS_CONTRIB_VERSION "{chibios_contrib_version}"', 122
116 ] 123 if skip_git:
124 git_version = "NA"
125 chibios_version = "NA"
126 chibios_contrib_version = "NA"
127 else:
128 git_version = get_git_version(current_time)
129 chibios_version = get_git_version(current_time, "chibios", "os")
130 chibios_contrib_version = get_git_version(current_time, "chibios-contrib", "os")
131
132 version_h_lines = f"""/* This file was automatically generated. Do not edit or copy.
133 */
134
135#pragma once
136
137#define QMK_VERSION "{git_version}"
138#define QMK_BUILDDATE "{current_time}"
139#define CHIBIOS_VERSION "{chibios_version}"
140#define CHIBIOS_CONTRIB_VERSION "{chibios_contrib_version}"
141"""
117 142
118 version_h_file = Path('quantum/version.h') 143 return version_h_lines
119 version_h_file.write_text('\n'.join(version_h))
120 144
121 145
122def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_vars): 146def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_vars):
@@ -149,13 +173,8 @@ def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_va
149 keymap_dir.mkdir(exist_ok=True, parents=True) 173 keymap_dir.mkdir(exist_ok=True, parents=True)
150 keymap_c.write_text(c_text) 174 keymap_c.write_text(c_text)
151 175
152 # Write the version.h file 176 version_h = Path('quantum/version.h')
153 git_version = get_git_version() 177 version_h.write_text(create_version_h())
154 build_date = strftime('%Y-%m-%d-%H:%M:%S')
155 chibios_version = get_git_version("lib/chibios", "lib/chibios/os")
156 chibios_contrib_version = get_git_version("lib/chibios-contrib", "lib/chibios-contrib/os")
157
158 write_version_h(git_version, build_date, chibios_version, chibios_contrib_version)
159 178
160 # Return a command that can be run to make the keymap and flash if given 179 # Return a command that can be run to make the keymap and flash if given
161 verbose = 'true' if cli.config.general.verbose else 'false' 180 verbose = 'true' if cli.config.general.verbose else 'false'
@@ -181,10 +200,6 @@ def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_va
181 make_command.append(f'{key}={value}') 200 make_command.append(f'{key}={value}')
182 201
183 make_command.extend([ 202 make_command.extend([
184 f'GIT_VERSION={git_version}',
185 f'BUILD_DATE={build_date}',
186 f'CHIBIOS_VERSION={chibios_version}',
187 f'CHIBIOS_CONTRIB_VERSION={chibios_contrib_version}',
188 f'KEYBOARD={user_keymap["keyboard"]}', 203 f'KEYBOARD={user_keymap["keyboard"]}',
189 f'KEYMAP={user_keymap["keymap"]}', 204 f'KEYMAP={user_keymap["keymap"]}',
190 f'KEYBOARD_FILESAFE={keyboard_filesafe}', 205 f'KEYBOARD_FILESAFE={keyboard_filesafe}',
@@ -223,3 +238,80 @@ def parse_configurator_json(configurator_file):
223 user_keymap['layout'] = aliases[orig_keyboard]['layouts'][user_keymap['layout']] 238 user_keymap['layout'] = aliases[orig_keyboard]['layouts'][user_keymap['layout']]
224 239
225 return user_keymap 240 return user_keymap
241
242
243def git_get_username():
244 """Retrieves user's username from Git config, if set.
245 """
246 git_username = cli.run(['git', 'config', '--get', 'user.name'])
247
248 if git_username.returncode == 0 and git_username.stdout:
249 return git_username.stdout.strip()
250
251
252def git_check_repo():
253 """Checks that the .git directory exists inside QMK_HOME.
254
255 This is a decent enough indicator that the qmk_firmware directory is a
256 proper Git repository, rather than a .zip download from GitHub.
257 """
258 dot_git_dir = QMK_FIRMWARE / '.git'
259
260 return dot_git_dir.is_dir()
261
262
263def git_get_branch():
264 """Returns the current branch for a repo, or None.
265 """
266 git_branch = cli.run(['git', 'branch', '--show-current'])
267 if not git_branch.returncode != 0 or not git_branch.stdout:
268 # Workaround for Git pre-2.22
269 git_branch = cli.run(['git', 'rev-parse', '--abbrev-ref', 'HEAD'])
270
271 if git_branch.returncode == 0:
272 return git_branch.stdout.strip()
273
274
275def git_is_dirty():
276 """Returns 1 if repo is dirty, or 0 if clean
277 """
278 git_diff_staged_cmd = ['git', 'diff', '--quiet']
279 git_diff_unstaged_cmd = [*git_diff_staged_cmd, '--cached']
280
281 unstaged = cli.run(git_diff_staged_cmd)
282 staged = cli.run(git_diff_unstaged_cmd)
283
284 return unstaged.returncode != 0 or staged.returncode != 0
285
286
287def git_get_remotes():
288 """Returns the current remotes for a repo.
289 """
290 remotes = {}
291
292 git_remote_show_cmd = ['git', 'remote', 'show']
293 git_remote_get_cmd = ['git', 'remote', 'get-url']
294
295 git_remote_show = cli.run(git_remote_show_cmd)
296 if git_remote_show.returncode == 0:
297 for name in git_remote_show.stdout.splitlines():
298 git_remote_name = cli.run([*git_remote_get_cmd, name])
299 remotes[name.strip()] = {"url": git_remote_name.stdout.strip()}
300
301 return remotes
302
303
304def git_check_deviation(active_branch):
305 """Return True if branch has custom commits
306 """
307 cli.run(['git', 'fetch', 'upstream', active_branch])
308 deviations = cli.run(['git', '--no-pager', 'log', f'upstream/{active_branch}...{active_branch}'])
309 return bool(deviations.returncode)
310
311
312def in_virtualenv():
313 """Check if running inside a virtualenv.
314 Based on https://stackoverflow.com/a/1883251
315 """
316 active_prefix = getattr(sys, "base_prefix", None) or getattr(sys, "real_prefix", None) or sys.prefix
317 return active_prefix != sys.prefix
diff --git a/lib/python/qmk/constants.py b/lib/python/qmk/constants.py
index 49e5e0eb4..71a6c91c7 100644
--- a/lib/python/qmk/constants.py
+++ b/lib/python/qmk/constants.py
@@ -6,11 +6,14 @@ from pathlib import Path
6# The root of the qmk_firmware tree. 6# The root of the qmk_firmware tree.
7QMK_FIRMWARE = Path.cwd() 7QMK_FIRMWARE = Path.cwd()
8 8
9# Upstream repo url
10QMK_FIRMWARE_UPSTREAM = 'qmk/qmk_firmware'
11
9# This is the number of directories under `qmk_firmware/keyboards` that will be traversed. This is currently a limitation of our make system. 12# This is the number of directories under `qmk_firmware/keyboards` that will be traversed. This is currently a limitation of our make system.
10MAX_KEYBOARD_SUBFOLDERS = 5 13MAX_KEYBOARD_SUBFOLDERS = 5
11 14
12# Supported processor types 15# Supported processor types
13CHIBIOS_PROCESSORS = 'cortex-m0', 'cortex-m0plus', 'cortex-m3', 'cortex-m4', 'MKL26Z64', 'MK20DX128', 'MK20DX256', 'MK66F18', 'STM32F042', 'STM32F072', 'STM32F103', 'STM32F303', 'STM32F401', 'STM32F411', 'STM32F446', 'STM32G431', 'STM32G474', 'STM32L433', 'STM32L443' 16CHIBIOS_PROCESSORS = 'cortex-m0', 'cortex-m0plus', 'cortex-m3', 'cortex-m4', 'MKL26Z64', 'MK20DX128', 'MK20DX256', 'MK66F18', 'STM32F042', 'STM32F072', 'STM32F103', 'STM32F303', 'STM32F401', 'STM32F407', 'STM32F411', 'STM32F446', 'STM32G431', 'STM32G474', 'STM32L412', 'STM32L422', 'STM32L433', 'STM32L443'
14LUFA_PROCESSORS = 'at90usb162', 'atmega16u2', 'atmega32u2', 'atmega16u4', 'atmega32u4', 'at90usb646', 'at90usb647', 'at90usb1286', 'at90usb1287', None 17LUFA_PROCESSORS = 'at90usb162', 'atmega16u2', 'atmega32u2', 'atmega16u4', 'atmega32u4', 'at90usb646', 'at90usb647', 'at90usb1286', 'at90usb1287', None
15VUSB_PROCESSORS = 'atmega32a', 'atmega328p', 'atmega328', 'attiny85' 18VUSB_PROCESSORS = 'atmega32a', 'atmega328p', 'atmega328', 'attiny85'
16 19
diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py
index d23b3592e..7f9907a50 100644
--- a/lib/python/qmk/info.py
+++ b/lib/python/qmk/info.py
@@ -9,7 +9,7 @@ from milc import cli
9 9
10from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS 10from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS
11from qmk.c_parse import find_layouts 11from qmk.c_parse import find_layouts
12from qmk.json_schema import deep_update, json_load, keyboard_validate, keyboard_api_validate 12from qmk.json_schema import deep_update, json_load, validate
13from qmk.keyboard import config_h, rules_mk 13from qmk.keyboard import config_h, rules_mk
14from qmk.keymap import list_keymaps 14from qmk.keymap import list_keymaps
15from qmk.makefile import parse_rules_mk_file 15from qmk.makefile import parse_rules_mk_file
@@ -64,9 +64,12 @@ def info_json(keyboard):
64 info_data = _extract_config_h(info_data) 64 info_data = _extract_config_h(info_data)
65 info_data = _extract_rules_mk(info_data) 65 info_data = _extract_rules_mk(info_data)
66 66
67 # Ensure that we have matrix row and column counts
68 info_data = _matrix_size(info_data)
69
67 # Validate against the jsonschema 70 # Validate against the jsonschema
68 try: 71 try:
69 keyboard_api_validate(info_data) 72 validate(info_data, 'qmk.api.keyboard.v1')
70 73
71 except jsonschema.ValidationError as e: 74 except jsonschema.ValidationError as e:
72 json_path = '.'.join([str(p) for p in e.absolute_path]) 75 json_path = '.'.join([str(p) for p in e.absolute_path])
@@ -90,6 +93,9 @@ def info_json(keyboard):
90 if layout_name not in info_data.get('layouts', {}) and layout_name not in info_data.get('layout_aliases', {}): 93 if layout_name not in info_data.get('layouts', {}) and layout_name not in info_data.get('layout_aliases', {}):
91 _log_error(info_data, 'Claims to support community layout %s but no %s() macro found' % (layout, layout_name)) 94 _log_error(info_data, 'Claims to support community layout %s but no %s() macro found' % (layout, layout_name))
92 95
96 # Check that the reported matrix size is consistent with the actual matrix size
97 _check_matrix(info_data)
98
93 return info_data 99 return info_data
94 100
95 101
@@ -143,10 +149,7 @@ def _pin_name(pin):
143 elif pin == 'NO_PIN': 149 elif pin == 'NO_PIN':
144 return None 150 return None
145 151
146 elif pin[0] in 'ABCDEFGHIJK' and pin[1].isdigit(): 152 return pin
147 return pin
148
149 raise ValueError(f'Invalid pin: {pin}')
150 153
151 154
152def _extract_pins(pins): 155def _extract_pins(pins):
@@ -341,6 +344,46 @@ def _extract_rules_mk(info_data):
341 return info_data 344 return info_data
342 345
343 346
347def _matrix_size(info_data):
348 """Add info_data['matrix_size'] if it doesn't exist.
349 """
350 if 'matrix_size' not in info_data and 'matrix_pins' in info_data:
351 info_data['matrix_size'] = {}
352
353 if 'direct' in info_data['matrix_pins']:
354 info_data['matrix_size']['cols'] = len(info_data['matrix_pins']['direct'][0])
355 info_data['matrix_size']['rows'] = len(info_data['matrix_pins']['direct'])
356 elif 'cols' in info_data['matrix_pins'] and 'rows' in info_data['matrix_pins']:
357 info_data['matrix_size']['cols'] = len(info_data['matrix_pins']['cols'])
358 info_data['matrix_size']['rows'] = len(info_data['matrix_pins']['rows'])
359
360 return info_data
361
362
363def _check_matrix(info_data):
364 """Check the matrix to ensure that row/column count is consistent.
365 """
366 if 'matrix_pins' in info_data and 'matrix_size' in info_data:
367 actual_col_count = info_data['matrix_size'].get('cols', 0)
368 actual_row_count = info_data['matrix_size'].get('rows', 0)
369 col_count = row_count = 0
370
371 if 'direct' in info_data['matrix_pins']:
372 col_count = len(info_data['matrix_pins']['direct'][0])
373 row_count = len(info_data['matrix_pins']['direct'])
374 elif 'cols' in info_data['matrix_pins'] and 'rows' in info_data['matrix_pins']:
375 col_count = len(info_data['matrix_pins']['cols'])
376 row_count = len(info_data['matrix_pins']['rows'])
377
378 if col_count != actual_col_count and col_count != (actual_col_count / 2):
379 # FIXME: once we can we should detect if split is enabled to do the actual_col_count/2 check.
380 _log_error(info_data, f'MATRIX_COLS is inconsistent with the size of MATRIX_COL_PINS: {col_count} != {actual_col_count}')
381
382 if row_count != actual_row_count and row_count != (actual_row_count / 2):
383 # FIXME: once we can we should detect if split is enabled to do the actual_row_count/2 check.
384 _log_error(info_data, f'MATRIX_ROWS is inconsistent with the size of MATRIX_ROW_PINS: {row_count} != {actual_row_count}')
385
386
344def _merge_layouts(info_data, new_info_data): 387def _merge_layouts(info_data, new_info_data):
345 """Merge new_info_data into info_data in an intelligent way. 388 """Merge new_info_data into info_data in an intelligent way.
346 """ 389 """
@@ -493,7 +536,7 @@ def merge_info_jsons(keyboard, info_data):
493 continue 536 continue
494 537
495 try: 538 try:
496 keyboard_validate(new_info_data) 539 validate(new_info_data, 'qmk.keyboard.v1')
497 except jsonschema.ValidationError as e: 540 except jsonschema.ValidationError as e:
498 json_path = '.'.join([str(p) for p in e.absolute_path]) 541 json_path = '.'.join([str(p) for p in e.absolute_path])
499 cli.log.error('Not including data from file: %s', info_file) 542 cli.log.error('Not including data from file: %s', info_file)
diff --git a/lib/python/qmk/json_encoders.py b/lib/python/qmk/json_encoders.py
index 9f3da022b..72e91973a 100755
--- a/lib/python/qmk/json_encoders.py
+++ b/lib/python/qmk/json_encoders.py
@@ -102,9 +102,6 @@ class InfoJSONEncoder(QMKJSONEncoder):
102 elif key == 'maintainer': 102 elif key == 'maintainer':
103 return '12maintainer' 103 return '12maintainer'
104 104
105 elif key in ('height', 'width'):
106 return '40' + str(key)
107
108 elif key == 'community_layouts': 105 elif key == 'community_layouts':
109 return '97community_layouts' 106 return '97community_layouts'
110 107
diff --git a/lib/python/qmk/json_schema.py b/lib/python/qmk/json_schema.py
index f3992ee71..cbc5bff51 100644
--- a/lib/python/qmk/json_schema.py
+++ b/lib/python/qmk/json_schema.py
@@ -27,9 +27,10 @@ def json_load(json_file):
27 27
28def load_jsonschema(schema_name): 28def load_jsonschema(schema_name):
29 """Read a jsonschema file from disk. 29 """Read a jsonschema file from disk.
30
31 FIXME(skullydazed/anyone): Refactor to make this a public function.
32 """ 30 """
31 if Path(schema_name).exists():
32 return json_load(schema_name)
33
33 schema_path = Path(f'data/schemas/{schema_name}.jsonschema') 34 schema_path = Path(f'data/schemas/{schema_name}.jsonschema')
34 35
35 if not schema_path.exists(): 36 if not schema_path.exists():
@@ -38,28 +39,33 @@ def load_jsonschema(schema_name):
38 return json_load(schema_path) 39 return json_load(schema_path)
39 40
40 41
41def keyboard_validate(data): 42def create_validator(schema):
42 """Validates data against the keyboard jsonschema. 43 """Creates a validator for the given schema id.
43 """ 44 """
44 schema = load_jsonschema('keyboard') 45 schema_store = {}
45 validator = jsonschema.Draft7Validator(schema).validate
46 46
47 return validator(data) 47 for schema_file in Path('data/schemas').glob('*.jsonschema'):
48 schema_data = load_jsonschema(schema_file)
49 if not isinstance(schema_data, dict):
50 cli.log.debug('Skipping schema file %s', schema_file)
51 continue
52 schema_store[schema_data['$id']] = schema_data
53
54 resolver = jsonschema.RefResolver.from_schema(schema_store['qmk.keyboard.v1'], store=schema_store)
55
56 return jsonschema.Draft7Validator(schema_store[schema], resolver=resolver).validate
48 57
49 58
50def keyboard_api_validate(data): 59def validate(data, schema):
51 """Validates data against the api_keyboard jsonschema. 60 """Validates data against a schema.
52 """ 61 """
53 base = load_jsonschema('keyboard') 62 validator = create_validator(schema)
54 relative = load_jsonschema('api_keyboard')
55 resolver = jsonschema.RefResolver.from_schema(base)
56 validator = jsonschema.Draft7Validator(relative, resolver=resolver).validate
57 63
58 return validator(data) 64 return validator(data)
59 65
60 66
61def deep_update(origdict, newdict): 67def deep_update(origdict, newdict):
62 """Update a dictionary in place, recursing to do a deep copy. 68 """Update a dictionary in place, recursing to do a depth-first deep copy.
63 """ 69 """
64 for key, value in newdict.items(): 70 for key, value in newdict.items():
65 if isinstance(value, Mapping): 71 if isinstance(value, Mapping):
diff --git a/lib/python/qmk/tests/minimal_info.json b/lib/python/qmk/tests/minimal_info.json
index b91c23bd3..11ef12fef 100644
--- a/lib/python/qmk/tests/minimal_info.json
+++ b/lib/python/qmk/tests/minimal_info.json
@@ -1,8 +1,6 @@
1{ 1{
2 "keyboard_name": "tester", 2 "keyboard_name": "tester",
3 "maintainer": "qmk", 3 "maintainer": "qmk",
4 "height": 5,
5 "width": 15,
6 "layouts": { 4 "layouts": {
7 "LAYOUT": { 5 "LAYOUT": {
8 "layout": [ 6 "layout": [
diff --git a/lib/python/qmk/tests/test_cli_commands.py b/lib/python/qmk/tests/test_cli_commands.py
index afdbc8142..b39fe5e46 100644
--- a/lib/python/qmk/tests/test_cli_commands.py
+++ b/lib/python/qmk/tests/test_cli_commands.py
@@ -31,13 +31,13 @@ def check_returncode(result, expected=[0]):
31 assert result.returncode in expected 31 assert result.returncode in expected
32 32
33 33
34def test_cformat(): 34def test_format_c():
35 result = check_subcommand('cformat', '-n', 'quantum/matrix.c') 35 result = check_subcommand('format-c', '-n', 'quantum/matrix.c')
36 check_returncode(result) 36 check_returncode(result)
37 37
38 38
39def test_cformat_all(): 39def test_format_c_all():
40 result = check_subcommand('cformat', '-n', '-a') 40 result = check_subcommand('format-c', '-n', '-a')
41 check_returncode(result, [0, 1]) 41 check_returncode(result, [0, 1])
42 42
43 43
@@ -80,8 +80,8 @@ def test_hello():
80 assert 'Hello,' in result.stdout 80 assert 'Hello,' in result.stdout
81 81
82 82
83def test_pyformat(): 83def test_format_python():
84 result = check_subcommand('pyformat', '--dry-run') 84 result = check_subcommand('format-python', '--dry-run')
85 check_returncode(result) 85 check_returncode(result)
86 assert 'Python code in `bin/qmk` and `lib/python` is correctly formatted.' in result.stdout 86 assert 'Python code in `bin/qmk` and `lib/python` is correctly formatted.' in result.stdout
87 87
@@ -258,6 +258,12 @@ def test_generate_rules_mk():
258 assert 'MCU ?= atmega32u4' in result.stdout 258 assert 'MCU ?= atmega32u4' in result.stdout
259 259
260 260
261def test_generate_version_h():
262 result = check_subcommand('generate-version-h')
263 check_returncode(result)
264 assert '#define QMK_VERSION' in result.stdout
265
266
261def test_generate_layouts(): 267def test_generate_layouts():
262 result = check_subcommand('generate-layouts', '-kb', 'handwired/pytest/basic') 268 result = check_subcommand('generate-layouts', '-kb', 'handwired/pytest/basic')
263 check_returncode(result) 269 check_returncode(result)
@@ -267,7 +273,7 @@ def test_generate_layouts():
267def test_format_json_keyboard(): 273def test_format_json_keyboard():
268 result = check_subcommand('format-json', '--format', 'keyboard', 'lib/python/qmk/tests/minimal_info.json') 274 result = check_subcommand('format-json', '--format', 'keyboard', 'lib/python/qmk/tests/minimal_info.json')
269 check_returncode(result) 275 check_returncode(result)
270 assert result.stdout == '{\n "keyboard_name": "tester",\n "maintainer": "qmk",\n "height": 5,\n "width": 15,\n "layouts": {\n "LAYOUT": {\n "layout": [\n { "label": "KC_A", "matrix": [0, 0], "x": 0, "y": 0 }\n ]\n }\n }\n}\n' 276 assert result.stdout == '{\n "keyboard_name": "tester",\n "maintainer": "qmk",\n "layouts": {\n "LAYOUT": {\n "layout": [\n { "label": "KC_A", "matrix": [0, 0], "x": 0, "y": 0 }\n ]\n }\n }\n}\n'
271 277
272 278
273def test_format_json_keymap(): 279def test_format_json_keymap():
@@ -279,7 +285,7 @@ def test_format_json_keymap():
279def test_format_json_keyboard_auto(): 285def test_format_json_keyboard_auto():
280 result = check_subcommand('format-json', '--format', 'auto', 'lib/python/qmk/tests/minimal_info.json') 286 result = check_subcommand('format-json', '--format', 'auto', 'lib/python/qmk/tests/minimal_info.json')
281 check_returncode(result) 287 check_returncode(result)
282 assert result.stdout == '{\n "keyboard_name": "tester",\n "maintainer": "qmk",\n "height": 5,\n "width": 15,\n "layouts": {\n "LAYOUT": {\n "layout": [\n { "label": "KC_A", "matrix": [0, 0], "x": 0, "y": 0 }\n ]\n }\n }\n}\n' 288 assert result.stdout == '{\n "keyboard_name": "tester",\n "maintainer": "qmk",\n "layouts": {\n "LAYOUT": {\n "layout": [\n { "label": "KC_A", "matrix": [0, 0], "x": 0, "y": 0 }\n ]\n }\n }\n}\n'
283 289
284 290
285def test_format_json_keymap_auto(): 291def test_format_json_keymap_auto():