Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/aiida/cmdline/commands/cmd_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ def echo_node_dict(nodes: list[Node], keys: list, fmt: str, identifier: str, raw
id_value = node.uuid # type: ignore[assignment]

if use_attrs:
node_dict = node.base.attributes.all
node_dict = node.base.attributes.get_dict()
dict_name = 'attributes'
else:
node_dict = node.base.extras.all
Expand Down
31 changes: 19 additions & 12 deletions src/aiida/orm/nodes/attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,24 +40,31 @@ def __contains__(self, key: str) -> bool:

@property
def all(self) -> Dict[str, Any]:
"""Return the complete attributes dictionary.
"""Return a deep copy of the complete attributes dictionary.

.. warning:: While the entity is unstored, this will return references of the attributes on the database model,
meaning that changes on the returned values (if they are mutable themselves, e.g. a list or dictionary) will
automatically be reflected on the database model as well. As soon as the entity is stored, the returned
attributes will be a deep copy and mutations of the database attributes will have to go through the
appropriate set methods. Therefore, once stored, retrieving a deep copy can be a heavy operation. If you
only need the keys or some values, use the iterators `keys` and `items`, or the
getters `get` and `get_many` instead.
.. deprecated:: 3.0
Use :meth:`get_dict` instead.

:return: the attributes as a dictionary
"""
attributes = self._backend_node.attributes
from aiida.common.warnings import warn_deprecation

if self._node.is_stored:
attributes = copy.deepcopy(attributes)
warn_deprecation(
'The `all` property is deprecated. Please use the `get_dict()` method instead.',
version=3
)
return self.get_dict()

return attributes
def get_dict(self) -> Dict[str, Any]:
"""Return a deep copy of the complete attributes dictionary.

This method will always return a deep copy, ensuring that modifications
to the returned dictionary do not implicitly modify the node's attributes.
Use `set` or `set_many` to modify attributes.

:return: the attributes as a dictionary
"""
return copy.deepcopy(self._backend_node.attributes)

def get(self, key: str, default=_NO_DEFAULT) -> Any:
"""Return the value of an attribute.
Expand Down
2 changes: 1 addition & 1 deletion src/aiida/orm/nodes/data/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def clone(self):

backend_clone = self.backend_entity.clone()
clone = from_backend_entity(self.__class__, backend_clone)
clone.base.attributes.reset(copy.deepcopy(self.base.attributes.all))
clone.base.attributes.reset(copy.deepcopy(self.base.attributes.get_dict()))
clone.base.repository._clone(self.base.repository)

return clone
Expand Down
4 changes: 2 additions & 2 deletions src/aiida/orm/nodes/data/dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ def get_dict(self):

:return: dictionary
"""
return dict(self.base.attributes.all)
return dict(self.base.attributes.get_dict())

def keys(self):
"""Iterator of valid keys stored in the Dict object.
Expand All @@ -155,7 +155,7 @@ def value(self) -> dict[str, t.Any]:

:return: The dictionary content.
"""
return self.base.attributes.all
return self.base.attributes.get_dict()

@property
def dict(self):
Expand Down
2 changes: 1 addition & 1 deletion src/aiida/orm/nodes/data/enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,6 @@ def __eq__(self, other: t.Any) -> bool:
except (ImportError, ValueError):
return False
elif isinstance(other, EnumData):
return self.base.attributes.all == other.base.attributes.all
return self.base.attributes.get_dict() == other.base.attributes.get_dict()

return False
2 changes: 1 addition & 1 deletion src/aiida/orm/nodes/data/jsonable.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ def _get_object(self) -> JsonSerializableProtocol:
try:
return self._obj
except AttributeError:
attributes = self.base.attributes.all
attributes = self.base.attributes.get_dict()
class_name = attributes.pop('@class')
module_name = attributes.pop('@module')

Expand Down
8 changes: 6 additions & 2 deletions src/aiida/orm/nodes/data/structure.py
Original file line number Diff line number Diff line change
Expand Up @@ -1307,7 +1307,9 @@ def append_kind(self, kind):
raise ValueError(f'A kind with the same name ({kind.name}) already exists.')

# If here, no exceptions have been raised, so I add the site.
self.base.attributes.all.setdefault('kinds', []).append(new_kind.get_raw())
kinds = self.base.attributes.get('kinds', [])
kinds.append(new_kind.get_raw())
self.base.attributes.set('kinds', kinds)
# Note, this is a dict (with integer keys) so it allows for empty spots!
if self._internal_kind_tags is None:
self._internal_kind_tags = {}
Expand All @@ -1334,7 +1336,9 @@ def append_site(self, site):
)

# If here, no exceptions have been raised, so I add the site.
self.base.attributes.all.setdefault('sites', []).append(new_site.get_raw())
sites = self.base.attributes.get('sites', [])
sites.append(new_site.get_raw())
self.base.attributes.set('sites', sites)

def append_atom(self, **kwargs):
"""Append an atom to the Structure, taking care of creating the
Expand Down
4 changes: 2 additions & 2 deletions src/aiida/orm/nodes/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ class Model(Entity.Model):
None,
description='The node attributes',
is_attribute=False,
orm_to_model=lambda node, _: node.base.attributes.all, # type: ignore[attr-defined]
orm_to_model=lambda node, _: node.base.attributes.get_dict(), # type: ignore[attr-defined]
is_subscriptable=True,
exclude_from_cli=True,
exclude_to_orm=True,
Expand Down Expand Up @@ -640,7 +640,7 @@ def _store_from_cache(self, cache_node: 'Node') -> None:
# Make sure to reinitialize the repository instance of the clone to that of the source node.
self.base.repository._copy(cache_node.base.repository)

for key, value in cache_node.base.attributes.all.items():
for key, value in cache_node.base.attributes.get_dict().items():
if key != Sealable.SEALED_KEY:
self.base.attributes.set(key, value)

Expand Down
2 changes: 1 addition & 1 deletion src/aiida/restapi/translator/nodes/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ def _get_content(self):
if self._content_type == 'attributes':
# Get all attrs if attributes_filter is None
if self._attributes_filter is None:
data = {self._content_type: node.base.attributes.all}
data = {self._content_type: node.base.attributes.get_dict()}
# Get all attrs contained in attributes_filter
else:
attrs = {}
Expand Down
2 changes: 1 addition & 1 deletion src/aiida/tools/_dumping/executors/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ def _write(
node_dict['Computer data'] = computer_dict

if self.config.include_attributes:
node_attributes = process_node.base.attributes.all
node_attributes = process_node.base.attributes.get_dict()
if node_attributes:
node_dict['Node attributes'] = node_attributes

Expand Down
2 changes: 1 addition & 1 deletion tests/cmdline/commands/test_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,7 @@ def test_code_export(run_cli_command, aiida_code_installed, tmp_path, file_regre
cmd_code.code_create, ['core.code.installed', '--non-interactive', '--config', filepath, '--label', new_label]
)
new_code = load_code(new_label)
assert code.base.attributes.all == new_code.base.attributes.all
assert code.base.attributes.get_dict() == new_code.base.attributes.get_dict()
assert isinstance(new_code, InstalledCode)


Expand Down
22 changes: 14 additions & 8 deletions tests/orm/nodes/test_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,24 +137,30 @@ def setup_method(self):
self.node = Data()

def test_attributes(self):
"""Test the `Node.base.attributes.all` property."""
"""Test the `Node.base.attributes.get_dict` method."""
original_attribute = {'nested': {'a': 1}}

self.node.base.attributes.set('key', original_attribute)
node_attributes = self.node.base.attributes.all
node_attributes = self.node.base.attributes.get_dict()
assert node_attributes['key'] == original_attribute
node_attributes['key']['nested']['a'] = 2

assert original_attribute['nested']['a'] == 2

# Now store the node and verify that `attributes` then returns a deep copy
self.node.store()
node_attributes = self.node.base.attributes.all
node_attributes = self.node.base.attributes.get_dict()

# We change the returned node attributes but the original attribute should remain unchanged
node_attributes['key']['nested']['a'] = 3
assert original_attribute['nested']['a'] == 2

def test_attributes_all_deprecated(self):
"""Test the deprecated `Node.base.attributes.all` property."""
from aiida.common.warnings import AiidaDeprecationWarning
with pytest.warns(AiidaDeprecationWarning):
_ = self.node.base.attributes.all

def test_get_attribute(self):
"""Test the `Node.get_attribute` method."""
original_attribute = {'nested': {'a': 1}}
Expand Down Expand Up @@ -232,9 +238,9 @@ def test_reset_attribute(self):
attributes_illegal = {'attribute.illegal': 'value', 'attribute_four': 'value'}

self.node.base.attributes.set_many(attributes_before)
assert self.node.base.attributes.all == attributes_before
assert self.node.base.attributes.get_dict() == attributes_before
self.node.base.attributes.reset(attributes_after)
assert self.node.base.attributes.all == attributes_after
assert self.node.base.attributes.get_dict() == attributes_after

with pytest.raises(exceptions.ValidationError):
self.node.base.attributes.reset(attributes_illegal)
Expand Down Expand Up @@ -267,10 +273,10 @@ def test_clear_attributes(self):
"""Test the `Node.clear_attributes` method."""
attributes = {'attribute_one': 'value', 'attribute_two': 'value'}
self.node.base.attributes.set_many(attributes)
assert self.node.base.attributes.all == attributes
assert self.node.base.attributes.get_dict() == attributes

self.node.base.attributes.clear()
assert self.node.base.attributes.all == {}
assert self.node.base.attributes.get_dict() == {}

# Repeat for stored node
self.node.store()
Expand Down Expand Up @@ -1024,7 +1030,7 @@ def test_subclasses_are_distinguished(self):
"""Test that subclasses get different hashes even if they contain the same attributes."""
node_int = Int(5).store()
node_data = Data()
node_data.base.attributes.set_many(node_int.base.attributes.all)
node_data.base.attributes.set_many(node_int.base.attributes.get_dict())
node_data.store()
assert node_int.base.caching.get_hash() != node_data.base.caching.get_hash()

Expand Down
14 changes: 7 additions & 7 deletions tests/test_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ def test_get_attrs_before_storing(self):
}

# Now I check if I can retrieve them, before the storage
assert a.base.attributes.all == target_attrs
assert a.base.attributes.get_dict() == target_attrs

# And now I try to delete the keys
a.base.attributes.delete('k1')
Expand All @@ -355,7 +355,7 @@ def test_get_attrs_before_storing(self):
a.base.attributes.delete('k8')
a.base.attributes.delete('k9')

assert a.base.attributes.all == {}
assert a.base.attributes.get_dict() == {}

def test_get_attrs_after_storing(self):
a = orm.Data()
Expand Down Expand Up @@ -384,7 +384,7 @@ def test_get_attrs_after_storing(self):
}

# Now I check if I can retrieve them, before the storage
assert a.base.attributes.all == target_attrs
assert a.base.attributes.get_dict() == target_attrs

def test_store_object(self):
"""Trying to set objects as attributes should fail, because they are not json-serializable."""
Expand Down Expand Up @@ -427,7 +427,7 @@ def test_attributes_on_clone(self):
b_expected_attributes['new'] = 'cvb'

# I check before storing that the attributes are ok
assert b.base.attributes.all == b_expected_attributes
assert b.base.attributes.get_dict() == b_expected_attributes
# Note that during copy, I do not copy the extras!
assert b.base.extras.all == {}

Expand All @@ -438,11 +438,11 @@ def test_attributes_on_clone(self):
b_expected_extras = {'meta': 'textofext', '_aiida_hash': AnyValue()}

# Now I check that the attributes of the original node have not changed
assert a.base.attributes.all == attrs_to_set
assert a.base.attributes.get_dict() == attrs_to_set

# I check then on the 'b' copy
assert b.base.extras.all == b_expected_extras
assert b.base.attributes.all == b_expected_attributes
assert b.base.attributes.get_dict() == b_expected_attributes

def test_files(self):
a = orm.Data()
Expand Down Expand Up @@ -777,7 +777,7 @@ def test_attr_listing(self):
assert set(list(a.base.extras.keys())) == set(all_extras.keys())
assert set(list(a.base.attributes.keys())) == set(attrs_to_set.keys())

assert a.base.attributes.all == attrs_to_set
assert a.base.attributes.get_dict() == attrs_to_set

assert a.base.extras.all == all_extras

Expand Down
6 changes: 3 additions & 3 deletions tests/tools/archive/test_complex.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,13 +175,13 @@ def get_hash_from_db_content(grouplabel):
hash_ = make_hash(
[
(
item['param']['*'].base.attributes.all,
item['param']['*'].base.attributes.get_dict(),
item['param']['*'].uuid,
item['param']['*'].label,
item['param']['*'].description,
item['calc']['*'].uuid,
item['calc']['*'].base.attributes.all,
item['array']['*'].base.attributes.all,
item['calc']['*'].base.attributes.get_dict(),
item['array']['*'].base.attributes.get_dict(),
[item['array']['*'].get_array(name).tolist() for name in item['array']['*'].get_arraynames()],
item['array']['*'].uuid,
item['group']['*'].uuid,
Expand Down
4 changes: 2 additions & 2 deletions tests/tools/archive/test_specific_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,8 @@ def test_cycle_structure_data(aiida_profile_clean, aiida_localhost, tmp_path):
# Verify that orm.CalculationNodes have non-empty attribute dictionaries
builder = orm.QueryBuilder().append(orm.CalculationNode)
for [calculation] in builder.iterall():
assert isinstance(calculation.base.attributes.all, dict)
assert len(calculation.base.attributes.all) != 0
assert isinstance(calculation.base.attributes.get_dict(), dict)
assert len(calculation.base.attributes.get_dict()) != 0

# Verify that the structure data maintained its label, cell and kinds
builder = orm.QueryBuilder().append(orm.StructureData)
Expand Down
2 changes: 1 addition & 1 deletion tests/tools/groups/test_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ def test_walk_nodes():
node.store()
group.add_nodes(node)
group_path = GroupPath()
assert [(r.group_path.path, r.node.base.attributes.all) for r in group_path.walk_nodes()] == [
assert [(r.group_path.path, r.node.base.attributes.get_dict()) for r in group_path.walk_nodes()] == [
('a', {'i': 1, 'j': 2})
]

Expand Down