Skip to content

Commit

Permalink
Merge pull request #1111 from sphuber/fix_1109_mutable_attributes
Browse files Browse the repository at this point in the history
Restore proper mutability implementation of Node attributes
  • Loading branch information
giovannipizzi authored Feb 22, 2018
2 parents fb1d25a + dbf3de9 commit 7fcea23
Show file tree
Hide file tree
Showing 20 changed files with 357 additions and 363 deletions.
49 changes: 49 additions & 0 deletions aiida/backends/djsite/db/migrations/0008_code_hidden_to_extra.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
###########################################################################
# Copyright (c), The AiiDA team. All rights reserved. #
# This file is part of the AiiDA code. #
# #
# The code is hosted on GitHub at https://github.com/aiidateam/aiida_core #
# For further information on the license, see the LICENSE.txt file #
# For further information please visit http://www.aiida.net #
###########################################################################
from __future__ import unicode_literals

from django.db import models, migrations
from aiida.backends.djsite.db.migrations import update_schema_version


SCHEMA_VERSION = "1.0.8"

class Migration(migrations.Migration):

dependencies = [
('db', '0007_update_linktypes'),
]

operations = [
# The 'hidden' property of AbstractCode has been changed from an attribute to an extra
# Therefore we find all nodes of type Code and if they have an attribute with the key 'hidden'
# we move that value to the extra table
#
# First we copy the 'hidden' attributes from code.Code. nodes to the db_extra table
migrations.RunSQL("""
INSERT INTO db_dbextra (key, datatype, tval, fval, ival, bval, dval, dbnode_id) (
SELECT db_dbattribute.key, db_dbattribute.datatype, db_dbattribute.tval, db_dbattribute.fval, db_dbattribute.ival, db_dbattribute.bval, db_dbattribute.dval, db_dbattribute.dbnode_id
FROM db_dbattribute JOIN db_dbnode ON db_dbnode.id = db_dbattribute.dbnode_id
WHERE db_dbattribute.key = 'hidden'
AND db_dbnode.type = 'code.Code.'
);
"""),
# Secondly, we delete the original entries from the DbAttribute table
migrations.RunSQL("""
DELETE FROM db_dbattribute
WHERE id in (
SELECT db_dbattribute.id
FROM db_dbattribute
JOIN db_dbnode ON db_dbnode.id = db_dbattribute.dbnode_id
WHERE db_dbattribute.key = 'hidden' AND db_dbnode.type = 'code.Code.'
);
"""),
update_schema_version(SCHEMA_VERSION)
]
2 changes: 1 addition & 1 deletion aiida/backends/djsite/db/migrations/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
###########################################################################


LATEST_MIGRATION = '0007_update_linktypes'
LATEST_MIGRATION = '0008_code_hidden_to_extra'


def _update_schema_version(version, apps, schema_editor):
Expand Down
6 changes: 1 addition & 5 deletions aiida/backends/djsite/db/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,7 @@ class DbNode(m.Model):
:note: Attributes in the DbAttribute table have to be thought as belonging
to the DbNode, (this is the reason for which there is no 'user' field
in the DbAttribute field). Moreover, Attributes define uniquely the
Node so should be immutable (except for the few ones defined in the
_updatable_attributes attribute of the Node() class, that are updatable:
these are Attributes that are set by AiiDA, so the user should not
modify them, but can be changed (e.g., the append_text of a code, that
can be redefined if the code has to be recompiled).
Node so should be immutable
"""
uuid = UUIDField(auto=True, version=AIIDANODES_UUID_VERSION, db_index=True)
# in the form data.upffile., data.structure., calculation., ...
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-
###########################################################################
# Copyright (c), The AiiDA team. All rights reserved. #
# This file is part of the AiiDA code. #
# #
# The code is hosted on GitHub at https://github.com/aiidateam/aiida_core #
# For further information on the license, see the LICENSE.txt file #
# For further information please visit http://www.aiida.net #
###########################################################################
"""Migrating 'hidden' properties from DbAttribute to DbExtra for code.Code. nodes
Revision ID: 35d4ee9a1b0e
Revises: 89176227b25
Create Date: 2018-02-21 22:00:43.460534
"""
from alembic import op
from sqlalchemy.orm.session import Session
from sqlalchemy.orm.attributes import flag_modified
from aiida.backends.sqlalchemy.models.node import DbNode


# revision identifiers, used by Alembic.
revision = '35d4ee9a1b0e'
down_revision = '89176227b25'
branch_labels = None
depends_on = None


def upgrade():
conn = op.get_bind()
session = Session(bind=conn)

# The 'hidden' property of AbstractCode has been changed from an attribute to an extra
# Therefore we find all nodes of type Code and if they have an attribute with the key 'hidden'
# we move that value to the extra field
codes = session.query(DbNode).filter(DbNode.type == 'code.Code.')
for code in codes:
if 'hidden' in code.attributes:
session.add(code)

hidden = code.attributes.pop('hidden')
code.extras['hidden'] = hidden

flag_modified(code, 'attributes')
flag_modified(code, 'extras')

session.flush()
session.commit()


def downgrade():
conn = op.get_bind()
session = Session(bind=conn)

# Reverse logic from the upgrade
codes = session.query(DbNode).filter(DbNode.type == 'code.Code.')
for code in codes:
if 'hidden' in code.extras:
session.add(code)

hidden = code.extras.pop('hidden')
code.attributes['hidden'] = hidden

flag_modified(code, 'attributes')
flag_modified(code, 'extras')

session.flush()
session.commit()
141 changes: 35 additions & 106 deletions aiida/backends/tests/calculation_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,10 @@
# For further information on the license, see the LICENSE.txt file #
# For further information please visit http://www.aiida.net #
###########################################################################
"""
Tests for calculation nodes, attributes and links
"""

from aiida.common.exceptions import ModificationNotAllowed
from aiida.backends.testbase import AiidaTestCase
from aiida.common.exceptions import ModificationNotAllowed
from aiida.orm.calculation import Calculation


class TestCalcNode(AiidaTestCase):
Expand All @@ -23,142 +21,73 @@ class TestCalcNode(AiidaTestCase):
boolval = True
intval = 123
floatval = 4.56
stringval = "aaaa"
# A recursive dictionary
dictval = {'num': 3, 'something': 'else', 'emptydict': {},
'recursive': {'a': 1, 'b': True, 'c': 1.2, 'd': [1, 2, None],
'e': {'z': 'z', 'x': None, 'xx': {}, 'yy': []}}}
listval = [1, "s", True, None]
emptydict = {}
emptylist = []

def test_updatable_not_copied(self):
stringval = 'aaaa'
listval = [1, 's', True, None]
dictval = {
'num': 3, 'something': 'else', 'emptydict': {},
'recursive': {
'a': 1, 'b': True, 'c': 1.2, 'd': [1, 2, None],
'e': {
'z': 'z', 'x': None, 'xx': {}, 'yy': []
}
}
}
stateval = 'RUNNING'

def test_calculation_updatable_not_copied(self):
"""
Checks the versioning.
Check that updatable attributes of Calculation are not copied
"""
from aiida.orm.test import myNodeWithFields

# Has 'state' as updatable attribute
a = myNodeWithFields()
a._set_attr('state', 267)
a = Calculation()
a._set_attr('state', self.stateval)
a.store()
b = a.copy()

# updatable attributes are not copied
with self.assertRaises(AttributeError):
b.get_attr('state')

def test_delete_updatable_attributes(self):
def test_calculation_updatable_attribute(self):
"""
Checks the versioning.
Check that updatable attributes and only those can be mutated for a stored but unsealed Calculation
"""
from aiida.orm.test import myNodeWithFields

# Has 'state' as updatable attribute
a = myNodeWithFields()
a = Calculation()
attrs_to_set = {
'bool': self.boolval,
'integer': self.intval,
'float': self.floatval,
'string': self.stringval,
'dict': self.dictval,
'list': self.listval,
'state': 267, # updatable
'state': self.stateval
}

for k, v in attrs_to_set.iteritems():
a._set_attr(k, v)

# Check before storing
self.assertEquals(267, a.get_attr('state'))
self.assertEquals(a.get_attr('state'), self.stateval)

a.store()

# Check after storing
self.assertEquals(267, a.get_attr('state'))
self.assertEquals(a.get_attr('state'), self.stateval)

# Even if I stored many attributes, this should stay at 1
self.assertEquals(a.dbnode.nodeversion, 1)

# I should be able to delete the attribute
# I should be able to mutate the updatable attribute but not the others
a._set_attr('state', 'FINISHED')
a._del_attr('state')

# I check increment on new version
self.assertEquals(a.dbnode.nodeversion, 2)

with self.assertRaises(AttributeError):
# I check that I cannot modify this attribute
_ = a.get_attr('state')

def test_versioning_and_updatable_attributes(self):
"""
Checks the versioning.
"""
from aiida.orm.test import myNodeWithFields

# Has 'state' as updatable attribute
a = myNodeWithFields()
attrs_to_set = {
'bool': self.boolval,
'integer': self.intval,
'float': self.floatval,
'string': self.stringval,
'dict': self.dictval,
'list': self.listval,
'state': 267,
}

expected_version = 1

for k, v in attrs_to_set.iteritems():
a._set_attr(k, v)

# Check before storing
self.assertEquals(267, a.get_attr('state'))

a.store()
with self.assertRaises(ModificationNotAllowed):
a._set_attr('bool', False)

# Even if I stored many attributes, this should stay at 1
self.assertEquals(a.dbnode.nodeversion, expected_version)
with self.assertRaises(ModificationNotAllowed):
a._del_attr('bool')

# Sealing ups the version number
a.seal()
expected_version += 1
self.assertEquals(a.dbnode.nodeversion, expected_version)

# Check after storing
self.assertEquals(267, a.get_attr('state'))
self.assertEquals(a.dbnode.nodeversion, expected_version)

# I check increment on new version
a.set_extra('a', 'b')
expected_version += 1
self.assertEquals(a.dbnode.nodeversion, expected_version)

# I check that I can set this attribute
a._set_attr('state', 999)
expected_version += 1

# I check increment on new version
self.assertEquals(a.dbnode.nodeversion, expected_version)

# After sealing, even updatable attributes should be immutable
with self.assertRaises(ModificationNotAllowed):
# I check that I cannot modify this attribute
a._set_attr('otherattribute', 222)

# I check that the counter was not incremented
self.assertEquals(a.dbnode.nodeversion, expected_version)

# In both cases, the node version must increase
a.label = 'test'
expected_version += 1
self.assertEquals(a.dbnode.nodeversion, expected_version)

a.description = 'test description'
expected_version += 1
self.assertEquals(a.dbnode.nodeversion, expected_version)
a._set_attr('state', 'FINISHED')

b = a.copy()
# updatable attributes are not copied
with self.assertRaises(AttributeError):
b.get_attr('state')
with self.assertRaises(ModificationNotAllowed):
a._del_attr('state')
Loading

0 comments on commit 7fcea23

Please sign in to comment.