1. Pypsi Shell and Command Tutorial¶
This guide will walk through the API and design of Pypsi commands and plugins.
1.1. Command API¶
All Pypsi commands will inherit from the base command class:
pypsi.core.Command
. Each command has several attributes that determines
how the command is added to the shell. These are:
name
- the command name that the user will type. Commands may contain letters, numbers, underscores, and dashes.brief
- a short description of what the command does.usage
- the usage message displayed when the user requests help.topic
- the topic ID used to categorize the command. This is useful when the user lists all available commands. The shell will attempt to categorize commands under headings. All builtin commands have, by default, their topic set to"shell"
.
To ensure that commands are pluggable, commands should accept all of these attribtues in their constructor so that a shell may customize the command when loading. However, this is not a requirement. All builtin commands accept all of the attribute in their constructor.
The command hook setup()
is called once the command
has been created and added to the shell. The setup hook is where setup and
initialization code will reside. Commands should not hold any configuration or
state information in the command itself. However, use the
shell’s ctx
attribute to hold stateful information. Storing
information in the shell’s context is done for two reasons:
- The shell’s state is stored in a single place. This allows for easy and uniform serialization and deserialization to enable persistent shell sessions.
- Multiple instances of a shell can contain share the same command instance.
When a command is executed by the user, the command’s
run()
function is called.
1.1.1. Accepting Arguments¶
All of Pypsi’s bultin commands use a wrapped version of Python’s argparse
module for argument parsing. The class PypsiArgParser
wraps ArgumentParser
to change the parser’s behavior when the
user passes invalid arguments or asks for help. By default, the base
ArgumentParser will exit the entire program, which isn’t ideal for Pypsi.
The only difference between the Pypsi pypsi.core.PypsiArgParser
and
argparse.ArgumentParser
is that the former will raise a
pypsi.core.CommandShortCircuit
exception when the arguments aren’t
valid or the user requests help (via -h
or --help
.)
1.1.2. Printing¶
Pypsi wraps the print()
function with its own
pypsi_print()
function, which handles automatic word
wrapping and smart coloring.
1.1.2.1. Colors¶
Pypsi supplies color constants that can be printed to the screen. Colors will
only print if the output stream is a terminal (ie. the stream’s isatty()
returns True
.) This means that commands don’t need to handle printing
colors and not printing colors, the pypsi_print()
function
handles it.
Color codes are held in the AnsiCodes
object. Using this
constant is straight forward. In this example, the text “Hello, World!” is
printed in red and then in green:
print(AnsiCodes.red, "Hello, ", AnsiCodes.green, "World!", AnsiCodes.reset, sep='')
It is important to pass in sep=''
when printing colors. Otherwise, the above
statement would add a space around each color, which will be confusing to the
user.
1.1.3. Example¶
This example is the source code of the
EchoCommand
, which prints the arguments passed
into it to the screen:
# Pypsi imports
from pypsi.core import Command, PypsiArgParser, CommandShortCircuit
import argparse
# Custom usage message
EchoCmdUsage = "%(prog)s [-n] [-h] message"
class EchoCommand(Command):
'''
Prints text to the screen.
'''
def __init__(self, name='echo', topic='shell', brief='print a line of text', **kwargs):
self.parser = PypsiArgParser(
prog=name,
description=brief,
usage=EchoCmdUsage
)
subcmd = self.parser.add_argument_group(title='Stream')
self.parser.add_argument(
'message', help='message to print', nargs=argparse.REMAINDER,
metavar="MESSAGE"
)
self.parser.add_argument(
'-n', '--nolf', help="don't print newline character", action='store_true'
)
super(EchoCommand, self).__init__(
name=name, usage=self.parser.format_help(), topic=topic,
brief=brief, **kwargs
)
def run(self, shell, args, ctx):
try:
ns = self.parser.parse_args(args)
except CommandShortCircuit as e:
return e.code
tail = '' if ns.nolf else '\n'
print(' '.join(ns.message), sep='', end=tail)
return 0
The echo command only accepts a single argument: -n|--nolf
. The command
itself mirrors a simple Python command line application. This means that porting
existing applications to Pypsi commands is extremely easy. Also, notice the
try...except...
around parser.parse_args
. This is catching the
CommandShortCircuit
exception, which in this case will be
thrown if the user enters any of the following:
-h
,--help
- print usage information-x
- an invalid argument for the echo command
1.2. Shell API¶
Pypsi shells are typically barebones and do not contain much. This is a similar design to ORM libraries such as Django and MongoEngine, where the database table (or document) just holds the list of attributes as class variables.
All shells much inherit from the base class Shell
. Then,
add command instances. In this example, a new shell is created, given the name
“example” and the echo command is added, but renamed to print
:
# Pypsi Imports
from pypsi.shell import Shell
from pypsi.commands.echo import EchoCommand
class MyShell(Shell):
echo_cmd = EchoCommand(name='print')
shell = MyShell(name='example')
shell.cmdloop()
Once running, the user will be presented with a prompt and will be able to use
the print
command (which is the EchoCommand
.)
Several hooks exist in the shell that can be overriden. These include:
on_shell_ready()
- called when the shell is createdon_cmdloop_begin()
- called when thecmdloop()
function is calledon_cmdloop_end()
- called when the cmdloop has ended (usually because the user is exiting the shell)