aboutsummaryrefslogtreecommitdiff
path: root/lib/python/milc.py
diff options
context:
space:
mode:
authorskullydazed <skullydazed@users.noreply.github.com>2019-11-18 14:54:50 -0800
committerGitHub <noreply@github.com>2019-11-18 14:54:50 -0800
commit9c58da6b121ca36f616f8a9a78dd4c2234bd5942 (patch)
tree3ea17693bdc689f5174aefb9dbfd908130761c1f /lib/python/milc.py
parentb608bddc5edae7ccce8108d9c7777437160f3cb3 (diff)
downloadqmk_firmware-9c58da6b121ca36f616f8a9a78dd4c2234bd5942.tar.gz
qmk_firmware-9c58da6b121ca36f616f8a9a78dd4c2234bd5942.zip
Improve a number of things about how MILC operates (#7344)
* Pull in updates for MILC * Remove the shadow argparser * Make it easier to reason about arguments and how they're translated into the config tree * Populate self.config during init to support setting user.qmk_home for the global CLI * Remove the short argument -c so that we can unambiguously determine the config file location without doing full argument processing * Remove the --save-config option as it's a little confusing anyway * Use Pathlib for path manipulation * Fix commands with no arguments
Diffstat (limited to 'lib/python/milc.py')
-rw-r--r--lib/python/milc.py167
1 files changed, 82 insertions, 85 deletions
diff --git a/lib/python/milc.py b/lib/python/milc.py
index 7b130bdea..e8599eff3 100644
--- a/lib/python/milc.py
+++ b/lib/python/milc.py
@@ -20,6 +20,7 @@ import re
20import shlex 20import shlex
21import sys 21import sys
22from decimal import Decimal 22from decimal import Decimal
23from pathlib import Path
23from tempfile import NamedTemporaryFile 24from tempfile import NamedTemporaryFile
24from time import sleep 25from time import sleep
25 26
@@ -39,7 +40,7 @@ import colorama
39from appdirs import user_config_dir 40from appdirs import user_config_dir
40 41
41# Disable logging until we can configure it how the user wants 42# Disable logging until we can configure it how the user wants
42logging.basicConfig(filename='/dev/null') 43logging.basicConfig(stream=os.devnull)
43 44
44# Log Level Representations 45# Log Level Representations
45EMOJI_LOGLEVELS = { 46EMOJI_LOGLEVELS = {
@@ -96,7 +97,6 @@ def format_ansi(text):
96class ANSIFormatter(logging.Formatter): 97class ANSIFormatter(logging.Formatter):
97 """A log formatter that inserts ANSI color. 98 """A log formatter that inserts ANSI color.
98 """ 99 """
99
100 def format(self, record): 100 def format(self, record):
101 msg = super(ANSIFormatter, self).format(record) 101 msg = super(ANSIFormatter, self).format(record)
102 return format_ansi(msg) 102 return format_ansi(msg)
@@ -105,7 +105,6 @@ class ANSIFormatter(logging.Formatter):
105class ANSIEmojiLoglevelFormatter(ANSIFormatter): 105class ANSIEmojiLoglevelFormatter(ANSIFormatter):
106 """A log formatter that makes the loglevel an emoji on UTF capable terminals. 106 """A log formatter that makes the loglevel an emoji on UTF capable terminals.
107 """ 107 """
108
109 def format(self, record): 108 def format(self, record):
110 if UNICODE_SUPPORT: 109 if UNICODE_SUPPORT:
111 record.levelname = EMOJI_LOGLEVELS[record.levelname].format(**ansi_colors) 110 record.levelname = EMOJI_LOGLEVELS[record.levelname].format(**ansi_colors)
@@ -115,7 +114,6 @@ class ANSIEmojiLoglevelFormatter(ANSIFormatter):
115class ANSIStrippingFormatter(ANSIFormatter): 114class ANSIStrippingFormatter(ANSIFormatter):
116 """A log formatter that strips ANSI. 115 """A log formatter that strips ANSI.
117 """ 116 """
118
119 def format(self, record): 117 def format(self, record):
120 msg = super(ANSIStrippingFormatter, self).format(record) 118 msg = super(ANSIStrippingFormatter, self).format(record)
121 return ansi_escape.sub('', msg) 119 return ansi_escape.sub('', msg)
@@ -127,7 +125,6 @@ class Configuration(object):
127 This class never raises IndexError, instead it will return None if a 125 This class never raises IndexError, instead it will return None if a
128 section or option does not yet exist. 126 section or option does not yet exist.
129 """ 127 """
130
131 def __contains__(self, key): 128 def __contains__(self, key):
132 return self._config.__contains__(key) 129 return self._config.__contains__(key)
133 130
@@ -214,9 +211,8 @@ def handle_store_boolean(self, *args, **kwargs):
214 211
215 212
216class SubparserWrapper(object): 213class SubparserWrapper(object):
217 """Wrap subparsers so we can populate the normal and the shadow parser. 214 """Wrap subparsers so we can track what options the user passed.
218 """ 215 """
219
220 def __init__(self, cli, submodule, subparser): 216 def __init__(self, cli, submodule, subparser):
221 self.cli = cli 217 self.cli = cli
222 self.submodule = submodule 218 self.submodule = submodule
@@ -232,26 +228,30 @@ class SubparserWrapper(object):
232 self.subparser.completer = completer 228 self.subparser.completer = completer
233 229
234 def add_argument(self, *args, **kwargs): 230 def add_argument(self, *args, **kwargs):
231 """Add an argument for this subcommand.
232
233 This also stores the default for the argument in `self.cli.default_arguments`.
234 """
235 if 'action' in kwargs and kwargs['action'] == 'store_boolean': 235 if 'action' in kwargs and kwargs['action'] == 'store_boolean':
236 # Store boolean will call us again with the enable/disable flag arguments
236 return handle_store_boolean(self, *args, **kwargs) 237 return handle_store_boolean(self, *args, **kwargs)
237 238
238 self.cli.acquire_lock() 239 self.cli.acquire_lock()
239 self.subparser.add_argument(*args, **kwargs) 240 self.subparser.add_argument(*args, **kwargs)
240 241 if self.submodule not in self.cli.default_arguments:
241 if 'default' in kwargs: 242 self.cli.default_arguments[self.submodule] = {}
242 del kwargs['default'] 243 self.cli.default_arguments[self.submodule][self.cli.get_argument_name(*args, **kwargs)] = kwargs.get('default')
243 if 'action' in kwargs and kwargs['action'] == 'store_false':
244 kwargs['action'] == 'store_true'
245 self.cli.subcommands_default[self.submodule].add_argument(*args, **kwargs)
246 self.cli.release_lock() 244 self.cli.release_lock()
247 245
248 246
249class MILC(object): 247class MILC(object):
250 """MILC - An Opinionated Batteries Included Framework 248 """MILC - An Opinionated Batteries Included Framework
251 """ 249 """
252
253 def __init__(self): 250 def __init__(self):
254 """Initialize the MILC object. 251 """Initialize the MILC object.
252
253 version
254 The version string to associate with your CLI program
255 """ 255 """
256 # Setup a lock for thread safety 256 # Setup a lock for thread safety
257 self._lock = threading.RLock() if thread else None 257 self._lock = threading.RLock() if thread else None
@@ -263,9 +263,10 @@ class MILC(object):
263 self._inside_context_manager = False 263 self._inside_context_manager = False
264 self.ansi = ansi_colors 264 self.ansi = ansi_colors
265 self.arg_only = [] 265 self.arg_only = []
266 self.config = Configuration() 266 self.config = None
267 self.config_file = None 267 self.config_file = None
268 self.version = os.environ.get('QMK_VERSION', 'unknown') 268 self.default_arguments = {}
269 self.version = 'unknown'
269 self.release_lock() 270 self.release_lock()
270 271
271 # Figure out our program name 272 # Figure out our program name
@@ -273,6 +274,7 @@ class MILC(object):
273 self.prog_name = self.prog_name.split('/')[-1] 274 self.prog_name = self.prog_name.split('/')[-1]
274 275
275 # Initialize all the things 276 # Initialize all the things
277 self.read_config_file()
276 self.initialize_argparse() 278 self.initialize_argparse()
277 self.initialize_logging() 279 self.initialize_logging()
278 280
@@ -282,7 +284,7 @@ class MILC(object):
282 284
283 @description.setter 285 @description.setter
284 def description(self, value): 286 def description(self, value):
285 self._description = self._arg_parser.description = self._arg_defaults.description = value 287 self._description = self._arg_parser.description = value
286 288
287 def echo(self, text, *args, **kwargs): 289 def echo(self, text, *args, **kwargs):
288 """Print colorized text to stdout. 290 """Print colorized text to stdout.
@@ -311,12 +313,9 @@ class MILC(object):
311 313
312 self.acquire_lock() 314 self.acquire_lock()
313 self.subcommands = {} 315 self.subcommands = {}
314 self.subcommands_default = {}
315 self._subparsers = None 316 self._subparsers = None
316 self._subparsers_default = None
317 self.argwarn = argcomplete.warn 317 self.argwarn = argcomplete.warn
318 self.args = None 318 self.args = None
319 self._arg_defaults = argparse.ArgumentParser(**kwargs)
320 self._arg_parser = argparse.ArgumentParser(**kwargs) 319 self._arg_parser = argparse.ArgumentParser(**kwargs)
321 self.set_defaults = self._arg_parser.set_defaults 320 self.set_defaults = self._arg_parser.set_defaults
322 self.print_usage = self._arg_parser.print_usage 321 self.print_usage = self._arg_parser.print_usage
@@ -329,25 +328,18 @@ class MILC(object):
329 self._arg_parser.completer = completer 328 self._arg_parser.completer = completer
330 329
331 def add_argument(self, *args, **kwargs): 330 def add_argument(self, *args, **kwargs):
332 """Wrapper to add arguments to both the main and the shadow argparser. 331 """Wrapper to add arguments and track whether they were passed on the command line.
333 """ 332 """
334 if 'action' in kwargs and kwargs['action'] == 'store_boolean': 333 if 'action' in kwargs and kwargs['action'] == 'store_boolean':
335 return handle_store_boolean(self, *args, **kwargs) 334 return handle_store_boolean(self, *args, **kwargs)
336 335
337 if kwargs.get('add_dest', True) and args[0][0] == '-':
338 kwargs['dest'] = 'general_' + self.get_argument_name(*args, **kwargs)
339 if 'add_dest' in kwargs:
340 del kwargs['add_dest']
341
342 self.acquire_lock() 336 self.acquire_lock()
337
343 self._arg_parser.add_argument(*args, **kwargs) 338 self._arg_parser.add_argument(*args, **kwargs)
339 if 'general' not in self.default_arguments:
340 self.default_arguments['general'] = {}
341 self.default_arguments['general'][self.get_argument_name(*args, **kwargs)] = kwargs.get('default')
344 342
345 # Populate the shadow parser
346 if 'default' in kwargs:
347 del kwargs['default']
348 if 'action' in kwargs and kwargs['action'] == 'store_false':
349 kwargs['action'] == 'store_true'
350 self._arg_defaults.add_argument(*args, **kwargs)
351 self.release_lock() 343 self.release_lock()
352 344
353 def initialize_logging(self): 345 def initialize_logging(self):
@@ -374,15 +366,14 @@ class MILC(object):
374 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.') 366 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.')
375 self.add_argument('--log-file', help='File to write log messages to') 367 self.add_argument('--log-file', help='File to write log messages to')
376 self.add_argument('--color', action='store_boolean', default=True, help='color in output') 368 self.add_argument('--color', action='store_boolean', default=True, help='color in output')
377 self.add_argument('-c', '--config-file', help='The config file to read and/or write') 369 self.add_argument('--config-file', help='The location for the configuration file')
378 self.add_argument('--save-config', action='store_true', help='Save the running configuration to the config file') 370 self.arg_only.append('config_file')
379 371
380 def add_subparsers(self, title='Sub-commands', **kwargs): 372 def add_subparsers(self, title='Sub-commands', **kwargs):
381 if self._inside_context_manager: 373 if self._inside_context_manager:
382 raise RuntimeError('You must run this before the with statement!') 374 raise RuntimeError('You must run this before the with statement!')
383 375
384 self.acquire_lock() 376 self.acquire_lock()
385 self._subparsers_default = self._arg_defaults.add_subparsers(title=title, dest='subparsers', **kwargs)
386 self._subparsers = self._arg_parser.add_subparsers(title=title, dest='subparsers', **kwargs) 377 self._subparsers = self._arg_parser.add_subparsers(title=title, dest='subparsers', **kwargs)
387 self.release_lock() 378 self.release_lock()
388 379
@@ -404,10 +395,12 @@ class MILC(object):
404 if self.config_file: 395 if self.config_file:
405 return self.config_file 396 return self.config_file
406 397
407 if self.args and self.args.general_config_file: 398 if '--config-file' in sys.argv:
408 return self.args.general_config_file 399 return Path(sys.argv[sys.argv.index('--config-file') + 1]).expanduser().resolve()
409 400
410 return os.path.join(user_config_dir(appname='qmk', appauthor='QMK'), '%s.ini' % self.prog_name) 401 filedir = user_config_dir(appname='qmk', appauthor='QMK')
402 filename = '%s.ini' % self.prog_name
403 return Path(filedir) / filename
411 404
412 def get_argument_name(self, *args, **kwargs): 405 def get_argument_name(self, *args, **kwargs):
413 """Takes argparse arguments and returns the dest name. 406 """Takes argparse arguments and returns the dest name.
@@ -446,7 +439,7 @@ class MILC(object):
446 def arg_passed(self, arg): 439 def arg_passed(self, arg):
447 """Returns True if arg was passed on the command line. 440 """Returns True if arg was passed on the command line.
448 """ 441 """
449 return self.args_passed[arg] in (None, False) 442 return self.default_arguments.get(arg) != self.args[arg]
450 443
451 def parse_args(self): 444 def parse_args(self):
452 """Parse the CLI args. 445 """Parse the CLI args.
@@ -459,25 +452,22 @@ class MILC(object):
459 452
460 self.acquire_lock() 453 self.acquire_lock()
461 self.args = self._arg_parser.parse_args() 454 self.args = self._arg_parser.parse_args()
462 self.args_passed = self._arg_defaults.parse_args()
463 455
464 if 'entrypoint' in self.args: 456 if 'entrypoint' in self.args:
465 self._entrypoint = self.args.entrypoint 457 self._entrypoint = self.args.entrypoint
466 458
467 if self.args.general_config_file:
468 self.config_file = self.args.general_config_file
469
470 self.release_lock() 459 self.release_lock()
471 460
472 def read_config(self): 461 def read_config_file(self):
473 """Parse the configuration file and determine the runtime configuration. 462 """Read in the configuration file and store it in self.config.
474 """ 463 """
475 self.acquire_lock() 464 self.acquire_lock()
465 self.config = Configuration()
476 self.config_file = self.find_config_file() 466 self.config_file = self.find_config_file()
477 467
478 if self.config_file and os.path.exists(self.config_file): 468 if self.config_file and self.config_file.exists():
479 config = RawConfigParser(self.config) 469 config = RawConfigParser(self.config)
480 config.read(self.config_file) 470 config.read(str(self.config_file))
481 471
482 # Iterate over the config file options and write them into self.config 472 # Iterate over the config file options and write them into self.config
483 for section in config.sections(): 473 for section in config.sections():
@@ -487,8 +477,10 @@ class MILC(object):
487 # Coerce values into useful datatypes 477 # Coerce values into useful datatypes
488 if value.lower() in ['1', 'yes', 'true', 'on']: 478 if value.lower() in ['1', 'yes', 'true', 'on']:
489 value = True 479 value = True
490 elif value.lower() in ['0', 'no', 'false', 'none', 'off']: 480 elif value.lower() in ['0', 'no', 'false', 'off']:
491 value = False 481 value = False
482 elif value.lower() in ['none']:
483 continue
492 elif value.replace('.', '').isdigit(): 484 elif value.replace('.', '').isdigit():
493 if '.' in value: 485 if '.' in value:
494 value = Decimal(value) 486 value = Decimal(value)
@@ -497,32 +489,44 @@ class MILC(object):
497 489
498 self.config[section][option] = value 490 self.config[section][option] = value
499 491
500 # Fold the CLI args into self.config 492 self.release_lock()
493
494 def merge_args_into_config(self):
495 """Merge CLI arguments into self.config to create the runtime configuration.
496 """
497 self.acquire_lock()
501 for argument in vars(self.args): 498 for argument in vars(self.args):
502 if argument in ('subparsers', 'entrypoint'): 499 if argument in ('subparsers', 'entrypoint'):
503 continue 500 continue
504 501
505 if '_' in argument: 502 if argument not in self.arg_only:
506 section, option = argument.split('_', 1) 503 # Find the argument's section
507 else: 504 if self._entrypoint.__name__ in self.default_arguments and argument in self.default_arguments[self._entrypoint.__name__]:
508 section = self._entrypoint.__name__ 505 argument_found = True
509 option = argument 506 section = self._entrypoint.__name__
510 507 if argument in self.default_arguments['general']:
511 if option not in self.arg_only: 508 argument_found = True
512 if hasattr(self.args_passed, argument): 509 section = 'general'
510
511 if not argument_found:
512 raise RuntimeError('Could not find argument in `self.default_arguments`. This should be impossible!')
513 exit(1)
514
515 # Merge this argument into self.config
516 if argument in self.default_arguments:
513 arg_value = getattr(self.args, argument) 517 arg_value = getattr(self.args, argument)
514 if arg_value: 518 if arg_value:
515 self.config[section][option] = arg_value 519 self.config[section][argument] = arg_value
516 else: 520 else:
517 if option not in self.config[section]: 521 if argument not in self.config[section]:
518 self.config[section][option] = getattr(self.args, argument) 522 self.config[section][argument] = getattr(self.args, argument)
519 523
520 self.release_lock() 524 self.release_lock()
521 525
522 def save_config(self): 526 def save_config(self):
523 """Save the current configuration to the config file. 527 """Save the current configuration to the config file.
524 """ 528 """
525 self.log.debug("Saving config file to '%s'", self.config_file) 529 self.log.debug("Saving config file to '%s'", str(self.config_file))
526 530
527 if not self.config_file: 531 if not self.config_file:
528 self.log.warning('%s.config_file file not set, not saving config!', self.__class__.__name__) 532 self.log.warning('%s.config_file file not set, not saving config!', self.__class__.__name__)
@@ -530,31 +534,34 @@ class MILC(object):
530 534
531 self.acquire_lock() 535 self.acquire_lock()
532 536
537 # Generate a sanitized version of our running configuration
533 config = RawConfigParser() 538 config = RawConfigParser()
534 config_dir = os.path.dirname(self.config_file)
535
536 for section_name, section in self.config._config.items(): 539 for section_name, section in self.config._config.items():
537 config.add_section(section_name) 540 config.add_section(section_name)
538 for option_name, value in section.items(): 541 for option_name, value in section.items():
539 if section_name == 'general': 542 if section_name == 'general':
540 if option_name in ['save_config']: 543 if option_name in ['config_file']:
541 continue 544 continue
542 config.set(section_name, option_name, str(value)) 545 if value is not None:
546 config.set(section_name, option_name, str(value))
543 547
544 if not os.path.exists(config_dir): 548 # Write out the config file
545 os.makedirs(config_dir) 549 config_dir = self.config_file.parent
550 if not config_dir.exists():
551 config_dir.mkdir(parents=True, exist_ok=True)
546 552
547 with NamedTemporaryFile(mode='w', dir=config_dir, delete=False) as tmpfile: 553 with NamedTemporaryFile(mode='w', dir=str(config_dir), delete=False) as tmpfile:
548 config.write(tmpfile) 554 config.write(tmpfile)
549 555
550 # Move the new config file into place atomically 556 # Move the new config file into place atomically
551 if os.path.getsize(tmpfile.name) > 0: 557 if os.path.getsize(tmpfile.name) > 0:
552 os.rename(tmpfile.name, self.config_file) 558 os.rename(tmpfile.name, str(self.config_file))
553 else: 559 else:
554 self.log.warning('Config file saving failed, not replacing %s with %s.', self.config_file, tmpfile.name) 560 self.log.warning('Config file saving failed, not replacing %s with %s.', str(self.config_file), tmpfile.name)
555 561
562 # Housekeeping
556 self.release_lock() 563 self.release_lock()
557 cli.log.info('Wrote configuration to %s', shlex.quote(self.config_file)) 564 cli.log.info('Wrote configuration to %s', shlex.quote(str(self.config_file)))
558 565
559 def __call__(self): 566 def __call__(self):
560 """Execute the entrypoint function. 567 """Execute the entrypoint function.
@@ -603,16 +610,11 @@ class MILC(object):
603 name = handler.__name__.replace("_", "-") 610 name = handler.__name__.replace("_", "-")
604 611
605 self.acquire_lock() 612 self.acquire_lock()
613
606 kwargs['help'] = description 614 kwargs['help'] = description
607 self.subcommands_default[name] = self._subparsers_default.add_parser(name, **kwargs)
608 self.subcommands[name] = SubparserWrapper(self, name, self._subparsers.add_parser(name, **kwargs)) 615 self.subcommands[name] = SubparserWrapper(self, name, self._subparsers.add_parser(name, **kwargs))
609 self.subcommands[name].set_defaults(entrypoint=handler) 616 self.subcommands[name].set_defaults(entrypoint=handler)
610 617
611 if name not in self.__dict__:
612 self.__dict__[name] = self.subcommands[name]
613 else:
614 self.log.debug("Could not add subcommand '%s' to attributes, key already exists!", name)
615
616 self.release_lock() 618 self.release_lock()
617 619
618 return handler 620 return handler
@@ -620,7 +622,6 @@ class MILC(object):
620 def subcommand(self, description, **kwargs): 622 def subcommand(self, description, **kwargs):
621 """Decorator to register a subcommand. 623 """Decorator to register a subcommand.
622 """ 624 """
623
624 def subcommand_function(handler): 625 def subcommand_function(handler):
625 return self.add_subcommand(handler, description, **kwargs) 626 return self.add_subcommand(handler, description, **kwargs)
626 627
@@ -644,9 +645,9 @@ class MILC(object):
644 self.log_format = self.config['general']['log_fmt'] 645 self.log_format = self.config['general']['log_fmt']
645 646
646 if self.config.general.color: 647 if self.config.general.color:
647 self.log_format = ANSIEmojiLoglevelFormatter(self.args.general_log_fmt, self.config.general.datetime_fmt) 648 self.log_format = ANSIEmojiLoglevelFormatter(self.args.log_fmt, self.config.general.datetime_fmt)
648 else: 649 else:
649 self.log_format = ANSIStrippingFormatter(self.args.general_log_fmt, self.config.general.datetime_fmt) 650 self.log_format = ANSIStrippingFormatter(self.args.log_fmt, self.config.general.datetime_fmt)
650 651
651 if self.log_file: 652 if self.log_file:
652 self.log_file_handler = logging.FileHandler(self.log_file, self.log_file_mode) 653 self.log_file_handler = logging.FileHandler(self.log_file, self.log_file_mode)
@@ -673,13 +674,9 @@ class MILC(object):
673 674
674 colorama.init() 675 colorama.init()
675 self.parse_args() 676 self.parse_args()
676 self.read_config() 677 self.merge_args_into_config()
677 self.setup_logging() 678 self.setup_logging()
678 679
679 if 'save_config' in self.config.general and self.config.general.save_config:
680 self.save_config()
681 exit(0)
682
683 return self 680 return self
684 681
685 def __exit__(self, exc_type, exc_val, exc_tb): 682 def __exit__(self, exc_type, exc_val, exc_tb):