home .. developer info ..
Creating an EDS External

Abstract: This article takes you through the development and testing of a simple external using the new External Development System. No IDE is needed; only a text editor and Hermes BBS.

Introduction

The Python-based External Development System was designed from the very beginning to make it as easy as possible to write externals for the Hermes BBS. I wanted anyone to be able to write an external, even if they had never written one using the old APIs.

To accomplish the goal, I built a clean, full-featured API that does all of the “heavy lifting” for you. Loading and saving of preferences, user data, statistics, etc. is all taken care of by the API. Menu display and processing is also handled by the API. All of this allows the external developer to focus on the code for their external without worrying about the behind-the-scenes details.

This article shows you how to get started with the new External Development System. You can make the most of this document by installing Hermes on your computer and following along with the examples in this article.

Folder Structure

Classic Externals (those written before the EDS was available) were traditionally written in Pascal and compiled into a Mac OS code resource. Static data such a strings or menu text could be included directly in the source code or stored in the external’s resource fork. Data for the external (preferences, high scores, user information, etc.) could be stored wherever the external desired.

All of this has changed with the new Python-based API. Classic externals were usually shipped as a single file that contained all of the compiled code and data for the external. Python externals, on the other hand, are shipped as a directory of source files and resource files.

This directory does not store any of the external’s runtime data such as preferences or user information. That information is stored by the Hermes Python Runtime and can be accessed using a number of simple APIs (described in the article on Data Properties).

File Types

There are two types of files contained within an external’s bundle directory:

Before we discuss the directory layout itself, let’s talk about these file types.

Python Modules

Python modules are stored in text files that end in “.py”. At minimum, the Hermes Python Runtime requires a main module. In Python terms, this would be stored in the main.py file. This is the file that will be loaded and executed when a user enters your external from the BBS.

You can divide the source for your external into as many – or as few – Python modules as you wish. This is purely an architectural consideration and does not affect the operation of your external. The only requirement is that you supply a main module that Hermes can use to start your external.

My recommendation is to start out with a single module and refactor your source code into multiple modules as your external gets more complex.

Resource Files

Resource files – not to be confused with Mac OS resource forks – contain the static text and data for your external. There are three basic types of resource files:

The format of each resource type is described in the documentation for the function that manipulates that type: loadStringList, loadTextFile, and runMenu.

Directory Layout

Every external built with the External Development System is stored in a directory or bundle. The name of this directory is the name that will be displayed for the external in the BBS’ external menu.

At minimum, this directory must contain a file called main.py. This is the file that will be loaded by the Hermes Python Runtime to start your external when a user selects it from the external menu. You can have other Python files in this directory (or even entire Python packages if you want), all of which must end in “.py”.

It is important to note that the external’s directory is not a Python package. No __init__.py file is necessary. Hermes constructs a Python interpreter specifically for your external and runs it directly from your external’s directory.

Your resource files are also stored in the external’s bundle, although they are located in a directory called resources and further segmented by type. Text files are in the text directory, string lists are in the strings directory, and menu files are in the menu directory.

Let’s take a look at the directory layout for Leech 2000 to see how a complex external might be organized:

As you can see, the Leech 2000 source code is split into four Python modules: the main module and three other modules that contain the code for various portions of the game. There is a single menu resource, three string resources, and four text resources.

The MainMenu is stored in a text resource instead of a menu resource so that I could maintain the DOS-style main menu from the original Leech game. In Leech (and Leech 2000) you have to type in the complete text of the menu item at the main menu. To implement this, I stored the text of the menu itself in a text resource and decode the menu commands in the main.py file.

Leech 2000’s main menu is an anomaly; you would rarely need to implement a menu this way. Most BBS menus use a single letter (or number) to activate a menu item. Menu resources were designed to support those kinds of menus and make them easy to implement. For a better example of menu handling, see the code for the LLL menu in the LLL.py file.

Simple externals, such as the Hello Hermes World described in the next section, may only contain a single file in their bundle: the main.py source file.

“Hello Hermes World”

This section of the article takes you through the design and installation of a simple “Hello World” external. Ours is, of course, called “Hello Hermes World”.

Step 1: Create the bundle

Make a directory called Hello Hermes World in the Externals folder of your Hermes BBS. This folder is located in the Hermes Files folder.

Step 2: Create the main.py file

Open up your new Hello Hermes World directory and – using your favorite text editor – create a file called main.py inside of this directory. Make sure that you understand how Python uses indentation before continuing. You need to decide if you will use spaces or tabs (PEP 8 recommends spaces) when writing your Python source and then consistently apply that decision throughout your Python career.

Type the following code into your main.py file:

from hermes import *

print 'Hello Hermes World!'

This code is pretty simple, which is of course the whole point of the new External Development System. Save the file and close your text editor.

Step 3: Set access permissions

Load up Hermes, log in, and go to the external menu. You may or may not be surprised to find out that your external is not in the menu. This is because the access permissions for the external have not yet been set. By default, all Python externals are hidden from all users (including the SysOps) until the SysOp has configured the access permissions for the external.

In the old days you would use a program called SetHRMS to set the minimum security level (SL) and optional access letter for each external. Python-based externals do not have a resource fork, so this method cannot be used.

Instead, EDS externals use a “magic directory” to specify access permissions. This directory is placed in the Externals folder along with the external bundle, but has a special name that sets the permissions for the external. The directory itself is empty, only the name is important.

The format of the magic directory name is as follows:

If you want to restrict access to Hello Hermes World to users with an SL of 10 or higher, you would create an empty directory named Hello Hermes World;10 and put it in the Externals directory along with the Hello Hermes World bundle.

Here are some sample magic directory names:

These magic directory names are invalid:

Create a magic directory for your external (use Hello Hermes World;255 if you only want it to be accessible by SysOps) and put it in the Externals folder. Quit the BBS and restart it (externals and magic directories are only loaded when the BBS starts up), log in, then take a look at the external menu. Your external should appear in the menu.

If you run your external, it should say “Hello Hermes World” and then return you to the external menu.

Step 4: Make some changes

While you are logged in to the BBS, and while you are still sitting at the external menu, open the main.py file back up in your text editor and change the file to read:

from hermes import *

print 'Hello Hermes World!'
print 'More exciting text.'

Save the file and select your external from the external menu. The new text appears! You did not have to restart the BBS, or even log off to get this new functionality. Python externals are loaded each time the user enters the external; you can do all of your development without ever logging off from the BBS.

It is important to note that any changes you make while a user is accessing the external will not take effect until the next time that the user enters the external. In other words, you must quit the external, then re-run it from the external menu after making changes to the source code.

The exception to this rule is resource files. Resource files are loaded every time they are accessed. For example, every time a menu is displayed the resource file for that menu is re-read. This allows you to modify menus, string lists, and text files while the external is running. This is especially helpful when you are designing ANSI menus: just keep refreshing the menu while you make changes to the resource file in your text editor.

Using the Hermes Python Console

This web site contains a number of documents and articles that explain the Hermes API. Although you could read all of these documents, then sit down and write an external from scratch, some aspects of the new system are easier to play with than to read about. The Hermes Python Console gives you a way to interactively try out different portions of the API. You can even use the Console to test and debug your external as we will see later on.

Interacting with the API

One of the primary uses of the Hermes Python Console is to allow external developers to interactively experiment with the Hermes API. Python encourages exploratory programming and this is a concept that I carried forward into the Hermes API.

This section of the article will take you through the development of the Update Phone external. Instead of providing the entire source file as was done with the Hello Hermes World external, I will instead guide you through the development of the external using the Hermes Python Console.

Exploring the user object

Our Update Phone External will do three things: display the user’s current phone number, ask them if they want to change their phone number, and then update the user’s phone number if they answer “Yes” in step 2. Let’s tackle the first step: retrieving and displaying the user’s phone number.

Log on to your BBS and start the Hermes Python Console. You will be greeted with the welcome banner and given a Python prompt:

Hermes Python Console v1.0 -- type "exit" when done
>>>

Every Python-based Hermes external needs to import the hermes module in order to get access to the BBS’ data structures, the Hermes API prompts, and other functionality provided by the External Development System. Import the hermes module into the Console:

>>> from hermes import *
>>>

The Python interpreter has now brought all of the functions, constants, and global variables into the Console’s namespace. You can confirm this by asking for the value of the user object:

>>> user
<user Michael Alyn Miller>
>>>

Since I am the one currently accessing the external, the Console prints out a Python object that references my user information. We can use this user object to get additional information about my account:

>>> user.id
1
>>> user.name
'Michael Alyn Miller'
>>> user.sysop
1
>>> user.phone
'800-555-1212'
>>>

The last item, user.phone is the piece of information that we are interested in. Our Update Phone will print it out like so:

>>> print 'Current phone number: %s' % (user.phone)
Current phone number: 800-555-1212
>>>

Looks good. Now on to the next step: asking the user if they want to change their phone number.

Trying out prompts

The hermes module provides a number of different types of prompts that you can use to interact with the user. Under the covers, all of these prompts are implemented by using the stdin and stdout objects stored in Python’s sys module. You could read and write those objects directly, but the prompt functions defined in the hermes module take the drudgery out of prompting the user for information.

There are two types of prompts that we will need for our Update Phone external: a yesNoPrompt to ask the user if they want to change their phone number and a textPrompt to retrieve the new phone number.

Let’s try these prompts out in the Hermes Python Console. As always, begin by importing the hermes module if you have not already done so. Here is the yesNoPrompt in action:

>>> yesNoPrompt('Change your phone number? ')
Change your phone number? No
0
>>> yesNoPrompt('Change your phone number? ')
Change your phone number? Yes
1
>>>

The prompt returns 0 (which is the same as False) if the user responds with “No” and 1 (or True) if the user responds with “Yes”. The yesNoPrompt supports a number of additional arguments, but at a minimum it only requires the prompt text. If we wanted the prompt to default to “No” (if the user hits Return instead of pressing Y or N), then we would use the defaultValue argument:

>>> yesNoPrompt('Change your phone number? ', defaultValue=False)
Change your phone number? No
0
>>>

In this example, I just hit Return and the prompt automatically selected the “No” option. Notice that the defaultValue argument does not take the character that will be entered, but the actual value (True or False) that the prompt should return if the user hits Return.

Now that we can ask the user if they want to change their phone number, let’s explore the textPrompt function that will let us get the new phone number:

>>> textPrompt('New phone number: ', 10)
New phone number: 8001234567
'8001234567'
>>>

The prompt takes the user’s input, which we specify can be no longer than ten characters, and returns the result as a string.

Our sample code works for US phone numbers, but is not very friendly to international numbers. It also lets the user enter any kind of text that they want into the prompt:

>>> textPrompt('New phone number: ', 10)
New phone number: mynumber
'mynumber'
>>>

This is not very helpful. Let’s use some of the extra arguments to the textPrompt function to force the user to enter only those characters that would appear in a normal phone number. We will also increase the maximum response length so that international numbers can be entered. Let’s try out the new arguments:

>>> textPrompt('New phone number: ', 24, validChars='0123456789-+() ')
New phone number: 123-4567
'123-4567'
>>> textPrompt('New phone number: ', 24, validChars='0123456789-+() ')
New phone number: (800) 123-4567
'(800) 123-4567'
>>> textPrompt('New phone number: ', 24, validChars='0123456789-+() ')
New phone number: +1 (800) 123-4567
'+1 (800) 123-4567'
>>>

We still don’t enforce the format of the number, but at least the characters will be valid. This is good enough for now, but feel free to explore the implementation of the hermes module if you would like to write a prompt specifically for phone numbers.

Putting it all together

We now have enough information to write our Update Phone external. Here is what the completed external looks like:

# Import the hermes module.
from hermes import *

# Print out the user's current phone number.
print 'Current phone number: %s' % (user.phone)

# See if they want to change their phone number.
if yesNoPrompt('Change your phone number? ', defaultValue=False):
    
# Get the new number.
    
newPhone = textPrompt(
        
'New phone number: ', 24,
        
validChars='0123456789-+() ')

    
# Change the phone number if the user typed something into
    
# the prompt.
    
if newPhone:
        
user.phone = newPhone
        
print 'Your phone number has been changed.'
    
else:
        
print 'Your phone number has not been changed.'

The next section of this article takes a look at how to interactively test and debug an external using the Hermes Python Console.

Testing and debugging

Simple externals such as Update Phone require little testing. There are only a handful of different code paths, all of which can be easily tested by running the external. More complex externals benefit from an interactive testing and debugging approach, especially when user-specific variables get involved.

The new Update Phone

This section of the document will show you how to test the functionality of an external using the Hermes Python Console. We will again use the Update Phone external, but I have modified it to make it both easier to test and easier to read:

# Import the hermes module.
from hermes import *


def changePhoneNumber():
    
"""Asks the user for their new phone number and updates
    their user record if we get a valid response."""


    
# Get the new number.
    
newPhone = textPrompt(
        
'New phone number: ', 24,
        
validChars='0123456789-+() ')

    
# Change the phone number if the user typed something into
    
# the prompt.
    
if newPhone:
        
user.phone = newPhone
        
print 'Your phone number has been changed.'
    
else:
        
print 'Your phone number has not been changed.'


def main():
    
"""Main entry point for the external."""

    
# Print out the user's current phone number.
    
print 'Current phone number: %s' % (user.phone)

    
# See if they want to change their phone number.
    
if yesNoPrompt('Change your phone number? ', defaultValue=False):
        
changePhoneNumber()


if __name__ == '__main__':
    
main()

A couple of changes have been made to the source code. First, the external no longer runs as soon as it has been imported. The contents of Python’s __name__ global variable is checked and the code for the external executes only if this value is set to __main__. (See the Python documentation for more information about this idiom.)

The code has also been split into two functions: one that implements the main loop of the external (the main function) and one that changes the user’s phone number (the changePhoneNumber function). The second function is not really necessary, but it will make it much easier to demonstrate the interactive debugging features of the Hermes Python Console.

Create an external bundle called New Update Phone and save the Python source code above in a file called main.py. Create the magic directory for New Update Phone as well. Restart the Hermes BBS and make sure that the external works.

Testing Update Phone

The Hermes Python Console is an external just like the (New) Update Phone external. But the Console is unique in that it gives you a way to interactively manipulate the Python runtime environment. We are going to use this functionality to call into different parts of our New Update Phone external.

In order to do this, the code for New Update Phone must be available to the Hermes Python Console. Python externals can only access code in their own external bundle, so we need to copy the main.py file from the New Update Phone bundle into the Hermes Python Console. The Console external already has a main.py file, so we will need to call the new file something else. Create a copy of New Update Phone’s main.py file, rename it to nup.py, and move the copy into the Hermes Python Console bundle.

Enter the Hermes Python Console and import the New Update Phone module:

Hermes Python Console v1.0 -- type "exit" when done
>>> import nup
>>>

The contents of the nup module have now been loaded into the Console’s namespace. Because we loaded this module interactively (instead of running it from the external menu) the __name__ global variable is not set to __main__. As a result, the code for the New Update Phone external was not executed.

If you would like to run the external, call its main method:

>>> nup.main()
Current phone number: 800-555-1212
Change your phone number? No
>>>

Directly calling the main method is exactly what would happen if you ran the New Update Phone external from the external menu. But using the Hermes Python Console we can also call other methods:

>>> nup.changePhoneNumber()
New phone number: +1 (800) 555-1212
Your phone number has been changed.
>>>

Notice that we were not asked if we wanted to change our phone number. This is because the main method was never run: we jumped directly to the changePhoneNumber method.

Manipulating data properties

The ability to interactively test an external is especially valuable when your external makes use of data properties. In these cases, the output of an external may depend heavily on the current state of the user or instance objects.

Leech 2000 stores a number of data properties about the user. One of these is the user’s current level in the game. The user begins at level 1; at level 31 they are forced to battle the LLL computer. There are two ways that I could have tested the “final battle” code:

  1. Play the game until I reach level 31. Fun, but time consuming.
  2. Use the Hermes Python Console to “play” Leech 2000, manipulating the game’s data properties as necessary.

I chose the second option. Here is how I did it:

  1. Copy all of Leech 2000’s .py files as well as its resources folder into the Hermes Python Console’s external bundle. I renamed Leech 2000’s main.py file to leech2000.py in the process.

  2. Start the Hermes Python Console and import the Leech 2000 code:

    Hermes Python Console v1.0 -- type "exit" when done
    >>> import leech2000
    >>>
  3. Play Leech 2000 once inside of the Hermes Python Console environment so that all of the game and user data is initialized:

    >>> leech2000.main()

    (Leech 2000 entry, menu, etc.)

    >>>

    All of the {bbs,prefs,user}.data.* properties for an external are stored in a special storage area that is based on the name of the external that is running. Even if you had played the real Leech 2000 game, none of that data would be available to the Leech 2000 game that is being tested in the Hermes Python Console.

    Playing Leech 2000 once in the Hermes Python Console ensures that the game’s data properties are initialized before we start editing properties. We also need a user object to edit, so creating a Leech 2000 player serves a dual purpose.

  4. Now that I have a Leech 2000 character, I can manually set my user.data properties so that I am a level 31 Leech:

    >>> from hermes import *
    >>> user.data.level = 31
    >>> user.data.totalMegs = 500
    >>> user.data.softwareType = 14
    >>> user.data.backupType = 14
    >>>

    I gave myself the most powerful software and backup type as well as plenty of megs. This way I could make sure to beat the LLL computer. I also tested Leech 2000 with the minimum values to ensure that the failure scenario worked correctly as well.

  5. Play Leech 2000 again and make sure that the final battle code works as expected:

    >>> leech2000.main()

    (Leech 2000 entry, final battle, etc.)

    >>>

This made it much easier to test Leech 2000 and ensured that a user wouldn’t get all the way to level 31 only to find a bug in a piece of code that I had never run before.

Testing this way is especially important in Python, which is a dynamically-typed language. Certain types of software bugs will not be detected until the first time that a function is run. You want to find these sorts of errors during the development and testing phase, not when one of your users is playing the game.

A couple of final notes regarding this testing method:

The Hermes Python Console makes it much easier to test externals, utility functions, or just to prototype an idea. I recommend that you spend some time playing with the Console as you write your first external. I am sure you will find it to be an invaluable tool.

What Next?

Hopefully this document has given you a good understanding of the External Development System and the new, Python-based Hermes API. Here are some next steps for you to consider:

Thanks for taking the time to read this article. As always, please feel free to contact me if you have any comments or questions about this document.