Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SQLAlchemy commit changes to object modified through __dict__

I am developing a multiplayer game. When I use an object from inventory, it should update the user creature's stats with the values of the attributes of an object.

This is my code:

try:
    obj = self._get_obj_by_id(self.query['ObjectID']).first()

    # Get user's current creature
    cur_creature = self.user.get_current_creature()

    # Applying object attributes to user attributes
    for attribute in obj.attributes:
        cur_creature.__dict__[str(attribute.Name)] += attribute.Value

    dbObjs.session.commit()
except (KeyError, AttributeError) as err:
    self.query_failed(err)

Now, this doesn't commit things properly for some reason, so I tried:

cur_creature.Health  = 100
logging.warning(cur_creature.Health)
dbObjs.session.commit()

Which works, but is not very convenient (since I would need a big if statement to updated the different stats of the creature)

So I tried:

cur_creature.__dict__['Health'] = 100
logging.warning(cur_creature.Health)
dbObjs.session.commit()

I get 100 in logs, but no changes, so I tried:

cur_creature.__dict__['Health'] = 100
cur_creature.Health  = cur_creature.__dict__['Health']
logging.warning(cur_creature.Health)
dbObjs.session.commit()

Still '100' in logs, but no changes, so I tried:

cur_creature.__dict__['Health'] = 100
cur_creature.Health  = 100
logging.warning(cur_creature.Health)
dbObjs.session.commit()

Which still writes 100 in the logs, but doesn't commit changes to the database. Now, this is weird, since it only differ by the working version for the fact that it has this line on top:

cur_creature.__dict__['Health'] = 100

Summary: If I modify an attribute directly, commit works fine. Instead, if I modify an attribute through the class' dictionary, then, no matter how I modify it afterwards, it doesn't commit changes to the db.

Any ideas?

Thanks in advance

UPDATE 1:

Also, this updates Health in the db, but not Hunger:

cur_creature.__dict__['Hunger'] = 0
cur_creature.Health  = 100
cur_creature.Hunger = 0
logging.warning(cur_creature.Health)
dbObjs.session.commit()

So just accessing the dictionary is not a problem for attributes in general, but modifying an attribute through the dictionary, prevents the changes to that attributes from being committed.

Update 2:

As a temporary fix, I've overridden the function __set_item__(self) in the class Creatures:

def __setitem__(self, key, value):
    if key == "Health":
       self.Health = value
    elif key == "Hunger":
       self.Hunger = value

So that the new code for 'use object' is:

try:
    obj = self._get_obj_by_id(self.query['ObjectID']).first()

    # Get user's current creature
    cur_creature = self.user.get_current_creature()

    # Applying object attributes to user attributes
    for attribute in obj.attributes:
        cur_creature[str(attribute.Name)] += attribute.Value

    dbObjs.session.commit()
except (KeyError, AttributeError) as err:
    self.query_failed(err)

Update 3:

By having a look at the suggestions in the answers, I settled down for this solution:

In Creatures

def __setitem__(self, key, value):
    if key in self.__dict__:
       setattr(self, key, value)
    else:
       raise KeyError(key)

In the other method

 # Applying object attributes to user attributes
 for attribute in obj.attributes:
     cur_creature[str(attribute.Name)] += attribute.Value
like image 226
user1735527 Avatar asked Jun 09 '13 20:06

user1735527


2 Answers

The problem does not reside in SQLAlchemy but is due to Python's descriptors mechanism. Every Column attribute is a descriptor: this is how SQLAlchemy 'hooks' the attribute retrieval and modification to produce database requests.

Let's try with a simpler example:

class Desc(object):
    def __get__(self, obj, type=None):
        print '__get__'
    def __set__(self, obj, value):
        print '__set__'

class A(object):
    desc = Desc()

a = A()
a.desc                  # prints '__get__'
a.desc = 2              # prints '__set__'

However, if you go through a instance dictionary and set another value for 'desc', you bypass the descriptor protocol (see Invoking Descriptors):

a.__dict__['desc'] = 0  # Does not print anything !

Here, we just created a new instance attribute called 'desc' with a value of 0. The Desc.__set__ method was never called, and in your case SQLAlchemy wouldn't get a chance to 'catch' the assignment.

The solution is to use setattr, which is exactly equivalent to writing a.desc:

setattr(a, 'desc', 1)   # Prints '__set__'
like image 58
icecrime Avatar answered Oct 20 '22 07:10

icecrime


Don't use __dict__. Use getattr and setattr to modify attributes by name:

for attribute in obj.attributes:
    setattr(cur_creature,str(attribute.Name), getattr(cur_creature,str(attribute.Name)) + attribute.Value)

More info:

setattr

getattr

like image 37
korylprince Avatar answered Oct 20 '22 08:10

korylprince