Add help entry to project_rename utility.

Also merge in latest master changes to devel branch.
This commit is contained in:
Griatch 2017-07-13 20:31:30 +02:00
commit 6d846c6c82
16 changed files with 492 additions and 124 deletions

View File

@ -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()]

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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,

View File

@ -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(

View File

@ -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

View File

@ -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 "\<string\>", 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

View File

@ -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."""

View File

@ -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).

View File

@ -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

View File

@ -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))

View File

@ -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

View File

@ -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

View File

@ -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