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.py826
1 files changed, 0 insertions, 826 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()