From 475361ad280f13324f74c61a627338b8f4677f1f Mon Sep 17 00:00:00 2001 From: Griatch Date: Tue, 20 Sep 2011 12:37:45 +0200 Subject: [PATCH] Added functionality to Attributes to store and update dicts and lists dynamically. One side effect of this is that dicts and tuples need to be stored as custom object types which means that e.g. isintance(obj.db.mylist) == type(list) will return False. In order to do checks like this, use src.utils.utils.inherits_from() instead. The Attribute system now also supports tuples. All other iterables except dicts, lists and tuples are stored and retrieved as lists, same as before. This fixes issue 189. --- src/commands/default/system.py | 35 ++++---- src/objects/objects.py | 1 - src/server/server.py | 2 +- src/typeclasses/models.py | 154 ++++++++++++++++++++++++++++++--- 4 files changed, 161 insertions(+), 31 deletions(-) diff --git a/src/commands/default/system.py b/src/commands/default/system.py index 74fa5c5528..ec3f212304 100644 --- a/src/commands/default/system.py +++ b/src/commands/default/system.py @@ -102,22 +102,24 @@ class CmdPy(MuxCommand): Usage: @py - In this limited python environment, there are a - few variables made available to give access to - the system. + Separate multiple commands by ';'. A few variables are made + available for convenience in order to offer access to the system + (you can import more at execution time). + + Available variables in @py environment: + self, me : caller + here : caller.location + obj : dummy obj instance + script : dummy script instance + config : dummy conf instance + ObjectDB : ObjectDB class + ScriptDB : ScriptDB class + ServerConfig : ServerConfig class + inherits_from(obj, parent) : check object inheritance + + {rNote: In the wrong hands this command is a severe security risk. + It should only be accessible by trusted server admins/superusers.{n - available_vars: 'self','me' : caller - 'here' : caller.location - 'obj' : dummy obj instance - 'script': dummy script instance - 'config': dummy conf instance - 'ObjectDB' : ObjectDB class - 'ScriptDB' : ScriptDB class - 'ServerConfig' ServerConfig class - only two - variables are defined: 'self'/'me' which refers to one's - own object, and 'here' which refers to self's current - location. """ key = "@py" aliases = ["!"] @@ -141,12 +143,15 @@ class CmdPy(MuxCommand): key='testobject') conf = ServerConfig() # used to access conf values + # import useful checker + available_vars = {'self':caller, 'me':caller, 'here':caller.location, 'obj':obj, 'script':script, 'config':conf, + 'inherits_from':utils.inherits_from, 'ObjectDB':ObjectDB, 'ScriptDB':ScriptDB, 'ServerConfig':ServerConfig} diff --git a/src/objects/objects.py b/src/objects/objects.py index a0ae4dfe49..84ea9edbc8 100644 --- a/src/objects/objects.py +++ b/src/objects/objects.py @@ -472,7 +472,6 @@ class Exit(Object): for handling reloads and avoid tracebacks if this is called while the typeclass system is rebooting. """ - #print "Exit:create_exit_cmdset " class ExitCommand(command.Command): """ This is a command that simply cause the caller diff --git a/src/server/server.py b/src/server/server.py index ef6434bb39..b2883da4cb 100644 --- a/src/server/server.py +++ b/src/server/server.py @@ -149,7 +149,7 @@ class Evennia(object): from src.objects.models import ObjectDB from src.players.models import PlayerDB - print "run_init_hooks:", ObjectDB.get_all_cached_instances() + #print "run_init_hooks:", ObjectDB.get_all_cached_instances() [(o.typeclass(o), o.at_init()) for o in ObjectDB.get_all_cached_instances()] [(p.typeclass(p), p.at_init()) for p in PlayerDB.get_all_cached_instances()] diff --git a/src/typeclasses/models.py b/src/typeclasses/models.py index 3c6bce5e8e..9ed24fe0d6 100644 --- a/src/typeclasses/models.py +++ b/src/typeclasses/models.py @@ -59,11 +59,117 @@ PARENTS = { #------------------------------------------------------------ class PackedDBobject(object): - "Simple helper class for storing database object ids." + """ + Attribute helper class. + A container for storing and easily identifying database objects in + the database (which doesn't suppport storing dbobjects directly). + """ def __init__(self, ID, db_model): self.id = ID self.db_model = db_model +class PackedDict(dict): + """ + Attribute helper class. + A variant of dict that stores itself to the database when + updating one of its keys. This is called and handled by + Attribute.validate_data(). + """ + def __init__(self, db_obj): + """ + Sets up the packing dict. The db_store variable + is set by Attribute.validate_data() when returned in + order to allow custom updates to the dict. + + db_obj - the Attribute object storing this dict. + + """ + self.db_obj = db_obj + self.db_store = False + super(PackedDict, self).__init__() + def db_save(self): + "save data to Attribute, if db_store is active" + if self.db_store: + self.db_obj.value = self + def __setitem__(self, *args, **kwargs): + "Custom setitem that stores changed dict to database." + super(PackedDict, self).__setitem__(*args, **kwargs) + self.db_save() + def clear(self, *args, **kwargs): + "Custom clear" + super(PackedDict, self).clear(*args, **kwargs) + self.db_save() + def pop(self, *args, **kwargs): + "Custom pop" + super(PackedDict, self).pop(*args, **kwargs) + self.db_save() + def popitem(self, *args, **kwargs): + "Custom popitem" + super(PackedDict, self).popitem(*args, **kwargs) + self.db_save() + def update(self, *args, **kwargs): + "Custom update" + super(PackedDict, self).update(*args, **kwargs) + self.db_save() + +class PackedList(list): + """ + Attribute helper class. + A variant of list that stores itself to the database when + updating one of its keys. This is called and handled by + Attribute.validate_data(). + """ + def __init__(self, db_obj): + """ + Sets up the packing list. The db_store variable + is set by Attribute.validate_data() when returned in + order to allow custom updates to the list. + + db_obj - the Attribute object storing this dict. + + """ + self.db_obj = db_obj + self.db_store = False + super(PackedList, self).__init__() + def db_save(self): + "save data to Attribute, if db_store is active" + if self.db_store: + self.db_obj.value = self + def __setitem__(self, *args, **kwargs): + "Custom setitem that stores changed dict to database." + super(PackedList, self).__setitem__(*args, **kwargs) + self.db_save() + def append(self, *args, **kwargs): + "Custom append" + super(PackedList, self).append(*args, **kwargs) + self.db_save() + def extend(self, *args, **kwargs): + "Custom extend" + super(PackedList, self).extend(*args, **kwargs) + self.db_save() + def insert(self, *args, **kwargs): + "Custom insert" + super(PackedList, self).insert(*args, **kwargs) + self.db_save() + def remove(self, *args, **kwargs): + "Custom remove" + super(PackedList, self).remove(*args, **kwargs) + self.db_save() + def pop(self, *args, **kwargs): + "Custom pop" + super(PackedList, self).pop(*args, **kwargs) + self.db_save() + def reverse(self, *args, **kwargs): + "Custom reverse" + super(PackedList, self).reverse(*args, **kwargs) + self.db_save() + def sort(self, *args, **kwargs): + "Custom sort" + super(PackedList, self).sort(*args, **kwargs) + self.db_save() + + + class Attribute(SharedMemoryModel): """ Abstract django model. @@ -181,7 +287,7 @@ class Attribute(SharedMemoryModel): Getter. Allows for value = self.value. """ try: - return utils.to_unicode(self.validate_data(pickle.loads(utils.to_str(self.db_value)))) + return utils.to_unicode(self.validate_data(pickle.loads(utils.to_str(self.db_value)), getmode=True)) except pickle.UnpicklingError: return self.db_value #@value.setter @@ -224,20 +330,30 @@ class Attribute(SharedMemoryModel): def __unicode__(self): return u"%s(%s)" % (self.key, self.id) - def validate_data(self, item): + def validate_data(self, item, getmode=False): """ - We have to make sure to not store database objects raw, since this will - crash the system. Instead we must store their IDs and make sure to convert - back when the attribute is read back later. + We have to make sure to not store database objects raw, since + this will crash the system. Instead we must store their IDs + and make sure to convert back when the attribute is read back + later. - We handle only lists and dicts for iterables. + Due to this it's criticial that we check all iterables + recursively, converting all found database objects to a form + the database can handle. We handle lists, tuples and dicts + (and any nested combination of them) this way, all other + iterables are stored and returned as lists. + + getmode (bool) - This is relevant only to iterables; it makes sure to hide the + storage version of packed iterables by converting them to "normal" + Python types when returning. This way self.db.mylist == type(list) will + work as expected. """ - #print "in validate_data:", item + if isinstance(item, basestring): # a string is unmodified ret = item elif type(item) == PackedDBobject: - # unpack a previously packed object + # unpack a previously packed db_object try: #print "unpack:", item.id, item.db_model mclass = ContentType.objects.get(model=item.db_model).model_class() @@ -248,16 +364,24 @@ class Attribute(SharedMemoryModel): except Exception: logger.log_trace("Attribute error: %s, %s" % (item.db_model, item.id)) #TODO: Remove when stable? ret = None - elif type(item) == dict: + elif type(item) == dict or type(item) == PackedDict: # handle dictionaries - ret = {} + ret = PackedDict(self) for key, it in item.items(): ret[key] = self.validate_data(it) - elif is_iter(item): - # Note: ALL other iterables are considered to be lists! + ret.db_store = True + elif type(item) == tuple: + # handle tuples ret = [] for it in item: ret.append(self.validate_data(it)) + ret = tuple(ret) + elif is_iter(item): + # Note: ALL other iterables except dicts and tuples are stored&retrieved as lists! + ret = PackedList(self) + for it in item: + ret.append(self.validate_data(it)) + ret.db_store = True elif has_parent('django.db.models.base.Model', item) or has_parent(PARENTS['typeclass'], item): # db models must be stored as dbrefs db_model = [parent for parent, path in PARENTS.items() if has_parent(path, item)] @@ -650,7 +774,7 @@ class TypedObject(SharedMemoryModel): object.__setattr__(self, 'db_typeclass_path', tpath) object.__getattribute__(self, 'save')() object.__setattr__(self, "cached_typeclass_path", tpath) - object.__setattr__(self, "cached_typeclass", typeclass) + object.__setattr__(self, "cached_typeclass", typeclass) return typeclass elif hasattr(typeclass, '__file__'): errstring += "\n%s seems to be just the path to a module. You need" % tpath @@ -1011,7 +1135,9 @@ class TypedObject(SharedMemoryModel): if attr: return attr return object.__getattribute__(self, 'all') + val = obj.get_attribute(attrname) return obj.get_attribute(attrname) + def __setattr__(self, attrname, value): obj = object.__getattribute__(self, 'obj') obj.set_attribute(attrname, value)