Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

♻️ REFACTOR: EntityAttributesMixin -> NodeAttributes #5442

Merged
merged 7 commits into from
Apr 8, 2022
Merged
Show file tree
Hide file tree
Changes from 4 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 .molecule/default/files/polish/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ def run_via_daemon(workchains, inputs, sleep, timeout):
except AttributeError:
click.secho('Failed: ', fg='red', bold=True, nl=False)
click.secho(f'the workchain<{workchain.pk}> did not return a result output node', bold=True)
click.echo(str(workchain.attributes))
click.echo(str(workchain.base.attributes.all))
return None

return result, workchain, total_time
Expand Down
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ repos:
aiida/orm/users.py|
aiida/orm/nodes/data/enum.py|
aiida/orm/nodes/data/jsonable.py|
aiida/orm/nodes/attributes.py|
aiida/orm/nodes/node.py|
aiida/orm/nodes/process/.*py|
aiida/orm/nodes/repository.py|
Expand Down
2 changes: 1 addition & 1 deletion aiida/cmdline/commands/cmd_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ def echo_node_dict(nodes, keys, fmt, identifier, raw, use_attrs=True):
id_value = node.uuid

if use_attrs:
node_dict = node.attributes
node_dict = node.base.attributes.all
dict_name = 'attributes'
else:
node_dict = node.extras
Expand Down
5 changes: 4 additions & 1 deletion aiida/cmdline/utils/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,10 @@ def format_flat_links(links, headers):
table = []

for link_triple in links:
table.append([link_triple.link_label, link_triple.node.pk, link_triple.node.get_attribute('process_label', '')])
table.append([
link_triple.link_label, link_triple.node.pk,
link_triple.node.base.attributes.get('process_label', '')
])

result = f'\n{tabulate(table, headers=headers)}'

Expand Down
2 changes: 1 addition & 1 deletion aiida/engine/processes/calcjobs/calcjob.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ def _perform_import(self):
)
retrieve_calculation(self.node, transport, retrieved_temporary_folder.abspath)
self.node.set_state(CalcJobState.PARSING)
self.node.set_attribute(orm.CalcJobNode.IMMIGRATED_KEY, True)
self.node.base.attributes.set(orm.CalcJobNode.IMMIGRATED_KEY, True)
return self.parse(retrieved_temporary_folder.abspath)

def parse(self, retrieved_temporary_folder: Optional[str] = None) -> ExitCode:
Expand Down
2 changes: 1 addition & 1 deletion aiida/engine/processes/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -685,7 +685,7 @@ def _setup_db_record(self) -> None:
def _setup_metadata(self) -> None:
"""Store the metadata on the ProcessNode."""
version_info = self.runner.plugin_version_provider.get_version_info(self.__class__)
self.node.set_attribute_many(version_info)
self.node.base.attributes.set_many(version_info)

for name, metadata in self.metadata.items():
if name in ['store_provenance', 'dry_run', 'call_link_label']:
Expand Down
2 changes: 1 addition & 1 deletion aiida/orm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@
'Data',
'Dict',
'Entity',
'EntityAttributesMixin',
'EntityExtrasMixin',
'EntityTypes',
'EnumData',
Expand All @@ -70,6 +69,7 @@
'List',
'Log',
'Node',
'NodeAttributes',
'NodeEntityLoader',
'NodeLinksManager',
'NodeRepository',
Expand Down
162 changes: 1 addition & 161 deletions aiida/orm/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,14 @@

from plumpy.base.utils import call_with_super_check, super_check

from aiida.common import exceptions
from aiida.common.lang import classproperty, type_check
from aiida.manage import get_manager

if TYPE_CHECKING:
from aiida.orm.implementation import BackendEntity, StorageBackend
from aiida.orm.querybuilder import FilterType, OrderByType, QueryBuilder

__all__ = ('Entity', 'Collection', 'EntityAttributesMixin', 'EntityExtrasMixin', 'EntityTypes')
__all__ = ('Entity', 'Collection', 'EntityExtrasMixin', 'EntityTypes')

CollectionType = TypeVar('CollectionType', bound='Collection')
EntityType = TypeVar('EntityType', bound='Entity')
Expand Down Expand Up @@ -260,165 +259,6 @@ def is_stored(self) -> bool:
...


class EntityAttributesMixin:
"""Mixin class that adds all methods for the attributes column to an entity."""

@property
def attributes(self: EntityProtocol) -> Dict[str, Any]:
"""Return 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 `attributes_keys` and `attributes_items`, or the
getters `get_attribute` and `get_attribute_many` instead.
:return: the attributes as a dictionary
"""
attributes = self.backend_entity.attributes

if self.is_stored:
attributes = copy.deepcopy(attributes)

return attributes

def get_attribute(self: EntityProtocol, key: str, default=_NO_DEFAULT) -> Any:
"""Return the value of an attribute.
.. warning:: While the entity is unstored, this will return a reference of the attribute on the database model,
meaning that changes on the returned value (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
attribute will be a deep copy and mutations of the database attributes will have to go through the
appropriate set methods.
:param key: name of the attribute
:param default: return this value instead of raising if the attribute does not exist
:return: the value of the attribute
:raises AttributeError: if the attribute does not exist and no default is specified
"""
try:
attribute = self.backend_entity.get_attribute(key)
except AttributeError:
if default is _NO_DEFAULT:
raise
attribute = default

if self.is_stored:
attribute = copy.deepcopy(attribute)

return attribute

def get_attribute_many(self: EntityProtocol, keys: List[str]) -> List[Any]:
"""Return the values of multiple attributes.
.. 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 `attributes_keys` and `attributes_items`, or the
getters `get_attribute` and `get_attribute_many` instead.
:param keys: a list of attribute names
:return: a list of attribute values
:raises AttributeError: if at least one attribute does not exist
"""
attributes = self.backend_entity.get_attribute_many(keys)

if self.is_stored:
attributes = copy.deepcopy(attributes)

return attributes

def set_attribute(self: EntityProtocol, key: str, value: Any) -> None:
"""Set an attribute to the given value.
:param key: name of the attribute
:param value: value of the attribute
:raise aiida.common.ValidationError: if the key is invalid, i.e. contains periods
:raise aiida.common.ModificationNotAllowed: if the entity is stored
"""
if self.is_stored:
raise exceptions.ModificationNotAllowed('the attributes of a stored entity are immutable')

self.backend_entity.set_attribute(key, value)

def set_attribute_many(self: EntityProtocol, attributes: Dict[str, Any]) -> None:
"""Set multiple attributes.
.. note:: This will override any existing attributes that are present in the new dictionary.
:param attributes: a dictionary with the attributes to set
:raise aiida.common.ValidationError: if any of the keys are invalid, i.e. contain periods
:raise aiida.common.ModificationNotAllowed: if the entity is stored
"""
if self.is_stored:
raise exceptions.ModificationNotAllowed('the attributes of a stored entity are immutable')

self.backend_entity.set_attribute_many(attributes)

def reset_attributes(self: EntityProtocol, attributes: Dict[str, Any]) -> None:
"""Reset the attributes.
.. note:: This will completely clear any existing attributes and replace them with the new dictionary.
:param attributes: a dictionary with the attributes to set
:raise aiida.common.ValidationError: if any of the keys are invalid, i.e. contain periods
:raise aiida.common.ModificationNotAllowed: if the entity is stored
"""
if self.is_stored:
raise exceptions.ModificationNotAllowed('the attributes of a stored entity are immutable')

self.backend_entity.reset_attributes(attributes)

def delete_attribute(self: EntityProtocol, key: str) -> None:
"""Delete an attribute.
:param key: name of the attribute
:raises AttributeError: if the attribute does not exist
:raise aiida.common.ModificationNotAllowed: if the entity is stored
"""
if self.is_stored:
raise exceptions.ModificationNotAllowed('the attributes of a stored entity are immutable')

self.backend_entity.delete_attribute(key)

def delete_attribute_many(self: EntityProtocol, keys: List[str]) -> None:
"""Delete multiple attributes.
:param keys: names of the attributes to delete
:raises AttributeError: if at least one of the attribute does not exist
:raise aiida.common.ModificationNotAllowed: if the entity is stored
"""
if self.is_stored:
raise exceptions.ModificationNotAllowed('the attributes of a stored entity are immutable')

self.backend_entity.delete_attribute_many(keys)

def clear_attributes(self: EntityProtocol) -> None:
"""Delete all attributes."""
if self.is_stored:
raise exceptions.ModificationNotAllowed('the attributes of a stored entity are immutable')

self.backend_entity.clear_attributes()

def attributes_items(self: EntityProtocol):
"""Return an iterator over the attributes.
:return: an iterator with attribute key value pairs
"""
return self.backend_entity.attributes_items()

def attributes_keys(self: EntityProtocol):
"""Return an iterator over the attribute keys.
:return: an iterator with attribute keys
"""
return self.backend_entity.attributes_keys()


class EntityExtrasMixin:
"""Mixin class that adds all methods for the extras column to an entity."""

Expand Down
2 changes: 2 additions & 0 deletions aiida/orm/nodes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
# yapf: disable
# pylint: disable=wildcard-import

from .attributes import *
from .data import *
from .node import *
from .process import *
Expand All @@ -40,6 +41,7 @@
'KpointsData',
'List',
'Node',
'NodeAttributes',
'NodeRepository',
'NumericType',
'OrbitalData',
Expand Down
Loading