From a3fcd5f5e1bdd23e480247aff71ce4696fa93d00 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Wed, 8 Oct 2025 17:19:31 +0200 Subject: [PATCH 01/59] Fix ORM pydantic schemas --- src/aiida/cmdline/commands/cmd_code.py | 6 +- src/aiida/cmdline/groups/dynamic.py | 23 +++-- src/aiida/common/datastructures.py | 4 +- src/aiida/common/pydantic.py | 16 +++- src/aiida/orm/authinfos.py | 10 +-- src/aiida/orm/comments.py | 24 +++-- src/aiida/orm/computers.py | 41 +++++++-- src/aiida/orm/entities.py | 88 ++++++++++++++++--- src/aiida/orm/fields.py | 6 +- src/aiida/orm/groups.py | 46 +++++++--- src/aiida/orm/logs.py | 38 ++++++-- src/aiida/orm/nodes/data/array/kpoints.py | 77 +++++++++++++--- src/aiida/orm/nodes/data/array/trajectory.py | 63 ++++++++++++- src/aiida/orm/nodes/data/bool.py | 9 ++ src/aiida/orm/nodes/data/cif.py | 20 +++-- src/aiida/orm/nodes/data/code/installed.py | 33 ++++--- src/aiida/orm/nodes/data/code/portable.py | 6 +- src/aiida/orm/nodes/data/data.py | 6 +- src/aiida/orm/nodes/data/float.py | 8 ++ src/aiida/orm/nodes/data/int.py | 8 ++ src/aiida/orm/nodes/data/remote/base.py | 7 +- src/aiida/orm/nodes/data/singlefile.py | 26 +++++- src/aiida/orm/nodes/data/str.py | 9 ++ src/aiida/orm/nodes/data/structure.py | 26 +++++- src/aiida/orm/nodes/node.py | 85 +++++++++++------- .../orm/nodes/process/calculation/calcjob.py | 57 ++++++++---- src/aiida/orm/nodes/process/process.py | 40 +++++++-- src/aiida/orm/users.py | 23 ++++- src/aiida/orm/utils/mixins.py | 5 +- .../storage/psql_dos/orm/querybuilder/main.py | 3 + 30 files changed, 640 insertions(+), 173 deletions(-) diff --git a/src/aiida/cmdline/commands/cmd_code.py b/src/aiida/cmdline/commands/cmd_code.py index f463b8755c..9e59f5ee53 100644 --- a/src/aiida/cmdline/commands/cmd_code.py +++ b/src/aiida/cmdline/commands/cmd_code.py @@ -16,6 +16,7 @@ import click +from aiida import orm from aiida.cmdline.commands.cmd_data.cmd_export import data_export from aiida.cmdline.commands.cmd_verdi import verdi from aiida.cmdline.groups.dynamic import DynamicEntryPointCommandGroup @@ -32,10 +33,11 @@ def verdi_code(): """Setup and manage codes.""" -def create_code(ctx: click.Context, cls, **kwargs) -> None: +def create_code(ctx: click.Context, cls: orm.Code, **kwargs) -> None: """Create a new `Code` instance.""" try: - instance = cls._from_model(cls.Model(**kwargs)) + Model = cls.Model.as_input_model() + instance = cls.from_model(Model(**kwargs)) except (TypeError, ValueError) as exception: echo.echo_critical(f'Failed to create instance `{cls}`: {exception}') diff --git a/src/aiida/cmdline/groups/dynamic.py b/src/aiida/cmdline/groups/dynamic.py index c7949a9065..df310fcf1e 100644 --- a/src/aiida/cmdline/groups/dynamic.py +++ b/src/aiida/cmdline/groups/dynamic.py @@ -8,6 +8,7 @@ import click +from aiida import orm from aiida.common import exceptions from aiida.plugins.entry_point import ENTRY_POINT_GROUP_FACTORY_MAPPING, get_entry_point_names from aiida.plugins.factories import BaseFactory @@ -97,8 +98,9 @@ def call_command(self, ctx: click.Context, cls: t.Any, non_interactive: bool, ** if hasattr(cls, 'Model'): # The plugin defines a pydantic model: use it to validate the provided arguments + Model = cls.Model.as_input_model() if issubclass(cls, orm.Entity) else cls.Model try: - cls.Model(**kwargs) + Model(**kwargs) except ValidationError as exception: param_hint = [ f'--{loc.replace("_", "-")}' # type: ignore[union-attr] @@ -168,19 +170,26 @@ def list_options(self, entry_point: str) -> list[t.Callable[[FC], FC]]: options_spec = self.factory(entry_point).get_cli_options() # type: ignore[union-attr] return [self.create_option(*item) for item in options_spec] + Model = cls.Model.as_input_model() if issubclass(cls, orm.Entity) else cls.Model + options_spec = {} - for key, field_info in cls.Model.model_fields.items(): + for key, field_info in Model.model_fields.items(): if get_metadata(field_info, 'exclude_from_cli'): continue default = field_info.default_factory if field_info.default is PydanticUndefined else field_info.default - # If the annotation has the ``__args__`` attribute it is an instance of a type from ``typing`` and the real - # type can be gotten from the arguments. For example it could be ``typing.Union[str, None]`` calling - # ``typing.Union[str, None].__args__`` will return the tuple ``(str, NoneType)``. So to get the real type, - # we simply remove all ``NoneType`` and the remaining type should be the type of the option. - if hasattr(field_info.annotation, '__args__'): + option_type = get_metadata(field_info, 'option_type', None) + + if option_type is not None: + field_type = option_type + elif hasattr(field_info.annotation, '__args__'): + # If the annotation has the ``__args__`` attribute it is an instance of a type from ``typing`` and + # the real type can be gotten from the arguments. For example it could be ``typing.Union[str, None]`` + # calling ``typing.Union[str, None].__args__`` will return the tuple ``(str, NoneType)``. So to get + # the real type, we simply remove all ``NoneType`` and the remaining type should be the type of the + # option. args = list(filter(lambda e: e is not type(None), field_info.annotation.__args__)) # Click parameters only support specifying a single type, so we default to the first one even if the # pydantic model defines multiple. diff --git a/src/aiida/common/datastructures.py b/src/aiida/common/datastructures.py index 58198f051a..94658c09a2 100644 --- a/src/aiida/common/datastructures.py +++ b/src/aiida/common/datastructures.py @@ -167,8 +167,8 @@ class CalcInfo(DefaultFieldsAttributeDict): max_wallclock_seconds: None | int max_memory_kb: None | int rerunnable: bool - retrieve_list: None | list[str | tuple[str, str, str]] - retrieve_temporary_list: None | list[str | tuple[str, str, str]] + retrieve_list: None | list[str | tuple[str, str, int]] + retrieve_temporary_list: None | list[str | tuple[str, str, int]] local_copy_list: None | list[tuple[str, str, str]] remote_copy_list: None | list[tuple[str, str, str]] remote_symlink_list: None | list[tuple[str, str, str]] diff --git a/src/aiida/common/pydantic.py b/src/aiida/common/pydantic.py index 60a3913aad..68b7458783 100644 --- a/src/aiida/common/pydantic.py +++ b/src/aiida/common/pydantic.py @@ -36,6 +36,7 @@ def MetadataField( # noqa: N802 option_cls: t.Any | None = None, orm_class: type[Entity[t.Any, t.Any]] | str | None = None, orm_to_model: t.Callable[[Entity[t.Any, t.Any], Path], t.Any] | None = None, + option_type: type | None = None, model_to_orm: t.Callable[['BaseModel'], t.Any] | None = None, exclude_to_orm: bool = False, exclude_from_cli: bool = False, @@ -65,8 +66,11 @@ class Model(BaseModel): :param priority: Used to order the list of all fields in the model. Ordering is done from small to large priority. :param short_name: Optional short name to use for an option on a command line interface. :param option_cls: The :class:`click.Option` class to use to construct the option. + :param option_type: The type to use for the option on the command line interface. This is typically a built-in type + such as ``str``, ``int``, ``float``, or ``bool``. If not specified, the type will be inferred from the type + annotation of the field. :param orm_class: The class, or entry point name thereof, to which the field should be converted. If this field is - defined, the value of this field should acccept an integer which will automatically be converted to an instance + defined, the value of this field should accept an integer which will automatically be converted to an instance of said ORM class using ``orm_class.collection.get(id={field_value})``. This is useful, for example, where a field represents an instance of a different entity, such as an instance of ``User``. The serialized data would store the ``pk`` of the user, but the ORM entity instance would receive the actual ``User`` instance with that @@ -77,8 +81,8 @@ class Model(BaseModel): through ``Entity.from_model``. :param exclude_from_cli: When set to ``True``, this field value will not be exposed on the CLI command that is dynamically generated to create a new instance. - :param is_attribute: Whether the field is stored as an attribute. - :param is_subscriptable: Whether the field can be indexed like a list or dictionary. + :param is_attribute: Whether the field is stored as an attribute. Used by `QbFields`. + :param is_subscriptable: Whether the field can be indexed like a list or dictionary. Used by `QbFields`. """ field_info = Field(default, **kwargs) @@ -86,6 +90,7 @@ class Model(BaseModel): ('priority', priority), ('short_name', short_name), ('option_cls', option_cls), + ('option_type', option_type), ('orm_class', orm_class), ('orm_to_model', orm_to_model), ('model_to_orm', model_to_orm), @@ -97,4 +102,9 @@ class Model(BaseModel): if value is not None: field_info.metadata.append({key: value}) + if exclude_to_orm: + extra = getattr(field_info, 'json_schema_extra', None) or {} + extra.update({'readOnly': True}) + field_info.json_schema_extra = extra + return field_info diff --git a/src/aiida/orm/authinfos.py b/src/aiida/orm/authinfos.py index 8282e944fb..55c39c1b98 100644 --- a/src/aiida/orm/authinfos.py +++ b/src/aiida/orm/authinfos.py @@ -10,7 +10,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Dict, Optional, Type +from typing import TYPE_CHECKING, Any, Dict, Optional, Type, cast from aiida.common import exceptions from aiida.common.pydantic import MetadataField @@ -55,13 +55,13 @@ class Model(entities.Entity.Model): description='The PK of the computer', is_attribute=False, orm_class=Computer, - orm_to_model=lambda auth_info, _: auth_info.computer.pk, # type: ignore[attr-defined] + orm_to_model=lambda auth_info, _: cast('AuthInfo', auth_info).computer.pk, ) user: int = MetadataField( description='The PK of the user', is_attribute=False, orm_class=User, - orm_to_model=lambda auth_info, _: auth_info.user.pk, # type: ignore[attr-defined] + orm_to_model=lambda auth_info, _: cast('AuthInfo', auth_info).user.pk, ) enabled: bool = MetadataField( True, @@ -84,8 +84,8 @@ def __init__( computer: 'Computer', user: 'User', enabled: bool = True, - auth_params: Dict[str, Any] | None = None, - metadata: Dict[str, Any] | None = None, + auth_params: Optional[Dict[str, Any]] = None, + metadata: Optional[Dict[str, Any]] = None, backend: Optional['StorageBackend'] = None, ) -> None: """Create an `AuthInfo` instance for the given computer and user. diff --git a/src/aiida/orm/comments.py b/src/aiida/orm/comments.py index 8d6614de38..d615b63dbd 100644 --- a/src/aiida/orm/comments.py +++ b/src/aiida/orm/comments.py @@ -10,6 +10,7 @@ from datetime import datetime from typing import TYPE_CHECKING, List, Optional, Type, cast +from uuid import UUID from aiida.common.pydantic import MetadataField from aiida.manage import get_manager @@ -68,14 +69,20 @@ class Comment(entities.Entity['BackendComment', CommentCollection]): _CLS_COLLECTION = CommentCollection class Model(entities.Entity.Model): - uuid: Optional[str] = MetadataField( - description='The UUID of the comment', is_attribute=False, exclude_to_orm=True + uuid: UUID = MetadataField( + description='The UUID of the comment', + is_attribute=False, + exclude_to_orm=True, ) - ctime: Optional[datetime] = MetadataField( - description='Creation time of the comment', is_attribute=False, exclude_to_orm=True + ctime: datetime = MetadataField( + description='Creation time of the comment', + is_attribute=False, + exclude_to_orm=True, ) - mtime: Optional[datetime] = MetadataField( - description='Modified time of the comment', is_attribute=False, exclude_to_orm=True + mtime: datetime = MetadataField( + description='Modified time of the comment', + is_attribute=False, + exclude_to_orm=True, ) node: int = MetadataField( description='Node PK that the comment is attached to', @@ -89,7 +96,10 @@ class Model(entities.Entity.Model): orm_class='core.user', orm_to_model=lambda comment, _: cast('Comment', comment).user.pk, ) - content: str = MetadataField(description='Content of the comment', is_attribute=False) + content: str = MetadataField( + description='Content of the comment', + is_attribute=False, + ) def __init__( self, node: 'Node', user: 'User', content: Optional[str] = None, backend: Optional['StorageBackend'] = None diff --git a/src/aiida/orm/computers.py b/src/aiida/orm/computers.py index e8afea0b65..fd628754ca 100644 --- a/src/aiida/orm/computers.py +++ b/src/aiida/orm/computers.py @@ -8,8 +8,11 @@ ########################################################################### """Module for Computer entities""" +from __future__ import annotations + import os from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type, Union +from uuid import UUID from aiida.common import exceptions from aiida.common.log import AIIDA_LOGGER, AiidaLoggerType @@ -74,13 +77,37 @@ class Computer(entities.Entity['BackendComputer', ComputerCollection]): _CLS_COLLECTION = ComputerCollection class Model(entities.Entity.Model): - uuid: str = MetadataField(description='The UUID of the computer', is_attribute=False, exclude_to_orm=True) - label: str = MetadataField(description='Label for the computer', is_attribute=False) - description: str = MetadataField(description='Description of the computer', is_attribute=False) - hostname: str = MetadataField(description='Hostname of the computer', is_attribute=False) - transport_type: str = MetadataField(description='Transport type of the computer', is_attribute=False) - scheduler_type: str = MetadataField(description='Scheduler type of the computer', is_attribute=False) - metadata: Dict[str, Any] = MetadataField(description='Metadata of the computer', is_attribute=False) + uuid: UUID = MetadataField( + description='The UUID of the computer', + is_attribute=False, + exclude_to_orm=True, + ) + label: str = MetadataField( + description='Label for the computer', + is_attribute=False, + ) + description: str = MetadataField( + '', + description='Description of the computer', + is_attribute=False, + ) + hostname: str = MetadataField( + description='Hostname of the computer', + is_attribute=False, + ) + transport_type: str = MetadataField( + description='Transport type of the computer', + is_attribute=False, + ) + scheduler_type: str = MetadataField( + description='Scheduler type of the computer', + is_attribute=False, + ) + metadata: Dict[str, Any] = MetadataField( + default_factory=dict, + description='Metadata of the computer', + is_attribute=False, + ) def __init__( self, diff --git a/src/aiida/orm/entities.py b/src/aiida/orm/entities.py index bc9fef6554..03c52a4d67 100644 --- a/src/aiida/orm/entities.py +++ b/src/aiida/orm/entities.py @@ -14,10 +14,10 @@ import pathlib from enum import Enum from functools import lru_cache -from typing import TYPE_CHECKING, Any, Generic, List, NoReturn, Optional, Type, TypeVar, Union +from typing import TYPE_CHECKING, Any, Generic, List, Literal, NoReturn, Optional, Type, TypeVar, Union, cast from plumpy.base.utils import call_with_super_check, super_check -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict, create_model from pydantic.fields import FieldInfo from typing_extensions import Self @@ -38,6 +38,7 @@ CollectionType = TypeVar('CollectionType', bound='Collection[Any]') EntityType = TypeVar('EntityType', bound='Entity[Any,Any]') +EntityModelType = TypeVar('EntityModelType', bound=BaseModel) BackendEntityType = TypeVar('BackendEntityType', bound='BackendEntity') @@ -147,16 +148,18 @@ def find( filters: Optional['FilterType'] = None, order_by: Optional['OrderByType'] = None, limit: Optional[int] = None, + offset: Optional[int] = None, ) -> List[EntityType]: """Find collection entries matching the filter criteria. :param filters: the keyword value pair filters to match :param order_by: a list of (key, direction) pairs specifying the sort order :param limit: the maximum number of results to return + :param offset: number of initial results to be skipped :return: a list of resulting matches """ - query = self.query(filters=filters, order_by=order_by, limit=limit) + query = self.query(filters=filters, order_by=order_by, limit=limit, offset=offset) return query.all(flat=True) def all(self) -> List[EntityType]: @@ -183,14 +186,67 @@ class Entity(abc.ABC, Generic[BackendEntityType, CollectionType], metaclass=Enti _logger = log.AIIDA_LOGGER.getChild('orm.entities') class Model(BaseModel, defer_build=True): - pk: Optional[int] = MetadataField( - None, - description='The primary key of the entity. Can be `None` if the entity is not yet stored.', + model_config = ConfigDict(extra='forbid') + + pk: int = MetadataField( + description='The primary key of the entity', is_attribute=False, exclude_to_orm=True, exclude_from_cli=True, ) + @classmethod + def __pydantic_init_subclass__(cls, **kwargs: Any) -> None: + super().__pydantic_init_subclass__(**kwargs) + cls.model_config['title'] = cls.__qualname__.replace('.', '') + + @classmethod + def as_input_model(cls: Type[EntityModelType]) -> Type[EntityModelType]: + """Return a derived model class with read-only fields removed. + + This also removes any serializers/validators defined on those fields. + + :return: The derived input model class. + """ + + # Derive the input model from the original model + new_name = cls.__qualname__.replace('.Model', 'InputModel') + InputModel = create_model( + new_name, + __base__=cls, + __doc__=f'Input version of {cls.__name__}.', + ) + InputModel.__qualname__ = new_name + InputModel.__module__ = cls.__module__ + + # Identify read-only fields + readonly_fields = [ + name + for name, field in InputModel.model_fields.items() + if getattr(field, 'json_schema_extra', {}) and field.json_schema_extra.get('readOnly') + ] + + # Remove read-only fields + for name in readonly_fields: + InputModel.model_fields.pop(name, None) + if hasattr(InputModel, name): + delattr(InputModel, name) + + # Prune field validators/serializers referring to read-only fields + decorators = InputModel.__pydantic_decorators__ + + def _prune_field_decorators(field_decorators: dict[str, Any]) -> dict[str, Any]: + return { + name: decorator + for name, decorator in field_decorators.items() + if all(field not in readonly_fields for field in decorator.info.fields) + } + + decorators.field_validators = _prune_field_decorators(decorators.field_validators) + decorators.field_serializers = _prune_field_decorators(decorators.field_serializers) + + return cast(Type[EntityModelType], InputModel) + @classmethod def model_to_orm_fields(cls) -> dict[str, FieldInfo]: return { @@ -228,7 +284,7 @@ def model_to_orm_field_values(cls, model: Model) -> dict[str, Any]: return fields - def _to_model(self, repository_path: pathlib.Path) -> Model: + def to_model(self, repository_path: Optional[pathlib.Path] = None) -> Model: """Return the entity instance as an instance of its model.""" fields = {} @@ -241,16 +297,24 @@ def _to_model(self, repository_path: pathlib.Path) -> Model: return self.Model(**fields) @classmethod - def _from_model(cls, model: Model) -> Self: + def from_model(cls, model: Model) -> Self: """Return an entity instance from an instance of its model.""" fields = cls.model_to_orm_field_values(model) return cls(**fields) - def serialize(self, repository_path: Union[pathlib.Path, None] = None) -> dict[str, Any]: + def serialize( + self, + repository_path: Optional[pathlib.Path] = None, + mode: Literal['json', 'python'] = 'json', + ) -> dict[str, Any]: """Serialize the entity instance to JSON. - :param repository_path: If the orm node has files in the repository, this path is used to dump the repostiory + :param repository_path: If the orm node has files in the repository, this path is used to dump the repository files to. If no path is specified a temporary path is created using the entities pk. + :param mode: The serialization mode, either 'json' or 'python'. The 'json' mode is the most strict and ensures + that the output is JSON serializable, whereas the 'python' mode allows for more complex Python types, such + as `datetime` objects. + :return: A dictionary that can be serialized to JSON. """ self.logger.warning( 'Serialization through pydantic is still an experimental feature and might break in future releases.' @@ -265,7 +329,7 @@ def serialize(self, repository_path: Union[pathlib.Path, None] = None) -> dict[s raise ValueError(f'The repository_path `{repository_path}` does not exist.') if not repository_path.is_dir(): raise ValueError(f'The repository_path `{repository_path}` is not a directory.') - return self._to_model(repository_path).model_dump() + return self.to_model(repository_path).model_dump(mode=mode) @classmethod def from_serialized(cls, **kwargs: dict[str, Any]) -> Self: @@ -273,7 +337,7 @@ def from_serialized(cls, **kwargs: dict[str, Any]) -> Self: cls._logger.warning( 'Serialization through pydantic is still an experimental feature and might break in future releases.' ) - return cls._from_model(cls.Model(**kwargs)) + return cls.from_model(cls.Model(**kwargs)) @classproperty def objects(cls: EntityType) -> CollectionType: # noqa: N805 diff --git a/src/aiida/orm/fields.py b/src/aiida/orm/fields.py index f0e8ca390b..60c6b9aab2 100644 --- a/src/aiida/orm/fields.py +++ b/src/aiida/orm/fields.py @@ -354,6 +354,10 @@ class QbFields: def __init__(self, fields: t.Optional[t.Dict[str, QbField]] = None): self._fields = fields or {} + def keys(self) -> list[str]: + """Return the field keys, prefixed with 'attribute.' if field is an attribute.""" + return [field.backend_key for field in self._fields.values()] + def __repr__(self) -> str: return pformat({key: str(value) for key, value in self._fields.items()}) @@ -464,7 +468,7 @@ def __init__(cls, name, bases, classdict): for key, field in cls.Model.model_fields.items(): fields[key] = add_field( key, - alias=get_metadata(field, 'alias', None), + alias=field.alias, dtype=field.annotation, doc=field.description, is_attribute=get_metadata(field, 'is_attribute', False), diff --git a/src/aiida/orm/groups.py b/src/aiida/orm/groups.py index 8582ee56b9..4504e79ae2 100644 --- a/src/aiida/orm/groups.py +++ b/src/aiida/orm/groups.py @@ -6,17 +6,18 @@ # For further information on the license, see the LICENSE.txt file # # For further information please visit http://www.aiida.net # ########################################################################### -"""AiiDA Group entites""" +"""AiiDA Group entities""" import datetime import warnings from functools import cached_property from pathlib import Path from typing import TYPE_CHECKING, Any, ClassVar, Dict, Optional, Sequence, Tuple, Type, Union, cast +from uuid import UUID from typing_extensions import Self -from aiida.common import exceptions +from aiida.common import exceptions, timezone from aiida.common.lang import classproperty, type_check from aiida.common.pydantic import MetadataField from aiida.common.warnings import warn_deprecation @@ -111,24 +112,45 @@ class Group(entities.Entity['BackendGroup', GroupCollection]): __type_string: ClassVar[Optional[str]] class Model(entities.Entity.Model): - uuid: str = MetadataField(description='The UUID of the group', is_attribute=False, exclude_to_orm=True) - type_string: str = MetadataField(description='The type of the group', is_attribute=False, exclude_to_orm=True) + uuid: UUID = MetadataField( + description='The UUID of the group', + is_attribute=False, + exclude_to_orm=True, + ) + type_string: str = MetadataField( + description='The type of the group', + is_attribute=False, + exclude_to_orm=True, + ) user: int = MetadataField( - description='The group owner', + default_factory=lambda: users.User.collection.get_default().pk, + description='The PK of the group owner, defaults to the current user', is_attribute=False, orm_class='core.user', - orm_to_model=lambda group, _: group.user.pk, # type: ignore[attr-defined] + orm_to_model=lambda group, _: cast('Group', group).user.pk, + exclude_to_orm=True, ) - time: Optional[datetime.datetime] = MetadataField( - description='The creation time of the node', is_attribute=False + time: datetime.datetime = MetadataField( + default_factory=timezone.now, + description='The creation time of the node, defaults to now (timezone-aware)', + is_attribute=False, + exclude_to_orm=True, + ) + label: str = MetadataField( + description='The group label', + is_attribute=False, + ) + description: str = MetadataField( + '', + description='The group description', + is_attribute=False, ) - label: str = MetadataField(description='The group label', is_attribute=False) - description: Optional[str] = MetadataField(description='The group description', is_attribute=False) - extras: Optional[Dict[str, Any]] = MetadataField( + extras: Dict[str, Any] = MetadataField( + default_factory=dict, description='The group extras', is_attribute=False, is_subscriptable=True, - orm_to_model=lambda group, _: group.base.extras.all, # type: ignore[attr-defined] + orm_to_model=lambda group, _: cast('Group', group).base.extras.all, ) _CLS_COLLECTION = GroupCollection diff --git a/src/aiida/orm/logs.py b/src/aiida/orm/logs.py index ed07deb847..981642c1f4 100644 --- a/src/aiida/orm/logs.py +++ b/src/aiida/orm/logs.py @@ -11,6 +11,7 @@ import logging from datetime import datetime from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type +from uuid import UUID from aiida.common import timezone from aiida.common.pydantic import MetadataField @@ -130,13 +131,36 @@ class Log(entities.Entity['BackendLog', LogCollection]): _CLS_COLLECTION = LogCollection class Model(entities.Entity.Model): - uuid: str = MetadataField(description='The UUID of the node', is_attribute=False, exclude_to_orm=True) - loggername: str = MetadataField(description='The name of the logger', is_attribute=False) - levelname: str = MetadataField(description='The name of the log level', is_attribute=False) - message: str = MetadataField(description='The message of the log', is_attribute=False) - time: datetime = MetadataField(description='The time at which the log was created', is_attribute=False) - metadata: Dict[str, Any] = MetadataField(description='The metadata of the log', is_attribute=False) - dbnode_id: int = MetadataField(description='Associated node', is_attribute=False) + uuid: UUID = MetadataField( + description='The UUID of the node', + is_attribute=False, + exclude_to_orm=True, + ) + loggername: str = MetadataField( + description='The name of the logger', + is_attribute=False, + ) + levelname: str = MetadataField( + description='The name of the log level', + is_attribute=False, + ) + message: str = MetadataField( + description='The message of the log', + is_attribute=False, + ) + time: datetime = MetadataField( + description='The time at which the log was created', + is_attribute=False, + ) + metadata: Dict[str, Any] = MetadataField( + default_factory=dict, + description='The metadata of the log', + is_attribute=False, + ) + dbnode_id: int = MetadataField( + description='Associated node', + is_attribute=False, + ) def __init__( self, diff --git a/src/aiida/orm/nodes/data/array/kpoints.py b/src/aiida/orm/nodes/data/array/kpoints.py index e7958970a4..7f8c2a0a39 100644 --- a/src/aiida/orm/nodes/data/array/kpoints.py +++ b/src/aiida/orm/nodes/data/array/kpoints.py @@ -39,14 +39,66 @@ class KpointsData(ArrayData): """ class Model(ArrayData.Model): - labels: t.List[str] = MetadataField(description='Labels associated with the list of kpoints') - label_numbers: t.List[int] = MetadataField(description='Index of the labels in the list of kpoints') - mesh: t.List[int] = MetadataField(description='Mesh of kpoints') - offset: t.List[float] = MetadataField(description='Offset of kpoints') - cell: t.List[t.List[float]] = MetadataField(description='Unit cell of the crystal, in Angstroms') - pbc1: bool = MetadataField(description='True if the first lattice vector is periodic') - pbc2: bool = MetadataField(description='True if the second lattice vector is periodic') - pbc3: bool = MetadataField(description='True if the third lattice vector is periodic') + labels: t.Optional[t.List[str]] = MetadataField( + None, + description='Labels associated with the list of kpoints', + orm_to_model=lambda node, _: t.cast('KpointsData', node).base.attributes.get('labels', None), + ) + label_numbers: t.Optional[t.List[int]] = MetadataField( + None, + description='Index of the labels in the list of kpoints', + orm_to_model=lambda node, _: t.cast('KpointsData', node).base.attributes.get('label_numbers', None), + ) + mesh: t.Optional[t.List[int]] = MetadataField( + None, + description='Mesh of kpoints', + orm_to_model=lambda node, _: t.cast('KpointsData', node).base.attributes.get('mesh', None), + ) + offset: t.Optional[t.List[float]] = MetadataField( + None, + description='Offset of kpoints', + orm_to_model=lambda node, _: t.cast('KpointsData', node).base.attributes.get('offset', None), + ) + cell: t.Optional[t.List[t.List[float]]] = MetadataField( + None, + description='Unit cell of the crystal, in Angstroms', + ) + pbc1: bool = MetadataField( + description='True if the first lattice vector is periodic', + orm_to_model=lambda node, _: t.cast('KpointsData', node).pbc[0], + ) + pbc2: bool = MetadataField( + description='True if the second lattice vector is periodic', + orm_to_model=lambda node, _: t.cast('KpointsData', node).pbc[1], + ) + pbc3: bool = MetadataField( + description='True if the third lattice vector is periodic', + orm_to_model=lambda node, _: t.cast('KpointsData', node).pbc[2], + ) + + def __init__( + self, + labels: list[str] | None = None, + label_numbers: list[int] | None = None, + mesh: list[int] | None = None, + offset: list[float] | None = None, + cell: list[list[float]] | None = None, + pbc1: bool | None = None, + pbc2: bool | None = None, + pbc3: bool | None = None, + **kwargs, + ): + super().__init__(**kwargs) + if cell is not None: + self.cell = cell + if any(pbc is not None for pbc in (pbc1, pbc2, pbc3)): + self.pbc = (pbc1 or False, pbc2 or False, pbc3 or False) + if labels is not None and label_numbers is not None: + if len(labels) != len(label_numbers): + raise ValueError('Labels and label numbers must have the same length') + self.labels = list(zip(label_numbers, labels)) + if mesh is not None: + self.set_kpoints_mesh(mesh, offset=offset) def get_description(self): """Returns a string with infos retrieved from kpoints node's properties. @@ -69,7 +121,7 @@ def cell(self): """The crystal unit cell. Rows are the crystal vectors in Angstroms. :return: a 3x3 numpy.array """ - return numpy.array(self.base.attributes.get('cell')) + return numpy.array(self.base.attributes.get('cell', [])) @cell.setter def cell(self, value): @@ -99,8 +151,11 @@ def pbc(self): :return: a tuple of three booleans, each one tells if there are periodic boundary conditions for the i-th real-space direction (i=1,2,3) """ - # return copy.deepcopy(self._pbc) - return (self.base.attributes.get('pbc1'), self.base.attributes.get('pbc2'), self.base.attributes.get('pbc3')) + return ( + self.base.attributes.get('pbc1', False), + self.base.attributes.get('pbc2', False), + self.base.attributes.get('pbc3', False), + ) @pbc.setter def pbc(self, value): diff --git a/src/aiida/orm/nodes/data/array/trajectory.py b/src/aiida/orm/nodes/data/array/trajectory.py index 3d6356ebd2..dee116f76c 100644 --- a/src/aiida/orm/nodes/data/array/trajectory.py +++ b/src/aiida/orm/nodes/data/array/trajectory.py @@ -8,13 +8,20 @@ ########################################################################### """AiiDA class to deal with crystal structure trajectories.""" +from __future__ import annotations + import collections.abc -from typing import List +from typing import TYPE_CHECKING, List, Optional + +import numpy as np from aiida.common.pydantic import MetadataField from .array import ArrayData +if TYPE_CHECKING: + from aiida import orm + __all__ = ('TrajectoryData',) @@ -24,14 +31,42 @@ class TrajectoryData(ArrayData): """ class Model(ArrayData.Model): - units_positions: str = MetadataField(alias='units|positions', description='Unit of positions') - units_times: str = MetadataField(alias='units|times', description='Unit of time') + units_positions: Optional[str] = MetadataField( + None, + serialization_alias='units|positions', + description='Unit of positions', + ) + units_times: Optional[str] = MetadataField( + None, + serialization_alias='units|times', + description='Unit of time', + ) symbols: List[str] = MetadataField(description='List of symbols') - def __init__(self, structurelist=None, **kwargs): + + def __init__( + self, + structurelist: list[orm.StructureData] | None = None, + units_positions: str | None = None, + units_times: str | None = None, + symbols: list[str] | None = None, + arrays: np.ndarray | dict[str, np.ndarray] | None = None, + **kwargs, + ): super().__init__(**kwargs) + self.unit_positions = units_positions + self.unit_times = units_times if structurelist is not None: self.set_structurelist(structurelist) + elif arrays is not None: + self.set_trajectory( + symbols=symbols, + positions=arrays['positions'], + stepids=arrays.get('steps'), + cells=arrays.get('cells'), + times=arrays.get('times'), + velocities=arrays.get('velocities'), + ) def _internal_validate(self, stepids, cells, symbols, positions, times, velocities): """Internal function to validate the type and shape of the arrays. See @@ -256,6 +291,26 @@ def get_cells(self): except (AttributeError, KeyError): return None + @property + def units_positions(self) -> str | None: + """Units for the positions array.""" + return self.base.attributes.get('units|positions', None) + + @units_positions.setter + def units_positions(self, units: str) -> None: + """Set units for the positions array.""" + self.base.attributes.set('units|positions', units) + + @property + def units_times(self) -> str | None: + """Units for the times array.""" + return self.base.attributes.get('units|times', None) + + @units_times.setter + def units_times(self, units: str) -> None: + """Set units for the times array.""" + self.base.attributes.set('units|times', units) + @property def symbols(self) -> List[str]: """Return the array of symbols, if it has already been set. diff --git a/src/aiida/orm/nodes/data/bool.py b/src/aiida/orm/nodes/data/bool.py index 61c8e61d5e..52ef060677 100644 --- a/src/aiida/orm/nodes/data/bool.py +++ b/src/aiida/orm/nodes/data/bool.py @@ -10,7 +10,10 @@ import numpy +from aiida.common.pydantic import MetadataField + from .base import BaseType, to_aiida_type +from .numeric import NumericType __all__ = ('Bool',) @@ -20,6 +23,12 @@ class Bool(BaseType): _type = bool + class Model(NumericType.Model): + value: bool = MetadataField( + title='Boolean value.', + description='The value of the boolean', + ) + def __int__(self): return int(bool(self)) diff --git a/src/aiida/orm/nodes/data/cif.py b/src/aiida/orm/nodes/data/cif.py index 8421a617eb..12170dbbdc 100644 --- a/src/aiida/orm/nodes/data/cif.py +++ b/src/aiida/orm/nodes/data/cif.py @@ -9,7 +9,7 @@ """Tools for handling Crystallographic Information Files (CIF)""" import re -import typing as t +from typing import List, Optional from aiida.common.pydantic import MetadataField from aiida.common.utils import Capturing @@ -251,14 +251,20 @@ class CifData(SinglefileData): _ase = None class Model(SinglefileData.Model): - formulae: t.Optional[t.List[str]] = MetadataField( - None, description='List of formulae contained in the CIF file.', exclude_to_orm=True + formulae: Optional[List[str]] = MetadataField( + None, + description='List of formulae contained in the CIF file.', + exclude_to_orm=True, ) - spacegroup_numbers: t.Optional[t.List[str]] = MetadataField( - None, description='List of space group numbers of the structure.', exclude_to_orm=True + spacegroup_numbers: Optional[List[str]] = MetadataField( + None, + description='List of space group numbers of the structure.', + exclude_to_orm=True, ) - md5: t.Optional[str] = MetadataField( - None, description='MD5 checksum of the file contents.', exclude_to_orm=True + md5: Optional[str] = MetadataField( + None, + description='MD5 checksum of the file contents.', + exclude_to_orm=True, ) def __init__(self, ase=None, file=None, filename=None, values=None, scan_type=None, parse_policy=None, **kwargs): diff --git a/src/aiida/orm/nodes/data/code/installed.py b/src/aiida/orm/nodes/data/code/installed.py index 3401f43ed2..03246dfe86 100644 --- a/src/aiida/orm/nodes/data/code/installed.py +++ b/src/aiida/orm/nodes/data/code/installed.py @@ -19,7 +19,7 @@ import pathlib from typing import cast -from pydantic import field_serializer, field_validator +from pydantic import field_validator from aiida.common import exceptions from aiida.common.lang import type_check @@ -44,16 +44,17 @@ class InstalledCode(Code): class Model(AbstractCode.Model): """Model describing required information to create an instance.""" - computer: str = MetadataField( # type: ignore[assignment] - ..., + computer: int = MetadataField( title='Computer', description='The remote computer on which the executable resides.', - orm_to_model=lambda node, _: cast('InstalledCode', node).computer.label, + is_attribute=False, + orm_to_model=lambda node, _: cast('InstalledCode', node).computer.pk, + orm_class=Computer, + option_type=str, short_name='-Y', priority=2, ) filepath_executable: str = MetadataField( - ..., title='Filepath executable', description='Filepath of the executable on the remote computer.', orm_to_model=lambda node, _: str(cast('InstalledCode', node).filepath_executable), @@ -61,21 +62,27 @@ class Model(AbstractCode.Model): priority=1, ) - @field_validator('computer') + @field_validator('computer', mode='before') @classmethod - def validate_computer(cls, value: str) -> Computer: - """Override the validator for the ``label`` of the base class since uniqueness is defined on full label.""" + def validate_computer(cls, value: str | int) -> int: + """Validate computer input. + + If provided a string (e.g., via CLI computer setup), try to load the computer and return its PK. + + :param value: The input value, either a string or an integer + :return: The PK of the computer + :raises ValueError: If the computer does not exist + """ from aiida.orm import load_computer + if isinstance(value, int): + return value + try: - return load_computer(value) + return cast(int, load_computer(value).pk) except exceptions.NotExistent as exception: raise ValueError(exception) from exception - @field_serializer('computer') - def serialize_computer(self, computer: Computer, _info): - return computer.label - def __init__(self, computer: Computer, filepath_executable: str, **kwargs): """Construct a new instance. diff --git a/src/aiida/orm/nodes/data/code/portable.py b/src/aiida/orm/nodes/data/code/portable.py index 911855318e..263eb2cfaf 100644 --- a/src/aiida/orm/nodes/data/code/portable.py +++ b/src/aiida/orm/nodes/data/code/portable.py @@ -36,7 +36,7 @@ _LOGGER = logging.getLogger(__name__) -def _export_filpath_files_from_repo(portable_code: PortableCode, repository_path: pathlib.Path) -> str: +def _export_filepath_files_from_repo(portable_code: PortableCode, repository_path: pathlib.Path) -> str: for root, _, filenames in portable_code.base.repository.walk(): for filename in filenames: rel_path = str(root / filename) @@ -72,7 +72,7 @@ class Model(AbstractCode.Model): short_name='-F', is_attribute=False, priority=2, - orm_to_model=_export_filpath_files_from_repo, # type: ignore[arg-type] + orm_to_model=_export_filepath_files_from_repo, # type: ignore[arg-type] ) def __init__( @@ -201,7 +201,7 @@ def _prepare_yaml(self, *args, **kwargs): """Export code to a YAML file.""" result = super()._prepare_yaml(*args, **kwargs)[0] target = pathlib.Path().cwd() / f'{self.label}' - _export_filpath_files_from_repo(self, target) + _export_filepath_files_from_repo(self, target) _LOGGER.info(f'Repository files for PortableCode <{self.pk}> dumped to folder `{target}`.') return result, {} diff --git a/src/aiida/orm/nodes/data/data.py b/src/aiida/orm/nodes/data/data.py index 56b3dcbdbb..1b5494565f 100644 --- a/src/aiida/orm/nodes/data/data.py +++ b/src/aiida/orm/nodes/data/data.py @@ -48,7 +48,11 @@ class Data(Node): class Model(Node.Model): source: Optional[dict] = MetadataField( - None, description='Source of the data.', is_subscriptable=True, exclude_from_cli=True + None, + description='Source of the data.', + is_subscriptable=True, + exclude_to_orm=True, + exclude_from_cli=True, ) def __init__(self, *args, source=None, **kwargs): diff --git a/src/aiida/orm/nodes/data/float.py b/src/aiida/orm/nodes/data/float.py index 88e535e067..cdc3524e92 100644 --- a/src/aiida/orm/nodes/data/float.py +++ b/src/aiida/orm/nodes/data/float.py @@ -10,6 +10,8 @@ import numbers +from aiida.common.pydantic import MetadataField + from .base import to_aiida_type from .numeric import NumericType @@ -21,6 +23,12 @@ class Float(NumericType): _type = float + class Model(NumericType.Model): + value: float = MetadataField( + title='Float value.', + description='The value of the float', + ) + @to_aiida_type.register(numbers.Real) def _(value): diff --git a/src/aiida/orm/nodes/data/int.py b/src/aiida/orm/nodes/data/int.py index 43ffa918c3..8bce700cf9 100644 --- a/src/aiida/orm/nodes/data/int.py +++ b/src/aiida/orm/nodes/data/int.py @@ -10,6 +10,8 @@ import numbers +from aiida.common.pydantic import MetadataField + from .base import to_aiida_type from .numeric import NumericType @@ -21,6 +23,12 @@ class Int(NumericType): _type = int + class Model(NumericType.Model): + value: int = MetadataField( + title='Integer value.', + description='The value of the integer', + ) + @to_aiida_type.register(numbers.Integral) def _(value): diff --git a/src/aiida/orm/nodes/data/remote/base.py b/src/aiida/orm/nodes/data/remote/base.py index 8bae0fed02..a926b1f056 100644 --- a/src/aiida/orm/nodes/data/remote/base.py +++ b/src/aiida/orm/nodes/data/remote/base.py @@ -13,7 +13,7 @@ import logging import os from pathlib import Path -from typing import Union +from typing import Optional from aiida.common.pydantic import MetadataField from aiida.orm import AuthInfo @@ -35,13 +35,14 @@ class RemoteData(Data): KEY_EXTRA_CLEANED = 'cleaned' class Model(Data.Model): - remote_path: Union[str, None] = MetadataField( + remote_path: Optional[str] = MetadataField( + None, title='Remote path', description='Filepath on the remote computer.', orm_to_model=lambda node, _: node.get_remote_path(), ) - def __init__(self, remote_path: Union[str, None] = None, **kwargs): + def __init__(self, remote_path: Optional[str] = None, **kwargs): super().__init__(**kwargs) if remote_path is not None: self.set_remote_path(remote_path) diff --git a/src/aiida/orm/nodes/data/singlefile.py b/src/aiida/orm/nodes/data/singlefile.py index 742a5702c6..102197c8d0 100644 --- a/src/aiida/orm/nodes/data/singlefile.py +++ b/src/aiida/orm/nodes/data/singlefile.py @@ -10,12 +10,15 @@ from __future__ import annotations +import base64 import contextlib import io import os import pathlib import typing as t +from pydantic import field_serializer, field_validator + from aiida.common import exceptions from aiida.common.pydantic import MetadataField from aiida.common.typing import FilePath @@ -33,9 +36,28 @@ class SinglefileData(Data): class Model(Data.Model): content: bytes = MetadataField( description='The file content.', - model_to_orm=lambda model: io.BytesIO(model.content), # type: ignore[attr-defined] + model_to_orm=lambda model: io.BytesIO(t.cast('SinglefileData', model).content), + ) + filename: str = MetadataField( + 'file.txt', + description='The name of the stored file.', ) - filename: t.Optional[str] = MetadataField(None, description='The filename. Defaults to `file.txt`.') + + @field_validator('content') + @classmethod + def _decode_content(cls, value: str | bytes) -> bytes: + """Decode base64 content if needed.""" + if isinstance(value, str): + try: + return base64.b64decode(value, validate=True) + except Exception as exc: + raise ValueError('if `content` is a string, it must be valid base64-encoded data') from exc + return value + + @field_serializer('content') + def _encode_content(self, value: bytes) -> str: + """Encode content as base64 string for serialization.""" + return base64.b64encode(value).decode() @classmethod def from_string(cls, content: str, filename: str | pathlib.Path | None = None, **kwargs: t.Any) -> 'SinglefileData': diff --git a/src/aiida/orm/nodes/data/str.py b/src/aiida/orm/nodes/data/str.py index 66b631ef7c..0d0c771de1 100644 --- a/src/aiida/orm/nodes/data/str.py +++ b/src/aiida/orm/nodes/data/str.py @@ -8,7 +8,10 @@ ########################################################################### """`Data` sub class to represent a string value.""" +from aiida.common.pydantic import MetadataField + from .base import BaseType, to_aiida_type +from .numeric import NumericType __all__ = ('Str',) @@ -18,6 +21,12 @@ class Str(BaseType): _type = str + class Model(NumericType.Model): + value: str = MetadataField( + title='String value.', + description='The value of the string', + ) + @to_aiida_type.register(str) def _(value): diff --git a/src/aiida/orm/nodes/data/structure.py b/src/aiida/orm/nodes/data/structure.py index 0e3e09ef5b..437d2f902b 100644 --- a/src/aiida/orm/nodes/data/structure.py +++ b/src/aiida/orm/nodes/data/structure.py @@ -16,6 +16,8 @@ import json import typing as t +from pydantic import field_validator + from aiida.common.constants import elements from aiida.common.exceptions import UnsupportedSpeciesError from aiida.common.pydantic import MetadataField @@ -689,8 +691,28 @@ class Model(Data.Model): pbc2: bool = MetadataField(description='Whether periodic in the b direction') pbc3: bool = MetadataField(description='Whether periodic in the c direction') cell: t.List[t.List[float]] = MetadataField(description='The cell parameters') - kinds: t.Optional[t.List[dict]] = MetadataField(description='The kinds of atoms') - sites: t.Optional[t.List[dict]] = MetadataField(description='The atomic sites') + kinds: t.Optional[t.List[dict]] = MetadataField( + None, + description='The kinds of atoms', + ) + sites: t.Optional[t.List[dict]] = MetadataField( + None, + description='The atomic sites', + ) + + @field_validator('kinds', mode='before') + @classmethod + def _validate_kinds(cls, value: t.Optional[t.List['Kind']]) -> t.Optional[t.List[t.Dict]]: + if value is None: + return None + return [kind.get_raw() for kind in value] + + @field_validator('sites', mode='before') + @classmethod + def _validate_sites(cls, value: t.Optional[t.List['Site']]) -> t.Optional[t.List[t.Dict]]: + if value is None: + return None + return [site.get_raw() for site in value] def __init__( self, diff --git a/src/aiida/orm/nodes/node.py b/src/aiida/orm/nodes/node.py index 129a059d89..975c616531 100644 --- a/src/aiida/orm/nodes/node.py +++ b/src/aiida/orm/nodes/node.py @@ -13,12 +13,26 @@ import base64 import datetime from functools import cached_property -from typing import TYPE_CHECKING, Any, ClassVar, Dict, Generic, Iterator, List, NoReturn, Optional, Tuple, Type, TypeVar +from typing import ( + TYPE_CHECKING, + Any, + ClassVar, + Dict, + Generic, + Iterator, + List, + NoReturn, + Optional, + Tuple, + Type, + TypeVar, + cast, +) from uuid import UUID from typing_extensions import Self -from aiida.common import exceptions +from aiida.common import exceptions, timezone from aiida.common.lang import classproperty, type_check from aiida.common.links import LinkType from aiida.common.log import AIIDA_LOGGER @@ -196,11 +210,17 @@ def _query_type_string(cls) -> str: # noqa: N805 _unstorable_message = 'only Data, WorkflowNode, CalculationNode or their subclasses can be stored' class Model(Entity.Model): - uuid: Optional[str] = MetadataField( - None, description='The UUID of the node', is_attribute=False, exclude_to_orm=True, exclude_from_cli=True + uuid: UUID = MetadataField( + description='The UUID of the node', + is_attribute=False, + exclude_to_orm=True, + exclude_from_cli=True, ) - node_type: Optional[str] = MetadataField( - None, description='The type of the node', is_attribute=False, exclude_to_orm=True, exclude_from_cli=True + node_type: str = MetadataField( + description='The type of the node', + is_attribute=False, + exclude_to_orm=True, + exclude_from_cli=True, ) process_type: Optional[str] = MetadataField( None, @@ -209,76 +229,81 @@ class Model(Entity.Model): exclude_to_orm=True, exclude_from_cli=True, ) - repository_metadata: Optional[Dict[str, Any]] = MetadataField( - None, + repository_metadata: Dict[str, Any] = MetadataField( + default_factory=dict, description='Virtual hierarchy of the file repository.', is_attribute=False, orm_to_model=lambda node, _: node.base.repository.metadata, # type: ignore[attr-defined] exclude_to_orm=True, exclude_from_cli=True, ) - ctime: Optional[datetime.datetime] = MetadataField( - None, + ctime: datetime.datetime = MetadataField( + default_factory=timezone.now, description='The creation time of the node', is_attribute=False, exclude_to_orm=True, exclude_from_cli=True, ) - mtime: Optional[datetime.datetime] = MetadataField( - None, + mtime: datetime.datetime = MetadataField( + default_factory=timezone.now, description='The modification time of the node', is_attribute=False, exclude_to_orm=True, exclude_from_cli=True, ) - label: Optional[str] = MetadataField( - None, description='The node label', is_attribute=False, exclude_from_cli=True + label: str = MetadataField( + '', + description='The node label', + is_attribute=False, ) - description: Optional[str] = MetadataField( - None, description='The node description', is_attribute=False, exclude_from_cli=True + description: str = MetadataField( + '', + description='The node description', + is_attribute=False, ) - attributes: Optional[Dict[str, Any]] = MetadataField( - None, + attributes: Dict[str, Any] = MetadataField( + default_factory=dict, 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, _: cast('Node', node).base.attributes.all, is_subscriptable=True, exclude_from_cli=True, exclude_to_orm=True, ) - extras: Optional[Dict[str, Any]] = MetadataField( - None, + extras: Dict[str, Any] = MetadataField( + default_factory=dict, description='The node extras', is_attribute=False, - orm_to_model=lambda node, _: node.base.extras.all, # type: ignore[attr-defined] + orm_to_model=lambda node, _: cast('Node', node).base.extras.all, is_subscriptable=True, exclude_from_cli=True, - exclude_to_orm=True, ) computer: Optional[int] = MetadataField( None, description='The PK of the computer', is_attribute=False, - orm_to_model=lambda node, _: node.computer.pk if node.computer else None, # type: ignore[attr-defined] + orm_to_model=lambda node, _: cast('Node', node).computer.pk if cast('Node', node).computer else None, orm_class=Computer, exclude_from_cli=True, + exclude_to_orm=True, ) - user: Optional[int] = MetadataField( - None, + user: int = MetadataField( + default_factory=lambda: User.collection.get_default().pk, description='The PK of the user who owns the node', is_attribute=False, - orm_to_model=lambda node, _: node.user.pk, # type: ignore[attr-defined] + orm_to_model=lambda node, _: cast('Node', node).user.pk, orm_class=User, + exclude_to_orm=True, exclude_from_cli=True, ) - repository_content: Optional[dict[str, bytes]] = MetadataField( - None, + repository_content: dict[str, bytes] = MetadataField( + default_factory=dict, description='Dictionary of file repository content. Keys are relative filepaths and values are binary file ' 'contents encoded as base64.', is_attribute=False, orm_to_model=lambda node, _: { key: base64.encodebytes(content) - for key, content in node.base.repository.serialize_content().items() # type: ignore[attr-defined] + for key, content in cast('Node', node).base.repository.serialize_content().items() }, exclude_from_cli=True, exclude_to_orm=True, diff --git a/src/aiida/orm/nodes/process/calculation/calcjob.py b/src/aiida/orm/nodes/process/calculation/calcjob.py index a526fc3b9c..d74e7f6cde 100644 --- a/src/aiida/orm/nodes/process/calculation/calcjob.py +++ b/src/aiida/orm/nodes/process/calculation/calcjob.py @@ -9,7 +9,7 @@ """Module with `Node` sub class for calculation job processes.""" import datetime -from typing import TYPE_CHECKING, Any, AnyStr, Dict, List, Optional, Sequence, Tuple, Type, Union +from typing import TYPE_CHECKING, Any, AnyStr, Dict, List, Optional, Sequence, Tuple, Type, Union, cast from aiida.common import exceptions from aiida.common.datastructures import CalcJobState @@ -66,40 +66,56 @@ class CalcJobNode(CalculationNode): class Model(CalculationNode.Model): scheduler_state: Optional[str] = MetadataField( - description='The state of the scheduler', orm_to_model=lambda node, _: node.get_scheduler_state() + None, + description='The state of the scheduler', + orm_to_model=lambda node, _: cast('CalcJobNode', node).get_scheduler_state(), ) state: Optional[str] = MetadataField( - description='The active state of the calculation job', orm_to_model=lambda node, _: node.get_state() + None, + description='The active state of the calculation job', + orm_to_model=lambda node, _: cast('CalcJobNode', node).get_state(), ) remote_workdir: Optional[str] = MetadataField( + None, description='The path to the remote (on cluster) scratch folder', - orm_to_model=lambda node, _: node.get_remote_workdir(), + orm_to_model=lambda node, _: cast('CalcJobNode', node).get_remote_workdir(), ) job_id: Optional[str] = MetadataField( - description='The scheduler job id', orm_to_model=lambda node, _: node.get_job_id() + None, + description='The scheduler job id', + orm_to_model=lambda node, _: cast('CalcJobNode', node).get_job_id(), ) scheduler_lastchecktime: Optional[datetime.datetime] = MetadataField( + None, description='The last time the scheduler was checked, in isoformat', - orm_to_model=lambda node, _: node.get_scheduler_lastchecktime(), + orm_to_model=lambda node, _: cast('CalcJobNode', node).get_scheduler_lastchecktime(), + exclude_to_orm=True, ) last_job_info: Optional[dict] = MetadataField( + None, description='The last job info returned by the scheduler', - orm_to_model=lambda node, _: dict(node.get_last_job_info() or {}), + orm_to_model=lambda node, _: dict(cast('CalcJobNode', node).get_last_job_info() or {}), ) detailed_job_info: Optional[dict] = MetadataField( + None, description='The detailed job info returned by the scheduler', - orm_to_model=lambda node, _: node.get_detailed_job_info(), + orm_to_model=lambda node, _: cast('CalcJobNode', node).get_detailed_job_info(), ) - retrieve_list: Optional[List[str]] = MetadataField( + retrieve_list: Optional[Sequence[Union[str, Tuple[str, str, int]]]] = MetadataField( + None, description='The list of files to retrieve from the remote cluster', - orm_to_model=lambda node, _: node.get_retrieve_list(), + orm_to_model=lambda node, _: cast('CalcJobNode', node).get_retrieve_list(), ) - retrieve_temporary_list: Optional[List[str]] = MetadataField( + retrieve_temporary_list: Optional[Sequence[Union[str, Tuple[str, str, int]]]] = MetadataField( + None, description='The list of temporary files to retrieve from the remote cluster', - orm_to_model=lambda node, _: node.get_retrieve_temporary_list(), + orm_to_model=lambda node, _: cast('CalcJobNode', node).get_retrieve_temporary_list(), ) imported: Optional[bool] = MetadataField( - description='Whether the node has been migrated', orm_to_model=lambda node, _: node.is_imported + None, + description='Whether the node has been migrated', + orm_to_model=lambda node, _: cast('CalcJobNode', node).is_imported, + exclude_to_orm=True, ) # An optional entry point for a CalculationTools instance @@ -259,7 +275,7 @@ def get_remote_workdir(self) -> Optional[str]: return self.base.attributes.get(self.REMOTE_WORKDIR_KEY, None) @staticmethod - def _validate_retrieval_directive(directives: Sequence[Union[str, Tuple[str, str, str]]]) -> None: + def _validate_retrieval_directive(directives: Sequence[Union[str, Tuple[str, str, int]]]) -> None: """Validate a list or tuple of file retrieval directives. :param directives: a list or tuple of file retrieval directives @@ -286,7 +302,7 @@ def _validate_retrieval_directive(directives: Sequence[Union[str, Tuple[str, str if not isinstance(directive[2], (int, type(None))): raise ValueError('invalid directive, third element has to be an integer representing the depth') - def set_retrieve_list(self, retrieve_list: Sequence[Union[str, Tuple[str, str, str]]]) -> None: + def set_retrieve_list(self, retrieve_list: Sequence[Union[str, Tuple[str, str, int]]]) -> None: """Set the retrieve list. This list of directives will instruct the daemon what files to retrieve after the calculation has completed. @@ -297,14 +313,14 @@ def set_retrieve_list(self, retrieve_list: Sequence[Union[str, Tuple[str, str, s self._validate_retrieval_directive(retrieve_list) self.base.attributes.set(self.RETRIEVE_LIST_KEY, retrieve_list) - def get_retrieve_list(self) -> Optional[Sequence[Union[str, Tuple[str, str, str]]]]: + def get_retrieve_list(self) -> Optional[Sequence[Union[str, Tuple[str, str, int]]]]: """Return the list of files/directories to be retrieved on the cluster after the calculation has completed. :return: a list of file directives """ return self.base.attributes.get(self.RETRIEVE_LIST_KEY, None) - def set_retrieve_temporary_list(self, retrieve_temporary_list: Sequence[Union[str, Tuple[str, str, str]]]) -> None: + def set_retrieve_temporary_list(self, retrieve_temporary_list: Sequence[Union[str, Tuple[str, str, int]]]) -> None: """Set the retrieve temporary list. The retrieve temporary list stores files that are retrieved after completion and made available during parsing @@ -315,7 +331,7 @@ def set_retrieve_temporary_list(self, retrieve_temporary_list: Sequence[Union[st self._validate_retrieval_directive(retrieve_temporary_list) self.base.attributes.set(self.RETRIEVE_TEMPORARY_LIST_KEY, retrieve_temporary_list) - def get_retrieve_temporary_list(self) -> Optional[Sequence[Union[str, Tuple[str, str, str]]]]: + def get_retrieve_temporary_list(self) -> Optional[Sequence[Union[str, Tuple[str, str, int]]]]: """Return list of files to be retrieved from the cluster which will be available during parsing. :return: a list of file directives @@ -399,6 +415,11 @@ def set_last_job_info(self, last_job_info: 'JobInfo') -> None: :param last_job_info: a `JobInfo` object """ + from aiida.schedulers.datastructures import JobInfo + + if not isinstance(last_job_info, JobInfo): + raise ValueError(f'last job info should be an instance of JobInfo, got: {last_job_info}') + self.base.attributes.set(self.SCHEDULER_LAST_JOB_INFO_KEY, last_job_info.get_dict()) def get_last_job_info(self) -> Optional['JobInfo']: diff --git a/src/aiida/orm/nodes/process/process.py b/src/aiida/orm/nodes/process/process.py index 37369d14f6..507d69457a 100644 --- a/src/aiida/orm/nodes/process/process.py +++ b/src/aiida/orm/nodes/process/process.py @@ -189,13 +189,35 @@ def _updatable_attributes(cls) -> Tuple[str, ...]: # noqa: N805 ) class Model(Node.Model, Sealable.Model): - process_label: Optional[str] = MetadataField(description='The process label') - process_state: Optional[str] = MetadataField(description='The process state enum') - process_status: Optional[str] = MetadataField(description='The process status is a generic status message') - exit_status: Optional[int] = MetadataField(description='The process exit status') - exit_message: Optional[str] = MetadataField(description='The process exit message') - exception: Optional[str] = MetadataField(description='The process exception message') - paused: bool = MetadataField(description='Whether the process is paused') + process_label: Optional[str] = MetadataField( + None, + description='The process label', + ) + process_state: Optional[str] = MetadataField( + None, + description='The process state enum', + ) + process_status: Optional[str] = MetadataField( + None, + description='The process status is a generic status message', + ) + exit_status: Optional[int] = MetadataField( + None, + description='The process exit status', + ) + exit_message: Optional[str] = MetadataField( + None, + description='The process exit message', + ) + exception: Optional[str] = MetadataField( + None, + description='The process exception message', + ) + paused: Optional[bool] = MetadataField( + None, + description='Whether the process is paused', + exclude_to_orm=True, + ) def set_metadata_inputs(self, value: Dict[str, Any]) -> None: """Set the mapping of inputs corresponding to ``metadata`` ports that were passed to the process.""" @@ -462,10 +484,10 @@ def exit_status(self) -> Optional[int]: """ return self.base.attributes.get(self.EXIT_STATUS_KEY, None) - def set_exit_status(self, status: Union[None, enum.Enum, int]) -> None: + def set_exit_status(self, status: Optional[Union[enum.Enum, int]] = None) -> None: """Set the exit status of the process - :param state: an integer exit code or None, which will be interpreted as zero + :param status: the exit status, an integer exit code, or None """ if status is None: status = 0 diff --git a/src/aiida/orm/users.py b/src/aiida/orm/users.py index bb091b9fa0..50b02280fb 100644 --- a/src/aiida/orm/users.py +++ b/src/aiida/orm/users.py @@ -54,10 +54,25 @@ class User(entities.Entity['BackendUser', UserCollection]): _CLS_COLLECTION = UserCollection class Model(entities.Entity.Model): - email: str = MetadataField(description='The user email', is_attribute=False) - first_name: str = MetadataField(description='The user first name', is_attribute=False) - last_name: str = MetadataField(description='The user last name', is_attribute=False) - institution: str = MetadataField(description='The user institution', is_attribute=False) + email: str = MetadataField( + description='The user email', + is_attribute=False, + ) + first_name: str = MetadataField( + '', + description='The user first name', + is_attribute=False, + ) + last_name: str = MetadataField( + '', + description='The user last name', + is_attribute=False, + ) + institution: str = MetadataField( + '', + description='The user institution', + is_attribute=False, + ) def __init__( self, diff --git a/src/aiida/orm/utils/mixins.py b/src/aiida/orm/utils/mixins.py index 4d8379079a..1e5c8403e9 100644 --- a/src/aiida/orm/utils/mixins.py +++ b/src/aiida/orm/utils/mixins.py @@ -184,7 +184,10 @@ class Sealable: SEALED_KEY = 'sealed' class Model(pydantic.BaseModel, defer_build=True): - sealed: bool = MetadataField(description='Whether the node is sealed') + sealed: bool = MetadataField( + description='Whether the node is sealed', + exclude_to_orm=True, + ) @classproperty def _updatable_attributes(cls) -> tuple[str, ...]: # noqa: N805 diff --git a/src/aiida/storage/psql_dos/orm/querybuilder/main.py b/src/aiida/storage/psql_dos/orm/querybuilder/main.py index 70e18620b7..8167ef4a88 100644 --- a/src/aiida/storage/psql_dos/orm/querybuilder/main.py +++ b/src/aiida/storage/psql_dos/orm/querybuilder/main.py @@ -60,6 +60,9 @@ 'computer_pk': 'dbcomputer_id', 'user_pk': 'user_id', }, + 'db_dbuser': { + 'pk': 'id', + }, 'db_dbcomputer': { 'pk': 'id', }, From 85da446f1c32d41e77a74d477ac9580bb51a17e3 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Wed, 8 Oct 2025 18:00:30 +0200 Subject: [PATCH 02/59] Add Kpoints constructor --- src/aiida/orm/nodes/data/array/kpoints.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/aiida/orm/nodes/data/array/kpoints.py b/src/aiida/orm/nodes/data/array/kpoints.py index 7f8c2a0a39..5a9b307490 100644 --- a/src/aiida/orm/nodes/data/array/kpoints.py +++ b/src/aiida/orm/nodes/data/array/kpoints.py @@ -11,6 +11,8 @@ periodic crystal structure). """ +from __future__ import annotations + import typing as t import numpy From 2eedd020a52668dd25558ec71327524e5cb2ba83 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Wed, 8 Oct 2025 18:29:06 +0200 Subject: [PATCH 03/59] Fix `Node.from_model` --- src/aiida/orm/nodes/node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aiida/orm/nodes/node.py b/src/aiida/orm/nodes/node.py index 975c616531..8e6490eb24 100644 --- a/src/aiida/orm/nodes/node.py +++ b/src/aiida/orm/nodes/node.py @@ -336,7 +336,7 @@ def __init__( self.base.extras.set_many(extras) @classmethod - def _from_model(cls, model: Model) -> Self: # type: ignore[override] + def from_model(cls, model: Model) -> Self: # type: ignore[override] """Return an entity instance from an instance of its model.""" fields = cls.model_to_orm_field_values(model) From 1c767b9a89647728e4543b6b96ca334ac4369cfa Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Wed, 8 Oct 2025 18:37:22 +0200 Subject: [PATCH 04/59] Missed cast --- src/aiida/orm/nodes/node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aiida/orm/nodes/node.py b/src/aiida/orm/nodes/node.py index 8e6490eb24..ca44f6df46 100644 --- a/src/aiida/orm/nodes/node.py +++ b/src/aiida/orm/nodes/node.py @@ -233,7 +233,7 @@ class Model(Entity.Model): default_factory=dict, description='Virtual hierarchy of the file repository.', is_attribute=False, - orm_to_model=lambda node, _: node.base.repository.metadata, # type: ignore[attr-defined] + orm_to_model=lambda node, _: cast('Node', node).base.repository.metadata, exclude_to_orm=True, exclude_from_cli=True, ) From fd7dd8261185e817dc7555f43154371d1e33bb91 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Wed, 8 Oct 2025 18:49:29 +0200 Subject: [PATCH 05/59] Discard direct `orm` import from `cmdline` package --- src/aiida/cmdline/commands/cmd_code.py | 8 +++++--- src/aiida/cmdline/groups/dynamic.py | 5 ++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/aiida/cmdline/commands/cmd_code.py b/src/aiida/cmdline/commands/cmd_code.py index 9e59f5ee53..004b876ebe 100644 --- a/src/aiida/cmdline/commands/cmd_code.py +++ b/src/aiida/cmdline/commands/cmd_code.py @@ -12,11 +12,10 @@ import warnings from collections import defaultdict from functools import partial -from typing import Any +from typing import TYPE_CHECKING, Any import click -from aiida import orm from aiida.cmdline.commands.cmd_data.cmd_export import data_export from aiida.cmdline.commands.cmd_verdi import verdi from aiida.cmdline.groups.dynamic import DynamicEntryPointCommandGroup @@ -27,13 +26,16 @@ from aiida.cmdline.utils.decorators import with_dbenv from aiida.common import exceptions +if TYPE_CHECKING: + from aiida.orm import Code + @verdi.group('code') def verdi_code(): """Setup and manage codes.""" -def create_code(ctx: click.Context, cls: orm.Code, **kwargs) -> None: +def create_code(ctx: click.Context, cls: 'Code', **kwargs) -> None: """Create a new `Code` instance.""" try: Model = cls.Model.as_input_model() diff --git a/src/aiida/cmdline/groups/dynamic.py b/src/aiida/cmdline/groups/dynamic.py index df310fcf1e..415910a05e 100644 --- a/src/aiida/cmdline/groups/dynamic.py +++ b/src/aiida/cmdline/groups/dynamic.py @@ -8,7 +8,6 @@ import click -from aiida import orm from aiida.common import exceptions from aiida.plugins.entry_point import ENTRY_POINT_GROUP_FACTORY_MAPPING, get_entry_point_names from aiida.plugins.factories import BaseFactory @@ -98,7 +97,7 @@ def call_command(self, ctx: click.Context, cls: t.Any, non_interactive: bool, ** if hasattr(cls, 'Model'): # The plugin defines a pydantic model: use it to validate the provided arguments - Model = cls.Model.as_input_model() if issubclass(cls, orm.Entity) else cls.Model + Model = cls.Model.as_input_model() if hasattr(cls.Model, 'as_input_model') else cls.Model try: Model(**kwargs) except ValidationError as exception: @@ -170,7 +169,7 @@ def list_options(self, entry_point: str) -> list[t.Callable[[FC], FC]]: options_spec = self.factory(entry_point).get_cli_options() # type: ignore[union-attr] return [self.create_option(*item) for item in options_spec] - Model = cls.Model.as_input_model() if issubclass(cls, orm.Entity) else cls.Model + Model = cls.Model.as_input_model() if hasattr(cls.Model, 'as_input_model') else cls.Model options_spec = {} From 201d7c13be11e23afdf89d19bee9def81ef8176c Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Wed, 8 Oct 2025 19:44:10 +0200 Subject: [PATCH 06/59] Allow unstored entity (de)serialization --- src/aiida/orm/entities.py | 40 ++++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/src/aiida/orm/entities.py b/src/aiida/orm/entities.py index 03c52a4d67..a45fbaf455 100644 --- a/src/aiida/orm/entities.py +++ b/src/aiida/orm/entities.py @@ -284,21 +284,34 @@ def model_to_orm_field_values(cls, model: Model) -> dict[str, Any]: return fields - def to_model(self, repository_path: Optional[pathlib.Path] = None) -> Model: - """Return the entity instance as an instance of its model.""" + def to_model(self, repository_path: Optional[pathlib.Path] = None, unstored: bool = False) -> Model: + """Return the entity instance as an instance of its model. + + :param repository_path: If the orm node has files in the repository, this path is used to read the repository + files from. If no path is specified a temporary path is created using the entities pk. + :param unstored: If True, the input version of the model is used (via `.as_input_model`), which strips + read-only fields, i.e., fields with `exclude_to_orm=True`. + :return: An instance of the entity's model class. + """ fields = {} - for key, field in self.Model.model_fields.items(): + Model = self.Model.as_input_model() if unstored else self.Model + + for key, field in Model.model_fields.items(): if orm_to_model := get_metadata(field, 'orm_to_model'): fields[key] = orm_to_model(self, repository_path) else: fields[key] = getattr(self, key) - return self.Model(**fields) + return Model(**fields) @classmethod def from_model(cls, model: Model) -> Self: - """Return an entity instance from an instance of its model.""" + """Return an entity instance from an instance of its model. + + :param model: An instance of the entity's model class. + :return: An instance of the entity class. + """ fields = cls.model_to_orm_field_values(model) return cls(**fields) @@ -306,6 +319,7 @@ def serialize( self, repository_path: Optional[pathlib.Path] = None, mode: Literal['json', 'python'] = 'json', + unstored: bool = False, ) -> dict[str, Any]: """Serialize the entity instance to JSON. @@ -314,6 +328,8 @@ def serialize( :param mode: The serialization mode, either 'json' or 'python'. The 'json' mode is the most strict and ensures that the output is JSON serializable, whereas the 'python' mode allows for more complex Python types, such as `datetime` objects. + :param unstored: If True, the input version of the model is used (via `.as_input_model`), which strips + read-only fields, i.e., fields with `exclude_to_orm=True`. :return: A dictionary that can be serialized to JSON. """ self.logger.warning( @@ -329,15 +345,21 @@ def serialize( raise ValueError(f'The repository_path `{repository_path}` does not exist.') if not repository_path.is_dir(): raise ValueError(f'The repository_path `{repository_path}` is not a directory.') - return self.to_model(repository_path).model_dump(mode=mode) + return self.to_model(repository_path, unstored=unstored).model_dump(mode=mode) @classmethod - def from_serialized(cls, **kwargs: dict[str, Any]) -> Self: - """Construct an entity instance from JSON serialized data.""" + def from_serialized(cls, unstored: bool = False, **kwargs: dict[str, Any]) -> Self: + """Construct an entity instance from JSON serialized data. + + :param unstored: If True, the input version of the model is used (via `.as_input_model`), which strips + read-only fields, i.e., fields with `exclude_to_orm=True`. + :return: An instance of the entity class. + """ cls._logger.warning( 'Serialization through pydantic is still an experimental feature and might break in future releases.' ) - return cls.from_model(cls.Model(**kwargs)) + Model = cls.Model.as_input_model() if unstored else cls.Model + return cls.from_model(Model(**kwargs)) @classproperty def objects(cls: EntityType) -> CollectionType: # noqa: N805 From 017f9febc324e67154fd269cbea5bd214b0dcadb Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Wed, 8 Oct 2025 19:44:20 +0200 Subject: [PATCH 07/59] Update tests --- tests/orm/data/code/test_abstract.py | 2 +- tests/orm/data/code/test_installed.py | 2 +- tests/orm/data/code/test_portable.py | 2 +- tests/orm/models/test_models.py | 10 ++--- tests/orm/test_fields/fields_AuthInfo.yml | 2 +- tests/orm/test_fields/fields_Comment.yml | 8 ++-- tests/orm/test_fields/fields_Computer.yml | 4 +- tests/orm/test_fields/fields_Group.yml | 12 +++--- tests/orm/test_fields/fields_Log.yml | 4 +- tests/orm/test_fields/fields_User.yml | 2 +- ...fields_aiida.data.core.array.ArrayData.yml | 31 +++++++------ ..._aiida.data.core.array.bands.BandsData.yml | 43 ++++++++++--------- ...da.data.core.array.kpoints.KpointsData.yml | 43 ++++++++++--------- ...a.core.array.projection.ProjectionData.yml | 31 +++++++------ ...a.core.array.trajectory.TrajectoryData.yml | 35 ++++++++------- ...fields_aiida.data.core.array.xy.XyData.yml | 31 +++++++------ .../fields_aiida.data.core.base.BaseType.yml | 31 +++++++------ .../fields_aiida.data.core.bool.Bool.yml | 33 +++++++------- .../fields_aiida.data.core.cif.CifData.yml | 33 +++++++------- .../fields_aiida.data.core.code.Code.yml | 27 ++++++------ ...a.data.core.code.abstract.AbstractCode.yml | 27 ++++++------ ...e.code.containerized.ContainerizedCode.yml | 29 ++++++------- ...data.core.code.installed.InstalledCode.yml | 29 ++++++------- ...a.data.core.code.portable.PortableCode.yml | 27 ++++++------ .../fields_aiida.data.core.dict.Dict.yml | 31 +++++++------ .../fields_aiida.data.core.enum.EnumData.yml | 31 +++++++------ .../fields_aiida.data.core.float.Float.yml | 33 +++++++------- ...elds_aiida.data.core.folder.FolderData.yml | 31 +++++++------ .../fields_aiida.data.core.int.Int.yml | 33 +++++++------- ..._aiida.data.core.jsonable.JsonableData.yml | 31 +++++++------ .../fields_aiida.data.core.list.List.yml | 31 +++++++------ ...ds_aiida.data.core.numeric.NumericType.yml | 31 +++++++------ ...ds_aiida.data.core.orbital.OrbitalData.yml | 31 +++++++------ ...elds_aiida.data.core.remote.RemoteData.yml | 31 +++++++------ ...data.core.remote.stash.RemoteStashData.yml | 31 +++++++------ ...ash.compress.RemoteStashCompressedData.yml | 31 +++++++------ ...ote.stash.custom.RemoteStashCustomData.yml | 31 +++++++------ ...ote.stash.folder.RemoteStashFolderData.yml | 31 +++++++------ ...da.data.core.singlefile.SinglefileData.yml | 33 +++++++------- .../fields_aiida.data.core.str.Str.yml | 33 +++++++------- ...iida.data.core.structure.StructureData.yml | 31 +++++++------ .../fields_aiida.data.core.upf.UpfData.yml | 33 +++++++------- 42 files changed, 519 insertions(+), 547 deletions(-) diff --git a/tests/orm/data/code/test_abstract.py b/tests/orm/data/code/test_abstract.py index 791595c9f4..df47eed8de 100644 --- a/tests/orm/data/code/test_abstract.py +++ b/tests/orm/data/code/test_abstract.py @@ -72,4 +72,4 @@ def test_serialization(): label = 'some-label' code = MockCode(label=label) - MockCode.from_serialized(**code.serialize()) + MockCode.from_serialized(unstored=True, **code.serialize(unstored=True)) diff --git a/tests/orm/data/code/test_installed.py b/tests/orm/data/code/test_installed.py index eb474bfd1b..7d2b13c02f 100644 --- a/tests/orm/data/code/test_installed.py +++ b/tests/orm/data/code/test_installed.py @@ -152,4 +152,4 @@ def test_serialization(aiida_localhost, bash_path): """Test the deprecated :meth:`aiida.orm.nodes.data.code.installed.InstalledCode.get_execname` method.""" code = InstalledCode(label='some-label', computer=aiida_localhost, filepath_executable=str(bash_path.absolute())) - InstalledCode.from_serialized(**code.serialize()) + InstalledCode.from_serialized(unstored=True, **code.serialize(unstored=True)) diff --git a/tests/orm/data/code/test_portable.py b/tests/orm/data/code/test_portable.py index 455804773e..bb09591281 100644 --- a/tests/orm/data/code/test_portable.py +++ b/tests/orm/data/code/test_portable.py @@ -176,4 +176,4 @@ def test_serialization(tmp_path, chdir_tmp_path): (filepath_files / 'subdir').mkdir() (filepath_files / 'subdir/test').write_text('test') code = PortableCode(label='some-label', filepath_executable='bash', filepath_files=filepath_files) - PortableCode.from_serialized(**code.serialize()) + PortableCode.from_serialized(unstored=True, **code.serialize(unstored=True)) diff --git a/tests/orm/models/test_models.py b/tests/orm/models/test_models.py index 7209a8fdaf..ddc1975b46 100644 --- a/tests/orm/models/test_models.py +++ b/tests/orm/models/test_models.py @@ -169,18 +169,18 @@ def test_roundtrip(required_arguments, tmp_path): assert isinstance(entity, cls) # Get the model instance from the entity instance - model = entity._to_model(tmp_path) + model = entity.to_model(tmp_path, unstored=True) assert isinstance(model, BaseModel) # Reconstruct the entity instance from the model instance - roundtrip = cls._from_model(model) + roundtrip = cls.from_model(model) assert isinstance(roundtrip, cls) # Get the model instance again from the reconstructed entity and check that the fields that would be passed to the # ORM entity constructor are identical of the original model. The ``model_to_orm_field_values`` excludes values of # fields that define ``exclude_to_orm=True`` because these can change during roundtrips. This because these # typically correspond to entity fields that have defaults set on the database level, e.g., UUIDs. - roundtrip_model = roundtrip._to_model(tmp_path) + roundtrip_model = roundtrip.to_model(tmp_path, unstored=True) original_field_values = cls.model_to_orm_field_values(model) for key, value in cls.model_to_orm_field_values(roundtrip_model).items(): @@ -206,5 +206,5 @@ def test_roundtrip_serialization(required_arguments, tmp_path): assert isinstance(entity, cls) # Get the model instance from the entity instance - serialized_entity = entity.serialize(tmp_path) - entity.from_serialized(**serialized_entity) + serialized_entity = entity.serialize(tmp_path, unstored=True, mode='python') + entity.from_serialized(unstored=True, **serialized_entity) diff --git a/tests/orm/test_fields/fields_AuthInfo.yml b/tests/orm/test_fields/fields_AuthInfo.yml index 505c96da91..91ba1e63f6 100644 --- a/tests/orm/test_fields/fields_AuthInfo.yml +++ b/tests/orm/test_fields/fields_AuthInfo.yml @@ -2,5 +2,5 @@ auth_params: QbDictField('auth_params', dtype=typing.Dict[str, typing.Any], is_a computer: QbNumericField('computer', dtype=, is_attribute=False) enabled: QbField('enabled', dtype=, is_attribute=False) metadata: QbDictField('metadata', dtype=typing.Dict[str, typing.Any], is_attribute=False) -pk: QbNumericField('pk', dtype=typing.Optional[int], is_attribute=False) +pk: QbNumericField('pk', dtype=, is_attribute=False) user: QbNumericField('user', dtype=, is_attribute=False) diff --git a/tests/orm/test_fields/fields_Comment.yml b/tests/orm/test_fields/fields_Comment.yml index 30aedcef79..f227b00247 100644 --- a/tests/orm/test_fields/fields_Comment.yml +++ b/tests/orm/test_fields/fields_Comment.yml @@ -1,7 +1,7 @@ content: QbStrField('content', dtype=, is_attribute=False) -ctime: QbNumericField('ctime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -mtime: QbNumericField('mtime', dtype=typing.Optional[datetime.datetime], is_attribute=False) +ctime: QbNumericField('ctime', dtype=, is_attribute=False) +mtime: QbNumericField('mtime', dtype=, is_attribute=False) node: QbNumericField('node', dtype=, is_attribute=False) -pk: QbNumericField('pk', dtype=typing.Optional[int], is_attribute=False) +pk: QbNumericField('pk', dtype=, is_attribute=False) user: QbNumericField('user', dtype=, is_attribute=False) -uuid: QbStrField('uuid', dtype=typing.Optional[str], is_attribute=False) +uuid: QbField('uuid', dtype=, is_attribute=False) diff --git a/tests/orm/test_fields/fields_Computer.yml b/tests/orm/test_fields/fields_Computer.yml index 7d4e37168b..fc980dec16 100644 --- a/tests/orm/test_fields/fields_Computer.yml +++ b/tests/orm/test_fields/fields_Computer.yml @@ -2,7 +2,7 @@ description: QbStrField('description', dtype=, is_attribute=False) hostname: QbStrField('hostname', dtype=, is_attribute=False) label: QbStrField('label', dtype=, is_attribute=False) metadata: QbDictField('metadata', dtype=typing.Dict[str, typing.Any], is_attribute=False) -pk: QbNumericField('pk', dtype=typing.Optional[int], is_attribute=False) +pk: QbNumericField('pk', dtype=, is_attribute=False) scheduler_type: QbStrField('scheduler_type', dtype=, is_attribute=False) transport_type: QbStrField('transport_type', dtype=, is_attribute=False) -uuid: QbStrField('uuid', dtype=, is_attribute=False) +uuid: QbField('uuid', dtype=, is_attribute=False) diff --git a/tests/orm/test_fields/fields_Group.yml b/tests/orm/test_fields/fields_Group.yml index 537c76a11d..1844039c7e 100644 --- a/tests/orm/test_fields/fields_Group.yml +++ b/tests/orm/test_fields/fields_Group.yml @@ -1,9 +1,9 @@ -description: QbStrField('description', dtype=typing.Optional[str], is_attribute=False) -extras: QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) +description: QbStrField('description', dtype=, is_attribute=False) +extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) label: QbStrField('label', dtype=, is_attribute=False) -pk: QbNumericField('pk', dtype=typing.Optional[int], is_attribute=False) -time: QbNumericField('time', dtype=typing.Optional[datetime.datetime], is_attribute=False) +pk: QbNumericField('pk', dtype=, is_attribute=False) +time: QbNumericField('time', dtype=, is_attribute=False) type_string: QbStrField('type_string', dtype=, is_attribute=False) user: QbNumericField('user', dtype=, is_attribute=False) -uuid: QbStrField('uuid', dtype=, is_attribute=False) +uuid: QbField('uuid', dtype=, is_attribute=False) diff --git a/tests/orm/test_fields/fields_Log.yml b/tests/orm/test_fields/fields_Log.yml index 90bf0a5b0b..0cacd5ac55 100644 --- a/tests/orm/test_fields/fields_Log.yml +++ b/tests/orm/test_fields/fields_Log.yml @@ -3,6 +3,6 @@ levelname: QbStrField('levelname', dtype=, is_attribute=False) loggername: QbStrField('loggername', dtype=, is_attribute=False) message: QbStrField('message', dtype=, is_attribute=False) metadata: QbDictField('metadata', dtype=typing.Dict[str, typing.Any], is_attribute=False) -pk: QbNumericField('pk', dtype=typing.Optional[int], is_attribute=False) +pk: QbNumericField('pk', dtype=, is_attribute=False) time: QbNumericField('time', dtype=, is_attribute=False) -uuid: QbStrField('uuid', dtype=, is_attribute=False) +uuid: QbField('uuid', dtype=, is_attribute=False) diff --git a/tests/orm/test_fields/fields_User.yml b/tests/orm/test_fields/fields_User.yml index 06fb9cd05c..247accb431 100644 --- a/tests/orm/test_fields/fields_User.yml +++ b/tests/orm/test_fields/fields_User.yml @@ -2,4 +2,4 @@ email: QbStrField('email', dtype=, is_attribute=False) first_name: QbStrField('first_name', dtype=, is_attribute=False) institution: QbStrField('institution', dtype=, is_attribute=False) last_name: QbStrField('last_name', dtype=, is_attribute=False) -pk: QbNumericField('pk', dtype=typing.Optional[int], is_attribute=False) +pk: QbNumericField('pk', dtype=, is_attribute=False) diff --git a/tests/orm/test_fields/fields_aiida.data.core.array.ArrayData.yml b/tests/orm/test_fields/fields_aiida.data.core.array.ArrayData.yml index c31304e6f5..fc78fd8d8f 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.array.ArrayData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.array.ArrayData.yml @@ -1,20 +1,19 @@ arrays: QbDictField('arrays', dtype=typing.Optional[dict[str, bytes]], is_attribute=True) -attributes: QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) +attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) -ctime: QbNumericField('ctime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -description: QbStrField('description', dtype=typing.Optional[str], is_attribute=False) -extras: QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) -label: QbStrField('label', dtype=typing.Optional[str], is_attribute=False) -mtime: QbNumericField('mtime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -node_type: QbStrField('node_type', dtype=typing.Optional[str], is_attribute=False) -pk: QbNumericField('pk', dtype=typing.Optional[int], is_attribute=False) +ctime: QbNumericField('ctime', dtype=, is_attribute=False) +description: QbStrField('description', dtype=, is_attribute=False) +extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) +label: QbStrField('label', dtype=, is_attribute=False) +mtime: QbNumericField('mtime', dtype=, is_attribute=False) +node_type: QbStrField('node_type', dtype=, is_attribute=False) +pk: QbNumericField('pk', dtype=, is_attribute=False) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) -repository_content: QbDictField('repository_content', dtype=typing.Optional[dict[str, - bytes]], is_attribute=False) -repository_metadata: QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, - typing.Any]], is_attribute=False) +repository_content: QbDictField('repository_content', dtype=dict[str, bytes], is_attribute=False) +repository_metadata: QbDictField('repository_metadata', dtype=typing.Dict[str, typing.Any], + is_attribute=False) source: QbDictField('source', dtype=typing.Optional[dict], is_attribute=True, is_subscriptable=True) -user: QbNumericField('user', dtype=typing.Optional[int], is_attribute=False) -uuid: QbStrField('uuid', dtype=typing.Optional[str], is_attribute=False) +user: QbNumericField('user', dtype=, is_attribute=False) +uuid: QbField('uuid', dtype=, is_attribute=False) diff --git a/tests/orm/test_fields/fields_aiida.data.core.array.bands.BandsData.yml b/tests/orm/test_fields/fields_aiida.data.core.array.bands.BandsData.yml index f0bca48c81..73aae4db4f 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.array.bands.BandsData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.array.bands.BandsData.yml @@ -1,31 +1,32 @@ array_labels: QbArrayField('array_labels', dtype=typing.Optional[typing.List[str]], is_attribute=True) arrays: QbDictField('arrays', dtype=typing.Optional[dict[str, bytes]], is_attribute=True) -attributes: QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) -cell: QbArrayField('cell', dtype=typing.List[typing.List[float]], is_attribute=True) +attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) +cell: QbArrayField('cell', dtype=typing.Optional[typing.List[typing.List[float]]], + is_attribute=True) computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) -ctime: QbNumericField('ctime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -description: QbStrField('description', dtype=typing.Optional[str], is_attribute=False) -extras: QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) -label: QbStrField('label', dtype=typing.Optional[str], is_attribute=False) -label_numbers: QbArrayField('label_numbers', dtype=typing.List[int], is_attribute=True) -labels: QbArrayField('labels', dtype=typing.List[str], is_attribute=True) -mesh: QbArrayField('mesh', dtype=typing.List[int], is_attribute=True) -mtime: QbNumericField('mtime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -node_type: QbStrField('node_type', dtype=typing.Optional[str], is_attribute=False) -offset: QbArrayField('offset', dtype=typing.List[float], is_attribute=True) +ctime: QbNumericField('ctime', dtype=, is_attribute=False) +description: QbStrField('description', dtype=, is_attribute=False) +extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) +label: QbStrField('label', dtype=, is_attribute=False) +label_numbers: QbArrayField('label_numbers', dtype=typing.Optional[typing.List[int]], + is_attribute=True) +labels: QbArrayField('labels', dtype=typing.Optional[typing.List[str]], is_attribute=True) +mesh: QbArrayField('mesh', dtype=typing.Optional[typing.List[int]], is_attribute=True) +mtime: QbNumericField('mtime', dtype=, is_attribute=False) +node_type: QbStrField('node_type', dtype=, is_attribute=False) +offset: QbArrayField('offset', dtype=typing.Optional[typing.List[float]], is_attribute=True) pbc1: QbField('pbc1', dtype=, is_attribute=True) pbc2: QbField('pbc2', dtype=, is_attribute=True) pbc3: QbField('pbc3', dtype=, is_attribute=True) -pk: QbNumericField('pk', dtype=typing.Optional[int], is_attribute=False) +pk: QbNumericField('pk', dtype=, is_attribute=False) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) -repository_content: QbDictField('repository_content', dtype=typing.Optional[dict[str, - bytes]], is_attribute=False) -repository_metadata: QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, - typing.Any]], is_attribute=False) +repository_content: QbDictField('repository_content', dtype=dict[str, bytes], is_attribute=False) +repository_metadata: QbDictField('repository_metadata', dtype=typing.Dict[str, typing.Any], + is_attribute=False) source: QbDictField('source', dtype=typing.Optional[dict], is_attribute=True, is_subscriptable=True) units: QbStrField('units', dtype=, is_attribute=True) -user: QbNumericField('user', dtype=typing.Optional[int], is_attribute=False) -uuid: QbStrField('uuid', dtype=typing.Optional[str], is_attribute=False) +user: QbNumericField('user', dtype=, is_attribute=False) +uuid: QbField('uuid', dtype=, is_attribute=False) diff --git a/tests/orm/test_fields/fields_aiida.data.core.array.kpoints.KpointsData.yml b/tests/orm/test_fields/fields_aiida.data.core.array.kpoints.KpointsData.yml index 6d0aaa2b6f..3f307be7fc 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.array.kpoints.KpointsData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.array.kpoints.KpointsData.yml @@ -1,28 +1,29 @@ arrays: QbDictField('arrays', dtype=typing.Optional[dict[str, bytes]], is_attribute=True) -attributes: QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) -cell: QbArrayField('cell', dtype=typing.List[typing.List[float]], is_attribute=True) +attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) +cell: QbArrayField('cell', dtype=typing.Optional[typing.List[typing.List[float]]], + is_attribute=True) computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) -ctime: QbNumericField('ctime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -description: QbStrField('description', dtype=typing.Optional[str], is_attribute=False) -extras: QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) -label: QbStrField('label', dtype=typing.Optional[str], is_attribute=False) -label_numbers: QbArrayField('label_numbers', dtype=typing.List[int], is_attribute=True) -labels: QbArrayField('labels', dtype=typing.List[str], is_attribute=True) -mesh: QbArrayField('mesh', dtype=typing.List[int], is_attribute=True) -mtime: QbNumericField('mtime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -node_type: QbStrField('node_type', dtype=typing.Optional[str], is_attribute=False) -offset: QbArrayField('offset', dtype=typing.List[float], is_attribute=True) +ctime: QbNumericField('ctime', dtype=, is_attribute=False) +description: QbStrField('description', dtype=, is_attribute=False) +extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) +label: QbStrField('label', dtype=, is_attribute=False) +label_numbers: QbArrayField('label_numbers', dtype=typing.Optional[typing.List[int]], + is_attribute=True) +labels: QbArrayField('labels', dtype=typing.Optional[typing.List[str]], is_attribute=True) +mesh: QbArrayField('mesh', dtype=typing.Optional[typing.List[int]], is_attribute=True) +mtime: QbNumericField('mtime', dtype=, is_attribute=False) +node_type: QbStrField('node_type', dtype=, is_attribute=False) +offset: QbArrayField('offset', dtype=typing.Optional[typing.List[float]], is_attribute=True) pbc1: QbField('pbc1', dtype=, is_attribute=True) pbc2: QbField('pbc2', dtype=, is_attribute=True) pbc3: QbField('pbc3', dtype=, is_attribute=True) -pk: QbNumericField('pk', dtype=typing.Optional[int], is_attribute=False) +pk: QbNumericField('pk', dtype=, is_attribute=False) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) -repository_content: QbDictField('repository_content', dtype=typing.Optional[dict[str, - bytes]], is_attribute=False) -repository_metadata: QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, - typing.Any]], is_attribute=False) +repository_content: QbDictField('repository_content', dtype=dict[str, bytes], is_attribute=False) +repository_metadata: QbDictField('repository_metadata', dtype=typing.Dict[str, typing.Any], + is_attribute=False) source: QbDictField('source', dtype=typing.Optional[dict], is_attribute=True, is_subscriptable=True) -user: QbNumericField('user', dtype=typing.Optional[int], is_attribute=False) -uuid: QbStrField('uuid', dtype=typing.Optional[str], is_attribute=False) +user: QbNumericField('user', dtype=, is_attribute=False) +uuid: QbField('uuid', dtype=, is_attribute=False) diff --git a/tests/orm/test_fields/fields_aiida.data.core.array.projection.ProjectionData.yml b/tests/orm/test_fields/fields_aiida.data.core.array.projection.ProjectionData.yml index c31304e6f5..fc78fd8d8f 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.array.projection.ProjectionData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.array.projection.ProjectionData.yml @@ -1,20 +1,19 @@ arrays: QbDictField('arrays', dtype=typing.Optional[dict[str, bytes]], is_attribute=True) -attributes: QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) +attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) -ctime: QbNumericField('ctime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -description: QbStrField('description', dtype=typing.Optional[str], is_attribute=False) -extras: QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) -label: QbStrField('label', dtype=typing.Optional[str], is_attribute=False) -mtime: QbNumericField('mtime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -node_type: QbStrField('node_type', dtype=typing.Optional[str], is_attribute=False) -pk: QbNumericField('pk', dtype=typing.Optional[int], is_attribute=False) +ctime: QbNumericField('ctime', dtype=, is_attribute=False) +description: QbStrField('description', dtype=, is_attribute=False) +extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) +label: QbStrField('label', dtype=, is_attribute=False) +mtime: QbNumericField('mtime', dtype=, is_attribute=False) +node_type: QbStrField('node_type', dtype=, is_attribute=False) +pk: QbNumericField('pk', dtype=, is_attribute=False) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) -repository_content: QbDictField('repository_content', dtype=typing.Optional[dict[str, - bytes]], is_attribute=False) -repository_metadata: QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, - typing.Any]], is_attribute=False) +repository_content: QbDictField('repository_content', dtype=dict[str, bytes], is_attribute=False) +repository_metadata: QbDictField('repository_metadata', dtype=typing.Dict[str, typing.Any], + is_attribute=False) source: QbDictField('source', dtype=typing.Optional[dict], is_attribute=True, is_subscriptable=True) -user: QbNumericField('user', dtype=typing.Optional[int], is_attribute=False) -uuid: QbStrField('uuid', dtype=typing.Optional[str], is_attribute=False) +user: QbNumericField('user', dtype=, is_attribute=False) +uuid: QbField('uuid', dtype=, is_attribute=False) diff --git a/tests/orm/test_fields/fields_aiida.data.core.array.trajectory.TrajectoryData.yml b/tests/orm/test_fields/fields_aiida.data.core.array.trajectory.TrajectoryData.yml index 87aaa30148..471dc80bbd 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.array.trajectory.TrajectoryData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.array.trajectory.TrajectoryData.yml @@ -1,23 +1,22 @@ arrays: QbDictField('arrays', dtype=typing.Optional[dict[str, bytes]], is_attribute=True) -attributes: QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) +attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) -ctime: QbNumericField('ctime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -description: QbStrField('description', dtype=typing.Optional[str], is_attribute=False) -extras: QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) -label: QbStrField('label', dtype=typing.Optional[str], is_attribute=False) -mtime: QbNumericField('mtime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -node_type: QbStrField('node_type', dtype=typing.Optional[str], is_attribute=False) -pk: QbNumericField('pk', dtype=typing.Optional[int], is_attribute=False) +ctime: QbNumericField('ctime', dtype=, is_attribute=False) +description: QbStrField('description', dtype=, is_attribute=False) +extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) +label: QbStrField('label', dtype=, is_attribute=False) +mtime: QbNumericField('mtime', dtype=, is_attribute=False) +node_type: QbStrField('node_type', dtype=, is_attribute=False) +pk: QbNumericField('pk', dtype=, is_attribute=False) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) -repository_content: QbDictField('repository_content', dtype=typing.Optional[dict[str, - bytes]], is_attribute=False) -repository_metadata: QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, - typing.Any]], is_attribute=False) +repository_content: QbDictField('repository_content', dtype=dict[str, bytes], is_attribute=False) +repository_metadata: QbDictField('repository_metadata', dtype=typing.Dict[str, typing.Any], + is_attribute=False) source: QbDictField('source', dtype=typing.Optional[dict], is_attribute=True, is_subscriptable=True) symbols: QbArrayField('symbols', dtype=typing.List[str], is_attribute=True) -units_positions: QbStrField('units_positions', dtype=, is_attribute=True) -units_times: QbStrField('units_times', dtype=, is_attribute=True) -user: QbNumericField('user', dtype=typing.Optional[int], is_attribute=False) -uuid: QbStrField('uuid', dtype=typing.Optional[str], is_attribute=False) +units_positions: QbStrField('units_positions', dtype=typing.Optional[str], is_attribute=True) +units_times: QbStrField('units_times', dtype=typing.Optional[str], is_attribute=True) +user: QbNumericField('user', dtype=, is_attribute=False) +uuid: QbField('uuid', dtype=, is_attribute=False) diff --git a/tests/orm/test_fields/fields_aiida.data.core.array.xy.XyData.yml b/tests/orm/test_fields/fields_aiida.data.core.array.xy.XyData.yml index c31304e6f5..fc78fd8d8f 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.array.xy.XyData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.array.xy.XyData.yml @@ -1,20 +1,19 @@ arrays: QbDictField('arrays', dtype=typing.Optional[dict[str, bytes]], is_attribute=True) -attributes: QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) +attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) -ctime: QbNumericField('ctime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -description: QbStrField('description', dtype=typing.Optional[str], is_attribute=False) -extras: QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) -label: QbStrField('label', dtype=typing.Optional[str], is_attribute=False) -mtime: QbNumericField('mtime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -node_type: QbStrField('node_type', dtype=typing.Optional[str], is_attribute=False) -pk: QbNumericField('pk', dtype=typing.Optional[int], is_attribute=False) +ctime: QbNumericField('ctime', dtype=, is_attribute=False) +description: QbStrField('description', dtype=, is_attribute=False) +extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) +label: QbStrField('label', dtype=, is_attribute=False) +mtime: QbNumericField('mtime', dtype=, is_attribute=False) +node_type: QbStrField('node_type', dtype=, is_attribute=False) +pk: QbNumericField('pk', dtype=, is_attribute=False) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) -repository_content: QbDictField('repository_content', dtype=typing.Optional[dict[str, - bytes]], is_attribute=False) -repository_metadata: QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, - typing.Any]], is_attribute=False) +repository_content: QbDictField('repository_content', dtype=dict[str, bytes], is_attribute=False) +repository_metadata: QbDictField('repository_metadata', dtype=typing.Dict[str, typing.Any], + is_attribute=False) source: QbDictField('source', dtype=typing.Optional[dict], is_attribute=True, is_subscriptable=True) -user: QbNumericField('user', dtype=typing.Optional[int], is_attribute=False) -uuid: QbStrField('uuid', dtype=typing.Optional[str], is_attribute=False) +user: QbNumericField('user', dtype=, is_attribute=False) +uuid: QbField('uuid', dtype=, is_attribute=False) diff --git a/tests/orm/test_fields/fields_aiida.data.core.base.BaseType.yml b/tests/orm/test_fields/fields_aiida.data.core.base.BaseType.yml index 457621f596..e92d2eee53 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.base.BaseType.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.base.BaseType.yml @@ -1,20 +1,19 @@ -attributes: QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) +attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) -ctime: QbNumericField('ctime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -description: QbStrField('description', dtype=typing.Optional[str], is_attribute=False) -extras: QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) -label: QbStrField('label', dtype=typing.Optional[str], is_attribute=False) -mtime: QbNumericField('mtime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -node_type: QbStrField('node_type', dtype=typing.Optional[str], is_attribute=False) -pk: QbNumericField('pk', dtype=typing.Optional[int], is_attribute=False) +ctime: QbNumericField('ctime', dtype=, is_attribute=False) +description: QbStrField('description', dtype=, is_attribute=False) +extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) +label: QbStrField('label', dtype=, is_attribute=False) +mtime: QbNumericField('mtime', dtype=, is_attribute=False) +node_type: QbStrField('node_type', dtype=, is_attribute=False) +pk: QbNumericField('pk', dtype=, is_attribute=False) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) -repository_content: QbDictField('repository_content', dtype=typing.Optional[dict[str, - bytes]], is_attribute=False) -repository_metadata: QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, - typing.Any]], is_attribute=False) +repository_content: QbDictField('repository_content', dtype=dict[str, bytes], is_attribute=False) +repository_metadata: QbDictField('repository_metadata', dtype=typing.Dict[str, typing.Any], + is_attribute=False) source: QbDictField('source', dtype=typing.Optional[dict], is_attribute=True, is_subscriptable=True) -user: QbNumericField('user', dtype=typing.Optional[int], is_attribute=False) -uuid: QbStrField('uuid', dtype=typing.Optional[str], is_attribute=False) +user: QbNumericField('user', dtype=, is_attribute=False) +uuid: QbField('uuid', dtype=, is_attribute=False) value: QbField('value', dtype=typing.Any, is_attribute=True) diff --git a/tests/orm/test_fields/fields_aiida.data.core.bool.Bool.yml b/tests/orm/test_fields/fields_aiida.data.core.bool.Bool.yml index 457621f596..1eca0fdc91 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.bool.Bool.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.bool.Bool.yml @@ -1,20 +1,19 @@ -attributes: QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) +attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) -ctime: QbNumericField('ctime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -description: QbStrField('description', dtype=typing.Optional[str], is_attribute=False) -extras: QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) -label: QbStrField('label', dtype=typing.Optional[str], is_attribute=False) -mtime: QbNumericField('mtime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -node_type: QbStrField('node_type', dtype=typing.Optional[str], is_attribute=False) -pk: QbNumericField('pk', dtype=typing.Optional[int], is_attribute=False) +ctime: QbNumericField('ctime', dtype=, is_attribute=False) +description: QbStrField('description', dtype=, is_attribute=False) +extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) +label: QbStrField('label', dtype=, is_attribute=False) +mtime: QbNumericField('mtime', dtype=, is_attribute=False) +node_type: QbStrField('node_type', dtype=, is_attribute=False) +pk: QbNumericField('pk', dtype=, is_attribute=False) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) -repository_content: QbDictField('repository_content', dtype=typing.Optional[dict[str, - bytes]], is_attribute=False) -repository_metadata: QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, - typing.Any]], is_attribute=False) +repository_content: QbDictField('repository_content', dtype=dict[str, bytes], is_attribute=False) +repository_metadata: QbDictField('repository_metadata', dtype=typing.Dict[str, typing.Any], + is_attribute=False) source: QbDictField('source', dtype=typing.Optional[dict], is_attribute=True, is_subscriptable=True) -user: QbNumericField('user', dtype=typing.Optional[int], is_attribute=False) -uuid: QbStrField('uuid', dtype=typing.Optional[str], is_attribute=False) -value: QbField('value', dtype=typing.Any, is_attribute=True) +user: QbNumericField('user', dtype=, is_attribute=False) +uuid: QbField('uuid', dtype=, is_attribute=False) +value: QbField('value', dtype=, is_attribute=True) diff --git a/tests/orm/test_fields/fields_aiida.data.core.cif.CifData.yml b/tests/orm/test_fields/fields_aiida.data.core.cif.CifData.yml index da4e1d40d9..a4a5cf39d1 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.cif.CifData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.cif.CifData.yml @@ -1,25 +1,24 @@ -attributes: QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) +attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) content: QbField('content', dtype=, is_attribute=True) -ctime: QbNumericField('ctime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -description: QbStrField('description', dtype=typing.Optional[str], is_attribute=False) -extras: QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) -filename: QbStrField('filename', dtype=typing.Optional[str], is_attribute=True) +ctime: QbNumericField('ctime', dtype=, is_attribute=False) +description: QbStrField('description', dtype=, is_attribute=False) +extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) +filename: QbStrField('filename', dtype=, is_attribute=True) formulae: QbArrayField('formulae', dtype=typing.Optional[typing.List[str]], is_attribute=True) -label: QbStrField('label', dtype=typing.Optional[str], is_attribute=False) +label: QbStrField('label', dtype=, is_attribute=False) md5: QbStrField('md5', dtype=typing.Optional[str], is_attribute=True) -mtime: QbNumericField('mtime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -node_type: QbStrField('node_type', dtype=typing.Optional[str], is_attribute=False) -pk: QbNumericField('pk', dtype=typing.Optional[int], is_attribute=False) +mtime: QbNumericField('mtime', dtype=, is_attribute=False) +node_type: QbStrField('node_type', dtype=, is_attribute=False) +pk: QbNumericField('pk', dtype=, is_attribute=False) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) -repository_content: QbDictField('repository_content', dtype=typing.Optional[dict[str, - bytes]], is_attribute=False) -repository_metadata: QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, - typing.Any]], is_attribute=False) +repository_content: QbDictField('repository_content', dtype=dict[str, bytes], is_attribute=False) +repository_metadata: QbDictField('repository_metadata', dtype=typing.Dict[str, typing.Any], + is_attribute=False) source: QbDictField('source', dtype=typing.Optional[dict], is_attribute=True, is_subscriptable=True) spacegroup_numbers: QbArrayField('spacegroup_numbers', dtype=typing.Optional[typing.List[str]], is_attribute=True) -user: QbNumericField('user', dtype=typing.Optional[int], is_attribute=False) -uuid: QbStrField('uuid', dtype=typing.Optional[str], is_attribute=False) +user: QbNumericField('user', dtype=, is_attribute=False) +uuid: QbField('uuid', dtype=, is_attribute=False) diff --git a/tests/orm/test_fields/fields_aiida.data.core.code.Code.yml b/tests/orm/test_fields/fields_aiida.data.core.code.Code.yml index 8ebaa0804d..9d42d122e3 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.code.Code.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.code.Code.yml @@ -1,29 +1,28 @@ append_text: QbStrField('append_text', dtype=, is_attribute=True) -attributes: QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) +attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) -ctime: QbNumericField('ctime', dtype=typing.Optional[datetime.datetime], is_attribute=False) +ctime: QbNumericField('ctime', dtype=, is_attribute=False) default_calc_job_plugin: QbStrField('default_calc_job_plugin', dtype=typing.Optional[str], is_attribute=True) description: QbStrField('description', dtype=, is_attribute=True) -extras: QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) +extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) input_plugin: QbStrField('input_plugin', dtype=typing.Optional[str], is_attribute=True) is_local: QbField('is_local', dtype=typing.Optional[bool], is_attribute=True) label: QbStrField('label', dtype=, is_attribute=True) local_executable: QbStrField('local_executable', dtype=typing.Optional[str], is_attribute=True) -mtime: QbNumericField('mtime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -node_type: QbStrField('node_type', dtype=typing.Optional[str], is_attribute=False) -pk: QbNumericField('pk', dtype=typing.Optional[int], is_attribute=False) +mtime: QbNumericField('mtime', dtype=, is_attribute=False) +node_type: QbStrField('node_type', dtype=, is_attribute=False) +pk: QbNumericField('pk', dtype=, is_attribute=False) prepend_text: QbStrField('prepend_text', dtype=, is_attribute=True) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) remote_exec_path: QbStrField('remote_exec_path', dtype=typing.Optional[str], is_attribute=True) -repository_content: QbDictField('repository_content', dtype=typing.Optional[dict[str, - bytes]], is_attribute=False) -repository_metadata: QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, - typing.Any]], is_attribute=False) +repository_content: QbDictField('repository_content', dtype=dict[str, bytes], is_attribute=False) +repository_metadata: QbDictField('repository_metadata', dtype=typing.Dict[str, typing.Any], + is_attribute=False) source: QbDictField('source', dtype=typing.Optional[dict], is_attribute=True, is_subscriptable=True) use_double_quotes: QbField('use_double_quotes', dtype=, is_attribute=True) -user: QbNumericField('user', dtype=typing.Optional[int], is_attribute=False) -uuid: QbStrField('uuid', dtype=typing.Optional[str], is_attribute=False) +user: QbNumericField('user', dtype=, is_attribute=False) +uuid: QbField('uuid', dtype=, is_attribute=False) with_mpi: QbField('with_mpi', dtype=typing.Optional[bool], is_attribute=True) diff --git a/tests/orm/test_fields/fields_aiida.data.core.code.abstract.AbstractCode.yml b/tests/orm/test_fields/fields_aiida.data.core.code.abstract.AbstractCode.yml index 4dc178d9fc..d42b0bfe44 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.code.abstract.AbstractCode.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.code.abstract.AbstractCode.yml @@ -1,25 +1,24 @@ append_text: QbStrField('append_text', dtype=, is_attribute=True) -attributes: QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) +attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) -ctime: QbNumericField('ctime', dtype=typing.Optional[datetime.datetime], is_attribute=False) +ctime: QbNumericField('ctime', dtype=, is_attribute=False) default_calc_job_plugin: QbStrField('default_calc_job_plugin', dtype=typing.Optional[str], is_attribute=True) description: QbStrField('description', dtype=, is_attribute=True) -extras: QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) +extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) label: QbStrField('label', dtype=, is_attribute=True) -mtime: QbNumericField('mtime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -node_type: QbStrField('node_type', dtype=typing.Optional[str], is_attribute=False) -pk: QbNumericField('pk', dtype=typing.Optional[int], is_attribute=False) +mtime: QbNumericField('mtime', dtype=, is_attribute=False) +node_type: QbStrField('node_type', dtype=, is_attribute=False) +pk: QbNumericField('pk', dtype=, is_attribute=False) prepend_text: QbStrField('prepend_text', dtype=, is_attribute=True) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) -repository_content: QbDictField('repository_content', dtype=typing.Optional[dict[str, - bytes]], is_attribute=False) -repository_metadata: QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, - typing.Any]], is_attribute=False) +repository_content: QbDictField('repository_content', dtype=dict[str, bytes], is_attribute=False) +repository_metadata: QbDictField('repository_metadata', dtype=typing.Dict[str, typing.Any], + is_attribute=False) source: QbDictField('source', dtype=typing.Optional[dict], is_attribute=True, is_subscriptable=True) use_double_quotes: QbField('use_double_quotes', dtype=, is_attribute=True) -user: QbNumericField('user', dtype=typing.Optional[int], is_attribute=False) -uuid: QbStrField('uuid', dtype=typing.Optional[str], is_attribute=False) +user: QbNumericField('user', dtype=, is_attribute=False) +uuid: QbField('uuid', dtype=, is_attribute=False) with_mpi: QbField('with_mpi', dtype=typing.Optional[bool], is_attribute=True) diff --git a/tests/orm/test_fields/fields_aiida.data.core.code.containerized.ContainerizedCode.yml b/tests/orm/test_fields/fields_aiida.data.core.code.containerized.ContainerizedCode.yml index e7f12a1a94..60fe67b7d7 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.code.containerized.ContainerizedCode.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.code.containerized.ContainerizedCode.yml @@ -1,29 +1,28 @@ append_text: QbStrField('append_text', dtype=, is_attribute=True) -attributes: QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) -computer: QbStrField('computer', dtype=, is_attribute=True) -ctime: QbNumericField('ctime', dtype=typing.Optional[datetime.datetime], is_attribute=False) +attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) +computer: QbNumericField('computer', dtype=, is_attribute=False) +ctime: QbNumericField('ctime', dtype=, is_attribute=False) default_calc_job_plugin: QbStrField('default_calc_job_plugin', dtype=typing.Optional[str], is_attribute=True) description: QbStrField('description', dtype=, is_attribute=True) engine_command: QbStrField('engine_command', dtype=, is_attribute=True) -extras: QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) +extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) filepath_executable: QbStrField('filepath_executable', dtype=, is_attribute=True) image_name: QbStrField('image_name', dtype=, is_attribute=True) label: QbStrField('label', dtype=, is_attribute=True) -mtime: QbNumericField('mtime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -node_type: QbStrField('node_type', dtype=typing.Optional[str], is_attribute=False) -pk: QbNumericField('pk', dtype=typing.Optional[int], is_attribute=False) +mtime: QbNumericField('mtime', dtype=, is_attribute=False) +node_type: QbStrField('node_type', dtype=, is_attribute=False) +pk: QbNumericField('pk', dtype=, is_attribute=False) prepend_text: QbStrField('prepend_text', dtype=, is_attribute=True) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) -repository_content: QbDictField('repository_content', dtype=typing.Optional[dict[str, - bytes]], is_attribute=False) -repository_metadata: QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, - typing.Any]], is_attribute=False) +repository_content: QbDictField('repository_content', dtype=dict[str, bytes], is_attribute=False) +repository_metadata: QbDictField('repository_metadata', dtype=typing.Dict[str, typing.Any], + is_attribute=False) source: QbDictField('source', dtype=typing.Optional[dict], is_attribute=True, is_subscriptable=True) use_double_quotes: QbField('use_double_quotes', dtype=, is_attribute=True) -user: QbNumericField('user', dtype=typing.Optional[int], is_attribute=False) -uuid: QbStrField('uuid', dtype=typing.Optional[str], is_attribute=False) +user: QbNumericField('user', dtype=, is_attribute=False) +uuid: QbField('uuid', dtype=, is_attribute=False) with_mpi: QbField('with_mpi', dtype=typing.Optional[bool], is_attribute=True) wrap_cmdline_params: QbField('wrap_cmdline_params', dtype=, is_attribute=True) diff --git a/tests/orm/test_fields/fields_aiida.data.core.code.installed.InstalledCode.yml b/tests/orm/test_fields/fields_aiida.data.core.code.installed.InstalledCode.yml index 15089b4a3d..a7141d189f 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.code.installed.InstalledCode.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.code.installed.InstalledCode.yml @@ -1,26 +1,25 @@ append_text: QbStrField('append_text', dtype=, is_attribute=True) -attributes: QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) -computer: QbStrField('computer', dtype=, is_attribute=True) -ctime: QbNumericField('ctime', dtype=typing.Optional[datetime.datetime], is_attribute=False) +attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) +computer: QbNumericField('computer', dtype=, is_attribute=False) +ctime: QbNumericField('ctime', dtype=, is_attribute=False) default_calc_job_plugin: QbStrField('default_calc_job_plugin', dtype=typing.Optional[str], is_attribute=True) description: QbStrField('description', dtype=, is_attribute=True) -extras: QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) +extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) filepath_executable: QbStrField('filepath_executable', dtype=, is_attribute=True) label: QbStrField('label', dtype=, is_attribute=True) -mtime: QbNumericField('mtime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -node_type: QbStrField('node_type', dtype=typing.Optional[str], is_attribute=False) -pk: QbNumericField('pk', dtype=typing.Optional[int], is_attribute=False) +mtime: QbNumericField('mtime', dtype=, is_attribute=False) +node_type: QbStrField('node_type', dtype=, is_attribute=False) +pk: QbNumericField('pk', dtype=, is_attribute=False) prepend_text: QbStrField('prepend_text', dtype=, is_attribute=True) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) -repository_content: QbDictField('repository_content', dtype=typing.Optional[dict[str, - bytes]], is_attribute=False) -repository_metadata: QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, - typing.Any]], is_attribute=False) +repository_content: QbDictField('repository_content', dtype=dict[str, bytes], is_attribute=False) +repository_metadata: QbDictField('repository_metadata', dtype=typing.Dict[str, typing.Any], + is_attribute=False) source: QbDictField('source', dtype=typing.Optional[dict], is_attribute=True, is_subscriptable=True) use_double_quotes: QbField('use_double_quotes', dtype=, is_attribute=True) -user: QbNumericField('user', dtype=typing.Optional[int], is_attribute=False) -uuid: QbStrField('uuid', dtype=typing.Optional[str], is_attribute=False) +user: QbNumericField('user', dtype=, is_attribute=False) +uuid: QbField('uuid', dtype=, is_attribute=False) with_mpi: QbField('with_mpi', dtype=typing.Optional[bool], is_attribute=True) diff --git a/tests/orm/test_fields/fields_aiida.data.core.code.portable.PortableCode.yml b/tests/orm/test_fields/fields_aiida.data.core.code.portable.PortableCode.yml index b874b26466..be524b7ce8 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.code.portable.PortableCode.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.code.portable.PortableCode.yml @@ -1,27 +1,26 @@ append_text: QbStrField('append_text', dtype=, is_attribute=True) -attributes: QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) +attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) -ctime: QbNumericField('ctime', dtype=typing.Optional[datetime.datetime], is_attribute=False) +ctime: QbNumericField('ctime', dtype=, is_attribute=False) default_calc_job_plugin: QbStrField('default_calc_job_plugin', dtype=typing.Optional[str], is_attribute=True) description: QbStrField('description', dtype=, is_attribute=True) -extras: QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) +extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) filepath_executable: QbStrField('filepath_executable', dtype=, is_attribute=True) filepath_files: QbStrField('filepath_files', dtype=, is_attribute=False) label: QbStrField('label', dtype=, is_attribute=True) -mtime: QbNumericField('mtime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -node_type: QbStrField('node_type', dtype=typing.Optional[str], is_attribute=False) -pk: QbNumericField('pk', dtype=typing.Optional[int], is_attribute=False) +mtime: QbNumericField('mtime', dtype=, is_attribute=False) +node_type: QbStrField('node_type', dtype=, is_attribute=False) +pk: QbNumericField('pk', dtype=, is_attribute=False) prepend_text: QbStrField('prepend_text', dtype=, is_attribute=True) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) -repository_content: QbDictField('repository_content', dtype=typing.Optional[dict[str, - bytes]], is_attribute=False) -repository_metadata: QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, - typing.Any]], is_attribute=False) +repository_content: QbDictField('repository_content', dtype=dict[str, bytes], is_attribute=False) +repository_metadata: QbDictField('repository_metadata', dtype=typing.Dict[str, typing.Any], + is_attribute=False) source: QbDictField('source', dtype=typing.Optional[dict], is_attribute=True, is_subscriptable=True) use_double_quotes: QbField('use_double_quotes', dtype=, is_attribute=True) -user: QbNumericField('user', dtype=typing.Optional[int], is_attribute=False) -uuid: QbStrField('uuid', dtype=typing.Optional[str], is_attribute=False) +user: QbNumericField('user', dtype=, is_attribute=False) +uuid: QbField('uuid', dtype=, is_attribute=False) with_mpi: QbField('with_mpi', dtype=typing.Optional[bool], is_attribute=True) diff --git a/tests/orm/test_fields/fields_aiida.data.core.dict.Dict.yml b/tests/orm/test_fields/fields_aiida.data.core.dict.Dict.yml index 710d253b4d..072cf14701 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.dict.Dict.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.dict.Dict.yml @@ -1,21 +1,20 @@ -attributes: QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) +attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) -ctime: QbNumericField('ctime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -description: QbStrField('description', dtype=typing.Optional[str], is_attribute=False) -extras: QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) -label: QbStrField('label', dtype=typing.Optional[str], is_attribute=False) -mtime: QbNumericField('mtime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -node_type: QbStrField('node_type', dtype=typing.Optional[str], is_attribute=False) -pk: QbNumericField('pk', dtype=typing.Optional[int], is_attribute=False) +ctime: QbNumericField('ctime', dtype=, is_attribute=False) +description: QbStrField('description', dtype=, is_attribute=False) +extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) +label: QbStrField('label', dtype=, is_attribute=False) +mtime: QbNumericField('mtime', dtype=, is_attribute=False) +node_type: QbStrField('node_type', dtype=, is_attribute=False) +pk: QbNumericField('pk', dtype=, is_attribute=False) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) -repository_content: QbDictField('repository_content', dtype=typing.Optional[dict[str, - bytes]], is_attribute=False) -repository_metadata: QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, - typing.Any]], is_attribute=False) +repository_content: QbDictField('repository_content', dtype=dict[str, bytes], is_attribute=False) +repository_metadata: QbDictField('repository_metadata', dtype=typing.Dict[str, typing.Any], + is_attribute=False) source: QbDictField('source', dtype=typing.Optional[dict], is_attribute=True, is_subscriptable=True) -user: QbNumericField('user', dtype=typing.Optional[int], is_attribute=False) -uuid: QbStrField('uuid', dtype=typing.Optional[str], is_attribute=False) +user: QbNumericField('user', dtype=, is_attribute=False) +uuid: QbField('uuid', dtype=, is_attribute=False) value: QbDictField('value', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) diff --git a/tests/orm/test_fields/fields_aiida.data.core.enum.EnumData.yml b/tests/orm/test_fields/fields_aiida.data.core.enum.EnumData.yml index cfc3976079..fd0996222c 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.enum.EnumData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.enum.EnumData.yml @@ -1,20 +1,19 @@ -attributes: QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) +attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) -ctime: QbNumericField('ctime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -description: QbStrField('description', dtype=typing.Optional[str], is_attribute=False) -extras: QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) -label: QbStrField('label', dtype=typing.Optional[str], is_attribute=False) +ctime: QbNumericField('ctime', dtype=, is_attribute=False) +description: QbStrField('description', dtype=, is_attribute=False) +extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) +label: QbStrField('label', dtype=, is_attribute=False) member: QbField('member', dtype=, is_attribute=True) -mtime: QbNumericField('mtime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -node_type: QbStrField('node_type', dtype=typing.Optional[str], is_attribute=False) -pk: QbNumericField('pk', dtype=typing.Optional[int], is_attribute=False) +mtime: QbNumericField('mtime', dtype=, is_attribute=False) +node_type: QbStrField('node_type', dtype=, is_attribute=False) +pk: QbNumericField('pk', dtype=, is_attribute=False) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) -repository_content: QbDictField('repository_content', dtype=typing.Optional[dict[str, - bytes]], is_attribute=False) -repository_metadata: QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, - typing.Any]], is_attribute=False) +repository_content: QbDictField('repository_content', dtype=dict[str, bytes], is_attribute=False) +repository_metadata: QbDictField('repository_metadata', dtype=typing.Dict[str, typing.Any], + is_attribute=False) source: QbDictField('source', dtype=typing.Optional[dict], is_attribute=True, is_subscriptable=True) -user: QbNumericField('user', dtype=typing.Optional[int], is_attribute=False) -uuid: QbStrField('uuid', dtype=typing.Optional[str], is_attribute=False) +user: QbNumericField('user', dtype=, is_attribute=False) +uuid: QbField('uuid', dtype=, is_attribute=False) diff --git a/tests/orm/test_fields/fields_aiida.data.core.float.Float.yml b/tests/orm/test_fields/fields_aiida.data.core.float.Float.yml index 457621f596..7329e70729 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.float.Float.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.float.Float.yml @@ -1,20 +1,19 @@ -attributes: QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) +attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) -ctime: QbNumericField('ctime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -description: QbStrField('description', dtype=typing.Optional[str], is_attribute=False) -extras: QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) -label: QbStrField('label', dtype=typing.Optional[str], is_attribute=False) -mtime: QbNumericField('mtime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -node_type: QbStrField('node_type', dtype=typing.Optional[str], is_attribute=False) -pk: QbNumericField('pk', dtype=typing.Optional[int], is_attribute=False) +ctime: QbNumericField('ctime', dtype=, is_attribute=False) +description: QbStrField('description', dtype=, is_attribute=False) +extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) +label: QbStrField('label', dtype=, is_attribute=False) +mtime: QbNumericField('mtime', dtype=, is_attribute=False) +node_type: QbStrField('node_type', dtype=, is_attribute=False) +pk: QbNumericField('pk', dtype=, is_attribute=False) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) -repository_content: QbDictField('repository_content', dtype=typing.Optional[dict[str, - bytes]], is_attribute=False) -repository_metadata: QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, - typing.Any]], is_attribute=False) +repository_content: QbDictField('repository_content', dtype=dict[str, bytes], is_attribute=False) +repository_metadata: QbDictField('repository_metadata', dtype=typing.Dict[str, typing.Any], + is_attribute=False) source: QbDictField('source', dtype=typing.Optional[dict], is_attribute=True, is_subscriptable=True) -user: QbNumericField('user', dtype=typing.Optional[int], is_attribute=False) -uuid: QbStrField('uuid', dtype=typing.Optional[str], is_attribute=False) -value: QbField('value', dtype=typing.Any, is_attribute=True) +user: QbNumericField('user', dtype=, is_attribute=False) +uuid: QbField('uuid', dtype=, is_attribute=False) +value: QbNumericField('value', dtype=, is_attribute=True) diff --git a/tests/orm/test_fields/fields_aiida.data.core.folder.FolderData.yml b/tests/orm/test_fields/fields_aiida.data.core.folder.FolderData.yml index 5bee2ef441..5f6f9a6152 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.folder.FolderData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.folder.FolderData.yml @@ -1,19 +1,18 @@ -attributes: QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) +attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) -ctime: QbNumericField('ctime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -description: QbStrField('description', dtype=typing.Optional[str], is_attribute=False) -extras: QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) -label: QbStrField('label', dtype=typing.Optional[str], is_attribute=False) -mtime: QbNumericField('mtime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -node_type: QbStrField('node_type', dtype=typing.Optional[str], is_attribute=False) -pk: QbNumericField('pk', dtype=typing.Optional[int], is_attribute=False) +ctime: QbNumericField('ctime', dtype=, is_attribute=False) +description: QbStrField('description', dtype=, is_attribute=False) +extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) +label: QbStrField('label', dtype=, is_attribute=False) +mtime: QbNumericField('mtime', dtype=, is_attribute=False) +node_type: QbStrField('node_type', dtype=, is_attribute=False) +pk: QbNumericField('pk', dtype=, is_attribute=False) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) -repository_content: QbDictField('repository_content', dtype=typing.Optional[dict[str, - bytes]], is_attribute=False) -repository_metadata: QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, - typing.Any]], is_attribute=False) +repository_content: QbDictField('repository_content', dtype=dict[str, bytes], is_attribute=False) +repository_metadata: QbDictField('repository_metadata', dtype=typing.Dict[str, typing.Any], + is_attribute=False) source: QbDictField('source', dtype=typing.Optional[dict], is_attribute=True, is_subscriptable=True) -user: QbNumericField('user', dtype=typing.Optional[int], is_attribute=False) -uuid: QbStrField('uuid', dtype=typing.Optional[str], is_attribute=False) +user: QbNumericField('user', dtype=, is_attribute=False) +uuid: QbField('uuid', dtype=, is_attribute=False) diff --git a/tests/orm/test_fields/fields_aiida.data.core.int.Int.yml b/tests/orm/test_fields/fields_aiida.data.core.int.Int.yml index 457621f596..45bebdb32c 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.int.Int.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.int.Int.yml @@ -1,20 +1,19 @@ -attributes: QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) +attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) -ctime: QbNumericField('ctime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -description: QbStrField('description', dtype=typing.Optional[str], is_attribute=False) -extras: QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) -label: QbStrField('label', dtype=typing.Optional[str], is_attribute=False) -mtime: QbNumericField('mtime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -node_type: QbStrField('node_type', dtype=typing.Optional[str], is_attribute=False) -pk: QbNumericField('pk', dtype=typing.Optional[int], is_attribute=False) +ctime: QbNumericField('ctime', dtype=, is_attribute=False) +description: QbStrField('description', dtype=, is_attribute=False) +extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) +label: QbStrField('label', dtype=, is_attribute=False) +mtime: QbNumericField('mtime', dtype=, is_attribute=False) +node_type: QbStrField('node_type', dtype=, is_attribute=False) +pk: QbNumericField('pk', dtype=, is_attribute=False) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) -repository_content: QbDictField('repository_content', dtype=typing.Optional[dict[str, - bytes]], is_attribute=False) -repository_metadata: QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, - typing.Any]], is_attribute=False) +repository_content: QbDictField('repository_content', dtype=dict[str, bytes], is_attribute=False) +repository_metadata: QbDictField('repository_metadata', dtype=typing.Dict[str, typing.Any], + is_attribute=False) source: QbDictField('source', dtype=typing.Optional[dict], is_attribute=True, is_subscriptable=True) -user: QbNumericField('user', dtype=typing.Optional[int], is_attribute=False) -uuid: QbStrField('uuid', dtype=typing.Optional[str], is_attribute=False) -value: QbField('value', dtype=typing.Any, is_attribute=True) +user: QbNumericField('user', dtype=, is_attribute=False) +uuid: QbField('uuid', dtype=, is_attribute=False) +value: QbNumericField('value', dtype=, is_attribute=True) diff --git a/tests/orm/test_fields/fields_aiida.data.core.jsonable.JsonableData.yml b/tests/orm/test_fields/fields_aiida.data.core.jsonable.JsonableData.yml index 1166fbc570..8ddfceb496 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.jsonable.JsonableData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.jsonable.JsonableData.yml @@ -1,21 +1,20 @@ -attributes: QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) +attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) -ctime: QbNumericField('ctime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -description: QbStrField('description', dtype=typing.Optional[str], is_attribute=False) -extras: QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) -label: QbStrField('label', dtype=typing.Optional[str], is_attribute=False) -mtime: QbNumericField('mtime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -node_type: QbStrField('node_type', dtype=typing.Optional[str], is_attribute=False) +ctime: QbNumericField('ctime', dtype=, is_attribute=False) +description: QbStrField('description', dtype=, is_attribute=False) +extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) +label: QbStrField('label', dtype=, is_attribute=False) +mtime: QbNumericField('mtime', dtype=, is_attribute=False) +node_type: QbStrField('node_type', dtype=, is_attribute=False) obj: QbField('obj', dtype=, is_attribute=True) -pk: QbNumericField('pk', dtype=typing.Optional[int], is_attribute=False) +pk: QbNumericField('pk', dtype=, is_attribute=False) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) -repository_content: QbDictField('repository_content', dtype=typing.Optional[dict[str, - bytes]], is_attribute=False) -repository_metadata: QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, - typing.Any]], is_attribute=False) +repository_content: QbDictField('repository_content', dtype=dict[str, bytes], is_attribute=False) +repository_metadata: QbDictField('repository_metadata', dtype=typing.Dict[str, typing.Any], + is_attribute=False) source: QbDictField('source', dtype=typing.Optional[dict], is_attribute=True, is_subscriptable=True) -user: QbNumericField('user', dtype=typing.Optional[int], is_attribute=False) -uuid: QbStrField('uuid', dtype=typing.Optional[str], is_attribute=False) +user: QbNumericField('user', dtype=, is_attribute=False) +uuid: QbField('uuid', dtype=, is_attribute=False) diff --git a/tests/orm/test_fields/fields_aiida.data.core.list.List.yml b/tests/orm/test_fields/fields_aiida.data.core.list.List.yml index 4edd6d3380..7b2a004b3e 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.list.List.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.list.List.yml @@ -1,20 +1,19 @@ -attributes: QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) +attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) -ctime: QbNumericField('ctime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -description: QbStrField('description', dtype=typing.Optional[str], is_attribute=False) -extras: QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) -label: QbStrField('label', dtype=typing.Optional[str], is_attribute=False) -mtime: QbNumericField('mtime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -node_type: QbStrField('node_type', dtype=typing.Optional[str], is_attribute=False) -pk: QbNumericField('pk', dtype=typing.Optional[int], is_attribute=False) +ctime: QbNumericField('ctime', dtype=, is_attribute=False) +description: QbStrField('description', dtype=, is_attribute=False) +extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) +label: QbStrField('label', dtype=, is_attribute=False) +mtime: QbNumericField('mtime', dtype=, is_attribute=False) +node_type: QbStrField('node_type', dtype=, is_attribute=False) +pk: QbNumericField('pk', dtype=, is_attribute=False) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) -repository_content: QbDictField('repository_content', dtype=typing.Optional[dict[str, - bytes]], is_attribute=False) -repository_metadata: QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, - typing.Any]], is_attribute=False) +repository_content: QbDictField('repository_content', dtype=dict[str, bytes], is_attribute=False) +repository_metadata: QbDictField('repository_metadata', dtype=typing.Dict[str, typing.Any], + is_attribute=False) source: QbDictField('source', dtype=typing.Optional[dict], is_attribute=True, is_subscriptable=True) -user: QbNumericField('user', dtype=typing.Optional[int], is_attribute=False) -uuid: QbStrField('uuid', dtype=typing.Optional[str], is_attribute=False) +user: QbNumericField('user', dtype=, is_attribute=False) +uuid: QbField('uuid', dtype=, is_attribute=False) value: QbArrayField('value', dtype=typing.List[typing.Any], is_attribute=True) diff --git a/tests/orm/test_fields/fields_aiida.data.core.numeric.NumericType.yml b/tests/orm/test_fields/fields_aiida.data.core.numeric.NumericType.yml index 457621f596..e92d2eee53 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.numeric.NumericType.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.numeric.NumericType.yml @@ -1,20 +1,19 @@ -attributes: QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) +attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) -ctime: QbNumericField('ctime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -description: QbStrField('description', dtype=typing.Optional[str], is_attribute=False) -extras: QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) -label: QbStrField('label', dtype=typing.Optional[str], is_attribute=False) -mtime: QbNumericField('mtime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -node_type: QbStrField('node_type', dtype=typing.Optional[str], is_attribute=False) -pk: QbNumericField('pk', dtype=typing.Optional[int], is_attribute=False) +ctime: QbNumericField('ctime', dtype=, is_attribute=False) +description: QbStrField('description', dtype=, is_attribute=False) +extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) +label: QbStrField('label', dtype=, is_attribute=False) +mtime: QbNumericField('mtime', dtype=, is_attribute=False) +node_type: QbStrField('node_type', dtype=, is_attribute=False) +pk: QbNumericField('pk', dtype=, is_attribute=False) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) -repository_content: QbDictField('repository_content', dtype=typing.Optional[dict[str, - bytes]], is_attribute=False) -repository_metadata: QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, - typing.Any]], is_attribute=False) +repository_content: QbDictField('repository_content', dtype=dict[str, bytes], is_attribute=False) +repository_metadata: QbDictField('repository_metadata', dtype=typing.Dict[str, typing.Any], + is_attribute=False) source: QbDictField('source', dtype=typing.Optional[dict], is_attribute=True, is_subscriptable=True) -user: QbNumericField('user', dtype=typing.Optional[int], is_attribute=False) -uuid: QbStrField('uuid', dtype=typing.Optional[str], is_attribute=False) +user: QbNumericField('user', dtype=, is_attribute=False) +uuid: QbField('uuid', dtype=, is_attribute=False) value: QbField('value', dtype=typing.Any, is_attribute=True) diff --git a/tests/orm/test_fields/fields_aiida.data.core.orbital.OrbitalData.yml b/tests/orm/test_fields/fields_aiida.data.core.orbital.OrbitalData.yml index 5bee2ef441..5f6f9a6152 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.orbital.OrbitalData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.orbital.OrbitalData.yml @@ -1,19 +1,18 @@ -attributes: QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) +attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) -ctime: QbNumericField('ctime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -description: QbStrField('description', dtype=typing.Optional[str], is_attribute=False) -extras: QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) -label: QbStrField('label', dtype=typing.Optional[str], is_attribute=False) -mtime: QbNumericField('mtime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -node_type: QbStrField('node_type', dtype=typing.Optional[str], is_attribute=False) -pk: QbNumericField('pk', dtype=typing.Optional[int], is_attribute=False) +ctime: QbNumericField('ctime', dtype=, is_attribute=False) +description: QbStrField('description', dtype=, is_attribute=False) +extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) +label: QbStrField('label', dtype=, is_attribute=False) +mtime: QbNumericField('mtime', dtype=, is_attribute=False) +node_type: QbStrField('node_type', dtype=, is_attribute=False) +pk: QbNumericField('pk', dtype=, is_attribute=False) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) -repository_content: QbDictField('repository_content', dtype=typing.Optional[dict[str, - bytes]], is_attribute=False) -repository_metadata: QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, - typing.Any]], is_attribute=False) +repository_content: QbDictField('repository_content', dtype=dict[str, bytes], is_attribute=False) +repository_metadata: QbDictField('repository_metadata', dtype=typing.Dict[str, typing.Any], + is_attribute=False) source: QbDictField('source', dtype=typing.Optional[dict], is_attribute=True, is_subscriptable=True) -user: QbNumericField('user', dtype=typing.Optional[int], is_attribute=False) -uuid: QbStrField('uuid', dtype=typing.Optional[str], is_attribute=False) +user: QbNumericField('user', dtype=, is_attribute=False) +uuid: QbField('uuid', dtype=, is_attribute=False) diff --git a/tests/orm/test_fields/fields_aiida.data.core.remote.RemoteData.yml b/tests/orm/test_fields/fields_aiida.data.core.remote.RemoteData.yml index 5086780659..809880670b 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.remote.RemoteData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.remote.RemoteData.yml @@ -1,20 +1,19 @@ -attributes: QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) +attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) -ctime: QbNumericField('ctime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -description: QbStrField('description', dtype=typing.Optional[str], is_attribute=False) -extras: QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) -label: QbStrField('label', dtype=typing.Optional[str], is_attribute=False) -mtime: QbNumericField('mtime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -node_type: QbStrField('node_type', dtype=typing.Optional[str], is_attribute=False) -pk: QbNumericField('pk', dtype=typing.Optional[int], is_attribute=False) +ctime: QbNumericField('ctime', dtype=, is_attribute=False) +description: QbStrField('description', dtype=, is_attribute=False) +extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) +label: QbStrField('label', dtype=, is_attribute=False) +mtime: QbNumericField('mtime', dtype=, is_attribute=False) +node_type: QbStrField('node_type', dtype=, is_attribute=False) +pk: QbNumericField('pk', dtype=, is_attribute=False) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) remote_path: QbStrField('remote_path', dtype=typing.Optional[str], is_attribute=True) -repository_content: QbDictField('repository_content', dtype=typing.Optional[dict[str, - bytes]], is_attribute=False) -repository_metadata: QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, - typing.Any]], is_attribute=False) +repository_content: QbDictField('repository_content', dtype=dict[str, bytes], is_attribute=False) +repository_metadata: QbDictField('repository_metadata', dtype=typing.Dict[str, typing.Any], + is_attribute=False) source: QbDictField('source', dtype=typing.Optional[dict], is_attribute=True, is_subscriptable=True) -user: QbNumericField('user', dtype=typing.Optional[int], is_attribute=False) -uuid: QbStrField('uuid', dtype=typing.Optional[str], is_attribute=False) +user: QbNumericField('user', dtype=, is_attribute=False) +uuid: QbField('uuid', dtype=, is_attribute=False) diff --git a/tests/orm/test_fields/fields_aiida.data.core.remote.stash.RemoteStashData.yml b/tests/orm/test_fields/fields_aiida.data.core.remote.stash.RemoteStashData.yml index 35e6e4188e..a76a52caab 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.remote.stash.RemoteStashData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.remote.stash.RemoteStashData.yml @@ -1,20 +1,19 @@ -attributes: QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) +attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) -ctime: QbNumericField('ctime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -description: QbStrField('description', dtype=typing.Optional[str], is_attribute=False) -extras: QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) -label: QbStrField('label', dtype=typing.Optional[str], is_attribute=False) -mtime: QbNumericField('mtime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -node_type: QbStrField('node_type', dtype=typing.Optional[str], is_attribute=False) -pk: QbNumericField('pk', dtype=typing.Optional[int], is_attribute=False) +ctime: QbNumericField('ctime', dtype=, is_attribute=False) +description: QbStrField('description', dtype=, is_attribute=False) +extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) +label: QbStrField('label', dtype=, is_attribute=False) +mtime: QbNumericField('mtime', dtype=, is_attribute=False) +node_type: QbStrField('node_type', dtype=, is_attribute=False) +pk: QbNumericField('pk', dtype=, is_attribute=False) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) -repository_content: QbDictField('repository_content', dtype=typing.Optional[dict[str, - bytes]], is_attribute=False) -repository_metadata: QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, - typing.Any]], is_attribute=False) +repository_content: QbDictField('repository_content', dtype=dict[str, bytes], is_attribute=False) +repository_metadata: QbDictField('repository_metadata', dtype=typing.Dict[str, typing.Any], + is_attribute=False) source: QbDictField('source', dtype=typing.Optional[dict], is_attribute=True, is_subscriptable=True) stash_mode: QbField('stash_mode', dtype=, is_attribute=True) -user: QbNumericField('user', dtype=typing.Optional[int], is_attribute=False) -uuid: QbStrField('uuid', dtype=typing.Optional[str], is_attribute=False) +user: QbNumericField('user', dtype=, is_attribute=False) +uuid: QbField('uuid', dtype=, is_attribute=False) diff --git a/tests/orm/test_fields/fields_aiida.data.core.remote.stash.compress.RemoteStashCompressedData.yml b/tests/orm/test_fields/fields_aiida.data.core.remote.stash.compress.RemoteStashCompressedData.yml index 40e941f933..8161a94814 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.remote.stash.compress.RemoteStashCompressedData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.remote.stash.compress.RemoteStashCompressedData.yml @@ -1,23 +1,22 @@ -attributes: QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) +attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) -ctime: QbNumericField('ctime', dtype=typing.Optional[datetime.datetime], is_attribute=False) +ctime: QbNumericField('ctime', dtype=, is_attribute=False) dereference: QbField('dereference', dtype=, is_attribute=True) -description: QbStrField('description', dtype=typing.Optional[str], is_attribute=False) -extras: QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) -label: QbStrField('label', dtype=typing.Optional[str], is_attribute=False) -mtime: QbNumericField('mtime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -node_type: QbStrField('node_type', dtype=typing.Optional[str], is_attribute=False) -pk: QbNumericField('pk', dtype=typing.Optional[int], is_attribute=False) +description: QbStrField('description', dtype=, is_attribute=False) +extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) +label: QbStrField('label', dtype=, is_attribute=False) +mtime: QbNumericField('mtime', dtype=, is_attribute=False) +node_type: QbStrField('node_type', dtype=, is_attribute=False) +pk: QbNumericField('pk', dtype=, is_attribute=False) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) -repository_content: QbDictField('repository_content', dtype=typing.Optional[dict[str, - bytes]], is_attribute=False) -repository_metadata: QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, - typing.Any]], is_attribute=False) +repository_content: QbDictField('repository_content', dtype=dict[str, bytes], is_attribute=False) +repository_metadata: QbDictField('repository_metadata', dtype=typing.Dict[str, typing.Any], + is_attribute=False) source: QbDictField('source', dtype=typing.Optional[dict], is_attribute=True, is_subscriptable=True) source_list: QbArrayField('source_list', dtype=typing.List[str], is_attribute=True) stash_mode: QbField('stash_mode', dtype=, is_attribute=True) target_basepath: QbStrField('target_basepath', dtype=, is_attribute=True) -user: QbNumericField('user', dtype=typing.Optional[int], is_attribute=False) -uuid: QbStrField('uuid', dtype=typing.Optional[str], is_attribute=False) +user: QbNumericField('user', dtype=, is_attribute=False) +uuid: QbField('uuid', dtype=, is_attribute=False) diff --git a/tests/orm/test_fields/fields_aiida.data.core.remote.stash.custom.RemoteStashCustomData.yml b/tests/orm/test_fields/fields_aiida.data.core.remote.stash.custom.RemoteStashCustomData.yml index 82e177e738..bf55326669 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.remote.stash.custom.RemoteStashCustomData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.remote.stash.custom.RemoteStashCustomData.yml @@ -1,22 +1,21 @@ -attributes: QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) +attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) -ctime: QbNumericField('ctime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -description: QbStrField('description', dtype=typing.Optional[str], is_attribute=False) -extras: QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) -label: QbStrField('label', dtype=typing.Optional[str], is_attribute=False) -mtime: QbNumericField('mtime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -node_type: QbStrField('node_type', dtype=typing.Optional[str], is_attribute=False) -pk: QbNumericField('pk', dtype=typing.Optional[int], is_attribute=False) +ctime: QbNumericField('ctime', dtype=, is_attribute=False) +description: QbStrField('description', dtype=, is_attribute=False) +extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) +label: QbStrField('label', dtype=, is_attribute=False) +mtime: QbNumericField('mtime', dtype=, is_attribute=False) +node_type: QbStrField('node_type', dtype=, is_attribute=False) +pk: QbNumericField('pk', dtype=, is_attribute=False) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) -repository_content: QbDictField('repository_content', dtype=typing.Optional[dict[str, - bytes]], is_attribute=False) -repository_metadata: QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, - typing.Any]], is_attribute=False) +repository_content: QbDictField('repository_content', dtype=dict[str, bytes], is_attribute=False) +repository_metadata: QbDictField('repository_metadata', dtype=typing.Dict[str, typing.Any], + is_attribute=False) source: QbDictField('source', dtype=typing.Optional[dict], is_attribute=True, is_subscriptable=True) source_list: QbArrayField('source_list', dtype=typing.List[str], is_attribute=True) stash_mode: QbField('stash_mode', dtype=, is_attribute=True) target_basepath: QbStrField('target_basepath', dtype=, is_attribute=True) -user: QbNumericField('user', dtype=typing.Optional[int], is_attribute=False) -uuid: QbStrField('uuid', dtype=typing.Optional[str], is_attribute=False) +user: QbNumericField('user', dtype=, is_attribute=False) +uuid: QbField('uuid', dtype=, is_attribute=False) diff --git a/tests/orm/test_fields/fields_aiida.data.core.remote.stash.folder.RemoteStashFolderData.yml b/tests/orm/test_fields/fields_aiida.data.core.remote.stash.folder.RemoteStashFolderData.yml index 82e177e738..bf55326669 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.remote.stash.folder.RemoteStashFolderData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.remote.stash.folder.RemoteStashFolderData.yml @@ -1,22 +1,21 @@ -attributes: QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) +attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) -ctime: QbNumericField('ctime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -description: QbStrField('description', dtype=typing.Optional[str], is_attribute=False) -extras: QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) -label: QbStrField('label', dtype=typing.Optional[str], is_attribute=False) -mtime: QbNumericField('mtime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -node_type: QbStrField('node_type', dtype=typing.Optional[str], is_attribute=False) -pk: QbNumericField('pk', dtype=typing.Optional[int], is_attribute=False) +ctime: QbNumericField('ctime', dtype=, is_attribute=False) +description: QbStrField('description', dtype=, is_attribute=False) +extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) +label: QbStrField('label', dtype=, is_attribute=False) +mtime: QbNumericField('mtime', dtype=, is_attribute=False) +node_type: QbStrField('node_type', dtype=, is_attribute=False) +pk: QbNumericField('pk', dtype=, is_attribute=False) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) -repository_content: QbDictField('repository_content', dtype=typing.Optional[dict[str, - bytes]], is_attribute=False) -repository_metadata: QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, - typing.Any]], is_attribute=False) +repository_content: QbDictField('repository_content', dtype=dict[str, bytes], is_attribute=False) +repository_metadata: QbDictField('repository_metadata', dtype=typing.Dict[str, typing.Any], + is_attribute=False) source: QbDictField('source', dtype=typing.Optional[dict], is_attribute=True, is_subscriptable=True) source_list: QbArrayField('source_list', dtype=typing.List[str], is_attribute=True) stash_mode: QbField('stash_mode', dtype=, is_attribute=True) target_basepath: QbStrField('target_basepath', dtype=, is_attribute=True) -user: QbNumericField('user', dtype=typing.Optional[int], is_attribute=False) -uuid: QbStrField('uuid', dtype=typing.Optional[str], is_attribute=False) +user: QbNumericField('user', dtype=, is_attribute=False) +uuid: QbField('uuid', dtype=, is_attribute=False) diff --git a/tests/orm/test_fields/fields_aiida.data.core.singlefile.SinglefileData.yml b/tests/orm/test_fields/fields_aiida.data.core.singlefile.SinglefileData.yml index 71f35c46f7..765c28aded 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.singlefile.SinglefileData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.singlefile.SinglefileData.yml @@ -1,21 +1,20 @@ -attributes: QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) +attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) content: QbField('content', dtype=, is_attribute=True) -ctime: QbNumericField('ctime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -description: QbStrField('description', dtype=typing.Optional[str], is_attribute=False) -extras: QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) -filename: QbStrField('filename', dtype=typing.Optional[str], is_attribute=True) -label: QbStrField('label', dtype=typing.Optional[str], is_attribute=False) -mtime: QbNumericField('mtime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -node_type: QbStrField('node_type', dtype=typing.Optional[str], is_attribute=False) -pk: QbNumericField('pk', dtype=typing.Optional[int], is_attribute=False) +ctime: QbNumericField('ctime', dtype=, is_attribute=False) +description: QbStrField('description', dtype=, is_attribute=False) +extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) +filename: QbStrField('filename', dtype=, is_attribute=True) +label: QbStrField('label', dtype=, is_attribute=False) +mtime: QbNumericField('mtime', dtype=, is_attribute=False) +node_type: QbStrField('node_type', dtype=, is_attribute=False) +pk: QbNumericField('pk', dtype=, is_attribute=False) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) -repository_content: QbDictField('repository_content', dtype=typing.Optional[dict[str, - bytes]], is_attribute=False) -repository_metadata: QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, - typing.Any]], is_attribute=False) +repository_content: QbDictField('repository_content', dtype=dict[str, bytes], is_attribute=False) +repository_metadata: QbDictField('repository_metadata', dtype=typing.Dict[str, typing.Any], + is_attribute=False) source: QbDictField('source', dtype=typing.Optional[dict], is_attribute=True, is_subscriptable=True) -user: QbNumericField('user', dtype=typing.Optional[int], is_attribute=False) -uuid: QbStrField('uuid', dtype=typing.Optional[str], is_attribute=False) +user: QbNumericField('user', dtype=, is_attribute=False) +uuid: QbField('uuid', dtype=, is_attribute=False) diff --git a/tests/orm/test_fields/fields_aiida.data.core.str.Str.yml b/tests/orm/test_fields/fields_aiida.data.core.str.Str.yml index 457621f596..0a5ee56ef8 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.str.Str.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.str.Str.yml @@ -1,20 +1,19 @@ -attributes: QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) +attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) -ctime: QbNumericField('ctime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -description: QbStrField('description', dtype=typing.Optional[str], is_attribute=False) -extras: QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) -label: QbStrField('label', dtype=typing.Optional[str], is_attribute=False) -mtime: QbNumericField('mtime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -node_type: QbStrField('node_type', dtype=typing.Optional[str], is_attribute=False) -pk: QbNumericField('pk', dtype=typing.Optional[int], is_attribute=False) +ctime: QbNumericField('ctime', dtype=, is_attribute=False) +description: QbStrField('description', dtype=, is_attribute=False) +extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) +label: QbStrField('label', dtype=, is_attribute=False) +mtime: QbNumericField('mtime', dtype=, is_attribute=False) +node_type: QbStrField('node_type', dtype=, is_attribute=False) +pk: QbNumericField('pk', dtype=, is_attribute=False) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) -repository_content: QbDictField('repository_content', dtype=typing.Optional[dict[str, - bytes]], is_attribute=False) -repository_metadata: QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, - typing.Any]], is_attribute=False) +repository_content: QbDictField('repository_content', dtype=dict[str, bytes], is_attribute=False) +repository_metadata: QbDictField('repository_metadata', dtype=typing.Dict[str, typing.Any], + is_attribute=False) source: QbDictField('source', dtype=typing.Optional[dict], is_attribute=True, is_subscriptable=True) -user: QbNumericField('user', dtype=typing.Optional[int], is_attribute=False) -uuid: QbStrField('uuid', dtype=typing.Optional[str], is_attribute=False) -value: QbField('value', dtype=typing.Any, is_attribute=True) +user: QbNumericField('user', dtype=, is_attribute=False) +uuid: QbField('uuid', dtype=, is_attribute=False) +value: QbStrField('value', dtype=, is_attribute=True) diff --git a/tests/orm/test_fields/fields_aiida.data.core.structure.StructureData.yml b/tests/orm/test_fields/fields_aiida.data.core.structure.StructureData.yml index 8e94962d01..b4b87257ab 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.structure.StructureData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.structure.StructureData.yml @@ -1,25 +1,24 @@ -attributes: QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) +attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) cell: QbArrayField('cell', dtype=typing.List[typing.List[float]], is_attribute=True) computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) -ctime: QbNumericField('ctime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -description: QbStrField('description', dtype=typing.Optional[str], is_attribute=False) -extras: QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) +ctime: QbNumericField('ctime', dtype=, is_attribute=False) +description: QbStrField('description', dtype=, is_attribute=False) +extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) kinds: QbArrayField('kinds', dtype=typing.Optional[typing.List[dict]], is_attribute=True) -label: QbStrField('label', dtype=typing.Optional[str], is_attribute=False) -mtime: QbNumericField('mtime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -node_type: QbStrField('node_type', dtype=typing.Optional[str], is_attribute=False) +label: QbStrField('label', dtype=, is_attribute=False) +mtime: QbNumericField('mtime', dtype=, is_attribute=False) +node_type: QbStrField('node_type', dtype=, is_attribute=False) pbc1: QbField('pbc1', dtype=, is_attribute=True) pbc2: QbField('pbc2', dtype=, is_attribute=True) pbc3: QbField('pbc3', dtype=, is_attribute=True) -pk: QbNumericField('pk', dtype=typing.Optional[int], is_attribute=False) +pk: QbNumericField('pk', dtype=, is_attribute=False) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) -repository_content: QbDictField('repository_content', dtype=typing.Optional[dict[str, - bytes]], is_attribute=False) -repository_metadata: QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, - typing.Any]], is_attribute=False) +repository_content: QbDictField('repository_content', dtype=dict[str, bytes], is_attribute=False) +repository_metadata: QbDictField('repository_metadata', dtype=typing.Dict[str, typing.Any], + is_attribute=False) sites: QbArrayField('sites', dtype=typing.Optional[typing.List[dict]], is_attribute=True) source: QbDictField('source', dtype=typing.Optional[dict], is_attribute=True, is_subscriptable=True) -user: QbNumericField('user', dtype=typing.Optional[int], is_attribute=False) -uuid: QbStrField('uuid', dtype=typing.Optional[str], is_attribute=False) +user: QbNumericField('user', dtype=, is_attribute=False) +uuid: QbField('uuid', dtype=, is_attribute=False) diff --git a/tests/orm/test_fields/fields_aiida.data.core.upf.UpfData.yml b/tests/orm/test_fields/fields_aiida.data.core.upf.UpfData.yml index 71f35c46f7..765c28aded 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.upf.UpfData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.upf.UpfData.yml @@ -1,21 +1,20 @@ -attributes: QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) +attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) content: QbField('content', dtype=, is_attribute=True) -ctime: QbNumericField('ctime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -description: QbStrField('description', dtype=typing.Optional[str], is_attribute=False) -extras: QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], - is_attribute=False, is_subscriptable=True) -filename: QbStrField('filename', dtype=typing.Optional[str], is_attribute=True) -label: QbStrField('label', dtype=typing.Optional[str], is_attribute=False) -mtime: QbNumericField('mtime', dtype=typing.Optional[datetime.datetime], is_attribute=False) -node_type: QbStrField('node_type', dtype=typing.Optional[str], is_attribute=False) -pk: QbNumericField('pk', dtype=typing.Optional[int], is_attribute=False) +ctime: QbNumericField('ctime', dtype=, is_attribute=False) +description: QbStrField('description', dtype=, is_attribute=False) +extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, + is_subscriptable=True) +filename: QbStrField('filename', dtype=, is_attribute=True) +label: QbStrField('label', dtype=, is_attribute=False) +mtime: QbNumericField('mtime', dtype=, is_attribute=False) +node_type: QbStrField('node_type', dtype=, is_attribute=False) +pk: QbNumericField('pk', dtype=, is_attribute=False) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) -repository_content: QbDictField('repository_content', dtype=typing.Optional[dict[str, - bytes]], is_attribute=False) -repository_metadata: QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, - typing.Any]], is_attribute=False) +repository_content: QbDictField('repository_content', dtype=dict[str, bytes], is_attribute=False) +repository_metadata: QbDictField('repository_metadata', dtype=typing.Dict[str, typing.Any], + is_attribute=False) source: QbDictField('source', dtype=typing.Optional[dict], is_attribute=True, is_subscriptable=True) -user: QbNumericField('user', dtype=typing.Optional[int], is_attribute=False) -uuid: QbStrField('uuid', dtype=typing.Optional[str], is_attribute=False) +user: QbNumericField('user', dtype=, is_attribute=False) +uuid: QbField('uuid', dtype=, is_attribute=False) From fa9582abcae7158a4bb83981b312bad7317e6d59 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Wed, 8 Oct 2025 21:01:14 +0200 Subject: [PATCH 08/59] Fix computer type --- src/aiida/cmdline/groups/dynamic.py | 6 +---- src/aiida/common/pydantic.py | 5 ---- src/aiida/orm/nodes/data/code/installed.py | 29 +++++++--------------- src/aiida/orm/nodes/node.py | 23 +++++++++++++---- 4 files changed, 28 insertions(+), 35 deletions(-) diff --git a/src/aiida/cmdline/groups/dynamic.py b/src/aiida/cmdline/groups/dynamic.py index 415910a05e..4b4a46b949 100644 --- a/src/aiida/cmdline/groups/dynamic.py +++ b/src/aiida/cmdline/groups/dynamic.py @@ -179,11 +179,7 @@ def list_options(self, entry_point: str) -> list[t.Callable[[FC], FC]]: default = field_info.default_factory if field_info.default is PydanticUndefined else field_info.default - option_type = get_metadata(field_info, 'option_type', None) - - if option_type is not None: - field_type = option_type - elif hasattr(field_info.annotation, '__args__'): + if hasattr(field_info.annotation, '__args__'): # If the annotation has the ``__args__`` attribute it is an instance of a type from ``typing`` and # the real type can be gotten from the arguments. For example it could be ``typing.Union[str, None]`` # calling ``typing.Union[str, None].__args__`` will return the tuple ``(str, NoneType)``. So to get diff --git a/src/aiida/common/pydantic.py b/src/aiida/common/pydantic.py index 68b7458783..f12c76952f 100644 --- a/src/aiida/common/pydantic.py +++ b/src/aiida/common/pydantic.py @@ -36,7 +36,6 @@ def MetadataField( # noqa: N802 option_cls: t.Any | None = None, orm_class: type[Entity[t.Any, t.Any]] | str | None = None, orm_to_model: t.Callable[[Entity[t.Any, t.Any], Path], t.Any] | None = None, - option_type: type | None = None, model_to_orm: t.Callable[['BaseModel'], t.Any] | None = None, exclude_to_orm: bool = False, exclude_from_cli: bool = False, @@ -66,9 +65,6 @@ class Model(BaseModel): :param priority: Used to order the list of all fields in the model. Ordering is done from small to large priority. :param short_name: Optional short name to use for an option on a command line interface. :param option_cls: The :class:`click.Option` class to use to construct the option. - :param option_type: The type to use for the option on the command line interface. This is typically a built-in type - such as ``str``, ``int``, ``float``, or ``bool``. If not specified, the type will be inferred from the type - annotation of the field. :param orm_class: The class, or entry point name thereof, to which the field should be converted. If this field is defined, the value of this field should accept an integer which will automatically be converted to an instance of said ORM class using ``orm_class.collection.get(id={field_value})``. This is useful, for example, where a @@ -90,7 +86,6 @@ class Model(BaseModel): ('priority', priority), ('short_name', short_name), ('option_cls', option_cls), - ('option_type', option_type), ('orm_class', orm_class), ('orm_to_model', orm_to_model), ('model_to_orm', model_to_orm), diff --git a/src/aiida/orm/nodes/data/code/installed.py b/src/aiida/orm/nodes/data/code/installed.py index 03246dfe86..b46a94cfed 100644 --- a/src/aiida/orm/nodes/data/code/installed.py +++ b/src/aiida/orm/nodes/data/code/installed.py @@ -19,8 +19,6 @@ import pathlib from typing import cast -from pydantic import field_validator - from aiida.common import exceptions from aiida.common.lang import type_check from aiida.common.log import override_log_level @@ -44,13 +42,12 @@ class InstalledCode(Code): class Model(AbstractCode.Model): """Model describing required information to create an instance.""" - computer: int = MetadataField( + computer: str = MetadataField( title='Computer', - description='The remote computer on which the executable resides.', + description='The label of the remote computer on which the executable resides.', is_attribute=False, - orm_to_model=lambda node, _: cast('InstalledCode', node).computer.pk, - orm_class=Computer, - option_type=str, + orm_to_model=lambda node, _: cast('InstalledCode', node).computer.label, + model_to_orm=lambda model: cast('InstalledCode.Model', model).load_computer(), short_name='-Y', priority=2, ) @@ -62,24 +59,16 @@ class Model(AbstractCode.Model): priority=1, ) - @field_validator('computer', mode='before') - @classmethod - def validate_computer(cls, value: str | int) -> int: - """Validate computer input. - - If provided a string (e.g., via CLI computer setup), try to load the computer and return its PK. + def load_computer(self) -> Computer: + """Load the computer instance. - :param value: The input value, either a string or an integer - :return: The PK of the computer - :raises ValueError: If the computer does not exist + :return: The computer instance. + :raises ValueError: If the computer does not exist. """ from aiida.orm import load_computer - if isinstance(value, int): - return value - try: - return cast(int, load_computer(value).pk) + return load_computer(self.computer) except exceptions.NotExistent as exception: raise ValueError(exception) from exception diff --git a/src/aiida/orm/nodes/node.py b/src/aiida/orm/nodes/node.py index ca44f6df46..8703ddb28d 100644 --- a/src/aiida/orm/nodes/node.py +++ b/src/aiida/orm/nodes/node.py @@ -278,17 +278,17 @@ class Model(Entity.Model): is_subscriptable=True, exclude_from_cli=True, ) - computer: Optional[int] = MetadataField( + computer: Optional[str] = MetadataField( None, - description='The PK of the computer', + description='The label of the computer', is_attribute=False, - orm_to_model=lambda node, _: cast('Node', node).computer.pk if cast('Node', node).computer else None, - orm_class=Computer, + orm_to_model=lambda node, _: cast('Node', node).computer.label if cast('Node', node).computer else None, # type: ignore[union-attr] + model_to_orm=lambda model: cast('Node.Model', model).load_computer(), exclude_from_cli=True, exclude_to_orm=True, ) user: int = MetadataField( - default_factory=lambda: User.collection.get_default().pk, + default_factory=lambda: User.collection.get_default().pk, # type: ignore[union-attr] description='The PK of the user who owns the node', is_attribute=False, orm_to_model=lambda node, _: cast('Node', node).user.pk, @@ -309,6 +309,19 @@ class Model(Entity.Model): exclude_to_orm=True, ) + def load_computer(self) -> Computer: + """Load the computer instance. + + :return: The computer instance. + :raises ValueError: If the computer does not exist. + """ + from aiida.orm import load_computer + + try: + return load_computer(self.computer) + except exceptions.NotExistent as exception: + raise ValueError(exception) from exception + def __init__( self, backend: Optional['StorageBackend'] = None, From 9c1ba17bd33279ebb73733c5a09c051f75534b49 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Thu, 9 Oct 2025 07:47:54 +0200 Subject: [PATCH 09/59] Fix some typing issues --- src/aiida/cmdline/commands/cmd_code.py | 2 +- src/aiida/cmdline/groups/dynamic.py | 4 ++-- src/aiida/orm/entities.py | 14 ++++++++------ src/aiida/orm/groups.py | 2 +- src/aiida/orm/nodes/data/array/trajectory.py | 5 ++--- 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/aiida/cmdline/commands/cmd_code.py b/src/aiida/cmdline/commands/cmd_code.py index 004b876ebe..bd546d22d0 100644 --- a/src/aiida/cmdline/commands/cmd_code.py +++ b/src/aiida/cmdline/commands/cmd_code.py @@ -38,7 +38,7 @@ def verdi_code(): def create_code(ctx: click.Context, cls: 'Code', **kwargs) -> None: """Create a new `Code` instance.""" try: - Model = cls.Model.as_input_model() + Model = cls.Model.as_input_model() # noqa: N806 instance = cls.from_model(Model(**kwargs)) except (TypeError, ValueError) as exception: echo.echo_critical(f'Failed to create instance `{cls}`: {exception}') diff --git a/src/aiida/cmdline/groups/dynamic.py b/src/aiida/cmdline/groups/dynamic.py index 4b4a46b949..15416e5c2f 100644 --- a/src/aiida/cmdline/groups/dynamic.py +++ b/src/aiida/cmdline/groups/dynamic.py @@ -97,7 +97,7 @@ def call_command(self, ctx: click.Context, cls: t.Any, non_interactive: bool, ** if hasattr(cls, 'Model'): # The plugin defines a pydantic model: use it to validate the provided arguments - Model = cls.Model.as_input_model() if hasattr(cls.Model, 'as_input_model') else cls.Model + Model = cls.Model.as_input_model() if hasattr(cls.Model, 'as_input_model') else cls.Model # noqa: N806 try: Model(**kwargs) except ValidationError as exception: @@ -169,7 +169,7 @@ def list_options(self, entry_point: str) -> list[t.Callable[[FC], FC]]: options_spec = self.factory(entry_point).get_cli_options() # type: ignore[union-attr] return [self.create_option(*item) for item in options_spec] - Model = cls.Model.as_input_model() if hasattr(cls.Model, 'as_input_model') else cls.Model + Model = cls.Model.as_input_model() if hasattr(cls.Model, 'as_input_model') else cls.Model # noqa: N806 options_spec = {} diff --git a/src/aiida/orm/entities.py b/src/aiida/orm/entities.py index a45fbaf455..5d65b724f2 100644 --- a/src/aiida/orm/entities.py +++ b/src/aiida/orm/entities.py @@ -14,7 +14,7 @@ import pathlib from enum import Enum from functools import lru_cache -from typing import TYPE_CHECKING, Any, Generic, List, Literal, NoReturn, Optional, Type, TypeVar, Union, cast +from typing import TYPE_CHECKING, Any, Generic, List, Literal, NoReturn, Optional, Type, TypeVar, Union from plumpy.base.utils import call_with_super_check, super_check from pydantic import BaseModel, ConfigDict, create_model @@ -211,7 +211,7 @@ def as_input_model(cls: Type[EntityModelType]) -> Type[EntityModelType]: # Derive the input model from the original model new_name = cls.__qualname__.replace('.Model', 'InputModel') - InputModel = create_model( + InputModel = create_model( # noqa: N806 new_name, __base__=cls, __doc__=f'Input version of {cls.__name__}.', @@ -223,7 +223,9 @@ def as_input_model(cls: Type[EntityModelType]) -> Type[EntityModelType]: readonly_fields = [ name for name, field in InputModel.model_fields.items() - if getattr(field, 'json_schema_extra', {}) and field.json_schema_extra.get('readOnly') + if hasattr(field, 'json_schema_extra') + and isinstance(field.json_schema_extra, dict) + and field.json_schema_extra.get('readOnly') ] # Remove read-only fields @@ -245,7 +247,7 @@ def _prune_field_decorators(field_decorators: dict[str, Any]) -> dict[str, Any]: decorators.field_validators = _prune_field_decorators(decorators.field_validators) decorators.field_serializers = _prune_field_decorators(decorators.field_serializers) - return cast(Type[EntityModelType], InputModel) + return InputModel @classmethod def model_to_orm_fields(cls) -> dict[str, FieldInfo]: @@ -295,7 +297,7 @@ def to_model(self, repository_path: Optional[pathlib.Path] = None, unstored: boo """ fields = {} - Model = self.Model.as_input_model() if unstored else self.Model + Model = self.Model.as_input_model() if unstored else self.Model # noqa: N806 for key, field in Model.model_fields.items(): if orm_to_model := get_metadata(field, 'orm_to_model'): @@ -358,7 +360,7 @@ def from_serialized(cls, unstored: bool = False, **kwargs: dict[str, Any]) -> Se cls._logger.warning( 'Serialization through pydantic is still an experimental feature and might break in future releases.' ) - Model = cls.Model.as_input_model() if unstored else cls.Model + Model = cls.Model.as_input_model() if unstored else cls.Model # noqa: N806 return cls.from_model(Model(**kwargs)) @classproperty diff --git a/src/aiida/orm/groups.py b/src/aiida/orm/groups.py index 4504e79ae2..d62bd19fb9 100644 --- a/src/aiida/orm/groups.py +++ b/src/aiida/orm/groups.py @@ -123,7 +123,7 @@ class Model(entities.Entity.Model): exclude_to_orm=True, ) user: int = MetadataField( - default_factory=lambda: users.User.collection.get_default().pk, + default_factory=lambda: users.User.collection.get_default().pk, # type: ignore[union-attr] description='The PK of the group owner, defaults to the current user', is_attribute=False, orm_class='core.user', diff --git a/src/aiida/orm/nodes/data/array/trajectory.py b/src/aiida/orm/nodes/data/array/trajectory.py index dee116f76c..723d4906a1 100644 --- a/src/aiida/orm/nodes/data/array/trajectory.py +++ b/src/aiida/orm/nodes/data/array/trajectory.py @@ -43,7 +43,6 @@ class Model(ArrayData.Model): ) symbols: List[str] = MetadataField(description='List of symbols') - def __init__( self, structurelist: list[orm.StructureData] | None = None, @@ -105,7 +104,7 @@ def _internal_validate(self, stepids, cells, symbols, positions, times, velociti numatoms = len(symbols) if positions.shape != (numsteps, numatoms, 3): raise ValueError( - 'TrajectoryData.positions must have shape (s,n,3), ' 'with s=number of steps and n=number of symbols' + 'TrajectoryData.positions must have shape (s,n,3), with s=number of steps and n=number of symbols' ) if times is not None: if times.shape != (numsteps,): @@ -426,7 +425,7 @@ def get_step_structure(self, index, custom_kinds=None): for k in custom_kinds: if not isinstance(k, Kind): raise TypeError( - 'Each element of the custom_kinds list must ' 'be a aiida.orm.nodes.data.structure.Kind object' + 'Each element of the custom_kinds list must be a aiida.orm.nodes.data.structure.Kind object' ) kind_names.append(k.name) if len(kind_names) != len(set(kind_names)): From b729290f01458fe2949e9eb06600afcd36f27886 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Fri, 10 Oct 2025 15:05:42 +0200 Subject: [PATCH 10/59] Allow (de)serialization of unstored nodes --- src/aiida/cmdline/commands/cmd_code.py | 2 +- src/aiida/cmdline/groups/dynamic.py | 4 ++-- src/aiida/orm/entities.py | 24 ++++++++++++++++-------- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/aiida/cmdline/commands/cmd_code.py b/src/aiida/cmdline/commands/cmd_code.py index bd546d22d0..fadb69ef67 100644 --- a/src/aiida/cmdline/commands/cmd_code.py +++ b/src/aiida/cmdline/commands/cmd_code.py @@ -38,7 +38,7 @@ def verdi_code(): def create_code(ctx: click.Context, cls: 'Code', **kwargs) -> None: """Create a new `Code` instance.""" try: - Model = cls.Model.as_input_model() # noqa: N806 + Model = cls.InputModel # noqa: N806 instance = cls.from_model(Model(**kwargs)) except (TypeError, ValueError) as exception: echo.echo_critical(f'Failed to create instance `{cls}`: {exception}') diff --git a/src/aiida/cmdline/groups/dynamic.py b/src/aiida/cmdline/groups/dynamic.py index 15416e5c2f..485b059d0b 100644 --- a/src/aiida/cmdline/groups/dynamic.py +++ b/src/aiida/cmdline/groups/dynamic.py @@ -97,7 +97,7 @@ def call_command(self, ctx: click.Context, cls: t.Any, non_interactive: bool, ** if hasattr(cls, 'Model'): # The plugin defines a pydantic model: use it to validate the provided arguments - Model = cls.Model.as_input_model() if hasattr(cls.Model, 'as_input_model') else cls.Model # noqa: N806 + Model = cls.InputModel if hasattr(cls, 'InputModel') else cls.Model # noqa: N806 try: Model(**kwargs) except ValidationError as exception: @@ -169,7 +169,7 @@ def list_options(self, entry_point: str) -> list[t.Callable[[FC], FC]]: options_spec = self.factory(entry_point).get_cli_options() # type: ignore[union-attr] return [self.create_option(*item) for item in options_spec] - Model = cls.Model.as_input_model() if hasattr(cls.Model, 'as_input_model') else cls.Model # noqa: N806 + Model = cls.InputModel if hasattr(cls, 'InputModel') else cls.Model # noqa: N806 options_spec = {} diff --git a/src/aiida/orm/entities.py b/src/aiida/orm/entities.py index 5d65b724f2..7520912955 100644 --- a/src/aiida/orm/entities.py +++ b/src/aiida/orm/entities.py @@ -249,6 +249,14 @@ def _prune_field_decorators(field_decorators: dict[str, Any]) -> dict[str, Any]: return InputModel + @classproperty + def InputModel(cls) -> Type[Model]: # noqa: N805 + """Return the input version of the model class for this entity. + + :return: The input model class, with read-only fields removed. + """ + return cls.Model.as_input_model() + @classmethod def model_to_orm_fields(cls) -> dict[str, FieldInfo]: return { @@ -291,13 +299,13 @@ def to_model(self, repository_path: Optional[pathlib.Path] = None, unstored: boo :param repository_path: If the orm node has files in the repository, this path is used to read the repository files from. If no path is specified a temporary path is created using the entities pk. - :param unstored: If True, the input version of the model is used (via `.as_input_model`), which strips - read-only fields, i.e., fields with `exclude_to_orm=True`. + :param unstored: If True, the input version of the model is used, which strips read-only fields, i.e., fields + with `exclude_to_orm=True`. :return: An instance of the entity's model class. """ fields = {} - Model = self.Model.as_input_model() if unstored else self.Model # noqa: N806 + Model = self.InputModel if unstored else self.Model # noqa: N806 for key, field in Model.model_fields.items(): if orm_to_model := get_metadata(field, 'orm_to_model'): @@ -330,8 +338,8 @@ def serialize( :param mode: The serialization mode, either 'json' or 'python'. The 'json' mode is the most strict and ensures that the output is JSON serializable, whereas the 'python' mode allows for more complex Python types, such as `datetime` objects. - :param unstored: If True, the input version of the model is used (via `.as_input_model`), which strips - read-only fields, i.e., fields with `exclude_to_orm=True`. + :param unstored: If True, the input version of the model is used, which strips read-only fields, i.e., fields + with `exclude_to_orm=True`. :return: A dictionary that can be serialized to JSON. """ self.logger.warning( @@ -353,14 +361,14 @@ def serialize( def from_serialized(cls, unstored: bool = False, **kwargs: dict[str, Any]) -> Self: """Construct an entity instance from JSON serialized data. - :param unstored: If True, the input version of the model is used (via `.as_input_model`), which strips - read-only fields, i.e., fields with `exclude_to_orm=True`. + :param unstored: If True, the input version of the model is used, which strips read-only fields, i.e., fields + with `exclude_to_orm=True`. :return: An instance of the entity class. """ cls._logger.warning( 'Serialization through pydantic is still an experimental feature and might break in future releases.' ) - Model = cls.Model.as_input_model() if unstored else cls.Model # noqa: N806 + Model = cls.InputModel if unstored else cls.Model # noqa: N806 return cls.from_model(Model(**kwargs)) @classproperty From 45d02b07499c5aec0d2aae822f8210667c785839 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Fri, 10 Oct 2025 15:06:19 +0200 Subject: [PATCH 11/59] Add guard for unhandled attributes at parent `Data` constructor --- src/aiida/orm/nodes/data/data.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/aiida/orm/nodes/data/data.py b/src/aiida/orm/nodes/data/data.py index 1b5494565f..67813a1002 100644 --- a/src/aiida/orm/nodes/data/data.py +++ b/src/aiida/orm/nodes/data/data.py @@ -8,6 +8,7 @@ ########################################################################### """Module with `Node` sub class `Data` to be used as a base class for data structures.""" +from collections.abc import Iterable from typing import Dict, Optional from aiida.common import exceptions @@ -21,6 +22,19 @@ __all__ = ('Data',) +class UnhandledDataAttributesError(Exception): + """Exception raised when any data attributes are not handled prior to the Data constructor.""" + + def __init__(self, attributes: Iterable[str], class_name: str) -> None: + bullet_list = '\n'.join(f' • {attr}' for attr in attributes) + message = ( + f'\nThe following attributes must be handled in a constructor prior to the Data class:\n' + f'{bullet_list}\n\n' + f'Consider implementing a constructor in {class_name} to handle the listed attributes.' + ) + super().__init__(message) + + class Data(Node): """The base class for all Data nodes. @@ -57,7 +71,16 @@ class Model(Node.Model): def __init__(self, *args, source=None, **kwargs): """Construct a new instance, setting the ``source`` attribute if provided as a keyword argument.""" + + # We verify here that all attributes of Data plugins are handled in a constructor prior to the root + # Data class (here), gracefully rejecting them otherwise. + node_keys = set(Node.Model.model_fields.keys()) + unhandled_keys = {key for key in kwargs if key not in node_keys} + if unhandled_keys: + raise UnhandledDataAttributesError(unhandled_keys, self.__class__.__name__) + super().__init__(*args, **kwargs) + if source is not None: self.source = source From 5bdf5063355822a77101474480c5e1d1610cb1f9 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Fri, 10 Oct 2025 15:24:17 +0200 Subject: [PATCH 12/59] Implement constructors for `KpointsData` and `BandsData` --- src/aiida/orm/nodes/data/array/array.py | 5 +- src/aiida/orm/nodes/data/array/bands.py | 11 ++- src/aiida/orm/nodes/data/array/kpoints.py | 93 +++++++++++++++-------- 3 files changed, 74 insertions(+), 35 deletions(-) diff --git a/src/aiida/orm/nodes/data/array/array.py b/src/aiida/orm/nodes/data/array/array.py index 9118187b0c..4d11f06fc9 100644 --- a/src/aiida/orm/nodes/data/array/array.py +++ b/src/aiida/orm/nodes/data/array/array.py @@ -59,7 +59,7 @@ class Model(Data.Model): array_prefix = 'array|' default_array_name = 'default' - def __init__(self, arrays: np.ndarray | dict[str, np.ndarray] | None = None, **kwargs): + def __init__(self, arrays: np.ndarray | dict[str, np.ndarray] | list[list[float]] | None = None, **kwargs): """Construct a new instance and set one or multiple numpy arrays. :param arrays: An optional single numpy array, or dictionary of numpy arrays to store. @@ -69,6 +69,9 @@ def __init__(self, arrays: np.ndarray | dict[str, np.ndarray] | None = None, **k arrays = arrays if arrays is not None else {} + if isinstance(arrays, list): + arrays = np.array(arrays) + if isinstance(arrays, np.ndarray): arrays = {self.default_array_name: arrays} diff --git a/src/aiida/orm/nodes/data/array/bands.py b/src/aiida/orm/nodes/data/array/bands.py index f104d9222e..15d3cdf90a 100644 --- a/src/aiida/orm/nodes/data/array/bands.py +++ b/src/aiida/orm/nodes/data/array/bands.py @@ -214,8 +214,15 @@ class BandsData(KpointsData): """Class to handle bands data""" class Model(KpointsData.Model): - array_labels: t.Optional[t.List[str]] = MetadataField(description='Labels associated with the band arrays') - units: str = MetadataField(description='Units in which the data in bands were stored') + units: t.Optional[str] = MetadataField( + None, + description='Units in which the data in bands were stored', + orm_to_model=lambda node, _: t.cast('BandsData', node).base.attributes.get('units', None), + ) + + def __init__(self, *, units: str | None = None, **kwargs): + super().__init__(**kwargs) + self.units = units def set_kpointsdata(self, kpointsdata): """Load the kpoints from a kpoint object. diff --git a/src/aiida/orm/nodes/data/array/kpoints.py b/src/aiida/orm/nodes/data/array/kpoints.py index 5a9b307490..a10373e935 100644 --- a/src/aiida/orm/nodes/data/array/kpoints.py +++ b/src/aiida/orm/nodes/data/array/kpoints.py @@ -51,57 +51,90 @@ class Model(ArrayData.Model): description='Index of the labels in the list of kpoints', orm_to_model=lambda node, _: t.cast('KpointsData', node).base.attributes.get('label_numbers', None), ) - mesh: t.Optional[t.List[int]] = MetadataField( - None, - description='Mesh of kpoints', - orm_to_model=lambda node, _: t.cast('KpointsData', node).base.attributes.get('mesh', None), - ) - offset: t.Optional[t.List[float]] = MetadataField( - None, - description='Offset of kpoints', - orm_to_model=lambda node, _: t.cast('KpointsData', node).base.attributes.get('offset', None), - ) cell: t.Optional[t.List[t.List[float]]] = MetadataField( None, description='Unit cell of the crystal, in Angstroms', + orm_to_model=lambda node, _: t.cast('KpointsData', node).base.attributes.get('cell', None), ) - pbc1: bool = MetadataField( - description='True if the first lattice vector is periodic', + pbc1: t.Optional[bool] = MetadataField( + None, + description='Periodicity in the first lattice vector direction', orm_to_model=lambda node, _: t.cast('KpointsData', node).pbc[0], ) - pbc2: bool = MetadataField( - description='True if the second lattice vector is periodic', + pbc2: t.Optional[bool] = MetadataField( + None, + description='Periodicity in the second lattice vector direction', orm_to_model=lambda node, _: t.cast('KpointsData', node).pbc[1], ) - pbc3: bool = MetadataField( - description='True if the third lattice vector is periodic', + pbc3: t.Optional[bool] = MetadataField( + None, + description='Periodicity in the third lattice vector direction', orm_to_model=lambda node, _: t.cast('KpointsData', node).pbc[2], ) + mesh: t.Optional[t.List[int]] = MetadataField( + None, + description='Mesh of kpoints', + orm_to_model=lambda node, _: t.cast('KpointsData', node).base.attributes.get('mesh', None), + ) + offset: t.Optional[t.List[float]] = MetadataField( + None, + description='Offset of kpoints', + orm_to_model=lambda node, _: t.cast('KpointsData', node).base.attributes.get('offset', None), + ) def __init__( self, + *, labels: list[str] | None = None, label_numbers: list[int] | None = None, - mesh: list[int] | None = None, - offset: list[float] | None = None, cell: list[list[float]] | None = None, pbc1: bool | None = None, pbc2: bool | None = None, pbc3: bool | None = None, + mesh: list[int] | None = None, + offset: list[float] | None = None, **kwargs, ): + arrays = kwargs.pop('arrays', None) + super().__init__(**kwargs) - if cell is not None: - self.cell = cell - if any(pbc is not None for pbc in (pbc1, pbc2, pbc3)): - self.pbc = (pbc1 or False, pbc2 or False, pbc3 or False) - if labels is not None and label_numbers is not None: - if len(labels) != len(label_numbers): - raise ValueError('Labels and label numbers must have the same length') - self.labels = list(zip(label_numbers, labels)) + + if offset is not None and mesh is None: + raise ValueError('Cannot set an offset without a kpoints mesh') + + given_list_kwargs = any(kwarg is not None for kwarg in (labels, label_numbers, cell, pbc1, pbc2, pbc3)) + if mesh is not None: + if arrays is not None or given_list_kwargs: + raise ValueError('When providing a kpoints mesh, only mesh and offset are allowed') self.set_kpoints_mesh(mesh, offset=offset) + if arrays is not None: + if labels is not None and label_numbers is not None: + if len(labels) != len(label_numbers): + raise ValueError('Labels and label numbers must have the same length') + label_list = list(zip(label_numbers, labels)) + else: + label_list = None + + pbc = (pbc1 or False, pbc2 or False, pbc3 or False) + + if cell is not None: + self.set_cell(cell, pbc) + else: + self.pbc = pbc + + if isinstance(arrays, dict): + kpoints = arrays.get('kpoints', None) + if kpoints is None: + raise ValueError("When providing a dict of arrays, it must contain the key 'kpoints'") + arrays = kpoints + + self.set_kpoints(arrays, labels=label_list) + + elif given_list_kwargs: + raise ValueError('Missing kpoints list') + def get_description(self): """Returns a string with infos retrieved from kpoints node's properties. :param node: @@ -123,7 +156,7 @@ def cell(self): """The crystal unit cell. Rows are the crystal vectors in Angstroms. :return: a 3x3 numpy.array """ - return numpy.array(self.base.attributes.get('cell', [])) + return numpy.array(self.base.attributes.get('cell')) @cell.setter def cell(self, value): @@ -390,12 +423,8 @@ def _dimension(self): """Dimensionality of the structure, found from its pbc (i.e. 1 if it's a 1D structure, 2 if its 2D, 3 if it's 3D ...). :return dimensionality: 0, 1, 2 or 3 - :note: will return 3 if pbc has not been set beforehand """ - try: - return sum(self.pbc) - except AttributeError: - return 3 + return sum(self.pbc) def _validate_kpoints_weights(self, kpoints, weights): """Validate the list of kpoints and of weights before storage. From fa1fba783178814a2c5ec76b3dfd2b931035bf98 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Fri, 10 Oct 2025 15:24:23 +0200 Subject: [PATCH 13/59] Fix tests --- ...fields_aiida.data.core.array.ArrayData.yml | 2 +- ..._aiida.data.core.array.bands.BandsData.yml | 12 ++++------ ...da.data.core.array.kpoints.KpointsData.yml | 8 +++---- ...a.core.array.projection.ProjectionData.yml | 2 +- ...a.core.array.trajectory.TrajectoryData.yml | 2 +- ...fields_aiida.data.core.array.xy.XyData.yml | 2 +- .../fields_aiida.data.core.base.BaseType.yml | 2 +- .../fields_aiida.data.core.bool.Bool.yml | 2 +- .../fields_aiida.data.core.cif.CifData.yml | 2 +- .../fields_aiida.data.core.code.Code.yml | 2 +- ...a.data.core.code.abstract.AbstractCode.yml | 2 +- ...e.code.containerized.ContainerizedCode.yml | 2 +- ...data.core.code.installed.InstalledCode.yml | 2 +- ...a.data.core.code.portable.PortableCode.yml | 2 +- .../fields_aiida.data.core.dict.Dict.yml | 2 +- .../fields_aiida.data.core.enum.EnumData.yml | 2 +- .../fields_aiida.data.core.float.Float.yml | 2 +- ...elds_aiida.data.core.folder.FolderData.yml | 2 +- .../fields_aiida.data.core.int.Int.yml | 2 +- ..._aiida.data.core.jsonable.JsonableData.yml | 2 +- .../fields_aiida.data.core.list.List.yml | 2 +- ...ds_aiida.data.core.numeric.NumericType.yml | 2 +- ...ds_aiida.data.core.orbital.OrbitalData.yml | 2 +- ...elds_aiida.data.core.remote.RemoteData.yml | 2 +- ...data.core.remote.stash.RemoteStashData.yml | 2 +- ...ash.compress.RemoteStashCompressedData.yml | 2 +- ...ote.stash.custom.RemoteStashCustomData.yml | 2 +- ...ote.stash.folder.RemoteStashFolderData.yml | 2 +- ...da.data.core.singlefile.SinglefileData.yml | 2 +- .../fields_aiida.data.core.str.Str.yml | 2 +- ...iida.data.core.structure.StructureData.yml | 2 +- .../fields_aiida.data.core.upf.UpfData.yml | 2 +- .../fields_aiida.node.data.Data.yml | 14 +++++++---- .../fields_aiida.node.process.ProcessNode.yml | 14 +++++++---- ...de.process.calculation.CalculationNode.yml | 14 +++++++---- ...culation.calcfunction.CalcFunctionNode.yml | 14 +++++++---- ...rocess.calculation.calcjob.CalcJobNode.yml | 23 ++++++++++++------- ...ida.node.process.workflow.WorkflowNode.yml | 14 +++++++---- ...ocess.workflow.workchain.WorkChainNode.yml | 14 +++++++---- ...workflow.workfunction.WorkFunctionNode.yml | 14 +++++++---- 40 files changed, 117 insertions(+), 84 deletions(-) diff --git a/tests/orm/test_fields/fields_aiida.data.core.array.ArrayData.yml b/tests/orm/test_fields/fields_aiida.data.core.array.ArrayData.yml index fc78fd8d8f..0086bc2b59 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.array.ArrayData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.array.ArrayData.yml @@ -1,7 +1,7 @@ arrays: QbDictField('arrays', dtype=typing.Optional[dict[str, bytes]], is_attribute=True) attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) -computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) +computer: QbStrField('computer', dtype=typing.Optional[str], is_attribute=False) ctime: QbNumericField('ctime', dtype=, is_attribute=False) description: QbStrField('description', dtype=, is_attribute=False) extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, diff --git a/tests/orm/test_fields/fields_aiida.data.core.array.bands.BandsData.yml b/tests/orm/test_fields/fields_aiida.data.core.array.bands.BandsData.yml index 73aae4db4f..361d8c2c48 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.array.bands.BandsData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.array.bands.BandsData.yml @@ -1,11 +1,9 @@ -array_labels: QbArrayField('array_labels', dtype=typing.Optional[typing.List[str]], - is_attribute=True) arrays: QbDictField('arrays', dtype=typing.Optional[dict[str, bytes]], is_attribute=True) attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) cell: QbArrayField('cell', dtype=typing.Optional[typing.List[typing.List[float]]], is_attribute=True) -computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) +computer: QbStrField('computer', dtype=typing.Optional[str], is_attribute=False) ctime: QbNumericField('ctime', dtype=, is_attribute=False) description: QbStrField('description', dtype=, is_attribute=False) extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, @@ -18,15 +16,15 @@ mesh: QbArrayField('mesh', dtype=typing.Optional[typing.List[int]], is_attribute mtime: QbNumericField('mtime', dtype=, is_attribute=False) node_type: QbStrField('node_type', dtype=, is_attribute=False) offset: QbArrayField('offset', dtype=typing.Optional[typing.List[float]], is_attribute=True) -pbc1: QbField('pbc1', dtype=, is_attribute=True) -pbc2: QbField('pbc2', dtype=, is_attribute=True) -pbc3: QbField('pbc3', dtype=, is_attribute=True) +pbc1: QbField('pbc1', dtype=typing.Optional[bool], is_attribute=True) +pbc2: QbField('pbc2', dtype=typing.Optional[bool], is_attribute=True) +pbc3: QbField('pbc3', dtype=typing.Optional[bool], is_attribute=True) pk: QbNumericField('pk', dtype=, is_attribute=False) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) repository_content: QbDictField('repository_content', dtype=dict[str, bytes], is_attribute=False) repository_metadata: QbDictField('repository_metadata', dtype=typing.Dict[str, typing.Any], is_attribute=False) source: QbDictField('source', dtype=typing.Optional[dict], is_attribute=True, is_subscriptable=True) -units: QbStrField('units', dtype=, is_attribute=True) +units: QbStrField('units', dtype=typing.Optional[str], is_attribute=True) user: QbNumericField('user', dtype=, is_attribute=False) uuid: QbField('uuid', dtype=, is_attribute=False) diff --git a/tests/orm/test_fields/fields_aiida.data.core.array.kpoints.KpointsData.yml b/tests/orm/test_fields/fields_aiida.data.core.array.kpoints.KpointsData.yml index 3f307be7fc..e0a0e7533a 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.array.kpoints.KpointsData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.array.kpoints.KpointsData.yml @@ -3,7 +3,7 @@ attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_att is_subscriptable=True) cell: QbArrayField('cell', dtype=typing.Optional[typing.List[typing.List[float]]], is_attribute=True) -computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) +computer: QbStrField('computer', dtype=typing.Optional[str], is_attribute=False) ctime: QbNumericField('ctime', dtype=, is_attribute=False) description: QbStrField('description', dtype=, is_attribute=False) extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, @@ -16,9 +16,9 @@ mesh: QbArrayField('mesh', dtype=typing.Optional[typing.List[int]], is_attribute mtime: QbNumericField('mtime', dtype=, is_attribute=False) node_type: QbStrField('node_type', dtype=, is_attribute=False) offset: QbArrayField('offset', dtype=typing.Optional[typing.List[float]], is_attribute=True) -pbc1: QbField('pbc1', dtype=, is_attribute=True) -pbc2: QbField('pbc2', dtype=, is_attribute=True) -pbc3: QbField('pbc3', dtype=, is_attribute=True) +pbc1: QbField('pbc1', dtype=typing.Optional[bool], is_attribute=True) +pbc2: QbField('pbc2', dtype=typing.Optional[bool], is_attribute=True) +pbc3: QbField('pbc3', dtype=typing.Optional[bool], is_attribute=True) pk: QbNumericField('pk', dtype=, is_attribute=False) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) repository_content: QbDictField('repository_content', dtype=dict[str, bytes], is_attribute=False) diff --git a/tests/orm/test_fields/fields_aiida.data.core.array.projection.ProjectionData.yml b/tests/orm/test_fields/fields_aiida.data.core.array.projection.ProjectionData.yml index fc78fd8d8f..0086bc2b59 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.array.projection.ProjectionData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.array.projection.ProjectionData.yml @@ -1,7 +1,7 @@ arrays: QbDictField('arrays', dtype=typing.Optional[dict[str, bytes]], is_attribute=True) attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) -computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) +computer: QbStrField('computer', dtype=typing.Optional[str], is_attribute=False) ctime: QbNumericField('ctime', dtype=, is_attribute=False) description: QbStrField('description', dtype=, is_attribute=False) extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, diff --git a/tests/orm/test_fields/fields_aiida.data.core.array.trajectory.TrajectoryData.yml b/tests/orm/test_fields/fields_aiida.data.core.array.trajectory.TrajectoryData.yml index 471dc80bbd..0fa7353e3f 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.array.trajectory.TrajectoryData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.array.trajectory.TrajectoryData.yml @@ -1,7 +1,7 @@ arrays: QbDictField('arrays', dtype=typing.Optional[dict[str, bytes]], is_attribute=True) attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) -computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) +computer: QbStrField('computer', dtype=typing.Optional[str], is_attribute=False) ctime: QbNumericField('ctime', dtype=, is_attribute=False) description: QbStrField('description', dtype=, is_attribute=False) extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, diff --git a/tests/orm/test_fields/fields_aiida.data.core.array.xy.XyData.yml b/tests/orm/test_fields/fields_aiida.data.core.array.xy.XyData.yml index fc78fd8d8f..0086bc2b59 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.array.xy.XyData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.array.xy.XyData.yml @@ -1,7 +1,7 @@ arrays: QbDictField('arrays', dtype=typing.Optional[dict[str, bytes]], is_attribute=True) attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) -computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) +computer: QbStrField('computer', dtype=typing.Optional[str], is_attribute=False) ctime: QbNumericField('ctime', dtype=, is_attribute=False) description: QbStrField('description', dtype=, is_attribute=False) extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, diff --git a/tests/orm/test_fields/fields_aiida.data.core.base.BaseType.yml b/tests/orm/test_fields/fields_aiida.data.core.base.BaseType.yml index e92d2eee53..ac20a817c7 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.base.BaseType.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.base.BaseType.yml @@ -1,6 +1,6 @@ attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) -computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) +computer: QbStrField('computer', dtype=typing.Optional[str], is_attribute=False) ctime: QbNumericField('ctime', dtype=, is_attribute=False) description: QbStrField('description', dtype=, is_attribute=False) extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, diff --git a/tests/orm/test_fields/fields_aiida.data.core.bool.Bool.yml b/tests/orm/test_fields/fields_aiida.data.core.bool.Bool.yml index 1eca0fdc91..e01e9f21ed 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.bool.Bool.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.bool.Bool.yml @@ -1,6 +1,6 @@ attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) -computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) +computer: QbStrField('computer', dtype=typing.Optional[str], is_attribute=False) ctime: QbNumericField('ctime', dtype=, is_attribute=False) description: QbStrField('description', dtype=, is_attribute=False) extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, diff --git a/tests/orm/test_fields/fields_aiida.data.core.cif.CifData.yml b/tests/orm/test_fields/fields_aiida.data.core.cif.CifData.yml index a4a5cf39d1..6d1b4bc1a9 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.cif.CifData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.cif.CifData.yml @@ -1,6 +1,6 @@ attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) -computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) +computer: QbStrField('computer', dtype=typing.Optional[str], is_attribute=False) content: QbField('content', dtype=, is_attribute=True) ctime: QbNumericField('ctime', dtype=, is_attribute=False) description: QbStrField('description', dtype=, is_attribute=False) diff --git a/tests/orm/test_fields/fields_aiida.data.core.code.Code.yml b/tests/orm/test_fields/fields_aiida.data.core.code.Code.yml index 9d42d122e3..df9f20e1fc 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.code.Code.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.code.Code.yml @@ -1,7 +1,7 @@ append_text: QbStrField('append_text', dtype=, is_attribute=True) attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) -computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) +computer: QbStrField('computer', dtype=typing.Optional[str], is_attribute=False) ctime: QbNumericField('ctime', dtype=, is_attribute=False) default_calc_job_plugin: QbStrField('default_calc_job_plugin', dtype=typing.Optional[str], is_attribute=True) diff --git a/tests/orm/test_fields/fields_aiida.data.core.code.abstract.AbstractCode.yml b/tests/orm/test_fields/fields_aiida.data.core.code.abstract.AbstractCode.yml index d42b0bfe44..ee8bb7beef 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.code.abstract.AbstractCode.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.code.abstract.AbstractCode.yml @@ -1,7 +1,7 @@ append_text: QbStrField('append_text', dtype=, is_attribute=True) attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) -computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) +computer: QbStrField('computer', dtype=typing.Optional[str], is_attribute=False) ctime: QbNumericField('ctime', dtype=, is_attribute=False) default_calc_job_plugin: QbStrField('default_calc_job_plugin', dtype=typing.Optional[str], is_attribute=True) diff --git a/tests/orm/test_fields/fields_aiida.data.core.code.containerized.ContainerizedCode.yml b/tests/orm/test_fields/fields_aiida.data.core.code.containerized.ContainerizedCode.yml index 60fe67b7d7..4511495ff7 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.code.containerized.ContainerizedCode.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.code.containerized.ContainerizedCode.yml @@ -1,7 +1,7 @@ append_text: QbStrField('append_text', dtype=, is_attribute=True) attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) -computer: QbNumericField('computer', dtype=, is_attribute=False) +computer: QbStrField('computer', dtype=, is_attribute=False) ctime: QbNumericField('ctime', dtype=, is_attribute=False) default_calc_job_plugin: QbStrField('default_calc_job_plugin', dtype=typing.Optional[str], is_attribute=True) diff --git a/tests/orm/test_fields/fields_aiida.data.core.code.installed.InstalledCode.yml b/tests/orm/test_fields/fields_aiida.data.core.code.installed.InstalledCode.yml index a7141d189f..6bdfcaed89 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.code.installed.InstalledCode.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.code.installed.InstalledCode.yml @@ -1,7 +1,7 @@ append_text: QbStrField('append_text', dtype=, is_attribute=True) attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) -computer: QbNumericField('computer', dtype=, is_attribute=False) +computer: QbStrField('computer', dtype=, is_attribute=False) ctime: QbNumericField('ctime', dtype=, is_attribute=False) default_calc_job_plugin: QbStrField('default_calc_job_plugin', dtype=typing.Optional[str], is_attribute=True) diff --git a/tests/orm/test_fields/fields_aiida.data.core.code.portable.PortableCode.yml b/tests/orm/test_fields/fields_aiida.data.core.code.portable.PortableCode.yml index be524b7ce8..6d69c90cda 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.code.portable.PortableCode.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.code.portable.PortableCode.yml @@ -1,7 +1,7 @@ append_text: QbStrField('append_text', dtype=, is_attribute=True) attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) -computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) +computer: QbStrField('computer', dtype=typing.Optional[str], is_attribute=False) ctime: QbNumericField('ctime', dtype=, is_attribute=False) default_calc_job_plugin: QbStrField('default_calc_job_plugin', dtype=typing.Optional[str], is_attribute=True) diff --git a/tests/orm/test_fields/fields_aiida.data.core.dict.Dict.yml b/tests/orm/test_fields/fields_aiida.data.core.dict.Dict.yml index 072cf14701..95d389477e 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.dict.Dict.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.dict.Dict.yml @@ -1,6 +1,6 @@ attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) -computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) +computer: QbStrField('computer', dtype=typing.Optional[str], is_attribute=False) ctime: QbNumericField('ctime', dtype=, is_attribute=False) description: QbStrField('description', dtype=, is_attribute=False) extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, diff --git a/tests/orm/test_fields/fields_aiida.data.core.enum.EnumData.yml b/tests/orm/test_fields/fields_aiida.data.core.enum.EnumData.yml index fd0996222c..8df237bcef 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.enum.EnumData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.enum.EnumData.yml @@ -1,6 +1,6 @@ attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) -computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) +computer: QbStrField('computer', dtype=typing.Optional[str], is_attribute=False) ctime: QbNumericField('ctime', dtype=, is_attribute=False) description: QbStrField('description', dtype=, is_attribute=False) extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, diff --git a/tests/orm/test_fields/fields_aiida.data.core.float.Float.yml b/tests/orm/test_fields/fields_aiida.data.core.float.Float.yml index 7329e70729..6c891b8fa0 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.float.Float.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.float.Float.yml @@ -1,6 +1,6 @@ attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) -computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) +computer: QbStrField('computer', dtype=typing.Optional[str], is_attribute=False) ctime: QbNumericField('ctime', dtype=, is_attribute=False) description: QbStrField('description', dtype=, is_attribute=False) extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, diff --git a/tests/orm/test_fields/fields_aiida.data.core.folder.FolderData.yml b/tests/orm/test_fields/fields_aiida.data.core.folder.FolderData.yml index 5f6f9a6152..07acd8a330 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.folder.FolderData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.folder.FolderData.yml @@ -1,6 +1,6 @@ attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) -computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) +computer: QbStrField('computer', dtype=typing.Optional[str], is_attribute=False) ctime: QbNumericField('ctime', dtype=, is_attribute=False) description: QbStrField('description', dtype=, is_attribute=False) extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, diff --git a/tests/orm/test_fields/fields_aiida.data.core.int.Int.yml b/tests/orm/test_fields/fields_aiida.data.core.int.Int.yml index 45bebdb32c..0e34cfd29a 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.int.Int.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.int.Int.yml @@ -1,6 +1,6 @@ attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) -computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) +computer: QbStrField('computer', dtype=typing.Optional[str], is_attribute=False) ctime: QbNumericField('ctime', dtype=, is_attribute=False) description: QbStrField('description', dtype=, is_attribute=False) extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, diff --git a/tests/orm/test_fields/fields_aiida.data.core.jsonable.JsonableData.yml b/tests/orm/test_fields/fields_aiida.data.core.jsonable.JsonableData.yml index 8ddfceb496..3ea41ba032 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.jsonable.JsonableData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.jsonable.JsonableData.yml @@ -1,6 +1,6 @@ attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) -computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) +computer: QbStrField('computer', dtype=typing.Optional[str], is_attribute=False) ctime: QbNumericField('ctime', dtype=, is_attribute=False) description: QbStrField('description', dtype=, is_attribute=False) extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, diff --git a/tests/orm/test_fields/fields_aiida.data.core.list.List.yml b/tests/orm/test_fields/fields_aiida.data.core.list.List.yml index 7b2a004b3e..e91319b9d6 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.list.List.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.list.List.yml @@ -1,6 +1,6 @@ attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) -computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) +computer: QbStrField('computer', dtype=typing.Optional[str], is_attribute=False) ctime: QbNumericField('ctime', dtype=, is_attribute=False) description: QbStrField('description', dtype=, is_attribute=False) extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, diff --git a/tests/orm/test_fields/fields_aiida.data.core.numeric.NumericType.yml b/tests/orm/test_fields/fields_aiida.data.core.numeric.NumericType.yml index e92d2eee53..ac20a817c7 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.numeric.NumericType.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.numeric.NumericType.yml @@ -1,6 +1,6 @@ attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) -computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) +computer: QbStrField('computer', dtype=typing.Optional[str], is_attribute=False) ctime: QbNumericField('ctime', dtype=, is_attribute=False) description: QbStrField('description', dtype=, is_attribute=False) extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, diff --git a/tests/orm/test_fields/fields_aiida.data.core.orbital.OrbitalData.yml b/tests/orm/test_fields/fields_aiida.data.core.orbital.OrbitalData.yml index 5f6f9a6152..07acd8a330 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.orbital.OrbitalData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.orbital.OrbitalData.yml @@ -1,6 +1,6 @@ attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) -computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) +computer: QbStrField('computer', dtype=typing.Optional[str], is_attribute=False) ctime: QbNumericField('ctime', dtype=, is_attribute=False) description: QbStrField('description', dtype=, is_attribute=False) extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, diff --git a/tests/orm/test_fields/fields_aiida.data.core.remote.RemoteData.yml b/tests/orm/test_fields/fields_aiida.data.core.remote.RemoteData.yml index 809880670b..dae0981bc0 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.remote.RemoteData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.remote.RemoteData.yml @@ -1,6 +1,6 @@ attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) -computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) +computer: QbStrField('computer', dtype=typing.Optional[str], is_attribute=False) ctime: QbNumericField('ctime', dtype=, is_attribute=False) description: QbStrField('description', dtype=, is_attribute=False) extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, diff --git a/tests/orm/test_fields/fields_aiida.data.core.remote.stash.RemoteStashData.yml b/tests/orm/test_fields/fields_aiida.data.core.remote.stash.RemoteStashData.yml index a76a52caab..fdb6988347 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.remote.stash.RemoteStashData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.remote.stash.RemoteStashData.yml @@ -1,6 +1,6 @@ attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) -computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) +computer: QbStrField('computer', dtype=typing.Optional[str], is_attribute=False) ctime: QbNumericField('ctime', dtype=, is_attribute=False) description: QbStrField('description', dtype=, is_attribute=False) extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, diff --git a/tests/orm/test_fields/fields_aiida.data.core.remote.stash.compress.RemoteStashCompressedData.yml b/tests/orm/test_fields/fields_aiida.data.core.remote.stash.compress.RemoteStashCompressedData.yml index 8161a94814..710840981b 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.remote.stash.compress.RemoteStashCompressedData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.remote.stash.compress.RemoteStashCompressedData.yml @@ -1,6 +1,6 @@ attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) -computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) +computer: QbStrField('computer', dtype=typing.Optional[str], is_attribute=False) ctime: QbNumericField('ctime', dtype=, is_attribute=False) dereference: QbField('dereference', dtype=, is_attribute=True) description: QbStrField('description', dtype=, is_attribute=False) diff --git a/tests/orm/test_fields/fields_aiida.data.core.remote.stash.custom.RemoteStashCustomData.yml b/tests/orm/test_fields/fields_aiida.data.core.remote.stash.custom.RemoteStashCustomData.yml index bf55326669..6713968e95 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.remote.stash.custom.RemoteStashCustomData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.remote.stash.custom.RemoteStashCustomData.yml @@ -1,6 +1,6 @@ attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) -computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) +computer: QbStrField('computer', dtype=typing.Optional[str], is_attribute=False) ctime: QbNumericField('ctime', dtype=, is_attribute=False) description: QbStrField('description', dtype=, is_attribute=False) extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, diff --git a/tests/orm/test_fields/fields_aiida.data.core.remote.stash.folder.RemoteStashFolderData.yml b/tests/orm/test_fields/fields_aiida.data.core.remote.stash.folder.RemoteStashFolderData.yml index bf55326669..6713968e95 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.remote.stash.folder.RemoteStashFolderData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.remote.stash.folder.RemoteStashFolderData.yml @@ -1,6 +1,6 @@ attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) -computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) +computer: QbStrField('computer', dtype=typing.Optional[str], is_attribute=False) ctime: QbNumericField('ctime', dtype=, is_attribute=False) description: QbStrField('description', dtype=, is_attribute=False) extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, diff --git a/tests/orm/test_fields/fields_aiida.data.core.singlefile.SinglefileData.yml b/tests/orm/test_fields/fields_aiida.data.core.singlefile.SinglefileData.yml index 765c28aded..c320eb4374 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.singlefile.SinglefileData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.singlefile.SinglefileData.yml @@ -1,6 +1,6 @@ attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) -computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) +computer: QbStrField('computer', dtype=typing.Optional[str], is_attribute=False) content: QbField('content', dtype=, is_attribute=True) ctime: QbNumericField('ctime', dtype=, is_attribute=False) description: QbStrField('description', dtype=, is_attribute=False) diff --git a/tests/orm/test_fields/fields_aiida.data.core.str.Str.yml b/tests/orm/test_fields/fields_aiida.data.core.str.Str.yml index 0a5ee56ef8..73117515a3 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.str.Str.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.str.Str.yml @@ -1,6 +1,6 @@ attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) -computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) +computer: QbStrField('computer', dtype=typing.Optional[str], is_attribute=False) ctime: QbNumericField('ctime', dtype=, is_attribute=False) description: QbStrField('description', dtype=, is_attribute=False) extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, diff --git a/tests/orm/test_fields/fields_aiida.data.core.structure.StructureData.yml b/tests/orm/test_fields/fields_aiida.data.core.structure.StructureData.yml index b4b87257ab..0b7b4518ec 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.structure.StructureData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.structure.StructureData.yml @@ -1,7 +1,7 @@ attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) cell: QbArrayField('cell', dtype=typing.List[typing.List[float]], is_attribute=True) -computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) +computer: QbStrField('computer', dtype=typing.Optional[str], is_attribute=False) ctime: QbNumericField('ctime', dtype=, is_attribute=False) description: QbStrField('description', dtype=, is_attribute=False) extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, diff --git a/tests/orm/test_fields/fields_aiida.data.core.upf.UpfData.yml b/tests/orm/test_fields/fields_aiida.data.core.upf.UpfData.yml index 765c28aded..c320eb4374 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.upf.UpfData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.upf.UpfData.yml @@ -1,6 +1,6 @@ attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) -computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) +computer: QbStrField('computer', dtype=typing.Optional[str], is_attribute=False) content: QbField('content', dtype=, is_attribute=True) ctime: QbNumericField('ctime', dtype=, is_attribute=False) description: QbStrField('description', dtype=, is_attribute=False) diff --git a/tests/orm/test_fields/fields_aiida.node.data.Data.yml b/tests/orm/test_fields/fields_aiida.node.data.Data.yml index 5bee2ef441..cf63271c5e 100644 --- a/tests/orm/test_fields/fields_aiida.node.data.Data.yml +++ b/tests/orm/test_fields/fields_aiida.node.data.Data.yml @@ -1,18 +1,22 @@ -attributes: QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], +attributes: + QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], is_attribute=False, is_subscriptable=True) -computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) +computer: QbNumericField('computer', dtype=typing.Optional[str], is_attribute=False) ctime: QbNumericField('ctime', dtype=typing.Optional[datetime.datetime], is_attribute=False) description: QbStrField('description', dtype=typing.Optional[str], is_attribute=False) -extras: QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], +extras: + QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], is_attribute=False, is_subscriptable=True) label: QbStrField('label', dtype=typing.Optional[str], is_attribute=False) mtime: QbNumericField('mtime', dtype=typing.Optional[datetime.datetime], is_attribute=False) node_type: QbStrField('node_type', dtype=typing.Optional[str], is_attribute=False) pk: QbNumericField('pk', dtype=typing.Optional[int], is_attribute=False) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) -repository_content: QbDictField('repository_content', dtype=typing.Optional[dict[str, +repository_content: + QbDictField('repository_content', dtype=typing.Optional[dict[str, bytes]], is_attribute=False) -repository_metadata: QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, +repository_metadata: + QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, typing.Any]], is_attribute=False) source: QbDictField('source', dtype=typing.Optional[dict], is_attribute=True, is_subscriptable=True) user: QbNumericField('user', dtype=typing.Optional[int], is_attribute=False) diff --git a/tests/orm/test_fields/fields_aiida.node.process.ProcessNode.yml b/tests/orm/test_fields/fields_aiida.node.process.ProcessNode.yml index d8928ee1a4..6639d94fba 100644 --- a/tests/orm/test_fields/fields_aiida.node.process.ProcessNode.yml +++ b/tests/orm/test_fields/fields_aiida.node.process.ProcessNode.yml @@ -1,12 +1,14 @@ -attributes: QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], +attributes: + QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], is_attribute=False, is_subscriptable=True) -computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) +computer: QbNumericField('computer', dtype=typing.Optional[str], is_attribute=False) ctime: QbNumericField('ctime', dtype=typing.Optional[datetime.datetime], is_attribute=False) description: QbStrField('description', dtype=typing.Optional[str], is_attribute=False) exception: QbStrField('exception', dtype=typing.Optional[str], is_attribute=True) exit_message: QbStrField('exit_message', dtype=typing.Optional[str], is_attribute=True) exit_status: QbNumericField('exit_status', dtype=typing.Optional[int], is_attribute=True) -extras: QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], +extras: + QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], is_attribute=False, is_subscriptable=True) label: QbStrField('label', dtype=typing.Optional[str], is_attribute=False) mtime: QbNumericField('mtime', dtype=typing.Optional[datetime.datetime], is_attribute=False) @@ -17,9 +19,11 @@ process_label: QbStrField('process_label', dtype=typing.Optional[str], is_attrib process_state: QbStrField('process_state', dtype=typing.Optional[str], is_attribute=True) process_status: QbStrField('process_status', dtype=typing.Optional[str], is_attribute=True) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) -repository_content: QbDictField('repository_content', dtype=typing.Optional[dict[str, +repository_content: + QbDictField('repository_content', dtype=typing.Optional[dict[str, bytes]], is_attribute=False) -repository_metadata: QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, +repository_metadata: + QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, typing.Any]], is_attribute=False) sealed: QbField('sealed', dtype=, is_attribute=True) user: QbNumericField('user', dtype=typing.Optional[int], is_attribute=False) diff --git a/tests/orm/test_fields/fields_aiida.node.process.calculation.CalculationNode.yml b/tests/orm/test_fields/fields_aiida.node.process.calculation.CalculationNode.yml index d8928ee1a4..6639d94fba 100644 --- a/tests/orm/test_fields/fields_aiida.node.process.calculation.CalculationNode.yml +++ b/tests/orm/test_fields/fields_aiida.node.process.calculation.CalculationNode.yml @@ -1,12 +1,14 @@ -attributes: QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], +attributes: + QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], is_attribute=False, is_subscriptable=True) -computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) +computer: QbNumericField('computer', dtype=typing.Optional[str], is_attribute=False) ctime: QbNumericField('ctime', dtype=typing.Optional[datetime.datetime], is_attribute=False) description: QbStrField('description', dtype=typing.Optional[str], is_attribute=False) exception: QbStrField('exception', dtype=typing.Optional[str], is_attribute=True) exit_message: QbStrField('exit_message', dtype=typing.Optional[str], is_attribute=True) exit_status: QbNumericField('exit_status', dtype=typing.Optional[int], is_attribute=True) -extras: QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], +extras: + QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], is_attribute=False, is_subscriptable=True) label: QbStrField('label', dtype=typing.Optional[str], is_attribute=False) mtime: QbNumericField('mtime', dtype=typing.Optional[datetime.datetime], is_attribute=False) @@ -17,9 +19,11 @@ process_label: QbStrField('process_label', dtype=typing.Optional[str], is_attrib process_state: QbStrField('process_state', dtype=typing.Optional[str], is_attribute=True) process_status: QbStrField('process_status', dtype=typing.Optional[str], is_attribute=True) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) -repository_content: QbDictField('repository_content', dtype=typing.Optional[dict[str, +repository_content: + QbDictField('repository_content', dtype=typing.Optional[dict[str, bytes]], is_attribute=False) -repository_metadata: QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, +repository_metadata: + QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, typing.Any]], is_attribute=False) sealed: QbField('sealed', dtype=, is_attribute=True) user: QbNumericField('user', dtype=typing.Optional[int], is_attribute=False) diff --git a/tests/orm/test_fields/fields_aiida.node.process.calculation.calcfunction.CalcFunctionNode.yml b/tests/orm/test_fields/fields_aiida.node.process.calculation.calcfunction.CalcFunctionNode.yml index d8928ee1a4..6639d94fba 100644 --- a/tests/orm/test_fields/fields_aiida.node.process.calculation.calcfunction.CalcFunctionNode.yml +++ b/tests/orm/test_fields/fields_aiida.node.process.calculation.calcfunction.CalcFunctionNode.yml @@ -1,12 +1,14 @@ -attributes: QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], +attributes: + QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], is_attribute=False, is_subscriptable=True) -computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) +computer: QbNumericField('computer', dtype=typing.Optional[str], is_attribute=False) ctime: QbNumericField('ctime', dtype=typing.Optional[datetime.datetime], is_attribute=False) description: QbStrField('description', dtype=typing.Optional[str], is_attribute=False) exception: QbStrField('exception', dtype=typing.Optional[str], is_attribute=True) exit_message: QbStrField('exit_message', dtype=typing.Optional[str], is_attribute=True) exit_status: QbNumericField('exit_status', dtype=typing.Optional[int], is_attribute=True) -extras: QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], +extras: + QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], is_attribute=False, is_subscriptable=True) label: QbStrField('label', dtype=typing.Optional[str], is_attribute=False) mtime: QbNumericField('mtime', dtype=typing.Optional[datetime.datetime], is_attribute=False) @@ -17,9 +19,11 @@ process_label: QbStrField('process_label', dtype=typing.Optional[str], is_attrib process_state: QbStrField('process_state', dtype=typing.Optional[str], is_attribute=True) process_status: QbStrField('process_status', dtype=typing.Optional[str], is_attribute=True) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) -repository_content: QbDictField('repository_content', dtype=typing.Optional[dict[str, +repository_content: + QbDictField('repository_content', dtype=typing.Optional[dict[str, bytes]], is_attribute=False) -repository_metadata: QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, +repository_metadata: + QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, typing.Any]], is_attribute=False) sealed: QbField('sealed', dtype=, is_attribute=True) user: QbNumericField('user', dtype=typing.Optional[int], is_attribute=False) diff --git a/tests/orm/test_fields/fields_aiida.node.process.calculation.calcjob.CalcJobNode.yml b/tests/orm/test_fields/fields_aiida.node.process.calculation.calcjob.CalcJobNode.yml index 8da4b34cb8..fc0b31b968 100644 --- a/tests/orm/test_fields/fields_aiida.node.process.calculation.calcjob.CalcJobNode.yml +++ b/tests/orm/test_fields/fields_aiida.node.process.calculation.calcjob.CalcJobNode.yml @@ -1,13 +1,15 @@ -attributes: QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], +attributes: + QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], is_attribute=False, is_subscriptable=True) -computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) +computer: QbNumericField('computer', dtype=typing.Optional[str], is_attribute=False) ctime: QbNumericField('ctime', dtype=typing.Optional[datetime.datetime], is_attribute=False) description: QbStrField('description', dtype=typing.Optional[str], is_attribute=False) detailed_job_info: QbDictField('detailed_job_info', dtype=typing.Optional[dict], is_attribute=True) exception: QbStrField('exception', dtype=typing.Optional[str], is_attribute=True) exit_message: QbStrField('exit_message', dtype=typing.Optional[str], is_attribute=True) exit_status: QbNumericField('exit_status', dtype=typing.Optional[int], is_attribute=True) -extras: QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], +extras: + QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], is_attribute=False, is_subscriptable=True) imported: QbField('imported', dtype=typing.Optional[bool], is_attribute=True) job_id: QbStrField('job_id', dtype=typing.Optional[str], is_attribute=True) @@ -22,15 +24,20 @@ process_state: QbStrField('process_state', dtype=typing.Optional[str], is_attrib process_status: QbStrField('process_status', dtype=typing.Optional[str], is_attribute=True) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) remote_workdir: QbStrField('remote_workdir', dtype=typing.Optional[str], is_attribute=True) -repository_content: QbDictField('repository_content', dtype=typing.Optional[dict[str, +repository_content: + QbDictField('repository_content', dtype=typing.Optional[dict[str, bytes]], is_attribute=False) -repository_metadata: QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, +repository_metadata: + QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, typing.Any]], is_attribute=False) -retrieve_list: QbArrayField('retrieve_list', dtype=typing.Optional[typing.List[str]], +retrieve_list: + QbArrayField('retrieve_list', dtype=typing.Optional[typing.List[str]], is_attribute=True) -retrieve_temporary_list: QbArrayField('retrieve_temporary_list', dtype=typing.Optional[typing.List[str]], +retrieve_temporary_list: + QbArrayField('retrieve_temporary_list', dtype=typing.Optional[typing.List[str]], is_attribute=True) -scheduler_lastchecktime: QbStrField('scheduler_lastchecktime', dtype=typing.Optional[str], +scheduler_lastchecktime: + QbStrField('scheduler_lastchecktime', dtype=typing.Optional[str], is_attribute=True) scheduler_state: QbStrField('scheduler_state', dtype=typing.Optional[str], is_attribute=True) sealed: QbField('sealed', dtype=, is_attribute=True) diff --git a/tests/orm/test_fields/fields_aiida.node.process.workflow.WorkflowNode.yml b/tests/orm/test_fields/fields_aiida.node.process.workflow.WorkflowNode.yml index d8928ee1a4..6639d94fba 100644 --- a/tests/orm/test_fields/fields_aiida.node.process.workflow.WorkflowNode.yml +++ b/tests/orm/test_fields/fields_aiida.node.process.workflow.WorkflowNode.yml @@ -1,12 +1,14 @@ -attributes: QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], +attributes: + QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], is_attribute=False, is_subscriptable=True) -computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) +computer: QbNumericField('computer', dtype=typing.Optional[str], is_attribute=False) ctime: QbNumericField('ctime', dtype=typing.Optional[datetime.datetime], is_attribute=False) description: QbStrField('description', dtype=typing.Optional[str], is_attribute=False) exception: QbStrField('exception', dtype=typing.Optional[str], is_attribute=True) exit_message: QbStrField('exit_message', dtype=typing.Optional[str], is_attribute=True) exit_status: QbNumericField('exit_status', dtype=typing.Optional[int], is_attribute=True) -extras: QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], +extras: + QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], is_attribute=False, is_subscriptable=True) label: QbStrField('label', dtype=typing.Optional[str], is_attribute=False) mtime: QbNumericField('mtime', dtype=typing.Optional[datetime.datetime], is_attribute=False) @@ -17,9 +19,11 @@ process_label: QbStrField('process_label', dtype=typing.Optional[str], is_attrib process_state: QbStrField('process_state', dtype=typing.Optional[str], is_attribute=True) process_status: QbStrField('process_status', dtype=typing.Optional[str], is_attribute=True) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) -repository_content: QbDictField('repository_content', dtype=typing.Optional[dict[str, +repository_content: + QbDictField('repository_content', dtype=typing.Optional[dict[str, bytes]], is_attribute=False) -repository_metadata: QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, +repository_metadata: + QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, typing.Any]], is_attribute=False) sealed: QbField('sealed', dtype=, is_attribute=True) user: QbNumericField('user', dtype=typing.Optional[int], is_attribute=False) diff --git a/tests/orm/test_fields/fields_aiida.node.process.workflow.workchain.WorkChainNode.yml b/tests/orm/test_fields/fields_aiida.node.process.workflow.workchain.WorkChainNode.yml index d8928ee1a4..6639d94fba 100644 --- a/tests/orm/test_fields/fields_aiida.node.process.workflow.workchain.WorkChainNode.yml +++ b/tests/orm/test_fields/fields_aiida.node.process.workflow.workchain.WorkChainNode.yml @@ -1,12 +1,14 @@ -attributes: QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], +attributes: + QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], is_attribute=False, is_subscriptable=True) -computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) +computer: QbNumericField('computer', dtype=typing.Optional[str], is_attribute=False) ctime: QbNumericField('ctime', dtype=typing.Optional[datetime.datetime], is_attribute=False) description: QbStrField('description', dtype=typing.Optional[str], is_attribute=False) exception: QbStrField('exception', dtype=typing.Optional[str], is_attribute=True) exit_message: QbStrField('exit_message', dtype=typing.Optional[str], is_attribute=True) exit_status: QbNumericField('exit_status', dtype=typing.Optional[int], is_attribute=True) -extras: QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], +extras: + QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], is_attribute=False, is_subscriptable=True) label: QbStrField('label', dtype=typing.Optional[str], is_attribute=False) mtime: QbNumericField('mtime', dtype=typing.Optional[datetime.datetime], is_attribute=False) @@ -17,9 +19,11 @@ process_label: QbStrField('process_label', dtype=typing.Optional[str], is_attrib process_state: QbStrField('process_state', dtype=typing.Optional[str], is_attribute=True) process_status: QbStrField('process_status', dtype=typing.Optional[str], is_attribute=True) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) -repository_content: QbDictField('repository_content', dtype=typing.Optional[dict[str, +repository_content: + QbDictField('repository_content', dtype=typing.Optional[dict[str, bytes]], is_attribute=False) -repository_metadata: QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, +repository_metadata: + QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, typing.Any]], is_attribute=False) sealed: QbField('sealed', dtype=, is_attribute=True) user: QbNumericField('user', dtype=typing.Optional[int], is_attribute=False) diff --git a/tests/orm/test_fields/fields_aiida.node.process.workflow.workfunction.WorkFunctionNode.yml b/tests/orm/test_fields/fields_aiida.node.process.workflow.workfunction.WorkFunctionNode.yml index d8928ee1a4..6639d94fba 100644 --- a/tests/orm/test_fields/fields_aiida.node.process.workflow.workfunction.WorkFunctionNode.yml +++ b/tests/orm/test_fields/fields_aiida.node.process.workflow.workfunction.WorkFunctionNode.yml @@ -1,12 +1,14 @@ -attributes: QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], +attributes: + QbDictField('attributes', dtype=typing.Optional[typing.Dict[str, typing.Any]], is_attribute=False, is_subscriptable=True) -computer: QbNumericField('computer', dtype=typing.Optional[int], is_attribute=False) +computer: QbNumericField('computer', dtype=typing.Optional[str], is_attribute=False) ctime: QbNumericField('ctime', dtype=typing.Optional[datetime.datetime], is_attribute=False) description: QbStrField('description', dtype=typing.Optional[str], is_attribute=False) exception: QbStrField('exception', dtype=typing.Optional[str], is_attribute=True) exit_message: QbStrField('exit_message', dtype=typing.Optional[str], is_attribute=True) exit_status: QbNumericField('exit_status', dtype=typing.Optional[int], is_attribute=True) -extras: QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], +extras: + QbDictField('extras', dtype=typing.Optional[typing.Dict[str, typing.Any]], is_attribute=False, is_subscriptable=True) label: QbStrField('label', dtype=typing.Optional[str], is_attribute=False) mtime: QbNumericField('mtime', dtype=typing.Optional[datetime.datetime], is_attribute=False) @@ -17,9 +19,11 @@ process_label: QbStrField('process_label', dtype=typing.Optional[str], is_attrib process_state: QbStrField('process_state', dtype=typing.Optional[str], is_attribute=True) process_status: QbStrField('process_status', dtype=typing.Optional[str], is_attribute=True) process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribute=False) -repository_content: QbDictField('repository_content', dtype=typing.Optional[dict[str, +repository_content: + QbDictField('repository_content', dtype=typing.Optional[dict[str, bytes]], is_attribute=False) -repository_metadata: QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, +repository_metadata: + QbDictField('repository_metadata', dtype=typing.Optional[typing.Dict[str, typing.Any]], is_attribute=False) sealed: QbField('sealed', dtype=, is_attribute=True) user: QbNumericField('user', dtype=typing.Optional[int], is_attribute=False) From 78c3ea511727e72e463a59c88afa0f40c5baf6b5 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Fri, 10 Oct 2025 16:40:21 +0200 Subject: [PATCH 14/59] Serialize/validate arrays as numpy arrays, not bytes --- src/aiida/orm/nodes/data/array/array.py | 29 ++++++++++++------- ...fields_aiida.data.core.array.ArrayData.yml | 2 +- ..._aiida.data.core.array.bands.BandsData.yml | 2 +- ...da.data.core.array.kpoints.KpointsData.yml | 2 +- ...a.core.array.projection.ProjectionData.yml | 2 +- ...a.core.array.trajectory.TrajectoryData.yml | 2 +- ...fields_aiida.data.core.array.xy.XyData.yml | 2 +- 7 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/aiida/orm/nodes/data/array/array.py b/src/aiida/orm/nodes/data/array/array.py index 4d11f06fc9..e9e9d2128e 100644 --- a/src/aiida/orm/nodes/data/array/array.py +++ b/src/aiida/orm/nodes/data/array/array.py @@ -12,10 +12,11 @@ import base64 import io +from collections.abc import Iterable from typing import Any, Iterator, Optional import numpy as np -from pydantic import ConfigDict +from pydantic import ConfigDict, field_serializer, field_validator from aiida.common.pydantic import MetadataField @@ -49,17 +50,27 @@ class ArrayData(Data): class Model(Data.Model): model_config = ConfigDict(arbitrary_types_allowed=True) - arrays: Optional[dict[str, bytes]] = MetadataField( + + arrays: Optional[dict[str, np.ndarray]] = MetadataField( None, - description='The dictionary of numpy arrays.', - orm_to_model=lambda node, _: ArrayData.save_arrays(node.arrays), # type: ignore[attr-defined] - model_to_orm=lambda model: ArrayData.load_arrays(model.arrays), # type: ignore[attr-defined] + description='The dictionary of numpy arrays', ) + @field_validator('arrays', mode='before') + @classmethod + def validate_arrays(cls, value: dict[str, Iterable] | None) -> dict[str, np.ndarray] | None: + if value is None: + return value + return {k: np.array(v) for k, v in value.items()} + + @field_serializer('arrays', when_used='json-unless-none') + def serialize_arrays(self, value: dict[str, np.ndarray]) -> dict[str, list]: + return {k: v.tolist() for k, v in value.items()} + array_prefix = 'array|' default_array_name = 'default' - def __init__(self, arrays: np.ndarray | dict[str, np.ndarray] | list[list[float]] | None = None, **kwargs): + def __init__(self, arrays: np.ndarray | dict[str, np.ndarray] | None = None, **kwargs): """Construct a new instance and set one or multiple numpy arrays. :param arrays: An optional single numpy array, or dictionary of numpy arrays to store. @@ -69,9 +80,6 @@ def __init__(self, arrays: np.ndarray | dict[str, np.ndarray] | list[list[float] arrays = arrays if arrays is not None else {} - if isinstance(arrays, list): - arrays = np.array(arrays) - if isinstance(arrays, np.ndarray): arrays = {self.default_array_name: arrays} @@ -230,8 +238,7 @@ def set_array(self, name: str, array: np.ndarray) -> None: # Check if the name is valid if not name or re.sub('[0-9a-zA-Z_]', '', name): raise ValueError( - 'The name assigned to the array ({}) is not valid,' - 'it can only contain digits, letters and underscores' + 'The name assigned to the array ({}) is not valid,it can only contain digits, letters and underscores' ) # Write the array to a temporary file, and then add it to the repository of the node diff --git a/tests/orm/test_fields/fields_aiida.data.core.array.ArrayData.yml b/tests/orm/test_fields/fields_aiida.data.core.array.ArrayData.yml index 0086bc2b59..5e38f108f0 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.array.ArrayData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.array.ArrayData.yml @@ -1,4 +1,4 @@ -arrays: QbDictField('arrays', dtype=typing.Optional[dict[str, bytes]], is_attribute=True) +arrays: QbDictField('arrays', dtype=typing.Optional[dict[str, numpy.ndarray]], is_attribute=True) attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) computer: QbStrField('computer', dtype=typing.Optional[str], is_attribute=False) diff --git a/tests/orm/test_fields/fields_aiida.data.core.array.bands.BandsData.yml b/tests/orm/test_fields/fields_aiida.data.core.array.bands.BandsData.yml index 361d8c2c48..d2405f11db 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.array.bands.BandsData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.array.bands.BandsData.yml @@ -1,4 +1,4 @@ -arrays: QbDictField('arrays', dtype=typing.Optional[dict[str, bytes]], is_attribute=True) +arrays: QbDictField('arrays', dtype=typing.Optional[dict[str, numpy.ndarray]], is_attribute=True) attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) cell: QbArrayField('cell', dtype=typing.Optional[typing.List[typing.List[float]]], diff --git a/tests/orm/test_fields/fields_aiida.data.core.array.kpoints.KpointsData.yml b/tests/orm/test_fields/fields_aiida.data.core.array.kpoints.KpointsData.yml index e0a0e7533a..5787873d7a 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.array.kpoints.KpointsData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.array.kpoints.KpointsData.yml @@ -1,4 +1,4 @@ -arrays: QbDictField('arrays', dtype=typing.Optional[dict[str, bytes]], is_attribute=True) +arrays: QbDictField('arrays', dtype=typing.Optional[dict[str, numpy.ndarray]], is_attribute=True) attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) cell: QbArrayField('cell', dtype=typing.Optional[typing.List[typing.List[float]]], diff --git a/tests/orm/test_fields/fields_aiida.data.core.array.projection.ProjectionData.yml b/tests/orm/test_fields/fields_aiida.data.core.array.projection.ProjectionData.yml index 0086bc2b59..5e38f108f0 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.array.projection.ProjectionData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.array.projection.ProjectionData.yml @@ -1,4 +1,4 @@ -arrays: QbDictField('arrays', dtype=typing.Optional[dict[str, bytes]], is_attribute=True) +arrays: QbDictField('arrays', dtype=typing.Optional[dict[str, numpy.ndarray]], is_attribute=True) attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) computer: QbStrField('computer', dtype=typing.Optional[str], is_attribute=False) diff --git a/tests/orm/test_fields/fields_aiida.data.core.array.trajectory.TrajectoryData.yml b/tests/orm/test_fields/fields_aiida.data.core.array.trajectory.TrajectoryData.yml index 0fa7353e3f..1c92a3d0f9 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.array.trajectory.TrajectoryData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.array.trajectory.TrajectoryData.yml @@ -1,4 +1,4 @@ -arrays: QbDictField('arrays', dtype=typing.Optional[dict[str, bytes]], is_attribute=True) +arrays: QbDictField('arrays', dtype=typing.Optional[dict[str, numpy.ndarray]], is_attribute=True) attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) computer: QbStrField('computer', dtype=typing.Optional[str], is_attribute=False) diff --git a/tests/orm/test_fields/fields_aiida.data.core.array.xy.XyData.yml b/tests/orm/test_fields/fields_aiida.data.core.array.xy.XyData.yml index 0086bc2b59..5e38f108f0 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.array.xy.XyData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.array.xy.XyData.yml @@ -1,4 +1,4 @@ -arrays: QbDictField('arrays', dtype=typing.Optional[dict[str, bytes]], is_attribute=True) +arrays: QbDictField('arrays', dtype=typing.Optional[dict[str, numpy.ndarray]], is_attribute=True) attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) computer: QbStrField('computer', dtype=typing.Optional[str], is_attribute=False) From 959221df03816d72f6acbc323f7938da016656d2 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Fri, 10 Oct 2025 17:01:27 +0200 Subject: [PATCH 15/59] Fix typing --- src/aiida/cmdline/commands/cmd_code.py | 2 +- src/aiida/orm/entities.py | 2 +- src/aiida/orm/nodes/data/array/bands.py | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/aiida/cmdline/commands/cmd_code.py b/src/aiida/cmdline/commands/cmd_code.py index fadb69ef67..f94fea9d41 100644 --- a/src/aiida/cmdline/commands/cmd_code.py +++ b/src/aiida/cmdline/commands/cmd_code.py @@ -39,7 +39,7 @@ def create_code(ctx: click.Context, cls: 'Code', **kwargs) -> None: """Create a new `Code` instance.""" try: Model = cls.InputModel # noqa: N806 - instance = cls.from_model(Model(**kwargs)) + instance = cls.from_model(Model(**kwargs)) # type: ignore[arg-type] except (TypeError, ValueError) as exception: echo.echo_critical(f'Failed to create instance `{cls}`: {exception}') diff --git a/src/aiida/orm/entities.py b/src/aiida/orm/entities.py index 7520912955..fbb1779dc4 100644 --- a/src/aiida/orm/entities.py +++ b/src/aiida/orm/entities.py @@ -250,7 +250,7 @@ def _prune_field_decorators(field_decorators: dict[str, Any]) -> dict[str, Any]: return InputModel @classproperty - def InputModel(cls) -> Type[Model]: # noqa: N805 + def InputModel(cls) -> Type[Model]: # noqa: N802, N805 """Return the input version of the model class for this entity. :return: The input model class, with read-only fields removed. diff --git a/src/aiida/orm/nodes/data/array/bands.py b/src/aiida/orm/nodes/data/array/bands.py index 15d3cdf90a..8cc5d15716 100644 --- a/src/aiida/orm/nodes/data/array/bands.py +++ b/src/aiida/orm/nodes/data/array/bands.py @@ -10,6 +10,8 @@ in a Brillouin zone, and how to operate on them. """ +from __future__ import annotations + import json import typing as t from string import Template From 8079411d18af60b1ed189d476de2ac1350f3205d Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Fri, 10 Oct 2025 19:48:27 +0200 Subject: [PATCH 16/59] Fix unhandled attributes check --- src/aiida/orm/nodes/data/data.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/aiida/orm/nodes/data/data.py b/src/aiida/orm/nodes/data/data.py index 67813a1002..eb9bae5deb 100644 --- a/src/aiida/orm/nodes/data/data.py +++ b/src/aiida/orm/nodes/data/data.py @@ -74,7 +74,7 @@ def __init__(self, *args, source=None, **kwargs): # We verify here that all attributes of Data plugins are handled in a constructor prior to the root # Data class (here), gracefully rejecting them otherwise. - node_keys = set(Node.Model.model_fields.keys()) + node_keys = set(Node.Model.model_fields.keys()) | {'backend'} unhandled_keys = {key for key in kwargs if key not in node_keys} if unhandled_keys: raise UnhandledDataAttributesError(unhandled_keys, self.__class__.__name__) @@ -140,7 +140,7 @@ def source(self, source): raise ValueError('Source must be supplied as a dictionary') unknown_attrs = tuple(set(source.keys()) - set(self._source_attributes)) if unknown_attrs: - raise KeyError(f"Unknown source parameters: {', '.join(unknown_attrs)}") + raise KeyError(f'Unknown source parameters: {", ".join(unknown_attrs)}') self.base.attributes.set('source', source) @@ -188,13 +188,13 @@ def _exportcontent(self, fileformat, main_file_name='', **kwargs): except KeyError: if exporters.keys(): raise ValueError( - 'The format {} is not implemented for {}. ' 'Currently implemented are: {}.'.format( + 'The format {} is not implemented for {}. Currently implemented are: {}.'.format( fileformat, self.__class__.__name__, ','.join(exporters.keys()) ) ) else: raise ValueError( - 'The format {} is not implemented for {}. ' 'No formats are implemented yet.'.format( + 'The format {} is not implemented for {}. No formats are implemented yet.'.format( fileformat, self.__class__.__name__ ) ) @@ -296,13 +296,13 @@ def importstring(self, inputstring, fileformat, **kwargs): except KeyError: if importers.keys(): raise ValueError( - 'The format {} is not implemented for {}. ' 'Currently implemented are: {}.'.format( + 'The format {} is not implemented for {}. Currently implemented are: {}.'.format( fileformat, self.__class__.__name__, ','.join(importers.keys()) ) ) else: raise ValueError( - 'The format {} is not implemented for {}. ' 'No formats are implemented yet.'.format( + 'The format {} is not implemented for {}. No formats are implemented yet.'.format( fileformat, self.__class__.__name__ ) ) @@ -353,13 +353,13 @@ def convert(self, object_format=None, *args): except KeyError: if converters.keys(): raise ValueError( - 'The format {} is not implemented for {}. ' 'Currently implemented are: {}.'.format( + 'The format {} is not implemented for {}. Currently implemented are: {}.'.format( object_format, self.__class__.__name__, ','.join(converters.keys()) ) ) else: raise ValueError( - 'The format {} is not implemented for {}. ' 'No formats are implemented yet.'.format( + 'The format {} is not implemented for {}. No formats are implemented yet.'.format( object_format, self.__class__.__name__ ) ) From b637cb4c0324b09035fec4ec577b4fa902386704 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Fri, 10 Oct 2025 20:38:19 +0200 Subject: [PATCH 17/59] Add `array_labels` back to `BandsData.Model` without constructor handling for now --- src/aiida/orm/nodes/data/array/bands.py | 16 +++++++++++++--- ...lds_aiida.data.core.array.bands.BandsData.yml | 2 ++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/aiida/orm/nodes/data/array/bands.py b/src/aiida/orm/nodes/data/array/bands.py index 8cc5d15716..690b628654 100644 --- a/src/aiida/orm/nodes/data/array/bands.py +++ b/src/aiida/orm/nodes/data/array/bands.py @@ -143,7 +143,7 @@ def nint(num): lumo = [_[0][_[1] + 1] for _ in zip(bands, homo_indexes)] except IndexError: raise ValueError( - 'To understand if it is a metal or insulator, ' 'need more bands than n_band=number_electrons' + 'To understand if it is a metal or insulator, need more bands than n_band=number_electrons' ) else: @@ -160,7 +160,7 @@ def nint(num): lumo = [i[number_electrons // number_electrons_per_band] for i in bands] # take the n+1th level except IndexError: raise ValueError( - 'To understand if it is a metal or insulator, ' 'need more bands than n_band=number_electrons' + 'To understand if it is a metal or insulator, need more bands than n_band=number_electrons' ) if number_electrons % 2 == 1 and len(stored_bands.shape) == 2: @@ -216,13 +216,23 @@ class BandsData(KpointsData): """Class to handle bands data""" class Model(KpointsData.Model): + array_labels: t.Optional[t.List[str]] = MetadataField( + None, + description='Labels associated with the band arrays', + ) units: t.Optional[str] = MetadataField( None, description='Units in which the data in bands were stored', orm_to_model=lambda node, _: t.cast('BandsData', node).base.attributes.get('units', None), ) - def __init__(self, *, units: str | None = None, **kwargs): + def __init__( + self, + *, + array_labels: list[str] | None = None, + units: str | None = None, + **kwargs, + ): super().__init__(**kwargs) self.units = units diff --git a/tests/orm/test_fields/fields_aiida.data.core.array.bands.BandsData.yml b/tests/orm/test_fields/fields_aiida.data.core.array.bands.BandsData.yml index d2405f11db..79d9e78394 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.array.bands.BandsData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.array.bands.BandsData.yml @@ -1,3 +1,5 @@ +array_labels: QbArrayField('array_labels', dtype=typing.Optional[typing.List[str]], + is_attribute=True) arrays: QbDictField('arrays', dtype=typing.Optional[dict[str, numpy.ndarray]], is_attribute=True) attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) From fda8bc13320ee54160e9bcba3381529168003105 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Fri, 10 Oct 2025 20:38:28 +0200 Subject: [PATCH 18/59] Nitpick some classes --- docs/source/nitpick-exceptions | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/source/nitpick-exceptions b/docs/source/nitpick-exceptions index 033cccffb6..588a44d82a 100644 --- a/docs/source/nitpick-exceptions +++ b/docs/source/nitpick-exceptions @@ -26,6 +26,7 @@ py:class json.encoder.JSONEncoder py:class EXPOSED_TYPE py:class EVENT_CALLBACK_TYPE py:class datetime +py:class UUID py:class types.LambdaType py:meth tempfile.TemporaryDirectory @@ -68,6 +69,8 @@ py:class aiida.orm.groups.SelfType py:class aiida.orm.implementation.entitites.EntityType py:class aiida.engine.processes.functions.FunctionType py:class aiida.engine.processes.workchains.workchain.MethodType +py:class aiida.orm.entities.EntityInputModel +py:class aiida.orm.entities.EntityModelType py:class aiida.orm.entities.EntityType py:class aiida.orm.entities.BackendEntityType py:class aiida.orm.entities.CollectionType From 546e076f35dcfd07adaef2596c5f2c28e3f4f798 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Fri, 10 Oct 2025 20:56:18 +0200 Subject: [PATCH 19/59] Fix docstring --- src/aiida/orm/entities.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/aiida/orm/entities.py b/src/aiida/orm/entities.py index fbb1779dc4..a9b3c5125e 100644 --- a/src/aiida/orm/entities.py +++ b/src/aiida/orm/entities.py @@ -197,6 +197,11 @@ class Model(BaseModel, defer_build=True): @classmethod def __pydantic_init_subclass__(cls, **kwargs: Any) -> None: + """Sets the JSON schema title of the model. + + The qualified name of the class is used, with dots removed. For example, `Node.Model` becomes `NodeModel` + in the JSON schema. + """ super().__pydantic_init_subclass__(**kwargs) cls.model_config['title'] = cls.__qualname__.replace('.', '') From 2a77c7203aeb2d2cecba3e2595ee31067f40fe52 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Sat, 11 Oct 2025 07:38:53 +0200 Subject: [PATCH 20/59] Remove user/ctime/mtime default factories from read-only fields --- src/aiida/orm/groups.py | 4 +--- src/aiida/orm/nodes/node.py | 5 +---- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/aiida/orm/groups.py b/src/aiida/orm/groups.py index d62bd19fb9..bd1428f71f 100644 --- a/src/aiida/orm/groups.py +++ b/src/aiida/orm/groups.py @@ -17,7 +17,7 @@ from typing_extensions import Self -from aiida.common import exceptions, timezone +from aiida.common import exceptions from aiida.common.lang import classproperty, type_check from aiida.common.pydantic import MetadataField from aiida.common.warnings import warn_deprecation @@ -123,7 +123,6 @@ class Model(entities.Entity.Model): exclude_to_orm=True, ) user: int = MetadataField( - default_factory=lambda: users.User.collection.get_default().pk, # type: ignore[union-attr] description='The PK of the group owner, defaults to the current user', is_attribute=False, orm_class='core.user', @@ -131,7 +130,6 @@ class Model(entities.Entity.Model): exclude_to_orm=True, ) time: datetime.datetime = MetadataField( - default_factory=timezone.now, description='The creation time of the node, defaults to now (timezone-aware)', is_attribute=False, exclude_to_orm=True, diff --git a/src/aiida/orm/nodes/node.py b/src/aiida/orm/nodes/node.py index 8703ddb28d..c4ca44de75 100644 --- a/src/aiida/orm/nodes/node.py +++ b/src/aiida/orm/nodes/node.py @@ -32,7 +32,7 @@ from typing_extensions import Self -from aiida.common import exceptions, timezone +from aiida.common import exceptions from aiida.common.lang import classproperty, type_check from aiida.common.links import LinkType from aiida.common.log import AIIDA_LOGGER @@ -238,14 +238,12 @@ class Model(Entity.Model): exclude_from_cli=True, ) ctime: datetime.datetime = MetadataField( - default_factory=timezone.now, description='The creation time of the node', is_attribute=False, exclude_to_orm=True, exclude_from_cli=True, ) mtime: datetime.datetime = MetadataField( - default_factory=timezone.now, description='The modification time of the node', is_attribute=False, exclude_to_orm=True, @@ -288,7 +286,6 @@ class Model(Entity.Model): exclude_to_orm=True, ) user: int = MetadataField( - default_factory=lambda: User.collection.get_default().pk, # type: ignore[union-attr] description='The PK of the user who owns the node', is_attribute=False, orm_to_model=lambda node, _: cast('Node', node).user.pk, From 193af729512bfcac15679786a60ffd639dca0c1a Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Sat, 11 Oct 2025 08:14:02 +0200 Subject: [PATCH 21/59] Include attributes in node input model --- src/aiida/orm/nodes/data/structure.py | 24 +++++++------------ src/aiida/orm/nodes/node.py | 7 +++++- tests/orm/models/test_models.py | 9 ++++++- ...iida.data.core.structure.StructureData.yml | 4 ++-- 4 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/aiida/orm/nodes/data/structure.py b/src/aiida/orm/nodes/data/structure.py index 437d2f902b..6534b6e872 100644 --- a/src/aiida/orm/nodes/data/structure.py +++ b/src/aiida/orm/nodes/data/structure.py @@ -10,6 +10,8 @@ functions to operate on them. """ +from __future__ import annotations + import copy import functools import itertools @@ -691,28 +693,18 @@ class Model(Data.Model): pbc2: bool = MetadataField(description='Whether periodic in the b direction') pbc3: bool = MetadataField(description='Whether periodic in the c direction') cell: t.List[t.List[float]] = MetadataField(description='The cell parameters') - kinds: t.Optional[t.List[dict]] = MetadataField( - None, - description='The kinds of atoms', - ) - sites: t.Optional[t.List[dict]] = MetadataField( - None, - description='The atomic sites', - ) + kinds: t.List[dict] = MetadataField(description='The kinds of atoms') + sites: t.List[dict] = MetadataField(description='The atomic sites') @field_validator('kinds', mode='before') @classmethod - def _validate_kinds(cls, value: t.Optional[t.List['Kind']]) -> t.Optional[t.List[t.Dict]]: - if value is None: - return None - return [kind.get_raw() for kind in value] + def _validate_kinds(cls, value: t.List[Kind | dict[str, t.Any]]) -> t.List[t.Dict]: + return [kind.get_raw() if isinstance(kind, Kind) else kind for kind in value] @field_validator('sites', mode='before') @classmethod - def _validate_sites(cls, value: t.Optional[t.List['Site']]) -> t.Optional[t.List[t.Dict]]: - if value is None: - return None - return [site.get_raw() for site in value] + def _validate_sites(cls, value: t.List[Site | dict[str, t.Any]]) -> t.List[t.Dict]: + return [site.get_raw() if isinstance(site, Site) else site for site in value] def __init__( self, diff --git a/src/aiida/orm/nodes/node.py b/src/aiida/orm/nodes/node.py index c4ca44de75..d786364add 100644 --- a/src/aiida/orm/nodes/node.py +++ b/src/aiida/orm/nodes/node.py @@ -266,7 +266,6 @@ class Model(Entity.Model): orm_to_model=lambda node, _: cast('Node', node).base.attributes.all, is_subscriptable=True, exclude_from_cli=True, - exclude_to_orm=True, ) extras: Dict[str, Any] = MetadataField( default_factory=dict, @@ -338,10 +337,16 @@ def __init__( if user is None: raise ValueError('the user cannot be None') + attributes = kwargs.pop('attributes', {}) + backend_entity = backend.nodes.create( node_type=self.class_node_type, user=user.backend_entity, computer=backend_computer, **kwargs ) super().__init__(backend_entity) + + if attributes: + self.base.attributes.set_many(attributes) + if extras is not None: self.base.extras.set_many(extras) diff --git a/tests/orm/models/test_models.py b/tests/orm/models/test_models.py index ddc1975b46..4268ab92ea 100644 --- a/tests/orm/models/test_models.py +++ b/tests/orm/models/test_models.py @@ -140,7 +140,14 @@ def required_arguments(request, default_user, aiida_localhost, tmp_path): if request.param is Str: return Str, {'value': 'string'} if request.param is StructureData: - return StructureData, {'cell': [[1, 0, 0], [0, 1, 0], [0, 0, 1]]} + return StructureData, { + 'cell': [[1, 0, 0], [0, 1, 0], [0, 0, 1]], + 'pbc1': True, + 'pbc2': True, + 'pbc3': True, + 'sites': [{'kind_name': 'H', 'position': (0.0, 0.0, 0.0)}], + 'kinds': [{'name': 'H', 'mass': 1.0, 'symbols': ('H',), 'weights': (1.0,)}], + } if request.param is RemoteData: return RemoteData, {'remote_path': '/some/path'} if request.param is RemoteStashData: diff --git a/tests/orm/test_fields/fields_aiida.data.core.structure.StructureData.yml b/tests/orm/test_fields/fields_aiida.data.core.structure.StructureData.yml index 0b7b4518ec..bf702d4905 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.structure.StructureData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.structure.StructureData.yml @@ -6,7 +6,7 @@ ctime: QbNumericField('ctime', dtype=, is_attribute=F description: QbStrField('description', dtype=, is_attribute=False) extras: QbDictField('extras', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) -kinds: QbArrayField('kinds', dtype=typing.Optional[typing.List[dict]], is_attribute=True) +kinds: QbArrayField('kinds', dtype=typing.List[dict], is_attribute=True) label: QbStrField('label', dtype=, is_attribute=False) mtime: QbNumericField('mtime', dtype=, is_attribute=False) node_type: QbStrField('node_type', dtype=, is_attribute=False) @@ -18,7 +18,7 @@ process_type: QbStrField('process_type', dtype=typing.Optional[str], is_attribut repository_content: QbDictField('repository_content', dtype=dict[str, bytes], is_attribute=False) repository_metadata: QbDictField('repository_metadata', dtype=typing.Dict[str, typing.Any], is_attribute=False) -sites: QbArrayField('sites', dtype=typing.Optional[typing.List[dict]], is_attribute=True) +sites: QbArrayField('sites', dtype=typing.List[dict], is_attribute=True) source: QbDictField('source', dtype=typing.Optional[dict], is_attribute=True, is_subscriptable=True) user: QbNumericField('user', dtype=, is_attribute=False) uuid: QbField('uuid', dtype=, is_attribute=False) From b7c8a6535bb93103280a3bd2f550a6fea008c41f Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Sun, 12 Oct 2025 10:50:13 +0200 Subject: [PATCH 22/59] Remove some unrelated changes Also some that are no longer required --- src/aiida/cmdline/commands/cmd_code.py | 8 +++-- src/aiida/cmdline/groups/dynamic.py | 9 +++--- src/aiida/orm/authinfos.py | 4 +-- src/aiida/orm/groups.py | 2 +- src/aiida/orm/nodes/data/array/array.py | 3 +- src/aiida/orm/nodes/data/array/bands.py | 4 +-- src/aiida/orm/nodes/data/array/trajectory.py | 4 +-- src/aiida/orm/nodes/data/base.py | 2 +- src/aiida/orm/nodes/data/bool.py | 2 +- src/aiida/orm/nodes/data/data.py | 14 ++++----- src/aiida/orm/nodes/data/float.py | 2 +- src/aiida/orm/nodes/data/int.py | 2 +- src/aiida/orm/nodes/data/str.py | 2 +- .../orm/nodes/process/calculation/calcjob.py | 29 ++++++++----------- src/aiida/orm/nodes/process/process.py | 1 - src/aiida/orm/utils/mixins.py | 5 +--- 16 files changed, 43 insertions(+), 50 deletions(-) diff --git a/src/aiida/cmdline/commands/cmd_code.py b/src/aiida/cmdline/commands/cmd_code.py index f94fea9d41..050d710a7b 100644 --- a/src/aiida/cmdline/commands/cmd_code.py +++ b/src/aiida/cmdline/commands/cmd_code.py @@ -8,6 +8,8 @@ ########################################################################### """`verdi code` command.""" +from __future__ import annotations + import pathlib import warnings from collections import defaultdict @@ -35,11 +37,11 @@ def verdi_code(): """Setup and manage codes.""" -def create_code(ctx: click.Context, cls: 'Code', **kwargs) -> None: +def create_code(ctx: click.Context, cls: Code, **kwargs) -> None: """Create a new `Code` instance.""" try: - Model = cls.InputModel # noqa: N806 - instance = cls.from_model(Model(**kwargs)) # type: ignore[arg-type] + model = cls.InputModel(**kwargs) + instance = cls.from_model(model) # type: ignore[arg-type] except (TypeError, ValueError) as exception: echo.echo_critical(f'Failed to create instance `{cls}`: {exception}') diff --git a/src/aiida/cmdline/groups/dynamic.py b/src/aiida/cmdline/groups/dynamic.py index 485b059d0b..d4c2527aa3 100644 --- a/src/aiida/cmdline/groups/dynamic.py +++ b/src/aiida/cmdline/groups/dynamic.py @@ -179,12 +179,11 @@ def list_options(self, entry_point: str) -> list[t.Callable[[FC], FC]]: default = field_info.default_factory if field_info.default is PydanticUndefined else field_info.default + # If the annotation has the ``__args__`` attribute it is an instance of a type from ``typing`` and the real + # type can be gotten from the arguments. For example it could be ``typing.Union[str, None]`` calling + # ``typing.Union[str, None].__args__`` will return the tuple ``(str, NoneType)``. So to get the real type, + # we simply remove all ``NoneType`` and the remaining type should be the type of the option. if hasattr(field_info.annotation, '__args__'): - # If the annotation has the ``__args__`` attribute it is an instance of a type from ``typing`` and - # the real type can be gotten from the arguments. For example it could be ``typing.Union[str, None]`` - # calling ``typing.Union[str, None].__args__`` will return the tuple ``(str, NoneType)``. So to get - # the real type, we simply remove all ``NoneType`` and the remaining type should be the type of the - # option. args = list(filter(lambda e: e is not type(None), field_info.annotation.__args__)) # Click parameters only support specifying a single type, so we default to the first one even if the # pydantic model defines multiple. diff --git a/src/aiida/orm/authinfos.py b/src/aiida/orm/authinfos.py index 55c39c1b98..c37ecbef09 100644 --- a/src/aiida/orm/authinfos.py +++ b/src/aiida/orm/authinfos.py @@ -84,8 +84,8 @@ def __init__( computer: 'Computer', user: 'User', enabled: bool = True, - auth_params: Optional[Dict[str, Any]] = None, - metadata: Optional[Dict[str, Any]] = None, + auth_params: Dict[str, Any] | None = None, + metadata: Dict[str, Any] | None = None, backend: Optional['StorageBackend'] = None, ) -> None: """Create an `AuthInfo` instance for the given computer and user. diff --git a/src/aiida/orm/groups.py b/src/aiida/orm/groups.py index bd1428f71f..ef4a8d552f 100644 --- a/src/aiida/orm/groups.py +++ b/src/aiida/orm/groups.py @@ -123,7 +123,7 @@ class Model(entities.Entity.Model): exclude_to_orm=True, ) user: int = MetadataField( - description='The PK of the group owner, defaults to the current user', + description='The PK of the group owner', is_attribute=False, orm_class='core.user', orm_to_model=lambda group, _: cast('Group', group).user.pk, diff --git a/src/aiida/orm/nodes/data/array/array.py b/src/aiida/orm/nodes/data/array/array.py index e9e9d2128e..4489b9b69d 100644 --- a/src/aiida/orm/nodes/data/array/array.py +++ b/src/aiida/orm/nodes/data/array/array.py @@ -238,7 +238,8 @@ def set_array(self, name: str, array: np.ndarray) -> None: # Check if the name is valid if not name or re.sub('[0-9a-zA-Z_]', '', name): raise ValueError( - 'The name assigned to the array ({}) is not valid,it can only contain digits, letters and underscores' + 'The name assigned to the array ({}) is not valid,' + 'it can only contain digits, letters and underscores' ) # Write the array to a temporary file, and then add it to the repository of the node diff --git a/src/aiida/orm/nodes/data/array/bands.py b/src/aiida/orm/nodes/data/array/bands.py index 690b628654..f032208512 100644 --- a/src/aiida/orm/nodes/data/array/bands.py +++ b/src/aiida/orm/nodes/data/array/bands.py @@ -143,7 +143,7 @@ def nint(num): lumo = [_[0][_[1] + 1] for _ in zip(bands, homo_indexes)] except IndexError: raise ValueError( - 'To understand if it is a metal or insulator, need more bands than n_band=number_electrons' + 'To understand if it is a metal or insulator, ' 'need more bands than n_band=number_electrons' ) else: @@ -160,7 +160,7 @@ def nint(num): lumo = [i[number_electrons // number_electrons_per_band] for i in bands] # take the n+1th level except IndexError: raise ValueError( - 'To understand if it is a metal or insulator, need more bands than n_band=number_electrons' + 'To understand if it is a metal or insulator, ' 'need more bands than n_band=number_electrons' ) if number_electrons % 2 == 1 and len(stored_bands.shape) == 2: diff --git a/src/aiida/orm/nodes/data/array/trajectory.py b/src/aiida/orm/nodes/data/array/trajectory.py index 723d4906a1..ba1de93136 100644 --- a/src/aiida/orm/nodes/data/array/trajectory.py +++ b/src/aiida/orm/nodes/data/array/trajectory.py @@ -104,7 +104,7 @@ def _internal_validate(self, stepids, cells, symbols, positions, times, velociti numatoms = len(symbols) if positions.shape != (numsteps, numatoms, 3): raise ValueError( - 'TrajectoryData.positions must have shape (s,n,3), with s=number of steps and n=number of symbols' + 'TrajectoryData.positions must have shape (s,n,3), ' 'with s=number of steps and n=number of symbols' ) if times is not None: if times.shape != (numsteps,): @@ -425,7 +425,7 @@ def get_step_structure(self, index, custom_kinds=None): for k in custom_kinds: if not isinstance(k, Kind): raise TypeError( - 'Each element of the custom_kinds list must be a aiida.orm.nodes.data.structure.Kind object' + 'Each element of the custom_kinds list must ' 'be a aiida.orm.nodes.data.structure.Kind object' ) kind_names.append(k.name) if len(kind_names) != len(set(kind_names)): diff --git a/src/aiida/orm/nodes/data/base.py b/src/aiida/orm/nodes/data/base.py index adc0f3a98e..f697b51c93 100644 --- a/src/aiida/orm/nodes/data/base.py +++ b/src/aiida/orm/nodes/data/base.py @@ -30,7 +30,7 @@ class BaseType(Data): class Model(Data.Model): value: t.Any = MetadataField( ..., - title='Data value.', + title='Data value', description='The value of the data', ) diff --git a/src/aiida/orm/nodes/data/bool.py b/src/aiida/orm/nodes/data/bool.py index 52ef060677..e669b9a722 100644 --- a/src/aiida/orm/nodes/data/bool.py +++ b/src/aiida/orm/nodes/data/bool.py @@ -25,7 +25,7 @@ class Bool(BaseType): class Model(NumericType.Model): value: bool = MetadataField( - title='Boolean value.', + title='Boolean value', description='The value of the boolean', ) diff --git a/src/aiida/orm/nodes/data/data.py b/src/aiida/orm/nodes/data/data.py index eb9bae5deb..e98ead5d26 100644 --- a/src/aiida/orm/nodes/data/data.py +++ b/src/aiida/orm/nodes/data/data.py @@ -140,7 +140,7 @@ def source(self, source): raise ValueError('Source must be supplied as a dictionary') unknown_attrs = tuple(set(source.keys()) - set(self._source_attributes)) if unknown_attrs: - raise KeyError(f'Unknown source parameters: {", ".join(unknown_attrs)}') + raise KeyError(f"Unknown source parameters: {', '.join(unknown_attrs)}") self.base.attributes.set('source', source) @@ -188,13 +188,13 @@ def _exportcontent(self, fileformat, main_file_name='', **kwargs): except KeyError: if exporters.keys(): raise ValueError( - 'The format {} is not implemented for {}. Currently implemented are: {}.'.format( + 'The format {} is not implemented for {}. ' 'Currently implemented are: {}.'.format( fileformat, self.__class__.__name__, ','.join(exporters.keys()) ) ) else: raise ValueError( - 'The format {} is not implemented for {}. No formats are implemented yet.'.format( + 'The format {} is not implemented for {}. ' 'No formats are implemented yet.'.format( fileformat, self.__class__.__name__ ) ) @@ -296,13 +296,13 @@ def importstring(self, inputstring, fileformat, **kwargs): except KeyError: if importers.keys(): raise ValueError( - 'The format {} is not implemented for {}. Currently implemented are: {}.'.format( + 'The format {} is not implemented for {}. ' 'Currently implemented are: {}.'.format( fileformat, self.__class__.__name__, ','.join(importers.keys()) ) ) else: raise ValueError( - 'The format {} is not implemented for {}. No formats are implemented yet.'.format( + 'The format {} is not implemented for {}. ' 'No formats are implemented yet.'.format( fileformat, self.__class__.__name__ ) ) @@ -353,13 +353,13 @@ def convert(self, object_format=None, *args): except KeyError: if converters.keys(): raise ValueError( - 'The format {} is not implemented for {}. Currently implemented are: {}.'.format( + 'The format {} is not implemented for {}. ' 'Currently implemented are: {}.'.format( object_format, self.__class__.__name__, ','.join(converters.keys()) ) ) else: raise ValueError( - 'The format {} is not implemented for {}. No formats are implemented yet.'.format( + 'The format {} is not implemented for {}. ' 'No formats are implemented yet.'.format( object_format, self.__class__.__name__ ) ) diff --git a/src/aiida/orm/nodes/data/float.py b/src/aiida/orm/nodes/data/float.py index cdc3524e92..a5453b0725 100644 --- a/src/aiida/orm/nodes/data/float.py +++ b/src/aiida/orm/nodes/data/float.py @@ -25,7 +25,7 @@ class Float(NumericType): class Model(NumericType.Model): value: float = MetadataField( - title='Float value.', + title='Float value', description='The value of the float', ) diff --git a/src/aiida/orm/nodes/data/int.py b/src/aiida/orm/nodes/data/int.py index 8bce700cf9..fbd8dc9918 100644 --- a/src/aiida/orm/nodes/data/int.py +++ b/src/aiida/orm/nodes/data/int.py @@ -25,7 +25,7 @@ class Int(NumericType): class Model(NumericType.Model): value: int = MetadataField( - title='Integer value.', + title='Integer value', description='The value of the integer', ) diff --git a/src/aiida/orm/nodes/data/str.py b/src/aiida/orm/nodes/data/str.py index 0d0c771de1..927b2d4168 100644 --- a/src/aiida/orm/nodes/data/str.py +++ b/src/aiida/orm/nodes/data/str.py @@ -23,7 +23,7 @@ class Str(BaseType): class Model(NumericType.Model): value: str = MetadataField( - title='String value.', + title='String value', description='The value of the string', ) diff --git a/src/aiida/orm/nodes/process/calculation/calcjob.py b/src/aiida/orm/nodes/process/calculation/calcjob.py index d74e7f6cde..4c25f91423 100644 --- a/src/aiida/orm/nodes/process/calculation/calcjob.py +++ b/src/aiida/orm/nodes/process/calculation/calcjob.py @@ -8,6 +8,8 @@ ########################################################################### """Module with `Node` sub class for calculation job processes.""" +from __future__ import annotations + import datetime from typing import TYPE_CHECKING, Any, AnyStr, Dict, List, Optional, Sequence, Tuple, Type, Union, cast @@ -68,54 +70,52 @@ class Model(CalculationNode.Model): scheduler_state: Optional[str] = MetadataField( None, description='The state of the scheduler', - orm_to_model=lambda node, _: cast('CalcJobNode', node).get_scheduler_state(), + orm_to_model=lambda node, _: cast(CalcJobNode, node).get_scheduler_state(), ) state: Optional[str] = MetadataField( None, description='The active state of the calculation job', - orm_to_model=lambda node, _: cast('CalcJobNode', node).get_state(), + orm_to_model=lambda node, _: cast(CalcJobNode, node).get_state(), ) remote_workdir: Optional[str] = MetadataField( None, description='The path to the remote (on cluster) scratch folder', - orm_to_model=lambda node, _: cast('CalcJobNode', node).get_remote_workdir(), + orm_to_model=lambda node, _: cast(CalcJobNode, node).get_remote_workdir(), ) job_id: Optional[str] = MetadataField( None, description='The scheduler job id', - orm_to_model=lambda node, _: cast('CalcJobNode', node).get_job_id(), + orm_to_model=lambda node, _: cast(CalcJobNode, node).get_job_id(), ) scheduler_lastchecktime: Optional[datetime.datetime] = MetadataField( None, description='The last time the scheduler was checked, in isoformat', - orm_to_model=lambda node, _: cast('CalcJobNode', node).get_scheduler_lastchecktime(), - exclude_to_orm=True, + orm_to_model=lambda node, _: cast(CalcJobNode, node).get_scheduler_lastchecktime(), ) last_job_info: Optional[dict] = MetadataField( None, description='The last job info returned by the scheduler', - orm_to_model=lambda node, _: dict(cast('CalcJobNode', node).get_last_job_info() or {}), + orm_to_model=lambda node, _: dict(cast(CalcJobNode, node).get_last_job_info() or {}), ) detailed_job_info: Optional[dict] = MetadataField( None, description='The detailed job info returned by the scheduler', - orm_to_model=lambda node, _: cast('CalcJobNode', node).get_detailed_job_info(), + orm_to_model=lambda node, _: cast(CalcJobNode, node).get_detailed_job_info(), ) retrieve_list: Optional[Sequence[Union[str, Tuple[str, str, int]]]] = MetadataField( None, description='The list of files to retrieve from the remote cluster', - orm_to_model=lambda node, _: cast('CalcJobNode', node).get_retrieve_list(), + orm_to_model=lambda node, _: cast(CalcJobNode, node).get_retrieve_list(), ) retrieve_temporary_list: Optional[Sequence[Union[str, Tuple[str, str, int]]]] = MetadataField( None, description='The list of temporary files to retrieve from the remote cluster', - orm_to_model=lambda node, _: cast('CalcJobNode', node).get_retrieve_temporary_list(), + orm_to_model=lambda node, _: cast(CalcJobNode, node).get_retrieve_temporary_list(), ) imported: Optional[bool] = MetadataField( None, description='Whether the node has been migrated', - orm_to_model=lambda node, _: cast('CalcJobNode', node).is_imported, - exclude_to_orm=True, + orm_to_model=lambda node, _: cast(CalcJobNode, node).is_imported, ) # An optional entry point for a CalculationTools instance @@ -415,11 +415,6 @@ def set_last_job_info(self, last_job_info: 'JobInfo') -> None: :param last_job_info: a `JobInfo` object """ - from aiida.schedulers.datastructures import JobInfo - - if not isinstance(last_job_info, JobInfo): - raise ValueError(f'last job info should be an instance of JobInfo, got: {last_job_info}') - self.base.attributes.set(self.SCHEDULER_LAST_JOB_INFO_KEY, last_job_info.get_dict()) def get_last_job_info(self) -> Optional['JobInfo']: diff --git a/src/aiida/orm/nodes/process/process.py b/src/aiida/orm/nodes/process/process.py index 507d69457a..26825048a8 100644 --- a/src/aiida/orm/nodes/process/process.py +++ b/src/aiida/orm/nodes/process/process.py @@ -216,7 +216,6 @@ class Model(Node.Model, Sealable.Model): paused: Optional[bool] = MetadataField( None, description='Whether the process is paused', - exclude_to_orm=True, ) def set_metadata_inputs(self, value: Dict[str, Any]) -> None: diff --git a/src/aiida/orm/utils/mixins.py b/src/aiida/orm/utils/mixins.py index 1e5c8403e9..4d8379079a 100644 --- a/src/aiida/orm/utils/mixins.py +++ b/src/aiida/orm/utils/mixins.py @@ -184,10 +184,7 @@ class Sealable: SEALED_KEY = 'sealed' class Model(pydantic.BaseModel, defer_build=True): - sealed: bool = MetadataField( - description='Whether the node is sealed', - exclude_to_orm=True, - ) + sealed: bool = MetadataField(description='Whether the node is sealed') @classproperty def _updatable_attributes(cls) -> tuple[str, ...]: # noqa: N805 From 2a37eff6ecd89453f6a329ba13dda7754c514af0 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Tue, 4 Nov 2025 10:40:27 +0100 Subject: [PATCH 23/59] Make repo content serialization opt-in --- src/aiida/orm/entities.py | 33 +++++++++++++++++++++++++++------ src/aiida/orm/nodes/node.py | 8 +++++--- tests/orm/models/test_models.py | 6 +++--- 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/src/aiida/orm/entities.py b/src/aiida/orm/entities.py index a9b3c5125e..2f02b959f8 100644 --- a/src/aiida/orm/entities.py +++ b/src/aiida/orm/entities.py @@ -299,11 +299,19 @@ def model_to_orm_field_values(cls, model: Model) -> dict[str, Any]: return fields - def to_model(self, repository_path: Optional[pathlib.Path] = None, unstored: bool = False) -> Model: + def to_model( + self, + *, + repository_path: Optional[pathlib.Path] = None, + include_repository_content: bool = False, + unstored: bool = False, + ) -> Model: """Return the entity instance as an instance of its model. :param repository_path: If the orm node has files in the repository, this path is used to read the repository files from. If no path is specified a temporary path is created using the entities pk. + :param include_repository_content: If True, repository file content is serialized in the model. + This field can be very large, so it is excluded by default. :param unstored: If True, the input version of the model is used, which strips read-only fields, i.e., fields with `exclude_to_orm=True`. :return: An instance of the entity's model class. @@ -314,7 +322,12 @@ def to_model(self, repository_path: Optional[pathlib.Path] = None, unstored: boo for key, field in Model.model_fields.items(): if orm_to_model := get_metadata(field, 'orm_to_model'): - fields[key] = orm_to_model(self, repository_path) + if key == 'filepath_files': + fields[key] = orm_to_model(self, repository_path) + elif key == 'repository_content': + fields[key] = orm_to_model(self, include_repository_content) + else: + fields[key] = orm_to_model(self) else: fields[key] = getattr(self, key) @@ -332,19 +345,23 @@ def from_model(cls, model: Model) -> Self: def serialize( self, + *, repository_path: Optional[pathlib.Path] = None, - mode: Literal['json', 'python'] = 'json', + include_repository_content: bool = False, unstored: bool = False, + mode: Literal['json', 'python'] = 'json', ) -> dict[str, Any]: """Serialize the entity instance to JSON. :param repository_path: If the orm node has files in the repository, this path is used to dump the repository files to. If no path is specified a temporary path is created using the entities pk. + :param include_repository_content: If True, repository file content is serialized in the model. + This field can be very large, so it is excluded by default. + :param unstored: If True, the input version of the model is used, which strips read-only fields, i.e., fields + with `exclude_to_orm=True`. :param mode: The serialization mode, either 'json' or 'python'. The 'json' mode is the most strict and ensures that the output is JSON serializable, whereas the 'python' mode allows for more complex Python types, such as `datetime` objects. - :param unstored: If True, the input version of the model is used, which strips read-only fields, i.e., fields - with `exclude_to_orm=True`. :return: A dictionary that can be serialized to JSON. """ self.logger.warning( @@ -360,7 +377,11 @@ def serialize( raise ValueError(f'The repository_path `{repository_path}` does not exist.') if not repository_path.is_dir(): raise ValueError(f'The repository_path `{repository_path}` is not a directory.') - return self.to_model(repository_path, unstored=unstored).model_dump(mode=mode) + return self.to_model( + repository_path=repository_path, + include_repository_content=include_repository_content, + unstored=unstored, + ).model_dump(mode=mode) @classmethod def from_serialized(cls, unstored: bool = False, **kwargs: dict[str, Any]) -> Self: diff --git a/src/aiida/orm/nodes/node.py b/src/aiida/orm/nodes/node.py index d786364add..f74f13f661 100644 --- a/src/aiida/orm/nodes/node.py +++ b/src/aiida/orm/nodes/node.py @@ -297,10 +297,12 @@ class Model(Entity.Model): description='Dictionary of file repository content. Keys are relative filepaths and values are binary file ' 'contents encoded as base64.', is_attribute=False, - orm_to_model=lambda node, _: { + orm_to_model=lambda node, serialize_content: { key: base64.encodebytes(content) - for key, content in cast('Node', node).base.repository.serialize_content().items() - }, + for key, content in cast(Node, node).base.repository.serialize_content().items() + } + if serialize_content + else {}, exclude_from_cli=True, exclude_to_orm=True, ) diff --git a/tests/orm/models/test_models.py b/tests/orm/models/test_models.py index 4268ab92ea..544ffe7262 100644 --- a/tests/orm/models/test_models.py +++ b/tests/orm/models/test_models.py @@ -176,7 +176,7 @@ def test_roundtrip(required_arguments, tmp_path): assert isinstance(entity, cls) # Get the model instance from the entity instance - model = entity.to_model(tmp_path, unstored=True) + model = entity.to_model(repository_path=tmp_path, unstored=True) assert isinstance(model, BaseModel) # Reconstruct the entity instance from the model instance @@ -187,7 +187,7 @@ def test_roundtrip(required_arguments, tmp_path): # ORM entity constructor are identical of the original model. The ``model_to_orm_field_values`` excludes values of # fields that define ``exclude_to_orm=True`` because these can change during roundtrips. This because these # typically correspond to entity fields that have defaults set on the database level, e.g., UUIDs. - roundtrip_model = roundtrip.to_model(tmp_path, unstored=True) + roundtrip_model = roundtrip.to_model(repository_path=tmp_path, unstored=True) original_field_values = cls.model_to_orm_field_values(model) for key, value in cls.model_to_orm_field_values(roundtrip_model).items(): @@ -213,5 +213,5 @@ def test_roundtrip_serialization(required_arguments, tmp_path): assert isinstance(entity, cls) # Get the model instance from the entity instance - serialized_entity = entity.serialize(tmp_path, unstored=True, mode='python') + serialized_entity = entity.serialize(repository_path=tmp_path, unstored=True, mode='python') entity.from_serialized(unstored=True, **serialized_entity) From 48c605e20de400b6983084d75c30ee78b73e3e6f Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Tue, 4 Nov 2025 10:41:16 +0100 Subject: [PATCH 24/59] Restore array base64 serialization --- src/aiida/orm/nodes/data/array/array.py | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/src/aiida/orm/nodes/data/array/array.py b/src/aiida/orm/nodes/data/array/array.py index 4489b9b69d..3a07841c3c 100644 --- a/src/aiida/orm/nodes/data/array/array.py +++ b/src/aiida/orm/nodes/data/array/array.py @@ -12,11 +12,9 @@ import base64 import io -from collections.abc import Iterable -from typing import Any, Iterator, Optional +from typing import Any, Iterator, Optional, cast import numpy as np -from pydantic import ConfigDict, field_serializer, field_validator from aiida.common.pydantic import MetadataField @@ -49,24 +47,15 @@ class ArrayData(Data): """ class Model(Data.Model): - model_config = ConfigDict(arbitrary_types_allowed=True) + # model_config = ConfigDict(arbitrary_types_allowed=True) - arrays: Optional[dict[str, np.ndarray]] = MetadataField( + arrays: Optional[dict[str, bytes]] = MetadataField( None, - description='The dictionary of numpy arrays', + description='The dictionary of numpy arrays.', + orm_to_model=lambda node: ArrayData.save_arrays(cast(ArrayData, node).arrays), + model_to_orm=lambda model: ArrayData.load_arrays(cast(ArrayData.Model, model).arrays), ) - @field_validator('arrays', mode='before') - @classmethod - def validate_arrays(cls, value: dict[str, Iterable] | None) -> dict[str, np.ndarray] | None: - if value is None: - return value - return {k: np.array(v) for k, v in value.items()} - - @field_serializer('arrays', when_used='json-unless-none') - def serialize_arrays(self, value: dict[str, np.ndarray]) -> dict[str, list]: - return {k: v.tolist() for k, v in value.items()} - array_prefix = 'array|' default_array_name = 'default' From d461378db724b248cb2de43a272b7853644c15d3 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Tue, 4 Nov 2025 10:44:36 +0100 Subject: [PATCH 25/59] Discard explicit quotation for annotations --- src/aiida/orm/authinfos.py | 6 ++-- src/aiida/orm/comments.py | 8 +++-- src/aiida/orm/computers.py | 8 ++--- src/aiida/orm/groups.py | 16 ++++++---- src/aiida/orm/logs.py | 8 +++-- src/aiida/orm/nodes/data/array/bands.py | 2 +- src/aiida/orm/nodes/data/array/kpoints.py | 16 +++++----- src/aiida/orm/nodes/data/code/installed.py | 6 ++-- src/aiida/orm/nodes/data/code/portable.py | 2 +- src/aiida/orm/nodes/data/remote/base.py | 2 +- src/aiida/orm/nodes/data/singlefile.py | 8 ++--- src/aiida/orm/nodes/node.py | 32 +++++++++---------- .../orm/nodes/process/calculation/calcjob.py | 32 +++++++++---------- src/aiida/orm/nodes/process/process.py | 14 ++++---- src/aiida/orm/users.py | 8 +++-- 15 files changed, 88 insertions(+), 80 deletions(-) diff --git a/src/aiida/orm/authinfos.py b/src/aiida/orm/authinfos.py index c37ecbef09..0e4ddcd673 100644 --- a/src/aiida/orm/authinfos.py +++ b/src/aiida/orm/authinfos.py @@ -33,7 +33,7 @@ class AuthInfoCollection(entities.Collection['AuthInfo']): """The collection of `AuthInfo` entries.""" @staticmethod - def _entity_base_cls() -> Type['AuthInfo']: + def _entity_base_cls() -> Type[AuthInfo]: return AuthInfo def delete(self, pk: int) -> None: @@ -55,13 +55,13 @@ class Model(entities.Entity.Model): description='The PK of the computer', is_attribute=False, orm_class=Computer, - orm_to_model=lambda auth_info, _: cast('AuthInfo', auth_info).computer.pk, + orm_to_model=lambda auth_info: cast(AuthInfo, auth_info).computer.pk, ) user: int = MetadataField( description='The PK of the user', is_attribute=False, orm_class=User, - orm_to_model=lambda auth_info, _: cast('AuthInfo', auth_info).user.pk, + orm_to_model=lambda auth_info: cast(AuthInfo, auth_info).user.pk, ) enabled: bool = MetadataField( True, diff --git a/src/aiida/orm/comments.py b/src/aiida/orm/comments.py index d615b63dbd..f11ac0317a 100644 --- a/src/aiida/orm/comments.py +++ b/src/aiida/orm/comments.py @@ -8,6 +8,8 @@ ########################################################################### """Comment objects and functions""" +from __future__ import annotations + from datetime import datetime from typing import TYPE_CHECKING, List, Optional, Type, cast from uuid import UUID @@ -30,7 +32,7 @@ class CommentCollection(entities.Collection['Comment']): """The collection of Comment entries.""" @staticmethod - def _entity_base_cls() -> Type['Comment']: + def _entity_base_cls() -> Type[Comment]: return Comment def delete(self, pk: int) -> None: @@ -88,13 +90,13 @@ class Model(entities.Entity.Model): description='Node PK that the comment is attached to', is_attribute=False, orm_class='core.node', - orm_to_model=lambda comment, _: cast('Comment', comment).node.pk, + orm_to_model=lambda comment: cast(Comment, comment).node.pk, ) user: int = MetadataField( description='User PK that created the comment', is_attribute=False, orm_class='core.user', - orm_to_model=lambda comment, _: cast('Comment', comment).user.pk, + orm_to_model=lambda comment: cast(Comment, comment).user.pk, ) content: str = MetadataField( description='Content of the comment', diff --git a/src/aiida/orm/computers.py b/src/aiida/orm/computers.py index fd628754ca..6df4b1d9b3 100644 --- a/src/aiida/orm/computers.py +++ b/src/aiida/orm/computers.py @@ -35,10 +35,10 @@ class ComputerCollection(entities.Collection['Computer']): """The collection of Computer entries.""" @staticmethod - def _entity_base_cls() -> Type['Computer']: + def _entity_base_cls() -> Type[Computer]: return Computer - def get_or_create(self, label: str, **kwargs: Any) -> Tuple[bool, 'Computer']: + def get_or_create(self, label: str, **kwargs: Any) -> Tuple[bool, Computer]: """Try to retrieve a Computer from the DB with the given arguments; create (and store) a new Computer if such a Computer was not present yet. @@ -288,11 +288,11 @@ def default_memory_per_machine_validator(cls, def_memory_per_machine: Optional[i f'Invalid value for def_memory_per_machine, must be a positive int, got: {def_memory_per_machine}' ) - def copy(self) -> 'Computer': + def copy(self) -> Computer: """Return a copy of the current object to work with, not stored yet.""" return entities.from_backend_entity(Computer, self._backend_entity.copy()) - def store(self) -> 'Computer': + def store(self) -> Computer: """Store the computer in the DB. Differently from Nodes, a computer can be re-stored if its properties diff --git a/src/aiida/orm/groups.py b/src/aiida/orm/groups.py index ef4a8d552f..9bdc5e26b7 100644 --- a/src/aiida/orm/groups.py +++ b/src/aiida/orm/groups.py @@ -8,6 +8,8 @@ ########################################################################### """AiiDA Group entities""" +from __future__ import annotations + import datetime import warnings from functools import cached_property @@ -35,7 +37,7 @@ __all__ = ('AutoGroup', 'Group', 'ImportGroup', 'UpfFamily') -def load_group_class(type_string: str) -> Type['Group']: +def load_group_class(type_string: str) -> Type[Group]: """Load the sub class of `Group` that corresponds to the given `type_string`. .. note:: will fall back on `aiida.orm.groups.Group` if `type_string` cannot be resolved to loadable entry point. @@ -60,10 +62,10 @@ class GroupCollection(entities.Collection['Group']): """Collection of Groups""" @staticmethod - def _entity_base_cls() -> Type['Group']: + def _entity_base_cls() -> Type[Group]: return Group - def get_or_create(self, label: Optional[str] = None, **kwargs) -> Tuple['Group', bool]: + def get_or_create(self, label: Optional[str] = None, **kwargs) -> Tuple[Group, bool]: """Try to retrieve a group from the DB with the given arguments; create (and store) a new group if such a group was not present yet. @@ -96,9 +98,9 @@ def delete(self, pk: int) -> None: class GroupBase: """A namespace for group related functionality, that is not directly related to its user-facing properties.""" - def __init__(self, group: 'Group') -> None: + def __init__(self, group: Group) -> None: """Construct a new instance of the base namespace.""" - self._group: 'Group' = group + self._group: Group = group @cached_property def extras(self) -> extras.EntityExtras: @@ -126,7 +128,7 @@ class Model(entities.Entity.Model): description='The PK of the group owner', is_attribute=False, orm_class='core.user', - orm_to_model=lambda group, _: cast('Group', group).user.pk, + orm_to_model=lambda group: cast(Group, group).user.pk, exclude_to_orm=True, ) time: datetime.datetime = MetadataField( @@ -148,7 +150,7 @@ class Model(entities.Entity.Model): description='The group extras', is_attribute=False, is_subscriptable=True, - orm_to_model=lambda group, _: cast('Group', group).base.extras.all, + orm_to_model=lambda group: cast(Group, group).base.extras.all, ) _CLS_COLLECTION = GroupCollection diff --git a/src/aiida/orm/logs.py b/src/aiida/orm/logs.py index 981642c1f4..1a9a09f8e2 100644 --- a/src/aiida/orm/logs.py +++ b/src/aiida/orm/logs.py @@ -8,6 +8,8 @@ ########################################################################### """Module for orm logging abstract classes""" +from __future__ import annotations + import logging from datetime import datetime from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type @@ -41,10 +43,10 @@ class LogCollection(entities.Collection['Log']): """ @staticmethod - def _entity_base_cls() -> Type['Log']: + def _entity_base_cls() -> Type[Log]: return Log - def create_entry_from_record(self, record: logging.LogRecord) -> Optional['Log']: + def create_entry_from_record(self, record: logging.LogRecord) -> Optional[Log]: """Helper function to create a log entry from a record created as by the python logging library :param record: The record created by the logging module @@ -82,7 +84,7 @@ def create_entry_from_record(self, record: logging.LogRecord) -> Optional['Log'] backend=self.backend, ) - def get_logs_for(self, entity: 'Node', order_by: Optional['OrderByType'] = None) -> List['Log']: + def get_logs_for(self, entity: 'Node', order_by: Optional['OrderByType'] = None) -> List[Log]: """Get all the log messages for a given node and optionally sort :param entity: the entity to get logs for diff --git a/src/aiida/orm/nodes/data/array/bands.py b/src/aiida/orm/nodes/data/array/bands.py index f032208512..40a4d4c489 100644 --- a/src/aiida/orm/nodes/data/array/bands.py +++ b/src/aiida/orm/nodes/data/array/bands.py @@ -223,7 +223,7 @@ class Model(KpointsData.Model): units: t.Optional[str] = MetadataField( None, description='Units in which the data in bands were stored', - orm_to_model=lambda node, _: t.cast('BandsData', node).base.attributes.get('units', None), + orm_to_model=lambda node: t.cast(BandsData, node).base.attributes.get('units', None), ) def __init__( diff --git a/src/aiida/orm/nodes/data/array/kpoints.py b/src/aiida/orm/nodes/data/array/kpoints.py index a10373e935..b61f407986 100644 --- a/src/aiida/orm/nodes/data/array/kpoints.py +++ b/src/aiida/orm/nodes/data/array/kpoints.py @@ -44,42 +44,42 @@ class Model(ArrayData.Model): labels: t.Optional[t.List[str]] = MetadataField( None, description='Labels associated with the list of kpoints', - orm_to_model=lambda node, _: t.cast('KpointsData', node).base.attributes.get('labels', None), + orm_to_model=lambda node: t.cast(KpointsData, node).base.attributes.get('labels', None), ) label_numbers: t.Optional[t.List[int]] = MetadataField( None, description='Index of the labels in the list of kpoints', - orm_to_model=lambda node, _: t.cast('KpointsData', node).base.attributes.get('label_numbers', None), + orm_to_model=lambda node: t.cast(KpointsData, node).base.attributes.get('label_numbers', None), ) cell: t.Optional[t.List[t.List[float]]] = MetadataField( None, description='Unit cell of the crystal, in Angstroms', - orm_to_model=lambda node, _: t.cast('KpointsData', node).base.attributes.get('cell', None), + orm_to_model=lambda node: t.cast(KpointsData, node).base.attributes.get('cell', None), ) pbc1: t.Optional[bool] = MetadataField( None, description='Periodicity in the first lattice vector direction', - orm_to_model=lambda node, _: t.cast('KpointsData', node).pbc[0], + orm_to_model=lambda node: t.cast(KpointsData, node).pbc[0], ) pbc2: t.Optional[bool] = MetadataField( None, description='Periodicity in the second lattice vector direction', - orm_to_model=lambda node, _: t.cast('KpointsData', node).pbc[1], + orm_to_model=lambda node: t.cast(KpointsData, node).pbc[1], ) pbc3: t.Optional[bool] = MetadataField( None, description='Periodicity in the third lattice vector direction', - orm_to_model=lambda node, _: t.cast('KpointsData', node).pbc[2], + orm_to_model=lambda node: t.cast(KpointsData, node).pbc[2], ) mesh: t.Optional[t.List[int]] = MetadataField( None, description='Mesh of kpoints', - orm_to_model=lambda node, _: t.cast('KpointsData', node).base.attributes.get('mesh', None), + orm_to_model=lambda node: t.cast(KpointsData, node).base.attributes.get('mesh', None), ) offset: t.Optional[t.List[float]] = MetadataField( None, description='Offset of kpoints', - orm_to_model=lambda node, _: t.cast('KpointsData', node).base.attributes.get('offset', None), + orm_to_model=lambda node: t.cast(KpointsData, node).base.attributes.get('offset', None), ) def __init__( diff --git a/src/aiida/orm/nodes/data/code/installed.py b/src/aiida/orm/nodes/data/code/installed.py index b46a94cfed..bcb1a3f755 100644 --- a/src/aiida/orm/nodes/data/code/installed.py +++ b/src/aiida/orm/nodes/data/code/installed.py @@ -46,15 +46,15 @@ class Model(AbstractCode.Model): title='Computer', description='The label of the remote computer on which the executable resides.', is_attribute=False, - orm_to_model=lambda node, _: cast('InstalledCode', node).computer.label, - model_to_orm=lambda model: cast('InstalledCode.Model', model).load_computer(), + orm_to_model=lambda node: cast(InstalledCode, node).computer.label, + model_to_orm=lambda model: cast(InstalledCode.Model, model).load_computer(), short_name='-Y', priority=2, ) filepath_executable: str = MetadataField( title='Filepath executable', description='Filepath of the executable on the remote computer.', - orm_to_model=lambda node, _: str(cast('InstalledCode', node).filepath_executable), + orm_to_model=lambda node: str(cast(InstalledCode, node).filepath_executable), short_name='-X', priority=1, ) diff --git a/src/aiida/orm/nodes/data/code/portable.py b/src/aiida/orm/nodes/data/code/portable.py index 263eb2cfaf..498535c911 100644 --- a/src/aiida/orm/nodes/data/code/portable.py +++ b/src/aiida/orm/nodes/data/code/portable.py @@ -63,7 +63,7 @@ class Model(AbstractCode.Model): description='Relative filepath of executable with directory of code files.', short_name='-X', priority=1, - orm_to_model=lambda node, _: str(node.filepath_executable), # type: ignore[attr-defined] + orm_to_model=lambda node: str(node.filepath_executable), # type: ignore[attr-defined] ) filepath_files: str = MetadataField( ..., diff --git a/src/aiida/orm/nodes/data/remote/base.py b/src/aiida/orm/nodes/data/remote/base.py index a926b1f056..5b9d1253dd 100644 --- a/src/aiida/orm/nodes/data/remote/base.py +++ b/src/aiida/orm/nodes/data/remote/base.py @@ -39,7 +39,7 @@ class Model(Data.Model): None, title='Remote path', description='Filepath on the remote computer.', - orm_to_model=lambda node, _: node.get_remote_path(), + orm_to_model=lambda node: node.get_remote_path(), ) def __init__(self, remote_path: Optional[str] = None, **kwargs): diff --git a/src/aiida/orm/nodes/data/singlefile.py b/src/aiida/orm/nodes/data/singlefile.py index 102197c8d0..78d24dbed0 100644 --- a/src/aiida/orm/nodes/data/singlefile.py +++ b/src/aiida/orm/nodes/data/singlefile.py @@ -36,7 +36,7 @@ class SinglefileData(Data): class Model(Data.Model): content: bytes = MetadataField( description='The file content.', - model_to_orm=lambda model: io.BytesIO(t.cast('SinglefileData', model).content), + model_to_orm=lambda model: io.BytesIO(t.cast(SinglefileData, model).content), ) filename: str = MetadataField( 'file.txt', @@ -60,7 +60,7 @@ def _encode_content(self, value: bytes) -> str: return base64.b64encode(value).decode() @classmethod - def from_string(cls, content: str, filename: str | pathlib.Path | None = None, **kwargs: t.Any) -> 'SinglefileData': + def from_string(cls, content: str, filename: str | pathlib.Path | None = None, **kwargs: t.Any) -> SinglefileData: """Construct a new instance and set ``content`` as its contents. :param content: The content as a string. @@ -69,9 +69,7 @@ def from_string(cls, content: str, filename: str | pathlib.Path | None = None, * return cls(io.StringIO(content), filename, **kwargs) @classmethod - def from_bytes( - cls, content: bytes, filename: str | pathlib.Path | None = None, **kwargs: t.Any - ) -> 'SinglefileData': + def from_bytes(cls, content: bytes, filename: str | pathlib.Path | None = None, **kwargs: t.Any) -> SinglefileData: """Construct a new instance and set ``content`` as its contents. :param content: The content as bytes. diff --git a/src/aiida/orm/nodes/node.py b/src/aiida/orm/nodes/node.py index f74f13f661..591a3542be 100644 --- a/src/aiida/orm/nodes/node.py +++ b/src/aiida/orm/nodes/node.py @@ -74,7 +74,7 @@ class NodeCollection(EntityCollection[NodeType], Generic[NodeType]): """The collection of nodes.""" @staticmethod - def _entity_base_cls() -> Type['Node']: # type: ignore[override] + def _entity_base_cls() -> Type[Node]: # type: ignore[override] return Node def delete(self, pk: int) -> None: @@ -119,39 +119,39 @@ def iter_repo_keys( class NodeBase: """A namespace for node related functionality, that is not directly related to its user-facing properties.""" - def __init__(self, node: 'Node') -> None: + def __init__(self, node: Node) -> None: """Construct a new instance of the base namespace.""" self._node = node @cached_property - def repository(self) -> 'NodeRepository': + def repository(self) -> NodeRepository: """Return the repository for this node.""" from .repository import NodeRepository return NodeRepository(self._node) @cached_property - def caching(self) -> 'NodeCaching': + def caching(self) -> NodeCaching: """Return an interface to interact with the caching of this node.""" return self._node._CLS_NODE_CACHING(self._node) @cached_property - def comments(self) -> 'NodeComments': + def comments(self) -> NodeComments: """Return an interface to interact with the comments of this node.""" return NodeComments(self._node) @cached_property - def attributes(self) -> 'NodeAttributes': + def attributes(self) -> NodeAttributes: """Return an interface to interact with the attributes of this node.""" return NodeAttributes(self._node) @cached_property - def extras(self) -> 'EntityExtras': + def extras(self) -> EntityExtras: """Return an interface to interact with the extras of this node.""" return EntityExtras(self._node) @cached_property - def links(self) -> 'NodeLinks': + def links(self) -> NodeLinks: """Return an interface to interact with the links of this node.""" return self._node._CLS_NODE_LINKS(self._node) @@ -233,7 +233,7 @@ class Model(Entity.Model): default_factory=dict, description='Virtual hierarchy of the file repository.', is_attribute=False, - orm_to_model=lambda node, _: cast('Node', node).base.repository.metadata, + orm_to_model=lambda node: cast(Node, node).base.repository.metadata, exclude_to_orm=True, exclude_from_cli=True, ) @@ -263,7 +263,7 @@ class Model(Entity.Model): default_factory=dict, description='The node attributes', is_attribute=False, - orm_to_model=lambda node, _: cast('Node', node).base.attributes.all, + orm_to_model=lambda node: cast(Node, node).base.attributes.all, is_subscriptable=True, exclude_from_cli=True, ) @@ -271,7 +271,7 @@ class Model(Entity.Model): default_factory=dict, description='The node extras', is_attribute=False, - orm_to_model=lambda node, _: cast('Node', node).base.extras.all, + orm_to_model=lambda node: cast(Node, node).base.extras.all, is_subscriptable=True, exclude_from_cli=True, ) @@ -279,15 +279,15 @@ class Model(Entity.Model): None, description='The label of the computer', is_attribute=False, - orm_to_model=lambda node, _: cast('Node', node).computer.label if cast('Node', node).computer else None, # type: ignore[union-attr] - model_to_orm=lambda model: cast('Node.Model', model).load_computer(), + orm_to_model=lambda node: cast(Node, node).computer.label if cast(Node, node).computer else None, # type: ignore[union-attr] + model_to_orm=lambda model: cast(Node.Model, model).load_computer(), exclude_from_cli=True, exclude_to_orm=True, ) user: int = MetadataField( description='The PK of the user who owns the node', is_attribute=False, - orm_to_model=lambda node, _: cast('Node', node).user.pk, + orm_to_model=lambda node: cast(Node, node).user.pk, orm_class=User, exclude_to_orm=True, exclude_from_cli=True, @@ -657,7 +657,7 @@ def _verify_are_parents_stored(self) -> None: f'Cannot store because source node of link triple {link_triple} is not stored' ) - def _store_from_cache(self, cache_node: 'Node') -> None: + def _store_from_cache(self, cache_node: Node) -> None: """Store this node from an existing cache node. .. note:: @@ -690,7 +690,7 @@ def _store_from_cache(self, cache_node: 'Node') -> None: self._add_outputs_from_cache(cache_node) self.base.extras.set(self.base.caching.CACHED_FROM_KEY, cache_node.uuid) - def _add_outputs_from_cache(self, cache_node: 'Node') -> None: + def _add_outputs_from_cache(self, cache_node: Node) -> None: """Replicate the output links and nodes from the cached node onto this node.""" for entry in cache_node.base.links.get_outgoing(link_type=LinkType.CREATE): new_node = entry.node.clone() diff --git a/src/aiida/orm/nodes/process/calculation/calcjob.py b/src/aiida/orm/nodes/process/calculation/calcjob.py index 4c25f91423..4244ac867a 100644 --- a/src/aiida/orm/nodes/process/calculation/calcjob.py +++ b/src/aiida/orm/nodes/process/calculation/calcjob.py @@ -70,52 +70,52 @@ class Model(CalculationNode.Model): scheduler_state: Optional[str] = MetadataField( None, description='The state of the scheduler', - orm_to_model=lambda node, _: cast(CalcJobNode, node).get_scheduler_state(), + orm_to_model=lambda node: cast(CalcJobNode, node).get_scheduler_state(), ) state: Optional[str] = MetadataField( None, description='The active state of the calculation job', - orm_to_model=lambda node, _: cast(CalcJobNode, node).get_state(), + orm_to_model=lambda node: cast(CalcJobNode, node).get_state(), ) remote_workdir: Optional[str] = MetadataField( None, description='The path to the remote (on cluster) scratch folder', - orm_to_model=lambda node, _: cast(CalcJobNode, node).get_remote_workdir(), + orm_to_model=lambda node: cast(CalcJobNode, node).get_remote_workdir(), ) job_id: Optional[str] = MetadataField( None, description='The scheduler job id', - orm_to_model=lambda node, _: cast(CalcJobNode, node).get_job_id(), + orm_to_model=lambda node: cast(CalcJobNode, node).get_job_id(), ) scheduler_lastchecktime: Optional[datetime.datetime] = MetadataField( None, description='The last time the scheduler was checked, in isoformat', - orm_to_model=lambda node, _: cast(CalcJobNode, node).get_scheduler_lastchecktime(), + orm_to_model=lambda node: cast(CalcJobNode, node).get_scheduler_lastchecktime(), ) last_job_info: Optional[dict] = MetadataField( None, description='The last job info returned by the scheduler', - orm_to_model=lambda node, _: dict(cast(CalcJobNode, node).get_last_job_info() or {}), + orm_to_model=lambda node: dict(cast(CalcJobNode, node).get_last_job_info() or {}), ) detailed_job_info: Optional[dict] = MetadataField( None, description='The detailed job info returned by the scheduler', - orm_to_model=lambda node, _: cast(CalcJobNode, node).get_detailed_job_info(), + orm_to_model=lambda node: cast(CalcJobNode, node).get_detailed_job_info(), ) retrieve_list: Optional[Sequence[Union[str, Tuple[str, str, int]]]] = MetadataField( None, description='The list of files to retrieve from the remote cluster', - orm_to_model=lambda node, _: cast(CalcJobNode, node).get_retrieve_list(), + orm_to_model=lambda node: cast(CalcJobNode, node).get_retrieve_list(), ) retrieve_temporary_list: Optional[Sequence[Union[str, Tuple[str, str, int]]]] = MetadataField( None, description='The list of temporary files to retrieve from the remote cluster', - orm_to_model=lambda node, _: cast(CalcJobNode, node).get_retrieve_temporary_list(), + orm_to_model=lambda node: cast(CalcJobNode, node).get_retrieve_temporary_list(), ) imported: Optional[bool] = MetadataField( None, description='Whether the node has been migrated', - orm_to_model=lambda node, _: cast(CalcJobNode, node).is_imported, + orm_to_model=lambda node: cast(CalcJobNode, node).is_imported, ) # An optional entry point for a CalculationTools instance @@ -354,7 +354,7 @@ def get_job_id(self) -> Optional[str]: """ return self.base.attributes.get(self.SCHEDULER_JOB_ID_KEY, None) - def set_scheduler_state(self, state: 'JobState') -> None: + def set_scheduler_state(self, state: JobState) -> None: """Set the scheduler state. :param state: an instance of `JobState` @@ -439,7 +439,7 @@ def get_last_job_info(self) -> Optional['JobInfo']: return job_info - def get_authinfo(self) -> 'AuthInfo': + def get_authinfo(self) -> AuthInfo: """Return the `AuthInfo` that is configured for the `Computer` set for this node. :return: `AuthInfo` @@ -451,7 +451,7 @@ def get_authinfo(self) -> 'AuthInfo': return computer.get_authinfo(self.user) - def get_transport(self) -> 'Transport': + def get_transport(self) -> Transport: """Return the transport for this calculation. :return: Transport configured @@ -459,7 +459,7 @@ def get_transport(self) -> 'Transport': """ return self.get_authinfo().get_transport() - def get_parser_class(self) -> Optional[Type['Parser']]: + def get_parser_class(self) -> Optional[Type[Parser]]: """Return the output parser object for this calculation or None if no parser is set. :return: a `Parser` class. @@ -479,7 +479,7 @@ def link_label_retrieved(self) -> str: """Return the link label used for the retrieved FolderData node.""" return 'retrieved' - def get_retrieved_node(self) -> Optional['FolderData']: + def get_retrieved_node(self) -> Optional[FolderData]: """Return the retrieved data folder. :return: the retrieved FolderData node or None if not found @@ -496,7 +496,7 @@ def get_retrieved_node(self) -> Optional['FolderData']: return None @property - def res(self) -> 'CalcJobResultManager': + def res(self) -> CalcJobResultManager: """To be used to get direct access to the parsed parameters. :return: an instance of the CalcJobResultManager. diff --git a/src/aiida/orm/nodes/process/process.py b/src/aiida/orm/nodes/process/process.py index 26825048a8..659a65a405 100644 --- a/src/aiida/orm/nodes/process/process.py +++ b/src/aiida/orm/nodes/process/process.py @@ -8,6 +8,8 @@ ########################################################################### """Module with `Node` sub class for processes.""" +from __future__ import annotations + import enum from pathlib import Path from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type, Union @@ -258,7 +260,7 @@ def recursive_merge(cls, left: dict[Any, Any], right: dict[Any, Any]) -> None: else: left[key] = value - def get_builder_restart(self) -> 'ProcessBuilder': + def get_builder_restart(self) -> ProcessBuilder: """Return a `ProcessBuilder` that is ready to relaunch the process that created this node. The process class will be set based on the `process_type` of this node and the inputs of the builder will be @@ -274,7 +276,7 @@ def get_builder_restart(self) -> 'ProcessBuilder': return builder @property - def process_class(self) -> Type['Process']: + def process_class(self) -> Type[Process]: """Return the process class that was used to create this node. :return: `Process` class @@ -458,7 +460,7 @@ def is_failed(self) -> bool: return self.is_finished and self.exit_status != 0 @property - def exit_code(self) -> Optional['ExitCode']: + def exit_code(self) -> Optional[ExitCode]: """Return the exit code of the process. It is reconstituted from the ``exit_status`` and ``exit_message`` attributes if both of those are defined. @@ -593,7 +595,7 @@ def unpause(self) -> None: pass @property - def called(self) -> List['ProcessNode']: + def called(self) -> List[ProcessNode]: """Return a list of nodes that the process called :returns: list of process nodes called by this process @@ -601,7 +603,7 @@ def called(self) -> List['ProcessNode']: return self.base.links.get_outgoing(link_type=(LinkType.CALL_CALC, LinkType.CALL_WORK)).all_nodes() @property - def called_descendants(self) -> List['ProcessNode']: + def called_descendants(self) -> List[ProcessNode]: """Return a list of all nodes that have been called downstream of this process This will recursively find all the called processes for this process and its children. @@ -615,7 +617,7 @@ def called_descendants(self) -> List['ProcessNode']: return descendants @property - def caller(self) -> Optional['ProcessNode']: + def caller(self) -> Optional[ProcessNode]: """Return the process node that called this process node, or None if it does not have a caller :returns: process node that called this process node instance or None diff --git a/src/aiida/orm/users.py b/src/aiida/orm/users.py index 50b02280fb..4b8a38d3f0 100644 --- a/src/aiida/orm/users.py +++ b/src/aiida/orm/users.py @@ -8,6 +8,8 @@ ########################################################################### """Module for the ORM user class.""" +from __future__ import annotations + from typing import TYPE_CHECKING, Optional, Tuple, Type from aiida.common import exceptions @@ -27,10 +29,10 @@ class UserCollection(entities.Collection['User']): """The collection of users stored in a backend.""" @staticmethod - def _entity_base_cls() -> Type['User']: + def _entity_base_cls() -> Type[User]: return User - def get_or_create(self, email: str, **kwargs) -> Tuple[bool, 'User']: + def get_or_create(self, email: str, **kwargs) -> Tuple[bool, User]: """Get the existing user with a given email address or create an unstored one :param kwargs: The properties of the user to get or create @@ -43,7 +45,7 @@ def get_or_create(self, email: str, **kwargs) -> Tuple[bool, 'User']: except exceptions.NotExistent: return True, User(backend=self.backend, email=email, **kwargs) - def get_default(self) -> Optional['User']: + def get_default(self) -> Optional[User]: """Get the current default user""" return self.backend.default_user From 807b238ca6f5317fd65ce9b8426a508692886f8d Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Tue, 4 Nov 2025 10:52:26 +0100 Subject: [PATCH 26/59] Implement repo methods to extract object size and objects as a zipfile (bytes) --- src/aiida/orm/nodes/repository.py | 32 +++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/aiida/orm/nodes/repository.py b/src/aiida/orm/nodes/repository.py index 5694a0f67d..c9b07a5f2a 100644 --- a/src/aiida/orm/nodes/repository.py +++ b/src/aiida/orm/nodes/repository.py @@ -9,6 +9,7 @@ import shutil import tempfile import typing as t +import zipfile from aiida.common import exceptions from aiida.manage import get_config_option @@ -273,6 +274,37 @@ def get_object_content(self, path: str, mode: t.Literal['r', 'rb'] = 'r') -> str return self._repository.get_object_content(path) + def get_object_size(self, path: str) -> int: + """Return the size of the object located at the given path. + + :param path: the relative path of the object within the repository. + :return: the size of the object in bytes. + :raises TypeError: if the path is not a string and relative path. + :raises FileNotFoundError: if the file does not exist. + :raises IsADirectoryError: if the object is a directory and not a file. + :raises OSError: if the file could not be opened. + """ + with self.open(path, mode='rb') as handle: + handle.seek(0, io.SEEK_END) + size = handle.tell() + return size + + def get_zipped_objects(self, compression: int = zipfile.ZIP_DEFLATED) -> bytes: + """Return the zipped content of the repository or a sub path within it. + + :param compression: the compression method to use. Defaults to `zipfile.ZIP_DEFLATED` (8). + :return: the zipped content as bytes. + """ + + zip_bytes_io = io.BytesIO() + + with zipfile.ZipFile(zip_bytes_io, mode='w', compression=compression) as zip_file: + for object_name in self.list_object_names(): + file_content = self.get_object_content(object_name, mode='rb') + zip_file.writestr(object_name, file_content) + + return zip_bytes_io.getvalue() + def put_object_from_bytes(self, content: bytes, path: str) -> None: """Store the given content in the repository at the given path. From 4930ca0045dad9d71af1d8fc56251b4ec36036ef Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Tue, 4 Nov 2025 14:02:18 +0100 Subject: [PATCH 27/59] Add field validator to transform computer pk to label --- src/aiida/orm/nodes/data/code/installed.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/aiida/orm/nodes/data/code/installed.py b/src/aiida/orm/nodes/data/code/installed.py index bcb1a3f755..c12b738170 100644 --- a/src/aiida/orm/nodes/data/code/installed.py +++ b/src/aiida/orm/nodes/data/code/installed.py @@ -19,6 +19,8 @@ import pathlib from typing import cast +from pydantic import field_validator + from aiida.common import exceptions from aiida.common.lang import type_check from aiida.common.log import override_log_level @@ -47,7 +49,7 @@ class Model(AbstractCode.Model): description='The label of the remote computer on which the executable resides.', is_attribute=False, orm_to_model=lambda node: cast(InstalledCode, node).computer.label, - model_to_orm=lambda model: cast(InstalledCode.Model, model).load_computer(), + model_to_orm=lambda model: cast(InstalledCode.Model, model).load_computer(model.computer), short_name='-Y', priority=2, ) @@ -59,7 +61,21 @@ class Model(AbstractCode.Model): priority=1, ) - def load_computer(self) -> Computer: + @field_validator('computer', mode='before') + @classmethod + def validate_computer(cls, value: str | int) -> str: + """Validate the ``computer`` field. + + :param value: The value to validate. + :return: The validated value. + :raises ValueError: If the value is not a string or integer. + """ + if isinstance(value, int): + return cls.load_computer(value).label + return value + + @classmethod + def load_computer(cls, comp_id: str | int) -> Computer: """Load the computer instance. :return: The computer instance. @@ -68,7 +84,7 @@ def load_computer(self) -> Computer: from aiida.orm import load_computer try: - return load_computer(self.computer) + return load_computer(comp_id) except exceptions.NotExistent as exception: raise ValueError(exception) from exception From 96bd7fae196a0f52ec17bca484804ff598e96c73 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Tue, 4 Nov 2025 14:52:26 +0100 Subject: [PATCH 28/59] Fix `orm_to_model` type --- src/aiida/common/pydantic.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/aiida/common/pydantic.py b/src/aiida/common/pydantic.py index f12c76952f..8943b66deb 100644 --- a/src/aiida/common/pydantic.py +++ b/src/aiida/common/pydantic.py @@ -35,8 +35,11 @@ def MetadataField( # noqa: N802 short_name: str | None = None, option_cls: t.Any | None = None, orm_class: type[Entity[t.Any, t.Any]] | str | None = None, - orm_to_model: t.Callable[[Entity[t.Any, t.Any], Path], t.Any] | None = None, - model_to_orm: t.Callable[['BaseModel'], t.Any] | None = None, + orm_to_model: t.Callable[[Entity[t.Any, t.Any], Path], t.Any] # see `PortableCode.Model.filepath_files` + | t.Callable[[Entity[t.Any, t.Any], bool], t.Any] # see `Node.Model.repository_content` + | t.Callable[[Entity[t.Any, t.Any]], t.Any] # all other cases + | None = None, + model_to_orm: t.Callable[[BaseModel], t.Any] | None = None, exclude_to_orm: bool = False, exclude_from_cli: bool = False, is_attribute: bool = True, From 1ac855372ec3504889cf504af306f8d070c45008 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Tue, 4 Nov 2025 16:38:39 +0100 Subject: [PATCH 29/59] Fix types --- src/aiida/orm/nodes/data/array/array.py | 12 ++++++----- src/aiida/orm/nodes/data/code/installed.py | 23 +++++++--------------- src/aiida/orm/nodes/data/code/portable.py | 3 ++- src/aiida/orm/nodes/data/enum.py | 2 +- 4 files changed, 17 insertions(+), 23 deletions(-) diff --git a/src/aiida/orm/nodes/data/array/array.py b/src/aiida/orm/nodes/data/array/array.py index 3a07841c3c..b5ef6c4db4 100644 --- a/src/aiida/orm/nodes/data/array/array.py +++ b/src/aiida/orm/nodes/data/array/array.py @@ -83,7 +83,7 @@ def __init__(self, arrays: np.ndarray | dict[str, np.ndarray] | None = None, **k @staticmethod def save_arrays(arrays: dict[str, np.ndarray]) -> dict[str, bytes]: - results = {} + results: dict[str, bytes] = {} for key, array in arrays.items(): stream = io.BytesIO() @@ -94,8 +94,11 @@ def save_arrays(arrays: dict[str, np.ndarray]) -> dict[str, bytes]: return results @staticmethod - def load_arrays(arrays: dict[str, bytes]) -> dict[str, np.ndarray]: - results = {} + def load_arrays(arrays: dict[str, bytes] | None) -> dict[str, np.ndarray]: + results: dict[str, np.ndarray] = {} + + if arrays is None: + return results for key, encoded in arrays.items(): stream = io.BytesIO(base64.decodebytes(encoded)) @@ -227,8 +230,7 @@ def set_array(self, name: str, array: np.ndarray) -> None: # Check if the name is valid if not name or re.sub('[0-9a-zA-Z_]', '', name): raise ValueError( - 'The name assigned to the array ({}) is not valid,' - 'it can only contain digits, letters and underscores' + 'The name assigned to the array ({}) is not valid,it can only contain digits, letters and underscores' ) # Write the array to a temporary file, and then add it to the repository of the node diff --git a/src/aiida/orm/nodes/data/code/installed.py b/src/aiida/orm/nodes/data/code/installed.py index c12b738170..2fff717320 100644 --- a/src/aiida/orm/nodes/data/code/installed.py +++ b/src/aiida/orm/nodes/data/code/installed.py @@ -49,7 +49,7 @@ class Model(AbstractCode.Model): description='The label of the remote computer on which the executable resides.', is_attribute=False, orm_to_model=lambda node: cast(InstalledCode, node).computer.label, - model_to_orm=lambda model: cast(InstalledCode.Model, model).load_computer(model.computer), + model_to_orm=lambda model: cast(InstalledCode.Model, model).load_computer(), short_name='-Y', priority=2, ) @@ -70,23 +70,14 @@ def validate_computer(cls, value: str | int) -> str: :return: The validated value. :raises ValueError: If the value is not a string or integer. """ - if isinstance(value, int): - return cls.load_computer(value).label - return value - - @classmethod - def load_computer(cls, comp_id: str | int) -> Computer: - """Load the computer instance. - - :return: The computer instance. - :raises ValueError: If the computer does not exist. - """ from aiida.orm import load_computer - try: - return load_computer(comp_id) - except exceptions.NotExistent as exception: - raise ValueError(exception) from exception + if isinstance(value, int): + try: + return load_computer(value).label + except exceptions.NotExistent as exception: + raise ValueError(f'No computer found for the given id: {value}') from exception + return value def __init__(self, computer: Computer, filepath_executable: str, **kwargs): """Construct a new instance. diff --git a/src/aiida/orm/nodes/data/code/portable.py b/src/aiida/orm/nodes/data/code/portable.py index 498535c911..4395894aaf 100644 --- a/src/aiida/orm/nodes/data/code/portable.py +++ b/src/aiida/orm/nodes/data/code/portable.py @@ -21,6 +21,7 @@ import logging import pathlib +from typing import cast from aiida.common import exceptions from aiida.common.folders import Folder @@ -63,7 +64,7 @@ class Model(AbstractCode.Model): description='Relative filepath of executable with directory of code files.', short_name='-X', priority=1, - orm_to_model=lambda node: str(node.filepath_executable), # type: ignore[attr-defined] + orm_to_model=lambda node: str(cast(PortableCode, node).filepath_executable), ) filepath_files: str = MetadataField( ..., diff --git a/src/aiida/orm/nodes/data/enum.py b/src/aiida/orm/nodes/data/enum.py index 1c6fb41c79..c1c3af4fc8 100644 --- a/src/aiida/orm/nodes/data/enum.py +++ b/src/aiida/orm/nodes/data/enum.py @@ -53,7 +53,7 @@ class of the enumeration) in the ``KEY_NAME``, ``KEY_VALUE`` and ``KEY_IDENTIFIE class Model(Data.Model): member: Enum = MetadataField( description='The member name.', - orm_to_model=lambda node, _: node.get_member(), # type: ignore[attr-defined] + orm_to_model=lambda node: t.cast(EnumData, node).get_member(), ) def __init__(self, member: Enum, *args, **kwargs): From 35b103047485f6f30c97967d1afd363c1a612b3f Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Wed, 5 Nov 2025 10:05:57 +0100 Subject: [PATCH 30/59] Mark `InstalledCode.Model.computer` as an attribute --- src/aiida/orm/nodes/data/code/installed.py | 1 - src/aiida/orm/nodes/node.py | 11 ++++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/aiida/orm/nodes/data/code/installed.py b/src/aiida/orm/nodes/data/code/installed.py index 2fff717320..6a0ab7ad25 100644 --- a/src/aiida/orm/nodes/data/code/installed.py +++ b/src/aiida/orm/nodes/data/code/installed.py @@ -47,7 +47,6 @@ class Model(AbstractCode.Model): computer: str = MetadataField( title='Computer', description='The label of the remote computer on which the executable resides.', - is_attribute=False, orm_to_model=lambda node: cast(InstalledCode, node).computer.label, model_to_orm=lambda model: cast(InstalledCode.Model, model).load_computer(), short_name='-Y', diff --git a/src/aiida/orm/nodes/node.py b/src/aiida/orm/nodes/node.py index 591a3542be..8ae6d6d7be 100644 --- a/src/aiida/orm/nodes/node.py +++ b/src/aiida/orm/nodes/node.py @@ -279,7 +279,7 @@ class Model(Entity.Model): None, description='The label of the computer', is_attribute=False, - orm_to_model=lambda node: cast(Node, node).computer.label if cast(Node, node).computer else None, # type: ignore[union-attr] + orm_to_model=lambda node: cast(Node, node).get_computer_label(), model_to_orm=lambda model: cast(Node.Model, model).load_computer(), exclude_from_cli=True, exclude_to_orm=True, @@ -704,6 +704,15 @@ def get_description(self) -> str: """ return '' + def get_computer_label(self) -> Optional[str]: + """Get the label of the computer of this node. + + :return: The computer label or None if no computer is set. + """ + if self.computer is None: + return None + return self.computer.label + @property def is_valid_cache(self) -> bool: """Hook to exclude certain ``Node`` classes from being considered a valid cache. From aa85ff1782743145945528927c23fbc6094bea83 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Wed, 5 Nov 2025 10:27:31 +0100 Subject: [PATCH 31/59] Fix `_prepare_yaml` --- src/aiida/orm/nodes/data/code/abstract.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/aiida/orm/nodes/data/code/abstract.py b/src/aiida/orm/nodes/data/code/abstract.py index eb72649b9c..f439fbc710 100644 --- a/src/aiida/orm/nodes/data/code/abstract.py +++ b/src/aiida/orm/nodes/data/code/abstract.py @@ -377,8 +377,10 @@ def _prepare_yaml(self, *args, **kwargs): continue elif (orm_to_model := get_metadata(field, 'orm_to_model')) is None: value = getattr(self, key) - else: + elif key == 'filepath_files': value = orm_to_model(self, pathlib.Path.cwd() / f'{self.label}') + else: + value = orm_to_model(self) # If the attribute is not set, for example ``with_mpi`` do not export it # so that there are no null-values in the resulting YAML file From 67a2dd7273a6a89af46fa0b5999056018eec0192 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Wed, 5 Nov 2025 11:06:16 +0100 Subject: [PATCH 32/59] Update regression tests --- .../orm/test_fields/fields_aiida.data.core.array.ArrayData.yml | 2 +- .../fields_aiida.data.core.array.bands.BandsData.yml | 2 +- .../fields_aiida.data.core.array.kpoints.KpointsData.yml | 2 +- .../fields_aiida.data.core.array.projection.ProjectionData.yml | 2 +- .../fields_aiida.data.core.array.trajectory.TrajectoryData.yml | 2 +- .../orm/test_fields/fields_aiida.data.core.array.xy.XyData.yml | 2 +- ...lds_aiida.data.core.code.containerized.ContainerizedCode.yml | 2 +- .../fields_aiida.data.core.code.installed.InstalledCode.yml | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/orm/test_fields/fields_aiida.data.core.array.ArrayData.yml b/tests/orm/test_fields/fields_aiida.data.core.array.ArrayData.yml index 5e38f108f0..0086bc2b59 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.array.ArrayData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.array.ArrayData.yml @@ -1,4 +1,4 @@ -arrays: QbDictField('arrays', dtype=typing.Optional[dict[str, numpy.ndarray]], is_attribute=True) +arrays: QbDictField('arrays', dtype=typing.Optional[dict[str, bytes]], is_attribute=True) attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) computer: QbStrField('computer', dtype=typing.Optional[str], is_attribute=False) diff --git a/tests/orm/test_fields/fields_aiida.data.core.array.bands.BandsData.yml b/tests/orm/test_fields/fields_aiida.data.core.array.bands.BandsData.yml index 79d9e78394..a4d635a7bd 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.array.bands.BandsData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.array.bands.BandsData.yml @@ -1,6 +1,6 @@ array_labels: QbArrayField('array_labels', dtype=typing.Optional[typing.List[str]], is_attribute=True) -arrays: QbDictField('arrays', dtype=typing.Optional[dict[str, numpy.ndarray]], is_attribute=True) +arrays: QbDictField('arrays', dtype=typing.Optional[dict[str, bytes]], is_attribute=True) attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) cell: QbArrayField('cell', dtype=typing.Optional[typing.List[typing.List[float]]], diff --git a/tests/orm/test_fields/fields_aiida.data.core.array.kpoints.KpointsData.yml b/tests/orm/test_fields/fields_aiida.data.core.array.kpoints.KpointsData.yml index 5787873d7a..e0a0e7533a 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.array.kpoints.KpointsData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.array.kpoints.KpointsData.yml @@ -1,4 +1,4 @@ -arrays: QbDictField('arrays', dtype=typing.Optional[dict[str, numpy.ndarray]], is_attribute=True) +arrays: QbDictField('arrays', dtype=typing.Optional[dict[str, bytes]], is_attribute=True) attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) cell: QbArrayField('cell', dtype=typing.Optional[typing.List[typing.List[float]]], diff --git a/tests/orm/test_fields/fields_aiida.data.core.array.projection.ProjectionData.yml b/tests/orm/test_fields/fields_aiida.data.core.array.projection.ProjectionData.yml index 5e38f108f0..0086bc2b59 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.array.projection.ProjectionData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.array.projection.ProjectionData.yml @@ -1,4 +1,4 @@ -arrays: QbDictField('arrays', dtype=typing.Optional[dict[str, numpy.ndarray]], is_attribute=True) +arrays: QbDictField('arrays', dtype=typing.Optional[dict[str, bytes]], is_attribute=True) attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) computer: QbStrField('computer', dtype=typing.Optional[str], is_attribute=False) diff --git a/tests/orm/test_fields/fields_aiida.data.core.array.trajectory.TrajectoryData.yml b/tests/orm/test_fields/fields_aiida.data.core.array.trajectory.TrajectoryData.yml index 1c92a3d0f9..0fa7353e3f 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.array.trajectory.TrajectoryData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.array.trajectory.TrajectoryData.yml @@ -1,4 +1,4 @@ -arrays: QbDictField('arrays', dtype=typing.Optional[dict[str, numpy.ndarray]], is_attribute=True) +arrays: QbDictField('arrays', dtype=typing.Optional[dict[str, bytes]], is_attribute=True) attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) computer: QbStrField('computer', dtype=typing.Optional[str], is_attribute=False) diff --git a/tests/orm/test_fields/fields_aiida.data.core.array.xy.XyData.yml b/tests/orm/test_fields/fields_aiida.data.core.array.xy.XyData.yml index 5e38f108f0..0086bc2b59 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.array.xy.XyData.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.array.xy.XyData.yml @@ -1,4 +1,4 @@ -arrays: QbDictField('arrays', dtype=typing.Optional[dict[str, numpy.ndarray]], is_attribute=True) +arrays: QbDictField('arrays', dtype=typing.Optional[dict[str, bytes]], is_attribute=True) attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) computer: QbStrField('computer', dtype=typing.Optional[str], is_attribute=False) diff --git a/tests/orm/test_fields/fields_aiida.data.core.code.containerized.ContainerizedCode.yml b/tests/orm/test_fields/fields_aiida.data.core.code.containerized.ContainerizedCode.yml index 4511495ff7..3b886b7b82 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.code.containerized.ContainerizedCode.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.code.containerized.ContainerizedCode.yml @@ -1,7 +1,7 @@ append_text: QbStrField('append_text', dtype=, is_attribute=True) attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) -computer: QbStrField('computer', dtype=, is_attribute=False) +computer: QbStrField('computer', dtype=, is_attribute=True) ctime: QbNumericField('ctime', dtype=, is_attribute=False) default_calc_job_plugin: QbStrField('default_calc_job_plugin', dtype=typing.Optional[str], is_attribute=True) diff --git a/tests/orm/test_fields/fields_aiida.data.core.code.installed.InstalledCode.yml b/tests/orm/test_fields/fields_aiida.data.core.code.installed.InstalledCode.yml index 6bdfcaed89..4ed121db3a 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.code.installed.InstalledCode.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.code.installed.InstalledCode.yml @@ -1,7 +1,7 @@ append_text: QbStrField('append_text', dtype=, is_attribute=True) attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) -computer: QbStrField('computer', dtype=, is_attribute=False) +computer: QbStrField('computer', dtype=, is_attribute=True) ctime: QbNumericField('ctime', dtype=, is_attribute=False) default_calc_job_plugin: QbStrField('default_calc_job_plugin', dtype=typing.Optional[str], is_attribute=True) From 22453f9f3237f7d071c0b53a1eb2bc5559cf08f0 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Wed, 5 Nov 2025 15:24:58 +0100 Subject: [PATCH 33/59] Centralize model field collection --- src/aiida/orm/entities.py | 55 +++++++++++++++++------ src/aiida/orm/nodes/data/code/abstract.py | 27 ++++------- 2 files changed, 50 insertions(+), 32 deletions(-) diff --git a/src/aiida/orm/entities.py b/src/aiida/orm/entities.py index 2f02b959f8..c5c8383d0c 100644 --- a/src/aiida/orm/entities.py +++ b/src/aiida/orm/entities.py @@ -316,21 +316,12 @@ def to_model( with `exclude_to_orm=True`. :return: An instance of the entity's model class. """ - fields = {} - Model = self.InputModel if unstored else self.Model # noqa: N806 - - for key, field in Model.model_fields.items(): - if orm_to_model := get_metadata(field, 'orm_to_model'): - if key == 'filepath_files': - fields[key] = orm_to_model(self, repository_path) - elif key == 'repository_content': - fields[key] = orm_to_model(self, include_repository_content) - else: - fields[key] = orm_to_model(self) - else: - fields[key] = getattr(self, key) - + fields = self._collect_model_field_values( + repository_path=repository_path, + include_repository_content=include_repository_content, + skip_cli_excluded=False, + ) return Model(**fields) @classmethod @@ -518,6 +509,42 @@ def backend_entity(self) -> BackendEntityType: """Get the implementing class for this object""" return self._backend_entity + def _collect_model_field_values( + self, + *, + repository_path: Optional[pathlib.Path] = None, + include_repository_content: bool = False, + skip_cli_excluded: bool = False, + ) -> dict[str, Any]: + """Collect values for the ``Model``'s fields from this entity. + + Centralizes mapping of ORM -> Model values, including handling of ``orm_to_model`` functions + and optional filtering based on field metadata (e.g., excluding CLI-only fields). + + :param repository_path: Optional path to use for repository-based fields. + :param include_repository_content: Whether to include repository file content. + :param skip_cli_excluded: When True, fields marked with ``exclude_from_cli`` metadata are skipped. + :return: Mapping of field name to value. + """ + fields: dict[str, Any] = {} + + for key, field in self.Model.model_fields.items(): + if skip_cli_excluded and get_metadata(field, 'exclude_from_cli'): + continue + + orm_to_model = get_metadata(field, 'orm_to_model') + if orm_to_model: + if key == 'filepath_files': + fields[key] = orm_to_model(self, repository_path) + elif key == 'repository_content': + fields[key] = orm_to_model(self, include_repository_content) + else: + fields[key] = orm_to_model(self) + else: + fields[key] = getattr(self, key) + + return fields + def from_backend_entity(cls: Type[EntityType], backend_entity: BackendEntityType) -> EntityType: """Construct an entity from a backend entity instance diff --git a/src/aiida/orm/nodes/data/code/abstract.py b/src/aiida/orm/nodes/data/code/abstract.py index f439fbc710..03e6c9b012 100644 --- a/src/aiida/orm/nodes/data/code/abstract.py +++ b/src/aiida/orm/nodes/data/code/abstract.py @@ -19,7 +19,7 @@ from aiida.common import exceptions from aiida.common.folders import Folder from aiida.common.lang import type_check -from aiida.common.pydantic import MetadataField, get_metadata +from aiida.common.pydantic import MetadataField from aiida.orm import Computer from aiida.plugins import CalculationFactory @@ -369,23 +369,14 @@ def _prepare_yaml(self, *args, **kwargs): """Export code to a YAML file.""" import yaml - code_data = {} - sort = kwargs.get('sort', False) - - for key, field in self.Model.model_fields.items(): - if get_metadata(field, 'exclude_from_cli'): - continue - elif (orm_to_model := get_metadata(field, 'orm_to_model')) is None: - value = getattr(self, key) - elif key == 'filepath_files': - value = orm_to_model(self, pathlib.Path.cwd() / f'{self.label}') - else: - value = orm_to_model(self) - - # If the attribute is not set, for example ``with_mpi`` do not export it - # so that there are no null-values in the resulting YAML file - code_data[key] = value - return yaml.dump(code_data, sort_keys=sort, encoding='utf-8'), {} + code_data = self._collect_model_field_values( + repository_path=pathlib.Path.cwd() / f'{self.label}', + skip_cli_excluded=True, + ) + + # If the attribute is not set, for example ``with_mpi`` do not export it + # so that there are no null-values in the resulting YAML file + return yaml.dump(code_data, sort_keys=kwargs.get('sort', False), encoding='utf-8'), {} def _prepare_yml(self, *args, **kwargs): """Also allow for export as .yml""" From 4b036372600ffea6b5fd05a473b31ea5f082f435 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Thu, 6 Nov 2025 08:31:07 +0100 Subject: [PATCH 34/59] Move unhandled node attributes gate to `Node` class --- src/aiida/orm/nodes/data/data.py | 21 --------------------- src/aiida/orm/nodes/node.py | 21 +++++++++++++++++++++ 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/aiida/orm/nodes/data/data.py b/src/aiida/orm/nodes/data/data.py index e98ead5d26..6faa31c9e4 100644 --- a/src/aiida/orm/nodes/data/data.py +++ b/src/aiida/orm/nodes/data/data.py @@ -8,7 +8,6 @@ ########################################################################### """Module with `Node` sub class `Data` to be used as a base class for data structures.""" -from collections.abc import Iterable from typing import Dict, Optional from aiida.common import exceptions @@ -22,19 +21,6 @@ __all__ = ('Data',) -class UnhandledDataAttributesError(Exception): - """Exception raised when any data attributes are not handled prior to the Data constructor.""" - - def __init__(self, attributes: Iterable[str], class_name: str) -> None: - bullet_list = '\n'.join(f' • {attr}' for attr in attributes) - message = ( - f'\nThe following attributes must be handled in a constructor prior to the Data class:\n' - f'{bullet_list}\n\n' - f'Consider implementing a constructor in {class_name} to handle the listed attributes.' - ) - super().__init__(message) - - class Data(Node): """The base class for all Data nodes. @@ -72,13 +58,6 @@ class Model(Node.Model): def __init__(self, *args, source=None, **kwargs): """Construct a new instance, setting the ``source`` attribute if provided as a keyword argument.""" - # We verify here that all attributes of Data plugins are handled in a constructor prior to the root - # Data class (here), gracefully rejecting them otherwise. - node_keys = set(Node.Model.model_fields.keys()) | {'backend'} - unhandled_keys = {key for key in kwargs if key not in node_keys} - if unhandled_keys: - raise UnhandledDataAttributesError(unhandled_keys, self.__class__.__name__) - super().__init__(*args, **kwargs) if source is not None: diff --git a/src/aiida/orm/nodes/node.py b/src/aiida/orm/nodes/node.py index 8ae6d6d7be..742d845f6f 100644 --- a/src/aiida/orm/nodes/node.py +++ b/src/aiida/orm/nodes/node.py @@ -12,6 +12,7 @@ import base64 import datetime +from collections.abc import Iterable from functools import cached_property from typing import ( TYPE_CHECKING, @@ -70,6 +71,19 @@ NodeType = TypeVar('NodeType', bound='Node') +class UnhandledNodeAttributesError(Exception): + """Exception raised when any node attributes are not handled prior to the Node constructor.""" + + def __init__(self, attributes: Iterable[str], class_name: str) -> None: + bullet_list = '\n'.join(f' • {attr}' for attr in attributes) + message = ( + f'\nThe following attributes must be handled in a constructor prior to the Node class:\n' + f'{bullet_list}\n\n' + f'Consider implementing a constructor in {class_name} to handle the listed attributes.' + ) + super().__init__(message) + + class NodeCollection(EntityCollection[NodeType], Generic[NodeType]): """The collection of nodes.""" @@ -328,6 +342,13 @@ def __init__( extras: Optional[Dict[str, Any]] = None, **kwargs: Any, ) -> None: + # We verify here that all attributes are handled in a constructor prior to the root + # Node class (here), gracefully rejecting them otherwise. + node_keys = set(Node.Model.model_fields.keys()) + unhandled_keys = {key for key in kwargs if key not in node_keys} + if unhandled_keys: + raise UnhandledNodeAttributesError(unhandled_keys, self.__class__.__name__) + backend = backend or get_manager().get_profile_storage() if computer and not computer.is_stored: From e8eddd2042caa59d218ac8bef0be1713993bdcdd Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Thu, 6 Nov 2025 08:31:40 +0100 Subject: [PATCH 35/59] Add `attributes` to `Node` constructor --- src/aiida/orm/nodes/node.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/aiida/orm/nodes/node.py b/src/aiida/orm/nodes/node.py index 742d845f6f..d1aab45eff 100644 --- a/src/aiida/orm/nodes/node.py +++ b/src/aiida/orm/nodes/node.py @@ -340,6 +340,7 @@ def __init__( user: Optional[User] = None, computer: Optional[Computer] = None, extras: Optional[Dict[str, Any]] = None, + attributes: Optional[Dict[str, Any]] = None, **kwargs: Any, ) -> None: # We verify here that all attributes are handled in a constructor prior to the root @@ -360,8 +361,6 @@ def __init__( if user is None: raise ValueError('the user cannot be None') - attributes = kwargs.pop('attributes', {}) - backend_entity = backend.nodes.create( node_type=self.class_node_type, user=user.backend_entity, computer=backend_computer, **kwargs ) From 09fe76f60e095043f1c59925577bf09612e20813 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Thu, 6 Nov 2025 09:55:06 +0100 Subject: [PATCH 36/59] Fix code fields --- src/aiida/cmdline/commands/cmd_code.py | 26 ++++------------------ src/aiida/orm/nodes/data/code/installed.py | 1 + 2 files changed, 5 insertions(+), 22 deletions(-) diff --git a/src/aiida/cmdline/commands/cmd_code.py b/src/aiida/cmdline/commands/cmd_code.py index 050d710a7b..b9a2b00075 100644 --- a/src/aiida/cmdline/commands/cmd_code.py +++ b/src/aiida/cmdline/commands/cmd_code.py @@ -242,37 +242,19 @@ def show(code): table.append(['Type', code.entry_point.name]) for field_name, field_info in code.Model.model_fields.items(): - # Skip fields excluded from CLI - if get_metadata( - field_info, - key='exclude_from_cli', - default=False, - ): + if get_metadata(field_info, key='exclude_from_cli'): continue - # Skip fields that are not stored in the attributes column - # NOTE: this also catches e.g., filepath_files for PortableCode, which is actually a "misuse" - # of the is_attribute metadata flag, as there it is flagging that the field is not stored at all! - # TODO (edan-bainglass) consider improving this by introducing a new metadata flag or reworking PortableCode - # TODO see also Dict and InstalledCode for other potential misuses of is_attribute - if not get_metadata( - field_info, - key='is_attribute', - default=True, - ): + # FIXME resolve this hardcoded special case properly + if field_name == 'filepath_files': continue value = getattr(code, field_name) - # Special handling for computer field to show additional info if field_name == 'computer': value = f'{value.label} ({value.hostname}), pk: {value.pk}' - # Use the field's title as display name. - # This allows for custom titles (class-cased by default from Pydantic). - display_name = field_info.title - - table.append([display_name, value]) + table.append([field_info.title, value]) if is_verbose(): table.append(['Calculations', len(code.base.links.get_outgoing().all())]) diff --git a/src/aiida/orm/nodes/data/code/installed.py b/src/aiida/orm/nodes/data/code/installed.py index 6a0ab7ad25..2fff717320 100644 --- a/src/aiida/orm/nodes/data/code/installed.py +++ b/src/aiida/orm/nodes/data/code/installed.py @@ -47,6 +47,7 @@ class Model(AbstractCode.Model): computer: str = MetadataField( title='Computer', description='The label of the remote computer on which the executable resides.', + is_attribute=False, orm_to_model=lambda node: cast(InstalledCode, node).computer.label, model_to_orm=lambda model: cast(InstalledCode.Model, model).load_computer(), short_name='-Y', From 712ab72c2c718c57677f6b7c220619fb4272dc46 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Thu, 6 Nov 2025 12:13:05 +0100 Subject: [PATCH 37/59] Fix centralized model field collection --- src/aiida/orm/entities.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/aiida/orm/entities.py b/src/aiida/orm/entities.py index c5c8383d0c..c1a4a1dbd5 100644 --- a/src/aiida/orm/entities.py +++ b/src/aiida/orm/entities.py @@ -321,6 +321,7 @@ def to_model( repository_path=repository_path, include_repository_content=include_repository_content, skip_cli_excluded=False, + unstored=unstored, ) return Model(**fields) @@ -515,6 +516,7 @@ def _collect_model_field_values( repository_path: Optional[pathlib.Path] = None, include_repository_content: bool = False, skip_cli_excluded: bool = False, + unstored: bool = False, ) -> dict[str, Any]: """Collect values for the ``Model``'s fields from this entity. @@ -524,11 +526,15 @@ def _collect_model_field_values( :param repository_path: Optional path to use for repository-based fields. :param include_repository_content: Whether to include repository file content. :param skip_cli_excluded: When True, fields marked with ``exclude_from_cli`` metadata are skipped. + :param unstored: If True, the input version of the model is used, which strips read-only fields, i.e., fields + with `exclude_to_orm=True`. :return: Mapping of field name to value. """ fields: dict[str, Any] = {} - for key, field in self.Model.model_fields.items(): + Model = self.InputModel if unstored else self.Model # noqa: N806 + + for key, field in Model.model_fields.items(): if skip_cli_excluded and get_metadata(field, 'exclude_from_cli'): continue From e77d0d991545bb3db13ef986a170eb6cd5b435bc Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Thu, 6 Nov 2025 12:13:08 +0100 Subject: [PATCH 38/59] Fix tests --- ...lds_aiida.data.core.code.containerized.ContainerizedCode.yml | 2 +- .../fields_aiida.data.core.code.installed.InstalledCode.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/orm/test_fields/fields_aiida.data.core.code.containerized.ContainerizedCode.yml b/tests/orm/test_fields/fields_aiida.data.core.code.containerized.ContainerizedCode.yml index 3b886b7b82..4511495ff7 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.code.containerized.ContainerizedCode.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.code.containerized.ContainerizedCode.yml @@ -1,7 +1,7 @@ append_text: QbStrField('append_text', dtype=, is_attribute=True) attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) -computer: QbStrField('computer', dtype=, is_attribute=True) +computer: QbStrField('computer', dtype=, is_attribute=False) ctime: QbNumericField('ctime', dtype=, is_attribute=False) default_calc_job_plugin: QbStrField('default_calc_job_plugin', dtype=typing.Optional[str], is_attribute=True) diff --git a/tests/orm/test_fields/fields_aiida.data.core.code.installed.InstalledCode.yml b/tests/orm/test_fields/fields_aiida.data.core.code.installed.InstalledCode.yml index 4ed121db3a..6bdfcaed89 100644 --- a/tests/orm/test_fields/fields_aiida.data.core.code.installed.InstalledCode.yml +++ b/tests/orm/test_fields/fields_aiida.data.core.code.installed.InstalledCode.yml @@ -1,7 +1,7 @@ append_text: QbStrField('append_text', dtype=, is_attribute=True) attributes: QbDictField('attributes', dtype=typing.Dict[str, typing.Any], is_attribute=False, is_subscriptable=True) -computer: QbStrField('computer', dtype=, is_attribute=True) +computer: QbStrField('computer', dtype=, is_attribute=False) ctime: QbNumericField('ctime', dtype=, is_attribute=False) default_calc_job_plugin: QbStrField('default_calc_job_plugin', dtype=typing.Optional[str], is_attribute=True) From a502917d77b7ce7458677f14972ce3082310b8aa Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Thu, 6 Nov 2025 17:58:27 +0100 Subject: [PATCH 39/59] Enable `ArrayNode` numpy array POST payloads via model validator --- src/aiida/orm/nodes/data/array/array.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/aiida/orm/nodes/data/array/array.py b/src/aiida/orm/nodes/data/array/array.py index b5ef6c4db4..9a665bf9e0 100644 --- a/src/aiida/orm/nodes/data/array/array.py +++ b/src/aiida/orm/nodes/data/array/array.py @@ -12,9 +12,11 @@ import base64 import io +from collections.abc import Iterable from typing import Any, Iterator, Optional, cast import numpy as np +from pydantic import field_validator from aiida.common.pydantic import MetadataField @@ -56,6 +58,19 @@ class Model(Data.Model): model_to_orm=lambda model: ArrayData.load_arrays(cast(ArrayData.Model, model).arrays), ) + @field_validator('arrays', mode='before') + @classmethod + def validate_arrays(cls, value: Any) -> Any: + if value is None: + return value + if not isinstance(value, dict): + raise TypeError(f'`arrays` should be a dictionary but got: {value}') + arrays: dict[str, bytes] = {} + for key, array in value.items(): + if isinstance(array, Iterable): + arrays |= ArrayData.save_arrays({key: array}) + return arrays + array_prefix = 'array|' default_array_name = 'default' From 8d532c7c238450e0ace50f5c1c5181d7c5426c8c Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Thu, 6 Nov 2025 18:19:07 +0100 Subject: [PATCH 40/59] Allow arguments in `orm_to_model` --- src/aiida/common/pydantic.py | 6 ++---- src/aiida/orm/entities.py | 20 ++++++++++---------- src/aiida/orm/nodes/data/code/portable.py | 5 ++++- src/aiida/orm/nodes/node.py | 4 ++-- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/aiida/common/pydantic.py b/src/aiida/common/pydantic.py index 8943b66deb..6118ad2d1f 100644 --- a/src/aiida/common/pydantic.py +++ b/src/aiida/common/pydantic.py @@ -3,7 +3,6 @@ from __future__ import annotations import typing as t -from pathlib import Path from pydantic import Field from pydantic_core import PydanticUndefined @@ -35,9 +34,8 @@ def MetadataField( # noqa: N802 short_name: str | None = None, option_cls: t.Any | None = None, orm_class: type[Entity[t.Any, t.Any]] | str | None = None, - orm_to_model: t.Callable[[Entity[t.Any, t.Any], Path], t.Any] # see `PortableCode.Model.filepath_files` - | t.Callable[[Entity[t.Any, t.Any], bool], t.Any] # see `Node.Model.repository_content` - | t.Callable[[Entity[t.Any, t.Any]], t.Any] # all other cases + orm_to_model: t.Callable[[Entity[t.Any, t.Any]], t.Any] # without arguments + | t.Callable[[Entity[t.Any, t.Any], dict[str, t.Any]], t.Any] # with arguments | None = None, model_to_orm: t.Callable[[BaseModel], t.Any] | None = None, exclude_to_orm: bool = False, diff --git a/src/aiida/orm/entities.py b/src/aiida/orm/entities.py index c1a4a1dbd5..057186a045 100644 --- a/src/aiida/orm/entities.py +++ b/src/aiida/orm/entities.py @@ -303,14 +303,14 @@ def to_model( self, *, repository_path: Optional[pathlib.Path] = None, - include_repository_content: bool = False, + serialize_repository_content: bool = False, unstored: bool = False, ) -> Model: """Return the entity instance as an instance of its model. :param repository_path: If the orm node has files in the repository, this path is used to read the repository files from. If no path is specified a temporary path is created using the entities pk. - :param include_repository_content: If True, repository file content is serialized in the model. + :param serialize_repository_content: If True, repository file content is serialized in the model. This field can be very large, so it is excluded by default. :param unstored: If True, the input version of the model is used, which strips read-only fields, i.e., fields with `exclude_to_orm=True`. @@ -319,7 +319,7 @@ def to_model( Model = self.InputModel if unstored else self.Model # noqa: N806 fields = self._collect_model_field_values( repository_path=repository_path, - include_repository_content=include_repository_content, + serialize_repository_content=serialize_repository_content, skip_cli_excluded=False, unstored=unstored, ) @@ -339,7 +339,7 @@ def serialize( self, *, repository_path: Optional[pathlib.Path] = None, - include_repository_content: bool = False, + serialize_repository_content: bool = False, unstored: bool = False, mode: Literal['json', 'python'] = 'json', ) -> dict[str, Any]: @@ -347,7 +347,7 @@ def serialize( :param repository_path: If the orm node has files in the repository, this path is used to dump the repository files to. If no path is specified a temporary path is created using the entities pk. - :param include_repository_content: If True, repository file content is serialized in the model. + :param serialize_repository_content: If True, repository file content is serialized in the model. This field can be very large, so it is excluded by default. :param unstored: If True, the input version of the model is used, which strips read-only fields, i.e., fields with `exclude_to_orm=True`. @@ -371,7 +371,7 @@ def serialize( raise ValueError(f'The repository_path `{repository_path}` is not a directory.') return self.to_model( repository_path=repository_path, - include_repository_content=include_repository_content, + serialize_repository_content=serialize_repository_content, unstored=unstored, ).model_dump(mode=mode) @@ -514,7 +514,7 @@ def _collect_model_field_values( self, *, repository_path: Optional[pathlib.Path] = None, - include_repository_content: bool = False, + serialize_repository_content: bool = False, skip_cli_excluded: bool = False, unstored: bool = False, ) -> dict[str, Any]: @@ -524,7 +524,7 @@ def _collect_model_field_values( and optional filtering based on field metadata (e.g., excluding CLI-only fields). :param repository_path: Optional path to use for repository-based fields. - :param include_repository_content: Whether to include repository file content. + :param serialize_repository_content: Whether to include repository file content. :param skip_cli_excluded: When True, fields marked with ``exclude_from_cli`` metadata are skipped. :param unstored: If True, the input version of the model is used, which strips read-only fields, i.e., fields with `exclude_to_orm=True`. @@ -541,9 +541,9 @@ def _collect_model_field_values( orm_to_model = get_metadata(field, 'orm_to_model') if orm_to_model: if key == 'filepath_files': - fields[key] = orm_to_model(self, repository_path) + fields[key] = orm_to_model(self, {'repository_path': repository_path}) elif key == 'repository_content': - fields[key] = orm_to_model(self, include_repository_content) + fields[key] = orm_to_model(self, {'serialize_repository_content': serialize_repository_content}) else: fields[key] = orm_to_model(self) else: diff --git a/src/aiida/orm/nodes/data/code/portable.py b/src/aiida/orm/nodes/data/code/portable.py index 4395894aaf..85447772ec 100644 --- a/src/aiida/orm/nodes/data/code/portable.py +++ b/src/aiida/orm/nodes/data/code/portable.py @@ -73,7 +73,10 @@ class Model(AbstractCode.Model): short_name='-F', is_attribute=False, priority=2, - orm_to_model=_export_filepath_files_from_repo, # type: ignore[arg-type] + orm_to_model=lambda node, kwargs: _export_filepath_files_from_repo( + cast(PortableCode, node), + kwargs.get('repository_path', pathlib.Path.cwd() / f'code_{node.pk}'), + ), ) def __init__( diff --git a/src/aiida/orm/nodes/node.py b/src/aiida/orm/nodes/node.py index d1aab45eff..5d1c283cd9 100644 --- a/src/aiida/orm/nodes/node.py +++ b/src/aiida/orm/nodes/node.py @@ -311,11 +311,11 @@ class Model(Entity.Model): description='Dictionary of file repository content. Keys are relative filepaths and values are binary file ' 'contents encoded as base64.', is_attribute=False, - orm_to_model=lambda node, serialize_content: { + orm_to_model=lambda node, kwargs: { key: base64.encodebytes(content) for key, content in cast(Node, node).base.repository.serialize_content().items() } - if serialize_content + if kwargs.get('serialize_repository_content') else {}, exclude_from_cli=True, exclude_to_orm=True, From a24dc2e60d7eed87b7da91c159edfeafc6ece083 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Thu, 6 Nov 2025 18:23:54 +0100 Subject: [PATCH 41/59] Fix formatting --- src/aiida/orm/nodes/data/array/array.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/aiida/orm/nodes/data/array/array.py b/src/aiida/orm/nodes/data/array/array.py index 9a665bf9e0..6f546e5a81 100644 --- a/src/aiida/orm/nodes/data/array/array.py +++ b/src/aiida/orm/nodes/data/array/array.py @@ -245,7 +245,8 @@ def set_array(self, name: str, array: np.ndarray) -> None: # Check if the name is valid if not name or re.sub('[0-9a-zA-Z_]', '', name): raise ValueError( - 'The name assigned to the array ({}) is not valid,it can only contain digits, letters and underscores' + 'The name assigned to the array ({}) is not valid,' + 'it can only contain digits, letters and underscores' ) # Write the array to a temporary file, and then add it to the repository of the node From b2a7bae6d160b1cd56e34578551bf6a2e0d7e3d8 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Thu, 6 Nov 2025 18:28:53 +0100 Subject: [PATCH 42/59] Fix `repository_path` default in portable code --- src/aiida/orm/nodes/data/code/portable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aiida/orm/nodes/data/code/portable.py b/src/aiida/orm/nodes/data/code/portable.py index 85447772ec..86c95edf34 100644 --- a/src/aiida/orm/nodes/data/code/portable.py +++ b/src/aiida/orm/nodes/data/code/portable.py @@ -75,7 +75,7 @@ class Model(AbstractCode.Model): priority=2, orm_to_model=lambda node, kwargs: _export_filepath_files_from_repo( cast(PortableCode, node), - kwargs.get('repository_path', pathlib.Path.cwd() / f'code_{node.pk}'), + kwargs.get('repository_path', pathlib.Path.cwd() / f'{cast(PortableCode, node).label}'), ), ) From b1a65ba089c8b545ed0b316fe05105bbb4c3fad2 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Fri, 7 Nov 2025 06:23:17 +0100 Subject: [PATCH 43/59] Fix iterable array posting --- src/aiida/orm/nodes/data/array/array.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/aiida/orm/nodes/data/array/array.py b/src/aiida/orm/nodes/data/array/array.py index 6f546e5a81..f8453dcf21 100644 --- a/src/aiida/orm/nodes/data/array/array.py +++ b/src/aiida/orm/nodes/data/array/array.py @@ -13,7 +13,7 @@ import base64 import io from collections.abc import Iterable -from typing import Any, Iterator, Optional, cast +from typing import Any, Iterator, Optional, Union, cast import numpy as np from pydantic import field_validator @@ -60,15 +60,19 @@ class Model(Data.Model): @field_validator('arrays', mode='before') @classmethod - def validate_arrays(cls, value: Any) -> Any: + def validate_arrays(cls, value: Optional[dict[str, Union[bytes, Any]]]) -> Any: if value is None: return value if not isinstance(value, dict): raise TypeError(f'`arrays` should be a dictionary but got: {value}') arrays: dict[str, bytes] = {} for key, array in value.items(): - if isinstance(array, Iterable): - arrays |= ArrayData.save_arrays({key: array}) + if isinstance(array, bytes): + arrays[key] = array + elif isinstance(array, Iterable): + arrays |= ArrayData.save_arrays({key: np.array(array)}) + else: + arrays[key] = array return arrays array_prefix = 'array|' @@ -245,8 +249,7 @@ def set_array(self, name: str, array: np.ndarray) -> None: # Check if the name is valid if not name or re.sub('[0-9a-zA-Z_]', '', name): raise ValueError( - 'The name assigned to the array ({}) is not valid,' - 'it can only contain digits, letters and underscores' + 'The name assigned to the array ({}) is not valid,it can only contain digits, letters and underscores' ) # Write the array to a temporary file, and then add it to the repository of the node From 9d60f2992166969ec07963296a72266d4562e6fd Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Fri, 7 Nov 2025 07:23:59 +0100 Subject: [PATCH 44/59] Fix model inheritance --- src/aiida/orm/nodes/data/bool.py | 3 +-- src/aiida/orm/nodes/data/str.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/aiida/orm/nodes/data/bool.py b/src/aiida/orm/nodes/data/bool.py index e669b9a722..cfac4cf0fc 100644 --- a/src/aiida/orm/nodes/data/bool.py +++ b/src/aiida/orm/nodes/data/bool.py @@ -13,7 +13,6 @@ from aiida.common.pydantic import MetadataField from .base import BaseType, to_aiida_type -from .numeric import NumericType __all__ = ('Bool',) @@ -23,7 +22,7 @@ class Bool(BaseType): _type = bool - class Model(NumericType.Model): + class Model(BaseType.Model): value: bool = MetadataField( title='Boolean value', description='The value of the boolean', diff --git a/src/aiida/orm/nodes/data/str.py b/src/aiida/orm/nodes/data/str.py index 927b2d4168..0ca3b642cf 100644 --- a/src/aiida/orm/nodes/data/str.py +++ b/src/aiida/orm/nodes/data/str.py @@ -11,7 +11,6 @@ from aiida.common.pydantic import MetadataField from .base import BaseType, to_aiida_type -from .numeric import NumericType __all__ = ('Str',) @@ -21,7 +20,7 @@ class Str(BaseType): _type = str - class Model(NumericType.Model): + class Model(BaseType.Model): value: str = MetadataField( title='String value', description='The value of the string', From b05ccdf05395e83b5b0cfb3c8b2bf28de845e4ed Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Fri, 7 Nov 2025 11:48:10 +0100 Subject: [PATCH 45/59] Fix `from_serialized` --- src/aiida/orm/entities.py | 5 +++-- tests/orm/data/code/test_abstract.py | 2 +- tests/orm/data/code/test_installed.py | 2 +- tests/orm/data/code/test_portable.py | 2 +- tests/orm/models/test_models.py | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/aiida/orm/entities.py b/src/aiida/orm/entities.py index 057186a045..ce97b1ab47 100644 --- a/src/aiida/orm/entities.py +++ b/src/aiida/orm/entities.py @@ -376,9 +376,10 @@ def serialize( ).model_dump(mode=mode) @classmethod - def from_serialized(cls, unstored: bool = False, **kwargs: dict[str, Any]) -> Self: + def from_serialized(cls, serialized: dict[str, Any], unstored: bool = False) -> Self: """Construct an entity instance from JSON serialized data. + :param serialized: A dictionary representing the serialized entity. :param unstored: If True, the input version of the model is used, which strips read-only fields, i.e., fields with `exclude_to_orm=True`. :return: An instance of the entity class. @@ -387,7 +388,7 @@ def from_serialized(cls, unstored: bool = False, **kwargs: dict[str, Any]) -> Se 'Serialization through pydantic is still an experimental feature and might break in future releases.' ) Model = cls.InputModel if unstored else cls.Model # noqa: N806 - return cls.from_model(Model(**kwargs)) + return cls.from_model(Model(**serialized)) @classproperty def objects(cls: EntityType) -> CollectionType: # noqa: N805 diff --git a/tests/orm/data/code/test_abstract.py b/tests/orm/data/code/test_abstract.py index df47eed8de..6b0a898d89 100644 --- a/tests/orm/data/code/test_abstract.py +++ b/tests/orm/data/code/test_abstract.py @@ -72,4 +72,4 @@ def test_serialization(): label = 'some-label' code = MockCode(label=label) - MockCode.from_serialized(unstored=True, **code.serialize(unstored=True)) + MockCode.from_serialized(code.serialize(unstored=True), unstored=True) diff --git a/tests/orm/data/code/test_installed.py b/tests/orm/data/code/test_installed.py index 7d2b13c02f..cb6f6526b0 100644 --- a/tests/orm/data/code/test_installed.py +++ b/tests/orm/data/code/test_installed.py @@ -152,4 +152,4 @@ def test_serialization(aiida_localhost, bash_path): """Test the deprecated :meth:`aiida.orm.nodes.data.code.installed.InstalledCode.get_execname` method.""" code = InstalledCode(label='some-label', computer=aiida_localhost, filepath_executable=str(bash_path.absolute())) - InstalledCode.from_serialized(unstored=True, **code.serialize(unstored=True)) + InstalledCode.from_serialized(code.serialize(unstored=True), unstored=True) diff --git a/tests/orm/data/code/test_portable.py b/tests/orm/data/code/test_portable.py index bb09591281..2d3a4df231 100644 --- a/tests/orm/data/code/test_portable.py +++ b/tests/orm/data/code/test_portable.py @@ -176,4 +176,4 @@ def test_serialization(tmp_path, chdir_tmp_path): (filepath_files / 'subdir').mkdir() (filepath_files / 'subdir/test').write_text('test') code = PortableCode(label='some-label', filepath_executable='bash', filepath_files=filepath_files) - PortableCode.from_serialized(unstored=True, **code.serialize(unstored=True)) + PortableCode.from_serialized(code.serialize(unstored=True), unstored=True) diff --git a/tests/orm/models/test_models.py b/tests/orm/models/test_models.py index 544ffe7262..507844d062 100644 --- a/tests/orm/models/test_models.py +++ b/tests/orm/models/test_models.py @@ -214,4 +214,4 @@ def test_roundtrip_serialization(required_arguments, tmp_path): # Get the model instance from the entity instance serialized_entity = entity.serialize(repository_path=tmp_path, unstored=True, mode='python') - entity.from_serialized(unstored=True, **serialized_entity) + entity.from_serialized(serialized_entity, unstored=True) From 636195a3c97302ff5ee56631e76901130a252002 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Sat, 8 Nov 2025 12:01:53 +0100 Subject: [PATCH 46/59] Rename `InputModel` and `as_input_model` to `CreateModel` and `as_create_model` --- src/aiida/cmdline/commands/cmd_code.py | 2 +- src/aiida/cmdline/groups/dynamic.py | 4 +-- src/aiida/orm/entities.py | 44 +++++++++++++------------- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/aiida/cmdline/commands/cmd_code.py b/src/aiida/cmdline/commands/cmd_code.py index b9a2b00075..24c7ac678d 100644 --- a/src/aiida/cmdline/commands/cmd_code.py +++ b/src/aiida/cmdline/commands/cmd_code.py @@ -40,7 +40,7 @@ def verdi_code(): def create_code(ctx: click.Context, cls: Code, **kwargs) -> None: """Create a new `Code` instance.""" try: - model = cls.InputModel(**kwargs) + model = cls.CreateModel(**kwargs) instance = cls.from_model(model) # type: ignore[arg-type] except (TypeError, ValueError) as exception: echo.echo_critical(f'Failed to create instance `{cls}`: {exception}') diff --git a/src/aiida/cmdline/groups/dynamic.py b/src/aiida/cmdline/groups/dynamic.py index d4c2527aa3..a2d3af0d6d 100644 --- a/src/aiida/cmdline/groups/dynamic.py +++ b/src/aiida/cmdline/groups/dynamic.py @@ -97,7 +97,7 @@ def call_command(self, ctx: click.Context, cls: t.Any, non_interactive: bool, ** if hasattr(cls, 'Model'): # The plugin defines a pydantic model: use it to validate the provided arguments - Model = cls.InputModel if hasattr(cls, 'InputModel') else cls.Model # noqa: N806 + Model = cls.CreateModel if hasattr(cls, 'CreateModel') else cls.Model # noqa: N806 try: Model(**kwargs) except ValidationError as exception: @@ -169,7 +169,7 @@ def list_options(self, entry_point: str) -> list[t.Callable[[FC], FC]]: options_spec = self.factory(entry_point).get_cli_options() # type: ignore[union-attr] return [self.create_option(*item) for item in options_spec] - Model = cls.InputModel if hasattr(cls, 'InputModel') else cls.Model # noqa: N806 + Model = cls.CreateModel if hasattr(cls, 'CreateModel') else cls.Model # noqa: N806 options_spec = {} diff --git a/src/aiida/orm/entities.py b/src/aiida/orm/entities.py index ce97b1ab47..3de8514953 100644 --- a/src/aiida/orm/entities.py +++ b/src/aiida/orm/entities.py @@ -206,28 +206,28 @@ def __pydantic_init_subclass__(cls, **kwargs: Any) -> None: cls.model_config['title'] = cls.__qualname__.replace('.', '') @classmethod - def as_input_model(cls: Type[EntityModelType]) -> Type[EntityModelType]: - """Return a derived model class with read-only fields removed. + def as_create_model(cls: Type[EntityModelType]) -> Type[EntityModelType]: + """Return a derived creation model class with read-only fields removed. This also removes any serializers/validators defined on those fields. - :return: The derived input model class. + :return: The derived creation model class. """ - # Derive the input model from the original model - new_name = cls.__qualname__.replace('.Model', 'InputModel') - InputModel = create_model( # noqa: N806 + # Derive the creation model from the original model + new_name = cls.__qualname__.replace('.Model', 'CreateModel') + CreateModel = create_model( # noqa: N806 new_name, __base__=cls, - __doc__=f'Input version of {cls.__name__}.', + __doc__=f'Creation version of {cls.__name__}.', ) - InputModel.__qualname__ = new_name - InputModel.__module__ = cls.__module__ + CreateModel.__qualname__ = new_name + CreateModel.__module__ = cls.__module__ # Identify read-only fields readonly_fields = [ name - for name, field in InputModel.model_fields.items() + for name, field in CreateModel.model_fields.items() if hasattr(field, 'json_schema_extra') and isinstance(field.json_schema_extra, dict) and field.json_schema_extra.get('readOnly') @@ -235,12 +235,12 @@ def as_input_model(cls: Type[EntityModelType]) -> Type[EntityModelType]: # Remove read-only fields for name in readonly_fields: - InputModel.model_fields.pop(name, None) - if hasattr(InputModel, name): - delattr(InputModel, name) + CreateModel.model_fields.pop(name, None) + if hasattr(CreateModel, name): + delattr(CreateModel, name) # Prune field validators/serializers referring to read-only fields - decorators = InputModel.__pydantic_decorators__ + decorators = CreateModel.__pydantic_decorators__ def _prune_field_decorators(field_decorators: dict[str, Any]) -> dict[str, Any]: return { @@ -252,15 +252,15 @@ def _prune_field_decorators(field_decorators: dict[str, Any]) -> dict[str, Any]: decorators.field_validators = _prune_field_decorators(decorators.field_validators) decorators.field_serializers = _prune_field_decorators(decorators.field_serializers) - return InputModel + return CreateModel @classproperty - def InputModel(cls) -> Type[Model]: # noqa: N802, N805 - """Return the input version of the model class for this entity. + def CreateModel(cls) -> Type[Model]: # noqa: N802, N805 + """Return the creation version of the model class for this entity. - :return: The input model class, with read-only fields removed. + :return: The creation model class, with read-only fields removed. """ - return cls.Model.as_input_model() + return cls.Model.as_create_model() @classmethod def model_to_orm_fields(cls) -> dict[str, FieldInfo]: @@ -316,7 +316,7 @@ def to_model( with `exclude_to_orm=True`. :return: An instance of the entity's model class. """ - Model = self.InputModel if unstored else self.Model # noqa: N806 + Model = self.CreateModel if unstored else self.Model # noqa: N806 fields = self._collect_model_field_values( repository_path=repository_path, serialize_repository_content=serialize_repository_content, @@ -387,7 +387,7 @@ def from_serialized(cls, serialized: dict[str, Any], unstored: bool = False) -> cls._logger.warning( 'Serialization through pydantic is still an experimental feature and might break in future releases.' ) - Model = cls.InputModel if unstored else cls.Model # noqa: N806 + Model = cls.CreateModel if unstored else cls.Model # noqa: N806 return cls.from_model(Model(**serialized)) @classproperty @@ -533,7 +533,7 @@ def _collect_model_field_values( """ fields: dict[str, Any] = {} - Model = self.InputModel if unstored else self.Model # noqa: N806 + Model = self.CreateModel if unstored else self.Model # noqa: N806 for key, field in Model.model_fields.items(): if skip_cli_excluded and get_metadata(field, 'exclude_from_cli'): From d654e3a1f1d4ec23dcee0596423420fe51245ce3 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Tue, 11 Nov 2025 17:06:32 +0100 Subject: [PATCH 47/59] Discard `exclude_from_cli` --- src/aiida/cmdline/commands/cmd_code.py | 2 +- src/aiida/cmdline/groups/dynamic.py | 2 +- src/aiida/common/pydantic.py | 4 -- src/aiida/orm/entities.py | 87 +++++++++++------------ src/aiida/orm/nodes/data/code/abstract.py | 4 +- src/aiida/orm/nodes/data/data.py | 15 ++-- src/aiida/orm/nodes/node.py | 10 --- 7 files changed, 54 insertions(+), 70 deletions(-) diff --git a/src/aiida/cmdline/commands/cmd_code.py b/src/aiida/cmdline/commands/cmd_code.py index 24c7ac678d..bc58b09fc2 100644 --- a/src/aiida/cmdline/commands/cmd_code.py +++ b/src/aiida/cmdline/commands/cmd_code.py @@ -242,7 +242,7 @@ def show(code): table.append(['Type', code.entry_point.name]) for field_name, field_info in code.Model.model_fields.items(): - if get_metadata(field_info, key='exclude_from_cli'): + if get_metadata(field_info, key='exclude_to_orm') or field_name == 'extras': continue # FIXME resolve this hardcoded special case properly diff --git a/src/aiida/cmdline/groups/dynamic.py b/src/aiida/cmdline/groups/dynamic.py index a2d3af0d6d..490218e9ac 100644 --- a/src/aiida/cmdline/groups/dynamic.py +++ b/src/aiida/cmdline/groups/dynamic.py @@ -174,7 +174,7 @@ def list_options(self, entry_point: str) -> list[t.Callable[[FC], FC]]: options_spec = {} for key, field_info in Model.model_fields.items(): - if get_metadata(field_info, 'exclude_from_cli'): + if get_metadata(field_info, 'exclude_to_orm') or key == 'extras': continue default = field_info.default_factory if field_info.default is PydanticUndefined else field_info.default diff --git a/src/aiida/common/pydantic.py b/src/aiida/common/pydantic.py index 6118ad2d1f..48db9a7313 100644 --- a/src/aiida/common/pydantic.py +++ b/src/aiida/common/pydantic.py @@ -39,7 +39,6 @@ def MetadataField( # noqa: N802 | None = None, model_to_orm: t.Callable[[BaseModel], t.Any] | None = None, exclude_to_orm: bool = False, - exclude_from_cli: bool = False, is_attribute: bool = True, is_subscriptable: bool = False, **kwargs: t.Any, @@ -76,8 +75,6 @@ class Model(BaseModel): :param model_to_orm: Optional callable to convert the value of a field from a model instance to an ORM instance. :param exclude_to_orm: When set to ``True``, this field value will not be passed to the ORM entity constructor through ``Entity.from_model``. - :param exclude_from_cli: When set to ``True``, this field value will not be exposed on the CLI command that is - dynamically generated to create a new instance. :param is_attribute: Whether the field is stored as an attribute. Used by `QbFields`. :param is_subscriptable: Whether the field can be indexed like a list or dictionary. Used by `QbFields`. """ @@ -91,7 +88,6 @@ class Model(BaseModel): ('orm_to_model', orm_to_model), ('model_to_orm', model_to_orm), ('exclude_to_orm', exclude_to_orm), - ('exclude_from_cli', exclude_from_cli), ('is_attribute', is_attribute), ('is_subscriptable', is_subscriptable), ): diff --git a/src/aiida/orm/entities.py b/src/aiida/orm/entities.py index 3de8514953..b875edaf1c 100644 --- a/src/aiida/orm/entities.py +++ b/src/aiida/orm/entities.py @@ -192,7 +192,6 @@ class Model(BaseModel, defer_build=True): description='The primary key of the entity', is_attribute=False, exclude_to_orm=True, - exclude_from_cli=True, ) @classmethod @@ -299,6 +298,47 @@ def model_to_orm_field_values(cls, model: Model) -> dict[str, Any]: return fields + def orm_to_model_field_values( + self, + *, + repository_path: Optional[pathlib.Path] = None, + serialize_repository_content: bool = False, + skip_read_only: bool = False, + unstored: bool = False, + ) -> dict[str, Any]: + """Collect values for the ``Model``'s fields from this entity. + + Centralizes mapping of ORM -> Model values, including handling of ``orm_to_model`` functions + and optional filtering based on field metadata (e.g., excluding CLI-only fields). + + :param repository_path: Optional path to use for repository-based fields. + :param serialize_repository_content: Whether to include repository file content. + :param skip_read_only: When True, fields marked with ``exclude_to_orm`` metadata are skipped. + :param unstored: If True, the input version of the model is used, which strips read-only fields, i.e., fields + with `exclude_to_orm=True`. + :return: Mapping of field name to value. + """ + fields: dict[str, Any] = {} + + Model = self.CreateModel if unstored else self.Model # noqa: N806 + + for key, field in Model.model_fields.items(): + if (skip_read_only and get_metadata(field, 'exclude_to_orm')) or key == 'extras': + continue + + orm_to_model = get_metadata(field, 'orm_to_model') + if orm_to_model: + if key == 'filepath_files': + fields[key] = orm_to_model(self, {'repository_path': repository_path}) + elif key == 'repository_content': + fields[key] = orm_to_model(self, {'serialize_repository_content': serialize_repository_content}) + else: + fields[key] = orm_to_model(self) + else: + fields[key] = getattr(self, key) + + return fields + def to_model( self, *, @@ -317,10 +357,10 @@ def to_model( :return: An instance of the entity's model class. """ Model = self.CreateModel if unstored else self.Model # noqa: N806 - fields = self._collect_model_field_values( + fields = self.orm_to_model_field_values( repository_path=repository_path, serialize_repository_content=serialize_repository_content, - skip_cli_excluded=False, + skip_read_only=False, unstored=unstored, ) return Model(**fields) @@ -511,47 +551,6 @@ def backend_entity(self) -> BackendEntityType: """Get the implementing class for this object""" return self._backend_entity - def _collect_model_field_values( - self, - *, - repository_path: Optional[pathlib.Path] = None, - serialize_repository_content: bool = False, - skip_cli_excluded: bool = False, - unstored: bool = False, - ) -> dict[str, Any]: - """Collect values for the ``Model``'s fields from this entity. - - Centralizes mapping of ORM -> Model values, including handling of ``orm_to_model`` functions - and optional filtering based on field metadata (e.g., excluding CLI-only fields). - - :param repository_path: Optional path to use for repository-based fields. - :param serialize_repository_content: Whether to include repository file content. - :param skip_cli_excluded: When True, fields marked with ``exclude_from_cli`` metadata are skipped. - :param unstored: If True, the input version of the model is used, which strips read-only fields, i.e., fields - with `exclude_to_orm=True`. - :return: Mapping of field name to value. - """ - fields: dict[str, Any] = {} - - Model = self.CreateModel if unstored else self.Model # noqa: N806 - - for key, field in Model.model_fields.items(): - if skip_cli_excluded and get_metadata(field, 'exclude_from_cli'): - continue - - orm_to_model = get_metadata(field, 'orm_to_model') - if orm_to_model: - if key == 'filepath_files': - fields[key] = orm_to_model(self, {'repository_path': repository_path}) - elif key == 'repository_content': - fields[key] = orm_to_model(self, {'serialize_repository_content': serialize_repository_content}) - else: - fields[key] = orm_to_model(self) - else: - fields[key] = getattr(self, key) - - return fields - def from_backend_entity(cls: Type[EntityType], backend_entity: BackendEntityType) -> EntityType: """Construct an entity from a backend entity instance diff --git a/src/aiida/orm/nodes/data/code/abstract.py b/src/aiida/orm/nodes/data/code/abstract.py index 03e6c9b012..f5ca8068e5 100644 --- a/src/aiida/orm/nodes/data/code/abstract.py +++ b/src/aiida/orm/nodes/data/code/abstract.py @@ -369,9 +369,9 @@ def _prepare_yaml(self, *args, **kwargs): """Export code to a YAML file.""" import yaml - code_data = self._collect_model_field_values( + code_data = self.orm_to_model_field_values( repository_path=pathlib.Path.cwd() / f'{self.label}', - skip_cli_excluded=True, + skip_read_only=True, ) # If the attribute is not set, for example ``with_mpi`` do not export it diff --git a/src/aiida/orm/nodes/data/data.py b/src/aiida/orm/nodes/data/data.py index 6faa31c9e4..904c4e1c44 100644 --- a/src/aiida/orm/nodes/data/data.py +++ b/src/aiida/orm/nodes/data/data.py @@ -52,7 +52,6 @@ class Model(Node.Model): description='Source of the data.', is_subscriptable=True, exclude_to_orm=True, - exclude_from_cli=True, ) def __init__(self, *args, source=None, **kwargs): @@ -119,7 +118,7 @@ def source(self, source): raise ValueError('Source must be supplied as a dictionary') unknown_attrs = tuple(set(source.keys()) - set(self._source_attributes)) if unknown_attrs: - raise KeyError(f"Unknown source parameters: {', '.join(unknown_attrs)}") + raise KeyError(f'Unknown source parameters: {", ".join(unknown_attrs)}') self.base.attributes.set('source', source) @@ -167,13 +166,13 @@ def _exportcontent(self, fileformat, main_file_name='', **kwargs): except KeyError: if exporters.keys(): raise ValueError( - 'The format {} is not implemented for {}. ' 'Currently implemented are: {}.'.format( + 'The format {} is not implemented for {}. Currently implemented are: {}.'.format( fileformat, self.__class__.__name__, ','.join(exporters.keys()) ) ) else: raise ValueError( - 'The format {} is not implemented for {}. ' 'No formats are implemented yet.'.format( + 'The format {} is not implemented for {}. No formats are implemented yet.'.format( fileformat, self.__class__.__name__ ) ) @@ -275,13 +274,13 @@ def importstring(self, inputstring, fileformat, **kwargs): except KeyError: if importers.keys(): raise ValueError( - 'The format {} is not implemented for {}. ' 'Currently implemented are: {}.'.format( + 'The format {} is not implemented for {}. Currently implemented are: {}.'.format( fileformat, self.__class__.__name__, ','.join(importers.keys()) ) ) else: raise ValueError( - 'The format {} is not implemented for {}. ' 'No formats are implemented yet.'.format( + 'The format {} is not implemented for {}. No formats are implemented yet.'.format( fileformat, self.__class__.__name__ ) ) @@ -332,13 +331,13 @@ def convert(self, object_format=None, *args): except KeyError: if converters.keys(): raise ValueError( - 'The format {} is not implemented for {}. ' 'Currently implemented are: {}.'.format( + 'The format {} is not implemented for {}. Currently implemented are: {}.'.format( object_format, self.__class__.__name__, ','.join(converters.keys()) ) ) else: raise ValueError( - 'The format {} is not implemented for {}. ' 'No formats are implemented yet.'.format( + 'The format {} is not implemented for {}. No formats are implemented yet.'.format( object_format, self.__class__.__name__ ) ) diff --git a/src/aiida/orm/nodes/node.py b/src/aiida/orm/nodes/node.py index 5d1c283cd9..992816969f 100644 --- a/src/aiida/orm/nodes/node.py +++ b/src/aiida/orm/nodes/node.py @@ -228,20 +228,17 @@ class Model(Entity.Model): description='The UUID of the node', is_attribute=False, exclude_to_orm=True, - exclude_from_cli=True, ) node_type: str = MetadataField( description='The type of the node', is_attribute=False, exclude_to_orm=True, - exclude_from_cli=True, ) process_type: Optional[str] = MetadataField( None, description='The process type of the node', is_attribute=False, exclude_to_orm=True, - exclude_from_cli=True, ) repository_metadata: Dict[str, Any] = MetadataField( default_factory=dict, @@ -249,19 +246,16 @@ class Model(Entity.Model): is_attribute=False, orm_to_model=lambda node: cast(Node, node).base.repository.metadata, exclude_to_orm=True, - exclude_from_cli=True, ) ctime: datetime.datetime = MetadataField( description='The creation time of the node', is_attribute=False, exclude_to_orm=True, - exclude_from_cli=True, ) mtime: datetime.datetime = MetadataField( description='The modification time of the node', is_attribute=False, exclude_to_orm=True, - exclude_from_cli=True, ) label: str = MetadataField( '', @@ -287,7 +281,6 @@ class Model(Entity.Model): is_attribute=False, orm_to_model=lambda node: cast(Node, node).base.extras.all, is_subscriptable=True, - exclude_from_cli=True, ) computer: Optional[str] = MetadataField( None, @@ -295,7 +288,6 @@ class Model(Entity.Model): is_attribute=False, orm_to_model=lambda node: cast(Node, node).get_computer_label(), model_to_orm=lambda model: cast(Node.Model, model).load_computer(), - exclude_from_cli=True, exclude_to_orm=True, ) user: int = MetadataField( @@ -304,7 +296,6 @@ class Model(Entity.Model): orm_to_model=lambda node: cast(Node, node).user.pk, orm_class=User, exclude_to_orm=True, - exclude_from_cli=True, ) repository_content: dict[str, bytes] = MetadataField( default_factory=dict, @@ -317,7 +308,6 @@ class Model(Entity.Model): } if kwargs.get('serialize_repository_content') else {}, - exclude_from_cli=True, exclude_to_orm=True, ) From aba646a8c626400a89a3e7d5178879d51e7d40e2 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Tue, 11 Nov 2025 17:06:48 +0100 Subject: [PATCH 48/59] Exclude `attributes` from ORM --- src/aiida/orm/nodes/node.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/aiida/orm/nodes/node.py b/src/aiida/orm/nodes/node.py index 992816969f..e4d8cc1412 100644 --- a/src/aiida/orm/nodes/node.py +++ b/src/aiida/orm/nodes/node.py @@ -273,7 +273,7 @@ class Model(Entity.Model): is_attribute=False, orm_to_model=lambda node: cast(Node, node).base.attributes.all, is_subscriptable=True, - exclude_from_cli=True, + exclude_to_orm=True, ) extras: Dict[str, Any] = MetadataField( default_factory=dict, @@ -330,7 +330,6 @@ def __init__( user: Optional[User] = None, computer: Optional[Computer] = None, extras: Optional[Dict[str, Any]] = None, - attributes: Optional[Dict[str, Any]] = None, **kwargs: Any, ) -> None: # We verify here that all attributes are handled in a constructor prior to the root @@ -356,9 +355,6 @@ def __init__( ) super().__init__(backend_entity) - if attributes: - self.base.attributes.set_many(attributes) - if extras is not None: self.base.extras.set_many(extras) From 84ce866d1f6bafb31b4dc75a8ac5b1441a8d241c Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Wed, 12 Nov 2025 07:34:31 +0100 Subject: [PATCH 49/59] Discard `unstored` from `from_serialized` --- src/aiida/orm/entities.py | 7 ++----- tests/orm/data/code/test_abstract.py | 2 +- tests/orm/data/code/test_installed.py | 2 +- tests/orm/data/code/test_portable.py | 2 +- tests/orm/models/test_models.py | 2 +- 5 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/aiida/orm/entities.py b/src/aiida/orm/entities.py index b875edaf1c..3b3765e711 100644 --- a/src/aiida/orm/entities.py +++ b/src/aiida/orm/entities.py @@ -416,19 +416,16 @@ def serialize( ).model_dump(mode=mode) @classmethod - def from_serialized(cls, serialized: dict[str, Any], unstored: bool = False) -> Self: + def from_serialized(cls, serialized: dict[str, Any]) -> Self: """Construct an entity instance from JSON serialized data. :param serialized: A dictionary representing the serialized entity. - :param unstored: If True, the input version of the model is used, which strips read-only fields, i.e., fields - with `exclude_to_orm=True`. :return: An instance of the entity class. """ cls._logger.warning( 'Serialization through pydantic is still an experimental feature and might break in future releases.' ) - Model = cls.CreateModel if unstored else cls.Model # noqa: N806 - return cls.from_model(Model(**serialized)) + return cls.from_model(cls.CreateModel(**serialized)) @classproperty def objects(cls: EntityType) -> CollectionType: # noqa: N805 diff --git a/tests/orm/data/code/test_abstract.py b/tests/orm/data/code/test_abstract.py index 6b0a898d89..67fd24949a 100644 --- a/tests/orm/data/code/test_abstract.py +++ b/tests/orm/data/code/test_abstract.py @@ -72,4 +72,4 @@ def test_serialization(): label = 'some-label' code = MockCode(label=label) - MockCode.from_serialized(code.serialize(unstored=True), unstored=True) + MockCode.from_serialized(code.serialize(unstored=True)) diff --git a/tests/orm/data/code/test_installed.py b/tests/orm/data/code/test_installed.py index cb6f6526b0..39d2fdbc67 100644 --- a/tests/orm/data/code/test_installed.py +++ b/tests/orm/data/code/test_installed.py @@ -152,4 +152,4 @@ def test_serialization(aiida_localhost, bash_path): """Test the deprecated :meth:`aiida.orm.nodes.data.code.installed.InstalledCode.get_execname` method.""" code = InstalledCode(label='some-label', computer=aiida_localhost, filepath_executable=str(bash_path.absolute())) - InstalledCode.from_serialized(code.serialize(unstored=True), unstored=True) + InstalledCode.from_serialized(code.serialize(unstored=True)) diff --git a/tests/orm/data/code/test_portable.py b/tests/orm/data/code/test_portable.py index 2d3a4df231..488aa8e518 100644 --- a/tests/orm/data/code/test_portable.py +++ b/tests/orm/data/code/test_portable.py @@ -176,4 +176,4 @@ def test_serialization(tmp_path, chdir_tmp_path): (filepath_files / 'subdir').mkdir() (filepath_files / 'subdir/test').write_text('test') code = PortableCode(label='some-label', filepath_executable='bash', filepath_files=filepath_files) - PortableCode.from_serialized(code.serialize(unstored=True), unstored=True) + PortableCode.from_serialized(code.serialize(unstored=True)) diff --git a/tests/orm/models/test_models.py b/tests/orm/models/test_models.py index 507844d062..45e8a33c09 100644 --- a/tests/orm/models/test_models.py +++ b/tests/orm/models/test_models.py @@ -214,4 +214,4 @@ def test_roundtrip_serialization(required_arguments, tmp_path): # Get the model instance from the entity instance serialized_entity = entity.serialize(repository_path=tmp_path, unstored=True, mode='python') - entity.from_serialized(serialized_entity, unstored=True) + entity.from_serialized(serialized_entity) From 2a87bd5adfc86b3eb5df053b9160dc80baa7eddd Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Wed, 12 Nov 2025 07:48:09 +0100 Subject: [PATCH 50/59] Discard `unstored` serialization parameter --- src/aiida/orm/entities.py | 18 ++++-------------- tests/orm/data/code/test_abstract.py | 2 +- tests/orm/data/code/test_installed.py | 2 +- tests/orm/data/code/test_portable.py | 2 +- tests/orm/models/test_models.py | 6 +++--- 5 files changed, 10 insertions(+), 20 deletions(-) diff --git a/src/aiida/orm/entities.py b/src/aiida/orm/entities.py index 3b3765e711..d73eb2e1b4 100644 --- a/src/aiida/orm/entities.py +++ b/src/aiida/orm/entities.py @@ -222,6 +222,7 @@ def as_create_model(cls: Type[EntityModelType]) -> Type[EntityModelType]: ) CreateModel.__qualname__ = new_name CreateModel.__module__ = cls.__module__ + CreateModel.model_config['extra'] = 'ignore' # Identify read-only fields readonly_fields = [ @@ -304,7 +305,6 @@ def orm_to_model_field_values( repository_path: Optional[pathlib.Path] = None, serialize_repository_content: bool = False, skip_read_only: bool = False, - unstored: bool = False, ) -> dict[str, Any]: """Collect values for the ``Model``'s fields from this entity. @@ -313,14 +313,12 @@ def orm_to_model_field_values( :param repository_path: Optional path to use for repository-based fields. :param serialize_repository_content: Whether to include repository file content. - :param skip_read_only: When True, fields marked with ``exclude_to_orm`` metadata are skipped. - :param unstored: If True, the input version of the model is used, which strips read-only fields, i.e., fields - with `exclude_to_orm=True`. + :param skip_read_only: When True, fields marked with ``exclude_to_orm`` are skipped. :return: Mapping of field name to value. """ fields: dict[str, Any] = {} - Model = self.CreateModel if unstored else self.Model # noqa: N806 + Model = self.Model if self.pk else self.CreateModel # noqa: N806 for key, field in Model.model_fields.items(): if (skip_read_only and get_metadata(field, 'exclude_to_orm')) or key == 'extras': @@ -344,7 +342,6 @@ def to_model( *, repository_path: Optional[pathlib.Path] = None, serialize_repository_content: bool = False, - unstored: bool = False, ) -> Model: """Return the entity instance as an instance of its model. @@ -352,17 +349,14 @@ def to_model( files from. If no path is specified a temporary path is created using the entities pk. :param serialize_repository_content: If True, repository file content is serialized in the model. This field can be very large, so it is excluded by default. - :param unstored: If True, the input version of the model is used, which strips read-only fields, i.e., fields - with `exclude_to_orm=True`. :return: An instance of the entity's model class. """ - Model = self.CreateModel if unstored else self.Model # noqa: N806 fields = self.orm_to_model_field_values( repository_path=repository_path, serialize_repository_content=serialize_repository_content, skip_read_only=False, - unstored=unstored, ) + Model = self.Model if self.pk else self.CreateModel # noqa: N806 return Model(**fields) @classmethod @@ -380,7 +374,6 @@ def serialize( *, repository_path: Optional[pathlib.Path] = None, serialize_repository_content: bool = False, - unstored: bool = False, mode: Literal['json', 'python'] = 'json', ) -> dict[str, Any]: """Serialize the entity instance to JSON. @@ -389,8 +382,6 @@ def serialize( files to. If no path is specified a temporary path is created using the entities pk. :param serialize_repository_content: If True, repository file content is serialized in the model. This field can be very large, so it is excluded by default. - :param unstored: If True, the input version of the model is used, which strips read-only fields, i.e., fields - with `exclude_to_orm=True`. :param mode: The serialization mode, either 'json' or 'python'. The 'json' mode is the most strict and ensures that the output is JSON serializable, whereas the 'python' mode allows for more complex Python types, such as `datetime` objects. @@ -412,7 +403,6 @@ def serialize( return self.to_model( repository_path=repository_path, serialize_repository_content=serialize_repository_content, - unstored=unstored, ).model_dump(mode=mode) @classmethod diff --git a/tests/orm/data/code/test_abstract.py b/tests/orm/data/code/test_abstract.py index 67fd24949a..d4008156c0 100644 --- a/tests/orm/data/code/test_abstract.py +++ b/tests/orm/data/code/test_abstract.py @@ -72,4 +72,4 @@ def test_serialization(): label = 'some-label' code = MockCode(label=label) - MockCode.from_serialized(code.serialize(unstored=True)) + MockCode.from_serialized(code.serialize()) diff --git a/tests/orm/data/code/test_installed.py b/tests/orm/data/code/test_installed.py index 39d2fdbc67..a110b84def 100644 --- a/tests/orm/data/code/test_installed.py +++ b/tests/orm/data/code/test_installed.py @@ -152,4 +152,4 @@ def test_serialization(aiida_localhost, bash_path): """Test the deprecated :meth:`aiida.orm.nodes.data.code.installed.InstalledCode.get_execname` method.""" code = InstalledCode(label='some-label', computer=aiida_localhost, filepath_executable=str(bash_path.absolute())) - InstalledCode.from_serialized(code.serialize(unstored=True)) + InstalledCode.from_serialized(code.serialize()) diff --git a/tests/orm/data/code/test_portable.py b/tests/orm/data/code/test_portable.py index 488aa8e518..e8d3634997 100644 --- a/tests/orm/data/code/test_portable.py +++ b/tests/orm/data/code/test_portable.py @@ -176,4 +176,4 @@ def test_serialization(tmp_path, chdir_tmp_path): (filepath_files / 'subdir').mkdir() (filepath_files / 'subdir/test').write_text('test') code = PortableCode(label='some-label', filepath_executable='bash', filepath_files=filepath_files) - PortableCode.from_serialized(code.serialize(unstored=True)) + PortableCode.from_serialized(code.serialize()) diff --git a/tests/orm/models/test_models.py b/tests/orm/models/test_models.py index 45e8a33c09..1da8314036 100644 --- a/tests/orm/models/test_models.py +++ b/tests/orm/models/test_models.py @@ -176,7 +176,7 @@ def test_roundtrip(required_arguments, tmp_path): assert isinstance(entity, cls) # Get the model instance from the entity instance - model = entity.to_model(repository_path=tmp_path, unstored=True) + model = entity.to_model(repository_path=tmp_path) assert isinstance(model, BaseModel) # Reconstruct the entity instance from the model instance @@ -187,7 +187,7 @@ def test_roundtrip(required_arguments, tmp_path): # ORM entity constructor are identical of the original model. The ``model_to_orm_field_values`` excludes values of # fields that define ``exclude_to_orm=True`` because these can change during roundtrips. This because these # typically correspond to entity fields that have defaults set on the database level, e.g., UUIDs. - roundtrip_model = roundtrip.to_model(repository_path=tmp_path, unstored=True) + roundtrip_model = roundtrip.to_model(repository_path=tmp_path) original_field_values = cls.model_to_orm_field_values(model) for key, value in cls.model_to_orm_field_values(roundtrip_model).items(): @@ -213,5 +213,5 @@ def test_roundtrip_serialization(required_arguments, tmp_path): assert isinstance(entity, cls) # Get the model instance from the entity instance - serialized_entity = entity.serialize(repository_path=tmp_path, unstored=True, mode='python') + serialized_entity = entity.serialize(repository_path=tmp_path, mode='python') entity.from_serialized(serialized_entity) From b69c1c70455ea78958508c2b7e4b73301297227b Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Wed, 12 Nov 2025 08:43:05 +0100 Subject: [PATCH 51/59] Always pass `kwargs` to `orm_to_model` --- src/aiida/common/pydantic.py | 4 +-- src/aiida/orm/authinfos.py | 4 +-- src/aiida/orm/comments.py | 4 +-- src/aiida/orm/entities.py | 15 ++++------ src/aiida/orm/groups.py | 4 +-- src/aiida/orm/nodes/data/array/array.py | 2 +- src/aiida/orm/nodes/data/array/bands.py | 8 ++--- src/aiida/orm/nodes/data/array/kpoints.py | 29 ++++++++++--------- src/aiida/orm/nodes/data/code/installed.py | 4 +-- src/aiida/orm/nodes/data/code/portable.py | 2 +- src/aiida/orm/nodes/data/enum.py | 2 +- src/aiida/orm/nodes/data/remote/base.py | 2 +- src/aiida/orm/nodes/node.py | 10 +++---- .../orm/nodes/process/calculation/calcjob.py | 20 ++++++------- 14 files changed, 53 insertions(+), 57 deletions(-) diff --git a/src/aiida/common/pydantic.py b/src/aiida/common/pydantic.py index 48db9a7313..942a74115b 100644 --- a/src/aiida/common/pydantic.py +++ b/src/aiida/common/pydantic.py @@ -34,9 +34,7 @@ def MetadataField( # noqa: N802 short_name: str | None = None, option_cls: t.Any | None = None, orm_class: type[Entity[t.Any, t.Any]] | str | None = None, - orm_to_model: t.Callable[[Entity[t.Any, t.Any]], t.Any] # without arguments - | t.Callable[[Entity[t.Any, t.Any], dict[str, t.Any]], t.Any] # with arguments - | None = None, + orm_to_model: t.Callable[[Entity[t.Any, t.Any], dict[str, t.Any]], t.Any] | None = None, model_to_orm: t.Callable[[BaseModel], t.Any] | None = None, exclude_to_orm: bool = False, is_attribute: bool = True, diff --git a/src/aiida/orm/authinfos.py b/src/aiida/orm/authinfos.py index 0e4ddcd673..fd07af1d3f 100644 --- a/src/aiida/orm/authinfos.py +++ b/src/aiida/orm/authinfos.py @@ -55,13 +55,13 @@ class Model(entities.Entity.Model): description='The PK of the computer', is_attribute=False, orm_class=Computer, - orm_to_model=lambda auth_info: cast(AuthInfo, auth_info).computer.pk, + orm_to_model=lambda auth_info, _: cast(AuthInfo, auth_info).computer.pk, ) user: int = MetadataField( description='The PK of the user', is_attribute=False, orm_class=User, - orm_to_model=lambda auth_info: cast(AuthInfo, auth_info).user.pk, + orm_to_model=lambda auth_info, _: cast(AuthInfo, auth_info).user.pk, ) enabled: bool = MetadataField( True, diff --git a/src/aiida/orm/comments.py b/src/aiida/orm/comments.py index f11ac0317a..699bb8859e 100644 --- a/src/aiida/orm/comments.py +++ b/src/aiida/orm/comments.py @@ -90,13 +90,13 @@ class Model(entities.Entity.Model): description='Node PK that the comment is attached to', is_attribute=False, orm_class='core.node', - orm_to_model=lambda comment: cast(Comment, comment).node.pk, + orm_to_model=lambda comment, _: cast(Comment, comment).node.pk, ) user: int = MetadataField( description='User PK that created the comment', is_attribute=False, orm_class='core.user', - orm_to_model=lambda comment: cast(Comment, comment).user.pk, + orm_to_model=lambda comment, _: cast(Comment, comment).user.pk, ) content: str = MetadataField( description='Content of the comment', diff --git a/src/aiida/orm/entities.py b/src/aiida/orm/entities.py index d73eb2e1b4..2065759f5f 100644 --- a/src/aiida/orm/entities.py +++ b/src/aiida/orm/entities.py @@ -324,14 +324,12 @@ def orm_to_model_field_values( if (skip_read_only and get_metadata(field, 'exclude_to_orm')) or key == 'extras': continue - orm_to_model = get_metadata(field, 'orm_to_model') - if orm_to_model: - if key == 'filepath_files': - fields[key] = orm_to_model(self, {'repository_path': repository_path}) - elif key == 'repository_content': - fields[key] = orm_to_model(self, {'serialize_repository_content': serialize_repository_content}) - else: - fields[key] = orm_to_model(self) + if orm_to_model := get_metadata(field, 'orm_to_model'): + kwargs = { + 'serialize_repository_content': serialize_repository_content, # see `Node` model + 'repository_path': repository_path, # see `PortableCode` model + } + fields[key] = orm_to_model(self, kwargs) else: fields[key] = getattr(self, key) @@ -354,7 +352,6 @@ def to_model( fields = self.orm_to_model_field_values( repository_path=repository_path, serialize_repository_content=serialize_repository_content, - skip_read_only=False, ) Model = self.Model if self.pk else self.CreateModel # noqa: N806 return Model(**fields) diff --git a/src/aiida/orm/groups.py b/src/aiida/orm/groups.py index 9bdc5e26b7..ea08bf37c3 100644 --- a/src/aiida/orm/groups.py +++ b/src/aiida/orm/groups.py @@ -128,7 +128,7 @@ class Model(entities.Entity.Model): description='The PK of the group owner', is_attribute=False, orm_class='core.user', - orm_to_model=lambda group: cast(Group, group).user.pk, + orm_to_model=lambda group, _: cast(Group, group).user.pk, exclude_to_orm=True, ) time: datetime.datetime = MetadataField( @@ -150,7 +150,7 @@ class Model(entities.Entity.Model): description='The group extras', is_attribute=False, is_subscriptable=True, - orm_to_model=lambda group: cast(Group, group).base.extras.all, + orm_to_model=lambda group, _: cast(Group, group).base.extras.all, ) _CLS_COLLECTION = GroupCollection diff --git a/src/aiida/orm/nodes/data/array/array.py b/src/aiida/orm/nodes/data/array/array.py index f8453dcf21..bcc484e317 100644 --- a/src/aiida/orm/nodes/data/array/array.py +++ b/src/aiida/orm/nodes/data/array/array.py @@ -54,7 +54,7 @@ class Model(Data.Model): arrays: Optional[dict[str, bytes]] = MetadataField( None, description='The dictionary of numpy arrays.', - orm_to_model=lambda node: ArrayData.save_arrays(cast(ArrayData, node).arrays), + orm_to_model=lambda node, _: ArrayData.save_arrays(cast(ArrayData, node).arrays), model_to_orm=lambda model: ArrayData.load_arrays(cast(ArrayData.Model, model).arrays), ) diff --git a/src/aiida/orm/nodes/data/array/bands.py b/src/aiida/orm/nodes/data/array/bands.py index 40a4d4c489..15a3ea08af 100644 --- a/src/aiida/orm/nodes/data/array/bands.py +++ b/src/aiida/orm/nodes/data/array/bands.py @@ -143,7 +143,7 @@ def nint(num): lumo = [_[0][_[1] + 1] for _ in zip(bands, homo_indexes)] except IndexError: raise ValueError( - 'To understand if it is a metal or insulator, ' 'need more bands than n_band=number_electrons' + 'To understand if it is a metal or insulator, need more bands than n_band=number_electrons' ) else: @@ -160,7 +160,7 @@ def nint(num): lumo = [i[number_electrons // number_electrons_per_band] for i in bands] # take the n+1th level except IndexError: raise ValueError( - 'To understand if it is a metal or insulator, ' 'need more bands than n_band=number_electrons' + 'To understand if it is a metal or insulator, need more bands than n_band=number_electrons' ) if number_electrons % 2 == 1 and len(stored_bands.shape) == 2: @@ -223,7 +223,7 @@ class Model(KpointsData.Model): units: t.Optional[str] = MetadataField( None, description='Units in which the data in bands were stored', - orm_to_model=lambda node: t.cast(BandsData, node).base.attributes.get('units', None), + orm_to_model=lambda node, _: t.cast(BandsData, node).base.attributes.get('units', None), ) def __init__( @@ -323,7 +323,7 @@ def _validate_bands_occupations(self, bands, occupations=None, labels=None): the_labels = [str(_) for _ in labels] else: raise ValidationError( - 'Band labels have an unrecognized type ({})' 'but should be a string or a list of strings'.format( + 'Band labels have an unrecognized type ({})but should be a string or a list of strings'.format( labels.__class__ ) ) diff --git a/src/aiida/orm/nodes/data/array/kpoints.py b/src/aiida/orm/nodes/data/array/kpoints.py index b61f407986..112940e367 100644 --- a/src/aiida/orm/nodes/data/array/kpoints.py +++ b/src/aiida/orm/nodes/data/array/kpoints.py @@ -44,42 +44,42 @@ class Model(ArrayData.Model): labels: t.Optional[t.List[str]] = MetadataField( None, description='Labels associated with the list of kpoints', - orm_to_model=lambda node: t.cast(KpointsData, node).base.attributes.get('labels', None), + orm_to_model=lambda node, _: t.cast(KpointsData, node).base.attributes.get('labels', None), ) label_numbers: t.Optional[t.List[int]] = MetadataField( None, description='Index of the labels in the list of kpoints', - orm_to_model=lambda node: t.cast(KpointsData, node).base.attributes.get('label_numbers', None), + orm_to_model=lambda node, _: t.cast(KpointsData, node).base.attributes.get('label_numbers', None), ) cell: t.Optional[t.List[t.List[float]]] = MetadataField( None, description='Unit cell of the crystal, in Angstroms', - orm_to_model=lambda node: t.cast(KpointsData, node).base.attributes.get('cell', None), + orm_to_model=lambda node, _: t.cast(KpointsData, node).base.attributes.get('cell', None), ) pbc1: t.Optional[bool] = MetadataField( None, description='Periodicity in the first lattice vector direction', - orm_to_model=lambda node: t.cast(KpointsData, node).pbc[0], + orm_to_model=lambda node, _: t.cast(KpointsData, node).pbc[0], ) pbc2: t.Optional[bool] = MetadataField( None, description='Periodicity in the second lattice vector direction', - orm_to_model=lambda node: t.cast(KpointsData, node).pbc[1], + orm_to_model=lambda node, _: t.cast(KpointsData, node).pbc[1], ) pbc3: t.Optional[bool] = MetadataField( None, description='Periodicity in the third lattice vector direction', - orm_to_model=lambda node: t.cast(KpointsData, node).pbc[2], + orm_to_model=lambda node, _: t.cast(KpointsData, node).pbc[2], ) mesh: t.Optional[t.List[int]] = MetadataField( None, description='Mesh of kpoints', - orm_to_model=lambda node: t.cast(KpointsData, node).base.attributes.get('mesh', None), + orm_to_model=lambda node, _: t.cast(KpointsData, node).base.attributes.get('mesh', None), ) offset: t.Optional[t.List[float]] = MetadataField( None, description='Offset of kpoints', - orm_to_model=lambda node: t.cast(KpointsData, node).base.attributes.get('offset', None), + orm_to_model=lambda node, _: t.cast(KpointsData, node).base.attributes.get('offset', None), ) def __init__( @@ -284,7 +284,7 @@ def set_cell_from_structure(self, structuredata): if not isinstance(structuredata, StructureData): raise ValueError( - 'An instance of StructureData should be passed to ' 'the KpointsData, found instead {}'.format( + 'An instance of StructureData should be passed to the KpointsData, found instead {}'.format( structuredata.__class__ ) ) @@ -441,9 +441,9 @@ def _validate_kpoints_weights(self, kpoints, weights): kpoints = numpy.array([[0.0, 0.0, 0.0]]) else: raise ValueError( - 'empty kpoints list is valid only in zero dimension' - '; instead here with have {} dimensions' - ''.format(self._dimension) + 'empty kpoints list is valid only in zero dimension; instead here with have {} dimensions'.format( + self._dimension + ) ) if len(kpoints.shape) <= 1: @@ -459,8 +459,9 @@ def _validate_kpoints_weights(self, kpoints, weights): if kpoints.shape[1] < self._dimension: raise ValueError( - 'In a system which has {0} dimensions, kpoint need' - 'more than {0} coordinates (found instead {1})'.format(self._dimension, kpoints.shape[1]) + 'In a system which has {0} dimensions, kpoint needmore than {0} coordinates (found instead {1})'.format( + self._dimension, kpoints.shape[1] + ) ) if weights is not None: diff --git a/src/aiida/orm/nodes/data/code/installed.py b/src/aiida/orm/nodes/data/code/installed.py index 2fff717320..91edce53f3 100644 --- a/src/aiida/orm/nodes/data/code/installed.py +++ b/src/aiida/orm/nodes/data/code/installed.py @@ -48,7 +48,7 @@ class Model(AbstractCode.Model): title='Computer', description='The label of the remote computer on which the executable resides.', is_attribute=False, - orm_to_model=lambda node: cast(InstalledCode, node).computer.label, + orm_to_model=lambda node, _: cast(InstalledCode, node).computer.label, model_to_orm=lambda model: cast(InstalledCode.Model, model).load_computer(), short_name='-Y', priority=2, @@ -56,7 +56,7 @@ class Model(AbstractCode.Model): filepath_executable: str = MetadataField( title='Filepath executable', description='Filepath of the executable on the remote computer.', - orm_to_model=lambda node: str(cast(InstalledCode, node).filepath_executable), + orm_to_model=lambda node, _: str(cast(InstalledCode, node).filepath_executable), short_name='-X', priority=1, ) diff --git a/src/aiida/orm/nodes/data/code/portable.py b/src/aiida/orm/nodes/data/code/portable.py index 86c95edf34..61d6826614 100644 --- a/src/aiida/orm/nodes/data/code/portable.py +++ b/src/aiida/orm/nodes/data/code/portable.py @@ -64,7 +64,7 @@ class Model(AbstractCode.Model): description='Relative filepath of executable with directory of code files.', short_name='-X', priority=1, - orm_to_model=lambda node: str(cast(PortableCode, node).filepath_executable), + orm_to_model=lambda node, _: str(cast(PortableCode, node).filepath_executable), ) filepath_files: str = MetadataField( ..., diff --git a/src/aiida/orm/nodes/data/enum.py b/src/aiida/orm/nodes/data/enum.py index c1c3af4fc8..0c12ddbdba 100644 --- a/src/aiida/orm/nodes/data/enum.py +++ b/src/aiida/orm/nodes/data/enum.py @@ -53,7 +53,7 @@ class of the enumeration) in the ``KEY_NAME``, ``KEY_VALUE`` and ``KEY_IDENTIFIE class Model(Data.Model): member: Enum = MetadataField( description='The member name.', - orm_to_model=lambda node: t.cast(EnumData, node).get_member(), + orm_to_model=lambda node, _: t.cast(EnumData, node).get_member(), ) def __init__(self, member: Enum, *args, **kwargs): diff --git a/src/aiida/orm/nodes/data/remote/base.py b/src/aiida/orm/nodes/data/remote/base.py index 5b9d1253dd..a926b1f056 100644 --- a/src/aiida/orm/nodes/data/remote/base.py +++ b/src/aiida/orm/nodes/data/remote/base.py @@ -39,7 +39,7 @@ class Model(Data.Model): None, title='Remote path', description='Filepath on the remote computer.', - orm_to_model=lambda node: node.get_remote_path(), + orm_to_model=lambda node, _: node.get_remote_path(), ) def __init__(self, remote_path: Optional[str] = None, **kwargs): diff --git a/src/aiida/orm/nodes/node.py b/src/aiida/orm/nodes/node.py index e4d8cc1412..24973a4e0c 100644 --- a/src/aiida/orm/nodes/node.py +++ b/src/aiida/orm/nodes/node.py @@ -244,7 +244,7 @@ class Model(Entity.Model): default_factory=dict, description='Virtual hierarchy of the file repository.', is_attribute=False, - orm_to_model=lambda node: cast(Node, node).base.repository.metadata, + orm_to_model=lambda node, _: cast(Node, node).base.repository.metadata, exclude_to_orm=True, ) ctime: datetime.datetime = MetadataField( @@ -271,7 +271,7 @@ class Model(Entity.Model): default_factory=dict, description='The node attributes', is_attribute=False, - orm_to_model=lambda node: cast(Node, node).base.attributes.all, + orm_to_model=lambda node, _: cast(Node, node).base.attributes.all, is_subscriptable=True, exclude_to_orm=True, ) @@ -279,21 +279,21 @@ class Model(Entity.Model): default_factory=dict, description='The node extras', is_attribute=False, - orm_to_model=lambda node: cast(Node, node).base.extras.all, + orm_to_model=lambda node, _: cast(Node, node).base.extras.all, is_subscriptable=True, ) computer: Optional[str] = MetadataField( None, description='The label of the computer', is_attribute=False, - orm_to_model=lambda node: cast(Node, node).get_computer_label(), + orm_to_model=lambda node, _: cast(Node, node).get_computer_label(), model_to_orm=lambda model: cast(Node.Model, model).load_computer(), exclude_to_orm=True, ) user: int = MetadataField( description='The PK of the user who owns the node', is_attribute=False, - orm_to_model=lambda node: cast(Node, node).user.pk, + orm_to_model=lambda node, _: cast(Node, node).user.pk, orm_class=User, exclude_to_orm=True, ) diff --git a/src/aiida/orm/nodes/process/calculation/calcjob.py b/src/aiida/orm/nodes/process/calculation/calcjob.py index 4244ac867a..5b886dd0a2 100644 --- a/src/aiida/orm/nodes/process/calculation/calcjob.py +++ b/src/aiida/orm/nodes/process/calculation/calcjob.py @@ -70,52 +70,52 @@ class Model(CalculationNode.Model): scheduler_state: Optional[str] = MetadataField( None, description='The state of the scheduler', - orm_to_model=lambda node: cast(CalcJobNode, node).get_scheduler_state(), + orm_to_model=lambda node, _: cast(CalcJobNode, node).get_scheduler_state(), ) state: Optional[str] = MetadataField( None, description='The active state of the calculation job', - orm_to_model=lambda node: cast(CalcJobNode, node).get_state(), + orm_to_model=lambda node, _: cast(CalcJobNode, node).get_state(), ) remote_workdir: Optional[str] = MetadataField( None, description='The path to the remote (on cluster) scratch folder', - orm_to_model=lambda node: cast(CalcJobNode, node).get_remote_workdir(), + orm_to_model=lambda node, _: cast(CalcJobNode, node).get_remote_workdir(), ) job_id: Optional[str] = MetadataField( None, description='The scheduler job id', - orm_to_model=lambda node: cast(CalcJobNode, node).get_job_id(), + orm_to_model=lambda node, _: cast(CalcJobNode, node).get_job_id(), ) scheduler_lastchecktime: Optional[datetime.datetime] = MetadataField( None, description='The last time the scheduler was checked, in isoformat', - orm_to_model=lambda node: cast(CalcJobNode, node).get_scheduler_lastchecktime(), + orm_to_model=lambda node, _: cast(CalcJobNode, node).get_scheduler_lastchecktime(), ) last_job_info: Optional[dict] = MetadataField( None, description='The last job info returned by the scheduler', - orm_to_model=lambda node: dict(cast(CalcJobNode, node).get_last_job_info() or {}), + orm_to_model=lambda node, _: dict(cast(CalcJobNode, node).get_last_job_info() or {}), ) detailed_job_info: Optional[dict] = MetadataField( None, description='The detailed job info returned by the scheduler', - orm_to_model=lambda node: cast(CalcJobNode, node).get_detailed_job_info(), + orm_to_model=lambda node, _: cast(CalcJobNode, node).get_detailed_job_info(), ) retrieve_list: Optional[Sequence[Union[str, Tuple[str, str, int]]]] = MetadataField( None, description='The list of files to retrieve from the remote cluster', - orm_to_model=lambda node: cast(CalcJobNode, node).get_retrieve_list(), + orm_to_model=lambda node, _: cast(CalcJobNode, node).get_retrieve_list(), ) retrieve_temporary_list: Optional[Sequence[Union[str, Tuple[str, str, int]]]] = MetadataField( None, description='The list of temporary files to retrieve from the remote cluster', - orm_to_model=lambda node: cast(CalcJobNode, node).get_retrieve_temporary_list(), + orm_to_model=lambda node, _: cast(CalcJobNode, node).get_retrieve_temporary_list(), ) imported: Optional[bool] = MetadataField( None, description='Whether the node has been migrated', - orm_to_model=lambda node: cast(CalcJobNode, node).is_imported, + orm_to_model=lambda node, _: cast(CalcJobNode, node).is_imported, ) # An optional entry point for a CalculationTools instance From 0285a39ed37c0d7442ea78e9cf0db53ec57ee5a9 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Wed, 12 Nov 2025 08:48:34 +0100 Subject: [PATCH 52/59] Discard redundant class method in favor of correct model selection --- src/aiida/orm/entities.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/aiida/orm/entities.py b/src/aiida/orm/entities.py index 2065759f5f..b36fec857f 100644 --- a/src/aiida/orm/entities.py +++ b/src/aiida/orm/entities.py @@ -18,7 +18,6 @@ from plumpy.base.utils import call_with_super_check, super_check from pydantic import BaseModel, ConfigDict, create_model -from pydantic.fields import FieldInfo from typing_extensions import Self from aiida.common import exceptions, log @@ -262,19 +261,13 @@ def CreateModel(cls) -> Type[Model]: # noqa: N802, N805 """ return cls.Model.as_create_model() - @classmethod - def model_to_orm_fields(cls) -> dict[str, FieldInfo]: - return { - key: field for key, field in cls.Model.model_fields.items() if not get_metadata(field, 'exclude_to_orm') - } - @classmethod def model_to_orm_field_values(cls, model: Model) -> dict[str, Any]: from aiida.plugins.factories import BaseFactory fields = {} - for key, field in cls.model_to_orm_fields().items(): + for key, field in cls.CreateModel.model_fields.items(): field_value = getattr(model, key) if field_value is None: From bc8a5d32fafe84e9dbdba6581bd48ce61bf4bf75 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Wed, 12 Nov 2025 09:35:36 +0100 Subject: [PATCH 53/59] Use `is_stored` for model selection --- src/aiida/orm/entities.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/aiida/orm/entities.py b/src/aiida/orm/entities.py index b36fec857f..07bed245de 100644 --- a/src/aiida/orm/entities.py +++ b/src/aiida/orm/entities.py @@ -311,7 +311,7 @@ def orm_to_model_field_values( """ fields: dict[str, Any] = {} - Model = self.Model if self.pk else self.CreateModel # noqa: N806 + Model = self.Model if self.is_stored else self.CreateModel # noqa: N806 for key, field in Model.model_fields.items(): if (skip_read_only and get_metadata(field, 'exclude_to_orm')) or key == 'extras': @@ -346,7 +346,7 @@ def to_model( repository_path=repository_path, serialize_repository_content=serialize_repository_content, ) - Model = self.Model if self.pk else self.CreateModel # noqa: N806 + Model = self.Model if self.is_stored else self.CreateModel # noqa: N806 return Model(**fields) @classmethod From 435d423c97ef2bd9d6cde88e2326969535f5b0b8 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Wed, 19 Nov 2025 11:35:01 +0100 Subject: [PATCH 54/59] Restore serialization of node `extras` --- src/aiida/cmdline/commands/cmd_code.py | 6 +++--- src/aiida/cmdline/groups/dynamic.py | 9 ++++----- src/aiida/orm/entities.py | 2 +- src/aiida/orm/nodes/data/code/abstract.py | 3 +++ 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/aiida/cmdline/commands/cmd_code.py b/src/aiida/cmdline/commands/cmd_code.py index bc58b09fc2..6c80d3e08d 100644 --- a/src/aiida/cmdline/commands/cmd_code.py +++ b/src/aiida/cmdline/commands/cmd_code.py @@ -232,7 +232,6 @@ def code_duplicate(ctx, code, non_interactive, **kwargs): def show(code): """Display detailed information for a code.""" from aiida.cmdline import is_verbose - from aiida.common.pydantic import get_metadata table = [] @@ -241,8 +240,9 @@ def show(code): table.append(['UUID', code.uuid]) table.append(['Type', code.entry_point.name]) - for field_name, field_info in code.Model.model_fields.items(): - if get_metadata(field_info, key='exclude_to_orm') or field_name == 'extras': + for field_name, field_info in code.CreateModel.model_fields.items(): + # We don't show extras for codes + if field_name == 'extras': continue # FIXME resolve this hardcoded special case properly diff --git a/src/aiida/cmdline/groups/dynamic.py b/src/aiida/cmdline/groups/dynamic.py index 490218e9ac..7b4a7839cf 100644 --- a/src/aiida/cmdline/groups/dynamic.py +++ b/src/aiida/cmdline/groups/dynamic.py @@ -97,7 +97,7 @@ def call_command(self, ctx: click.Context, cls: t.Any, non_interactive: bool, ** if hasattr(cls, 'Model'): # The plugin defines a pydantic model: use it to validate the provided arguments - Model = cls.CreateModel if hasattr(cls, 'CreateModel') else cls.Model # noqa: N806 + Model = getattr(cls, 'CreateModel', cls.Model) # noqa: N806 try: Model(**kwargs) except ValidationError as exception: @@ -154,8 +154,6 @@ def list_options(self, entry_point: str) -> list[t.Callable[[FC], FC]]: """ from pydantic_core import PydanticUndefined - from aiida.common.pydantic import get_metadata - cls = self.factory(entry_point) if not hasattr(cls, 'Model'): @@ -169,12 +167,13 @@ def list_options(self, entry_point: str) -> list[t.Callable[[FC], FC]]: options_spec = self.factory(entry_point).get_cli_options() # type: ignore[union-attr] return [self.create_option(*item) for item in options_spec] - Model = cls.CreateModel if hasattr(cls, 'CreateModel') else cls.Model # noqa: N806 + Model = getattr(cls, 'CreateModel', cls.Model) # noqa: N806 options_spec = {} for key, field_info in Model.model_fields.items(): - if get_metadata(field_info, 'exclude_to_orm') or key == 'extras': + # We do not prompt for extras + if key == 'extras': continue default = field_info.default_factory if field_info.default is PydanticUndefined else field_info.default diff --git a/src/aiida/orm/entities.py b/src/aiida/orm/entities.py index 07bed245de..adfa0ab35c 100644 --- a/src/aiida/orm/entities.py +++ b/src/aiida/orm/entities.py @@ -314,7 +314,7 @@ def orm_to_model_field_values( Model = self.Model if self.is_stored else self.CreateModel # noqa: N806 for key, field in Model.model_fields.items(): - if (skip_read_only and get_metadata(field, 'exclude_to_orm')) or key == 'extras': + if skip_read_only and get_metadata(field, 'exclude_to_orm'): continue if orm_to_model := get_metadata(field, 'orm_to_model'): diff --git a/src/aiida/orm/nodes/data/code/abstract.py b/src/aiida/orm/nodes/data/code/abstract.py index f5ca8068e5..97e504f586 100644 --- a/src/aiida/orm/nodes/data/code/abstract.py +++ b/src/aiida/orm/nodes/data/code/abstract.py @@ -374,6 +374,9 @@ def _prepare_yaml(self, *args, **kwargs): skip_read_only=True, ) + # Extras are not read-only, but we do not want to export them + code_data.pop('extras', None) + # If the attribute is not set, for example ``with_mpi`` do not export it # so that there are no null-values in the resulting YAML file return yaml.dump(code_data, sort_keys=kwargs.get('sort', False), encoding='utf-8'), {} From 94cad38b4101ee68adb67756eb9971f58ea6c2b8 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Wed, 19 Nov 2025 14:39:25 +0100 Subject: [PATCH 55/59] Avoid using JSON Schema fields in `CreateModel` construction --- src/aiida/orm/entities.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/aiida/orm/entities.py b/src/aiida/orm/entities.py index adfa0ab35c..e0f8879c80 100644 --- a/src/aiida/orm/entities.py +++ b/src/aiida/orm/entities.py @@ -225,11 +225,7 @@ def as_create_model(cls: Type[EntityModelType]) -> Type[EntityModelType]: # Identify read-only fields readonly_fields = [ - name - for name, field in CreateModel.model_fields.items() - if hasattr(field, 'json_schema_extra') - and isinstance(field.json_schema_extra, dict) - and field.json_schema_extra.get('readOnly') + name for name, field in CreateModel.model_fields.items() if get_metadata(field, 'exclude_to_orm') ] # Remove read-only fields From dfc573436cd8245dfb91bc9e093d841c744574df Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Wed, 19 Nov 2025 14:40:09 +0100 Subject: [PATCH 56/59] Fix `readOnly` logic in `MetadataField` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thanks @mikibonacci for the assist 🙏 --- src/aiida/common/pydantic.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/aiida/common/pydantic.py b/src/aiida/common/pydantic.py index 942a74115b..1e5968e67e 100644 --- a/src/aiida/common/pydantic.py +++ b/src/aiida/common/pydantic.py @@ -76,6 +76,11 @@ class Model(BaseModel): :param is_attribute: Whether the field is stored as an attribute. Used by `QbFields`. :param is_subscriptable: Whether the field can be indexed like a list or dictionary. Used by `QbFields`. """ + if exclude_to_orm: + extra = kwargs.pop('json_schema_extra', {}) + extra.update({'readOnly': True}) + kwargs['json_schema_extra'] = extra + field_info = Field(default, **kwargs) for key, value in ( @@ -92,9 +97,4 @@ class Model(BaseModel): if value is not None: field_info.metadata.append({key: value}) - if exclude_to_orm: - extra = getattr(field_info, 'json_schema_extra', None) or {} - extra.update({'readOnly': True}) - field_info.json_schema_extra = extra - return field_info From c3f6c555841ebb7917d05add64a8dc815bbf7384 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Sun, 30 Nov 2025 12:31:52 +0100 Subject: [PATCH 57/59] Cleanup `CreateModel` definition --- src/aiida/orm/entities.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/aiida/orm/entities.py b/src/aiida/orm/entities.py index e0f8879c80..2c7eddc39d 100644 --- a/src/aiida/orm/entities.py +++ b/src/aiida/orm/entities.py @@ -217,10 +217,9 @@ def as_create_model(cls: Type[EntityModelType]) -> Type[EntityModelType]: CreateModel = create_model( # noqa: N806 new_name, __base__=cls, - __doc__=f'Creation version of {cls.__name__}.', + __module__=cls.__module__, ) CreateModel.__qualname__ = new_name - CreateModel.__module__ = cls.__module__ CreateModel.model_config['extra'] = 'ignore' # Identify read-only fields From ce17b7b6212a4ba7cf1e968037249373de000950 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Sun, 30 Nov 2025 12:32:27 +0100 Subject: [PATCH 58/59] Add serialization for `JsonSerializableProtocol` model field type --- src/aiida/orm/nodes/data/jsonable.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/aiida/orm/nodes/data/jsonable.py b/src/aiida/orm/nodes/data/jsonable.py index 24309abd72..d059fa936f 100644 --- a/src/aiida/orm/nodes/data/jsonable.py +++ b/src/aiida/orm/nodes/data/jsonable.py @@ -4,7 +4,7 @@ import json import typing -from pydantic import ConfigDict +from pydantic import ConfigDict, WithJsonSchema from aiida.common.pydantic import MetadataField @@ -52,7 +52,16 @@ class JsonableData(Data): class Model(Data.Model): model_config = ConfigDict(arbitrary_types_allowed=True) - obj: JsonSerializableProtocol = MetadataField(description='The JSON-serializable object.') + obj: typing.Annotated[ + JsonSerializableProtocol, + WithJsonSchema( + { + 'type': 'object', + 'title': 'JSON-serializable object', + 'description': 'The JSON-serializable object.', + } + ), + ] = MetadataField(description='The JSON-serializable object.') def __init__(self, obj: JsonSerializableProtocol, *args, **kwargs): """Construct the node for the to be wrapped object.""" From 440e49a4d041df4d6d1bae871323c8b08a5ae1a9 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Sun, 30 Nov 2025 12:50:02 +0100 Subject: [PATCH 59/59] Refactor entity models out of entity classes --- docs/source/nitpick-exceptions | 23 +- pyproject.toml | 4 +- src/aiida/common/pydantic.py | 4 +- src/aiida/orm/__init__.py | 2 +- src/aiida/orm/authinfos.py | 66 +++--- src/aiida/orm/comments.py | 73 +++--- src/aiida/orm/computers.py | 73 +++--- src/aiida/orm/entities.py | 167 ++++++++------ src/aiida/orm/fields.py | 4 +- src/aiida/orm/groups.py | 87 +++---- src/aiida/orm/logs.py | 71 +++--- src/aiida/orm/nodes/__init__.py | 1 + src/aiida/orm/nodes/data/array/array.py | 57 ++--- src/aiida/orm/nodes/data/array/bands.py | 31 +-- src/aiida/orm/nodes/data/array/kpoints.py | 91 ++++---- src/aiida/orm/nodes/data/array/trajectory.py | 38 +-- src/aiida/orm/nodes/data/base.py | 19 +- src/aiida/orm/nodes/data/bool.py | 20 +- src/aiida/orm/nodes/data/cif.py | 43 ++-- src/aiida/orm/nodes/data/code/abstract.py | 123 +++++----- .../orm/nodes/data/code/containerized.py | 57 ++--- src/aiida/orm/nodes/data/code/installed.py | 81 +++---- src/aiida/orm/nodes/data/code/legacy.py | 37 +-- src/aiida/orm/nodes/data/code/portable.py | 53 +++-- src/aiida/orm/nodes/data/data.py | 21 +- src/aiida/orm/nodes/data/dict.py | 19 +- src/aiida/orm/nodes/data/enum.py | 19 +- src/aiida/orm/nodes/data/float.py | 19 +- src/aiida/orm/nodes/data/int.py | 19 +- src/aiida/orm/nodes/data/jsonable.py | 32 +-- src/aiida/orm/nodes/data/list.py | 17 +- src/aiida/orm/nodes/data/numeric.py | 11 +- src/aiida/orm/nodes/data/remote/base.py | 25 +- src/aiida/orm/nodes/data/remote/stash/base.py | 13 +- .../orm/nodes/data/remote/stash/compress.py | 29 +-- .../orm/nodes/data/remote/stash/custom.py | 15 +- .../orm/nodes/data/remote/stash/folder.py | 15 +- src/aiida/orm/nodes/data/singlefile.py | 59 ++--- src/aiida/orm/nodes/data/str.py | 20 +- src/aiida/orm/nodes/data/structure.py | 55 +++-- src/aiida/orm/nodes/node.py | 216 +++++++++--------- .../orm/nodes/process/calculation/calcjob.py | 111 ++++----- .../nodes/process/calculation/calculation.py | 10 +- src/aiida/orm/nodes/process/process.py | 73 +++--- src/aiida/orm/users.py | 49 ++-- src/aiida/orm/utils/mixins.py | 9 +- src/aiida/tools/graph/age_entities.py | 2 +- tests/orm/test_fields.py | 18 +- 48 files changed, 1120 insertions(+), 981 deletions(-) diff --git a/docs/source/nitpick-exceptions b/docs/source/nitpick-exceptions index 588a44d82a..5b1f3e279f 100644 --- a/docs/source/nitpick-exceptions +++ b/docs/source/nitpick-exceptions @@ -33,8 +33,9 @@ py:meth tempfile.TemporaryDirectory ### AiiDA py:class ReturnType py:class SelfType -py:class CollectionType +py:class EntityCollectionType py:class EntityType +py:class EntityModelType py:class EntityClsType py:class ProjectType py:class BackendEntityType @@ -51,8 +52,9 @@ py:class UserDefinedType py:class LinkAnnotateType py:obj ReturnType py:obj SelfType -py:obj CollectionType +py:obj EntityCollectionType py:obj EntityType +py:obj EntityModelType py:obj BackendEntityType py:obj BackendNodeType py:obj TransactionType @@ -62,23 +64,19 @@ py:class aiida.common.ReturnType py:class aiida.common.SelfType py:class aiida.common.lang.MethodType py:class aiida.cmdline.params._shims.C -py:class aiida.orm.entitites.CollectionType -py:class aiida.orm.entitites.EntityType -py:class aiida.orm.entitites.BackendEntityType py:class aiida.orm.groups.SelfType -py:class aiida.orm.implementation.entitites.EntityType py:class aiida.engine.processes.functions.FunctionType py:class aiida.engine.processes.workchains.workchain.MethodType py:class aiida.orm.entities.EntityInputModel -py:class aiida.orm.entities.EntityModelType py:class aiida.orm.entities.EntityType +py:class aiida.orm.entities.EntityModelType py:class aiida.orm.entities.BackendEntityType -py:class aiida.orm.entities.CollectionType +py:class aiida.orm.entities.EntityCollectionType py:class aiida.orm.implementation.nodes.BackendNodeType py:class aiida.orm.implementation.storage_backend.TransactionType py:class aiida.orm.implementation.entities.EntityType py:class aiida.orm.nodes.data.enum.EnumType -py:class aiida.orm.nodes.NodeType +py:class aiida.orm.nodes.node.NodeType py:class aiida.storage.psql_dos.orm.ModelType py:class aiida.storage.psql_dos.orm.SelfType py:class aiida.storage.psql_dos.orm.entities.SelfType @@ -91,18 +89,15 @@ py:class aiida.common.lang.ReturnType py:obj aiida.common.ReturnType py:obj aiida.common.SelfType py:obj aiida.common.lang.ReturnType -py:obj aiida.orm.entitites.CollectionType -py:obj aiida.orm.entitites.EntityType -py:obj aiida.orm.entitites.BackendEntityType py:obj aiida.orm.groups.SelfType -py:obj aiida.orm.entities.CollectionType +py:obj aiida.orm.entities.EntityCollectionType py:obj aiida.orm.entities.BackendEntityType py:obj aiida.orm.entities.EntityType +py:obj aiida.orm.entities.EntityModelType py:obj aiida.orm.implementation.entities.EntityType py:obj aiida.orm.implementation.nodes.BackendNodeType py:obj aiida.orm.implementation.storage_backend.TransactionType py:obj aiida.orm.nodes.node.NodeType -py:obj aiida.orm.nodes.NodeType py:obj aiida.storage.psql_dos.orm.ModelType py:obj aiida.storage.psql_dos.orm.SelfType py:obj aiida.storage.psql_dos.orm.entities.ModelType diff --git a/pyproject.toml b/pyproject.toml index 4a73d7c15c..31ace7b176 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -331,7 +331,7 @@ enable_error_code = [ 'truthy-bool' ] plugins = [ - 'pydantic.mypy', + # 'pydantic.mypy', 'sqlalchemy.ext.mypy.plugin' ] scripts_are_modules = true @@ -480,7 +480,7 @@ select = [ # Mark some classes as generic, per https://docs.astral.sh/ruff/settings/#lint_pyflakes_extend-generics # Needed due to https://github.com/astral-sh/ruff/issues/9298 [tool.ruff.lint.pyflakes] -extend-generics = ["aiida.orm.entities.Entity", "aiida.orm.entities.Collection"] +extend-generics = ["aiida.orm.entities.Entity", "aiida.orm.entities.EntityCollection", "aiida.orm.entities.EntityModel"] [tool.tox] legacy_tox_ini = """ diff --git a/src/aiida/common/pydantic.py b/src/aiida/common/pydantic.py index 1e5968e67e..3646be6ecb 100644 --- a/src/aiida/common/pydantic.py +++ b/src/aiida/common/pydantic.py @@ -33,8 +33,8 @@ def MetadataField( # noqa: N802 priority: int = 0, short_name: str | None = None, option_cls: t.Any | None = None, - orm_class: type[Entity[t.Any, t.Any]] | str | None = None, - orm_to_model: t.Callable[[Entity[t.Any, t.Any], dict[str, t.Any]], t.Any] | None = None, + orm_class: type[Entity[t.Any, t.Any, t.Any]] | str | None = None, + orm_to_model: t.Callable[[Entity[t.Any, t.Any, t.Any], dict[str, t.Any]], t.Any] | None = None, model_to_orm: t.Callable[[BaseModel], t.Any] | None = None, exclude_to_orm: bool = False, is_attribute: bool = True, diff --git a/src/aiida/orm/__init__.py b/src/aiida/orm/__init__.py index ab98581c0a..b8dd837497 100644 --- a/src/aiida/orm/__init__.py +++ b/src/aiida/orm/__init__.py @@ -45,7 +45,6 @@ 'CifData', 'Code', 'CodeEntityLoader', - 'Collection', 'Comment', 'Computer', 'ComputerEntityLoader', @@ -53,6 +52,7 @@ 'Data', 'Dict', 'Entity', + 'EntityCollection', 'EntityExtras', 'EntityTypes', 'EnumData', diff --git a/src/aiida/orm/authinfos.py b/src/aiida/orm/authinfos.py index fd07af1d3f..eba29dd850 100644 --- a/src/aiida/orm/authinfos.py +++ b/src/aiida/orm/authinfos.py @@ -29,7 +29,7 @@ __all__ = ('AuthInfo',) -class AuthInfoCollection(entities.Collection['AuthInfo']): +class AuthInfoCollection(entities.EntityCollection['AuthInfo']): """The collection of `AuthInfo` entries.""" @staticmethod @@ -44,40 +44,44 @@ def delete(self, pk: int) -> None: self._backend.authinfos.delete(pk) -class AuthInfo(entities.Entity['BackendAuthInfo', AuthInfoCollection]): +class AuthInfoModel(entities.EntityModel): + computer: int = MetadataField( + description='The PK of the computer', + is_attribute=False, + orm_class=Computer, + orm_to_model=lambda auth_info, _: cast(AuthInfo, auth_info).computer.pk, + ) + user: int = MetadataField( + description='The PK of the user', + is_attribute=False, + orm_class=User, + orm_to_model=lambda auth_info, _: cast(AuthInfo, auth_info).user.pk, + ) + enabled: bool = MetadataField( + True, + description='Whether the instance is enabled', + is_attribute=False, + ) + auth_params: Dict[str, Any] = MetadataField( + default_factory=dict, + description='Dictionary of authentication parameters', + is_attribute=False, + ) + metadata: Dict[str, Any] = MetadataField( + default_factory=dict, + description='Dictionary of metadata', + is_attribute=False, + ) + + +class AuthInfo(entities.Entity['BackendAuthInfo', AuthInfoCollection, AuthInfoModel]): """ORM class that models the authorization information that allows a `User` to connect to a `Computer`.""" + Model = AuthInfoModel + _CLS_COLLECTION = AuthInfoCollection - PROPERTY_WORKDIR = 'workdir' - class Model(entities.Entity.Model): - computer: int = MetadataField( - description='The PK of the computer', - is_attribute=False, - orm_class=Computer, - orm_to_model=lambda auth_info, _: cast(AuthInfo, auth_info).computer.pk, - ) - user: int = MetadataField( - description='The PK of the user', - is_attribute=False, - orm_class=User, - orm_to_model=lambda auth_info, _: cast(AuthInfo, auth_info).user.pk, - ) - enabled: bool = MetadataField( - True, - description='Whether the instance is enabled', - is_attribute=False, - ) - auth_params: Dict[str, Any] = MetadataField( - default_factory=dict, - description='Dictionary of authentication parameters', - is_attribute=False, - ) - metadata: Dict[str, Any] = MetadataField( - default_factory=dict, - description='Dictionary of metadata', - is_attribute=False, - ) + PROPERTY_WORKDIR = 'workdir' def __init__( self, diff --git a/src/aiida/orm/comments.py b/src/aiida/orm/comments.py index 699bb8859e..3311c7367c 100644 --- a/src/aiida/orm/comments.py +++ b/src/aiida/orm/comments.py @@ -28,7 +28,7 @@ __all__ = ('Comment',) -class CommentCollection(entities.Collection['Comment']): +class CommentCollection(entities.EntityCollection['Comment']): """The collection of Comment entries.""" @staticmethod @@ -65,43 +65,46 @@ def delete_many(self, filters: dict) -> List[int]: return self._backend.comments.delete_many(filters) -class Comment(entities.Entity['BackendComment', CommentCollection]): +class CommentModel(entities.EntityModel): + uuid: UUID = MetadataField( + description='The UUID of the comment', + is_attribute=False, + exclude_to_orm=True, + ) + ctime: datetime = MetadataField( + description='Creation time of the comment', + is_attribute=False, + exclude_to_orm=True, + ) + mtime: datetime = MetadataField( + description='Modified time of the comment', + is_attribute=False, + exclude_to_orm=True, + ) + node: int = MetadataField( + description='Node PK that the comment is attached to', + is_attribute=False, + orm_class='core.node', + orm_to_model=lambda comment, _: cast(Comment, comment).node.pk, + ) + user: int = MetadataField( + description='User PK that created the comment', + is_attribute=False, + orm_class='core.user', + orm_to_model=lambda comment, _: cast(Comment, comment).user.pk, + ) + content: str = MetadataField( + description='Content of the comment', + is_attribute=False, + ) + + +class Comment(entities.Entity['BackendComment', CommentCollection, CommentModel]): """Base class to map a DbComment that represents a comment attached to a certain Node.""" - _CLS_COLLECTION = CommentCollection + Model = CommentModel - class Model(entities.Entity.Model): - uuid: UUID = MetadataField( - description='The UUID of the comment', - is_attribute=False, - exclude_to_orm=True, - ) - ctime: datetime = MetadataField( - description='Creation time of the comment', - is_attribute=False, - exclude_to_orm=True, - ) - mtime: datetime = MetadataField( - description='Modified time of the comment', - is_attribute=False, - exclude_to_orm=True, - ) - node: int = MetadataField( - description='Node PK that the comment is attached to', - is_attribute=False, - orm_class='core.node', - orm_to_model=lambda comment, _: cast(Comment, comment).node.pk, - ) - user: int = MetadataField( - description='User PK that created the comment', - is_attribute=False, - orm_class='core.user', - orm_to_model=lambda comment, _: cast(Comment, comment).user.pk, - ) - content: str = MetadataField( - description='Content of the comment', - is_attribute=False, - ) + _CLS_COLLECTION = CommentCollection def __init__( self, node: 'Node', user: 'User', content: Optional[str] = None, backend: Optional['StorageBackend'] = None diff --git a/src/aiida/orm/computers.py b/src/aiida/orm/computers.py index 6df4b1d9b3..6e9674ba3e 100644 --- a/src/aiida/orm/computers.py +++ b/src/aiida/orm/computers.py @@ -31,7 +31,7 @@ __all__ = ('Computer',) -class ComputerCollection(entities.Collection['Computer']): +class ComputerCollection(entities.EntityCollection['Computer']): """The collection of Computer entries.""" @staticmethod @@ -64,9 +64,45 @@ def delete(self, pk: int) -> None: return self._backend.computers.delete(pk) -class Computer(entities.Entity['BackendComputer', ComputerCollection]): +class ComputerModel(entities.EntityModel): + uuid: UUID = MetadataField( + description='The UUID of the computer', + is_attribute=False, + exclude_to_orm=True, + ) + label: str = MetadataField( + description='Label for the computer', + is_attribute=False, + ) + description: str = MetadataField( + '', + description='Description of the computer', + is_attribute=False, + ) + hostname: str = MetadataField( + description='Hostname of the computer', + is_attribute=False, + ) + transport_type: str = MetadataField( + description='Transport type of the computer', + is_attribute=False, + ) + scheduler_type: str = MetadataField( + description='Scheduler type of the computer', + is_attribute=False, + ) + metadata: Dict[str, Any] = MetadataField( + default_factory=dict, + description='Metadata of the computer', + is_attribute=False, + ) + + +class Computer(entities.Entity['BackendComputer', ComputerCollection, ComputerModel]): """Computer entity.""" + Model = ComputerModel + _logger = AIIDA_LOGGER.getChild('orm.computers') PROPERTY_MINIMUM_SCHEDULER_POLL_INTERVAL = 'minimum_scheduler_poll_interval' @@ -76,39 +112,6 @@ class Computer(entities.Entity['BackendComputer', ComputerCollection]): _CLS_COLLECTION = ComputerCollection - class Model(entities.Entity.Model): - uuid: UUID = MetadataField( - description='The UUID of the computer', - is_attribute=False, - exclude_to_orm=True, - ) - label: str = MetadataField( - description='Label for the computer', - is_attribute=False, - ) - description: str = MetadataField( - '', - description='Description of the computer', - is_attribute=False, - ) - hostname: str = MetadataField( - description='Hostname of the computer', - is_attribute=False, - ) - transport_type: str = MetadataField( - description='Transport type of the computer', - is_attribute=False, - ) - scheduler_type: str = MetadataField( - description='Scheduler type of the computer', - is_attribute=False, - ) - metadata: Dict[str, Any] = MetadataField( - default_factory=dict, - description='Metadata of the computer', - is_attribute=False, - ) - def __init__( self, label: Optional[str] = None, diff --git a/src/aiida/orm/entities.py b/src/aiida/orm/entities.py index 2c7eddc39d..230b884785 100644 --- a/src/aiida/orm/entities.py +++ b/src/aiida/orm/entities.py @@ -18,7 +18,7 @@ from plumpy.base.utils import call_with_super_check, super_check from pydantic import BaseModel, ConfigDict, create_model -from typing_extensions import Self +from typing_extensions import Self, overload from aiida.common import exceptions, log from aiida.common.exceptions import EntryPointError, InvalidOperation, NotExistent @@ -33,12 +33,12 @@ from aiida.orm.implementation import BackendEntity, StorageBackend from aiida.orm.querybuilder import FilterType, OrderByType, QueryBuilder -__all__ = ('Collection', 'Entity', 'EntityTypes') +__all__ = ('Entity', 'EntityCollection', 'EntityTypes') -CollectionType = TypeVar('CollectionType', bound='Collection[Any]') -EntityType = TypeVar('EntityType', bound='Entity[Any,Any]') -EntityModelType = TypeVar('EntityModelType', bound=BaseModel) BackendEntityType = TypeVar('BackendEntityType', bound='BackendEntity') +EntityCollectionType = TypeVar('EntityCollectionType', bound='EntityCollection[Any]') +EntityModelType = TypeVar('EntityModelType', bound='EntityModel') +EntityType = TypeVar('EntityType', bound='Entity[Any,Any,Any]') class EntityTypes(Enum): @@ -55,7 +55,7 @@ class EntityTypes(Enum): GROUP_NODE = 'group_node' -class Collection(abc.ABC, Generic[EntityType]): +class EntityCollection(abc.ABC, Generic[EntityType]): """Container class that represents the collection of objects of a particular entity type.""" @staticmethod @@ -178,78 +178,71 @@ def count(self, filters: Optional['FilterType'] = None) -> int: return self.query(filters=filters).count() -class Entity(abc.ABC, Generic[BackendEntityType, CollectionType], metaclass=EntityFieldMeta): - """An AiiDA entity""" +class EntityModel(BaseModel, defer_build=True): + model_config = ConfigDict(extra='forbid') - _CLS_COLLECTION: Type[CollectionType] = Collection # type: ignore[assignment] - _logger = log.AIIDA_LOGGER.getChild('orm.entities') + pk: int = MetadataField( + description='The primary key of the entity', + is_attribute=False, + exclude_to_orm=True, + ) - class Model(BaseModel, defer_build=True): - model_config = ConfigDict(extra='forbid') + @classmethod + def as_create_model(cls: type[EntityModel]) -> type[BaseModel]: + """Return a derived creation model class with read-only fields removed. - pk: int = MetadataField( - description='The primary key of the entity', - is_attribute=False, - exclude_to_orm=True, + This also removes any serializers/validators defined on those fields. + + :return: The derived creation model class. + """ + + # Derive the creation model from the original model + new_name = cls.__qualname__.replace('.Model', 'CreateModel') + CreateModel = create_model( # noqa: N806 + new_name, + __base__=cls, + __module__=cls.__module__, ) + CreateModel.__qualname__ = new_name + CreateModel.model_config['extra'] = 'ignore' - @classmethod - def __pydantic_init_subclass__(cls, **kwargs: Any) -> None: - """Sets the JSON schema title of the model. - - The qualified name of the class is used, with dots removed. For example, `Node.Model` becomes `NodeModel` - in the JSON schema. - """ - super().__pydantic_init_subclass__(**kwargs) - cls.model_config['title'] = cls.__qualname__.replace('.', '') - - @classmethod - def as_create_model(cls: Type[EntityModelType]) -> Type[EntityModelType]: - """Return a derived creation model class with read-only fields removed. - - This also removes any serializers/validators defined on those fields. - - :return: The derived creation model class. - """ - - # Derive the creation model from the original model - new_name = cls.__qualname__.replace('.Model', 'CreateModel') - CreateModel = create_model( # noqa: N806 - new_name, - __base__=cls, - __module__=cls.__module__, - ) - CreateModel.__qualname__ = new_name - CreateModel.model_config['extra'] = 'ignore' - - # Identify read-only fields - readonly_fields = [ - name for name, field in CreateModel.model_fields.items() if get_metadata(field, 'exclude_to_orm') - ] - - # Remove read-only fields - for name in readonly_fields: - CreateModel.model_fields.pop(name, None) - if hasattr(CreateModel, name): - delattr(CreateModel, name) - - # Prune field validators/serializers referring to read-only fields - decorators = CreateModel.__pydantic_decorators__ - - def _prune_field_decorators(field_decorators: dict[str, Any]) -> dict[str, Any]: - return { - name: decorator - for name, decorator in field_decorators.items() - if all(field not in readonly_fields for field in decorator.info.fields) - } + # Identify read-only fields + readonly_fields = [ + name for name, field in CreateModel.model_fields.items() if get_metadata(field, 'exclude_to_orm') + ] + + # Remove read-only fields + for name in readonly_fields: + CreateModel.model_fields.pop(name, None) + if hasattr(CreateModel, name): + delattr(CreateModel, name) + + # Prune field validators/serializers referring to read-only fields + decorators = CreateModel.__pydantic_decorators__ - decorators.field_validators = _prune_field_decorators(decorators.field_validators) - decorators.field_serializers = _prune_field_decorators(decorators.field_serializers) + def _prune_field_decorators(field_decorators: dict[str, Any]) -> dict[str, Any]: + return { + name: decorator + for name, decorator in field_decorators.items() + if all(field not in readonly_fields for field in decorator.info.fields) + } - return CreateModel + decorators.field_validators = _prune_field_decorators(decorators.field_validators) + decorators.field_serializers = _prune_field_decorators(decorators.field_serializers) + + return CreateModel + + +class Entity(abc.ABC, Generic[BackendEntityType, EntityCollectionType, EntityModelType], metaclass=EntityFieldMeta): + """An AiiDA entity""" + + Model: type[EntityModelType] = EntityModel # type: ignore[assignment] + + _CLS_COLLECTION: type[EntityCollectionType] = EntityCollection # type: ignore[assignment] + _logger = log.AIIDA_LOGGER.getChild('orm.entities') @classproperty - def CreateModel(cls) -> Type[Model]: # noqa: N802, N805 + def CreateModel(cls) -> type[BaseModel]: # noqa: N802, N805 """Return the creation version of the model class for this entity. :return: The creation model class, with read-only fields removed. @@ -257,7 +250,7 @@ def CreateModel(cls) -> Type[Model]: # noqa: N802, N805 return cls.Model.as_create_model() @classmethod - def model_to_orm_field_values(cls, model: Model) -> dict[str, Any]: + def model_to_orm_field_values(cls, model: BaseModel) -> dict[str, Any]: from aiida.plugins.factories import BaseFactory fields = {} @@ -323,12 +316,36 @@ def orm_to_model_field_values( return fields + @overload + def to_model( + self, + *, + repository_path: Optional[pathlib.Path] = None, + serialize_repository_content: bool = False, + ) -> EntityModelType: ... + + @overload + def to_model( + self, + *, + repository_path: Optional[pathlib.Path] = None, + serialize_repository_content: bool = False, + ) -> BaseModel: ... + + @overload + def to_model( + self, + *, + repository_path: Optional[pathlib.Path] = None, + serialize_repository_content: bool = False, + ) -> BaseModel: ... + def to_model( self, *, repository_path: Optional[pathlib.Path] = None, serialize_repository_content: bool = False, - ) -> Model: + ) -> BaseModel: """Return the entity instance as an instance of its model. :param repository_path: If the orm node has files in the repository, this path is used to read the repository @@ -345,7 +362,7 @@ def to_model( return Model(**fields) @classmethod - def from_model(cls, model: Model) -> Self: + def from_model(cls, model: BaseModel) -> Self: """Return an entity instance from an instance of its model. :param model: An instance of the entity's model class. @@ -403,7 +420,7 @@ def from_serialized(cls, serialized: dict[str, Any]) -> Self: return cls.from_model(cls.CreateModel(**serialized)) @classproperty - def objects(cls: EntityType) -> CollectionType: # noqa: N805 + def objects(cls: EntityType) -> EntityCollectionType: # noqa: N805 """Get a collection for objects of this type, with the default backend. .. deprecated:: This will be removed in v3, use ``collection`` instead. @@ -414,7 +431,7 @@ def objects(cls: EntityType) -> CollectionType: # noqa: N805 return cls.collection @classproperty - def collection(cls) -> CollectionType: # noqa: N805 + def collection(cls) -> EntityCollectionType: # noqa: N805 """Get a collection for objects of this type, with the default backend. :return: an object that can be used to access entities of this type @@ -422,7 +439,7 @@ def collection(cls) -> CollectionType: # noqa: N805 return cls._CLS_COLLECTION.get_cached(cls, get_manager().get_profile_storage()) @classmethod - def get_collection(cls, backend: 'StorageBackend') -> CollectionType: + def get_collection(cls, backend: 'StorageBackend') -> EntityCollectionType: """Get a collection for objects of this type for a given backend. .. note:: Use the ``collection`` class property instead if the currently loaded backend or backend of the diff --git a/src/aiida/orm/fields.py b/src/aiida/orm/fields.py index 60c6b9aab2..68619b0197 100644 --- a/src/aiida/orm/fields.py +++ b/src/aiida/orm/fields.py @@ -461,8 +461,8 @@ def __init__(cls, name, bases, classdict): if model_bases != cls_model_bases and not getattr(cls, '_SKIP_MODEL_INHERITANCE_CHECK', False): bases = [f'{e.__module__}.{e.__name__}.Model' for e in cls_bases_with_model_leaves] raise RuntimeError( - f'`{cls.__name__}.Model` does not subclass all necessary base classes. It should be: ' - f'`class Model({", ".join(sorted(bases))}):`' + f'`{cls.__name__}Model` does not subclass all necessary base classes. It should be: ' + f'`class {cls.__name__}Model({", ".join(sorted(bases))}):`' ) for key, field in cls.Model.model_fields.items(): diff --git a/src/aiida/orm/groups.py b/src/aiida/orm/groups.py index ea08bf37c3..d8bfdec63b 100644 --- a/src/aiida/orm/groups.py +++ b/src/aiida/orm/groups.py @@ -58,7 +58,7 @@ def load_group_class(type_string: str) -> Type[Group]: return group_class -class GroupCollection(entities.Collection['Group']): +class GroupCollection(entities.EntityCollection['Group']): """Collection of Groups""" @staticmethod @@ -108,50 +108,53 @@ def extras(self) -> extras.EntityExtras: return extras.EntityExtras(self._group) -class Group(entities.Entity['BackendGroup', GroupCollection]): +class GroupModel(entities.EntityModel): + uuid: UUID = MetadataField( + description='The UUID of the group', + is_attribute=False, + exclude_to_orm=True, + ) + type_string: str = MetadataField( + description='The type of the group', + is_attribute=False, + exclude_to_orm=True, + ) + user: int = MetadataField( + description='The PK of the group owner', + is_attribute=False, + orm_class='core.user', + orm_to_model=lambda group, _: cast(Group, group).user.pk, + exclude_to_orm=True, + ) + time: datetime.datetime = MetadataField( + description='The creation time of the node, defaults to now (timezone-aware)', + is_attribute=False, + exclude_to_orm=True, + ) + label: str = MetadataField( + description='The group label', + is_attribute=False, + ) + description: str = MetadataField( + '', + description='The group description', + is_attribute=False, + ) + extras: Dict[str, Any] = MetadataField( + default_factory=dict, + description='The group extras', + is_attribute=False, + is_subscriptable=True, + orm_to_model=lambda group, _: cast(Group, group).base.extras.all, + ) + + +class Group(entities.Entity['BackendGroup', GroupCollection, GroupModel]): """An AiiDA ORM implementation of group of nodes.""" - __type_string: ClassVar[Optional[str]] + Model = GroupModel - class Model(entities.Entity.Model): - uuid: UUID = MetadataField( - description='The UUID of the group', - is_attribute=False, - exclude_to_orm=True, - ) - type_string: str = MetadataField( - description='The type of the group', - is_attribute=False, - exclude_to_orm=True, - ) - user: int = MetadataField( - description='The PK of the group owner', - is_attribute=False, - orm_class='core.user', - orm_to_model=lambda group, _: cast(Group, group).user.pk, - exclude_to_orm=True, - ) - time: datetime.datetime = MetadataField( - description='The creation time of the node, defaults to now (timezone-aware)', - is_attribute=False, - exclude_to_orm=True, - ) - label: str = MetadataField( - description='The group label', - is_attribute=False, - ) - description: str = MetadataField( - '', - description='The group description', - is_attribute=False, - ) - extras: Dict[str, Any] = MetadataField( - default_factory=dict, - description='The group extras', - is_attribute=False, - is_subscriptable=True, - orm_to_model=lambda group, _: cast(Group, group).base.extras.all, - ) + __type_string: ClassVar[Optional[str]] _CLS_COLLECTION = GroupCollection diff --git a/src/aiida/orm/logs.py b/src/aiida/orm/logs.py index 1a9a09f8e2..b5e19adf48 100644 --- a/src/aiida/orm/logs.py +++ b/src/aiida/orm/logs.py @@ -37,7 +37,7 @@ def OrderSpecifier(field, direction): # noqa: N802 return {field: direction} -class LogCollection(entities.Collection['Log']): +class LogCollection(entities.EntityCollection['Log']): """This class represents the collection of logs and can be used to create and retrieve logs. """ @@ -127,42 +127,45 @@ def delete_many(self, filters: 'FilterType') -> List[int]: return self._backend.logs.delete_many(filters) -class Log(entities.Entity['BackendLog', LogCollection]): +class LogModel(entities.EntityModel): + uuid: UUID = MetadataField( + description='The UUID of the node', + is_attribute=False, + exclude_to_orm=True, + ) + loggername: str = MetadataField( + description='The name of the logger', + is_attribute=False, + ) + levelname: str = MetadataField( + description='The name of the log level', + is_attribute=False, + ) + message: str = MetadataField( + description='The message of the log', + is_attribute=False, + ) + time: datetime = MetadataField( + description='The time at which the log was created', + is_attribute=False, + ) + metadata: Dict[str, Any] = MetadataField( + default_factory=dict, + description='The metadata of the log', + is_attribute=False, + ) + dbnode_id: int = MetadataField( + description='Associated node', + is_attribute=False, + ) + + +class Log(entities.Entity['BackendLog', LogCollection, LogModel]): """An AiiDA Log entity. Corresponds to a logged message against a particular AiiDA node.""" - _CLS_COLLECTION = LogCollection + Model = LogModel - class Model(entities.Entity.Model): - uuid: UUID = MetadataField( - description='The UUID of the node', - is_attribute=False, - exclude_to_orm=True, - ) - loggername: str = MetadataField( - description='The name of the logger', - is_attribute=False, - ) - levelname: str = MetadataField( - description='The name of the log level', - is_attribute=False, - ) - message: str = MetadataField( - description='The message of the log', - is_attribute=False, - ) - time: datetime = MetadataField( - description='The time at which the log was created', - is_attribute=False, - ) - metadata: Dict[str, Any] = MetadataField( - default_factory=dict, - description='The metadata of the log', - is_attribute=False, - ) - dbnode_id: int = MetadataField( - description='Associated node', - is_attribute=False, - ) + _CLS_COLLECTION = LogCollection def __init__( self, diff --git a/src/aiida/orm/nodes/__init__.py b/src/aiida/orm/nodes/__init__.py index e8ef1da246..4a21214d3c 100644 --- a/src/aiida/orm/nodes/__init__.py +++ b/src/aiida/orm/nodes/__init__.py @@ -44,6 +44,7 @@ 'Node', 'NodeAttributes', 'NodeRepository', + 'NodeType', 'NumericType', 'OrbitalData', 'PortableCode', diff --git a/src/aiida/orm/nodes/data/array/array.py b/src/aiida/orm/nodes/data/array/array.py index bcc484e317..b6cfdd7482 100644 --- a/src/aiida/orm/nodes/data/array/array.py +++ b/src/aiida/orm/nodes/data/array/array.py @@ -20,8 +20,8 @@ from aiida.common.pydantic import MetadataField +from .. import data from ..base import to_aiida_type -from ..data import Data __all__ = ('ArrayData',) @@ -31,7 +31,33 @@ def _(value): return ArrayData(value) -class ArrayData(Data): +class ArrayDataModel(data.DataModel): + arrays: Optional[dict[str, bytes]] = MetadataField( + None, + description='The dictionary of numpy arrays.', + orm_to_model=lambda node, _: ArrayData.save_arrays(cast(ArrayData, node).arrays), + model_to_orm=lambda model: ArrayData.load_arrays(cast(ArrayDataModel, model).arrays), + ) + + @field_validator('arrays', mode='before') + @classmethod + def validate_arrays(cls, value: Optional[dict[str, Union[bytes, Any]]]) -> Any: + if value is None: + return value + if not isinstance(value, dict): + raise TypeError(f'`arrays` should be a dictionary but got: {value}') + arrays: dict[str, bytes] = {} + for key, array in value.items(): + if isinstance(array, bytes): + arrays[key] = array + elif isinstance(array, Iterable): + arrays |= ArrayData.save_arrays({key: np.array(array)}) + else: + arrays[key] = array + return arrays + + +class ArrayData(data.Data): """Store a set of arrays on disk (rather than on the database) in an efficient way Arrays are stored using numpy and therefore this class requires numpy to be installed. @@ -48,32 +74,7 @@ class ArrayData(Data): """ - class Model(Data.Model): - # model_config = ConfigDict(arbitrary_types_allowed=True) - - arrays: Optional[dict[str, bytes]] = MetadataField( - None, - description='The dictionary of numpy arrays.', - orm_to_model=lambda node, _: ArrayData.save_arrays(cast(ArrayData, node).arrays), - model_to_orm=lambda model: ArrayData.load_arrays(cast(ArrayData.Model, model).arrays), - ) - - @field_validator('arrays', mode='before') - @classmethod - def validate_arrays(cls, value: Optional[dict[str, Union[bytes, Any]]]) -> Any: - if value is None: - return value - if not isinstance(value, dict): - raise TypeError(f'`arrays` should be a dictionary but got: {value}') - arrays: dict[str, bytes] = {} - for key, array in value.items(): - if isinstance(array, bytes): - arrays[key] = array - elif isinstance(array, Iterable): - arrays |= ArrayData.save_arrays({key: np.array(array)}) - else: - arrays[key] = array - return arrays + Model = ArrayDataModel array_prefix = 'array|' default_array_name = 'default' diff --git a/src/aiida/orm/nodes/data/array/bands.py b/src/aiida/orm/nodes/data/array/bands.py index 15a3ea08af..fc755be40b 100644 --- a/src/aiida/orm/nodes/data/array/bands.py +++ b/src/aiida/orm/nodes/data/array/bands.py @@ -22,7 +22,7 @@ from aiida.common.pydantic import MetadataField from aiida.common.utils import join_labels, prettify_labels -from .kpoints import KpointsData +from . import kpoints __all__ = ('BandsData', 'find_bandgap') @@ -212,19 +212,22 @@ def nint(num): return True, gap -class BandsData(KpointsData): +class BandsDataModel(kpoints.KpointsDataModel): + array_labels: t.Optional[t.List[str]] = MetadataField( + None, + description='Labels associated with the band arrays', + ) + units: t.Optional[str] = MetadataField( + None, + description='Units in which the data in bands were stored', + orm_to_model=lambda node, _: t.cast(BandsData, node).base.attributes.get('units', None), + ) + + +class BandsData(kpoints.KpointsData): """Class to handle bands data""" - class Model(KpointsData.Model): - array_labels: t.Optional[t.List[str]] = MetadataField( - None, - description='Labels associated with the band arrays', - ) - units: t.Optional[str] = MetadataField( - None, - description='Units in which the data in bands were stored', - orm_to_model=lambda node, _: t.cast(BandsData, node).base.attributes.get('units', None), - ) + Model = BandsDataModel def __init__( self, @@ -240,7 +243,7 @@ def set_kpointsdata(self, kpointsdata): """Load the kpoints from a kpoint object. :param kpointsdata: an instance of KpointsData class """ - if not isinstance(kpointsdata, KpointsData): + if not isinstance(kpointsdata, kpoints.KpointsData): raise ValueError('kpointsdata must be of the KpointsData class') try: self.cell = kpointsdata.cell @@ -323,7 +326,7 @@ def _validate_bands_occupations(self, bands, occupations=None, labels=None): the_labels = [str(_) for _ in labels] else: raise ValidationError( - 'Band labels have an unrecognized type ({})but should be a string or a list of strings'.format( + 'Band labels have an unrecognized type ({}) but should be a string or a list of strings'.format( labels.__class__ ) ) diff --git a/src/aiida/orm/nodes/data/array/kpoints.py b/src/aiida/orm/nodes/data/array/kpoints.py index 112940e367..f9081cd2fe 100644 --- a/src/aiida/orm/nodes/data/array/kpoints.py +++ b/src/aiida/orm/nodes/data/array/kpoints.py @@ -19,7 +19,7 @@ from aiida.common.pydantic import MetadataField -from .array import ArrayData +from . import array __all__ = ('KpointsData',) @@ -27,7 +27,50 @@ _DEFAULT_EPSILON_ANGLE = 1e-5 -class KpointsData(ArrayData): +class KpointsDataModel(array.ArrayDataModel): + labels: t.Optional[t.List[str]] = MetadataField( + None, + description='Labels associated with the list of kpoints', + orm_to_model=lambda node, _: t.cast(KpointsData, node).base.attributes.get('labels', None), + ) + label_numbers: t.Optional[t.List[int]] = MetadataField( + None, + description='Index of the labels in the list of kpoints', + orm_to_model=lambda node, _: t.cast(KpointsData, node).base.attributes.get('label_numbers', None), + ) + cell: t.Optional[t.List[t.List[float]]] = MetadataField( + None, + description='Unit cell of the crystal, in Angstroms', + orm_to_model=lambda node, _: t.cast(KpointsData, node).base.attributes.get('cell', None), + ) + pbc1: t.Optional[bool] = MetadataField( + None, + description='Periodicity in the first lattice vector direction', + orm_to_model=lambda node, _: t.cast(KpointsData, node).pbc[0], + ) + pbc2: t.Optional[bool] = MetadataField( + None, + description='Periodicity in the second lattice vector direction', + orm_to_model=lambda node, _: t.cast(KpointsData, node).pbc[1], + ) + pbc3: t.Optional[bool] = MetadataField( + None, + description='Periodicity in the third lattice vector direction', + orm_to_model=lambda node, _: t.cast(KpointsData, node).pbc[2], + ) + mesh: t.Optional[t.List[int]] = MetadataField( + None, + description='Mesh of kpoints', + orm_to_model=lambda node, _: t.cast(KpointsData, node).base.attributes.get('mesh', None), + ) + offset: t.Optional[t.List[float]] = MetadataField( + None, + description='Offset of kpoints', + orm_to_model=lambda node, _: t.cast(KpointsData, node).base.attributes.get('offset', None), + ) + + +class KpointsData(array.ArrayData): """Class to handle array of kpoints in the Brillouin zone. Provide methods to generate either user-defined k-points or path of k-points along symmetry lines. @@ -40,47 +83,7 @@ class KpointsData(ArrayData): set_cell_from_structure methods. """ - class Model(ArrayData.Model): - labels: t.Optional[t.List[str]] = MetadataField( - None, - description='Labels associated with the list of kpoints', - orm_to_model=lambda node, _: t.cast(KpointsData, node).base.attributes.get('labels', None), - ) - label_numbers: t.Optional[t.List[int]] = MetadataField( - None, - description='Index of the labels in the list of kpoints', - orm_to_model=lambda node, _: t.cast(KpointsData, node).base.attributes.get('label_numbers', None), - ) - cell: t.Optional[t.List[t.List[float]]] = MetadataField( - None, - description='Unit cell of the crystal, in Angstroms', - orm_to_model=lambda node, _: t.cast(KpointsData, node).base.attributes.get('cell', None), - ) - pbc1: t.Optional[bool] = MetadataField( - None, - description='Periodicity in the first lattice vector direction', - orm_to_model=lambda node, _: t.cast(KpointsData, node).pbc[0], - ) - pbc2: t.Optional[bool] = MetadataField( - None, - description='Periodicity in the second lattice vector direction', - orm_to_model=lambda node, _: t.cast(KpointsData, node).pbc[1], - ) - pbc3: t.Optional[bool] = MetadataField( - None, - description='Periodicity in the third lattice vector direction', - orm_to_model=lambda node, _: t.cast(KpointsData, node).pbc[2], - ) - mesh: t.Optional[t.List[int]] = MetadataField( - None, - description='Mesh of kpoints', - orm_to_model=lambda node, _: t.cast(KpointsData, node).base.attributes.get('mesh', None), - ) - offset: t.Optional[t.List[float]] = MetadataField( - None, - description='Offset of kpoints', - orm_to_model=lambda node, _: t.cast(KpointsData, node).base.attributes.get('offset', None), - ) + Model = KpointsDataModel def __init__( self, @@ -459,7 +462,7 @@ def _validate_kpoints_weights(self, kpoints, weights): if kpoints.shape[1] < self._dimension: raise ValueError( - 'In a system which has {0} dimensions, kpoint needmore than {0} coordinates (found instead {1})'.format( + 'In a system with {0} dimensions, kpoints need more than {0} coordinates (found instead {1})'.format( self._dimension, kpoints.shape[1] ) ) diff --git a/src/aiida/orm/nodes/data/array/trajectory.py b/src/aiida/orm/nodes/data/array/trajectory.py index ba1de93136..05c1c8be76 100644 --- a/src/aiida/orm/nodes/data/array/trajectory.py +++ b/src/aiida/orm/nodes/data/array/trajectory.py @@ -17,7 +17,10 @@ from aiida.common.pydantic import MetadataField -from .array import ArrayData +from . import array + +if TYPE_CHECKING: + from aiida import orm if TYPE_CHECKING: from aiida import orm @@ -25,23 +28,26 @@ __all__ = ('TrajectoryData',) -class TrajectoryData(ArrayData): +class TrajectoryDataModel(array.ArrayDataModel): + units_positions: Optional[str] = MetadataField( + None, + serialization_alias='units|positions', + description='Unit of positions', + ) + units_times: Optional[str] = MetadataField( + None, + serialization_alias='units|times', + description='Unit of time', + ) + symbols: List[str] = MetadataField(description='List of symbols') + + +class TrajectoryData(array.ArrayData): """Stores a trajectory (a sequence of crystal structures with timestamps, and possibly with velocities). """ - class Model(ArrayData.Model): - units_positions: Optional[str] = MetadataField( - None, - serialization_alias='units|positions', - description='Unit of positions', - ) - units_times: Optional[str] = MetadataField( - None, - serialization_alias='units|times', - description='Unit of time', - ) - symbols: List[str] = MetadataField(description='List of symbols') + Model = TrajectoryDataModel def __init__( self, @@ -104,7 +110,7 @@ def _internal_validate(self, stepids, cells, symbols, positions, times, velociti numatoms = len(symbols) if positions.shape != (numsteps, numatoms, 3): raise ValueError( - 'TrajectoryData.positions must have shape (s,n,3), ' 'with s=number of steps and n=number of symbols' + 'TrajectoryData.positions must have shape (s,n,3), with s=number of steps and n=number of symbols' ) if times is not None: if times.shape != (numsteps,): @@ -425,7 +431,7 @@ def get_step_structure(self, index, custom_kinds=None): for k in custom_kinds: if not isinstance(k, Kind): raise TypeError( - 'Each element of the custom_kinds list must ' 'be a aiida.orm.nodes.data.structure.Kind object' + 'Each element of the custom_kinds list must be a aiida.orm.nodes.data.structure.Kind object' ) kind_names.append(k.name) if len(kind_names) != len(set(kind_names)): diff --git a/src/aiida/orm/nodes/data/base.py b/src/aiida/orm/nodes/data/base.py index f697b51c93..937f6876a9 100644 --- a/src/aiida/orm/nodes/data/base.py +++ b/src/aiida/orm/nodes/data/base.py @@ -13,7 +13,7 @@ from aiida.common.pydantic import MetadataField -from .data import Data +from . import data __all__ = ('BaseType', 'to_aiida_type') @@ -24,15 +24,18 @@ def to_aiida_type(value): raise TypeError(f'Cannot convert value of type {type(value)} to AiiDA type.') -class BaseType(Data): +class BaseTypeModel(data.DataModel): + value: t.Any = MetadataField( + ..., + title='Data value', + description='The value of the data', + ) + + +class BaseType(data.Data): """`Data` sub class to be used as a base for data containers that represent base python data types.""" - class Model(Data.Model): - value: t.Any = MetadataField( - ..., - title='Data value', - description='The value of the data', - ) + Model = BaseTypeModel def __init__(self, value=None, **kwargs): try: diff --git a/src/aiida/orm/nodes/data/bool.py b/src/aiida/orm/nodes/data/bool.py index cfac4cf0fc..06a615a3e9 100644 --- a/src/aiida/orm/nodes/data/bool.py +++ b/src/aiida/orm/nodes/data/bool.py @@ -12,21 +12,25 @@ from aiida.common.pydantic import MetadataField -from .base import BaseType, to_aiida_type +from . import base +from .base import to_aiida_type __all__ = ('Bool',) -class Bool(BaseType): +class BoolModel(base.BaseTypeModel): + value: bool = MetadataField( + title='Boolean value', + description='The value of the boolean', + ) + + +class Bool(base.BaseType): """`Data` sub class to represent a boolean value.""" - _type = bool + Model = BoolModel - class Model(BaseType.Model): - value: bool = MetadataField( - title='Boolean value', - description='The value of the boolean', - ) + _type = bool def __int__(self): return int(bool(self)) diff --git a/src/aiida/orm/nodes/data/cif.py b/src/aiida/orm/nodes/data/cif.py index 12170dbbdc..4d514a8d05 100644 --- a/src/aiida/orm/nodes/data/cif.py +++ b/src/aiida/orm/nodes/data/cif.py @@ -14,7 +14,7 @@ from aiida.common.pydantic import MetadataField from aiida.common.utils import Capturing -from .singlefile import SinglefileData +from . import singlefile __all__ = ('CifData', 'cif_from_ase', 'has_pycifrw', 'pycifrw_from_cif') @@ -232,7 +232,25 @@ def chemcount_str_to_number(string): # Note: Method 'query' is abstract in class 'Node' but is not overridden -class CifData(SinglefileData): +class CifDataModel(singlefile.SinglefileDataModel): + formulae: Optional[List[str]] = MetadataField( + None, + description='List of formulae contained in the CIF file.', + exclude_to_orm=True, + ) + spacegroup_numbers: Optional[List[str]] = MetadataField( + None, + description='List of space group numbers of the structure.', + exclude_to_orm=True, + ) + md5: Optional[str] = MetadataField( + None, + description='MD5 checksum of the file contents.', + exclude_to_orm=True, + ) + + +class CifData(singlefile.SinglefileData): """Wrapper for Crystallographic Interchange File (CIF) .. note:: the file (physical) is held as the authoritative source of @@ -241,6 +259,8 @@ class CifData(SinglefileData): first, the values are updated from the physical CIF file. """ + Model = CifDataModel + _SET_INCOMPATIBILITIES = [('ase', 'file'), ('ase', 'values'), ('file', 'values')] _SCAN_TYPES = ('standard', 'flex') _SCAN_TYPE_DEFAULT = 'standard' @@ -250,23 +270,6 @@ class CifData(SinglefileData): _values = None _ase = None - class Model(SinglefileData.Model): - formulae: Optional[List[str]] = MetadataField( - None, - description='List of formulae contained in the CIF file.', - exclude_to_orm=True, - ) - spacegroup_numbers: Optional[List[str]] = MetadataField( - None, - description='List of space group numbers of the structure.', - exclude_to_orm=True, - ) - md5: Optional[str] = MetadataField( - None, - description='MD5 checksum of the file contents.', - exclude_to_orm=True, - ) - def __init__(self, ase=None, file=None, filename=None, values=None, scan_type=None, parse_policy=None, **kwargs): """Construct a new instance and set the contents to that of the file. @@ -378,7 +381,7 @@ def get_or_create(cls, filename, use_first=False, store_cif=True): return (cifs[0], False) raise ValueError( - 'More than one copy of a CIF file ' 'with the same MD5 has been found in ' 'the DB. pks={}'.format( + 'More than one copy of a CIF file with the same MD5 has been found in the DB. pks={}'.format( ','.join([str(i.pk) for i in cifs]) ) ) diff --git a/src/aiida/orm/nodes/data/code/abstract.py b/src/aiida/orm/nodes/data/code/abstract.py index 97e504f586..a04412db84 100644 --- a/src/aiida/orm/nodes/data/code/abstract.py +++ b/src/aiida/orm/nodes/data/code/abstract.py @@ -23,7 +23,7 @@ from aiida.orm import Computer from aiida.plugins import CalculationFactory -from ..data import Data +from ..data import Data, DataModel if t.TYPE_CHECKING: from aiida.engine import ProcessBuilder @@ -31,9 +31,71 @@ __all__ = ('AbstractCode',) +class AbstractCodeModel(DataModel, defer_build=True): + """Model describing required information to create an instance.""" + + label: str = MetadataField( + ..., + title='Label', + description='A unique label to identify the code by.', + short_name='-L', + ) + description: str = MetadataField( + '', + title='Description', + description='Human-readable description, ideally including version and compilation environment.', + short_name='-D', + ) + default_calc_job_plugin: t.Optional[str] = MetadataField( + None, + title='Default `CalcJob` plugin', + description='Entry point name of the default plugin (as listed in `verdi plugin list aiida.calculations`).', + short_name='-P', + ) + use_double_quotes: bool = MetadataField( + False, + title='Escape using double quotes', + description='Whether the executable and arguments of the code in the submission script should be escaped ' + 'with single or double quotes.', + ) + with_mpi: t.Optional[bool] = MetadataField( + None, + title='Run with MPI', + description='Whether the executable should be run as an MPI program. This option can be left unspecified ' + 'in which case `None` will be set and it is left up to the calculation job plugin or inputs ' + 'whether to run with MPI.', + ) + prepend_text: str = MetadataField( + '', + title='Prepend script', + description='Bash commands that should be prepended to the run line in all submit scripts for this code.', + option_cls=functools.partial( + TemplateInteractiveOption, + extension='.bash', + header='PREPEND_TEXT: if there is any bash commands that should be prepended to the executable call ' + 'in all submit scripts for this code, type that between the equal signs below and save the file.', + footer='All lines that start with `#=`: will be ignored.', + ), + ) + append_text: str = MetadataField( + '', + title='Append script', + description='Bash commands that should be appended to the run line in all submit scripts for this code.', + option_cls=functools.partial( + TemplateInteractiveOption, + extension='.bash', + header='APPEND_TEXT: if there is any bash commands that should be appended to the executable call ' + 'in all submit scripts for this code, type that between the equal signs below and save the file.', + footer='All lines that start with `#=`: will be ignored.', + ), + ) + + class AbstractCode(Data, metaclass=abc.ABCMeta): """Abstract data plugin representing an executable code.""" + Model = AbstractCodeModel + # Should become ``default_calc_job_plugin`` once ``Code`` is dropped in ``aiida-core==3.0`` _KEY_ATTRIBUTE_DEFAULT_CALC_JOB_PLUGIN: str = 'input_plugin' _KEY_ATTRIBUTE_APPEND_TEXT: str = 'append_text' @@ -43,65 +105,6 @@ class AbstractCode(Data, metaclass=abc.ABCMeta): _KEY_ATTRIBUTE_WRAP_CMDLINE_PARAMS: str = 'wrap_cmdline_params' _KEY_EXTRA_IS_HIDDEN: str = 'hidden' # Should become ``is_hidden`` once ``Code`` is dropped - class Model(Data.Model, defer_build=True): - """Model describing required information to create an instance.""" - - label: str = MetadataField( - ..., - title='Label', - description='A unique label to identify the code by.', - short_name='-L', - ) - description: str = MetadataField( - '', - title='Description', - description='Human-readable description, ideally including version and compilation environment.', - short_name='-D', - ) - default_calc_job_plugin: t.Optional[str] = MetadataField( - None, - title='Default `CalcJob` plugin', - description='Entry point name of the default plugin (as listed in `verdi plugin list aiida.calculations`).', - short_name='-P', - ) - use_double_quotes: bool = MetadataField( - False, - title='Escape using double quotes', - description='Whether the executable and arguments of the code in the submission script should be escaped ' - 'with single or double quotes.', - ) - with_mpi: t.Optional[bool] = MetadataField( - None, - title='Run with MPI', - description='Whether the executable should be run as an MPI program. This option can be left unspecified ' - 'in which case `None` will be set and it is left up to the calculation job plugin or inputs ' - 'whether to run with MPI.', - ) - prepend_text: str = MetadataField( - '', - title='Prepend script', - description='Bash commands that should be prepended to the run line in all submit scripts for this code.', - option_cls=functools.partial( - TemplateInteractiveOption, - extension='.bash', - header='PREPEND_TEXT: if there is any bash commands that should be prepended to the executable call ' - 'in all submit scripts for this code, type that between the equal signs below and save the file.', - footer='All lines that start with `#=`: will be ignored.', - ), - ) - append_text: str = MetadataField( - '', - title='Append script', - description='Bash commands that should be appended to the run line in all submit scripts for this code.', - option_cls=functools.partial( - TemplateInteractiveOption, - extension='.bash', - header='APPEND_TEXT: if there is any bash commands that should be appended to the executable call ' - 'in all submit scripts for this code, type that between the equal signs below and save the file.', - footer='All lines that start with `#=`: will be ignored.', - ), - ) - def __init__( self, default_calc_job_plugin: str | None = None, diff --git a/src/aiida/orm/nodes/data/code/containerized.py b/src/aiida/orm/nodes/data/code/containerized.py index 85fd539cfa..b5265f8dd5 100644 --- a/src/aiida/orm/nodes/data/code/containerized.py +++ b/src/aiida/orm/nodes/data/code/containerized.py @@ -19,43 +19,46 @@ from aiida.common.lang import type_check from aiida.common.pydantic import MetadataField -from .installed import InstalledCode +from .installed import InstalledCode, InstalledCodeModel __all__ = ('ContainerizedCode',) +class ContainerizedCodeModel(InstalledCodeModel): + """Model describing required information to create an instance.""" + + engine_command: str = MetadataField( + ..., + title='Engine command', + description='The command to run the container. It must contain the placeholder {image_name} that will be ' + 'replaced with the `image_name`.', + short_name='-E', + priority=3, + ) + image_name: str = MetadataField( + ..., + title='Image name', + description='Name of the image container in which to the run the executable.', + short_name='-I', + priority=2, + ) + wrap_cmdline_params: bool = MetadataField( + False, + title='Wrap command line parameters', + description='Whether all command line parameters to be passed to the engine command should be wrapped in ' + 'a double quotes to form a single argument. This should be set to `True` for Docker.', + priority=1, + ) + + class ContainerizedCode(InstalledCode): """Data plugin representing an executable code in container on a remote computer.""" + Model = ContainerizedCodeModel + _KEY_ATTRIBUTE_ENGINE_COMMAND: str = 'engine_command' _KEY_ATTRIBUTE_IMAGE_NAME: str = 'image_name' - class Model(InstalledCode.Model): - """Model describing required information to create an instance.""" - - engine_command: str = MetadataField( - ..., - title='Engine command', - description='The command to run the container. It must contain the placeholder {image_name} that will be ' - 'replaced with the `image_name`.', - short_name='-E', - priority=3, - ) - image_name: str = MetadataField( - ..., - title='Image name', - description='Name of the image container in which to the run the executable.', - short_name='-I', - priority=2, - ) - wrap_cmdline_params: bool = MetadataField( - False, - title='Wrap command line parameters', - description='Whether all command line parameters to be passed to the engine command should be wrapped in ' - 'a double quotes to form a single argument. This should be set to `True` for Docker.', - priority=1, - ) - def __init__(self, engine_command: str, image_name: str, **kwargs): super().__init__(**kwargs) self.engine_command = engine_command diff --git a/src/aiida/orm/nodes/data/code/installed.py b/src/aiida/orm/nodes/data/code/installed.py index 91edce53f3..cc5aefece5 100644 --- a/src/aiida/orm/nodes/data/code/installed.py +++ b/src/aiida/orm/nodes/data/code/installed.py @@ -28,57 +28,60 @@ from aiida.orm import Computer from aiida.orm.entities import from_backend_entity -from .abstract import AbstractCode +from .abstract import AbstractCodeModel from .legacy import Code __all__ = ('InstalledCode',) +class InstalledCodeModel(AbstractCodeModel): + """Model describing required information to create an instance.""" + + computer: str = MetadataField( + title='Computer', + description='The label of the remote computer on which the executable resides.', + is_attribute=False, + orm_to_model=lambda node, _: cast(InstalledCode, node).computer.label, + model_to_orm=lambda model: cast(InstalledCodeModel, model).load_computer(), + short_name='-Y', + priority=2, + ) + filepath_executable: str = MetadataField( + title='Filepath executable', + description='Filepath of the executable on the remote computer.', + orm_to_model=lambda node, _: str(cast(InstalledCode, node).filepath_executable), + short_name='-X', + priority=1, + ) + + @field_validator('computer', mode='before') + @classmethod + def validate_computer(cls, value: str | int) -> str: + """Validate the ``computer`` field. + + :param value: The value to validate. + :return: The validated value. + :raises ValueError: If the value is not a string or integer. + """ + from aiida.orm import load_computer + + if isinstance(value, int): + try: + return load_computer(value).label + except exceptions.NotExistent as exception: + raise ValueError(f'No computer found for the given id: {value}') from exception + return value + + class InstalledCode(Code): """Data plugin representing an executable code on a remote computer.""" + Model = InstalledCodeModel # type: ignore[assignment] + _EMIT_CODE_DEPRECATION_WARNING: bool = False _KEY_ATTRIBUTE_FILEPATH_EXECUTABLE: str = 'filepath_executable' _SKIP_MODEL_INHERITANCE_CHECK: bool = True - class Model(AbstractCode.Model): - """Model describing required information to create an instance.""" - - computer: str = MetadataField( - title='Computer', - description='The label of the remote computer on which the executable resides.', - is_attribute=False, - orm_to_model=lambda node, _: cast(InstalledCode, node).computer.label, - model_to_orm=lambda model: cast(InstalledCode.Model, model).load_computer(), - short_name='-Y', - priority=2, - ) - filepath_executable: str = MetadataField( - title='Filepath executable', - description='Filepath of the executable on the remote computer.', - orm_to_model=lambda node, _: str(cast(InstalledCode, node).filepath_executable), - short_name='-X', - priority=1, - ) - - @field_validator('computer', mode='before') - @classmethod - def validate_computer(cls, value: str | int) -> str: - """Validate the ``computer`` field. - - :param value: The value to validate. - :return: The validated value. - :raises ValueError: If the value is not a string or integer. - """ - from aiida.orm import load_computer - - if isinstance(value, int): - try: - return load_computer(value).label - except exceptions.NotExistent as exception: - raise ValueError(f'No computer found for the given id: {value}') from exception - return value - def __init__(self, computer: Computer, filepath_executable: str, **kwargs): """Construct a new instance. diff --git a/src/aiida/orm/nodes/data/code/legacy.py b/src/aiida/orm/nodes/data/code/legacy.py index b462d8ad7c..a477c7326a 100644 --- a/src/aiida/orm/nodes/data/code/legacy.py +++ b/src/aiida/orm/nodes/data/code/legacy.py @@ -18,11 +18,26 @@ from aiida.common.warnings import warn_deprecation from aiida.orm import Computer -from .abstract import AbstractCode +from .abstract import AbstractCode, AbstractCodeModel __all__ = ('Code',) +class CodeModel(AbstractCodeModel): + prepend_text: str = MetadataField( + '', + description='The code that will be put in the scheduler script before the execution of the code', + ) + append_text: str = MetadataField( + '', + description='The code that will be put in the scheduler script after the execution of the code', + ) + input_plugin: t.Optional[str] = MetadataField(description='The name of the input plugin to be used for this code') + local_executable: t.Optional[str] = MetadataField(description='Path to a local executable') + remote_exec_path: t.Optional[str] = MetadataField(description='Remote path to executable') + is_local: t.Optional[bool] = MetadataField(description='Whether the code is local or remote') + + class Code(AbstractCode): """A code entity. It can either be 'local', or 'remote'. @@ -39,21 +54,7 @@ class Code(AbstractCode): for the code to be run). """ - class Model(AbstractCode.Model): - prepend_text: str = MetadataField( - '', - description='The code that will be put in the scheduler script before the execution of the code', - ) - append_text: str = MetadataField( - '', - description='The code that will be put in the scheduler script after the execution of the code', - ) - input_plugin: t.Optional[str] = MetadataField( - description='The name of the input plugin to be used for this code' - ) - local_executable: t.Optional[str] = MetadataField(description='Path to a local executable') - remote_exec_path: t.Optional[str] = MetadataField(description='Remote path to executable') - is_local: t.Optional[bool] = MetadataField(description='Whether the code is local or remote') + Model = CodeModel def __init__(self, remote_computer_exec=None, local_executable=None, input_plugin_name=None, files=None, **kwargs): super().__init__(**kwargs) @@ -220,7 +221,7 @@ def get_code_helper(cls, label, machinename=None, backend=None): elif query.count() > 1: codes = query.all(flat=True) retstr = f"There are multiple codes with label '{label}', having IDs: " - retstr += f"{', '.join(sorted([str(c.pk) for c in codes]))}.\n" + retstr += f'{", ".join(sorted([str(c.pk) for c in codes]))}.\n' retstr += 'Relabel them (using their ID), or refer to them with their ID.' raise MultipleObjectsError(retstr) else: @@ -333,7 +334,7 @@ def _validate(self): if self.is_local(): if not self.get_local_executable(): raise exceptions.ValidationError( - 'You have to set which file is the local executable ' 'using the set_exec_filename() method' + 'You have to set which file is the local executable using the set_exec_filename() method' ) if self.get_local_executable() not in self.base.repository.list_object_names(): raise exceptions.ValidationError( diff --git a/src/aiida/orm/nodes/data/code/portable.py b/src/aiida/orm/nodes/data/code/portable.py index 61d6826614..25f0b58115 100644 --- a/src/aiida/orm/nodes/data/code/portable.py +++ b/src/aiida/orm/nodes/data/code/portable.py @@ -30,7 +30,7 @@ from aiida.common.typing import FilePath from aiida.orm import Computer -from .abstract import AbstractCode +from .abstract import AbstractCodeModel from .legacy import Code __all__ = ('PortableCode',) @@ -48,37 +48,40 @@ def _export_filepath_files_from_repo(portable_code: PortableCode, repository_pat return str(repository_path) +class PortableCodeModel(AbstractCodeModel): + """Model describing required information to create an instance.""" + + filepath_executable: str = MetadataField( + ..., + title='Filepath executable', + description='Relative filepath of executable with directory of code files.', + short_name='-X', + priority=1, + orm_to_model=lambda node, _: str(cast(PortableCode, node).filepath_executable), + ) + filepath_files: str = MetadataField( + ..., + title='Code directory', + description='Filepath to directory containing code files.', + short_name='-F', + is_attribute=False, + priority=2, + orm_to_model=lambda node, kwargs: _export_filepath_files_from_repo( + cast(PortableCode, node), + kwargs.get('repository_path', pathlib.Path.cwd() / f'{cast(PortableCode, node).label}'), + ), + ) + + class PortableCode(Code): """Data plugin representing an executable code stored in AiiDA's storage.""" + Model = PortableCodeModel + _EMIT_CODE_DEPRECATION_WARNING: bool = False _KEY_ATTRIBUTE_FILEPATH_EXECUTABLE: str = 'filepath_executable' _SKIP_MODEL_INHERITANCE_CHECK: bool = True - class Model(AbstractCode.Model): - """Model describing required information to create an instance.""" - - filepath_executable: str = MetadataField( - ..., - title='Filepath executable', - description='Relative filepath of executable with directory of code files.', - short_name='-X', - priority=1, - orm_to_model=lambda node, _: str(cast(PortableCode, node).filepath_executable), - ) - filepath_files: str = MetadataField( - ..., - title='Code directory', - description='Filepath to directory containing code files.', - short_name='-F', - is_attribute=False, - priority=2, - orm_to_model=lambda node, kwargs: _export_filepath_files_from_repo( - cast(PortableCode, node), - kwargs.get('repository_path', pathlib.Path.cwd() / f'{cast(PortableCode, node).label}'), - ), - ) - def __init__( self, filepath_executable: FilePath, diff --git a/src/aiida/orm/nodes/data/data.py b/src/aiida/orm/nodes/data/data.py index 904c4e1c44..af24ae4c86 100644 --- a/src/aiida/orm/nodes/data/data.py +++ b/src/aiida/orm/nodes/data/data.py @@ -16,11 +16,20 @@ from aiida.common.pydantic import MetadataField from aiida.orm.entities import from_backend_entity -from ..node import Node +from ..node import Node, NodeModel __all__ = ('Data',) +class DataModel(NodeModel): + source: Optional[dict] = MetadataField( + None, + description='Source of the data.', + is_subscriptable=True, + exclude_to_orm=True, + ) + + class Data(Node): """The base class for all Data nodes. @@ -31,6 +40,8 @@ class Data(Node): Nodes are responsible for validating their content (see _validate method). """ + Model = DataModel + _source_attributes = ['db_name', 'db_uri', 'uri', 'id', 'version', 'extras', 'source_md5', 'description', 'license'] # Replace this with a dictionary in each subclass that, given a file @@ -46,14 +57,6 @@ class Data(Node): _storable = True _unstorable_message = 'storing for this node has been disabled' - class Model(Node.Model): - source: Optional[dict] = MetadataField( - None, - description='Source of the data.', - is_subscriptable=True, - exclude_to_orm=True, - ) - def __init__(self, *args, source=None, **kwargs): """Construct a new instance, setting the ``source`` attribute if provided as a keyword argument.""" diff --git a/src/aiida/orm/nodes/data/dict.py b/src/aiida/orm/nodes/data/dict.py index 99673de6fd..86082270d6 100644 --- a/src/aiida/orm/nodes/data/dict.py +++ b/src/aiida/orm/nodes/data/dict.py @@ -16,13 +16,21 @@ from aiida.common import exceptions from aiida.common.pydantic import MetadataField +from . import data from .base import to_aiida_type -from .data import Data __all__ = ('Dict',) -class Dict(Data): +class DictModel(data.DataModel): + value: t.Dict[str, t.Any] = MetadataField( + description='Dictionary content.', + is_attribute=False, + is_subscriptable=True, + ) + + +class Dict(data.Data): """`Data` sub class to represent a dictionary. The dictionary contents of a `Dict` node are stored in the database as attributes. The dictionary @@ -50,12 +58,7 @@ class Dict(Data): Finally, all dictionary mutations will be forbidden once the node is stored. """ - class Model(Data.Model): - value: t.Dict[str, t.Any] = MetadataField( - description='Dictionary content.', - is_attribute=False, - is_subscriptable=True, - ) + Model = DictModel def __init__(self, value=None, **kwargs): """Initialise a ``Dict`` node instance. diff --git a/src/aiida/orm/nodes/data/enum.py b/src/aiida/orm/nodes/data/enum.py index 0c12ddbdba..6def13fc34 100644 --- a/src/aiida/orm/nodes/data/enum.py +++ b/src/aiida/orm/nodes/data/enum.py @@ -23,8 +23,8 @@ class Color(Enum): from aiida.common.lang import type_check from aiida.common.pydantic import MetadataField +from . import data from .base import to_aiida_type -from .data import Data __all__ = ('EnumData',) @@ -36,7 +36,14 @@ def _(value): return EnumData(member=value) -class EnumData(Data): +class EnumDataModel(data.DataModel): + member: Enum = MetadataField( + description='The member name.', + orm_to_model=lambda node, _: t.cast(EnumData, node).get_member(), + ) + + +class EnumData(data.Data): """Data plugin that allows to easily wrap an :class:`enum.Enum` member. The enum member is stored in the database by storing the value, name and the identifier (string that represents the @@ -46,16 +53,12 @@ class of the enumeration) in the ``KEY_NAME``, ``KEY_VALUE`` and ``KEY_IDENTIFIE the ``name`` and ``value`` properties which return the name and value of the enum member, respectively. """ + Model = EnumDataModel + KEY_NAME = 'name' KEY_VALUE = 'value' KEY_IDENTIFIER = 'identifier' - class Model(Data.Model): - member: Enum = MetadataField( - description='The member name.', - orm_to_model=lambda node, _: t.cast(EnumData, node).get_member(), - ) - def __init__(self, member: Enum, *args, **kwargs): """Construct the node for the to enum member that is to be wrapped.""" type_check(member, Enum) diff --git a/src/aiida/orm/nodes/data/float.py b/src/aiida/orm/nodes/data/float.py index a5453b0725..9439b2648d 100644 --- a/src/aiida/orm/nodes/data/float.py +++ b/src/aiida/orm/nodes/data/float.py @@ -12,22 +12,25 @@ from aiida.common.pydantic import MetadataField +from . import numeric from .base import to_aiida_type -from .numeric import NumericType __all__ = ('Float',) -class Float(NumericType): +class FloatModel(numeric.NumericTypeModel): + value: float = MetadataField( + title='Float value', + description='The value of the float', + ) + + +class Float(numeric.NumericType): """`Data` sub class to represent a float value.""" - _type = float + Model = FloatModel - class Model(NumericType.Model): - value: float = MetadataField( - title='Float value', - description='The value of the float', - ) + _type = float @to_aiida_type.register(numbers.Real) diff --git a/src/aiida/orm/nodes/data/int.py b/src/aiida/orm/nodes/data/int.py index fbd8dc9918..420c253b91 100644 --- a/src/aiida/orm/nodes/data/int.py +++ b/src/aiida/orm/nodes/data/int.py @@ -12,22 +12,25 @@ from aiida.common.pydantic import MetadataField +from . import numeric from .base import to_aiida_type -from .numeric import NumericType __all__ = ('Int',) -class Int(NumericType): +class IntModel(numeric.NumericTypeModel): + value: int = MetadataField( + title='Integer value', + description='The value of the integer', + ) + + +class Int(numeric.NumericType): """`Data` sub class to represent an integer value.""" - _type = int + Model = IntModel - class Model(NumericType.Model): - value: int = MetadataField( - title='Integer value', - description='The value of the integer', - ) + _type = int @to_aiida_type.register(numbers.Integral) diff --git a/src/aiida/orm/nodes/data/jsonable.py b/src/aiida/orm/nodes/data/jsonable.py index d059fa936f..0f75c67fa7 100644 --- a/src/aiida/orm/nodes/data/jsonable.py +++ b/src/aiida/orm/nodes/data/jsonable.py @@ -8,7 +8,7 @@ from aiida.common.pydantic import MetadataField -from .data import Data +from . import data __all__ = ('JsonableData',) @@ -18,7 +18,22 @@ class JsonSerializableProtocol(typing.Protocol): def as_dict(self) -> typing.MutableMapping[typing.Any, typing.Any]: ... -class JsonableData(Data): +class JsonableDataModel(data.DataModel): + model_config = ConfigDict(arbitrary_types_allowed=True) + + obj: typing.Annotated[ + JsonSerializableProtocol, + WithJsonSchema( + { + 'type': 'object', + 'title': 'JSON-serializable object', + 'description': 'The JSON-serializable object.', + } + ), + ] = MetadataField(description='The JSON-serializable object.') + + +class JsonableData(data.Data): """Data plugin that allows to easily wrap objects that are JSON-able. Any class that implements the ``as_dict`` method, returning a dictionary that is a JSON serializable representation @@ -50,18 +65,7 @@ class JsonableData(Data): environment, or an ``ImportError`` will be raised. """ - class Model(Data.Model): - model_config = ConfigDict(arbitrary_types_allowed=True) - obj: typing.Annotated[ - JsonSerializableProtocol, - WithJsonSchema( - { - 'type': 'object', - 'title': 'JSON-serializable object', - 'description': 'The JSON-serializable object.', - } - ), - ] = MetadataField(description='The JSON-serializable object.') + Model = JsonableDataModel def __init__(self, obj: JsonSerializableProtocol, *args, **kwargs): """Construct the node for the to be wrapped object.""" diff --git a/src/aiida/orm/nodes/data/list.py b/src/aiida/orm/nodes/data/list.py index f36dc70a8e..26dff4786d 100644 --- a/src/aiida/orm/nodes/data/list.py +++ b/src/aiida/orm/nodes/data/list.py @@ -14,21 +14,24 @@ from aiida.common.pydantic import MetadataField +from . import data from .base import to_aiida_type -from .data import Data __all__ = ('List',) -class List(Data, MutableSequence): +class ListModel(data.DataModel): + value: t.List[t.Any] = MetadataField( + description='Content of the data', + ) + + +class List(data.Data, MutableSequence): """`Data` sub class to represent a list.""" - _LIST_KEY = 'list' + Model = ListModel - class Model(Data.Model): - value: t.List[t.Any] = MetadataField( - description='Content of the data', - ) + _LIST_KEY = 'list' def __init__(self, value=None, **kwargs): """Initialise a ``List`` node instance. diff --git a/src/aiida/orm/nodes/data/numeric.py b/src/aiida/orm/nodes/data/numeric.py index f515140099..7b01153d82 100644 --- a/src/aiida/orm/nodes/data/numeric.py +++ b/src/aiida/orm/nodes/data/numeric.py @@ -8,7 +8,8 @@ ########################################################################### """Module for defintion of base `Data` sub class for numeric based data types.""" -from .base import BaseType, to_aiida_type +from . import base +from .base import to_aiida_type __all__ = ('NumericType',) @@ -39,9 +40,15 @@ def inner(self, other): return inner -class NumericType(BaseType): +class NumericTypeModel(base.BaseTypeModel): + """Placeholder model""" + + +class NumericType(base.BaseType): """Sub class of Data to store numbers, overloading common operators (``+``, ``*``, ...).""" + Model = NumericTypeModel + @_left_operator def __add__(self, other): return self + other diff --git a/src/aiida/orm/nodes/data/remote/base.py b/src/aiida/orm/nodes/data/remote/base.py index a926b1f056..54283bf377 100644 --- a/src/aiida/orm/nodes/data/remote/base.py +++ b/src/aiida/orm/nodes/data/remote/base.py @@ -13,34 +13,37 @@ import logging import os from pathlib import Path -from typing import Optional +from typing import Optional, cast from aiida.common.pydantic import MetadataField from aiida.orm import AuthInfo from aiida.transports import Transport -from ..data import Data +from .. import data _logger = logging.getLogger(__name__) __all__ = ('RemoteData',) -class RemoteData(Data): +class RemoteDataModel(data.DataModel): + remote_path: Optional[str] = MetadataField( + None, + title='Remote path', + description='Filepath on the remote computer.', + orm_to_model=lambda node, _: cast(RemoteData, node).get_remote_path(), + ) + + +class RemoteData(data.Data): """Store a link to a file or folder on a remote machine. Remember to pass a computer! """ - KEY_EXTRA_CLEANED = 'cleaned' + Model = RemoteDataModel - class Model(Data.Model): - remote_path: Optional[str] = MetadataField( - None, - title='Remote path', - description='Filepath on the remote computer.', - orm_to_model=lambda node, _: node.get_remote_path(), - ) + KEY_EXTRA_CLEANED = 'cleaned' def __init__(self, remote_path: Optional[str] = None, **kwargs): super().__init__(**kwargs) diff --git a/src/aiida/orm/nodes/data/remote/stash/base.py b/src/aiida/orm/nodes/data/remote/stash/base.py index 06a59269d5..47cf589b39 100644 --- a/src/aiida/orm/nodes/data/remote/stash/base.py +++ b/src/aiida/orm/nodes/data/remote/stash/base.py @@ -12,12 +12,16 @@ from aiida.common.lang import type_check from aiida.common.pydantic import MetadataField -from ...data import Data +from ... import data __all__ = ('RemoteStashData',) -class RemoteStashData(Data): +class RemoteStashDataModel(data.DataModel): + stash_mode: StashMode = MetadataField(description='The mode with which the data was stashed') + + +class RemoteStashData(data.Data): """Data plugin that models an archived folder on a remote computer. A stashed folder is essentially an instance of ``RemoteData`` that has been archived. Archiving in this context can @@ -34,10 +38,9 @@ class RemoteStashData(Data): methods of the class will only be available or function properly based on the ``stash_mode``. """ - _storable = False + Model = RemoteStashDataModel - class Model(Data.Model): - stash_mode: StashMode = MetadataField(description='The mode with which the data was stashed') + _storable = False def __init__(self, stash_mode: StashMode, **kwargs): """Construct a new instance diff --git a/src/aiida/orm/nodes/data/remote/stash/compress.py b/src/aiida/orm/nodes/data/remote/stash/compress.py index 70fadc3cc2..960c6df14b 100644 --- a/src/aiida/orm/nodes/data/remote/stash/compress.py +++ b/src/aiida/orm/nodes/data/remote/stash/compress.py @@ -14,26 +14,29 @@ from aiida.common.lang import type_check from aiida.common.pydantic import MetadataField -from .base import RemoteStashData +from . import base __all__ = ('RemoteStashCompressedData',) -class RemoteStashCompressedData(RemoteStashData): +class RemoteStashCompressedDataModel(base.RemoteStashDataModel): + target_basepath: str = MetadataField( + description='The the target basepath', + ) + source_list: List[str] = MetadataField( + description='The list of source files that were stashed', + ) + dereference: bool = MetadataField( + description='The format of the compression used when stashed', + ) + + +class RemoteStashCompressedData(base.RemoteStashData): """Data plugin that models a compressed stashed file on a remote computer.""" - _storable = True + Model = RemoteStashCompressedDataModel - class Model(RemoteStashData.Model): - target_basepath: str = MetadataField( - description='The the target basepath', - ) - source_list: List[str] = MetadataField( - description='The list of source files that were stashed', - ) - dereference: bool = MetadataField( - description='The format of the compression used when stashed', - ) + _storable = True def __init__( self, diff --git a/src/aiida/orm/nodes/data/remote/stash/custom.py b/src/aiida/orm/nodes/data/remote/stash/custom.py index d46cbc7ae1..b76c7ef342 100644 --- a/src/aiida/orm/nodes/data/remote/stash/custom.py +++ b/src/aiida/orm/nodes/data/remote/stash/custom.py @@ -14,19 +14,22 @@ from aiida.common.lang import type_check from aiida.common.pydantic import MetadataField -from .base import RemoteStashData +from . import base __all__ = ('RemoteStashCustomData',) -class RemoteStashCustomData(RemoteStashData): +class RemoteStashCustomDataModel(base.RemoteStashDataModel): + target_basepath: str = MetadataField(description='The the target basepath') + source_list: List[str] = MetadataField(description='The list of source files that were stashed') + + +class RemoteStashCustomData(base.RemoteStashData): """Data plugin that models stashed data on a remote computer, which was done via a custom script.""" - _storable = True + Model = RemoteStashCustomDataModel - class Model(RemoteStashData.Model): - target_basepath: str = MetadataField(description='The the target basepath') - source_list: List[str] = MetadataField(description='The list of source files that were stashed') + _storable = True def __init__( self, diff --git a/src/aiida/orm/nodes/data/remote/stash/folder.py b/src/aiida/orm/nodes/data/remote/stash/folder.py index 22afd57491..2a5bf20683 100644 --- a/src/aiida/orm/nodes/data/remote/stash/folder.py +++ b/src/aiida/orm/nodes/data/remote/stash/folder.py @@ -14,22 +14,25 @@ from aiida.common.lang import type_check from aiida.common.pydantic import MetadataField -from .base import RemoteStashData +from . import base __all__ = ('RemoteStashFolderData',) -class RemoteStashFolderData(RemoteStashData): +class RemoteStashFolderDataModel(base.RemoteStashDataModel): + target_basepath: str = MetadataField(description='The the target basepath') + source_list: List[str] = MetadataField(description='The list of source files that were stashed') + + +class RemoteStashFolderData(base.RemoteStashData): """Data plugin that models a folder with files of a completed calculation job that has been stashed through a copy. This data plugin can and should be used to stash files if and only if the stash mode is `StashMode.COPY`. """ - _storable = True + Model = RemoteStashFolderDataModel - class Model(RemoteStashData.Model): - target_basepath: str = MetadataField(description='The the target basepath') - source_list: List[str] = MetadataField(description='The list of source files that were stashed') + _storable = True def __init__(self, stash_mode: StashMode, target_basepath: str, source_list: List, **kwargs): """Construct a new instance diff --git a/src/aiida/orm/nodes/data/singlefile.py b/src/aiida/orm/nodes/data/singlefile.py index 78d24dbed0..c82200c2f8 100644 --- a/src/aiida/orm/nodes/data/singlefile.py +++ b/src/aiida/orm/nodes/data/singlefile.py @@ -23,41 +23,44 @@ from aiida.common.pydantic import MetadataField from aiida.common.typing import FilePath -from .data import Data +from . import data __all__ = ('SinglefileData',) -class SinglefileData(Data): +class SinglefileDataModel(data.DataModel): + content: bytes = MetadataField( + description='The file content.', + model_to_orm=lambda model: io.BytesIO(t.cast(SinglefileData, model).content), + ) + filename: str = MetadataField( + 'file.txt', + description='The name of the stored file.', + ) + + @field_validator('content') + @classmethod + def _decode_content(cls, value: str | bytes) -> bytes: + """Decode base64 content if needed.""" + if isinstance(value, str): + try: + return base64.b64decode(value, validate=True) + except Exception as exc: + raise ValueError('if `content` is a string, it must be valid base64-encoded data') from exc + return value + + @field_serializer('content') + def _encode_content(self, value: bytes) -> str: + """Encode content as base64 string for serialization.""" + return base64.b64encode(value).decode() + + +class SinglefileData(data.Data): """Data class that can be used to store a single file in its repository.""" - DEFAULT_FILENAME = 'file.txt' + Model = SinglefileDataModel - class Model(Data.Model): - content: bytes = MetadataField( - description='The file content.', - model_to_orm=lambda model: io.BytesIO(t.cast(SinglefileData, model).content), - ) - filename: str = MetadataField( - 'file.txt', - description='The name of the stored file.', - ) - - @field_validator('content') - @classmethod - def _decode_content(cls, value: str | bytes) -> bytes: - """Decode base64 content if needed.""" - if isinstance(value, str): - try: - return base64.b64decode(value, validate=True) - except Exception as exc: - raise ValueError('if `content` is a string, it must be valid base64-encoded data') from exc - return value - - @field_serializer('content') - def _encode_content(self, value: bytes) -> str: - """Encode content as base64 string for serialization.""" - return base64.b64encode(value).decode() + DEFAULT_FILENAME = 'file.txt' @classmethod def from_string(cls, content: str, filename: str | pathlib.Path | None = None, **kwargs: t.Any) -> SinglefileData: diff --git a/src/aiida/orm/nodes/data/str.py b/src/aiida/orm/nodes/data/str.py index 0ca3b642cf..616c846a11 100644 --- a/src/aiida/orm/nodes/data/str.py +++ b/src/aiida/orm/nodes/data/str.py @@ -10,21 +10,25 @@ from aiida.common.pydantic import MetadataField -from .base import BaseType, to_aiida_type +from . import base +from .base import to_aiida_type __all__ = ('Str',) -class Str(BaseType): +class StrModel(base.BaseTypeModel): + value: str = MetadataField( + title='String value', + description='The value of the string', + ) + + +class Str(base.BaseType): """`Data` sub class to represent a string value.""" - _type = str + Model = StrModel - class Model(BaseType.Model): - value: str = MetadataField( - title='String value', - description='The value of the string', - ) + _type = str @to_aiida_type.register(str) diff --git a/src/aiida/orm/nodes/data/structure.py b/src/aiida/orm/nodes/data/structure.py index 6534b6e872..9eccc998e4 100644 --- a/src/aiida/orm/nodes/data/structure.py +++ b/src/aiida/orm/nodes/data/structure.py @@ -24,7 +24,7 @@ from aiida.common.exceptions import UnsupportedSpeciesError from aiida.common.pydantic import MetadataField -from .data import Data +from . import data __all__ = ('Kind', 'Site', 'StructureData') @@ -507,7 +507,7 @@ def get_symbols_string(symbols, weights): pieces.append(f'{symbol}{weight:4.2f}') if has_vacancies(weights): pieces.append(f'X{1.0 - sum(weights):4.2f}') - return f"{{{''.join(sorted(pieces))}}}" + return f'{{{"".join(sorted(pieces))}}}' def has_vacancies(weights): @@ -661,13 +661,34 @@ def atom_kinds_to_html(atom_kind): return html_formula -class StructureData(Data): +class StructureDataModel(data.DataModel): + pbc1: bool = MetadataField(description='Whether periodic in the a direction') + pbc2: bool = MetadataField(description='Whether periodic in the b direction') + pbc3: bool = MetadataField(description='Whether periodic in the c direction') + cell: t.List[t.List[float]] = MetadataField(description='The cell parameters') + kinds: t.List[dict] = MetadataField(description='The kinds of atoms') + sites: t.List[dict] = MetadataField(description='The atomic sites') + + @field_validator('kinds', mode='before') + @classmethod + def _validate_kinds(cls, value: t.List[Kind | dict[str, t.Any]]) -> t.List[t.Dict]: + return [kind.get_raw() if isinstance(kind, Kind) else kind for kind in value] + + @field_validator('sites', mode='before') + @classmethod + def _validate_sites(cls, value: t.List[Site | dict[str, t.Any]]) -> t.List[t.Dict]: + return [site.get_raw() if isinstance(site, Site) else site for site in value] + + +class StructureData(data.Data): """Data class that represents an atomic structure. The data is organized as a collection of sites together with a cell, the boundary conditions (whether they are periodic or not) and other related useful information. """ + Model = StructureDataModel + _set_incompatibilities = [ ('ase', 'cell'), ('ase', 'pbc'), @@ -688,24 +709,6 @@ class StructureData(Data): _dimensionality_label = {0: '', 1: 'length', 2: 'surface', 3: 'volume'} _internal_kind_tags = None - class Model(Data.Model): - pbc1: bool = MetadataField(description='Whether periodic in the a direction') - pbc2: bool = MetadataField(description='Whether periodic in the b direction') - pbc3: bool = MetadataField(description='Whether periodic in the c direction') - cell: t.List[t.List[float]] = MetadataField(description='The cell parameters') - kinds: t.List[dict] = MetadataField(description='The kinds of atoms') - sites: t.List[dict] = MetadataField(description='The atomic sites') - - @field_validator('kinds', mode='before') - @classmethod - def _validate_kinds(cls, value: t.List[Kind | dict[str, t.Any]]) -> t.List[t.Dict]: - return [kind.get_raw() if isinstance(kind, Kind) else kind for kind in value] - - @field_validator('sites', mode='before') - @classmethod - def _validate_sites(cls, value: t.List[Site | dict[str, t.Any]]) -> t.List[t.Dict]: - return [site.get_raw() if isinstance(site, Site) else site for site in value] - def __init__( self, cell=None, @@ -1385,7 +1388,7 @@ def append_atom(self, **kwargs): if aseatom is not None: if kwargs: raise ValueError( - "If you pass 'ase' as a parameter to " 'append_atom, you cannot pass any further' 'parameter' + "If you pass 'ase' as a parameter to append_atom, you cannot pass any further parameter" ) position = aseatom.position kind = Kind(ase=aseatom) @@ -2141,9 +2144,7 @@ def weights(self, value): weights_tuple = _create_weights_tuple(value) if len(weights_tuple) != len(self._symbols): - raise ValueError( - 'Cannot change the number of weights. Use the ' 'set_symbols_and_weights function instead.' - ) + raise ValueError('Cannot change the number of weights. Use the set_symbols_and_weights function instead.') validate_weights_tuple(weights_tuple, _SUM_THRESHOLD) self._weights = weights_tuple @@ -2196,9 +2197,7 @@ def symbols(self, value): symbols_tuple = _create_symbols_tuple(value) if len(symbols_tuple) != len(self._weights): - raise ValueError( - 'Cannot change the number of symbols. Use the ' 'set_symbols_and_weights function instead.' - ) + raise ValueError('Cannot change the number of symbols. Use the set_symbols_and_weights function instead.') validate_symbols_tuple(symbols_tuple) self._symbols = symbols_tuple diff --git a/src/aiida/orm/nodes/node.py b/src/aiida/orm/nodes/node.py index 24973a4e0c..3d1a7e392c 100644 --- a/src/aiida/orm/nodes/node.py +++ b/src/aiida/orm/nodes/node.py @@ -47,8 +47,7 @@ ) from ..computers import Computer -from ..entities import Collection as EntityCollection -from ..entities import Entity, from_backend_entity +from ..entities import Entity, EntityCollection, EntityModel, from_backend_entity from ..extras import EntityExtras from ..querybuilder import QueryBuilder from ..users import User @@ -66,7 +65,7 @@ from ..implementation.nodes import BackendNode from .repository import NodeRepository -__all__ = ('Node',) +__all__ = ('Node', 'NodeType') NodeType = TypeVar('NodeType', bound='Node') @@ -170,7 +169,109 @@ def links(self) -> NodeLinks: return self._node._CLS_NODE_LINKS(self._node) -class Node(Entity['BackendNode', NodeCollection['Node']], metaclass=AbstractNodeMeta): +class NodeModel(EntityModel): + uuid: UUID = MetadataField( + description='The UUID of the node', + is_attribute=False, + exclude_to_orm=True, + ) + node_type: str = MetadataField( + description='The type of the node', + is_attribute=False, + exclude_to_orm=True, + ) + process_type: Optional[str] = MetadataField( + None, + description='The process type of the node', + is_attribute=False, + exclude_to_orm=True, + ) + repository_metadata: Dict[str, Any] = MetadataField( + default_factory=dict, + description='Virtual hierarchy of the file repository.', + is_attribute=False, + orm_to_model=lambda node, _: cast(Node, node).base.repository.metadata, + exclude_to_orm=True, + ) + ctime: datetime.datetime = MetadataField( + description='The creation time of the node', + is_attribute=False, + exclude_to_orm=True, + ) + mtime: datetime.datetime = MetadataField( + description='The modification time of the node', + is_attribute=False, + exclude_to_orm=True, + ) + label: str = MetadataField( + '', + description='The node label', + is_attribute=False, + ) + description: str = MetadataField( + '', + description='The node description', + is_attribute=False, + ) + attributes: Dict[str, Any] = MetadataField( + default_factory=dict, + description='The node attributes', + is_attribute=False, + orm_to_model=lambda node, _: cast(Node, node).base.attributes.all, + is_subscriptable=True, + exclude_to_orm=True, + ) + extras: Dict[str, Any] = MetadataField( + default_factory=dict, + description='The node extras', + is_attribute=False, + orm_to_model=lambda node, _: cast(Node, node).base.extras.all, + is_subscriptable=True, + ) + computer: Optional[str] = MetadataField( + None, + description='The label of the computer', + is_attribute=False, + orm_to_model=lambda node, _: cast(Node, node).get_computer_label(), + model_to_orm=lambda model: cast(NodeModel, model).load_computer(), + exclude_to_orm=True, + ) + user: int = MetadataField( + description='The PK of the user who owns the node', + is_attribute=False, + orm_to_model=lambda node, _: cast(Node, node).user.pk, + orm_class=User, + exclude_to_orm=True, + ) + repository_content: dict[str, bytes] = MetadataField( + default_factory=dict, + description='Dictionary of file repository content. Keys are relative filepaths and values are binary file ' + 'contents encoded as base64.', + is_attribute=False, + orm_to_model=lambda node, kwargs: { + key: base64.encodebytes(content) + for key, content in cast(Node, node).base.repository.serialize_content().items() + } + if kwargs.get('serialize_repository_content') + else {}, + exclude_to_orm=True, + ) + + def load_computer(self) -> Computer: + """Load the computer instance. + + :return: The computer instance. + :raises ValueError: If the computer does not exist. + """ + from aiida.orm import load_computer + + try: + return load_computer(self.computer) + except exceptions.NotExistent as exception: + raise ValueError(exception) from exception + + +class Node(Entity['BackendNode', NodeCollection['Node'], NodeModel], metaclass=AbstractNodeMeta): """Base class for all nodes in AiiDA. Stores attributes starting with an underscore. @@ -185,6 +286,8 @@ class Node(Entity['BackendNode', NodeCollection['Node']], metaclass=AbstractNode the 'type' field. """ + Model = NodeModel + _CLS_COLLECTION = NodeCollection['Node'] _CLS_NODE_LINKS = NodeLinks _CLS_NODE_CACHING = NodeCaching @@ -223,107 +326,6 @@ def _query_type_string(cls) -> str: # noqa: N805 _storable = False _unstorable_message = 'only Data, WorkflowNode, CalculationNode or their subclasses can be stored' - class Model(Entity.Model): - uuid: UUID = MetadataField( - description='The UUID of the node', - is_attribute=False, - exclude_to_orm=True, - ) - node_type: str = MetadataField( - description='The type of the node', - is_attribute=False, - exclude_to_orm=True, - ) - process_type: Optional[str] = MetadataField( - None, - description='The process type of the node', - is_attribute=False, - exclude_to_orm=True, - ) - repository_metadata: Dict[str, Any] = MetadataField( - default_factory=dict, - description='Virtual hierarchy of the file repository.', - is_attribute=False, - orm_to_model=lambda node, _: cast(Node, node).base.repository.metadata, - exclude_to_orm=True, - ) - ctime: datetime.datetime = MetadataField( - description='The creation time of the node', - is_attribute=False, - exclude_to_orm=True, - ) - mtime: datetime.datetime = MetadataField( - description='The modification time of the node', - is_attribute=False, - exclude_to_orm=True, - ) - label: str = MetadataField( - '', - description='The node label', - is_attribute=False, - ) - description: str = MetadataField( - '', - description='The node description', - is_attribute=False, - ) - attributes: Dict[str, Any] = MetadataField( - default_factory=dict, - description='The node attributes', - is_attribute=False, - orm_to_model=lambda node, _: cast(Node, node).base.attributes.all, - is_subscriptable=True, - exclude_to_orm=True, - ) - extras: Dict[str, Any] = MetadataField( - default_factory=dict, - description='The node extras', - is_attribute=False, - orm_to_model=lambda node, _: cast(Node, node).base.extras.all, - is_subscriptable=True, - ) - computer: Optional[str] = MetadataField( - None, - description='The label of the computer', - is_attribute=False, - orm_to_model=lambda node, _: cast(Node, node).get_computer_label(), - model_to_orm=lambda model: cast(Node.Model, model).load_computer(), - exclude_to_orm=True, - ) - user: int = MetadataField( - description='The PK of the user who owns the node', - is_attribute=False, - orm_to_model=lambda node, _: cast(Node, node).user.pk, - orm_class=User, - exclude_to_orm=True, - ) - repository_content: dict[str, bytes] = MetadataField( - default_factory=dict, - description='Dictionary of file repository content. Keys are relative filepaths and values are binary file ' - 'contents encoded as base64.', - is_attribute=False, - orm_to_model=lambda node, kwargs: { - key: base64.encodebytes(content) - for key, content in cast(Node, node).base.repository.serialize_content().items() - } - if kwargs.get('serialize_repository_content') - else {}, - exclude_to_orm=True, - ) - - def load_computer(self) -> Computer: - """Load the computer instance. - - :return: The computer instance. - :raises ValueError: If the computer does not exist. - """ - from aiida.orm import load_computer - - try: - return load_computer(self.computer) - except exceptions.NotExistent as exception: - raise ValueError(exception) from exception - def __init__( self, backend: Optional['StorageBackend'] = None, @@ -334,7 +336,7 @@ def __init__( ) -> None: # We verify here that all attributes are handled in a constructor prior to the root # Node class (here), gracefully rejecting them otherwise. - node_keys = set(Node.Model.model_fields.keys()) + node_keys = set(NodeModel.model_fields.keys()) unhandled_keys = {key for key in kwargs if key not in node_keys} if unhandled_keys: raise UnhandledNodeAttributesError(unhandled_keys, self.__class__.__name__) @@ -359,7 +361,7 @@ def __init__( self.base.extras.set_many(extras) @classmethod - def from_model(cls, model: Model) -> Self: # type: ignore[override] + def from_model(cls, model: NodeModel) -> Self: # type: ignore[override] """Return an entity instance from an instance of its model.""" fields = cls.model_to_orm_field_values(model) diff --git a/src/aiida/orm/nodes/process/calculation/calcjob.py b/src/aiida/orm/nodes/process/calculation/calcjob.py index 5b886dd0a2..48639fbe69 100644 --- a/src/aiida/orm/nodes/process/calculation/calcjob.py +++ b/src/aiida/orm/nodes/process/calculation/calcjob.py @@ -19,7 +19,7 @@ from aiida.common.pydantic import MetadataField from ..process import ProcessNodeCaching -from .calculation import CalculationNode +from . import calculation if TYPE_CHECKING: from aiida.orm import FolderData @@ -50,9 +50,64 @@ def get_objects_to_hash(self) -> List[Any]: return objects -class CalcJobNode(CalculationNode): +class CalcJobNodeModel(calculation.CalculationNodeModel): + scheduler_state: Optional[str] = MetadataField( + None, + description='The state of the scheduler', + orm_to_model=lambda node, _: cast(CalcJobNode, node).get_scheduler_state(), + ) + state: Optional[str] = MetadataField( + None, + description='The active state of the calculation job', + orm_to_model=lambda node, _: cast(CalcJobNode, node).get_state(), + ) + remote_workdir: Optional[str] = MetadataField( + None, + description='The path to the remote (on cluster) scratch folder', + orm_to_model=lambda node, _: cast(CalcJobNode, node).get_remote_workdir(), + ) + job_id: Optional[str] = MetadataField( + None, + description='The scheduler job id', + orm_to_model=lambda node, _: cast(CalcJobNode, node).get_job_id(), + ) + scheduler_lastchecktime: Optional[datetime.datetime] = MetadataField( + None, + description='The last time the scheduler was checked, in isoformat', + orm_to_model=lambda node, _: cast(CalcJobNode, node).get_scheduler_lastchecktime(), + ) + last_job_info: Optional[dict] = MetadataField( + None, + description='The last job info returned by the scheduler', + orm_to_model=lambda node, _: dict(cast(CalcJobNode, node).get_last_job_info() or {}), + ) + detailed_job_info: Optional[dict] = MetadataField( + None, + description='The detailed job info returned by the scheduler', + orm_to_model=lambda node, _: cast(CalcJobNode, node).get_detailed_job_info(), + ) + retrieve_list: Optional[Sequence[Union[str, Tuple[str, str, int]]]] = MetadataField( + None, + description='The list of files to retrieve from the remote cluster', + orm_to_model=lambda node, _: cast(CalcJobNode, node).get_retrieve_list(), + ) + retrieve_temporary_list: Optional[Sequence[Union[str, Tuple[str, str, int]]]] = MetadataField( + None, + description='The list of temporary files to retrieve from the remote cluster', + orm_to_model=lambda node, _: cast(CalcJobNode, node).get_retrieve_temporary_list(), + ) + imported: Optional[bool] = MetadataField( + None, + description='Whether the node has been migrated', + orm_to_model=lambda node, _: cast(CalcJobNode, node).is_imported, + ) + + +class CalcJobNode(calculation.CalculationNode): """ORM class for all nodes representing the execution of a CalcJob.""" + Model = CalcJobNodeModel + _CLS_NODE_CACHING = CalcJobNodeCaching IMMIGRATED_KEY = 'imported' @@ -66,58 +121,6 @@ class CalcJobNode(CalculationNode): SCHEDULER_LAST_JOB_INFO_KEY = 'last_job_info' SCHEDULER_DETAILED_JOB_INFO_KEY = 'detailed_job_info' - class Model(CalculationNode.Model): - scheduler_state: Optional[str] = MetadataField( - None, - description='The state of the scheduler', - orm_to_model=lambda node, _: cast(CalcJobNode, node).get_scheduler_state(), - ) - state: Optional[str] = MetadataField( - None, - description='The active state of the calculation job', - orm_to_model=lambda node, _: cast(CalcJobNode, node).get_state(), - ) - remote_workdir: Optional[str] = MetadataField( - None, - description='The path to the remote (on cluster) scratch folder', - orm_to_model=lambda node, _: cast(CalcJobNode, node).get_remote_workdir(), - ) - job_id: Optional[str] = MetadataField( - None, - description='The scheduler job id', - orm_to_model=lambda node, _: cast(CalcJobNode, node).get_job_id(), - ) - scheduler_lastchecktime: Optional[datetime.datetime] = MetadataField( - None, - description='The last time the scheduler was checked, in isoformat', - orm_to_model=lambda node, _: cast(CalcJobNode, node).get_scheduler_lastchecktime(), - ) - last_job_info: Optional[dict] = MetadataField( - None, - description='The last job info returned by the scheduler', - orm_to_model=lambda node, _: dict(cast(CalcJobNode, node).get_last_job_info() or {}), - ) - detailed_job_info: Optional[dict] = MetadataField( - None, - description='The detailed job info returned by the scheduler', - orm_to_model=lambda node, _: cast(CalcJobNode, node).get_detailed_job_info(), - ) - retrieve_list: Optional[Sequence[Union[str, Tuple[str, str, int]]]] = MetadataField( - None, - description='The list of files to retrieve from the remote cluster', - orm_to_model=lambda node, _: cast(CalcJobNode, node).get_retrieve_list(), - ) - retrieve_temporary_list: Optional[Sequence[Union[str, Tuple[str, str, int]]]] = MetadataField( - None, - description='The list of temporary files to retrieve from the remote cluster', - orm_to_model=lambda node, _: cast(CalcJobNode, node).get_retrieve_temporary_list(), - ) - imported: Optional[bool] = MetadataField( - None, - description='Whether the node has been migrated', - orm_to_model=lambda node, _: cast(CalcJobNode, node).is_imported, - ) - # An optional entry point for a CalculationTools instance _tools = None diff --git a/src/aiida/orm/nodes/process/calculation/calculation.py b/src/aiida/orm/nodes/process/calculation/calculation.py index 1bbd234e00..a1b2390fdb 100644 --- a/src/aiida/orm/nodes/process/calculation/calculation.py +++ b/src/aiida/orm/nodes/process/calculation/calculation.py @@ -11,14 +11,20 @@ from aiida.common.links import LinkType from aiida.orm.utils.managers import NodeLinksManager -from ..process import ProcessNode +from .. import process __all__ = ('CalculationNode',) -class CalculationNode(ProcessNode): +class CalculationNodeModel(process.ProcessNodeModel): + """Placeholder model.""" + + +class CalculationNode(process.ProcessNode): """Base class for all nodes representing the execution of a calculation process.""" + Model = CalculationNodeModel + _storable = True # Calculation nodes are storable _cachable = True # Calculation nodes can be cached from _unstorable_message = 'storing for this node has been disabled' diff --git a/src/aiida/orm/nodes/process/process.py b/src/aiida/orm/nodes/process/process.py index 659a65a405..3745066c8c 100644 --- a/src/aiida/orm/nodes/process/process.py +++ b/src/aiida/orm/nodes/process/process.py @@ -20,10 +20,10 @@ from aiida.common.lang import classproperty from aiida.common.links import LinkType from aiida.common.pydantic import MetadataField -from aiida.orm.utils.mixins import Sealable +from aiida.orm.utils.mixins import Sealable, SealableModel +from .. import node from ..caching import NodeCaching -from ..node import Node, NodeLinks if TYPE_CHECKING: from aiida.engine.processes import ExitCode, Process @@ -101,10 +101,10 @@ def get_objects_to_hash(self) -> List[Any]: return res -class ProcessNodeLinks(NodeLinks): +class ProcessNodeLinks(node.NodeLinks): """Interface for links of a node instance.""" - def validate_incoming(self, source: Node, link_type: LinkType, link_label: str) -> None: + def validate_incoming(self, source: node.Node, link_type: LinkType, link_label: str) -> None: """Validate adding a link of the given type from a given node to ourself. Adding an input link to a `ProcessNode` once it is stored is illegal because this should be taken care of @@ -141,7 +141,38 @@ def validate_outgoing(self, target, link_type, link_label): super().validate_outgoing(target, link_type=link_type, link_label=link_label) -class ProcessNode(Sealable, Node): +class ProcessNodeModel(node.NodeModel, SealableModel): + process_label: Optional[str] = MetadataField( + None, + description='The process label', + ) + process_state: Optional[str] = MetadataField( + None, + description='The process state enum', + ) + process_status: Optional[str] = MetadataField( + None, + description='The process status is a generic status message', + ) + exit_status: Optional[int] = MetadataField( + None, + description='The process exit status', + ) + exit_message: Optional[str] = MetadataField( + None, + description='The process exit message', + ) + exception: Optional[str] = MetadataField( + None, + description='The process exception message', + ) + paused: Optional[bool] = MetadataField( + None, + description='Whether the process is paused', + ) + + +class ProcessNode(Sealable, node.Node): """Base class for all nodes representing the execution of a process This class and its subclasses serve as proxies in the database, for actual `Process` instances being run. The @@ -151,6 +182,8 @@ class ProcessNode(Sealable, Node): provenance graph, after the execution has terminated. """ + Model = ProcessNodeModel + _CLS_NODE_LINKS = ProcessNodeLinks _CLS_NODE_CACHING = ProcessNodeCaching @@ -190,36 +223,6 @@ def _updatable_attributes(cls) -> Tuple[str, ...]: # noqa: N805 cls.PROCESS_STATUS_KEY, ) - class Model(Node.Model, Sealable.Model): - process_label: Optional[str] = MetadataField( - None, - description='The process label', - ) - process_state: Optional[str] = MetadataField( - None, - description='The process state enum', - ) - process_status: Optional[str] = MetadataField( - None, - description='The process status is a generic status message', - ) - exit_status: Optional[int] = MetadataField( - None, - description='The process exit status', - ) - exit_message: Optional[str] = MetadataField( - None, - description='The process exit message', - ) - exception: Optional[str] = MetadataField( - None, - description='The process exception message', - ) - paused: Optional[bool] = MetadataField( - None, - description='Whether the process is paused', - ) - def set_metadata_inputs(self, value: Dict[str, Any]) -> None: """Set the mapping of inputs corresponding to ``metadata`` ports that were passed to the process.""" return self.base.attributes.set(self.METADATA_INPUTS_KEY, value) diff --git a/src/aiida/orm/users.py b/src/aiida/orm/users.py index 4b8a38d3f0..979889eeb5 100644 --- a/src/aiida/orm/users.py +++ b/src/aiida/orm/users.py @@ -25,7 +25,7 @@ __all__ = ('User',) -class UserCollection(entities.Collection['User']): +class UserCollection(entities.EntityCollection['User']): """The collection of users stored in a backend.""" @staticmethod @@ -50,31 +50,34 @@ def get_default(self) -> Optional[User]: return self.backend.default_user -class User(entities.Entity['BackendUser', UserCollection]): +class UserModel(entities.EntityModel): + email: str = MetadataField( + description='The user email', + is_attribute=False, + ) + first_name: str = MetadataField( + '', + description='The user first name', + is_attribute=False, + ) + last_name: str = MetadataField( + '', + description='The user last name', + is_attribute=False, + ) + institution: str = MetadataField( + '', + description='The user institution', + is_attribute=False, + ) + + +class User(entities.Entity['BackendUser', UserCollection, UserModel]): """AiiDA User""" - _CLS_COLLECTION = UserCollection + Model = UserModel - class Model(entities.Entity.Model): - email: str = MetadataField( - description='The user email', - is_attribute=False, - ) - first_name: str = MetadataField( - '', - description='The user first name', - is_attribute=False, - ) - last_name: str = MetadataField( - '', - description='The user last name', - is_attribute=False, - ) - institution: str = MetadataField( - '', - description='The user institution', - is_attribute=False, - ) + _CLS_COLLECTION = UserCollection def __init__( self, diff --git a/src/aiida/orm/utils/mixins.py b/src/aiida/orm/utils/mixins.py index 4d8379079a..15d0ce4248 100644 --- a/src/aiida/orm/utils/mixins.py +++ b/src/aiida/orm/utils/mixins.py @@ -178,13 +178,16 @@ def get_source_code_function(self) -> str | None: return '\n'.join(content_list[start_line - 1 : end_line]) +class SealableModel(pydantic.BaseModel, defer_build=True): + sealed: bool = MetadataField(description='Whether the node is sealed') + + class Sealable: """Mixin to mark a Node as `sealable`.""" - SEALED_KEY = 'sealed' + Model = SealableModel - class Model(pydantic.BaseModel, defer_build=True): - sealed: bool = MetadataField(description='Whether the node is sealed') + SEALED_KEY = 'sealed' @classproperty def _updatable_attributes(cls) -> tuple[str, ...]: # noqa: N805 diff --git a/src/aiida/tools/graph/age_entities.py b/src/aiida/tools/graph/age_entities.py index cad3a6f052..d13c120305 100644 --- a/src/aiida/tools/graph/age_entities.py +++ b/src/aiida/tools/graph/age_entities.py @@ -135,7 +135,7 @@ def __len__(self) -> int: return len(self.keyset) def __repr__(self) -> str: - return f"{{{','.join(map(str, self.keyset))}}}" + return f'{{{",".join(map(str, self.keyset))}}}' def __eq__(self, other: Any) -> Any: return self.keyset == other.keyset diff --git a/tests/orm/test_fields.py b/tests/orm/test_fields.py index f22fdc3c73..38d60547db 100644 --- a/tests/orm/test_fields.py +++ b/tests/orm/test_fields.py @@ -53,11 +53,13 @@ def test_all_node_fields(node_and_data_entry_points: list[tuple[str, str]], data def test_add_field(): """Test the `add_field` API.""" + class NewNodeModel(orm.nodes.data.data.DataModel): + key1: str = MetadataField( # type: ignore[annotation-unchecked] + is_subscriptable=False, + ) + class NewNode(orm.Data): - class Model(orm.Data.Model): - key1: str = MetadataField( # type: ignore[annotation-unchecked] - is_subscriptable=False, - ) + Model = NewNodeModel node = NewNode() @@ -101,10 +103,12 @@ def _dummy(*args, **kwargs): _dummy, ) + class NewNodeModel(orm.nodes.data.data.DataModel): + some_label: str = MetadataField() # type: ignore[annotation-unchecked] + some_value: int = MetadataField() # type: ignore[annotation-unchecked] + class NewNode(orm.Data): - class Model(orm.Data.Model): - some_label: str = MetadataField() # type: ignore[annotation-unchecked] - some_value: int = MetadataField() # type: ignore[annotation-unchecked] + Model = NewNodeModel node = NewNode() node.base.attributes.set_many({'some_label': 'A', 'some_value': 1})