Skip to content
Merged
Show file tree
Hide file tree
Changes from 69 commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
8dcccea
Update entity model system
edan-bainglass Mar 23, 2026
bcfeb48
Implement an override of alias-based serialization to support `verdi …
edan-bainglass Mar 23, 2026
d984fe3
Update containerized code `code_show` regression data
edan-bainglass Mar 23, 2026
2f5b06b
Update how we serialize and validate by alias
edan-bainglass Mar 23, 2026
2a6112c
Implement `Node.attach_file`
edan-bainglass Mar 23, 2026
343f6fe
Mypy fixes
edan-bainglass Mar 23, 2026
f54430f
More test fixing
edan-bainglass Mar 23, 2026
5427fcd
Synthesize `CliModel` dynamically
edan-bainglass Mar 24, 2026
728766c
Use `AiiDABaseModel` consistently
edan-bainglass Mar 25, 2026
6114bca
Fix qualname handling (Python 3.9 compatible)
edan-bainglass Mar 25, 2026
72591b2
Schema title fix
edan-bainglass Mar 25, 2026
94c17ad
Pin pydantic to `~=2.12`
edan-bainglass Mar 25, 2026
f294b14
Fix mypy
edan-bainglass Mar 25, 2026
fb34307
More mypy fixes
edan-bainglass Mar 25, 2026
2ae9fbd
Don't forbid extras on `CliModel`
edan-bainglass Mar 25, 2026
cd9abf6
Infer `schema` in `_model_to_orm_field_values` if not provided
edan-bainglass Mar 25, 2026
399a65b
Move `_as_write_model` to `Entity.ReadModel`
edan-bainglass Mar 25, 2026
f3f2b60
Replace `EntityFieldMeta` with `__init_subclass__` patching
edan-bainglass Mar 25, 2026
2db7859
Introduce `_as_write_model` via new `WritableOrmModel`
edan-bainglass Mar 25, 2026
3a0a322
Run QB fields patching after Node model patching
edan-bainglass Mar 25, 2026
e33eb03
Refactor shared backend node creation
edan-bainglass Mar 25, 2026
145e6ef
Fix mypy
edan-bainglass Mar 25, 2026
9f3f832
Replace `_as_write_model` with patch handling at runtime
edan-bainglass Mar 26, 2026
c2d2d93
Correct code inheritance
edan-bainglass Mar 26, 2026
affa35a
Fix deprecation warning
edan-bainglass Mar 26, 2026
a21adc2
Fix typing
edan-bainglass Mar 26, 2026
77425f6
Make `ConstructorArgsModel` only available to those who define it
edan-bainglass Mar 26, 2026
ad7686c
Fix tests
edan-bainglass Mar 26, 2026
a3db6b2
Fix model rebuild issue
edan-bainglass Mar 27, 2026
1100bcb
Discard too-specific model exceptions classes
edan-bainglass Mar 27, 2026
8bf4755
Simplify schema check
edan-bainglass Mar 27, 2026
b9571de
Move CLI patching to a method
edan-bainglass Mar 27, 2026
db89383
Support type checking for constructor and CLI models
edan-bainglass Mar 27, 2026
9f8d07a
Standardize exceptions
edan-bainglass Mar 28, 2026
7ed02ae
Fix `ArrayData`
edan-bainglass Mar 28, 2026
ff2bedf
Fix mypy
edan-bainglass Mar 28, 2026
b5a2573
Type `schema` in `to_model` and `serialize` as `Literal`
edan-bainglass Mar 28, 2026
6293200
Clean up `_from_write_model`
edan-bainglass Mar 29, 2026
38bc4b1
Fix bug in `SinglefileData` handling of `SpooledTemporaryFile`
edan-bainglass Mar 29, 2026
3be67e5
Use callables when handling files along with models
edan-bainglass Mar 29, 2026
93c9d43
Enable serialization via CLI models in `AbstractCode`
edan-bainglass Mar 29, 2026
48b36f8
Fix mypy
edan-bainglass Mar 29, 2026
551bcb9
Use CLI model for code yaml export
edan-bainglass Mar 29, 2026
3ea5b8e
Fix `AttributesWriteModel` name
edan-bainglass Mar 29, 2026
0ea4bb9
Reorder things [BIG]
edan-bainglass Mar 29, 2026
f96ec2a
Fix portable code bug
edan-bainglass Mar 29, 2026
57743db
Discard custom UUID serialization
edan-bainglass Mar 29, 2026
e373e15
Ignore `.dev` directory (dev files)
edan-bainglass Mar 30, 2026
a8a44cf
Draft implementation for `node.serialize(..., repository_dump_path)`
edan-bainglass Mar 30, 2026
6ce6c06
Fix `fileobj_callable` handling in `_from_write_model`
edan-bainglass Mar 31, 2026
52106f4
Discard redundant json schema extra definitions
edan-bainglass Apr 17, 2026
644583f
Fix xy data
edan-bainglass Apr 17, 2026
fdde6dc
Handle model field extraction for array data
edan-bainglass Apr 17, 2026
63974b7
Really fix xy data (and add tests)
edan-bainglass Apr 17, 2026
867935e
Move `wrap_cmdline_params` model field to `AbstractCode`
edan-bainglass Apr 18, 2026
4552c70
Add missing default `None` value to trajectory data `pbc` model field
edan-bainglass Apr 18, 2026
85b0145
Regenerate code regression test files
edan-bainglass Apr 19, 2026
c38e8fc
Add missing `CifData` schema fields
edan-bainglass Apr 19, 2026
6cb3fef
Allow string label for `InstalledCode` computer argument
edan-bainglass Apr 19, 2026
31fb210
Update tests
edan-bainglass Apr 19, 2026
d56db28
Add `node` property to `Log`
edan-bainglass Apr 19, 2026
c634992
Update model testing
edan-bainglass Apr 19, 2026
cd0b0e0
Fix tests
edan-bainglass Apr 19, 2026
898cd81
Support external data plugins by serializing missing attributes
edan-bainglass Apr 19, 2026
6573729
Handle `FolderData` with empty directories
edan-bainglass Apr 22, 2026
ceeeae6
Do not exclude null values by default on entity serialization
edan-bainglass Apr 23, 2026
31f1ba5
Serialize UTC `datetime` with `+00:00` in general
edan-bainglass Apr 24, 2026
7d67420
Fix process serialization
edan-bainglass Apr 24, 2026
f2e8d24
Update model testing
edan-bainglass Apr 27, 2026
2330923
Mark `FolderData.ConstructorArgsModel.tree` as write-only
edan-bainglass May 14, 2026
6655968
Make `to_model_field_values` "public"
edan-bainglass May 14, 2026
0e80533
Add documentation
edan-bainglass May 14, 2026
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,6 @@ docker-bake.override.json

# benchmark
.benchmarks/

# dev files
.dev
1 change: 1 addition & 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
2 changes: 1 addition & 1 deletion environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ dependencies:
- pgsu~=0.3.0
- psutil~=7.0
- psycopg[binary]<4,>=3.0.2
- pydantic~=2.6
- pydantic~=2.12
- pytz~=2021.1
- pyyaml~=6.0
- requests~=2.0
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ dependencies = [
'pgsu~=0.3.0',
'psutil~=7.0',
'psycopg[binary]>=3.0.2,<4',
'pydantic~=2.6',
'pydantic~=2.12',
'pytz~=2021.1',
'pyyaml~=6.0',
'requests~=2.0',
Expand Down
57 changes: 20 additions & 37 deletions src/aiida/cmdline/commands/cmd_code.py
Comment thread
edan-bainglass marked this conversation as resolved.
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: type[Code], **kwargs) -> None:
"""Create a new `Code` instance."""
try:
instance = cls._from_model(cls.Model(**kwargs))
model = cls.CliModel(**kwargs)
instance = cls.from_model(model)
except (TypeError, ValueError) as exception:
echo.echo_critical(f'Failed to create instance `{cls}`: {exception}')

Expand Down Expand Up @@ -223,50 +229,27 @@ def code_duplicate(ctx, code, non_interactive, **kwargs):
@verdi_code.command()
@arguments.CODE()
@with_dbenv()
def show(code):
def show(code: Code):
"""Display detailed information for a code."""
from aiida.cmdline import is_verbose
from aiida.common.pydantic import get_metadata

table = []

# These are excluded from the CLI, so we add them manually
table.append(['PK', code.pk])
table.append(['UUID', code.uuid])
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,
):
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,
):
continue

value = getattr(code, field_name)
table.append(['Type', code.entry_point.name if code.entry_point else None])
table.append(['Label', code.label])
table.append(['Description', code.description or ''])

# Special handling for computer field to show additional info
if field_name == 'computer':
value = f'{value.label} ({value.hostname}), pk: {value.pk}'
if code.computer is not None:
table.append(['Computer', f'{code.computer.label} ({code.computer.hostname}), pk: {code.computer.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])
for key, field in code.AttributesModel.model_fields.items():
if key == 'source':
continue
value = getattr(code, key)
table.append([field.title, value])

if is_verbose():
table.append(['Calculations', len(code.base.links.get_outgoing().all())])
Expand Down
8 changes: 4 additions & 4 deletions src/aiida/cmdline/commands/cmd_computer.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ def _computer_use_login_shell_performance(transport, scheduler, authinfo, comput

if not isclose(timing_true, timing_false, rel_tol=rel_tol, abs_tol=abs_tol):
return True, (
f"\n\n{click.style('Warning:', fg='yellow', bold=True)} "
f'\n\n{click.style("Warning:", fg="yellow", bold=True)} '
'The computer is configured to use a login shell, which is slower compared to a normal shell.\n'
f'Command execution time of {timing_true:.3f} versus {timing_false:.3f} seconds, respectively).\n'
'Unless this setting is really necessary, consider disabling it with:\n'
Expand Down Expand Up @@ -347,7 +347,7 @@ def computer_duplicate(ctx, computer, non_interactive, **kwargs):
from aiida.orm.utils.builders.computer import ComputerBuilder

if kwargs['label'] in get_computer_names():
echo.echo_critical(f"A computer called {kwargs['label']} already exists")
echo.echo_critical(f'A computer called {kwargs["label"]} already exists')

kwargs['transport'] = kwargs['transport'].name
kwargs['scheduler'] = kwargs['scheduler'].name
Expand Down Expand Up @@ -652,7 +652,7 @@ def computer_delete(computer, dry_run):
# Sofar, we can only get this info with QueryBuilder
builder = QueryBuilder()
builder.append(Computer, filters={'label': label}, tag='computer')
builder.append(Node, with_computer='computer', project=Node.fields.pk) # type: ignore[arg-type]
builder.append(Node, with_computer='computer', project='pk')
associated_nodes_pk = builder.all(flat=True)

echo.echo_report(f'This computer has {len(associated_nodes_pk)} associated nodes')
Expand Down Expand Up @@ -741,7 +741,7 @@ def computer_config_show(computer, user, defaults, as_option_string):
if config.get(option.name) or config.get(option.name) is False:
if t_opt.get('switch'):
option_value = (
option.opts[-1] if config.get(option.name) else f"--no-{option.name.replace('_', '-')}" # type: ignore[union-attr]
option.opts[-1] if config.get(option.name) else f'--no-{option.name.replace("_", "-")}' # type: ignore[union-attr]
)
elif t_opt.get('is_flag'):
is_default = config.get(option.name) == transport_cli.transport_option_default(
Expand Down
43 changes: 20 additions & 23 deletions src/aiida/cmdline/groups/dynamic.py
Comment thread
edan-bainglass marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -95,20 +95,22 @@ def call_command(self, ctx: click.Context, cls: t.Any, non_interactive: bool, **
"""Call the ``command`` after validating the provided inputs."""
from pydantic import ValidationError

if hasattr(cls, 'Model'):
# The plugin defines a pydantic model: use it to validate the provided arguments
try:
cls.Model(**kwargs)
except ValidationError as exception:
param_hint = [
f'--{loc.replace("_", "-")}' # type: ignore[union-attr]
for loc in exception.errors()[0]['loc']
]
message = '\n'.join([str(e['msg']) for e in exception.errors()])
raise click.BadParameter(
message,
param_hint=param_hint or 'one or more parameters', # type: ignore[arg-type]
) from exception
CliModel = getattr(cls, 'CliModel', None) # noqa: N806
if not CliModel:
return self._command(ctx, cls, **kwargs)

try:
CliModel(**kwargs)
except ValidationError as exception:
param_hint = [
f'--{loc.replace("_", "-")}' # type: ignore[union-attr]
for loc in exception.errors()[0]['loc']
]
message = '\n'.join([str(e['msg']) for e in exception.errors()])
raise click.BadParameter(
message,
param_hint=param_hint or 'one or more parameters', # type: ignore[arg-type]
) from exception

return self._command(ctx, cls, **kwargs)

Expand Down Expand Up @@ -153,27 +155,22 @@ 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'):
CliModel = getattr(cls, 'CliModel', None) # noqa: N806
if not CliModel:
from aiida.common.warnings import warn_deprecation

warn_deprecation(
'Relying on `_get_cli_options` is deprecated. The options should be defined through a '
'`pydantic.BaseModel` that should be assigned to the `Model` class attribute.',
'Relying on `_get_cli_options` is deprecated. The options should be defined through a `CliModel`.',
version=3,
)
options_spec = self.factory(entry_point).get_cli_options() # type: ignore[union-attr]
return [self.create_option(*item) for item in options_spec]

options_spec = {}

for key, field_info in cls.Model.model_fields.items():
if get_metadata(field_info, 'exclude_from_cli'):
continue

for key, field_info in CliModel.model_fields.items():
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
Expand Down
1 change: 1 addition & 0 deletions src/aiida/common/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
'TransportTaskException',
'UniquenessError',
'UnstashTargetMode',
'UnsupportedSchemaError',
'UnsupportedSpeciesError',
'ValidationError',
'create_callback',
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 thread
edan-bainglass marked this conversation as resolved.
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
5 changes: 5 additions & 0 deletions src/aiida/common/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
'TestsNotAllowedError',
'TransportTaskException',
'UniquenessError',
'UnsupportedSchemaError',
'UnsupportedSpeciesError',
'ValidationError',
)
Expand Down Expand Up @@ -294,3 +295,7 @@ class LockedProfileError(AiidaException):

class LockingProfileError(AiidaException):
"""Raised if the profile can`t be locked"""


class UnsupportedSchemaError(AiidaException):
"""Raised when a schema (model) is not supported by the entity."""
Loading
Loading