Skip to content
Merged
8 changes: 7 additions & 1 deletion geoh5py/ui_json/annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from geoh5py.data import DataAssociationEnum, DataTypeEnum
from geoh5py.groups import Group
from geoh5py.objects import ObjectBase
from geoh5py.shared.utils import enum_name_to_str, stringify
from geoh5py.shared.utils import enum_name_to_str, none2str, str2none, stringify
from geoh5py.shared.validators import (
to_class,
to_list,
Expand Down Expand Up @@ -80,6 +80,12 @@ def deprecate(value, info):
PlainSerializer(types_to_string, when_used="json"),
]

OptionalPath = Annotated[
Path | None,
BeforeValidator(str2none),
PlainSerializer(none2str),
]

OptionalUUID = Annotated[
UUID | None,
BeforeValidator(optional_uuid_mapper),
Expand Down
33 changes: 26 additions & 7 deletions geoh5py/ui_json/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,10 @@ class BaseForm(BaseModel):

model_config = ConfigDict(
extra="allow",
frozen=True,
populate_by_name=True,
loc_by_alias=True,
alias_generator=to_camel,
validate_assignment=True,
)

label: str
Expand Down Expand Up @@ -129,8 +129,6 @@ class and lastly fall back on type checking the value field of the form.
fields to avoid false positives.

:param data: Form data.
:param form_types: Pre-compute all the base classes to check against.
:param indicators: Pre-compute the indicator attributes for each subclass.
"""

data = {to_snake(k): v for k, v in data.items()}
Expand All @@ -154,6 +152,13 @@ def flatten(self):
def validate_data(self, params: dict[str, Any]):
"""Validate the form data."""

def set_value(self, value: Any):
"""Set the form value."""
self.value = value

if "optional" in self.model_fields_set:
self.enabled = self.value is not None


class StringForm(BaseForm):
"""
Expand Down Expand Up @@ -556,17 +561,31 @@ def property_if_not_is_value(self):
and not isinstance(self.property, UUID) # pylint: disable=unsupported-membership-test
):
raise ValueError("A property must be provided if is_value is used.")

return self

def flatten(self) -> UUID | float | int | None:
"""Returns the data for the form."""
if (
"is_value" in self.model_fields_set # pylint: disable=unsupported-membership-test
and not self.is_value
):
if "is_value" in self.model_fields_set and not self.is_value:
return self.property
return self.value

def set_value(self, value: Any):
"""Set the form value."""
try:
self.value = value
self.is_value = True
except ValidationError:
if value is not None:
self.property = value
self.is_value = False
else:
self.is_value = True
self.property = None

if "optional" in self.model_fields_set:
self.enabled = value is not None


class MultiSelectDataForm(DataFormMixin, BaseForm):
"""
Expand Down
62 changes: 18 additions & 44 deletions geoh5py/ui_json/ui_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,12 @@
import json
import logging
from pathlib import Path
from typing import Annotated, Any
from typing import Any
from uuid import UUID

from pydantic import (
BaseModel,
BeforeValidator,
ConfigDict,
PlainSerializer,
create_model,
field_validator,
)
Expand All @@ -39,24 +37,18 @@
from geoh5py.groups import PropertyGroup, UIJsonGroup
from geoh5py.shared import Entity
from geoh5py.shared.utils import (
as_str_if_uuid,
dict_mapper,
entity2uuid,
fetch_active_workspace,
none2str,
str2none,
str2uuid,
stringify,
)
from geoh5py.ui_json.annotations import OptionalPath
from geoh5py.ui_json.forms import BaseForm
from geoh5py.ui_json.validation import ErrorPool, UIJsonError, get_validations


logger = logging.getLogger(__name__)

OptionalPath = Annotated[
Path | None,
BeforeValidator(str2none),
PlainSerializer(none2str),
]


class BaseUIJson(BaseModel):
"""
Expand Down Expand Up @@ -259,7 +251,7 @@ def flatten(self, skip_disabled=False, active_only=False) -> dict[str, Any]:

return data

def fill(self, copy: bool = False, **kwargs) -> BaseUIJson:
def set_values(self, copy: bool = False, **kwargs) -> BaseUIJson:
"""
Fill the UIJson with new values.

Expand All @@ -269,38 +261,20 @@ def fill(self, copy: bool = False, **kwargs) -> BaseUIJson:

:return: A new UIJson object with the updated values.
"""
temp_properties = {}
for key, form in dict(self).items():
if not isinstance(form, BaseForm):
if key in kwargs:
if not isinstance(kwargs[key], str):
raise TypeError(
"Only string values can be updated for non-form fields. "
)
temp_properties[key] = kwargs[key]
continue

updates: dict[str, Any] = {}

# if a value has no default value, set enabled to false
if not bool(form.value) if form.value != [""] else False:
updates["enabled"] = False

if key in kwargs:
updates["value"] = str2uuid(stringify(kwargs[key]))
updates["enabled"] = True

if updates:
temp_properties[key] = form.model_copy(update=updates)

updated_model = self.model_copy(update=temp_properties)
if copy:
uijson = self.model_copy(deep=True)
else:
uijson = self

if not copy:
for field_name in type(self).model_fields:
setattr(self, field_name, getattr(updated_model, field_name))
return self
demotion = [entity2uuid, as_str_if_uuid]
for key, value in kwargs.items():
form = getattr(uijson, key, None)
if isinstance(form, BaseForm):
form.set_value(value)
else:
setattr(uijson, key, dict_mapper(value, demotion))

return updated_model
return uijson

def to_params(self, workspace: Workspace | None = None) -> dict[str, Any]:
"""
Expand Down
2 changes: 1 addition & 1 deletion recipe.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ schema_version: 1

context:
name: "geoh5py"
version: "0.0.0.dev0" # This will be replaced by the actual version in the build process
version: "0.12.1rc2.dev279+3df1d110" # This will be replaced by the actual version in the build process
python_min: "3.12"
module_name: ${{ name|lower|replace("-", "_") }}

Expand Down
32 changes: 26 additions & 6 deletions tests/ui_json/forms_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,6 @@ def test_base_form_config_extra(sample_form):
assert form.model_extra == {"extra": "stuff"}


def test_base_form_config_frozen(sample_form):
form = sample_form(label="name", value="test")
with pytest.raises(ValidationError, match="Instance is frozen"):
form.label = "new"


def test_base_form_config_alias(sample_form):
form = sample_form(
label="name",
Expand Down Expand Up @@ -515,6 +509,32 @@ def test_data_or_value_form():
property="",
)

form.set_value(None)
assert form.is_value

optional_form = DataOrValueForm(
label="name",
value=1.0,
parent="my_param",
association="Vertex",
data_type="Float",
is_value=True,
property="",
optional=True,
)
assert optional_form.enabled
optional_form.set_value(None)
assert optional_form.is_value
assert not optional_form.enabled

optional_form.set_value(data_uid)
assert not optional_form.is_value
assert optional_form.property == uuid.UUID(data_uid)
assert optional_form.enabled

form.set_value(2)
assert form.value == 2


def test_multichoice_data_form():
data_uid_1 = str(uuid.uuid4())
Expand Down
34 changes: 10 additions & 24 deletions tests/ui_json/uijson_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -617,7 +617,7 @@ class MyUIJson(BaseUIJson):
"my_int_parameter": {"label": "b", "value": 1},
},
)
result = uijson.fill(my_string_parameter="updated")
result = uijson.set_values(my_string_parameter="updated")

assert result is uijson
assert uijson.my_string_parameter.value == "updated"
Expand All @@ -635,31 +635,15 @@ class MyUIJson(BaseUIJson):
uijson=MyUIJson,
data={"my_string_parameter": {"label": "a", "value": "original"}},
)
copy = uijson.fill(copy=True, my_string_parameter="updated", title="ok")
copy = uijson.set_values(copy=True, my_string_parameter="updated", title="ok")

assert copy is not uijson
assert copy.my_string_parameter.value == "updated"
assert uijson.my_string_parameter.value == "original"
assert copy.title == "ok"

with pytest.raises(TypeError, match="Only string"):
_ = uijson.fill(copy=True, my_string_parameter="updated", title=666)


def test_fill_disables_forms_with_falsy_value(tmp_path):
ws = Workspace(tmp_path / "test.geoh5")

class MyUIJson(BaseUIJson):
my_zero_param: FloatForm

uijson = generate_test_uijson(
ws,
uijson=MyUIJson,
data={"my_zero_param": {"label": "a", "value": 0.0}},
)
uijson.fill()

assert uijson.my_zero_param.enabled is False
with pytest.raises(ValidationError):
_ = uijson.set_values(copy=True, my_string_parameter="updated", title=666)


def test_fill_truthy_value_leaves_updates_empty(tmp_path):
Expand All @@ -675,7 +659,7 @@ class MyUIJson(BaseUIJson):
data={"my_param": {"label": "a", "value": 3.14}},
)
original_enabled = uijson.my_param.enabled
uijson.fill()
uijson.set_values()

assert uijson.my_param.enabled == original_enabled
assert uijson.my_param.value == 3.14
Expand All @@ -690,9 +674,11 @@ class MyUIJson(BaseUIJson):
uijson = generate_test_uijson(
ws,
uijson=MyUIJson,
data={"my_param": {"label": "a", "value": 0.0, "enabled": False}},
data={
"my_param": {"label": "a", "value": 0.0, "enabled": False, "optional": True}
},
)
uijson.fill(my_param=5.0)
uijson.set_values(my_param=5.0)

assert uijson.my_param.enabled is True
assert uijson.my_param.value == 5.0
Expand All @@ -717,7 +703,7 @@ class MyUIJson(BaseUIJson):
}
},
)
uijson.fill(my_object_parameter=pts2.uid)
uijson.set_values(my_object_parameter=pts2.uid)

assert uijson.my_object_parameter.value == pts2.uid

Expand Down
Loading