Resolve merge conflicts

This commit is contained in:
Griatch 2020-09-16 23:49:05 +02:00
commit 9cdc37355c
25 changed files with 726 additions and 444 deletions

View file

@ -56,9 +56,7 @@ without arguments starts a full interactive Python console.
of texts (such as tables). New `justify` bool. Old `justify_kwargs` remains of texts (such as tables). New `justify` bool. Old `justify_kwargs` remains
but is now only used to pass extra kwargs into the justify function. but is now only used to pass extra kwargs into the justify function.
- EvMore `text` argument can now also be a list or a queryset. Querysets will be - EvMore `text` argument can now also be a list or a queryset. Querysets will be
sliced to only return the required data per page. EvMore takes a new kwarg sliced to only return the required data per page.
`page_formatter` which will be called for each page. This allows to customize
the display of queryset data, build a new EvTable per page etc.
- Improve performance of `find` and `objects` commands on large data sets (strikaco) - Improve performance of `find` and `objects` commands on large data sets (strikaco)
- New `CHANNEL_HANDLER_CLASS` setting allows for replacing the ChannelHandler entirely. - New `CHANNEL_HANDLER_CLASS` setting allows for replacing the ChannelHandler entirely.
- Made `py` interactive mode support regular quit() and more verbose. - Made `py` interactive mode support regular quit() and more verbose.
@ -88,7 +86,13 @@ without arguments starts a full interactive Python console.
- Make `INLINEFUNC_STACK_MAXSIZE` default visible in `settings_default.py`. - Make `INLINEFUNC_STACK_MAXSIZE` default visible in `settings_default.py`.
- Change how `ic` finds puppets; non-priveleged users will use `_playable_characters` list as - Change how `ic` finds puppets; non-priveleged users will use `_playable_characters` list as
candidates, Builders+ will use list, local search and only global search if no match found. candidates, Builders+ will use list, local search and only global search if no match found.
- Make `cmd.at_post_cmd()` always run after `cmd.func()`, even when the latter uses delays
with yield.
- `EvMore` support for db queries and django paginators as well as easier to override for custom
pagination (e.g. to create EvTables for every page instead of splittine one table)
- Using `EvMore pagination`, dramatically improves performance of `spawn/list` and `scripts` listings
(100x speed increase for displaying 1000+ prototypes/scripts).
## Evennia 0.9 (2018-2019) ## Evennia 0.9 (2018-2019)

View file

@ -1262,7 +1262,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
] ]
except Exception: except Exception:
logger.log_trace() logger.log_trace()
now = timezone.now() now = timezone.localtime()
now = "%02i-%02i-%02i(%02i:%02i)" % (now.year, now.month, now.day, now.hour, now.minute) now = "%02i-%02i-%02i(%02i:%02i)" % (now.year, now.month, now.day, now.hour, now.minute)
if _MUDINFO_CHANNEL: if _MUDINFO_CHANNEL:
_MUDINFO_CHANNEL.tempmsg(f"[{_MUDINFO_CHANNEL.key}, {now}]: {message}") _MUDINFO_CHANNEL.tempmsg(f"[{_MUDINFO_CHANNEL.key}, {now}]: {message}")

View file

@ -174,6 +174,27 @@ def _msg_err(receiver, stringtuple):
) )
def _process_input(caller, prompt, result, cmd, generator):
"""
Specifically handle the get_input value to send to _progressive_cmd_run as
part of yielding from a Command's `func`.
Args:
caller (Character, Account or Session): the caller.
prompt (str): The sent prompt.
result (str): The unprocessed answer.
cmd (Command): The command itself.
generator (GeneratorType): The generator.
Returns:
result (bool): Always `False` (stop processing).
"""
# We call it using a Twisted deferLater to make sure the input is properly closed.
deferLater(reactor, 0, _progressive_cmd_run, cmd, generator, response=result)
return False
def _progressive_cmd_run(cmd, generator, response=None): def _progressive_cmd_run(cmd, generator, response=None):
""" """
Progressively call the command that was given in argument. Used Progressively call the command that was given in argument. Used
@ -206,7 +227,15 @@ def _progressive_cmd_run(cmd, generator, response=None):
else: else:
value = generator.send(response) value = generator.send(response)
except StopIteration: except StopIteration:
pass # duplicated from cmdhandler._run_command, to have these
# run in the right order while staying inside the deferred
cmd.at_post_cmd()
if cmd.save_for_next:
# store a reference to this command, possibly
# accessible by the next command.
cmd.caller.ndb.last_cmd = copy(cmd)
else:
cmd.caller.ndb.last_cmd = None
else: else:
if isinstance(value, (int, float)): if isinstance(value, (int, float)):
utils.delay(value, _progressive_cmd_run, cmd, generator) utils.delay(value, _progressive_cmd_run, cmd, generator)
@ -216,27 +245,6 @@ def _progressive_cmd_run(cmd, generator, response=None):
raise ValueError("unknown type for a yielded value in command: {}".format(type(value))) raise ValueError("unknown type for a yielded value in command: {}".format(type(value)))
def _process_input(caller, prompt, result, cmd, generator):
"""
Specifically handle the get_input value to send to _progressive_cmd_run as
part of yielding from a Command's `func`.
Args:
caller (Character, Account or Session): the caller.
prompt (str): The sent prompt.
result (str): The unprocessed answer.
cmd (Command): The command itself.
generator (GeneratorType): The generator.
Returns:
result (bool): Always `False` (stop processing).
"""
# We call it using a Twisted deferLater to make sure the input is properly closed.
deferLater(reactor, 0, _progressive_cmd_run, cmd, generator, response=result)
return False
# custom Exceptions # custom Exceptions
@ -632,19 +640,23 @@ def cmdhandler(
if isinstance(ret, types.GeneratorType): if isinstance(ret, types.GeneratorType):
# cmd.func() is a generator, execute progressively # cmd.func() is a generator, execute progressively
_progressive_cmd_run(cmd, ret) _progressive_cmd_run(cmd, ret)
yield None ret = yield ret
# note that the _progressive_cmd_run will itself run
# the at_post_cmd etc as it finishes; this is a bit of
# code duplication but there seems to be no way to
# catch the StopIteration here (it's not in the same
# frame since this is in a deferred chain)
else: else:
ret = yield ret ret = yield ret
# post-command hook
yield cmd.at_post_cmd()
# post-command hook if cmd.save_for_next:
yield cmd.at_post_cmd() # store a reference to this command, possibly
# accessible by the next command.
if cmd.save_for_next: caller.ndb.last_cmd = yield copy(cmd)
# store a reference to this command, possibly else:
# accessible by the next command. caller.ndb.last_cmd = None
caller.ndb.last_cmd = yield copy(cmd)
else:
caller.ndb.last_cmd = None
# return result to the deferred # return result to the deferred
returnValue(ret) returnValue(ret)

View file

@ -3076,9 +3076,9 @@ class CmdScript(COMMAND_DEFAULT_CLASS):
result.append("No scripts defined on %s." % obj.get_display_name(caller)) result.append("No scripts defined on %s." % obj.get_display_name(caller))
elif not self.switches: elif not self.switches:
# view all scripts # view all scripts
from evennia.commands.default.system import format_script_list from evennia.commands.default.system import ScriptEvMore
ScriptEvMore(self.caller, scripts.order_by("id"), session=self.session)
result.append(format_script_list(scripts)) return
elif "start" in self.switches: elif "start" in self.switches:
num = sum([obj.scripts.start(script.key) for script in scripts]) num = sum([obj.scripts.start(script.key) for script in scripts])
result.append("%s scripts started on %s." % (num, obj.get_display_name(caller))) result.append("%s scripts started on %s." % (num, obj.get_display_name(caller)))
@ -3285,6 +3285,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
spawn/search [prototype_keykey][;tag[,tag]] spawn/search [prototype_keykey][;tag[,tag]]
spawn/list [tag, tag, ...] spawn/list [tag, tag, ...]
spawn/list modules - list only module-based prototypes
spawn/show [<prototype_key>] spawn/show [<prototype_key>]
spawn/update <prototype_key> spawn/update <prototype_key>
@ -3476,16 +3477,11 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
elif query: elif query:
self.caller.msg(f"No prototype named '{query}' was found.") self.caller.msg(f"No prototype named '{query}' was found.")
else: else:
self.caller.msg(f"No prototypes found.") self.caller.msg("No prototypes found.")
def _list_prototypes(self, key=None, tags=None): def _list_prototypes(self, key=None, tags=None):
"""Display prototypes as a list, optionally limited by key/tags. """ """Display prototypes as a list, optionally limited by key/tags. """
table = protlib.list_prototypes(self.caller, key=key, tags=tags) protlib.list_prototypes(self.caller, key=key, tags=tags, session=self.session)
if not table:
return True
EvMore(
self.caller, str(table), exit_on_lastpage=True, justify_kwargs=False,
)
@interactive @interactive
def _update_existing_objects(self, caller, prototype_key, quiet=False): def _update_existing_objects(self, caller, prototype_key, quiet=False):

View file

@ -37,7 +37,10 @@ __all__ = (
"CmdCdesc", "CmdCdesc",
"CmdPage", "CmdPage",
"CmdIRC2Chan", "CmdIRC2Chan",
"CmdIRCStatus",
"CmdRSS2Chan", "CmdRSS2Chan",
"CmdGrapevine2Chan",
) )
_DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH _DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH

View file

@ -16,6 +16,7 @@ import twisted
import time import time
from django.conf import settings from django.conf import settings
from django.core.paginator import Paginator
from evennia.server.sessionhandler import SESSIONS from evennia.server.sessionhandler import SESSIONS
from evennia.scripts.models import ScriptDB from evennia.scripts.models import ScriptDB
from evennia.objects.models import ObjectDB from evennia.objects.models import ObjectDB
@ -408,59 +409,71 @@ class CmdPy(COMMAND_DEFAULT_CLASS):
) )
# helper function. Kept outside so it can be imported and run class ScriptEvMore(EvMore):
# by other commands. """
Listing 1000+ Scripts can be very slow and memory-consuming. So
we use this custom EvMore child to build en EvTable only for
each page of the list.
"""
def format_script_list(scripts): def init_pages(self, scripts):
"""Takes a list of scripts and formats the output.""" """Prepare the script list pagination"""
if not scripts: script_pages = Paginator(scripts, max(1, int(self.height / 2)))
return "<No scripts>" super().init_pages(script_pages)
table = EvTable( def page_formatter(self, scripts):
"|wdbref|n", """Takes a page of scripts and formats the output
"|wobj|n", into an EvTable."""
"|wkey|n",
"|wintval|n",
"|wnext|n",
"|wrept|n",
"|wdb",
"|wtypeclass|n",
"|wdesc|n",
align="r",
border="tablecols",
)
for script in scripts: if not scripts:
return "<No scripts>"
nextrep = script.time_until_next_repeat() table = EvTable(
if nextrep is None: "|wdbref|n",
nextrep = "PAUSED" if script.db._paused_time else "--" "|wobj|n",
else: "|wkey|n",
nextrep = "%ss" % nextrep "|wintval|n",
"|wnext|n",
maxrepeat = script.repeats "|wrept|n",
remaining = script.remaining_repeats() or 0 "|wdb",
if maxrepeat: "|wtypeclass|n",
rept = "%i/%i" % (maxrepeat - remaining, maxrepeat) "|wdesc|n",
else: align="r",
rept = "-/-" border="tablecols",
width=self.width
table.add_row(
script.id,
f"{script.obj.key}({script.obj.dbref})"
if (hasattr(script, "obj") and script.obj)
else "<Global>",
script.key,
script.interval if script.interval > 0 else "--",
nextrep,
rept,
"*" if script.persistent else "-",
script.typeclass_path.rsplit(".", 1)[-1],
crop(script.desc, width=20),
) )
return "%s" % table for script in scripts:
nextrep = script.time_until_next_repeat()
if nextrep is None:
nextrep = "PAUSED" if script.db._paused_time else "--"
else:
nextrep = "%ss" % nextrep
maxrepeat = script.repeats
remaining = script.remaining_repeats() or 0
if maxrepeat:
rept = "%i/%i" % (maxrepeat - remaining, maxrepeat)
else:
rept = "-/-"
table.add_row(
script.id,
f"{script.obj.key}({script.obj.dbref})"
if (hasattr(script, "obj") and script.obj)
else "<Global>",
script.key,
script.interval if script.interval > 0 else "--",
nextrep,
rept,
"*" if script.persistent else "-",
script.typeclass_path.rsplit(".", 1)[-1],
crop(script.desc, width=20),
)
return str(table)
class CmdScripts(COMMAND_DEFAULT_CLASS): class CmdScripts(COMMAND_DEFAULT_CLASS):
@ -549,7 +562,7 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
caller.msg(string) caller.msg(string)
else: else:
# multiple matches. # multiple matches.
EvMore(caller, scripts, page_formatter=format_script_list) ScriptEvMore(caller, scripts, session=self.session)
caller.msg("Multiple script matches. Please refine your search") caller.msg("Multiple script matches. Please refine your search")
elif self.switches and self.switches[0] in ("validate", "valid", "val"): elif self.switches and self.switches[0] in ("validate", "valid", "val"):
# run validation on all found scripts # run validation on all found scripts
@ -559,7 +572,7 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
caller.msg(string) caller.msg(string)
else: else:
# No stopping or validation. We just want to view things. # No stopping or validation. We just want to view things.
EvMore(caller, scripts, page_formatter=format_script_list) ScriptEvMore(caller, scripts.order_by('id'), session=self.session)
class CmdObjects(COMMAND_DEFAULT_CLASS): class CmdObjects(COMMAND_DEFAULT_CLASS):

View file

@ -1159,7 +1159,7 @@ class TestBuilding(CommandTest):
"= Obj", "= Obj",
"To create a global script you need scripts/add <typeclass>.", "To create a global script you need scripts/add <typeclass>.",
) )
self.call(building.CmdScript(), "Obj = ", "dbref obj") self.call(building.CmdScript(), "Obj ", "dbref ")
self.call( self.call(
building.CmdScript(), "/start Obj", "0 scripts started on Obj" building.CmdScript(), "/start Obj", "0 scripts started on Obj"
@ -1252,10 +1252,11 @@ class TestBuilding(CommandTest):
) )
def test_spawn(self): def test_spawn(self):
def getObject(commandTest, objKeyStr):
def get_object(commandTest, obj_key):
# A helper function to get a spawned object and # A helper function to get a spawned object and
# check that it exists in the process. # check that it exists in the process.
query = search_object(objKeyStr) query = search_object(obj_key)
commandTest.assertIsNotNone(query) commandTest.assertIsNotNone(query)
commandTest.assertTrue(bool(query)) commandTest.assertTrue(bool(query))
obj = query[0] obj = query[0]
@ -1284,7 +1285,7 @@ class TestBuilding(CommandTest):
) )
self.call(building.CmdSpawn(), "/search ", "Key ") self.call(building.CmdSpawn(), "/search ", "Key ")
self.call(building.CmdSpawn(), "/search test;test2", "") self.call(building.CmdSpawn(), "/search test;test2", "No prototypes found.")
self.call( self.call(
building.CmdSpawn(), building.CmdSpawn(),
@ -1294,11 +1295,11 @@ class TestBuilding(CommandTest):
) )
self.call(building.CmdSpawn(), "/list", "Key ") self.call(building.CmdSpawn(), "/list", "Key ")
self.call(building.CmdSpawn(), "testprot", "Spawned Test Char") self.call(building.CmdSpawn(), "testprot", "Spawned Test Char")
# Tests that the spawned object's location is the same as the caharacter's location, since
# Tests that the spawned object's location is the same as the character's location, since
# we did not specify it. # we did not specify it.
testchar = getObject(self, "Test Char") testchar = get_object(self, "Test Char")
self.assertEqual(testchar.location, self.char1.location) self.assertEqual(testchar.location, self.char1.location)
testchar.delete() testchar.delete()
@ -1315,7 +1316,7 @@ class TestBuilding(CommandTest):
"'key':'goblin', 'location':'%s'}" % spawnLoc.dbref, "'key':'goblin', 'location':'%s'}" % spawnLoc.dbref,
"Spawned goblin", "Spawned goblin",
) )
goblin = getObject(self, "goblin") goblin = get_object(self, "goblin")
# Tests that the spawned object's type is a DefaultCharacter. # Tests that the spawned object's type is a DefaultCharacter.
self.assertIsInstance(goblin, DefaultCharacter) self.assertIsInstance(goblin, DefaultCharacter)
self.assertEqual(goblin.location, spawnLoc) self.assertEqual(goblin.location, spawnLoc)
@ -1334,7 +1335,7 @@ class TestBuilding(CommandTest):
# Tests "spawn <prototype_name>" # Tests "spawn <prototype_name>"
self.call(building.CmdSpawn(), "testball", "Spawned Ball") self.call(building.CmdSpawn(), "testball", "Spawned Ball")
ball = getObject(self, "Ball") ball = get_object(self, "Ball")
self.assertEqual(ball.location, self.char1.location) self.assertEqual(ball.location, self.char1.location)
self.assertIsInstance(ball, DefaultObject) self.assertIsInstance(ball, DefaultObject)
ball.delete() ball.delete()
@ -1344,7 +1345,7 @@ class TestBuilding(CommandTest):
self.call( self.call(
building.CmdSpawn(), "/n 'BALL'", "Spawned Ball" building.CmdSpawn(), "/n 'BALL'", "Spawned Ball"
) # /n switch is abbreviated form of /noloc ) # /n switch is abbreviated form of /noloc
ball = getObject(self, "Ball") ball = get_object(self, "Ball")
self.assertIsNone(ball.location) self.assertIsNone(ball.location)
ball.delete() ball.delete()
@ -1363,7 +1364,7 @@ class TestBuilding(CommandTest):
% spawnLoc.dbref, % spawnLoc.dbref,
"Spawned Ball", "Spawned Ball",
) )
ball = getObject(self, "Ball") ball = get_object(self, "Ball")
self.assertEqual(ball.location, spawnLoc) self.assertEqual(ball.location, spawnLoc)
ball.delete() ball.delete()

View file

@ -120,7 +120,7 @@ class GenderCharacter(DefaultCharacter):
pronoun = _GENDER_PRONOUN_MAP[gender][typ.lower()] pronoun = _GENDER_PRONOUN_MAP[gender][typ.lower()]
return pronoun.capitalize() if typ.isupper() else pronoun return pronoun.capitalize() if typ.isupper() else pronoun
def msg(self, text, from_obj=None, session=None, **kwargs): def msg(self, text=None, from_obj=None, session=None, **kwargs):
""" """
Emits something to a session attached to the object. Emits something to a session attached to the object.
Overloads the default msg() implementation to include Overloads the default msg() implementation to include
@ -141,6 +141,10 @@ class GenderCharacter(DefaultCharacter):
All extra kwargs will be passed on to the protocol. All extra kwargs will be passed on to the protocol.
""" """
if text is None:
super().msg(from_obj=from_obj, session=session, **kwargs)
return
try: try:
if text and isinstance(text, tuple): if text and isinstance(text, tuple):
text = (_RE_GENDER_PRONOUN.sub(self._get_pronoun, text[0]), *text[1:]) text = (_RE_GENDER_PRONOUN.sub(self._get_pronoun, text[0]), *text[1:])

View file

@ -872,20 +872,17 @@ class TestCustomGameTime(EvenniaTest):
# Test dice module # Test dice module
from evennia.contrib import dice # noqa
@patch("random.randint", return_value=5)
@patch("evennia.contrib.dice.randint", return_value=5)
class TestDice(CommandTest): class TestDice(CommandTest):
def test_roll_dice(self, mocked_randint): def test_roll_dice(self, mocked_randint):
# we must import dice here for the mocked randint to apply correctly.
from evennia.contrib import dice
self.assertEqual(dice.roll_dice(6, 6, modifier=("+", 4)), mocked_randint() * 6 + 4) self.assertEqual(dice.roll_dice(6, 6, modifier=("+", 4)), mocked_randint() * 6 + 4)
self.assertEqual(dice.roll_dice(6, 6, conditional=("<", 35)), True) self.assertEqual(dice.roll_dice(6, 6, conditional=("<", 35)), True)
self.assertEqual(dice.roll_dice(6, 6, conditional=(">", 33)), False) self.assertEqual(dice.roll_dice(6, 6, conditional=(">", 33)), False)
def test_cmddice(self, mocked_randint): def test_cmddice(self, mocked_randint):
from evennia.contrib import dice
self.call( self.call(
dice.CmdDice(), "3d6 + 4", "You roll 3d6 + 4.| Roll(s): 5, 5 and 5. Total result is 19." dice.CmdDice(), "3d6 + 4", "You roll 3d6 + 4.| Roll(s): 5, 5 and 5. Total result is 19."
) )
@ -896,7 +893,7 @@ class TestDice(CommandTest):
# Test email-login # Test email-login
from evennia.contrib import email_login from evennia.contrib import email_login # noqa
class TestEmailLogin(CommandTest): class TestEmailLogin(CommandTest):

View file

@ -39,7 +39,7 @@
# #
# HEADER #HEADER
# everything in this block will be appended to the beginning of # everything in this block will be appended to the beginning of
# all other #CODE blocks when they are executed. # all other #CODE blocks when they are executed.
@ -51,7 +51,7 @@ from evennia import DefaultObject
limbo = search_object("Limbo")[0] limbo = search_object("Limbo")[0]
# CODE #CODE
# This is the first code block. Within each block, Python # This is the first code block. Within each block, Python
# code works as normal. Note how we make use if imports and # code works as normal. Note how we make use if imports and
@ -67,7 +67,7 @@ red_button = create_object(
# we take a look at what we created # we take a look at what we created
caller.msg("A %s was created." % red_button.key) caller.msg("A %s was created." % red_button.key)
# CODE #CODE
# this code block has 'table' and 'chair' set as deletable # this code block has 'table' and 'chair' set as deletable
# objects. This means that when the batchcode processor runs in # objects. This means that when the batchcode processor runs in

View file

@ -20,11 +20,11 @@ needed on the Evennia side.
MSSPTable = { MSSPTable = {
# Required fields # Required fields
"NAME": "Evennia", "NAME": "Mygame", # usually the same as SERVERNAME
# Generic # Generic
"CRAWL DELAY": "-1", # limit how often crawler updates the listing. -1 for no limit "CRAWL DELAY": "-1", # limit how often crawler may update the listing. -1 for no limit
"HOSTNAME": "", # current or new hostname "HOSTNAME": "", # telnet hostname
"PORT": ["4000"], # most important port should be *last* in list! "PORT": ["4000"], # telnet port - most important port should be *last* in list!
"CODEBASE": "Evennia", "CODEBASE": "Evennia",
"CONTACT": "", # email for contacting the mud "CONTACT": "", # email for contacting the mud
"CREATED": "", # year MUD was created "CREATED": "", # year MUD was created
@ -33,7 +33,7 @@ MSSPTable = {
"LANGUAGE": "", # name of language used, e.g. English "LANGUAGE": "", # name of language used, e.g. English
"LOCATION": "", # full English name of server country "LOCATION": "", # full English name of server country
"MINIMUM AGE": "0", # set to 0 if not applicable "MINIMUM AGE": "0", # set to 0 if not applicable
"WEBSITE": "www.evennia.com", "WEBSITE": "", # http:// address to your game website
# Categorisation # Categorisation
"FAMILY": "Custom", # evennia goes under 'Custom' "FAMILY": "Custom", # evennia goes under 'Custom'
"GENRE": "None", # Adult, Fantasy, Historical, Horror, Modern, None, or Science Fiction "GENRE": "None", # Adult, Fantasy, Historical, Horror, Modern, None, or Science Fiction
@ -41,10 +41,10 @@ MSSPTable = {
# Player versus Player, Player versus Environment, # Player versus Player, Player versus Environment,
# Roleplaying, Simulation, Social or Strategy # Roleplaying, Simulation, Social or Strategy
"GAMEPLAY": "", "GAMEPLAY": "",
"STATUS": "Open Beta", # Alpha, Closed Beta, Open Beta, Live "STATUS": "Open Beta", # Allowed: Alpha, Closed Beta, Open Beta, Live
"GAMESYSTEM": "Custom", # D&D, d20 System, World of Darkness, etc. Use Custom if homebrew "GAMESYSTEM": "Custom", # D&D, d20 System, World of Darkness, etc. Use Custom if homebrew
# Subgenre: LASG, Medieval Fantasy, World War II, Frankenstein, # Subgenre: LASG, Medieval Fantasy, World War II, Frankenstein,
# Cyberpunk, Dragonlance, etc. Or None if not available. # Cyberpunk, Dragonlance, etc. Or None if not applicable.
"SUBGENRE": "None", "SUBGENRE": "None",
# World # World
"AREAS": "0", "AREAS": "0",
@ -56,7 +56,7 @@ MSSPTable = {
"LEVELS": "0", # use 0 if level-less "LEVELS": "0", # use 0 if level-less
"RACES": "0", # use 0 if race-less "RACES": "0", # use 0 if race-less
"SKILLS": "0", # use 0 if skill-less "SKILLS": "0", # use 0 if skill-less
# Protocols set to 1 or 0) # Protocols set to 1 or 0; should usually not be changed)
"ANSI": "1", "ANSI": "1",
"GMCP": "1", "GMCP": "1",
"MSDP": "1", "MSDP": "1",

View file

@ -2,40 +2,56 @@
Prototypes Prototypes
A prototype is a simple way to create individualized instances of a A prototype is a simple way to create individualized instances of a
given `Typeclass`. For example, you might have a Sword typeclass that given typeclass. It is dictionary with specific key names.
implements everything a Sword would need to do. The only difference
between different individual Swords would be their key, description
and some Attributes. The Prototype system allows to create a range of
such Swords with only minor variations. Prototypes can also inherit
and combine together to form entire hierarchies (such as giving all
Sabres and all Broadswords some common properties). Note that bigger
variations, such as custom commands or functionality belong in a
hierarchy of typeclasses instead.
Example prototypes are read by the `@spawn` command but is also easily For example, you might have a Sword typeclass that implements everything a
available to use from code via `evennia.spawn` or `evennia.utils.spawner`. Sword would need to do. The only difference between different individual Swords
Each prototype should be a dictionary. Use the same name as the would be their key, description and some Attributes. The Prototype system
variable to refer to other prototypes. allows to create a range of such Swords with only minor variations. Prototypes
can also inherit and combine together to form entire hierarchies (such as
giving all Sabres and all Broadswords some common properties). Note that bigger
variations, such as custom commands or functionality belong in a hierarchy of
typeclasses instead.
A prototype can either be a dictionary placed into a global variable in a
python module (a 'module-prototype') or stored in the database as a dict on a
special Script (a db-prototype). The former can be created just by adding dicts
to modules Evennia looks at for prototypes, the latter is easiest created
in-game via the `olc` command/menu.
Prototypes are read and used to create new objects with the `spawn` command
or directly via `evennia.spawn` or the full path `evennia.prototypes.spawner.spawn`.
A prototype dictionary have the following keywords:
Possible keywords are: Possible keywords are:
prototype_parent - string pointing to parent prototype of this structure. - `prototype_key` - the name of the prototype. This is required for db-prototypes,
key - string, the main object identifier. for module-prototypes, the global variable name of the dict is used instead
typeclass - string, if not set, will use `settings.BASE_OBJECT_TYPECLASS`. - `prototype_parent` - string pointing to parent prototype if any. Prototype inherits
location - this should be a valid object or #dbref. in a similar way as classes, with children overriding values in their partents.
home - valid object or #dbref. - `key` - string, the main object identifier.
destination - only valid for exits (object or dbref). - `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 to use for the spawned object.
- `aliases` - string or list of strings.
- `attrs` - Attributes, expressed as a list of tuples on the form `(attrname, value)`,
`(attrname, value, category)`, or `(attrname, value, category, locks)`. If using one
of the shorter forms, defaults are used for the rest.
- `tags` - Tags, as a list of tuples `(tag,)`, `(tag, category)` or `(tag, category, data)`.
- Any other keywords are interpreted as Attributes with no category or lock.
These will internally be added to `attrs` (eqivalent to `(attrname, value)`.
permissions - string or list of permission strings. See the `spawn` command and `evennia.prototypes.spawner.spawn` for more info.
locks - a lock-string.
aliases - string or list of strings.
ndb_<name> - value of a nattribute (the "ndb_" part is ignored).
any other keywords are interpreted as Attributes and their values.
See the `@spawn` command and `evennia.utils.spawner` for more info.
""" """
## example of module-based prototypes using
## the variable name as `prototype_key` and
## simple Attributes
# from random import randint # from random import randint
# #
# GOBLIN = { # GOBLIN = {
@ -43,7 +59,8 @@ See the `@spawn` command and `evennia.utils.spawner` for more info.
# "health": lambda: randint(20,30), # "health": lambda: randint(20,30),
# "resists": ["cold", "poison"], # "resists": ["cold", "poison"],
# "attacks": ["fists"], # "attacks": ["fists"],
# "weaknesses": ["fire", "light"] # "weaknesses": ["fire", "light"],
# "tags": = [("greenskin", "monster"), ("humanoid", "monster")]
# } # }
# #
# GOBLIN_WIZARD = { # GOBLIN_WIZARD = {

View file

@ -343,22 +343,23 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
singular (str): The singular form to display. singular (str): The singular form to display.
plural (str): The determined plural form of the key, including the count. plural (str): The determined plural form of the key, including the count.
""" """
plural_category = "plural_key"
key = kwargs.get("key", self.key) key = kwargs.get("key", self.key)
key = ansi.ANSIString(key) # this is needed to allow inflection of colored names key = ansi.ANSIString(key) # this is needed to allow inflection of colored names
try: try:
plural = _INFLECT.plural(key, 2) plural = _INFLECT.plural(key, count)
plural = "%s %s" % (_INFLECT.number_to_words(count, threshold=12), plural) plural = "{} {}".format(_INFLECT.number_to_words(count, threshold=12), plural)
except IndexError: except IndexError:
# this is raised by inflect if the input is not a proper noun # this is raised by inflect if the input is not a proper noun
plural = key plural = key
singular = _INFLECT.an(key) singular = _INFLECT.an(key)
if not self.aliases.get(plural, category="plural_key"): if not self.aliases.get(plural, category=plural_category):
# we need to wipe any old plurals/an/a in case key changed in the interrim # we need to wipe any old plurals/an/a in case key changed in the interrim
self.aliases.clear(category="plural_key") self.aliases.clear(category=plural_category)
self.aliases.add(plural, category="plural_key") self.aliases.add(plural, category=plural_category)
# save the singular form as an alias here too so we can display "an egg" and also # save the singular form as an alias here too so we can display "an egg" and also
# look at 'an egg'. # look at 'an egg'.
self.aliases.add(singular, category="plural_key") self.aliases.add(singular, category=plural_category)
return singular, plural return singular, plural
def search( def search(

View file

@ -9,9 +9,13 @@ import hashlib
import time import time
from ast import literal_eval from ast import literal_eval
from django.conf import settings from django.conf import settings
from django.db.models import Q, Subquery
from django.core.paginator import Paginator
from evennia.scripts.scripts import DefaultScript from evennia.scripts.scripts import DefaultScript
from evennia.objects.models import ObjectDB from evennia.objects.models import ObjectDB
from evennia.typeclasses.attributes import Attribute
from evennia.utils.create import create_script from evennia.utils.create import create_script
from evennia.utils.evmore import EvMore
from evennia.utils.utils import ( from evennia.utils.utils import (
all_from_module, all_from_module,
make_iter, make_iter,
@ -163,7 +167,8 @@ for mod in settings.PROTOTYPE_MODULES:
if "prototype_locks" in prot if "prototype_locks" in prot
else "use:all();edit:false()" else "use:all();edit:false()"
), ),
"prototype_tags": list(set(make_iter(prot.get("prototype_tags", [])) + ["module"])), "prototype_tags": list(set(list(
make_iter(prot.get("prototype_tags", []))) + ["module"])),
} }
) )
_MODULE_PROTOTYPES[actual_prot_key] = prot _MODULE_PROTOTYPES[actual_prot_key] = prot
@ -320,7 +325,7 @@ def delete_prototype(prototype_key, caller=None):
return True return True
def search_prototype(key=None, tags=None, require_single=False): def search_prototype(key=None, tags=None, require_single=False, return_iterators=False):
""" """
Find prototypes based on key and/or tags, or all prototypes. Find prototypes based on key and/or tags, or all prototypes.
@ -331,11 +336,17 @@ def search_prototype(key=None, tags=None, require_single=False):
tag category. tag category.
require_single (bool): If set, raise KeyError if the result require_single (bool): If set, raise KeyError if the result
was not found or if there are multiple matches. was not found or if there are multiple matches.
return_iterators (bool): Optimized return for large numbers of db-prototypes.
If set, separate returns of module based prototypes and paginate
the db-prototype return.
Return: Return:
matches (list): All found prototype dicts. Empty list if matches (list): Default return, all found prototype dicts. Empty list if
no match was found. Note that if neither `key` nor `tags` no match was found. Note that if neither `key` nor `tags`
were given, *all* available prototypes will be returned. were given, *all* available prototypes will be returned.
list, queryset: If `return_iterators` are found, this is a list of
module-based prototypes followed by a *paginated* queryset of
db-prototypes.
Raises: Raises:
KeyError: If `require_single` is True and there are 0 or >1 matches. KeyError: If `require_single` is True and there are 0 or >1 matches.
@ -381,33 +392,51 @@ def search_prototype(key=None, tags=None, require_single=False):
# exact match on tag(s) # exact match on tag(s)
tags = make_iter(tags) tags = make_iter(tags)
tag_categories = ["db_prototype" for _ in tags] tag_categories = ["db_prototype" for _ in tags]
db_matches = DbPrototype.objects.get_by_tag(tags, tag_categories) db_matches = DbPrototype.objects.get_by_tag(
tags, tag_categories)
else: else:
db_matches = DbPrototype.objects.all().order_by("id") db_matches = DbPrototype.objects.all()
if key: if key:
# exact or partial match on key # exact or partial match on key
db_matches = ( exact_match = (
db_matches.filter(db_key=key) or db_matches.filter(db_key__icontains=key) db_matches
).order_by("id") .filter(
# return prototype Q(db_key__iexact=key))
db_prototypes = [dbprot.prototype for dbprot in db_matches] .order_by("db_key")
)
if not exact_match:
# try with partial match instead
db_matches = (
db_matches
.filter(
Q(db_key__icontains=key))
.order_by("db_key")
)
else:
db_matches = exact_match
matches = db_prototypes + module_prototypes # convert to prototype
nmatches = len(matches) db_ids = db_matches.values_list("id", flat=True)
if nmatches > 1 and key: db_matches = (
key = key.lower() Attribute.objects
# avoid duplicates if an exact match exist between the two types .filter(scriptdb__pk__in=db_ids, db_key="prototype")
filter_matches = [ .values_list("db_value", flat=True)
mta for mta in matches if mta.get("prototype_key") and mta["prototype_key"] == key .order_by("scriptdb__db_key")
] )
if filter_matches and len(filter_matches) < nmatches: if key and require_single:
matches = filter_matches nmodules = len(module_prototypes)
ndbprots = db_matches.count()
if nmodules + ndbprots != 1:
raise KeyError(f"Found {nmodules + ndbprots} matching prototypes.")
nmatches = len(matches) if return_iterators:
if nmatches != 1 and require_single: # trying to get the entire set of prototypes - we must paginate
raise KeyError("Found {} matching prototypes.".format(nmatches)) # the result instead of trying to fetch the entire set at once
return db_matches, module_prototypes
return matches else:
# full fetch, no pagination (compatibility mode)
return list(db_matches) + module_prototypes
def search_objects_with_prototype(prototype_key): def search_objects_with_prototype(prototype_key):
@ -424,7 +453,109 @@ def search_objects_with_prototype(prototype_key):
return ObjectDB.objects.get_by_tag(key=prototype_key, category=PROTOTYPE_TAG_CATEGORY) return ObjectDB.objects.get_by_tag(key=prototype_key, category=PROTOTYPE_TAG_CATEGORY)
def list_prototypes(caller, key=None, tags=None, show_non_use=False, show_non_edit=True): class PrototypeEvMore(EvMore):
"""
Listing 1000+ prototypes can be very slow. So we customize EvMore to
display an EvTable per paginated page rather than to try creating an
EvTable for the entire dataset and then paginate it.
"""
def __init__(self, caller, *args, session=None, **kwargs):
"""Store some extra properties on the EvMore class"""
self.show_non_use = kwargs.pop("show_non_use", False)
self.show_non_edit = kwargs.pop("show_non_edit", False)
super().__init__(caller, *args, session=session, **kwargs)
def init_pages(self, inp):
"""
This will be initialized with a tuple (mod_prototype_list, paginated_db_query)
and we must handle these separately since they cannot be paginated in the same
way. We will build the prototypes so that the db-prototypes come first (they
are likely the most volatile), followed by the mod-prototypes.
"""
dbprot_query, modprot_list = inp
# set the number of entries per page to half the reported height of the screen
# to account for long descs etc
dbprot_paged = Paginator(dbprot_query, max(1, int(self.height / 2)))
# we separate the different types of data, so we track how many pages there are
# of each.
n_mod = len(modprot_list)
self._npages_mod = n_mod // self.height + (0 if n_mod % self.height == 0 else 1)
self._db_count = dbprot_paged.count
self._npages_db = dbprot_paged.num_pages if self._db_count > 0 else 0
# total number of pages
self._npages = self._npages_mod + self._npages_db
self._data = (dbprot_paged, modprot_list)
self._paginator = self.prototype_paginator
def prototype_paginator(self, pageno):
"""
The listing is separated in db/mod prototypes, so we need to figure out which
one to pick based on the page number. Also, pageno starts from 0.
"""
dbprot_pages, modprot_list = self._data
if self._db_count and pageno < self._npages_db:
return dbprot_pages.page(pageno + 1)
else:
# get the correct slice, adjusted for the db-prototypes
pageno = max(0, pageno - self._npages_db)
return modprot_list[pageno * self.height: pageno * self.height + self.height]
def page_formatter(self, page):
"""Input is a queryset page from django.Paginator"""
caller = self._caller
# get use-permissions of readonly attributes (edit is always False)
display_tuples = []
table = EvTable(
"|wKey|n",
"|wSpawn/Edit|n",
"|wTags|n",
"|wDesc|n",
border="tablecols",
crop=True,
width=self.width
)
for prototype in page:
lock_use = caller.locks.check_lockstring(
caller, prototype.get("prototype_locks", ""), access_type="spawn", default=True
)
if not self.show_non_use and not lock_use:
continue
if prototype.get("prototype_key", "") in _MODULE_PROTOTYPES:
lock_edit = False
else:
lock_edit = caller.locks.check_lockstring(
caller, prototype.get("prototype_locks", ""), access_type="edit", default=True
)
if not self.show_non_edit and not lock_edit:
continue
ptags = []
for ptag in prototype.get("prototype_tags", []):
if is_iter(ptag):
if len(ptag) > 1:
ptags.append("{}".format(ptag[0]))
else:
ptags.append(ptag[0])
else:
ptags.append(str(ptag))
table.add_row(
prototype.get("prototype_key", "<unset>"),
"{}/{}".format("Y" if lock_use else "N", "Y" if lock_edit else "N"),
", ".join(list(set(ptags))),
prototype.get("prototype_desc", "<unset>"),
)
return str(table)
def list_prototypes(caller, key=None, tags=None, show_non_use=False,
show_non_edit=True, session=None):
""" """
Collate a list of found prototypes based on search criteria and access. Collate a list of found prototypes based on search criteria and access.
@ -434,66 +565,26 @@ def list_prototypes(caller, key=None, tags=None, show_non_use=False, show_non_ed
tags (str or list, optional): Tag key or keys to query for. tags (str or list, optional): Tag key or keys to query for.
show_non_use (bool, optional): Show also prototypes the caller may not use. show_non_use (bool, optional): Show also prototypes the caller may not use.
show_non_edit (bool, optional): Show also prototypes the caller may not edit. show_non_edit (bool, optional): Show also prototypes the caller may not edit.
session (Session, optional): If given, this is used for display formatting.
Returns: Returns:
table (EvTable or None): An EvTable representation of the prototypes. None PrototypeEvMore: An EvMore subclass optimized for prototype listings.
if no prototypes were found. None: If no matches were found. In this case the caller has already been notified.
""" """
# this allows us to pass lists of empty strings # this allows us to pass lists of empty strings
tags = [tag for tag in make_iter(tags) if tag] tags = [tag for tag in make_iter(tags) if tag]
# get prototypes for readonly and db-based prototypes dbprot_query, modprot_list = search_prototype(key, tags, return_iterators=True)
prototypes = search_prototype(key, tags)
# get use-permissions of readonly attributes (edit is always False) if not dbprot_query and not modprot_list:
display_tuples = [] caller.msg("No prototypes found.", session=session)
for prototype in sorted(prototypes, key=lambda d: d.get("prototype_key", "")): return None
lock_use = caller.locks.check_lockstring(
caller, prototype.get("prototype_locks", ""), access_type="spawn", default=True
)
if not show_non_use and not lock_use:
continue
if prototype.get("prototype_key", "") in _MODULE_PROTOTYPES:
lock_edit = False
else:
lock_edit = caller.locks.check_lockstring(
caller, prototype.get("prototype_locks", ""), access_type="edit", default=True
)
if not show_non_edit and not lock_edit:
continue
ptags = []
for ptag in prototype.get("prototype_tags", []):
if is_iter(ptag):
if len(ptag) > 1:
ptags.append("{} (category: {}".format(ptag[0], ptag[1]))
else:
ptags.append(ptag[0])
else:
ptags.append(str(ptag))
display_tuples.append(
(
prototype.get("prototype_key", "<unset>"),
prototype.get("prototype_desc", "<unset>"),
"{}/{}".format("Y" if lock_use else "N", "Y" if lock_edit else "N"),
",".join(ptags),
)
)
if not display_tuples:
return ""
table = []
width = 78
for i in range(len(display_tuples[0])):
table.append([str(display_tuple[i]) for display_tuple in display_tuples])
table = EvTable("Key", "Desc", "Spawn/Edit", "Tags", table=table, crop=True, width=width)
table.reformat_column(0, width=22)
table.reformat_column(1, width=29)
table.reformat_column(2, width=11, align="c")
table.reformat_column(3, width=16)
return table
# get specific prototype (one value or exception)
return PrototypeEvMore(caller, (dbprot_query, modprot_list),
session=session,
show_non_use=show_non_use,
show_non_edit=show_non_edit)
def validate_prototype( def validate_prototype(
prototype, protkey=None, protparents=None, is_prototype_base=True, strict=True, _flags=None prototype, protkey=None, protparents=None, is_prototype_base=True, strict=True, _flags=None
@ -569,7 +660,7 @@ def validate_prototype(
protparent = protparents.get(protstring) protparent = protparents.get(protstring)
if not protparent: if not protparent:
_flags["errors"].append( _flags["errors"].append(
"Prototype {}'s prototype_parent '{}' was not found.".format((protkey, protstring)) "Prototype {}'s prototype_parent '{}' was not found.".format(protkey, protstring)
) )
if id(prototype) in _flags["visited"]: if id(prototype) in _flags["visited"]:
_flags["errors"].append( _flags["errors"].append(

View file

@ -3,8 +3,10 @@ Unit tests for the prototypes and spawner
""" """
from random import randint from random import randint, sample
import mock import mock
import uuid
from time import time
from anything import Something from anything import Something
from django.test.utils import override_settings from django.test.utils import override_settings
from evennia.utils.test_resources import EvenniaTest from evennia.utils.test_resources import EvenniaTest
@ -628,8 +630,10 @@ class TestPrototypeStorage(EvenniaTest):
# partial match # partial match
with mock.patch("evennia.prototypes.prototypes._MODULE_PROTOTYPES", {}): with mock.patch("evennia.prototypes.prototypes._MODULE_PROTOTYPES", {}):
self.assertEqual(list(protlib.search_prototype("prot")), [prot1b, prot2, prot3]) self.assertCountEqual(
self.assertEqual(list(protlib.search_prototype(tags="foo1")), [prot1b, prot2, prot3]) protlib.search_prototype("prot"), [prot1b, prot2, prot3])
self.assertCountEqual(
protlib.search_prototype(tags="foo1"), [prot1b, prot2, prot3])
self.assertTrue(str(str(protlib.list_prototypes(self.char1)))) self.assertTrue(str(str(protlib.list_prototypes(self.char1))))
@ -1073,3 +1077,29 @@ class TestOLCMenu(TestEvMenu):
["node_index", "node_index", "node_index"], ["node_index", "node_index", "node_index"],
], ],
] ]
class PrototypeCrashTest(EvenniaTest):
# increase this to 1000 for optimization testing
num_prototypes = 10
def create(self, num=None):
if not num:
num = self.num_prototypes
# print(f"Creating {num} additional prototypes...")
for x in range(num):
prot = {
'prototype_key': str(uuid.uuid4()),
'some_attributes': [str(uuid.uuid4()) for x in range(10)],
'prototype_tags': list(sample(['demo', 'test', 'stuff'], 2)),
}
protlib.save_prototype(prot)
def test_prototype_dos(self, *args, **kwargs):
num_prototypes = self.num_prototypes
for x in range(2):
self.create(num_prototypes)
# print("Attempting to list prototypes...")
# start_time = time()
self.char1.execute_cmd('spawn/list')
# print(f"Prototypes listed in {time()-start_time} seconds.")

View file

@ -437,7 +437,7 @@ class DefaultScript(ScriptBase):
if self.is_active and not force_restart: if self.is_active and not force_restart:
# The script is already running, but make sure we have a _task if # The script is already running, but make sure we have a _task if
# this is after a cache flush # this is after a cache flush
if not self.ndb._task and self.db_interval >= 0: if not self.ndb._task and self.db_interval > 0:
self.ndb._task = ExtendedLoopingCall(self._step_task) self.ndb._task = ExtendedLoopingCall(self._step_task)
try: try:
start_delay, callcount = SCRIPT_FLUSH_TIMERS[self.id] start_delay, callcount = SCRIPT_FLUSH_TIMERS[self.id]

View file

@ -44,7 +44,7 @@ class ConnectionWizard(object):
resp = str(default) resp = str(default)
if resp.lower() in options: if resp.lower() in options:
self.display(f" Selected '{resp}'.") # self.display(f" Selected '{resp}'.")
desc, callback, kwargs = options[resp.lower()] desc, callback, kwargs = options[resp.lower()]
callback(self, **kwargs) callback(self, **kwargs)
elif resp.lower() in ("quit", "q"): elif resp.lower() in ("quit", "q"):
@ -161,8 +161,10 @@ class ConnectionWizard(object):
def node_start(wizard): def node_start(wizard):
text = """ text = """
This wizard helps activate external networks with Evennia. It will create This wizard helps to attach your Evennia server to external networks. It
a config that will be attached to the bottom of the game settings file. will save to a file `server/conf/connection_settings.py` that will be
imported from the bottom of your game settings file. Once generated you can
also modify that file directly.
Make sure you have at least started the game once before continuing! Make sure you have at least started the game once before continuing!
@ -174,11 +176,18 @@ def node_start(wizard):
node_game_index_start, node_game_index_start,
{}, {},
), ),
# "2": ("Add MSSP information (for mud-list crawlers)", "2": ("MSSP setup (for mud-list crawlers)",
# node_mssp_start, {}), node_mssp_start, {}
),
# "3": ("Add Grapevine listing", # "3": ("Add Grapevine listing",
# node_grapevine_start, {}), # node_grapevine_start, {}),
"2": ("View and Save created settings", node_view_and_apply_settings, {}), # "4": ("Add IRC link",
# "node_irc_start", {}),
# "5" ("Add RSS feed",
# "node_rss_start", {}),
"s": ("View and (optionally) Save created settings",
node_view_and_apply_settings, {}),
"q": ("Quit", lambda *args: sys.exit(), {}),
} }
wizard.display(text) wizard.display(text)
@ -189,13 +198,13 @@ def node_start(wizard):
def node_game_index_start(wizard, **kwargs): def node_game_index_start(wizard, **kwargs):
text = f""" text = """
The Evennia game index (http://games.evennia.com) lists both active Evennia The Evennia game index (http://games.evennia.com) lists both active Evennia
games as well as games in various stages of development. games as well as games in various stages of development.
You can put up your game in the index also if you are not (yet) open for You can put up your game in the index also if you are not (yet) open for
players. If so, put 'None' for the connection details. Just tell us you players. If so, put 'None' for the connection details - you are just telling
are out there and make us excited about your upcoming game! us that you are out there, making us excited about your upcoming game!
Please check the listing online first to see that your exact game name is Please check the listing online first to see that your exact game name is
not colliding with an existing game-name in the list (be nice!). not colliding with an existing game-name in the list (be nice!).
@ -222,9 +231,9 @@ def node_game_index_fields(wizard, status=None):
- pre-alpha: a game in its very early stages, mostly unfinished or unstarted - pre-alpha: a game in its very early stages, mostly unfinished or unstarted
- alpha: a working concept, probably lots of bugs and incomplete features - alpha: a working concept, probably lots of bugs and incomplete features
- beta: a working game, but expect bugs and changing features - beta: a working game, but expect bugs and changing features
- launched: a full, working game that may still be expanded upon and improved later - launched: a full, working game (that may still be expanded upon and improved later)
Current value: Current value (return to keep):
{status_default} {status_default}
""" """
@ -233,6 +242,31 @@ def node_game_index_fields(wizard, status=None):
wizard.display(text) wizard.display(text)
wizard.game_index_listing["game_status"] = wizard.ask_choice("Select one: ", options) wizard.game_index_listing["game_status"] = wizard.ask_choice("Select one: ", options)
# game name
name_default = settings.SERVERNAME
text = f"""
Your game's name should usually be the same as `settings.SERVERNAME`, but
you can set it to something else here if you want.
Current value:
{name_default}
"""
def name_validator(inp):
tmax = 80
tlen = len(inp)
if tlen > tmax:
print(f"The name must be shorter than {tmax} characters (was {tlen}).")
wizard.ask_continue()
return False
return True
wizard.display(text)
wizard.game_index_listing['game_name'] = wizard.ask_input(
default=name_default, validator=name_validator
)
# short desc # short desc
sdesc_default = wizard.game_index_listing.get("short_description", None) sdesc_default = wizard.game_index_listing.get("short_description", None)
@ -249,7 +283,7 @@ def node_game_index_fields(wizard, status=None):
def sdesc_validator(inp): def sdesc_validator(inp):
tmax = 255 tmax = 255
tlen = len(inp) tlen = len(inp)
if tlen > 255: if tlen > tmax:
print(f"The short desc must be shorter than {tmax} characters (was {tlen}).") print(f"The short desc must be shorter than {tmax} characters (was {tlen}).")
wizard.ask_continue() wizard.ask_continue()
return False return False
@ -341,7 +375,7 @@ def node_game_index_fields(wizard, status=None):
Evennia is its own web server and runs your game's website. Enter the Evennia is its own web server and runs your game's website. Enter the
URL of the website here, like http://yourwebsite.com, here. URL of the website here, like http://yourwebsite.com, here.
Wtite 'None' if you are not offering a publicly visible website at this time. Write 'None' if you are not offering a publicly visible website at this time.
Current value: Current value:
{website_default} {website_default}
@ -359,7 +393,7 @@ def node_game_index_fields(wizard, status=None):
your specific URL here (when clicking this link you should launch into the your specific URL here (when clicking this link you should launch into the
web client) web client)
Wtite 'None' if you don't want to list a publicly accessible webclient. Write 'None' if you don't want to list a publicly accessible webclient.
Current value: Current value:
{webclient_default} {webclient_default}
@ -388,24 +422,26 @@ def node_game_index_fields(wizard, status=None):
def node_mssp_start(wizard): def node_mssp_start(wizard):
mssp_module = mod_import(settings.MSSP_META_MODULE) mssp_module = mod_import(settings.MSSP_META_MODULE or "server.conf.mssp")
filename = mssp_module.__file__ try:
filename = mssp_module.__file__
except AttributeError:
filename = "server/conf/mssp.py"
text = f""" text = f"""
MSSP (Mud Server Status Protocol) allows online MUD-listing sites/crawlers MSSP (Mud Server Status Protocol) has a vast amount of options so it must
to continuously monitor your game and list information about it. Some of be modified outside this wizard by directly editing its config file here:
this, like active player-count, Evennia will automatically add for you,
whereas many fields are manually added info about your game. '{filename}'
MSSP allows traditional online MUD-listing sites/crawlers to continuously
monitor your game and list information about it. Some of this, like active
player-count, Evennia will automatically add for you, whereas most fields
you need to set manually.
To use MSSP you should generally have a publicly open game that external To use MSSP you should generally have a publicly open game that external
players can connect to. You also need to register at a MUD listing site to players can connect to. You also need to register at a MUD listing site to
tell them to list your game. tell them to crawl your game.
MSSP has a large number of configuration options and we found it was simply
a lot easier to set them in a file rather than using this wizard. So to
configure MSSP, edit the empty template listing found here:
'{filename}'
""" """
wizard.display(text) wizard.display(text)
@ -456,25 +492,31 @@ def node_view_and_apply_settings(wizard):
pp = pprint.PrettyPrinter(indent=4) pp = pprint.PrettyPrinter(indent=4)
saves = False saves = False
game_index_txt = "No changes to save for Game Index." # game index
if hasattr(wizard, "game_index_listing"): game_index_save_text = ""
if wizard.game_index_listing != settings.GAME_INDEX_LISTING: game_index_listing = (wizard.game_index_listing if
game_index_txt = "No changes to save for Game Index." hasattr(wizard, "game_index_listing") else None)
else: if not game_index_listing and settings.GAME_INDEX_ENABLED:
game_index_txt = "GAME_INDEX_ENABLED = True\n" "GAME_INDEX_LISTING = \\\n" + pp.pformat( game_index_listing = settings.GAME_INDEX_LISTING
wizard.game_index_listing if game_index_listing:
) game_index_save_text = (
saves = True "GAME_INDEX_ENABLED = True\n"
"GAME_INDEX_LISTING = \\\n" + pp.pformat(game_index_listing)
)
saves = True
else:
game_index_save_text = "# No Game Index settings found."
text = game_index_txt # potentially add other wizards in the future
text = game_index_save_text
wizard.display(f"Settings to save:\n\n{text}") wizard.display(f"Settings to save:\n\n{text}")
if saves: if saves:
if wizard.ask_yesno("Do you want to save these settings?") == "yes": if wizard.ask_yesno("\nDo you want to save these settings?") == "yes":
wizard.save_output = text wizard.save_output = text
_save_changes(wizard) _save_changes(wizard)
wizard.display("... saved!") wizard.display("... saved!\nThe changes will apply after you reload your server.")
else: else:
wizard.display("... cancelled.") wizard.display("... cancelled.")
wizard.ask_continue() wizard.ask_continue()

View file

@ -93,8 +93,8 @@ SRESET = chr(19) # shutdown server in reset mode
# requirements # requirements
PYTHON_MIN = "3.7" PYTHON_MIN = "3.7"
TWISTED_MIN = "18.0.0" TWISTED_MIN = "18.0.0"
DJANGO_MIN = "2.1" DJANGO_MIN = "2.2.5"
DJANGO_REC = "2.2" DJANGO_LT = "3.0"
try: try:
sys.path[1] = EVENNIA_ROOT sys.path[1] = EVENNIA_ROOT
@ -374,8 +374,8 @@ ERROR_NOTWISTED = """
""" """
ERROR_DJANGO_MIN = """ ERROR_DJANGO_MIN = """
ERROR: Django {dversion} found. Evennia requires version {django_min} ERROR: Django {dversion} found. Evennia requires at least version {django_min} (but
or higher. no higher than {django_lt}).
If you are using a virtualenv, use the command `pip install --upgrade -e evennia` where If you are using a virtualenv, use the command `pip install --upgrade -e evennia` where
`evennia` is the folder to where you cloned the Evennia library. If not `evennia` is the folder to where you cloned the Evennia library. If not
@ -386,14 +386,9 @@ ERROR_DJANGO_MIN = """
any warnings and don't run `makemigrate` even if told to. any warnings and don't run `makemigrate` even if told to.
""" """
NOTE_DJANGO_MIN = """
NOTE: Django {dversion} found. This will work, but Django {django_rec} is
recommended for production.
"""
NOTE_DJANGO_NEW = """ NOTE_DJANGO_NEW = """
NOTE: Django {dversion} found. This is newer than Evennia's NOTE: Django {dversion} found. This is newer than Evennia's
recommended version ({django_rec}). It might work, but may be new recommended version ({django_rec}). It might work, but is new
enough to not be fully tested yet. Report any issues. enough to not be fully tested yet. Report any issues.
""" """
@ -1283,12 +1278,11 @@ def check_main_evennia_dependencies():
# only the main version (1.5, not 1.5.4.0) # only the main version (1.5, not 1.5.4.0)
dversion_main = ".".join(dversion.split(".")[:2]) dversion_main = ".".join(dversion.split(".")[:2])
if LooseVersion(dversion) < LooseVersion(DJANGO_MIN): if LooseVersion(dversion) < LooseVersion(DJANGO_MIN):
print(ERROR_DJANGO_MIN.format(dversion=dversion_main, django_min=DJANGO_MIN)) print(ERROR_DJANGO_MIN.format(dversion=dversion_main, django_min=DJANGO_MIN,
django_lt=DJANGO_LT))
error = True error = True
elif LooseVersion(DJANGO_MIN) <= LooseVersion(dversion) < LooseVersion(DJANGO_REC): elif LooseVersion(DJANGO_LT) <= LooseVersion(dversion_main):
print(NOTE_DJANGO_MIN.format(dversion=dversion_main, django_rec=DJANGO_REC)) print(NOTE_DJANGO_NEW.format(dversion=dversion_main, django_rec=DJANGO_LT))
elif LooseVersion(DJANGO_REC) < LooseVersion(dversion_main):
print(NOTE_DJANGO_NEW.format(dversion=dversion_main, django_rec=DJANGO_REC))
except ImportError: except ImportError:
print(ERROR_NODJANGO) print(ERROR_NODJANGO)
error = True error = True
@ -1368,10 +1362,10 @@ def create_settings_file(init=True, secret_settings=False):
if not init: if not init:
# if not --init mode, settings file may already exist from before # if not --init mode, settings file may already exist from before
if os.path.exists(settings_path): if os.path.exists(settings_path):
inp = eval(input("%s already exists. Do you want to reset it? y/[N]> " % settings_path)) inp = input("%s already exists. Do you want to reset it? y/[N]> " % settings_path)
if not inp.lower() == "y": if not inp.lower() == "y":
print("Aborted.") print("Aborted.")
return sys.exit()
else: else:
print("Reset the settings file.") print("Reset the settings file.")

View file

@ -160,7 +160,7 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
""" """
# start the Server # start the Server
print("Portal starting server ... {}".format(server_twistd_cmd)) print("Portal starting server ... ")
process = None process = None
with open(settings.SERVER_LOG_FILE, "a") as logfile: with open(settings.SERVER_LOG_FILE, "a") as logfile:
# we link stdout to a file in order to catch # we link stdout to a file in order to catch

View file

@ -740,9 +740,9 @@ CHANNEL_CONNECTINFO = None
GAME_INDEX_ENABLED = False GAME_INDEX_ENABLED = False
# This dict # This dict
GAME_INDEX_LISTING = { GAME_INDEX_LISTING = {
"game_name": SERVERNAME, "game_name": "Mygame", # usually SERVERNAME
"game_status": "pre-alpha", # pre-alpha, alpha, beta or launched "game_status": "pre-alpha", # pre-alpha, alpha, beta or launched
"short_description": GAME_SLOGAN, "short_description": "", # could be GAME_SLOGAN
"long_description": "", "long_description": "",
"listing_contact": "", # email "listing_contact": "", # email
"telnet_hostname": "", # mygame.com "telnet_hostname": "", # mygame.com

View file

@ -778,6 +778,7 @@ class InMemoryAttributeBackend(IAttributeBackend):
See parent class. See parent class.
strvalue has no meaning for InMemory attributes. strvalue has no meaning for InMemory attributes.
""" """
new_attr = self._attrclass( new_attr = self._attrclass(
pk=self._next_id(), key=key, category=category, lock_storage=lockstring, value=value pk=self._next_id(), key=key, category=category, lock_storage=lockstring, value=value

View file

@ -35,6 +35,7 @@ the `caller.msg()` construct every time the page is updated.
""" """
from django.conf import settings from django.conf import settings
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from django.core.paginator import Paginator
from evennia import Command, CmdSet from evennia import Command, CmdSet
from evennia.commands import cmdhandler from evennia.commands import cmdhandler
from evennia.utils.ansi import ANSIString from evennia.utils.ansi import ANSIString
@ -140,7 +141,7 @@ class EvMore(object):
def __init__( def __init__(
self, self,
caller, caller,
text, inp,
always_page=False, always_page=False,
session=None, session=None,
justify=False, justify=False,
@ -152,29 +153,28 @@ class EvMore(object):
): ):
""" """
EvMore pager Initialization of the EvMore pager
Args: Args:
caller (Object or Account): Entity reading the text. caller (Object or Account): Entity reading the text.
text (str, EvTable or iterator): The text or data to put under paging. inp (str, EvTable, Paginator or iterator): The text or data to put under paging.
- If a string, paginage normally. If this text contains
- If a string, paginate normally. If this text contains one or more `\\\\f` format one or more `\f` format symbol, automatic pagination and justification
symbols, automatic pagination and justification are force-disabled and page-breaks are force-disabled and page-breaks will only happen after each `\f`.
will only happen after each `\\\\f`.
- If `EvTable`, the EvTable will be paginated with the same - If `EvTable`, the EvTable will be paginated with the same
setting on each page if it is too long. The table setting on each page if it is too long. The table
decorations will be considered in the size of the page. decorations will be considered in the size of the page.
- Otherwise `text` is converted to an iterator, where each step is - Otherwise `inp` is converted to an iterator, where each step is
expected to be a line in the final display. Each line expected to be a line in the final display. Each line
will be run through `iter_callable`. will be run through `iter_callable`.
always_page (bool, optional): If `False`, the always_page (bool, optional): If `False`, the
pager will only kick in if `text` is too big pager will only kick in if `inp` is too big
to fit the screen. to fit the screen.
session (Session, optional): If given, this session will be used session (Session, optional): If given, this session will be used
to determine the screen width and will receive all output. to determine the screen width and will receive all output.
justify (bool, optional): If set, auto-justify long lines. This must be turned justify (bool, optional): If set, auto-justify long lines. This must be turned
off for fixed-width or formatted output, like tables. It's force-disabled off for fixed-width or formatted output, like tables. It's force-disabled
if `text` is an EvTable. if `inp` is an EvTable.
justify_kwargs (dict, optional): Keywords for the justifiy function. Used only justify_kwargs (dict, optional): Keywords for the justifiy function. Used only
if `justify` is True. If this is not set, default arguments will be used. if `justify` is True. If this is not set, default arguments will be used.
exit_on_lastpage (bool, optional): If reaching the last page without the exit_on_lastpage (bool, optional): If reaching the last page without the
@ -185,12 +185,6 @@ class EvMore(object):
the caller when the more page exits. Note that this will be using whatever 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 cmdset the user had *before* the evmore pager was activated (so none of
the evmore commands will be available when this is run). the evmore commands will be available when this is run).
page_formatter (callable, optional): If given, this function will be passed the
contents of each extracted page. This is useful when paginating
data consisting something other than a string or a list of strings. Especially
queryset data is likely to always need this argument specified. Note however,
that all size calculations assume this function to return one single line
per element on the page!
kwargs (any, optional): These will be passed on to the `caller.msg` method. kwargs (any, optional): These will be passed on to the `caller.msg` method.
Examples: Examples:
@ -198,16 +192,21 @@ class EvMore(object):
```python ```python
super_long_text = " ... " super_long_text = " ... "
EvMore(caller, super_long_text) EvMore(caller, super_long_text)
```
Paginator
```python
from django.core.paginator import Paginator from django.core.paginator import Paginator
query = ObjectDB.objects.all() query = ObjectDB.objects.all()
pages = Paginator(query, 10) # 10 objs per page pages = Paginator(query, 10) # 10 objs per page
EvMore(caller, pages) # will repr() each object per line, 10 to a page EvMore(caller, pages)
```
multi_page_table = [ [[..],[..]], ...] Every page an EvTable
EvMore(caller, multi_page_table, use_evtable=True, ```python
evtable_args=("Header1", "Header2"), from evennia import EvTable
evtable_kwargs={"align": "r", "border": "tablecols"}) def _to_evtable(page):
table = ... # convert page to a table
return EvTable(*headers, table=table, ...)
EvMore(caller, pages, page_formatter=_to_evtable)
``` ```
""" """
@ -228,141 +227,40 @@ class EvMore(object):
self.exit_on_lastpage = exit_on_lastpage self.exit_on_lastpage = exit_on_lastpage
self.exit_cmd = exit_cmd self.exit_cmd = exit_cmd
self._exit_msg = "Exited |wmore|n pager." self._exit_msg = "Exited |wmore|n pager."
self._page_formatter = page_formatter
self._kwargs = kwargs self._kwargs = kwargs
self._data = None self._data = None
self._paginator = None
self._pages = [] self._pages = []
self._npages = 1
self._npos = 0 self._npos = 0
self._npages = 1
self._paginator = self.paginator_index
self._page_formatter = str
# set up individual pages for different sessions # set up individual pages for different sessions
height = max(4, session.protocol_flags.get("SCREENHEIGHT", {0: _SCREEN_HEIGHT})[0] - 4) height = max(4, session.protocol_flags.get("SCREENHEIGHT", {0: _SCREEN_HEIGHT})[0] - 4)
self.width = session.protocol_flags.get("SCREENWIDTH", {0: _SCREEN_WIDTH})[0] self.width = session.protocol_flags.get("SCREENWIDTH", {0: _SCREEN_WIDTH})[0]
# always limit number of chars to 10 000 per page # always limit number of chars to 10 000 per page
self.height = min(10000 // max(1, self.width), height) self.height = min(10000 // max(1, self.width), height)
if inherits_from(text, "evennia.utils.evtable.EvTable"): # does initial parsing of input
# an EvTable self.init_pages(inp)
self.init_evtable(text)
elif isinstance(text, QuerySet):
# a queryset
self.init_queryset(text)
elif not isinstance(text, str):
# anything else not a str
self.init_iterable(text)
elif "\f" in text:
# string with \f line-break markers in it
self.init_f_str(text)
else:
# a string
self.init_str(text)
# kick things into gear # kick things into gear
self.start() self.start()
# page formatter # EvMore functional methods
def format_page(self, page):
"""
Page formatter. Uses the page_formatter callable by default.
This allows to easier override the class if needed.
"""
return self._page_formatter(page)
# paginators - responsible for extracting a specific page number
def paginator_index(self, pageno):
"""Paginate to specific, known index"""
return self._data[pageno]
def paginator_slice(self, pageno):
"""
Paginate by slice. This is done with an eye on memory efficiency (usually for
querysets); to avoid fetching all objects at the same time.
"""
return self._data[pageno * self.height : pageno * self.height + self.height]
# inits for different input types
def init_evtable(self, table):
"""The input is an EvTable."""
if table.height:
# enforced height of each paged table, plus space for evmore extras
self.height = table.height - 4
# convert table to string
text = str(table)
self._justify = False
self._justify_kwargs = None # enforce
self.init_str(text)
def init_queryset(self, qs):
"""The input is a queryset"""
nsize = qs.count() # we assume each will be a line
self._npages = nsize // self.height + (0 if nsize % self.height == 0 else 1)
self._data = qs
self._paginator = self.paginator_slice
def init_iterable(self, inp):
"""The input is something other than a string - convert to iterable of strings"""
inp = make_iter(inp)
nsize = len(inp)
self._npages = nsize // self.height + (0 if nsize % self.height == 0 else 1)
self._data = inp
self._paginator = self.paginator_slice
def init_f_str(self, text):
"""
The input contains `\\f` markers. We use `\\f` to indicate the user wants to
enforce their line breaks on their own. If so, we do no automatic
line-breaking/justification at all.
Args:
text (str): The string to format with f-markers.
"""
self._data = text.split("\f")
self._npages = len(self._data)
self._paginator = self.paginator_index
def init_str(self, text):
"""The input is a string"""
if self._justify:
# we must break very long lines into multiple ones. Note that this
# will also remove spurious whitespace.
justify_kwargs = self._justify_kwargs or {}
width = self._justify_kwargs.get("width", self.width)
justify_kwargs["width"] = width
justify_kwargs["align"] = self._justify_kwargs.get("align", "l")
justify_kwargs["indent"] = self._justify_kwargs.get("indent", 0)
lines = []
for line in text.split("\n"):
if len(line) > width:
lines.extend(justify(line, **justify_kwargs).split("\n"))
else:
lines.append(line)
else:
# no justification. Simple division by line
lines = text.split("\n")
self._data = [
_LBR.join(lines[i : i + self.height]) for i in range(0, len(lines), self.height)
]
self._npages = len(self._data)
self._paginator = self.paginator_index
# display helpers and navigation
def display(self, show_footer=True): def display(self, show_footer=True):
""" """
Pretty-print the page. Pretty-print the page.
""" """
pos = self._npos pos = 0
text = self.format_page(self._paginator(pos)) text = "[no content]"
if self._npages > 0:
pos = self._npos
text = self.page_formatter(self.paginator(pos))
if show_footer: if show_footer:
page = _DISPLAY.format(text=text, pageno=pos + 1, pagemax=self._npages) page = _DISPLAY.format(text=text, pageno=pos + 1, pagemax=self._npages)
else: else:
@ -442,6 +340,180 @@ class EvMore(object):
self.page_top() self.page_top()
# default paginators - responsible for extracting a specific page number
def paginator_index(self, pageno):
"""Paginate to specific, known index"""
return self._data[pageno]
def paginator_slice(self, pageno):
"""
Paginate by slice. This is done with an eye on memory efficiency (usually for
querysets); to avoid fetching all objects at the same time.
"""
return self._data[pageno * self.height: pageno * self.height + self.height]
def paginator_django(self, pageno):
"""
Paginate using the django queryset Paginator API. Note that his is indexed from 1.
"""
return self._data.page(pageno + 1)
# default helpers to set up particular input types
def init_evtable(self, table):
"""The input is an EvTable."""
if table.height:
# enforced height of each paged table, plus space for evmore extras
self.height = table.height - 4
# convert table to string
text = str(table)
self._justify = False
self._justify_kwargs = None # enforce
self.init_str(text)
def init_queryset(self, qs):
"""The input is a queryset"""
nsize = qs.count() # we assume each will be a line
self._npages = nsize // self.height + (0 if nsize % self.height == 0 else 1)
self._data = qs
def init_django_paginator(self, pages):
"""
The input is a django Paginator object.
"""
self._npages = pages.num_pages
self._data = pages
def init_iterable(self, inp):
"""The input is something other than a string - convert to iterable of strings"""
inp = make_iter(inp)
nsize = len(inp)
self._npages = nsize // self.height + (0 if nsize % self.height == 0 else 1)
self._data = inp
def init_f_str(self, text):
"""
The input contains `\\f` markers. We use `\\f` to indicate the user wants to
enforce their line breaks on their own. If so, we do no automatic
line-breaking/justification at all.
Args:
text (str): The string to format with f-markers.
"""
self._data = text.split("\f")
self._npages = len(self._data)
def init_str(self, text):
"""The input is a string"""
if self._justify:
# we must break very long lines into multiple ones. Note that this
# will also remove spurious whitespace.
justify_kwargs = self._justify_kwargs or {}
width = self._justify_kwargs.get("width", self.width)
justify_kwargs["width"] = width
justify_kwargs["align"] = self._justify_kwargs.get("align", "l")
justify_kwargs["indent"] = self._justify_kwargs.get("indent", 0)
lines = []
for line in text.split("\n"):
if len(line) > width:
lines.extend(justify(line, **justify_kwargs).split("\n"))
else:
lines.append(line)
else:
# no justification. Simple division by line
lines = text.split("\n")
self._data = [
_LBR.join(lines[i : i + self.height]) for i in range(0, len(lines), self.height)
]
self._npages = len(self._data)
# Hooks for customizing input handling and formatting (override in a child class)
def init_pages(self, inp):
"""
Initialize the pagination. By default, will analyze input type to determine
how pagination automatically.
Args:
inp (any): Incoming data to be paginated. By default, handles pagination of
strings, querysets, django.Paginator, EvTables and any iterables with strings.
Notes:
If overridden, this method must perform the following actions:
- read and re-store `self._data` (the incoming data set) if needed for pagination to work.
- set `self._npages` to the total number of pages. Default is 1.
- set `self._paginator` to a callable that will take a page number 1...N and return
the data to display on that page (not any decorations or next/prev buttons). If only
wanting to change the paginator, override `self.paginator` instead.
- set `self._page_formatter` to a callable that will receive the page from `self._paginator`
and format it with one element per line. Default is `str`. Or override `self.page_formatter`
directly instead.
By default, helper methods are called that perform these actions
depending on supported inputs.
"""
if inherits_from(inp, "evennia.utils.evtable.EvTable"):
# an EvTable
self.init_evtable(inp)
self._paginator = self.paginator_index
elif isinstance(inp, QuerySet):
# a queryset
self.init_queryset(inp)
self._paginator = self.paginator_slice
elif isinstance(inp, Paginator):
self.init_django_paginator(inp)
self._paginator = self.paginator_django
elif not isinstance(inp, str):
# anything else not a str
self.init_iterable(inp)
self._paginator = self.paginator_slice
elif "\f" in inp:
# string with \f line-break markers in it
self.init_f_str(inp)
self._paginator = self.paginator_index
else:
# a string
self.init_str(inp)
self._paginator = self.paginator_index
def paginator(self, pageno):
"""
Paginator. The data operated upon is in `self._data`.
Args:
pageno (int): The page number to view, from 0...N-1
Returns:
str: The page to display (without any decorations, those are added
by EvMore).
"""
return self._paginator(pageno)
def page_formatter(self, page):
"""
Page formatter. Every page passes through this method. Override
it to customize behvaior per-page. A common use is to generate a new
EvTable for every page (this is more efficient than to generate one huge
EvTable across many pages and feed it into EvMore all at once).
Args:
page (any): A piece of data representing one page to display. This must
Returns:
str: A ready-formatted page to display. Extra footer with help about
switching to the next/prev page will be added automatically
"""
return self._page_formatter(page)
# helper function # helper function

View file

@ -159,7 +159,7 @@ class TestCreateMessage(EvenniaTest):
locks=locks, locks=locks,
tags=tags, tags=tags,
) )
self.assertEqual(msg.receivers, [self.char1, self.char2]) self.assertEqual(set(msg.receivers), set([self.char1, self.char2]))
self.assertTrue(all(lock in msg.locks.all() for lock in locks.split(";"))) self.assertTrue(all(lock in msg.locks.all() for lock in locks.split(";")))
self.assertEqual(msg.tags.all(), tags) self.assertEqual(msg.tags.all(), tags)

View file

@ -2099,6 +2099,10 @@ def at_search_result(matches, caller, query="", quiet=False, **kwargs):
for num, result in enumerate(matches): for num, result in enumerate(matches):
# we need to consider Commands, where .aliases is a list # we need to consider Commands, where .aliases is a list
aliases = result.aliases.all() if hasattr(result.aliases, "all") else result.aliases aliases = result.aliases.all() if hasattr(result.aliases, "all") else result.aliases
# remove any pluralization aliases
aliases = [alias for alias in aliases if
hasattr(alias, "category")
and alias.category not in ("plural_key", )]
error += _MULTIMATCH_TEMPLATE.format( error += _MULTIMATCH_TEMPLATE.format(
number=num + 1, number=num + 1,
name=result.get_display_name(caller) name=result.get_display_name(caller)

View file

@ -2,7 +2,7 @@
# general # general
attrs >= 19.2.0 attrs >= 19.2.0
django >= 2.2.5, < 2.3 django >= 2.2.5, < 3.0
twisted >= 20.3.0, < 21.0.0 twisted >= 20.3.0, < 21.0.0
pytz pytz
djangorestframework >= 3.10.3, < 3.12 djangorestframework >= 3.10.3, < 3.12