I started work on this concept many months ago. However I never had enough free time to get this to complete stage. Today finally I prepared the demo will all crucial features embedded. So, first, let me explain the concept. Generally the idea is to have a command that will be easily extensible with new features. The best way to achieve that is by having a kind of plugin architecture, where there is main program – in this case just an interface in fact as it does not provide any useful features on its own. And there are a lot of plugins that somehow registers to main program.
Crucial thing here is to have it done automatically, so all the user has to do is to install the plugin. Another important feature is to provide bash completion for this program, so that plugin developer can focus on interface itself, not the way of integrating it with bash and still get quite good completion. Of course in more complicated cases developer still has to do some development, but let’s ignore that here as this is not so important.
To achieve all the above I created two demo projects. One is called menu and it implements interface for plugins, do all the necessary jobs, so that plugins can live like almost independent applications, only called by menu. The other is demo plugin that uses all this magic to appear as command in menu just after its installation.
So from user perspective there are two main steps:
- Install menu and all plugins he needs with pip
- Enable bash completion as is usually done with argcomplete
I described the usage in README of menu already, so will not repeat that here.
Results
With this kind of architecture, what you can achieve is, with only few lines of code, interface that outputs something like this:
$ m --help usage: m [-h] {demo} ... Menu positional arguments: {demo} demo Show a demo on how plugin can have its own command options: -h, --help show this help message and exit $ m demo --help usage: m demo [-h] [-d] options: -h, --help show this help message and exit -d, --demonstrate Demonstrate use of argument
And this is split into two Python modules and easily can be extended to new ones.
Internals
Now to the most interesting part, so the internals. Internally there are three interesting features here. First is how main menu finds its plugins. This can be seen in __main__.py and looks like that:
29 plugin_list = [] 30 for p in pkgutil.iter_modules(): 31 if p.name.startswith(PLUGIN_PACKAGE_PREFIX): 32 plugin_list.append(p.name)
Second interesting feature is usage of argcomplete, as it makes completion almost effortless. And this is as simple as:
54 argcomplete.autocomplete(parser) 55 args = parser.parse_args(argv) 56 return args.func(args)
And the last one is having logger that colors terminal output depending on level of log to print. This one is a bit longer as it consists of 3 parts. First is formatter class:
class ColoringFormatter(logging.Formatter): fmt = '{start}%(levelname)s/%(name)s [%(asctime)s]: %(message)s{stop}' FORMATS = { logging.FATAL: fmt.format(start=Format(Style.BOLD, Color.RED), stop=Format(Style.OFF)), logging.ERROR: fmt.format(start=Format(color=Color.LIGHT_RED), stop=Format(Style.OFF)), logging.WARNING: fmt.format(start=Format(color=Color.LIGHT_YELLOW), stop=Format(Style.OFF)), logging.DEBUG: fmt.format(start=Format(color=Color.DARK_GREY), stop=Format(Style.OFF)), logging.NOTSET: fmt.format(start='', stop=''), } def format(self, record): if record.levelno in self.FORMATS: fmt = self.FORMATS[record.levelno] else: fmt = self.FORMATS[logging.NOTSET] formatter = logging.Formatter(fmt) return formatter.format(record)
Second is format class that moves some escape code creation magic outside (color and style are ordinary Enums):
class Format: def __init__(self, style=None, color=None): self.style = style self.color = color def __str__(self): stack = [] if self.color is not None: stack.append(str(self.color.value)) if self.style is not None: stack.append(str(self.style.value)) seq_list = ';'.join(stack) return f'\033[{seq_list}m'
And third is, where finally logger is assembled together:
# create main logger self.main_logger = logging.getLogger(appname) self.main_logger.setLevel(logging.DEBUG) # create console handler handler = logging.StreamHandler() handler.setLevel(logging.DEBUG) # set console handler's formatter to coloring handler.setFormatter(ColoringFormatter()) self.main_logger.addHandler(handler)
Idea for colored logger obviously comes from StackOverflow. And basically this is it. The rest is in my opinion boring stuff that always has to be done.