Merge with develop and fix merge conflicts

This commit is contained in:
Griatch 2018-10-01 20:58:16 +02:00
commit 72f4fedcbe
148 changed files with 20005 additions and 2718 deletions

View file

@ -54,7 +54,8 @@ _GA = object.__getattribute__
def create_object(typeclass=None, key=None, location=None, home=None,
permissions=None, locks=None, aliases=None, tags=None,
destination=None, report_to=None, nohome=False):
destination=None, report_to=None, nohome=False, attributes=None,
nattributes=None):
"""
Create a new in-game object.
@ -68,13 +69,18 @@ def create_object(typeclass=None, key=None, location=None, home=None,
permissions (list): A list of permission strings or tuples (permstring, category).
locks (str): one or more lockstrings, separated by semicolons.
aliases (list): A list of alternative keys or tuples (aliasstring, category).
tags (list): List of tag keys or tuples (tagkey, category).
tags (list): List of tag keys or tuples (tagkey, category) or (tagkey, category, data).
destination (Object or str): Obj or #dbref to use as an Exit's
target.
report_to (Object): The object to return error messages to.
nohome (bool): This allows the creation of objects without a
default home location; only used when creating the default
location itself or during unittests.
attributes (list): Tuples on the form (key, value) or (key, value, category),
(key, value, lockstring) or (key, value, lockstring, default_access).
to set as Attributes on the new object.
nattributes (list): Non-persistent tuples on the form (key, value). Note that
adding this rarely makes sense since this data will not survive a reload.
Returns:
object (Object): A newly created object of the given typeclass.
@ -95,6 +101,7 @@ def create_object(typeclass=None, key=None, location=None, home=None,
locks = make_iter(locks) if locks is not None else None
aliases = make_iter(aliases) if aliases is not None else None
tags = make_iter(tags) if tags is not None else None
attributes = make_iter(attributes) if attributes is not None else None
if isinstance(typeclass, str):
@ -122,7 +129,8 @@ def create_object(typeclass=None, key=None, location=None, home=None,
# store the call signature for the signal
new_object._createdict = dict(key=key, location=location, destination=destination, home=home,
typeclass=typeclass.path, permissions=permissions, locks=locks,
aliases=aliases, tags=tags, report_to=report_to, nohome=nohome)
aliases=aliases, tags=tags, report_to=report_to, nohome=nohome,
attributes=attributes, nattributes=nattributes)
# this will trigger the save signal which in turn calls the
# at_first_save hook on the typeclass, where the _createdict can be
# used.
@ -139,7 +147,8 @@ object = create_object
def create_script(typeclass=None, key=None, obj=None, account=None, locks=None,
interval=None, start_delay=None, repeats=None,
persistent=None, autostart=True, report_to=None, desc=None):
persistent=None, autostart=True, report_to=None, desc=None,
tags=None, attributes=None):
"""
Create a new script. All scripts are a combination of a database
object that communicates with the database, and an typeclass that
@ -169,7 +178,9 @@ def create_script(typeclass=None, key=None, obj=None, account=None, locks=None,
created or if the `start` method must be called explicitly.
report_to (Object): The object to return error messages to.
desc (str): Optional description of script
tags (list): List of tags or tuples (tag, category).
attributes (list): List if tuples (key, value) or (key, value, category)
(key, value, lockstring) or (key, value, lockstring, default_access).
See evennia.scripts.manager for methods to manipulate existing
scripts in the database.
@ -190,9 +201,9 @@ def create_script(typeclass=None, key=None, obj=None, account=None, locks=None,
if key:
kwarg["db_key"] = key
if account:
kwarg["db_account"] = dbid_to_obj(account, _ScriptDB)
kwarg["db_account"] = dbid_to_obj(account, _AccountDB)
if obj:
kwarg["db_obj"] = dbid_to_obj(obj, _ScriptDB)
kwarg["db_obj"] = dbid_to_obj(obj, _ObjectDB)
if interval:
kwarg["db_interval"] = interval
if start_delay:
@ -203,6 +214,8 @@ def create_script(typeclass=None, key=None, obj=None, account=None, locks=None,
kwarg["db_persistent"] = persistent
if desc:
kwarg["db_desc"] = desc
tags = make_iter(tags) if tags is not None else None
attributes = make_iter(attributes) if attributes is not None else None
# create new instance
new_script = typeclass(**kwarg)
@ -210,7 +223,8 @@ def create_script(typeclass=None, key=None, obj=None, account=None, locks=None,
# store the call signature for the signal
new_script._createdict = dict(key=key, obj=obj, account=account, locks=locks, interval=interval,
start_delay=start_delay, repeats=repeats, persistent=persistent,
autostart=autostart, report_to=report_to)
autostart=autostart, report_to=report_to, desc=desc,
tags=tags, attributes=attributes)
# this will trigger the save signal which in turn calls the
# at_first_save hook on the typeclass, where the _createdict
# can be used.

View file

@ -29,7 +29,7 @@ except ImportError:
from pickle import dumps, loads
from django.core.exceptions import ObjectDoesNotExist
from django.contrib.contenttypes.models import ContentType
from evennia.utils.utils import to_str, uses_database
from evennia.utils.utils import to_str, uses_database, is_iter
from evennia.utils import logger
__all__ = ("to_pickle", "from_pickle", "do_pickle", "do_unpickle",
@ -237,10 +237,13 @@ class _SaverList(_SaverMutable, MutableSequence):
self._data = list()
@_save
def __add__(self, otherlist):
def __iadd__(self, otherlist):
self._data = self._data.__add__(otherlist)
return self._data
def __add__(self, otherlist):
return list(self._data) + otherlist
@_save
def insert(self, index, value):
self._data.insert(index, self._convert_mutables(value))
@ -361,6 +364,31 @@ class _SaverDeque(_SaverMutable):
def rotate(self, *args):
self._data.rotate(*args)
_DESERIALIZE_MAPPING = {_SaverList.__name__: list, _SaverDict.__name__: dict,
_SaverSet.__name__: set, _SaverOrderedDict.__name__: OrderedDict,
_SaverDeque.__name__: deque}
def deserialize(obj):
"""
Make sure to *fully* decouple a structure from the database, by turning all _Saver*-mutables
inside it back into their normal Python forms.
"""
def _iter(obj):
typ = type(obj)
tname = typ.__name__
if tname in ('_SaverDict', 'dict'):
return {_iter(key): _iter(val) for key, val in obj.items()}
elif tname in _DESERIALIZE_MAPPING:
return _DESERIALIZE_MAPPING[tname](_iter(val) for val in obj)
elif is_iter(obj):
return typ(_iter(val) for val in obj)
return obj
return _iter(obj)
#
# serialization helpers

View file

@ -153,6 +153,22 @@ INVALID_FORMCHARS = r"\s\/\|\\\*\_\-\#\<\>\~\^\:\;\.\,"
_ANSI_ESCAPE = re.compile(r"\|\|")
def _to_rect(lines):
"""
Forces all lines to be as long as the longest
Args:
lines (list): list of `ANSIString`s
Returns:
(list): list of `ANSIString`s of
same length as the longest input line
"""
maxl = max(len(line) for line in lines)
return [line + ' ' * (maxl - len(line)) for line in lines]
def _to_ansi(obj, regexable=False):
"convert to ANSIString"
if isinstance(obj, str):
@ -184,7 +200,7 @@ class EvForm(object):
filename (str): Path to template file.
cells (dict): A dictionary mapping of {id:text}
tables (dict): A dictionary mapping of {id:EvTable}.
form (dict): A dictionary of {"CELLCHAR":char,
form (dict): A dictionary of {"FORMCHAR":char,
"TABLECHAR":char,
"FORM":templatestring}
if this is given, filename is not read.
@ -407,7 +423,9 @@ class EvForm(object):
self.tablechar = tablechar[0] if len(tablechar) > 1 else tablechar
# split into a list of list of lines. Form can be indexed with form[iy][ix]
self.raw_form = _to_ansi(datadict.get("FORM", "").split("\n"))
raw_form = _to_ansi(datadict.get("FORM", "").split("\n"))
self.raw_form = _to_rect(raw_form)
# strip first line
self.raw_form = self.raw_form[1:] if self.raw_form else self.raw_form
@ -439,7 +457,8 @@ def _test():
6: 5,
7: 18,
8: 10,
9: 3})
9: 3,
"F": "rev 1"})
# create the EvTables
tableA = EvTable("HP", "MV", "MP",
table=[["**"], ["*****"], ["***"]],

View file

@ -43,13 +43,18 @@ command definition too) with function definitions:
def node_with_other_name(caller, input_string):
# code
return text, options
def another_node(caller, input_string, **kwargs):
# code
return text, options
```
Where caller is the object using the menu and input_string is the
command entered by the user on the *previous* node (the command
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.
both one or two arguments interchangeably. It also accepts nodes
that takes **kwargs.
The menu tree itself is available on the caller as
`caller.ndb._menutree`. This makes it a convenient place to store
@ -82,12 +87,14 @@ menu is immediately exited and the default "look" command is called.
the callable. Those kwargs will also be passed into the next node if possible.
Such a callable should return either a str or a (str, dict), where the
string is the name of the next node to go to and the dict is the new,
(possibly modified) kwarg to pass into the next node.
(possibly modified) kwarg to pass into the next node. If the callable returns
None or the empty string, the current node will be revisited.
- `exec` (str, callable or tuple, optional): This takes the same input as `goto` above
and runs before it. If given a node name, the node will be executed but will not
be considered the next node. If node/callback returns str or (str, dict), these will
replace the `goto` step (`goto` callbacks will not fire), with the string being the
next node name and the optional dict acting as the kwargs-input for the next node.
If an exec callable returns the empty string (only), the current node is re-run.
If key is not given, the option will automatically be identified by
its number 1..N.
@ -158,16 +165,16 @@ evennia.utils.evmenu`.
"""
import random
import inspect
from builtins import object, range
from textwrap import dedent
from inspect import isfunction, getargspec
from django.conf import settings
from evennia import Command, CmdSet
from evennia.utils import logger
from evennia.utils.evtable import EvTable
from evennia.utils.ansi import strip_ansi
from evennia.utils.utils import mod_import, make_iter, pad, m_len, is_iter
from evennia.utils.utils import mod_import, make_iter, pad, to_str, m_len, is_iter, dedent, crop
from evennia.commands import cmdhandler
# read from protocol NAWS later?
@ -182,7 +189,8 @@ _CMD_NOINPUT = cmdhandler.CMD_NOINPUT
# i18n
from django.utils.translation import ugettext as _
_ERR_NOT_IMPLEMENTED = _("Menu node '{nodename}' is not implemented. Make another choice.")
_ERR_NOT_IMPLEMENTED = _("Menu node '{nodename}' is either not implemented or "
"caused an error. Make another choice.")
_ERR_GENERAL = _("Error in menu node '{nodename}'.")
_ERR_NO_OPTION_DESC = _("No description.")
_HELP_FULL = _("Commands: <menu option>, help, quit")
@ -315,7 +323,7 @@ class EvMenu(object):
auto_quit=True, auto_look=True, auto_help=True,
cmd_on_exit="look",
persistent=False, startnode_input="", session=None,
**kwargs):
debug=False, **kwargs):
"""
Initialize the menu tree and start the caller onto the first node.
@ -368,7 +376,8 @@ class EvMenu(object):
*pickle*. When the server is reloaded, the latest node shown will be completely
re-run with the same input arguments - so be careful if you are counting
up some persistent counter or similar - the counter may be run twice if
reload happens on the node that does that.
reload happens on the node that does that. Note that if `debug` is True,
this setting is ignored and assumed to be False.
startnode_input (str or (str, dict), optional): Send an input text to `startnode` as if
a user input text from a fictional previous node. If including the dict, this will
be passed as **kwargs to that node. When the server reloads,
@ -378,6 +387,10 @@ class EvMenu(object):
for the very first display of the first node - after that, EvMenu itself
will keep the session updated from the command input. So a persistent
menu will *not* be using this same session anymore after a reload.
debug (bool, optional): If set, the 'menudebug' command will be made available
by default in all nodes of the menu. This will print out the current state of
the menu. Deactivate for production use! When the debug flag is active, the
`persistent` flag is deactivated.
Kwargs:
any (any): All kwargs will become initialization variables on `caller.ndb._menutree`,
@ -401,7 +414,7 @@ class EvMenu(object):
"""
self._startnode = startnode
self._menutree = self._parse_menudata(menudata)
self._persistent = persistent
self._persistent = persistent if not debug else False
self._quitting = False
if startnode not in self._menutree:
@ -415,6 +428,7 @@ class EvMenu(object):
self.auto_quit = auto_quit
self.auto_look = auto_look
self.auto_help = auto_help
self.debug_mode = debug
self._session = session
if isinstance(cmd_on_exit, str):
# At this point menu._session will have been replaced by the
@ -573,6 +587,7 @@ class EvMenu(object):
except EvMenuError:
errmsg = _ERR_GENERAL.format(nodename=callback)
self.caller.msg(errmsg, self._session)
logger.log_trace()
raise
return ret
@ -606,9 +621,11 @@ class EvMenu(object):
nodetext, options = ret, None
except KeyError:
self.caller.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename), session=self._session)
logger.log_trace()
raise EvMenuError
except Exception:
self.caller.msg(_ERR_GENERAL.format(nodename=nodename), session=self._session)
logger.log_trace()
raise
# store options to make them easier to test
@ -665,45 +682,65 @@ class EvMenu(object):
if isinstance(ret, str):
# only return a value if a string (a goto target), ignore all other returns
if not ret:
# an empty string - rerun the same node
return self.nodename
return ret, kwargs
return None
def extract_goto_exec(self, nodename, option_dict):
"""
Helper: Get callables and their eventual kwargs.
Args:
nodename (str): The current node name (used for error reporting).
option_dict (dict): The seleted option's dict.
Returns:
goto (str, callable or None): The goto directive in the option.
goto_kwargs (dict): Kwargs for `goto` if the former is callable, otherwise empty.
execute (callable or None): Executable given by the `exec` directive.
exec_kwargs (dict): Kwargs for `execute` if it's callable, otherwise empty.
"""
goto_kwargs, exec_kwargs = {}, {}
goto, execute = option_dict.get("goto", None), option_dict.get("exec", None)
if goto and isinstance(goto, (tuple, list)):
if len(goto) > 1:
goto, goto_kwargs = goto[:2] # ignore any extra arguments
if not hasattr(goto_kwargs, "__getitem__"):
# not a dict-like structure
raise EvMenuError("EvMenu node {}: goto kwargs is not a dict: {}".format(
nodename, goto_kwargs))
else:
goto = goto[0]
if execute and isinstance(execute, (tuple, list)):
if len(execute) > 1:
execute, exec_kwargs = execute[:2] # ignore any extra arguments
if not hasattr(exec_kwargs, "__getitem__"):
# not a dict-like structure
raise EvMenuError("EvMenu node {}: exec kwargs is not a dict: {}".format(
nodename, goto_kwargs))
else:
execute = execute[0]
return goto, goto_kwargs, execute, exec_kwargs
def goto(self, nodename, raw_string, **kwargs):
"""
Run a node by name, optionally dynamically generating that name first.
Args:
nodename (str or callable): Name of node or a callable
to be called as `function(caller, raw_string)` or `function(caller)`
to return the actual goto string.
to be called as `function(caller, raw_string, **kwargs)` or
`function(caller, **kwargs)` to return the actual goto string or
a ("nodename", kwargs) tuple.
raw_string (str): The raw default string entered on the
previous node (only used if the node accepts it as an
argument)
Kwargs:
any: Extra arguments to goto callables.
"""
def _extract_goto_exec(option_dict):
"Helper: Get callables and their eventual kwargs"
goto_kwargs, exec_kwargs = {}, {}
goto, execute = option_dict.get("goto", None), option_dict.get("exec", None)
if goto and isinstance(goto, (tuple, list)):
if len(goto) > 1:
goto, goto_kwargs = goto[:2] # ignore any extra arguments
if not hasattr(goto_kwargs, "__getitem__"):
# not a dict-like structure
raise EvMenuError("EvMenu node {}: goto kwargs is not a dict: {}".format(
nodename, goto_kwargs))
else:
goto = goto[0]
if execute and isinstance(execute, (tuple, list)):
if len(execute) > 1:
execute, exec_kwargs = execute[:2] # ignore any extra arguments
if not hasattr(exec_kwargs, "__getitem__"):
# not a dict-like structure
raise EvMenuError("EvMenu node {}: exec kwargs is not a dict: {}".format(
nodename, goto_kwargs))
else:
execute = execute[0]
return goto, goto_kwargs, execute, exec_kwargs
if callable(nodename):
# run the "goto" callable, if possible
@ -714,6 +751,9 @@ class EvMenu(object):
raise EvMenuError(
"{}: goto callable must return str or (str, dict)".format(inp_nodename))
nodename, kwargs = nodename[:2]
if not nodename:
# no nodename return. Re-run current node
nodename = self.nodename
try:
# execute the found node, make use of the returns.
nodetext, options = self._execute_node(nodename, raw_string, **kwargs)
@ -746,12 +786,12 @@ class EvMenu(object):
desc = dic.get("desc", dic.get("text", None))
if "_default" in keys:
keys = [key for key in keys if key != "_default"]
goto, goto_kwargs, execute, exec_kwargs = _extract_goto_exec(dic)
goto, goto_kwargs, execute, exec_kwargs = self.extract_goto_exec(nodename, dic)
self.default = (goto, goto_kwargs, execute, exec_kwargs)
else:
# use the key (only) if set, otherwise use the running number
keys = list(make_iter(dic.get("key", str(inum + 1).strip())))
goto, goto_kwargs, execute, exec_kwargs = _extract_goto_exec(dic)
goto, goto_kwargs, execute, exec_kwargs = self.extract_goto_exec(nodename, dic)
if keys:
display_options.append((keys[0], desc))
for key in keys:
@ -765,7 +805,7 @@ class EvMenu(object):
# handle the helptext
if helptext:
self.helptext = helptext
self.helptext = self.helptext_formatter(helptext)
elif options:
self.helptext = _HELP_FULL if self.auto_quit else _HELP_NO_QUIT
else:
@ -814,6 +854,51 @@ class EvMenu(object):
if self.cmd_on_exit is not None:
self.cmd_on_exit(self.caller, self)
def print_debug_info(self, arg):
"""
Messages the caller with the current menu state, for debug purposes.
Args:
arg (str): Arg to debug instruction, either nothing, 'full' or the name
of a property to inspect.
"""
all_props = inspect.getmembers(self)
all_methods = [name for name, _ in inspect.getmembers(self, predicate=inspect.ismethod)]
all_builtins = [name for name, _ in inspect.getmembers(self, predicate=inspect.isbuiltin)]
props = {prop: value for prop, value in all_props if prop not in all_methods and
prop not in all_builtins and not prop.endswith("__")}
local = {key: var for key, var in locals().items()
if key not in all_props and not key.endswith("__")}
if arg:
if arg in props:
debugtxt = " |y* {}:|n\n{}".format(arg, props[arg])
elif arg in local:
debugtxt = " |y* {}:|n\n{}".format(arg, local[arg])
elif arg == 'full':
debugtxt = ("|yMENU DEBUG full ... |n\n" + "\n".join(
"|y *|n {}: {}".format(key, val)
for key, val in sorted(props.items())) +
"\n |yLOCAL VARS:|n\n" + "\n".join(
"|y *|n {}: {}".format(key, val)
for key, val in sorted(local.items())) +
"\n |y... END MENU DEBUG|n")
else:
debugtxt = "|yUsage: menudebug full|<name of property>|n"
else:
debugtxt = ("|yMENU DEBUG properties ... |n\n" + "\n".join(
"|y *|n {}: {}".format(
key, crop(to_str(val, force_string=True), width=50))
for key, val in sorted(props.items())) +
"\n |yLOCAL VARS:|n\n" + "\n".join(
"|y *|n {}: {}".format(
key, crop(to_str(val, force_string=True), width=50))
for key, val in sorted(local.items())) +
"\n |y... END MENU DEBUG|n")
self.caller.msg(debugtxt)
def parse_input(self, raw_string):
"""
Parses the incoming string from the menu user.
@ -840,16 +925,14 @@ class EvMenu(object):
self.display_helptext()
elif self.auto_quit and cmd in ("quit", "q", "exit"):
self.close_menu()
elif self.debug_mode and cmd.startswith("menudebug"):
self.print_debug_info(cmd[9:].strip())
elif self.default:
goto, goto_kwargs, execfunc, exec_kwargs = self.default
self.run_exec_then_goto(execfunc, goto, raw_string, exec_kwargs, goto_kwargs)
else:
self.caller.msg(_HELP_NO_OPTION_MATCH, session=self._session)
if not (self.options or self.default):
# no options - we are at the end of the menu.
self.close_menu()
def display_nodetext(self):
self.caller.msg(self.nodetext, session=self._session)
@ -869,7 +952,20 @@ class EvMenu(object):
nodetext (str): The formatted node text.
"""
return dedent(nodetext).strip()
return dedent(nodetext.strip('\n'), baseline_index=0).rstrip()
def helptext_formatter(self, helptext):
"""
Format the node's help text
Args:
helptext (str): The unformatted help text for the node.
Returns:
helptext (str): The formatted help text.
"""
return dedent(helptext.strip('\n'), baseline_index=0).rstrip()
def options_formatter(self, optionlist):
"""
@ -949,14 +1045,188 @@ class EvMenu(object):
node (str): The formatted node to display.
"""
if self._session:
screen_width = self._session.protocol_flags.get(
"SCREENWIDTH", {0: _MAX_TEXT_WIDTH})[0]
else:
screen_width = _MAX_TEXT_WIDTH
nodetext_width_max = max(m_len(line) for line in nodetext.split("\n"))
options_width_max = max(m_len(line) for line in optionstext.split("\n"))
total_width = max(options_width_max, nodetext_width_max)
total_width = min(screen_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 + "|n" + nodetext + "|n" + separator2 + "|n" + optionstext
# -----------------------------------------------------------
#
# List node (decorator turning a node into a list with
# look/edit/add functionality for the elements)
#
# -----------------------------------------------------------
def list_node(option_generator, select=None, pagesize=10):
"""
Decorator for making an EvMenu node into a multi-page list node. Will add new options,
prepending those options added in the node.
Args:
option_generator (callable or list): A list of strings indicating the options, or a callable
that is called as option_generator(caller) to produce such a list.
select (callable or str, optional): Node to redirect a selection to. Its `**kwargs` will
contain the `available_choices` list and `selection` will hold one of the elements in
that list. If a callable, it will be called as select(caller, menuchoice) where
menuchoice is the chosen option as a string. Should return the target node to goto after
this selection (or None to repeat the list-node). Note that if this is not given, the
decorated node must itself provide a way to continue from the node!
pagesize (int): How many options to show per page.
Example:
@list_node(['foo', 'bar'], select)
def node_index(caller):
text = "describing the list"
return text, []
Notes:
All normal `goto` or `exec` callables returned from the decorated nodes will, if they accept
**kwargs, get a new kwarg 'available_choices' injected. These are the ordered list of named
options (descs) visible on the current node page.
"""
def decorator(func):
def _select_parser(caller, raw_string, **kwargs):
"""
Parse the select action
"""
available_choices = kwargs.get("available_choices", [])
try:
index = int(raw_string.strip()) - 1
selection = available_choices[index]
except Exception:
caller.msg("|rInvalid choice.|n")
else:
if callable(select):
try:
if bool(getargspec(select).keywords):
return select(caller, selection, available_choices=available_choices)
else:
return select(caller, selection)
except Exception:
logger.log_trace()
elif select:
# we assume a string was given, we inject the result into the kwargs
# to pass on to the next node
kwargs['selection'] = selection
return str(select)
# this means the previous node will be re-run with these same kwargs
return None
def _list_node(caller, raw_string, **kwargs):
option_list = option_generator(caller) \
if callable(option_generator) else option_generator
npages = 0
page_index = 0
page = []
options = []
if option_list:
nall_options = len(option_list)
pages = [option_list[ind:ind + pagesize]
for ind in range(0, nall_options, pagesize)]
npages = len(pages)
page_index = max(0, min(npages - 1, kwargs.get("optionpage_index", 0)))
page = pages[page_index]
text = ""
extra_text = None
# dynamic, multi-page option list. Each selection leads to the `select`
# callback being called with a result from the available choices
options.extend([{"desc": opt,
"goto": (_select_parser,
{"available_choices": page})} for opt in page])
if npages > 1:
# if the goto callable returns None, the same node is rerun, and
# kwargs not used by the callable are passed on to the node. This
# allows us to call ourselves over and over, using different kwargs.
options.append({"key": ("|Wcurrent|n", "c"),
"desc": "|W({}/{})|n".format(page_index + 1, npages),
"goto": (lambda caller: None,
{"optionpage_index": page_index})})
if page_index > 0:
options.append({"key": ("|wp|Wrevious page|n", "p"),
"goto": (lambda caller: None,
{"optionpage_index": page_index - 1})})
if page_index < npages - 1:
options.append({"key": ("|wn|Wext page|n", "n"),
"goto": (lambda caller: None,
{"optionpage_index": page_index + 1})})
# add data from the decorated node
decorated_options = []
supports_kwargs = bool(getargspec(func).keywords)
try:
if supports_kwargs:
text, decorated_options = func(caller, raw_string, **kwargs)
else:
text, decorated_options = func(caller, raw_string)
except TypeError:
try:
if supports_kwargs:
text, decorated_options = func(caller, **kwargs)
else:
text, decorated_options = func(caller)
except Exception:
raise
except Exception:
logger.log_trace()
else:
if isinstance(decorated_options, dict):
decorated_options = [decorated_options]
else:
decorated_options = make_iter(decorated_options)
extra_options = []
if isinstance(decorated_options, dict):
decorated_options = [decorated_options]
for eopt in decorated_options:
cback = ("goto" in eopt and "goto") or ("exec" in eopt and "exec") or None
if cback:
signature = eopt[cback]
if callable(signature):
# callable with no kwargs defined
eopt[cback] = (signature, {"available_choices": page})
elif is_iter(signature):
if len(signature) > 1 and isinstance(signature[1], dict):
signature[1]["available_choices"] = page
eopt[cback] = signature
elif signature:
# a callable alone in a tuple (i.e. no previous kwargs)
eopt[cback] = (signature[0], {"available_choices": page})
else:
# malformed input.
logger.log_err("EvMenu @list_node decorator found "
"malformed option to decorate: {}".format(eopt))
extra_options.append(eopt)
options.extend(extra_options)
text = text + "\n\n" + extra_text if extra_text else text
return text, options
return _list_node
return decorator
# -------------------------------------------------------------------------------------------------
#
# Simple input shortcuts

View file

@ -122,7 +122,8 @@ class EvMore(object):
"""
def __init__(self, caller, text, always_page=False, session=None,
justify_kwargs=None, exit_on_lastpage=False, **kwargs):
justify_kwargs=None, exit_on_lastpage=False,
exit_cmd=None, **kwargs):
"""
Initialization of the text handler.
@ -141,6 +142,10 @@ class EvMore(object):
page being completely filled, exit pager immediately. If unset,
another move forward is required to exit. If set, the pager
exit message will not be shown.
exit_cmd (str, optional): If given, this command-string will be executed on
the caller when the more page exits. Note that this will be using whatever
cmdset the user had *before* the evmore pager was activated (so none of
the evmore commands will be available when this is run).
kwargs (any, optional): These will be passed on
to the `caller.msg` method.
@ -151,6 +156,7 @@ class EvMore(object):
self._npages = []
self._npos = []
self.exit_on_lastpage = exit_on_lastpage
self.exit_cmd = exit_cmd
self._exit_msg = "Exited |wmore|n pager."
if not session:
# if not supplied, use the first session to
@ -202,15 +208,18 @@ class EvMore(object):
# goto top of the text
self.page_top()
def display(self):
def display(self, show_footer=True):
"""
Pretty-print the page.
"""
pos = self._pos
text = self._pages[pos]
page = _DISPLAY.format(text=text,
pageno=pos + 1,
pagemax=self._npages)
if show_footer:
page = _DISPLAY.format(text=text,
pageno=pos + 1,
pagemax=self._npages)
else:
page = text
# check to make sure our session is still valid
sessions = self._caller.sessions.get()
if not sessions:
@ -245,9 +254,11 @@ class EvMore(object):
self.page_quit()
else:
self._pos += 1
self.display()
if self.exit_on_lastpage and self._pos >= self._npages - 1:
self.page_quit()
if self.exit_on_lastpage and self._pos >= (self._npages - 1):
self.display(show_footer=False)
self.page_quit(quiet=True)
else:
self.display()
def page_back(self):
"""
@ -256,16 +267,20 @@ class EvMore(object):
self._pos = max(0, self._pos - 1)
self.display()
def page_quit(self):
def page_quit(self, quiet=False):
"""
Quit the pager
"""
del self._caller.ndb._more
self._caller.msg(text=self._exit_msg, **self._kwargs)
if not quiet:
self._caller.msg(text=self._exit_msg, **self._kwargs)
self._caller.cmdset.remove(CmdSetMore)
if self.exit_cmd:
self._caller.execute_cmd(self.exit_cmd, session=self._session)
def msg(caller, text="", always_page=False, session=None, justify_kwargs=None, **kwargs):
def msg(caller, text="", always_page=False, session=None,
justify_kwargs=None, exit_on_lastpage=True, **kwargs):
"""
More-supported version of msg, mimicking the normal msg method.
@ -280,9 +295,10 @@ def msg(caller, text="", always_page=False, session=None, justify_kwargs=None, *
justify_kwargs (dict, bool or None, optional): If given, this should
be valid keyword arguments to the utils.justify() function. If False,
no justification will be done.
exit_on_lastpage (bool, optional): Immediately exit pager when reaching the last page.
kwargs (any, optional): These will be passed on
to the `caller.msg` method.
"""
EvMore(caller, text, always_page=always_page, session=session,
justify_kwargs=justify_kwargs, **kwargs)
justify_kwargs=justify_kwargs, exit_on_lastpage=exit_on_lastpage, **kwargs)

View file

@ -899,6 +899,9 @@ class EvColumn(object):
"""
col = self.column
# fixed options for the column will override those requested in the call!
# this is particularly relevant to things like width/height, to avoid
# fixed-widths columns from being auto-balanced
kwargs.update(self.options)
# use fixed width or adjust to the largest cell
if "width" not in kwargs:
@ -1289,25 +1292,59 @@ class EvTable(object):
cwidths_min = [max(cell.get_min_width() for cell in col) for col in self.worktable]
cwmin = sum(cwidths_min)
if cwmin > width:
# we cannot shrink any more
raise Exception("Cannot shrink table width to %s. Minimum size is %s." % (self.width, cwmin))
# get which cols have separately set widths - these should be locked
# note that we need to remove cwidths_min for each lock to avoid counting
# it twice (in cwmin and in locked_cols)
locked_cols = {icol: col.options['width'] - cwidths_min[icol]
for icol, col in enumerate(self.worktable) if 'width' in col.options}
locked_width = sum(locked_cols.values())
excess = width - cwmin - locked_width
if len(locked_cols) >= ncols and excess:
# we can't adjust the width at all - all columns are locked
raise Exception("Cannot balance table to width %s - "
"all columns have a set, fixed width summing to %s!" % (
self.width, sum(cwidths)))
if excess < 0:
# the locked cols makes it impossible
raise Exception("Cannot shrink table width to %s. "
"Minimum size (and/or fixed-width columns) "
"sets minimum at %s." % (self.width, cwmin + locked_width))
excess = width - cwmin
if self.evenwidth:
# make each column of equal width
for _ in range(excess):
# use cwidths as a work-array to track weights
cwidths = copy(cwidths_min)
correction = 0
while correction < excess:
# flood-fill the minimum table starting with the smallest columns
ci = cwidths_min.index(min(cwidths_min))
cwidths_min[ci] += 1
ci = cwidths.index(min(cwidths))
if ci in locked_cols:
# locked column, make sure it's not picked again
cwidths[ci] += 9999
cwidths_min[ci] = locked_cols[ci]
else:
cwidths_min[ci] += 1
correction += 1
cwidths = cwidths_min
else:
# make each column expand more proportional to their data size
for _ in range(excess):
# we use cwidth as a work-array to track weights
correction = 0
while correction < excess:
# fill wider columns first
ci = cwidths.index(max(cwidths))
cwidths_min[ci] += 1
cwidths[ci] -= 3
if ci in locked_cols:
# locked column, make sure it's not picked again
cwidths[ci] -= 9999
cwidths_min[ci] = locked_cols[ci]
else:
cwidths_min[ci] += 1
correction += 1
# give a just changed col less prio next run
cwidths[ci] -= 3
cwidths = cwidths_min
# reformat worktable (for width align)
@ -1329,28 +1366,46 @@ class EvTable(object):
for cell in (col[iy] for col in self.worktable)) for iy in range(nrowmax)]
chmin = sum(cheights_min)
# get which cols have separately set heights - these should be locked
# note that we need to remove cheights_min for each lock to avoid counting
# it twice (in chmin and in locked_cols)
locked_cols = {icol: col.options['height'] - cheights_min[icol]
for icol, col in enumerate(self.worktable) if 'height' in col.options}
locked_height = sum(locked_cols.values())
excess = self.height - chmin - locked_height
if chmin > self.height:
# we cannot shrink any more
raise Exception("Cannot shrink table height to %s. Minimum size is %s." % (self.height, chmin))
raise Exception("Cannot shrink table height to %s. Minimum "
"size (and/or fixed-height rows) sets minimum at %s." % (
self.height, chmin + locked_height))
# now we add all the extra height up to the desired table-height.
# We do this so that the tallest cells gets expanded first (and
# thus avoid getting cropped)
excess = self.height - chmin
even = self.height % 2 == 0
for position in range(excess):
correction = 0
while correction < excess:
# expand the cells with the most rows first
if 0 <= position < nrowmax and nrowmax > 1:
if 0 <= correction < nrowmax and nrowmax > 1:
# avoid adding to header first round (looks bad on very small tables)
ci = cheights[1:].index(max(cheights[1:])) + 1
else:
ci = cheights.index(max(cheights))
cheights_min[ci] += 1
if ci == 0 and self.header:
# it doesn't look very good if header expands too fast
cheights[ci] -= 2 if even else 3
cheights[ci] -= 2 if even else 1
if ci in locked_cols:
# locked row, make sure it's not picked again
cheights[ci] -= 9999
cheights_min[ci] = locked_cols[ci]
else:
cheights_min[ci] += 1
# change balance
if ci == 0 and self.header:
# it doesn't look very good if header expands too fast
cheights[ci] -= 2 if even else 3
cheights[ci] -= 2 if even else 1
correction += 1
cheights = cheights_min
# we must tell cells to crop instead of expanding
@ -1560,6 +1615,8 @@ class EvTable(object):
"""
if index > len(self.table):
raise Exception("Not a valid column index")
# we update the columns' options which means eventual width/height
# will be 'locked in' and withstand auto-balancing width/height from the table later
self.table[index].options.update(kwargs)
self.table[index].reformat(**kwargs)
@ -1575,6 +1632,7 @@ class EvTable(object):
def __str__(self):
"""print table (this also balances it)"""
# h = "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
return str(str(ANSIString("\n").join([line for line in self._generate_lines()])))
def __unicode__(self):

View file

@ -19,6 +19,8 @@ from evennia.utils.create import create_script
# to real time.
TIMEFACTOR = settings.TIME_FACTOR
IGNORE_DOWNTIMES = settings.TIME_IGNORE_DOWNTIMES
# Only set if gametime_reset was called at some point.
GAME_TIME_OFFSET = ServerConfig.objects.conf("gametime_offset", default=0)
@ -133,7 +135,10 @@ def gametime(absolute=False):
"""
epoch = game_epoch() if absolute else 0
gtime = epoch + (runtime() - GAME_TIME_OFFSET) * TIMEFACTOR
if IGNORE_DOWNTIMES:
gtime = epoch + (time.time() - server_epoch()) * TIMEFACTOR
else:
gtime = epoch + (runtime() - GAME_TIME_OFFSET) * TIMEFACTOR
return gtime

View file

@ -5,10 +5,6 @@ from django.db.models.manager import Manager
class SharedMemoryManager(Manager):
# CL: this ensures our manager is used when accessing instances via
# ForeignKey etc. (see docs)
use_for_related_fields = True
# TODO: improve on this implementation
# We need a way to handle reverse lookups so that this model can
# still use the singleton cache, but the active model isn't required

View file

@ -17,14 +17,14 @@ class RegularCategory(models.Model):
class Article(SharedMemoryModel):
name = models.CharField(max_length=32)
category = models.ForeignKey(Category)
category2 = models.ForeignKey(RegularCategory)
category = models.ForeignKey(Category, on_delete=models.CASCADE)
category2 = models.ForeignKey(RegularCategory, on_delete=models.CASCADE)
class RegularArticle(models.Model):
name = models.CharField(max_length=32)
category = models.ForeignKey(Category)
category2 = models.ForeignKey(RegularCategory)
category = models.ForeignKey(Category, on_delete=models.CASCADE)
category2 = models.ForeignKey(RegularCategory, on_delete=models.CASCADE)
class SharedMemorysTest(TestCase):

View file

@ -61,8 +61,10 @@ Error handling:
"""
import re
import fnmatch
from django.conf import settings
from evennia.utils import utils
from evennia.utils import utils, logger
# example/testing inline functions
@ -157,12 +159,32 @@ def clr(*args, **kwargs):
return text
def null(*args, **kwargs):
return args[0] if args else ''
def nomatch(name, *args, **kwargs):
"""
Default implementation of nomatch returns the function as-is as a string.
"""
kwargs.pop("inlinefunc_stack_depth", None)
kwargs.pop("session")
return "${name}({args}{kwargs})".format(
name=name,
args=",".join(args),
kwargs=",".join("{}={}".format(key, val) for key, val in kwargs.items()))
_INLINE_FUNCS = {}
# we specify a default nomatch function to use if no matching func was
# found. This will be overloaded by any nomatch function defined in
# the imported modules.
_INLINE_FUNCS = {"nomatch": lambda *args, **kwargs: "<UKNOWN>",
"stackfull": lambda *args, **kwargs: "\n (not parsed: inlinefunc stack size exceeded.)"}
_DEFAULT_FUNCS = {"nomatch": lambda *args, **kwargs: "<UNKNOWN>",
"stackfull": lambda *args, **kwargs: "\n (not parsed: "}
_INLINE_FUNCS.update(_DEFAULT_FUNCS)
# load custom inline func modules.
for module in utils.make_iter(settings.INLINEFUNC_MODULES):
@ -172,15 +194,12 @@ for module in utils.make_iter(settings.INLINEFUNC_MODULES):
if module == "server.conf.inlinefuncs":
# a temporary warning since the default module changed name
raise ImportError("Error: %s\nPossible reason: mygame/server/conf/inlinefunc.py should "
"be renamed to mygame/server/conf/inlinefuncs.py (note the S at the end)." % err)
"be renamed to mygame/server/conf/inlinefuncs.py (note "
"the S at the end)." % err)
else:
raise
# remove the core function if we include examples in this module itself
#_INLINE_FUNCS.pop("inline_func_parse", None)
# The stack size is a security measure. Set to <=0 to disable.
try:
_STACK_MAXSIZE = settings.INLINEFUNC_STACK_MAXSIZE
@ -189,18 +208,21 @@ except AttributeError:
# regex definitions
_RE_STARTTOKEN = re.compile(r"(?<!\\)\$(\w+)\(") # unescaped $funcname{ (start of function call)
_RE_STARTTOKEN = re.compile(r"(?<!\\)\$(\w+)\(") # unescaped $funcname( (start of function call)
# note: this regex can be experimented with at https://regex101.com/r/kGR3vE/2
_RE_TOKEN = re.compile(r"""
(?<!\\)\'\'\'(?P<singlequote>.*?)(?<!\\)\'\'\'| # unescaped single-triples (escapes all inside them)
(?<!\\)\"\"\"(?P<doublequote>.*?)(?<!\\)\"\"\"| # unescaped normal triple quotes (escapes all inside them)
(?P<comma>(?<!\\)\,)| # unescaped , (argument separator)
(?P<end>(?<!\\)\))| # unescaped ) (end of function call)
(?P<start>(?<!\\)\$\w+\()| # unescaped $funcname( (start of function call)
(?P<escaped>\\'|\\"|\\\)|\\$\w+\()| # escaped tokens should re-appear in text
(?P<rest>[\w\s.-\/#!%\^&\*;:=\-_`~\|\(}{\[\]]+|\"{1}|\'{1}) # everything else should also be included""",
re.UNICODE + re.IGNORECASE + re.VERBOSE + re.DOTALL)
(?<!\\)\'\'\'(?P<singlequote>.*?)(?<!\\)\'\'\'| # single-triplets escape all inside
(?<!\\)\"\"\"(?P<doublequote>.*?)(?<!\\)\"\"\"| # double-triplets escape all inside
(?P<comma>(?<!\\)\,)| # , (argument sep)
(?P<end>(?<!\\)\))| # ) (possible end of func call)
(?P<leftparens>(?<!\\)\()| # ( (lone left-parens)
(?P<start>(?<!\\)\$\w+\()| # $funcname (start of func call)
(?P<escaped> # escaped tokens to re-insert sans backslash
\\\'|\\\"|\\\)|\\\$\w+\(|\\\()|
(?P<rest> # everything else to re-insert verbatim
\$(?!\w+\()|\'|\"|\\|[^),$\'\"\\\(]+)""",
re.UNICODE | re.IGNORECASE | re.VERBOSE | re.DOTALL)
# Cache for function lookups.
_PARSING_CACHE = utils.LimitedSizeOrderedDict(size_limit=1000)
@ -257,7 +279,7 @@ class InlinefuncError(RuntimeError):
pass
def parse_inlinefunc(string, strip=False, **kwargs):
def parse_inlinefunc(string, strip=False, available_funcs=None, stacktrace=False, **kwargs):
"""
Parse the incoming string.
@ -265,6 +287,9 @@ def parse_inlinefunc(string, strip=False, **kwargs):
string (str): The incoming string to parse.
strip (bool, optional): Whether to strip function calls rather than
execute them.
available_funcs (dict, optional): Define an alternative source of functions to parse for.
If unset, use the functions found through `settings.INLINEFUNC_MODULES`.
stacktrace (bool, optional): If set, print the stacktrace to log.
Kwargs:
session (Session): This is sent to this function by Evennia when triggering
it. It is passed to the inlinefunc.
@ -273,7 +298,17 @@ def parse_inlinefunc(string, strip=False, **kwargs):
"""
global _PARSING_CACHE
if string in _PARSING_CACHE:
usecache = False
if not available_funcs:
available_funcs = _INLINE_FUNCS
usecache = True
else:
# make sure the default keys are available, but also allow overriding
tmp = _DEFAULT_FUNCS.copy()
tmp.update(available_funcs)
available_funcs = tmp
if usecache and string in _PARSING_CACHE:
# stack is already cached
stack = _PARSING_CACHE[string]
elif not _RE_STARTTOKEN.search(string):
@ -285,13 +320,36 @@ def parse_inlinefunc(string, strip=False, **kwargs):
# process string on stack
ncallable = 0
nlparens = 0
nvalid = 0
if stacktrace:
out = "STRING: {} =>".format(string)
print(out)
logger.log_info(out)
for match in _RE_TOKEN.finditer(string):
gdict = match.groupdict()
if stacktrace:
out = " MATCH: {}".format({key: val for key, val in gdict.items() if val})
print(out)
logger.log_info(out)
if gdict["singlequote"]:
stack.append(gdict["singlequote"])
elif gdict["doublequote"]:
stack.append(gdict["doublequote"])
elif gdict["leftparens"]:
# we have a left-parens inside a callable
if ncallable:
nlparens += 1
stack.append("(")
elif gdict["end"]:
if nlparens > 0:
nlparens -= 1
stack.append(")")
continue
if ncallable <= 0:
stack.append(")")
continue
@ -309,10 +367,12 @@ def parse_inlinefunc(string, strip=False, **kwargs):
funcname = _RE_STARTTOKEN.match(gdict["start"]).group(1)
try:
# try to fetch the matching inlinefunc from storage
stack.append(_INLINE_FUNCS[funcname])
stack.append(available_funcs[funcname])
nvalid += 1
except KeyError:
stack.append(_INLINE_FUNCS["nomatch"])
stack.append(available_funcs["nomatch"])
stack.append(funcname)
stack.append(None)
ncallable += 1
elif gdict["escaped"]:
# escaped tokens
@ -335,11 +395,11 @@ def parse_inlinefunc(string, strip=False, **kwargs):
# this means not all inlinefuncs were complete
return string
if _STACK_MAXSIZE > 0 and _STACK_MAXSIZE < len(stack):
if _STACK_MAXSIZE > 0 and _STACK_MAXSIZE < nvalid:
# if stack is larger than limit, throw away parsing
return string + gdict["stackfull"](*args, **kwargs)
else:
# cache the stack
return string + available_funcs["stackfull"](*args, **kwargs)
elif usecache:
# cache the stack - we do this also if we don't check the cache above
_PARSING_CACHE[string] = stack
# run the stack recursively
@ -362,9 +422,14 @@ def parse_inlinefunc(string, strip=False, **kwargs):
kwargs["inlinefunc_stack_depth"] = depth
retval = "" if strip else func(*args, **kwargs)
return utils.to_str(retval, force_string=True)
retval = "".join(_run_stack(item) for item in stack)
if stacktrace:
out = "STACK: \n{} => {}\n".format(stack, retval)
print(out)
logger.log_info(out)
# execute the stack from the cache
return "".join(_run_stack(item) for item in _PARSING_CACHE[string])
# execute the stack
return retval
#
# Nick templating
@ -398,7 +463,6 @@ Custom arg markers
$N argument position (1-99)
"""
import fnmatch
_RE_NICK_ARG = re.compile(r"\\(\$)([1-9][0-9]?)")
_RE_NICK_TEMPLATE_ARG = re.compile(r"(\$)([1-9][0-9]?)")
_RE_NICK_SPACE = re.compile(r"\\ ")
@ -439,7 +503,6 @@ def initialize_nick_templates(in_template, out_template):
# validate the tempaltes - they should at least have the same number of args
n_outargs = len(_RE_NICK_TEMPLATE_ARG.findall(out_template))
if n_inargs != n_outargs:
print(n_inargs, n_outargs)
raise NickTemplateInvalid
return re.compile(regex_string), template_string

View file

@ -20,6 +20,7 @@ import time
from datetime import datetime
from traceback import format_exc
from twisted.python import log, logfile
from twisted.python import util as twisted_util
from twisted.internet.threads import deferToThread
@ -29,10 +30,15 @@ _TIMEZONE = None
_CHANNEL_LOG_NUM_TAIL_LINES = None
# logging overrides
def timeformat(when=None):
"""
This helper function will format the current time in the same
way as twisted's logger does, including time zone info.
way as the twisted logger does, including time zone info. Only
difference from official logger is that we only use two digits
for the year and don't show timezone for CET times.
Args:
when (int, optional): This is a time in POSIX seconds on the form
@ -49,14 +55,86 @@ def timeformat(when=None):
tz_offset = tz_offset.days * 86400 + tz_offset.seconds
# correct given time to utc
when = datetime.utcfromtimestamp(when - tz_offset)
tz_hour = abs(int(tz_offset // 3600))
tz_mins = abs(int(tz_offset // 60 % 60))
tz_sign = "-" if tz_offset >= 0 else "+"
return '%d-%02d-%02d %02d:%02d:%02d%s%02d%02d' % (
when.year, when.month, when.day,
when.hour, when.minute, when.second,
tz_sign, tz_hour, tz_mins)
if tz_offset == 0:
tz = ""
else:
tz_hour = abs(int(tz_offset // 3600))
tz_mins = abs(int(tz_offset // 60 % 60))
tz_sign = "-" if tz_offset >= 0 else "+"
tz = "%s%02d%s" % (tz_sign, tz_hour,
(":%02d" % tz_mins if tz_mins else ""))
return '%d-%02d-%02d %02d:%02d:%02d%s' % (
when.year - 2000, when.month, when.day,
when.hour, when.minute, when.second, tz)
class WeeklyLogFile(logfile.DailyLogFile):
"""
Log file that rotates once per week
"""
day_rotation = 7
def shouldRotate(self):
"""Rotate when the date has changed since last write"""
# all dates here are tuples (year, month, day)
now = self.toDate()
then = self.lastDate
return now[0] > then[0] or now[1] > then[1] or now[2] > (then[2] + self.day_rotation)
def write(self, data):
"Write data to log file"
logfile.BaseLogFile.write(self, data)
self.lastDate = max(self.lastDate, self.toDate())
class PortalLogObserver(log.FileLogObserver):
"""
Reformat logging
"""
timeFormat = None
prefix = " |Portal| "
def emit(self, eventDict):
"""
Copied from Twisted parent, to change logging output
"""
text = log.textFromEventDict(eventDict)
if text is None:
return
# timeStr = self.formatTime(eventDict["time"])
timeStr = timeformat(eventDict["time"])
fmtDict = {
"text": text.replace("\n", "\n\t")}
msgStr = log._safeFormat("%(text)s\n", fmtDict)
twisted_util.untilConcludes(self.write, timeStr + "%s" % self.prefix + msgStr)
twisted_util.untilConcludes(self.flush)
class ServerLogObserver(PortalLogObserver):
prefix = " "
def log_msg(msg):
"""
Wrapper around log.msg call to catch any exceptions that might
occur in logging. If an exception is raised, we'll print to
stdout instead.
Args:
msg: The message that was passed to log.msg
"""
try:
log.msg(msg)
except Exception:
print("Exception raised while writing message to log. Original message: %s" % msg)
def log_trace(errmsg=None):
@ -80,9 +158,9 @@ def log_trace(errmsg=None):
except Exception as e:
errmsg = str(e)
for line in errmsg.splitlines():
log.msg('[EE] %s' % line)
log_msg('[EE] %s' % line)
except Exception:
log.msg('[EE] %s' % errmsg)
log_msg('[EE] %s' % errmsg)
log_tracemsg = log_trace
@ -101,13 +179,27 @@ def log_err(errmsg):
except Exception as e:
errmsg = str(e)
for line in errmsg.splitlines():
log.msg('[EE] %s' % line)
log_msg('[EE] %s' % line)
# log.err('ERROR: %s' % (errmsg,))
log_errmsg = log_err
def log_server(servermsg):
"""
This is for the Portal to log captured Server stdout messages (it's
usually only used during startup, before Server log is open)
"""
try:
servermsg = str(servermsg)
except Exception as e:
servermsg = str(e)
for line in servermsg.splitlines():
log_msg('[Server] %s' % line)
def log_warn(warnmsg):
"""
Prints/logs any warnings that aren't critical but should be noted.
@ -121,7 +213,7 @@ def log_warn(warnmsg):
except Exception as e:
warnmsg = str(e)
for line in warnmsg.splitlines():
log.msg('[WW] %s' % line)
log_msg('[WW] %s' % line)
# log.msg('WARNING: %s' % (warnmsg,))
@ -139,7 +231,7 @@ def log_info(infomsg):
except Exception as e:
infomsg = str(e)
for line in infomsg.splitlines():
log.msg('[..] %s' % line)
log_msg('[..] %s' % line)
log_infomsg = log_info
@ -157,11 +249,28 @@ def log_dep(depmsg):
except Exception as e:
depmsg = str(e)
for line in depmsg.splitlines():
log.msg('[DP] %s' % line)
log_msg('[DP] %s' % line)
log_depmsg = log_dep
def log_sec(secmsg):
"""
Prints a security-related message.
Args:
secmsg (str): The security message to log.
"""
try:
secmsg = str(secmsg)
except Exception as e:
secmsg = str(e)
for line in secmsg.splitlines():
log_msg('[SS] %s' % line)
log_secmsg = log_sec
# Arbitrary file logger
@ -219,6 +328,8 @@ class EvenniaLogFile(logfile.LogFile):
_LOG_FILE_HANDLES = {} # holds open log handles
_LOG_FILE_HANDLE_COUNTS = {}
_LOG_FILE_HANDLE_RESET = 500
def _open_log_file(filename):
@ -226,10 +337,15 @@ def _open_log_file(filename):
Helper to open the log file (always in the log dir) and cache its
handle. Will create a new file in the log dir if one didn't
exist.
To avoid keeping the filehandle open indefinitely we reset it every
_LOG_FILE_HANDLE_RESET accesses. This may help resolve issues for very
long uptimes and heavy log use.
"""
# we delay import of settings to keep logger module as free
# from django as possible.
global _LOG_FILE_HANDLES, _LOGDIR, _LOG_ROTATE_SIZE
global _LOG_FILE_HANDLES, _LOG_FILE_HANDLE_COUNTS, _LOGDIR, _LOG_ROTATE_SIZE
if not _LOGDIR:
from django.conf import settings
_LOGDIR = settings.LOG_DIR
@ -237,16 +353,22 @@ def _open_log_file(filename):
filename = os.path.join(_LOGDIR, filename)
if filename in _LOG_FILE_HANDLES:
# cache the handle
return _LOG_FILE_HANDLES[filename]
else:
try:
filehandle = EvenniaLogFile.fromFullPath(filename, rotateLength=_LOG_ROTATE_SIZE)
# filehandle = open(filename, "a+") # append mode + reading
_LOG_FILE_HANDLES[filename] = filehandle
return filehandle
except IOError:
log_trace()
_LOG_FILE_HANDLE_COUNTS[filename] += 1
if _LOG_FILE_HANDLE_COUNTS[filename] > _LOG_FILE_HANDLE_RESET:
# close/refresh handle
_LOG_FILE_HANDLES[filename].close()
del _LOG_FILE_HANDLES[filename]
else:
# return cached handle
return _LOG_FILE_HANDLES[filename]
try:
filehandle = EvenniaLogFile.fromFullPath(filename, rotateLength=_LOG_ROTATE_SIZE)
# filehandle = open(filename, "a+") # append mode + reading
_LOG_FILE_HANDLES[filename] = filehandle
_LOG_FILE_HANDLE_COUNTS[filename] = 0
return filehandle
except IOError:
log_trace()
return None

View file

@ -120,9 +120,11 @@ def dbsafe_decode(value, compress_object=False):
class PickledWidget(Textarea):
def render(self, name, value, attrs=None):
"""Display of the PickledField in django admin"""
value = repr(value)
try:
literal_eval(value)
# necessary to convert it back after repr(), otherwise validation errors will mutate it
value = literal_eval(value)
except ValueError:
return value

View file

@ -1,342 +0,0 @@
"""
Spawner
The spawner takes input files containing object definitions in
dictionary forms. These use a prototype architecture to define
unique objects without having to make a Typeclass for each.
The main function is `spawn(*prototype)`, where the `prototype`
is a dictionary like this:
```python
GOBLIN = {
"typeclass": "types.objects.Monster",
"key": "goblin grunt",
"health": lambda: randint(20,30),
"resists": ["cold", "poison"],
"attacks": ["fists"],
"weaknesses": ["fire", "light"]
"tags": ["mob", "evil", ('greenskin','mob')]
"args": [("weapon", "sword")]
}
```
Possible keywords are:
prototype - string parent prototype
key - string, the main object identifier
typeclass - string, if not set, will use `settings.BASE_OBJECT_TYPECLASS`
location - this should be a valid object or #dbref
home - valid object or #dbref
destination - only valid for exits (object or dbref)
permissions - string or list of permission strings
locks - a lock-string
aliases - string or list of strings
exec - this is a string of python code to execute or a list of such codes.
This can be used e.g. to trigger custom handlers on the object. The
execution namespace contains 'evennia' for the library and 'obj'
tags - string or list of strings or tuples `(tagstr, category)`. Plain
strings will be result in tags with no category (default tags).
attrs - tuple or list of tuples of Attributes to add. This form allows
more complex Attributes to be set. Tuples at least specify `(key, value)`
but can also specify up to `(key, value, category, lockstring)`. If
you want to specify a lockstring but not a category, set the category
to `None`.
ndb_<name> - value of a nattribute (ndb_ is stripped)
other - any other name is interpreted as the key of an Attribute with
its value. Such Attributes have no categories.
Each value can also be a callable that takes no arguments. It should
return the value to enter into the field and will be called every time
the prototype is used to spawn an object. Note, if you want to store
a callable in an Attribute, embed it in a tuple to the `args` keyword.
By specifying the "prototype" key, the prototype becomes a child of
that prototype, inheritng all prototype slots it does not explicitly
define itself, while overloading those that it does specify.
```python
GOBLIN_WIZARD = {
"prototype": GOBLIN,
"key": "goblin wizard",
"spells": ["fire ball", "lighting bolt"]
}
GOBLIN_ARCHER = {
"prototype": GOBLIN,
"key": "goblin archer",
"attacks": ["short bow"]
}
```
One can also have multiple prototypes. These are inherited from the
left, with the ones further to the right taking precedence.
```python
ARCHWIZARD = {
"attack": ["archwizard staff", "eye of doom"]
GOBLIN_ARCHWIZARD = {
"key" : "goblin archwizard"
"prototype": (GOBLIN_WIZARD, ARCHWIZARD),
}
```
The *goblin archwizard* will have some different attacks, but will
otherwise have the same spells as a *goblin wizard* who in turn shares
many traits with a normal *goblin*.
"""
import copy
from django.conf import settings
from random import randint
import evennia
from evennia.objects.models import ObjectDB
from evennia.utils.utils import make_iter, all_from_module, dbid_to_obj
_CREATE_OBJECT_KWARGS = ("key", "location", "home", "destination")
def _handle_dbref(inp):
return dbid_to_obj(inp, ObjectDB)
def _validate_prototype(key, prototype, protparents, visited):
"""
Run validation on a prototype, checking for inifinite regress.
"""
assert isinstance(prototype, dict)
if id(prototype) in visited:
raise RuntimeError("%s has infinite nesting of prototypes." % key or prototype)
visited.append(id(prototype))
protstrings = prototype.get("prototype")
if protstrings:
for protstring in make_iter(protstrings):
if key is not None and protstring == key:
raise RuntimeError("%s tries to prototype itself." % key or prototype)
protparent = protparents.get(protstring)
if not protparent:
raise RuntimeError("%s's prototype '%s' was not found." % (key or prototype, protstring))
_validate_prototype(protstring, protparent, protparents, visited)
def _get_prototype(dic, prot, protparents):
"""
Recursively traverse a prototype dictionary, including multiple
inheritance. Use _validate_prototype before this, we don't check
for infinite recursion here.
"""
if "prototype" in dic:
# move backwards through the inheritance
for prototype in make_iter(dic["prototype"]):
# Build the prot dictionary in reverse order, overloading
new_prot = _get_prototype(protparents.get(prototype, {}), prot, protparents)
prot.update(new_prot)
prot.update(dic)
prot.pop("prototype", None) # we don't need this anymore
return prot
def _batch_create_object(*objparams):
"""
This is a cut-down version of the create_object() function,
optimized for speed. It does NOT check and convert various input
so make sure the spawned Typeclass works before using this!
Args:
objsparams (tuple): Parameters for the respective creation/add
handlers in the following order:
- `create_kwargs` (dict): For use as new_obj = `ObjectDB(**create_kwargs)`.
- `permissions` (str): Permission string used with `new_obj.batch_add(permission)`.
- `lockstring` (str): Lockstring used with `new_obj.locks.add(lockstring)`.
- `aliases` (list): A list of alias strings for
adding with `new_object.aliases.batch_add(*aliases)`.
- `nattributes` (list): list of tuples `(key, value)` to be loop-added to
add with `new_obj.nattributes.add(*tuple)`.
- `attributes` (list): list of tuples `(key, value[,category[,lockstring]])` for
adding with `new_obj.attributes.batch_add(*attributes)`.
- `tags` (list): list of tuples `(key, category)` for adding
with `new_obj.tags.batch_add(*tags)`.
- `execs` (list): Code strings to execute together with the creation
of each object. They will be executed with `evennia` and `obj`
(the newly created object) available in the namespace. Execution
will happend after all other properties have been assigned and
is intended for calling custom handlers etc.
for the respective creation/add handlers in the following
order: (create_kwargs, permissions, locks, aliases, nattributes,
attributes, tags, execs)
Returns:
objects (list): A list of created objects
Notes:
The `exec` list will execute arbitrary python code so don't allow this to be available to
unprivileged users!
"""
# bulk create all objects in one go
# unfortunately this doesn't work since bulk_create doesn't creates pks;
# the result would be duplicate objects at the next stage, so we comment
# it out for now:
# dbobjs = _ObjectDB.objects.bulk_create(dbobjs)
dbobjs = [ObjectDB(**objparam[0]) for objparam in objparams]
objs = []
for iobj, obj in enumerate(dbobjs):
# call all setup hooks on each object
objparam = objparams[iobj]
# setup
obj._createdict = {"permissions": objparam[1],
"locks": objparam[2],
"aliases": objparam[3],
"nattributes": objparam[4],
"attributes": objparam[5],
"tags": objparam[6]}
# this triggers all hooks
obj.save()
# run eventual extra code
for code in objparam[7]:
if code:
exec(code, {}, {"evennia": evennia, "obj": obj})
objs.append(obj)
return objs
def spawn(*prototypes, **kwargs):
"""
Spawn a number of prototyped objects.
Args:
prototypes (dict): Each argument should be a prototype
dictionary.
Kwargs:
prototype_modules (str or list): A python-path to a prototype
module, or a list of such paths. These will be used to build
the global protparents dictionary accessible by the input
prototypes. If not given, it will instead look for modules
defined by settings.PROTOTYPE_MODULES.
prototype_parents (dict): A dictionary holding a custom
prototype-parent dictionary. Will overload same-named
prototypes from prototype_modules.
return_prototypes (bool): Only return a list of the
prototype-parents (no object creation happens)
"""
protparents = {}
protmodules = make_iter(kwargs.get("prototype_modules", []))
if not protmodules and hasattr(settings, "PROTOTYPE_MODULES"):
protmodules = make_iter(settings.PROTOTYPE_MODULES)
for prototype_module in protmodules:
protparents.update(dict((key, val) for key, val in
list(all_from_module(prototype_module).items()) if isinstance(val, dict)))
# overload module's protparents with specifically given protparents
protparents.update(kwargs.get("prototype_parents", {}))
for key, prototype in protparents.items():
_validate_prototype(key, prototype, protparents, [])
if "return_prototypes" in kwargs:
# only return the parents
return copy.deepcopy(protparents)
objsparams = []
for prototype in prototypes:
_validate_prototype(None, prototype, protparents, [])
prot = _get_prototype(prototype, {}, protparents)
if not prot:
continue
# extract the keyword args we need to create the object itself. If we get a callable,
# call that to get the value (don't catch errors)
create_kwargs = {}
keyval = prot.pop("key", "Spawned Object %06i" % randint(1, 100000))
create_kwargs["db_key"] = keyval() if callable(keyval) else keyval
locval = prot.pop("location", None)
create_kwargs["db_location"] = locval() if callable(locval) else _handle_dbref(locval)
homval = prot.pop("home", settings.DEFAULT_HOME)
create_kwargs["db_home"] = homval() if callable(homval) else _handle_dbref(homval)
destval = prot.pop("destination", None)
create_kwargs["db_destination"] = destval() if callable(destval) else _handle_dbref(destval)
typval = prot.pop("typeclass", settings.BASE_OBJECT_TYPECLASS)
create_kwargs["db_typeclass_path"] = typval() if callable(typval) else typval
# extract calls to handlers
permval = prot.pop("permissions", [])
permission_string = permval() if callable(permval) else permval
lockval = prot.pop("locks", "")
lock_string = lockval() if callable(lockval) else lockval
aliasval = prot.pop("aliases", "")
alias_string = aliasval() if callable(aliasval) else aliasval
tagval = prot.pop("tags", [])
tags = tagval() if callable(tagval) else tagval
attrval = prot.pop("attrs", [])
attributes = attrval() if callable(tagval) else attrval
exval = prot.pop("exec", "")
execs = make_iter(exval() if callable(exval) else exval)
# extract ndb assignments
nattributes = dict((key.split("_", 1)[1], value() if callable(value) else value)
for key, value in prot.items() if key.startswith("ndb_"))
# the rest are attributes
simple_attributes = [(key, value()) if callable(value) else (key, value)
for key, value in prot.items() if not key.startswith("ndb_")]
attributes = attributes + simple_attributes
attributes = [tup for tup in attributes if not tup[0] in _CREATE_OBJECT_KWARGS]
# pack for call into _batch_create_object
objsparams.append((create_kwargs, permission_string, lock_string,
alias_string, nattributes, attributes, tags, execs))
return _batch_create_object(*objsparams)
if __name__ == "__main__":
# testing
protparents = {
"NOBODY": {},
# "INFINITE" : {
# "prototype":"INFINITE"
# },
"GOBLIN": {
"key": "goblin grunt",
"health": lambda: randint(20, 30),
"resists": ["cold", "poison"],
"attacks": ["fists"],
"weaknesses": ["fire", "light"]
},
"GOBLIN_WIZARD": {
"prototype": "GOBLIN",
"key": "goblin wizard",
"spells": ["fire ball", "lighting bolt"]
},
"GOBLIN_ARCHER": {
"prototype": "GOBLIN",
"key": "goblin archer",
"attacks": ["short bow"]
},
"ARCHWIZARD": {
"attacks": ["archwizard staff"],
},
"GOBLIN_ARCHWIZARD": {
"key": "goblin archwizard",
"prototype": ("GOBLIN_WIZARD", "ARCHWIZARD")
}
}
# test
print([o.key for o in spawn(protparents["GOBLIN"],
protparents["GOBLIN_ARCHWIZARD"],
prototype_parents=protparents)])

View file

@ -6,6 +6,7 @@ Test form
FORMCHAR = "x"
TABLECHAR = "c"
FORM = """
.------------------------------------------------.
| |
@ -27,4 +28,6 @@ FORM = """
| ccccccccc | ccccccccccccccccBccccccccccccccccc |
| | |
-----------`-------------------------------------
Footer: xxxFxxx
info
"""

View file

@ -10,44 +10,47 @@ class TestEvForm(TestCase):
def test_form(self):
self.maxDiff = None
self.assertEqual(evform._test(),
'.------------------------------------------------.\n'
'| |\n'
'| Name: \x1b[0m\x1b[1m\x1b[32mTom\x1b[1m\x1b[32m \x1b'
'[1m\x1b[32mthe\x1b[1m\x1b[32m \x1b[0m \x1b[0m '
'Account: \x1b[0m\x1b[1m\x1b[33mGriatch '
'\x1b[0m\x1b[0m\x1b[1m\x1b[32m\x1b[1m\x1b[32m\x1b[1m\x1b[32m\x1b[1m\x1b[32m\x1b[0m\x1b[0m '
'|\n'
'| \x1b[0m\x1b[1m\x1b[32mBouncer\x1b[0m \x1b[0m |\n'
'| |\n'
' >----------------------------------------------<\n'
'| |\n'
'| Desc: \x1b[0mA sturdy \x1b[0m \x1b[0m'
' STR: \x1b[0m12 \x1b[0m\x1b[0m\x1b[0m\x1b[0m'
' DEX: \x1b[0m10 \x1b[0m\x1b[0m\x1b[0m\x1b[0m |\n'
'| \x1b[0mfellow\x1b[0m \x1b[0m'
' INT: \x1b[0m5 \x1b[0m\x1b[0m\x1b[0m\x1b[0m'
' STA: \x1b[0m18 \x1b[0m\x1b[0m\x1b[0m\x1b[0m |\n'
'| \x1b[0m \x1b[0m'
' LUC: \x1b[0m10 \x1b[0m\x1b[0m\x1b[0m'
' MAG: \x1b[0m3 \x1b[0m\x1b[0m\x1b[0m |\n'
'| |\n'
' >----------.-----------------------------------<\n'
'| | |\n'
'| \x1b[0mHP\x1b[0m|\x1b[0mMV \x1b[0m|\x1b[0mMP\x1b[0m '
'| \x1b[0mSkill \x1b[0m|\x1b[0mValue \x1b[0m'
'|\x1b[0mExp \x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m |\n'
'| ~~+~~~+~~ | ~~~~~~~~~~~+~~~~~~~~~~+~~~~~~~~~~~ |\n'
'| \x1b[0m**\x1b[0m|\x1b[0m***\x1b[0m\x1b[0m|\x1b[0m**\x1b[0m\x1b[0m '
'| \x1b[0mShooting \x1b[0m|\x1b[0m12 \x1b[0m'
'|\x1b[0m550/1200 \x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m |\n'
'| \x1b[0m \x1b[0m|\x1b[0m**\x1b[0m \x1b[0m|\x1b[0m*\x1b[0m \x1b[0m '
'| \x1b[0mHerbalism \x1b[0m|\x1b[0m14 \x1b[0m'
'|\x1b[0m990/1400 \x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m |\n'
'| \x1b[0m \x1b[0m|\x1b[0m \x1b[0m|\x1b[0m \x1b[0m '
'| \x1b[0mSmithing \x1b[0m|\x1b[0m9 \x1b[0m'
'|\x1b[0m205/900 \x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m |\n'
'| | |\n'
' -----------`-------------------------------------\n')
u'.------------------------------------------------.\n'
u'| |\n'
u'| Name: \x1b[0m\x1b[1m\x1b[32mTom\x1b[1m\x1b[32m \x1b'
u'[1m\x1b[32mthe\x1b[1m\x1b[32m \x1b[0m \x1b[0m '
u'Account: \x1b[0m\x1b[1m\x1b[33mGriatch '
u'\x1b[0m\x1b[0m\x1b[1m\x1b[32m\x1b[1m\x1b[32m\x1b[1m\x1b[32m\x1b[1m\x1b[32m\x1b[0m\x1b[0m '
u'|\n'
u'| \x1b[0m\x1b[1m\x1b[32mBouncer\x1b[0m \x1b[0m |\n'
u'| |\n'
u' >----------------------------------------------< \n'
u'| |\n'
u'| Desc: \x1b[0mA sturdy \x1b[0m \x1b[0m'
u' STR: \x1b[0m12 \x1b[0m\x1b[0m\x1b[0m\x1b[0m'
u' DEX: \x1b[0m10 \x1b[0m\x1b[0m\x1b[0m\x1b[0m |\n'
u'| \x1b[0mfellow\x1b[0m \x1b[0m'
u' INT: \x1b[0m5 \x1b[0m\x1b[0m\x1b[0m\x1b[0m'
u' STA: \x1b[0m18 \x1b[0m\x1b[0m\x1b[0m\x1b[0m |\n'
u'| \x1b[0m \x1b[0m'
u' LUC: \x1b[0m10 \x1b[0m\x1b[0m\x1b[0m'
u' MAG: \x1b[0m3 \x1b[0m\x1b[0m\x1b[0m |\n'
u'| |\n'
u' >----------.-----------------------------------< \n'
u'| | |\n'
u'| \x1b[0mHP\x1b[0m|\x1b[0mMV \x1b[0m|\x1b[0mMP\x1b[0m '
u'| \x1b[0mSkill \x1b[0m|\x1b[0mValue \x1b[0m'
u'|\x1b[0mExp \x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m |\n'
u'| ~~+~~~+~~ | ~~~~~~~~~~~+~~~~~~~~~~+~~~~~~~~~~~ |\n'
u'| \x1b[0m**\x1b[0m|\x1b[0m***\x1b[0m\x1b[0m|\x1b[0m**\x1b[0m\x1b[0m '
u'| \x1b[0mShooting \x1b[0m|\x1b[0m12 \x1b[0m'
u'|\x1b[0m550/1200 \x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m |\n'
u'| \x1b[0m \x1b[0m|\x1b[0m**\x1b[0m \x1b[0m|\x1b[0m*\x1b[0m \x1b[0m '
u'| \x1b[0mHerbalism \x1b[0m|\x1b[0m14 \x1b[0m'
u'|\x1b[0m990/1400 \x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m |\n'
u'| \x1b[0m \x1b[0m|\x1b[0m \x1b[0m|\x1b[0m \x1b[0m '
u'| \x1b[0mSmithing \x1b[0m|\x1b[0m9 \x1b[0m'
u'|\x1b[0m205/900 \x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m |\n'
u'| | |\n'
u' -----------`-------------------------------------\n'
u' Footer: \x1b[0mrev 1 \x1b[0m \n'
u' info \n'
u' ')
def test_ansi_escape(self):
# note that in a msg() call, the result would be the correct |-----,

View file

@ -58,7 +58,7 @@ class TestEvMenu(TestCase):
def _debug_output(self, indent, msg):
if self.debug_output:
print((" " * indent + msg))
print(" " * indent + ansi.strip_ansi(msg))
def _test_menutree(self, menu):
"""
@ -82,6 +82,8 @@ class TestEvMenu(TestCase):
self.assertIsNotNone(
bool(node_text),
"node: {}: node-text is None, which was not expected.".format(nodename))
if isinstance(node_text, tuple):
node_text, helptext = node_text
node_text = ansi.strip_ansi(node_text.strip())
self.assertTrue(
node_text.startswith(compare_text),
@ -168,6 +170,7 @@ class TestEvMenu(TestCase):
self.caller2.msg = MagicMock()
self.session = MagicMock()
self.session2 = MagicMock()
self.menu = evmenu.EvMenu(self.caller, self.menutree, startnode=self.startnode,
cmdset_mergetype=self.cmdset_mergetype,
cmdset_priority=self.cmdset_priority,

View file

@ -16,30 +16,29 @@ import math
import re
import textwrap
import random
import pickle
from os.path import join as osjoin
from importlib import import_module
from importlib.util import find_spec, module_from_spec
from inspect import ismodule, trace, getmembers, getmodule
from inspect import ismodule, trace, getmembers, getmodule, getmro
from collections import defaultdict, OrderedDict
from twisted.internet import threads, reactor, task
from django.conf import settings
from django.utils import timezone
from django.utils.translation import ugettext as _
from django.apps import apps
from evennia.utils import logger
_MULTIMATCH_TEMPLATE = settings.SEARCH_MULTIMATCH_TEMPLATE
_EVENNIA_DIR = settings.EVENNIA_DIR
_GAME_DIR = settings.GAME_DIR
import pickle
ENCODINGS = settings.ENCODINGS
_GA = object.__getattribute__
_SA = object.__setattr__
_DA = object.__delattr__
_DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
def is_iter(obj):
"""
@ -81,7 +80,7 @@ def make_iter(obj):
return not is_iter(obj) and [obj] or obj
def wrap(text, width=_DEFAULT_WIDTH, indent=0):
def wrap(text, width=None, indent=0):
"""
Safely wrap text to a certain number of characters.
@ -94,6 +93,7 @@ def wrap(text, width=_DEFAULT_WIDTH, indent=0):
text (str): Properly wrapped text.
"""
width = width if width else settings.CLIENT_DEFAULT_WIDTH
if not text:
return ""
indent = " " * indent
@ -104,7 +104,7 @@ def wrap(text, width=_DEFAULT_WIDTH, indent=0):
fill = wrap
def pad(text, width=_DEFAULT_WIDTH, align="c", fillchar=" "):
def pad(text, width=None, align="c", fillchar=" "):
"""
Pads to a given width.
@ -119,6 +119,7 @@ def pad(text, width=_DEFAULT_WIDTH, align="c", fillchar=" "):
text (str): The padded text.
"""
width = width if width else settings.CLIENT_DEFAULT_WIDTH
align = align if align in ('c', 'l', 'r') else 'c'
fillchar = fillchar[0] if fillchar else " "
if align == 'l':
@ -129,7 +130,7 @@ def pad(text, width=_DEFAULT_WIDTH, align="c", fillchar=" "):
return text.center(width, fillchar)
def crop(text, width=_DEFAULT_WIDTH, suffix="[...]"):
def crop(text, width=None, suffix="[...]"):
"""
Crop text to a certain width, throwing away text from too-long
lines.
@ -147,7 +148,7 @@ def crop(text, width=_DEFAULT_WIDTH, suffix="[...]"):
text (str): The cropped text.
"""
width = width if width else settings.CLIENT_DEFAULT_WIDTH
ltext = len(text)
if ltext <= width:
return text
@ -157,12 +158,16 @@ def crop(text, width=_DEFAULT_WIDTH, suffix="[...]"):
return to_str(text)
def dedent(text):
def dedent(text, baseline_index=None):
"""
Safely clean all whitespace at the left of a paragraph.
Args:
text (str): The text to dedent.
baseline_index (int or None, optional): Which row to use as a 'base'
for the indentation. Lines will be dedented to this level but
no further. If None, indent so as to completely deindent the
least indented text.
Returns:
text (str): Dedented string.
@ -175,10 +180,17 @@ def dedent(text):
"""
if not text:
return ""
return textwrap.dedent(text)
if baseline_index is None:
return textwrap.dedent(text)
else:
lines = text.split('\n')
baseline = lines[baseline_index]
spaceremove = len(baseline) - len(baseline.lstrip(' '))
return "\n".join(line[min(spaceremove, len(line) - len(line.lstrip(' '))):]
for line in lines)
def justify(text, width=_DEFAULT_WIDTH, align="f", indent=0):
def justify(text, width=None, align="f", indent=0):
"""
Fully justify a text so that it fits inside `width`. When using
full justification (default) this will be done by padding between
@ -197,6 +209,7 @@ def justify(text, width=_DEFAULT_WIDTH, align="f", indent=0):
justified (str): The justified and indented block of text.
"""
width = width if width else settings.CLIENT_DEFAULT_WIDTH
def _process_line(line):
"""
@ -208,18 +221,27 @@ def justify(text, width=_DEFAULT_WIDTH, align="f", indent=0):
gap = " " # minimum gap between words
if line_rest > 0:
if align == 'l':
line[-1] += " " * line_rest
if line[-1] == "\n\n":
line[-1] = " " * (line_rest-1) + "\n" + " " * width + "\n" + " " * width
else:
line[-1] += " " * line_rest
elif align == 'r':
line[0] = " " * line_rest + line[0]
elif align == 'c':
pad = " " * (line_rest // 2)
line[0] = pad + line[0]
line[-1] = line[-1] + pad + " " * (line_rest % 2)
if line[-1] == "\n\n":
line[-1] += pad + " " * (line_rest % 2 - 1) + \
"\n" + " " * width + "\n" + " " * width
else:
line[-1] = line[-1] + pad + " " * (line_rest % 2)
else: # align 'f'
gap += " " * (line_rest // max(1, ngaps))
rest_gap = line_rest % max(1, ngaps)
for i in range(rest_gap):
line[i] += " "
elif not any(line):
return [" " * width]
return gap.join(line)
# split into paragraphs and words
@ -260,6 +282,62 @@ def justify(text, width=_DEFAULT_WIDTH, align="f", indent=0):
return "\n".join([indentstring + line for line in lines])
def columnize(string, columns=2, spacing=4, align='l', width=None):
"""
Break a string into a number of columns, using as little
vertical space as possible.
Args:
string (str): The string to columnize.
columns (int, optional): The number of columns to use.
spacing (int, optional): How much space to have between columns.
width (int, optional): The max width of the columns.
Defaults to client's default width.
Returns:
columns (str): Text divided into columns.
Raises:
RuntimeError: If given invalid values.
"""
columns = max(1, columns)
spacing = max(1, spacing)
width = width if width else settings.CLIENT_DEFAULT_WIDTH
w_spaces = (columns - 1) * spacing
w_txt = max(1, width - w_spaces)
if w_spaces + columns > width: # require at least 1 char per column
raise RuntimeError("Width too small to fit columns")
colwidth = int(w_txt / (1.0 * columns))
# first make a single column which we then split
onecol = justify(string, width=colwidth, align=align)
onecol = onecol.split("\n")
nrows, dangling = divmod(len(onecol), columns)
nrows = [nrows + 1 if i < dangling else nrows for i in range(columns)]
height = max(nrows)
cols = []
istart = 0
for irows in nrows:
cols.append(onecol[istart:istart+irows])
istart = istart + irows
for col in cols:
if len(col) < height:
col.append(" " * colwidth)
sep = " " * spacing
rows = []
for irow in range(height):
rows.append(sep.join(col[irow] for col in cols))
return "\n".join(rows)
def list_to_string(inlist, endsep="and", addquote=False):
"""
This pretty-formats a list as string output, adding an optional
@ -867,24 +945,24 @@ def delay(timedelay, callback, *args, **kwargs):
Delay the return of a value.
Args:
timedelay (int or float): The delay in seconds
callback (callable): Will be called with optional
arguments after `timedelay` seconds.
args (any, optional): Will be used as arguments to callback
timedelay (int or float): The delay in seconds
callback (callable): Will be called as `callback(*args, **kwargs)`
after `timedelay` seconds.
args (any, optional): Will be used as arguments to callback
Kwargs:
persistent (bool, optional): should make the delay persistent
over a reboot or reload
any (any): Will be used to call the callback.
persistent (bool, optional): should make the delay persistent
over a reboot or reload
any (any): Will be used as keyword arguments to callback.
Returns:
deferred (deferred): Will fire fire with callback after
deferred (deferred): Will fire with callback after
`timedelay` seconds. Note that if `timedelay()` is used in the
commandhandler callback chain, the callback chain can be
defined directly in the command body and don't need to be
specified here.
Note:
The task handler (`evennia.scripts.taskhandler.TASK_HANDLEr`) will
The task handler (`evennia.scripts.taskhandler.TASK_HANDLER`) will
be called for persistent or non-persistent tasks.
If persistent is set to True, the callback, its arguments
and other keyword arguments will be saved in the database,
@ -1483,6 +1561,7 @@ def format_table(table, extra_space=1):
Examples:
```python
ftable = format_table([[...], [...], ...])
for ir, row in enumarate(ftable):
if ir == 0:
# make first row white
@ -1816,3 +1895,29 @@ def get_game_dir_path():
else:
os.chdir(os.pardir)
raise RuntimeError("server/conf/settings.py not found: Must start from inside game dir.")
def get_all_typeclasses(parent=None):
"""
List available typeclasses from all available modules.
Args:
parent (str, optional): If given, only return typeclasses inheriting (at any distance)
from this parent.
Returns:
typeclasses (dict): On the form {"typeclass.path": typeclass, ...}
Notes:
This will dynamicall retrieve all abstract django models inheriting at any distance
from the TypedObject base (aka a Typeclass) so it will work fine with any custom
classes being added.
"""
from evennia.typeclasses.models import TypedObject
typeclasses = {"{}.{}".format(model.__module__, model.__name__): model
for model in apps.get_models() if TypedObject in getmro(model)}
if parent:
typeclasses = {name: typeclass for name, typeclass in typeclasses.items()
if inherits_from(typeclass, parent)}
return typeclasses