diff --git a/bin/project_rename.py b/bin/project_rename.py index 6723e3fd0c..9d55bdcfc7 100644 --- a/bin/project_rename.py +++ b/bin/project_rename.py @@ -25,6 +25,25 @@ FAKE_MODE = False # if these words are longer than output word, retain given case CASE_WORD_EXCEPTIONS = ('an', ) +_HELP_TEXT = """This program interactively renames words in all files of your project. It's +currently renaming {sources} to {targets}. + +If it wants to replace text in a file, it will show all lines (and line numbers) it wants to +replace, each directly followed by the suggested replacement. + +If a rename is not okay, you can de-select it by entering 'i' followed by one or more +comma-separated line numbers. You cannot ignore partial lines, those you need to remember to change +manually later. + +[q]uit - exits the program immediately. +[h]elp - this help. +[s]kip file - make no changes at all in this file, continue on to the next. +[i]ignore lines - specify line numbers to not change. +[c]lear ignores - this reverts all your ignores if you make a mistake. +[a]accept/save file - apply all accepted renames and continue on to the next file. + +(return to continue) +""" # Helper functions @@ -227,10 +246,11 @@ def rename_in_file(path, in_list, out_list, is_interactive): ret = raw_input(_green("Choose: " "[q]uit, " + "[h]elp, " "[s]kip file, " "[i]gnore lines, " "[c]lear ignores, " - "[a]ccept/save file: ")) + "[a]ccept/save file: ".lower())) if ret == "s": # skip file entirely @@ -252,8 +272,10 @@ def rename_in_file(path, in_list, out_list, is_interactive): print(" ... Saved file %s" % path) return elif ret == "q": - print("Quit renaming.") + print("Quit renaming program.") sys.exit() + elif ret == "h": + raw_input(_HELP_TEXT.format(sources=in_list, targets=out_list)) elif ret.startswith("i"): # ignore one or more lines ignores = [int(ind)-1 for ind in ret[1:].split(',') if ind.strip().isdigit()] diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index 6f1415a14e..bc9c36760e 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -75,6 +75,9 @@ class CommandTest(EvenniaTest): cmdobj.parse() cmdobj.func() cmdobj.at_post_cmd() + except InterruptCommand: + pass + finally: # clean out evtable 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] @@ -90,11 +93,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/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/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 cb53563003..43a238a580 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 fce7b306c9..420089e2e9 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 50795abd99..1a1800c6ae 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 6f822d0778..5bce96bb2e 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 diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index c02accd4db..0f6887ad58 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -451,13 +451,13 @@ class TestChargen(CommandTest): self.assertTrue(self.account.db._character_dbrefs) self.call(chargen.CmdOOCLook(), "", "You, TestAccount, are an OOC ghost without form.",caller=self.account) self.call(chargen.CmdOOCLook(), "testchar", "testchar(", caller=self.account) - + # 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") @@ -500,7 +500,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") @@ -520,28 +520,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 @@ -849,7 +849,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!") @@ -857,9 +857,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") @@ -936,3 +936,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..6a77b7277d --- /dev/null +++ b/evennia/contrib/unixcommand.py @@ -0,0 +1,294 @@ +""" +Unix-like Command style parent + +Evennia contribution, Vincent Le Geoff 2017 + +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: + +```python +class CmdPlant(UnixCommand): + + ''' + Plant a tree or plant. + + This command is used to plant something 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 diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index 45588cf3a3..470463c2ea 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -613,7 +613,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, + **kwargs): """ Moves this object to a new location. @@ -634,6 +635,9 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): (at_before/after_move etc) with quiet=True, this is as quiet a move as can be done. + 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. This method may also return various error messages to the @@ -699,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, **kwargs) except Exception as err: logerr(errtxt % "at_announce_move()", err) return False @@ -714,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, **kwargs) except Exception as err: logerr(errtxt % "announce_move_to()", err) return False diff --git a/evennia/typeclasses/attributes.py b/evennia/typeclasses/attributes.py index 125181a318..35d8d10f2b 100644 --- a/evennia/typeclasses/attributes.py +++ b/evennia/typeclasses/attributes.py @@ -220,7 +220,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( @@ -278,7 +278,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} @@ -303,7 +303,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