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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 45 additions & 7 deletions src/aiida/orm/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@

import datetime
import typing as t
from collections.abc import Sequence
from copy import deepcopy
from functools import singledispatchmethod
from pprint import pformat
from types import UnionType
from uuid import UUID

from aiida.common.lang import isidentifier
from aiida.common.warnings import warn_deprecation
Expand Down Expand Up @@ -187,6 +189,29 @@ def __ge__(self, value):
return QbFieldFilters(((self, '>=', value),))


class QbBoolField(QbField):
"""A boolean (`bool`) flavor of `QbField`."""

def as_filter(self) -> QbFieldFilters:
"""Return a filter for only values that are True."""
return QbFieldFilters(((self, '==', True),))

def __and__(self, other: QbFieldFilters | QbBoolField) -> QbFieldFilters:
"""Return a filter for only values that are True and satisfy the other filter."""
return self.as_filter() & other

def __or__(self, other: QbFieldFilters | QbBoolField) -> QbFieldFilters:
"""Return a filter for only values that are True or satisfy the other filter."""
return self.as_filter() | other

def __invert__(self) -> QbFieldFilters:
"""Return a filter for only values that are not `True`.

Some booleans are optional, so not `False`, but rather `None` (absent).
"""
return QbFieldFilters(((self, '!==', True),))


class QbArrayField(QbField):
"""An array (`list`) flavor of `QbField`."""

Expand Down Expand Up @@ -244,7 +269,7 @@ def has_key(self, value):
"""Return a filter for only values with these keys"""
return QbFieldFilters(((self, 'has_key', value),))

def __getitem__(self, key: str) -> QbAnyField:
def __getitem__(self, key: str) -> QbField:
"""Return a new `QbField` with a nested key."""
return QbAnyField(
key=f'{self.key}.{key}',
Expand Down Expand Up @@ -283,6 +308,13 @@ def __getattr__(self, key: str) -> QbField:

raise AttributeError(key)

def __getitem__(self, key: str) -> QbField:
"""Return a typed child field if known; otherwise return a generic QbAnyField."""
children = getattr(self, '_typed_children', None) or {}
if key in children:
return children[key]
return super().__getitem__(key)

def __dir__(self) -> list[str]:
"""Expose typed children for autocompletion."""
children = getattr(self, '_typed_children', None) or {}
Expand Down Expand Up @@ -350,13 +382,17 @@ def __eq__(self, other: object) -> bool:
raise TypeError(f'cannot compare QbFieldFilters to {type(other)}')
return self.filters == other.filters

def __and__(self, other: QbFieldFilters) -> QbFieldFilters:
def __and__(self, other: QbFieldFilters | QbBoolField) -> QbFieldFilters:
"""``a & b`` -> {'and': [`a.filters`, `b.filters`]}."""
return self._resolve_redundancy(other, 'and') or QbFieldFilters({'and': [self.filters, other.filters]})
qb_filters = other.as_filter() if isinstance(other, QbBoolField) else other
resolved = self._resolve_redundancy(qb_filters, 'and')
return resolved or QbFieldFilters({'and': [self.filters, qb_filters.filters]})

def __or__(self, other: QbFieldFilters) -> QbFieldFilters:
def __or__(self, other: QbFieldFilters | QbBoolField) -> QbFieldFilters:
"""``a | b`` -> {'or': [`a.filters`, `b.filters`]}."""
return self._resolve_redundancy(other, 'or') or QbFieldFilters({'or': [self.filters, other.filters]})
qb_filters = other.as_filter() if isinstance(other, QbBoolField) else other
resolved = self._resolve_redundancy(qb_filters, 'or')
return resolved or QbFieldFilters({'or': [self.filters, qb_filters.filters]})

def __invert__(self) -> QbFieldFilters:
"""~(a > b) -> a !> b; ~(a !> b) -> a > b"""
Expand Down Expand Up @@ -492,9 +528,11 @@ def add_field(
root_type = extract_root_type(dtype) if dtype else None
if root_type in (int, float, datetime.datetime):
return QbNumericField(**kwargs)
elif root_type in (list, tuple):
elif root_type is bool:
return QbBoolField(**kwargs)
elif root_type in (list, tuple, Sequence):
return QbArrayField(**kwargs)
elif root_type in (str, t.Literal):
elif root_type in (str, t.Literal, UUID):
return QbStrField(**kwargs)
elif root_type is dict:
return QbDictField(**kwargs)
Expand Down
8 changes: 8 additions & 0 deletions src/aiida/orm/nodes/data/numeric.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
###########################################################################
"""Module for defintion of base `Data` sub class for numeric based data types."""

from aiida.orm.pydantic import OrmMetadataField

from .base import BaseType, to_aiida_type

__all__ = ('NumericType',)
Expand Down Expand Up @@ -42,6 +44,12 @@ def inner(self, other):
class NumericType(BaseType):
"""Sub class of Data to store numbers, overloading common operators (``+``, ``*``, ...)."""

class AttributesModel(BaseType.AttributesModel):
value: int | float = OrmMetadataField(
title='Numeric value',
description='The value of the numeric data',
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why was this change only done in this pr and not in the bigger pydantic upgrade?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Missed

@_left_operator
def __add__(self, other):
return self + other
Expand Down
4 changes: 3 additions & 1 deletion src/aiida/orm/querybuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
# re-usable type annotations
EntityClsType = type[Union[entities.Entity, 'Process']]
ProjectType = str | dict | Sequence[str | dict]
FilterType = dict[str, Any] | fields.QbFieldFilters
FilterType = dict[str, Any] | fields.QbFieldFilters | fields.QbBoolField
OrderByType = dict | list[dict] | tuple[dict, ...]

LOGGER = AIIDA_LOGGER.getChild('querybuilder')
Expand Down Expand Up @@ -706,6 +706,8 @@ def add_filter(self, tagspec: str | EntityClsType, filter_spec: FilterType) -> Q
@staticmethod
def _process_filters(filters: FilterType) -> dict[str, Any]:
"""Process filters."""
if isinstance(filters, fields.QbBoolField):
filters = filters.as_filter()
if not isinstance(filters, (dict, fields.QbFieldFilters)):
raise TypeError('Filters must be either a dictionary or QbFieldFilters')

Expand Down
92 changes: 92 additions & 0 deletions tests/orm/test_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
###########################################################################
"""Test for entity fields"""

import typing as t

import pytest
from importlib_metadata import entry_points

Expand Down Expand Up @@ -228,3 +230,93 @@ def test_query_subscriptable():
.all()
)
assert result == [[1, 2]]


@pytest.mark.usefixtures('aiida_profile_clean')
def test_boolean_query():
Comment thread
edan-bainglass marked this conversation as resolved.
"""Test using boolean fields in a query."""
orm.Bool(True, label='true').store()
orm.Bool(False, label='false').store()

def query(filters):
return (
orm.QueryBuilder()
.append(
orm.Bool,
filters=filters,
project=orm.Bool.fields.value,
)
.all(flat=True)
)

result = query(filters=orm.Bool.fields.value)
assert len(result) == 1
assert result == [True]

result = query(filters=~orm.Bool.fields.value)
assert len(result) == 1
assert result == [False]

result = query(filters=orm.Bool.fields.value | ~orm.Bool.fields.value)
assert len(result) == 2
assert set(result) == {True, False}

result = query(filters=~orm.Bool.fields.value & orm.Bool.fields.value)
assert len(result) == 0
assert result == []

result = query(filters=(orm.Bool.fields.label == 'true') & orm.Bool.fields.value)
assert len(result) == 1
assert result == [True]

result = query(filters=~orm.Bool.fields.value & (orm.Bool.fields.label == 'false'))
assert len(result) == 1
assert result == [False]


@pytest.mark.usefixtures('aiida_profile_clean')
def test_boolean_query_absent_attribute():
"""Test sparse boolean field negation.

Flag-style attributes like ``paused`` are stored as ``True`` or not at all: ``unpause()``
deletes the key rather than storing ``False``. So ``~field`` has to match every row where
the attribute is not ``True``, absent rows included.
"""
# One node stays paused: the `paused` attribute is stored as `True`.
paused_node = orm.CalculationNode().store()
paused_node.pause()

# One node is paused and then unpaused: the `paused` attribute is deleted, not set to `False`.
unpaused_node = orm.CalculationNode().store()
unpaused_node.pause()
unpaused_node.unpause()

# The stored state the query relies on: `True` on one node, absent on the other, even though
# the `.paused` property reads back `False` for the absent case (via `attributes.get(key, False)`).
assert paused_node.base.attributes.all == {'paused': True}
assert unpaused_node.base.attributes.all == {}
assert unpaused_node.paused is False

def count(filters):
return orm.QueryBuilder().append(orm.CalculationNode, filters=filters).count()

assert count(orm.CalculationNode.fields.paused) == 1 # only the paused node
assert count(~orm.CalculationNode.fields.paused) == 1 # only the unpaused node
assert count(orm.CalculationNode.fields.paused | ~orm.CalculationNode.fields.paused) == 2 # both


def test_attribute_field_access():
"""Test both modes of attribute field access."""
node = orm.Int(42)
value_attr_field = node.fields.value
assert node.fields.attributes.value is value_attr_field
assert node.fields.attributes['value'] is value_attr_field


def test_unknown_attribute_field_access():
"""Test unknown attribute access returns a generic `QbAnyField`."""
node = orm.Data()
unknown_attr = node.fields.attributes['unknown']
assert isinstance(unknown_attr, orm.fields.QbAnyField)
assert unknown_attr.key == 'attributes.unknown'
assert unknown_attr.dtype is t.Any
2 changes: 1 addition & 1 deletion tests/orm/test_fields/fields_AuthInfo.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
auth_params: QbDictField('auth_params', dtype=dict[str, typing.Any], doc='Dictionary
of authentication parameters')
computer: QbNumericField('computer', dtype=<class 'int'>, doc='The PK of the computer')
enabled: QbAnyField('enabled', dtype=<class 'bool'>, doc='Whether the instance is
enabled: QbBoolField('enabled', dtype=<class 'bool'>, doc='Whether the instance is
enabled')
metadata: QbDictField('metadata', dtype=dict[str, typing.Any], doc='Dictionary of
metadata')
Expand Down
2 changes: 1 addition & 1 deletion tests/orm/test_fields/fields_Comment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ node: QbNumericField('node', dtype=<class 'int'>, doc='Node PK that the comment
attached to')
pk: QbNumericField('pk', dtype=<class 'int'>, doc='The primary key of the entity')
user: QbNumericField('user', dtype=<class 'int'>, doc='User PK that created the comment')
uuid: QbAnyField('uuid', dtype=<class 'uuid.UUID'>, doc='The UUID of the comment')
uuid: QbStrField('uuid', dtype=<class 'uuid.UUID'>, doc='The UUID of the comment')
2 changes: 1 addition & 1 deletion tests/orm/test_fields/fields_Computer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ scheduler_type: QbStrField('scheduler_type', dtype=<class 'str'>, doc='Scheduler
of the computer')
transport_type: QbStrField('transport_type', dtype=<class 'str'>, doc='Transport type
of the computer')
uuid: QbAnyField('uuid', dtype=<class 'uuid.UUID'>, doc='The UUID of the computer')
uuid: QbStrField('uuid', dtype=<class 'uuid.UUID'>, doc='The UUID of the computer')
2 changes: 1 addition & 1 deletion tests/orm/test_fields/fields_Group.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ time: QbNumericField('time', dtype=<class 'datetime.datetime'>, doc='The creatio
time of the node, defaults to now (timezone-aware)')
type_string: QbStrField('type_string', dtype=<class 'str'>, doc='The type of the group')
user: QbNumericField('user', dtype=<class 'int'>, doc='The PK of the group owner')
uuid: QbAnyField('uuid', dtype=<class 'uuid.UUID'>, doc='The UUID of the group')
uuid: QbStrField('uuid', dtype=<class 'uuid.UUID'>, doc='The UUID of the group')
2 changes: 1 addition & 1 deletion tests/orm/test_fields/fields_Log.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ node: QbNumericField('node', dtype=<class 'int'>, doc='Associated node')
pk: QbNumericField('pk', dtype=<class 'int'>, doc='The primary key of the entity')
time: QbNumericField('time', dtype=<class 'datetime.datetime'>, doc='The time at which
the log was created')
uuid: QbAnyField('uuid', dtype=<class 'uuid.UUID'>, doc='The UUID of the node')
uuid: QbStrField('uuid', dtype=<class 'uuid.UUID'>, doc='The UUID of the node')
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ repository_metadata: QbDictField('repository_metadata', dtype=dict[str, typing.A
source: QbDictField('attributes.source', dtype=dict | None, doc='Source of the data')
user: QbNumericField('user', dtype=<class 'int'>, doc='The PK of the user who owns
the node')
uuid: QbAnyField('uuid', dtype=<class 'uuid.UUID'>, doc='The UUID of the node')
uuid: QbStrField('uuid', dtype=<class 'uuid.UUID'>, doc='The UUID of the node')

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why also the update to str, i thought u only add bool in this PR

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Please refer to the PR description

Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ node_type: QbStrField('node_type', dtype=typing.Literal['data.core.array.bands.B
doc='The type of the node.')
offset: QbArrayField('attributes.offset', dtype=list[float] | None, doc='Offset of
kpoints')
pbc1: QbAnyField('attributes.pbc1', dtype=bool | None, doc='Periodicity in the first
pbc1: QbBoolField('attributes.pbc1', dtype=bool | None, doc='Periodicity in the first
lattice vector direction')
pbc2: QbAnyField('attributes.pbc2', dtype=bool | None, doc='Periodicity in the second
pbc2: QbBoolField('attributes.pbc2', dtype=bool | None, doc='Periodicity in the second
lattice vector direction')
pbc3: QbAnyField('attributes.pbc3', dtype=bool | None, doc='Periodicity in the third
pbc3: QbBoolField('attributes.pbc3', dtype=bool | None, doc='Periodicity in the third
lattice vector direction')
pk: QbNumericField('pk', dtype=<class 'int'>, doc='The primary key of the entity')
process_type: QbStrField('process_type', dtype=str | None, doc='The process type of
Expand All @@ -37,4 +37,4 @@ units: QbStrField('attributes.units', dtype=str | None, doc='Units in which the
in bands were stored')
user: QbNumericField('user', dtype=<class 'int'>, doc='The PK of the user who owns
the node')
uuid: QbAnyField('uuid', dtype=<class 'uuid.UUID'>, doc='The UUID of the node')
uuid: QbStrField('uuid', dtype=<class 'uuid.UUID'>, doc='The UUID of the node')
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ node_type: QbStrField('node_type', dtype=typing.Literal['data.core.array.kpoints
doc='The type of the node.')
offset: QbArrayField('attributes.offset', dtype=list[float] | None, doc='Offset of
kpoints')
pbc1: QbAnyField('attributes.pbc1', dtype=bool | None, doc='Periodicity in the first
pbc1: QbBoolField('attributes.pbc1', dtype=bool | None, doc='Periodicity in the first
lattice vector direction')
pbc2: QbAnyField('attributes.pbc2', dtype=bool | None, doc='Periodicity in the second
pbc2: QbBoolField('attributes.pbc2', dtype=bool | None, doc='Periodicity in the second
lattice vector direction')
pbc3: QbAnyField('attributes.pbc3', dtype=bool | None, doc='Periodicity in the third
pbc3: QbBoolField('attributes.pbc3', dtype=bool | None, doc='Periodicity in the third
lattice vector direction')
pk: QbNumericField('pk', dtype=<class 'int'>, doc='The primary key of the entity')
process_type: QbStrField('process_type', dtype=str | None, doc='The process type of
Expand All @@ -33,4 +33,4 @@ repository_metadata: QbDictField('repository_metadata', dtype=dict[str, typing.A
source: QbDictField('attributes.source', dtype=dict | None, doc='Source of the data')
user: QbNumericField('user', dtype=<class 'int'>, doc='The PK of the user who owns
the node')
uuid: QbAnyField('uuid', dtype=<class 'uuid.UUID'>, doc='The UUID of the node')
uuid: QbStrField('uuid', dtype=<class 'uuid.UUID'>, doc='The UUID of the node')
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ repository_metadata: QbDictField('repository_metadata', dtype=dict[str, typing.A
source: QbDictField('attributes.source', dtype=dict | None, doc='Source of the data')
user: QbNumericField('user', dtype=<class 'int'>, doc='The PK of the user who owns
the node')
uuid: QbAnyField('uuid', dtype=<class 'uuid.UUID'>, doc='The UUID of the node')
uuid: QbStrField('uuid', dtype=<class 'uuid.UUID'>, doc='The UUID of the node')
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ source: QbDictField('attributes.source', dtype=dict | None, doc='Source of the d
symbols: QbArrayField('attributes.symbols', dtype=list[str], doc='List of symbols')
user: QbNumericField('user', dtype=<class 'int'>, doc='The PK of the user who owns
the node')
uuid: QbAnyField('uuid', dtype=<class 'uuid.UUID'>, doc='The UUID of the node')
uuid: QbStrField('uuid', dtype=<class 'uuid.UUID'>, doc='The UUID of the node')
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ repository_metadata: QbDictField('repository_metadata', dtype=dict[str, typing.A
source: QbDictField('attributes.source', dtype=dict | None, doc='Source of the data')
user: QbNumericField('user', dtype=<class 'int'>, doc='The PK of the user who owns
the node')
uuid: QbAnyField('uuid', dtype=<class 'uuid.UUID'>, doc='The UUID of the node')
uuid: QbStrField('uuid', dtype=<class 'uuid.UUID'>, doc='The UUID of the node')
x_name: QbStrField('attributes.x_name', dtype=<class 'str'>, doc='The name of the
x array')
x_units: QbStrField('attributes.x_units', dtype=<class 'str'>, doc='The units of the
x array')
y_names: QbAnyField('attributes.y_names', dtype=collections.abc.Sequence[str], doc='The
y_names: QbArrayField('attributes.y_names', dtype=collections.abc.Sequence[str], doc='The
names of the y arrays')
y_units: QbAnyField('attributes.y_units', dtype=collections.abc.Sequence[str], doc='The
y_units: QbArrayField('attributes.y_units', dtype=collections.abc.Sequence[str], doc='The
units of the y arrays')
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@ repository_metadata: QbDictField('repository_metadata', dtype=dict[str, typing.A
source: QbDictField('attributes.source', dtype=dict | None, doc='Source of the data')
user: QbNumericField('user', dtype=<class 'int'>, doc='The PK of the user who owns
the node')
uuid: QbAnyField('uuid', dtype=<class 'uuid.UUID'>, doc='The UUID of the node')
uuid: QbStrField('uuid', dtype=<class 'uuid.UUID'>, doc='The UUID of the node')
value: QbAnyField('attributes.value', dtype=typing.Any, doc='The value of the data')
4 changes: 2 additions & 2 deletions tests/orm/test_fields/fields_aiida.data.core.bool.Bool.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ repository_metadata: QbDictField('repository_metadata', dtype=dict[str, typing.A
source: QbDictField('attributes.source', dtype=dict | None, doc='Source of the data')
user: QbNumericField('user', dtype=<class 'int'>, doc='The PK of the user who owns
the node')
uuid: QbAnyField('uuid', dtype=<class 'uuid.UUID'>, doc='The UUID of the node')
value: QbAnyField('attributes.value', dtype=<class 'bool'>, doc='The value of the
uuid: QbStrField('uuid', dtype=<class 'uuid.UUID'>, doc='The UUID of the node')
value: QbBoolField('attributes.value', dtype=<class 'bool'>, doc='The value of the
boolean')
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@ spacegroup_numbers: QbArrayField('attributes.spacegroup_numbers', dtype=list[str
| None, doc='List of space group numbers of the structure')
user: QbNumericField('user', dtype=<class 'int'>, doc='The PK of the user who owns
the node')
uuid: QbAnyField('uuid', dtype=<class 'uuid.UUID'>, doc='The UUID of the node')
uuid: QbStrField('uuid', dtype=<class 'uuid.UUID'>, doc='The UUID of the node')
Loading
Loading