Further cleanup of source; making class methods _private for clarity in the API.

This commit is contained in:
Griatch 2012-03-31 15:09:22 +02:00
parent fc156b5a54
commit c8df141e89
18 changed files with 607 additions and 588 deletions

View file

@ -4,7 +4,7 @@ Models for the comsystem.
The comsystem's main component is the Message, which
carries the actual information between two parties.
Msgs are stored in the database and usually not
deleted.
deleted.
A Msg always have one sender (a user), but can have
any number targets, both users and channels.
@ -16,7 +16,7 @@ be able to delete connections on the fly).
from django.db import models
from src.utils.idmapper.models import SharedMemoryModel
from src.comms import managers
from src.comms import managers
from src.locks.lockhandler import LockHandler
from src.utils import logger
from src.utils.utils import is_iter, to_str
@ -35,18 +35,18 @@ def obj_to_id(inp):
"""
dbref = is_dbref(inp)
if dbref:
return str(dbref)
return str(dbref)
if hasattr(inp, 'id'):
return str(inp.id)
if hasattr(inp, 'dbobj') and hasattr(inp.dbobj, 'id'):
return str(inp.dbobj.id)
return str(inp)
return str(inp)
def id_to_obj(dbref, db_model='PlayerDB'):
"""
loads from dbref to object. Uses the db_model to search
for the id.
"""
for the id.
"""
if db_model == 'PlayerDB':
from src.players.models import PlayerDB as db_model
else:
@ -55,7 +55,7 @@ def id_to_obj(dbref, db_model='PlayerDB'):
dbref = int(dbref.strip())
return db_model.objects.get(id=dbref)
except Exception:
return None
return None
#------------------------------------------------------------
#
@ -67,18 +67,18 @@ class Msg(SharedMemoryModel):
"""
A single message. This model describes all ooc messages
sent in-game, both to channels and between players.
The Msg class defines the following properties:
sender - sender of message
receivers - list of target objects for message
channels - list of channels message was sent to
message - the text being sent
date_sent - time message was sent
hide_from_sender - bool if message should be hidden from sender
hide_from_sender - bool if message should be hidden from sender
hide_from_receivers - list of receiver objects to hide message from
hide_from_channels - list of channels objects to hide message from
permissions - perm strings
"""
#
# Msg database model setup
@ -90,24 +90,24 @@ class Msg(SharedMemoryModel):
# There must always be one sender of the message.
db_sender = models.ForeignKey("players.PlayerDB", related_name='sender_set', null=True, verbose_name='sender')
# in the case of external senders, no Player object might be available
db_sender_external = models.CharField('external sender', max_length=255, null=True, blank=True,
db_sender_external = models.CharField('external sender', max_length=255, null=True, blank=True,
help_text="identifier for external sender, for example a sender over an IRC connection (i.e. someone who doesn't have an exixtence in-game).")
# The destination objects of this message. Stored as a
# comma-separated string of object dbrefs. Can be defined along
# with channels below.
db_receivers = models.CharField('receivers', max_length=255, null=True, blank=True,
help_text='comma-separated list of object dbrefs this message is aimed at.')
db_receivers = models.CharField('receivers', max_length=255, null=True, blank=True,
help_text='comma-separated list of object dbrefs this message is aimed at.')
# The channels this message was sent to. Stored as a
# comma-separated string of channel dbrefs. A message can both
# have channel targets and destination objects.
db_channels = models.CharField('channels', max_length=255, null=True, blank=True,
db_channels = models.CharField('channels', max_length=255, null=True, blank=True,
help_text='comma-separated list of channel dbrefs this message is aimed at.')
# The actual message and a timestamp. The message field
# should itself handle eventual headers etc.
# should itself handle eventual headers etc.
db_message = models.TextField('messsage')
db_date_sent = models.DateTimeField('date sent', editable=False, auto_now_add=True)
# lock storage
db_lock_storage = models.CharField('locks', max_length=512, blank=True,
db_lock_storage = models.CharField('locks', max_length=512, blank=True,
help_text='access locks on this message.')
# These are settable by senders/receivers/channels respectively.
# Stored as a comma-separated string of dbrefs. Can be used by the
@ -117,9 +117,9 @@ class Msg(SharedMemoryModel):
db_hide_from_receivers = models.CharField(max_length=255, null=True, blank=True)
db_hide_from_channels = models.CharField(max_length=255, null=True, blank=True)
# Storage of lock strings
#db_lock_storage = models.TextField(null=True)
# Database manager
#db_lock_storage = models.TextField(null=True)
# Database manager
objects = managers.MsgManager()
def __init__(self, *args, **kwargs):
@ -134,8 +134,8 @@ class Msg(SharedMemoryModel):
# @property decorators that allows to access these fields using
# normal python operations (without having to remember to save()
# etc). So e.g. a property 'attr' has a get/set/del decorator
# defined that allows the user to do self.attr = value,
# value = self.attr and del self.attr respectively (where self
# defined that allows the user to do self.attr = value,
# value = self.attr and del self.attr respectively (where self
# is the object in question).
# sender property (wraps db_sender)
@ -183,7 +183,7 @@ class Msg(SharedMemoryModel):
if is_iter(value):
value = ",".join([obj_to_id(val) for val in value])
self.db_receivers = obj_to_id(value)
self.save()
self.save()
#@receivers.deleter
def receivers_del(self):
"Deleter. Allows for del self.receivers"
@ -204,7 +204,7 @@ class Msg(SharedMemoryModel):
if is_iter(value):
value = ",".join([obj_to_id(val) for val in value])
self.db_channels = obj_to_id(value)
self.save()
self.save()
#@channels.deleter
def channels_del(self):
"Deleter. Allows for del self.channels"
@ -253,7 +253,7 @@ class Msg(SharedMemoryModel):
def hide_from_sender_set(self, value):
"Setter. Allows for self.hide_from_senders = value."
self.db_hide_from_sender = value
self.save()
self.save()
#@hide_from_sender.deleter
def hide_from_sender_del(self):
"Deleter. Allows for del self.hide_from_senders"
@ -274,7 +274,7 @@ class Msg(SharedMemoryModel):
if is_iter(value):
value = ",".join([obj_to_id(val) for val in value])
self.db_hide_from_receivers = obj_to_id(value)
self.save()
self.save()
#@hide_from_receivers.deleter
def hide_from_receivers_del(self):
"Deleter. Allows for del self.hide_from_receivers"
@ -286,7 +286,7 @@ class Msg(SharedMemoryModel):
#@property
def hide_from_channels_get(self):
"Getter. Allows for value = self.hide_from_channels. Returns a list of hide_from_channels."
if self.db_hide_from_channels:
if self.db_hide_from_channels:
return [id_to_obj(dbref) for dbref in self.db_hide_from_channels.split(',')]
return []
#@hide_from_channels.setter
@ -295,7 +295,7 @@ class Msg(SharedMemoryModel):
if is_iter(value):
value = ",".join([obj_to_id(val) for val in value])
self.db_hide_from_channels = obj_to_id(value)
self.save()
self.save()
#@hide_from_channels.deleter
def hide_from_channels_del(self):
"Deleter. Allows for del self.hide_from_channels"
@ -304,7 +304,7 @@ class Msg(SharedMemoryModel):
hide_from_channels = property(hide_from_channels_get, hide_from_channels_set, hide_from_channels_del)
# lock_storage property (wraps db_lock_storage)
#@property
#@property
def lock_storage_get(self):
"Getter. Allows for value = self.lock_storage"
return self.db_lock_storage
@ -321,15 +321,15 @@ class Msg(SharedMemoryModel):
db_model_name = "msg" # used by attributes to safely store objects
#
#
# Msg class methods
#
#
def __str__(self):
"Print text"
if self.channels:
return "%s -> %s: %s" % (self.sender.key,
", ".join([chan.key for chan in self.channels]),
if self.channels:
return "%s -> %s: %s" % (self.sender.key,
", ".join([chan.key for chan in self.channels]),
self.message)
else:
return "%s -> %s: %s" % (self.sender.key,
@ -341,7 +341,7 @@ class Msg(SharedMemoryModel):
accessing_obj - object trying to access this one
access_type - type of access sought
default - what to return if no lock of access_type was found
"""
"""
return self.locks.check(accessing_obj, access_type=access_type, default=default)
@ -353,61 +353,61 @@ class Msg(SharedMemoryModel):
class TempMsg(object):
"""
This is a non-persistent object for sending
temporary messages that will not be stored.
It mimics the "real" Msg object, but don't require
sender to be given.
This is a non-persistent object for sending
temporary messages that will not be stored.
It mimics the "real" Msg object, but don't require
sender to be given.
"""
def __init__(self, sender=None, receivers=[], channels=[], message="", permissions=[]):
self.sender = sender
self.receivers = receivers
self.receivers = receivers
self.message = message
self.permissions = permissions
self.hide_from_sender = False
self.hide_from_sender = receivers = False
self.hide_from_channels = False
self.hide_from_channels = False
#------------------------------------------------------------
#
# Channel
#
#------------------------------------------------------------
class Channel(SharedMemoryModel):
"""
This is the basis of a comm channel, only implementing
the very basics of distributing messages.
the very basics of distributing messages.
The Channel class defines the following properties:
key - main name for channel
desc - optional description of channel
aliases - alternative names for the channel
keep_log - bool if the channel should remember messages
permissions - perm strings
"""
#
# Channel database model setup
#
#
# These databse fields are all set using their corresponding properties,
# named same as the field, but withtout the db_* prefix.
# unique identifier for this channel
db_key = models.CharField('key', max_length=255, unique=True, db_index=True)
# optional description of channel
db_desc = models.CharField('description', max_length=80, blank=True, null=True)
# optional description of channel
db_desc = models.CharField('description', max_length=80, blank=True, null=True)
# aliases for the channel. These are searched by cmdhandler
# as well to determine if a command is the name of a channel.
# Several aliases are separated by commas.
db_aliases = models.CharField('aliases', max_length=255)
# Several aliases are separated by commas.
db_aliases = models.CharField('aliases', max_length=255)
# Whether this channel should remember its past messages
db_keep_log = models.BooleanField(default=True)
# Storage of lock definitions
db_lock_storage = models.CharField('locks', max_length=512, blank=True)
# Database manager
objects = managers.ChannelManager()
@ -416,15 +416,15 @@ class Channel(SharedMemoryModel):
verbose_name = "Channel"
def __init__(self, *args, **kwargs):
SharedMemoryModel.__init__(self, *args, **kwargs)
SharedMemoryModel.__init__(self, *args, **kwargs)
self.locks = LockHandler(self)
# Wrapper properties to easily set database fields. These are
# @property decorators that allows to access these fields using
# normal python operations (without having to remember to save()
# etc). So e.g. a property 'attr' has a get/set/del decorator
# defined that allows the user to do self.attr = value,
# value = self.attr and del self.attr respectively (where self
# defined that allows the user to do self.attr = value,
# value = self.attr and del self.attr respectively (where self
# is the object in question).
# key property (wraps db_key)
@ -473,14 +473,14 @@ class Channel(SharedMemoryModel):
if is_iter(value):
value = ",".join([str(val).strip().lower() for val in value])
self.db_aliases = value
self.save()
self.save()
#@aliases_del.deleter
def aliases_del(self):
"Deleter. Allows for del self.aliases"
self.db_aliases = ""
self.save()
aliases = property(aliases_get, aliases_set, aliases_del)
# keep_log property (wraps db_keep_log)
#@property
def keep_log_get(self):
@ -499,7 +499,7 @@ class Channel(SharedMemoryModel):
keep_log = property(keep_log_get, keep_log_set, keep_log_del)
# lock_storage property (wraps db_lock_storage)
#@property
#@property
def lock_storage_get(self):
"Getter. Allows for value = self.lock_storage"
return self.db_lock_storage
@ -527,11 +527,11 @@ class Channel(SharedMemoryModel):
def __str__(self):
return "Channel '%s' (%s)" % (self.key, self.desc)
def has_connection(self, player):
"""
Checks so this player is actually listening
to this channel.
to this channel.
"""
return PlayerChannelConnection.objects.has_connection(player, self)
@ -539,54 +539,54 @@ class Channel(SharedMemoryModel):
"""
Send the given message to all players connected to channel. Note that
no permission-checking is done here; it is assumed to have been
done before calling this method.
done before calling this method.
msgobj - a Msg instance or a message string. In the latter case a Msg will be created.
msgobj - a Msg instance or a message string. In the latter case a Msg will be created.
from_obj - if msgobj is not an Msg-instance, this is used to create
a message on the fly. If from_obj is None, no Msg object will
be created and the message will be sent without being logged.
"""
be created and the message will be sent without being logged.
"""
if isinstance(msgobj, basestring):
# given msgobj is a string
if from_obj:
if isinstance(from_obj, basestring):
msgobj = Msg(db_sender_external=from_obj, db_message=msgobj)
else:
msgobj = Msg(db_sender=from_obj, db_message=msgobj)
# try to use
msgobj = Msg(db_sender=from_obj, db_message=msgobj)
# try to use
msgobj.save()
msgobj.channels = [self]
msg = msgobj.message
msgobj.channels = [self]
msg = msgobj.message
else:
# this just sends a message, without any sender
# this just sends a message, without any sender
# (and without storing it in a persistent Msg object)
msg = to_str(msgobj)
else:
msg = msgobj.message
# get all players connected to this channel and send to them
for conn in Channel.objects.get_all_connections(self):
for conn in Channel.objects.get_all_connections(self):
try:
conn.player.msg(msg, from_obj)
except AttributeError:
try:
try:
conn.to_external(msg, from_obj, from_channel=self)
except Exception:
logger.log_trace("Cannot send msg to connection '%s'" % conn)
return True
return True
def tempmsg(self, message):
"""
A wrapper for sending non-persistent messages. Nothing
will be stored in the database.
will be stored in the database.
message - a Msg object or a text string.
message - a Msg object or a text string.
"""
if type(msgobj) == Msg:
# extract only the string
message = message.message
if type(msgobj) == Msg:
# extract only the string
message = message.message
return self.msg(message)
def connect_to(self, player):
"Connect the user to this channel"
if not self.access(player, 'listen'):
@ -594,7 +594,7 @@ class Channel(SharedMemoryModel):
conn = PlayerChannelConnection.objects.create_connection(player, self)
if conn:
return True
return False
return False
def disconnect_from(self, player):
"Disconnect user from this channel."
@ -611,14 +611,14 @@ class Channel(SharedMemoryModel):
accessing_obj - object trying to access this one
access_type - type of access sought
default - what to return if no lock of access_type was found
"""
"""
return self.locks.check(accessing_obj, access_type=access_type, default=default)
class PlayerChannelConnection(SharedMemoryModel):
"""
This connects a player object to a particular comm channel.
The advantage of making it like this is that one can easily
break the connection just by deleting this object.
break the connection just by deleting this object.
"""
# Player connected to a channel
@ -672,19 +672,19 @@ class PlayerChannelConnection(SharedMemoryModel):
class ExternalChannelConnection(SharedMemoryModel):
"""
This defines an external protocol connecting to
This defines an external protocol connecting to
a channel, while storing some critical info about
that connection.
that connection.
"""
# evennia channel connecting to
db_channel = models.ForeignKey(Channel, verbose_name='channel',
db_channel = models.ForeignKey(Channel, verbose_name='channel',
help_text='which channel this connection is tied to.')
# external connection identifier
db_external_key = models.CharField('external key', max_length=128,
db_external_key = models.CharField('external key', max_length=128,
help_text='external connection identifier, used by calling protocol.')
# eval-code to use when the channel tries to send a message
# to the external protocol.
db_external_send_code = models.TextField('executable code', blank=True,
# to the external protocol.
db_external_send_code = models.TextField('executable code', blank=True,
help_text='this is a custom snippet of Python code to connect the external protocol to the in-game channel.')
# custom config for the connection
db_external_config = models.TextField('external config', blank=True,
@ -693,10 +693,10 @@ class ExternalChannelConnection(SharedMemoryModel):
db_is_enabled = models.BooleanField('is enabled', default=True, help_text='turn on/off the connection.')
objects = managers.ExternalChannelConnectionManager()
class Meta:
verbose_name = "External Channel Connection"
def __str__(self):
return "%s <-> external %s" % (self.channel.key, self.db_external_key)
@ -778,7 +778,7 @@ class ExternalChannelConnection(SharedMemoryModel):
self.save()
#@is_enabled.deleter
def is_enabled_del(self):
"Deleter. Allows for del self.is_enabled. Deletes connection."
"Deleter. Allows for del self.is_enabled. Deletes connection."
self.delete()
is_enabled = property(is_enabled_get, is_enabled_set, is_enabled_del)
@ -789,23 +789,22 @@ class ExternalChannelConnection(SharedMemoryModel):
def to_channel(self, message, from_obj=None):
"Send external -> channel"
if not from_obj:
from_obj = self.external_key
from_obj = self.external_key
self.channel.msg(message, from_obj=from_obj)
def to_external(self, message, from_obj=None, from_channel=None):
"Send channel -> external"
# make sure we are not echoing back our own message to ourselves
# (this would result in a nasty infinite loop)
# make sure we are not echoing back our own message to ourselves
# (this would result in a nasty infinite loop)
if from_obj == self.external_key:
return
return
try:
# we execute the code snippet that should make it possible for the
# we execute the code snippet that should make it possible for the
# connection to contact the protocol correctly (as set by the protocol).
# Note that the code block has access to the variables here, such
# as message, from_obj and from_channel.
# as message, from_obj and from_channel.
exec(to_str(self.external_send_code))
except Exception:
logger.log_trace("Channel %s could not send to External %s" % (self.channel, self.external_key))

View file

@ -3,6 +3,7 @@ Custom manager for HelpEntry objects.
"""
from django.db import models
from src.utils import logger, utils
__all__ = ("HelpEntryManager",)
class HelpEntryManager(models.Manager):
"""

View file

@ -15,6 +15,7 @@ from src.help.manager import HelpEntryManager
from src.utils import ansi
from src.locks.lockhandler import LockHandler
from src.utils.utils import is_iter
__all__ = ("HelpEntry",)
#------------------------------------------------------------
#
@ -32,6 +33,9 @@ class HelpEntry(SharedMemoryModel):
entrytext - the actual help text
permissions - perm strings
Method:
access
"""
#
@ -78,88 +82,88 @@ class HelpEntry(SharedMemoryModel):
# key property (wraps db_key)
#@property
def key_get(self):
def __key_get(self):
"Getter. Allows for value = self.key"
return self.db_key
#@key.setter
def key_set(self, value):
def __key_set(self, value):
"Setter. Allows for self.key = value"
self.db_key = value
self.save()
#@key.deleter
def key_del(self):
def __key_del(self):
"Deleter. Allows for del self.key. Deletes entry."
self.delete()
key = property(key_get, key_set, key_del)
key = property(__key_get, __key_set, __key_del)
# help_category property (wraps db_help_category)
#@property
def help_category_get(self):
def __help_category_get(self):
"Getter. Allows for value = self.help_category"
return self.db_help_category
#@help_category.setter
def help_category_set(self, value):
def __help_category_set(self, value):
"Setter. Allows for self.help_category = value"
self.db_help_category = value
self.save()
#@help_category.deleter
def help_category_del(self):
def __help_category_del(self):
"Deleter. Allows for del self.help_category"
self.db_help_category = "General"
self.save()
help_category = property(help_category_get, help_category_set, help_category_del)
help_category = property(__help_category_get, __help_category_set, __help_category_del)
# entrytext property (wraps db_entrytext)
#@property
def entrytext_get(self):
def __entrytext_get(self):
"Getter. Allows for value = self.entrytext"
return self.db_entrytext
#@entrytext.setter
def entrytext_set(self, value):
def __entrytext_set(self, value):
"Setter. Allows for self.entrytext = value"
self.db_entrytext = value
self.save()
#@entrytext.deleter
def entrytext_del(self):
def __entrytext_del(self):
"Deleter. Allows for del self.entrytext"
self.db_entrytext = ""
self.save()
entrytext = property(entrytext_get, entrytext_set, entrytext_del)
entrytext = property(__entrytext_get, __entrytext_set, __entrytext_del)
# permissions property
#@property
def permissions_get(self):
def __permissions_get(self):
"Getter. Allows for value = self.permissions. Returns a list of permissions."
return [perm.strip() for perm in self.db_permissions.split(',')]
#@permissions.setter
def permissions_set(self, value):
def __permissions_set(self, value):
"Setter. Allows for self.permissions = value. Stores as a comma-separated string."
if is_iter(value):
value = ",".join([str(val).strip().lower() for val in value])
self.db_permissions = value
self.save()
#@permissions.deleter
def permissions_del(self):
def __permissions_del(self):
"Deleter. Allows for del self.permissions"
self.db_permissions = ""
self.save()
permissions = property(permissions_get, permissions_set, permissions_del)
permissions = property(__permissions_get, __permissions_set, __permissions_del)
# lock_storage property (wraps db_lock_storage)
#@property
def lock_storage_get(self):
def __lock_storage_get(self):
"Getter. Allows for value = self.lock_storage"
return self.db_lock_storage
#@nick.setter
def lock_storage_set(self, value):
def __lock_storage_set(self, value):
"""Saves the lock_storagetodate. This is usually not called directly, but through self.lock()"""
self.db_lock_storage = value
self.save()
#@nick.deleter
def lock_storage_del(self):
def __lock_storage_del(self):
"Deleter is disabled. Use the lockhandler.delete (self.lock.delete) instead"""
logger.log_errmsg("Lock_Storage (on %s) cannot be deleted. Use obj.lock.delete() instead." % self)
lock_storage = property(lock_storage_get, lock_storage_set, lock_storage_del)
lock_storage = property(__lock_storage_get, __lock_storage_set, __lock_storage_del)
#

View file

@ -3,13 +3,13 @@ This module provides a set of permission lock functions for use
with Evennia's permissions system.
To call these locks, make sure this module is included in the
settings tuple PERMISSION_FUNC_MODULES then define a lock on the form
'<access_type>:func(args)' and add it to the object's lockhandler.
Run the access() method of the handler to execute the lock check.
settings tuple PERMISSION_FUNC_MODULES then define a lock on the form
'<access_type>:func(args)' and add it to the object's lockhandler.
Run the access() method of the handler to execute the lock check.
Note that accessing_obj and accessed_obj can be any object type
with a lock variable/field, so be careful to not expect
a certain object type.
a certain object type.
Appendix: MUX locks
@ -22,7 +22,7 @@ available like the MUX ones. So many of these are not available in
basic Evennia, but could all be implemented easily if needed for the
individual game.
MUX Name: Affects: Effect:
MUX Name: Affects: Effect:
-------------------------------------------------------------------------------
DefaultLock: Exits: controls who may traverse the exit to
its destination.
@ -34,43 +34,43 @@ DefaultLock: Exits: controls who may traverse the exit to
Players/Things: controls who may GET the object.
Evennia: "get:<lockfunc()"
EnterLock: Players/Things: controls who may ENTER the object
Evennia:
Evennia:
GetFromLock: All but Exits: controls who may gets things from a given
location.
Evennia:
Evennia:
GiveLock: Players/Things: controls who may give the object.
Evennia:
Evennia:
LeaveLock: Players/Things: controls who may LEAVE the object.
Evennia:
Evennia:
LinkLock: All but Exits: controls who may link to the location if the
location is LINK_OK (for linking exits or
setting drop-tos) or ABODE (for setting
homes)
Evennia:
Evennia:
MailLock: Players: controls who may @mail the player.
Evennia:
Evennia:
OpenLock: All but Exits: controls who may open an exit.
Evennia:
Evennia:
PageLock: Players: controls who may page the player.
Evennia: "send:<lockfunc()>"
ParentLock: All: controls who may make @parent links to the
object.
Evennia: Typeclasses and "puppet:<lockstring()>"
ReceiveLock: Players/Things: controls who may give things to the object.
Evennia:
Evennia:
SpeechLock: All but Exits: controls who may speak in that location
Evennia:
Evennia:
TeloutLock: All but Exits: controls who may teleport out of the
location.
Evennia:
Evennia:
TportLock: Rooms/Things: controls who may teleport there
Evennia:
Evennia:
UseLock: All but Exits: controls who may USE the object, GIVE the
object money and have the PAY attributes
run, have their messages heard and possibly
acted on by LISTEN and AxHEAR, and invoke
$-commands stored on the object.
Evennia: Commands and Cmdsets.
Evennia: Commands and Cmdsets.
DropLock: All but rooms: controls who may drop that object.
Evennia:
VisibleLock: All: Controls object visibility when the object
@ -85,7 +85,7 @@ from django.conf import settings
from src.utils import search
from src.utils import utils
PERMISSION_HIERARCHY = [p.lower() for p in settings.PERMISSION_HIERARCHY]
_PERMISSION_HIERARCHY = [p.lower() for p in settings.PERMISSION_HIERARCHY]
def _to_player(accessing_obj):
"Helper function. Makes sure an accessing object is a player object"
@ -95,85 +95,85 @@ def _to_player(accessing_obj):
return accessing_obj
# lock functions
# lock functions
def true(*args, **kwargs):
"Always returns True."
return True
def all(*args, **kwargs):
return True
return True
def false(*args, **kwargs):
"Always returns False"
return False
return False
def none(*args, **kwargs):
return False
return False
def perm(accessing_obj, accessed_obj, *args, **kwargs):
"""
The basic permission-checker. Ignores case.
The basic permission-checker. Ignores case.
Usage:
Usage:
perm(<permission>)
where <permission> is the permission accessing_obj must
have in order to pass the lock. If the given permission
is part of PERMISSION_HIERARCHY, permission is also granted
to all ranks higher up in the hierarchy.
have in order to pass the lock. If the given permission
is part of _PERMISSION_HIERARCHY, permission is also granted
to all ranks higher up in the hierarchy.
"""
try:
perm = args[0].lower()
permissions = [p.lower() for p in accessing_obj.permissions]
except AttributeError, IndexError:
return False
return False
if perm in permissions:
# simplest case - we have a direct match
return True
if perm in PERMISSION_HIERARCHY:
return True
if perm in _PERMISSION_HIERARCHY:
# check if we have a higher hierarchy position
ppos = PERMISSION_HIERARCHY.index(perm)
return any(1 for hpos, hperm in enumerate(PERMISSION_HIERARCHY)
ppos = _PERMISSION_HIERARCHY.index(perm)
return any(1 for hpos, hperm in enumerate(_PERMISSION_HIERARCHY)
if hperm in permissions and hpos > ppos)
return False
return False
def perm_above(accessing_obj, accessed_obj, *args, **kwargs):
"""
Only allow objects with a permission *higher* in the permission
hierarchy than the one given. If there is no such higher rank,
hierarchy than the one given. If there is no such higher rank,
it's assumed we refer to superuser. If no hierarchy is defined,
this function has no meaning and returns False.
this function has no meaning and returns False.
"""
try:
perm = args[0].lower()
except IndexError:
return False
return False
if perm in PERMISSION_HIERARCHY:
ppos = PERMISSION_HIERARCHY.index(perm)
return any(1 for hpos, hperm in enumerate(PERMISSION_HIERARCHY)
if perm in _PERMISSION_HIERARCHY:
ppos = _PERMISSION_HIERARCHY.index(perm)
return any(1 for hpos, hperm in enumerate(_PERMISSION_HIERARCHY)
if hperm in [p.lower() for p in accessing_obj.permissions] and hpos > ppos)
return False
return False
def pperm(accessing_obj, accessed_obj, *args, **kwargs):
"""
The basic permission-checker for Player objects. Ignores case.
The basic permission-checker for Player objects. Ignores case.
Usage:
Usage:
pperm(<permission>)
where <permission> is the permission accessing_obj must
have in order to pass the lock. If the given permission
is part of PERMISSION_HIERARCHY, permission is also granted
to all ranks higher up in the hierarchy.
have in order to pass the lock. If the given permission
is part of _PERMISSION_HIERARCHY, permission is also granted
to all ranks higher up in the hierarchy.
"""
return perm(_to_player(accessing_obj), accessed_obj, *args, **kwargs)
def pperm_above(accessing_obj, accessed_obj, *args, **kwargs):
"""
Only allow Player objects with a permission *higher* in the permission
hierarchy than the one given. If there is no such higher rank,
hierarchy than the one given. If there is no such higher rank,
it's assumed we refer to superuser. If no hierarchy is defined,
this function has no meaning and returns False.
this function has no meaning and returns False.
"""
return perm_above(_to_player(accessing_obj), accessed_obj, *args, **kwargs)
@ -181,7 +181,7 @@ def dbref(accessing_obj, accessed_obj, *args, **kwargs):
"""
Usage:
dbref(3)
This lock type checks if the checking object
has a particular dbref. Note that this only
works for checking objects that are stored
@ -195,7 +195,7 @@ def dbref(accessing_obj, accessed_obj, *args, **kwargs):
return False
if hasattr(accessing_obj, 'id'):
return dbref == accessing_obj.id
return False
return False
def pdbref(accessing_obj, accessed_obj, *args, **kwargs):
"""
@ -212,7 +212,7 @@ def pid(accessing_obj, accessed_obj, *args, **kwargs):
return dbref(_to_player(accessing_obj), accessed_obj, *args, **kwargs)
# this is more efficient than multiple if ... elif statments
# this is more efficient than multiple if ... elif statments
CF_MAPPING = {'eq': lambda val1, val2: val1 == val2 or int(val1) == int(val2),
'gt': lambda val1, val2: int(val1) > int(val2),
'lt': lambda val1, val2: int(val1) < int(val2),
@ -232,7 +232,7 @@ def attr(accessing_obj, accessed_obj, *args, **kwargs):
how the value should be compared with one on accessing_obj (so
compare=gt means the accessing_obj must have a value greater than
the one given).
Searches attributes *and* properties stored on the checking
object. The first form works like a flag - if the
attribute/property exists on the object, the value is checked for
@ -240,17 +240,17 @@ def attr(accessing_obj, accessed_obj, *args, **kwargs):
attribute/property matches. Note that all retrieved values will be
converted to strings before doing the comparison.
"""
# deal with arguments
# deal with arguments
if not args:
return False
attrname = args[0].strip()
value = None
value = None
if len(args) > 1:
value = args[1].strip()
compare = 'eq'
if kwargs:
compare = kwargs.get('compare', 'eq')
def valcompare(val1, val2, typ='eq'):
"compare based on type"
try:
@ -258,20 +258,20 @@ def attr(accessing_obj, accessed_obj, *args, **kwargs):
except Exception, e:
#print e
# this might happen if we try to compare two things that cannot be compared
return False
return False
# first, look for normal properties on the object trying to gain access
# first, look for normal properties on the object trying to gain access
if hasattr(accessing_obj, attrname):
if value:
return valcompare(str(getattr(accessing_obj, attrname)), value, compare)
return valcompare(str(getattr(accessing_obj, attrname)), value, compare)
return bool(getattr(accessing_obj, attrname)) # will return Fail on False value etc
# check attributes, if they exist
# check attributes, if they exist
if (hasattr(accessing_obj, 'has_attribute') and accessing_obj.has_attribute(attrname)):
if value:
return (hasattr(accessing_obj, 'get_attribute')
return (hasattr(accessing_obj, 'get_attribute')
and valcompare(accessing_obj.get_attribute(attrname), value, compare))
return bool(accessing_obj.get_attribute(attrname)) # fails on False/None values
return False
return False
def objattr(accessing_obj, accessed_obj, *args, **kwargs):
"""
@ -280,7 +280,7 @@ def objattr(accessing_obj, accessed_obj, *args, **kwargs):
objattr(attrname, value)
objattr(attrname, value, compare=type)
Works like attr, except it looks for an attribute on
Works like attr, except it looks for an attribute on
accessing_obj.obj, if such an entity exists. Suitable
for commands.
@ -295,8 +295,8 @@ def locattr(accessing_obj, accessed_obj, *args, **kwargs):
locattr(attrname, value)
locattr(attrname, value, compare=type)
Works like attr, except it looks for an attribute on
accessing_obj.location, if such an entity exists.
Works like attr, except it looks for an attribute on
accessing_obj.location, if such an entity exists.
"""
if hasattr(accessing_obj, "location"):
@ -305,14 +305,14 @@ def locattr(accessing_obj, accessed_obj, *args, **kwargs):
def attr_eq(accessing_obj, accessed_obj, *args, **kwargs):
"""
Usage:
Usage:
attr_gt(attrname, 54)
"""
return attr(accessing_obj, accessed_obj, *args, **kwargs)
def attr_gt(accessing_obj, accessed_obj, *args, **kwargs):
"""
Usage:
Usage:
attr_gt(attrname, 54)
Only true if access_obj's attribute > the value given.
@ -320,7 +320,7 @@ def attr_gt(accessing_obj, accessed_obj, *args, **kwargs):
return attr(accessing_obj, accessed_obj, *args, **{'compare':'gt'})
def attr_ge(accessing_obj, accessed_obj, *args, **kwargs):
"""
Usage:
Usage:
attr_gt(attrname, 54)
Only true if access_obj's attribute >= the value given.
@ -328,7 +328,7 @@ def attr_ge(accessing_obj, accessed_obj, *args, **kwargs):
return attr(accessing_obj, accessed_obj, *args, **{'compare':'ge'})
def attr_lt(accessing_obj, accessed_obj, *args, **kwargs):
"""
Usage:
Usage:
attr_gt(attrname, 54)
Only true if access_obj's attribute < the value given.
@ -336,7 +336,7 @@ def attr_lt(accessing_obj, accessed_obj, *args, **kwargs):
return attr(accessing_obj, accessed_obj, *args, **{'compare':'lt'})
def attr_le(accessing_obj, accessed_obj, *args, **kwargs):
"""
Usage:
Usage:
attr_gt(attrname, 54)
Only true if access_obj's attribute <= the value given.
@ -344,7 +344,7 @@ def attr_le(accessing_obj, accessed_obj, *args, **kwargs):
return attr(accessing_obj, accessed_obj, *args, **{'compare':'le'})
def attr_ne(accessing_obj, accessed_obj, *args, **kwargs):
"""
Usage:
Usage:
attr_gt(attrname, 54)
Only true if access_obj's attribute != the value given.
@ -353,7 +353,7 @@ def attr_ne(accessing_obj, accessed_obj, *args, **kwargs):
def holds(accessing_obj, accessed_obj, *args, **kwargs):
"""
Usage:
Usage:
holds() # checks if accessed_obj or accessed_obj.obj is held by accessing_obj
holds(key/dbref) # checks if accessing_obj holds an object with given key/dbref
@ -364,50 +364,50 @@ def holds(accessing_obj, accessed_obj, *args, **kwargs):
try:
# commands and scripts don't have contents, so we are usually looking
# for the contents of their .obj property instead (i.e. the object the
# command/script is attached to).
# command/script is attached to).
contents = accessing_obj.contents
except AttributeError:
try:
try:
contents = accessing_obj.obj.contents
except AttributeError:
return False
return False
def check_holds(objid):
# helper function. Compares both dbrefs and keys/aliases.
objid = str(objid)
dbref = utils.dbref(objid)
dbref = utils.dbref(objid)
if dbref and any((True for obj in contents if obj.id == dbref)):
return True
return True
objid = objid.lower()
return any((True for obj in contents
return any((True for obj in contents
if obj.key.lower() == objid or objid in [al.lower() for al in obj.aliases]))
if args and args[0]:
return check_holds(args[0])
else:
else:
try:
if check_holds(accessed_obj.id):
#print "holds: accessed_obj.id - True"
return True
except Exception:
pass
pass
#print "holds: accessed_obj.obj.id -", hasattr(accessed_obj, "obj") and check_holds(accessed_obj.obj.id)
return hasattr(accessed_obj, "obj") and check_holds(accessed_obj.obj.id)
def superuser(*args, **kwargs):
"""
Only accepts an accesing_obj that is superuser (e.g. user #1)
Since a superuser would not ever reach this check (superusers
bypass the lock entirely), any user who gets this far cannot be a
superuser, hence we just return False. :)
"""
return False
return False
def serversetting(accessing_obj, accessed_obj, *args, **kwargs):
"""
Only returns true if the Evennia settings exists, alternatively has a certain value.
Usage:
serversetting(IRC_ENABLED)
serversetting(BASE_SCRIPT_PATH, [game.gamesrc.scripts])
@ -418,16 +418,16 @@ def serversetting(accessing_obj, accessed_obj, *args, **kwargs):
return False
if len(args) < 2:
setting = args[0]
val = "True"
val = "True"
else:
setting, val = args[0], args[1]
# convert
if val == 'True':
val = True
elif val == 'False':
val = False
val = False
elif val.isdigit():
val = int(val)
if setting in settings._wrapped.__dict__:
return settings._wrapped.__dict__[setting] == val
return False
return False

View file

@ -5,15 +5,15 @@ A lock defines access to a particular subsystem or property of
Evennia. For example, the "owner" property can be impmemented as a
lock. Or the disability to lift an object or to ban users.
A lock consists of three parts:
A lock consists of three parts:
- access_type - this defines what kind of access this lock regulates. This
just a string.
just a string.
- function call - this is one or many calls to functions that will determine
if the lock is passed or not.
- lock function(s). These are regular python functions with a special
- lock function(s). These are regular python functions with a special
set of allowed arguments. They should always return a boolean depending
on if they allow access or not.
on if they allow access or not.
# Lock function
@ -26,26 +26,26 @@ take four arguments looking like this:
The accessing object is the object wanting to gain access.
The accessed object is the object this lock resides on
args and kwargs will hold optional arguments and/or keyword arguments
args and kwargs will hold optional arguments and/or keyword arguments
to the function as a list and a dictionary respectively.
Example:
perm(accessing_obj, accessed_obj, *args, **kwargs):
"Checking if the object has a particular, desired permission"
if args:
desired_perm = args[0]
desired_perm = args[0]
return desired_perm in accessing_obj.permissions
return False
return False
Lock functions should most often be pretty general and ideally possible to
re-use and combine in various ways to build clever locks.
re-use and combine in various ways to build clever locks.
# Lock definition ("Lock string")
A lock definition is a string with a special syntax. It is added to
each object's lockhandler, making that lock available from then on.
A lock definition is a string with a special syntax. It is added to
each object's lockhandler, making that lock available from then on.
The lock definition looks like this:
@ -54,15 +54,15 @@ The lock definition looks like this:
That is, the access_type, a colon followed by calls to lock functions
combined with AND or OR. NOT negates the result of the following call.
Example:
We want to limit who may edit a particular object (let's call this access_type
Example:
We want to limit who may edit a particular object (let's call this access_type
for 'edit', it depends on what the command is looking for). We want this to
only work for those with the Permission 'Builders'. So we use our lock
function above and define it like this:
'edit:perm(Builders)'
Here, the lock-function perm() will be called with the string
'Builders' (accessing_obj and accessed_obj are added automatically,
you only need to add the args/kwargs, if any).
@ -73,17 +73,17 @@ could use AND:
'edit:perm(Builders) AND perm(GoodGuy)'
To allow EITHER Builders and GoodGuys, we replace AND with OR. perm() is just one example,
the lock function can do anything and compare any properties of the calling object to
the lock function can do anything and compare any properties of the calling object to
decide if the lock is passed or not.
'lift:attrib(very_strong) AND NOT attrib(bad_back)'
To make these work, add the string to the lockhandler of the object you want
to apply the lock to:
to apply the lock to:
obj.lockhandler.add('edit:perm(Builders)')
From then on, a command that wants to check for 'edit' access on this
From then on, a command that wants to check for 'edit' access on this
object would do something like this:
if not target_obj.lockhandler.has_perm(caller, 'edit'):
@ -93,8 +93,8 @@ All objects also has a shortcut called 'access' that is recommended to use inste
if not target_obj.access(caller, 'edit'):
caller.msg("Sorry, you cannot edit that.")
# Permissions
# Permissions
Permissions are just text strings stored in a comma-separated list on
typeclassed objects. The default perm() lock function uses them,
@ -106,8 +106,8 @@ to any other identifier you can use.
import re, inspect
from django.conf import settings
from src.utils import logger, utils
from src.utils import logger, utils
__all__ = ("LockHandler", )
#
# Exception class
#
@ -120,17 +120,17 @@ class LockException(Exception):
# Cached lock functions
#
LOCKFUNCS = {}
def cache_lockfuncs():
_LOCKFUNCS = {}
def _cache_lockfuncs():
"Updates the cache."
global LOCKFUNCS
LOCKFUNCS = {}
global _LOCKFUNCS
_LOCKFUNCS = {}
for modulepath in settings.LOCK_FUNC_MODULES:
modulepath = utils.pypath_to_realpath(modulepath)
mod = utils.mod_import(modulepath)
if mod:
if mod:
for tup in (tup for tup in inspect.getmembers(mod) if callable(tup[1])):
LOCKFUNCS[tup[0]] = tup[1]
_LOCKFUNCS[tup[0]] = tup[1]
else:
logger.log_errmsg("Couldn't load %s from PERMISSION_FUNC_MODULES." % modulepath)
@ -138,21 +138,21 @@ def cache_lockfuncs():
# pre-compiled regular expressions
#
RE_FUNCS = re.compile(r"\w+\([^)]*\)")
RE_SEPS = re.compile(r"(?<=[ )])AND(?=\s)|(?<=[ )])OR(?=\s)|(?<=[ )])NOT(?=\s)")
RE_OK = re.compile(r"%s|and|or|not")
_RE_FUNCS = re.compile(r"\w+\([^)]*\)")
_RE_SEPS = re.compile(r"(?<=[ )])AND(?=\s)|(?<=[ )])OR(?=\s)|(?<=[ )])NOT(?=\s)")
_RE_OK = re.compile(r"%s|and|or|not")
#
#
# Lock handler
# Lock handler
#
#
class LockHandler(object):
"""
This handler should be attached to all objects implementing
permission checks, under the property 'lockhandler'.
This handler should be attached to all objects implementing
permission checks, under the property 'lockhandler'.
"""
def __init__(self, obj):
@ -160,13 +160,13 @@ class LockHandler(object):
Loads and pre-caches all relevant locks and their
functions.
"""
if not LOCKFUNCS:
cache_lockfuncs()
if not _LOCKFUNCS:
_cache_lockfuncs()
self.obj = obj
self.locks = {}
self.log_obj = None
self.locks = {}
self.log_obj = None
self.no_errors = True
self.reset_flag = False
self.reset_flag = False
self._cache_locks(self.obj.lock_storage)
@ -177,7 +177,7 @@ class LockHandler(object):
def _log_error(self, message):
"Try to log errors back to object"
if self.log_obj and hasattr(self.log_obj, 'msg'):
self.log_obj.msg(message)
self.log_obj.msg(message)
elif hasattr(self.obj, 'msg'):
self.obj.msg(message)
else:
@ -192,12 +192,12 @@ class LockHandler(object):
"""
locks = {}
if not storage_lockstring:
return locks
return locks
nlocks = storage_lockstring.count(';') + 1
duplicates = 0
elist = [] # errors
elist = [] # errors
wlist = [] # warnings
for raw_lockstring in storage_lockstring.split(';'):
for raw_lockstring in storage_lockstring.split(';'):
lock_funcs = []
try:
access_type, rhs = (part.strip() for part in raw_lockstring.split(':', 1))
@ -206,42 +206,42 @@ class LockHandler(object):
return locks
# parse the lock functions and separators
funclist = RE_FUNCS.findall(rhs)
funclist = _RE_FUNCS.findall(rhs)
evalstring = rhs.replace('AND','and').replace('OR','or').replace('NOT','not')
nfuncs = len(funclist)
for funcstring in funclist:
for funcstring in funclist:
funcname, rest = (part.strip().strip(')') for part in funcstring.split('(', 1))
func = LOCKFUNCS.get(funcname, None)
func = _LOCKFUNCS.get(funcname, None)
if not callable(func):
elist.append("Lock: function '%s' is not available." % funcstring)
continue
continue
args = list(arg.strip() for arg in rest.split(',') if not '=' in arg)
kwargs = dict([arg.split('=', 1) for arg in rest.split(',') if '=' in arg])
kwargs = dict([arg.split('=', 1) for arg in rest.split(',') if '=' in arg])
lock_funcs.append((func, args, kwargs))
evalstring = evalstring.replace(funcstring, '%s')
if len(lock_funcs) < nfuncs:
evalstring = evalstring.replace(funcstring, '%s')
if len(lock_funcs) < nfuncs:
continue
try:
# purge the eval string of any superfluos items, then test it
evalstring = " ".join(RE_OK.findall(evalstring))
evalstring = " ".join(_RE_OK.findall(evalstring))
eval(evalstring % tuple(True for func in funclist))
except Exception:
elist.append("Lock: definition '%s' has syntax errors." % raw_lockstring)
continue
if access_type in locks:
if access_type in locks:
duplicates += 1
wlist.append("Lock: access type '%s' changed from '%s' to '%s' " % \
(access_type, locks[access_type][2], raw_lockstring))
locks[access_type] = (evalstring, tuple(lock_funcs), raw_lockstring)
locks[access_type] = (evalstring, tuple(lock_funcs), raw_lockstring)
if wlist and self.log_obj:
# a warning text was set, it's not an error, so only report if log_obj is available.
self._log_error("\n".join(wlist))
if elist:
# an error text was set, raise exception.
raise LockException("\n".join(elist))
# an error text was set, raise exception.
raise LockException("\n".join(elist))
self.no_errors = False
# return the gathered locks in an easily executable form
return locks
# return the gathered locks in an easily executable form
return locks
def _cache_locks(self, storage_lockstring):
"""Store data"""
@ -253,29 +253,29 @@ class LockHandler(object):
def add(self, lockstring, log_obj=None):
"""
Add a new lockstring on the form '<access_type>:<functions>'. Multiple
access types should be separated by semicolon (;).
Add a new lockstring on the form '<access_type>:<functions>'. Multiple
access types should be separated by semicolon (;).
If log_obj is given, it will be fed error information.
"""
if log_obj:
self.log_obj = log_obj
self.no_errors = True
# sanity checks
for lockdef in lockstring.split(';'):
self.no_errors = True
# sanity checks
for lockdef in lockstring.split(';'):
if not ':' in lockstring:
self._log_error("Lock: '%s' contains no colon (:)." % lockdef)
return False
return False
access_type, rhs = [part.strip() for part in lockdef.split(':', 1)]
if not access_type:
self._log_error("Lock: '%s' has no access_type (left-side of colon is empty)." % lockdef)
return False
return False
if rhs.count('(') != rhs.count(')'):
self._log_error("Lock: '%s' has mismatched parentheses." % lockdef)
return False
if not RE_FUNCS.findall(rhs):
return False
if not _RE_FUNCS.findall(rhs):
self._log_error("Lock: '%s' has no valid lock functions." % lockdef)
return False
return False
# get the lock string
storage_lockstring = self.obj.lock_storage
if storage_lockstring:
@ -285,8 +285,8 @@ class LockHandler(object):
# cache the locks will get rid of eventual doublets
self._cache_locks(storage_lockstring)
self._save_locks()
self.log_obj = None
return self.no_errors
self.log_obj = None
return self.no_errors
def get(self, access_type):
"get the lockstring of a particular type"
@ -297,8 +297,8 @@ class LockHandler(object):
if access_type in self.locks:
del self.locks[access_type]
self._save_locks()
return True
return False
return True
return False
def clear(self):
"Remove all locks"
@ -307,10 +307,10 @@ class LockHandler(object):
def reset(self):
"""
Set the reset flag, so the the lock will be re-cached at next checking.
This is usually set by @reload.
This is usually set by @reload.
"""
self.reset_flag = True
def check(self, accessing_obj, access_type, default=False, no_superuser_bypass=False):
"""
Checks a lock of the correct type by passing execution
@ -322,8 +322,8 @@ class LockHandler(object):
no_superuser_bypass - don't use this unless you really, really need to,
it makes supersusers susceptible to the lock check.
A lock is executed in the follwoing way:
A lock is executed in the follwoing way:
Parsing the lockstring, we (during cache) extract the valid
lock functions and store their function objects in the right
order along with their args/kwargs. These are now executed in
@ -340,11 +340,11 @@ class LockHandler(object):
"""
if self.reset_flag:
# rebuild cache
# rebuild cache
self._cache_locks(self.obj.lock_storage)
self.reset_flag = False
self.reset_flag = False
if (not no_superuser_bypass
if (not no_superuser_bypass
and ((hasattr(accessing_obj, 'is_superuser') and accessing_obj.is_superuser)
or (hasattr(accessing_obj, 'player') and hasattr(accessing_obj.player, 'is_superuser') and accessing_obj.player.is_superuser)
or (hasattr(accessing_obj, 'get_player') and (accessing_obj.get_player()==None or accessing_obj.get_player().is_superuser)))):
@ -353,11 +353,11 @@ class LockHandler(object):
return True
if access_type in self.locks:
# we have a lock, test it.
evalstring, func_tup, raw_string = self.locks[access_type]
# we have a lock, test it.
evalstring, func_tup, raw_string = self.locks[access_type]
# execute all lock funcs in the correct order, producing a tuple of True/False results.
true_false = tuple(bool(tup[0](accessing_obj, self.obj, *tup[1], **tup[2])) for tup in func_tup)
# the True/False tuple goes into evalstring, which combines them
# the True/False tuple goes into evalstring, which combines them
# with AND/OR/NOT in order to get the final result.
return eval(evalstring % true_false)
else:
@ -367,22 +367,22 @@ class LockHandler(object):
"""
Do a direct check against a lockstring ('atype:func()..'), without any
intermediary storage on the accessed object (this can be left
to None if the lock functions called don't access it). atype can also be
to None if the lock functions called don't access it). atype can also be
put to a dummy value since no lock selection is made.
"""
if ((hasattr(accessing_obj, 'is_superuser') and accessing_obj.is_superuser)
or (hasattr(accessing_obj, 'player') and hasattr(accessing_obj.player, 'is_superuser') and accessing_obj.player.is_superuser)
or (hasattr(accessing_obj, 'get_player') and (accessing_obj.get_player()==None or accessing_obj.get_player().is_superuser))):
return True
return True
locks = self. _parse_lockstring(lockstring)
for access_type in locks:
evalstring, func_tup, raw_string = locks[access_type]
true_false = tuple(tup[0](accessing_obj, self.obj, *tup[1], **tup[2]) for tup in func_tup)
return eval(evalstring % true_false)
def test():
# testing
def _test():
# testing
class TestObj(object):
pass
@ -390,9 +390,9 @@ def test():
import pdb
obj1 = TestObj()
obj2 = TestObj()
#obj1.lock_storage = "owner:dbref(#4);edit:dbref(#5) or perm(Wizards);examine:perm(Builders);delete:perm(Wizards);get:all()"
#obj1.lock_storage = "cmd:all();admin:id(1);listen:all();send:all()"
#obj1.lock_storage = "cmd:all();admin:id(1);listen:all();send:all()"
obj1.lock_storage = "listen:perm(Immortals)"
pdb.set_trace()

View file

@ -2,7 +2,7 @@
"""
This is part of Evennia's unittest framework, for testing
the stability and integrrity of the codebase during updates.
the stability and integrrity of the codebase during updates.
This module tests the lock functionality of Evennia.
@ -49,7 +49,7 @@ class TestLockfuncs(LockTest):
def testrun(self):
self.obj2.permissions = ['Wizards']
self.assertEquals(True, lockfuncs.true(self.obj2, self.obj1))
self.assertEquals(False, lockfuncs.false(self.obj2, self.obj1))
self.assertEquals(False, lockfuncs.false(self.obj2, self.obj1))
self.assertEquals(True, lockfuncs.perm(self.obj2, self.obj1, 'Wizards'))
self.assertEquals(True, lockfuncs.perm_above(self.obj2, self.obj1, 'Builders'))
dbref = self.obj2.dbref

View file

@ -10,9 +10,11 @@ from src.utils import utils
from src.utils.utils import to_unicode
from src.utils import logger
__all__ = ("ObjectManager",)
# Try to use a custom way to parse id-tagged multimatches.
AT_MULTIMATCH_INPUT = utils.mod_import(*settings.SEARCH_AT_MULTIMATCH_INPUT.rsplit('.', 1))
_AT_MULTIMATCH_INPUT = utils.mod_import(*settings.SEARCH_AT_MULTIMATCH_INPUT.rsplit('.', 1))
class ObjectManager(TypedObjectManager):
"""
@ -288,7 +290,7 @@ class ObjectManager(TypedObjectManager):
matches = local_and_global_search(ostring, exact=True)
if not matches:
# if we have no match, check if we are dealing with an "N-keyword" query - if so, strip it.
match_number, ostring = AT_MULTIMATCH_INPUT(ostring)
match_number, ostring = _AT_MULTIMATCH_INPUT(ostring)
if match_number != None and ostring:
# Run search again, without match number:
matches = local_and_global_search(ostring, exact=True)

View file

@ -32,7 +32,8 @@ from src.scripts.scripthandler import ScriptHandler
from src.utils import logger
from src.utils.utils import make_iter, to_unicode, to_str, mod_import
#PlayerDB = ContentType.objects.get(app_label="players", model="playerdb").model_class()
#__all__ = ("ObjAttribute", "Alias", "ObjectNick", "ObjectDB")
_AT_SEARCH_RESULT = mod_import(*settings.SEARCH_AT_RESULT.rsplit('.', 1))
@ -211,7 +212,7 @@ class ObjectDB(TypedObject):
# aliases property (wraps (db_aliases)
#@property
def aliases_get(self):
def __aliases_get(self):
"Getter. Allows for value = self.aliases"
try:
return _GA(self, "_cached_aliases")
@ -220,23 +221,23 @@ class ObjectDB(TypedObject):
_SA(self, "_cached_aliases", aliases)
return aliases
#@aliases.setter
def aliases_set(self, aliases):
def __aliases_set(self, aliases):
"Setter. Allows for self.aliases = value"
for alias in make_iter(aliases):
new_alias = Alias(db_key=alias, db_obj=self)
new_alias.save()
_SA(self, "_cached_aliases", aliases)
#@aliases.deleter
def aliases_del(self):
def __aliases_del(self):
"Deleter. Allows for del self.aliases"
for alias in Alias.objects.filter(db_obj=self):
alias.delete()
_DA(self, "_cached_aliases")
aliases = property(aliases_get, aliases_set, aliases_del)
aliases = property(__aliases_get, __aliases_set, __aliases_del)
# player property (wraps db_player)
#@property
def player_get(self):
def __player_get(self):
"""
Getter. Allows for value = self.player.
We have to be careful here since Player is also
@ -244,29 +245,29 @@ class ObjectDB(TypedObject):
"""
return _get_cache(self, "player")
#@player.setter
def player_set(self, player):
def __player_set(self, player):
"Setter. Allows for self.player = value"
if isinstance(player, TypeClass):
player = player.dbobj
_set_cache(self, "player", player)
#@player.deleter
def player_del(self):
def __player_del(self):
"Deleter. Allows for del self.player"
self.db_player = None
self.save()
_del_cache(self, "player")
player = property(player_get, player_set, player_del)
player = property(__player_get, __player_set, __player_del)
# location property (wraps db_location)
#@property
def location_get(self):
def __location_get(self):
"Getter. Allows for value = self.location."
loc = _get_cache(self, "location")
if loc:
return loc.typeclass
return None
#@location.setter
def location_set(self, location):
def __location_set(self, location):
"Setter. Allows for self.location = location"
try:
if location == None or type(location) == ObjectDB:
@ -289,23 +290,23 @@ class ObjectDB(TypedObject):
logger.log_trace(string)
raise
#@location.deleter
def location_del(self):
def __location_del(self):
"Deleter. Allows for del self.location"
self.db_location = None
self.save()
_del_cache(self, "location")
location = property(location_get, location_set, location_del)
location = property(__location_get, __location_set, __location_del)
# home property (wraps db_home)
#@property
def home_get(self):
def __home_get(self):
"Getter. Allows for value = self.home"
home = _get_cache(self, "home")
if home:
return home.typeclass
return None
#@home.setter
def home_set(self, home):
def __home_set(self, home):
"Setter. Allows for self.home = value"
try:
if home == None or type(home) == ObjectDB:
@ -326,23 +327,23 @@ class ObjectDB(TypedObject):
logger.log_trace(string)
#raise
#@home.deleter
def home_del(self):
def __home_del(self):
"Deleter. Allows for del self.home."
self.db_home = None
self.save()
_del_cache(self, "home")
home = property(home_get, home_set, home_del)
home = property(__home_get, __home_set, __home_del)
# destination property (wraps db_destination)
#@property
def destination_get(self):
def __destination_get(self):
"Getter. Allows for value = self.destination."
dest = _get_cache(self, "destination")
if dest:
return dest.typeclass
return None
#@destination.setter
def destination_set(self, destination):
def __destination_set(self, destination):
"Setter. Allows for self.destination = destination"
try:
if destination == None or type(destination) == ObjectDB:
@ -365,33 +366,33 @@ class ObjectDB(TypedObject):
logger.log_trace(string)
raise
#@destination.deleter
def destination_del(self):
def __destination_del(self):
"Deleter. Allows for del self.destination"
self.db_destination = None
self.save()
_del_cache(self, "destination")
destination = property(destination_get, destination_set, destination_del)
destination = property(__destination_get, __destination_set, __destination_del)
# cmdset_storage property.
# This seems very sensitive to caching, so leaving it be for now. /Griatch
#@property
def cmdset_storage_get(self):
def __cmdset_storage_get(self):
"Getter. Allows for value = self.name. Returns a list of cmdset_storage."
if self.db_cmdset_storage:
return [path.strip() for path in self.db_cmdset_storage.split(',')]
return []
#@cmdset_storage.setter
def cmdset_storage_set(self, value):
def __cmdset_storage_set(self, value):
"Setter. Allows for self.name = value. Stores as a comma-separated string."
value = ",".join(str(val).strip() for val in make_iter(value))
self.db_cmdset_storage = value
self.save()
#@cmdset_storage.deleter
def cmdset_storage_del(self):
def __cmdset_storage_del(self):
"Deleter. Allows for del self.name"
self.db_cmdset_storage = ""
self.save()
cmdset_storage = property(cmdset_storage_get, cmdset_storage_set, cmdset_storage_del)
cmdset_storage = property(__cmdset_storage_get, __cmdset_storage_set, __cmdset_storage_del)
class Meta:
"Define Django meta options"
@ -409,7 +410,7 @@ class ObjectDB(TypedObject):
_default_typeclass_path = settings.BASE_OBJECT_TYPECLASS or "src.objects.objects.Object"
#@property
def sessions_get(self):
def __sessions_get(self):
"""
Retrieve sessions connected to this object.
"""
@ -417,42 +418,43 @@ class ObjectDB(TypedObject):
if self.player:
return self.player.sessions
return []
sessions = property(sessions_get)
sessions = property(__sessions_get)
#@property
def has_player_get(self):
def __has_player_get(self):
"""
Convenience function for checking if an active player is
currently connected to this object
"""
return any(self.sessions)
has_player = property(has_player_get)
is_player = property(has_player_get)
has_player = property(__has_player_get)
is_player = property(__has_player_get)
#@property
def is_superuser_get(self):
def __is_superuser_get(self):
"Check if user has a player, and if so, if it is a superuser."
return any(self.sessions) and self.player.is_superuser
is_superuser = property(is_superuser_get)
is_superuser = property(__is_superuser_get)
#@property
def contents_get(self, exclude=None):
"""
Returns the contents of this object, i.e. all
objects that has this object set as its location.
This should be publically available.
"""
return ObjectDB.objects.get_contents(self, excludeobj=exclude)
contents = property(contents_get)
#@property
def exits_get(self):
def __exits_get(self):
"""
Returns all exits from this object, i.e. all objects
at this location having the property destination != None.
"""
return [exi for exi in self.contents
if exi.destination]
exits = property(exits_get)
exits = property(__exits_get)
#

View file

@ -17,6 +17,7 @@ they control by simply linking to a new object's user property.
from src.typeclasses.typeclass import TypeClass
from src.commands import cmdset, command
__all__ = ("Object", "Character", "Room", "Exit")
#
# Base class to inherit from.
@ -24,11 +25,10 @@ from src.commands import cmdset, command
class Object(TypeClass):
"""
This is the base class for all in-game objects.
Inherit from this to create different types of
objects in the game.
This is the base class for all in-game objects. Inherit from this
to create different types of objects in the game.
"""
# __init__ is only defined here in order to present docstring to API.
def __init__(self, dbobj):
"""
This is the root typeclass object, implementing an in-game Evennia

View file

@ -2,12 +2,13 @@
The managers for the custom Player object and permissions.
"""
import datetime
import datetime
from functools import update_wrapper
from django.db import models
from django.contrib.auth.models import User
from src.typeclasses.managers import returns_typeclass_list, returns_typeclass, TypedObjectManager
from src.utils import logger
__all__ = ("PlayerManager",)
#
# Player Manager
@ -35,16 +36,16 @@ def returns_player_list(method):
players.append(user.get_profile())
except Exception:
# there is something wrong with get_profile. But
# there is a 1-1 relation between Users-Players, so we
# there is a 1-1 relation between Users-Players, so we
# try to go the other way instead.
from src.players.models import PlayerDB
match = PlayerDB.objects.filter(user=user)
if match:
players.append(match[0])
else:
logger.log_trace("No connection User<->Player, maybe database was partially reset?")
logger.log_trace("No connection User<->Player, maybe database was partially reset?")
return players
return update_wrapper(func, method)
return update_wrapper(func, method)
def returns_player(method):
"""
@ -57,18 +58,18 @@ def returns_player(method):
if match:
return match[0]
else:
return None
return None
return update_wrapper(func, method)
class PlayerManager(TypedObjectManager):
"""
This PlayerManager implements methods for searching
This PlayerManager implements methods for searching
and manipulating Players directly from the database.
Evennia-specific search methods (will return Characters if
possible or a Typeclass/list of Typeclassed objects, whereas
Django-general methods will return Querysets or database objects):
dbref (converter)
dbref_search
get_dbref_range
@ -83,14 +84,14 @@ class PlayerManager(TypedObjectManager):
get_player_from_name
player_search (equivalent to ev.search_player)
swap_character
"""
def num_total_players(self):
"""
Returns the total number of registered users/players.
"""
return self.count()
@returns_typeclass_list
def get_connected_players(self):
"""
@ -99,7 +100,7 @@ class PlayerManager(TypedObjectManager):
return [player for player in self.all() if player.sessions]
@returns_typeclass_list
@returns_player_list
@returns_player_list
def get_recently_created_players(self, days=7):
"""
Returns a QuerySet containing the player User accounts that have been
@ -145,8 +146,8 @@ class PlayerManager(TypedObjectManager):
players = self.filter(user__username=uname)
if players:
return players[0]
return None
return None
# @returns_typeclass_list
# def get_players_with_perm(self, permstring):
# """
@ -159,7 +160,7 @@ class PlayerManager(TypedObjectManager):
# @returns_typeclass_list
# def get_players_with_group(self, groupstring):
# """
# Returns all players belonging to the given group.
# Returns all players belonging to the given group.
# """
# return [player.user for player in self.all()
# if player.has_group(groupstring)]
@ -167,9 +168,9 @@ class PlayerManager(TypedObjectManager):
@returns_typeclass_list
def player_search(self, ostring):
"""
Searches for a particular player by name or
Searches for a particular player by name or
database id.
ostring = a string or database id.
"""
ostring = ostring.lstrip("*")
@ -178,7 +179,7 @@ class PlayerManager(TypedObjectManager):
matches = self.filter(id=dbref)
if matches:
return matches
return self.filter(user__username__iexact=ostring)
return self.filter(user__username__iexact=ostring)
def swap_character(self, player, new_character, delete_old_character=False):
"""
@ -192,7 +193,7 @@ class PlayerManager(TypedObjectManager):
return False
# do the swap
old_character = player.character
old_character = player.character
if old_character:
old_character.player = None
try:
@ -202,8 +203,7 @@ class PlayerManager(TypedObjectManager):
# recover old setup
old_character.player = player
player.character = old_character
return False
return False
if delete_old_character:
old_character.delete()
return True
return True

View file

@ -53,6 +53,8 @@ from src.utils import logger, utils
from src.commands.cmdsethandler import CmdSetHandler
from src.commands import cmdhandler
__all__ = ("PlayerAttribute", "PlayerNick", "PlayerDB")
_AT_SEARCH_RESULT = utils.mod_import(*settings.SEARCH_AT_RESULT.rsplit('.', 1))
#------------------------------------------------------------

View file

@ -1,10 +1,10 @@
"""
Typeclass for Player objects
Note that this object is primarily intended to
store OOC information, not game info! This
Note that this object is primarily intended to
store OOC information, not game info! This
object represents the actual user (not their
character) and has NO actual precence in the
character) and has NO actual precence in the
game world (this is handled by the associated
character object, so you should customize that
instead for most things).
@ -12,12 +12,12 @@ instead for most things).
"""
from django.conf import settings
from src.typeclasses.typeclass import TypeClass
__all__ = ("Player",)
CMDSET_OOC = settings.CMDSET_OOC
class Player(TypeClass):
"""
Base typeclass for all Players.
Base typeclass for all Players.
"""
def __init__(self, dbobj):
"""
@ -27,7 +27,7 @@ class Player(TypeClass):
can connect to a Character Object in order to "enter" the
game.
Player Typeclass API:
Player Typeclass API:
* Available properties (only available on initiated typeclass objects)
@ -38,22 +38,22 @@ class Player(TypeClass):
dbobj (Player, read-only) - link to database model. dbobj.typeclass points back to this class
typeclass (Player, read-only) - this links back to this class as an identified only. Use self.swap_typeclass() to switch.
date_created (string) - time stamp of object creation
permissions (list of strings) - list of permission strings
permissions (list of strings) - list of permission strings
user (User, read-only) - django User authorization object
obj (Object) - game object controlled by player. 'character' can also be used.
sessions (list of Sessions) - sessions connected to this player
is_superuser (bool, read-only) - if the connected user is a superuser
* Handlers
* Handlers
locks - lock-handler: use locks.add() to add new lock strings
db - attribute-handler: store/retrieve database attributes on this self.db.myattr=val, val=self.db.myattr
ndb - non-persistent attribute handler: same as db but does not create a database entry when storing data
ndb - non-persistent attribute handler: same as db but does not create a database entry when storing data
scripts - script-handler. Add new scripts to object with scripts.add()
cmdset - cmdset-handler. Use cmdset.add() to add new cmdsets to object
nicks - nick-handler. New nicks with nicks.add().
* Helper methods
msg(outgoing_string, from_obj=None, data=None)
@ -68,12 +68,12 @@ class Player(TypeClass):
* Hook methods
basetype_setup()
at_player_creation()
at_player_creation()
- note that the following hooks are also found on Objects and are
usually handled on the character level:
at_init()
at_init()
at_cmdset_get()
at_first_login()
at_post_login()
@ -86,11 +86,11 @@ class Player(TypeClass):
"""
super(Player, self).__init__(dbobj)
## methods inherited from database model
## methods inherited from database model
def msg(self, outgoing_string, from_obj=None, data=None):
"""
Evennia -> User
"""
Evennia -> User
This is the main route for sending data back to the user from the server.
outgoing_string (string) - text data to send
@ -106,7 +106,7 @@ class Player(TypeClass):
new_character (Object) - character/object to swap to
delete_old_character (bool) - delete the old character when swapping
Returns: True/False depending on if swap suceeded or not.
"""
return self.dbobj.swap_character(new_character, delete_old_character=delete_old_character)
@ -117,44 +117,44 @@ class Player(TypeClass):
lets its typeclass execute the command. Evennia also calls
this method whenever the player sends a command on the command line.
Argument:
Argument:
raw_string (string) - raw command input
Returns Deferred - this is an asynchronous Twisted object that will
not fire until the command has actually finished executing. To overload
this one needs to attach callback functions to it, with addCallback(function).
this one needs to attach callback functions to it, with addCallback(function).
This function will be called with an eventual return value from the command
execution.
execution.
This return is not used at all by Evennia by default, but might be useful
for coders intending to implement some sort of nested command structure.
for coders intending to implement some sort of nested command structure.
"""
self.dbobj.execute_cmd(raw_string)
def search(self, ostring, global_search=False, attribute_name=None, use_nicks=False,
def search(self, ostring, global_search=False, attribute_name=None, use_nicks=False,
location=None, ignore_errors=False, player=False):
"""
This method mimicks object.search if self.character is set. Otherwise only
other Players can be searched with this method.
other Players can be searched with this method.
"""
self.dbobj.search(ostring, global_search=global_search, attribute_name=attribute_name, use_nicks=use_nicks,
self.dbobj.search(ostring, global_search=global_search, attribute_name=attribute_name, use_nicks=use_nicks,
location=location, ignore_errors=ignore_errors, player=player)
def is_typeclass(self, typeclass, exact=False):
"""
Returns true if this object has this type
OR has a typeclass which is an subclass of
the given typeclass.
typeclass - can be a class object or the
python path to such an object to match against.
python path to such an object to match against.
exact - returns true only if the object's
type is exactly this typeclass, ignoring
parents.
Returns: Boolean
"""
Returns: Boolean
"""
return self.dbobj.is_typeclass(typeclass, exact=exact)
def swap_typeclass(self, new_typeclass, clean_attributes=False, no_default=True):
@ -162,18 +162,18 @@ class Player(TypeClass):
This performs an in-situ swap of the typeclass. This means
that in-game, this object will suddenly be something else.
Player will not be affected. To 'move' a player to a different
object entirely (while retaining this object's type), use
object entirely (while retaining this object's type), use
self.player.swap_object().
Note that this might be an error prone operation if the
Note that this might be an error prone operation if the
old/new typeclass was heavily customized - your code
might expect one and not the other, so be careful to
might expect one and not the other, so be careful to
bug test your code if using this feature! Often its easiest
to create a new object and just swap the player over to
that one instead.
that one instead.
Arguments:
new_typeclass (path/classobj) - type to switch to
Arguments:
new_typeclass (path/classobj) - type to switch to
clean_attributes (bool/list) - will delete all attributes
stored on this object (but not any
of the database fields such as name or
@ -185,10 +185,10 @@ class Player(TypeClass):
no_default - if this is active, the swapper will not allow for
swapping to a default typeclass in case the given
one fails for some reason. Instead the old one
will be preserved.
Returns:
will be preserved.
Returns:
boolean True/False depending on if the swap worked or not.
"""
self.dbobj.swap_typeclass(new_typeclass, clean_attributes=clean_attributes, no_default=no_default)
@ -200,52 +200,52 @@ class Player(TypeClass):
accessing_obj (Object)- object trying to access this one
access_type (string) - type of access sought
default (bool) - what to return if no lock of access_type was found
"""
"""
return self.dbobj.access(accessing_obj, access_type=access_type, default=default)
def check_permstring(self, permstring):
"""
This explicitly checks the given string against this object's
'permissions' property without involving any locks.
permstring (string) - permission string that need to match a permission on the object.
(example: 'Builders')
"""
return self.dbobj.check_permstring(permstring)
## player hooks
def basetype_setup(self):
"""
This sets up the basic properties for a player.
This sets up the basic properties for a player.
Overload this with at_player_creation rather than
changing this method.
changing this method.
"""
# the text encoding to use.
self.db.encoding = "utf-8"
# A basic security setup
self.locks.add("examine:perm(Wizards)")
self.locks.add("edit:perm(Wizards)")
self.locks.add("delete:perm(Wizards)")
self.locks.add("boot:perm(Wizards)")
self.locks.add("boot:perm(Wizards)")
self.locks.add("msg:all()")
# The ooc player cmdset
# The ooc player cmdset
self.cmdset.add_default(CMDSET_OOC, permanent=True)
self.cmdset.outside_access = False
self.cmdset.outside_access = False
def at_player_creation(self):
"""
This is called once, the very first time
the player is created (i.e. first time they
register with the game). It's a good place
to store attributes all players should have,
like configuration values etc.
"""
like configuration values etc.
"""
pass
def at_init(self):
"""
@ -257,7 +257,7 @@ class Player(TypeClass):
happens the moment the player logs in or reconnects after a
reload.
"""
pass
pass
# Note that the hooks below also exist in the character object's
# typeclass. You can often ignore these and rely on the character
@ -292,7 +292,7 @@ class Player(TypeClass):
"""
Called at the end of the login
process, just before letting
them loose.
them loose.
"""
pass
@ -307,29 +307,29 @@ class Player(TypeClass):
"""
Called when any text is emitted to this
object. If it returns False, no text
will be sent automatically.
will be sent automatically.
"""
return True
return True
def at_message_send(self, message, to_object):
"""
Called whenever this object tries to send text
to another object. Only called if the object supplied
itself as a sender in the msg() call.
itself as a sender in the msg() call.
"""
pass
pass
def at_server_reload(self):
"""
This hook is called whenever the server is shutting down for restart/reboot.
This hook is called whenever the server is shutting down for restart/reboot.
If you want to, for example, save non-persistent properties across a restart,
this is the place to do it.
this is the place to do it.
"""
pass
def at_server_shutdown(self):
"""
This hook is called whenever the server is shutting down fully (i.e. not for
a restart).
This hook is called whenever the server is shutting down fully (i.e. not for
a restart).
"""
pass

View file

@ -4,18 +4,19 @@ The custom manager for Scripts.
from src.typeclasses.managers import TypedObjectManager
from src.typeclasses.managers import returns_typeclass_list
__all__ = ("ScriptManager",)
VALIDATE_ITERATION = 0
class ScriptManager(TypedObjectManager):
"""
This Scriptmanager implements methods for searching
This Scriptmanager implements methods for searching
and manipulating Scripts directly from the database.
Evennia-specific search methods (will return Typeclasses or
lists of Typeclasses, whereas Django-general methods will return
Querysets or database objects).
Querysets or database objects).
dbref (converter)
dbref_search
get_dbref_range
@ -38,15 +39,15 @@ class ScriptManager(TypedObjectManager):
if not obj:
return []
scripts = self.filter(db_obj=obj)
if key:
if key:
return scripts.filter(db_key=key)
return scripts
return scripts
@returns_typeclass_list
def get_all_scripts(self, key=None):
"""
Return all scripts, alternative only
scripts with a certain key/dbref or path.
scripts with a certain key/dbref or path.
"""
if key:
dbref = self.dbref(key)
@ -66,7 +67,7 @@ class ScriptManager(TypedObjectManager):
This stops and deletes a specific script directly
from the script database. This might be
needed for global scripts not tied to
a specific game object.
a specific game object.
"""
scripts = self.get_id(dbref)
for script in scripts:
@ -76,7 +77,7 @@ class ScriptManager(TypedObjectManager):
"""
This cleans up the script database of all non-persistent
scripts, or only those on obj. It is called every time the server restarts
and
and
"""
if obj:
to_stop = self.filter(db_persistent=False, db_obj=obj)
@ -86,70 +87,70 @@ class ScriptManager(TypedObjectManager):
for script in to_stop.filter(db_is_active=True):
script.stop()
for script in to_stop.filter(db_is_active=False):
script.delete()
script.delete()
return nr_deleted
def validate(self, scripts=None, obj=None, key=None, dbref=None,
def validate(self, scripts=None, obj=None, key=None, dbref=None,
init_mode=False):
"""
This will step through the script database and make sure
all objects run scripts that are still valid in the context
they are in. This is called by the game engine at regular
intervals but can also be initiated by player scripts.
intervals but can also be initiated by player scripts.
If key and/or obj is given, only update the related
script/object.
Only one of the arguments are supposed to be supplied
at a time, since they are exclusive to each other.
scripts = a list of scripts objects obtained somewhere.
obj = validate only scripts defined on a special object.
key = validate only scripts with a particular key
dbref = validate only the single script with this particular id.
dbref = validate only the single script with this particular id.
init_mode - This is used during server upstart and can have
three values:
three values:
False (no init mode). Called during run.
"reset" - server reboot. Kill non-persistent scripts
"reload" - server reload. Keep non-persistent scripts.
This method also makes sure start any scripts it validates,
this should be harmless, since already-active scripts
have the property 'is_running' set and will be skipped.
have the property 'is_running' set and will be skipped.
"""
# we store a variable that tracks if we are calling a
# validation from within another validation (avoids
# loops).
# we store a variable that tracks if we are calling a
# validation from within another validation (avoids
# loops).
global VALIDATE_ITERATION
global VALIDATE_ITERATION
if VALIDATE_ITERATION > 0:
# we are in a nested validation. Exit.
VALIDATE_ITERATION -= 1
return None, None
return None, None
VALIDATE_ITERATION += 1
# not in a validation - loop. Validate as normal.
nr_started = 0
nr_stopped = 0
nr_stopped = 0
if init_mode:
if init_mode == 'reset':
# special mode when server starts or object logs in.
# This deletes all non-persistent scripts from database
# special mode when server starts or object logs in.
# This deletes all non-persistent scripts from database
nr_stopped += self.remove_non_persistent(obj=obj)
# turn off the activity flag for all remaining scripts
scripts = self.get_all_scripts()
for script in scripts:
script.dbobj.is_active = False
script.dbobj.is_active = False
elif not scripts:
# normal operation
# normal operation
if dbref and self.dbref(dbref):
scripts = self.get_id(dbref)
elif obj:
scripts = self.get_all_scripts_on_obj(obj, key=key)
scripts = self.get_all_scripts_on_obj(obj, key=key)
else:
scripts = self.get_all_scripts(key=key) #self.model.get_all_cached_instances()
@ -158,46 +159,46 @@ class ScriptManager(TypedObjectManager):
VALIDATE_ITERATION -= 1
return None, None
#print "scripts to validate: [%s]" % (", ".join(script.key for script in scripts))
#print "scripts to validate: [%s]" % (", ".join(script.key for script in scripts))
for script in scripts:
#print "validating %s (%i) (init_mode=%s)" % (script.key, id(script.dbobj), init_mode)
if script.is_valid():
nr_started += script.start(force_restart=init_mode)
#print "validating %s (%i) (init_mode=%s)" % (script.key, id(script.dbobj), init_mode)
if script.is_valid():
nr_started += script.start(force_restart=init_mode)
#print "back from start. nr_started=", nr_started
else:
script.stop()
nr_stopped += 1
VALIDATE_ITERATION -= 1
return nr_started, nr_stopped
@returns_typeclass_list
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.
on a timer.
"""
ostring = ostring.strip()
dbref = self.dbref(ostring)
if dbref:
# this is a dbref, try to find the script directly
dbref_match = self.dbref_search(dbref)
if dbref_match:
ok = True
ok = True
if obj and obj != dbref_match.obj:
ok = False
if only_timed and dbref_match.interval:
ok = False
ok = False
if ok:
return [dbref_match]
# not a dbref; normal search
scripts = self.filter(db_key__iexact=ostring)
if obj:
scripts = scripts.exclude(db_obj=None).filter(db_obj__db_key__iexact=ostring)
if only_timed:

View file

@ -30,6 +30,8 @@ from src.typeclasses.models import Attribute, TypedObject
from django.contrib.contenttypes.models import ContentType
from src.scripts.manager import ScriptManager
__all__ = ("ScriptAttribute", "ScriptDB")
#------------------------------------------------------------
#
# ScriptAttribute
@ -121,122 +123,122 @@ class ScriptDB(TypedObject):
# desc property (wraps db_desc)
#@property
def desc_get(self):
def __desc_get(self):
"Getter. Allows for value = self.desc"
return self.db_desc
#@desc.setter
def desc_set(self, value):
def __desc_set(self, value):
"Setter. Allows for self.desc = value"
self.db_desc = value
self.save()
#@desc.deleter
def desc_del(self):
def __desc_del(self):
"Deleter. Allows for del self.desc"
self.db_desc = ""
self.save()
desc = property(desc_get, desc_set, desc_del)
desc = property(__desc_get, __desc_set, __desc_del)
# obj property (wraps db_obj)
#@property
def obj_get(self):
def __obj_get(self):
"Getter. Allows for value = self.obj"
return self.db_obj
#@obj.setter
def obj_set(self, value):
def __obj_set(self, value):
"Setter. Allows for self.obj = value"
self.db_obj = value
self.save()
#@obj.deleter
def obj_del(self):
def __obj_del(self):
"Deleter. Allows for del self.obj"
self.db_obj = None
self.save()
obj = property(obj_get, obj_set, obj_del)
obj = property(__obj_get, __obj_set, __obj_del)
# interval property (wraps db_interval)
#@property
def interval_get(self):
def __interval_get(self):
"Getter. Allows for value = self.interval"
return self.db_interval
#@interval.setter
def interval_set(self, value):
def __interval_set(self, value):
"Setter. Allows for self.interval = value"
self.db_interval = int(value)
self.save()
#@interval.deleter
def interval_del(self):
def __interval_del(self):
"Deleter. Allows for del self.interval"
self.db_interval = 0
self.save()
interval = property(interval_get, interval_set, interval_del)
interval = property(__interval_get, __interval_set, __interval_del)
# start_delay property (wraps db_start_delay)
#@property
def start_delay_get(self):
def __start_delay_get(self):
"Getter. Allows for value = self.start_delay"
return self.db_start_delay
#@start_delay.setter
def start_delay_set(self, value):
def __start_delay_set(self, value):
"Setter. Allows for self.start_delay = value"
self.db_start_delay = value
self.save()
#@start_delay.deleter
def start_delay_del(self):
def __start_delay_del(self):
"Deleter. Allows for del self.start_delay"
self.db_start_delay = False
self.save()
start_delay = property(start_delay_get, start_delay_set, start_delay_del)
start_delay = property(__start_delay_get, __start_delay_set, __start_delay_del)
# repeats property (wraps db_repeats)
#@property
def repeats_get(self):
def __repeats_get(self):
"Getter. Allows for value = self.repeats"
return self.db_repeats
#@repeats.setter
def repeats_set(self, value):
def __repeats_set(self, value):
"Setter. Allows for self.repeats = value"
self.db_repeats = int(value)
self.save()
#@repeats.deleter
def repeats_del(self):
def __repeats_del(self):
"Deleter. Allows for del self.repeats"
self.db_repeats = 0
self.save()
repeats = property(repeats_get, repeats_set, repeats_del)
repeats = property(__repeats_get, __repeats_set, __repeats_del)
# persistent property (wraps db_persistent)
#@property
def persistent_get(self):
def __persistent_get(self):
"Getter. Allows for value = self.persistent"
return self.db_persistent
#@persistent.setter
def persistent_set(self, value):
def __persistent_set(self, value):
"Setter. Allows for self.persistent = value"
self.db_persistent = value
self.save()
#@persistent.deleter
def persistent_del(self):
def __persistent_del(self):
"Deleter. Allows for del self.persistent"
self.db_persistent = False
self.save()
persistent = property(persistent_get, persistent_set, persistent_del)
persistent = property(__persistent_get, __persistent_set, __persistent_del)
# is_active property (wraps db_is_active)
#@property
def is_active_get(self):
def __is_active_get(self):
"Getter. Allows for value = self.is_active"
return self.db_is_active
#@is_active.setter
def is_active_set(self, value):
def __is_active_set(self, value):
"Setter. Allows for self.is_active = value"
self.db_is_active = value
self.save()
#@is_active.deleter
def is_active_del(self):
def __is_active_del(self):
"Deleter. Allows for del self.is_active"
self.db_is_active = False
self.save()
is_active = property(is_active_get, is_active_set, is_active_del)
is_active = property(__is_active_get, __is_active_set, __is_active_del)
#
#

View file

@ -2,27 +2,29 @@
This module contains the base Script class that all
scripts are inheriting from.
It also defines a few common scripts.
It also defines a few common scripts.
"""
from time import time
from time import time
from twisted.internet.defer import maybeDeferred
from twisted.internet.task import LoopingCall
from twisted.internet import task
from twisted.internet import task
from src.server.sessionhandler import SESSIONS
from src.typeclasses.typeclass import TypeClass
from src.scripts.models import ScriptDB
from src.comms import channelhandler
from src.comms import channelhandler
from src.utils import logger
__all__ = ("Script", "DoNothing", "CheckSessions", "ValidateScripts", "ValidateChannelHandler", "AddCmdSet")
#
# Base script, inherit from Script below instead.
# Base script, inherit from Script below instead.
#
class ScriptClass(TypeClass):
"""
Base class for scripts. Don't inherit from this, inherit from Script instead.
"""
# private methods
# private methods
def __eq__(self, other):
"""
@ -32,7 +34,7 @@ class ScriptClass(TypeClass):
try:
return other.id == self.id
except Exception:
return False
return False
def _start_task(self, start_now=True):
"start task runner"
@ -43,17 +45,17 @@ class ScriptClass(TypeClass):
#print " start with paused time:", self.key, self.ndb._paused_time
self.ndb.twisted_task.start(self.ndb._paused_time, now=False)
else:
# starting script anew.
# starting script anew.
#print "_start_task: self.interval:", self.key, self.dbobj.interval
self.ndb.twisted_task.start(self.dbobj.interval, now=start_now and not self.start_delay)
self.ndb.time_last_called = int(time())
self.ndb.time_last_called = int(time())
def _stop_task(self):
"stop task runner"
try:
#print "stopping twisted task:", id(self.ndb.twisted_task), self.obj
if self.ndb.twisted_task and self.ndb.twisted_task.running:
self.ndb.twisted_task.stop()
self.ndb.twisted_task.stop()
except Exception:
logger.log_trace()
def _step_err_callback(self, e):
@ -69,14 +71,14 @@ class ScriptClass(TypeClass):
"step task runner. No try..except needed due to defer wrap."
if not self.is_valid():
self.stop()
return
return
self.at_repeat()
repeats = self.dbobj.db_repeats
if repeats <= 0:
pass # infinite repeat
elif repeats == 1:
self.stop()
return
return
else:
self.dbobj.db_repeats -= 1
self.ndb.time_last_called = int(time())
@ -84,7 +86,7 @@ class ScriptClass(TypeClass):
if self.ndb._paused_time:
# this means we were running an unpaused script, for the time remaining
# after the pause. Now we start a normal-running timer again.
# after the pause. Now we start a normal-running timer again.
#print "switching to normal run:", self.key
del self.ndb._paused_time
self._stop_task()
@ -93,23 +95,23 @@ class ScriptClass(TypeClass):
def _step_task(self):
"step task"
try:
try:
d = maybeDeferred(self._step_succ_callback)
d.addErrback(self._step_err_callback)
d.addErrback(self._step_err_callback)
return d
except Exception:
logger.log_trace()
logger.log_trace()
# Public methods
# Public methods
def time_until_next_repeat(self):
"""
Returns the time in seconds until the script will be
run again. If this is not a stepping script, returns None.
run again. If this is not a stepping script, returns None.
This is not used in any way by the script's stepping
system; it's only here for the user to be able to
check in on their scripts and when they will next be run.
check in on their scripts and when they will next be run.
"""
try:
if self.ndb._paused_time:
@ -117,7 +119,7 @@ class ScriptClass(TypeClass):
else:
return max(0, (self.ndb.time_last_called + self.dbobj.db_interval) - int(time()))
except Exception:
return None
return None
def start(self, force_restart=False):
"""
@ -125,33 +127,33 @@ class ScriptClass(TypeClass):
persistent scripts, this is usually once every server start)
force_restart - if True, will always restart the script, regardless
of if it has started before.
returns 0 or 1 to indicated the script has been started or not. Used in counting.
of if it has started before.
returns 0 or 1 to indicated the script has been started or not. Used in counting.
"""
#print "Script %s (%s) start (active:%s, force:%s) ..." % (self.key, id(self.dbobj),
# self.is_active, force_restart)
#print "Script %s (%s) start (active:%s, force:%s) ..." % (self.key, id(self.dbobj),
# self.is_active, force_restart)
if self.dbobj.is_active and not force_restart:
# script already runs and should not be restarted.
return 0
return 0
obj = self.obj
if obj:
# check so the scripted object is valid and initalized
if obj:
# check so the scripted object is valid and initalized
try:
dummy = object.__getattribute__(obj, 'cmdset')
dummy = object.__getattribute__(obj, 'cmdset')
except AttributeError:
# this means the object is not initialized.
self.dbobj.is_active = False
return 0
return 0
# try to restart a paused script
# try to restart a paused script
if self.unpause():
return 1
# try to start the script from scratch
try:
# try to start the script from scratch
try:
self.dbobj.is_active = True
self.at_start()
if self.dbobj.db_interval > 0:
@ -159,15 +161,15 @@ class ScriptClass(TypeClass):
return 1
except Exception:
logger.log_trace()
self.dbobj.is_active = False
self.dbobj.is_active = False
return 0
def stop(self, kill=False):
"""
Called to stop the script from running.
This also deletes the script.
This also deletes the script.
kill - don't call finishing hooks.
kill - don't call finishing hooks.
"""
#print "stopping script %s" % self.key
#import pdb
@ -197,29 +199,29 @@ class ScriptClass(TypeClass):
#print "pausing", self.key, self.time_until_next_repeat()
dt = self.time_until_next_repeat()
if dt == None:
return
self.db._paused_time = dt
return
self.db._paused_time = dt
self._stop_task()
def unpause(self):
"""
Restart a paused script. This WILL call at_start().
Restart a paused script. This WILL call at_start().
"""
#print "unpausing", self.key, self.db._paused_time
dt = self.db._paused_time
dt = self.db._paused_time
if dt == None:
return False
try:
self.dbobj.is_active = True
self.at_start()
self.ndb._paused_time = dt
self.ndb._paused_time = dt
self._start_task(start_now=False)
del self.db._paused_time
except Exception, e:
logger.log_trace()
self.dbobj.is_active = False
return False
return True
return True
# hooks
def at_script_creation(self):
@ -230,7 +232,7 @@ class ScriptClass(TypeClass):
pass
def at_start(self):
"placeholder."
pass
pass
def at_stop(self):
"placeholder"
pass
@ -240,7 +242,7 @@ class ScriptClass(TypeClass):
def at_init(self):
"called when typeclass re-caches. Usually not used for scripts."
pass
#
# Base Script - inherit from this
@ -255,36 +257,36 @@ class Script(ScriptClass):
def __init__(self, dbobj):
"""
This is the base TypeClass for all Scripts. Scripts describe events, timers and states in game,
they can have a time component or describe a state that changes under certain conditions.
they can have a time component or describe a state that changes under certain conditions.
Script API:
* Available properties (only available on initiated Typeclass objects)
key (string) - name of object
key (string) - name of object
name (string)- same as key
aliases (list of strings) - aliases to the object. Will be saved to database as AliasDB entries but returned as strings.
dbref (int, read-only) - unique #id-number. Also "id" can be used.
dbobj (Object, read-only) - link to database model. dbobj.typeclass points back to this class
typeclass (Object, read-only) - this links back to this class as an identified only. Use self.swap_typeclass() to switch.
date_created (string) - time stamp of object creation
permissions (list of strings) - list of permission strings
permissions (list of strings) - list of permission strings
desc (string) - optional description of script, shown in listings
obj (Object) - optional object that this script is connected to and acts on (set automatically by obj.scripts.add())
interval (int) - how often script should run, in seconds. <0 turns off ticker
obj (Object) - optional object that this script is connected to and acts on (set automatically by obj.scripts.add())
interval (int) - how often script should run, in seconds. <=0 turns off ticker
start_delay (bool) - if the script should start repeating right away or wait self.interval seconds
repeats (int) - how many times the script should repeat before stopping. 0 means infinite repeats
repeats (int) - how many times the script should repeat before stopping. <=0 means infinite repeats
persistent (bool) - if script should survive a server shutdown or not
is_active (bool) - if script is currently running
is_active (bool) - if script is currently running
* Handlers
locks - lock-handler: use locks.add() to add new lock strings
db - attribute-handler: store/retrieve database attributes on this self.db.myattr=val, val=self.db.myattr
ndb - non-persistent attribute handler: same as db but does not create a database entry when storing data
ndb - non-persistent attribute handler: same as db but does not create a database entry when storing data
* Helper methods
* Helper methods
start() - start script (this usually happens automatically at creation and obj.script.add() etc)
stop() - stop script, and delete it
@ -292,48 +294,48 @@ class Script(ScriptClass):
unpause() - restart a previously paused script. The script will continue as if it was never paused.
time_until_next_repeat() - if a timed script (interval>0), returns time until next tick
* Hook methods
* Hook methods
at_script_creation() - called only once, when an object of this
class is first created.
is_valid() - is called to check if the script is valid to be running
at the current time. If is_valid() returns False, the running
script is stopped and removed from the game. You can use this
to check state changes (i.e. an script tracking some combat
stats at regular intervals is only valid to run while there is
actual combat going on).
to check state changes (i.e. an script tracking some combat
stats at regular intervals is only valid to run while there is
actual combat going on).
at_start() - Called every time the script is started, which for persistent
scripts is at least once every server start. Note that this is
unaffected by self.delay_start, which only delays the first call
to at_repeat().
to at_repeat().
at_repeat() - Called every self.interval seconds. It will be called immediately
upon launch unless self.delay_start is True, which will delay
the first call of this method by self.interval seconds. If
self.interval==0, this method will never be called.
the first call of this method by self.interval seconds. If
self.interval<=0, this method will never be called.
at_stop() - Called as the script object is stopped and is about to be removed from
the game, e.g. because is_valid() returned False.
at_server_reload() - Called when server reloads. Can be used to save temporary
at_server_reload() - Called when server reloads. Can be used to save temporary
variables you want should survive a reload.
at_server_shutdown() - called at a full server shutdown.
at_server_shutdown() - called at a full server shutdown.
"""
"""
super(Script, self).__init__(dbobj)
def at_script_creation(self):
"""
Only called once, by the create function.
"""
self.key = "<unnamed>"
self.key = "<unnamed>"
self.desc = ""
self.interval = 0 # infinite
self.start_delay = False
self.repeats = 0 # infinite
self.persistent = False
self.persistent = False
def is_valid(self):
"""
Is called to check if the script is valid to run at this time.
Is called to check if the script is valid to run at this time.
Should return a boolean. The method is assumed to collect all needed
information from its related self.obj.
"""
@ -342,7 +344,7 @@ class Script(ScriptClass):
def at_start(self):
"""
Called whenever the script is started, which for persistent
scripts is at least once every server start. It will also be called
scripts is at least once every server start. It will also be called
when starting again after a pause (such as after a server reload)
"""
pass
@ -350,10 +352,10 @@ class Script(ScriptClass):
def at_repeat(self):
"""
Called repeatedly if this Script is set to repeat
regularly.
regularly.
"""
pass
def at_stop(self):
"""
Called whenever when it's time for this script to stop
@ -363,38 +365,38 @@ class Script(ScriptClass):
def at_server_reload(self):
"""
This hook is called whenever the server is shutting down for restart/reboot.
This hook is called whenever the server is shutting down for restart/reboot.
If you want to, for example, save non-persistent properties across a restart,
this is the place to do it.
this is the place to do it.
"""
pass
def at_server_shutdown(self):
"""
This hook is called whenever the server is shutting down fully (i.e. not for
a restart).
This hook is called whenever the server is shutting down fully (i.e. not for
a restart).
"""
pass
# Some useful default Script types used by Evennia.
# Some useful default Script types used by Evennia.
class DoNothing(Script):
"An script that does nothing. Used as default fallback."
def at_script_creation(self):
"An script that does nothing. Used as default fallback."
def at_script_creation(self):
"Setup the script"
self.key = "sys_do_nothing"
self.desc = "This is a placeholder script."
self.desc = "This is a placeholder script."
class CheckSessions(Script):
"Check sessions regularly."
def at_script_creation(self):
"Setup the script"
self.key = "sys_session_check"
self.desc = "Checks sessions so they are live."
self.interval = 60 # repeat every 60 seconds
self.persistent = True
self.desc = "Checks sessions so they are live."
self.interval = 60 # repeat every 60 seconds
self.persistent = True
def at_repeat(self):
"called every 60 seconds"
@ -403,7 +405,7 @@ class CheckSessions(Script):
SESSIONS.validate_sessions()
class ValidateScripts(Script):
"Check script validation regularly"
"Check script validation regularly"
def at_script_creation(self):
"Setup the script"
self.key = "sys_scripts_validate"
@ -412,31 +414,31 @@ class ValidateScripts(Script):
self.persistent = True
def at_repeat(self):
"called every hour"
"called every hour"
#print "ValidateScripts run."
ScriptDB.objects.validate()
class ValidateChannelHandler(Script):
"Update the channelhandler to make sure it's in sync."
"Update the channelhandler to make sure it's in sync."
def at_script_creation(self):
"Setup the script"
self.key = "sys_channels_validate"
self.desc = "Updates the channel handler"
self.desc = "Updates the channel handler"
self.interval = 3700 # validate a little later than ValidateScripts
self.persistent = True
def at_repeat(self):
"called every hour+"
#print "ValidateChannelHandler run."
channelhandler.CHANNELHANDLER.update()
class AddCmdSet(Script):
"""
This script permanently assigns a command set
to an object whenever it is started. This is not
used by the core system anymore, it's here mostly
as an example.
as an example.
"""
def at_script_creation(self):
"Setup the script"
@ -444,12 +446,12 @@ class AddCmdSet(Script):
self.key = "add_cmdset"
if not self.desc:
self.desc = "Adds a cmdset to an object."
self.persistent = True
self.persistent = True
# this needs to be assigned to upon creation.
# It should be a string pointing to the right
# cmdset module and cmdset class name, e.g.
# 'examples.cmdset_redbutton.RedButtonCmdSet'
# cmdset module and cmdset class name, e.g.
# 'examples.cmdset_redbutton.RedButtonCmdSet'
# self.db.cmdset = <cmdset_path>
# self.db.add_default = <bool>
@ -461,12 +463,12 @@ class AddCmdSet(Script):
self.obj.cmdset.add_default(cmdset)
else:
self.obj.cmdset.add(cmdset)
def at_stop(self):
"""
This removes the cmdset when the script stops
"""
cmdset = self.db.cmdset
cmdset = self.db.cmdset
if cmdset:
if self.db.add_default:
self.obj.cmdset.delete_default()

View file

@ -7,7 +7,7 @@ from functools import update_wrapper
from django.db import models
from src.utils import idmapper
from src.utils.utils import make_iter
#from src.typeclasses import idmap
__all__ = ("AttributeManager", "TypedObjectManager")
# Managers

View file

@ -43,6 +43,8 @@ from src.locks.lockhandler import LockHandler
from src.utils import logger, utils
from src.utils.utils import make_iter, is_iter, has_parent, to_unicode, to_str
__all__ = ("Attribute", "TypeNick", "TypedObject")
_PERMISSION_HIERARCHY = [p.lower() for p in settings.PERMISSION_HIERARCHY]
_CTYPEGET = ContentType.objects.get

View file

@ -13,6 +13,8 @@ used by the typesystem or django itself.
from src.utils.logger import log_trace, log_errmsg
from django.conf import settings
__all__ = ("TypeClass",)
# these are called so many times it's worth to avoid lookup calls
_GA = object.__getattribute__
_SA = object.__setattr__