################################################################################ # # 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)