################################################################################
#
# Four Worlds
# Copyright (c) 2009-????
#
# Package: polc
# File: main.py
#
# The automatic Python OLC system, capable of generating an online content editor
# for a given Python class or dict by using dictionaries to discover properties.
#
# OLC Initialization and Rendering
#
# Author: Stendec
#
################################################################################
import copy, olc, parsers, renderers
from types import *
from data import *
################################################################################
# A few things to let people skip 'import olc' in their own functions.
################################################################################
MENU_NOCHOICE = olc.MENU_NOCHOICE
MENU_CHOICE_INVALID = olc.MENU_CHOICE_INVALID
################################################################################
# OLC Initialization
################################################################################
def do_olc(sock, object, saver = None, key = None, options = None, autosave = False):
'''Initialize the Python OLC system for the given object.'''
# First, if there isn't a valid socket or object to edit, quit.
if sock is None or object is None:
return
# Create a new data object for this editor.
data = OLCData()
# Store data into the data object.
if saver is None:
data.object = object
else:
data.object = copy.copy(object)
data.saver = saver
data.key = key
data.sock = sock
# Make sure we have a proper options dict.'''
if type(options) == dict: data.opts = options
else: data.opts = { }
if data.opts.has_key('autosave') and data.opts['autosave'] is True:
autosave = True
# Push our data through to the OLC menu.
olc.do_olc(sock, polc_menu, polc_chooser, polc_parser, polc_saver, data, autosave)
################################################################################
# OLC Helper Functions
################################################################################
def fancy_key(key):
'''Attempt to make a key name fancy. If it's in an array of default names, return the
corresponding fancy name. Otherwise, convert underscores to spaces and make it
title case.'''
if fancy_keys.has_key(key): return fancy_keys[key]
else: return key.replace('_',' ').title()
def get_name(object):
'''Attempts to get a name from an object for display in the menu.'''
if type(object) == dict:
if object.has_key('name'): return object['name']
elif object.has_key('title'): return object['title']
else: return 'unknown'
if hasattr(object, '__name__'): return object.__name__
elif hasattr(object, '__str__'): return object.__str__()
elif hasattr(object, '__repr__'): return object.__repr__().strip('<>')
else:
t = ''
if hasattr(object, '__class__'):
t = str(object.__class__).split('.')
if len(t) == 2: t = t[1]
else: t = t[0]
if t: t += ': '
if hasattr(object, 'name'): return t+object.name
elif hasattr(object, 'title'): return t+object.title
else: return t+"unknown"
def olc_enter(sock, data, key):
'''Open an editor for a subobject with autosave set to True to prevent
showing the user a 'Save?' question when they back out.'''
if type(data.object) == dict: object = data.object[key]
else: object = getattr(data.object, key)
opts = {}
# Is there a name option?
if not opts.has_key('name'):
if data.opts.has_key('_'+key) and data.opts['_'+key].has_key('name'):
opts['name'] = data.opts['_'+key]['name']
else:
opts['name'] = get_name(object)
# No name? Use the key.
if opts['name'] == 'unknown': opts['name'] = key.replace('_',' ').title()
# Load options if there are any for this key.
if data.opts.has_key('_'+key):
for k,v in data.opts['_'+key].iteritems():
opts[k] = v
# Enter the OLC system.
do_olc(sock, object, None, key, opts, True)
def notepad_enter(sock, data, key):
'''Enter the notepad system. Store a copy of the working copy of the aux data
in the copy of the aux data attached to the socket. Confusing, no?'''
sock.aux("olc").working = data
sock.aux("olc").key = key
t = data.k(key,'type') or 'text'
if type(data.object) is dict: val = data.object[key]
else: val = getattr(data.object, key)
if not val.endswith('\r\n'): val += '\r\n'
sock.edit_text(val, notepad_end, t)
def notepad_end(sock, text):
'''Finish the notepad system.'''
if text.endswith('\r\n'): text = text[:-2]
if type(sock.aux("olc").working.object) is dict:
sock.aux("olc").working.object[sock.aux("olc").key] = text
else: setattr(sock.aux("olc").working.object, sock.aux("olc").key, text)
sock.aux("olc").working = None
sock.aux("olc").key = None
################################################################################
# OLC Choice List Menu
################################################################################
def polc_choice_list(sock, data, key):
'''Display a submenu of avaliable choices.'''
data.ekey = key
olc.do_olc(sock, polc_choice_menu, polc_choice_chooser, polc_choice_parser, None, data, True)
def polc_choice_menu(sock, data):
k = data.ekey
n = data.k(k,'name') or fancy_key(k)
if type(data.object) == dict: val = data.object[k]
else: val = getattr(data.object, k)
# If it's a str, split the comma-separated values.
if type(val) == str: val = val.replace(', ',',').split(',')
sock.send("{nCurrent %s%s{n" % (n,renderers.render_choice_list(sock, data, k, val)))
sock.send(renderers.render_choice_select(sock, data, k, val))
def polc_choice_chooser(sock, data, opt):
try: opt = int(opt)
except: return olc.MENU_CHOICE_INVALID
k = data.ekey
if type(data.object) == dict: val = data.object[k]
else: val = getattr(data.object, k)
# If it's a str, split the comma-separated values.
is_str = type(val) == str
if is_str: val = val.replace(', ',',').split(',')
c = data.k(k,'choices')
if type(c) == dict: ks = sorted(c.keys())
else: ks = sorted(c)
if opt < 0 or opt >= len(ks): return olc.MENU_CHOICE_INVALID
v = ks[opt]
if v in val: val.remove(v)
else: val.append(v)
val = sorted(val)
if is_str: val = ', '.join(val)
if type(data.object) is dict: data.object[k] = val
else: setattr(data.object, k, val)
return olc.MENU_NOCHOICE
def polc_choice_parser(sock, data, opt, val):
return True
################################################################################
# OLC Menu Display
################################################################################
def polc_menu(sock, data):
'''Display the menu for the provided object.'''
# If we have an order option, use it. Else, use the raw keys
if data.opts.has_key('order'): keys = data.opts['order']
else:
if type(data.object) == dict: keys = data.object.keys()
else: keys = data.object.__dict__.keys()
keys.sort(key=str.lower)
# Output the name as the header.
if data.opts.has_key('name'): n = data.opts['name']
else:
n = get_name(data.object)
if n == 'unknown' and data.key is not None: n = data.key.replace('_',' ').title()
sock.send("{y[{c%s{y]" % n)
# Are any custom keys specified? If so, remove them from the list of keys to use.
mkeys = list(menukeys)
if data.opts.has_key('keys'):
for k,v in data.opts['keys'].iteritems(): mkeys.remove(v)
count = 0
for k in keys:
if not k: sock.send("")
elif not data.opts.has_key('hidden') or (data.opts.has_key('hidden') and not k in data.opts['hidden']):
hk = data.opts.has_key('_'+k)
# Are we assuming a certain value?
if hk and data.opts['_'+k].has_key('value'):
val = data.opts['_'+k]['value']
else:
if type(data.object) == dict: val = data.object[k]
else: val = getattr(data.object, k)
# Are we forcing this to display a certain way?
if hk and data.opts['_'+k].has_key('force'):
if data.opts['_'+k]['force'] == list:
# Convert to a list. For a string, separate by comma. Everything else, wrap it.
if type(val) == str: val = val.replace(', ',',').split(',')
else: val = [val]
else:
# Try converting it the messy way.
try: val = data.opts['_k']['force'](val)
except: val = "<ERROR>"
# Get the name
pk = hk and data.opts['_'+k].has_key('name')
if pk: n = data.opts['_'+k]['name']
else: n = fancy_key(k)
# Generate a string from the value.
# Is there a custom renderer?
if type(data.k(k,'renderer')) in (FunctionType,LambdaType,BuiltinFunctionType):
n = '%-15s' % n
v = ': {c' + data.k(k,'renderer')(sock, data, k, val)
elif type(data.k(k,'raw_renderer')) in (FunctionType,LambdaType,BuiltinFunctionType):
v = data.k(k,'raw_renderer')(sock, data, k, val)
elif type(data.k(k,'extend_renderer')) in (FunctionType,LambdaType,BuiltinFunctionType):
v = None
# Choices have to be taken care of separately.
elif data.k(k,'choices') is not None:
if type(val) in (list,tuple):
# List Display
v = renderers.render_choice_list(sock, data, k, val)
else:
# Normal Display
n = '%-15s' % n
v = renderers.render_choice(sock, data, k, val)
# Call a renderer, based on the type.
# Boolean
elif type(val) is bool:
n = '%-15s' % n
v = renderers.render_bool(sock, data, k, val)
# Number
elif type(val) in (int,float,long):
n = '%-15s' % n
v = renderers.render_number(sock, data, k, val)
# List
elif type(val) in (list,tuple):
v = renderers.render_list(sock, data, k, val)
# String
elif type(val) in (str,unicode):
v = renderers.render_string(sock, data, k, val)
# Sub-Item
elif type(val) in (dict,ClassType,InstanceType) or data.k(k,'options') is not None:
v = renderers.render_subitem(sock, data, k, val)
# Last Effort
else:
if hasattr(val, '__str__'): n,v = '%-15s' % n, ': {c' + str(val)
else: n,v = '%-15s' % n, ': {y<UNKNOWN>'
# Display the option
if type(data.k(k,'extend_renderer')) in (FunctionType,LambdaType,BuiltinFunctionType):
if data.opts.has_key('keys') and k in data.opts['keys']: sock.send_raw("{g%s) " % data.opts['keys'][k])
else:
sock.send_raw("{g%s) " % menukeys[count])
count += 1
data.k(k,'extend_renderer')(sock, data.object)
elif data.opts.has_key('readonly') and k in data.opts['readonly']:
sock.send("{g %s%s{n" % (n,v))
elif data.opts.has_key('keys') and k in data.opts['keys']:
sock.send("{g%s) %s%s{n" % (data.opts['keys'][k], n, v))
else:
sock.send("{g%s) %s%s{n" % (menukeys[count], n, v))
count += 1
def polc_chooser(sock, data, opt):
'''Determine if the provided option was valid.'''
# Is this a command for the character?
if hasattr(sock, 'ch') and data.opts.has_key('allow-commands') and opt.startswith('- '):
sock.ch.act(opt[2:])
return olc.MENU_NOCHOICE
# If we have an order option, use it. Else, use the raw keys
if data.opts.has_key('order'):
keys = copy.copy(data.opts['order'])
while '' in keys: keys.remove('')
else:
if type(data.object) == dict: keys = data.object.keys()
else: keys = data.object.__dict__.keys()
keys.sort(key=str.lower)
# Are any custom keys set? We have to build the list of potential keys specially in that case.
menkeys = list(menukeys)
if data.opts.has_key('keys'):
for k,v in data.opts['keys'].iteritems(): menkeys.remove(v)
mkeys = []
lkeys = []
count = 0
ro = data.opts.has_key('readonly')
hd = data.opts.has_key('hidden')
hk = data.opts.has_key('keys')
for k in keys:
if ro and k in data.opts['readonly']: continue
elif hd and k in data.opts['hidden']: continue
elif hk and k in data.opts['keys']: mkeys.append(data.opts['keys'][k].upper())
else:
mkeys.append(menkeys[count].upper())
count += 1
lkeys.append(k)
menkeys = mkeys
keys = lkeys
else:
lkeys = []
ro = data.opts.has_key('readonly')
hd = data.opts.has_key('hidden')
for k in keys:
if ro and k in data.opts['readonly']: continue
elif hd and k in data.opts['hidden']: continue
else: lkeys.append(k)
keys = lkeys
# It has to have a valid key.
if not opt[0].upper() in menkeys:
return olc.MENU_CHOICE_INVALID
opt = menkeys.index(opt[0].upper())
if opt < 0 or opt >= len(keys): return olc.MENU_CHOICE_INVALID
# Get the item to determine what to do now.
k = keys[opt]
# Are we assuming a certain value?
if data.opts.has_key('_'+k) and data.opts['_'+k].has_key('value'):
val = data.k(k,'value')
else:
if type(data.object) == dict: val = data.object[k]
else: val = getattr(data.object, k)
# Are we forcing this to display a certain way?
force = data.k(k,'force')
if not force is None:
if force == list:
# Convert to a list. For a string, separate by comma. Everything else, wrap it.
if type(val) == str: val = val.replace(', ',',').split(',')
else: val = [val]
else:
# Try converting it the messy way.
try: val = force(val)
except: val = "<ERROR>"
# Is there a custom handler?
func = data.k(k,'handler')
if type(func) in (FunctionType,LambdaType,BuiltinFunctionType):
return func(sock, data, k)
# Is there an extension-type handler?
func = data.k(k,'extend_handler')
if type(func) in (FunctionType,LambdaType,BuiltinFunctionType):
v = func(sock, data.object)
if v == olc.MENU_NOCHOICE: return v
elif v == olc.MENU_CHOICE_INVALID: return v
elif v == olc.MENU_CHOICE_OK: return opt + 1
# Is there a custom prompt?
prompt = data.k(k,'prompt')
n = data.k(k,'name')
if n is None: n = fancy_key(k)
# Do we have a custom parser?
func = data.k(k,'parser')
if type(func) in (FunctionType,LambdaType,BuiltinFunctionType):
if prompt: sock.send_raw(prompt)
else: sock.send_raw("Enter a new %s: " % n)
return opt + 1
# Is this a multiple-choice list?
elif data.k(k,'choices'):
if type(val) in (list, tuple):
# The value itself is a list. Show the special choice list menu.
polc_choice_list(sock, data, k)
return olc.MENU_NOCHOICE
else:
sock.send(renderers.render_choice_select(sock, data, k, val))
if prompt: sock.send_raw(prompt)
else: sock.send_raw("Pick a %s: " %n)
return opt + 1
# Boolean Value, If so, just toggle it and return.
elif type(val) is bool:
if type(data.object) == dict: data.object[k] = not val
else: setattr(data.object, k, not val)
return olc.MENU_NOCHOICE
# Numeric Value. Display a prompt.
elif type(val) in (int, float, long):
if prompt: sock.send_raw(prompt)
else:
min = data.k(k,'min')
max = data.k(k,'max')
range = data.k(k,'range')
if type(range) == str: min, max = [int(value) for value in range.split(',')]
elif type(range) in (list,tuple) and len(range) == 2: min, max = range[0], range[1]
if min is not None and max is not None: sock.send_raw("{gEnter a new %s (%d-%d): " % (n,min,max))
else: sock.send_raw("{gEnter a new %s: " % n)
return opt + 1
# List Value. Display a prompt.
elif type(val) in (list, tuple):
if prompt: sock.send_raw(prompt)
else: sock.send_raw("{gEnter comma-separated list of %s: " % n)
return opt + 1
# String Value. If necessary, load the text editor. Else, display a prompt.
elif type(val) in (str, unicode):
if data.k(k,'multi'):
notepad_enter(sock, data, k)
return olc.MENU_NOCHOICE
else:
if prompt: sock.send_raw(prompt)
else: sock.send_raw("{gEnter a new %s: " % n)
return opt + 1
# Sub-Item. Go into it.
elif type(val) in (dict,ClassType,InstanceType):
olc_enter(sock, data, k)
return olc.MENU_NOCHOICE
# Still here? You shouldn't be.
return olc.MENU_CHOICE_INVALID
def polc_parser(sock, data, opt, val):
'''Parse the user's input and validate it, then store it in the object.'''
opt -= 1
# If we have an order option, use it. Else, use the raw keys
if data.opts.has_key('order'):
keys = copy.copy(data.opts['order'])
while '' in keys: keys.remove('')
else:
if type(data.object) == dict: keys = data.object.keys()
else: keys = data.object.__dict__.keys()
keys.sort(key=str.lower)
# Are any custom keys set? We have to build the list of potential keys specially in that case.
lkeys = []
ro = data.opts.has_key('readonly')
hd = data.opts.has_key('hidden')
for k in keys:
if ro and k in data.opts['readonly']: continue
elif hd and k in data.opts['hidden']: continue
else: lkeys.append(k)
keys = lkeys
# Is it out of our range?
if opt < 0 or opt >= len(keys): return False
# Get the item to determine what to do now.
k = keys[opt]
# Are we assuming a certain value?
if data.hk(k,'value'): old = data.k(k,'value')
else:
if type(data.object) == dict: old = data.object[k]
else: old = getattr(data.object, k)
# Are we forcing this to display a certain way?
force = data.k(k,'force')
if not force is None:
if force == list:
# Convert to a list. For a string, separate by comma. Everything else, wrap it.
if type(old) == str: old = old.replace(', ',',').split(',')
else: old = [old]
else:
# Try converting it the messy way.
try: old = force(old)
except: old = "<ERROR>"
# Do we have a custom parser?
func = data.k(k,'parser')
if type(func) in (FunctionType,LambdaType,BuiltinFunctionType):
val = func(sock, data, k, val)
# Do we have a custom extend-type parser?
func = data.k(k,'extend_parser')
if type(func) in (FunctionType,LambdaType,BuiltinFunctionType):
val = func(sock, data.object, val)
if val is True: return True
else: return False
# Multiple-choice List
elif data.k(k,'choices'): val = parsers.parse_choice(sock, data, k, val)
# Call a function depending on the type of value.
elif type(old) in (int,float,long): val = parsers.parse_number(sock, data, k, val)
elif type(old) in (list, tuple): val = parsers.parse_list(sock, data, k, val)
elif type(old) in (str, unicode): val = parsers.parse_string(sock, data, k, val)
# Do we have a boolean? If so, return it.
if type(val) is bool: return val
# Is there a validator? If so, call it.
func = data.k(k,'validate')
if type(func) in (FunctionType,LambdaType,BuiltinFunctionType):
if not func(sock, data, k, val): return False
# It's okay. Store it.
if type(data.object) is dict: data.object[k] = val
else: setattr(data.object, k, val)
return True
def polc_saver(data):
'''Return the saved data to the saver function, if one exists.'''
if data.saver:
data.saver(data.sock, data.key, data.object)