Merge with develop and fix merge conflicts
This commit is contained in:
commit
72f4fedcbe
148 changed files with 20005 additions and 2718 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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=[["**"], ["*****"], ["***"]],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)])
|
||||
|
|
@ -6,6 +6,7 @@ Test form
|
|||
FORMCHAR = "x"
|
||||
TABLECHAR = "c"
|
||||
|
||||
|
||||
FORM = """
|
||||
.------------------------------------------------.
|
||||
| |
|
||||
|
|
@ -27,4 +28,6 @@ FORM = """
|
|||
| ccccccccc | ccccccccccccccccBccccccccccccccccc |
|
||||
| | |
|
||||
-----------`-------------------------------------
|
||||
Footer: xxxFxxx
|
||||
info
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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 |-----,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue