Fixed conflicts against master.

This commit is contained in:
Griatch 2015-10-18 12:43:43 +02:00
commit b952a290b5
18 changed files with 307 additions and 223 deletions

View file

@ -95,7 +95,7 @@ _ERROR_NOCMDSETS = "No command sets found! This is a sign of a critical bug." \
"\nsome other means for assistance."
_ERROR_CMDHANDLER = "{traceback}\n"\
"Above traceback is from a Command handler bug." \
"Above traceback is from a Command handler bug. " \
"Please file a bug report with the Evennia project."
_ERROR_RECURSION_LIMIT = "Command recursion limit ({recursion_limit}) " \

View file

@ -1,6 +1,6 @@
#
# This is Evennia's default connection screen. It is imported
# and run from world/connection_screens.py.
# and run from server/conf/connection_screens.py.
#
from django.conf import settings

View file

@ -1093,7 +1093,8 @@ class CmdName(ObjManipCommand):
Usage:
@name obj = name;alias1;alias2
Rename an object to something new.
Rename an object to something new. Use *obj to
rename a player.
"""
@ -1107,12 +1108,30 @@ class CmdName(ObjManipCommand):
caller = self.caller
if not self.args:
string = "Usage: @name <obj> = <newname>[;alias;alias;...]"
caller.msg(string)
caller.msg("Usage: @name <obj> = <newname>[;alias;alias;...]")
return
if self.lhs_objs:
objname = self.lhs_objs[0]['name']
if objname.startswith("*"):
# player mode
obj = caller.player.search(objname.lstrip("*"))
if obj:
if self.rhs_objs[0]['aliases']:
caller.msg("Players can't have aliases.")
return
newname = self.rhs
if not newname:
caller.msg("No name defined!")
return
if not obj.access(caller, "edit"):
caller.mgs("You don't have right to edit this player %s." % obj)
return
obj.username = newname
obj.save()
caller.msg("Player's name changed to '%s'." % newname)
return
# object search, also with *
obj = caller.search(objname)
if not obj:
return
@ -1125,6 +1144,9 @@ class CmdName(ObjManipCommand):
if not newname and not aliases:
caller.msg("No names or aliases defined!")
return
if not obj.access(caller, "edit"):
caller.msg("You don't have the right to edit %s." % obj)
return
# change the name and set aliases:
if newname:
obj.name = newname

View file

@ -519,17 +519,15 @@ class CmdChannelCreate(MuxPlayerCommand):
channame = lhs
aliases = None
if ';' in lhs:
channame, aliases = [part.strip().lower()
for part in lhs.split(';', 1) if part.strip()]
aliases = [alias.strip().lower()
for alias in aliases.split(';') if alias.strip()]
channame, aliases = lhs.split(';', 1)
aliases = [alias.strip().lower() for alias in aliases.split(';')]
channel = ChannelDB.objects.channel_search(channame)
if channel:
self.msg("A channel with that name already exists.")
return
# Create and set the channel up
lockstring = "send:all();listen:all();control:id(%s)" % caller.id
new_chan = create.create_channel(channame,
new_chan = create.create_channel(channame.strip(),
aliases,
description,
locks=lockstring)

View file

@ -74,6 +74,7 @@ class CmdHelp(Command):
"""
key = "help"
locks = "cmd:all()"
arg_regex = r"\s|$"
# this is a special cmdhandler flag that makes the cmdhandler also pack
# the current cmdset with the call to self.func().

View file

@ -194,23 +194,23 @@ class CmdPy(MuxCommand):
t0 = timemeasure()
ret = eval(pycode_compiled, {}, available_vars)
t1 = timemeasure()
duration = " (%.4f ms)" % ((t1 - t0) * 1000)
duration = " (runtime ~ %.4f ms)" % ((t1 - t0) * 1000)
else:
ret = eval(pycode_compiled, {}, available_vars)
if mode == "eval":
ret = "{n<<< %s%s" % (str(ret), duration)
ret = "<<< %s%s" % (str(ret), duration)
else:
ret = "{n<<< Done.%s" % duration
ret = "<<< Done (use self.msg() if you want to catch output)%s" % duration
except Exception:
errlist = traceback.format_exc().split('\n')
if len(errlist) > 4:
errlist = errlist[4:]
ret = "\n".join("{n<<< %s" % line for line in errlist if line)
ret = "\n".join("<<< %s" % line for line in errlist if line)
try:
self.msg(ret, sessid=self.sessid)
self.msg(ret, sessid=self.sessid, raw=True)
except TypeError:
self.msg(ret)
self.msg(ret, raw=True)
# helper function. Kept outside so it can be imported and run

View file

@ -15,14 +15,14 @@ Install is simple:
To your settings file, add/edit the line:
CMDSET_UNLOGGEDIN = "contrib.menu_login.UnloggedInCmdSet"
CMDSET_UNLOGGEDIN = "contrib.menu_login.UnloggedinCmdSet"
That's it. Reload the server and try to log in to see it.
You will want to change the login "graphic", which defaults to give
information about commands which are not used in this version of the
login. You can change the screen used by editing
`mygame/server/conf/connection_screens.py`.
`$GAME_DIR/server/conf/connection_screens.py`.
"""
@ -323,13 +323,13 @@ node3 = MenuNode("node3", text=LOGIN_SCREEN_HELP,
# access commands
class UnloggedInCmdSet(CmdSet):
class UnloggedinCmdSet(CmdSet):
"Cmdset for the unloggedin state"
key = "DefaultUnloggedin"
priority = 0
def at_cmdset_creation(self):
"Called when cmdset is first created"
"Called when cmdset is first created."
self.add(CmdUnloggedinLook())
@ -337,7 +337,7 @@ class CmdUnloggedinLook(Command):
"""
An unloggedin version of the look command. This is called by the server
when the player first connects. It sets up the menu before handing off
to the menu's own look command..
to the menu's own look command.
"""
key = CMD_LOGINSTART
# obs, this should NOT have aliases for look or l, this will clash with the menu version!

View file

@ -281,7 +281,7 @@ class MenuNode(object):
text (str, optional): The text that will be displayed at
top when viewing this node.
Kwargs:
links (list): A liist of keys for unique menunodes this is connected to.
links (list): A list of keys for unique menunodes this is connected to.
The actual keys will not printed - keywords will be
used (or a number)
linktexts (list)- A list of texts to describe the links. Must

View file

@ -62,6 +62,8 @@ DATABASES = {{
######################################################################
# Django web features
# (don't remove these entries, they are needed to override the default
# locations with your actual GAME_DIR locations at run-time)
######################################################################
# Absolute path to the directory that holds file uploads from web apps.

View file

@ -370,7 +370,7 @@ class DefaultObject(ObjectDB):
if quiet:
return results
return _AT_SEARCH_RESULT(self, searchdata, results, global_search=True)
return _AT_SEARCH_RESULT(results, self, query=searchdata)
def execute_cmd(self, raw_string, sessid=None, **kwargs):
"""

View file

@ -250,7 +250,7 @@ class MsgPortal2Server(amp.Command):
"""
key = "MsgPortal2Server"
arguments = [('data', Compressed())]
arguments = [('packed_data', Compressed())]
errors = [(Exception, 'EXCEPTION')]
response = []
@ -261,7 +261,7 @@ class MsgServer2Portal(amp.Command):
"""
key = "MsgServer2Portal"
arguments = [('data', Compressed())]
arguments = [('packed_data', Compressed())]
errors = [(Exception, 'EXCEPTION')]
response = []
@ -275,7 +275,7 @@ class AdminPortal2Server(amp.Command):
"""
key = "AdminPortal2Server"
arguments = [('data', Compressed())]
arguments = [('packed_data', Compressed())]
errors = [(Exception, 'EXCEPTION')]
response = []
@ -289,7 +289,7 @@ class AdminServer2Portal(amp.Command):
"""
key = "AdminServer2Portal"
arguments = [('data', Compressed())]
arguments = [('packed_data', Compressed())]
errors = [(Exception, 'EXCEPTION')]
response = []
@ -362,7 +362,7 @@ class AMPProtocol(amp.AMP):
sessdata = self.factory.portal.sessions.get_all_sync_data()
self.send_AdminPortal2Server(0,
PSYNC,
data=sessdata)
sessiondata=sessdata)
self.factory.portal.sessions.at_server_connection()
if hasattr(self.factory, "server_restart_mode"):
del self.factory.server_restart_mode
@ -399,53 +399,49 @@ class AMPProtocol(amp.AMP):
(sessid, kwargs).
"""
batch = dumps((sessid, kwargs))
return self.callRemote(command,
data=batch).addErrback(self.errback, command.key)
packed_data=dumps((sessid, kwargs))
).addErrback(self.errback, command.key)
# Message definition + helper methods to call/create each message type
# Portal -> Server Msg
@MsgPortal2Server.responder
def server_receive_msgportal2server(self, data):
def server_receive_msgportal2server(self, packed_data):
"""
Receives message arriving to server. This method is executed
on the Server.
Args:
data (str): Data to receive (a pickled tuple (sessid,kwargs))
packed_data (str): Data to receive (a pickled tuple (sessid,kwargs))
"""
sessid, kwargs = loads(data)
sessid, kwargs = loads(packed_data)
#print "msg portal -> server (server side):", sessid, msg, loads(ret["data"])
self.factory.server.sessions.data_in(sessid,
text=kwargs["msg"],
data=kwargs["data"])
self.factory.server.sessions.data_in(sessid, **kwargs)
return {}
def send_MsgPortal2Server(self, sessid, msg="", data=""):
def send_MsgPortal2Server(self, sessid, text="", **kwargs):
"""
Access method called by the Portal and executed on the Portal.
Args:
sessid (int): Unique Session id.
msg (str): Message to send over the wire.
data (str, optional): Optional data.
kwargs (any, optional): Optional data.
Returns:
deferred (Deferred): Asynchronous return.
"""
#print "msg portal->server (portal side):", sessid, msg, data
return self.send_data(MsgPortal2Server, sessid,
msg=msg,
data=data)
return self.send_data(MsgPortal2Server, sessid, text=text, **kwargs)
# Server -> Portal message
@MsgServer2Portal.responder
def portal_receive_server2portal(self, data):
def portal_receive_server2portal(self, packed_data):
"""
Receives message arriving to Portal from Server.
This method is executed on the Portal.
@ -456,17 +452,15 @@ class AMPProtocol(amp.AMP):
before continuing.
Args:
data (str): Pickled data (sessid, kwargs) coming over the wire.
packed_data (str): Pickled data (sessid, kwargs) coming over the wire.
"""
sessid, kwargs = loads(data)
sessid, kwargs = loads(packed_data)
#print "msg server->portal (portal side):", sessid, ret["text"], loads(ret["data"])
self.factory.portal.sessions.data_out(sessid,
text=kwargs["msg"],
data=kwargs["data"])
self.factory.portal.sessions.data_out(sessid, **kwargs)
return {}
def send_MsgServer2Portal(self, sessid, msg="", data=""):
def send_MsgServer2Portal(self, sessid, text="", **kwargs):
"""
Access method - executed on the Server for sending data
to Portal.
@ -474,39 +468,37 @@ class AMPProtocol(amp.AMP):
Args:
sessid (int): Unique Session id.
msg (str, optional): Message to send over the wire.
data (str, optional): Extra data.
kwargs (any, optiona): Extra data.
"""
#print "msg server->portal (server side):", sessid, msg, data
return self.send_data(MsgServer2Portal, sessid, msg=msg, data=data)
return self.send_data(MsgServer2Portal, sessid, text=text, **kwargs)
# Server administration from the Portal side
@AdminPortal2Server.responder
def server_receive_adminportal2server(self, data):
def server_receive_adminportal2server(self, packed_data):
"""
Receives admin data from the Portal (allows the portal to
perform admin operations on the server). This is executed on
the Server.
Args:
data (str): Data to send (often a part of a batch)
packed_data (str): Incoming, pickled data.
"""
#print "serveradmin (server side):", hashid, ipart, nparts
sessid, kwargs = loads(data)
operation = kwargs["operation"]
data = kwargs["data"]
sessid, kwargs = loads(packed_data)
operation = kwargs.pop("operation", "")
server_sessionhandler = self.factory.server.sessions
#print "serveradmin (server side):", sessid, ord(operation), data
if operation == PCONN: # portal_session_connect
# create a new session and sync it
server_sessionhandler.portal_connect(data)
server_sessionhandler.portal_connect(kwargs.get("sessiondata"))
elif operation == PCONNSYNC: #portal_session_sync
server_sessionhandler.portal_session_sync(data)
server_sessionhandler.portal_session_sync(kwargs.get("sessiondata"))
elif operation == PDISCONN: # portal_session_disconnect
# session closed from portal side
@ -518,12 +510,12 @@ class AMPProtocol(amp.AMP):
# contains a dict {sessid: {arg1:val1,...}}
# representing the attributes to sync for each
# session.
server_sessionhandler.portal_sessions_sync(data)
server_sessionhandler.portal_sessions_sync(kwargs.get("sessiondata"))
else:
raise Exception("operation %(op)s not recognized." % {'op': operation})
return {}
def send_AdminPortal2Server(self, sessid, operation="", data=""):
def send_AdminPortal2Server(self, sessid, operation="", **kwargs):
"""
Send Admin instructions from the Portal to the Server.
Executed
@ -533,42 +525,41 @@ class AMPProtocol(amp.AMP):
sessid (int): Session id.
operation (char, optional): Identifier for the server operation, as defined by the
global variables in `evennia/server/amp.py`.
data (str, optional): Data going into the adminstrative operation.
data (str or dict, optional): Data used in the administrative operation.
"""
#print "serveradmin (portal side):", sessid, ord(operation), data
return self.send_data(AdminPortal2Server, sessid, operation=operation, data=data)
return self.send_data(AdminPortal2Server, sessid, operation=operation, **kwargs)
# Portal administraton from the Server side
@AdminServer2Portal.responder
def portal_receive_adminserver2portal(self, data):
def portal_receive_adminserver2portal(self, packed_data):
"""
Receives and handles admin operations sent to the Portal
This is executed on the Portal.
Args:
data (str): Data received, a pickled tuple (sessid, kwargs).
packed_data (str): Data received, a pickled tuple (sessid, kwargs).
"""
#print "portaladmin (portal side):", sessid, ord(operation), data
sessid, kwargs = loads(data)
operation = kwargs["operation"]
data = kwargs["data"]
sessid, kwargs = loads(packed_data)
operation = kwargs.pop("operation")
portal_sessionhandler = self.factory.portal.sessions
if operation == SLOGIN: # server_session_login
# a session has authenticated; sync it.
portal_sessionhandler.server_logged_in(sessid, data)
portal_sessionhandler.server_logged_in(sessid, kwargs.get("sessiondata"))
elif operation == SDISCONN: # server_session_disconnect
# the server is ordering to disconnect the session
portal_sessionhandler.server_disconnect(sessid, reason=data)
portal_sessionhandler.server_disconnect(sessid, reason=kwargs.get("reason"))
elif operation == SDISCONNALL: # server_session_disconnect_all
# server orders all sessions to disconnect
portal_sessionhandler.server_disconnect_all(reason=data)
portal_sessionhandler.server_disconnect_all(reason=kwargs.get("reason"))
elif operation == SSHUTD: # server_shutdown
# the server orders the portal to shut down
@ -577,18 +568,18 @@ class AMPProtocol(amp.AMP):
elif operation == SSYNC: # server_session_sync
# server wants to save session data to the portal,
# maybe because it's about to shut down.
portal_sessionhandler.server_session_sync(data)
portal_sessionhandler.server_session_sync(kwargs.get("sessiondata"))
# set a flag in case we are about to shut down soon
self.factory.server_restart_mode = True
elif operation == SCONN: # server_force_connection (for irc/imc2 etc)
portal_sessionhandler.server_connect(**data)
portal_sessionhandler.server_connect(**kwargs)
else:
raise Exception("operation %(op)s not recognized." % {'op': operation})
return {}
def send_AdminServer2Portal(self, sessid, operation="", data=""):
def send_AdminServer2Portal(self, sessid, operation="", **kwargs):
"""
Administrative access method called by the Server to send an
instruction to the Portal.
@ -598,16 +589,15 @@ class AMPProtocol(amp.AMP):
operation (char, optional): Identifier for the server
operation, as defined by the global variables in
`evennia/server/amp.py`.
data (str, optional): Data going into the adminstrative
operation.
data (str or dict, optional): Data going into the adminstrative.
"""
return self.send_data(AdminServer2Portal, sessid, operation=operation, data=data)
return self.send_data(AdminServer2Portal, sessid, operation=operation, **kwargs)
# Extra functions
@FunctionCall.responder
def receive_functioncall(self, module, function, args, **kwargs):
def receive_functioncall(self, module, function, func_args, func_kwargs):
"""
This allows Portal- and Server-process to call an arbitrary
function in the other process. It is intended for use by
@ -618,12 +608,12 @@ class AMPProtocol(amp.AMP):
`function` to call.
function (str): The name of the function to call in
`module`.
args, kwargs (any): These will be used as args/kwargs to
`function`.
func_args (str): Pickled args tuple for use in `function` call.
func_kwargs (str): Pickled kwargs dict for use in `function` call.
"""
args = loads(args)
kwargs = loads(kwargs)
args = loads(func_args)
kwargs = loads(func_kwargs)
# call the function (don't catch tracebacks here)
result = variable_from_module(module, function)(*args, **kwargs)

View file

@ -114,7 +114,7 @@ class PortalSessionHandler(SessionHandler):
#print "connecting", session.sessid, " number:", len(self.sessions)
self.portal.amp_protocol.send_AdminPortal2Server(session.sessid,
operation=PCONN,
data=sessdata)
sessiondata=sessdata)
def sync(self, session):
"""
@ -143,7 +143,7 @@ class PortalSessionHandler(SessionHandler):
"server_data",))
self.portal.amp_protocol.send_AdminPortal2Server(session.sessid,
operation=PCONNSYNC,
data=sessdata)
sessiondata=sessdata)
def disconnect(self, session):
"""
@ -390,7 +390,6 @@ class PortalSessionHandler(SessionHandler):
# data throttle (anti DoS measure)
now = time()
dT = now - self.command_counter_reset
print(" command rate:", _MAX_COMMAND_RATE / dT, dT, self.command_counter)
self.command_counter = 0
self.command_counter_reset = now
self.command_overflow = dT < 1.0
@ -402,8 +401,8 @@ class PortalSessionHandler(SessionHandler):
# relay data to Server
self.command_counter += 1
self.portal.amp_protocol.send_MsgPortal2Server(session.sessid,
msg=text,
data=kwargs)
text=text,
**kwargs)
else:
# called by the callLater callback
if self.command_overflow:

View file

@ -208,12 +208,15 @@ class ServerSession(Session):
idle timers and command counters.
"""
# Idle time used for timeout calcs.
self.cmd_last = time()
# Store the timestamp of the user's last command.
if not idle:
# Increment the user's command counter.
self.cmd_total += 1
# Player-visible idle time, not used in idle timeout calcs.
self.cmd_last_visible = time()
self.cmd_last_visible = self.cmd_last
def data_in(self, text=None, **kwargs):
"""

View file

@ -282,11 +282,8 @@ class ServerSessionHandler(SessionHandler):
the Server.
"""
data = {"protocol_path":protocol_path,
"config":configdict}
self.server.amp_protocol.send_AdminServer2Portal(0,
operation=SCONN,
data=data)
self.server.amp_protocol.send_AdminServer2Portal(0, operation=SCONN,
protocol_path=protocol_path, config=configdict)
def portal_shutdown(self):
"""
@ -294,8 +291,7 @@ class ServerSessionHandler(SessionHandler):
"""
self.server.amp_protocol.send_AdminServer2Portal(0,
operation=SSHUTD,
data="")
operation=SSHUTD)
def login(self, session, player, testmode=False):
"""
@ -338,14 +334,12 @@ class ServerSessionHandler(SessionHandler):
string = "Logged in: {player} {address} ({nsessions} session(s) total)"
string = string.format(player=player,address=session.address, nsessions=nsess)
session.log(string)
session.logged_in = True
# sync the portal to the session
sessdata = {"logged_in": True}
if not testmode:
self.server.amp_protocol.send_AdminServer2Portal(session.sessid,
operation=SLOGIN,
data=sessdata)
sessiondata={"logged_in": True})
player.at_post_login(sessid=session.sessid)
def disconnect(self, session, reason=""):
@ -375,7 +369,7 @@ class ServerSessionHandler(SessionHandler):
# inform portal that session should be closed.
self.server.amp_protocol.send_AdminServer2Portal(sessid,
operation=SDISCONN,
data=reason)
reason=reason)
def all_sessions_portal_sync(self):
"""
@ -386,7 +380,7 @@ class ServerSessionHandler(SessionHandler):
sessdata = self.get_all_sync_data()
return self.server.amp_protocol.send_AdminServer2Portal(0,
operation=SSYNC,
data=sessdata)
sessiondata=sessdata)
def disconnect_all_sessions(self, reason="You have been disconnected."):
"""
@ -402,7 +396,7 @@ class ServerSessionHandler(SessionHandler):
# tell portal to disconnect all sessions
self.server.amp_protocol.send_AdminServer2Portal(0,
operation=SDISCONNALL,
data=reason)
reason=reason)
def disconnect_duplicate_sessions(self, curr_session,
reason=_("Logged in from elsewhere. Disconnecting.")):
@ -586,8 +580,8 @@ class ServerSessionHandler(SessionHandler):
# send to all found sessions
for session in sessions:
self.server.amp_protocol.send_MsgServer2Portal(sessid=session.sessid,
msg=text,
data=kwargs)
text=text,
**kwargs)
def data_in(self, sessid, text="", **kwargs):
"""

View file

@ -45,23 +45,29 @@ entered to get to this node). The node function code will only be
executed once per node-visit and the system will accept nodes with
both one or two arguments interchangeably.
The menu tree itself is available on the caller as
`caller.ndb._menutree`. This makes it a convenient place to store
temporary state variables between nodes, since this NAttribute is
deleted when the menu is exited.
The return values must be given in the above order, but each can be
returned as None as well. If the options are returned as None, the
menu is immediately exited and the default "look" command is called.
text (str, tuple or None): Text shown at this node. If a tuple, the second
element in the tuple is a help text to display at this node when
the user enters the menu help command there.
options (tuple, dict or None): ( {'key': name, # can also be a list of aliases. A special key is "_default", which
# marks this option as the default fallback when no other
# option matches the user input.
'desc': description, # option description
'goto': nodekey, # node to go to when chosen
'exec': nodekey, # node or callback to trigger as callback when chosen. If a node
# key is given the node will be executed once but its return u
# values are ignored. If a callable is given, it must accept
# one or two args, like any node.
{...}, ...)
text (str, tuple or None): Text shown at this node. If a tuple, the
second element in the tuple is a help text to display at this
node when the user enters the menu help command there.
options (tuple, dict or None): (
{'key': name, # can also be a list of aliases. A special key is
# "_default", which marks this option as the default
# fallback when no other option matches the user input.
'desc': description, # optional description
'goto': nodekey, # node to go to when chosen
'exec': nodekey}, # node or callback to trigger as callback when chosen.
# If a node key is given, the node will be executed once
# but its return values are ignored. If a callable is
# given, it must accept one or two args, like any node.
{...}, ...)
If key is not given, the option will automatically be identified by
its number 1..N.
@ -122,9 +128,9 @@ The menu tree is exited either by using the in-menu quit command or by
reaching a node without any options.
For a menu demo, import CmdTestDemo from this module and add it to
your default cmdset. Run it with this module, like `testdemo
evennia.utils.evdemo`.
For a menu demo, import CmdTestMenu from this module and add it to
your default cmdset. Run it with this module, like `testmenu
evennia.utils.evmenu`.
"""
from __future__ import print_function
@ -176,65 +182,25 @@ class EvMenuError(RuntimeError):
class CmdEvMenuNode(Command):
"""
Menu options.
"""
key = "look"
aliases = ["l", _CMD_NOMATCH, _CMD_NOINPUT]
key = _CMD_NOINPUT
aliases = [_CMD_NOMATCH]
locks = "cmd:all()"
help_category = "Menu"
def func(self):
"""
Implement all menu commands.
"""
caller = self.caller
menu = caller.ndb._menutree
if not menu:
err = "Menu object not found as %s.ndb._menutree!" % (caller)
self.caller.msg(err)
caller.msg(err)
raise EvMenuError(err)
# flags and data
raw_string = self.raw_string
cmd = raw_string.strip().lower()
options = menu.options
allow_quit = menu.allow_quit
cmd_on_quit = menu.cmd_on_quit
default = menu.default
print("cmd, options:", cmd, options)
if cmd in options:
# this will overload the other commands
# if it has the same name!
goto, callback = options[cmd]
if callback:
menu.callback(callback, raw_string)
if goto:
menu.goto(goto, raw_string)
elif cmd in ("look", "l"):
caller.msg(menu.nodetext)
elif cmd in ("help", "h"):
caller.msg(menu.helptext)
elif allow_quit and cmd in ("quit", "q", "exit"):
menu.close_menu()
if cmd_on_quit is not None:
caller.execute_cmd(cmd_on_quit)
elif default:
goto, callback = default
if callback:
menu.callback(callback, raw_string)
if goto:
menu.goto(goto, raw_string)
else:
caller.msg(_HELP_NO_OPTION_MATCH)
if not (options or default):
# no options - we are at the end of the menu.
menu.close_menu()
if cmd_on_quit is not None:
caller.execute_cmd(cmd_on_quit)
menu.parse_input(self.raw_string)
class EvMenuCmdSet(CmdSet):
@ -269,7 +235,9 @@ class EvMenu(object):
"""
def __init__(self, caller, menudata, startnode="start",
cmdset_mergetype="Replace", cmdset_priority=1,
allow_quit=True, cmd_on_quit="look"):
allow_quit=True, cmd_on_quit="look",
nodetext_formatter=None, options_formatter=None,
node_formatter=None):
"""
Initialize the menu tree and start the caller onto the first node.
@ -299,11 +267,35 @@ class EvMenu(object):
allow_quit (bool, optional): Allow user to use quit or
exit to leave the menu at any point. Recommended during
development!
cmd_on_quit (str or None, optional): When exiting the menu
cmd_on_quit (callable, str or None, optional): When exiting the menu
(either by reaching a node with no options or by using the
in-built quit command (activated with `allow_quit`), this
command string will be executed. Set to None to not call
any command.
callback function or command string will be executed.
The callback function takes two parameters, the caller then the
EvMenu object. This is called after cleanup is complete.
Set to None to not call any command.
nodetext_formatter (callable, optional): This callable should be on
the form `function(nodetext, has_options)`, where `nodetext` is the
node text string and `has_options` a boolean specifying if there
are options associated with this node. It must return a formatted
string.
options_formatter (callable, optional): This callable should be on
the form `function(optionlist)`, where ` optionlist is a list
of option dictionaries, like
[{"key":..., "desc",..., "goto": ..., "exec",...}, ...]
Each dictionary describes each possible option. Note that this
will also be called if there are no options, and so should be
able to handle an empty list. This should
be formatted into an options list and returned as a string,
including the required separator to use between the node text
and the options. If not given the default EvMenu style will be used.
node_formatter (callable, optional): This callable should be on the
form `func(nodetext, optionstext)` where the arguments are strings
representing the node text and options respectively (possibly prepared
by `nodetext_formatter`/`options_formatter` or by the default styles).
It should return a string representing the final look of the node. This
can e.g. be used to create line separators that take into account the
dynamic width of the parts.
Raises:
EvMenuError: If the start/end node is not found in menu tree.
@ -313,17 +305,27 @@ class EvMenu(object):
self._startnode = startnode
self._menutree = self._parse_menudata(menudata)
self._nodetext_formatter = nodetext_formatter
self._options_formatter = nodetext_formatter
self._node_formatter = node_formatter
if startnode not in self._menutree:
raise EvMenuError("Start node '%s' not in menu tree!" % startnode)
# variables made available to the command
self.allow_quit = allow_quit
self.cmd_on_quit = cmd_on_quit
if isinstance(cmd_on_quit, str):
self.cmd_on_quit = lambda caller, menu: caller.execute_cmd(cmd_on_quit)
elif callable(cmd_on_quit):
self.cmd_on_quit = cmd_on_quit
else:
self.cmd_on_quit = None
self.default = None
self.nodetext = None
self.helptext = None
self.options = None
# store ourself on the object
self._caller.ndb._menutree = self
@ -382,74 +384,85 @@ class EvMenu(object):
# handle the node text
#
nodetext = dedent(nodetext).strip()
if self._nodetext_formatter:
# use custom formatter
nodetext = self._nodetext_formatter(nodetext, len(optionlist))
else:
nodetext = dedent(nodetext).strip()
nodetext_width_max = max(m_len(line) for line in nodetext.split("\n"))
if not optionlist:
# return the node text "naked".
separator1 = "_" * nodetext_width_max + "\n\n" if nodetext_width_max else ""
separator2 = "\n" if nodetext_width_max else "" + "_" * nodetext_width_max
return separator1 + nodetext + separator2
#
# handle the options
#
# column separation distance
colsep = 4
if self._options_formatter:
# use custom formatter
optionstext = self._options_formatter(optionlist)
elif optionlist:
# column separation distance
colsep = 4
nlist = len(optionlist)
nlist = len(optionlist)
# get the widest option line in the table.
table_width_max = -1
table = []
for key, desc in optionlist:
table_width_max = max(table_width_max,
max(m_len(p) for p in key.split("\n")) +
max(m_len(p) for p in desc.split("\n")) + colsep)
raw_key = strip_ansi(key)
if raw_key != key:
# already decorations in key definition
table.append(ANSIString(" {lc%s{lt%s{le: %s" % (raw_key, key, desc)))
else:
# add a default white color to key
table.append(ANSIString(" {lc%s{lt{w%s{n{le: %s" % (raw_key, raw_key, desc)))
# get the widest option line in the table.
table_width_max = -1
table = []
for key, desc in optionlist:
table_width_max = max(table_width_max,
max(m_len(p) for p in key.split("\n")) +
max(m_len(p) for p in desc.split("\n")) + colsep)
raw_key = strip_ansi(key)
if raw_key != key:
# already decorations in key definition
table.append(ANSIString(" {lc%s{lt%s{le: %s" % (raw_key, key, desc)))
else:
# add a default white color to key
table.append(ANSIString(" {lc%s{lt{w%s{n{le: %s" % (raw_key, raw_key, desc)))
ncols = (_MAX_TEXT_WIDTH // table_width_max) + 1 # number of ncols
nlastcol = nlist % ncols # number of elements left in last row
ncols = (_MAX_TEXT_WIDTH // table_width_max) + 1 # number of ncols
nlastcol = nlist % ncols # number of elements left in last row
# get the amount of rows needed (start with 4 rows)
nrows = 4
while nrows * ncols < nlist:
nrows += 1
ncols = nlist // nrows # number of full columns
nlastcol = nlist % nrows # number of elements in last column
# get the amount of rows needed (start with 4 rows)
nrows = 4
while nrows * ncols < nlist:
nrows += 1
ncols = nlist // nrows # number of full columns
nlastcol = nlist % nrows # number of elements in last column
# get the final column count
ncols = ncols + 1 if nlastcol > 0 else ncols
if ncols > 1:
# only extend if longer than one column
table.extend([" " for i in xrange(nrows-nlastcol)])
# get the final column count
ncols = ncols + 1 if nlastcol > 0 else ncols
if ncols > 1:
# only extend if longer than one column
table.extend([" " for i in xrange(nrows-nlastcol)])
# build the actual table grid
table = [table[icol*nrows:(icol*nrows) + nrows] for icol in xrange(0, ncols)]
# build the actual table grid
table = [table[icol*nrows:(icol*nrows) + nrows] for icol in xrange(0, ncols)]
# adjust the width of each column
total_width = 0
for icol in xrange(len(table)):
col_width = max(max(m_len(p) for p in part.split("\n")) for part in table[icol]) + colsep
table[icol] = [pad(part, width=col_width + colsep, align="l") for part in table[icol]]
total_width += col_width
# adjust the width of each column
for icol in xrange(len(table)):
col_width = max(max(m_len(p) for p in part.split("\n")) for part in table[icol]) + colsep
table[icol] = [pad(part, width=col_width + colsep, align="l") for part in table[icol]]
# format the table into columns
table = EvTable(table=table, border="none")
# format the table into columns
optionstext = unicode(EvTable(table=table, border="none"))
else:
optionstext = ""
# build the page
total_width = max(total_width, nodetext_width_max)
separator1 = "_" * total_width + "\n\n" if nodetext_width_max else ""
separator2 = "\n" + "_" * total_width + "\n\n" if total_width else ""
return separator1 + nodetext + separator2 + unicode(table)
options_width_max = max(m_len(line) for line in optionstext.split("\n"))
#
# format the entire node
#
if self._node_formatter:
# use custom formatter
return self._node_formatter(nodetext, optionstext)
else:
# build the page
total_width = max(options_width_max, nodetext_width_max)
separator1 = "_" * total_width + "\n\n" if nodetext_width_max else ""
separator2 = "\n" + "_" * total_width + "\n\n" if total_width else ""
return separator1 + nodetext + separator2 + optionstext
def _execute_node(self, nodename, raw_string):
"""
@ -488,6 +501,56 @@ class EvMenu(object):
return nodetext, options
def _display_nodetext(self):
self._caller.msg(self.nodetext)
def _display_helptext(self):
self._caller.msg(self.helptext)
def _callback_goto(self, callback, goto, raw_string):
if callback:
self.callback(callback, raw_string)
if goto:
self.goto(goto, raw_string)
def parse_input(self, raw_string):
"""
Processes the user' node inputs.
Args:
raw_string (str): The incoming raw_string from the menu
command.
"""
caller = self._caller
cmd = raw_string.strip().lower()
allow_quit = self.allow_quit
if cmd in self.options:
# this will take precedence over the default commands
# below
goto, callback = self.options[cmd]
self._callback_goto(callback, goto, raw_string)
elif cmd in ("look", "l"):
self._display_nodetext()
elif cmd in ("help", "h"):
self._display_helptext()
elif allow_quit and cmd in ("quit", "q", "exit"):
self.close_menu()
elif self.default:
goto, callback = self.default
self._callback_goto(callback, goto, raw_string)
else:
caller.msg(_HELP_NO_OPTION_MATCH)
if not (self.options or self.default):
# no options - we are at the end of the menu.
self.close_menu()
def callback(self, nodename, raw_string):
"""
Run a node as a callback. This makes no use of the return
@ -582,7 +645,7 @@ class EvMenu(object):
else:
self.helptext = _HELP_NO_OPTIONS if self.allow_quit else _HELP_NO_OPTIONS_NO_QUIT
self._caller.execute_cmd("look")
self._display_nodetext()
def close_menu(self):
"""
@ -590,6 +653,8 @@ class EvMenu(object):
"""
self._caller.cmdset.remove(EvMenuCmdSet)
del self._caller.ndb._menutree
if self.cmd_on_quit is not None:
self.cmd_on_quit(self._caller, self)
# -------------------------------------------------------------------------------------------------
@ -689,6 +754,9 @@ def test_start_node(caller):
"desc": "Set an attribute on yourself.",
"exec": lambda caller: caller.attributes.add("menuattrtest", "Test value"),
"goto": "test_set_node"},
{"key": ("{yL{nook", "l"),
"desc": "Look and see a custom message.",
"goto": "test_look_node"},
{"key": ("{yV{niew", "v"),
"desc": "View your own name",
"goto": "test_view_node"},
@ -700,6 +768,13 @@ def test_start_node(caller):
return text, options
def test_look_node(caller):
text = "Looking again will take you back to the previous message."
options = {"key": ("{yL{nook", "l"),
"desc": "Go back to the previous menu.",
"goto": "test_start_node"}
return text, options
def test_set_node(caller):
text = ("""
The attribute 'menuattrtest' was set to

View file

@ -1537,7 +1537,7 @@ def at_search_result(matches, caller, query="", quiet=False, **kwargs):
error = ""
if not matches:
# no results.
error = kwargs.get("nofound_string", _("Could not find '%s'." % query))
error = kwargs.get("nofound_string") or _("Could not find '%s'." % query)
matches = None
elif len(matches) > 1:
error = kwargs.get("multimatch_string", None)

View file

@ -3,5 +3,5 @@
django >= 1.8, < 1.9
twisted >= 15.2.1
mock >= 1.0.1
pillow
pillow == 2.9.0
pytz

View file

@ -6,5 +6,5 @@ pypiwin32
django >= 1.8, < 1.9
twisted >= 15.2.1
mock >= 1.0.1
pillow
pillow == 2.9.0
pytz