MushPy: a Python framework on top of Mushclient's API

Posted by Icewolf on Sat 15 May 2010 02:41 AM — 10 posts, 43,754 views.

#0
This is to announce a Python framework for Mushclient's API. It is a
more "Pythonic" way to develop MUD automations. Currently, there are
several modules available:

* terminal.py:
Construct a Python file-like object for the Mushclient's notepads

* objects.py:
* Trigger
* Alias
* Timer

* coroutine.py:
* Decorate a callable to use it as a coroutine.

The package can be downloaded from here: http://code.google.com/p/mushpy/downloads/list
#1

=============
Terminal
=============

Help on class McTerminal in module mushpy.terminals:

class McTerminal
 |  Construct a Python file-like object for the Mushclient's notepads.
 |  
 |  This is an abstraction of Mushclient's notepads for the ease of use in
 |  Python. An example is better than thousand words:
 |  
 |      >>> import sys
 |      >>> sys.stdout = McTerminal("Standard Output")
 |  
 |      >>> print "Hello world"
 |  
 |  Once sys.stdout is been replace by an McTerminal instance, all codes that
 |  write to stdout (print, etc.) will be directed to a Mushclient's notepad
 |  titled "Standard Output". Fancy, heh?
 |  
 |  When instantiating, several keyword options can be used:
 |  
 |      * time_stamp: (boolean) prefix a time stamp to each line, default to False
 |      * prompt_to_save: (boolean) prompt to save on exit, default to False
 |      * read_only: (boolean) if the notepad is readonly. default to True
 |  
 |  An example:
 |      
 |      >>> notepad = McTerminal( "An McTerminal Example", time_stamp=True, read_only=True )
 |      >>> print >> notepad, "Hello world!"
 |      >>> print >> notepad, "Yeah! It's so easy to use Mushclient!"
 |  
 |  The output:
 |  
 |      05/14/10 22:01:26: Initializing...
 |      05/14/10 22:03:41: Hello world!
 |      05/14/10 22:03:57: Yeah! It's so easy to use Mushclient!

#2

============
Trigger
============

Help on class Trigger in module mushpy.objects:

class Trigger(McObject)
 |  The Python wrapper of a trigger representation.
 |  
 |  This is a thin layer on top of Mushclient's trigger API.
 |  
 |  The Trigger class only can be instantiate by a name (optional):
 |  
 |      >> trig = Trigger("trigger_name")
 |  
 |  or without a name:
 |  
 |      >> trig = Trigger()
 |  
 |  One can test if a trigger object has a trigger object exists in Mushclient:
 |  
 |      >> trig.exists()
 |  
 |  A trigger w/o name will return False. If a trigger does not exists, one can
 |  create one with:
 |  
 |      >> trig.create( "pattern", group="group_name", one_shot=1, ... )
 |  
 |  The options are listed in Mushclient's document under SetTriggerOption().
 |  
 |  If the trigger is instantiated without a name specified, a unique one will
 |  be assigned to be when created in Mushclient.
 |  
 |  If the trigger with the specified name exists, many details about it can be
 |  retreived or set, like a Python dict:
 |  
 |      >> print trig['group']
 |      >> print trig['match']
 |      >> print trig['enabled']
 |      >> print trig['one_shot']
 |      >> # ...
 |  
 |      >> trig['group'] = 'new_group_name'
 |      >> trig['enabled'] = 0 # disable
 |  
 |  Some methods can be used:
 |  
 |      >>> trig.disable()
 |      >>> trig.enable()
 |      >>> trig.delete()   # this will delete the underlying mushclient trigger
 |  
 |  One powerfull feature built beyond Mushclient is the callback function:
 |  
 |      >>> def func(name, line, wc):
 |      ...     pass
 |  
 |      >>> trig.callback = func
 |  
 |  Once the trigger is matched, func will be called. Callback can be accessed like an attribute:
 |  
 |      >>> print trig.callback
 |  
 |  It can also be deleted:
 |  
 |      >>> del trig.callback
 |  
 |  Note that the callback, once set, will overwrite the existing "script"
 |  option. Even it is deleted, the original "script" option will not be
 |  restored. However, it is more convenient to use callback so that "script"
 |  option need not to be set explicitly any more.
 |  
 |  WARNING: Do not try to modify a trigger's "script" option once a callback
 |  is used. Because MushPy uses a global dispatcher to dispatch (invoke)
 |  callbacks for all triggers that has a callback registered. In other words,
 |  when a callback is set for a trigger, the trigger's "script" option is set
 |  to the global dispatcher. Once changed, when the trigger fires, dispatcher
 |  will not be invoked, as a result, the desired call back will not be invoked
 |  either.
 |  
 |  On top of the powerfull callback, the decorator is proudly presented:
 |  
 |      >>> @Trigger.make( "some pattern", group="group_name", one_shot=1, ... )
 |      ... def callback( name, line, wc ):
 |      ...     pass
 |  
 |  Therefore, a trigger will be created. The function "callback" will be
 |  registered as the trigger's callback. 
 |  
 |  The trigger object is accessible as the callback's attribute. Since a
 |  callback function may be used by multiple triggers, multiple aliases, and
 |  multiple timers, it has a set, "mc_objects", to store all its connected
 |  triggers/aliases/timers.
 |  
 |      >>> for obj in trig.mc_objects:
 |      ...     print obj['name']


#3

==============
Alias
==============

Help on class Alias in module mushpy.objects:

class Alias(McObject)
 |  The Python wrapper of Mushclient's Alias
 |  
 |  Usage is the same as Trigger.

=============
Timer
=============

Help on class Timer in module mushpy.objects:

class Timer(McObject)
 |  Python wrapper of Mushclient's Timer.
 |  
 |  Usage is similar to Trigger/Alias, with a few exceptions.
 |  
 |  To instantiate:
 |      
 |      >>> timer = Timer( hour, minute, second, option1=value1, option2=value2, ... )
 |  
 |  To use as a decorator
 |  
 |      >>> @Timer.make( hour, minute, second, [keyword options...] )
 |      >>> def callback( name ):
 |      ...     pass

#4

==============
Coroutine
==============

Help on class coroutine in module mushpy.coroutine:

class coroutine(__builtin__.object)
 |  Decorate a callable to use it as a coroutine.
 |  
 |  Usage:
 |      >>> @coroutine()
 |      >>> def bla():
 |      ...     pass
 |  
 |  or
 |      >>> def bla(): pass
 |      >>> bla = coroutine()(bla)
 |  
 |  To write a coroutine, there is a protocol to follow:
 |      
 |      1. The decorated callable takes the arguments to generate the
 |      generator. If you don't know what it is, just ignore it and use no
 |      arguments.
 |  
 |      2. On the first line of the coroutine, you must use the following yield
 |      statement to receive the arguments from the caller.
 |  
 |          >>> args, kwargs = yield
 |  
 |      3. Likewise, each time you yield, the return value of the yield
 |      statement will be (args, kwargs) from the caller.
 |  
 |      4. (Feature) You can use the following yields to specify when to resume
 |      running the code below the yields:
 |  
 |          >>> # resume after 5 seconds
 |          >>> args, kwargs = yield ('timer', 0,0,5)
 |  
 |          >>> # resume when the pattern matched
 |          >>> args, kwargs = yield ('match', r'some regular expression pattern')
 |  
 |      5. When the coroutine returns, the caller will receive the return value
 |      StopIteration. Otherwise, the caller will always receive the return
 |      value None. Since it's not the attemp of a coroutine to return
 |      something to the caller, but resume doing something at times, I
 |      currently do not plan to provide a protocol for returning a meaningfull
 |      value to the caller.
 |  
 |  An example of using coroutines:
 |  
 |  >>> @coroutine(arg_to_aCoroutine)
 |  ... def aCoroutine(arg_to_aCoroutine):
 |  ...     args, kwargs = yield    # Must have this at the first line
 |  ...     print 'arguments of the first call', args, kwargs
 |  ... 
 |  ...     args, kwargs = yield ('timer', 0,0,3)
 |  ...     print '3 seconds have passed. Arguments:', args, kwargs
 |  ... 
 |  ...     args, kwargs = yield ('match', r'hello world')
 |  ...     print 'I see "hello world" from the MUD output. Arguments:', args, kwargs
 |  ... 
 |  ...     # Coroutine returns here. Caller will receive StopIteration
 |  ...     return

Amended on Sat 15 May 2010 02:47 AM by Icewolf
USA #5
Wow, this looks awesome! I think this could turn out to be a really powerful tool.
#6
Coderunner said:

Wow, this looks awesome! I think this could turn out to be a really powerful tool.


Thanks. I actually already have something small based on this "framework". It'd be great if ppl join me or at least give me some feedback:)
Poland #7
One little addition I'd suggest - in consts.py you define class to hold return codes. I ran it through simple regexp to obtain this dictionary:


smart_ret = { 0 : "No error",
30001 : "The world is already open",
30002 : "The world is closed, this action cannot be performed",
30003 : "No name has been specified where one is required",
30004 : "The sound file could not be played",
30005 : "The specified trigger name does not exist",
30006 : "Attempt to add a trigger that already exists",
30007 : "The trigger 'match' string cannot be empty",
30008 : "The name of this object is invalid",
30009 : "Script name is not in the script file",
30010 : "The specified alias name does not exist",
30011 : "Attempt to add a alias that already exists",
30012 : "The alias 'match' string cannot be empty",
30013 : "Unable to open requested file",
30014 : "Log file was not open",
30015 : "Log file was already open",
30016 : "Bad write to log file",
30017 : "The specified timer name does not exist",
30018 : "Attempt to add a timer that already exists",
30019 : "Attempt to delete a variable that does not exist",
30020 : "Attempt to use SetCommand with a non-empty command window",
30021 : "Bad regular expression syntax",
30022 : "Time given to AddTimer is invalid",
30023 : "Direction given to AddToMapper is invalid",
30024 : "No items in mapper",
30025 : "Option name not found",
30026 : "New value for option is out of range",
30027 : "Trigger sequence value invalid",
30028 : "Where to send trigger text to is invalid",
30029 : "Trigger label not specified/invalid for 'send to variable'",
30030 : "File name specified for plugin not found",
30031 : "There was a parsing or other problem loading the plugin",
30032 : "Plugin is not allowed to set this option",
30033 : "Plugin is not allowed to get this option",
30034 : "Requested plugin is not installed",
30035 : "Only a plugin can do this",
30036 : "Plugin does not support that subroutine (subroutine not in script)",
30037 : "Plugin does not support saving state",
30037 : "Plugin could not save state (eg. no state directory)",
30039 : "Plugin is currently disabled",
30040 : "Could not call plugin routine",
30041 : "Calls to 'Execute' nested too deeply",
30042 : "Unable to create socket for chat connection",
30043 : "Unable to do DNS (domain name) lookup for chat connection",
30044 : "No chat connections open",
30045 : "Requested chat person not connected",
30046 : "General problem with a parameter to a script call",
30047 : "Already listening for incoming chats",
30048 : "Chat session with that ID not found",
30049 : "Already connected to that server/port",
30050 : "Cannot get (text from the) clipboard",
30051 : "Cannot open the specified file",
30052 : "Already transferring a file",
30053 : "Not transferring a file",
30054 : "There is not a command of that name",
30055 : "That array already exists",
30056 : "That name is not permitted for a key",
30056 : "That array does not exist",
30057 : "Values to be imported into array are not in pairs",
30058 : "Import succeeded, however some values were overwritten",
30059 : "Import/export delimiter must be a single character, other than backslash",
30060 : "Array element set, existing value overwritten",
30061 : "Array key does not exist",
30062 : "Cannot import because cannot find unused temporary character", }


which I find more useful.

There are other things, I'll write about them later.
#8
On trying to import this package, I get the following traceback: https://gist.github.com/2986151
#9
What's wrong with it?

Script error
World: mymud
Execution of line 12 column 0
Immediate execution
Traceback (most recent call last):
File "<Script Block >", line 12, in <module>
from coroutine import coroutine
File "C:\Python27\Lib\coroutine.py", line 2, in <module>
_gbl = ax._scriptEngine_.globalNameSpaceModule
NameError: name 'ax' is not defined

Line in error:
from coroutine import coroutine
Error context in script:
8 :
9 : from common import *
10 : from consts import *
11 :
12*: from coroutine import coroutine
13 :
14 : from terminals import McTerminal
15 :
16 : from objects import Trigger