Trunk: Merged the Devel-branch (branches/griatch) into /trunk. This constitutes a major refactoring of Evennia. Development will now continue in trunk. See the wiki and the past posts to the mailing list for info. /Griatch

This commit is contained in:
Griatch 2010-08-29 18:46:58 +00:00
parent df29defbcd
commit f83c2bddf8
222 changed files with 22304 additions and 14371 deletions

0
src/utils/__init__.py Normal file
View file

155
src/utils/ansi.py Normal file
View file

@ -0,0 +1,155 @@
"""
ANSI - gives colour to text.
"""
import re
class ANSITable(object):
"""
A table of ANSI characters to use when replacing things.
"""
ansi = {}
ansi["beep"] = "\07"
ansi["escape"] = "\033"
ansi["normal"] = "\033[0m"
ansi["underline"] = "\033[4m"
ansi["hilite"] = "\033[1m"
ansi["blink"] = "\033[5m"
ansi["inverse"] = "\033[7m"
ansi["inv_hilite"] = "\033[1;7m"
ansi["inv_blink"] = "\033[7;5m"
ansi["blink_hilite"] = "\033[1;5m"
ansi["inv_blink_hilite"] = "\033[1;5;7m"
# Foreground colors
ansi["black"] = "\033[30m"
ansi["red"] = "\033[31m"
ansi["green"] = "\033[32m"
ansi["yellow"] = "\033[33m"
ansi["blue"] = "\033[34m"
ansi["magenta"] = "\033[35m"
ansi["cyan"] = "\033[36m"
ansi["white"] = "\033[37m"
# Background colors
ansi["back_black"] = "\033[40m"
ansi["back_red"] = "\033[41m"
ansi["back_green"] = "\033[42m"
ansi["back_yellow"] = "\033[43m"
ansi["back_blue"] = "\033[44m"
ansi["back_magenta"] = "\033[45m"
ansi["back_cyan"] = "\033[46m"
ansi["back_white"] = "\033[47m"
# Formatting Characters
ansi["return"] = "\r\n"
ansi["tab"] = "\t"
ansi["space"] = " "
class BaseParser(object):
def parse_ansi(self, string, strip_ansi=False, strip_formatting=False):
"""
Parses a string, subbing color codes as needed.
"""
if string == None or string == '':
return ''
# Convert to string to prevent problems with lists, ints, and other types.
string = str(string)
# if strip_formatting:
# char_return = ""
# char_tab = ""
# char_space = ""
# else:
# char_return = ANSITable.ansi["return"]
# char_tab = ANSITable.ansi["tab"]
# char_space = ANSITable.ansi["space"]
for sub in self.ansi_subs:
p = re.compile(sub[0], re.DOTALL)
if strip_ansi:
string = p.sub("", string)
else:
string = p.sub(sub[1], string)
if strip_ansi:
return '%s' % (string)
else:
return '%s%s' % (string, ANSITable.ansi["normal"])
class MuxANSIParser(BaseParser):
def __init__(self):
self.ansi_subs = [
(r'%r', ANSITable.ansi["return"]),
(r'%t', ANSITable.ansi["tab"]),
(r'%b', ANSITable.ansi["space"]),
(r'%cf', ANSITable.ansi["blink"]),
(r'%ci', ANSITable.ansi["inverse"]),
(r'%ch', ANSITable.ansi["hilite"]),
(r'%cn', ANSITable.ansi["normal"]),
(r'%cx', ANSITable.ansi["black"]),
(r'%cX', ANSITable.ansi["back_black"]),
(r'%cr', ANSITable.ansi["red"]),
(r'%cR', ANSITable.ansi["back_red"]),
(r'%cg', ANSITable.ansi["green"]),
(r'%cG', ANSITable.ansi["back_green"]),
(r'%cy', ANSITable.ansi["yellow"]),
(r'%cY', ANSITable.ansi["back_yellow"]),
(r'%cb', ANSITable.ansi["blue"]),
(r'%cB', ANSITable.ansi["back_blue"]),
(r'%cm', ANSITable.ansi["magenta"]),
(r'%cM', ANSITable.ansi["back_magenta"]),
(r'%cc', ANSITable.ansi["cyan"]),
(r'%cC', ANSITable.ansi["back_cyan"]),
(r'%cw', ANSITable.ansi["white"]),
(r'%cW', ANSITable.ansi["back_white"]),
]
class ExtendedANSIParser(MuxANSIParser):
"""
Extends the standard mux colour commands with {-style commands
(shortcuts for writing light/dark text without background)
"""
def __init__(self):
super(ExtendedANSIParser, self).__init__()
hilite = ANSITable.ansi['hilite']
normal = ANSITable.ansi['normal']
self.ansi_subs.extend( [
(r'{r', hilite + ANSITable.ansi['red']),
(r'{R', normal + ANSITable.ansi['red']),
(r'{g', hilite + ANSITable.ansi['green']),
(r'{G', normal + ANSITable.ansi['green']),
(r'{y', hilite + ANSITable.ansi['yellow']),
(r'{Y', normal + ANSITable.ansi['yellow']),
(r'{b', hilite + ANSITable.ansi['blue']),
(r'{B', normal + ANSITable.ansi['blue']),
(r'{m', hilite + ANSITable.ansi['magenta']),
(r'{M', normal + ANSITable.ansi['magenta']),
(r'{c', hilite + ANSITable.ansi['cyan']),
(r'{C', normal + ANSITable.ansi['cyan']),
(r'{w', hilite + ANSITable.ansi['white']), #white
(r'{W', normal + ANSITable.ansi['white']), #light grey
(r'{x', hilite + ANSITable.ansi['black']), #dark grey
(r'{X', normal + ANSITable.ansi['black']), #pure black
(r'{n', normal) #reset
] )
#ANSI_PARSER = MuxANSIParser()
ANSI_PARSER = ExtendedANSIParser()
def parse_ansi(string, strip_ansi=False, strip_formatting=False, parser=ANSI_PARSER):
"""
Parses a string, subbing color codes as needed.
"""
return parser.parse_ansi(string, strip_ansi=strip_ansi,
strip_formatting=strip_formatting)
def clean_ansi(string):
"""
Cleans all ansi symbols from a string
"""
# convert all to their ansi counterpart
string = parse_ansi(string)
# next, strip it all away
regex = re.compile("\033\[[0-9;]+m")
return regex.sub("", string) #replace all matches with empty strings

View file

@ -0,0 +1,399 @@
"""
This file contains the core methods for the Batch-command- and
Batch-code-processors respectively. In short, these are two different
ways to build a game world using a normal text-editor without having
to do so 'on the fly' in-game. They also serve as an automatic backup
so you can quickly recreate a world also after a server reset. The
functions in this module is meant to form the backbone of a system
called and accessed through game commands.
The Batch-command processor is the simplest. It simply runs a list of
in-game commands in sequence by reading them from a text file. The
advantage of this is that the builder only need to remember the normal
in-game commands. They are also executing with full permission checks
etc, making it relatively safe for builders to use. The drawback is
that in-game there is really a builder-character walking around
building things, and it can be important to create rooms and objects
in the right order, so the character can move between them. Also
objects that affects players (such as mobs, dark rooms etc) will
affect the building character too, requiring extra care to turn
off/on.
The Batch-code processor is a more advanced system that accepts full
Python code, executing in chunks. The advantage of this is much more
power; practically anything imaginable can be coded and handled using
the batch-code processor. There is no in-game character that moves and
that can be affected by what is being built - the database is
populated on the fly. The drawback is safety and entry threshold - the
code is executed as would any server code, without mud-specific
permission checks and you have full access to modifying objects
etc. You also need to know Python and Evennia's API. Hence it's
recommended that the batch-code processor is limited only to
superusers or highly trusted staff.
Batch-command processor file syntax
The batch-command processor accepts 'batchcommand files' e.g 'batch.ev',
containing a sequence of valid evennia commands in a simple
format. The engine runs each command in sequence, as if they had been
run at the game prompt.
This way entire game worlds can be created and planned offline; it is
especially useful in order to create long room descriptions where a
real offline text editor is often much better than any online text
editor or prompt.
Example of batch.ev file:
----------------------------
# batch file
# all lines starting with # are comments; they also indicate
# that a command definition is over.
@create box
# this comment ends the @create command.
@set box/desc = A large box.
Inside are some scattered piles of clothing.
It seems the bottom of the box is a bit loose.
# Again, this comment indicates the @set command is over. Note how
# the description could be freely added. Excess whitespace on a line
# is ignored. An empty line in the command definition is parsed as a \n
# (so two empty lines becomes a new paragraph).
@teleport #221
# (Assuming #221 is a warehouse or something.)
# (remember, this comment ends the @teleport command! Don'f forget it)
@drop box
# Done, the box is in the warehouse! (this last comment is not necessary to
# close the @drop command since it's the end of the file)
-------------------------
An example batch file is game/gamesrc/commands/examples/batch_example.ev.
Batch-code processor file syntax
The Batch-code processor accepts full python modules (e.g. "batch.py") that
looks identical to normal Python files with a few exceptions that allows them
to the executed in blocks. This way of working assures a sequential execution
of the file and allows for features like stepping from block to block
(without executing those coming before), as well as automatic deletion
of created objects etc. You can however also run a batch-code python file
directly using Python (and can also be de).
Code blocks are separated by python comments starting with special code words.
#HEADER - this denotes commands global to the entire file, such as
import statements and global variables. They will
automatically be made available for each block. Observe
that changes to these variables made in one block is not
preserved between blocks!)
#CODE [objname, objname, ...] - This designates a code block that will be executed like a
stand-alone piece of code together with any #HEADER
defined. <objname>s mark the (variable-)names of objects created in the code,
and which may be auto-deleted by the processor if desired (such as when
debugging the script). E.g., if the code contains the command
myobj = create.create_object(...), you could put 'myobj' in the #CODE header
regardless of what the created object is actually called in-game.
The following variables are automatically made available for the script:
caller - the object executing the script
Example batch.py file
-----------------------------------
#HEADER
import traceback
from django.config import settings
from src.utils import create
from game.gamesrc.typeclasses import basetypes
GOLD = 10
#CODE obj, obj2
obj = create.create_object(basetypes.Object)
obj2 = create.create_object(basetypes.Object)
obj.location = caller.location
obj.db.gold = GOLD
caller.msg("The object was created!")
#CODE
script = create.create_script()
"""
import re
from django.conf import settings
from src.utils import logger
from src.utils import utils
#from src.commands.cmdset import CmdSet
#from src.scripts.scripts import Script
from game import settings as settings_module
from django.core.management import setup_environ
from traceback import format_exc
# colours
WHITE = r"%cn%ch%cw"
RED = r"%cn%ch%cr"
GREEN = r"%cn%ci%cg"
YELLOW = r"%cn%ch%cy"
NORM = r"%cn"
#------------------------------------------------------------
#
# Batch-command processor
#
#------------------------------------------------------------
def read_batchcommand_file(pythonpath):
"""
This reads the contents of a batch-command file.
Filename is considered to be the name of the batch file
relative the directory specified in settings.py
"""
if pythonpath and not (pythonpath.startswith('src.') or
pythonpath.startswith('game.')):
pythonpath = "%s.%s" % (settings.BASE_BATCHPROCESS_PATH,
pythonpath)
abspath = utils.pypath_to_realpath(pythonpath, 'ev')
try:
fobj = open(abspath)
except IOError:
logger.log_errmsg("Could not open path '%s'." % pythonpath)
return None
lines = fobj.readlines()
fobj.close()
return lines
def parse_batchcommand_file(pythonpath):
"""
This parses the lines of a batchfile according to the following
rules:
1) # at the beginning of a line marks the end of the command before it.
It is also a comment and any number of # can exist on subsequent
lines (but not inside comments).
2) Commands are placed alone at the beginning of a line and their
arguments are considered to be everything following (on any
number of lines) until the next comment line beginning with #.
3) Newlines are ignored in command definitions
4) A completely empty line in a command line definition is condered
a newline (so two empty lines is a paragraph).
5) Excess spaces and indents inside arguments are stripped.
"""
#helper function
def identify_line(line):
"""
Identifies the line type (comment, commanddef or empty)
"""
try:
if line.strip()[0] == '#':
return "comment"
else:
return "commanddef"
except IndexError:
return "empty"
#read the indata, if possible.
lines = read_batchcommand_file(pythonpath)
if not lines:
return None
commands = []
curr_cmd = ""
#purge all superfluous whitespace and newlines from lines
reg1 = re.compile(r"\s+")
lines = [reg1.sub(" ", l) for l in lines]
#parse all command definitions into a list.
for line in lines:
typ = identify_line(line)
if typ == "commanddef":
curr_cmd += line
elif typ == "empty" and curr_cmd:
curr_cmd += "\r\n"
else: #comment
if curr_cmd:
commands.append(curr_cmd.strip())
curr_cmd = ""
if curr_cmd:
commands.append(curr_cmd.strip())
#second round to clean up now merged line edges etc.
reg2 = re.compile(r"[ \t\f\v]+")
commands = [reg2.sub(" ", c) for c in commands]
#remove eventual newline at the end of commands
commands = [c.strip('\r\n') for c in commands]
return commands
#------------------------------------------------------------
#
# Batch-code processor
#
#------------------------------------------------------------
def read_batchcode_file(pythonpath):
"""
This reads the contents of batchfile.
Filename is considered to be the name of the batch file
relative the directory specified in settings.py
"""
if pythonpath and not (pythonpath.startswith('src.') or
pythonpath.startswith('game.')):
pythonpath = "%s.%s" % (settings.BASE_BATCHPROCESS_PATH,
pythonpath)
abspath = utils.pypath_to_realpath(pythonpath)
try:
fobj = open(abspath)
except IOError:
logger.log_errmsg("Could not open path '%s'." % pythonpath)
return None
lines = fobj.readlines()
fobj.close()
return lines
def parse_batchcode_file(pythonpath):
"""
This parses the lines of a batchfile according to the following
rules:
1) Lines starting with #HEADER starts a header block (ends other blocks)
2) Lines starting with #CODE begins a code block (ends other blocks)
3) All lines outside blocks are stripped.
4) All excess whitespace beginning/ending a block is stripped.
"""
# helper function
def parse_line(line):
"""
Identifies the line type: block command, comment, empty or normal code.
"""
line = line.strip()
if line.startswith("#HEADER"):
return "header", ""
elif line.startswith("#CODE"):
# parse code command
line = line.lstrip("#CODE").strip()
objs = []
if line:
objs = [obj.strip() for obj in line.split(',')]
return "code", objs
elif line.startswith("#"):
return "comment", ""
else:
#normal line - return it with a line break.
return None, "\n%s" % line
# read indata
lines = read_batchcode_file(pythonpath)
if not lines:
return None
# parse file into blocks
header = ""
codes = []
in_header = False
in_code = False
for line in lines:
# parse line
mode, line = parse_line(line)
# try:
# print "::", in_header, in_code, mode, line.strip()
# except:
# print "::", in_header, in_code, mode, line
if mode == 'comment':
continue
elif mode == 'header':
in_header = True
in_code = False
elif mode == 'code':
in_header = False
in_code = True
# the line is a list of object variable names
# (or an empty list) at this point.
codedict = {'objs':line,
'code':""}
codes.append(codedict)
else:
# another type of line (empty or code)
if in_header:
header += line
elif in_code:
codes[-1]['code'] += line
else:
# not in a block (e.g. first in file). Ignore.
continue
# last, we merge the headers with all codes.
for codedict in codes:
codedict["firstline"] = codedict["code"].strip()[:min(35, len(codedict['code'].strip())-1)]
codedict["code"] = "%s\n%s" % (header, codedict["code"])
return codes
def batch_code_exec(codedict, extra_environ=None, debug=False):
"""
Execute a single code block, including imports and appending global vars
extra_environ - dict with environment variables
"""
# define the execution environment
environ = "setup_environ(settings_module)"
environdict = {"setup_environ":setup_environ,
"settings_module":settings_module}
if extra_environ:
for key, value in extra_environ.items():
environdict[key] = value
# merge all into one block
code = "%s\n%s" % (environ, codedict['code'])
if debug:
# try to delete marked objects
for obj in codedict['objs']:
code += "\ntry: %s.delete()\nexcept: pass" % obj
# execute the block
try:
exec(code, environdict)
except Exception:
errlist = format_exc().split('\n')
if len(errlist) > 4:
errlist = errlist[4:]
err = "\n".join("<<< %s" % line for line in errlist if line)
if debug:
# try to delete objects again.
try:
for obj in codedict['objs']:
eval("%s.delete()" % obj, environdict)
except Exception:
pass
return err
return None

443
src/utils/create.py Normal file
View file

@ -0,0 +1,443 @@
"""
This module gathers all the essential database-creation
methods for the game engine's various object types.
Only objects created 'stand-alone' are in here,
e.g. object Attributes are always created directly through their
respective objects.
The respective object managers hold more methods for
manipulating and searching objects already existing in
the database.
Models covered:
Objects
Scripts
Help
PermissionGroup
Message
Channel
Players
"""
from django.conf import settings
from django.contrib.auth.models import User
from django.db import IntegrityError
from src.players.models import PlayerDB
from src.help.models import HelpEntry
from src.comms.models import Msg, Channel
from src.comms import channelhandler
from src.comms.managers import to_object
from src.permissions.permissions import set_perm
from src.permissions.models import PermissionGroup
from src.utils import logger
from src.utils.utils import is_iter
#
# Game Object creation
#
def create_object(typeclass, key=None, location=None,
home=None, player=None, permissions=None, aliases=None):
"""
Create a new in-game object. Any game object is a combination
of a database object that stores data persistently to
the database, and a typeclass, which on-the-fly 'decorates'
the database object into whataver different type of object
it is supposed to be in the game.
See src.objects.managers for methods to manipulate existing objects
in the database. src.objects.objects holds the base typeclasses
and src.objects.models hold the database model.
"""
# deferred import to avoid loops
from src.objects.objects import Object
from src.objects.models import ObjectDB
#print "in create_object", typeclass
if isinstance(typeclass, ObjectDB):
# this is already an object instance!
new_db_object = typeclass
typeclass = new_db_object.typeclass
elif isinstance(typeclass, Object):
# this is already an object typeclass!
new_db_object = typeclass.dbobj
typeclass = typeclass.__class__
else:
# create database object
new_db_object = ObjectDB()
#new_db_object = ObjectDB()
if not callable(typeclass):
# this means typeclass might be a path. If not,
# the type mechanism will automatically assign
# the BASE_OBJECT_TYPE from settings.
if typeclass:
typeclass = str(typeclass)
new_db_object.typeclass_path = typeclass
new_db_object.save()
# this will either load the typeclass or the default one
typeclass = new_db_object.typeclass
new_db_object.save()
# the name/key is often set later in the typeclass. This
# is set here as a failsafe.
if key:
new_db_object.name = key
else:
dbref = new_db_object.id
if typeclass and hasattr(typeclass, '__name__'):
new_db_object.name = "%s%i" % (typeclass.__name__, dbref)
else:
new_db_object.name = "#%i" % dbref
# initialize an object of this typeclass.
new_object = typeclass(new_db_object)
# from now on we can use the typeclass object
# as if it was the database object.
if player:
# link a player and the object together
new_object.player = player
player.obj = new_object
if permissions:
set_perm(new_object, permissions)
if aliases:
if not is_iter(aliases):
aliases = [aliases]
new_object.aliases = ",".join([alias.strip() for alias in aliases])
# call the hook method. This is where all at_creation
# customization happens as the typeclass stores custom
# things on its database object.
new_object.at_object_creation()
# perform a move_to in order to display eventual messages.
if home:
new_object.home = home
if location:
new_object.move_to(location, quiet=True)
else:
# rooms would have location=None.
new_object.location = None
new_object.save()
return new_object
#
# Script creation
#
def create_script(typeclass, key=None, obj=None, autostart=True):
"""
Create a new script. All scripts are a combination
of a database object that communicates with the
database, and an typeclass that 'decorates' the
database object into being different types of scripts.
It's behaviour is similar to the game objects except
scripts has a time component and are more limited in
scope.
Argument 'typeclass' can be either an actual
typeclass object or a python path to such an object.
Only set key here if you want a unique name for this
particular script (set it in config to give
same key to all scripts of the same type). Set obj
to tie this script to a particular object.
See src.scripts.manager for methods to manipulate existing
scripts in the database.
"""
# deferred import to avoid loops.
from src.scripts.scripts import Script
#print "in create_script", typeclass
from src.scripts.models import ScriptDB
if isinstance(typeclass, ScriptDB):
#print "this is already a script instance!", typeclass, typeclass.__class__
new_db_object = typeclass
typeclass = new_db_object.typeclass
elif isinstance(typeclass, Script):
#print "this is already an object typeclass!"
new_db_object = typeclass.dbobj
typeclass = typeclass.__class__
else:
# create a new instance.
new_db_object = ScriptDB()
#new_db_object = ScriptDB()
if not callable(typeclass):
# try to load this in case it's a path
if typeclass:
typeclass = str(typeclass)
new_db_object.typeclass_path = typeclass
new_db_object.save()
# this will load either the typeclass or the default one
typeclass = new_db_object.typeclass
new_db_object.save()
# the typeclass is initialized
new_script = typeclass(new_db_object)
# store variables on the typeclass (which means
# it's actually transparently stored on the db object)
if key:
new_db_object.name = key
else:
if typeclass and hasattr(typeclass, '__name__'):
new_db_object.name = "%s" % typeclass.__name__
else:
new_db_object.name = "#%i" % new_db_object.id
if obj:
try:
new_script.obj = obj
except ValueError:
new_script.obj = obj.dbobj
# call the hook method. This is where all at_creation
# customization happens as the typeclass stores custom
# things on its database object.
new_script.at_script_creation()
new_script.save()
# a new created script should always be started, so
# we do this now.
if autostart:
new_script.start()
return new_script
#
# Help entry creation
#
def create_help_entry(key, entrytext, category="General", permissions=None):
"""
Create a static help entry in the help database. Note that Command
help entries are dynamic and directly taken from the __doc__ entries
of the command. The database-stored help entries are intended for more
general help on the game, more extensive info, in-game setting information
and so on.
"""
try:
new_help = HelpEntry()
new_help.key = key
new_help.entrytext = entrytext
new_help.help_category = category
if permissions:
set_perm(new_help, permissions)
new_help.save()
return new_help
except IntegrityError:
string = "Could not add help entry: key '%s' already exists." % key
logger.log_errmsg(string)
return None
except Exception:
logger.log_trace()
return None
#
# Permission groups
#
def create_permission_group(group_name, desc=None, group_perms=None,
permissions=None):
"""
Adds a new group
group_name - case sensitive, unique key for group.
desc - description of permission group
group_perms - the permissions stored in this group - can be
a list of permission strings, a single permission
or a comma-separated string of permissions.
permissions - can be a list of permission strings, a single
permission or a comma-separated string of permissions.
OBS-these are the group's OWN permissions, for editing
the group etc - NOT the permissions stored in it!
"""
new_group = PermissionGroup.objects.filter(db_key__exact=group_name)
if new_group:
new_group = new_group[0]
else:
new_group = PermissionGroup()
new_group.key = group_name
if desc:
new_group.desc = desc
if group_perms:
if is_iter(group_perms):
group_perms = ",".join([str(perm) for perm in group_perms])
new_group.group_permissions = group_perms
if permissions:
set_perm(new_group, permissions)
new_group.save()
return new_group
#
# Comm system methods
#
def create_message(senderobj, message, channels=None,
receivers=None, permissions=None):
"""
Create a new communication message. Msgs are used for all
player-to-player communication, both between individual players
and over channels.
senderobj - the player sending the message. This must be the actual object.
message - text with the message. Eventual headers, titles etc
should all be included in this text string. Formatting
will be retained.
channels - a channel or a list of channels to send to. The channels
may be actual channel objects or their unique key strings.
receivers - a player to send to, or a list of them. May be Player objects
or playernames.
permissions - permission string, or a list of permission strings.
The Comm system is created very open-ended, so it's fully possible
to let a message both go to several channels and to several receivers
at the same time, it's up to the command definitions to limit this as
desired.
Since messages are often directly created by the user, this method (and all
comm methods) raise CommErrors with different message strings to make it
easier for the Command definition to give proper feedback to the user.
"""
def to_player(obj):
"Make sure the object is a player object"
if hasattr(obj, 'user'):
return obj
elif hasattr(obj, 'player'):
return obj.player
else:
return None
if not message:
# we don't allow empty messages.
return
new_message = Msg()
new_message.sender = to_player(senderobj)
new_message.message = message
new_message.save()
if channels:
if not is_iter(channels):
channels = [channels]
new_message.channels = [channel for channel in
[to_object(channel, objtype='channel')
for channel in channels] if channel]
if receivers:
#print "Found receiver:", receivers
if not is_iter(receivers):
receivers = [receivers]
#print "to_player: %s" % to_player(receivers[0])
new_message.receivers = [to_player(receiver) for receiver in
[to_object(receiver) for receiver in receivers]
if receiver]
if permissions:
set_perm(new_message, permissions)
new_message.save()
return new_message
def create_channel(key, aliases=None, description=None,
permissions=None, keep_log=True):
"""
Create A communication Channel. A Channel serves as a central
hub for distributing Msgs to groups of people without
specifying the receivers explicitly. Instead players may
'connect' to the channel and follow the flow of messages. By
default the channel allows access to all old messages, but
this can be turned off with the keep_log switch.
key - this must be unique.
aliases - list of alternative (likely shorter) keynames.
listen/send/admin permissions are strings if permissions separated
by commas.
"""
try:
new_channel = Channel()
new_channel.key = key
if aliases:
if not is_iter(aliases):
aliases = [aliases]
new_channel.aliases = ",".join([str(alias) for alias in aliases])
new_channel.description = description
new_channel.keep_log = keep_log
except IntegrityError:
string = "Could not add channel: key '%s' already exists." % key
logger.log_errmsg(string)
return None
if permissions:
set_perm(new_channel, permissions)
new_channel.save()
channelhandler.CHANNELHANDLER.add_channel(new_channel)
return new_channel
#
# Player creation methods
#
def create_player(name, email, password,
permissions=None,
create_character=True,
location=None, typeclass=None, home=None,
is_superuser=False, user=None):
"""
This creates a new player, handling the creation of the User
object and its associated Player object. If create_character is
True, a game player object with the same name as the User/Player will
also be created. Returns the new game character, or the Player obj if no
character is created. For more info about the typeclass argument,
see create_objects() above.
Note: if user is supplied, it will NOT be modified (args name, email,
passw and is_superuser will be ignored). Change those properties
explicitly instead.
If no permissions are given (None), the default permission group
as defined in settings.PERMISSION_PLAYER_DEFAULT will be
assigned. If permissions are given, no automatic assignment will
occur.
Concerning is_superuser:
A superuser should have access to everything
in the game and on the server/web interface. The very first user
created in the database is always a superuser (that's using
django's own creation, not this one).
Usually only the server admin should need to be superuser, all
other access levels can be handled with more fine-grained
permissions or groups.
Since superuser overrules all permissions, we don't
set any here.
"""
# The system should already have checked so the name/email
# isn't already registered, and that the password is ok before
# getting here.
if user:
new_user = user
else:
if is_superuser:
new_user = User.objects.create_superuser(name, email, password)
else:
new_user = User.objects.create_user(name, email, password)
# create the associated Player for this User, and tie them together
new_player = PlayerDB(user=new_user)
new_player.save()
# assign mud permissions
if not permissions:
permissions = settings.PERMISSION_PLAYER_DEFAULT
set_perm(new_player, permissions)
# create *in-game* 'player' object
if create_character:
if not typeclass:
typeclass = settings.BASE_CHARACTER_TYPECLASS
# creating the object automatically links the player
# and object together by player.obj <-> obj.player
new_character = create_object(typeclass, name,
location, home,
player=new_player)
set_perm(new_character, permissions)
return new_character
return new_player

253
src/utils/debug.py Normal file
View file

@ -0,0 +1,253 @@
"""
Debug mechanisms for easier developing advanced game objects
The functions in this module are intended to stress-test various
aspects of an in-game entity, notably objects and scripts, during
development. This allows to run several automated tests on the
entity without having to do the testing by creating/deleting etc
in-game.
The default Evennia accesses the methods of this module through
a special state and cmdset, using the @debug command.
"""
from traceback import format_exc
from src.utils import create
def trace():
"Format the traceback."
errlist = format_exc().split('\n')
if len(errlist) > 4:
errlist = errlist[4:]
ret = "\n" + "\n".join("<<< {r%s{n" % line for line in errlist if line)
return ret
#
# Testing scripts
#
def debug_script(script_path, obj=None, auto_delete=True):
"""
This function takes a script database object (ScriptDB) tests
all its hooks for syntax errors. Note that no run-time errors
will be caught, only weird python syntax.
script_path - the full path to the script typeclass module and class.
"""
try:
string = "Test-creating a new script of this type ... "
scriptobj = create.create_script(script_path, autostart=False)
scriptobj.obj = obj
scriptobj.save()
string += "{gOk{n."
except Exception:
string += trace()
try: scriptobj.delete()
except: pass
return string
string += "\nRunning syntax check ..."
try:
string += "\nTesting syntax of at_script_creation(self) ... "
ret = scriptobj.at_script_creation()
string += "{gOk{n."
except Exception:
string += trace()
try:
string += "\nTesting syntax of is_valid(self) ... "
ret = scriptobj.is_valid()
string += "{gOk{n."
except Exception:
string += trace()
try:
string += "\nTesting syntax of at_start(self) ... "
ret = scriptobj.at_start()
string += "{gOk{n."
except Exception:
string += trace()
try:
string += "\nTesting syntax of at_repeat(self) ... "
ret = scriptobj.at_repeat()
string += "{gOk{n."
except Exception:
string += trace()
try:
string += "\nTesting syntax of at_stop(self) ... "
ret = scriptobj.at_script_creation()
string += "{gOk{n."
except Exception:
string += trace()
if auto_delete:
try:
scriptobj.delete()
except:
string += trace()
return string
#
# Testing objects
#
def debug_object(obj_path, caller):
"""
Auto-test an object's hooks and methods.
"""
try:
string = "\n Test-creating a new object of path {w%s{n ... " % obj_path
obj = create.create_object(obj_path)
obj.location = caller.location
string += "{gOk{n."
except Exception:
string += trace()
try: obj.delete()
except: pass
return string
string += "\nRunning syntax checks ..."
try:
string += "\nCalling at_first_login(self) ... "
ret = obj.at_first_login()
string += "{gOk{n."
except Exception:
string += trace()
try:
string += "\nCalling at_pre_login(self) ... "
ret = obj.at_pre_login()
string += "{gOk{n."
except Exception:
string += trace()
try:
string += "\nCalling at_post_login(self) ... "
ret = obj.at_post_login()
string += "{gOk{n."
except Exception:
string += trace()
try:
string += "\nCalling at_disconnect(self) ... "
ret = obj.at_disconnect()
string += "{gOk{n."
except Exception:
string += trace()
try:
string += "\nCalling at_before_move(self, dest) ... "
ret = obj.at_before_move(caller.location)
string += "{gOk{n: returns %s" % ret
except Exception:
string += trace()
try:
string += "\nCalling announce_move_from(self, dest) ... "
ret = obj.announce_move_from(caller.location)
string += "{gOk{n"
except Exception:
string += trace()
try:
string += "\nCalling announce_move_to(self, source_loc) ... "
ret = obj.announce_move_from(caller.location)
string += "{gOk{n."
except Exception:
string += trace()
try:
string += "\nCalling at_after_move(self, source_loc) ... "
ret = obj.at_after_move(caller.location)
string += "{gOk{n."
except Exception:
string += trace()
try:
string += "\nCalling at_object_receive(self, caller, source_loc) ... "
ret = obj.at_object_receive(caller, caller.location)
string += "{gOk{n."
except Exception:
string += trace()
try:
string += "\nCalling return_appearance(self, caller) ... "
ret = obj.return_appearance(caller)
string += "{gOk{n."
except Exception:
string += trace()
try:
string += "\nCalling at_msg_receive(self, msg, from_obj) ... "
ret = obj.at_msg_receive("test_message_receive", caller)
string += "{gOk{n."
except Exception:
string += trace()
try:
string += "\nCalling at_msg_send(self, msg, to_obj) ... "
ret = obj.at_msg_send("test_message_send", caller)
string += "{gOk{n."
except Exception:
string += trace()
try:
string += "\nCalling at_desc(self, looker) ... "
ret = obj.at_desc(caller)
string += "{gOk{n."
except Exception:
string += trace()
try:
string += "\nCalling at_object_delete(self) ... "
ret = obj.at_object_delete()
string += "{gOk{n."
except Exception:
string += trace()
try:
string += "\nCalling at_get(self, getter) ... "
ret = obj.at_get(caller)
string += "{gOk{n."
except Exception:
string += trace()
try:
string += "\nCalling at_drop(self, dropper) ... "
ret = obj.at_drop(caller)
string += "{gOk{n."
except Exception:
string += trace()
try:
string += "\nCalling at_say(self, speaker, message) ... "
ret = obj.at_say(caller, "test_message_say")
string += "{gOk{n."
except Exception:
string += trace()
try:
obj.delete()
except:
string += trace()
return string
def debug_object_scripts(obj_path, caller):
"""
Create an object and test all its associated scripts
independently.
"""
try:
string = "\n Testing scripts on {w%s{n ... " % obj_path
obj = create.create_object(obj_path)
obj.location = caller.location
string += "{gOk{n."
except Exception:
string += trace()
try: obj.delete()
except: pass
return string
scripts = obj.scripts.all()
if scripts:
string += "\n Running tests on %i object scripts ... " % (len(scripts))
for script in scripts:
string += "\n {wTesting %s{n ..." % script.key
path = script.typeclass_path
string += debug_script(path, obj=obj)
#string += debug_run_script(path, obj=obj)
else:
string += "\n No scripts defined on object."
try:
obj.delete()
except:
string += trace()
return string

208
src/utils/gametime.py Normal file
View file

@ -0,0 +1,208 @@
"""
The gametime module handles the global passage of time in the mud.
It also supplies some useful methods to convert between
in-mud time and real-world time as well allows to get the
total runtime of the server and the current uptime.
"""
from django.conf import settings
from src.scripts.scripts import Script
from src.scripts.models import ScriptDB
from src.utils.create import create_script
from src.utils import logger
# name of script that keeps track of the time
GAME_TIME_SCRIPT = "sys_game_time"
# Speed-up factor of the in-game time compared
# to real time.
TIMEFACTOR = settings.TIME_FACTOR
# Common real-life time measures, in seconds.
# You should not change these.
REAL_TICK = max(1.0, settings.TIME_TICK) #Smallest time unit (min 1s)
REAL_MIN = 60.0 # seconds per minute in real world
# Game-time units, in real-life seconds. These are supplied as
# a convenient measure for determining the current in-game time,
# e.g. when defining events. The words month, week and year can
# of course mean whatever units of time are used in the game.
TICK = REAL_TICK / TIMEFACTOR
MIN = REAL_MIN / TIMEFACTOR
HOUR = MIN * settings.TIME_MIN_PER_HOUR
DAY = HOUR * settings.TIME_HOUR_PER_DAY
WEEK = DAY * settings.TIME_DAY_PER_WEEK
MONTH = WEEK * settings.TIME_WEEK_PER_MONTH
YEAR = MONTH * settings.TIME_MONTH_PER_YEAR
class GameTime(Script):
"""
This sets up an script that keeps track of the
in-game time and some other time units.
"""
def at_script_creation(self):
"""
Setup the script
"""
self.key = "sys_game_time"
self.desc = "Keeps track of the game time"
self.interval = REAL_MIN # update every minute
self.persistent = True
self.attr("game_time", 0.0) #IC time
self.attr("run_time", 0.0) #OOC time
self.attr("up_time", 0.0) #OOC time
def at_repeat(self):
"""
Called every minute to update the timers.
"""
# We store values as floats to avoid drift over time
game_time = float(self.attr("game_time"))
run_time = float(self.attr("run_time"))
up_time = float(self.attr("up_time"))
self.attr("game_time", game_time + MIN)
self.attr("run_time", run_time + REAL_MIN)
self.attr("up_time", up_time + REAL_MIN)
def at_start(self):
"""
This is called once every server restart.
We reset the up time.
"""
self.attr("up_time", 0.0)
# Access routines
def gametime_format(seconds):
"""
Converts the count in seconds into an integer tuple of the form
(years, months, weeks, days, hours, minutes, seconds) where
several of the entries may be 0.
We want to keep a separate version of this (rather than just
rescale the real time once and use the normal realtime_format
below) since the admin might for example decide to change how many
hours a 'day' is in their game etc.
"""
# have to re-multiply in the TIMEFACTOR
# do this or we cancel the already counted
# timefactor in the timer script...
sec = int(seconds * TIMEFACTOR)
years, sec = sec/YEAR, sec % YEAR
months, sec = sec/MONTH, sec % MONTH
weeks, sec = sec/WEEK, sec % WEEK
days, sec = sec/DAY, sec % DAY
hours, sec = sec/HOUR, sec % HOUR
minutes, sec = sec/MIN, sec % MIN
return (years, months, weeks, days, hours, minutes, sec)
def realtime_format(seconds):
"""
As gametime format, but with real time units
"""
sec = int(seconds)
years, sec = sec/29030400, sec % 29030400
months, sec = sec/2419200, sec % 2419200
weeks, sec = sec/604800, sec % 604800
days, sec = sec/86400, sec % 86400
hours, sec = sec/3600, sec % 3600
minutes, sec = sec/60, sec % 60
return (years, months, weeks, days, hours, minutes, sec)
def gametime(format=False):
"""
Find the current in-game time (in seconds) since the start of the mud.
The value returned from this function can be used to track the 'true'
in-game time since only the time the game has actually been active will
be adding up (ignoring downtimes).
format - instead of returning result in seconds, format to (game-) time
units.
"""
try:
script = ScriptDB.objects.get_all_scripts(GAME_TIME_SCRIPT)[0]
except KeyError:
logger.log_trace("GameTime script not found.")
return
# we return this as an integer (second-precision is good enough)
game_time = int(script.attr("game_time"))
if format:
return gametime_format(game_time)
return game_time
def runtime(format=False):
"""
Get the total actual time the server has been running (minus downtimes)
"""
try:
script = ScriptDB.objects.get_all_scripts(GAME_TIME_SCRIPT)[0]
except KeyError:
logger.log_trace("GameTime script not found.")
return
# we return this as an integer (second-precision is good enough)
run_time = int(script.attr("run_time"))
if format:
return realtime_format(run_time)
return run_time
def uptime(format=False):
"""
Get the actual time the server has been running since last downtime.
"""
try:
script = ScriptDB.objects.get_all_scripts(GAME_TIME_SCRIPT)[0]
except KeyError:
logger.log_trace("GameTime script not found.")
return
# we return this as an integer (second-precision is good enough)
up_time = int(script.attr("up_time"))
if format:
return realtime_format(up_time)
return up_time
def gametime_to_realtime(secs=0, mins=0, hrs=0, days=0,
weeks=0, months=0, yrs=0):
"""
This method helps to figure out the real-world time it will take until a in-game time
has passed. E.g. if an event should take place a month later in-game, you will be able
to find the number of real-world seconds this corresponds to (hint: Interval events deal
with real life seconds).
Example:
gametime_to_realtime(days=2) -> number of seconds in real life from now after which
2 in-game days will have passed.
"""
real_time = secs/TIMEFACTOR + mins*MIN + hrs*HOUR + \
days*DAY + weeks*WEEK + months*MONTH + yrs*YEAR
return real_time
def realtime_to_gametime(secs=0, mins=0, hrs=0, days=0,
weeks=0, months=0, yrs=0):
"""
This method calculates how large an in-game time a real-world time interval would
correspond to. This is usually a lot less interesting than the other way around.
Example:
realtime_to_gametime(days=2) -> number of game-world seconds
corresponding to 2 real days.
"""
game_time = TIMEFACTOR * (secs + mins*60 + hrs*3600 + days*86400 + \
weeks*604800 + months*2419200 + yrs*29030400)
return game_time
# Time administration routines
def init_gametime():
"""
This is called once, when the server starts for the very first time.
"""
# create the GameTime script and start it
game_time = create_script(GameTime)
game_time.start()

View file

@ -0,0 +1,9 @@
Copyright (c) 2009, David Cramer <dcramer@gmail.com>
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

41
src/utils/idmapper/__init__.py Executable file
View file

@ -0,0 +1,41 @@
import os.path
import warnings
__version__ = (0, 2)
def _get_git_revision(path):
revision_file = os.path.join(path, 'refs', 'heads', 'master')
if not os.path.exists(revision_file):
return None
fh = open(revision_file, 'r')
try:
return fh.read()
finally:
fh.close()
def get_revision():
"""
:returns: Revision number of this branch/checkout, if available. None if
no revision number can be determined.
"""
package_dir = os.path.dirname(__file__)
checkout_dir = os.path.normpath(os.path.join(package_dir, '..'))
path = os.path.join(checkout_dir, '.git')
if os.path.exists(path):
return _get_git_revision(path)
return None
__build__ = get_revision()
def lazy_object(location):
def inner(*args, **kwargs):
parts = location.rsplit('.', 1)
warnings.warn('`idmapper.%s` is deprecated. Please use `%s` instead.' % (parts[1], location), DeprecationWarning)
imp = __import__(parts[0], globals(), locals(), [parts[1]], -1)
func = getattr(imp, parts[1])
if callable(func):
return func(*args, **kwargs)
return func
return inner
SharedMemoryModel = lazy_object('idmapper.models.SharedMemoryModel')

129
src/utils/idmapper/base.py Executable file
View file

@ -0,0 +1,129 @@
from weakref import WeakValueDictionary
from django.db.models.base import Model, ModelBase
from manager import SharedMemoryManager
class SharedMemoryModelBase(ModelBase):
def __new__(cls, name, bases, attrs):
super_new = super(ModelBase, cls).__new__
parents = [b for b in bases if isinstance(b, SharedMemoryModelBase)]
if not parents:
# If this isn't a subclass of Model, don't do anything special.
return super_new(cls, name, bases, attrs)
return super(SharedMemoryModelBase, cls).__new__(cls, name, bases, attrs)
def __call__(cls, *args, **kwargs):
"""
this method will either create an instance (by calling the default implementation)
or try to retrieve one from the class-wide cache by infering the pk value from
args and kwargs. If instance caching is enabled for this class, the cache is
populated whenever possible (ie when it is possible to infer the pk value).
"""
def new_instance():
return super(SharedMemoryModelBase, cls).__call__(*args, **kwargs)
instance_key = cls._get_cache_key(args, kwargs)
# depending on the arguments, we might not be able to infer the PK, so in that case we create a new instance
if instance_key is None:
return new_instance()
cached_instance = cls.get_cached_instance(instance_key)
if cached_instance is None:
cached_instance = new_instance()
cls.cache_instance(cached_instance)
return cached_instance
def _prepare(cls):
cls.__instance_cache__ = WeakValueDictionary()
super(SharedMemoryModelBase, cls)._prepare()
class SharedMemoryModel(Model):
# XXX: this is creating a model and it shouldn't be.. how do we properly
# subclass now?
__metaclass__ = SharedMemoryModelBase
def _get_cache_key(cls, args, kwargs):
"""
This method is used by the caching subsystem to infer the PK value from the constructor arguments.
It is used to decide if an instance has to be built or is already in the cache.
"""
result = None
# Quick hack for my composites work for now.
if hasattr(cls._meta, 'pks'):
pk = cls._meta.pks[0]
else:
pk = cls._meta.pk
# get the index of the pk in the class fields. this should be calculated *once*, but isn't atm
pk_position = cls._meta.fields.index(pk)
if len(args) > pk_position:
# if it's in the args, we can get it easily by index
result = args[pk_position]
elif pk.attname in kwargs:
# retrieve the pk value. Note that we use attname instead of name, to handle the case where the pk is a
# a ForeignKey.
result = kwargs[pk.attname]
elif pk.name != pk.attname and pk.name in kwargs:
# ok we couldn't find the value, but maybe it's a FK and we can find the corresponding object instead
result = kwargs[pk.name]
if result is not None and isinstance(result, Model):
# if the pk value happens to be a model instance (which can happen wich a FK), we'd rather use its own pk as the key
result = result._get_pk_val()
return result
_get_cache_key = classmethod(_get_cache_key)
def get_cached_instance(cls, id):
"""
Method to retrieve a cached instance by pk value. Returns None when not found
(which will always be the case when caching is disabled for this class). Please
note that the lookup will be done even when instance caching is disabled.
"""
return cls.__instance_cache__.get(id)
get_cached_instance = classmethod(get_cached_instance)
def cache_instance(cls, instance):
"""
Method to store an instance in the cache.
"""
if instance._get_pk_val() is not None:
cls.__instance_cache__[instance._get_pk_val()] = instance
cache_instance = classmethod(cache_instance)
def _flush_cached_by_key(cls, key):
del cls.__instance_cache__[key]
_flush_cached_by_key = classmethod(_flush_cached_by_key)
def flush_cached_instance(cls, instance):
"""
Method to flush an instance from the cache. The instance will always be flushed from the cache,
since this is most likely called from delete(), and we want to make sure we don't cache dead objects.
"""
cls._flush_cached_by_key(instance._get_pk_val())
flush_cached_instance = classmethod(flush_cached_instance)
def save(self, *args, **kwargs):
super(SharedMemoryModel, self).save(*args, **kwargs)
self.__class__.cache_instance(self)
# TODO: This needs moved to the prepare stage (I believe?)
objects = SharedMemoryManager()
from django.db.models.signals import pre_delete
# Use a signal so we make sure to catch cascades.
def flush_singleton_cache(sender, instance, **kwargs):
# XXX: Is this the best way to make sure we can flush?
if isinstance(instance, SharedMemoryModel):
instance.__class__.flush_cached_instance(instance)
pre_delete.connect(flush_singleton_cache)
# XXX: It's to be determined if we should use this or not.
# def update_singleton_cache(sender, instance, **kwargs):
# if isinstance(instance.__class__, SharedMemoryModel):
# instance.__class__.cache_instance(instance)
# post_save.connect(flush_singleton_cache)

15
src/utils/idmapper/manager.py Executable file
View file

@ -0,0 +1,15 @@
from django.db.models.manager import Manager
class SharedMemoryManager(Manager):
# TODO: improve on this implementation
# We need a way to handle reverse lookups so that this model can
# still use the singleton cache, but the active model isn't required
# to be a SharedMemoryModel.
def get(self, **kwargs):
items = kwargs.keys()
inst = None
if len(items) == 1 and items[0] in ('pk', self.model._meta.pk.attname):
inst = self.model.get_cached_instance(kwargs[items[0]])
if inst is None:
inst = super(SharedMemoryManager, self).get(**kwargs)
return inst

2
src/utils/idmapper/models.py Executable file
View file

@ -0,0 +1,2 @@
from django.db.models import *
from base import SharedMemoryModel

70
src/utils/idmapper/tests.py Executable file
View file

@ -0,0 +1,70 @@
from django.test import TestCase
from base import SharedMemoryModel
from django.db import models
class Category(SharedMemoryModel):
name = models.CharField(max_length=32)
class RegularCategory(models.Model):
name = models.CharField(max_length=32)
class Article(SharedMemoryModel):
name = models.CharField(max_length=32)
category = models.ForeignKey(Category)
category2 = models.ForeignKey(RegularCategory)
class RegularArticle(models.Model):
name = models.CharField(max_length=32)
category = models.ForeignKey(Category)
category2 = models.ForeignKey(RegularCategory)
class SharedMemorysTest(TestCase):
# TODO: test for cross model relation (singleton to regular)
def setUp(self):
n = 0
category = Category.objects.create(name="Category %d" % (n,))
regcategory = RegularCategory.objects.create(name="Category %d" % (n,))
for n in xrange(0, 10):
Article.objects.create(name="Article %d" % (n,), category=category, category2=regcategory)
RegularArticle.objects.create(name="Article %d" % (n,), category=category, category2=regcategory)
def testSharedMemoryReferences(self):
article_list = Article.objects.all().select_related('category')
last_article = article_list[0]
for article in article_list[1:]:
self.assertEquals(article.category is last_article.category, True)
last_article = article
def testRegularReferences(self):
article_list = RegularArticle.objects.all().select_related('category')
last_article = article_list[0]
for article in article_list[1:]:
self.assertEquals(article.category2 is last_article.category2, False)
last_article = article
def testMixedReferences(self):
article_list = RegularArticle.objects.all().select_related('category')
last_article = article_list[0]
for article in article_list[1:]:
self.assertEquals(article.category is last_article.category, True)
last_article = article
article_list = Article.objects.all().select_related('category')
last_article = article_list[0]
for article in article_list[1:]:
self.assertEquals(article.category2 is last_article.category2, False)
last_article = article
def testObjectDeletion(self):
# This must execute first so its guaranteed to be in memory.
article_list = list(Article.objects.all().select_related('category'))
article = Article.objects.all()[0:1].get()
pk = article.pk
article.delete()
self.assertEquals(pk not in Article.__instance_cache__, True)

69
src/utils/logger.py Normal file
View file

@ -0,0 +1,69 @@
"""
Logging facilities
This file should have an absolute minimum in imports. If you'd like to layer
additional functionality on top of some of the methods below, wrap them in
a higher layer module.
"""
from traceback import format_exc
from twisted.python import log
from src.utils import utils
def log_trace(errmsg=None):
"""
Log a traceback to the log. This should be called
from within an exception. errmsg is optional and
adds an extra line with added info.
"""
tracestring = format_exc()
if tracestring:
for line in tracestring.splitlines():
log.msg('[::] %s' % line)
if errmsg:
try:
errmsg = utils.to_str(errmsg)
except Exception, e:
errmsg = str(e)
for line in errmsg.splitlines():
log.msg('[EE] %s' % line)
def log_errmsg(errmsg):
"""
Prints/logs an error message to the server log.
errormsg: (string) The message to be logged.
"""
try:
errmsg = utils.to_str(errmsg)
except Exception, e:
errmsg = str(e)
for line in errmsg.splitlines():
log.msg('[EE] %s' % line)
#log.err('ERROR: %s' % (errormsg,))
def log_warnmsg(warnmsg):
"""
Prints/logs any warnings that aren't critical but should be noted.
warnmsg: (string) The message to be logged.
"""
try:
warnmsg = utils.to_str(warnmsg)
except Exception, e:
warnmsg = str(e)
for line in warnmsg.splitlines():
log.msg('[WW] %s' % line)
#log.msg('WARNING: %s' % (warnmsg,))
def log_infomsg(infomsg):
"""
Prints any generic debugging/informative info that should appear in the log.
infomsg: (string) The message to be logged.
"""
try:
infomsg = utils.to_str(infomsg)
except Exception, e:
infomsg = str(e)
for line in infomsg.splitlines():
log.msg('[..] %s' % line)

658
src/utils/reimport.py Normal file
View file

@ -0,0 +1,658 @@
# MIT Licensed
# Copyright (c) 2009-2010 Peter Shinners <pete@shinners.org>
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use,
# copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following
# conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
"""
This module intends to be a full featured replacement for Python's reload
function. It is targeted towards making a reload that works for Python
plugins and extensions used by longer running applications.
Reimport currently supports Python 2.4 through 2.6.
By its very nature, this is not a completely solvable problem. The goal of
this module is to make the most common sorts of updates work well. It also
allows individual modules and package to assist in the process. A more
detailed description of what happens is at
http://code.google.com/p/reimport .
"""
__all__ = ["reimport", "modified"]
import sys
import os
import gc
import inspect
import weakref
import traceback
import time
__version__ = "1.2"
__author__ = "Peter Shinners <pete@shinners.org>"
__license__ = "MIT"
__url__ = "http://code.google.com/p/reimport"
_previous_scan_time = time.time() - 1.0
_module_timestamps = {}
# find the 'instance' old style type
class _OldClass: pass
_InstanceType = type(_OldClass())
del _OldClass
def reimport(*modules):
"""Reimport python modules. Multiple modules can be passed either by
name or by reference. Only pure python modules can be reimported.
For advanced control, global variables can be placed in modules
that allows finer control of the reimport process.
If a package module has a true value for "__package_reimport__"
then that entire package will be reimported when any of its children
packages or modules are reimported.
If a package module defines __reimported__ it must be a callable
function that accepts one argument and returns a bool. The argument
is the reference to the old version of that module before any
cleanup has happend. The function should normally return True to
allow the standard reimport cleanup. If the function returns false
then cleanup will be disabled for only that module. Any exceptions
raised during the callback will be handled by traceback.print_exc,
similar to what happens with tracebacks in the __del__ method.
"""
__internal_swaprefs_ignore__ = "reimport"
reloadSet = set()
if not modules:
return
# Get names of all modules being reloaded
for module in modules:
name, target = _find_exact_target(module)
if not target:
raise ValueError("Module %r not found" % module)
if not _is_code_module(target):
raise ValueError("Cannot reimport extension, %r" % name)
reloadSet.update(_find_reloading_modules(name))
# Sort module names
reloadNames = _package_depth_sort(reloadSet, False)
# Check for SyntaxErrors ahead of time. This won't catch all
# possible SyntaxErrors or any other ImportErrors. But these
# should be the most common problems, and now is the cleanest
# time to abort.
# I know this gets compiled again anyways. It could be
# avoided with py_compile, but I will not be the creator
# of messy .pyc files!
for name in reloadNames:
filename = getattr(sys.modules[name], "__file__", None)
if not filename:
continue
pyname = os.path.splitext(filename)[0] + ".py"
try:
data = open(pyname, "rU").read() + "\n"
except (IOError, OSError):
continue
compile(data, pyname, "exec", 0, False) # Let this raise exceptions
# Move modules out of sys
oldModules = {}
for name in reloadNames:
oldModules[name] = sys.modules.pop(name)
ignores = (id(oldModules), id(__builtins__))
prevNames = set(sys.modules)
# Python will munge the parent package on import. Remember original value
parentPackageName = name.rsplit(".", 1)
parentPackage = None
parentPackageDeleted = lambda: None
if len(parentPackageName) == 2:
parentPackage = sys.modules.get(parentPackageName[0], None)
parentValue = getattr(parentPackage, parentPackageName[1], parentPackageDeleted)
# Reimport modules, trying to rollback on exceptions
try:
for name in reloadNames:
if name not in sys.modules:
__import__(name)
except StandardError:
# Try to dissolve any newly import modules and revive the old ones
newNames = set(sys.modules) - prevNames
newNames = _package_depth_sort(newNames, True)
for name in newNames:
_unimport_module(sys.modules[name], ignores)
assert name not in sys.modules
sys.modules.update(oldModules)
raise
newNames = set(sys.modules) - prevNames
newNames = _package_depth_sort(newNames, True)
# Update timestamps for loaded time
now = time.time() - 1.0
for name in newNames:
_module_timestamps[name] = (now, True)
# Fix Python automatically shoving of children into parent packages
if parentPackage and parentValue:
if parentValue == parentPackageDeleted:
delattr(parentPackage, parentPackageName[1])
else:
setattr(parentPackage, parentPackageName[1], parentValue)
parentValue = parentPackage = parentPackageDeleted = None
# Push exported namespaces into parent packages
pushSymbols = {}
for name in newNames:
oldModule = oldModules.get(name)
if not oldModule:
continue
parents = _find_parent_importers(name, oldModule, newNames)
pushSymbols[name] = parents
for name, parents in pushSymbols.iteritems():
for parent in parents:
oldModule = oldModules[name]
newModule = sys.modules[name]
_push_imported_symbols(newModule, oldModule, parent)
# Rejigger the universe
for name in newNames:
old = oldModules.get(name)
if not old:
continue
new = sys.modules[name]
rejigger = True
reimported = getattr(new, "__reimported__", None)
if reimported:
try:
rejigger = reimported(old)
except StandardError:
# What else can we do? the callbacks must go on
# Note, this is same as __del__ behaviour. /shrug
traceback.print_exc()
if rejigger:
_rejigger_module(old, new, ignores)
else:
_unimport_module(new, ignores)
def modified(path=None):
"""Find loaded modules that have changed on disk under the given path.
If no path is given then all modules are searched.
"""
global _previous_scan_time
modules = []
if path:
path = os.path.normpath(path) + os.sep
defaultTime = (_previous_scan_time, False)
pycExt = __debug__ and ".pyc" or ".pyo"
for name, module in sys.modules.items():
filename = _is_code_module(module)
if not filename:
continue
filename = os.path.normpath(filename)
prevTime, prevScan = _module_timestamps.setdefault(name, defaultTime)
if path and not filename.startswith(path):
continue
# Get timestamp of .pyc if this is first time checking this module
if not prevScan:
pycName = os.path.splitext(filename)[0] + pycExt
if pycName != filename:
try:
prevTime = os.path.getmtime(pycName)
except OSError:
pass
_module_timestamps[name] = (prevTime, True)
# Get timestamp of source file
try:
diskTime = os.path.getmtime(filename)
except OSError:
diskTime = None
if diskTime is not None and prevTime < diskTime:
modules.append(name)
_previous_scan_time = time.time()
return modules
def _is_code_module(module):
"""Determine if a module comes from python code"""
# getsourcefile will not return "bare" pyc modules. we can reload those?
try:
return inspect.getsourcefile(module) or ""
except TypeError:
return ""
def _find_exact_target(module):
"""Given a module name or object, find the
base module where reimport will happen."""
# Given a name or a module, find both the name and the module
actualModule = sys.modules.get(module)
if actualModule is not None:
name = module
else:
for name, mod in sys.modules.iteritems():
if mod is module:
actualModule = module
break
else:
return "", None
# Find highest level parent package that has package_reimport magic
parentName = name
while True:
splitName = parentName.rsplit(".", 1)
if len(splitName) <= 1:
return name, actualModule
parentName = splitName[0]
parentModule = sys.modules.get(parentName)
if getattr(parentModule, "__package_reimport__", None):
name = parentName
actualModule = parentModule
def _find_reloading_modules(name):
"""Find all modules that will be reloaded from given name"""
modules = [name]
childNames = name + "."
for name in sys.modules.keys():
if name.startswith(childNames) and _is_code_module(sys.modules[name]):
modules.append(name)
return modules
def _package_depth_sort(names, reverse):
"""Sort a list of module names by their package depth"""
def packageDepth(name):
return name.count(".")
return sorted(names, key=packageDepth, reverse=reverse)
def _find_module_exports(module):
allNames = getattr(module, "__all__", ())
if not allNames:
allNames = [n for n in dir(module) if n[0] != "_"]
return set(allNames)
def _find_parent_importers(name, oldModule, newNames):
"""Find parents of reimported module that have all exported symbols"""
parents = []
# Get exported symbols
exports = _find_module_exports(oldModule)
if not exports:
return parents
# Find non-reimported parents that have all old symbols
parent = name
while True:
names = parent.rsplit(".", 1)
if len(names) <= 1:
break
parent = names[0]
if parent in newNames:
continue
parentModule = sys.modules[parent]
if not exports - set(dir(parentModule)):
parents.append(parentModule)
return parents
def _push_imported_symbols(newModule, oldModule, parent):
"""Transfer changes symbols from a child module to a parent package"""
# This assumes everything in oldModule is already found in parent
oldExports = _find_module_exports(oldModule)
newExports = _find_module_exports(newModule)
# Delete missing symbols
for name in oldExports - newExports:
delattr(parent, name)
# Add new symbols
for name in newExports - oldExports:
setattr(parent, name, getattr(newModule, name))
# Update existing symbols
for name in newExports & oldExports:
oldValue = getattr(oldModule, name)
if getattr(parent, name) is oldValue:
setattr(parent, name, getattr(newModule, name))
# To rejigger is to copy internal values from new to old
# and then to swap external references from old to new
def _rejigger_module(old, new, ignores):
"""Mighty morphin power modules"""
__internal_swaprefs_ignore__ = "rejigger_module"
oldVars = vars(old)
newVars = vars(new)
ignores += (id(oldVars),)
old.__doc__ = new.__doc__
# Get filename used by python code
filename = new.__file__
for name, value in newVars.iteritems():
if name in oldVars:
oldValue = oldVars[name]
if oldValue is value:
continue
if _from_file(filename, value):
if inspect.isclass(value):
_rejigger_class(oldValue, value, ignores)
elif inspect.isfunction(value):
_rejigger_func(oldValue, value, ignores)
setattr(old, name, value)
for name in oldVars.keys():
if name not in newVars:
value = getattr(old, name)
delattr(old, name)
if _from_file(filename, value):
if inspect.isclass(value) or inspect.isfunction(value):
_remove_refs(value, ignores)
_swap_refs(old, new, ignores)
def _from_file(filename, value):
"""Test if object came from a filename, works for pyc/py confusion"""
try:
objfile = inspect.getsourcefile(value)
except TypeError:
return False
return bool(objfile) and objfile.startswith(filename)
def _rejigger_class(old, new, ignores):
"""Mighty morphin power classes"""
__internal_swaprefs_ignore__ = "rejigger_class"
oldVars = vars(old)
newVars = vars(new)
ignores += (id(oldVars),)
for name, value in newVars.iteritems():
if name in ("__dict__", "__doc__", "__weakref__"):
continue
if name in oldVars:
oldValue = oldVars[name]
if oldValue is value:
continue
if inspect.isclass(value) and value.__module__ == new.__module__:
_rejigger_class(oldValue, value, ignores)
elif inspect.isfunction(value):
_rejigger_func(oldValue, value, ignores)
setattr(old, name, value)
for name in oldVars.keys():
if name not in newVars:
value = getattr(old, name)
delattr(old, name)
_remove_refs(value, ignores)
_swap_refs(old, new, ignores)
def _rejigger_func(old, new, ignores):
"""Mighty morphin power functions"""
__internal_swaprefs_ignore__ = "rejigger_func"
old.func_code = new.func_code
old.func_doc = new.func_doc
old.func_defaults = new.func_defaults
old.func_dict = new.func_dict
_swap_refs(old, new, ignores)
def _unimport_module(old, ignores):
"""Remove traces of a module"""
__internal_swaprefs_ignore__ = "unimport_module"
oldValues = vars(old).values()
ignores += (id(oldValues),)
# Get filename used by python code
filename = old.__file__
fileext = os.path.splitext(filename)
if fileext in (".pyo", ".pyc", ".pyw"):
filename = filename[:-1]
for value in oldValues:
try: objfile = inspect.getsourcefile(value)
except TypeError: objfile = ""
if objfile == filename:
if inspect.isclass(value):
_unimport_class(value, ignores)
elif inspect.isfunction(value):
_remove_refs(value, ignores)
_remove_refs(old, ignores)
def _unimport_class(old, ignores):
"""Remove traces of a class"""
__internal_swaprefs_ignore__ = "unimport_class"
oldItems = vars(old).items()
ignores += (id(oldItems),)
for name, value in oldItems:
if name in ("__dict__", "__doc__", "__weakref__"):
continue
if inspect.isclass(value) and value.__module__ == old.__module__:
_unimport_class(value, ignores)
elif inspect.isfunction(value):
_remove_refs(value, ignores)
_remove_refs(old, ignores)
_recursive_tuple_swap = set()
def _bonus_containers():
"""Find additional container types, if they are loaded. Returns
(deque, defaultdict).
Any of these will be None if not loaded.
"""
deque = defaultdict = None
collections = sys.modules.get("collections", None)
if collections:
deque = getattr(collections, "collections", None)
defaultdict = getattr(collections, "defaultdict", None)
return deque, defaultdict
def _find_sequence_indices(container, value):
"""Find indices of value in container. The indices will
be in reverse order, to allow safe editing.
"""
indices = []
for i in range(len(container)-1, -1, -1):
if container[i] is value:
indices.append(i)
return indices
def _swap_refs(old, new, ignores):
"""Swap references from one object to another"""
__internal_swaprefs_ignore__ = "swap_refs"
# Swap weak references
refs = weakref.getweakrefs(old)
if refs:
try:
newRef = weakref.ref(new)
except ValueError:
pass
else:
for oldRef in refs:
_swap_refs(oldRef, newRef, ignores + (id(refs),))
del refs
deque, defaultdict = _bonus_containers()
# Swap through garbage collector
referrers = gc.get_referrers(old)
for container in referrers:
if id(container) in ignores:
continue
containerType = type(container)
if containerType is list or containerType is deque:
for index in _find_sequence_indices(container, old):
container[index] = new
elif containerType is tuple:
# protect from recursive tuples
orig = container
if id(orig) in _recursive_tuple_swap:
continue
_recursive_tuple_swap.add(id(orig))
try:
container = list(container)
for index in _find_sequence_indices(container, old):
container[index] = new
container = tuple(container)
_swap_refs(orig, container, ignores + (id(referrers),))
finally:
_recursive_tuple_swap.remove(id(orig))
elif containerType is dict or containerType is defaultdict:
if "__internal_swaprefs_ignore__" not in container:
try:
if old in container:
container[new] = container.pop(old)
except TypeError: # Unhashable old value
pass
for k,v in container.iteritems():
if v is old:
container[k] = new
elif containerType is set:
container.remove(old)
container.add(new)
elif containerType is type:
if old in container.__bases__:
bases = list(container.__bases__)
bases[bases.index(old)] = new
container.__bases__ = tuple(bases)
elif type(container) is old:
container.__class__ = new
elif containerType is _InstanceType:
if container.__class__ is old:
container.__class__ = new
def _remove_refs(old, ignores):
"""Remove references to a discontinued object"""
__internal_swaprefs_ignore__ = "remove_refs"
# Ignore builtin immutables that keep no other references
if old is None or isinstance(old, (int, basestring, float, complex)):
return
deque, defaultdict = _bonus_containers()
# Remove through garbage collector
for container in gc.get_referrers(old):
if id(container) in ignores:
continue
containerType = type(container)
if containerType is list or containerType is deque:
for index in _find_sequence_indices(container, old):
del container[index]
elif containerType is tuple:
orig = container
container = list(container)
for index in _find_sequence_indices(container, old):
del container[index]
container = tuple(container)
_swap_refs(orig, container, ignores)
elif containerType is dict or containerType is defaultdict:
if "__internal_swaprefs_ignore__" not in container:
try:
container.pop(old, None)
except TypeError: # Unhashable old value
pass
for k,v in container.items():
if v is old:
del container[k]
elif containerType is set:
container.remove(old)

140
src/utils/reloads.py Normal file
View file

@ -0,0 +1,140 @@
"""
This holds the mechanism for reloading the game modules on the
fly. It's in this separate module since it's not a good idea to
keep it in server.py since it messes with importing, and it's
also not good to tie such important functionality to a user-definable
command class.
"""
from django.db.models.loading import AppCache
from django.utils.datastructures import SortedDict
from django.conf import settings
from src.scripts.models import ScriptDB
from src.typeclasses import models as typeclassmodels
from src.objects import exithandler
from src.comms import channelhandler
from src.comms.models import Channel
from src.utils import reimport
from src.utils import logger
def reload_modules():
"""
Reload modules that don't have any variables that can be reset.
Note that python reloading is a tricky art and strange things have
been known to happen if debugging and reloading a lot while
working with src/ modules. A cold reboot is often needed
eventually.
"""
# We protect e.g. src/ from reload since reloading it in a running
# server can create unexpected results (and besides, we should
# never need to do that anyway. Updating src requires a server
# reboot).
protected_dirs = ('src.',)
# flag 'dangerous' typeclasses (those which retain a memory
# reference, notably Scripts with a timer component) for
# non-reload, since these cannot be safely cleaned from memory
# without causing havoc. A server reboot is required for updating
# these.
unsafe_modules = []
for scriptobj in ScriptDB.objects.get_all_scripts():
if scriptobj.interval and scriptobj.typeclass_path:
unsafe_modules.append(scriptobj.typeclass_path)
unsafe_modules = list(set(unsafe_modules))
def safe_dir_to_reload(modpath):
"Check so modpath is not a subdir of a protected dir"
return not any(modpath.startswith(pdir) for pdir in protected_dirs)
def safe_mod_to_reload(modpath):
"Check so modpath is not in an unsafe module"
return not any(mpath.startswith(modpath) for mpath in unsafe_modules)
cemit_info('-'*50 +"\n Cleaning module caches ...")
# clean as much of the caches as we can
cache = AppCache()
cache.app_store = SortedDict()
cache.app_models = SortedDict()
cache.app_errors = {}
cache.handled = {}
cache.loaded = False
# find modified modules
modified = reimport.modified()
safe_dir_modified = [mod for mod in modified if safe_dir_to_reload(mod)]
unsafe_dir_modified = [mod for mod in modified if mod not in safe_dir_modified]
safe_modified = [mod for mod in safe_dir_modified if safe_mod_to_reload(mod)]
unsafe_mod_modified = [mod for mod in safe_dir_modified if mod not in safe_modified]
string = ""
if unsafe_dir_modified or unsafe_mod_modified:
string += "\n WARNING: Some modules can not be reloaded"
string += "\n since it would not be safe to do so.\n"
if unsafe_dir_modified:
string += "\n-The following module(s) is/are located in the src/ directory and"
string += "\n should not be reloaded without a server reboot:\n %s\n"
string = string % unsafe_dir_modified
if unsafe_mod_modified:
string += "\n-The following modules contains at least one Script class with a timer"
string += "\n component and which has already spawned instances - these cannot be "
string += "\n safely cleaned from memory on the fly. Stop all the affected scripts "
string += "\n or restart the server to safely reload:\n %s\n"
string = string % unsafe_mod_modified
if string:
cemit_info(string)
if safe_modified:
cemit_info(" Reloading module(s):\n %s ..." % safe_modified)
reimport.reimport(*safe_modified)
cemit_info(" ...all safe modules reloaded.")
else:
cemit_info(" Nothing was reloaded.")
# clean out cache dictionary of typeclasses, exits and channe
typeclassmodels.reset()
exithandler.EXITHANDLER.reset()
channelhandler.CHANNELHANDLER.reset()
def reload_scripts(scripts=None, obj=None, key=None,
dbref=None, init_mode=False):
"""
Run a validation of the script database.
obj - only validate scripts on this object
key - only validate scripts with this key
dbref - only validate the script with this unique idref
emit_to_obj - which object to receive error message
init_mode - during init-mode, non-persistent scripts are
cleaned out. All persistent scripts are force-started.
"""
cemit_info(" Validating scripts ...")
nr_started, nr_stopped = ScriptDB.objects.validate(scripts=scripts,
obj=obj, key=key,
dbref=dbref,
init_mode=init_mode)
string = " Started %s script(s). Stopped %s invalid script(s)." % \
(nr_started, nr_stopped)
cemit_info(string)
def reload_commands():
from src.commands import cmdsethandler
cmdsethandler.CACHED_CMDSETS = {}
cemit_info(" Cleaned cmdset cache.\n" + '-'*50)
def cemit_info(message):
"""
Sends the info to a pre-set channel. This channel is
set by CHANNEL_MUDINFO in settings.
"""
logger.log_infomsg(message)
try:
infochan = settings.CHANNEL_MUDINFO
infochan = Channel.objects.get_channel(infochan[0])
except Exception:
return
cname = infochan.key
cmessage = "\n".join(["[%s]: %s" % (cname, line) for line in message.split('\n')])
infochan.msg(cmessage)

167
src/utils/search.py Normal file
View file

@ -0,0 +1,167 @@
"""
This is a convenient container gathering all the main
search methods for the various database tables.
It is intended to be used e.g. as
> from src.utils import search
> match = search.objects(...)
Note that this is not intended to be a complete listing of all search
methods! You need to refer to the respective manager to get all
possible search methods. To get to the managers from your code, import
the database model and call its 'objects' property.
Also remember that all commands in this file return lists (also if
there is only one match) unless noted otherwise.
Example: To reach the search method 'get_object_with_user'
in src/objects/managers.py:
> from src.objects.models import ObjectDB
> match = Object.objects.get_object_with_user(...)
"""
# Import the manager methods to be wrapped
from src.objects.models import ObjectDB
from src.players.models import PlayerDB
from src.scripts.models import ScriptDB
from src.comms.models import Msg, Channel
from src.help.models import HelpEntry
from src.permissions.models import PermissionGroup
from src.config.models import ConfigValue
#
# Search objects as a character
#
# NOTE: A more powerful wrapper of this method
# is reachable from within each command class
# by using self.caller.search()!
#
# def object_search(self, character, ostring,
# global_search=False,
# attribute_name=None):
# """
# Search as an object and return results.
#
# character: (Object) The object performing the search.
# ostring: (string) The string to compare names against.
# Can be a dbref. If name is appended by *, a player is searched for.
# global_search: Search all objects, not just the current location/inventory
# attribute_name: (string) Which attribute to search in each object.
# If None, the default 'name' attribute is used.
# """
objects = ObjectDB.objects.object_search
#
# Search for players
#
# NOTE: Most usually you would do such searches from
# from inseide command definitions using
# self.caller.search() by appending an '*' to the
# beginning of the search criterion.
#
# def player_search(self, ostring):
# """
# Searches for a particular player by name or
# database id.
#
# ostring = a string or database id.
# """
players = PlayerDB.objects.player_search
#
# Searching for scripts
#
# def script_search(self, ostring, obj=None, only_timed=False):
# """
# Search for a particular script.
#
# ostring - search criterion - a script ID or key
# obj - limit search to scripts defined on this object
# only_timed - limit search only to scripts that run
# on a timer.
# """
scripts = ScriptDB.objects.script_search
#
# Searching for communication messages
#
#
# def message_search(self, sender=None, receiver=None, channel=None, freetext=None):
# """
# Search the message database for particular messages. At least one
# of the arguments must be given to do a search.
#
# sender - get messages sent by a particular player
# receiver - get messages received by a certain player
# channel - get messages sent to a particular channel
# freetext - Search for a text string in a message.
# NOTE: This can potentially be slow, so make sure to supply
# one of the other arguments to limit the search.
# """
messages = Msg.objects.message_search
#
# Search for Communication Channels
#
# def channel_search(self, ostring)
# """
# Search the channel database for a particular channel.
#
# ostring - the key or database id of the channel.
# """
channels = Channel.objects.channel_search
#
# Find help entry objects.
#
# def search_help(self, ostring, help_category=None):
# """
# Retrieve a search entry object.
#
# ostring - the help topic to look for
# category - limit the search to a particular help topic
# """
helpentries = HelpEntry.objects.search_help
#
# Search for a permission group
# Note that the name is case sensitive.
#
# def search_permissiongroup(self, ostring):
# """
# Find a permission group
#
# ostring = permission group name (case sensitive)
# or database dbref
# """
permgroups = PermissionGroup.objects.search_permgroup
#
# Get a configuration value
#
# OBS - this returns a unique value (or None),
# not a list!
#
# def config_search(self, ostring):
# """
# Retrieve a configuration value.
# ostring - a (unique) configuration key
# """
configvalue = ConfigValue.objects.config_search

250
src/utils/utils.py Normal file
View file

@ -0,0 +1,250 @@
"""
General helper functions that don't fit neatly under any given category.
They provide some useful string and conversion methods that might
be of use when designing your own game.
"""
import os
import textwrap
from django.conf import settings
def is_iter(iterable):
"""
Checks if an object behaves iterably. However,
strings are not accepted as iterable (although
they are actually iterable), since string iterations
are usually not what we want to do with a string.
"""
if isinstance(iterable, basestring):
# skip all forms of strings (str, unicode etc)
return False
try:
# check if object implements iter protocol
return iter(iterable)
except TypeError:
return False
def fill(text, width=78):
"""
Safely wrap text to a certain number of characters.
text: (str) The text to wrap.
width: (int) The number of characters to wrap to.
"""
if not text:
return ""
return textwrap.fill(str(text), width)
def dedent(text):
"""
Safely clean all whitespace at the left
of a paragraph. This is useful for preserving
triple-quoted string indentation while still
shifting it all to be next to the left edge of
the display.
"""
if not text:
return ""
return textwrap.dedent(text)
def wildcard_to_regexp(instring):
"""
Converts a player-supplied string that may have wildcards in it to regular
expressions. This is useful for name matching.
instring: (string) A string that may potentially contain wildcards (* or ?).
"""
regexp_string = ""
# If the string starts with an asterisk, we can't impose the beginning of
# string (^) limiter.
if instring[0] != "*":
regexp_string += "^"
# Replace any occurances of * or ? with the appropriate groups.
regexp_string += instring.replace("*","(.*)").replace("?", "(.{1})")
# If there's an asterisk at the end of the string, we can't impose the
# end of string ($) limiter.
if instring[-1] != "*":
regexp_string += "$"
return regexp_string
def time_format(seconds, style=0):
"""
Function to return a 'prettified' version of a value in seconds.
Style 0: 1d 08:30
Style 1: 1d
Style 2: 1 day, 8 hours, 30 minutes, 10 seconds
"""
if seconds < 0:
seconds = 0
else:
# We'll just use integer math, no need for decimal precision.
seconds = int(seconds)
days = seconds / 86400
seconds -= days * 86400
hours = seconds / 3600
seconds -= hours * 3600
minutes = seconds / 60
seconds -= minutes * 60
if style is 0:
"""
Standard colon-style output.
"""
if days > 0:
retval = '%id %02i:%02i' % (days, hours, minutes,)
else:
retval = '%02i:%02i' % (hours, minutes,)
return retval
elif style is 1:
"""
Simple, abbreviated form that only shows the highest time amount.
"""
if days > 0:
return '%id' % (days,)
elif hours > 0:
return '%ih' % (hours,)
elif minutes > 0:
return '%im' % (minutes,)
else:
return '%is' % (seconds,)
elif style is 2:
"""
Full-detailed, long-winded format.
"""
days_str = hours_str = minutes_str = ''
if days > 0:
days_str = '%i days, ' % (days,)
if days or hours > 0:
hours_str = '%i hours, ' % (hours,)
if hours or minutes > 0:
minutes_str = '%i minutes, ' % (minutes,)
seconds_str = '%i seconds' % (seconds,)
retval = '%s%s%s%s' % (days_str, hours_str, minutes_str, seconds_str,)
return retval
def host_os_is(osname):
"""
Check to see if the host OS matches the query.
"""
if os.name == osname:
return True
return False
def get_evennia_version():
"""
Check for the evennia version info.
"""
version_file_path = "%s%s%s" % (settings.BASE_PATH, os.sep, "VERSION")
try:
return open(version_file_path).readline().strip('\n').strip()
except IOError:
return "Unknown version"
def pypath_to_realpath(python_path, file_ending='py'):
"""
Converts a path on dot python form (e.g. src.objects.models)
to a system path (src/objects/models.py). Calculates all
paths starting from the evennia main directory.
"""
pathsplit = python_path.strip().split('.')
if not pathsplit:
return python_path
path = settings.BASE_PATH
for directory in pathsplit:
path = os.path.join(path, directory)
return "%s.%s" % (path, file_ending)
def dbref(dbref):
"""
Converts/checks if input is a valid dbref
Valid forms of dbref (database reference number)
are either a string '#N' or an integer N.
Output is the integer part.
"""
if type(dbref) == str:
dbref = dbref.lstrip('#')
try:
dbref = int(dbref)
if dbref < 1:
return None
except Exception:
return None
return dbref
def to_unicode(obj, encoding='utf-8'):
"""
This decodes a suitable object to
the unicode format. Note that one
needs to encode it back to utf-8
before writing to disk or printing.
"""
if isinstance(obj, basestring) \
and not isinstance(obj, unicode):
try:
obj = unicode(obj, encoding)
except UnicodeDecodeError:
raise Exception("Error: '%s' contains invalid character(s) not in %s." % (obj, encoding))
return obj
def to_str(obj, encoding='utf-8'):
"""
This encodes a unicode string
back to byte-representation,
for printing, writing to disk etc.
"""
if isinstance(obj, basestring) \
and isinstance(obj, unicode):
try:
obj = obj.encode(encoding)
except UnicodeEncodeError:
raise Exception("Error: Unicode could not encode unicode string '%s'(%s) to a bytestring. " % (obj, encoding))
return obj
def validate_email_address(emailaddress):
"""
Checks if an email address is syntactically correct.
(This snippet was adapted from
http://commandline.org.uk/python/email-syntax-check.)
"""
emailaddress = r"%s" % emailaddress
domains = ("aero", "asia", "biz", "cat", "com", "coop",
"edu", "gov", "info", "int", "jobs", "mil", "mobi", "museum",
"name", "net", "org", "pro", "tel", "travel")
# Email address must be more than 7 characters in total.
if len(emailaddress) < 7:
return False # Address too short.
# Split up email address into parts.
try:
localpart, domainname = emailaddress.rsplit('@', 1)
host, toplevel = domainname.rsplit('.', 1)
except ValueError:
return False # Address does not have enough parts.
# Check for Country code or Generic Domain.
if len(toplevel) != 2 and toplevel not in domains:
return False # Not a domain name.
for i in '-_.%+.':
localpart = localpart.replace(i, "")
for i in '-_.':
host = host.replace(i, "")
if localpart.isalnum() and host.isalnum():
return True # Email address is fine.
else:
return False # Email address has funny characters.