mirror of
https://github.com/evennia/evennia.git
synced 2026-03-31 21:17:17 +02:00
Cleanup, bug fixes, refactoring
This commit is contained in:
parent
174113b9ab
commit
a29b46d091
5 changed files with 404 additions and 302 deletions
|
|
@ -1092,7 +1092,7 @@ def _add_attr(caller, attr_string, **kwargs):
|
|||
attrname, category = nameparts
|
||||
elif nparts > 2:
|
||||
attrname, category, locks = nameparts
|
||||
attr_tuple = (attrname, value, category, locks)
|
||||
attr_tuple = (attrname, value, category, str(locks))
|
||||
|
||||
if attrname:
|
||||
prot = _get_menu_prototype(caller)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"""
|
||||
|
||||
Handling storage of prototypes, both database-based ones (DBPrototypes) and those defined in modules
|
||||
(Read-only prototypes).
|
||||
(Read-only prototypes). Also contains utility functions, formatters and manager functions.
|
||||
|
||||
"""
|
||||
|
||||
|
|
@ -31,7 +31,6 @@ _PROTOTYPE_TAG_CATEGORY = "from_prototype"
|
|||
_PROTOTYPE_TAG_META_CATEGORY = "db_prototype"
|
||||
PROT_FUNCS = {}
|
||||
|
||||
|
||||
_RE_DBREF = re.compile(r"(?<!\$obj\()(#[0-9]+)")
|
||||
|
||||
|
||||
|
|
@ -46,248 +45,29 @@ class ValidationError(RuntimeError):
|
|||
pass
|
||||
|
||||
|
||||
# Protfunc parsing
|
||||
|
||||
for mod in settings.PROT_FUNC_MODULES:
|
||||
try:
|
||||
callables = callables_from_module(mod)
|
||||
PROT_FUNCS.update(callables)
|
||||
except ImportError:
|
||||
logger.log_trace()
|
||||
raise
|
||||
|
||||
|
||||
def protfunc_parser(value, available_functions=None, testing=False, stacktrace=False, **kwargs):
|
||||
def homogenize_prototype(prototype, custom_keys=None):
|
||||
"""
|
||||
Parse a prototype value string for a protfunc and process it.
|
||||
|
||||
Available protfuncs are specified as callables in one of the modules of
|
||||
`settings.PROTFUNC_MODULES`, or specified on the command line.
|
||||
Homogenize the more free-form prototype (where undefined keys are non-category attributes)
|
||||
into the stricter form using `attrs` required by the system.
|
||||
|
||||
Args:
|
||||
value (any): The value to test for a parseable protfunc. Only strings will be parsed for
|
||||
protfuncs, all other types are returned as-is.
|
||||
available_functions (dict, optional): Mapping of name:protfunction to use for this parsing.
|
||||
If not set, use default sources.
|
||||
testing (bool, optional): Passed to protfunc. If in a testing mode, some protfuncs may
|
||||
behave differently.
|
||||
stacktrace (bool, optional): If set, print the stack parsing process of the protfunc-parser.
|
||||
|
||||
Kwargs:
|
||||
session (Session): Passed to protfunc. Session of the entity spawning the prototype.
|
||||
protototype (dict): Passed to protfunc. The dict this protfunc is a part of.
|
||||
current_key(str): Passed to protfunc. The key in the prototype that will hold this value.
|
||||
any (any): Passed on to the protfunc.
|
||||
prototype (dict): Prototype.
|
||||
custom_keys (list, optional): Custom keys which should not be interpreted as attrs, beyond
|
||||
the default reserved keys.
|
||||
|
||||
Returns:
|
||||
testresult (tuple): If `testing` is set, returns a tuple (error, result) where error is
|
||||
either None or a string detailing the error from protfunc_parser or seen when trying to
|
||||
run `literal_eval` on the parsed string.
|
||||
any (any): A structure to replace the string on the prototype level. If this is a
|
||||
callable or a (callable, (args,)) structure, it will be executed as if one had supplied
|
||||
it to the prototype directly. This structure is also passed through literal_eval so one
|
||||
can get actual Python primitives out of it (not just strings). It will also identify
|
||||
eventual object #dbrefs in the output from the protfunc.
|
||||
|
||||
homogenized (dict): Prototype where all non-identified keys grouped as attributes.
|
||||
"""
|
||||
if not isinstance(value, basestring):
|
||||
try:
|
||||
value = value.dbref
|
||||
except AttributeError:
|
||||
pass
|
||||
value = to_str(value, force_string=True)
|
||||
|
||||
available_functions = PROT_FUNCS if available_functions is None else available_functions
|
||||
|
||||
# insert $obj(#dbref) for #dbref
|
||||
value = _RE_DBREF.sub("$obj(\\1)", value)
|
||||
|
||||
result = inlinefuncs.parse_inlinefunc(
|
||||
value, available_funcs=available_functions,
|
||||
stacktrace=stacktrace, testing=testing, **kwargs)
|
||||
|
||||
err = None
|
||||
try:
|
||||
result = literal_eval(result)
|
||||
except ValueError:
|
||||
pass
|
||||
except Exception as err:
|
||||
err = str(err)
|
||||
if testing:
|
||||
return err, result
|
||||
return result
|
||||
|
||||
|
||||
def format_available_protfuncs():
|
||||
"""
|
||||
Get all protfuncs in a pretty-formatted form.
|
||||
|
||||
Args:
|
||||
clr (str, optional): What coloration tag to use.
|
||||
"""
|
||||
out = []
|
||||
for protfunc_name, protfunc in PROT_FUNCS.items():
|
||||
out.append("- |c${name}|n - |W{docs}".format(
|
||||
name=protfunc_name, docs=protfunc.__doc__.strip().replace("\n", "")))
|
||||
return justify("\n".join(out), indent=8)
|
||||
|
||||
|
||||
# helper functions
|
||||
|
||||
def value_to_obj(value, force=True):
|
||||
"Always convert value(s) to Object, or None"
|
||||
stype = type(value)
|
||||
if is_iter(value):
|
||||
if stype == dict:
|
||||
return {value_to_obj_or_any(key): value_to_obj_or_any(val) for key, val in value.iter()}
|
||||
reserved = _PROTOTYPE_RESERVED_KEYS + (custom_keys or ())
|
||||
attrs = list(prototype.get('attrs', [])) # break reference
|
||||
homogenized = {}
|
||||
for key, val in prototype.items():
|
||||
if key in reserved:
|
||||
homogenized[key] = val
|
||||
else:
|
||||
return stype([value_to_obj_or_any(val) for val in value])
|
||||
return dbid_to_obj(value, ObjectDB)
|
||||
|
||||
|
||||
def value_to_obj_or_any(value):
|
||||
"Convert value(s) to Object if possible, otherwise keep original value"
|
||||
stype = type(value)
|
||||
if is_iter(value):
|
||||
if stype == dict:
|
||||
return {value_to_obj_or_any(key):
|
||||
value_to_obj_or_any(val) for key, val in value.items()}
|
||||
else:
|
||||
return stype([value_to_obj_or_any(val) for val in value])
|
||||
obj = dbid_to_obj(value, ObjectDB)
|
||||
return obj if obj is not None else value
|
||||
|
||||
|
||||
def prototype_to_str(prototype):
|
||||
"""
|
||||
Format a prototype to a nice string representation.
|
||||
|
||||
Args:
|
||||
prototype (dict): The prototype.
|
||||
"""
|
||||
|
||||
header = """
|
||||
|cprototype-key:|n {prototype_key}, |c-tags:|n {prototype_tags}, |c-locks:|n {prototype_locks}|n
|
||||
|c-desc|n: {prototype_desc}
|
||||
|cprototype-parent:|n {prototype_parent}
|
||||
\n""".format(
|
||||
prototype_key=prototype.get('prototype_key', '|r[UNSET](required)|n'),
|
||||
prototype_tags=prototype.get('prototype_tags', '|wNone|n'),
|
||||
prototype_locks=prototype.get('prototype_locks', '|wNone|n'),
|
||||
prototype_desc=prototype.get('prototype_desc', '|wNone|n'),
|
||||
prototype_parent=prototype.get('prototype_parent', '|wNone|n'))
|
||||
|
||||
key = prototype.get('key', '')
|
||||
if key:
|
||||
key = "|ckey:|n {key}".format(key=key)
|
||||
aliases = prototype.get("aliases", '')
|
||||
if aliases:
|
||||
aliases = "|caliases:|n {aliases}".format(
|
||||
aliases=", ".join(aliases))
|
||||
attrs = prototype.get("attrs", '')
|
||||
if attrs:
|
||||
out = []
|
||||
for (attrkey, value, category, locks) in attrs:
|
||||
locks = ", ".join(lock for lock in locks if lock)
|
||||
category = "|ccategory:|n {}".format(category) if category else ''
|
||||
cat_locks = ""
|
||||
if category or locks:
|
||||
cat_locks = " (|ccategory:|n {category}, ".format(
|
||||
category=category if category else "|wNone|n")
|
||||
out.append(
|
||||
"{attrkey}{cat_locks} |c=|n {value}".format(
|
||||
attrkey=attrkey,
|
||||
cat_locks=cat_locks,
|
||||
locks=locks if locks else "|wNone|n",
|
||||
value=value))
|
||||
attrs = "|cattrs:|n\n {attrs}".format(attrs="\n ".join(out))
|
||||
tags = prototype.get('tags', '')
|
||||
if tags:
|
||||
out = []
|
||||
for (tagkey, category, data) in tags:
|
||||
out.append("{tagkey} (category: {category}{dat})".format(
|
||||
tagkey=tagkey, category=category, dat=", data: {}".format(data) if data else ""))
|
||||
tags = "|ctags:|n\n {tags}".format(tags=", ".join(out))
|
||||
locks = prototype.get('locks', '')
|
||||
if locks:
|
||||
locks = "|clocks:|n\n {locks}".format(locks=locks)
|
||||
permissions = prototype.get("permissions", '')
|
||||
if permissions:
|
||||
permissions = "|cpermissions:|n {perms}".format(perms=", ".join(permissions))
|
||||
location = prototype.get("location", '')
|
||||
if location:
|
||||
location = "|clocation:|n {location}".format(location=location)
|
||||
home = prototype.get("home", '')
|
||||
if home:
|
||||
home = "|chome:|n {home}".format(home=home)
|
||||
destination = prototype.get("destination", '')
|
||||
if destination:
|
||||
destination = "|cdestination:|n {destination}".format(destination=destination)
|
||||
|
||||
body = "\n".join(part for part in (key, aliases, attrs, tags, locks, permissions,
|
||||
location, home, destination) if part)
|
||||
|
||||
return header.lstrip() + body.strip()
|
||||
|
||||
|
||||
def check_permission(prototype_key, action, default=True):
|
||||
"""
|
||||
Helper function to check access to actions on given prototype.
|
||||
|
||||
Args:
|
||||
prototype_key (str): The prototype to affect.
|
||||
action (str): One of "spawn" or "edit".
|
||||
default (str): If action is unknown or prototype has no locks
|
||||
|
||||
Returns:
|
||||
passes (bool): If permission for action is granted or not.
|
||||
|
||||
"""
|
||||
if action == 'edit':
|
||||
if prototype_key in _MODULE_PROTOTYPES:
|
||||
mod = _MODULE_PROTOTYPE_MODULES.get(prototype_key, "N/A")
|
||||
logger.log_err("{} is a read-only prototype "
|
||||
"(defined as code in {}).".format(prototype_key, mod))
|
||||
return False
|
||||
|
||||
prototype = search_prototype(key=prototype_key)
|
||||
if not prototype:
|
||||
logger.log_err("Prototype {} not found.".format(prototype_key))
|
||||
return False
|
||||
|
||||
lockstring = prototype.get("prototype_locks")
|
||||
|
||||
if lockstring:
|
||||
return check_lockstring(None, lockstring, default=default, access_type=action)
|
||||
return default
|
||||
|
||||
|
||||
def init_spawn_value(value, validator=None):
|
||||
"""
|
||||
Analyze the prototype value and produce a value useful at the point of spawning.
|
||||
|
||||
Args:
|
||||
value (any): This can be:
|
||||
callable - will be called as callable()
|
||||
(callable, (args,)) - will be called as callable(*args)
|
||||
other - will be assigned depending on the variable type
|
||||
validator (callable, optional): If given, this will be called with the value to
|
||||
check and guarantee the outcome is of a given type.
|
||||
|
||||
Returns:
|
||||
any (any): The (potentially pre-processed value to use for this prototype key)
|
||||
|
||||
"""
|
||||
value = protfunc_parser(value)
|
||||
validator = validator if validator else lambda o: o
|
||||
if callable(value):
|
||||
return validator(value())
|
||||
elif value and is_iter(value) and callable(value[0]):
|
||||
# a structure (callable, (args, ))
|
||||
args = value[1:]
|
||||
return validator(value[0](*make_iter(args)))
|
||||
else:
|
||||
return validator(value)
|
||||
attrs.append((key, val, None, ''))
|
||||
homogenized['attrs'] = attrs
|
||||
return homogenized
|
||||
|
||||
|
||||
# module-based prototypes
|
||||
|
|
@ -295,7 +75,8 @@ def init_spawn_value(value, validator=None):
|
|||
for mod in settings.PROTOTYPE_MODULES:
|
||||
# to remove a default prototype, override it with an empty dict.
|
||||
# internally we store as (key, desc, locks, tags, prototype_dict)
|
||||
prots = [(prototype_key.lower(), prot) for prototype_key, prot in all_from_module(mod).items()
|
||||
prots = [(prototype_key.lower(), homogenize_prototype(prot))
|
||||
for prototype_key, prot in all_from_module(mod).items()
|
||||
if prot and isinstance(prot, dict)]
|
||||
# assign module path to each prototype_key for easy reference
|
||||
_MODULE_PROTOTYPE_MODULES.update({prototype_key.lower(): mod for prototype_key, _ in prots})
|
||||
|
|
@ -347,6 +128,8 @@ def save_prototype(**kwargs):
|
|||
|
||||
"""
|
||||
|
||||
kwargs = homogenize_prototype(kwargs)
|
||||
|
||||
def _to_batchtuple(inp, *args):
|
||||
"build tuple suitable for batch-creation"
|
||||
if is_iter(inp):
|
||||
|
|
@ -403,8 +186,7 @@ def save_prototype(**kwargs):
|
|||
attributes=[("prototype", prototype)])
|
||||
return stored_prototype.db.prototype
|
||||
|
||||
# alias
|
||||
create_prototype = save_prototype
|
||||
create_prototype = save_prototype # alias
|
||||
|
||||
|
||||
def delete_prototype(prototype_key, caller=None):
|
||||
|
|
@ -640,7 +422,8 @@ def validate_prototype(prototype, protkey=None, protparents=None,
|
|||
_flags['warnings'].append("Prototype {} can only be used as a mixin since it lacks "
|
||||
"a typeclass or a prototype_parent.".format(protkey))
|
||||
|
||||
if strict and typeclass and typeclass not in get_all_typeclasses("evennia.objects.models.ObjectDB"):
|
||||
if (strict and typeclass and typeclass not
|
||||
in get_all_typeclasses("evennia.objects.models.ObjectDB")):
|
||||
_flags['errors'].append(
|
||||
"Prototype {} is based on typeclass {}, which could not be imported!".format(
|
||||
protkey, typeclass))
|
||||
|
|
@ -685,7 +468,8 @@ def validate_prototype(prototype, protkey=None, protparents=None,
|
|||
|
||||
# make sure prototype_locks are set to defaults
|
||||
prototype_locks = [lstring.split(":", 1)
|
||||
for lstring in prototype.get("prototype_locks", "").split(';') if ":" in lstring]
|
||||
for lstring in prototype.get("prototype_locks", "").split(';')
|
||||
if ":" in lstring]
|
||||
locktypes = [tup[0].strip() for tup in prototype_locks]
|
||||
if "spawn" not in locktypes:
|
||||
prototype_locks.append(("spawn", "all()"))
|
||||
|
|
@ -693,3 +477,249 @@ def validate_prototype(prototype, protkey=None, protparents=None,
|
|||
prototype_locks.append(("edit", "all()"))
|
||||
prototype_locks = ";".join(":".join(tup) for tup in prototype_locks)
|
||||
prototype['prototype_locks'] = prototype_locks
|
||||
|
||||
|
||||
# Protfunc parsing (in-prototype functions)
|
||||
|
||||
for mod in settings.PROT_FUNC_MODULES:
|
||||
try:
|
||||
callables = callables_from_module(mod)
|
||||
PROT_FUNCS.update(callables)
|
||||
except ImportError:
|
||||
logger.log_trace()
|
||||
raise
|
||||
|
||||
|
||||
def protfunc_parser(value, available_functions=None, testing=False, stacktrace=False, **kwargs):
|
||||
"""
|
||||
Parse a prototype value string for a protfunc and process it.
|
||||
|
||||
Available protfuncs are specified as callables in one of the modules of
|
||||
`settings.PROTFUNC_MODULES`, or specified on the command line.
|
||||
|
||||
Args:
|
||||
value (any): The value to test for a parseable protfunc. Only strings will be parsed for
|
||||
protfuncs, all other types are returned as-is.
|
||||
available_functions (dict, optional): Mapping of name:protfunction to use for this parsing.
|
||||
If not set, use default sources.
|
||||
testing (bool, optional): Passed to protfunc. If in a testing mode, some protfuncs may
|
||||
behave differently.
|
||||
stacktrace (bool, optional): If set, print the stack parsing process of the protfunc-parser.
|
||||
|
||||
Kwargs:
|
||||
session (Session): Passed to protfunc. Session of the entity spawning the prototype.
|
||||
protototype (dict): Passed to protfunc. The dict this protfunc is a part of.
|
||||
current_key(str): Passed to protfunc. The key in the prototype that will hold this value.
|
||||
any (any): Passed on to the protfunc.
|
||||
|
||||
Returns:
|
||||
testresult (tuple): If `testing` is set, returns a tuple (error, result) where error is
|
||||
either None or a string detailing the error from protfunc_parser or seen when trying to
|
||||
run `literal_eval` on the parsed string.
|
||||
any (any): A structure to replace the string on the prototype level. If this is a
|
||||
callable or a (callable, (args,)) structure, it will be executed as if one had supplied
|
||||
it to the prototype directly. This structure is also passed through literal_eval so one
|
||||
can get actual Python primitives out of it (not just strings). It will also identify
|
||||
eventual object #dbrefs in the output from the protfunc.
|
||||
|
||||
"""
|
||||
if not isinstance(value, basestring):
|
||||
try:
|
||||
value = value.dbref
|
||||
except AttributeError:
|
||||
pass
|
||||
value = to_str(value, force_string=True)
|
||||
|
||||
available_functions = PROT_FUNCS if available_functions is None else available_functions
|
||||
|
||||
# insert $obj(#dbref) for #dbref
|
||||
value = _RE_DBREF.sub("$obj(\\1)", value)
|
||||
|
||||
result = inlinefuncs.parse_inlinefunc(
|
||||
value, available_funcs=available_functions,
|
||||
stacktrace=stacktrace, testing=testing, **kwargs)
|
||||
|
||||
err = None
|
||||
try:
|
||||
result = literal_eval(result)
|
||||
except ValueError:
|
||||
pass
|
||||
except Exception as err:
|
||||
err = str(err)
|
||||
if testing:
|
||||
return err, result
|
||||
return result
|
||||
|
||||
|
||||
# Various prototype utilities
|
||||
|
||||
def format_available_protfuncs():
|
||||
"""
|
||||
Get all protfuncs in a pretty-formatted form.
|
||||
|
||||
Args:
|
||||
clr (str, optional): What coloration tag to use.
|
||||
"""
|
||||
out = []
|
||||
for protfunc_name, protfunc in PROT_FUNCS.items():
|
||||
out.append("- |c${name}|n - |W{docs}".format(
|
||||
name=protfunc_name, docs=protfunc.__doc__.strip().replace("\n", "")))
|
||||
return justify("\n".join(out), indent=8)
|
||||
|
||||
|
||||
def prototype_to_str(prototype):
|
||||
"""
|
||||
Format a prototype to a nice string representation.
|
||||
|
||||
Args:
|
||||
prototype (dict): The prototype.
|
||||
"""
|
||||
|
||||
prototype = homogenize_prototype(prototype)
|
||||
|
||||
header = """
|
||||
|cprototype-key:|n {prototype_key}, |c-tags:|n {prototype_tags}, |c-locks:|n {prototype_locks}|n
|
||||
|c-desc|n: {prototype_desc}
|
||||
|cprototype-parent:|n {prototype_parent}
|
||||
\n""".format(
|
||||
prototype_key=prototype.get('prototype_key', '|r[UNSET](required)|n'),
|
||||
prototype_tags=prototype.get('prototype_tags', '|wNone|n'),
|
||||
prototype_locks=prototype.get('prototype_locks', '|wNone|n'),
|
||||
prototype_desc=prototype.get('prototype_desc', '|wNone|n'),
|
||||
prototype_parent=prototype.get('prototype_parent', '|wNone|n'))
|
||||
|
||||
key = prototype.get('key', '')
|
||||
if key:
|
||||
key = "|ckey:|n {key}".format(key=key)
|
||||
aliases = prototype.get("aliases", '')
|
||||
if aliases:
|
||||
aliases = "|caliases:|n {aliases}".format(
|
||||
aliases=", ".join(aliases))
|
||||
attrs = prototype.get("attrs", '')
|
||||
if attrs:
|
||||
out = []
|
||||
for (attrkey, value, category, locks) in attrs:
|
||||
locks = ", ".join(lock for lock in locks if lock)
|
||||
category = "|ccategory:|n {}".format(category) if category else ''
|
||||
cat_locks = ""
|
||||
if category or locks:
|
||||
cat_locks = " (|ccategory:|n {category}, ".format(
|
||||
category=category if category else "|wNone|n")
|
||||
out.append(
|
||||
"{attrkey}{cat_locks} |c=|n {value}".format(
|
||||
attrkey=attrkey,
|
||||
cat_locks=cat_locks,
|
||||
locks=locks if locks else "|wNone|n",
|
||||
value=value))
|
||||
attrs = "|cattrs:|n\n {attrs}".format(attrs="\n ".join(out))
|
||||
tags = prototype.get('tags', '')
|
||||
if tags:
|
||||
out = []
|
||||
for (tagkey, category, data) in tags:
|
||||
out.append("{tagkey} (category: {category}{dat})".format(
|
||||
tagkey=tagkey, category=category, dat=", data: {}".format(data) if data else ""))
|
||||
tags = "|ctags:|n\n {tags}".format(tags=", ".join(out))
|
||||
locks = prototype.get('locks', '')
|
||||
if locks:
|
||||
locks = "|clocks:|n\n {locks}".format(locks=locks)
|
||||
permissions = prototype.get("permissions", '')
|
||||
if permissions:
|
||||
permissions = "|cpermissions:|n {perms}".format(perms=", ".join(permissions))
|
||||
location = prototype.get("location", '')
|
||||
if location:
|
||||
location = "|clocation:|n {location}".format(location=location)
|
||||
home = prototype.get("home", '')
|
||||
if home:
|
||||
home = "|chome:|n {home}".format(home=home)
|
||||
destination = prototype.get("destination", '')
|
||||
if destination:
|
||||
destination = "|cdestination:|n {destination}".format(destination=destination)
|
||||
|
||||
body = "\n".join(part for part in (key, aliases, attrs, tags, locks, permissions,
|
||||
location, home, destination) if part)
|
||||
|
||||
return header.lstrip() + body.strip()
|
||||
|
||||
|
||||
def check_permission(prototype_key, action, default=True):
|
||||
"""
|
||||
Helper function to check access to actions on given prototype.
|
||||
|
||||
Args:
|
||||
prototype_key (str): The prototype to affect.
|
||||
action (str): One of "spawn" or "edit".
|
||||
default (str): If action is unknown or prototype has no locks
|
||||
|
||||
Returns:
|
||||
passes (bool): If permission for action is granted or not.
|
||||
|
||||
"""
|
||||
if action == 'edit':
|
||||
if prototype_key in _MODULE_PROTOTYPES:
|
||||
mod = _MODULE_PROTOTYPE_MODULES.get(prototype_key, "N/A")
|
||||
logger.log_err("{} is a read-only prototype "
|
||||
"(defined as code in {}).".format(prototype_key, mod))
|
||||
return False
|
||||
|
||||
prototype = search_prototype(key=prototype_key)
|
||||
if not prototype:
|
||||
logger.log_err("Prototype {} not found.".format(prototype_key))
|
||||
return False
|
||||
|
||||
lockstring = prototype.get("prototype_locks")
|
||||
|
||||
if lockstring:
|
||||
return check_lockstring(None, lockstring, default=default, access_type=action)
|
||||
return default
|
||||
|
||||
|
||||
def init_spawn_value(value, validator=None):
|
||||
"""
|
||||
Analyze the prototype value and produce a value useful at the point of spawning.
|
||||
|
||||
Args:
|
||||
value (any): This can be:
|
||||
callable - will be called as callable()
|
||||
(callable, (args,)) - will be called as callable(*args)
|
||||
other - will be assigned depending on the variable type
|
||||
validator (callable, optional): If given, this will be called with the value to
|
||||
check and guarantee the outcome is of a given type.
|
||||
|
||||
Returns:
|
||||
any (any): The (potentially pre-processed value to use for this prototype key)
|
||||
|
||||
"""
|
||||
value = protfunc_parser(value)
|
||||
validator = validator if validator else lambda o: o
|
||||
if callable(value):
|
||||
return validator(value())
|
||||
elif value and is_iter(value) and callable(value[0]):
|
||||
# a structure (callable, (args, ))
|
||||
args = value[1:]
|
||||
return validator(value[0](*make_iter(args)))
|
||||
else:
|
||||
return validator(value)
|
||||
|
||||
|
||||
def value_to_obj_or_any(value):
|
||||
"Convert value(s) to Object if possible, otherwise keep original value"
|
||||
stype = type(value)
|
||||
if is_iter(value):
|
||||
if stype == dict:
|
||||
return {value_to_obj_or_any(key):
|
||||
value_to_obj_or_any(val) for key, val in value.items()}
|
||||
else:
|
||||
return stype([value_to_obj_or_any(val) for val in value])
|
||||
obj = dbid_to_obj(value, ObjectDB)
|
||||
return obj if obj is not None else value
|
||||
|
||||
|
||||
def value_to_obj(value, force=True):
|
||||
"Always convert value(s) to Object, or None"
|
||||
stype = type(value)
|
||||
if is_iter(value):
|
||||
if stype == dict:
|
||||
return {value_to_obj_or_any(key): value_to_obj_or_any(val) for key, val in value.iter()}
|
||||
else:
|
||||
return stype([value_to_obj_or_any(val) for val in value])
|
||||
return dbid_to_obj(value, ObjectDB)
|
||||
|
|
|
|||
|
|
@ -5,11 +5,14 @@ The spawner takes input files containing object definitions in
|
|||
dictionary forms. These use a prototype architecture to define
|
||||
unique objects without having to make a Typeclass for each.
|
||||
|
||||
The main function is `spawn(*prototype)`, where the `prototype`
|
||||
There main function is `spawn(*prototype)`, where the `prototype`
|
||||
is a dictionary like this:
|
||||
|
||||
```python
|
||||
GOBLIN = {
|
||||
from evennia.prototypes import prototypes
|
||||
|
||||
prot = {
|
||||
"prototype_key": "goblin",
|
||||
"typeclass": "types.objects.Monster",
|
||||
"key": "goblin grunt",
|
||||
"health": lambda: randint(20,30),
|
||||
|
|
@ -18,7 +21,10 @@ GOBLIN = {
|
|||
"weaknesses": ["fire", "light"]
|
||||
"tags": ["mob", "evil", ('greenskin','mob')]
|
||||
"attrs": [("weapon", "sword")]
|
||||
}
|
||||
}
|
||||
|
||||
prot = prototypes.create_prototype(**prot)
|
||||
|
||||
```
|
||||
|
||||
Possible keywords are:
|
||||
|
|
@ -57,7 +63,7 @@ Possible keywords are:
|
|||
form allows more complex Attributes to be set. Tuples at least specify `(key, value)`
|
||||
but can also specify up to `(key, value, category, lockstring)`. If you want to specify a
|
||||
lockstring but not a category, set the category to `None`.
|
||||
ndb_<name> (any): value of a nattribute (ndb_ is stripped)
|
||||
ndb_<name> (any): value of a nattribute (ndb_ is stripped) - this is of limited use.
|
||||
other (any): any other name is interpreted as the key of an Attribute with
|
||||
its value. Such Attributes have no categories.
|
||||
|
||||
|
|
@ -66,15 +72,16 @@ return the value to enter into the field and will be called every time
|
|||
the prototype is used to spawn an object. Note, if you want to store
|
||||
a callable in an Attribute, embed it in a tuple to the `args` keyword.
|
||||
|
||||
By specifying the "prototype" key, the prototype becomes a child of
|
||||
that prototype, inheritng all prototype slots it does not explicitly
|
||||
By specifying the "prototype_parent" key, the prototype becomes a child of
|
||||
the given prototype, inheritng all prototype slots it does not explicitly
|
||||
define itself, while overloading those that it does specify.
|
||||
|
||||
```python
|
||||
import random
|
||||
|
||||
|
||||
GOBLIN_WIZARD = {
|
||||
{
|
||||
"prototype_key": "goblin_wizard",
|
||||
"prototype_parent": GOBLIN,
|
||||
"key": "goblin wizard",
|
||||
"spells": ["fire ball", "lighting bolt"]
|
||||
|
|
@ -189,7 +196,9 @@ def flatten_prototype(prototype, validate=False):
|
|||
flattened (dict): The final, flattened prototype.
|
||||
|
||||
"""
|
||||
|
||||
if prototype:
|
||||
prototype = protlib.homogenize_prototype(prototype)
|
||||
protparents = {prot['prototype_key'].lower(): prot for prot in protlib.search_prototype()}
|
||||
protlib.validate_prototype(prototype, None, protparents,
|
||||
is_prototype_base=validate, strict=validate)
|
||||
|
|
@ -253,7 +262,7 @@ def prototype_from_object(obj):
|
|||
for tag in obj.tags.get(return_tagobj=True, return_list=True) if tag]
|
||||
if tags:
|
||||
prot['tags'] = tags
|
||||
attrs = [(attr.key, attr.value, attr.category, attr.locks.all())
|
||||
attrs = [(attr.key, attr.value, attr.category, ';'.join(attr.locks.all()))
|
||||
for attr in obj.attributes.get(return_obj=True, return_list=True) if attr]
|
||||
if attrs:
|
||||
prot['attrs'] = attrs
|
||||
|
|
@ -261,7 +270,7 @@ def prototype_from_object(obj):
|
|||
return prot
|
||||
|
||||
|
||||
def prototype_diff(prototype1, prototype2):
|
||||
def prototype_diff(prototype1, prototype2, maxdepth=2):
|
||||
"""
|
||||
A 'detailed' diff specifies differences down to individual sub-sectiions
|
||||
of the prototype, like individual attributes, permissions etc. It is used
|
||||
|
|
@ -270,6 +279,9 @@ def prototype_diff(prototype1, prototype2):
|
|||
Args:
|
||||
prototype1 (dict): Original prototype.
|
||||
prototype2 (dict): Comparison prototype.
|
||||
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
|
||||
attr/tag (for example) are represented by a tuple.
|
||||
|
||||
Returns:
|
||||
diff (dict): A structure detailing how to convert prototype1 to prototype2. All
|
||||
|
|
@ -280,7 +292,7 @@ def prototype_diff(prototype1, prototype2):
|
|||
instruction can be one of "REMOVE", "ADD", "UPDATE" or "KEEP".
|
||||
|
||||
"""
|
||||
def _recursive_diff(old, new):
|
||||
def _recursive_diff(old, new, depth=0):
|
||||
|
||||
old_type = type(old)
|
||||
new_type = type(new)
|
||||
|
|
@ -292,14 +304,14 @@ def prototype_diff(prototype1, prototype2):
|
|||
return (old, new, "ADD")
|
||||
else:
|
||||
return (old, new, "UPDATE")
|
||||
elif new_type == dict:
|
||||
elif depth < maxdepth and new_type == dict:
|
||||
all_keys = set(old.keys() + new.keys())
|
||||
return {key: _recursive_diff(old.get(key), new.get(key)) for key in all_keys}
|
||||
elif is_iter(new):
|
||||
return {key: _recursive_diff(old.get(key), new.get(key), depth=depth + 1) for key in all_keys}
|
||||
elif depth < maxdepth and is_iter(new):
|
||||
old_map = {part[0] if is_iter(part) else part: part for part in old}
|
||||
new_map = {part[0] if is_iter(part) else part: part for part in new}
|
||||
all_keys = set(old_map.keys() + new_map.keys())
|
||||
return {key: _recursive_diff(old_map.get(key), new_map.get(key)) for key in all_keys}
|
||||
return {key: _recursive_diff(old_map.get(key), new_map.get(key), depth=depth + 1) for key in all_keys}
|
||||
elif old != new:
|
||||
return (old, new, "UPDATE")
|
||||
else:
|
||||
|
|
@ -346,13 +358,13 @@ def flatten_diff(diff):
|
|||
typ = type(diffpart)
|
||||
if typ == tuple and len(diffpart) == 3 and diffpart[2] in valid_instructions:
|
||||
out = [diffpart[2]]
|
||||
elif type == dict:
|
||||
elif typ == dict:
|
||||
# all other are dicts
|
||||
for val in diffpart.values():
|
||||
out.extend(_get_all_nested_diff_instructions(val))
|
||||
else:
|
||||
raise RuntimeError("Diff contains non-dicts that are not on the "
|
||||
"form (old, new, inst): {}".format(diff))
|
||||
"form (old, new, inst): {}".format(diffpart))
|
||||
return out
|
||||
|
||||
flat_diff = {}
|
||||
|
|
@ -402,7 +414,7 @@ def prototype_diff_from_object(prototype, obj):
|
|||
|
||||
"""
|
||||
obj_prototype = prototype_from_object(obj)
|
||||
diff = prototype_diff(obj_prototype, prototype)
|
||||
diff = prototype_diff(obj_prototype, protlib.homogenize_prototype(prototype))
|
||||
return diff, obj_prototype
|
||||
|
||||
|
||||
|
|
@ -421,6 +433,8 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None):
|
|||
changed (int): The number of objects that had changes applied to them.
|
||||
|
||||
"""
|
||||
prototype = protlib.homogenize_prototype(prototype)
|
||||
|
||||
if isinstance(prototype, basestring):
|
||||
new_prototype = protlib.search_prototype(prototype)
|
||||
else:
|
||||
|
|
@ -439,7 +453,6 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None):
|
|||
|
||||
# make sure the diff is flattened
|
||||
diff = flatten_diff(diff)
|
||||
|
||||
changed = 0
|
||||
for obj in objects:
|
||||
do_save = False
|
||||
|
|
@ -619,9 +632,9 @@ def spawn(*prototypes, **kwargs):
|
|||
(no object creation) and return the create-kwargs.
|
||||
|
||||
Returns:
|
||||
object (Object, dict or list): Spawned object. If `only_validate` is given, return
|
||||
object (Object, dict or list): Spawned object(s). If `only_validate` is given, return
|
||||
a list of the creation kwargs to build the object(s) without actually creating it. If
|
||||
`return_parents` is set, return dict of prototype parents.
|
||||
`return_parents` is set, instead return dict of prototype parents.
|
||||
|
||||
"""
|
||||
# get available protparents
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ class TestUtils(EvenniaTest):
|
|||
self.obj1.tags.add('foo')
|
||||
new_prot = spawner.prototype_from_object(self.obj1)
|
||||
self.assertEqual(
|
||||
{'attrs': [('test', 'testval', None, [''])],
|
||||
{'attrs': [('test', 'testval', None, '')],
|
||||
'home': Something,
|
||||
'key': 'Obj',
|
||||
'location': Something,
|
||||
|
|
@ -94,14 +94,15 @@ class TestUtils(EvenniaTest):
|
|||
def test_update_objects_from_prototypes(self):
|
||||
|
||||
self.maxDiff = None
|
||||
self.obj1.attributes.add('oldtest', 'to_remove')
|
||||
self.obj1.attributes.add('oldtest', 'to_keep')
|
||||
|
||||
old_prot = spawner.prototype_from_object(self.obj1)
|
||||
|
||||
# modify object away from prototype
|
||||
self.obj1.attributes.add('test', 'testval')
|
||||
self.obj1.attributes.add('desc', 'changed desc')
|
||||
self.obj1.aliases.add('foo')
|
||||
self.obj1.key = 'NewObj'
|
||||
self.obj1.tags.add('footag', 'foocategory')
|
||||
|
||||
# modify prototype
|
||||
old_prot['new'] = 'new_val'
|
||||
|
|
@ -109,53 +110,111 @@ class TestUtils(EvenniaTest):
|
|||
old_prot['permissions'] = 'Builder'
|
||||
# this will not update, since we don't update the prototype on-disk
|
||||
old_prot['prototype_desc'] = 'New version of prototype'
|
||||
old_prot['attrs'] += (("fooattr", "fooattrval", None, ''),)
|
||||
|
||||
# diff obj/prototype
|
||||
pdiff = spawner.prototype_diff_from_object(old_prot, self.obj1)
|
||||
old_prot_copy = old_prot.copy()
|
||||
|
||||
pdiff, obj_prototype = spawner.prototype_diff_from_object(old_prot, self.obj1)
|
||||
|
||||
self.assertEqual(old_prot_copy, old_prot)
|
||||
|
||||
self.assertEqual(obj_prototype,
|
||||
{'aliases': ['foo'],
|
||||
'attrs': [('oldtest', 'to_keep', None, ''),
|
||||
('test', 'testval', None, ''),
|
||||
('desc', 'changed desc', None, '')],
|
||||
'key': 'Obj',
|
||||
'home': '#1',
|
||||
'location': '#1',
|
||||
'locks': 'call:true();control:perm(Developer);delete:perm(Admin);'
|
||||
'edit:perm(Admin);examine:perm(Builder);get:all();'
|
||||
'puppet:pperm(Developer);tell:perm(Admin);view:all()',
|
||||
'prototype_desc': 'Built from Obj',
|
||||
'prototype_key': Something,
|
||||
'prototype_locks': 'spawn:all();edit:all()',
|
||||
'prototype_tags': [],
|
||||
'typeclass': 'evennia.objects.objects.DefaultObject'})
|
||||
|
||||
self.assertEqual(old_prot,
|
||||
{'attrs': [('oldtest', 'to_keep', None, ''),
|
||||
('fooattr', 'fooattrval', None, '')],
|
||||
'home': '#1',
|
||||
'key': 'Obj',
|
||||
'location': '#1',
|
||||
'locks': 'call:true();control:perm(Developer);delete:perm(Admin);'
|
||||
'edit:perm(Admin);examine:perm(Builder);get:all();'
|
||||
'puppet:pperm(Developer);tell:perm(Admin);view:all()',
|
||||
'new': 'new_val',
|
||||
'permissions': 'Builder',
|
||||
'prototype_desc': 'New version of prototype',
|
||||
'prototype_key': Something,
|
||||
'prototype_locks': 'spawn:all();edit:all()',
|
||||
'prototype_tags': [],
|
||||
'test': 'testval_changed',
|
||||
'typeclass': 'evennia.objects.objects.DefaultObject'})
|
||||
|
||||
# from evennia import set_trace; set_trace(term_size=(182, 50))
|
||||
self.assertEqual(
|
||||
pdiff,
|
||||
({'aliases': 'REMOVE',
|
||||
'attrs': 'REPLACE',
|
||||
'home': 'KEEP',
|
||||
'key': 'UPDATE',
|
||||
'location': 'KEEP',
|
||||
'locks': 'KEEP',
|
||||
'new': 'UPDATE',
|
||||
'permissions': 'UPDATE',
|
||||
'prototype_desc': 'UPDATE',
|
||||
'prototype_key': 'UPDATE',
|
||||
'prototype_locks': 'KEEP',
|
||||
'prototype_tags': 'KEEP',
|
||||
'test': 'UPDATE',
|
||||
'typeclass': 'KEEP'},
|
||||
{'attrs': [('oldtest', 'to_remove', None, ['']),
|
||||
('test', 'testval', None, [''])],
|
||||
'prototype_locks': 'spawn:all();edit:all()',
|
||||
'prototype_key': Something,
|
||||
'locks': ";".join([
|
||||
'call:true()', 'control:perm(Developer)',
|
||||
'delete:perm(Admin)', 'edit:perm(Admin)',
|
||||
'examine:perm(Builder)', 'get:all()',
|
||||
'puppet:pperm(Developer)', 'tell:perm(Admin)',
|
||||
'view:all()']),
|
||||
'prototype_tags': [],
|
||||
'location': "#1",
|
||||
'key': 'NewObj',
|
||||
'home': '#1',
|
||||
'typeclass': 'evennia.objects.objects.DefaultObject',
|
||||
'prototype_desc': 'Built from NewObj',
|
||||
'aliases': 'foo'})
|
||||
{'home': ('#1', '#1', 'KEEP'),
|
||||
'prototype_locks': ('spawn:all();edit:all()',
|
||||
'spawn:all();edit:all()', 'KEEP'),
|
||||
'prototype_key': (Something, Something, 'UPDATE'),
|
||||
'location': ('#1', '#1', 'KEEP'),
|
||||
'locks': ('call:true();control:perm(Developer);delete:perm(Admin);'
|
||||
'edit:perm(Admin);examine:perm(Builder);get:all();'
|
||||
'puppet:pperm(Developer);tell:perm(Admin);view:all()',
|
||||
'call:true();control:perm(Developer);delete:perm(Admin);'
|
||||
'edit:perm(Admin);examine:perm(Builder);get:all();'
|
||||
'puppet:pperm(Developer);tell:perm(Admin);view:all()', 'KEEP'),
|
||||
'prototype_tags': {},
|
||||
'attrs': {'oldtest': (('oldtest', 'to_keep', None, ''),
|
||||
('oldtest', 'to_keep', None, ''), 'KEEP'),
|
||||
'test': (('test', 'testval', None, ''),
|
||||
None, 'REMOVE'),
|
||||
'desc': (('desc', 'changed desc', None, ''),
|
||||
None, 'REMOVE'),
|
||||
'fooattr': (None, ('fooattr', 'fooattrval', None, ''), 'ADD'),
|
||||
'test': (('test', 'testval', None, ''),
|
||||
('test', 'testval_changed', None, ''), 'UPDATE'),
|
||||
'new': (None, ('new', 'new_val', None, ''), 'ADD')},
|
||||
'key': ('Obj', 'Obj', 'KEEP'),
|
||||
'typeclass': ('evennia.objects.objects.DefaultObject',
|
||||
'evennia.objects.objects.DefaultObject', 'KEEP'),
|
||||
'aliases': (['foo'], None, 'REMOVE'),
|
||||
'prototype_desc': ('Built from Obj',
|
||||
'New version of prototype', 'UPDATE'),
|
||||
'permissions': (None, 'Builder', 'ADD')}
|
||||
)
|
||||
|
||||
# from evennia import set_trace;set_trace()
|
||||
self.assertEqual(
|
||||
spawner.flatten_diff(pdiff),
|
||||
{'aliases': 'REMOVE',
|
||||
'attrs': 'REPLACE',
|
||||
'home': 'KEEP',
|
||||
'key': 'KEEP',
|
||||
'location': 'KEEP',
|
||||
'locks': 'KEEP',
|
||||
'permissions': 'UPDATE',
|
||||
'prototype_desc': 'UPDATE',
|
||||
'prototype_key': 'UPDATE',
|
||||
'prototype_locks': 'KEEP',
|
||||
'prototype_tags': 'KEEP',
|
||||
'typeclass': 'KEEP'}
|
||||
)
|
||||
|
||||
# apply diff
|
||||
count = spawner.batch_update_objects_with_prototype(
|
||||
old_prot, diff=pdiff[0], objects=[self.obj1])
|
||||
old_prot, diff=pdiff, objects=[self.obj1])
|
||||
self.assertEqual(count, 1)
|
||||
|
||||
new_prot = spawner.prototype_from_object(self.obj1)
|
||||
self.assertEqual({'attrs': [('test', 'testval_changed', None, ['']),
|
||||
('new', 'new_val', None, [''])],
|
||||
self.assertEqual({'attrs': [('oldtest', 'to_keep', None, ''),
|
||||
('fooattr', 'fooattrval', None, ''),
|
||||
('new', 'new_val', None, ''),
|
||||
('test', 'testval_changed', None, '')],
|
||||
'home': Something,
|
||||
'key': 'Obj',
|
||||
'location': Something,
|
||||
|
|
|
|||
|
|
@ -564,7 +564,7 @@ class AttributeHandler(object):
|
|||
ntup = len(tup)
|
||||
keystr = str(tup[0]).strip().lower()
|
||||
new_value = tup[1]
|
||||
category = str(tup[2]).strip().lower() if ntup > 2 else None
|
||||
category = str(tup[2]).strip().lower() if ntup > 2 and tup[2] is not None else None
|
||||
lockstring = tup[3] if ntup > 3 else ""
|
||||
|
||||
attr_objs = self._getcache(keystr, category)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue