Merge branch 'master' into develop

This commit is contained in:
Griatch 2020-04-09 23:40:32 +02:00
commit ae98526a7f
17 changed files with 866 additions and 466 deletions

View file

@ -35,11 +35,13 @@ without arguments starts a full interactive Python console.
- Allow running Evennia test suite from core repo with `make test`. - Allow running Evennia test suite from core repo with `make test`.
- Return `store_key` from `TickerHandler.add` and add `store_key` as a kwarg to - Return `store_key` from `TickerHandler.add` and add `store_key` as a kwarg to
the `TickerHandler.remove` method. This makes it easier to manage tickers. the `TickerHandler.remove` method. This makes it easier to manage tickers.
- EvMore `text` argument can now also be a list - each entry in the list is run
through str(eval()) and ends up on its own line. Good for paginated object lists.
- EvMore auto-justify now defaults to False since this works better with all types - EvMore auto-justify now defaults to False since this works better with all types
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
sliced to only return the required data per page. EvMore takes a new kwarg
`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.
@ -55,6 +57,8 @@ without arguments starts a full interactive Python console.
bugfixes. bugfixes.
- Remove `dummy@example.com` as a default account email when unset, a string is no longer - Remove `dummy@example.com` as a default account email when unset, a string is no longer
required by Django. required by Django.
- Fixes to `spawn`, make updating an existing prototype/object work better. Add `/raw` switch
to `spawn` command to extract the raw prototype dict for manual editing.
## Evennia 0.9 (2018-2019) ## Evennia 0.9 (2018-2019)

View file

@ -4,7 +4,7 @@ Typeclass for Account objects
Note that this object is primarily intended to Note that this object is primarily intended to
store OOC information, not game info! This store OOC information, not game info! This
object represents the actual user (not their object represents the actual user (not their
character) and has NO actual precence in the character) and has NO actual presence in the
game world (this is handled by the associated game world (this is handled by the associated
character object, so you should customize that character object, so you should customize that
instead for most things). instead for most things).

View file

@ -7,19 +7,19 @@ from django.db.models import Q, Min, Max
from evennia.objects.models import ObjectDB from evennia.objects.models import ObjectDB
from evennia.locks.lockhandler import LockException from evennia.locks.lockhandler import LockException
from evennia.commands.cmdhandler import get_and_merge_cmdsets from evennia.commands.cmdhandler import get_and_merge_cmdsets
from evennia.utils import create, utils, search from evennia.utils import create, utils, search, logger
from evennia.utils.utils import ( from evennia.utils.utils import (
inherits_from, inherits_from,
class_from_module, class_from_module,
get_all_typeclasses, get_all_typeclasses,
variable_from_module, variable_from_module,
dbref, dbref, interactive,
list_to_string
) )
from evennia.utils.eveditor import EvEditor from evennia.utils.eveditor import EvEditor
from evennia.utils.evmore import EvMore from evennia.utils.evmore import EvMore
from evennia.prototypes import spawner, prototypes as protlib, menus as olc_menus from evennia.prototypes import spawner, prototypes as protlib, menus as olc_menus
from evennia.utils.ansi import raw from evennia.utils.ansi import raw
from evennia.prototypes.menus import _format_diff_text_and_options
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
@ -2099,10 +2099,10 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
# to confirm changes. # to confirm changes.
if "prototype" in self.switches: if "prototype" in self.switches:
diff, _ = spawner.prototype_diff_from_object(prototype, obj) diff, _ = spawner.prototype_diff_from_object(prototype, obj)
txt, options = _format_diff_text_and_options(diff, objects=[obj]) txt = spawner.format_diff(diff)
prompt = ( prompt = (
"Applying prototype '%s' over '%s' will cause the follow changes:\n%s\n" "Applying prototype '%s' over '%s' will cause the follow changes:\n%s\n"
% (prototype["key"], obj.name, "\n".join(txt)) % (prototype["key"], obj.name, txt)
) )
if not reset: if not reset:
prompt += "\n|yWARNING:|n Use the /reset switch to apply the prototype over a blank state." prompt += "\n|yWARNING:|n Use the /reset switch to apply the prototype over a blank state."
@ -3227,6 +3227,10 @@ class CmdTag(COMMAND_DEFAULT_CLASS):
self.caller.msg(string) self.caller.msg(string)
# helper functions for spawn
class CmdSpawn(COMMAND_DEFAULT_CLASS): class CmdSpawn(COMMAND_DEFAULT_CLASS):
""" """
spawn objects from prototype spawn objects from prototype
@ -3250,13 +3254,14 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
search - search prototype by name or tags. search - search prototype by name or tags.
list - list available prototypes, optionally limit by tags. list - list available prototypes, optionally limit by tags.
show, examine - inspect prototype by key. If not given, acts like list. show, examine - inspect prototype by key. If not given, acts like list.
raw - show the raw dict of the prototype as a one-line string for manual editing.
save - save a prototype to the database. It will be listable by /list. save - save a prototype to the database. It will be listable by /list.
delete - remove a prototype from database, if allowed to. delete - remove a prototype from database, if allowed to.
update - find existing objects with the same prototype_key and update update - find existing objects with the same prototype_key and update
them with latest version of given prototype. If given with /save, them with latest version of given prototype. If given with /save,
will auto-update all objects with the old version of the prototype will auto-update all objects with the old version of the prototype
without asking first. without asking first.
edit, olc - create/manipulate prototype in a menu interface. edit, menu, olc - create/manipulate prototype in a menu interface.
Example: Example:
spawn GOBLIN spawn GOBLIN
@ -3298,6 +3303,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
"search", "search",
"list", "list",
"show", "show",
"raw",
"examine", "examine",
"save", "save",
"delete", "delete",
@ -3309,56 +3315,209 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
locks = "cmd:perm(spawn) or perm(Builder)" locks = "cmd:perm(spawn) or perm(Builder)"
help_category = "Building" help_category = "Building"
def _search_prototype(self, prototype_key, quiet=False):
"""
Search for prototype and handle no/multi-match and access.
Returns a single found prototype or None - in the
case, the caller has already been informed of the
search error we need not do any further action.
"""
prototypes = protlib.search_prototype(prototype_key)
nprots = len(prototypes)
# handle the search result
err = None
if not prototypes:
err = f"No prototype named '{prototype_key}' was found."
elif nprots > 1:
err = "Found {} prototypes matching '{}':\n {}".format(
nprots,
prototype_key,
", ".join(proto.get("prototype_key", "") for proto in prototypes),
)
else:
# we have a single prototype, check access
prototype = prototypes[0]
if not self.caller.locks.check_lockstring(
self.caller, prototype.get("prototype_locks", ""),
access_type="spawn", default=True):
err = "You don't have access to use this prototype."
if err:
# return None on any error
if not quiet:
self.caller.msg(err)
return
return prototype
def _parse_prototype(self, inp, expect=dict):
"""
Parse a prototype dict or key from the input and convert it safely
into a dict if appropriate.
Args:
inp (str): The input from user.
expect (type, optional):
Returns:
prototype (dict, str or None): The parsed prototype. If None, the error
was already reported.
"""
eval_err = None
try:
prototype = _LITERAL_EVAL(inp)
except (SyntaxError, ValueError) as err:
# treat as string
eval_err = err
prototype = utils.to_str(inp)
finally:
# it's possible that the input was a prototype-key, in which case
# it's okay for the LITERAL_EVAL to fail. Only if the result does not
# match the expected type do we have a problem.
if not isinstance(prototype, expect):
if eval_err:
string = (
f"{inp}\n{eval_err}\n|RCritical Python syntax error in argument. Only primitive "
"Python structures are allowed. \nMake sure to use correct "
"Python syntax. Remember especially to put quotes around all "
"strings inside lists and dicts.|n For more advanced uses, embed "
"inlinefuncs in the strings."
)
else:
string = "Expected {}, got {}.".format(expect, type(prototype))
self.caller.msg(string)
return
if expect == dict:
# an actual prototype. We need to make sure it's safe,
# so don't allow exec.
# TODO: Exec support is deprecated. Remove completely for 1.0.
if "exec" in prototype and not self.caller.check_permstring("Developer"):
self.caller.msg(
"Spawn aborted: You are not allowed to " "use the 'exec' prototype key."
)
return
try:
# we homogenize the protoype first, to be more lenient with free-form
protlib.validate_prototype(protlib.homogenize_prototype(prototype))
except RuntimeError as err:
self.caller.msg(str(err))
return
return prototype
def _get_prototype_detail(self, query=None, prototypes=None):
"""
Display the detailed specs of one or more prototypes.
Args:
query (str, optional): If this is given and `prototypes` is not, search for
the prototype(s) by this query. This may be a partial query which
may lead to multiple matches, all being displayed.
prototypes (list, optional): If given, ignore `query` and only show these
prototype-details.
Returns:
display (str, None): A formatted string of one or more prototype details.
If None, the caller was already informed of the error.
"""
if not prototypes:
# we need to query. Note that if query is None, all prototypes will
# be returned.
prototypes = protlib.search_prototype(key=query)
if prototypes:
return "\n".join(protlib.prototype_to_str(prot) for prot in prototypes)
elif query:
self.caller.msg(f"No prototype named '{query}' was found.")
else:
self.caller.msg(f"No prototypes found.")
def _list_prototypes(self, key=None, tags=None):
"""Display prototypes as a list, optionally limited by key/tags. """
table = protlib.list_prototypes(self.caller, key=key, tags=tags)
if not table:
return True
EvMore(
self.caller,
str(table),
exit_on_lastpage=True,
justify_kwargs=False,
)
@interactive
def _update_existing_objects(self, caller, prototype_key, quiet=False):
"""
Update existing objects (if any) with this prototype-key to the latest
prototype version.
Args:
caller (Object): This is necessary for @interactive to work.
prototype_key (str): The prototype to update.
quiet (bool, optional): If set, don't report to user if no
old objects were found to update.
Returns:
n_updated (int): Number of updated objects.
"""
prototype = self._search_prototype(prototype_key)
if not prototype:
return
existing_objects = protlib.search_objects_with_prototype(prototype_key)
if not existing_objects:
if not quiet:
caller.msg("No existing objects found with an older version of this prototype.")
return
if existing_objects:
n_existing = len(existing_objects)
slow = " (note that this may be slow)" if n_existing > 10 else ""
string = (
f"There are {n_existing} existing object(s) with an older version "
f"of prototype '{prototype_key}'. Should it be re-applied to them{slow}? [Y]/N"
)
answer = yield (string)
if answer.lower() in ["n", "no"]:
caller.msg(
"|rNo update was done of existing objects. "
"Use spawn/update <key> to apply later as needed.|n"
)
return
try:
n_updated = spawner.batch_update_objects_with_prototype(
prototype, objects=existing_objects)
except Exception:
logger.log_trace()
caller.msg(f"{n_updated} objects were updated.")
return
def _parse_key_desc_tags(self, argstring, desc=True):
"""
Parse ;-separated input list.
"""
key, desc, tags = "", "", []
if ";" in argstring:
parts = [part.strip().lower() for part in argstring.split(";")]
if len(parts) > 1 and desc:
key = parts[0]
desc = parts[1]
tags = parts[2:]
else:
key = parts[0]
tags = parts[1:]
else:
key = argstring.strip().lower()
return key, desc, tags
def func(self): def func(self):
"""Implements the spawner""" """Implements the spawner"""
def _parse_prototype(inp, expect=dict):
err = None
try:
prototype = _LITERAL_EVAL(inp)
except (SyntaxError, ValueError) as err:
# treat as string
prototype = utils.to_str(inp)
finally:
if not isinstance(prototype, expect):
if err:
string = (
"{}\n|RCritical Python syntax error in argument. Only primitive "
"Python structures are allowed. \nYou also need to use correct "
"Python syntax. Remember especially to put quotes around all "
"strings inside lists and dicts.|n For more advanced uses, embed "
"inline functions in the strings.".format(err)
)
else:
string = "Expected {}, got {}.".format(expect, type(prototype))
self.caller.msg(string)
return None
if expect == dict:
# an actual prototype. We need to make sure it's safe. Don't allow exec
if "exec" in prototype and not self.caller.check_permstring("Developer"):
self.caller.msg(
"Spawn aborted: You are not allowed to " "use the 'exec' prototype key."
)
return None
try:
# we homogenize first, to be more lenient
protlib.validate_prototype(protlib.homogenize_prototype(prototype))
except RuntimeError as err:
self.caller.msg(str(err))
return
return prototype
def _search_show_prototype(query, prototypes=None):
# prototype detail
if not prototypes:
prototypes = protlib.search_prototype(key=query)
if prototypes:
return "\n".join(protlib.prototype_to_str(prot) for prot in prototypes)
else:
return False
caller = self.caller caller = self.caller
noloc = "noloc" in self.switches
# run the menu/olc
if ( if (
self.cmdstring == "olc" self.cmdstring == "olc"
or "menu" in self.switches or "menu" in self.switches
@ -3368,94 +3527,122 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
# OLC menu mode # OLC menu mode
prototype = None prototype = None
if self.lhs: if self.lhs:
key = self.lhs prototype_key = self.lhs
prototype = protlib.search_prototype(key=key) prototype = self._search_prototype(prototype_key)
if len(prototype) > 1: if not prototype:
caller.msg(
"More than one match for {}:\n{}".format(
key, "\n".join(proto.get("prototype_key", "") for proto in prototype)
)
)
return
elif prototype:
# one match
prototype = prototype[0]
else:
# no match
caller.msg("No prototype '{}' was found.".format(key))
return return
olc_menus.start_olc(caller, session=self.session, prototype=prototype) olc_menus.start_olc(caller, session=self.session, prototype=prototype)
return return
if "search" in self.switches: if "search" in self.switches:
# query for a key match # query for a key match. The arg is a search query or nothing.
if not self.args: if not self.args:
self.switches.append("list") # an empty search returns the full list
else: self._list_prototypes()
key, tags = self.args.strip(), None
if ";" in self.args:
key, tags = (part.strip().lower() for part in self.args.split(";", 1))
tags = [tag.strip() for tag in tags.split(",")] if tags else None
EvMore(
caller,
str(protlib.list_prototypes(caller, key=key, tags=tags)),
exit_on_lastpage=True,
)
return return
# search for key;tag combinations
key, _, tags = self._parse_key_desc_tags(self.args, desc=False)
self._list_prototypes(key, tags)
return
if "raw" in self.switches:
# query for key match and return the prototype as a safe one-liner string.
if not self.args:
caller.msg("You need to specify a prototype-key to get the raw data for.")
prototype = self._search_prototype(self.args)
if not prototype:
return
caller.msg(str(prototype))
return
if "show" in self.switches or "examine" in self.switches: if "show" in self.switches or "examine" in self.switches:
# the argument is a key in this case (may be a partial key) # show a specific prot detail. The argument is a search query or empty.
if not self.args: if not self.args:
self.switches.append("list") # we don't show the list of all details, that's too spammy.
else: caller.msg("You need to specify a prototype-key to show.")
matchstring = _search_show_prototype(self.args)
if matchstring:
caller.msg(matchstring)
else:
caller.msg("No prototype '{}' was found.".format(self.args))
return return
if "list" in self.switches: detail_string = self._get_prototype_detail(self.args)
# for list, all optional arguments are tags if not detail_string:
# import pudb; pudb.set_trace() return
caller.msg(detail_string)
return
EvMore( if "list" in self.switches:
caller, # for list, all optional arguments are tags.
str(protlib.list_prototypes(caller, tags=self.lhslist)), tags = self.lhslist
exit_on_lastpage=True, err = self._list_prototypes(tags=tags)
justify_kwargs=False, if err:
) caller.msg("No prototypes found with prototype-tag(s): {}".format(
list_to_string(tags, "or")))
return return
if "save" in self.switches: if "save" in self.switches:
# store a prototype to the database store # store a prototype to the database store
if not self.args: if not self.args:
caller.msg( caller.msg(
"Usage: spawn/save <key>[;desc[;tag,tag[,...][;lockstring]]] = <prototype_dict>" "Usage: spawn/save [<key>[;desc[;tag,tag[,...][;lockstring]]]] = <prototype_dict>"
) )
return return
if self.rhs:
# input on the form key = prototype
prototype_key, prototype_desc, prototype_tags = self._parse_key_desc_tags(self.lhs)
prototype_key = None if not prototype_key else prototype_key
prototype_desc = None if not prototype_desc else prototype_desc
prototype_tags = None if not prototype_tags else prototype_tags
prototype_input = self.rhs.strip()
else:
prototype_key = prototype_desc = None
prototype_tags = None
prototype_input = self.lhs.strip()
# handle rhs: # handle parsing
prototype = _parse_prototype(self.lhs.strip()) prototype = self._parse_prototype(prototype_input)
if not prototype: if not prototype:
return return
# present prototype to save prot_prototype_key = prototype.get("prototype_key")
new_matchstring = _search_show_prototype("", prototypes=[prototype])
string = "|yCreating new prototype:|n\n{}".format(new_matchstring)
question = "\nDo you want to continue saving? [Y]/N"
prototype_key = prototype.get("prototype_key") if not (prototype_key or prot_prototype_key):
if not prototype_key: caller.msg("A prototype_key must be given, either as `prototype_key = <prototype>` "
caller.msg("\n|yTo save a prototype it must have the 'prototype_key' set.") "or as a key 'prototype_key' inside the prototype structure.")
return return
# check for existing prototype, if prototype_key is None:
old_matchstring = _search_show_prototype(prototype_key) prototype_key = prot_prototype_key
if old_matchstring: if prot_prototype_key != prototype_key:
string += "\n|yExisting saved prototype found:|n\n{}".format(old_matchstring) caller.msg("(Replacing `prototype_key` in prototype with given key.)")
question = "\n|yDo you want to replace the existing prototype?|n [Y]/N" prototype['prototype_key'] = prototype_key
if prototype_desc is not None and prot_prototype_key != prototype_desc:
caller.msg("(Replacing `prototype_desc` in prototype with given desc.)")
prototype['prototype_desc'] = prototype_desc
if prototype_tags is not None and prototype.get("prototype_tags") != prototype_tags:
caller.msg("(Replacing `prototype_tags` in prototype with given tag(s))" )
prototype['prototype_tags'] = prototype_tags
string = ""
# check for existing prototype (exact match)
old_prototype = self._search_prototype(prototype_key, quiet=True)
diff = spawner.prototype_diff(old_prototype, prototype, homogenize=True)
diffstr = spawner.format_diff(diff)
new_prototype_detail = self._get_prototype_detail(prototypes=[prototype])
if old_prototype:
if not diffstr:
string = f"|yAlready existing Prototype:|n\n{new_prototype_detail}\n"
question = "\nThere seems to be no changes. Do you still want to (re)save? [Y]/N"
else:
string = (f"|yExisting prototype \"{prototype_key}\" found. Change:|n\n{diffstr}\n"
f"|yNew changed prototype:|n\n{new_prototype_detail}")
question = "\n|yDo you want to apply the change to the existing prototype?|n [Y]/N"
else:
string = f"|yCreating new prototype:|n\n{new_prototype_detail}"
question = "\nDo you want to continue saving? [Y]/N"
answer = yield (string + question) answer = yield (string + question)
if answer.lower() in ["n", "no"]: if answer.lower() in ["n", "no"]:
@ -3474,82 +3661,52 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
caller.msg("|gSaved prototype:|n {}".format(prototype_key)) caller.msg("|gSaved prototype:|n {}".format(prototype_key))
# check if we want to update existing objects # check if we want to update existing objects
existing_objects = protlib.search_objects_with_prototype(prototype_key)
if existing_objects: self._update_existing_objects(self.caller, prototype_key, quiet=True)
if "update" not in self.switches:
n_existing = len(existing_objects)
slow = " (note that this may be slow)" if n_existing > 10 else ""
string = (
"There are {} objects already created with an older version "
"of prototype {}. Should it be re-applied to them{}? [Y]/N".format(
n_existing, prototype_key, slow
)
)
answer = yield (string)
if answer.lower() in ["n", "no"]:
caller.msg(
"|rNo update was done of existing objects. "
"Use spawn/update <key> to apply later as needed.|n"
)
return
n_updated = spawner.batch_update_objects_with_prototype(existing_objects, key)
caller.msg("{} objects were updated.".format(n_updated))
return return
if not self.args: if not self.args:
# all switches beyond this point gets a common non-arg return
ncount = len(protlib.search_prototype()) ncount = len(protlib.search_prototype())
caller.msg( caller.msg(
"Usage: spawn <prototype-key> or {{key: value, ...}}" "Usage: spawn <prototype-key> or {{key: value, ...}}"
"\n ({} existing prototypes. Use /list to inspect)".format(ncount) f"\n ({ncount} existing prototypes. Use /list to inspect)"
) )
return return
if "delete" in self.switches: if "delete" in self.switches:
# remove db-based prototype # remove db-based prototype
matchstring = _search_show_prototype(self.args) prototype_detail = self._get_prototype_detail(self.args)
if matchstring: if not prototype_detail:
string = "|rDeleting prototype:|n\n{}".format(matchstring)
question = "\nDo you want to continue deleting? [Y]/N"
answer = yield (string + question)
if answer.lower() in ["n", "no"]:
caller.msg("|rDeletion cancelled.|n")
return
try:
success = protlib.delete_prototype(self.args)
except protlib.PermissionError as err:
caller.msg("|rError deleting:|R {}|n".format(err))
caller.msg(
"Deletion {}.".format(
"successful" if success else "failed (does the prototype exist?)"
)
)
return return
string = f"|rDeleting prototype:|n\n{prototype_detail}"
question = "\nDo you want to continue deleting? [Y]/N"
answer = yield (string + question)
if answer.lower() in ["n", "no"]:
caller.msg("|rDeletion cancelled.|n")
return
try:
success = protlib.delete_prototype(self.args)
except protlib.PermissionError as err:
retmsg = f"|rError deleting:|R {err}|n"
else: else:
caller.msg("Could not find prototype '{}'".format(key)) retmsg = ("Deletion successful" if success else
"Deletion failed (does the prototype exist?)")
caller.msg(retmsg)
return
if "update" in self.switches: if "update" in self.switches:
# update existing prototypes # update existing prototypes
key = self.args.strip().lower() prototype_key = self.args.strip().lower()
existing_objects = protlib.search_objects_with_prototype(key) self._update_existing_objects(self.caller, prototype_key)
if existing_objects: return
n_existing = len(existing_objects)
slow = " (note that this may be slow)" if n_existing > 10 else ""
string = (
"There are {} objects already created with an older version "
"of prototype {}. Should it be re-applied to them{}? [Y]/N".format(
n_existing, key, slow
)
)
answer = yield (string)
if answer.lower() in ["n", "no"]:
caller.msg("|rUpdate cancelled.")
return
n_updated = spawner.batch_update_objects_with_prototype(existing_objects, key)
caller.msg("{} objects were updated.".format(n_updated))
# A direct creation of an object from a given prototype # If we get to this point, we use not switches but are trying a
# direct creation of an object from a given prototype or -key
prototype = _parse_prototype( prototype = self._parse_prototype(
self.args, expect=dict if self.args.strip().startswith("{") else str self.args, expect=dict if self.args.strip().startswith("{") else str
) )
if not prototype: if not prototype:
@ -3559,35 +3716,20 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
key = "<unnamed>" key = "<unnamed>"
if isinstance(prototype, str): if isinstance(prototype, str):
# A prototype key we are looking to apply # A prototype key we are looking to apply
key = prototype prototype_key = prototype
prototypes = protlib.search_prototype(prototype) prototype = self._search_prototype(prototype_key)
nprots = len(prototypes)
if not prototypes:
caller.msg("No prototype named '%s'." % prototype)
return
elif nprots > 1:
caller.msg(
"Found {} prototypes matching '{}':\n {}".format(
nprots,
prototype,
", ".join(proto.get("prototype_key", "") for proto in prototypes),
)
)
return
# we have a prototype, check access
prototype = prototypes[0]
if not caller.locks.check_lockstring(
caller, prototype.get("prototype_locks", ""), access_type="spawn", default=True
):
caller.msg("You don't have access to use this prototype.")
return
if "noloc" not in self.switches and "location" not in prototype: if not prototype:
prototype["location"] = self.caller.location return
# proceed to spawning # proceed to spawning
try: try:
for obj in spawner.spawn(prototype): for obj in spawner.spawn(prototype):
self.caller.msg("Spawned %s." % obj.get_display_name(self.caller)) self.caller.msg("Spawned %s." % obj.get_display_name(self.caller))
if not prototype.get('location') and not noloc:
# we don't hardcode the location in the prototype (unless the user
# did so manually) - that would lead to it having to be 'removed' every
# time we try to update objects with this prototype in the future.
obj.location = caller.location
except RuntimeError as err: except RuntimeError as err:
caller.msg(err) caller.msg(err)

View file

@ -487,6 +487,8 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
locks = "cmd:perm(listscripts) or perm(Admin)" locks = "cmd:perm(listscripts) or perm(Admin)"
help_category = "System" help_category = "System"
excluded_typeclass_paths = ["evennia.prototypes.prototypes.DbPrototype"]
def func(self): def func(self):
"""implement method""" """implement method"""
@ -519,6 +521,8 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
if not scripts: if not scripts:
caller.msg("No scripts are running.") caller.msg("No scripts are running.")
return return
# filter any found scripts by tag category.
scripts = scripts.exclude(db_typeclass_path__in=self.excluded_typeclass_paths)
if not scripts: if not scripts:
string = "No scripts found with a key '%s', or on an object named '%s'." % (args, args) string = "No scripts found with a key '%s', or on an object named '%s'." % (args, args)
@ -538,19 +542,20 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
# import pdb # DEBUG # import pdb # DEBUG
# pdb.set_trace() # DEBUG # pdb.set_trace() # DEBUG
ScriptDB.objects.validate() # just to be sure all is synced ScriptDB.objects.validate() # just to be sure all is synced
caller.msg(string)
else: else:
# multiple matches. # multiple matches.
string = "Multiple script matches. Please refine your search:\n" EvMore(caller, scripts, page_formatter=format_script_list)
string += format_script_list(scripts) 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
nr_started, nr_stopped = ScriptDB.objects.validate(scripts=scripts) nr_started, nr_stopped = ScriptDB.objects.validate(scripts=scripts)
string = "Validated %s scripts. " % ScriptDB.objects.all().count() string = "Validated %s scripts. " % ScriptDB.objects.all().count()
string += "Started %s and stopped %s scripts." % (nr_started, nr_stopped) string += "Started %s and stopped %s scripts." % (nr_started, nr_stopped)
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.
string = format_script_list(scripts) EvMore(caller, scripts, page_formatter=format_script_list)
EvMore(caller, string)
class CmdObjects(COMMAND_DEFAULT_CLASS): class CmdObjects(COMMAND_DEFAULT_CLASS):

View file

@ -1228,13 +1228,22 @@ class TestBuilding(CommandTest):
inputs=["y"], inputs=["y"],
) )
self.call(
building.CmdSpawn(),
"/save testprot2 = {'key':'Test Char', "
"'typeclass':'evennia.objects.objects.DefaultCharacter'}",
"(Replacing `prototype_key` in prototype with given key.)|Saved prototype: testprot2",
inputs=["y"],
)
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", "")
self.call( self.call(
building.CmdSpawn(), building.CmdSpawn(),
"/save {'key':'Test Char', " "'typeclass':'evennia.objects.objects.DefaultCharacter'}", "/save {'key':'Test Char', " "'typeclass':'evennia.objects.objects.DefaultCharacter'}",
"To save a prototype it must have the 'prototype_key' set.", "A prototype_key must be given, either as `prototype_key = <prototype>` or as "
"a key 'prototype_key' inside the prototype structure.",
) )
self.call(building.CmdSpawn(), "/list", "Key ") self.call(building.CmdSpawn(), "/list", "Key ")
@ -1312,7 +1321,7 @@ class TestBuilding(CommandTest):
ball.delete() ball.delete()
# test calling spawn with an invalid prototype. # test calling spawn with an invalid prototype.
self.call(building.CmdSpawn(), "'NO_EXIST'", "No prototype named 'NO_EXIST'") self.call(building.CmdSpawn(), "'NO_EXIST'", "No prototype named 'NO_EXIST' was found.")
# Test listing commands # Test listing commands
self.call(building.CmdSpawn(), "/list", "Key ") self.call(building.CmdSpawn(), "/list", "Key ")
@ -1343,13 +1352,12 @@ class TestBuilding(CommandTest):
# spawn/edit with invalid prototype # spawn/edit with invalid prototype
msg = self.call( msg = self.call(
building.CmdSpawn(), "/edit NO_EXISTS", "No prototype 'NO_EXISTS' was found." building.CmdSpawn(), "/edit NO_EXISTS", "No prototype named 'NO_EXISTS' was found."
) )
# spawn/examine (missing prototype) # spawn/examine (missing prototype)
# lists all prototypes that exist # lists all prototypes that exist
msg = self.call(building.CmdSpawn(), "/examine") self.call(building.CmdSpawn(), "/examine", "You need to specify a prototype-key to show.")
assert "testball" in msg and "testprot" in msg
# spawn/examine with valid prototype # spawn/examine with valid prototype
# prints the prototype # prints the prototype
@ -1358,7 +1366,7 @@ class TestBuilding(CommandTest):
# spawn/examine with invalid prototype # spawn/examine with invalid prototype
# shows error # shows error
self.call(building.CmdSpawn(), "/examine NO_EXISTS", "No prototype 'NO_EXISTS' was found.") self.call(building.CmdSpawn(), "/examine NO_EXISTS", "No prototype named 'NO_EXISTS' was found.")
class TestComms(CommandTest): class TestComms(CommandTest):

View file

@ -331,7 +331,7 @@ class LanguageHandler(DefaultScript):
# find out what preceeded this word # find out what preceeded this word
wpos = match.start() wpos = match.start()
preceeding = match.string[:wpos].strip() preceeding = match.string[:wpos].strip()
start_sentence = preceeding.endswith(".") or not preceeding start_sentence = preceeding.endswith((".", "!", "?")) or not preceeding
# make up translation on the fly. Length can # make up translation on the fly. Length can
# vary from un-translated word. # vary from un-translated word.

View file

@ -2066,9 +2066,6 @@ class DefaultCharacter(DefaultObject):
# Set the supplied key as the name of the intended object # Set the supplied key as the name of the intended object
kwargs["key"] = key kwargs["key"] = key
# Get home for character
kwargs["home"] = ObjectDB.objects.get_id(kwargs.get("home", settings.DEFAULT_HOME))
# Get permissions # Get permissions
kwargs["permissions"] = kwargs.get("permissions", settings.PERMISSION_ACCOUNT_DEFAULT) kwargs["permissions"] = kwargs.get("permissions", settings.PERMISSION_ACCOUNT_DEFAULT)

View file

@ -1488,7 +1488,7 @@ def node_tags(caller):
as the |cprototype_key|n and with a category "{tag_category}". This allows the spawner to as the |cprototype_key|n and with a category "{tag_category}". This allows the spawner to
optionally update previously spawned objects when their prototype changes. optionally update previously spawned objects when their prototype changes.
""".format( """.format(
tag_category=protlib._PROTOTYPE_TAG_CATEGORY tag_category=protlib.PROTOTYPE_TAG_CATEGORY
) )
text = (text, helptext) text = (text, helptext)
@ -2131,12 +2131,13 @@ def _keep_diff(caller, **kwargs):
tmp[path[-1]] = tuple(list(tmp[path[-1]][:-1]) + ["KEEP"]) tmp[path[-1]] = tuple(list(tmp[path[-1]][:-1]) + ["KEEP"])
def _format_diff_text_and_options(diff, **kwargs): def _format_diff_text_and_options(diff, minimal=True, **kwargs):
""" """
Reformat the diff in a way suitable for the olc menu. Reformat the diff in a way suitable for the olc menu.
Args: Args:
diff (dict): A diff as produced by `prototype_diff`. diff (dict): A diff as produced by `prototype_diff`.
minimal (bool, optional): Don't show KEEPs.
Kwargs: Kwargs:
any (any): Forwarded into the generated options as arguments to the callable. any (any): Forwarded into the generated options as arguments to the callable.
@ -2150,12 +2151,15 @@ def _format_diff_text_and_options(diff, **kwargs):
def _visualize(obj, rootname, get_name=False): def _visualize(obj, rootname, get_name=False):
if utils.is_iter(obj): if utils.is_iter(obj):
if not obj:
return str(obj)
if get_name: if get_name:
return obj[0] if obj[0] else "<unset>" return obj[0] if obj[0] else "<unset>"
if rootname == "attrs": if rootname == "attrs":
return "{} |W=|n {} |W(category:|n {}|W, locks:|n {}|W)|n".format(*obj) return "{} |W=|n {} |W(category:|n {}|W, locks:|n {}|W)|n".format(*obj)
elif rootname == "tags": elif rootname == "tags":
return "{} |W(category:|n {}|W)|n".format(obj[0], obj[1]) return "{} |W(category:|n {}|W)|n".format(obj[0], obj[1])
return "{}".format(obj) return "{}".format(obj)
def _parse_diffpart(diffpart, optnum, *args): def _parse_diffpart(diffpart, optnum, *args):
@ -2166,17 +2170,33 @@ def _format_diff_text_and_options(diff, **kwargs):
rootname = args[0] rootname = args[0]
old, new, instruction = diffpart old, new, instruction = diffpart
if instruction == "KEEP": if instruction == "KEEP":
texts.append(" |gKEEP|W:|n {old}".format(old=_visualize(old, rootname))) if not minimal:
texts.append(" |gKEEP|W:|n {old}".format(old=_visualize(old, rootname)))
else: else:
# instructions we should be able to revert by a menu choice
vold = _visualize(old, rootname) vold = _visualize(old, rootname)
vnew = _visualize(new, rootname) vnew = _visualize(new, rootname)
vsep = "" if len(vold) < 78 else "\n" vsep = "" if len(vold) < 78 else "\n"
vinst = "|rREMOVE|n" if instruction == "REMOVE" else "|y{}|n".format(instruction)
texts.append( if instruction == "ADD":
" |c[{num}] {inst}|W:|n {old} |W->|n{sep} {new}".format( texts.append(" |c[{optnum}] |yADD|n: {new}".format(
inst=vinst, num=optnum, old=vold, sep=vsep, new=vnew optnum=optnum, new=_visualize(new, rootname)))
elif instruction == "REMOVE" and not new:
if rootname == "tags" and old[1] == protlib.PROTOTYPE_TAG_CATEGORY:
# special exception for the prototype-tag mechanism
# this is added post-spawn automatically and should
# not be listed as REMOVE.
return texts, options, optnum
texts.append(" |c[{optnum}] |rREMOVE|n: {old}".format(
optnum=optnum, old=_visualize(old, rootname)))
else:
vinst = "|y{}|n".format(instruction)
texts.append(
" |c[{num}] {inst}|W:|n {old} |W->|n{sep} {new}".format(
inst=vinst, num=optnum, old=vold, sep=vsep, new=vnew
)
) )
)
options.append( options.append(
{ {
"key": str(optnum), "key": str(optnum),
@ -2203,11 +2223,8 @@ def _format_diff_text_and_options(diff, **kwargs):
for root_key in sorted(diff): for root_key in sorted(diff):
diffpart = diff[root_key] diffpart = diff[root_key]
text, option, optnum = _parse_diffpart(diffpart, optnum, root_key) text, option, optnum = _parse_diffpart(diffpart, optnum, root_key)
heading = "- |w{}:|n ".format(root_key) heading = "- |w{}:|n ".format(root_key)
if root_key in ("attrs", "tags", "permissions"): if text:
texts.append(heading)
elif text:
text = [heading + text[0]] + text[1:] text = [heading + text[0]] + text[1:]
else: else:
text = [heading] text = [heading]
@ -2277,7 +2294,8 @@ def node_apply_diff(caller, **kwargs):
if not custom_location: if not custom_location:
diff.pop("location", None) diff.pop("location", None)
txt, options = _format_diff_text_and_options(diff, objects=update_objects, base_obj=base_obj) txt, options = _format_diff_text_and_options(diff, objects=update_objects,
base_obj=base_obj, prototype=prototype)
if options: if options:
text = [ text = [

View file

@ -52,10 +52,12 @@ _PROTOTYPE_RESERVED_KEYS = _PROTOTYPE_META_NAMES + (
"tags", "tags",
"attrs", "attrs",
) )
_PROTOTYPE_TAG_CATEGORY = "from_prototype" PROTOTYPE_TAG_CATEGORY = "from_prototype"
_PROTOTYPE_TAG_META_CATEGORY = "db_prototype" _PROTOTYPE_TAG_META_CATEGORY = "db_prototype"
PROT_FUNCS = {} PROT_FUNCS = {}
_PROTOTYPE_FALLBACK_LOCK = "spawn:all();edit:all()"
class PermissionError(RuntimeError): class PermissionError(RuntimeError):
pass pass
@ -84,8 +86,19 @@ def homogenize_prototype(prototype, custom_keys=None):
homogenizations like adding missing prototype_keys and setting a default typeclass. homogenizations like adding missing prototype_keys and setting a default typeclass.
""" """
if not prototype or not isinstance(prototype, dict):
return {}
reserved = _PROTOTYPE_RESERVED_KEYS + (custom_keys or ()) reserved = _PROTOTYPE_RESERVED_KEYS + (custom_keys or ())
# correct cases of setting None for certain values
for protkey in prototype:
if prototype[protkey] is None:
if protkey in ("attrs", "tags", "prototype_tags"):
prototype[protkey] = []
elif protkey in ("prototype_key", "prototype_desc"):
prototype[protkey] = ""
attrs = list(prototype.get("attrs", [])) # break reference attrs = list(prototype.get("attrs", [])) # break reference
tags = make_iter(prototype.get("tags", [])) tags = make_iter(prototype.get("tags", []))
homogenized_tags = [] homogenized_tags = []
@ -111,12 +124,14 @@ def homogenize_prototype(prototype, custom_keys=None):
# add required missing parts that had defaults before # add required missing parts that had defaults before
if "prototype_key" not in prototype: homogenized["prototype_key"] = homogenized.get("prototype_key",
# assign a random hash as key # assign a random hash as key
homogenized["prototype_key"] = "prototype-{}".format( "prototype-{}".format(
hashlib.md5(bytes(str(time.time()), "utf-8")).hexdigest()[:7] hashlib.md5(bytes(str(time.time()), "utf-8")).hexdigest()[:7]))
) homogenized["prototype_tags"] = homogenized.get("prototype_tags", [])
homogenized["prototype_locks"] = homogenized.get(
"prototype_lock", _PROTOTYPE_FALLBACK_LOCK)
homogenized["prototype_desc"] = homogenized.get("prototype_desc", "")
if "typeclass" not in prototype and "prototype_parent" not in prototype: if "typeclass" not in prototype and "prototype_parent" not in prototype:
homogenized["typeclass"] = settings.BASE_OBJECT_TYPECLASS homogenized["typeclass"] = settings.BASE_OBJECT_TYPECLASS
@ -223,14 +238,11 @@ def save_prototype(prototype):
) )
# make sure meta properties are included with defaults # make sure meta properties are included with defaults
stored_prototype = DbPrototype.objects.filter(db_key=prototype_key)
prototype = stored_prototype[0].prototype if stored_prototype else {}
in_prototype["prototype_desc"] = in_prototype.get( in_prototype["prototype_desc"] = in_prototype.get(
"prototype_desc", prototype.get("prototype_desc", "") "prototype_desc", prototype.get("prototype_desc", "")
) )
prototype_locks = in_prototype.get( prototype_locks = in_prototype.get(
"prototype_locks", prototype.get("prototype_locks", "spawn:all();edit:perm(Admin)") "prototype_locks", prototype.get("prototype_locks", _PROTOTYPE_FALLBACK_LOCK)
) )
is_valid, err = validate_lockstring(prototype_locks) is_valid, err = validate_lockstring(prototype_locks)
if not is_valid: if not is_valid:
@ -245,27 +257,26 @@ def save_prototype(prototype):
] ]
in_prototype["prototype_tags"] = prototype_tags in_prototype["prototype_tags"] = prototype_tags
prototype.update(in_prototype) stored_prototype = DbPrototype.objects.filter(db_key=prototype_key)
if stored_prototype: if stored_prototype:
# edit existing prototype # edit existing prototype
stored_prototype = stored_prototype[0] stored_prototype = stored_prototype[0]
stored_prototype.desc = prototype["prototype_desc"] stored_prototype.desc = in_prototype["prototype_desc"]
if prototype_tags: if prototype_tags:
stored_prototype.tags.clear(category=_PROTOTYPE_TAG_CATEGORY) stored_prototype.tags.clear(category=PROTOTYPE_TAG_CATEGORY)
stored_prototype.tags.batch_add(*prototype["prototype_tags"]) stored_prototype.tags.batch_add(*in_prototype["prototype_tags"])
stored_prototype.locks.add(prototype["prototype_locks"]) stored_prototype.locks.add(in_prototype["prototype_locks"])
stored_prototype.attributes.add("prototype", prototype) stored_prototype.attributes.add("prototype", in_prototype)
else: else:
# create a new prototype # create a new prototype
stored_prototype = create_script( stored_prototype = create_script(
DbPrototype, DbPrototype,
key=prototype_key, key=prototype_key,
desc=prototype["prototype_desc"], desc=in_prototype["prototype_desc"],
persistent=True, persistent=True,
locks=prototype_locks, locks=prototype_locks,
tags=prototype["prototype_tags"], tags=in_prototype["prototype_tags"],
attributes=[("prototype", prototype)], attributes=[("prototype", in_prototype)],
) )
return stored_prototype.prototype return stored_prototype.prototype
@ -410,7 +421,7 @@ def search_objects_with_prototype(prototype_key):
matches (Queryset): All matching objects spawned from this prototype. matches (Queryset): All matching objects spawned from this prototype.
""" """
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): def list_prototypes(caller, key=None, tags=None, show_non_use=False, show_non_edit=True):
@ -713,15 +724,15 @@ def prototype_to_str(prototype):
prototype_desc=prototype.get("prototype_desc", "|wNone|n"), prototype_desc=prototype.get("prototype_desc", "|wNone|n"),
prototype_parent=prototype.get("prototype_parent", "|wNone|n"), prototype_parent=prototype.get("prototype_parent", "|wNone|n"),
) )
key = aliases = attrs = tags = locks = permissions = location = home = destination = ""
key = prototype.get("key", "") if "key" in prototype:
if key: key = prototype["key"]
key = "|ckey:|n {key}".format(key=key) key = "|ckey:|n {key}".format(key=key)
aliases = prototype.get("aliases", "") if "aliases" in prototype:
if aliases: aliases = prototype["aliases"]
aliases = "|caliases:|n {aliases}".format(aliases=", ".join(aliases)) aliases = "|caliases:|n {aliases}".format(aliases=", ".join(aliases))
attrs = prototype.get("attrs", "") if "attrs" in prototype:
if attrs: attrs = prototype["attrs"]
out = [] out = []
for (attrkey, value, category, locks) in attrs: for (attrkey, value, category, locks) in attrs:
locks = ", ".join(lock for lock in locks if lock) locks = ", ".join(lock for lock in locks if lock)
@ -740,8 +751,8 @@ def prototype_to_str(prototype):
) )
) )
attrs = "|cattrs:|n\n {attrs}".format(attrs="\n ".join(out)) attrs = "|cattrs:|n\n {attrs}".format(attrs="\n ".join(out))
tags = prototype.get("tags", "") if "tags" in prototype:
if tags: tags = prototype['tags']
out = [] out = []
for (tagkey, category, data) in tags: for (tagkey, category, data) in tags:
out.append( out.append(
@ -750,20 +761,20 @@ def prototype_to_str(prototype):
) )
) )
tags = "|ctags:|n\n {tags}".format(tags=", ".join(out)) tags = "|ctags:|n\n {tags}".format(tags=", ".join(out))
locks = prototype.get("locks", "") if "locks" in prototype:
if locks: locks = prototype["locks"]
locks = "|clocks:|n\n {locks}".format(locks=locks) locks = "|clocks:|n\n {locks}".format(locks=locks)
permissions = prototype.get("permissions", "") if "permissions" in prototype:
if permissions: permissions = prototype["permissions"]
permissions = "|cpermissions:|n {perms}".format(perms=", ".join(permissions)) permissions = "|cpermissions:|n {perms}".format(perms=", ".join(permissions))
location = prototype.get("location", "") if "location" in prototype:
if location: location = prototype["location"]
location = "|clocation:|n {location}".format(location=location) location = "|clocation:|n {location}".format(location=location)
home = prototype.get("home", "") if "home" in prototype:
if home: home = prototype["home"]
home = "|chome:|n {home}".format(home=home) home = "|chome:|n {home}".format(home=home)
destination = prototype.get("destination", "") if "destination" in prototype:
if destination: destination = prototype["destination"]
destination = "|cdestination:|n {destination}".format(destination=destination) destination = "|cdestination:|n {destination}".format(destination=destination)
body = "\n".join( body = "\n".join(

View file

@ -138,13 +138,14 @@ from django.conf import settings
import evennia import evennia
from evennia.objects.models import ObjectDB from evennia.objects.models import ObjectDB
from evennia.utils import logger
from evennia.utils.utils import make_iter, is_iter from evennia.utils.utils import make_iter, is_iter
from evennia.prototypes import prototypes as protlib from evennia.prototypes import prototypes as protlib
from evennia.prototypes.prototypes import ( from evennia.prototypes.prototypes import (
value_to_obj, value_to_obj,
value_to_obj_or_any, value_to_obj_or_any,
init_spawn_value, init_spawn_value,
_PROTOTYPE_TAG_CATEGORY, PROTOTYPE_TAG_CATEGORY,
) )
@ -165,6 +166,18 @@ _PROTOTYPE_ROOT_NAMES = (
_NON_CREATE_KWARGS = _CREATE_OBJECT_KWARGS + _PROTOTYPE_META_NAMES _NON_CREATE_KWARGS = _CREATE_OBJECT_KWARGS + _PROTOTYPE_META_NAMES
class Unset:
"""
Helper class representing a non-set diff element.
"""
def __bool__(self):
return False
def __str__(self):
return "<Unset>"
# Helper # Helper
@ -268,7 +281,7 @@ def prototype_from_object(obj):
""" """
# first, check if this object already has a prototype # first, check if this object already has a prototype
prot = obj.tags.get(category=_PROTOTYPE_TAG_CATEGORY, return_list=True) prot = obj.tags.get(category=PROTOTYPE_TAG_CATEGORY, return_list=True)
if prot: if prot:
prot = protlib.search_prototype(prot[0]) prot = protlib.search_prototype(prot[0])
@ -322,9 +335,9 @@ def prototype_from_object(obj):
return prot return prot
def prototype_diff(prototype1, prototype2, maxdepth=2): def prototype_diff(prototype1, prototype2, maxdepth=2, homogenize=False, implicit_keep=False):
""" """
A 'detailed' diff specifies differences down to individual sub-sectiions A 'detailed' diff specifies differences down to individual sub-sections
of the prototype, like individual attributes, permissions etc. It is used of the prototype, like individual attributes, permissions etc. It is used
by the menu to allow a user to customize what should be kept. by the menu to allow a user to customize what should be kept.
@ -334,6 +347,12 @@ def prototype_diff(prototype1, prototype2, maxdepth=2):
maxdepth (int, optional): The maximum depth into the diff we go before treating the elements maxdepth (int, optional): The maximum depth into the diff we go before treating the elements
of iterables as individual entities to compare. This is important since a single of iterables as individual entities to compare. This is important since a single
attr/tag (for example) are represented by a tuple. attr/tag (for example) are represented by a tuple.
homogenize (bool, optional): Auto-homogenize both prototypes for the best comparison.
This is most useful for displaying.
implicit_keep (bool, optional): If set, the resulting diff will assume KEEP unless the new
prototype explicitly change them. That is, if a key exists in `prototype1` and
not in `prototype2`, it will not be REMOVEd but set to KEEP instead. This is particularly
useful for auto-generated prototypes when updating objects.
Returns: Returns:
diff (dict): A structure detailing how to convert prototype1 to prototype2. All diff (dict): A structure detailing how to convert prototype1 to prototype2. All
@ -344,12 +363,16 @@ def prototype_diff(prototype1, prototype2, maxdepth=2):
instruction can be one of "REMOVE", "ADD", "UPDATE" or "KEEP". instruction can be one of "REMOVE", "ADD", "UPDATE" or "KEEP".
""" """
_unset = Unset()
def _recursive_diff(old, new, depth=0): def _recursive_diff(old, new, depth=0):
old_type = type(old) old_type = type(old)
new_type = type(new) new_type = type(new)
if old_type == new_type and not (old or new):
# both old and new are unset, like [] or None
return (None, None, "KEEP")
if old_type != new_type: if old_type != new_type:
if old and not new: if old and not new:
if depth < maxdepth and old_type == dict: if depth < maxdepth and old_type == dict:
@ -358,6 +381,9 @@ def prototype_diff(prototype1, prototype2, maxdepth=2):
return { return {
part[0] if is_iter(part) else part: (part, None, "REMOVE") for part in old part[0] if is_iter(part) else part: (part, None, "REMOVE") for part in old
} }
if isinstance(new, Unset) and implicit_keep:
# the new does not define any change, use implicit-keep
return (old, None, "KEEP")
return (old, new, "REMOVE") return (old, new, "REMOVE")
elif not old and new: elif not old and new:
if depth < maxdepth and new_type == dict: if depth < maxdepth and new_type == dict:
@ -371,7 +397,7 @@ def prototype_diff(prototype1, prototype2, maxdepth=2):
elif depth < maxdepth and new_type == dict: elif depth < maxdepth and new_type == dict:
all_keys = set(list(old.keys()) + list(new.keys())) all_keys = set(list(old.keys()) + list(new.keys()))
return { return {
key: _recursive_diff(old.get(key), new.get(key), depth=depth + 1) key: _recursive_diff(old.get(key, _unset), new.get(key, _unset), depth=depth + 1)
for key in all_keys for key in all_keys
} }
elif depth < maxdepth and is_iter(new): elif depth < maxdepth and is_iter(new):
@ -379,7 +405,7 @@ def prototype_diff(prototype1, prototype2, maxdepth=2):
new_map = {part[0] if is_iter(part) else part: part for part in new} new_map = {part[0] if is_iter(part) else part: part for part in new}
all_keys = set(list(old_map.keys()) + list(new_map.keys())) all_keys = set(list(old_map.keys()) + list(new_map.keys()))
return { return {
key: _recursive_diff(old_map.get(key), new_map.get(key), depth=depth + 1) key: _recursive_diff(old_map.get(key, _unset), new_map.get(key, _unset), depth=depth + 1)
for key in all_keys for key in all_keys
} }
elif old != new: elif old != new:
@ -387,7 +413,10 @@ def prototype_diff(prototype1, prototype2, maxdepth=2):
else: else:
return (old, new, "KEEP") return (old, new, "KEEP")
diff = _recursive_diff(prototype1, prototype2) prot1 = protlib.homogenize_prototype(prototype1) if homogenize else prototype1
prot2 = protlib.homogenize_prototype(prototype2) if homogenize else prototype2
diff = _recursive_diff(prot1, prot2)
return diff return diff
@ -460,7 +489,7 @@ def flatten_diff(diff):
return flat_diff return flat_diff
def prototype_diff_from_object(prototype, obj): def prototype_diff_from_object(prototype, obj, implicit_keep=True):
""" """
Get a simple diff for a prototype compared to an object which may or may not already have a Get a simple diff for a prototype compared to an object which may or may not already have a
prototype (or has one but changed locally). For more complex migratations a manual diff may be prototype (or has one but changed locally). For more complex migratations a manual diff may be
@ -474,6 +503,11 @@ def prototype_diff_from_object(prototype, obj):
diff (dict): Mapping for every prototype key: {"keyname": "REMOVE|UPDATE|KEEP", ...} diff (dict): Mapping for every prototype key: {"keyname": "REMOVE|UPDATE|KEEP", ...}
obj_prototype (dict): The prototype calculated for the given object. The diff is how to obj_prototype (dict): The prototype calculated for the given object. The diff is how to
convert this prototype into the new prototype. convert this prototype into the new prototype.
implicit_keep (bool, optional): This is usually what one wants for object updating. When
set, this means the prototype diff will assume KEEP on differences
between the object-generated prototype and that which is not explicitly set in the
new prototype. This means e.g. that even though the object has a location, and the
prototype does not specify the location, it will not be unset.
Notes: Notes:
The `diff` is on the following form: The `diff` is on the following form:
@ -486,11 +520,87 @@ def prototype_diff_from_object(prototype, obj):
""" """
obj_prototype = prototype_from_object(obj) obj_prototype = prototype_from_object(obj)
diff = prototype_diff(obj_prototype, protlib.homogenize_prototype(prototype)) diff = prototype_diff(obj_prototype, protlib.homogenize_prototype(prototype),
implicit_keep=implicit_keep)
return diff, obj_prototype return diff, obj_prototype
def batch_update_objects_with_prototype(prototype, diff=None, objects=None): def format_diff(diff, minimal=True):
"""
Reformat a diff for presentation. This is a shortened version
of the olc _format_diff_text_and_options without the options.
Args:
diff (dict): A diff as produced by `prototype_diff`.
minimal (bool, optional): Only show changes (remove KEEPs)
Returns:
texts (str): The formatted text.
"""
valid_instructions = ("KEEP", "REMOVE", "ADD", "UPDATE")
def _visualize(obj, rootname, get_name=False):
if is_iter(obj):
if not obj:
return str(obj)
if get_name:
return obj[0] if obj[0] else "<unset>"
if rootname == "attrs":
return "{} |w=|n {} |w(category:|n |n{}|w, locks:|n {}|w)|n".format(*obj)
elif rootname == "tags":
return "{} |w(category:|n {}|w)|n".format(obj[0], obj[1])
return "{}".format(obj)
def _parse_diffpart(diffpart, rootname):
typ = type(diffpart)
texts = []
if typ == tuple and len(diffpart) == 3 and diffpart[2] in valid_instructions:
old, new, instruction = diffpart
if instruction == "KEEP":
if not minimal:
texts.append(" |gKEEP|n: {old}".format(old=_visualize(old, rootname)))
elif instruction == "ADD":
texts.append(" |yADD|n: {new}".format(new=_visualize(new, rootname)))
elif instruction == "REMOVE" and not new:
texts.append(" |rREMOVE|n: {old}".format(old=_visualize(old, rootname)))
else:
vold = _visualize(old, rootname)
vnew = _visualize(new, rootname)
vsep = "" if len(vold) < 78 else "\n"
vinst = " |rREMOVE|n" if instruction == "REMOVE" else "|y{}|n".format(instruction)
varrow = "|r->|n" if instruction == "REMOVE" else "|y->|n"
texts.append(
" {inst}|W:|n {old} |W{varrow}|n{sep} {new}".format(
inst=vinst, old=vold, varrow=varrow, sep=vsep, new=vnew
)
)
else:
for key in sorted(list(diffpart.keys())):
subdiffpart = diffpart[key]
text = _parse_diffpart(subdiffpart, rootname)
texts.extend(text)
return texts
texts = []
for root_key in sorted(diff):
diffpart = diff[root_key]
text = _parse_diffpart(diffpart, root_key)
if text or not minimal:
heading = "- |w{}:|n\n".format(root_key)
if text:
text = [heading + text[0]] + text[1:]
else:
text = [heading]
texts.extend(text)
return "\n ".join(line for line in texts if line)
def batch_update_objects_with_prototype(prototype, diff=None, objects=None, exact=False):
""" """
Update existing objects with the latest version of the prototype. Update existing objects with the latest version of the prototype.
@ -501,6 +611,12 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None):
If not given this will be constructed from the first object found. If not given this will be constructed from the first object found.
objects (list, optional): List of objects to update. If not given, query for these objects (list, optional): List of objects to update. If not given, query for these
objects using the prototype's `prototype_key`. objects using the prototype's `prototype_key`.
exact (bool, optional): By default (`False`), keys not explicitly in the prototype will
not be applied to the object, but will be retained as-is. This is usually what is
expected - for example, one usually do not want to remove the object's location even
if it's not set in the prototype. With `exact=True`, all un-specified properties of the
objects will be removed if they exist. This will lead to a more accurate 1:1 correlation
between the object and the prototype but is usually impractical.
Returns: Returns:
changed (int): The number of objects that had changes applied to them. changed (int): The number of objects that had changes applied to them.
@ -515,7 +631,7 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None):
prototype_key = new_prototype["prototype_key"] prototype_key = new_prototype["prototype_key"]
if not objects: if not objects:
objects = ObjectDB.objects.get_by_tag(prototype_key, category=_PROTOTYPE_TAG_CATEGORY) objects = ObjectDB.objects.get_by_tag(prototype_key, category=PROTOTYPE_TAG_CATEGORY)
if not objects: if not objects:
return 0 return 0
@ -525,104 +641,117 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None):
# make sure the diff is flattened # make sure the diff is flattened
diff = flatten_diff(diff) diff = flatten_diff(diff)
changed = 0 changed = 0
for obj in objects: for obj in objects:
do_save = False do_save = False
old_prot_key = obj.tags.get(category=_PROTOTYPE_TAG_CATEGORY, return_list=True) old_prot_key = obj.tags.get(category=PROTOTYPE_TAG_CATEGORY, return_list=True)
old_prot_key = old_prot_key[0] if old_prot_key else None old_prot_key = old_prot_key[0] if old_prot_key else None
if prototype_key != old_prot_key:
obj.tags.clear(category=_PROTOTYPE_TAG_CATEGORY)
obj.tags.add(prototype_key, category=_PROTOTYPE_TAG_CATEGORY)
for key, directive in diff.items(): try:
if directive in ("UPDATE", "REPLACE"): for key, directive in diff.items():
if key in _PROTOTYPE_META_NAMES: if key not in new_prototype and not exact:
# prototype meta keys are not stored on-object # we don't update the object if the prototype does not actually
# contain the key (the diff will report REMOVE but we ignore it
# since exact=False)
continue continue
val = new_prototype[key] if directive in ("UPDATE", "REPLACE"):
do_save = True
if key == "key": if key in _PROTOTYPE_META_NAMES:
obj.db_key = init_spawn_value(val, str) # prototype meta keys are not stored on-object
elif key == "typeclass": continue
obj.db_typeclass_path = init_spawn_value(val, str)
elif key == "location": val = new_prototype[key]
obj.db_location = init_spawn_value(val, value_to_obj) do_save = True
elif key == "home":
obj.db_home = init_spawn_value(val, value_to_obj) if key == "key":
elif key == "destination": obj.db_key = init_spawn_value(val, str)
obj.db_destination = init_spawn_value(val, value_to_obj) elif key == "typeclass":
elif key == "locks": obj.db_typeclass_path = init_spawn_value(val, str)
if directive == "REPLACE": elif key == "location":
obj.locks.clear() obj.db_location = init_spawn_value(val, value_to_obj)
obj.locks.add(init_spawn_value(val, str)) elif key == "home":
elif key == "permissions": obj.db_home = init_spawn_value(val, value_to_obj)
if directive == "REPLACE": elif key == "destination":
obj.permissions.clear() obj.db_destination = init_spawn_value(val, value_to_obj)
obj.permissions.batch_add(*(init_spawn_value(perm, str) for perm in val)) elif key == "locks":
elif key == "aliases": if directive == "REPLACE":
if directive == "REPLACE": obj.locks.clear()
obj.aliases.clear() obj.locks.add(init_spawn_value(val, str))
obj.aliases.batch_add(*(init_spawn_value(alias, str) for alias in val)) elif key == "permissions":
elif key == "tags": if directive == "REPLACE":
if directive == "REPLACE": obj.permissions.clear()
obj.tags.clear() obj.permissions.batch_add(*(init_spawn_value(perm, str) for perm in val))
obj.tags.batch_add( elif key == "aliases":
*( if directive == "REPLACE":
(init_spawn_value(ttag, str), tcategory, tdata) obj.aliases.clear()
for ttag, tcategory, tdata in val obj.aliases.batch_add(*(init_spawn_value(alias, str) for alias in val))
) elif key == "tags":
) if directive == "REPLACE":
elif key == "attrs": obj.tags.clear()
if directive == "REPLACE": obj.tags.batch_add(
obj.attributes.clear() *(
obj.attributes.batch_add( (init_spawn_value(ttag, str), tcategory, tdata)
*( for ttag, tcategory, tdata in val
(
init_spawn_value(akey, str),
init_spawn_value(aval, value_to_obj),
acategory,
alocks,
) )
for akey, aval, acategory, alocks in val
) )
) elif key == "attrs":
elif key == "exec": if directive == "REPLACE":
# we don't auto-rerun exec statements, it would be huge security risk! obj.attributes.clear()
pass obj.attributes.batch_add(
else: *(
obj.attributes.add(key, init_spawn_value(val, value_to_obj)) (
elif directive == "REMOVE": init_spawn_value(akey, str),
do_save = True init_spawn_value(aval, value_to_obj),
if key == "key": acategory,
obj.db_key = "" alocks,
elif key == "typeclass": )
# fall back to default for akey, aval, acategory, alocks in val
obj.db_typeclass_path = settings.BASE_OBJECT_TYPECLASS )
elif key == "location": )
obj.db_location = None elif key == "exec":
elif key == "home": # we don't auto-rerun exec statements, it would be huge security risk!
obj.db_home = None pass
elif key == "destination": else:
obj.db_destination = None obj.attributes.add(key, init_spawn_value(val, value_to_obj))
elif key == "locks": elif directive == "REMOVE":
obj.locks.clear() do_save = True
elif key == "permissions": if key == "key":
obj.permissions.clear() obj.db_key = ""
elif key == "aliases": elif key == "typeclass":
obj.aliases.clear() # fall back to default
elif key == "tags": obj.db_typeclass_path = settings.BASE_OBJECT_TYPECLASS
obj.tags.clear() elif key == "location":
elif key == "attrs": obj.db_location = None
obj.attributes.clear() elif key == "home":
elif key == "exec": obj.db_home = None
# we don't auto-rerun exec statements, it would be huge security risk! elif key == "destination":
pass obj.db_destination = None
else: elif key == "locks":
obj.attributes.remove(key) obj.locks.clear()
elif key == "permissions":
obj.permissions.clear()
elif key == "aliases":
obj.aliases.clear()
elif key == "tags":
obj.tags.clear()
elif key == "attrs":
obj.attributes.clear()
elif key == "exec":
# we don't auto-rerun exec statements, it would be huge security risk!
pass
else:
obj.attributes.remove(key)
except Exception:
logger.log_trace(f"Failed to apply prototype '{prototype_key}' to {obj}.")
finally:
# we must always make sure to re-add the prototype tag
obj.tags.clear(category=PROTOTYPE_TAG_CATEGORY)
obj.tags.add(prototype_key, category=PROTOTYPE_TAG_CATEGORY)
if do_save: if do_save:
changed += 1 changed += 1
obj.save() obj.save()
@ -707,7 +836,7 @@ def spawn(*prototypes, **kwargs):
Args: Args:
prototypes (str or dict): Each argument should either be a prototypes (str or dict): Each argument should either be a
prototype_key (will be used to find the prototype) or a full prototype prototype_key (will be used to find the prototype) or a full prototype
dictionary. These will be batched-spawned as one object each. dictionary. These will be batched-spawned as one object each.
Kwargs: Kwargs:
prototype_modules (str or list): A python-path to a prototype prototype_modules (str or list): A python-path to a prototype
module, or a list of such paths. These will be used to build module, or a list of such paths. These will be used to build
@ -804,7 +933,7 @@ def spawn(*prototypes, **kwargs):
prototype_key = prototype.get("prototype_key", None) prototype_key = prototype.get("prototype_key", None)
if prototype_key: if prototype_key:
# we make sure to add a tag identifying which prototype created this object # we make sure to add a tag identifying which prototype created this object
tags.append((prototype_key, _PROTOTYPE_TAG_CATEGORY)) tags.append((prototype_key, PROTOTYPE_TAG_CATEGORY))
val = prot.pop("exec", "") val = prot.pop("exec", "")
execs = init_spawn_value(val, make_iter) execs = init_spawn_value(val, make_iter)

View file

@ -11,7 +11,7 @@ from evennia.utils.test_resources import EvenniaTest
from evennia.utils.tests.test_evmenu import TestEvMenu from evennia.utils.tests.test_evmenu import TestEvMenu
from evennia.prototypes import spawner, prototypes as protlib from evennia.prototypes import spawner, prototypes as protlib
from evennia.prototypes import menus as olc_menus from evennia.prototypes import menus as olc_menus
from evennia.prototypes import protfuncs as protofuncs from evennia.prototypes import protfuncs as protofuncs, spawner
from evennia.prototypes.prototypes import _PROTOTYPE_TAG_META_CATEGORY from evennia.prototypes.prototypes import _PROTOTYPE_TAG_META_CATEGORY
@ -212,22 +212,21 @@ class TestUtils(EvenniaTest):
"puppet:pperm(Developer);tell:perm(Admin);view:all()", "puppet:pperm(Developer);tell:perm(Admin);view:all()",
"KEEP", "KEEP",
), ),
"prototype_tags": {}, "prototype_tags": (None, None, 'KEEP'),
"attrs": { "attrs": {
"oldtest": ( "oldtest": (
("oldtest", "to_keep", None, ""), ("oldtest", "to_keep", None, ""),
("oldtest", "to_keep", None, ""), ("oldtest", "to_keep", None, ""),
"KEEP", "KEEP",
), ),
"test": (("test", "testval", None, ""), None, "REMOVE"), "desc": (("desc", "changed desc", None, ""), None, "KEEP"),
"desc": (("desc", "changed desc", None, ""), None, "REMOVE"), "fooattr": (Something, ("fooattr", "fooattrval", None, ""), "ADD"),
"fooattr": (None, ("fooattr", "fooattrval", None, ""), "ADD"),
"test": ( "test": (
("test", "testval", None, ""), ("test", "testval", None, ""),
("test", "testval_changed", None, ""), ("test", "testval_changed", None, ""),
"UPDATE", "UPDATE",
), ),
"new": (None, ("new", "new_val", None, ""), "ADD"), "new": (Something, ("new", "new_val", None, ""), "ADD"),
}, },
"key": ("Obj", "Obj", "KEEP"), "key": ("Obj", "Obj", "KEEP"),
"typeclass": ( "typeclass": (
@ -246,7 +245,7 @@ class TestUtils(EvenniaTest):
spawner.flatten_diff(pdiff), spawner.flatten_diff(pdiff),
{ {
"aliases": "REMOVE", "aliases": "REMOVE",
"attrs": "REPLACE", "attrs": "UPDATE",
"home": "KEEP", "home": "KEEP",
"key": "KEEP", "key": "KEEP",
"location": "KEEP", "location": "KEEP",
@ -270,7 +269,9 @@ class TestUtils(EvenniaTest):
new_prot = spawner.prototype_from_object(self.obj1) new_prot = spawner.prototype_from_object(self.obj1)
self.assertEqual( self.assertEqual(
{ {
"aliases": ['foo'],
"attrs": [ "attrs": [
("desc", "changed desc", None, ""),
("fooattr", "fooattrval", None, ""), ("fooattr", "fooattrval", None, ""),
("new", "new_val", None, ""), ("new", "new_val", None, ""),
("oldtest", "to_keep", None, ""), ("oldtest", "to_keep", None, ""),
@ -293,6 +294,9 @@ class TestUtils(EvenniaTest):
"view:all()", "view:all()",
] ]
), ),
'tags': [
('footag', 'foocategory', None),
(Something, 'from_prototype', None)],
"permissions": ["builder"], "permissions": ["builder"],
"prototype_desc": "Built from Obj", "prototype_desc": "Built from Obj",
"prototype_key": Something, "prototype_key": Something,
@ -851,7 +855,7 @@ class TestMenuModule(EvenniaTest):
self.assertEqual(obj.typeclass_path, "evennia.objects.objects.DefaultObject") self.assertEqual(obj.typeclass_path, "evennia.objects.objects.DefaultObject")
self.assertEqual( self.assertEqual(
obj.tags.get(category=spawner._PROTOTYPE_TAG_CATEGORY), self.test_prot["prototype_key"] obj.tags.get(category=spawner.PROTOTYPE_TAG_CATEGORY), self.test_prot["prototype_key"]
) )
# update helpers # update helpers
@ -912,24 +916,20 @@ class TestMenuModule(EvenniaTest):
texts, options = olc_menus._format_diff_text_and_options(obj_diff) texts, options = olc_menus._format_diff_text_and_options(obj_diff)
self.assertEqual( self.assertEqual(
"\n".join(texts), "\n".join(txt.strip() for txt in texts),
"- |wattrs:|n \n" "- |wattrs:|n |c[1] |yADD|n: foo |W=|n bar |W(category:|n None|W, locks:|n |W)|n"
" |gKEEP|W:|n desc |W=|n This is User #1. |W(category:|n None|W, locks:|n |W)|n\n" "\n- |whome:|n"
" |c[1] |yADD|n|W:|n None |W->|n foo |W=|n bar |W(category:|n None|W, locks:|n |W)|n\n" "\n- |wkey:|n"
" |gKEEP|W:|n prelogout_location |W=|n #2 |W(category:|n None|W, locks:|n |W)|n\n" "\n- |wlocks:|n"
"- |whome:|n |gKEEP|W:|n #2\n" "\n- |wpermissions:|n"
"- |wkey:|n |gKEEP|W:|n TestChar\n" "\n- |wprototype_desc:|n |c[2] |rREMOVE|n: Testobject build"
"- |wlocks:|n |gKEEP|W:|n boot:false();call:false();control:perm(Developer);delete:false();edit:false();examine:perm(Developer);get:false();msg:all();puppet:false();tell:perm(Admin);view:all()\n" "\n- |wprototype_key:|n"
"- |wpermissions:|n \n" "\n- |wprototype_locks:|n"
" |gKEEP|W:|n developer\n" "\n- |wprototype_tags:|n"
"- |wprototype_desc:|n |c[2] |rREMOVE|n|W:|n Testobject build |W->|n None\n" "\n- |wtags:|n |c[3] |yADD|n: foo |W(category:|n None|W)|n"
"- |wprototype_key:|n |gKEEP|W:|n TestDiffKey\n" "\n- |wtypeclass:|n"
"- |wprototype_locks:|n |gKEEP|W:|n spawn:all();edit:all()\n"
"- |wprototype_tags:|n \n"
"- |wtags:|n \n"
" |c[3] |yADD|n|W:|n None |W->|n foo |W(category:|n None|W)|n\n"
"- |wtypeclass:|n |gKEEP|W:|n typeclasses.characters.Character",
) )
self.assertEqual( self.assertEqual(
options, options,
[ [

View file

@ -70,14 +70,15 @@ class HTTPChannelWithXForwardedFor(http.HTTPChannel):
Check to see if this is a reverse proxied connection. Check to see if this is a reverse proxied connection.
""" """
CLIENT = 0 if self.requests:
http.HTTPChannel.allHeadersReceived(self) CLIENT = 0
req = self.requests[-1] http.HTTPChannel.allHeadersReceived(self)
client_ip, port = self.transport.client req = self.requests[-1]
proxy_chain = req.getHeader("X-FORWARDED-FOR") client_ip, port = self.transport.client
if proxy_chain and client_ip in _UPSTREAM_IPS: proxy_chain = req.getHeader("X-FORWARDED-FOR")
forwarded = proxy_chain.split(", ", 1)[CLIENT] if proxy_chain and client_ip in _UPSTREAM_IPS:
self.transport.client = (forwarded, port) forwarded = proxy_chain.split(", ", 1)[CLIENT]
self.transport.client = (forwarded, port)
# Monkey-patch Twisted to handle X-Forwarded-For. # Monkey-patch Twisted to handle X-Forwarded-For.

View file

@ -59,7 +59,7 @@ class Attribute(SharedMemoryModel):
# Attribute Database Model setup # Attribute Database Model setup
# #
# These database fields are all set using their corresponding properties, # These database fields are all set using their corresponding properties,
# named same as the field, but withtout the db_* prefix. # named same as the field, but without the db_* prefix.
db_key = models.CharField("key", max_length=255, db_index=True) db_key = models.CharField("key", max_length=255, db_index=True)
db_value = PickledObjectField( db_value = PickledObjectField(
"value", "value",

View file

@ -821,6 +821,8 @@ class ANSIString(str, metaclass=ANSIMeta):
by a number. by a number.
""" """
if not offset:
return []
return [i + offset for i in iterable] return [i + offset for i in iterable]
@classmethod @classmethod
@ -1063,7 +1065,7 @@ class ANSIString(str, metaclass=ANSIMeta):
clean_string = self._clean_string * other clean_string = self._clean_string * other
code_indexes = self._code_indexes[:] code_indexes = self._code_indexes[:]
char_indexes = self._char_indexes[:] char_indexes = self._char_indexes[:]
for i in range(1, other + 1): for i in range(other):
code_indexes.extend(self._shifter(self._code_indexes, i * len(self._raw_string))) code_indexes.extend(self._shifter(self._code_indexes, i * len(self._raw_string)))
char_indexes.extend(self._shifter(self._char_indexes, i * len(self._raw_string))) char_indexes.extend(self._shifter(self._char_indexes, i * len(self._raw_string)))
return ANSIString( return ANSIString(

View file

@ -486,8 +486,8 @@ def create_account(
Args: Args:
key (str): The account's name. This should be unique. key (str): The account's name. This should be unique.
email (str or None): Email on valid addr@addr.domain form. If email (str or None): Email on valid addr@addr.domain form. If
the empty string, will be set to None. the empty string, will be set to None.
password (str): Password in cleartext. password (str): Password in cleartext.
Kwargs: Kwargs:

View file

@ -28,9 +28,10 @@ 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 evennia import Command, CmdSet from evennia import Command, CmdSet
from evennia.commands import cmdhandler from evennia.commands import cmdhandler
from evennia.utils.utils import justify, make_iter from evennia.utils.utils import make_iter, inherits_from, justify
_CMD_NOMATCH = cmdhandler.CMD_NOMATCH _CMD_NOMATCH = cmdhandler.CMD_NOMATCH
_CMD_NOINPUT = cmdhandler.CMD_NOINPUT _CMD_NOINPUT = cmdhandler.CMD_NOINPUT
@ -117,6 +118,11 @@ class CmdSetMore(CmdSet):
self.add(CmdMoreLook()) self.add(CmdMoreLook())
# resources for handling queryset inputs
def queryset_maxsize(qs):
return qs.count()
class EvMore(object): class EvMore(object):
""" """
The main pager object The main pager object
@ -132,6 +138,7 @@ class EvMore(object):
justify_kwargs=None, justify_kwargs=None,
exit_on_lastpage=False, exit_on_lastpage=False,
exit_cmd=None, exit_cmd=None,
page_formatter=str,
**kwargs, **kwargs,
): ):
@ -149,7 +156,7 @@ class EvMore(object):
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 `text` 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 repr() (so one could pass a list of objects). 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 `text` is too big
to fit the screen. to fit the screen.
@ -168,6 +175,12 @@ 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:
@ -186,13 +199,7 @@ class EvMore(object):
""" """
self._caller = caller self._caller = caller
self._kwargs = kwargs self._always_page = always_page
self._pages = []
self._npages = 1
self._npos = 0
self.exit_on_lastpage = exit_on_lastpage
self.exit_cmd = exit_cmd
self._exit_msg = "Exited |wmore|n pager."
if not session: if not session:
# if not supplied, use the first session to # if not supplied, use the first session to
@ -203,81 +210,141 @@ class EvMore(object):
session = sessions[0] session = sessions[0]
self._session = session self._session = session
self._justify = justify
self._justify_kwargs = justify_kwargs
self.exit_on_lastpage = exit_on_lastpage
self.exit_cmd = exit_cmd
self._exit_msg = "Exited |wmore|n pager."
self._page_formatter = page_formatter
self._kwargs = kwargs
self._data = None
self._paginator = None
self._pages = []
self._npages = 1
self._npos = 0
# 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)
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
self.height = min(10000 // max(1, self.width), height)
if hasattr(text, "table") and hasattr(text, "get"): if inherits_from(text, "evennia.utils.evtable.EvTable"):
# This is an EvTable. # an EvTable
self.init_evtable(text)
table = text elif isinstance(text, QuerySet):
# a queryset
if table.height: self.init_queryset(text)
# enforced height of each paged table, plus space for evmore extras elif not isinstance(text, str):
height = table.height - 4 # anything else not a str
self.init_iterable(text)
# convert table to string elif "\f" in text:
text = str(text) # string with \f line-break markers in it
justify_kwargs = None # enforce self.init_f_str(text)
if not isinstance(text, str):
# not a string - pre-set pages of some form
text = "\n".join(str(repr(element)) for element in make_iter(text))
if "\f" in text:
# 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.
self._pages = text.split("\f")
self._npages = len(self._pages)
else: else:
if justify: # a string
# we must break very long lines into multiple ones. Note that this self.init_str(text)
# will also remove spurious whitespace.
justify_kwargs = justify_kwargs or {}
width = justify_kwargs.get("width", width)
justify_kwargs["width"] = width
justify_kwargs["align"] = justify_kwargs.get("align", "l")
justify_kwargs["indent"] = justify_kwargs.get("indent", 0)
lines = [] # kick things into gear
for line in text.split("\n"): self.start()
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")
# always limit number of chars to 10 000 per page # page formatter
height = min(10000 // max(1, width), height)
# figure out the pagination def format_page(self, page):
self._pages = ["\n".join(lines[i : i + height]) for i in range(0, len(lines), height)] """
self._npages = len(self._pages) Page formatter. Uses the page_formatter callable by default.
This allows to easier override the class if needed.
"""
return self._page_formatter(page)
if self._npages <= 1 and not always_page: # paginators - responsible for extracting a specific page number
# no need for paging; just pass-through.
caller.msg(text=self._get_page(0), session=self._session, **kwargs) 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_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.
"""
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: else:
# go into paging mode # no justification. Simple division by line
# first pass on the msg kwargs lines = text.split("\n")
caller.ndb._more = self
caller.cmdset.add(CmdSetMore)
# goto top of the text self._data = ["\n".join(lines[i: i + self.height])
self.page_top() for i in range(0, len(lines), self.height)]
self._npages = len(self._data)
self._paginator = self.paginator_index
def _get_page(self, pos): # display helpers and navigation
return self._pages[pos]
def display(self, show_footer=True): def display(self, show_footer=True):
""" """
Pretty-print the page. Pretty-print the page.
""" """
pos = self._npos pos = self._npos
text = self._get_page(pos) text = self.format_page(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:
@ -340,6 +407,22 @@ class EvMore(object):
if self.exit_cmd: if self.exit_cmd:
self._caller.execute_cmd(self.exit_cmd, session=self._session) self._caller.execute_cmd(self.exit_cmd, session=self._session)
def start(self):
"""
Starts the pagination
"""
if self._npages <= 1 and not self._always_page:
# no need for paging; just pass-through.
self.display(show_footer=False)
else:
# go into paging mode
# first pass on the msg kwargs
self._caller.ndb._more = self
self._caller.cmdset.add(CmdSetMore)
# goto top of the text
self.page_top()
# helper function # helper function

View file

@ -1113,7 +1113,7 @@ class HelpDetailView(HelpMixin, EvenniaDetailView):
# Check if this object was requested in a valid manner # Check if this object was requested in a valid manner
if not obj: if not obj:
raise HttpResponseBadRequest( return HttpResponseBadRequest(
"No %(verbose_name)s found matching the query" "No %(verbose_name)s found matching the query"
% {"verbose_name": queryset.model._meta.verbose_name} % {"verbose_name": queryset.model._meta.verbose_name}
) )