Resolve merge conflicts with master.

This commit is contained in:
Griatch 2017-08-19 21:30:42 +02:00
commit 7ff783fea1
21 changed files with 900 additions and 122 deletions

View file

@ -82,6 +82,7 @@ EvEditor = None
# Handlers
SESSION_HANDLER = None
TASK_HANDLER = None
TICKER_HANDLER = None
MONITOR_HANDLER = None
CHANNEL_HANDLER = None
@ -124,7 +125,7 @@ def _init():
global search_object, search_script, search_account, search_channel, search_help, search_tag
global create_object, create_script, create_account, create_channel, create_message, create_help_entry
global settings,lockfuncs, logger, utils, gametime, ansi, spawn, managers
global contrib, TICKER_HANDLER, MONITOR_HANDLER, SESSION_HANDLER, CHANNEL_HANDLER
global contrib, TICKER_HANDLER, MONITOR_HANDLER, SESSION_HANDLER, CHANNEL_HANDLER, TASK_HANDLER
from .accounts.accounts import DefaultAccount
from .accounts.accounts import DefaultGuest
@ -178,6 +179,7 @@ def _init():
# handlers
from .scripts.tickerhandler import TICKER_HANDLER
from .scripts.taskhandler import TASK_HANDLER
from .server.sessionhandler import SESSION_HANDLER
from .comms.channelhandler import CHANNEL_HANDLER
from .scripts.monitorhandler import MONITOR_HANDLER

View file

@ -80,6 +80,8 @@ __all__ = ("import_cmdset", "CmdSetHandler")
_CACHED_CMDSETS = {}
_CMDSET_PATHS = utils.make_iter(settings.CMDSET_PATHS)
_IN_GAME_ERRORS = settings.IN_GAME_ERRORS
_CMDSET_FALLBACKS = settings.CMDSET_FALLBACKS
# Output strings
@ -102,6 +104,16 @@ _ERROR_CMDSET_EXCEPTION = _(
Compile/Run error when loading cmdset '{path}'.",
(Traceback was logged {timestamp})""")
_ERROR_CMDSET_FALLBACK = _(
"""
Error encountered for cmdset at path '{path}'.
Replacing with fallback '{fallback_path}'.
""")
_ERROR_CMDSET_NO_FALLBACK = _(
"""Fallback path '{fallback_path}' failed to generate a cmdset."""
)
class _ErrorCmdSet(CmdSet):
"""
@ -351,6 +363,22 @@ class CmdSetHandler(object):
elif path:
cmdset = self._import_cmdset(path)
if cmdset:
if cmdset.key == '_CMDSET_ERROR':
# If a cmdset fails to load, check if we have a fallback path to use
fallback_path = _CMDSET_FALLBACKS.get(path, None)
if fallback_path:
err = _ERROR_CMDSET_FALLBACK.format(path=path, fallback_path=fallback_path)
logger.log_err(err)
if _IN_GAME_ERRORS:
self.obj.msg(err)
cmdset = self._import_cmdset(fallback_path)
# If no cmdset is returned from the fallback, we can't go further
if not cmdset:
err = _ERROR_CMDSET_NO_FALLBACK.format(fallback_path=fallback_path)
logger.log_err(err)
if _IN_GAME_ERRORS:
self.obj.msg(err)
continue
cmdset.permanent = cmdset.key != '_CMDSET_ERROR'
self.cmdset_stack.append(cmdset)

View file

@ -184,7 +184,8 @@ class CmdCopy(ObjManipCommand):
copy an object and its properties
Usage:
@copy[/reset] <original obj> [= new_name][;alias;alias..][:new_location] [,new_name2 ...]
@copy[/reset] <original obj> [= <new_name>][;alias;alias..]
[:<new_location>] [,<new_name2> ...]
switch:
reset - make a 'clean' copy off the object, thus
@ -205,7 +206,8 @@ class CmdCopy(ObjManipCommand):
caller = self.caller
args = self.args
if not args:
caller.msg("Usage: @copy <obj> [=new_name[;alias;alias..]][:new_location] [, new_name2...]")
caller.msg("Usage: @copy <obj> [=<new_name>[;alias;alias..]]"
"[:<new_location>] [, <new_name2>...]")
return
if not self.rhs:
@ -446,7 +448,7 @@ class CmdCreate(ObjManipCommand):
create new objects
Usage:
@create[/drop] objname[;alias;alias...][:typeclass], objname...
@create[/drop] <objname>[;alias;alias...][:typeclass], <objname>...
switch:
drop - automatically drop the new object into your current
@ -481,7 +483,7 @@ class CmdCreate(ObjManipCommand):
caller = self.caller
if not self.args:
string = "Usage: @create[/drop] <newname>[;alias;alias...] [:typeclass_path]"
string = "Usage: @create[/drop] <newname>[;alias;alias...] [:typeclass.path]"
caller.msg(string)
return
@ -684,9 +686,9 @@ class CmdDig(ObjManipCommand):
build new rooms and connect them to the current location
Usage:
@dig[/switches] roomname[;alias;alias...][:typeclass]
[= exit_to_there[;alias][:typeclass]]
[, exit_to_here[;alias][:typeclass]]
@dig[/switches] <roomname>[;alias;alias...][:typeclass]
[= <exit_to_there>[;alias][:typeclass]]
[, <exit_to_here>[;alias][:typeclass]]
Switches:
tel or teleport - move yourself to the new room
@ -718,9 +720,10 @@ class CmdDig(ObjManipCommand):
caller = self.caller
if not self.lhs:
string = "Usage: @dig[/teleport] roomname[;alias;alias...][:parent] [= exit_there"
string = "Usage: @dig[/teleport] <roomname>[;alias;alias...]" \
"[:parent] [= <exit_there>"
string += "[;alias;alias..][:parent]] "
string += "[, exit_back_here[;alias;alias..][:parent]]"
string += "[, <exit_back_here>[;alias;alias..][:parent]]"
caller.msg(string)
return
@ -823,7 +826,7 @@ class CmdTunnel(COMMAND_DEFAULT_CLASS):
create new rooms in cardinal directions only
Usage:
@tunnel[/switch] <direction> [= roomname[;alias;alias;...][:typeclass]]
@tunnel[/switch] <direction> [= <roomname>[;alias;alias;...][:typeclass]]
Switches:
oneway - do not create an exit back to the current location
@ -868,7 +871,8 @@ class CmdTunnel(COMMAND_DEFAULT_CLASS):
"Implements the tunnel command"
if not self.args or not self.lhs:
string = "Usage: @tunnel[/switch] <direction> [= roomname[;alias;alias;...][:typeclass]]"
string = "Usage: @tunnel[/switch] <direction> [= <roomname>" \
"[;alias;alias;...][:typeclass]]"
self.caller.msg(string)
return
if self.lhs not in self.directions:
@ -1025,7 +1029,7 @@ class CmdSetHome(CmdLink):
set an object's home location
Usage:
@home <obj> [= home_location]
@home <obj> [= <home_location>]
The "home" location is a "safety" location for objects; they
will be moved there if their current location ceases to exist. All
@ -1042,7 +1046,7 @@ class CmdSetHome(CmdLink):
def func(self):
"implement the command"
if not self.args:
string = "Usage: @home <obj> [= home_location]"
string = "Usage: @home <obj> [= <home_location>]"
self.caller.msg(string)
return
@ -1076,7 +1080,7 @@ class CmdListCmdSets(COMMAND_DEFAULT_CLASS):
list command sets defined on an object
Usage:
@cmdsets [obj]
@cmdsets <obj>
This displays all cmdsets assigned
to a user. Defaults to yourself.
@ -1105,7 +1109,7 @@ class CmdName(ObjManipCommand):
change the name and/or aliases of an object
Usage:
@name obj = name;alias1;alias2
@name <obj> = <newname>;alias1;alias2
Rename an object to something new. Use *obj to
rename an account.
@ -1322,7 +1326,7 @@ def _convert_from_string(cmd, strobj):
Python 2.6 and later:
Supports all Python structures through literal_eval as long as they
are valid Python syntax. If they are not (such as [test, test2], ie
withtout the quotes around the strings), the entire structure will
without the quotes around the strings), the entire structure will
be converted to a string and a warning will be given.
We need to convert like this since all data being sent over the
@ -1379,6 +1383,7 @@ def _convert_from_string(cmd, strobj):
# nested lists/dicts)
return rec_convert(strobj.strip())
class CmdSetAttribute(ObjManipCommand):
"""
set attribute on an object or account
@ -1398,7 +1403,7 @@ class CmdSetAttribute(ObjManipCommand):
(if any).
The most common data to save with this command are strings and
numbers. You can however also set Python primities such as lists,
numbers. You can however also set Python primitives such as lists,
dictionaries and tuples on objects (this might be important for
the functionality of certain custom objects). This is indicated
by you starting your value with one of |c'|n, |c"|n, |c(|n, |c[|n
@ -1557,7 +1562,7 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
set or change an object's typeclass
Usage:
@typclass[/switch] <object> [= <typeclass.path>]
@typeclass[/switch] <object> [= typeclass.path]
@type ''
@parent ''
@swap - this is a shorthand for using /force/reset flags.
@ -1574,7 +1579,7 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
Example:
@type button = examples.red_button.RedButton
If the typeclass.path is not given, the current object's
If the typeclass_path is not given, the current object's
typeclass is assumed.
View or set an object's typeclass. If setting, the creation hooks
@ -1603,7 +1608,7 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
caller = self.caller
if not self.args:
caller.msg("Usage: %s <object> [=<typeclass]" % self.cmdstring)
caller.msg("Usage: %s <object> [= typeclass]" % self.cmdstring)
return
# get object to swap on
@ -1674,7 +1679,7 @@ class CmdWipe(ObjManipCommand):
clear all attributes from an object
Usage:
@wipe <object>[/attribute[/attribute...]]
@wipe <object>[/<attr>[/<attr>...]]
Example:
@wipe box
@ -1695,7 +1700,7 @@ class CmdWipe(ObjManipCommand):
caller = self.caller
if not self.args:
caller.msg("Usage: @wipe <object>[/attribute/attribute...]")
caller.msg("Usage: @wipe <object>[/<attr>/<attr>...]")
return
# get the attributes set by our custom parser
@ -1727,7 +1732,7 @@ class CmdLock(ObjManipCommand):
Usage:
@lock <object>[ = <lockstring>]
or
@lock[/switch] object/<access_type>
@lock[/switch] <object>/<access_type>
Switch:
del - delete given access type
@ -1747,7 +1752,7 @@ class CmdLock(ObjManipCommand):
an object locked with this string will only be possible to
pick up by Admins or by object with id=25.
You can add several access_types after oneanother by separating
You can add several access_types after one another by separating
them by ';', i.e:
'get:id(25);delete:perm(Builder)'
"""
@ -1761,7 +1766,8 @@ class CmdLock(ObjManipCommand):
caller = self.caller
if not self.args:
string = "@lock <object>[ = <lockstring>] or @lock[/switch] object/<access_type>"
string = "@lock <object>[ = <lockstring>] or @lock[/switch] " \
"<object>/<access_type>"
caller.msg(string)
return
@ -2334,7 +2340,7 @@ class CmdScript(COMMAND_DEFAULT_CLASS):
attach a script to an object
Usage:
@script[/switch] <obj> [= <script.path or scriptkey>]
@script[/switch] <obj> [= script_path or <scriptkey>]
Switches:
start - start all non-running scripts on object, or a given script only
@ -2360,7 +2366,7 @@ class CmdScript(COMMAND_DEFAULT_CLASS):
caller = self.caller
if not self.args:
string = "Usage: @script[/switch] <obj> [= <script.path or script key>]"
string = "Usage: @script[/switch] <obj> [= script_path or <script key>]"
caller.msg(string)
return
@ -2561,7 +2567,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
Usage:
@spawn
@spawn[/switch] prototype_name
@spawn[/switch] <prototype_name>
@spawn[/switch] {prototype dictionary}
Switch:

View file

@ -16,6 +16,7 @@ from evennia.utils.eveditor import EvEditor
from evennia.utils.utils import string_suggestions, class_from_module
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
HELP_MORE = settings.HELP_MORE
# limit symbol import for API
__all__ = ("CmdHelp", "CmdSetHelp")
@ -45,9 +46,9 @@ class CmdHelp(Command):
return_cmdset = True
# Help messages are wrapped in an EvMore call (unless using the webclient
# with separate help popups) If you want to avoid this, simply set the
# 'help_more' flag to False.
help_more = True
# with separate help popups) If you want to avoid this, simply add
# 'HELP_MORE = False' in your settings/conf/settings.py
help_more = HELP_MORE
# suggestion cutoff, between 0 and 1 (1 => perfect match)
suggestion_cutoff = 0.6
@ -197,7 +198,8 @@ class CmdHelp(Command):
# retrieve all available commands and database topics
all_cmds = [cmd for cmd in cmdset if self.check_show_help(cmd, caller)]
all_topics = [topic for topic in HelpEntry.objects.all() if topic.access(caller, 'view', default=True)]
all_categories = list(set([cmd.help_category.lower() for cmd in all_cmds] + [topic.help_category.lower() for topic in all_topics]))
all_categories = list(set([cmd.help_category.lower() for cmd in all_cmds] + [topic.help_category.lower()
for topic in all_topics]))
if query in ("list", "all"):
# we want to list all available help entries, grouped by category
@ -221,7 +223,8 @@ class CmdHelp(Command):
if suggestion_maxnum > 0:
vocabulary = [cmd.key for cmd in all_cmds if cmd] + [topic.key for topic in all_topics] + all_categories
[vocabulary.extend(cmd.aliases) for cmd in all_cmds]
suggestions = [sugg for sugg in string_suggestions(query, set(vocabulary), cutoff=suggestion_cutoff, maxnum=suggestion_maxnum)
suggestions = [sugg for sugg in string_suggestions(query, set(vocabulary), cutoff=suggestion_cutoff,
maxnum=suggestion_maxnum)
if sugg != query]
if not suggestions:
suggestions = [sugg for sugg in vocabulary if sugg != query and sugg.startswith(query)]
@ -230,9 +233,9 @@ class CmdHelp(Command):
match = [cmd for cmd in all_cmds if cmd == query]
if len(match) == 1:
formatted = self.format_help_entry(match[0].key,
match[0].get_help(caller, cmdset),
aliases=match[0].aliases,
suggested=suggestions)
match[0].get_help(caller, cmdset),
aliases=match[0].aliases,
suggested=suggestions)
self.msg_help(formatted)
return
@ -240,16 +243,17 @@ class CmdHelp(Command):
match = list(HelpEntry.objects.find_topicmatch(query, exact=True))
if len(match) == 1:
formatted = self.format_help_entry(match[0].key,
match[0].entrytext,
aliases=match[0].aliases.all(),
suggested=suggestions)
match[0].entrytext,
aliases=match[0].aliases.all(),
suggested=suggestions)
self.msg_help(formatted)
return
# try to see if a category name was entered
if query in all_categories:
self.msg_help(self.format_help_list({query:[cmd.key for cmd in all_cmds if cmd.help_category==query]},
{query:[topic.key for topic in all_topics if topic.help_category==query]}))
self.msg_help(self.format_help_list({query: [cmd.key for cmd in all_cmds if cmd.help_category == query]},
{query: [topic.key for topic in all_topics
if topic.help_category == query]}))
return
# no exact matches found. Just give suggestions.
@ -263,6 +267,7 @@ def _loadhelp(caller):
else:
return ""
def _savehelp(caller, buffer):
entry = caller.db._editing_help
caller.msg("Saved help entry.")
@ -274,6 +279,7 @@ def _quithelp(caller):
caller.msg("Closing the editor.")
del caller.db._editing_help
class CmdSetHelp(COMMAND_DEFAULT_CLASS):
"""
Edit the help database.
@ -305,7 +311,7 @@ class CmdSetHelp(COMMAND_DEFAULT_CLASS):
help_category = "Building"
def func(self):
"Implement the function"
"""Implement the function"""
switches = self.switches
lhslist = self.lhslist
@ -327,7 +333,7 @@ class CmdSetHelp(COMMAND_DEFAULT_CLASS):
# check if we have an old entry with the same name
try:
for querystr in topicstrlist:
old_entry = HelpEntry.objects.find_topicmatch(querystr) # also search by alias
old_entry = HelpEntry.objects.find_topicmatch(querystr) # also search by alias
if old_entry:
old_entry = list(old_entry)[0]
break
@ -350,12 +356,12 @@ class CmdSetHelp(COMMAND_DEFAULT_CLASS):
else:
helpentry = create.create_help_entry(topicstr,
self.rhs, category=category,
locks=lockstring,aliases=aliases)
locks=lockstring, aliases=aliases)
self.caller.db._editing_help = helpentry
EvEditor(self.caller, loadfunc=_loadhelp, savefunc=_savehelp,
quitfunc=_quithelp, key="topic {}".format(topicstr),
persistent=True)
quitfunc=_quithelp, key="topic {}".format(topicstr),
persistent=True)
return
if 'append' in switches or "merge" in switches or "extend" in switches:
@ -399,21 +405,21 @@ class CmdSetHelp(COMMAND_DEFAULT_CLASS):
self.msg("Overwrote the old topic '%s'%s." % (topicstr, aliastxt))
else:
self.msg("Topic '%s'%s already exists. Use /replace to overwrite "
"or /append or /merge to add text to it." % (topicstr, aliastxt))
"or /append or /merge to add text to it." % (topicstr, aliastxt))
else:
# no old entry. Create a new one.
new_entry = create.create_help_entry(topicstr,
self.rhs, category=category,
locks=lockstring,aliases=aliases)
locks=lockstring, aliases=aliases)
if new_entry:
self.msg("Topic '%s'%s was successfully created." % (topicstr, aliastxt))
if 'edit' in switches:
# open the line editor to edit the helptext
self.caller.db._editing_help = new_entry
EvEditor(self.caller, loadfunc=_loadhelp,
savefunc=_savehelp, quitfunc=_quithelp,
key="topic {}".format(new_entry.key),
persistent=True)
savefunc=_savehelp, quitfunc=_quithelp,
key="topic {}".format(new_entry.key),
persistent=True)
return
else:
self.msg("Error when creating topic '%s'%s! Contact an admin." % (topicstr, aliastxt))

View file

@ -483,8 +483,10 @@ class SubscriptionHandler(object):
self._cache = None
def _recache(self):
self._cache = {account : True for account in self.obj.db_account_subscriptions.all()}
self._cache.update({obj : True for obj in self.obj.db_object_subscriptions.all()})
self._cache = {account: True for account in self.obj.db_account_subscriptions.all()
if hasattr(account, 'pk') and account.pk}
self._cache.update({obj: True for obj in self.obj.db_object_subscriptions.all()
if hasattr(obj, 'pk') and obj.pk})
def has(self, entity):
"""
@ -576,14 +578,23 @@ class SubscriptionHandler(object):
are puppeted by an online account.
"""
subs = []
recache_needed = False
for obj in self.all():
if hasattr(obj, 'account'):
if not obj.account:
from django.core.exceptions import ObjectDoesNotExist
try:
if hasattr(obj, 'account'):
if not obj.account:
continue
obj = obj.account
if not obj.is_connected:
continue
obj = obj.account
if not obj.is_connected:
except ObjectDoesNotExist:
# a subscribed object has already been deleted. Mark that we need a recache and ignore it
recache_needed = True
continue
subs.append(obj)
if recache_needed:
self._recache()
return subs
def clear(self):

View file

@ -31,8 +31,6 @@ 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
@ -41,6 +39,8 @@ things you want from here into your game folder and change them there.
* Menu Login (Vincent-lg 2016) - Alternate login system using EvMenu.
* Multidescer (Griatch 2016) - Advanced descriptions combined from
many separate description components, inspired by MUSH.
* Random String Generator (Vincent Le Goff 2017) - Simple pseudo-random
generator of strings with rules, avoiding repetitions.
* RPLanguage (Griatch 2015) - Dynamic obfuscation of emotes when
speaking unfamiliar languages. Also obfuscates whispers.
* RPSystem (Griatch 2015) - Full director-style emoting system
@ -60,6 +60,8 @@ things you want from here into your game folder and change them there.
* EGI_Client (gtaylor 2016) - Client for reporting game status
to the Evennia game index (games.evennia.com)
* In-game Python (Vincent Le Goff 2017) - Allow trusted builders to script
objects and events using Python from in-game.
* Tutorial examples (Griatch 2011, 2015) - A folder of basic
example objects, commands and scripts.
* Tutorial world (Griatch 2011, 2015) - A folder containing the

View file

@ -428,7 +428,7 @@ class CmdExtendedDesc(default_cmds.CmdDesc):
if self.switches and self.switches[0] in ("spring", "summer", "autumn", "winter"):
# a seasonal switch was given
if self.rhs:
caller.msg("Seasonal descs only works with rooms, not objects.")
caller.msg("Seasonal descs only work with rooms, not objects.")
return
switch = self.switches[0]
if not location:

View file

@ -10,7 +10,7 @@ import traceback
from django.conf import settings
from evennia import DefaultObject, DefaultScript, ChannelDB, ScriptDB
from evennia import logger
from evennia import logger, ObjectDB
from evennia.utils.ansi import raw
from evennia.utils.create import create_channel
from evennia.utils.dbserialize import dbserialize
@ -101,21 +101,29 @@ class EventHandler(DefaultScript):
Return a dictionary of events on this object.
Args:
obj (Object): the connected object.
obj (Object or typeclass): the connected object or a general typeclass.
Returns:
A dictionary of the object's events.
Note:
Notes:
Events would define what the object can have as
callbacks. Note, however, that chained callbacks will not
appear in events and are handled separately.
You can also request the events of a typeclass, not a
connected object. This is useful to get the global list
of events for a typeclass that has no object yet.
"""
events = {}
all_events = self.ndb.events
classes = Queue()
classes.put(type(obj))
if isinstance(obj, type):
classes.put(obj)
else:
classes.put(type(obj))
invalid = []
while not classes.empty():
typeclass = classes.get()

View file

@ -0,0 +1,345 @@
"""
Pseudo-random generator and registry
Evennia contribution - Vincent Le Goff 2017
This contrib can be used to generate pseudo-random strings of information
with specific criteria. You could, for instance, use it to generate
phone numbers, license plate numbers, validation codes, non-sensivite
passwords and so on. The strings generated by the generator will be
stored and won't be available again in order to avoid repetition.
Here's a very simple example:
```python
from evennia.contrib.random_string_generator import RandomStringGenerator
# Create a generator for phone numbers
phone_generator = RandomStringGenerator("phone number", r"555-[0-9]{3}-[0-9]{4}")
# Generate a phone number (555-XXX-XXXX with X as numbers)
number = phone_generator.get()
# `number` will contain something like: "555-981-2207"
# If you call `phone_generator.get`, it won't give the same anymore.phone_generator.all()
# Will return a list of all currently-used phone numbers
phone_generator.remove("555-981-2207")
# The number can be generated again
```
To use it, you will need to:
1. Import the `RandomStringGenerator` class from the contrib.
2. Create an instance of this class taking two arguments:
- The name of the gemerator (like "phone number", "license plate"...).
- The regular expression representing the expected results.
3. Use the generator's `all`, `get` and `remove` methods as shown above.
To understand how to read and create regular expressions, you can refer to
[the documentation on the re module](https://docs.python.org/2/library/re.html).
Some examples of regular expressions you could use:
- `r"555-\d{3}-\d{4}"`: 555, a dash, 3 digits, another dash, 4 digits.
- `r"[0-9]{3}[A-Z][0-9]{3}"`: 3 digits, a capital letter, 3 digits.
- `r"[A-Za-z0-9]{8,15}"`: between 8 and 15 letters and digits.
- ...
Behind the scenes, a script is created to store the generated information
for a single generator. The `RandomStringGenerator` object will also
read the regular expression you give to it to see what information is
required (letters, digits, a more restricted class, simple characters...)...
More complex regular expressions (with branches for instance) might not be
available.
"""
from random import choice, randint, seed
import re
import string
import time
from evennia import DefaultScript, ScriptDB
from evennia.utils.create import create_script
class RejectedRegex(RuntimeError):
"""The provided regular expression has been rejected.
More details regarding why this error occurred will be provided in
the message. The usual reason is the provided regular expression is
not specific enough and could lead to inconsistent generating.
"""
pass
class ExhaustedGenerator(RuntimeError):
"""The generator hasn't any available strings to generate anymore."""
pass
class RandomStringGeneratorScript(DefaultScript):
"""
The global script to hold all generators.
It will be automatically created the first time `generate` is called
on a RandomStringGenerator object.
"""
def at_script_creation(self):
"""Hook called when the script is created."""
self.key = "generator_script"
self.desc = "Global generator script"
self.persistent = True
# Permanent data to be stored
self.db.generated = {}
class RandomStringGenerator(object):
"""
A generator class to generate pseudo-random strings with a rule.
The "rule" defining what the generator should provide in terms of
string is given as a regular expression when creating instances of
this class. You can use the `all` method to get all generated strings,
the `get` method to generate a new string, the `remove` method
to remove a generated string, or the `clear` method to remove all
generated strings.
Bear in mind, however, that while the generated strings will be
stored to avoid repetition, the generator will not concern itself
with how the string is stored on the object you use. You probably
want to create a tag to mark this object. This is outside of the scope
of this class.
"""
# We keep the script as a class variable to optimize querying
# with multiple instandces
script = None
def __init__(self, name, regex):
"""
Create a new generator.
Args:
name (str): name of the generator to create.
regex (str): regular expression describing the generator.
Notes:
`name` should be an explicit name. If you use more than one
generator in your game, be sure to give them different names.
This name will be used to store the generated information
in the global script, and in case of errors.
The regular expression should describe the generator, what
it should generate: a phone number, a license plate, a password
or something else. Regular expressions allow you to use
pretty advanced criteria, but be aware that some regular
expressions will be rejected if not specific enough.
Raises:
RejectedRegex: the provided regular expression couldn't be
accepted as a valid generator description.
"""
self.name = name
self.elements = []
self.total = 1
# Analyze the regex if any
if regex:
self._find_elements(regex)
def __repr__(self):
return "<evennia.contrib.random_string_generator.RandomStringGenerator for {}>".format(self.name)
def _get_script(self):
"""Get or create the script."""
if type(self).script:
return type(self).script
try:
script = ScriptDB.objects.get(db_key="generator_script")
except ScriptDB.DoesNotExist:
script = create_script("contrib.random_string_generator.RandomStringGeneratorScript")
type(self).script = script
return script
def _find_elements(self, regex):
"""
Find the elements described in the regular expression. This will
analyze the provided regular expression and try to find elements.
Args:
regex (str): the regular expression.
"""
self.total = 1
self.elements = []
tree = re.sre_parse.parse(regex).data
# `tree` contains a list of elements in the regular expression
for element in tree:
# `eleemnt` is also a list, the first element is a string
name = element[0]
desc = {"min": 1, "max": 1}
# If `.`, break here
if name == "any":
raise RejectedRegex("the . definition is too broad, specify what you need more precisely")
elif name == "at":
# Either the beginning or end, we ignore it
continue
elif name == "min_repeat":
raise RejectedRegex("you have to provide a maximum number of this character class")
elif name == "max_repeat":
desc["min"] = element[1][0]
desc["max"] = element[1][1]
desc["chars"] = self._find_literal(element[1][2][0])
elif name == "in":
desc["chars"] = self._find_literal(element)
elif name == "literal":
desc["chars"] = self._find_literal(element)
else:
raise RejectedRegex("unhandled regex syntax:: {}".format(repr(name)))
self.elements.append(desc)
self.total *= len(desc["chars"]) ** desc["max"]
def _find_literal(self, element):
"""Find the literal corresponding to a piece of regular expression."""
chars = []
if element[0] == "literal":
chars.append(chr(element[1]))
elif element[0] == "in":
negate = False
if element[1][0][0] == "negate":
negate = True
chars = list(string.ascii_letters + string.digits)
for part in element[1]:
if part[0] == "negate":
continue
sublist = self._find_literal(part)
for char in sublist:
if negate:
if char in chars:
chars.remove(char)
else:
chars.append(char)
elif element[0] == "range":
chars = [chr(i) for i in range(element[1][0], element[1][1] + 1)]
elif element[0] == "category":
category = element[1]
if category == "category_digit":
chars = list(string.digits)
elif category == "category_word":
chars = list(string.letters)
else:
raise RejectedRegex("unknown category: {}".format(category))
else:
raise RejectedRegex("cannot find the literal: {}".format(element[0]))
return chars
def all(self):
"""
Return all generated strings for this generator.
Returns:
strings (list of strr): the list of strings that are already
used. The strings that were generated first come first in the list.
"""
script = self._get_script()
generated = list(script.db.generated.get(self.name, []))
return generated
def get(self, store=True, unique=True):
"""
Generate a pseudo-random string according to the regular expression.
Args:
store (bool, optional): store the generated string in the script.
unique (bool, optional): keep on trying if the string is already used.
Returns:
The newly-generated string.
Raises:
ExhaustedGenerator: if there's no available string in this generator.
Note:
Unless asked explicitly, the returned string can't repeat itself.
"""
script = self._get_script()
generated = script.db.generated.get(self.name)
if generated is None:
script.db.generated[self.name] = []
generated = script.db.generated[self.name]
if len(generated) >= self.total:
raise ExhaustedGenerator
# Generate a pseudo-random string that might be used already
result = ""
for element in self.elements:
number = randint(element["min"], element["max"])
chars = element["chars"]
for index in range(number):
char = choice(chars)
result += char
# If the string has already been generated, try again
if result in generated and unique:
# Change the random seed, incrementing it slowly
epoch = time.time()
while result in generated:
epoch += 1
seed(epoch)
result = self.get(store=False, unique=False)
if store:
generated.append(result)
return result
def remove(self, element):
"""
Remove a generated string from the list of stored strings.
Args:
element (str): the string to remove from the list of generated strings.
Raises:
ValueError: the specified value hasn't been generated and is not present.
Note:
The specified string has to be present in the script (so
has to have been generated). It will remove this entry
from the script, so this string could be generated again by
calling the `get` method.
"""
script = self._get_script()
generated = script.db.generated.get(self.name, [])
if element not in generated:
raise ValueError("the string {} isn't stored as generated by the generator {}".format(
element, self.name))
generated.remove(element)
def clear(self):
"""
Clear the generator of all generated strings.
"""
script = self._get_script()
generated = script.db.generated.get(self.name, [])
generated[:] = []

View file

@ -1046,3 +1046,23 @@ class TestColorMarkup(EvenniaTest):
bright_map = color_markups.MUX_COLOR_ANSI_XTERM256_BRIGHT_BG_EXTRA_MAP
self.assertEqual(bright_map[0][1], '%c[500')
self.assertEqual(bright_map[-1][1], '%c[222')
from evennia.contrib import random_string_generator
SIMPLE_GENERATOR = random_string_generator.RandomStringGenerator("simple", "[01]{2}")
class TestRandomStringGenerator(EvenniaTest):
def test_generate(self):
"""Generate and fail when exhausted."""
generated = []
for i in range(4):
generated.append(SIMPLE_GENERATOR.get())
generated.sort()
self.assertEqual(generated, ["00", "01", "10", "11"])
# At this point, we have generated 4 strings.
# We can't generate one more
with self.assertRaises(random_string_generator.ExhaustedGenerator):
SIMPLE_GENERATOR.get()

View file

@ -88,7 +88,7 @@ Customisation example:
def at_prepare_room(self, coordinates, caller, room):
"Any other changes done to the room before showing it"
x, y = coordinates
desc = "This is a room in the pyramid.
desc = "This is a room in the pyramid."
if y == 3 :
desc = "You can see far and wide from the top of the pyramid."
room.db.desc = desc
@ -157,7 +157,7 @@ def enter_wilderness(obj, coordinates=(0, 0), name="default"):
default one
Returns:
bool: True if obj succesfully moved into the wilderness.
bool: True if obj successfully moved into the wilderness.
"""
if not WildernessScript.objects.filter(db_key=name).exists():
return False
@ -253,6 +253,11 @@ class WildernessScript(DefaultScript):
room.ndb.wildernessscript = self
room.ndb.active_coordinates = coordinates
for item in self.db.itemcoordinates.keys():
# Items deleted from the wilderness leave None type 'ghosts'
# that must be cleaned out
if item is None:
del self.db.itemcoordinates[item]
continue
item.ndb.wilderness = self
def is_valid_coordinates(self, coordinates):
@ -298,6 +303,11 @@ class WildernessScript(DefaultScript):
"""
result = []
for item, item_coordinates in self.itemcoordinates.items():
# Items deleted from the wilderness leave None type 'ghosts'
# that must be cleaned out
if item is None:
del self.db.itemcoordinates[item]
continue
if coordinates == item_coordinates:
result.append(item)
return result
@ -503,7 +513,7 @@ class WildernessRoom(DefaultRoom):
moved_obj (Object): The object moved into this one.
source_location (Object): Where `moved_obj` came from.
"""
if moved_obj.destination and moved_obj.destination == moved_obj.location:
if isinstance(moved_obj, WildernessExit):
# Ignore exits looping back to themselves: those are the regular
# n, ne, ... exits.
return

View file

@ -0,0 +1,188 @@
"""
Module containing the task handler for Evennia deferred tasks, persistent or not.
"""
from datetime import datetime, timedelta
from twisted.internet import reactor, task
from evennia.server.models import ServerConfig
from evennia.utils.logger import log_trace, log_err
from evennia.utils.dbserialize import dbserialize, dbunserialize
TASK_HANDLER = None
class TaskHandler(object):
"""
A light singleton wrapper allowing to access permanent tasks.
When `utils.delay` is called, the task handler is used to create
the task. If `utils.delay` is called with `persistent=True`, the
task handler stores the new task and saves.
It's easier to access these tasks (should it be necessary) using
`evennia.scripts.taskhandler.TASK_HANDLER`, which contains one
instance of this class, and use its `add` and `remove` methods.
"""
def __init__(self):
self.tasks = {}
self.to_save = {}
def load(self):
"""Load from the ServerConfig.
Note:
This should be automatically called when Evennia starts.
It populates `self.tasks` according to the ServerConfig.
"""
value = ServerConfig.objects.conf("delayed_tasks", default={})
if isinstance(value, basestring):
tasks = dbunserialize(value)
else:
tasks = value
# At this point, `tasks` contains a dictionary of still-serialized tasks
for task_id, value in tasks.items():
date, callback, args, kwargs = dbunserialize(value)
if isinstance(callback, tuple):
# `callback` can be an object and name for instance methods
obj, method = callback
callback = getattr(obj, method)
self.tasks[task_id] = (date, callback, args, kwargs)
def save(self):
"""Save the tasks in ServerConfig."""
for task_id, (date, callback, args, kwargs) in self.tasks.items():
if task_id in self.to_save:
continue
if getattr(callback, "__self__", None):
# `callback` is an instance method
obj = callback.__self__
name = callback.__name__
callback = (obj, name)
# Check if callback can be pickled. args and kwargs have been checked
safe_callback = None
try:
dbserialize(callback)
except (TypeError, AttributeError):
raise ValueError("the specified callback {} cannot be pickled. " \
"It must be a top-level function in a module or an " \
"instance method.".format(callback))
else:
safe_callback = callback
self.to_save[task_id] = dbserialize((date, safe_callback, args, kwargs))
ServerConfig.objects.conf("delayed_tasks", self.to_save)
def add(self, timedelay, callback, *args, **kwargs):
"""Add a new persistent task in the configuration.
Args:
timedelay (int or float): time in sedconds before calling the callback.
callback (function or instance method): the callback itself
any (any): any additional positional arguments to send to the callback
Kwargs:
persistent (bool, optional): persist the task (store it).
any (any): additional keyword arguments to send to the callback
"""
persistent = kwargs.get("persistent", False)
if persistent:
del kwargs["persistent"]
now = datetime.now()
delta = timedelta(seconds=timedelay)
# Choose a free task_id
safe_args = []
safe_kwargs = {}
used_ids = self.tasks.keys()
task_id = 1
while task_id in used_ids:
task_id += 1
# Check that args and kwargs contain picklable information
for arg in args:
try:
dbserialize(arg)
except (TypeError, AttributeError):
logger.log_err("The positional argument {} cannot be " \
"pickled and will not be present in the arguments " \
"fed to the callback {}".format(arg, callback))
else:
safe_args.append(arg)
for key, value in kwargs.items():
try:
dbserialize(value)
except (TypeError, AttributeError):
logger.log_err("The {} keyword argument {} cannot be " \
"pickled and will not be present in the arguments " \
"fed to the callback {}".format(key, value, callback))
else:
safe_kwargs[key] = value
self.tasks[task_id] = (now + delta, callback, safe_args, safe_kwargs)
self.save()
callback = self.do_task
args = [task_id]
kwargs = {}
return task.deferLater(reactor, timedelay, callback, *args, **kwargs)
def remove(self, task_id):
"""Remove a persistent task without executing it.
Args:
task_id (int): an existing task ID.
Note:
A non-persistent task doesn't have a task_id, it is not stored
in the TaskHandler.
"""
del self.tasks[task_id]
if task_id in self.to_save:
del self.to_save[task_id]
self.save()
def do_task(self, task_id):
"""Execute the task (call its callback).
Args:
task_id (int): a valid task ID.
Note:
This will also remove it from the list of current tasks.
"""
date, callback, args, kwargs = self.tasks.pop(task_id)
if task_id in self.to_save:
del self.to_save[task_id]
self.save()
callback(*args, **kwargs)
def create_delays(self):
"""Create the delayed tasks for the persistent tasks.
Note:
This method should be automatically called when Evennia starts.
"""
now = datetime.now()
for task_id, (date, callbac, args, kwargs) in self.tasks.items():
seconds = max(0, (date - now).total_seconds())
task.deferLater(reactor, seconds, self.do_task, task_id)
# Create the soft singleton
TASK_HANDLER = TaskHandler()

View file

@ -161,7 +161,7 @@ ERROR_SETTINGS = \
1) You are not running this command from your game directory.
Change directory to your game directory and try again (or
create a new game directory using evennia --init <dirname>)
2) The ettings file contains a syntax error. If you see a
2) The settings file contains a syntax error. If you see a
traceback above, review it, resolve the problem and try again.
3) Django is not correctly installed. This usually shows as
errors mentioning 'DJANGO_SETTINGS_MODULE'. If you run a
@ -315,7 +315,7 @@ ERROR_LOGDIR_MISSING = \
will be created automatically).
(Explanation: Evennia creates the log directory automatically when
initializating a new game directory. This error usually happens if
initializing a new game directory. This error usually happens if
you used git to clone a pre-created game directory - since log
files are in .gitignore they will not be cloned, which leads to
the log directory also not being created.)
@ -1329,7 +1329,7 @@ def main():
arg, value = [p.strip() for p in arg.split("=", 1)]
else:
value = True
kwargs[arg.lstrip("--")] = [value]
kwargs[arg.lstrip("--")] = value
else:
args.append(arg)
try:

View file

@ -3,7 +3,7 @@
This runner is controlled by the evennia launcher and should normally
not be launched directly. It manages the two main Evennia processes
(Server and Portal) and most importanly runs a passive, threaded loop
(Server and Portal) and most importantly runs a passive, threaded loop
that makes sure to restart Server whenever it shuts down.
Since twistd does not allow for returning an optional exit code we
@ -137,7 +137,7 @@ def cycle_logfile(logfile):
def start_services(server_argv, portal_argv, doexit=False):
"""
This calls a threaded loop that launces the Portal and Server
This calls a threaded loop that launches the Portal and Server
and then restarts them when they finish.
"""
global SERVER, PORTAL

View file

@ -93,6 +93,7 @@ IRC_COLOR_MAP = dict([
(r'|_', " "), # space
(r'|*', ""), # invert
(r'|^', ""), # blinking text
(r'|h', IRC_BOLD), # highlight, use bold instead
(r'|r', IRC_COLOR + IRC_RED),
(r'|g', IRC_COLOR + IRC_GREEN),

View file

@ -455,6 +455,11 @@ class Evennia(object):
# (this also starts any that didn't yet start)
ScriptDB.objects.validate(init_mode=mode)
# start the task handler
from evennia.scripts.taskhandler import TASK_HANDLER
TASK_HANDLER.load()
TASK_HANDLER.create_delays()
# delete the temporary setting
ServerConfig.objects.conf("server_restart_mode", delete=True)

View file

@ -201,7 +201,12 @@ class SessionHandler(dict):
if _INLINEFUNC_ENABLED and not raw and isinstance(self, ServerSessionHandler):
# only parse inlinefuncs on the outgoing path (sessionhandler->)
data = parse_inlinefunc(data, strip=strip_inlinefunc, session=session)
return data
# At this point the object is certainly the right encoding, but may still be a unicode object--
# to_str does not actually force objects to become bytestrings.
# If the unicode object is a subclass of unicode, such as ANSIString, this can cause a problem,
# as special behavior for that class will still be in play. Since we're now transferring raw data,
# we must now force this to be a proper bytestring.
return str(data)
elif hasattr(data, "id") and hasattr(data, "db_date_created") \
and hasattr(data, '__dbclass__'):
# convert database-object to their string representation.

View file

@ -110,7 +110,7 @@ AMP_INTERFACE = '127.0.0.1'
EVENNIA_DIR = os.path.dirname(os.path.abspath(__file__))
# Path to the game directory (containing the server/conf/settings.py file)
# This is dynamically created- there is generally no need to change this!
if sys.argv[1] == 'test' if len(sys.argv)>1 else False:
if sys.argv[1] == 'test' if len(sys.argv) > 1 else False:
# unittesting mode
GAME_DIR = os.getcwd()
else:
@ -208,14 +208,14 @@ MAX_CONNECTION_RATE = 2
# from the client! To turn the limiter off, set to <= 0.
MAX_COMMAND_RATE = 80
# The warning to echo back to users if they send commands too fast
COMMAND_RATE_WARNING ="You entered commands too fast. Wait a moment and try again."
COMMAND_RATE_WARNING = "You entered commands too fast. Wait a moment and try again."
# Determine how large of a string can be sent to the server in number
# of characters. If they attempt to enter a string over this character
# limit, we stop them and send a message. To make unlimited, set to
# 0 or less.
MAX_CHAR_LIMIT = 6000
# The warning to echo back to users if they enter a very large string
MAX_CHAR_LIMIT_WARNING="You entered a string that was too long. Please break it up into multiple parts."
MAX_CHAR_LIMIT_WARNING = "You entered a string that was too long. Please break it up into multiple parts."
# If this is true, errors and tracebacks from the engine will be
# echoed as text in-game as well as to the log. This can speed up
# debugging. OBS: Showing full tracebacks to regular users could be a
@ -381,6 +381,12 @@ CMDSET_CHARACTER = "commands.default_cmdsets.CharacterCmdSet"
CMDSET_ACCOUNT = "commands.default_cmdsets.AccountCmdSet"
# Location to search for cmdsets if full path not given
CMDSET_PATHS = ["commands", "evennia", "contribs"]
# Fallbacks for cmdset paths that fail to load. Note that if you change the path for your default cmdsets,
# you will also need to copy CMDSET_FALLBACKS after your change in your settings file for it to detect the change.
CMDSET_FALLBACKS = {CMDSET_CHARACTER: 'evennia.commands.default.cmdset_character.CharacterCmdSet',
CMDSET_ACCOUNT: 'evennia.commands.default.cmdset_account.AccountCmdSet',
CMDSET_SESSION: 'evennia.commands.default.cmdset_session.SessionCmdSet',
CMDSET_UNLOGGEDIN: 'evennia.commands.default.cmdset_unloggedin.UnloggedinCmdSet'}
# Parent class for all default commands. Changing this class will
# modify all default commands, so do so carefully.
COMMAND_DEFAULT_CLASS = "evennia.commands.default.muxcommand.MuxCommand"
@ -527,8 +533,12 @@ PERMISSION_ACCOUNT_DEFAULT = "Player"
# Default sizes for client window (in number of characters), if client
# is not supplying this on its own
CLIENT_DEFAULT_WIDTH = 78
CLIENT_DEFAULT_HEIGHT = 45 # telnet standard is 24 but does anyone use such
# low-res displays anymore?
# telnet standard height is 24; does anyone use such low-res displays anymore?
CLIENT_DEFAULT_HEIGHT = 45
# Help output from CmdHelp are wrapped in an EvMore call
# (excluding webclient with separate help popups). If continuous scroll
# is preferred, change 'HELP_MORE' to False. EvMORE uses CLIENT_DEFAULT_HEIGHT
HELP_MORE = True
######################################################################
# Guest accounts
@ -602,8 +612,8 @@ IRC_ENABLED = False
# active. OBS: RSS support requires the python-feedparser package to
# be installed (through package manager or from the website
# http://code.google.com/p/feedparser/)
RSS_ENABLED=False
RSS_UPDATE_INTERVAL = 60*10 # 10 minutes
RSS_ENABLED = False
RSS_UPDATE_INTERVAL = 60*10 # 10 minutes
######################################################################
# Django web features
@ -619,7 +629,7 @@ DEBUG = False
TEMPLATE_DEBUG = DEBUG
# Emails are sent to these people if the above DEBUG value is False. If you'd
# rather prefer nobody receives emails, leave this commented out or empty.
ADMINS = () #'Your Name', 'your_email@domain.com'),)
ADMINS = () # 'Your Name', 'your_email@domain.com'),)
# These guys get broken link notifications when SEND_BROKEN_LINK_EMAILS is True.
MANAGERS = ADMINS
# Absolute path to the directory that holds file uploads from web apps.
@ -680,13 +690,13 @@ WEBSITE_TEMPLATE = 'website'
WEBCLIENT_TEMPLATE = 'webclient'
# The default options used by the webclient
WEBCLIENT_OPTIONS = {
"gagprompt": True, # Gags prompt from the output window and keep them
# together with the input bar
"helppopup": True, # Shows help files in a new popup window
"notification_popup": False, # Shows notifications of new messages as
# popup windows
"notification_sound": False # Plays a sound for notifications of new
# messages
"gagprompt": True, # Gags prompt from the output window and keep them
# together with the input bar
"helppopup": True, # Shows help files in a new popup window
"notification_popup": False, # Shows notifications of new messages as
# popup windows
"notification_sound": False # Plays a sound for notifications of new
# messages
}
# We setup the location of the website template as well as the admin site.

View file

@ -85,6 +85,7 @@ class ANSIParser(object):
We also allow to escape colour codes
by prepending with a \ for xterm256,
an extra | for Merc-style codes
"""
# Mapping using {r {n etc
@ -508,6 +509,7 @@ def strip_raw_ansi(string, parser=ANSI_PARSER):
Returns:
string (str): the stripped string.
"""
return parser.strip_raw_codes(string)
@ -524,13 +526,6 @@ def raw(string):
return string.replace('{', '{{').replace('|', '||')
def group(lst, n):
for i in range(0, len(lst), n):
val = lst[i:i+n]
if len(val) == n:
yield tuple(val)
def _spacing_preflight(func):
"""
This wrapper function is used to do some preflight checks on
@ -544,10 +539,10 @@ def _spacing_preflight(func):
raise TypeError("must be char, not %s" % type(fillchar))
if not isinstance(width, int):
raise TypeError("integer argument expected, got %s" % type(width))
difference = width - len(self)
if difference <= 0:
_difference = width - len(self)
if _difference <= 0:
return self
return func(self, width, fillchar, difference)
return func(self, width, fillchar, _difference)
return wrapped
@ -634,20 +629,30 @@ class ANSIMeta(type):
class ANSIString(with_metaclass(ANSIMeta, unicode)):
"""
String-like object that is aware of ANSI codes.
Unicode-like object that is aware of ANSI codes.
This isn't especially efficient, as it doesn't really have an
This class can be used nearly identically to unicode, in that it will
report string length, handle slices, etc, much like a unicode or
string object would. The methods should be used identically as unicode
methods are.
There is at least one exception to this (and there may be more, though
they have not come up yet). When using ''.join() or u''.join() on an
ANSIString, color information will get lost. You must use
ANSIString('').join() to preserve color information.
This implementation isn't perfectly clean, as it doesn't really have an
understanding of what the codes mean in order to eliminate
redundant characters. This could be made as an enhancement to ANSI_PARSER.
redundant characters-- though cleaning up the strings might end up being
inefficient and slow without some C code when dealing with larger values.
Such enhancements could be made as an enhancement to ANSI_PARSER
if needed, however.
If one is going to use ANSIString, one should generally avoid converting
away from it until one is about to send information on the wire. This is
because escape sequences in the string may otherwise already be decoded,
and taken literally the second time around.
Please refer to the Metaclass, ANSIMeta, which is used to apply wrappers
for several of the methods that need not be defined directly here.
"""
def __new__(cls, *args, **kwargs):
@ -895,6 +900,9 @@ class ANSIString(with_metaclass(ANSIMeta, unicode)):
"""
Return a unicode object without the ANSI escapes.
Returns:
clean_string (unicode): A unicode object with no ANSI escapes.
"""
return self._clean_string
@ -902,20 +910,31 @@ class ANSIString(with_metaclass(ANSIMeta, unicode)):
"""
Return a unicode object with the ANSI escapes.
Returns:
raw (unicode): A unicode object with the raw ANSI escape sequences.
"""
return self._raw_string
def partition(self, sep, reverse=False):
"""
Similar to split, but always creates a tuple with three items:
1. The part before the separator
2. The separator itself.
3. The part after.
Splits once into three sections (with the separator being the middle section)
We use the same techniques we used in split() to make sure each are
colored.
Args:
sep (str): The separator to split the string on.
reverse (boolean): Whether to split the string on the last
occurrence of the separator rather than the first.
Returns:
result (tuple):
prefix (ANSIString): The part of the string before the
separator
sep (ANSIString): The separator itself
postfix (ANSIString): The part of the string after the
separator.
"""
if hasattr(sep, '_clean_string'):
sep = sep.clean()
@ -1005,12 +1024,26 @@ class ANSIString(with_metaclass(ANSIMeta, unicode)):
def split(self, by=None, maxsplit=-1):
"""
Splits a string based on a separator.
Stolen from PyPy's pure Python string implementation, tweaked for
ANSIString.
PyPy is distributed under the MIT licence.
http://opensource.org/licenses/MIT
Args:
by (str): A string to search for which will be used to split
the string. For instance, ',' for 'Hello,world' would
result in ['Hello', 'world']
maxsplit (int): The maximum number of times to split the string.
For example, a maxsplit of 2 with a by of ',' on the string
'Hello,world,test,string' would result in
['Hello', 'world', 'test,string']
Returns:
result (list of ANSIStrings): A list of ANSIStrings derived from
this string.
"""
drop_spaces = by is None
if drop_spaces:
@ -1038,12 +1071,27 @@ class ANSIString(with_metaclass(ANSIMeta, unicode)):
def rsplit(self, by=None, maxsplit=-1):
"""
Like split, but starts from the end of the string rather than the
beginning.
Stolen from PyPy's pure Python string implementation, tweaked for
ANSIString.
PyPy is distributed under the MIT licence.
http://opensource.org/licenses/MIT
Args:
by (str): A string to search for which will be used to split
the string. For instance, ',' for 'Hello,world' would
result in ['Hello', 'world']
maxsplit (int): The maximum number of times to split the string.
For example, a maxsplit of 2 with a by of ',' on the string
'Hello,world,test,string' would result in
['Hello,world', 'test', 'string']
Returns:
result (list of ANSIStrings): A list of ANSIStrings derived from
this string.
"""
res = []
end = len(self)
@ -1072,6 +1120,15 @@ class ANSIString(with_metaclass(ANSIMeta, unicode)):
def strip(self, chars=None):
"""
Strip from both ends, taking ANSI markers into account.
Args:
chars (str, optional): A string containing individual characters
to strip off of both ends of the string. By default, any blank
spaces are trimmed.
Returns:
result (ANSIString): A new ANSIString with the ends trimmed of the
relevant characters.
"""
clean = self._clean_string
raw = self._raw_string
@ -1109,6 +1166,15 @@ class ANSIString(with_metaclass(ANSIMeta, unicode)):
def lstrip(self, chars=None):
"""
Strip from the left, taking ANSI markers into account.
Args:
chars (str, optional): A string containing individual characters
to strip off of the left end of the string. By default, any
blank spaces are trimmed.
Returns:
result (ANSIString): A new ANSIString with the left end trimmed of
the relevant characters.
"""
clean = self._clean_string
raw = self._raw_string
@ -1133,6 +1199,15 @@ class ANSIString(with_metaclass(ANSIMeta, unicode)):
def rstrip(self, chars=None):
"""
Strip from the right, taking ANSI markers into account.
Args:
chars (str, optional): A string containing individual characters
to strip off of the right end of the string. By default, any
blank spaces are trimmed.
Returns:
result (ANSIString): A new ANSIString with the right end trimmed of
the relevant characters.
"""
clean = self._clean_string
raw = self._raw_string
@ -1153,7 +1228,22 @@ class ANSIString(with_metaclass(ANSIMeta, unicode)):
def join(self, iterable):
"""
Joins together strings in an iterable.
Joins together strings in an iterable, using this string between each
one.
NOTE: This should always be used for joining strings when ANSIStrings
are involved. Otherwise color information will be discarded by
python, due to details in the C implementation of unicode strings.
Args:
iterable (list of strings): A list of strings to join together
Returns:
result (ANSIString): A single string with all of the iterable's
contents concatenated, with this string between each. For
example:
ANSIString(', ').join(['up', 'right', 'left', 'down'])
...Would return:
ANSIString('up, right, left, down')
"""
result = ANSIString('')
@ -1195,30 +1285,53 @@ class ANSIString(with_metaclass(ANSIMeta, unicode)):
raw_string, clean_string=line, char_indexes=char_indexes,
code_indexes=code_indexes)
# The following methods should not be called with the '_difference' argument explicitly. This is
# data provided by the wrapper _spacing_preflight.
@_spacing_preflight
def center(self, width, fillchar, difference):
def center(self, width, fillchar, _difference):
"""
Center some text with some spaces padding both sides.
Args:
width (int): The target width of the output string.
fillchar (str): A single character string to pad the output string
with.
Returns:
result (ANSIString): A string padded on both ends with fillchar.
"""
remainder = difference % 2
difference /= 2
spacing = self._filler(fillchar, difference)
remainder = _difference % 2
_difference /= 2
spacing = self._filler(fillchar, _difference)
result = spacing + self + spacing + self._filler(fillchar, remainder)
return result
@_spacing_preflight
def ljust(self, width, fillchar, difference):
def ljust(self, width, fillchar, _difference):
"""
Left justify some text.
Args:
width (int): The target width of the output string.
fillchar (str): A single character string to pad the output string
with.
Returns:
result (ANSIString): A string padded on the right with fillchar.
"""
return self + self._filler(fillchar, difference)
return self + self._filler(fillchar, _difference)
@_spacing_preflight
def rjust(self, width, fillchar, difference):
def rjust(self, width, fillchar, _difference):
"""
Right justify some text.
Args:
width (int): The target width of the output string.
fillchar (str): A single character string to pad the output string
with.
Returns:
result (ANSIString): A string padded on the left with fillchar.
"""
return self._filler(fillchar, difference) + self
return self._filler(fillchar, _difference) + self

View file

@ -706,6 +706,8 @@ class EvMenu(object):
self.helptext = _HELP_NO_OPTIONS if self.auto_quit else _HELP_NO_OPTIONS_NO_QUIT
self.display_nodetext()
if not options:
self.close_menu()
def close_menu(self):
"""

View file

@ -44,7 +44,6 @@ _DA = object.__delattr__
_DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
def is_iter(iterable):
"""
Checks if an object behaves iterably.
@ -920,6 +919,8 @@ def uses_database(name="sqlite3"):
return engine == "django.db.backends.%s" % name
_TASK_HANDLER = None
def delay(timedelay, callback, *args, **kwargs):
"""
Delay the return of a value.
@ -930,7 +931,9 @@ def delay(timedelay, callback, *args, **kwargs):
arguments after `timedelay` seconds.
args (any, optional): Will be used as arguments to callback
Kwargs:
any (any): Will be used to call the callback.
persistent (bool, optional): should make the delay persistent
over a reboot or reload
any (any): Will be used to call the callback.
Returns:
deferred (deferred): Will fire fire with callback after
@ -939,8 +942,21 @@ def delay(timedelay, callback, *args, **kwargs):
defined directly in the command body and don't need to be
specified here.
Note:
The task handler (`evennia.scripts.taskhandler.TASK_HANDLEr`) will
be called for persistent or non-persistent tasks.
If persistent is set to True, the callback, its arguments
and other keyword arguments will be saved in the database,
assuming they can be. The callback will be executed even after
a server restart/reload, taking into account the specified delay
(and server down time).
"""
return task.deferLater(reactor, timedelay, callback, *args, **kwargs)
global _TASK_HANDLER
# Do some imports here to avoid circular import and speed things up
if _TASK_HANDLER is None:
from evennia.scripts.taskhandler import TASK_HANDLER as _TASK_HANDLER
return _TASK_HANDLER.add(timedelay, callback, *args, **kwargs)
_TYPECLASSMODELS = None