Merge with develop and fix merge conflicts

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

View file

@ -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