diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index e6a6f36590..55896db1b2 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -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 diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 46016be3a1..bbf6d29d43 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -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)) diff --git a/evennia/contrib/unixcommand.py b/evennia/contrib/unixcommand.py new file mode 100644 index 0000000000..850ade42d3 --- /dev/null +++ b/evennia/contrib/unixcommand.py @@ -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