Added persistent option to EvMenu, making sure to safeguard against unsafe save states, which comes when trying to save un-picklable callables like methods and functions defined inside other functions.

This commit is contained in:
Griatch 2016-04-17 10:23:03 +02:00
parent 83242408b0
commit adc673f620
2 changed files with 129 additions and 48 deletions

View file

@ -512,6 +512,22 @@ class CmdCreate(ObjManipCommand):
caller.msg(string) caller.msg(string)
def _desc_load(caller):
return caller.db.evmenu_target.db.desc or ""
def _desc_save(caller, buf):
"""
Save line buffer to the desc prop. This should
return True if successful and also report its status to the user.
"""
caller.db.evmenu_target.db.desc = buf
caller.msg("Saved.")
return True
def _desc_quit(caller):
caller.attributes.remove("evmenu_target")
caller.msg("Exited editor.")
class CmdDesc(MuxCommand): class CmdDesc(MuxCommand):
""" """
describe an object describe an object
@ -543,19 +559,24 @@ class CmdDesc(MuxCommand):
return return
def load(caller): def load(caller):
return obj.db.desc or "" return caller.db.evmenu_target.db.desc or ""
def save(caller, buf): def save(caller, buf):
""" """
Save line buffer to the desc prop. This should Save line buffer to the desc prop. This should
return True if successful and also report its status to the user. return True if successful and also report its status to the user.
""" """
obj.db.desc = buf caller.db.evmenu_target.db.desc = buf
caller.msg("Saved.") caller.msg("Saved.")
return True return True
def quit(caller):
caller.attributes.remove("evmenu_target")
caller.msg("Exited editor.")
self.caller.db.evmenu_target = obj
# launch the editor # launch the editor
EvEditor(self.caller, loadfunc=load, savefunc=save, key="desc") EvEditor(self.caller, loadfunc=_desc_load, savefunc=_desc_save, quitfunc=_desc_quit, key="desc", persistent=True)
return return
def func(self): def func(self):

View file

@ -36,9 +36,10 @@ and initialize it:
from builtins import object from builtins import object
import re import re
import inspect
from django.conf import settings from django.conf import settings
from evennia import Command, CmdSet from evennia import Command, CmdSet
from evennia.utils import is_iter, fill, dedent from evennia.utils import is_iter, fill, dedent, logger
from evennia.commands import cmdhandler from evennia.commands import cmdhandler
# we use cmdhandler instead of evennia.syscmdkeys to # we use cmdhandler instead of evennia.syscmdkeys to
@ -59,9 +60,9 @@ _DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
_HELP_TEXT = \ _HELP_TEXT = \
""" """
<txt> - any non-command is appended to the end of the buffer. <txt> - any non-command is appended to the end of the buffer.
: <l> - view buffer or only line <l> : <l> - view buffer or only line(s) <l>
:: <l> - view buffer without line numbers or other parsing :: <l> - raw-view buffer or only line(s) <l>
::: - print a ':' as the only character on the line... ::: - escape - enter ':' as the only character on the line.
:h - this help. :h - this help.
:w - save the buffer (don't quit) :w - save the buffer (don't quit)
@ -73,13 +74,13 @@ _HELP_TEXT = \
:uu - (redo) step forward in undo history :uu - (redo) step forward in undo history
:UU - reset all changes back to initial state :UU - reset all changes back to initial state
:dd <l> - delete line <n> :dd <l> - delete last line or line(s) <l>
:dw <l> <w> - delete word or regex <w> in entire buffer or on line <l> :dw <l> <w> - delete word or regex <w> in entire buffer or on line <l>
:DD - clear buffer :DD - clear entire buffer
:y <l> - yank (copy) line <l> to the copy buffer :y <l> - yank (copy) line(s) <l> to the copy buffer
:x <l> - cut line <l> and store it in the copy buffer :x <l> - cut line(s) <l> and store it in the copy buffer
:p <l> - put (paste) previously copied line directly after <l> :p <l> - put (paste) previously copied line(s) directly after <l>
:i <l> <txt> - insert new text <txt> at line <l>. Old line will move down :i <l> <txt> - insert new text <txt> at line <l>. Old line will move down
:r <l> <txt> - replace line <l> with text <txt> :r <l> <txt> - replace line <l> with text <txt>
:I <l> <txt> - insert text at the beginning of line <l> :I <l> <txt> - insert text at the beginning of line <l>
@ -94,26 +95,26 @@ _HELP_TEXT = \
:echo - turn echoing of the input on/off (helpful for some clients) :echo - turn echoing of the input on/off (helpful for some clients)
Legend: Legend:
<l> - line numbers, or range lstart:lend, e.g. '3:7'. <l> - line number, like '5' or range, like '3:7'.
<w> - one word or several enclosed in quotes. <w> - a single word, or multiple words with quotes around them.
<txt> - longer string, usually not needed to be enclosed in quotes. <txt> - longer string, usually not needing quotes.
""" """
_ERROR_LOADFUNC = \ _ERROR_LOADFUNC = \
""" """
{error} {error}
{rBuffer load function error. Could not load initial data.{n |rBuffer load function error. Could not load initial data.|n
""" """
_ERROR_SAVEFUNC = \ _ERROR_SAVEFUNC = \
""" """
{error} {error}
{rSave function returned an error. Buffer not saved.{n |rSave function returned an error. Buffer not saved.|n
""" """
_ERROR_NO_SAVEFUNC = "{rNo save function defined. Buffer cannot be saved.{n" _ERROR_NO_SAVEFUNC = "|rNo save function defined. Buffer cannot be saved.|n"
_MSG_SAVE_NO_CHANGE = "No changes need saving" _MSG_SAVE_NO_CHANGE = "No changes need saving"
_DEFAULT_NO_QUITFUNC = "Exited editor." _DEFAULT_NO_QUITFUNC = "Exited editor."
@ -122,11 +123,21 @@ _ERROR_QUITFUNC = \
""" """
{error} {error}
{rQuit function gave an error. Skipping.{n |rQuit function gave an error. Skipping.|n
""" """
_MSG_NO_UNDO = "Nothing to undo" _ERROR_PERSISTENT_SAVING = \
_MSG_NO_REDO = "Nothing to redo" """
{error}
|rThe editor state could not be saved for persistent mode. Switching
to non-persistent mode (which means the editor session won't survive
an eventual server reload - so save often!)|n
"""
_MSG_NO_UNDO = "Nothing to undo."
_MSG_NO_REDO = "Nothing to redo."
_MSG_UNDO = "Undid one step." _MSG_UNDO = "Undid one step."
_MSG_REDO = "Redid one step." _MSG_REDO = "Redid one step."
@ -155,11 +166,11 @@ class CmdSaveYesNo(Command):
self.caller.cmdset.remove(SaveYesNoCmdSet) self.caller.cmdset.remove(SaveYesNoCmdSet)
if self.raw_string.strip().lower() in ("no", "n"): if self.raw_string.strip().lower() in ("no", "n"):
# answered no # answered no
self.caller.msg(self.caller.ndb._lineeditor.quit()) self.caller.msg(self.caller.ndb._eveditor.quit())
else: else:
# answered yes (default) # answered yes (default)
self.caller.ndb._lineeditor.save_buffer() self.caller.ndb._eveditor.save_buffer()
self.caller.ndb._lineeditor.quit() self.caller.ndb._eveditor.quit()
class SaveYesNoCmdSet(CmdSet): class SaveYesNoCmdSet(CmdSet):
@ -204,8 +215,15 @@ class CmdEditorBase(Command):
""" """
linebuffer = [] linebuffer = []
if self.editor: editor = self.caller.ndb._eveditor
if not editor:
# this will completely replace the editor
_load_editor(self.caller)
editor = self.caller.ndb._eveditor
self.editor = editor
linebuffer = self.editor.get_buffer().split("\n") linebuffer = self.editor.get_buffer().split("\n")
nlines = len(linebuffer) nlines = len(linebuffer)
# The regular expression will split the line by whitespaces, # The regular expression will split the line by whitespaces,
@ -285,6 +303,27 @@ class CmdEditorBase(Command):
self.arg2 = arg2 self.arg2 = arg2
def _load_editor(caller):
"""
Load persistent editor from storage.
"""
saved_options = caller.attributes.get("_eveditor_saved")
saved_buffer, saved_undo = caller.attributes.get("_eveditor_buffer_temp", (None, None))
if saved_options:
eveditor = EvEditor(caller, **saved_options[0])
if saved_buffer:
# we have to re-save the buffer data so we can handle subsequent restarts
caller.attributes.add("_eveditor_buffer_temp", (saved_buffer, saved_undo))
setattr(eveditor, "_buffer", saved_buffer)
setattr(eveditor, "_undo_buffer", saved_undo)
setattr(eveditor, "_undo_pos", len(saved_undo) - 1)
for key, value in saved_options[1].iteritems():
setattr(eveditor, key, value)
else:
# something went wrong. Cleanup.
caller.cmdset.remove(EvEditorCmdSet)
class CmdLineInput(CmdEditorBase): class CmdLineInput(CmdEditorBase):
""" """
No command match - Inputs line of text into buffer. No command match - Inputs line of text into buffer.
@ -296,7 +335,9 @@ class CmdLineInput(CmdEditorBase):
""" """
Adds the line without any formatting changes. Adds the line without any formatting changes.
""" """
editor = self.editor caller = self.caller
editor = caller.ndb._eveditor
buf = editor.get_buffer() buf = editor.get_buffer()
# add a line of text to buffer # add a line of text to buffer
@ -328,7 +369,8 @@ class CmdEditorGroup(CmdEditorBase):
efficient presentation. efficient presentation.
""" """
caller = self.caller caller = self.caller
editor = self.editor editor = caller.ndb._eveditor
linebuffer = self.linebuffer linebuffer = self.linebuffer
lstart, lend = self.lstart, self.lend lstart, lend = self.lstart, self.lend
cmd = self.cmdstring cmd = self.cmdstring
@ -532,6 +574,9 @@ class EvEditorCmdSet(CmdSet):
"CmdSet for the editor commands" "CmdSet for the editor commands"
key = "editorcmdset" key = "editorcmdset"
mergetype = "Replace" mergetype = "Replace"
def at_cmdset_creation(self):
self.add(CmdLineInput())
self.add(CmdEditorGroup())
#------------------------------------------------------------ #------------------------------------------------------------
# #
@ -548,7 +593,7 @@ class EvEditor(object):
""" """
def __init__(self, caller, loadfunc=None, savefunc=None, def __init__(self, caller, loadfunc=None, savefunc=None,
quitfunc=None, key=""): quitfunc=None, key="", persistent=False):
""" """
Args: Args:
caller (Object): Who is using the editor. caller (Object): Who is using the editor.
@ -568,13 +613,17 @@ class EvEditor(object):
supply to `quitfunc`. supply to `quitfunc`.
key (str, optional): An optional key for naming this key (str, optional): An optional key for naming this
session and make it unique from other editing sessions. session and make it unique from other editing sessions.
persistent (bool, optional): Make the editor survive a reboot. Note
that if this is set, all callables must be functions (not methods)
since they have to able to pickle.
""" """
self._key = key self._key = key
self._caller = caller self._caller = caller
self._caller.ndb._lineeditor = self self._caller.ndb._eveditor = self
self._buffer = "" self._buffer = ""
self._unsaved = False self._unsaved = False
self._persistent = persistent
if loadfunc: if loadfunc:
self._loadfunc = loadfunc self._loadfunc = loadfunc
@ -590,19 +639,6 @@ class EvEditor(object):
else: else:
self._quitfunc = lambda caller: caller.msg(_DEFAULT_NO_QUITFUNC) self._quitfunc = lambda caller: caller.msg(_DEFAULT_NO_QUITFUNC)
# Create the commands we need
cmd1 = CmdLineInput()
cmd1.editor = self
cmd1.obj = self
cmd2 = CmdEditorGroup()
cmd2.obj = self
cmd2.editor = self
# Populate cmdset and add it to caller
editor_cmdset = EvEditorCmdSet()
editor_cmdset.add(cmd1)
editor_cmdset.add(cmd2)
self._caller.cmdset.add(editor_cmdset)
# store the original version # store the original version
self._pristine_buffer = self._buffer self._pristine_buffer = self._buffer
self._sep = "-" self._sep = "-"
@ -615,6 +651,25 @@ class EvEditor(object):
# copy buffer # copy buffer
self._copy_buffer = [] self._copy_buffer = []
if persistent:
# save in tuple {kwargs, other options}
try:
caller.attributes.add("_eveditor_saved",(
{"loadfunc":loadfunc, "savefunc": savefunc,
"quitfunc": quitfunc, "key": key, "persistent": persistent},
{"_pristine_buffer": self._pristine_buffer,
"_sep": self._sep}))
caller.attributes.add("_eveditor_buffer_temp", (self._buffer, self._undo_buffer))
except Exception, err:
caller.msg(_ERROR_PERSISTENT_SAVING.format(error=err))
logger.log_trace("EvEditor persistent-mode error. Commonly, this is because one or "\
"more of the EvEditor callbacks could not be pickled, for example because it's "\
"a class method or is defined inside another function.")
persistent = False
# Create the commands we need
caller.cmdset.add(EvEditorCmdSet, permanent=persistent)
# echo inserted text back to caller # echo inserted text back to caller
self._echo_mode = True self._echo_mode = True
@ -628,6 +683,8 @@ class EvEditor(object):
try: try:
self._buffer = self._loadfunc(self._caller) self._buffer = self._loadfunc(self._caller)
except Exception as e: except Exception as e:
from evennia.utils import logger
logger.log_trace()
self._caller.msg(_ERROR_LOADFUNC.format(error=e)) self._caller.msg(_ERROR_LOADFUNC.format(error=e))
def get_buffer(self): def get_buffer(self):
@ -654,6 +711,8 @@ class EvEditor(object):
self._buffer = buf self._buffer = buf
self.update_undo() self.update_undo()
self._unsaved = True self._unsaved = True
if self._persistent:
self._caller.attributes.add("_eveditor_buffer_temp", (self._buffer, self._undo_buffer))
def quit(self): def quit(self):
""" """
@ -664,13 +723,14 @@ class EvEditor(object):
self._quitfunc(self._caller) self._quitfunc(self._caller)
except Exception as e: except Exception as e:
self._caller.msg(_ERROR_QUITFUNC.format(error=e)) self._caller.msg(_ERROR_QUITFUNC.format(error=e))
del self._caller.ndb._lineeditor self._caller.nattributes.remove("_eveditor")
self._caller.attributes.remove("_eveditor_buffer_temp")
self._caller.attributes.remove("_eveditor_saved")
self._caller.cmdset.remove(EvEditorCmdSet) self._caller.cmdset.remove(EvEditorCmdSet)
def save_buffer(self): def save_buffer(self):
""" """
Saves the content of the buffer. The 'quitting' argument is a bool Saves the content of the buffer.
indicating whether or not the editor intends to exit after saving.
""" """
if self._unsaved: if self._unsaved:
@ -742,8 +802,8 @@ class EvEditor(object):
nchars = len(buf) nchars = len(buf)
sep = self._sep sep = self._sep
header = "{n" + sep * 10 + "Line Editor [%s]" % self._key + sep * (_DEFAULT_WIDTH-25-len(self._key)) header = "|n" + sep * 10 + "Line Editor [%s]" % self._key + sep * (_DEFAULT_WIDTH-25-len(self._key))
footer = "{n" + sep * 10 + "[l:%02i w:%03i c:%04i]" % (nlines, nwords, nchars) \ footer = "|n" + sep * 10 + "[l:%02i w:%03i c:%04i]" % (nlines, nwords, nchars) \
+ sep * 12 + "(:h for help)" + sep * 23 + sep * 12 + "(:h for help)" + sep * 23
if linenums: if linenums:
main = "\n".join("{b%02i|{n %s" % (iline + 1 + offset, line) for iline, line in enumerate(lines)) main = "\n".join("{b%02i|{n %s" % (iline + 1 + offset, line) for iline, line in enumerate(lines))