diff options
| author | James Young <18669334+noroadsleft@users.noreply.github.com> | 2020-11-28 12:02:18 -0800 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-11-28 12:02:18 -0800 |
| commit | c66df1664497546f32662409778731143e45a552 (patch) | |
| tree | da73a2d532a27685a31d932b3a44a707d4a3af81 /lib | |
| parent | 15385d4113414d42bd062c60c9de5df797d3157f (diff) | |
| download | qmk_firmware-c66df1664497546f32662409778731143e45a552.tar.gz qmk_firmware-c66df1664497546f32662409778731143e45a552.zip | |
2020 November 28 Breaking Changes Update (#11053)
* Branch point for 2020 November 28 Breaking Change
* Remove matrix_col_t to allow MATRIX_ROWS > 32 (#10183)
* Add support for soft serial to ATmega32U2 (#10204)
* Change MIDI velocity implementation to allow direct control of velocity value (#9940)
* Add ability to build a subset of all keyboards based on platform.
* Actually use eeprom_driver_init().
* Make bootloader_jump weak for ChibiOS. (#10417)
* Joystick 16-bit support (#10439)
* Per-encoder resolutions (#10259)
* Share button state from mousekey to pointing_device (#10179)
* Add hotfix for chibios keyboards not wake (#10088)
* Add advanced/efficient RGB Matrix Indicators (#8564)
* Naming change.
* Support for STM32 GPIOF,G,H,I,J,K (#10206)
* Add milc as a dependency and remove the installed milc (#10563)
* ChibiOS upgrade: early init conversions (#10214)
* ChibiOS upgrade: configuration file migrator (#9952)
* Haptic and solenoid cleanup (#9700)
* XD75 cleanup (#10524)
* OLED display update interval support (#10388)
* Add definition based on currently-selected serial driver. (#10716)
* New feature: Retro Tapping per key (#10622)
* Allow for modification of output RGB values when using rgblight/rgb_matrix. (#10638)
* Add housekeeping task callbacks so that keyboards/keymaps are capable of executing code for each main loop iteration. (#10530)
* Rescale both ChibiOS and AVR backlighting.
* Reduce Helix keyboard build variation (#8669)
* Minor change to behavior allowing display updates to continue between task ticks (#10750)
* Some GPIO manipulations in matrix.c change to atomic. (#10491)
* qmk cformat (#10767)
* [Keyboard] Update the Speedo firmware for v3.0 (#10657)
* Maartenwut/Maarten namechange to evyd13/Evy (#10274)
* [quantum] combine repeated lines of code (#10837)
* Add step sequencer feature (#9703)
* aeboards/ext65 refactor (#10820)
* Refactor xelus/dawn60 for Rev2 later (#10584)
* add DEBUG_MATRIX_SCAN_RATE_ENABLE to common_features.mk (#10824)
* [Core] Added `add_oneshot_mods` & `del_oneshot_mods` (#10549)
* update chibios os usb for the otg driver (#8893)
* Remove HD44780 References, Part 4 (#10735)
* [Keyboard] Add Valor FRL TKL (+refactor) (#10512)
* Fix cursor position bug in oled_write_raw functions (#10800)
* Fixup version.h writing when using SKIP_VERSION=yes (#10972)
* Allow for certain code in the codebase assuming length of string. (#10974)
* Add AT90USB support for serial.c (#10706)
* Auto shift: support repeats and early registration (#9826)
* Rename ledmatrix.h to match .c file (#7949)
* Split RGB_MATRIX_ENABLE into _ENABLE and _DRIVER (#10231)
* Split LED_MATRIX_ENABLE into _ENABLE and _DRIVER (#10840)
* Merge point for 2020 Nov 28 Breaking Change
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/python/milc.py | 826 | ||||
| -rw-r--r-- | lib/python/qmk/cli/__init__.py | 1 | ||||
| -rw-r--r-- | lib/python/qmk/cli/chibios/__init__.py | 1 | ||||
| -rw-r--r-- | lib/python/qmk/cli/chibios/confmigrate.py | 161 | ||||
| -rwxr-xr-x | lib/python/qmk/cli/doctor.py | 2 | ||||
| -rw-r--r-- | lib/python/qmk/questions.py | 183 |
6 files changed, 164 insertions, 1010 deletions
diff --git a/lib/python/milc.py b/lib/python/milc.py deleted file mode 100644 index 0cdd43dc8..000000000 --- a/lib/python/milc.py +++ /dev/null | |||
| @@ -1,826 +0,0 @@ | |||
| 1 | #!/usr/bin/env python3 | ||
| 2 | # coding=utf-8 | ||
| 3 | """MILC - A CLI Framework | ||
| 4 | |||
| 5 | PYTHON_ARGCOMPLETE_OK | ||
| 6 | |||
| 7 | MILC is an opinionated framework for writing CLI apps. It optimizes for the | ||
| 8 | most common unix tool pattern- small tools that are run from the command | ||
| 9 | line but generally do not feature any user interaction while they run. | ||
| 10 | |||
| 11 | For more details see the MILC documentation: | ||
| 12 | |||
| 13 | <https://github.com/clueboard/milc/tree/master/docs> | ||
| 14 | """ | ||
| 15 | from __future__ import division, print_function, unicode_literals | ||
| 16 | import argparse | ||
| 17 | import logging | ||
| 18 | import os | ||
| 19 | import re | ||
| 20 | import shlex | ||
| 21 | import subprocess | ||
| 22 | import sys | ||
| 23 | from decimal import Decimal | ||
| 24 | from pathlib import Path | ||
| 25 | from platform import platform | ||
| 26 | from tempfile import NamedTemporaryFile | ||
| 27 | from time import sleep | ||
| 28 | |||
| 29 | try: | ||
| 30 | from ConfigParser import RawConfigParser | ||
| 31 | except ImportError: | ||
| 32 | from configparser import RawConfigParser | ||
| 33 | |||
| 34 | try: | ||
| 35 | import thread | ||
| 36 | import threading | ||
| 37 | except ImportError: | ||
| 38 | thread = None | ||
| 39 | |||
| 40 | import argcomplete | ||
| 41 | import colorama | ||
| 42 | from appdirs import user_config_dir | ||
| 43 | |||
| 44 | # Disable logging until we can configure it how the user wants | ||
| 45 | logging.basicConfig(stream=os.devnull) | ||
| 46 | |||
| 47 | # Log Level Representations | ||
| 48 | EMOJI_LOGLEVELS = { | ||
| 49 | 'CRITICAL': '{bg_red}{fg_white}¬_¬{style_reset_all}', | ||
| 50 | 'ERROR': '{fg_red}☒{style_reset_all}', | ||
| 51 | 'WARNING': '{fg_yellow}⚠{style_reset_all}', | ||
| 52 | 'INFO': '{fg_blue}ℹ{style_reset_all}', | ||
| 53 | 'DEBUG': '{fg_cyan}☐{style_reset_all}', | ||
| 54 | 'NOTSET': '{style_reset_all}¯\\_(o_o)_/¯' | ||
| 55 | } | ||
| 56 | EMOJI_LOGLEVELS['FATAL'] = EMOJI_LOGLEVELS['CRITICAL'] | ||
| 57 | EMOJI_LOGLEVELS['WARN'] = EMOJI_LOGLEVELS['WARNING'] | ||
| 58 | UNICODE_SUPPORT = sys.stdout.encoding.lower().startswith('utf') | ||
| 59 | |||
| 60 | # ANSI Color setup | ||
| 61 | # Regex was gratefully borrowed from kfir on stackoverflow: | ||
| 62 | # https://stackoverflow.com/a/45448194 | ||
| 63 | ansi_regex = r'\x1b(' \ | ||
| 64 | r'(\[\??\d+[hl])|' \ | ||
| 65 | r'([=<>a-kzNM78])|' \ | ||
| 66 | r'([\(\)][a-b0-2])|' \ | ||
| 67 | r'(\[\d{0,2}[ma-dgkjqi])|' \ | ||
| 68 | r'(\[\d+;\d+[hfy]?)|' \ | ||
| 69 | r'(\[;?[hf])|' \ | ||
| 70 | r'(#[3-68])|' \ | ||
| 71 | r'([01356]n)|' \ | ||
| 72 | r'(O[mlnp-z]?)|' \ | ||
| 73 | r'(/Z)|' \ | ||
| 74 | r'(\d+)|' \ | ||
| 75 | r'(\[\?\d;\d0c)|' \ | ||
| 76 | r'(\d;\dR))' | ||
| 77 | ansi_escape = re.compile(ansi_regex, flags=re.IGNORECASE) | ||
| 78 | ansi_styles = ( | ||
| 79 | ('fg', colorama.ansi.AnsiFore()), | ||
| 80 | ('bg', colorama.ansi.AnsiBack()), | ||
| 81 | ('style', colorama.ansi.AnsiStyle()), | ||
| 82 | ) | ||
| 83 | ansi_colors = {} | ||
| 84 | |||
| 85 | for prefix, obj in ansi_styles: | ||
| 86 | for color in [x for x in obj.__dict__ if not x.startswith('_')]: | ||
| 87 | ansi_colors[prefix + '_' + color.lower()] = getattr(obj, color) | ||
| 88 | |||
| 89 | |||
| 90 | def format_ansi(text): | ||
| 91 | """Return a copy of text with certain strings replaced with ansi. | ||
| 92 | """ | ||
| 93 | # Avoid .format() so we don't have to worry about the log content | ||
| 94 | for color in ansi_colors: | ||
| 95 | text = text.replace('{%s}' % color, ansi_colors[color]) | ||
| 96 | return text + ansi_colors['style_reset_all'] | ||
| 97 | |||
| 98 | |||
| 99 | class ANSIFormatterMixin(object): | ||
| 100 | """A log formatter mixin that inserts ANSI color. | ||
| 101 | """ | ||
| 102 | def format(self, record): | ||
| 103 | msg = super(ANSIFormatterMixin, self).format(record) | ||
| 104 | return format_ansi(msg) | ||
| 105 | |||
| 106 | |||
| 107 | class ANSIStrippingMixin(object): | ||
| 108 | """A log formatter mixin that strips ANSI. | ||
| 109 | """ | ||
| 110 | def format(self, record): | ||
| 111 | msg = super(ANSIStrippingMixin, self).format(record) | ||
| 112 | record.levelname = ansi_escape.sub('', record.levelname) | ||
| 113 | return ansi_escape.sub('', msg) | ||
| 114 | |||
| 115 | |||
| 116 | class EmojiLoglevelMixin(object): | ||
| 117 | """A log formatter mixin that makes the loglevel an emoji on UTF capable terminals. | ||
| 118 | """ | ||
| 119 | def format(self, record): | ||
| 120 | if UNICODE_SUPPORT: | ||
| 121 | record.levelname = EMOJI_LOGLEVELS[record.levelname].format(**ansi_colors) | ||
| 122 | return super(EmojiLoglevelMixin, self).format(record) | ||
| 123 | |||
| 124 | |||
| 125 | class ANSIFormatter(ANSIFormatterMixin, logging.Formatter): | ||
| 126 | """A log formatter that colorizes output. | ||
| 127 | """ | ||
| 128 | pass | ||
| 129 | |||
| 130 | |||
| 131 | class ANSIStrippingFormatter(ANSIStrippingMixin, ANSIFormatterMixin, logging.Formatter): | ||
| 132 | """A log formatter that strips ANSI | ||
| 133 | """ | ||
| 134 | pass | ||
| 135 | |||
| 136 | |||
| 137 | class ANSIEmojiLoglevelFormatter(EmojiLoglevelMixin, ANSIFormatterMixin, logging.Formatter): | ||
| 138 | """A log formatter that adds Emoji and ANSI | ||
| 139 | """ | ||
| 140 | pass | ||
| 141 | |||
| 142 | |||
| 143 | class ANSIStrippingEmojiLoglevelFormatter(ANSIStrippingMixin, EmojiLoglevelMixin, ANSIFormatterMixin, logging.Formatter): | ||
| 144 | """A log formatter that adds Emoji and strips ANSI | ||
| 145 | """ | ||
| 146 | pass | ||
| 147 | |||
| 148 | |||
| 149 | class Configuration(object): | ||
| 150 | """Represents the running configuration. | ||
| 151 | |||
| 152 | This class never raises IndexError, instead it will return None if a | ||
| 153 | section or option does not yet exist. | ||
| 154 | """ | ||
| 155 | def __contains__(self, key): | ||
| 156 | return self._config.__contains__(key) | ||
| 157 | |||
| 158 | def __iter__(self): | ||
| 159 | return self._config.__iter__() | ||
| 160 | |||
| 161 | def __len__(self): | ||
| 162 | return self._config.__len__() | ||
| 163 | |||
| 164 | def __repr__(self): | ||
| 165 | return self._config.__repr__() | ||
| 166 | |||
| 167 | def keys(self): | ||
| 168 | return self._config.keys() | ||
| 169 | |||
| 170 | def items(self): | ||
| 171 | return self._config.items() | ||
| 172 | |||
| 173 | def values(self): | ||
| 174 | return self._config.values() | ||
| 175 | |||
| 176 | def __init__(self, *args, **kwargs): | ||
| 177 | self._config = {} | ||
| 178 | |||
| 179 | def __getattr__(self, key): | ||
| 180 | return self.__getitem__(key) | ||
| 181 | |||
| 182 | def __getitem__(self, key): | ||
| 183 | """Returns a config section, creating it if it doesn't exist yet. | ||
| 184 | """ | ||
| 185 | if key not in self._config: | ||
| 186 | self.__dict__[key] = self._config[key] = ConfigurationSection(self) | ||
| 187 | |||
| 188 | return self._config[key] | ||
| 189 | |||
| 190 | def __setitem__(self, key, value): | ||
| 191 | self.__dict__[key] = value | ||
| 192 | self._config[key] = value | ||
| 193 | |||
| 194 | def __delitem__(self, key): | ||
| 195 | if key in self.__dict__ and key[0] != '_': | ||
| 196 | del self.__dict__[key] | ||
| 197 | if key in self._config: | ||
| 198 | del self._config[key] | ||
| 199 | |||
| 200 | |||
| 201 | class ConfigurationSection(Configuration): | ||
| 202 | def __init__(self, parent, *args, **kwargs): | ||
| 203 | super(ConfigurationSection, self).__init__(*args, **kwargs) | ||
| 204 | self.parent = parent | ||
| 205 | |||
| 206 | def __getitem__(self, key): | ||
| 207 | """Returns a config value, pulling from the `user` section as a fallback. | ||
| 208 | This is called when the attribute is accessed either via the get method or through [ ] index. | ||
| 209 | """ | ||
| 210 | if key in self._config and self._config.get(key) is not None: | ||
| 211 | return self._config[key] | ||
| 212 | |||
| 213 | elif key in self.parent.user: | ||
| 214 | return self.parent.user[key] | ||
| 215 | |||
| 216 | return None | ||
| 217 | |||
| 218 | def __getattr__(self, key): | ||
| 219 | """Returns the config value from the `user` section. | ||
| 220 | This is called when the attribute is accessed via dot notation but does not exists. | ||
| 221 | """ | ||
| 222 | if key in self.parent.user: | ||
| 223 | return self.parent.user[key] | ||
| 224 | |||
| 225 | return None | ||
| 226 | |||
| 227 | |||
| 228 | def handle_store_boolean(self, *args, **kwargs): | ||
| 229 | """Does the add_argument for action='store_boolean'. | ||
| 230 | """ | ||
| 231 | disabled_args = None | ||
| 232 | disabled_kwargs = kwargs.copy() | ||
| 233 | disabled_kwargs['action'] = 'store_false' | ||
| 234 | disabled_kwargs['dest'] = self.get_argument_name(*args, **kwargs) | ||
| 235 | disabled_kwargs['help'] = 'Disable ' + kwargs['help'] | ||
| 236 | kwargs['action'] = 'store_true' | ||
| 237 | kwargs['help'] = 'Enable ' + kwargs['help'] | ||
| 238 | |||
| 239 | for flag in args: | ||
| 240 | if flag[:2] == '--': | ||
| 241 | disabled_args = ('--no-' + flag[2:],) | ||
| 242 | break | ||
| 243 | |||
| 244 | self.add_argument(*args, **kwargs) | ||
| 245 | self.add_argument(*disabled_args, **disabled_kwargs) | ||
| 246 | |||
| 247 | return (args, kwargs, disabled_args, disabled_kwargs) | ||
| 248 | |||
| 249 | |||
| 250 | class SubparserWrapper(object): | ||
| 251 | """Wrap subparsers so we can track what options the user passed. | ||
| 252 | """ | ||
| 253 | def __init__(self, cli, submodule, subparser): | ||
| 254 | self.cli = cli | ||
| 255 | self.submodule = submodule | ||
| 256 | self.subparser = subparser | ||
| 257 | |||
| 258 | for attr in dir(subparser): | ||
| 259 | if not hasattr(self, attr): | ||
| 260 | setattr(self, attr, getattr(subparser, attr)) | ||
| 261 | |||
| 262 | def completer(self, completer): | ||
| 263 | """Add an arpcomplete completer to this subcommand. | ||
| 264 | """ | ||
| 265 | self.subparser.completer = completer | ||
| 266 | |||
| 267 | def add_argument(self, *args, **kwargs): | ||
| 268 | """Add an argument for this subcommand. | ||
| 269 | |||
| 270 | This also stores the default for the argument in `self.cli.default_arguments`. | ||
| 271 | """ | ||
| 272 | if kwargs.get('action') == 'store_boolean': | ||
| 273 | # Store boolean will call us again with the enable/disable flag arguments | ||
| 274 | return handle_store_boolean(self, *args, **kwargs) | ||
| 275 | |||
| 276 | self.cli.acquire_lock() | ||
| 277 | argument_name = self.cli.get_argument_name(*args, **kwargs) | ||
| 278 | |||
| 279 | self.subparser.add_argument(*args, **kwargs) | ||
| 280 | |||
| 281 | if kwargs.get('action') == 'store_false': | ||
| 282 | self.cli._config_store_false.append(argument_name) | ||
| 283 | |||
| 284 | if kwargs.get('action') == 'store_true': | ||
| 285 | self.cli._config_store_true.append(argument_name) | ||
| 286 | |||
| 287 | if self.submodule not in self.cli.default_arguments: | ||
| 288 | self.cli.default_arguments[self.submodule] = {} | ||
| 289 | self.cli.default_arguments[self.submodule][argument_name] = kwargs.get('default') | ||
| 290 | self.cli.release_lock() | ||
| 291 | |||
| 292 | |||
| 293 | class MILC(object): | ||
| 294 | """MILC - An Opinionated Batteries Included Framework | ||
| 295 | """ | ||
| 296 | def __init__(self): | ||
| 297 | """Initialize the MILC object. | ||
| 298 | |||
| 299 | version | ||
| 300 | The version string to associate with your CLI program | ||
| 301 | """ | ||
| 302 | # Setup a lock for thread safety | ||
| 303 | self._lock = threading.RLock() if thread else None | ||
| 304 | |||
| 305 | # Define some basic info | ||
| 306 | self.acquire_lock() | ||
| 307 | self._config_store_true = [] | ||
| 308 | self._config_store_false = [] | ||
| 309 | self._description = None | ||
| 310 | self._entrypoint = None | ||
| 311 | self._inside_context_manager = False | ||
| 312 | self.ansi = ansi_colors | ||
| 313 | self.arg_only = {} | ||
| 314 | self.config = self.config_source = None | ||
| 315 | self.config_file = None | ||
| 316 | self.default_arguments = {} | ||
| 317 | self.version = 'unknown' | ||
| 318 | self.platform = platform() | ||
| 319 | |||
| 320 | # Figure out our program name | ||
| 321 | self.prog_name = sys.argv[0][:-3] if sys.argv[0].endswith('.py') else sys.argv[0] | ||
| 322 | self.prog_name = self.prog_name.split('/')[-1] | ||
| 323 | self.release_lock() | ||
| 324 | |||
| 325 | # Initialize all the things | ||
| 326 | self.read_config_file() | ||
| 327 | self.initialize_argparse() | ||
| 328 | self.initialize_logging() | ||
| 329 | |||
| 330 | @property | ||
| 331 | def description(self): | ||
| 332 | return self._description | ||
| 333 | |||
| 334 | @description.setter | ||
| 335 | def description(self, value): | ||
| 336 | self._description = self._arg_parser.description = value | ||
| 337 | |||
| 338 | def echo(self, text, *args, **kwargs): | ||
| 339 | """Print colorized text to stdout. | ||
| 340 | |||
| 341 | ANSI color strings (such as {fg-blue}) will be converted into ANSI | ||
| 342 | escape sequences, and the ANSI reset sequence will be added to all | ||
| 343 | strings. | ||
| 344 | |||
| 345 | If *args or **kwargs are passed they will be used to %-format the strings. | ||
| 346 | |||
| 347 | If `self.config.general.color` is False any ANSI escape sequences in the text will be stripped. | ||
| 348 | """ | ||
| 349 | if args and kwargs: | ||
| 350 | raise RuntimeError('You can only specify *args or **kwargs, not both!') | ||
| 351 | |||
| 352 | args = args or kwargs | ||
| 353 | text = format_ansi(text) | ||
| 354 | |||
| 355 | if not self.config.general.color: | ||
| 356 | text = ansi_escape.sub('', text) | ||
| 357 | |||
| 358 | print(text % args) | ||
| 359 | |||
| 360 | def run(self, command, *args, **kwargs): | ||
| 361 | """Run a command with subprocess.run | ||
| 362 | The *args and **kwargs arguments get passed directly to `subprocess.run`. | ||
| 363 | """ | ||
| 364 | if isinstance(command, str): | ||
| 365 | raise TypeError('`command` must be a non-text sequence such as list or tuple.') | ||
| 366 | |||
| 367 | if 'windows' in self.platform.lower(): | ||
| 368 | safecmd = map(shlex.quote, command) | ||
| 369 | safecmd = ' '.join(safecmd) | ||
| 370 | command = [os.environ['SHELL'], '-c', safecmd] | ||
| 371 | |||
| 372 | self.log.debug('Running command: %s', command) | ||
| 373 | |||
| 374 | return subprocess.run(command, *args, **kwargs) | ||
| 375 | |||
| 376 | def initialize_argparse(self): | ||
| 377 | """Prepare to process arguments from sys.argv. | ||
| 378 | """ | ||
| 379 | kwargs = { | ||
| 380 | 'fromfile_prefix_chars': '@', | ||
| 381 | 'conflict_handler': 'resolve', | ||
| 382 | } | ||
| 383 | |||
| 384 | self.acquire_lock() | ||
| 385 | self.subcommands = {} | ||
| 386 | self._subparsers = None | ||
| 387 | self.argwarn = argcomplete.warn | ||
| 388 | self.args = None | ||
| 389 | self._arg_parser = argparse.ArgumentParser(**kwargs) | ||
| 390 | self.set_defaults = self._arg_parser.set_defaults | ||
| 391 | self.print_usage = self._arg_parser.print_usage | ||
| 392 | self.print_help = self._arg_parser.print_help | ||
| 393 | self.release_lock() | ||
| 394 | |||
| 395 | def completer(self, completer): | ||
| 396 | """Add an argcomplete completer to this subcommand. | ||
| 397 | """ | ||
| 398 | self._arg_parser.completer = completer | ||
| 399 | |||
| 400 | def add_argument(self, *args, **kwargs): | ||
| 401 | """Wrapper to add arguments and track whether they were passed on the command line. | ||
| 402 | """ | ||
| 403 | if 'action' in kwargs and kwargs['action'] == 'store_boolean': | ||
| 404 | return handle_store_boolean(self, *args, **kwargs) | ||
| 405 | |||
| 406 | self.acquire_lock() | ||
| 407 | |||
| 408 | self._arg_parser.add_argument(*args, **kwargs) | ||
| 409 | if 'general' not in self.default_arguments: | ||
| 410 | self.default_arguments['general'] = {} | ||
| 411 | self.default_arguments['general'][self.get_argument_name(*args, **kwargs)] = kwargs.get('default') | ||
| 412 | |||
| 413 | self.release_lock() | ||
| 414 | |||
| 415 | def initialize_logging(self): | ||
| 416 | """Prepare the defaults for the logging infrastructure. | ||
| 417 | """ | ||
| 418 | self.acquire_lock() | ||
| 419 | self.log_file = None | ||
| 420 | self.log_file_mode = 'a' | ||
| 421 | self.log_file_handler = None | ||
| 422 | self.log_print = True | ||
| 423 | self.log_print_to = sys.stderr | ||
| 424 | self.log_print_level = logging.INFO | ||
| 425 | self.log_file_level = logging.DEBUG | ||
| 426 | self.log_level = logging.INFO | ||
| 427 | self.log = logging.getLogger(self.__class__.__name__) | ||
| 428 | self.log.setLevel(logging.DEBUG) | ||
| 429 | logging.root.setLevel(logging.DEBUG) | ||
| 430 | self.release_lock() | ||
| 431 | |||
| 432 | self.add_argument('-V', '--version', version=self.version, action='version', help='Display the version and exit') | ||
| 433 | self.add_argument('-v', '--verbose', action='store_true', help='Make the logging more verbose') | ||
| 434 | self.add_argument('--datetime-fmt', default='%Y-%m-%d %H:%M:%S', help='Format string for datetimes') | ||
| 435 | self.add_argument('--log-fmt', default='%(levelname)s %(message)s', help='Format string for printed log output') | ||
| 436 | self.add_argument('--log-file-fmt', default='[%(levelname)s] [%(asctime)s] [file:%(pathname)s] [line:%(lineno)d] %(message)s', help='Format string for log file.') | ||
| 437 | self.add_argument('--log-file', help='File to write log messages to') | ||
| 438 | self.add_argument('--color', action='store_boolean', default=True, help='color in output') | ||
| 439 | self.add_argument('--config-file', help='The location for the configuration file') | ||
| 440 | self.arg_only['config_file'] = ['general'] | ||
| 441 | |||
| 442 | def add_subparsers(self, title='Sub-commands', **kwargs): | ||
| 443 | if self._inside_context_manager: | ||
| 444 | raise RuntimeError('You must run this before the with statement!') | ||
| 445 | |||
| 446 | self.acquire_lock() | ||
| 447 | self._subparsers = self._arg_parser.add_subparsers(title=title, dest='subparsers', **kwargs) | ||
| 448 | self.release_lock() | ||
| 449 | |||
| 450 | def acquire_lock(self): | ||
| 451 | """Acquire the MILC lock for exclusive access to properties. | ||
| 452 | """ | ||
| 453 | if self._lock: | ||
| 454 | self._lock.acquire() | ||
| 455 | |||
| 456 | def release_lock(self): | ||
| 457 | """Release the MILC lock. | ||
| 458 | """ | ||
| 459 | if self._lock: | ||
| 460 | self._lock.release() | ||
| 461 | |||
| 462 | def find_config_file(self): | ||
| 463 | """Locate the config file. | ||
| 464 | """ | ||
| 465 | if self.config_file: | ||
| 466 | return self.config_file | ||
| 467 | |||
| 468 | if '--config-file' in sys.argv: | ||
| 469 | return Path(sys.argv[sys.argv.index('--config-file') + 1]).expanduser().resolve() | ||
| 470 | |||
| 471 | filedir = user_config_dir(appname='qmk', appauthor='QMK') | ||
| 472 | filename = '%s.ini' % self.prog_name | ||
| 473 | return Path(filedir) / filename | ||
| 474 | |||
| 475 | def get_argument_name(self, *args, **kwargs): | ||
| 476 | """Takes argparse arguments and returns the dest name. | ||
| 477 | """ | ||
| 478 | try: | ||
| 479 | return self._arg_parser._get_optional_kwargs(*args, **kwargs)['dest'] | ||
| 480 | except ValueError: | ||
| 481 | return self._arg_parser._get_positional_kwargs(*args, **kwargs)['dest'] | ||
| 482 | |||
| 483 | def argument(self, *args, **kwargs): | ||
| 484 | """Decorator to call self.add_argument or self.<subcommand>.add_argument. | ||
| 485 | """ | ||
| 486 | if self._inside_context_manager: | ||
| 487 | raise RuntimeError('You must run this before the with statement!') | ||
| 488 | |||
| 489 | def argument_function(handler): | ||
| 490 | subcommand_name = handler.__name__.replace("_", "-") | ||
| 491 | |||
| 492 | if kwargs.get('arg_only'): | ||
| 493 | arg_name = self.get_argument_name(*args, **kwargs) | ||
| 494 | if arg_name not in self.arg_only: | ||
| 495 | self.arg_only[arg_name] = [] | ||
| 496 | self.arg_only[arg_name].append(subcommand_name) | ||
| 497 | del kwargs['arg_only'] | ||
| 498 | |||
| 499 | if handler is self._entrypoint: | ||
| 500 | self.add_argument(*args, **kwargs) | ||
| 501 | |||
| 502 | elif subcommand_name in self.subcommands: | ||
| 503 | self.subcommands[subcommand_name].add_argument(*args, **kwargs) | ||
| 504 | |||
| 505 | else: | ||
| 506 | raise RuntimeError('Decorated function is not entrypoint or subcommand!') | ||
| 507 | |||
| 508 | return handler | ||
| 509 | |||
| 510 | return argument_function | ||
| 511 | |||
| 512 | def arg_passed(self, arg): | ||
| 513 | """Returns True if arg was passed on the command line. | ||
| 514 | """ | ||
| 515 | return self.default_arguments.get(arg) != self.args[arg] | ||
| 516 | |||
| 517 | def parse_args(self): | ||
| 518 | """Parse the CLI args. | ||
| 519 | """ | ||
| 520 | if self.args: | ||
| 521 | self.log.debug('Warning: Arguments have already been parsed, ignoring duplicate attempt!') | ||
| 522 | return | ||
| 523 | |||
| 524 | argcomplete.autocomplete(self._arg_parser) | ||
| 525 | |||
| 526 | self.acquire_lock() | ||
| 527 | self.args = self._arg_parser.parse_args() | ||
| 528 | |||
| 529 | if 'entrypoint' in self.args: | ||
| 530 | self._entrypoint = self.args.entrypoint | ||
| 531 | |||
| 532 | self.release_lock() | ||
| 533 | |||
| 534 | def read_config_file(self): | ||
| 535 | """Read in the configuration file and store it in self.config. | ||
| 536 | """ | ||
| 537 | self.acquire_lock() | ||
| 538 | self.config = Configuration() | ||
| 539 | self.config_source = Configuration() | ||
| 540 | self.config_file = self.find_config_file() | ||
| 541 | |||
| 542 | if self.config_file and self.config_file.exists(): | ||
| 543 | config = RawConfigParser(self.config) | ||
| 544 | config.read(str(self.config_file)) | ||
| 545 | |||
| 546 | # Iterate over the config file options and write them into self.config | ||
| 547 | for section in config.sections(): | ||
| 548 | for option in config.options(section): | ||
| 549 | value = config.get(section, option) | ||
| 550 | |||
| 551 | # Coerce values into useful datatypes | ||
| 552 | if value.lower() in ['1', 'yes', 'true', 'on']: | ||
| 553 | value = True | ||
| 554 | elif value.lower() in ['0', 'no', 'false', 'off']: | ||
| 555 | value = False | ||
| 556 | elif value.lower() in ['none']: | ||
| 557 | continue | ||
| 558 | elif value.replace('.', '').isdigit(): | ||
| 559 | if '.' in value: | ||
| 560 | value = Decimal(value) | ||
| 561 | else: | ||
| 562 | value = int(value) | ||
| 563 | |||
| 564 | self.config[section][option] = value | ||
| 565 | self.config_source[section][option] = 'config_file' | ||
| 566 | |||
| 567 | self.release_lock() | ||
| 568 | |||
| 569 | def merge_args_into_config(self): | ||
| 570 | """Merge CLI arguments into self.config to create the runtime configuration. | ||
| 571 | """ | ||
| 572 | self.acquire_lock() | ||
| 573 | for argument in vars(self.args): | ||
| 574 | if argument in ('subparsers', 'entrypoint'): | ||
| 575 | continue | ||
| 576 | |||
| 577 | # Find the argument's section | ||
| 578 | # Underscores in command's names are converted to dashes during initialization. | ||
| 579 | # TODO(Erovia) Find a better solution | ||
| 580 | entrypoint_name = self._entrypoint.__name__.replace("_", "-") | ||
| 581 | if entrypoint_name in self.default_arguments and argument in self.default_arguments[entrypoint_name]: | ||
| 582 | argument_found = True | ||
| 583 | section = self._entrypoint.__name__ | ||
| 584 | if argument in self.default_arguments['general']: | ||
| 585 | argument_found = True | ||
| 586 | section = 'general' | ||
| 587 | |||
| 588 | if not argument_found: | ||
| 589 | raise RuntimeError('Could not find argument in `self.default_arguments`. This should be impossible!') | ||
| 590 | exit(1) | ||
| 591 | |||
| 592 | if argument not in self.arg_only or section not in self.arg_only[argument]: | ||
| 593 | # Determine the arg value and source | ||
| 594 | arg_value = getattr(self.args, argument) | ||
| 595 | if argument in self._config_store_true and arg_value: | ||
| 596 | passed_on_cmdline = True | ||
| 597 | elif argument in self._config_store_false and not arg_value: | ||
| 598 | passed_on_cmdline = True | ||
| 599 | elif arg_value is not None: | ||
| 600 | passed_on_cmdline = True | ||
| 601 | else: | ||
| 602 | passed_on_cmdline = False | ||
| 603 | |||
| 604 | # Merge this argument into self.config | ||
| 605 | if passed_on_cmdline and (argument in self.default_arguments['general'] or argument in self.default_arguments[entrypoint_name] or argument not in self.config[entrypoint_name]): | ||
| 606 | self.config[section][argument] = arg_value | ||
| 607 | self.config_source[section][argument] = 'argument' | ||
| 608 | |||
| 609 | self.release_lock() | ||
| 610 | |||
| 611 | def save_config(self): | ||
| 612 | """Save the current configuration to the config file. | ||
| 613 | """ | ||
| 614 | self.log.debug("Saving config file to '%s'", str(self.config_file)) | ||
| 615 | |||
| 616 | if not self.config_file: | ||
| 617 | self.log.warning('%s.config_file file not set, not saving config!', self.__class__.__name__) | ||
| 618 | return | ||
| 619 | |||
| 620 | self.acquire_lock() | ||
| 621 | |||
| 622 | # Generate a sanitized version of our running configuration | ||
| 623 | config = RawConfigParser() | ||
| 624 | for section_name, section in self.config._config.items(): | ||
| 625 | config.add_section(section_name) | ||
| 626 | for option_name, value in section.items(): | ||
| 627 | if section_name == 'general': | ||
| 628 | if option_name in ['config_file']: | ||
| 629 | continue | ||
| 630 | if value is not None: | ||
| 631 | config.set(section_name, option_name, str(value)) | ||
| 632 | |||
| 633 | # Write out the config file | ||
| 634 | config_dir = self.config_file.parent | ||
| 635 | if not config_dir.exists(): | ||
| 636 | config_dir.mkdir(parents=True, exist_ok=True) | ||
| 637 | |||
| 638 | with NamedTemporaryFile(mode='w', dir=str(config_dir), delete=False) as tmpfile: | ||
| 639 | config.write(tmpfile) | ||
| 640 | |||
| 641 | # Move the new config file into place atomically | ||
| 642 | if os.path.getsize(tmpfile.name) > 0: | ||
| 643 | os.replace(tmpfile.name, str(self.config_file)) | ||
| 644 | else: | ||
| 645 | self.log.warning('Config file saving failed, not replacing %s with %s.', str(self.config_file), tmpfile.name) | ||
| 646 | |||
| 647 | # Housekeeping | ||
| 648 | self.release_lock() | ||
| 649 | cli.log.info('Wrote configuration to %s', shlex.quote(str(self.config_file))) | ||
| 650 | |||
| 651 | def __call__(self): | ||
| 652 | """Execute the entrypoint function. | ||
| 653 | """ | ||
| 654 | if not self._inside_context_manager: | ||
| 655 | # If they didn't use the context manager use it ourselves | ||
| 656 | with self: | ||
| 657 | return self.__call__() | ||
| 658 | |||
| 659 | if not self._entrypoint: | ||
| 660 | raise RuntimeError('No entrypoint provided!') | ||
| 661 | |||
| 662 | return self._entrypoint(self) | ||
| 663 | |||
| 664 | def entrypoint(self, description): | ||
| 665 | """Set the entrypoint for when no subcommand is provided. | ||
| 666 | """ | ||
| 667 | if self._inside_context_manager: | ||
| 668 | raise RuntimeError('You must run this before cli()!') | ||
| 669 | |||
| 670 | self.acquire_lock() | ||
| 671 | self.description = description | ||
| 672 | self.release_lock() | ||
| 673 | |||
| 674 | def entrypoint_func(handler): | ||
| 675 | self.acquire_lock() | ||
| 676 | self._entrypoint = handler | ||
| 677 | self.release_lock() | ||
| 678 | |||
| 679 | return handler | ||
| 680 | |||
| 681 | return entrypoint_func | ||
| 682 | |||
| 683 | def add_subcommand(self, handler, description, name=None, hidden=False, **kwargs): | ||
| 684 | """Register a subcommand. | ||
| 685 | |||
| 686 | If name is not provided we use `handler.__name__`. | ||
| 687 | """ | ||
| 688 | |||
| 689 | if self._inside_context_manager: | ||
| 690 | raise RuntimeError('You must run this before the with statement!') | ||
| 691 | |||
| 692 | if self._subparsers is None: | ||
| 693 | self.add_subparsers(metavar="") | ||
| 694 | |||
| 695 | if not name: | ||
| 696 | name = handler.__name__.replace("_", "-") | ||
| 697 | |||
| 698 | self.acquire_lock() | ||
| 699 | if not hidden: | ||
| 700 | self._subparsers.metavar = "{%s,%s}" % (self._subparsers.metavar[1:-1], name) if self._subparsers.metavar else "{%s%s}" % (self._subparsers.metavar[1:-1], name) | ||
| 701 | kwargs['help'] = description | ||
| 702 | self.subcommands[name] = SubparserWrapper(self, name, self._subparsers.add_parser(name, **kwargs)) | ||
| 703 | self.subcommands[name].set_defaults(entrypoint=handler) | ||
| 704 | |||
| 705 | self.release_lock() | ||
| 706 | |||
| 707 | return handler | ||
| 708 | |||
| 709 | def subcommand(self, description, hidden=False, **kwargs): | ||
| 710 | """Decorator to register a subcommand. | ||
| 711 | """ | ||
| 712 | def subcommand_function(handler): | ||
| 713 | return self.add_subcommand(handler, description, hidden=hidden, **kwargs) | ||
| 714 | |||
| 715 | return subcommand_function | ||
| 716 | |||
| 717 | def setup_logging(self): | ||
| 718 | """Called by __enter__() to setup the logging configuration. | ||
| 719 | """ | ||
| 720 | if len(logging.root.handlers) != 0: | ||
| 721 | # MILC is the only thing that should have root log handlers | ||
| 722 | logging.root.handlers = [] | ||
| 723 | |||
| 724 | self.acquire_lock() | ||
| 725 | |||
| 726 | if self.config['general']['verbose']: | ||
| 727 | self.log_print_level = logging.DEBUG | ||
| 728 | |||
| 729 | self.log_file = self.config['general']['log_file'] or self.log_file | ||
| 730 | self.log_file_format = ANSIStrippingFormatter(self.config['general']['log_file_fmt'], self.config['general']['datetime_fmt']) | ||
| 731 | self.log_format = self.config['general']['log_fmt'] | ||
| 732 | |||
| 733 | if self.config.general.color: | ||
| 734 | self.log_format = ANSIEmojiLoglevelFormatter(self.config.general.log_fmt, self.config.general.datetime_fmt) | ||
| 735 | else: | ||
| 736 | self.log_format = ANSIStrippingEmojiLoglevelFormatter(self.config.general.log_fmt, self.config.general.datetime_fmt) | ||
| 737 | |||
| 738 | if self.log_file: | ||
| 739 | self.log_file_handler = logging.FileHandler(self.log_file, self.log_file_mode) | ||
| 740 | self.log_file_handler.setLevel(self.log_file_level) | ||
| 741 | self.log_file_handler.setFormatter(self.log_file_format) | ||
| 742 | logging.root.addHandler(self.log_file_handler) | ||
| 743 | |||
| 744 | if self.log_print: | ||
| 745 | self.log_print_handler = logging.StreamHandler(self.log_print_to) | ||
| 746 | self.log_print_handler.setLevel(self.log_print_level) | ||
| 747 | self.log_print_handler.setFormatter(self.log_format) | ||
| 748 | logging.root.addHandler(self.log_print_handler) | ||
| 749 | |||
| 750 | self.release_lock() | ||
| 751 | |||
| 752 | def __enter__(self): | ||
| 753 | if self._inside_context_manager: | ||
| 754 | self.log.debug('Warning: context manager was entered again. This usually means that self.__call__() was called before the with statement. You probably do not want to do that.') | ||
| 755 | return | ||
| 756 | |||
| 757 | self.acquire_lock() | ||
| 758 | self._inside_context_manager = True | ||
| 759 | self.release_lock() | ||
| 760 | |||
| 761 | colorama.init() | ||
| 762 | self.parse_args() | ||
| 763 | self.merge_args_into_config() | ||
| 764 | self.setup_logging() | ||
| 765 | |||
| 766 | return self | ||
| 767 | |||
| 768 | def __exit__(self, exc_type, exc_val, exc_tb): | ||
| 769 | self.acquire_lock() | ||
| 770 | self._inside_context_manager = False | ||
| 771 | self.release_lock() | ||
| 772 | |||
| 773 | if exc_type is not None and not isinstance(SystemExit(), exc_type): | ||
| 774 | print(exc_type) | ||
| 775 | logging.exception(exc_val) | ||
| 776 | exit(255) | ||
| 777 | |||
| 778 | |||
| 779 | cli = MILC() | ||
| 780 | |||
| 781 | if __name__ == '__main__': | ||
| 782 | |||
| 783 | @cli.argument('-c', '--comma', help='comma in output', default=True, action='store_boolean') | ||
| 784 | @cli.entrypoint('My useful CLI tool with subcommands.') | ||
| 785 | def main(cli): | ||
| 786 | comma = ',' if cli.config.general.comma else '' | ||
| 787 | cli.log.info('{bg_green}{fg_red}Hello%s World!', comma) | ||
| 788 | |||
| 789 | @cli.argument('-n', '--name', help='Name to greet', default='World') | ||
| 790 | @cli.subcommand('Description of hello subcommand here.') | ||
| 791 | def hello(cli): | ||
| 792 | comma = ',' if cli.config.general.comma else '' | ||
| 793 | cli.log.info('{fg_blue}Hello%s %s!', comma, cli.config.hello.name) | ||
| 794 | |||
| 795 | def goodbye(cli): | ||
| 796 | comma = ',' if cli.config.general.comma else '' | ||
| 797 | cli.log.info('{bg_red}Goodbye%s %s!', comma, cli.config.goodbye.name) | ||
| 798 | |||
| 799 | @cli.argument('-n', '--name', help='Name to greet', default='World') | ||
| 800 | @cli.subcommand('Think a bit before greeting the user.') | ||
| 801 | def thinking(cli): | ||
| 802 | comma = ',' if cli.config.general.comma else '' | ||
| 803 | spinner = cli.spinner(text='Just a moment...', spinner='earth') | ||
| 804 | spinner.start() | ||
| 805 | sleep(2) | ||
| 806 | spinner.stop() | ||
| 807 | |||
| 808 | with cli.spinner(text='Almost there!', spinner='moon'): | ||
| 809 | sleep(2) | ||
| 810 | |||
| 811 | cli.log.info('{fg_cyan}Hello%s %s!', comma, cli.config.thinking.name) | ||
| 812 | |||
| 813 | @cli.subcommand('Show off our ANSI colors.') | ||
| 814 | def pride(cli): | ||
| 815 | cli.echo('{bg_red} ') | ||
| 816 | cli.echo('{bg_lightred_ex} ') | ||
| 817 | cli.echo('{bg_lightyellow_ex} ') | ||
| 818 | cli.echo('{bg_green} ') | ||
| 819 | cli.echo('{bg_blue} ') | ||
| 820 | cli.echo('{bg_magenta} ') | ||
| 821 | |||
| 822 | # You can register subcommands using decorators as seen above, or using functions like like this: | ||
| 823 | cli.add_subcommand(goodbye, 'This will show up in --help output.') | ||
| 824 | cli.goodbye.add_argument('-n', '--name', help='Name to bid farewell to', default='World') | ||
| 825 | |||
| 826 | cli() # Automatically picks between main(), hello() and goodbye() | ||
diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py index 77724a244..10536bb23 100644 --- a/lib/python/qmk/cli/__init__.py +++ b/lib/python/qmk/cli/__init__.py | |||
| @@ -8,6 +8,7 @@ from milc import cli | |||
| 8 | 8 | ||
| 9 | from . import c2json | 9 | from . import c2json |
| 10 | from . import cformat | 10 | from . import cformat |
| 11 | from . import chibios | ||
| 11 | from . import clean | 12 | from . import clean |
| 12 | from . import compile | 13 | from . import compile |
| 13 | from . import config | 14 | from . import config |
diff --git a/lib/python/qmk/cli/chibios/__init__.py b/lib/python/qmk/cli/chibios/__init__.py new file mode 100644 index 000000000..4301837de --- /dev/null +++ b/lib/python/qmk/cli/chibios/__init__.py | |||
| @@ -0,0 +1 @@ | |||
| from . import confmigrate | |||
diff --git a/lib/python/qmk/cli/chibios/confmigrate.py b/lib/python/qmk/cli/chibios/confmigrate.py new file mode 100644 index 000000000..eae294a0c --- /dev/null +++ b/lib/python/qmk/cli/chibios/confmigrate.py | |||
| @@ -0,0 +1,161 @@ | |||
| 1 | """This script automates the copying of the default keymap into your own keymap. | ||
| 2 | """ | ||
| 3 | import re | ||
| 4 | import sys | ||
| 5 | import os | ||
| 6 | |||
| 7 | from qmk.constants import QMK_FIRMWARE | ||
| 8 | from qmk.path import normpath | ||
| 9 | from milc import cli | ||
| 10 | |||
| 11 | |||
| 12 | def eprint(*args, **kwargs): | ||
| 13 | print(*args, file=sys.stderr, **kwargs) | ||
| 14 | |||
| 15 | |||
| 16 | fileHeader = """\ | ||
| 17 | /* Copyright 2020 QMK | ||
| 18 | * | ||
| 19 | * This program is free software: you can redistribute it and/or modify | ||
| 20 | * it under the terms of the GNU General Public License as published by | ||
| 21 | * the Free Software Foundation, either version 2 of the License, or | ||
| 22 | * (at your option) any later version. | ||
| 23 | * | ||
| 24 | * This program is distributed in the hope that it will be useful, | ||
| 25 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 26 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 27 | * GNU General Public License for more details. | ||
| 28 | * | ||
| 29 | * You should have received a copy of the GNU General Public License | ||
| 30 | * along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
| 31 | */ | ||
| 32 | |||
| 33 | /* | ||
| 34 | * This file was auto-generated by: | ||
| 35 | * `qmk chibios-confupdate -i {0} -r {1}` | ||
| 36 | */ | ||
| 37 | |||
| 38 | #pragma once | ||
| 39 | """ | ||
| 40 | |||
| 41 | |||
| 42 | def collect_defines(filepath): | ||
| 43 | with open(filepath, 'r') as f: | ||
| 44 | content = f.read() | ||
| 45 | define_search = re.compile(r'(?m)^#\s*define\s+(?:.*\\\r?\n)*.*$', re.MULTILINE) | ||
| 46 | value_search = re.compile(r'^#\s*define\s+(?P<name>[a-zA-Z0-9_]+(\([^\)]*\))?)\s*(?P<value>.*)', re.DOTALL) | ||
| 47 | define_matches = define_search.findall(content) | ||
| 48 | |||
| 49 | defines = {"keys": [], "dict": {}} | ||
| 50 | for define_match in define_matches: | ||
| 51 | value_match = value_search.search(define_match) | ||
| 52 | defines["keys"].append(value_match.group("name")) | ||
| 53 | defines["dict"][value_match.group("name")] = value_match.group("value") | ||
| 54 | return defines | ||
| 55 | |||
| 56 | |||
| 57 | def check_diffs(input_defs, reference_defs): | ||
| 58 | not_present_in_input = [] | ||
| 59 | not_present_in_reference = [] | ||
| 60 | to_override = [] | ||
| 61 | |||
| 62 | for key in reference_defs["keys"]: | ||
| 63 | if key not in input_defs["dict"]: | ||
| 64 | not_present_in_input.append(key) | ||
| 65 | continue | ||
| 66 | |||
| 67 | for key in input_defs["keys"]: | ||
| 68 | if key not in input_defs["dict"]: | ||
| 69 | not_present_in_input.append(key) | ||
| 70 | continue | ||
| 71 | |||
| 72 | for key in input_defs["keys"]: | ||
| 73 | if key in reference_defs["keys"] and input_defs["dict"][key] != reference_defs["dict"][key]: | ||
| 74 | to_override.append((key, input_defs["dict"][key])) | ||
| 75 | |||
| 76 | return (to_override, not_present_in_input, not_present_in_reference) | ||
| 77 | |||
| 78 | |||
| 79 | def migrate_chconf_h(to_override, outfile): | ||
| 80 | print(fileHeader.format(cli.args.input.relative_to(QMK_FIRMWARE), cli.args.reference.relative_to(QMK_FIRMWARE)), file=outfile) | ||
| 81 | |||
| 82 | for override in to_override: | ||
| 83 | print("#define %s %s" % (override[0], override[1]), file=outfile) | ||
| 84 | print("", file=outfile) | ||
| 85 | |||
| 86 | print("#include_next <chconf.h>\n", file=outfile) | ||
| 87 | |||
| 88 | |||
| 89 | def migrate_halconf_h(to_override, outfile): | ||
| 90 | print(fileHeader.format(cli.args.input.relative_to(QMK_FIRMWARE), cli.args.reference.relative_to(QMK_FIRMWARE)), file=outfile) | ||
| 91 | |||
| 92 | for override in to_override: | ||
| 93 | print("#define %s %s" % (override[0], override[1]), file=outfile) | ||
| 94 | print("", file=outfile) | ||
| 95 | |||
| 96 | print("#include_next <halconf.h>\n", file=outfile) | ||
| 97 | |||
| 98 | |||
| 99 | def migrate_mcuconf_h(to_override, outfile): | ||
| 100 | print(fileHeader.format(cli.args.input.relative_to(QMK_FIRMWARE), cli.args.reference.relative_to(QMK_FIRMWARE)), file=outfile) | ||
| 101 | |||
| 102 | print("#include_next <mcuconf.h>\n", file=outfile) | ||
| 103 | |||
| 104 | for override in to_override: | ||
| 105 | print("#undef %s" % (override[0]), file=outfile) | ||
| 106 | print("#define %s %s" % (override[0], override[1]), file=outfile) | ||
| 107 | print("", file=outfile) | ||
| 108 | |||
| 109 | |||
| 110 | @cli.argument('-i', '--input', type=normpath, arg_only=True, help='Specify input config file.') | ||
| 111 | @cli.argument('-r', '--reference', type=normpath, arg_only=True, help='Specify the reference file to compare against') | ||
| 112 | @cli.argument('-o', '--overwrite', arg_only=True, action='store_true', help='Overwrites the input file during migration.') | ||
| 113 | @cli.argument('-d', '--delete', arg_only=True, action='store_true', help='If the file has no overrides, migration will delete the input file.') | ||
| 114 | @cli.subcommand('Generates a migrated ChibiOS configuration file, as a result of comparing the input against a reference') | ||
| 115 | def chibios_confmigrate(cli): | ||
| 116 | """Generates a usable ChibiOS replacement configuration file, based on a fully-defined conf and a reference config. | ||
| 117 | """ | ||
| 118 | |||
| 119 | input_defs = collect_defines(cli.args.input) | ||
| 120 | reference_defs = collect_defines(cli.args.reference) | ||
| 121 | |||
| 122 | (to_override, not_present_in_input, not_present_in_reference) = check_diffs(input_defs, reference_defs) | ||
| 123 | |||
| 124 | if len(not_present_in_input) > 0: | ||
| 125 | eprint("Keys not in input, but present inside reference (potential manual migration required):") | ||
| 126 | for key in not_present_in_input: | ||
| 127 | eprint(" %s" % (key)) | ||
| 128 | |||
| 129 | if len(not_present_in_reference) > 0: | ||
| 130 | eprint("Keys not in reference, but present inside input (potential manual migration required):") | ||
| 131 | for key in not_present_in_reference: | ||
| 132 | eprint(" %s" % (key)) | ||
| 133 | |||
| 134 | if len(to_override) == 0: | ||
| 135 | eprint('No overrides found! If there were no missing keys above, it should be safe to delete the input file.') | ||
| 136 | if cli.args.delete: | ||
| 137 | os.remove(cli.args.input) | ||
| 138 | else: | ||
| 139 | eprint('Overrides found:') | ||
| 140 | for override in to_override: | ||
| 141 | eprint("%40s: %s -> %s" % (override[0], reference_defs["dict"][override[0]].encode('unicode_escape').decode("utf-8"), override[1].encode('unicode_escape').decode("utf-8"))) | ||
| 142 | |||
| 143 | eprint('--------------------------------------') | ||
| 144 | |||
| 145 | if "CHCONF_H" in input_defs["dict"] or "_CHCONF_H_" in input_defs["dict"]: | ||
| 146 | migrate_chconf_h(to_override, outfile=sys.stdout) | ||
| 147 | if cli.args.overwrite: | ||
| 148 | with open(cli.args.input, "w") as out_file: | ||
| 149 | migrate_chconf_h(to_override, outfile=out_file) | ||
| 150 | |||
| 151 | elif "HALCONF_H" in input_defs["dict"] or "_HALCONF_H_" in input_defs["dict"]: | ||
| 152 | migrate_halconf_h(to_override, outfile=sys.stdout) | ||
| 153 | if cli.args.overwrite: | ||
| 154 | with open(cli.args.input, "w") as out_file: | ||
| 155 | migrate_halconf_h(to_override, outfile=out_file) | ||
| 156 | |||
| 157 | elif "MCUCONF_H" in input_defs["dict"] or "_MCUCONF_H_" in input_defs["dict"]: | ||
| 158 | migrate_mcuconf_h(to_override, outfile=sys.stdout) | ||
| 159 | if cli.args.overwrite: | ||
| 160 | with open(cli.args.input, "w") as out_file: | ||
| 161 | migrate_mcuconf_h(to_override, outfile=out_file) | ||
diff --git a/lib/python/qmk/cli/doctor.py b/lib/python/qmk/cli/doctor.py index a5eda555f..4fe318b63 100755 --- a/lib/python/qmk/cli/doctor.py +++ b/lib/python/qmk/cli/doctor.py | |||
| @@ -10,9 +10,9 @@ from pathlib import Path | |||
| 10 | from enum import Enum | 10 | from enum import Enum |
| 11 | 11 | ||
| 12 | from milc import cli | 12 | from milc import cli |
| 13 | from milc.questions import yesno | ||
| 13 | from qmk import submodules | 14 | from qmk import submodules |
| 14 | from qmk.constants import QMK_FIRMWARE | 15 | from qmk.constants import QMK_FIRMWARE |
| 15 | from qmk.questions import yesno | ||
| 16 | from qmk.commands import run | 16 | from qmk.commands import run |
| 17 | 17 | ||
| 18 | 18 | ||
diff --git a/lib/python/qmk/questions.py b/lib/python/qmk/questions.py deleted file mode 100644 index 865c6bbdc..000000000 --- a/lib/python/qmk/questions.py +++ /dev/null | |||
| @@ -1,183 +0,0 @@ | |||
| 1 | """Functions to collect user input. | ||
| 2 | """ | ||
| 3 | |||
| 4 | from milc import cli | ||
| 5 | |||
| 6 | try: | ||
| 7 | from milc import format_ansi | ||
| 8 | except ImportError: | ||
| 9 | from milc.ansi import format_ansi | ||
| 10 | |||
| 11 | |||
| 12 | def yesno(prompt, *args, default=None, **kwargs): | ||
| 13 | """Displays prompt to the user and gets a yes or no response. | ||
| 14 | |||
| 15 | Returns True for a yes and False for a no. | ||
| 16 | |||
| 17 | If you add `--yes` and `--no` arguments to your program the user can answer questions by passing command line flags. | ||
| 18 | |||
| 19 | @add_argument('-y', '--yes', action='store_true', arg_only=True, help='Answer yes to all questions.') | ||
| 20 | @add_argument('-n', '--no', action='store_true', arg_only=True, help='Answer no to all questions.') | ||
| 21 | |||
| 22 | Arguments: | ||
| 23 | prompt | ||
| 24 | The prompt to present to the user. Can include ANSI and format strings like milc's `cli.echo()`. | ||
| 25 | |||
| 26 | default | ||
| 27 | Whether to default to a Yes or No when the user presses enter. | ||
| 28 | |||
| 29 | None- force the user to enter Y or N | ||
| 30 | |||
| 31 | True- Default to yes | ||
| 32 | |||
| 33 | False- Default to no | ||
| 34 | """ | ||
| 35 | if not args and kwargs: | ||
| 36 | args = kwargs | ||
| 37 | |||
| 38 | if 'no' in cli.args and cli.args.no: | ||
| 39 | return False | ||
| 40 | |||
| 41 | if 'yes' in cli.args and cli.args.yes: | ||
| 42 | return True | ||
| 43 | |||
| 44 | if default is not None: | ||
| 45 | if default: | ||
| 46 | prompt = prompt + ' [Y/n] ' | ||
| 47 | else: | ||
| 48 | prompt = prompt + ' [y/N] ' | ||
| 49 | |||
| 50 | while True: | ||
| 51 | cli.echo('') | ||
| 52 | answer = input(format_ansi(prompt % args)) | ||
| 53 | cli.echo('') | ||
| 54 | |||
| 55 | if not answer and prompt is not None: | ||
| 56 | return default | ||
| 57 | |||
| 58 | elif answer.lower() in ['y', 'yes']: | ||
| 59 | return True | ||
| 60 | |||
| 61 | elif answer.lower() in ['n', 'no']: | ||
| 62 | return False | ||
| 63 | |||
| 64 | |||
| 65 | def question(prompt, *args, default=None, confirm=False, answer_type=str, validate=None, **kwargs): | ||
| 66 | """Prompt the user to answer a question with a free-form input. | ||
| 67 | |||
| 68 | Arguments: | ||
| 69 | prompt | ||
| 70 | The prompt to present to the user. Can include ANSI and format strings like milc's `cli.echo()`. | ||
| 71 | |||
| 72 | default | ||
| 73 | The value to return when the user doesn't enter any value. Use None to prompt until they enter a value. | ||
| 74 | |||
| 75 | confirm | ||
| 76 | Present the user with a confirmation dialog before accepting their answer. | ||
| 77 | |||
| 78 | answer_type | ||
| 79 | Specify a type function for the answer. Will re-prompt the user if the function raises any errors. Common choices here include int, float, and decimal.Decimal. | ||
| 80 | |||
| 81 | validate | ||
| 82 | This is an optional function that can be used to validate the answer. It should return True or False and have the following signature: | ||
| 83 | |||
| 84 | def function_name(answer, *args, **kwargs): | ||
| 85 | """ | ||
| 86 | if not args and kwargs: | ||
| 87 | args = kwargs | ||
| 88 | |||
| 89 | if default is not None: | ||
| 90 | prompt = '%s [%s] ' % (prompt, default) | ||
| 91 | |||
| 92 | while True: | ||
| 93 | cli.echo('') | ||
| 94 | answer = input(format_ansi(prompt % args)) | ||
| 95 | cli.echo('') | ||
| 96 | |||
| 97 | if answer: | ||
| 98 | if validate is not None and not validate(answer, *args, **kwargs): | ||
| 99 | continue | ||
| 100 | |||
| 101 | elif confirm: | ||
| 102 | if yesno('Is the answer "%s" correct?', answer, default=True): | ||
| 103 | try: | ||
| 104 | return answer_type(answer) | ||
| 105 | except Exception as e: | ||
| 106 | cli.log.error('Could not convert answer (%s) to type %s: %s', answer, answer_type.__name__, str(e)) | ||
| 107 | |||
| 108 | else: | ||
| 109 | try: | ||
| 110 | return answer_type(answer) | ||
| 111 | except Exception as e: | ||
| 112 | cli.log.error('Could not convert answer (%s) to type %s: %s', answer, answer_type.__name__, str(e)) | ||
| 113 | |||
| 114 | elif default is not None: | ||
| 115 | return default | ||
| 116 | |||
| 117 | |||
| 118 | def choice(heading, options, *args, default=None, confirm=False, prompt='Please enter your choice: ', **kwargs): | ||
| 119 | """Present the user with a list of options and let them pick one. | ||
| 120 | |||
| 121 | Users can enter either the number or the text of their choice. | ||
| 122 | |||
| 123 | This will return the value of the item they choose, not the numerical index. | ||
| 124 | |||
| 125 | Arguments: | ||
| 126 | heading | ||
| 127 | The text to place above the list of options. | ||
| 128 | |||
| 129 | options | ||
| 130 | A sequence of items to choose from. | ||
| 131 | |||
| 132 | default | ||
| 133 | The index of the item to return when the user doesn't enter any value. Use None to prompt until they enter a value. | ||
| 134 | |||
| 135 | confirm | ||
| 136 | Present the user with a confirmation dialog before accepting their answer. | ||
| 137 | |||
| 138 | prompt | ||
| 139 | The prompt to present to the user. Can include ANSI and format strings like milc's `cli.echo()`. | ||
| 140 | """ | ||
| 141 | if not args and kwargs: | ||
| 142 | args = kwargs | ||
| 143 | |||
| 144 | if prompt and default: | ||
| 145 | prompt = prompt + ' [%s] ' % (default + 1,) | ||
| 146 | |||
| 147 | while True: | ||
| 148 | # Prompt for an answer. | ||
| 149 | cli.echo('') | ||
| 150 | cli.echo(heading % args) | ||
| 151 | cli.echo('') | ||
| 152 | for i, option in enumerate(options, 1): | ||
| 153 | cli.echo('\t{fg_cyan}%d.{fg_reset} %s', i, option) | ||
| 154 | |||
| 155 | cli.echo('') | ||
| 156 | answer = input(format_ansi(prompt)) | ||
| 157 | cli.echo('') | ||
| 158 | |||
| 159 | # If the user types in one of the options exactly use that | ||
| 160 | if answer in options: | ||
| 161 | return answer | ||
| 162 | |||
| 163 | # Massage the answer into a valid integer | ||
| 164 | if answer == '' and default: | ||
| 165 | answer = default | ||
| 166 | else: | ||
| 167 | try: | ||
| 168 | answer = int(answer) - 1 | ||
| 169 | except Exception: | ||
| 170 | # Normally we would log the exception here, but in the interest of clean UI we do not. | ||
| 171 | cli.log.error('Invalid choice: %s', answer + 1) | ||
| 172 | continue | ||
| 173 | |||
| 174 | # Validate the answer | ||
| 175 | if answer >= len(options) or answer < 0: | ||
| 176 | cli.log.error('Invalid choice: %s', answer + 1) | ||
| 177 | continue | ||
| 178 | |||
| 179 | if confirm and not yesno('Is the answer "%s" correct?', answer + 1, default=True): | ||
| 180 | continue | ||
| 181 | |||
| 182 | # Return the answer they chose. | ||
| 183 | return options[answer] | ||
