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)
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):
"""
describe an object
@ -543,19 +559,24 @@ class CmdDesc(MuxCommand):
return
def load(caller):
return obj.db.desc or ""
return caller.db.evmenu_target.db.desc or ""
def save(caller, buf):
"""
Save line buffer to the desc prop. This should
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.")
return True
def quit(caller):
caller.attributes.remove("evmenu_target")
caller.msg("Exited editor.")
self.caller.db.evmenu_target = obj
# 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
def func(self):

View file

@ -36,9 +36,10 @@ and initialize it:
from builtins import object
import re
import inspect
from django.conf import settings
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
# we use cmdhandler instead of evennia.syscmdkeys to
@ -59,9 +60,9 @@ _DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
_HELP_TEXT = \
"""
<txt> - any non-command is appended to the end of the buffer.
: <l> - view buffer or only line <l>
:: <l> - view buffer without line numbers or other parsing
::: - print a ':' as the only character on the line...
: <l> - view buffer or only line(s) <l>
:: <l> - raw-view buffer or only line(s) <l>
::: - escape - enter ':' as the only character on the line.
:h - this help.
:w - save the buffer (don't quit)
@ -73,13 +74,13 @@ _HELP_TEXT = \
:uu - (redo) step forward in undo history
: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>
:DD - clear buffer
:DD - clear entire buffer
:y <l> - yank (copy) line <l> to the copy buffer
:x <l> - cut line <l> and store it in the copy buffer
:p <l> - put (paste) previously copied line directly after <l>
:y <l> - yank (copy) line(s) <l> to the copy buffer
:x <l> - cut line(s) <l> and store it in the copy buffer
: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
:r <l> <txt> - replace line <l> with text <txt>
: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)
Legend:
<l> - line numbers, or range lstart:lend, e.g. '3:7'.
<w> - one word or several enclosed in quotes.
<txt> - longer string, usually not needed to be enclosed in quotes.
<l> - line number, like '5' or range, like '3:7'.
<w> - a single word, or multiple words with quotes around them.
<txt> - longer string, usually not needing quotes.
"""
_ERROR_LOADFUNC = \
"""
{error}
{rBuffer load function error. Could not load initial data.{n
|rBuffer load function error. Could not load initial data.|n
"""
_ERROR_SAVEFUNC = \
"""
{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"
_DEFAULT_NO_QUITFUNC = "Exited editor."
@ -122,11 +123,21 @@ _ERROR_QUITFUNC = \
"""
{error}
{rQuit function gave an error. Skipping.{n
|rQuit function gave an error. Skipping.|n
"""
_MSG_NO_UNDO = "Nothing to undo"
_MSG_NO_REDO = "Nothing to redo"
_ERROR_PERSISTENT_SAVING = \
"""
{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_REDO = "Redid one step."
@ -155,11 +166,11 @@ class CmdSaveYesNo(Command):
self.caller.cmdset.remove(SaveYesNoCmdSet)
if self.raw_string.strip().lower() in ("no", "n"):
# answered no
self.caller.msg(self.caller.ndb._lineeditor.quit())
self.caller.msg(self.caller.ndb._eveditor.quit())
else:
# answered yes (default)
self.caller.ndb._lineeditor.save_buffer()
self.caller.ndb._lineeditor.quit()
self.caller.ndb._eveditor.save_buffer()
self.caller.ndb._eveditor.quit()
class SaveYesNoCmdSet(CmdSet):
@ -204,8 +215,15 @@ class CmdEditorBase(Command):
"""
linebuffer = []
if self.editor:
linebuffer = self.editor.get_buffer().split("\n")
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")
nlines = len(linebuffer)
# The regular expression will split the line by whitespaces,
@ -285,6 +303,27 @@ class CmdEditorBase(Command):
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):
"""
No command match - Inputs line of text into buffer.
@ -296,7 +335,9 @@ class CmdLineInput(CmdEditorBase):
"""
Adds the line without any formatting changes.
"""
editor = self.editor
caller = self.caller
editor = caller.ndb._eveditor
buf = editor.get_buffer()
# add a line of text to buffer
@ -328,7 +369,8 @@ class CmdEditorGroup(CmdEditorBase):
efficient presentation.
"""
caller = self.caller
editor = self.editor
editor = caller.ndb._eveditor
linebuffer = self.linebuffer
lstart, lend = self.lstart, self.lend
cmd = self.cmdstring
@ -532,6 +574,9 @@ class EvEditorCmdSet(CmdSet):
"CmdSet for the editor commands"
key = "editorcmdset"
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,
quitfunc=None, key=""):
quitfunc=None, key="", persistent=False):
"""
Args:
caller (Object): Who is using the editor.
@ -568,13 +613,17 @@ class EvEditor(object):
supply to `quitfunc`.
key (str, optional): An optional key for naming this
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._caller = caller
self._caller.ndb._lineeditor = self
self._caller.ndb._eveditor = self
self._buffer = ""
self._unsaved = False
self._persistent = persistent
if loadfunc:
self._loadfunc = loadfunc
@ -590,19 +639,6 @@ class EvEditor(object):
else:
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
self._pristine_buffer = self._buffer
self._sep = "-"
@ -615,6 +651,25 @@ class EvEditor(object):
# 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
self._echo_mode = True
@ -628,6 +683,8 @@ class EvEditor(object):
try:
self._buffer = self._loadfunc(self._caller)
except Exception as e:
from evennia.utils import logger
logger.log_trace()
self._caller.msg(_ERROR_LOADFUNC.format(error=e))
def get_buffer(self):
@ -654,6 +711,8 @@ class EvEditor(object):
self._buffer = buf
self.update_undo()
self._unsaved = True
if self._persistent:
self._caller.attributes.add("_eveditor_buffer_temp", (self._buffer, self._undo_buffer))
def quit(self):
"""
@ -664,13 +723,14 @@ class EvEditor(object):
self._quitfunc(self._caller)
except Exception as 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)
def save_buffer(self):
"""
Saves the content of the buffer. The 'quitting' argument is a bool
indicating whether or not the editor intends to exit after saving.
Saves the content of the buffer.
"""
if self._unsaved:
@ -742,8 +802,8 @@ class EvEditor(object):
nchars = len(buf)
sep = self._sep
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) \
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) \
+ sep * 12 + "(:h for help)" + sep * 23
if linenums:
main = "\n".join("{b%02i|{n %s" % (iline + 1 + offset, line) for iline, line in enumerate(lines))