diff options
Diffstat (limited to 'lib/python/milc.py')
-rw-r--r-- | lib/python/milc.py | 113 |
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 | |||
17 | import logging | 17 | import logging |
18 | import os | 18 | import os |
19 | import re | 19 | import re |
20 | import shlex | ||
20 | import sys | 21 | import sys |
21 | from decimal import Decimal | 22 | from decimal import Decimal |
22 | from tempfile import NamedTemporaryFile | 23 | from tempfile import NamedTemporaryFile |
@@ -35,6 +36,10 @@ except ImportError: | |||
35 | 36 | ||
36 | import argcomplete | 37 | import argcomplete |
37 | import colorama | 38 | import colorama |
39 | from appdirs import user_config_dir | ||
40 | |||
41 | # Disable logging until we can configure it how the user wants | ||
42 | logging.basicConfig(filename='/dev/null') | ||
38 | 43 | ||
39 | # Log Level Representations | 44 | # Log Level Representations |
40 | EMOJI_LOGLEVELS = { | 45 | EMOJI_LOGLEVELS = { |
@@ -47,6 +52,7 @@ EMOJI_LOGLEVELS = { | |||
47 | } | 52 | } |
48 | EMOJI_LOGLEVELS['FATAL'] = EMOJI_LOGLEVELS['CRITICAL'] | 53 | EMOJI_LOGLEVELS['FATAL'] = EMOJI_LOGLEVELS['CRITICAL'] |
49 | EMOJI_LOGLEVELS['WARN'] = EMOJI_LOGLEVELS['WARNING'] | 54 | EMOJI_LOGLEVELS['WARN'] = EMOJI_LOGLEVELS['WARNING'] |
55 | UNICODE_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 | ||
99 | class ANSIEmojiLoglevelFormatter(ANSIFormatter): | 105 | class 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 | ||
167 | class ConfigurationOption(Configuration): | 177 | class 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 | ||
181 | def handle_store_boolean(self, *args, **kwargs): | 194 | def 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())) | ||