User Tools

Site Tools


Sidebar

B3 Homepage


Main Links


Support Subjects


Related Links


This wiki was updated recently and all user accounts were reset. If you want to contribute to this documentation wiki drop me an email at xlr8or[at]bigbrotherbot[dot]net.

customize:plugin_sdk:tutorial2

Plugin tutorial 2 - reading from the plugin config file

In this tutorial, we'll see :

  • how to read values from the plugin config file
  • how to test our plugin with different test configs
  • how to provide default values when you fail to read from the config file
  • how to read different types of data from the config file

B3 plugins config files

In B3, config files are XML files. If this is the first time you hear about XML, I suggest you search the web for XML and find out the basics of the XML language.

Also, to work comfortably with XML files, I suggest you use a text editor which is able to understand the XML syntax to help you check if what your wrote is syntactically correct and also to colorize your code. my favorite is Notepad++ with its XML plugin

The first plugin of this tutorial will define one command : !helloworld. The plugin will read two values from its config file :

  • the minimum level required to use the command
  • the text to display to users when the command is used

The B3 framework provides methods for accessing values from the plugin config files, but for these method to work, our config file must follow a predefined structure :

  • the root element must be named configuration and have an plugin attribute that value is our plugin name
  • the first level child elements must be named settings and have a unique name attribute
  • the second level child elements must be named set and have a unique name attribute within a settings element

Here is how such a structure looks like :

plugin_tutorial2a.xml
<configuration plugin="tutorial2a">
 
    <settings name="command_level">
        <set name="helloworld">2</set>
    </settings>
 
    <settings name="otherstuff">
        <set name="helloworld_text">hello world :)</set>
    </settings>
 
</configuration>

Plugin tutorial2a

The plugin below is able to read the config file we just defined. When you compare this code to the code of tutorial1.py, you will notice we added a new method onLoadConfig. When B3 starts and instantiate your plugin, it will firstly call the onLoadConfig method, then it will call the onStartup method. This gives your plugin code the chance to read values from the config file before doing anything else.

tutorial2a.py
#
# HelloWorld Plugin for BigBrotherBot(B3) (www.bigbrotherbot.net)
# Copyright (C) 2011 Courgette
# 
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
#
 
__version__ = '1.0'
__author__  = 'Courgette'
 
import b3
import b3.events
import b3.plugin
 
 
class Tutorial2aPlugin(b3.plugin.Plugin):
 
    def onLoadConfig(self):
        self.cmd_helloworld_minlevel = self.config.get('command_level', 'helloworld')
        self.cmd_helloworld_text = self.config.get('otherstuff', 'helloworld_text')
 
 
 
    def onStartup(self):
        # get the admin plugin so we can register commands
        self._adminPlugin = self.console.getPlugin('admin')
 
        if not self._adminPlugin:
            # something is wrong, can't start without admin plugin
            self.error('Could not find admin plugin')
            return
 
        # Register our command
        self.debug("%s : %s" % (type(self.cmd_helloworld_minlevel), int(self.cmd_helloworld_minlevel)))
        self._adminPlugin.registerCommand(self, 'helloworld', self.cmd_helloworld_minlevel, self.cmd_helloWorld)
 
 
    def cmd_helloWorld(self,  data, client, cmd):
        """Say 'Hello World!' to everyone"""
        self.console.say(self.cmd_helloworld_text)

Reading values

The code used here to read the two values of our config file is

self.config.get('command_level', 'helloworld')

and

self.config.get('otherstuff', 'helloworld_text')

As you see, the get method takes two parameters which are :

  • the name of the settings element from the config
  • the nmae of the set element from the previous settings element

The minimum level required to use the command is saved in self.cmd_helloworld_minlevel while the text to display is saved in self.cmd_helloworld_text. Note how these two properties are used in onStartup when registering our command and in cmd_helloworld

Testing the plugin

If you followed the first tutorial, you learned that B3 comes with a testing facility : the fake module. The way we test a plugin which requires a config file is a bit different, but still very straight forward :

Test 1

tutorial2a.py
#
# HelloWorld Plugin for BigBrotherBot(B3) (www.bigbrotherbot.net)
# Copyright (C) 2011 Courgette
# 
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
#
 
__version__ = '1.0'
__author__  = 'Courgette'
 
import b3
import b3.events
import b3.plugin
 
 
class Tutorial2aPlugin(b3.plugin.Plugin):
 
    def onLoadConfig(self):
        self.cmd_helloworld_minlevel = self.config.get('command_level', 'helloworld')
        self.cmd_helloworld_text = self.config.get('otherstuff', 'helloworld_text')
 
 
 
    def onStartup(self):
        # get the admin plugin so we can register commands
        self._adminPlugin = self.console.getPlugin('admin')
 
        if not self._adminPlugin:
            # something is wrong, can't start without admin plugin
            self.error('Could not find admin plugin')
            return
 
        # Register our command
        self.debug("%s : %s" % (type(self.cmd_helloworld_minlevel), int(self.cmd_helloworld_minlevel)))
        self._adminPlugin.registerCommand(self, 'helloworld', self.cmd_helloworld_minlevel, self.cmd_helloWorld)
 
 
    def cmd_helloWorld(self,  data, client, cmd):
        """Say 'Hello World!' to everyone"""
        self.console.say(self.cmd_helloworld_text)
 
 
if __name__ == '__main__':
    """The code below is meant for tests.
    It will never be executed from B3.
 
    To run the tests on Windows, open a new DOS console and type :
    set PYTHONPATH=c:\where\b3\is\installed && c:\python27\python.exe tutorial2a.py
 
    To run the tests on Linux, open a new console and type :
    PYTHONPATH=/where/b3/is/installed python tutorial1.py
 
    """
 
    # import the fake console which emulates B3
    from b3.fake import fakeConsole, joe, moderator
 
    # import the config reader
    from b3.config import XmlConfigParser
 
    print("\n\n * * * * * * * * * * * *  Test 1 starting below * * * * * * * * * * * * \n\n")
 
    # create a config just for our tests
    conf1 = XmlConfigParser()
    conf1.setXml("""\
        <configuration plugin="tutorial2a">
 
            <settings name="command_level">
                <set name="helloworld">0</set>
            </settings>
 
            <settings name="otherstuff">
                <set name="helloworld_text">hello world :)</set>
            </settings>
 
        </configuration>
    """)
 
 
    # instanciate our plugin with our test config
    myplugin = Tutorial2aPlugin(fakeConsole, conf1)
 
    # we then call onStartup() as the real B3 would do
    myplugin.onStartup()
 
    # make Joe connect to the fake game server on slot 0
    joe.connects(cid=0)
 
    # make Joe try our command
    joe.says('!helloworld')
 

Run the tests as your learned in the first tutorial and you should obtain :

 * * * * * * * * * * * *  Test 1 starting below * * * * * * * * * * * * 


DEBUG    : Register Event: Stop Process: Tutorial2aPlugin
DEBUG    : Register Event: Program Exit: Tutorial2aPlugin
DEBUG    : AdminPlugin: Command "helloworld (None)" registered with cmd_helloWorld for level (0, 100)

Joe connects to the game on slot #0
VERBOSE  : 0 cid changed from None to 0
DEBUG    : Client Connected: [0] Joe - zaerezarezar ({})
DEBUG    : User not found zaerezarezar: 'No client found in fakestorage for {id: 0, guid: zaerezarezar}'
BOT      : Client not found in the storage zaerezarezar, create new
VERBOSE2 : Aborted Making Alias for cid: 0, name is the same
DEBUG    : Client Authorized: [0] Joe - zaerezarezar

Joe says "!helloworld"
VERBOSE  : Queueing event Say !helloworld
VERBOSE  : Parsing Event: Say: AdminPlugin
DEBUG    : AdminPlugin: OnSay handle 5:"!helloworld"
DEBUG    : AdminPlugin: Handle command !helloworld
>>> hello world :)

Test 2

Add the follwing test code to your plugin and run the tests again.

    print("\n\n * * * * * * * * * * * *  Test 2 starting below * * * * * * * * * * * * \n\n")
 
    # create a config just for our tests
    conf2 = XmlConfigParser()
    conf2.setXml("""\
        <configuration plugin="tutorial2a">
 
            <settings name="command_level">
                <set name="helloworld">2</set>
            </settings>
 
            <settings name="otherstuff">
                <set name="helloworld_text">a new text</set>
            </settings>
 
        </configuration>
    """)
 
 
    # instanciate our plugin with our test config
    myplugin = Tutorial2aPlugin(fakeConsole, conf2)
 
    # we then call onStartup() as the real B3 would do
    myplugin.onStartup()
 
    # make Joe connect to the fake game server on slot 0
    joe.connects(cid=0)
 
    # make Joe try our command
    joe.says('!helloworld')
 
    # make a moderator connect to the fake game server on slot 0
    moderator.connects(cid=1)
 
    # make the moderator try our command
    moderator.says('!helloworld')
 

See how we changed the minimum level required to use the command. Remember that Joe is a registered user (level 1) before reading the test results.

 * * * * * * * * * * * *  Test 2 starting below * * * * * * * * * * * * 


DEBUG    : Register Event: Stop Process: Tutorial2aPlugin
DEBUG    : Register Event: Program Exit: Tutorial2aPlugin
DEBUG    : AdminPlugin: Command "helloworld (None)" registered with cmd_helloWorld for level (2, 100)

Joe connects to the game on slot #0
VERBOSE  : 1 cid changed from 0 to 0
DEBUG    : Client Connected: [0] Joe - zaerezarezar ({})

Joe says "!helloworld"
VERBOSE  : Queueing event Say !helloworld
VERBOSE  : Parsing Event: Say: AdminPlugin
DEBUG    : AdminPlugin: OnSay handle 5:"!helloworld"
DEBUG    : AdminPlugin: Handle command !helloworld
sending msg to Joe: You do not have sufficient access to use !helloworld

Moderator connects to the game on slot #1
VERBOSE  : 0 cid changed from None to 1
DEBUG    : Client Connected: [1] Moderator - sdf455ezr ({})
DEBUG    : User not found sdf455ezr: 'No client found in fakestorage for {id: 0, guid: sdf455ezr}'
BOT      : Client not found in the storage sdf455ezr, create new
VERBOSE2 : Aborted Making Alias for cid: 1, name is the same
DEBUG    : Client Authorized: [1] Moderator - sdf455ezr

Moderator says "!helloworld"
VERBOSE  : Queueing event Say !helloworld
VERBOSE  : Parsing Event: Say: AdminPlugin
DEBUG    : AdminPlugin: OnSay handle 5:"!helloworld"
DEBUG    : AdminPlugin: Handle command !helloworld
>>> a new text

Test 3

Everything goes smoothly so far :) but while you (the plugin programmer) control the plugin code, the config file content is controlled by your users. And if your plugin is successful, lots of users ! But soon enough you will have to deal with users who are not so familiar with B3 and the XML syntax, they will mess up the config syntax and unexpected things can happen as described in this third test :

    print("\n\n * * * * * * * * * * * *  Test 3 starting below * * * * * * * * * * * * \n\n")
 
    # create a config just for our tests
    conf3 = XmlConfigParser()
    conf3.setXml("""\
        <configuration plugin="tutorial2a">
 
            <settings name="command_level">
                <set name="helloworld">this is obviously wrong</set>
            </settings>
 
            <settings name="otherstuff">
                <set name="helloworld_text">a third text</set>
            </settings>
 
        </configuration>
    """)
 
 
    # instanciate our plugin with our test config
    myplugin = Tutorial2aPlugin(fakeConsole, conf3)
 
    # we then call onStartup() as the real B3 would do
    myplugin.onStartup()
 
    # make superadmin connect to the fake game server on slot 0
    joe.connects(cid=0)
 
    # make superadmin try our commands
    joe.says('!helloworld')
 

Notice the value for the minimum required level, and now witness in the test results how that obviously wrong level number has been naively pass on the admin plugin when registering our command :

 * * * * * * * * * * * *  Test 3 starting below * * * * * * * * * * * * 


DEBUG    : Register Event: Stop Process: Tutorial2aPlugin
DEBUG    : Register Event: Program Exit: Tutorial2aPlugin
ERROR    : AdminPlugin: unknown group this is obviously wrong
ERROR    : AdminPlugin: Command "helloworld" registration failed invalid literal for int() with base 10: 'False'

Joe connects to the game on slot #0
VERBOSE  : 1 cid changed from 0 to 0
DEBUG    : Client Connected: [0] Joe - zaerezarezar ({})

Joe says "!helloworld"
VERBOSE  : Queueing event Say !helloworld
VERBOSE  : Parsing Event: Say: AdminPlugin
DEBUG    : AdminPlugin: OnSay handle 5:"!helloworld"
DEBUG    : AdminPlugin: Handle command !helloworld
sending msg to Joe: You do not have sufficient access to use !helloworld

Now, how to deal with such cases ?

Murphy's law : everything that can go wrong will go wrong

We deal with our users weird behavior by thinking defensively : never trust data coming from users !

Fail early

First, we need to validate the values we read from the config file as early as we can. That way we can regain control over them.

  • search for :
    self.cmd_helloworld_minlevel = self.config.get('command_level', 'helloworld')
  • replace with :
    self.cmd_helloworld_minlevel = self.config.getint('command_level', 'helloworld')

and run the test 3 again. Now the result is different :

 * * * * * * * * * * * *  Test 3 starting below * * * * * * * * * * * * 


Traceback (most recent call last):
  File "C:\Users\Thomas\workspace\b3\b3-plugins\b3-plugin-tutorials\tutorial2a.py", line 169, in <module>
    myplugin = Tutorial2aPlugin(fakeConsole, conf3)
  File "C:\Users\Thomas\workspace\b3\src\b3\plugin.py", line 64, in __init__
    self.onLoadConfig()
  File "C:\Users\Thomas\workspace\b3\b3-plugins\b3-plugin-tutorials\tutorial2a.py", line 31, in onLoadConfig
    self.cmd_helloworld_minlevel = self.config.getint('command_level', 'helloworld')
  File "C:\Users\Thomas\workspace\b3\src\b3\config.py", line 123, in getint
    return int(self.get(section, setting))
ValueError: invalid literal for int() with base 10: 'this is obviously wrong'

This result shows a Python exception of type ValueError raised by the self.config.getint method. B3 provides different methods to read different types of values from your config files :

Method Usage Description
get self.config.get('section', 'item') read a string from the config
getint self.config.getint('section', 'item') read a integer value from the config and raise a ValueError data is not an integer
getfloat self.config.getfloat('section', 'item') read a float value from the config and raise a ValueError data is not an float
getboolean self.config.getboolean('section', 'item') read a boolean value from the config. Recognized values are : 1, 0, 'true', 'false', 'on', 'off', 'yes', 'no' (case insensitive). Raise a ValueError if the value cannot be recognized
getDuration self.config.getDuration('section', 'item') read a duration from the config and return it as a number of minutes. Accepted time suffixes are 'h' (hour), 'm' (minute), 's' (second), 'd' (day), 'w' (week)
getpath self.config.getpath('section', 'item') read a file path from the config. Will expand '@b3' to the path where B3 is installed, '@conf' to the path of the main b3 config file and '~' to the user home directory (Linux) or the user 'My Documents' folder (Windows)

These methods will allow your code to fail early but also give your user more flexibility as with the getboolean method which accepts a large range of values.

Think defensively

But then, we cannot let our code crash. It is now time to think defensively by wrapping our code which read the config values in a try: / except: construct. Try the following code :

tutorial2b.py
#
# HelloWorld Plugin for BigBrotherBot(B3) (www.bigbrotherbot.net)
# Copyright (C) 2011 Courgette
# 
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
#
 
__version__ = '1.0'
__author__  = 'Courgette'
 
import b3
import b3.events
import b3.plugin
 
 
class Tutorial2bPlugin(b3.plugin.Plugin):
 
    def onLoadConfig(self):
        try:
            self.cmd_helloworld_minlevel = self.config.getint('command_level', 'helloworld')
        except:
            self.error('bad value reading "command_level/helloworld" from config file. Using default value instead')
            self.cmd_helloworld_minlevel = 1
        self.info("minimum level required for the command_level !helloworld is : %s" % self.cmd_helloworld_minlevel)
 
        try:
            self.cmd_helloworld_text = self.config.get('otherstuff', 'helloworld_text')
        except:
            self.error('bad value reading "otherstuff/helloworld_text" from config file. Using default value instead')
            self.cmd_helloworld_text = "default hello world text"
        self.info("text displayed for command !helloworld is : '%s'" % self.cmd_helloworld_text)
 
 
 
    def onStartup(self):
        # get the admin plugin so we can register commands
        self._adminPlugin = self.console.getPlugin('admin')
 
        if not self._adminPlugin:
            # something is wrong, can't start without admin plugin
            self.error('Could not find admin plugin')
            return
 
        # Register our command
        self._adminPlugin.registerCommand(self, 'helloworld', self.cmd_helloworld_minlevel, self.cmd_helloWorld)
 
 
    def cmd_helloWorld(self,  data, client, cmd):
        """Say 'Hello World!' to everyone"""
        self.console.say(self.cmd_helloworld_text)
 
 
if __name__ == '__main__':
    """The code below is meant for tests.
    It will never be executed from B3.
 
    To run the tests on Windows, open a new DOS console and type :
    set PYTHONPATH=c:\where\b3\is\installed && c:\python27\python.exe tutorial2a.py
 
    To run the tests on Linux, open a new console and type :
    PYTHONPATH=/where/b3/is/installed python tutorial1.py
 
    """
 
    # import the fake console which emulates B3
    from b3.fake import fakeConsole, joe, moderator
 
    # import the config reader
    from b3.config import XmlConfigParser
 
    print("\n\n * * * * * * * * * * * *  Test 3 starting below * * * * * * * * * * * * \n\n")
 
    # create a config just for our tests
    conf3 = XmlConfigParser()
    conf3.setXml("""\
        <configuration plugin="tutorial2a">
 
            <settings name="command_level">
                <set name="helloworld">this is obviously wrong</set>
            </settings>
 
            <settings name="otherstuff">
                <set name="helloworld_text">a third text</set>
            </settings>
 
        </configuration>
    """)
 
 
    # instanciate our plugin with our test config
    myplugin = Tutorial2bPlugin(fakeConsole, conf3)
 
    # we then call onStartup() as the real B3 would do
    myplugin.onStartup()
 
    # make superadmin connect to the fake game server on slot 0
    joe.connects(cid=0)
 
    # make superadmin try our commands
    joe.says('!helloworld')
 
 
 
    print("\n\n * * * * * * * * * * * *  Test 4 starting below * * * * * * * * * * * * \n\n")
 
    # create a config just for our tests
    conf3 = XmlConfigParser()
    conf3.setXml("""\
        <configuration plugin="tutorial2a">
 
            <settings name="command_level">
                <set name="helloworld">2</set>
            </settings>
 
        </configuration>
    """)
 
 
    # instanciate our plugin with our test config
    myplugin = Tutorial2bPlugin(fakeConsole, conf3)
 
    # we then call onStartup() as the real B3 would do
    myplugin.onStartup()
 
    # make superadmin connect to the fake game server on slot 0
    joe.connects(cid=0)
 
    # make superadmin try our commands
    joe.says('!helloworld')
 
 

And here's the test results

 * * * * * * * * * * * *  Test 3 starting below * * * * * * * * * * * * 


ERROR    : Tutorial2bPlugin: bad value reading "command_level/helloworld" from config file. Using default value instead
INFO     : Tutorial2bPlugin: minimum level required for the command_level !helloworld is : 1
INFO     : Tutorial2bPlugin: text displayed for command !helloworld is : 'a third text'
DEBUG    : Register Event: Stop Process: Tutorial2bPlugin
DEBUG    : Register Event: Program Exit: Tutorial2bPlugin
DEBUG    : AdminPlugin: Command "helloworld (None)" registered with cmd_helloWorld for level (1, 100)

Joe connects to the game on slot #0
VERBOSE  : 0 cid changed from None to 0
DEBUG    : Client Connected: [0] Joe - zaerezarezar ({})
DEBUG    : User not found zaerezarezar: 'No client found in fakestorage for {id: 0, guid: zaerezarezar}'
BOT      : Client not found in the storage zaerezarezar, create new
VERBOSE2 : Aborted Making Alias for cid: 0, name is the same
DEBUG    : Client Authorized: [0] Joe - zaerezarezar

Joe says "!helloworld"
VERBOSE  : Queueing event Say !helloworld
VERBOSE  : Parsing Event: Say: AdminPlugin
DEBUG    : AdminPlugin: OnSay handle 5:"!helloworld"
DEBUG    : AdminPlugin: Handle command !helloworld
>>> a third text


 * * * * * * * * * * * *  Test 4 starting below * * * * * * * * * * * * 


INFO     : Tutorial2bPlugin: minimum level required for the command_level !helloworld is : 2
ERROR    : Tutorial2bPlugin: bad value reading "otherstuff/helloworld_text" from config file. Using default value instead
INFO     : Tutorial2bPlugin: text displayed for command !helloworld is : 'default hello world text'
DEBUG    : Register Event: Stop Process: Tutorial2bPlugin
DEBUG    : Register Event: Program Exit: Tutorial2bPlugin
DEBUG    : AdminPlugin: Command "helloworld (None)" registered with cmd_helloWorld for level (2, 100)

Joe connects to the game on slot #0
VERBOSE  : 1 cid changed from 0 to 0
DEBUG    : Client Connected: [0] Joe - zaerezarezar ({})

Joe says "!helloworld"
VERBOSE  : Queueing event Say !helloworld
VERBOSE  : Parsing Event: Say: AdminPlugin
DEBUG    : AdminPlugin: OnSay handle 5:"!helloworld"
DEBUG    : AdminPlugin: Handle command !helloworld
sending msg to Joe: You do not have sufficient access to use !helloworld
customize/plugin_sdk/tutorial2.txt · Last modified: 2011/03/28 01:00 (external edit)

Page Tools