aboutsummaryrefslogtreecommitdiff
path: root/lib/python
diff options
context:
space:
mode:
authorJames Young <18669334+noroadsleft@users.noreply.github.com>2020-11-28 12:02:18 -0800
committerGitHub <noreply@github.com>2020-11-28 12:02:18 -0800
commitc66df1664497546f32662409778731143e45a552 (patch)
treeda73a2d532a27685a31d932b3a44a707d4a3af81 /lib/python
parent15385d4113414d42bd062c60c9de5df797d3157f (diff)
downloadqmk_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/python')
-rw-r--r--lib/python/milc.py826
-rw-r--r--lib/python/qmk/cli/__init__.py1
-rw-r--r--lib/python/qmk/cli/chibios/__init__.py1
-rw-r--r--lib/python/qmk/cli/chibios/confmigrate.py161
-rwxr-xr-xlib/python/qmk/cli/doctor.py2
-rw-r--r--lib/python/qmk/questions.py183
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
5PYTHON_ARGCOMPLETE_OK
6
7MILC is an opinionated framework for writing CLI apps. It optimizes for the
8most common unix tool pattern- small tools that are run from the command
9line but generally do not feature any user interaction while they run.
10
11For more details see the MILC documentation:
12
13 <https://github.com/clueboard/milc/tree/master/docs>
14"""
15from __future__ import division, print_function, unicode_literals
16import argparse
17import logging
18import os
19import re
20import shlex
21import subprocess
22import sys
23from decimal import Decimal
24from pathlib import Path
25from platform import platform
26from tempfile import NamedTemporaryFile
27from time import sleep
28
29try:
30 from ConfigParser import RawConfigParser
31except ImportError:
32 from configparser import RawConfigParser
33
34try:
35 import thread
36 import threading
37except ImportError:
38 thread = None
39
40import argcomplete
41import colorama
42from appdirs import user_config_dir
43
44# Disable logging until we can configure it how the user wants
45logging.basicConfig(stream=os.devnull)
46
47# Log Level Representations
48EMOJI_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}
56EMOJI_LOGLEVELS['FATAL'] = EMOJI_LOGLEVELS['CRITICAL']
57EMOJI_LOGLEVELS['WARN'] = EMOJI_LOGLEVELS['WARNING']
58UNICODE_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
63ansi_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))'
77ansi_escape = re.compile(ansi_regex, flags=re.IGNORECASE)
78ansi_styles = (
79 ('fg', colorama.ansi.AnsiFore()),
80 ('bg', colorama.ansi.AnsiBack()),
81 ('style', colorama.ansi.AnsiStyle()),
82)
83ansi_colors = {}
84
85for 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
90def 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
99class 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
107class 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
116class 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
125class ANSIFormatter(ANSIFormatterMixin, logging.Formatter):
126 """A log formatter that colorizes output.
127 """
128 pass
129
130
131class ANSIStrippingFormatter(ANSIStrippingMixin, ANSIFormatterMixin, logging.Formatter):
132 """A log formatter that strips ANSI
133 """
134 pass
135
136
137class ANSIEmojiLoglevelFormatter(EmojiLoglevelMixin, ANSIFormatterMixin, logging.Formatter):
138 """A log formatter that adds Emoji and ANSI
139 """
140 pass
141
142
143class ANSIStrippingEmojiLoglevelFormatter(ANSIStrippingMixin, EmojiLoglevelMixin, ANSIFormatterMixin, logging.Formatter):
144 """A log formatter that adds Emoji and strips ANSI
145 """
146 pass
147
148
149class 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
201class 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
228def 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
250class 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
293class 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
779cli = MILC()
780
781if __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
9from . import c2json 9from . import c2json
10from . import cformat 10from . import cformat
11from . import chibios
11from . import clean 12from . import clean
12from . import compile 13from . import compile
13from . import config 14from . 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"""
3import re
4import sys
5import os
6
7from qmk.constants import QMK_FIRMWARE
8from qmk.path import normpath
9from milc import cli
10
11
12def eprint(*args, **kwargs):
13 print(*args, file=sys.stderr, **kwargs)
14
15
16fileHeader = """\
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
42def 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
57def 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
79def 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
89def 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
99def 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')
115def 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
10from enum import Enum 10from enum import Enum
11 11
12from milc import cli 12from milc import cli
13from milc.questions import yesno
13from qmk import submodules 14from qmk import submodules
14from qmk.constants import QMK_FIRMWARE 15from qmk.constants import QMK_FIRMWARE
15from qmk.questions import yesno
16from qmk.commands import run 16from 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
4from milc import cli
5
6try:
7 from milc import format_ansi
8except ImportError:
9 from milc.ansi import format_ansi
10
11
12def 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
65def 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
118def 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]