Merge branch 'unixcommand' of https://github.com/vlegoff/evennia into vlegoff-unixcommand

This commit is contained in:
Griatch 2017-06-27 21:16:29 +02:00
commit 248f20e8e5
3 changed files with 356 additions and 21 deletions

View file

@ -73,6 +73,9 @@ class CommandTest(EvenniaTest):
cmdobj.parse()
cmdobj.func()
cmdobj.at_post_cmd()
except InterruptCommand:
pass
finally:
# clean out prettytable sugar. We only operate on text-type
stored_msg = [args[0] if args and args[0] else kwargs.get("text",utils.to_str(kwargs, force_string=True))
for name, args, kwargs in receiver.msg.mock_calls]
@ -88,11 +91,8 @@ class CommandTest(EvenniaTest):
retval = sep1 + msg.strip() + sep2 + returned_msg + sep3
raise AssertionError(retval)
else:
returned_msg = "\n".join(stored_msg)
returned_msg = "\n".join(str(msg) for msg in stored_msg)
returned_msg = ansi.parse_ansi(returned_msg, strip_ansi=noansi).strip()
except InterruptCommand:
pass
finally:
receiver.msg = old_msg
return returned_msg

View file

@ -452,13 +452,13 @@ class TestChargen(CommandTest):
self.assertTrue(self.player.db._character_dbrefs)
self.call(chargen.CmdOOCLook(), "", "You, TestPlayer, are an OOC ghost without form.",caller=self.player)
self.call(chargen.CmdOOCLook(), "testchar", "testchar(", caller=self.player)
# Testing clothing contrib
from evennia.contrib import clothing
from evennia.objects.objects import DefaultRoom
class TestClothingCmd(CommandTest):
def test_clothingcommands(self):
wearer = create_object(clothing.ClothedCharacter, key="Wearer")
friend = create_object(clothing.ClothedCharacter, key="Friend")
@ -501,7 +501,7 @@ class TestClothingCmd(CommandTest):
self.call(clothing.CmdInventory(), "", "You are not carrying or wearing anything.", caller=wearer)
class TestClothingFunc(EvenniaTest):
def test_clothingfunctions(self):
wearer = create_object(clothing.ClothedCharacter, key="Wearer")
room = create_object(DefaultRoom, key="room")
@ -521,28 +521,28 @@ class TestClothingFunc(EvenniaTest):
test_hat.wear(wearer, 'on the head')
self.assertEqual(test_hat.db.worn, 'on the head')
test_hat.remove(wearer)
self.assertEqual(test_hat.db.worn, False)
test_hat.worn = True
test_hat.at_get(wearer)
self.assertEqual(test_hat.db.worn, False)
clothes_list = [test_shirt, test_hat, test_pants]
self.assertEqual(clothing.order_clothes_list(clothes_list), [test_hat, test_shirt, test_pants])
test_hat.wear(wearer, True)
test_pants.wear(wearer, True)
self.assertEqual(clothing.get_worn_clothes(wearer), [test_hat, test_pants])
self.assertEqual(clothing.clothing_type_count(clothes_list), {'hat':1, 'top':1, 'bottom':1})
self.assertEqual(clothing.single_type_count(clothes_list, 'hat'), 1)
self.assertEqual(clothing.clothing_type_count(clothes_list), {'hat':1, 'top':1, 'bottom':1})
self.assertEqual(clothing.single_type_count(clothes_list, 'hat'), 1)
# Testing custom_gametime
from evennia.contrib import custom_gametime
@ -850,7 +850,7 @@ from evennia.contrib import turnbattle
from evennia.objects.objects import DefaultRoom
class TestTurnBattleCmd(CommandTest):
# Test combat commands
def test_turnbattlecmd(self):
self.call(turnbattle.CmdFight(), "", "You can't start a fight if you've been defeated!")
@ -858,9 +858,9 @@ class TestTurnBattleCmd(CommandTest):
self.call(turnbattle.CmdPass(), "", "You can only do that in combat. (see: help fight)")
self.call(turnbattle.CmdDisengage(), "", "You can only do that in combat. (see: help fight)")
self.call(turnbattle.CmdRest(), "", "Char rests to recover HP.")
class TestTurnBattleFunc(EvenniaTest):
# Test combat functions
def test_turnbattlefunc(self):
attacker = create_object(turnbattle.BattleCharacter, key="Attacker")
@ -937,3 +937,51 @@ class TestTurnBattleFunc(EvenniaTest):
self.assertTrue(turnhandler.db.fighters == [joiner, attacker, defender])
# Remove the script at the end
turnhandler.stop()
# Test of the unixcommand module
from evennia.contrib.unixcommand import UnixCommand
class CmdDummy(UnixCommand):
"""A dummy UnixCommand."""
key = "dummy"
def init_parser(self):
"""Fill out options."""
self.parser.add_argument("nb1", type=int, help="the first number")
self.parser.add_argument("nb2", type=int, help="the second number")
self.parser.add_argument("-v", "--verbose", action="store_true")
def func(self):
nb1 = self.opts.nb1
nb2 = self.opts.nb2
result = nb1 * nb2
verbose = self.opts.verbose
if verbose:
self.msg("{} times {} is {}".format(nb1, nb2, result))
else:
self.msg("{} * {} = {}".format(nb1, nb2, result))
class TestUnixCommand(CommandTest):
def test_success(self):
"""See the command parsing succeed."""
self.call(CmdDummy(), "5 10", "5 * 10 = 50")
self.call(CmdDummy(), "5 10 -v", "5 times 10 is 50")
def test_failure(self):
"""If not provided with the right info, should fail."""
ret = self.call(CmdDummy(), "5")
lines = ret.splitlines()
self.assertTrue(any(l.startswith("usage:") for l in lines))
self.assertTrue(any(l.startswith("dummy: error:") for l in lines))
# If we specify an incorrect number as parameter
ret = self.call(CmdDummy(), "five ten")
lines = ret.splitlines()
self.assertTrue(any(l.startswith("usage:") for l in lines))
self.assertTrue(any(l.startswith("dummy: error:") for l in lines))

View file

@ -0,0 +1,287 @@
"""
Module containing the UnixCommand class.
This command allows to use unix-like options in game commands. It is
not the best parser for players, but can be really useful for builders
when they need to have a single command to do many things with many
options.
The UnixCommand can be ovverridden to have your commands parsed.
You will need to override two methods:
- The `init_parser` method, which adds options to the parser.
- The `func` method, called to execute the command once parsed.
Here's a short example:
```python
class CmdPlant(UnixCommand):
'''
Plant a tree or plant.
This command is used to plant a tree or plant in the room you are in.
Examples:
plant orange -a 8
plant strawberry --hidden
plant potato --hidden --age 5
'''
key = "plant"
def init_parser(self):
"Add the arguments to the parser."
# 'self.parser' inherits `argparse.ArgumentParser`
self.parser.add_argument("key",
help="the key of the plant to be planted here")
self.parser.add_argument("-a", "--age", type=int,
default=1, help="the age of the plant to be planted")
self.parser.add_argument("--hidden", action="store_true",
help="should the newly-planted plant be hidden to players?")
def func(self):
"func is called only if the parser succeeded."
# 'self.opts' contains the parsed options
key = self.opts.key
age = self.opts.age
hidden = self.opts.hidden
self.msg("Going to plant '{}', age={}, hidden={}.".format(
key, age, hidden))
```
To see the full power of argparse and the types of supported options, visit
[the documentation of argparse](https://docs.python.org/2/library/argparse.html).
"""
import argparse
import shlex
from textwrap import dedent
from evennia import Command, InterruptCommand
from evennia.utils.ansi import raw
class ParseError(Exception):
"""An error occurred during parsing."""
pass
class UnixCommandParser(argparse.ArgumentParser):
"""A modifier command parser for unix commands.
This parser is used to replace `argparse.ArgumentParser`. It
is aware of the command calling it, and can more easily report to
the caller. Some features (like the "brutal exit" of the original
parser) are disabled or replaced. This parser is used by UnixCommand
and creating one directly isn't recommended nor necessary. Even
adding a sub-command will use this replaced parser automatically.
"""
def __init__(self, prog, description="", epilog="", command=None, **kwargs):
"""
Build a UnixCommandParser with a link to the command using it.
Args:
prog (str): the program name (usually the command key).
description (str): a very brief line to show in the usage text.
epilog (str): the epilog to show below options.
command (Command): the command calling the parser.
Kwargs:
Additional keyword arguments are directly sent to
`argparse.ArgumentParser`. You will find them on the
[parser's documentation](https://docs.python.org/2/library/argparse.html).
Note:
It's doubtful you would need to create this parser manually.
The `UnixCommand` does that automatically. If you create
sub-commands, this class will be used.
"""
prog = prog or command.key
super(UnixCommandParser, self).__init__(
prog=prog, description=description,
conflict_handler='resolve', add_help=False, **kwargs)
self.command = command
self.post_help = epilog
def n_exit(code=None, msg=None):
raise ParseError(msg)
self.exit = n_exit
# Replace the -h/--help
self.add_argument("-h", "--hel", nargs=0, action=HelpAction,
help="display the command help")
def format_usage(self):
"""Return the usage line.
Note:
This method is present to return the raw-escaped usage line,
in order to avoid unintentional color codes.
"""
return raw(super(UnixCommandParser, self).format_usage())
def format_help(self):
"""Return the parser help, including its epilog.
Note:
This method is present to return the raw-escaped help,
in order to avoid unintentional color codes. Color codes
in the epilog (the command docstring) are supported.
"""
autohelp = raw(super(UnixCommandParser, self).format_help())
return "\n" + autohelp + "\n" + self.post_help
def print_usage(self, file=None):
"""Print the usage to the caller.
Args:
file (file-object): not used here, the caller is used.
Note:
This method will override `argparse.ArgumentParser`'s in order
to not display the help on stdout or stderr, but to the
command's caller.
"""
if self.command:
self.command.msg(self.format_usage().strip())
def print_help(self, file=None):
"""Print the help to the caller.
Args:
file (file-object): not used here, the caller is used.
Note:
This method will override `argparse.ArgumentParser`'s in order
to not display the help on stdout or stderr, but to the
command's caller.
"""
if self.command:
self.command.msg(self.format_help().strip())
class HelpAction(argparse.Action):
"""Override the -h/--help action in the default parser.
Using the default -h/--help will call the exit function in different
ways, preventing the entire help message to be provided. Hence
this override.
"""
def __call__(self, parser, namespace, values, option_string=None):
"""If asked for help, display to the caller."""
if parser.command:
parser.command.msg(parser.format_help().strip())
parser.exit(0, "")
class UnixCommand(Command):
"""
Unix-type commands, supporting short and long options.
This command syntax uses the Unix-style commands with short options
(-X) and long options (--something). The `argparse` module is
used to parse the command.
In order to use it, you should override two methods:
- `init_parser`: this method is called when the command is created.
It can be used to set options in the parser. `self.parser`
contains the `argparse.ArgumentParser`, so you can add arguments
here.
- `func`: this method is called to execute the command, but after
the parser has checked the arguments given to it are valid.
You can access the namespace of valid arguments in `self.opts`
at this point.
The help of UnixCommands is derived from the docstring, in a
slightly different way than usual: the first line of the docstring
is used to represent the program description (the very short
line at the top of the help message). The other lines below are
used as the program's "epilog", displayed below the options. It
means in your docstring, you don't have to write the options.
They will be automatically provided by the parser and displayed
accordingly. The `argparse` module provides a default '-h' or
'--help' option on the command. Typing |whelp commandname|n will
display the same as |wcommandname -h|n, though this behavior can
be changed.
"""
def __init__(self, **kwargs):
"""
The lockhandler works the same as for objects.
optional kwargs will be set as properties on the Command at runtime,
overloading evential same-named class properties.
"""
super(UnixCommand, self).__init__(**kwargs)
# Create the empty UnixCommandParser, inheriting argparse.ArgumentParser
lines = dedent(self.__doc__.strip("\n")).splitlines()
description = lines[0].strip()
epilog = "\n".join(lines[1:]).strip()
self.parser = UnixCommandParser(None, description, epilog, command=self)
# Fill the argument parser
self.init_parser()
def init_parser(self):
"""
Configure the argument parser, adding in options.
Note:
This method is to be overridden in order to add options
to the argument parser. Use `self.parser`, which contains
the `argparse.ArgumentParser`. You can, for instance,
use its `add_argument` method.
"""
pass
def func(self):
"""Override to handle the command execution."""
pass
def get_help(self, caller, cmdset):
"""
Return the help message for this command and this caller.
Args:
caller (Object or Player): the caller asking for help on the command.
cmdset (CmdSet): the command set (if you need additional commands).
Returns:
docstring (str): the help text to provide the caller for this command.
"""
return self.parser.format_help()
def parse(self):
"""
Process arguments provided in `self.args`.
Note:
You should not override this method. Consider overriding
`init_parser` instead.
"""
try:
self.opts = self.parser.parse_args(shlex.split(self.args))
except ParseError as err:
msg = str(err)
if msg:
self.msg(msg)
raise InterruptCommand