Add the UnixCommand, to parse unix-like command options

This commit is contained in:
Vincent Le Goff 2017-06-17 16:35:57 -07:00
parent 74cf15b257
commit d6f2d6a305
3 changed files with 271 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."""
def init(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")
key = "dummy"
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,202 @@
"""
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` 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(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 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`: the init 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):
super(UnixCommand, self).__init__()
# Create the empty EvenniaParser, inheriting argparse.ArgumentParser
lines = dedent(self.__doc__.strip("\n")).splitlines()
description = lines[0].strip()
epilog = "\n".join(lines[1:]).strip()
self.parser = EvenniaParser(None, description, epilog, command=self)
# Fill the argument parser
self.init()
def init(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` instead.
"""
try:
self.opts = self.parser.parse_args(shlex.split(self.args))
except ParseError as err:
self.msg(str(err))
raise InterruptCommand
class ParseError(Exception):
"""An error occurred during parsing."""
pass
class EvenniaParser(argparse.ArgumentParser):
"""A parser just for Evennia."""
def __init__(self, prog, description="", epilog="", command=None, **kwargs):
prog = prog or command.key
super(EvenniaParser, self).__init__(
prog=prog, description=description,
formatter_class=argparse.RawDescriptionHelpFormatter,
conflict_handler='resolve', **kwargs)
self.command = command
self.post_help = epilog
def n_exit(code=None, msg=None):
if msg:
raise ParseError(msg)
self.exit = n_exit
def format_usage(self):
"""Return the usage line."""
return raw(super(EvenniaParser, self).format_usage())
def format_help(self):
"""Return the parser help, including its epilog."""
autohelp = raw(super(EvenniaParser, self).format_help())
return autohelp + "\n\n" + self.post_help
def print_usage(self, file=None):
"""Print the usage to the caller."""
if self.command:
self.command.msg(ParseError(self.format_usage()))
else:
raise ParseError(self.format_usage())
def print_help(self, file=None):
"""Print the help to the caller."""
if self.command:
self.command.msg(ParseError(self.format_help()))
else:
raise ParseError(self.format_help())