Migrate! Add nick-templating system, first version. This allows arg templating of nicks to allow a user to completely redefine how they enter commands. Still some bugs.

This commit is contained in:
Griatch 2016-06-25 23:22:45 +02:00
parent 893cffc3e1
commit b1de659e8b
3 changed files with 297 additions and 63 deletions

View file

@ -2,7 +2,8 @@
General Character commands usually availabe to all characters
"""
from django.conf import settings
from evennia.utils import utils, prettytable
from evennia.utils import utils, evtable
from evennia.typeclasses.attributes import NickTemplateInvalid
COMMAND_DEFAULT_CLASS = utils.class_from_module(settings.COMMAND_DEFAULT_CLASS)
@ -75,33 +76,39 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
define a personal alias/nick
Usage:
nick[/switches] <nickname> [= [<string>]]
alias ''
nick[/switches] <string> [= [replacement_string]]
nick[/switches] <template> = <replacement_template>
nick/delete <string> or number
nick/test <test string>
Switches:
object - alias an object
player - alias a player
clearall - clear all your aliases
list - show all defined aliases (also "nicks" works)
inputline - replace on the inputline (default)
object - replace on object-lookup
player - replace on player-lookup
delete - remove nick by number of by index given by /list
clearall - clear all nicks
list - show all defined aliases (also "nicks" works)
test - test input to see what it matches with
Examples:
nick hi = say Hello, I'm Sarah!
nick/object tom = the tall man
nick hi (shows the nick substitution)
nick hi = (removes nick 'hi')
nick build $1 $2 = @create/drop $1;$2 - (template)
nick tell $1 $2=@page $1=$2 - (template)
A 'nick' is a personal shortcut you create for your own use. When
you enter the nick, the alternative string will be sent instead.
The switches control in which situations the substitution will
happen. The default is that it will happen when you enter a
command. The 'object' and 'player' nick-types kick in only when
you use commands that requires an object or player as a target -
you can then use the nick to refer to them.
A 'nick' is a personal string replacement. Use $1, $2, ... to catch arguments.
Put the last $-marker without an ending space to catch all remaining text. You
can also use unix-glob matching:
* - matches everything
? - matches a single character
[seq] - matches all chars in sequence
[!seq] - matches everything not in sequence
Note that no objects are actually renamed or changed by this command - your nicks
are only available to you. If you want to permanently add keywords to an object
for everyone to use, you need build privileges and the @alias command.
Note that no objects are actually renamed or changed by this
command - the nick is only available to you. If you want to
permanently add keywords to an object for everyone to use, you
need build privileges and to use the @alias command.
"""
key = "nick"
aliases = ["nickname", "nicks", "@nick", "alias"]
@ -112,61 +119,79 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
caller = self.caller
switches = self.switches
nicksinputline = caller.nicks.get(category="inputline", return_obj=True)
nicksobjects = caller.nicks.get(category="object", return_obj=True)
nicksplayers = caller.nicks.get(category="player", return_obj=True)
nicktypes = [switch for switch in switches if switch in ("object", "player", "inputline")] or ["inputline"]
nicklist = utils.make_iter(caller.nicks.get(return_obj=True) or [])
if 'list' in switches:
if not nicksinputline and not nicksobjects and not nicksplayers:
if not nicklist:
string = "{wNo nicks defined.{n"
else:
table = prettytable.PrettyTable(["{wNickType",
"{wNickname",
"{wTranslates-to"])
for nicks in (nicksinputline, nicksobjects, nicksplayers):
for nick in utils.make_iter(nicks):
table.add_row([nick.db_category, nick.db_key, nick.db_strvalue])
table = evtable.EvTable("#", "Type", "Nick match", "Replacement")
for inum, nickobj in enumerate(nicklist):
_, _, nickvalue, replacement = nickobj.value
table.add_row(str(inum + 1), nickobj.db_category, nickvalue, replacement)
string = "{wDefined Nicks:{n\n%s" % table
caller.msg(string)
return
if 'clearall' in switches:
caller.nicks.clear()
caller.msg("Cleared all aliases.")
caller.msg("Cleared all nicks.")
return
if not self.args or not self.lhs:
caller.msg("Usage: nick[/switches] nickname = [realname]")
return
nick = self.lhs
real = self.rhs
if real == nick:
nickstring = self.lhs
replstring = self.rhs
if replstring == nickstring:
caller.msg("No point in setting nick same as the string to replace...")
return
# check so we have a suitable nick type
if not any(True for switch in switches if switch in ("object", "player", "inputline")):
switches = ["inputline"]
string = ""
for switch in switches:
oldnick = caller.nicks.get(key=nick, category=switch)
if not real:
if "=" in self.args:
if oldnick:
# clear the alias
string += "\nNick cleared: '{w%s{n' (-> '{w%s{n')." % (nick, oldnick)
caller.nicks.remove(nick, category=switch)
else:
string += "\nNo nick '%s' found, so it could not be removed." % nick
for nicktype in nicktypes:
oldnick = caller.nicks.get(key=nickstring, category=nicktype, return_obj=True)
oldnick = oldnick if oldnick.key is not None else None
if "delete" in switches or "del" in switches:
if oldnick:
_, _, nickstring, replstring = oldnick.value
else:
string += "\nNick: '{w%s{n'{n -> '{w%s{n'." % (nick, oldnick)
# no old nick, see if a number was given
if self.args.isdigit():
# we are given a index in nicklist
delindex = int(self.args)
if 0 < delindex <= len(nicklist):
oldnick = nicklist[delindex-1]
_, _, nickstring, replstring = oldnick.value
else:
caller.msg("Not a valid nick index.")
return
else:
caller.msg("Nick not found.")
return
# clear the nick
string += "\nNick removed: '|w%s|n' -> |w%s|n." % (nickstring, replstring)
caller.nicks.remove(nickstring, category=nicktype)
else:
elif replstring:
# creating new nick
if oldnick:
string += "\nNick '{w%s{n' reset from '{w%s{n' to '{w%s{n'." % (nick, oldnick, real)
string += "\nNick '{w%s{n' updated to map to '{w%s{n'." % (nickstring, replstring)
else:
string += "\nNick set: '{w%s{n' -> '{w%s{n'." % (nick, real)
caller.nicks.add(nick, real, category=switch)
string += "\nNick '{w%s{n' mapped to '{w%s{n'." % (nickstring, replstring)
try:
caller.nicks.add(nickstring, replstring, category=nicktype)
except NickTemplateInvalid:
caller.msg("You must use the same $-markers both in the nick and in the replacement.")
return
else:
# just looking at the nick
string += "\nNick: '{w%s{n'{n -> '{w%s{n'." % (nickstring, oldnick.key)
caller.msg(string)

View file

@ -279,6 +279,7 @@ class AttributeHandler(object):
class RetDefault(object):
"Holds default values"
def __init__(self):
self.key = None
self.value = default
self.strvalue = str(default) if default is not None else None
@ -532,6 +533,103 @@ class AttributeHandler(object):
return attrs
# Nick templating
#
"""
This supports the use of replacement templates in nicks:
This happens in two steps:
1) The user supplies a template that is converted to a regex according
to the unix-like templating language.
2) This regex is tested against nicks depending on which nick replacement
strategy is considered (most commonly inputline).
3) If there is a template match and there are templating markers,
these are replaced with the arguments actually given.
@desc $1 $2 $3
This will be converted to the following regex:
\@desc (?P<1>\w+) (?P<2>\w+) $(?P<3>\w+)
Supported template markers (through fnmatch)
* matches anything (non-greedy) -> .*?
? matches any single character ->
[seq] matches any entry in sequence
[!seq] matches entries not in sequence
Custom arg markers
$N argument position (1-99)
"""
import fnmatch
_RE_NICK_ARG = re.compile(r"\\(\$)([1-9][0-9]?)")
_RE_NICK_TEMPLATE_ARG = re.compile(r"(\$)([1-9][0-9]?)")
_RE_NICK_SPACE = re.compile(r"\\ ")
class NickTemplateInvalid(ValueError):
pass
def initialize_nick_templates(in_template, out_template):
"""
Initialize the nick templates for matching and remapping a string.
Args:
in_template (str): The template to be used for nick recognition.
out_template (str): The template to be used to replace the string
matched by the in_template.
Returns:
regex (regex): Regex to match against strings
template (str): Template with markers {arg1}, {arg2}, etc for
replacement using the standard .format method.
Raises:
NickTemplateInvalid: If the in/out template does not have a matching
number of $args.
"""
# create the regex for in_template
regex_string = fnmatch.translate(in_template)
# validate the templates
regex_args = [match.group(2) for match in _RE_NICK_ARG.finditer(regex_string)]
temp_args = [match.group(2) for match in _RE_NICK_TEMPLATE_ARG.finditer(out_template)]
if set(regex_args) != set(temp_args):
# We don't have the same $-tags in input/output.
raise NickTemplateInvalid
regex_string = _RE_NICK_SPACE.sub("\s+", regex_string)
regex_string = _RE_NICK_ARG.sub(lambda m: "(?P<arg%s>.+?)" % m.group(2), regex_string)
template_string = _RE_NICK_TEMPLATE_ARG.sub(lambda m: "{arg%s}" % m.group(2), out_template)
return regex_string, template_string
def parse_nick_template(string, template_regex, outtemplate):
"""
Parse a text using a template and map it to another template
Args:
string (str): The input string to processj
template_regex (regex): A template regex created with
initialize_nick_template.
outtemplate (str): The template to which to map the matches
produced by the template_regex. This should have $1, $2,
etc to match the regex.
"""
match = template_regex.match(string)
if match:
return True, outtemplate.format(**match.groupdict())
return False, string
class NickHandler(AttributeHandler):
"""
Handles the addition and removal of Nicks. Nicks are special
@ -541,6 +639,10 @@ class NickHandler(AttributeHandler):
"""
_attrtype = "nick"
def __init__(self, *args, **kwargs):
super(NickHandler, self).__init__(*args, **kwargs)
self._regex_cache = {}
def has(self, key, category="inputline"):
"""
Args:
@ -570,22 +672,23 @@ class NickHandler(AttributeHandler):
kwargs (any, optional): These are passed on to `AttributeHandler.get`.
"""
return super(NickHandler, self).get(key=key, category=category, strattr=True, **kwargs)
return super(NickHandler, self).get(key=key, category=category, **kwargs)
def add(self, key, replacement, category="inputline", **kwargs):
"""
Add a new nick.
Args:
key (str): A key for the nick to match for.
replacement (str): The string to replace `key` with (the "nickname").
key (str): A key (or template) for the nick to match for.
replacement (str): The string (or template) to replace `key` with (the "nickname").
category (str, optional): the category within which to
retrieve the nick. The "inputline" means replacing data
sent by the user.
kwargs (any, optional): These are passed on to `AttributeHandler.get`.
"""
super(NickHandler, self).add(key, replacement, category=category, strattr=True, **kwargs)
nick_regex, nick_template = initialize_nick_templates(key, replacement)
super(NickHandler, self).add(key, (nick_regex, nick_template, key, replacement), category=category, **kwargs)
def remove(self, key, category="inputline", **kwargs):
"""
@ -620,17 +723,27 @@ class NickHandler(AttributeHandler):
their nick equivalents.
"""
obj_nicks, player_nicks = [], []
nicks = {}
for category in make_iter(categories):
obj_nicks.extend([n for n in make_iter(self.get(category=category, return_obj=True)) if n])
nicks.update({nick.key: nick
for nick in make_iter(self.get(category=category, return_obj=True)) if nick and nick.key})
if include_player and self.obj.has_player:
for category in make_iter(categories):
player_nicks.extend([n for n in make_iter(self.obj.player.nicks.get(category=category, return_obj=True)) if n])
for nick in obj_nicks + player_nicks:
# make a case-insensitive match here
match = re.match(re.escape(nick.db_key), raw_string, re.IGNORECASE)
if match:
raw_string = raw_string.replace(match.group(), nick.db_strvalue, 1)
nicks.update({nick.key: nick
for nick in make_iter(self.obj.player.nicks.get(category=category, return_obj=True))
if nick and nick.key})
for key, nick in nicks.iteritems():
print "iterting over nicks:", key
nick_regex, template, _, _ = nick.value
regex = self._regex_cache.get(nick_regex)
if not regex:
regex = re.compile(nick_regex, re.I + re.DOTALL)
self._regex_cache[nick_regex] = regex
print "before parse_nick_template:", nick.value
is_match, raw_string = parse_nick_template(raw_string, regex, template)
print "is_match:", is_match, raw_string
if is_match:
break
return raw_string

View file

@ -356,3 +356,99 @@ def parse_inlinefunc(string, strip=False, **kwargs):
# execute the stack from the cache
return "".join(_run_stack(item) for item in _PARSING_CACHE[string])
# Nick templating
#
"""
This supports the use of replacement templates in nicks:
This happens in two steps:
1) The user supplies a template that is converted to a regex according
to the unix-like templating language.
2) This regex is tested against nicks depending on which nick replacement
strategy is considered (most commonly inputline).
3) If there is a template match and there are templating markers,
these are replaced with the arguments actually given.
@desc $1 $2 $3
This will be converted to the following regex:
\@desc (?P<1>\w+) (?P<2>\w+) $(?P<3>\w+)
Supported template markers (through fnmatch)
* matches anything (non-greedy) -> .*?
? matches any single character ->
[seq] matches any entry in sequence
[!seq] matches entries not in sequence
Custom arg markers
$N argument position (1-99)
"""
import fnmatch
_RE_NICK_ARG = re.compile(r"\\(\$)([1-9][0-9]?)")
_RE_NICK_TEMPLATE_ARG = re.compile(r"(\$)([1-9][0-9]?)")
_RE_NICK_SPACE = re.compile(r"\\ ")
class NickTemplateInvalid(ValueError):
pass
def initialize_nick_templates(in_template, out_template):
"""
Initialize the nick templates for matching and remapping a string.
Args:
in_template (str): The template to be used for nick recognition.
out_template (str): The template to be used to replace the string
matched by the in_template.
Returns:
regex (regex): Regex to match against strings
template (str): Template with markers {arg1}, {arg2}, etc for
replacement using the standard .format method.
Raises:
NickTemplateInvalid: If the in/out template does not have a matching
number of $args.
"""
# create the regex for in_template
regex_string = fnmatch.translate(in_template)
n_inargs = len(_RE_NICK_ARG.findall(regex_string))
regex_string = _RE_NICK_SPACE.sub("\s+", regex_string)
regex_string = _RE_NICK_ARG.sub(lambda m: "(?P<arg%s>.+?)" % m.group(2), regex_string)
# create the out_template
template_string = _RE_NICK_TEMPLATE_ARG.sub(lambda m: "{arg%s}" % m.group(2), out_template)
# validate the tempaltes - they should at least have the same number of args
n_outargs = len(_RE_NICK_TEMPLATE_ARG.findall(out_template))
if n_inargs != n_outargs:
print n_inargs, n_outargs
raise NickTemplateInvalid
return re.compile(regex_string), template_string
def parse_nick_template(string, template_regex, outtemplate):
"""
Parse a text using a template and map it to another template
Args:
string (str): The input string to processj
template_regex (regex): A template regex created with
initialize_nick_template.
outtemplate (str): The template to which to map the matches
produced by the template_regex. This should have $1, $2,
etc to match the regex.
"""
match = template_regex.match(string)
if match:
return outtemplate.format(**match.groupdict())
return string