world.GetOption proxy

Posted by Isthiriel on Thu 27 Dec 2007 09:56 AM — 10 posts, 47,140 views.

#0
In the style of my var.lua replacement, a similar dict-like object that proxies world.GetOption/world.GetAlphaOption/...

# ---------------------------------------------------------
# Python world.GetOption proxy dictionary
# ---------------------------------------------------------

import UserDict # if you don't have it already

class gopt(object, UserDict.DictMixin):
    __repr__ = UserDict.DictMixin.__repr__
    def canonize_name(self, name):
        # Global Options don't seem to have a canonical form.
        # In particular, "DefaultInputFontItalic " <-- trailing space
        # And "Tray Icon", "Icon Placement",
        #name = name.split()
        #if len(name) > 1:
        #    world.Note(repr(name))
        #    name = name[0] + [n.lower().capitalize() for n in name[1:]]
        #name = u"".join(name)
        ## Why, oh why!?
        #if name == u"DefaultInputFontItalic":
        #    name += " "
        return name
    def getopt(self, name):
        return world.GetGlobalOption(name)
    def setopt(self, name, value):
        raise TypeError("Cannot modify global options")
    def __getitem__(self, name):
        name = self.canonize_name(name)
        r = self.getopt(name)
        if None == r:
            raise KeyError(name)
    def __setitem__(self, name, value):
        assert value != None
        name = self.canonize_name(name)
        r = self.setopt(name, value)
        if eUnknownOption == r:
            raise KeyError(name)
    def __delitem__(self, name):
        raise TypeError("Cannot delete MUSHclient options")
    def keys(self):
        return world.GetGlobalOptionList
    def __getattr__(self, name):
        try:
            return self.__getitem__(name)
        except KeyError:
            raise AttributeError("MUSHclient has no option '%s'" % name)
    def __setattr__(self, name, value):
        try:
            self.__setitem__(name, value)
        except KeyError:
            raise AttributeError("MUSHclient has no option '%s'" % name)
    def __delattr__(self, name):
        raise TypeError("Cannot delete MUSHclient options")

class wopt(gopt):
    def canonize_name(self, name):
        name = name.lower()
        name = name.split()
        name = '_'.join(name)
        return name
    def getopt(self, name):
        return world.GetCurrentValue(name)
    def setopt(self, name, value):
        if name in world.GetAlphaOptionList:
            if isinstance(value, str):
                value = value.decode('utf8')
            assert isinstance(value, unicode)
            return world.SetAlphaOption(name, value)
        if name in world.GetOptionList:
            if isinstance(value, bool):
                value = int(value)
            assert isinstance(value, int)
            return world.SetOption(name, value)
        return eUnknownOption
    def keys(self):
        return sorted(list(world.GetAlphaOptionList) + list(world.GetOptionList))

class lwopt(wopt):
    def getopt(self, name):
        return world.GetLoadedValue(name)
    def setopt(self, name, value):
        raise TypeError("Cannot modify loadtime option values")

class dwopt(wopt):
    def getopt(self, name):
        return world.GetDefaultValue(name)
    def setopt(self, name, value):
        raise TypeError("Cannot modify default option values")

gopt = gopt()
wopt = wopt()
lwopt = lwopt()
dwopt = dwopt()


Hmm. I wonder if I should make a full OO over all the MUSHclient functions so you never need to explicitly reference world...
Amended on Thu 27 Dec 2007 11:08 AM by Isthiriel
#1
I am new to MUSHclient and looked at the Python scripting interface. I was very happy when I saw that MUSHclient is supporting Python. But after trying to use it, it seemed to be not very pythonic!

I would love to create a proper interface for that and your proxy classes are a good start. But somthing else:

- I love the duck-typing ability of Python. That means you can give a function any object you want as long as it behaves as expected. A basic example would be world.Note(world). At the current implementation this statement throws an error. But why, world does provide a string/unicode interface (world.__str__ or str(world)), acutally.

- Don't name attributes like functions! For example world.GetPluginID is of <type 'unicode'>, is supposed it is callable! Hence it should be named different, something like plugin_id

- Return error codes has to be transformed in exceptions with adequate helptext. That can be easily done based on the sample script, wich provides the ids and help texts.

- Triggers and aliases and timers should be classes. I haven't used such interfaces as GetAliasInfo but I hate them already. You should be able to do: alias.group = 'combat'

Ok that would be some work, but I think it is -realy- worth it.

I like the fact to have one world object. But a wrapper should ease the interface in the mentioned way. Personally I would add the globals, aliases and other lists to the world as an attribute: world.aliases['cast']

I have wrote that, after reading the first example in the help (world.Note...) so probably we have to do much more.

I am new to the community, what do you think? Please share your thoughts! I think Python is a very flexible and mighty language wich deserves a proper interface!

So far
Aryzu


EDIT: I have found something else:
- world.GetTriggerList should return a empty list not None, if it is empty

- Perhaps we should make a script class, containing the callback methods: OnWorldOpen, OnWorldClose, OnWorldConnect. And you have to inherit from it. Then the user have to instance his class and register it somehow. Thus you are able to generalise your scripts or give some options wich can be set up in the constructor, if you share your script.

- Why is the default script suffix '.pys'? They are valid python scripts, or aren't they?. You can't import files with *.pys and please add the script directory to the pythonpath so you can separate for example the wrapper script from the current script file.
Amended on Mon 19 Jan 2009 02:42 AM by Aryzu
Australia Forum Administrator #2
Quote:

They are valid python scripts, or aren't they?. You can't import files with *.pys and please add the script directory to the pythonpath so you can separate for example the wrapper script from the current script file.


I don't understand this part. Why can't you import files? And what do you mean by "are valid python scripts, or aren't they"?


USA #3
.pys is the default extension for PyScript, as set by the ActiveState Python distribution. MushClient doesn't have any links to the python engine, it uses the windows script host interface to go thru COM which goes thru pyWin32.py to get to the python engine.

As such, Mushclient is agnostic to which ActiveX engine you use (Lua is the only native engine), and doesn't set any configuration directives to your chosen script engine. You can configure the pythonpath yourself.

This is also why the object model isn't very 'pythonic'. It's very 'COMish' (I avoided COMic for you Nick ;)) and even doesn't conform to the Lua methodology of design since Lua was added in later versions.

Hope that helps. :)
#4
@WillFa: Thank's for clarifying that.

I thought, that the extension is hardcoded. Of course you can name your files as you want. Just add the path manually. And you can even add your script-folders to the python path. I have locked at os.curdir and it is set to C:\Documents and Settings\Administrator\

Hence importing images other file objects like databases can only be imported with absolute pathes or realtive pathes against your curdir.

@Nick Gammon: I hadn't understood why somebody would name python script files '.pys' because that has an disadvantage. If you have multiple scripts, a basic one (e.g. a wrapper class) and one with trigger callbacks. If you name them '.pys' you can't import the basic script in your trigger script. But if you name the '.py' it workes, assuming your pythonpath is adjusted. I thought about why somebody would name their script file pys, but couldn't find an answer, because it has many disadvantages. furthermore they are valid python files. I mean you can import them from a valid normal python program and they will work (with few preparations).

If you can't change the setup of the script engine it doesn't matter, but it would be nice, if the current working directory is set to the folder of your script file, or if that is to dynamic to the MUSHclient script folder.

The wrapper would be something extra. Then you could import a file and subclass a class and use it's world wrapper. It could require some tricks to get the world COMobject from the globals() or register an instance, but it should be possible.
Amended on Mon 19 Jan 2009 01:19 PM by Aryzu
Australia Forum Administrator #5
When I added Python support I thought the files were suffixed .pys - I have amended the source to also allow .py files when loading Python script files. This only affects the names that show up in the file browser, of course you can use any file names.

As for the current directory, that is something that tends to change as you browse around (eg. browse for a sound file). Thus in my scripts I use GetInfo to find the various paths (eg. scripts path, MUSHclient executable path), and use that to work out exactly where to load things from.


Quote:

Willfa:

This is also why the object model isn't very 'pythonic'. It's very 'COMish' (I avoided COMic for you Nick ;)) and even doesn't conform to the Lua methodology of design since Lua was added in later versions.


The object model isn't very good, I admit. When I started doing it I wasn't really sure of the best way of doing it, and wrestling with Microsoft's COM model, and their documentation, didn't help.

If I was starting again from scratch, certainly the Lua model would be better. In fact there would probably only *be* a Lua model <evilgrin>. However don't worry, I don't plan to start again from scratch in a hurry. ;)
USA #6
Quote:
The object model isn't very good, I admit.


I actually think you did a great job at making things accessible in an agnostic fashion. For example, if the original design centered on VBscript, then GetInfo probably would have been broken up into Collections, which jscript and perl would have problems with.


And for starting over with only Lua, well, it wouldn't affect me, but one of my favorite selling points to people is that it is language agnostic. So I'm glad that's not happening any time soon. :)
#7
Quote:
Aryzu wrote:
A basic example would be world.Note(world). At the current implementation this statement throws an error. But why, world does provide a string/unicode interface (world.__str__ or str(world)), acutally.

The problem there is that world.Note is not a python function, it's a COM function and the python interpreter doesn't know that it's expecting a string, so passes the world object without casting it to a string. The error comes from the COM interface, not Python.

Quote:
Aryzu wrote:
Triggers and aliases and timers should be classes. I haven't used such interfaces as GetAliasInfo but I hate them already. You should be able to do: alias.group = 'combat'

You mean like in my addxml.pys? :)
http://www.gammon.com.au/forum/?id=8398

It's not a really OO implementation, but, I do use it in the following ways:
x = XMLTrigger('NoteOnNewLine')
x.group = 'NoteOnNewLine'
x.script = 'OnNewLine'
x.send_to = eXmlSendOutput
x.send = ''
x.set_flags(eEnabled | eKeepEvaluating | eTriggerRegularExpression | eTemporary)
x.match = '$'
x.sequence = 20
x.add()

def TimerCreate():
	x = XMLTimer.get('TimerPoll')
	if not x:
		x = XMLTimer('TimerPoll')
	x.set_flags(eEnabled | eActiveWhenClosed | eTemporary)
	x.hour = 0
	x.minute = 0
	x.second = 0.1
	x.send_to = eXmlSendOutput
	x.send = ''
	x.group = 'TimerPoll'
	x.script = 'TimerPoll'
	x.add()

Of course, set_flags could probably be implemented better. (Pythonic set springs to mind.)

Quote:
Aryzu wrote:
If you name them '.pys' you can't import the basic script in your trigger script.

I was going to say that you can, because all my files are named .pys and work fine. (I take advantage of my editor's highlighting to light up the MUSHclient specific functions as well.)

However, on closer inspection, I actually have this disaster at the top of my main script file:
cwd = world.GetAlphaOption('script_filename')
cwd = os.path.split(cwd)[0]
os.chdir(cwd)
for d in [cwd, r"C:\Program Files\Python\25\Lib", r"C:\Program Files\Python\25\Lib\lib-tk", ]:
    if not d in sys.path:
        sys.path.append(d)

execfile('mushclient.pys')
execfile('world_options.pys')
execfile('world_variables.pys')

vTimer = 'ScrStartup_TimersEnabled'
try:
    wvar[vTimer]
except KeyError:
    wvar[vTimer] = wopt.enable_timers
    wopt.enable_timers = False
    wopt.save()
# ---------------------------------------------------------
# Pre-Amble above this line. DO NOT MODIFY
# ---------------------------------------------------------

ct = time.clock()
idle_timer = ct

execfile('note.pys')
execfile('log.pys')
execfile('mxp.pys')
execfile('world.pys')
execfile('addxml.pys')
execfile('timerpoll.pys')
execfile('disctime.pys')
execfile('discinfobar.pys')

The mucking about with the timers is because I have one that runs every 0.2s that produces a spectacular amount of spam if the script fails to compile. Disabling the timers here allows me to reenable them at the end of the script's main execution, assuming it all compiles.
# ---------------------------------------------------------
# Post-Amble below this line. DO NOT MODIFY
# ---------------------------------------------------------
try:
    wopt.enable_timers = wvar[vTimer]
    del wvar[vTimer]
    wopt.save()
except KeyError:
    pass
world.ColourNote('white', 'black', "Script Compiled")


execfile() does achieve most of what import does, however IIRC, when it fails, instead of getting a nice ImportError with a stacktrace, the message is fairly meaningless :(
#8
Quote:
Isthiriel wrote:
Quote:
Aryzu wrote:
A basic example would be world.Note(world). At the current implementation this statement throws an error. But why, world does provide a string/unicode interface (world.__str__ or str(world)), acutally.


The problem there is that world.Note is not a python function, it's a COM function and the python interpreter doesn't know that it's expecting a string, so passes the world object without casting it to a string. The error comes from the COM interface, not Python.

It wasen't realy meant as a question. It was more a ironic question, why the COM interface exspects strings, but doesn't accept -string like- object. That's absoltly not -pythonic- (to use this phrase again).

Quote:
Isthiriel wrote:
You mean like in my addxml.pys? :)
http://www.gammon.com.au/forum/?id=8398


Yes, something like that. A thin object wrapper, which uses the GetInfo methods with it's ugly numerical arguments to populte the attributes.

Quote:
Isthiriel wrote:
Of course, set_flags could probably be implemented better. (Pythonic set springs to mind.)

The flags are ok in my eyes. Sets would be an alternative, but I have never seen flags implemented as a set in Python. I don't know what the Python way to do something like that.

Quote:
Isthiriel wrote:
execfile() does achieve most of what import does, however IIRC, when it fails, instead of getting a nice ImportError with a stacktrace, the message is fairly meaningless :(

But "importing" with exec doesn't stick to the Zen of Python:
"Namespaces are one honking great idea -- let's do more of those!"
However, is there any advantage of using the '.pys' extensions? I would just use '.py'. Because exec imports have many disadvantages, like the broken error handling on import.
Amended on Thu 22 Jan 2009 03:08 PM by Aryzu
#9
I have written a interface class to write scripts in a more OOP way. The interface needs the globals dictionary of the script file in order to register global callbacks, which are defined by setting the descriptor 'extern'. You can have multiple script classes or objects. All the callback functions are all called sequentially. Here is a sample of a basic script file. What do you think so far?

import os, sys
cwd = os.path.split(world.GetAlphaOption('script_filename'))[0]
os.chdir(cwd)
if not cwd in sys.path:
    sys.path.append(cwd)
# ---------------------------------------------------------
# Pre-Amble above this line. DO NOT MODIFY
# ---------------------------------------------------------


import interface
from interface import extern

class MyScript(interface.Script):
    def __init__(self, *args, **kargs):
        interface.Script.__init__(self, *args, **kargs)
        self.world.Note('MyScript loaded')
    
    @extern
    def OnPrompt(self, name, line, wildcards):
        self.world.Note('MyScript.OnPrompt: %s, %s, %s' % 
                (name, line, wildcards))


class MySecondScript(interface.Script):
    def __init__(self, *args, **kargs):
        interface.Script.__init__(self, *args, **kargs)
        self.world.Note('MySecondScript loaded')
    
    @extern
    def OnPrompt(self, name, line, wildcards):
        self.world.Note('MyScript.OnPrompt: %s, %s, %s' % 
                (name, line, wildcards))


s1 = MyScript(globals())
s2 = MySecondScript(globals())



EDIT: You can get the source of interface.py from:
http://tinyurl.com/bjvjos