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:
parent
df29defbcd
commit
f83c2bddf8
222 changed files with 22304 additions and 14371 deletions
0
src/utils/__init__.py
Normal file
0
src/utils/__init__.py
Normal file
155
src/utils/ansi.py
Normal file
155
src/utils/ansi.py
Normal 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
|
||||
399
src/utils/batchprocessors.py
Normal file
399
src/utils/batchprocessors.py
Normal 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
443
src/utils/create.py
Normal 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
253
src/utils/debug.py
Normal 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
208
src/utils/gametime.py
Normal 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()
|
||||
9
src/utils/idmapper/LICENSE
Normal file
9
src/utils/idmapper/LICENSE
Normal 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
41
src/utils/idmapper/__init__.py
Executable 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
129
src/utils/idmapper/base.py
Executable 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
15
src/utils/idmapper/manager.py
Executable 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
2
src/utils/idmapper/models.py
Executable file
|
|
@ -0,0 +1,2 @@
|
|||
from django.db.models import *
|
||||
from base import SharedMemoryModel
|
||||
70
src/utils/idmapper/tests.py
Executable file
70
src/utils/idmapper/tests.py
Executable 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
69
src/utils/logger.py
Normal 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
658
src/utils/reimport.py
Normal 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
140
src/utils/reloads.py
Normal 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
167
src/utils/search.py
Normal 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
250
src/utils/utils.py
Normal 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue