Make your REPL extendable with your own toolset

AdrianAdrian
3 min read

The Cmd class in python is useful for a quick terminal ui. Just your own repl. In this article we will extend the repl with other repls. You can learn some python meta programming to extend your toolset.

First let’s create our “hello world” cmd in the file hw.py.

# store this in hw.py
from cmd import Cmd

class HelloWorld(Cmd):
    "Hello world cmd.Cmd as an example"
    def __init__(self):
        super().__init__()   # call the mother Cmd.__init__
        self.name = "world"  # give a initial name to get the classic hello world

    # every method starting with 'do_' is a repl command.
    # the args variable store a string with the arguments
    # i.e. you typed "name Albert Einstein" the
    # string "Albert Einstein" would be in args.
    def do_name(self, args):
        "set your name"
        self.name = args
        print(f"Your name {self.name} is set.", file=self.stdout)

    def do_hello(self, args):
        "say hello"
        print(f"Hello, {self.name}", file=self.stdout)

if __name__ == "__main__":
    # if you run "python hw.py" directly, this would be executed
    HelloWorld().cmdloop()

This example from my previous article is a good start. I’ve built a lot of tools with this Cmd class. Ai, todo lists, webscraping, accounting. A lot of times they call apis and other stuff. It’s not that fun to have everything in a mono-repl. Let’s have some fun to build a MasterRepl to load these Cmd classes’s as modules.

To manage other Repls we need only a handful of commands:

  • load: The core to load a python module with the classic dot notation ‘my.repls.helloworld’

  • list: List all the modules with the given description

  • default: A command which is triggered, when no “do_” method matched

Let’s dive into the code:

import importlib
from cmd import Cmd

class MasterRepl(Cmd):
    def __init__(self):
        super().__init__()
        self.modules = {}  # Here we will store our other repl's

    def do_exit(self, args):
        "exit"
        return True  # when True is returned, the Cmd().cmdloop() will end

    def do_load(self, args):
        "load another python as a module. `load <variable_name> <module_import>"
        try:
            # Let's split everything into two strings: the first word and the rest
            module_name, module_string = args.split(maxsplit=1)
            # import_module would be like "import hw as module_obj"
            module_obj = importlib.import_module(module_string)
            # dir(..) returns all attributes as a list of strings. All kind of
            # variables like __file__, __doc__, ... are also included. so we
            # have to find the Cmd. And we assume that in a class there is only
            # one repl stored.
            for n in dir(module_obj):
                # if n is a string of '__doc__' with getattr you get the same
                # as module_obj.__doc__
                maybe_cmd_subclass = getattr(module_obj, n)
                if type(maybe_cmd_subclass) is type and maybe_cmd_subclass != Cmd:
                    if issubclass(maybe_cmd_subclass, Cmd):
                        # so we found something. To get an Instance of the repl
                        # it has to be called with 'repl()'
                        self.modules[module_name] = maybe_cmd_subclass()
        except Exception as e:
            print(f"Error on loading the module {args}: {e}")

    def do_list(self, args):
        "list all the modules with their help text"
        try:
            for m_name in self.modules.keys():
                print(f"--- {m_name} ---")
                print(self.modules[m_name].__doc__)  # yes some documentation
        except Exception as e:
            print(f"Error: {e}")

    def default(self, args):
        try:
            module_name, call_string = args.split(maxsplit=1)  # same as in load
            # onecmd: "Interpret the argument as though it had been
            # typed in response to the prompt."
            # https://docs.python.org/3/library/cmd.html#cmd.Cmd.onecmd
            self.modules[module_name].onecmd(call_string)
        except Exception as e:
            print(f"Error: {e}")

if __name__ == "__main__":
    MasterRepl().cmdloop()

That’s it.

Let’s recap some special commands:

  • dir(obj): return a list of all the attributes as strings

  • getattribute(obj, name): return the attribute

  • issubclass(class_a, class_mother): return a bool when class_a is a subclass of class_mother

  • importlib.import_module(module_string): return the loaded module

Now let’s test our master class and load the hello world repl:

(Cmd) load hello_world hw

(Cmd) list
--- hello_world ---
Hello world cmd.Cmd as an example

(Cmd) hello_world name Peter
Your name Peter is set.

(Cmd) hello_world hello
Hello, Peter

(Cmd) hello_world help

Documented commands (type help <topic>):
========================================
hello  help  name

(Cmd) hello_world help name
set your name
0
Subscribe to my newsletter

Read articles from Adrian directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Adrian
Adrian