From d6f2d6a30590ccb5769879cd07449dad5548f4b1 Mon Sep 17 00:00:00 2001 From: Vincent Le Goff Date: Sat, 17 Jun 2017 16:35:57 -0700 Subject: [PATCH 1/9] Add the UnixCommand, to parse unix-like command options --- evennia/commands/default/tests.py | 8 +- evennia/contrib/tests.py | 82 +++++++++--- evennia/contrib/unixcommand.py | 202 ++++++++++++++++++++++++++++++ 3 files changed, 271 insertions(+), 21 deletions(-) create mode 100644 evennia/contrib/unixcommand.py 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..8bae394100 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.""" + + 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)) diff --git a/evennia/contrib/unixcommand.py b/evennia/contrib/unixcommand.py new file mode 100644 index 0000000000..0bc4ad9fbe --- /dev/null +++ b/evennia/contrib/unixcommand.py @@ -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()) From 9dc9df422721c0c0bfd3a1161655a40661b419c7 Mon Sep 17 00:00:00 2001 From: Tehom Date: Sun, 18 Jun 2017 17:21:33 -0400 Subject: [PATCH 2/9] Pass kwargs needed to overriding announce_move_to and announce_move_from in move_to --- evennia/objects/objects.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index 4d9bb05c2f..1c9b645cb5 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -614,7 +614,8 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): obj.msg(text=(outmessage, outkwargs), from_obj=from_obj, **kwargs) def move_to(self, destination, quiet=False, - emit_to_obj=None, use_destination=True, to_none=False, move_hooks=True): + emit_to_obj=None, use_destination=True, to_none=False, move_hooks=True, + msg=None, mapping=None): """ Moves this object to a new location. @@ -634,6 +635,8 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): move_hooks (bool): If False, turn off the calling of move-related hooks (at_before/after_move etc) with quiet=True, this is as quiet a move as can be done. + msg (str, optional): a replacement message. + mapping (dict, optional): additional mapping objects. Returns: result (bool): True/False depending on if there were problems with the move. @@ -700,7 +703,7 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): if not quiet: # tell the old room we are leaving try: - self.announce_move_from(destination) + self.announce_move_from(destination, msg=msg, mapping=mapping) except Exception as err: logerr(errtxt % "at_announce_move()", err) return False @@ -715,7 +718,7 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): if not quiet: # Tell the new room we are there. try: - self.announce_move_to(source_location) + self.announce_move_to(source_location, msg=msg, mapping=mapping) except Exception as err: logerr(errtxt % "announce_move_to()", err) return False From 916d7933aa4df4e61c6b6bcbc90529f91a5f1caa Mon Sep 17 00:00:00 2001 From: Vincent Le Goff Date: Sun, 18 Jun 2017 20:29:46 -0700 Subject: [PATCH 3/9] Fix minor errors in displaying the UnixCommand --- evennia/contrib/unixcommand.py | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/evennia/contrib/unixcommand.py b/evennia/contrib/unixcommand.py index 0bc4ad9fbe..dde36207da 100644 --- a/evennia/contrib/unixcommand.py +++ b/evennia/contrib/unixcommand.py @@ -149,7 +149,9 @@ class UnixCommand(Command): try: self.opts = self.parser.parse_args(shlex.split(self.args)) except ParseError as err: - self.msg(str(err)) + msg = str(err) + if msg: + self.msg(msg) raise InterruptCommand @@ -168,16 +170,17 @@ class EvenniaParser(argparse.ArgumentParser): prog = prog or command.key super(EvenniaParser, self).__init__( prog=prog, description=description, - formatter_class=argparse.RawDescriptionHelpFormatter, - conflict_handler='resolve', **kwargs) + conflict_handler='resolve', add_help=False, **kwargs) self.command = command self.post_help = epilog def n_exit(code=None, msg=None): - if msg: - raise ParseError(msg) + raise ParseError(msg) self.exit = n_exit + # Replace the -h/--help + self.add_argument("-h", "--hel", nargs=0, action=HelpAction, help="display heeeelp") + def format_usage(self): """Return the usage line.""" return raw(super(EvenniaParser, self).format_usage()) @@ -185,18 +188,24 @@ class EvenniaParser(argparse.ArgumentParser): 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 + return "\n" + autohelp + "\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()) + self.command.msg(self.format_usage().strip()) 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()) + self.command.msg(self.format_help().strip()) + + +class HelpAction(argparse.Action): + + """Override the -h/--he.p.""" + + def __call__(self, parser, namespace, values, option_string=None): + if parser.command: + parser.command.msg(parser.format_help().strip()) + parser.exit(0, "") From ca6b1f828af4c5754660355124ab84958779342c Mon Sep 17 00:00:00 2001 From: Vincent Le Goff Date: Mon, 19 Jun 2017 18:31:50 -0700 Subject: [PATCH 4/9] Rename the event system in in-game Python system --- .../{events => ingame_python}/README.md | 78 +++++++++---------- .../{events => ingame_python}/__init__.py | 0 .../callbackhandler.py | 4 +- .../{events => ingame_python}/commands.py | 7 +- .../{events => ingame_python}/eventfuncs.py | 6 +- .../{events => ingame_python}/scripts.py | 12 +-- .../{events => ingame_python}/tests.py | 44 +++++------ .../{events => ingame_python}/typeclasses.py | 34 ++++---- .../{events => ingame_python}/utils.py | 2 +- 9 files changed, 92 insertions(+), 95 deletions(-) rename evennia/contrib/{events => ingame_python}/README.md (92%) rename evennia/contrib/{events => ingame_python}/__init__.py (100%) rename evennia/contrib/{events => ingame_python}/callbackhandler.py (98%) rename evennia/contrib/{events => ingame_python}/commands.py (98%) rename evennia/contrib/{events => ingame_python}/eventfuncs.py (94%) rename evennia/contrib/{events => ingame_python}/scripts.py (98%) rename evennia/contrib/{events => ingame_python}/tests.py (91%) rename evennia/contrib/{events => ingame_python}/typeclasses.py (97%) rename evennia/contrib/{events => ingame_python}/utils.py (98%) diff --git a/evennia/contrib/events/README.md b/evennia/contrib/ingame_python/README.md similarity index 92% rename from evennia/contrib/events/README.md rename to evennia/contrib/ingame_python/README.md index d72bf16477..d4de7dc957 100644 --- a/evennia/contrib/events/README.md +++ b/evennia/contrib/ingame_python/README.md @@ -1,9 +1,9 @@ -# Evennia event system +# Evennia in-game Python system Vincent Le Goff 2017 -This contrib adds the system of events in Evennia, allowing immortals (or other trusted builders) to -dynamically add features to individual objects. Using events, every immortal or privileged users +This contrib adds the system of in-game Python in Evennia, allowing immortals (or other trusted builders) to +dynamically add features to individual objects. Using custom Python set in-game, every immortal or privileged users could have a specific room, exit, character, object or something else behave differently from its "cousins". For these familiar with the use of softcode in MU`*`, like SMAUG MudProgs, the ability to add arbitrary behavior to individual objects is a step toward freedom. Keep in mind, however, the @@ -11,26 +11,26 @@ warning below, and read it carefully before the rest of the documentation. ## A WARNING REGARDING SECURITY -Evennia's event system will run arbitrary Python code without much restriction. Such a system is as +Evennia's in-game Python system will run arbitrary Python code without much restriction. Such a system is as powerful as potentially dangerous, and you will have to keep in mind these points before deciding to install it: 1. Untrusted people can run Python code on your game server with this system. Be careful about who can use this system (see the permissions below). -2. You can do all of this in Python outside the game. The event system is not to replace all your +2. You can do all of this in Python outside the game. The in-game Python system is not to replace all your game feature. ## Basic structure and vocabulary -- At the basis of the event system are **events**. An **event** defines the context in which we - would like to call some arbitrary code. For instance, one event is defined on exits and will fire -every time a character traverses through this exit. Events are described on a -[typeclass](https://github.com/evennia/evennia/wiki/Typeclasses) (like -[exits](https://github.com/evennia/evennia/wiki/Objects#exits) in our example). All objects -inheriting from this typeclass will have access to this event. +- At the basis of the in-game Python system are **events**. An **event** defines the context in which we + would like to call some arbitrary code. For instance, one event is + defined on exits and will fire every time a character traverses through this exit. Events are described + on a [typeclass](https://github.com/evennia/evennia/wiki/Typeclasses) (like + [exits](https://github.com/evennia/evennia/wiki/Objects#exits) in our example). All objects inheriting + from this typeclass will have access to this event. - **Callbacks** can be set on individual objects, on events defined in code. These **callbacks** can contain arbitrary code and describe a specific behavior for an object. When the event fires, -all callbacks connected to this object's event are executed. + all callbacks connected to this object's event are executed. To see the system in context, when an object is picked up (using the default `get` command), a specific event is fired: @@ -41,10 +41,10 @@ specific event is fired: the "get" event on this object. 4. All callbacks tied to this object's "get" event will be executed in order. These callbacks act as functions containing Python code that you can write in-game, using specific variables that -will be listed when you edit the callback itself. + will be listed when you edit the callback itself. 5. In individual callbacks, you can add multiple lines of Python code that will be fired at this point. In this example, the `character` variable will contain the character who has picked up -the object, while `obj` will contain the object that was picked up. + the object, while `obj` will contain the object that was picked up. Following this example, if you create a callback "get" on the object "a sword", and put in it: @@ -59,11 +59,11 @@ When you pick up this object you should see something like: ## Installation -Being in a separate contrib, the event system isn't installed by default. You need to do it +Being in a separate contrib, the in-game Python system isn't installed by default. You need to do it manually, following these steps: 1. Launch the main script (important!): - ```@py evennia.create_script("evennia.contrib.events.scripts.EventHandler")``` + ```@py evennia.create_script("evennia.contrib.ingame_python.scripts.EventHandler")``` 2. Set the permissions (optional): - `EVENTS_WITH_VALIDATION`: a group that can edit callbacks, but will need approval (default to `None`). @@ -73,23 +73,23 @@ manually, following these steps: - `EVENTS_CALENDAR`: type of the calendar to be used (either `None`, `"standard"` or `"custom"`, default to `None`). 3. Add the `@call` command. -4. Inherit from the custom typeclasses of the event system. - - `evennia.contrib.events.typeclasses.EventCharacter`: to replace `DefaultCharacter`. - - `evennia.contrib.events.typeclasses.EventExit`: to replace `DefaultExit`. - - `evennia.contrib.events.typeclasses.EventObject`: to replace `DefaultObject`. - - `evennia.contrib.events.typeclasses.EventRoom`: to replace `DefaultRoom`. +4. Inherit from the custom typeclasses of the in-game Python system. + - `evennia.contrib.ingame_python.typeclasses.EventCharacter`: to replace `DefaultCharacter`. + - `evennia.contrib.ingame_python.typeclasses.EventExit`: to replace `DefaultExit`. + - `evennia.contrib.ingame_python.typeclasses.EventObject`: to replace `DefaultObject`. + - `evennia.contrib.ingame_python.typeclasses.EventRoom`: to replace `DefaultRoom`. The following sections describe in details each step of the installation. -> Note: If you were to start the game without having started the main script (such as when +> Note: If you were to start the game without having started the main script (such as when resetting your database) you will most likely face a traceback when logging in, telling you -that a 'callback' property is not defined. After performing step `1` the error will go away. +that a 'callback' property is not defined. After performing step `1` the error will go away. ### Starting the event script To start the event script, you only need a single command, using `@py`. - @py evennia.create_script("evennia.contrib.events.scripts.EventHandler") + @py evennia.create_script("evennia.contrib.ingame_python.scripts.EventHandler") This command will create a global script (that is, a script independent from any object). This script will hold basic configuration, individual callbacks and so on. You may access it directly, @@ -174,7 +174,7 @@ this: ```python from evennia import default_cmds -from evennia.contrib.events.commands import CmdCallback +from evennia.contrib.ingame_python.commands import CmdCallback class CharacterCmdSet(default_cmds.CharacterCmdSet): """ @@ -194,25 +194,25 @@ class CharacterCmdSet(default_cmds.CharacterCmdSet): ### Changing parent classes of typeclasses -Finally, to use the event system, you need to have your typeclasses inherit from the modified event +Finally, to use the in-game Python system, you need to have your typeclasses inherit from the modified event classes. For instance, in your `typeclasses/characters.py` module, you should change inheritance like this: ```python -from evennia.contrib.events.typeclasses import EventCharacter +from evennia.contrib.ingame_python.typeclasses import EventCharacter class Character(EventCharacter): # ... ``` -You should do the same thing for your rooms, exits and objects. Note that the event system works by +You should do the same thing for your rooms, exits and objects. Note that the in-game Python system works by overriding some hooks. Some of these features might not be accessible in your game if you don't call the parent methods when overriding hooks. ## Using the `@call` command -The event system relies, to a great extent, on its `@call` command. Who can execute this command, +The in-game Python system relies, to a great extent, on its `@call` command. Who can execute this command, and who can do what with it, will depend on your set of permissions. The `@call` command allows to add, edit and delete callbacks on specific objects' events. The event @@ -383,7 +383,7 @@ most complex. ### The eventfuncs -In order to make development a little easier, the event system provides eventfuncs to be used in +In order to make development a little easier, the in-game Python system provides eventfuncs to be used in callbacks themselves. You don't have to use them, they are just shortcuts. An eventfunc is just a simple function that can be used inside of your callback code. @@ -473,7 +473,7 @@ And if the character Wilfred takes this exit, others in the room will see: Wildred falls into a hole in the ground! -In this case, the event system placed the variable "message" in the callback locals, but will read +In this case, the in-game Python system placed the variable "message" in the callback locals, but will read from it when the event has been executed. ### Callbacks with parameters @@ -661,15 +661,15 @@ specific events fired. Adding new events should be done in your typeclasses. Events are contained in the `_events` class variable, a dictionary of event names as keys, and tuples to describe these events as values. You -also need to register this class, to tell the event system that it contains events to be added to +also need to register this class, to tell the in-game Python system that it contains events to be added to this typeclass. Here, we want to add a "push" event on objects. In your `typeclasses/objects.py` file, you should write something like: ```python -from evennia.contrib.events.utils import register_events -from evennia.contrib.events.typeclasses import EventObject +from evennia.contrib.ingame_python.utils import register_events +from evennia.contrib.ingame_python.typeclasses import EventObject EVENT_PUSH = """ A character push the object. @@ -692,7 +692,7 @@ class Object(EventObject): } ``` -- Line 1-2: we import several things we will need from the event system. Note that we use +- Line 1-2: we import several things we will need from the in-game Python system. Note that we use `EventObject` as a parent instead of `DefaultObject`, as explained in the installation. - Line 4-12: we usually define the help of the event in a separate variable, this is more readable, though there's no rule against doing it another way. Usually, the help should contain a short @@ -714,7 +714,7 @@ fired. ### Calling an event in code -The event system is accessible through a handler on all objects. This handler is named `callbacks` +The in-game Python system is accessible through a handler on all objects. This handler is named `callbacks` and can be accessed from any typeclassed object (your character, a room, an exit...). This handler offers several methods to examine and call an event or callback on this object. @@ -825,7 +825,7 @@ this is out of the scope of this documentation). The "say" command uses phrase parameters (you can set a "say" callback to fires if a phrase contains one specific word). -In both cases, you need to import a function from `evennia.contrib.events.utils` and use it as third +In both cases, you need to import a function from `evennia.contrib.ingame_python.utils` and use it as third parameter in your event definition. - `keyword_event` should be used for keyword parameters. @@ -834,7 +834,7 @@ parameter in your event definition. For example, here is the definition of the "say" event: ```python -from evennia.contrib.events.utils import register_events, phrase_event +from evennia.contrib.ingame_python.utils import register_events, phrase_event # ... @register_events class SomeTypeclass: @@ -865,5 +865,5 @@ The best way to do this is to use a custom setting, in your setting file EVENTS_DISABLED = True ``` -The event system will still be accessible (you will have access to the `@call` command, to debug), +The in-game Python system will still be accessible (you will have access to the `@call` command, to debug), but no event will be called automatically. diff --git a/evennia/contrib/events/__init__.py b/evennia/contrib/ingame_python/__init__.py similarity index 100% rename from evennia/contrib/events/__init__.py rename to evennia/contrib/ingame_python/__init__.py diff --git a/evennia/contrib/events/callbackhandler.py b/evennia/contrib/ingame_python/callbackhandler.py similarity index 98% rename from evennia/contrib/events/callbackhandler.py rename to evennia/contrib/ingame_python/callbackhandler.py index 9d6c0b1111..5baef04523 100644 --- a/evennia/contrib/events/callbackhandler.py +++ b/evennia/contrib/ingame_python/callbackhandler.py @@ -7,9 +7,9 @@ from collections import namedtuple class CallbackHandler(object): """ - The event handler for a specific object. + The callback handler for a specific object. - The script that contains all events will be reached through this + The script that contains all callbacks will be reached through this handler. This handler is therefore a shortcut to be used by developers. This handler (accessible through `obj.callbacks`) is a shortcut to manipulating callbacks within this object, getting, diff --git a/evennia/contrib/events/commands.py b/evennia/contrib/ingame_python/commands.py similarity index 98% rename from evennia/contrib/events/commands.py rename to evennia/contrib/ingame_python/commands.py index ddc42d42fa..3413cecbb6 100644 --- a/evennia/contrib/events/commands.py +++ b/evennia/contrib/ingame_python/commands.py @@ -1,5 +1,5 @@ """ -Module containing the commands of the callback system. +Module containing the commands of the in-game Python system. """ from datetime import datetime @@ -10,7 +10,7 @@ from evennia.utils.ansi import raw from evennia.utils.eveditor import EvEditor from evennia.utils.evtable import EvTable from evennia.utils.utils import class_from_module, time_format -from evennia.contrib.events.utils import get_event_handler +from evennia.contrib.ingame_python.utils import get_event_handler COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) @@ -358,9 +358,6 @@ class CmdCallback(COMMAND_DEFAULT_CLASS): # Open the editor callback = dict(callback) - callback["obj"] = obj - callback["name"] = callback_name - callback["number"] = number self.caller.db._callback = callback EvEditor(self.caller, loadfunc=_ev_load, savefunc=_ev_save, quitfunc=_ev_quit, key="Callback {} of {}".format( diff --git a/evennia/contrib/events/eventfuncs.py b/evennia/contrib/ingame_python/eventfuncs.py similarity index 94% rename from evennia/contrib/events/eventfuncs.py rename to evennia/contrib/ingame_python/eventfuncs.py index cf323771e6..12ec39f820 100644 --- a/evennia/contrib/events/eventfuncs.py +++ b/evennia/contrib/ingame_python/eventfuncs.py @@ -6,14 +6,14 @@ Eventfuncs are just Python functions that can be used inside of calllbacks. """ from evennia import ObjectDB, ScriptDB -from evennia.contrib.events.utils import InterruptEvent +from evennia.contrib.ingame_python.utils import InterruptEvent def deny(): """ - Deny, that is stop, the event here. + Deny, that is stop, the callback here. Notes: - This function will raise an exception to terminate the event + This function will raise an exception to terminate the callback in a controlled way. If you use this function in an event called prior to a command, the command will be cancelled as well. Good situations to use the `deny()` function are in events that begins diff --git a/evennia/contrib/events/scripts.py b/evennia/contrib/ingame_python/scripts.py similarity index 98% rename from evennia/contrib/events/scripts.py rename to evennia/contrib/ingame_python/scripts.py index a61d308e59..11a1b9e24a 100644 --- a/evennia/contrib/events/scripts.py +++ b/evennia/contrib/ingame_python/scripts.py @@ -1,5 +1,5 @@ """ -Scripts for the event system. +Scripts for the in-game Python system. """ from datetime import datetime, timedelta @@ -15,8 +15,8 @@ from evennia.utils.ansi import raw from evennia.utils.create import create_channel from evennia.utils.dbserialize import dbserialize from evennia.utils.utils import all_from_module, delay, pypath_to_realpath -from evennia.contrib.events.callbackhandler import CallbackHandler -from evennia.contrib.events.utils import get_next_wait, EVENTS, InterruptEvent +from evennia.contrib.ingame_python.callbackhandler import CallbackHandler +from evennia.contrib.ingame_python.utils import get_next_wait, EVENTS, InterruptEvent # Constants RE_LINE_ERROR = re.compile(r'^ File "\", line (\d+)') @@ -29,7 +29,7 @@ class EventHandler(DefaultScript): This script shouldn't be created more than once. It contains event (in a non-persistent attribute) and callbacks (in a persistent attribute). The script method would help adding, - editing and deleting these events. + editing and deleting these events and callbacks. """ @@ -68,7 +68,7 @@ class EventHandler(DefaultScript): # Generate locals self.ndb.current_locals = {} self.ndb.fresh_locals = {} - addresses = ["evennia.contrib.events.eventfuncs"] + addresses = ["evennia.contrib.ingame_python.eventfuncs"] addresses.extend(getattr(settings, "EVENTFUNCS_LOCATIONS", ["world.eventfuncs"])) for address in addresses: if pypath_to_realpath(address): @@ -85,7 +85,7 @@ class EventHandler(DefaultScript): delay(seconds, complete_task, task_id) # Place the script in the CallbackHandler - from evennia.contrib.events import typeclasses + from evennia.contrib.ingame_python import typeclasses CallbackHandler.script = self DefaultObject.callbacks = typeclasses.EventObject.callbacks diff --git a/evennia/contrib/events/tests.py b/evennia/contrib/ingame_python/tests.py similarity index 91% rename from evennia/contrib/events/tests.py rename to evennia/contrib/ingame_python/tests.py index c2a8bd4b04..10eed40a41 100644 --- a/evennia/contrib/events/tests.py +++ b/evennia/contrib/ingame_python/tests.py @@ -1,5 +1,5 @@ """ -Module containing the test cases for the event system. +Module containing the test cases for the in-game Python system. """ from mock import Mock @@ -12,8 +12,8 @@ from evennia.objects.objects import ExitCommand from evennia.utils import ansi, utils from evennia.utils.create import create_object, create_script from evennia.utils.test_resources import EvenniaTest -from evennia.contrib.events.commands import CmdCallback -from evennia.contrib.events.callbackhandler import CallbackHandler +from evennia.contrib.ingame_python.commands import CmdCallback +from evennia.contrib.ingame_python.callbackhandler import CallbackHandler # Force settings settings.EVENTS_CALENDAR = "standard" @@ -31,18 +31,18 @@ class TestEventHandler(EvenniaTest): """Create the event handler.""" super(TestEventHandler, self).setUp() self.handler = create_script( - "evennia.contrib.events.scripts.EventHandler") + "evennia.contrib.ingame_python.scripts.EventHandler") # Copy old events if necessary if OLD_EVENTS: self.handler.ndb.events = dict(OLD_EVENTS) # Alter typeclasses - self.char1.swap_typeclass("evennia.contrib.events.typeclasses.EventCharacter") - self.char2.swap_typeclass("evennia.contrib.events.typeclasses.EventCharacter") - self.room1.swap_typeclass("evennia.contrib.events.typeclasses.EventRoom") - self.room2.swap_typeclass("evennia.contrib.events.typeclasses.EventRoom") - self.exit.swap_typeclass("evennia.contrib.events.typeclasses.EventExit") + self.char1.swap_typeclass("evennia.contrib.ingame_python.typeclasses.EventCharacter") + self.char2.swap_typeclass("evennia.contrib.ingame_python.typeclasses.EventCharacter") + self.room1.swap_typeclass("evennia.contrib.ingame_python.typeclasses.EventRoom") + self.room2.swap_typeclass("evennia.contrib.ingame_python.typeclasses.EventRoom") + self.exit.swap_typeclass("evennia.contrib.ingame_python.typeclasses.EventExit") def tearDown(self): """Stop the event handler.""" @@ -249,18 +249,18 @@ class TestCmdCallback(CommandTest): """Create the callback handler.""" super(TestCmdCallback, self).setUp() self.handler = create_script( - "evennia.contrib.events.scripts.EventHandler") + "evennia.contrib.ingame_python.scripts.EventHandler") # Copy old events if necessary if OLD_EVENTS: self.handler.ndb.events = dict(OLD_EVENTS) # Alter typeclasses - self.char1.swap_typeclass("evennia.contrib.events.typeclasses.EventCharacter") - self.char2.swap_typeclass("evennia.contrib.events.typeclasses.EventCharacter") - self.room1.swap_typeclass("evennia.contrib.events.typeclasses.EventRoom") - self.room2.swap_typeclass("evennia.contrib.events.typeclasses.EventRoom") - self.exit.swap_typeclass("evennia.contrib.events.typeclasses.EventExit") + self.char1.swap_typeclass("evennia.contrib.ingame_python.typeclasses.EventCharacter") + self.char2.swap_typeclass("evennia.contrib.ingame_python.typeclasses.EventCharacter") + self.room1.swap_typeclass("evennia.contrib.ingame_python.typeclasses.EventRoom") + self.room2.swap_typeclass("evennia.contrib.ingame_python.typeclasses.EventRoom") + self.exit.swap_typeclass("evennia.contrib.ingame_python.typeclasses.EventExit") def tearDown(self): """Stop the callback handler.""" @@ -268,7 +268,7 @@ class TestCmdCallback(CommandTest): OLD_EVENTS.update(self.handler.ndb.events) self.handler.stop() for script in ScriptDB.objects.filter( - db_typeclass_path="evennia.contrib.events.scripts.TimeEventScript"): + db_typeclass_path="evennia.contrib.ingame_python.scripts.TimeEventScript"): script.stop() CallbackHandler.script = None @@ -414,18 +414,18 @@ class TestDefaultCallbacks(CommandTest): """Create the callback handler.""" super(TestDefaultCallbacks, self).setUp() self.handler = create_script( - "evennia.contrib.events.scripts.EventHandler") + "evennia.contrib.ingame_python.scripts.EventHandler") # Copy old events if necessary if OLD_EVENTS: self.handler.ndb.events = dict(OLD_EVENTS) # Alter typeclasses - self.char1.swap_typeclass("evennia.contrib.events.typeclasses.EventCharacter") - self.char2.swap_typeclass("evennia.contrib.events.typeclasses.EventCharacter") - self.room1.swap_typeclass("evennia.contrib.events.typeclasses.EventRoom") - self.room2.swap_typeclass("evennia.contrib.events.typeclasses.EventRoom") - self.exit.swap_typeclass("evennia.contrib.events.typeclasses.EventExit") + self.char1.swap_typeclass("evennia.contrib.ingame_python.typeclasses.EventCharacter") + self.char2.swap_typeclass("evennia.contrib.ingame_python.typeclasses.EventCharacter") + self.room1.swap_typeclass("evennia.contrib.ingame_python.typeclasses.EventRoom") + self.room2.swap_typeclass("evennia.contrib.ingame_python.typeclasses.EventRoom") + self.exit.swap_typeclass("evennia.contrib.ingame_python.typeclasses.EventExit") def tearDown(self): """Stop the callback handler.""" diff --git a/evennia/contrib/events/typeclasses.py b/evennia/contrib/ingame_python/typeclasses.py similarity index 97% rename from evennia/contrib/events/typeclasses.py rename to evennia/contrib/ingame_python/typeclasses.py index e8b0bdb6fd..71877e8571 100644 --- a/evennia/contrib/events/typeclasses.py +++ b/evennia/contrib/ingame_python/typeclasses.py @@ -1,5 +1,5 @@ """ -Typeclasses for the event system. +Typeclasses for the in-game Python system. To use thm, one should inherit from these classes (EventObject, EventRoom, EventCharacter and EventExit). @@ -9,8 +9,8 @@ EventRoom, EventCharacter and EventExit). from evennia import DefaultCharacter, DefaultExit, DefaultObject, DefaultRoom from evennia import ScriptDB from evennia.utils.utils import delay, inherits_from, lazy_property -from evennia.contrib.events.callbackhandler import CallbackHandler -from evennia.contrib.events.utils import register_events, time_event, phrase_event +from evennia.contrib.ingame_python.callbackhandler import CallbackHandler +from evennia.contrib.ingame_python.utils import register_events, time_event, phrase_event # Character help CHARACTER_CAN_DELETE = """ @@ -121,7 +121,7 @@ parameters that should be present, as separate words, in the spoken phrase. For instance, you can set an event tthat would fire if the phrase spoken by the character contains "menu" or "dinner" or "lunch": - @event/add ... = say menu, dinner, lunch + @call/add ... = say menu, dinner, lunch Then if one of the words is present in what the character says, this event will fire. @@ -135,12 +135,12 @@ CHARACTER_TIME = """ A repeated event to be called regularly. This event is scheduled to repeat at different times, specified as parameters. You can set it to run every day at 8:00 AM (game -time). You have to specify the time as an argument to @event/add, like: - @event/add here = time 8:00 +time). You have to specify the time as an argument to @call/add, like: + @call/add here = time 8:00 The parameter (8:00 here) must be a suite of digits separated by spaces, colons or dashes. Keep it as close from a recognizable date format, like this: - @event/add here = time 06-15 12:20 + @call/add here = time 06-15 12:20 This event will fire every year on June the 15th at 12 PM (still game time). Units have to be specified depending on your set calendar (ask a developer for more details). @@ -461,12 +461,12 @@ EXIT_TIME = """ A repeated event to be called regularly. This event is scheduled to repeat at different times, specified as parameters. You can set it to run every day at 8:00 AM (game -time). You have to specify the time as an argument to @event/add, like: - @event/add north = time 8:00 +time). You have to specify the time as an argument to @call/add, like: + @call/add north = time 8:00 The parameter (8:00 here) must be a suite of digits separated by spaces, colons or dashes. Keep it as close from a recognizable date format, like this: - @event/add south = time 06-15 12:20 + @call/add south = time 06-15 12:20 This event will fire every year on June the 15th at 12 PM (still game time). Units have to be specified depending on your set calendar (ask a developer for more details). @@ -559,12 +559,12 @@ OBJECT_TIME = """ A repeated event to be called regularly. This event is scheduled to repeat at different times, specified as parameters. You can set it to run every day at 8:00 AM (game -time). You have to specify the time as an argument to @event/add, like: - @event/add here = time 8:00 +time). You have to specify the time as an argument to @call/add, like: + @call/add here = time 8:00 The parameter (8:00 here) must be a suite of digits separated by spaces, colons or dashes. Keep it as close from a recognizable date format, like this: - @event/add here = time 06-15 12:20 + @call/add here = time 06-15 12:20 This event will fire every year on June the 15th at 12 PM (still game time). Units have to be specified depending on your set calendar (ask a developer for more details). @@ -702,7 +702,7 @@ specify a list of keywords as parameters that should be present, as separate words, in the spoken phrase. For instance, you can set an event tthat would fire if the phrase spoken by the character contains "menu" or "dinner" or "lunch": - @event/add ... = say menu, dinner, lunch + @call/add ... = say menu, dinner, lunch Then if one of the words is present in what the character says, this event will fire. @@ -716,12 +716,12 @@ ROOM_TIME = """ A repeated event to be called regularly. This event is scheduled to repeat at different times, specified as parameters. You can set it to run every day at 8:00 AM (game -time). You have to specify the time as an argument to @event/add, like: - @event/add here = time 8:00 +time). You have to specify the time as an argument to @call/add, like: + @call/add here = time 8:00 The parameter (8:00 here) must be a suite of digits separated by spaces, colons or dashes. Keep it as close from a recognizable date format, like this: - @event/add here = time 06-15 12:20 + @call/add here = time 06-15 12:20 This event will fire every year on June the 15th at 12 PM (still game time). Units have to be specified depending on your set calendar (ask a developer for more details). diff --git a/evennia/contrib/events/utils.py b/evennia/contrib/ingame_python/utils.py similarity index 98% rename from evennia/contrib/events/utils.py rename to evennia/contrib/ingame_python/utils.py index 4380f92b70..63f2966c9b 100644 --- a/evennia/contrib/events/utils.py +++ b/evennia/contrib/ingame_python/utils.py @@ -166,7 +166,7 @@ def time_event(obj, event_name, number, parameters): """ seconds, usual, key = get_next_wait(parameters) - script = create_script("evennia.contrib.events.scripts.TimeEventScript", interval=seconds, obj=obj) + script = create_script("evennia.contrib.ingame_python.scripts.TimeEventScript", interval=seconds, obj=obj) script.key = key script.desc = "event on {}".format(key) script.db.time_format = parameters From f6626e1bf8e2259960d71745f44784ec89298aad Mon Sep 17 00:00:00 2001 From: Tehom Date: Wed, 21 Jun 2017 00:42:04 -0400 Subject: [PATCH 5/9] Fix extremely bizarre issue in which the query using db_model in conjunction with other fields behaved as if it was unindexed if it did not include __iexact keyword. I have absolutely no idea why it acted this way. But __iexact added resolves it. --- evennia/typeclasses/attributes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/evennia/typeclasses/attributes.py b/evennia/typeclasses/attributes.py index 58755b4955..129175bed0 100644 --- a/evennia/typeclasses/attributes.py +++ b/evennia/typeclasses/attributes.py @@ -219,7 +219,7 @@ class AttributeHandler(object): def _fullcache(self): """Cache all attributes of this object""" query = {"%s__id" % self._model: self._objid, - "attribute__db_model": self._model, + "attribute__db_model__iexact": self._model, "attribute__db_attrtype": self._attrtype} attrs = [conn.attribute for conn in getattr(self.obj, self._m2m_fieldname).through.objects.filter(**query)] self._cache = dict(("%s-%s" % (to_str(attr.db_key).lower(), @@ -273,7 +273,7 @@ class AttributeHandler(object): return [] # no such attribute: return an empty list else: query = {"%s__id" % self._model: self._objid, - "attribute__db_model": self._model, + "attribute__db_model__iexact": self._model, "attribute__db_attrtype": self._attrtype, "attribute__db_key__iexact": key.lower(), "attribute__db_category__iexact": category.lower() if category else None} @@ -298,7 +298,7 @@ class AttributeHandler(object): else: # we have to query to make this category up-date in the cache query = {"%s__id" % self._model: self._objid, - "attribute__db_model": self._model, + "attribute__db_model__iexact": self._model, "attribute__db_attrtype": self._attrtype, "attribute__db_category__iexact": category.lower() if category else None} attrs = [conn.attribute for conn From 848b4c588cb5213151367a89e98caff6e9004066 Mon Sep 17 00:00:00 2001 From: Vincent Le Goff Date: Thu, 22 Jun 2017 16:08:10 -0700 Subject: [PATCH 6/9] Bring fixes suggested by the review on UnixCommand --- evennia/contrib/tests.py | 6 +- evennia/contrib/unixcommand.py | 182 +++++++++++++++++++++------------ 2 files changed, 119 insertions(+), 69 deletions(-) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 8bae394100..bbf6d29d43 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -947,14 +947,14 @@ class CmdDummy(UnixCommand): """A dummy UnixCommand.""" - def init(self): + 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") - key = "dummy" - def func(self): nb1 = self.opts.nb1 nb2 = self.opts.nb2 diff --git a/evennia/contrib/unixcommand.py b/evennia/contrib/unixcommand.py index dde36207da..cb65011bbf 100644 --- a/evennia/contrib/unixcommand.py +++ b/evennia/contrib/unixcommand.py @@ -8,7 +8,7 @@ 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 `init_parser` method, which adds options to the parser. - The `func` method, called to execute the command once parsed. Here's a short example: @@ -30,7 +30,7 @@ class CmdPlant(UnixCommand): key = "plant" - def init(self): + def init_parser(self): "Add the arguments to the parser." # 'self.parser' inherits `argparse.ArgumentParser` self.parser.add_argument("key", @@ -62,6 +62,112 @@ 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="", epilogue="", command=None, **kwargs): + 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 = epilogue + 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 epilogue. + + Note: + This method is present to return the raw-escaped help, + in order to avoid unintentional color codes. Color codes + in the epilogue (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. @@ -71,7 +177,7 @@ class UnixCommand(Command): 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. + - `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. @@ -84,7 +190,7 @@ class UnixCommand(Command): 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 + used as the program's "epilogue", 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 @@ -97,16 +203,16 @@ class UnixCommand(Command): def __init__(self, **kwargs): super(UnixCommand, self).__init__() - # Create the empty EvenniaParser, inheriting argparse.ArgumentParser + # 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 = EvenniaParser(None, description, epilog, command=self) + epilogue = "\n".join(lines[1:]).strip() + self.parser = UnixCommandParser(None, description, epilogue, command=self) # Fill the argument parser - self.init() + self.init_parser() - def init(self): + def init_parser(self): """ Configure the argument parser, adding in options. @@ -143,7 +249,7 @@ class UnixCommand(Command): Note: You should not override this method. Consider overriding - `init` instead. + `init_parser` instead. """ try: @@ -153,59 +259,3 @@ class UnixCommand(Command): if msg: self.msg(msg) 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, - 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 heeeelp") - - 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 "\n" + autohelp + "\n" + self.post_help - - def print_usage(self, file=None): - """Print the usage to the caller.""" - if self.command: - self.command.msg(self.format_usage().strip()) - - def print_help(self, file=None): - """Print the help to the caller.""" - if self.command: - self.command.msg(self.format_help().strip()) - - -class HelpAction(argparse.Action): - - """Override the -h/--he.p.""" - - def __call__(self, parser, namespace, values, option_string=None): - if parser.command: - parser.command.msg(parser.format_help().strip()) - parser.exit(0, "") From 20a576a6d92af14bbd122b0b7ca36c308d968867 Mon Sep 17 00:00:00 2001 From: Tehom Date: Sun, 25 Jun 2017 02:40:53 -0400 Subject: [PATCH 7/9] Convert to kwargs as suggested. --- evennia/objects/objects.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index 1c9b645cb5..cffb2e7cdf 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -615,7 +615,7 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): def move_to(self, destination, quiet=False, emit_to_obj=None, use_destination=True, to_none=False, move_hooks=True, - msg=None, mapping=None): + **kwargs): """ Moves this object to a new location. @@ -635,8 +635,9 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): move_hooks (bool): If False, turn off the calling of move-related hooks (at_before/after_move etc) with quiet=True, this is as quiet a move as can be done. - msg (str, optional): a replacement message. - mapping (dict, optional): additional mapping objects. + + Kwargs: + Passed on to announce_move_to and announce_move_from hooks. Returns: result (bool): True/False depending on if there were problems with the move. @@ -703,7 +704,7 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): if not quiet: # tell the old room we are leaving try: - self.announce_move_from(destination, msg=msg, mapping=mapping) + self.announce_move_from(destination, **kwargs) except Exception as err: logerr(errtxt % "at_announce_move()", err) return False @@ -718,7 +719,7 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): if not quiet: # Tell the new room we are there. try: - self.announce_move_to(source_location, msg=msg, mapping=mapping) + self.announce_move_to(source_location, **kwargs) except Exception as err: logerr(errtxt % "announce_move_to()", err) return False From 080cc8a7eb542715c55e7ec8d24981cd969f505d Mon Sep 17 00:00:00 2001 From: Vincent Le Goff Date: Tue, 27 Jun 2017 11:59:45 -0700 Subject: [PATCH 8/9] Clean up UnixCommand to take into account suggestions --- evennia/contrib/unixcommand.py | 42 +++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/evennia/contrib/unixcommand.py b/evennia/contrib/unixcommand.py index cb65011bbf..850ade42d3 100644 --- a/evennia/contrib/unixcommand.py +++ b/evennia/contrib/unixcommand.py @@ -82,13 +82,33 @@ class UnixCommandParser(argparse.ArgumentParser): """ - def __init__(self, prog, description="", epilogue="", command=None, **kwargs): + 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 = epilogue + self.post_help = epilog def n_exit(code=None, msg=None): raise ParseError(msg) @@ -109,12 +129,12 @@ class UnixCommandParser(argparse.ArgumentParser): return raw(super(UnixCommandParser, self).format_usage()) def format_help(self): - """Return the parser help, including its epilogue. + """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 epilogue (the command docstring) are supported. + in the epilog (the command docstring) are supported. """ autohelp = raw(super(UnixCommandParser, self).format_help()) @@ -190,7 +210,7 @@ class UnixCommand(Command): 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 "epilogue", displayed below the options. It + 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 @@ -201,13 +221,19 @@ class UnixCommand(Command): """ def __init__(self, **kwargs): - super(UnixCommand, self).__init__() + """ + 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() - epilogue = "\n".join(lines[1:]).strip() - self.parser = UnixCommandParser(None, description, epilogue, command=self) + epilog = "\n".join(lines[1:]).strip() + self.parser = UnixCommandParser(None, description, epilog, command=self) # Fill the argument parser self.init_parser() From dc859eae57157bd2d59996b68f8a2a4ce75ed583 Mon Sep 17 00:00:00 2001 From: Griatch Date: Tue, 27 Jun 2017 21:31:31 +0200 Subject: [PATCH 9/9] Update some docs, update contrib README. --- evennia/contrib/README.md | 3 +++ evennia/contrib/unixcommand.py | 29 ++++++++++++++++++----------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/evennia/contrib/README.md b/evennia/contrib/README.md index 510d9762bb..50329bf0eb 100644 --- a/evennia/contrib/README.md +++ b/evennia/contrib/README.md @@ -30,6 +30,8 @@ things you want from here into your game folder and change them there. multiple descriptions for time and season as well as details. * GenderSub (Griatch 2015) - Simple example (only) of storing gender on a character and access it in an emote with a custom marker. +* In-game Python (Vincent Le Geoff 2017) - Allow trusted builders to script + objects and events using Python from in-game. * Mail (grungies1138 2016) - An in-game mail system for communication. * Menu login (Griatch 2011) - A login system using menus asking for name/password rather than giving them as one command @@ -51,6 +53,7 @@ things you want from here into your game folder and change them there. as a start to build from. Has attack/disengage and turn timeouts. * Wilderness (titeuf87 2017) - Make infinitely large wilderness areas with dynamically created locations. +* UnixCommand (Vincent Le Geoff 2017) - Add commands with UNIX-style syntax. ## Contrib packages diff --git a/evennia/contrib/unixcommand.py b/evennia/contrib/unixcommand.py index 850ade42d3..6a77b7277d 100644 --- a/evennia/contrib/unixcommand.py +++ b/evennia/contrib/unixcommand.py @@ -1,15 +1,20 @@ """ -Module containing the UnixCommand class. +Unix-like Command style parent -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. +Evennia contribution, Vincent Le Geoff 2017 -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. +This module contains a command class that allows for unix-style command syntax in-game, using +--options, positional arguments and stuff like -n 10 etc similarly to a unix command. It might not +the best syntax for the average player but can be really useful for builders when they need to have +a single command do many things with many options. It uses the ArgumentParser from Python's standard +library under the hood. + +To use, inherit `UnixCommand` from this module from your own commands. You need +to override two methods: + +- The `init_parser` method, which adds options to the parser. Note that you should normally + *not* override the normal `parse` method when inheriting from `UnixCommand`. +- The `func` method, called to execute the command once parsed (like any Command). Here's a short example: @@ -19,7 +24,7 @@ class CmdPlant(UnixCommand): ''' Plant a tree or plant. - This command is used to plant a tree or plant in the room you are in. + This command is used to plant something in the room you are in. Examples: plant orange -a 8 @@ -62,6 +67,7 @@ from textwrap import dedent from evennia import Command, InterruptCommand from evennia.utils.ansi import raw + class ParseError(Exception): """An error occurred during parsing.""" @@ -109,6 +115,7 @@ class UnixCommandParser(argparse.ArgumentParser): conflict_handler='resolve', add_help=False, **kwargs) self.command = command self.post_help = epilog + def n_exit(code=None, msg=None): raise ParseError(msg) @@ -116,7 +123,7 @@ class UnixCommandParser(argparse.ArgumentParser): # Replace the -h/--help self.add_argument("-h", "--hel", nargs=0, action=HelpAction, - help="display the command help") + help="display the command help") def format_usage(self): """Return the usage line.