aboutsummaryrefslogtreecommitdiff
path: root/lib/python/milc.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/python/milc.py')
-rw-r--r--lib/python/milc.py113
1 files changed, 70 insertions, 43 deletions
diff --git a/lib/python/milc.py b/lib/python/milc.py
index c62c1b166..1a29bb25c 100644
--- a/lib/python/milc.py
+++ b/lib/python/milc.py
@@ -17,6 +17,7 @@ import argparse
17import logging 17import logging
18import os 18import os
19import re 19import re
20import shlex
20import sys 21import sys
21from decimal import Decimal 22from decimal import Decimal
22from tempfile import NamedTemporaryFile 23from tempfile import NamedTemporaryFile
@@ -35,6 +36,10 @@ except ImportError:
35 36
36import argcomplete 37import argcomplete
37import colorama 38import colorama
39from appdirs import user_config_dir
40
41# Disable logging until we can configure it how the user wants
42logging.basicConfig(filename='/dev/null')
38 43
39# Log Level Representations 44# Log Level Representations
40EMOJI_LOGLEVELS = { 45EMOJI_LOGLEVELS = {
@@ -47,6 +52,7 @@ EMOJI_LOGLEVELS = {
47} 52}
48EMOJI_LOGLEVELS['FATAL'] = EMOJI_LOGLEVELS['CRITICAL'] 53EMOJI_LOGLEVELS['FATAL'] = EMOJI_LOGLEVELS['CRITICAL']
49EMOJI_LOGLEVELS['WARN'] = EMOJI_LOGLEVELS['WARNING'] 54EMOJI_LOGLEVELS['WARN'] = EMOJI_LOGLEVELS['WARNING']
55UNICODE_SUPPORT = sys.stdout.encoding.lower().startswith('utf')
50 56
51# ANSI Color setup 57# ANSI Color setup
52# Regex was gratefully borrowed from kfir on stackoverflow: 58# Regex was gratefully borrowed from kfir on stackoverflow:
@@ -97,11 +103,12 @@ class ANSIFormatter(logging.Formatter):
97 103
98 104
99class ANSIEmojiLoglevelFormatter(ANSIFormatter): 105class ANSIEmojiLoglevelFormatter(ANSIFormatter):
100 """A log formatter that makes the loglevel an emoji. 106 """A log formatter that makes the loglevel an emoji on UTF capable terminals.
101 """ 107 """
102 108
103 def format(self, record): 109 def format(self, record):
104 record.levelname = EMOJI_LOGLEVELS[record.levelname].format(**ansi_colors) 110 if UNICODE_SUPPORT:
111 record.levelname = EMOJI_LOGLEVELS[record.levelname].format(**ansi_colors)
105 return super(ANSIEmojiLoglevelFormatter, self).format(record) 112 return super(ANSIEmojiLoglevelFormatter, self).format(record)
106 113
107 114
@@ -144,13 +151,15 @@ class Configuration(object):
144 151
145 def __init__(self, *args, **kwargs): 152 def __init__(self, *args, **kwargs):
146 self._config = {} 153 self._config = {}
147 self.default_container = ConfigurationOption 154
155 def __getattr__(self, key):
156 return self.__getitem__(key)
148 157
149 def __getitem__(self, key): 158 def __getitem__(self, key):
150 """Returns a config section, creating it if it doesn't exist yet. 159 """Returns a config section, creating it if it doesn't exist yet.
151 """ 160 """
152 if key not in self._config: 161 if key not in self._config:
153 self.__dict__[key] = self._config[key] = ConfigurationOption() 162 self.__dict__[key] = self._config[key] = ConfigurationSection(self)
154 163
155 return self._config[key] 164 return self._config[key]
156 165
@@ -161,30 +170,34 @@ class Configuration(object):
161 def __delitem__(self, key): 170 def __delitem__(self, key):
162 if key in self.__dict__ and key[0] != '_': 171 if key in self.__dict__ and key[0] != '_':
163 del self.__dict__[key] 172 del self.__dict__[key]
164 del self._config[key] 173 if key in self._config:
174 del self._config[key]
165 175
166 176
167class ConfigurationOption(Configuration): 177class ConfigurationSection(Configuration):
168 def __init__(self, *args, **kwargs): 178 def __init__(self, parent, *args, **kwargs):
169 super(ConfigurationOption, self).__init__(*args, **kwargs) 179 super(ConfigurationSection, self).__init__(*args, **kwargs)
170 self.default_container = dict 180 self.parent = parent
171 181
172 def __getitem__(self, key): 182 def __getitem__(self, key):
173 """Returns a config section, creating it if it doesn't exist yet. 183 """Returns a config value, pulling from the `user` section as a fallback.
174 """ 184 """
175 if key not in self._config: 185 if key in self._config:
176 self.__dict__[key] = self._config[key] = None 186 return self._config[key]
177 187
178 return self._config[key] 188 elif key in self.parent.user:
189 return self.parent.user[key]
190
191 return None
179 192
180 193
181def handle_store_boolean(self, *args, **kwargs): 194def handle_store_boolean(self, *args, **kwargs):
182 """Does the add_argument for action='store_boolean'. 195 """Does the add_argument for action='store_boolean'.
183 """ 196 """
184 kwargs['add_dest'] = False
185 disabled_args = None 197 disabled_args = None
186 disabled_kwargs = kwargs.copy() 198 disabled_kwargs = kwargs.copy()
187 disabled_kwargs['action'] = 'store_false' 199 disabled_kwargs['action'] = 'store_false'
200 disabled_kwargs['dest'] = self.get_argument_name(*args, **kwargs)
188 disabled_kwargs['help'] = 'Disable ' + kwargs['help'] 201 disabled_kwargs['help'] = 'Disable ' + kwargs['help']
189 kwargs['action'] = 'store_true' 202 kwargs['action'] = 'store_true'
190 kwargs['help'] = 'Enable ' + kwargs['help'] 203 kwargs['help'] = 'Enable ' + kwargs['help']
@@ -219,11 +232,6 @@ class SubparserWrapper(object):
219 self.subparser.completer = completer 232 self.subparser.completer = completer
220 233
221 def add_argument(self, *args, **kwargs): 234 def add_argument(self, *args, **kwargs):
222 if kwargs.get('add_dest', True):
223 kwargs['dest'] = self.submodule + '_' + self.cli.get_argument_name(*args, **kwargs)
224 if 'add_dest' in kwargs:
225 del kwargs['add_dest']
226
227 if 'action' in kwargs and kwargs['action'] == 'store_boolean': 235 if 'action' in kwargs and kwargs['action'] == 'store_boolean':
228 return handle_store_boolean(self, *args, **kwargs) 236 return handle_store_boolean(self, *args, **kwargs)
229 237
@@ -254,12 +262,16 @@ class MILC(object):
254 self._entrypoint = None 262 self._entrypoint = None
255 self._inside_context_manager = False 263 self._inside_context_manager = False
256 self.ansi = ansi_colors 264 self.ansi = ansi_colors
265 self.arg_only = []
257 self.config = Configuration() 266 self.config = Configuration()
258 self.config_file = None 267 self.config_file = None
259 self.prog_name = sys.argv[0][:-3] if sys.argv[0].endswith('.py') else sys.argv[0]
260 self.version = os.environ.get('QMK_VERSION', 'unknown') 268 self.version = os.environ.get('QMK_VERSION', 'unknown')
261 self.release_lock() 269 self.release_lock()
262 270
271 # Figure out our program name
272 self.prog_name = sys.argv[0][:-3] if sys.argv[0].endswith('.py') else sys.argv[0]
273 self.prog_name = self.prog_name.split('/')[-1]
274
263 # Initialize all the things 275 # Initialize all the things
264 self.initialize_argparse() 276 self.initialize_argparse()
265 self.initialize_logging() 277 self.initialize_logging()
@@ -273,7 +285,7 @@ class MILC(object):
273 self._description = self._arg_parser.description = self._arg_defaults.description = value 285 self._description = self._arg_parser.description = self._arg_defaults.description = value
274 286
275 def echo(self, text, *args, **kwargs): 287 def echo(self, text, *args, **kwargs):
276 """Print colorized text to stdout, as long as stdout is a tty. 288 """Print colorized text to stdout.
277 289
278 ANSI color strings (such as {fg-blue}) will be converted into ANSI 290 ANSI color strings (such as {fg-blue}) will be converted into ANSI
279 escape sequences, and the ANSI reset sequence will be added to all 291 escape sequences, and the ANSI reset sequence will be added to all
@@ -284,11 +296,10 @@ class MILC(object):
284 if args and kwargs: 296 if args and kwargs:
285 raise RuntimeError('You can only specify *args or **kwargs, not both!') 297 raise RuntimeError('You can only specify *args or **kwargs, not both!')
286 298
287 if sys.stdout.isatty(): 299 args = args or kwargs
288 args = args or kwargs 300 text = format_ansi(text)
289 text = format_ansi(text)
290 301
291 print(text % args) 302 print(text % args)
292 303
293 def initialize_argparse(self): 304 def initialize_argparse(self):
294 """Prepare to process arguments from sys.argv. 305 """Prepare to process arguments from sys.argv.
@@ -313,21 +324,21 @@ class MILC(object):
313 self.release_lock() 324 self.release_lock()
314 325
315 def completer(self, completer): 326 def completer(self, completer):
316 """Add an arpcomplete completer to this subcommand. 327 """Add an argcomplete completer to this subcommand.
317 """ 328 """
318 self._arg_parser.completer = completer 329 self._arg_parser.completer = completer
319 330
320 def add_argument(self, *args, **kwargs): 331 def add_argument(self, *args, **kwargs):
321 """Wrapper to add arguments to both the main and the shadow argparser. 332 """Wrapper to add arguments to both the main and the shadow argparser.
322 """ 333 """
334 if 'action' in kwargs and kwargs['action'] == 'store_boolean':
335 return handle_store_boolean(self, *args, **kwargs)
336
323 if kwargs.get('add_dest', True) and args[0][0] == '-': 337 if kwargs.get('add_dest', True) and args[0][0] == '-':
324 kwargs['dest'] = 'general_' + self.get_argument_name(*args, **kwargs) 338 kwargs['dest'] = 'general_' + self.get_argument_name(*args, **kwargs)
325 if 'add_dest' in kwargs: 339 if 'add_dest' in kwargs:
326 del kwargs['add_dest'] 340 del kwargs['add_dest']
327 341
328 if 'action' in kwargs and kwargs['action'] == 'store_boolean':
329 return handle_store_boolean(self, *args, **kwargs)
330
331 self.acquire_lock() 342 self.acquire_lock()
332 self._arg_parser.add_argument(*args, **kwargs) 343 self._arg_parser.add_argument(*args, **kwargs)
333 344
@@ -396,7 +407,7 @@ class MILC(object):
396 if self.args and self.args.general_config_file: 407 if self.args and self.args.general_config_file:
397 return self.args.general_config_file 408 return self.args.general_config_file
398 409
399 return os.path.abspath(os.path.expanduser('~/.%s.ini' % self.prog_name)) 410 return os.path.join(user_config_dir(appname='qmk', appauthor='QMK'), '%s.ini' % self.prog_name)
400 411
401 def get_argument_name(self, *args, **kwargs): 412 def get_argument_name(self, *args, **kwargs):
402 """Takes argparse arguments and returns the dest name. 413 """Takes argparse arguments and returns the dest name.
@@ -413,6 +424,11 @@ class MILC(object):
413 raise RuntimeError('You must run this before the with statement!') 424 raise RuntimeError('You must run this before the with statement!')
414 425
415 def argument_function(handler): 426 def argument_function(handler):
427 if 'arg_only' in kwargs and kwargs['arg_only']:
428 arg_name = self.get_argument_name(*args, **kwargs)
429 self.arg_only.append(arg_name)
430 del kwargs['arg_only']
431
416 if handler is self._entrypoint: 432 if handler is self._entrypoint:
417 self.add_argument(*args, **kwargs) 433 self.add_argument(*args, **kwargs)
418 434
@@ -485,15 +501,20 @@ class MILC(object):
485 if argument in ('subparsers', 'entrypoint'): 501 if argument in ('subparsers', 'entrypoint'):
486 continue 502 continue
487 503
488 if '_' not in argument: 504 if '_' in argument:
489 continue 505 section, option = argument.split('_', 1)
490
491 section, option = argument.split('_', 1)
492 if hasattr(self.args_passed, argument):
493 self.config[section][option] = getattr(self.args, argument)
494 else: 506 else:
495 if option not in self.config[section]: 507 section = self._entrypoint.__name__
496 self.config[section][option] = getattr(self.args, argument) 508 option = argument
509
510 if option not in self.arg_only:
511 if hasattr(self.args_passed, argument):
512 arg_value = getattr(self.args, argument)
513 if arg_value:
514 self.config[section][option] = arg_value
515 else:
516 if option not in self.config[section]:
517 self.config[section][option] = getattr(self.args, argument)
497 518
498 self.release_lock() 519 self.release_lock()
499 520
@@ -509,6 +530,8 @@ class MILC(object):
509 self.acquire_lock() 530 self.acquire_lock()
510 531
511 config = RawConfigParser() 532 config = RawConfigParser()
533 config_dir = os.path.dirname(self.config_file)
534
512 for section_name, section in self.config._config.items(): 535 for section_name, section in self.config._config.items():
513 config.add_section(section_name) 536 config.add_section(section_name)
514 for option_name, value in section.items(): 537 for option_name, value in section.items():
@@ -517,7 +540,10 @@ class MILC(object):
517 continue 540 continue
518 config.set(section_name, option_name, str(value)) 541 config.set(section_name, option_name, str(value))
519 542
520 with NamedTemporaryFile(mode='w', dir=os.path.dirname(self.config_file), delete=False) as tmpfile: 543 if not os.path.exists(config_dir):
544 os.makedirs(config_dir)
545
546 with NamedTemporaryFile(mode='w', dir=config_dir, delete=False) as tmpfile:
521 config.write(tmpfile) 547 config.write(tmpfile)
522 548
523 # Move the new config file into place atomically 549 # Move the new config file into place atomically
@@ -527,6 +553,7 @@ class MILC(object):
527 self.log.warning('Config file saving failed, not replacing %s with %s.', self.config_file, tmpfile.name) 553 self.log.warning('Config file saving failed, not replacing %s with %s.', self.config_file, tmpfile.name)
528 554
529 self.release_lock() 555 self.release_lock()
556 cli.log.info('Wrote configuration to %s', shlex.quote(self.config_file))
530 557
531 def __call__(self): 558 def __call__(self):
532 """Execute the entrypoint function. 559 """Execute the entrypoint function.
@@ -602,8 +629,8 @@ class MILC(object):
602 """Called by __enter__() to setup the logging configuration. 629 """Called by __enter__() to setup the logging configuration.
603 """ 630 """
604 if len(logging.root.handlers) != 0: 631 if len(logging.root.handlers) != 0:
605 # This is not a design decision. This is what I'm doing for now until I can examine and think about this situation in more detail. 632 # MILC is the only thing that should have root log handlers
606 raise RuntimeError('MILC should be the only system installing root log handlers!') 633 logging.root.handlers = []
607 634
608 self.acquire_lock() 635 self.acquire_lock()
609 636
@@ -648,8 +675,9 @@ class MILC(object):
648 self.read_config() 675 self.read_config()
649 self.setup_logging() 676 self.setup_logging()
650 677
651 if self.config.general.save_config: 678 if 'save_config' in self.config.general and self.config.general.save_config:
652 self.save_config() 679 self.save_config()
680 exit(0)
653 681
654 return self 682 return self
655 683
@@ -712,4 +740,3 @@ if __name__ == '__main__':
712 cli.goodbye.add_argument('-n', '--name', help='Name to bid farewell to', default='World') 740 cli.goodbye.add_argument('-n', '--name', help='Name to bid farewell to', default='World')
713 741
714 cli() # Automatically picks between main(), hello() and goodbye() 742 cli() # Automatically picks between main(), hello() and goodbye()
715 print(sorted(ansi_colors.keys()))