Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
7c191fa
Fix ORM pydantic schemas
edan-bainglass Oct 8, 2025
4922bb3
Add Kpoints constructor
edan-bainglass Oct 8, 2025
167cecf
Fix `Node.from_model`
edan-bainglass Oct 8, 2025
98af554
Missed cast
edan-bainglass Oct 8, 2025
2c3e5df
Discard direct `orm` import from `cmdline` package
edan-bainglass Oct 8, 2025
ee17f94
Allow unstored entity (de)serialization
edan-bainglass Oct 8, 2025
0b37ef6
Update tests
edan-bainglass Oct 8, 2025
6873d3c
Fix computer type
edan-bainglass Oct 8, 2025
a4a5b3e
Fix some typing issues
edan-bainglass Oct 9, 2025
0aa3f65
Allow (de)serialization of unstored nodes
edan-bainglass Oct 10, 2025
dd61d77
Add guard for unhandled attributes at parent `Data` constructor
edan-bainglass Oct 10, 2025
d7e2901
Implement constructors for `KpointsData` and `BandsData`
edan-bainglass Oct 10, 2025
32634ba
Fix tests
edan-bainglass Oct 10, 2025
c5a8533
Serialize/validate arrays as numpy arrays, not bytes
edan-bainglass Oct 10, 2025
d290eae
Fix typing
edan-bainglass Oct 10, 2025
31738ff
Fix unhandled attributes check
edan-bainglass Oct 10, 2025
2c37722
Add `array_labels` back to `BandsData.Model` without constructor hand…
edan-bainglass Oct 10, 2025
e5e982f
Nitpick some classes
edan-bainglass Oct 10, 2025
2368afb
Fix docstring
edan-bainglass Oct 10, 2025
adca253
Remove user/ctime/mtime default factories from read-only fields
edan-bainglass Oct 11, 2025
5dbb6b8
Include attributes in node input model
edan-bainglass Oct 11, 2025
7778e78
Remove some unrelated changes
edan-bainglass Oct 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/source/nitpick-exceptions
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
12 changes: 9 additions & 3 deletions src/aiida/cmdline/commands/cmd_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
###########################################################################
"""`verdi code` command."""

from __future__ import annotations

import pathlib
import warnings
from collections import defaultdict
from functools import partial
from typing import Any
from typing import TYPE_CHECKING, Any

import click

Expand All @@ -26,16 +28,20 @@
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, **kwargs) -> None:
def create_code(ctx: click.Context, cls: Code, **kwargs) -> None:
"""Create a new `Code` instance."""
try:
instance = cls._from_model(cls.Model(**kwargs))
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}')

Expand Down
7 changes: 5 additions & 2 deletions src/aiida/cmdline/groups/dynamic.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,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.InputModel if hasattr(cls, 'InputModel') else cls.Model # noqa: N806
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DynamicEntryPointCommandGroup is used also for non-Entity CLI construction. As such, we need to check if we are creating an Entity (which has the new InputModel proprty), in which case we pick out the InputModel to be used for construction.

try:
cls.Model(**kwargs)
Model(**kwargs)
except ValidationError as exception:
param_hint = [
f'--{loc.replace("_", "-")}' # type: ignore[union-attr]
Expand Down Expand Up @@ -168,9 +169,11 @@ 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

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

Expand Down
4 changes: 2 additions & 2 deletions src/aiida/common/datastructures.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]]
Comment on lines +170 to +171
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The last argument of the tuple is the depth, which "can be used to control what level of nesting of the source folder hierarchy should be maintained". It is an integer.

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]]
Expand Down
13 changes: 9 additions & 4 deletions src/aiida/common/pydantic.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ class Model(BaseModel):
: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 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
Expand All @@ -75,10 +75,10 @@ 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_to_orm: When set to ``True``, this field value will not be exposed on the CLI command that is
:param exclude_from_cli: When set to ``True``, this field value will not be exposed on the CLI command that is
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exclude_to_orm already covered in the line above. This was likely a typo.

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)

Expand All @@ -97,4 +97,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
Comment on lines +100 to +103
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Marking readOnly in the JSON Schema for clarity/correctness.


return field_info
5 changes: 3 additions & 2 deletions src/aiida/common/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@
from __future__ import annotations

import pathlib
import sys
from typing import Union

try:
if sys.version_info >= (3, 11):
from typing import Self
except ImportError:
else:
from typing_extensions import Self

__all__ = ('FilePath', 'Self')
Expand Down
6 changes: 3 additions & 3 deletions src/aiida/orm/authinfos.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
24 changes: 17 additions & 7 deletions src/aiida/orm/comments.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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',
Expand All @@ -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
Expand Down
41 changes: 34 additions & 7 deletions src/aiida/orm/computers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@
###########################################################################
"""Module for Computer entities"""

from __future__ import annotations

import logging
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.pydantic import MetadataField
Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading