diff --git a/geoh5py/shared/utils.py b/geoh5py/shared/utils.py
index 1310342b0..dfa6eb8bf 100644
--- a/geoh5py/shared/utils.py
+++ b/geoh5py/shared/utils.py
@@ -39,8 +39,6 @@
from .exceptions import Geoh5FileClosedError
-UidOrNumeric = UUID | float | int | None
-StringOrNumeric = str | float | int
# pylint: disable=too-many-lines
if TYPE_CHECKING:
@@ -837,7 +835,9 @@ def map_attributes(object_, **kwargs):
set_attributes(object_, **values)
-def stringify(values: dict[str, Any]) -> dict[str, Any]:
+def stringify(
+ values: Any | dict[str, Any],
+) -> dict[str, str] | dict[str, list[str]] | str | list[str]:
"""
Convert all values in a dictionary to string.
@@ -846,11 +846,13 @@ def stringify(values: dict[str, Any]) -> dict[str, Any]:
:return: Dictionary of string values.
"""
mappers = [
+ type2uuid,
entity2uuid,
nan2str,
inf2str,
as_str_if_uuid,
none2str,
+ enum_name_to_str,
workspace2path,
path2str,
]
@@ -889,31 +891,6 @@ def to_tuple(value: Any) -> tuple:
return (value,)
-class SetDict(dict):
- def __init__(self, **kwargs):
- kwargs = {k: self.make_set(v) for k, v in kwargs.items()}
- super().__init__(kwargs)
-
- def make_set(self, value):
- if isinstance(value, (set, tuple, list)):
- value = set(value)
- else:
- value = {value}
- return value
-
- def __setitem__(self, key, value):
- value = self.make_set(value)
- super().__setitem__(key, value)
-
- def update(self, value: dict, **kwargs) -> None: # type: ignore
- for key, val in value.items():
- val = self.make_set(val)
- if key in self:
- val = self[key].union(val)
- value[key] = val
- super().update(value, **kwargs)
-
-
def inf2str(value): # map np.inf to "inf"
if not isinstance(value, (int, float)):
return value
@@ -944,8 +921,15 @@ def nan2str(value):
return value
-def str2none(value):
- if value == "":
+def str2none(value: Any) -> Any:
+ """
+ Convert an empty string or zero UUID string to None.
+
+ :param value: Value to convert.
+
+ :return: None if value is an empty string or zero UUID, original value otherwise.
+ """
+ if value in ("", "{00000000-0000-0000-0000-000000000000}"):
return None
return value
@@ -1443,7 +1427,7 @@ def map_to_class(
return class_map
-def enum_name_to_str(value: Enum) -> str:
+def enum_name_to_str(value: Any | Enum) -> Any | str:
"""
Convert enum name to capitalized string.
@@ -1451,4 +1435,18 @@ def enum_name_to_str(value: Enum) -> str:
:return: Capitalized string.
"""
- return value.name.capitalize()
+ if isinstance(value, Enum):
+ return value.name.capitalize()
+
+ return value
+
+
+def type2uuid(value: Any) -> Any | UUID:
+ """
+ Convert an Entity type to its default uuid.
+
+ :param value: An entity type or any.
+ """
+ if isinstance(value, type) and hasattr(value, "default_type_uid"):
+ return value.default_type_uid()
+ return value
diff --git a/geoh5py/shared/validators.py b/geoh5py/shared/validators.py
index 7efe98e2f..cd631c7d5 100644
--- a/geoh5py/shared/validators.py
+++ b/geoh5py/shared/validators.py
@@ -145,12 +145,6 @@ def to_class(
return out
-def types_to_string(types: list) -> list[str] | str:
- if len(types) > 1:
- return [f"{{{k.default_type_uid()!s}}}" for k in types]
- return f"{{{types[0].default_type_uid()!s}}}"
-
-
class BaseValidator(ABC):
"""Concrete base class for validators."""
diff --git a/geoh5py/ui_json/__init__.py b/geoh5py/ui_json/__init__.py
index b6a441266..785e95913 100644
--- a/geoh5py/ui_json/__init__.py
+++ b/geoh5py/ui_json/__init__.py
@@ -27,4 +27,4 @@
from .utils import monitored_directory_copy
from .validation import InputValidation
from .forms import BaseForm
-from .ui_json import BaseUIJson
+from .ui_json import UIJson
diff --git a/geoh5py/ui_json/annotations.py b/geoh5py/ui_json/annotations.py
index 3cc38f48f..158f88b4d 100644
--- a/geoh5py/ui_json/annotations.py
+++ b/geoh5py/ui_json/annotations.py
@@ -27,13 +27,18 @@
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, none2str, str2none, stringify
+from geoh5py.shared.utils import (
+ enum_name_to_str,
+ none2str,
+ str2none,
+ stringify,
+ workspace2path,
+)
from geoh5py.shared.validators import (
to_class,
to_list,
to_path,
to_type_uid_or_class,
- types_to_string,
)
from geoh5py.ui_json.utils import optional_uuid_mapper
@@ -47,21 +52,20 @@ def deprecate(value, info):
return value
+Deprecated = Annotated[
+ Any,
+ Field(exclude=True),
+ BeforeValidator(deprecate),
+]
+
AssociationOptions = Annotated[
DataAssociationEnum,
- PlainSerializer(enum_name_to_str),
+ PlainSerializer(enum_name_to_str, when_used="json"),
]
DataTypeOptions = Annotated[
DataTypeEnum,
- PlainSerializer(enum_name_to_str),
-]
-
-
-Deprecated = Annotated[
- Any,
- Field(exclude=True),
- BeforeValidator(deprecate),
+ PlainSerializer(enum_name_to_str, when_used="json"),
]
GroupTypes = Annotated[
@@ -69,7 +73,7 @@ def deprecate(value, info):
BeforeValidator(to_class),
BeforeValidator(to_type_uid_or_class),
BeforeValidator(to_list),
- PlainSerializer(types_to_string, when_used="json"),
+ PlainSerializer(stringify, when_used="json"),
]
MeshTypes = Annotated[
@@ -77,25 +81,32 @@ def deprecate(value, info):
BeforeValidator(to_class),
BeforeValidator(to_type_uid_or_class),
BeforeValidator(to_list),
- PlainSerializer(types_to_string, when_used="json"),
+ PlainSerializer(stringify, when_used="json"),
]
OptionalPath = Annotated[
Path | None,
BeforeValidator(str2none),
- PlainSerializer(none2str),
+ BeforeValidator(workspace2path),
+ PlainSerializer(none2str, when_used="json"),
+]
+
+OptionalString = Annotated[
+ str | None,
+ BeforeValidator(str2none),
+ PlainSerializer(none2str, when_used="json"),
]
OptionalUUID = Annotated[
UUID | None,
BeforeValidator(optional_uuid_mapper),
- PlainSerializer(stringify),
+ PlainSerializer(stringify, when_used="json"),
]
OptionalUUIDList = Annotated[
- list[UUID] | None,
+ list[UUID] | UUID | None,
BeforeValidator(optional_uuid_mapper),
- PlainSerializer(stringify),
+ PlainSerializer(stringify, when_used="json"),
]
OptionalValueList = Annotated[
@@ -107,4 +118,5 @@ def deprecate(value, info):
list[Path],
BeforeValidator(to_path),
BeforeValidator(to_list),
+ PlainSerializer(stringify, when_used="json"),
]
diff --git a/geoh5py/ui_json/forms.py b/geoh5py/ui_json/forms.py
index c5f3d7cdc..16b1e13f4 100644
--- a/geoh5py/ui_json/forms.py
+++ b/geoh5py/ui_json/forms.py
@@ -149,15 +149,18 @@ def flatten(self):
"""Returns the data for the form."""
return self.value
- def validate_data(self, params: dict[str, Any]):
- """Validate the form data."""
-
def set_value(self, value: Any):
- """Set the form value."""
+ """
+ Set the form value.
+ """
self.value = value
- if "optional" in self.model_fields_set:
- self.enabled = self.value is not None
+ @property
+ def is_optional(self) -> bool:
+ """
+ Whether the field is optional or not.
+ """
+ return self.optional or self.group_optional or len(self.dependency) > 0
class StringForm(BaseForm):
@@ -472,9 +475,12 @@ class DataForm(DataFormMixin, BaseForm):
Geoh5py uijson form for data associated with an object.
Shares documented attributes with the BaseForm and DataFormMixin.
+
+ When ``multiselect`` is ``True`` the value may be a list of UUIDs; otherwise
+ a single UUID or ``None`` is expected.
"""
- value: OptionalUUID
+ value: OptionalUUIDList
class DataGroupForm(DataForm):
@@ -536,6 +542,27 @@ def to_list(cls, value: str | list[str]) -> str | list[str]:
raise TypeError(f"'value' must be a list of strings; got '{type(value)}'")
return value
+ def flatten(self) -> dict:
+ """Returns the property, data and is_complement values for the form."""
+ return {
+ "group_value": self.group_value,
+ "value": self.value,
+ }
+
+ def set_value(self, value: Any):
+ """
+ Set the form value.
+ """
+ if isinstance(value, dict):
+ for key, val in value.items():
+ setattr(self, key, val)
+
+ if isinstance(value, list | str):
+ self.value = value
+
+ if isinstance(value, UUID | None):
+ self.group_value = value
+
class DataOrValueForm(DataFormMixin, BaseForm):
"""
@@ -571,20 +598,22 @@ def flatten(self) -> UUID | float | int | None:
return self.value
def set_value(self, value: Any):
- """Set the form value."""
+ """
+ Set the form value.
+
+ Either a Numeric value or a UUID, in which case it will be assigned
+ to the `property` field and the `is_value` field will be set to False.
+ """
try:
self.value = value
self.is_value = True
except ValidationError:
- if value is not None:
+ if value is None:
+ self.is_value = True
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
+ self.property = value
+ self.is_value = False
class MultiSelectDataForm(DataFormMixin, BaseForm):
@@ -607,20 +636,6 @@ def only_multi_select(cls, value: bool) -> bool:
raise ValueError("MultiSelectForm must have multi_select: True.")
return value
- @field_validator("value", mode="before")
- @classmethod
- def to_list(cls, value: str | list[str]) -> list[str]:
- """
- Validate that value is a list, converting it if it's a string.
-
- :param value: The value to validate.
-
- :return: A list of strings representing the value.
- """
- if not isinstance(value, list):
- value = [value]
- return value
-
class DataRangeForm(DataFormMixin, BaseForm):
"""
@@ -645,6 +660,28 @@ class DataRangeForm(DataFormMixin, BaseForm):
allow_complement: bool = False
is_complement: bool = False
+ def flatten(self) -> dict:
+ """Returns the property, data and is_complement values for the form."""
+ return {
+ "is_complement": self.is_complement,
+ "property": self.property,
+ "value": self.value,
+ }
+
+ def set_value(self, value: Any):
+ """
+ Set the form value.
+ """
+ if isinstance(value, dict):
+ for key, val in value.items():
+ setattr(self, key, val)
+
+ if isinstance(value, list):
+ self.value = value
+
+ if isinstance(value, UUID | None):
+ self.property = value
+
def all_subclasses(type_object: type[BaseForm]) -> list[type[BaseForm]]:
"""Recursively find all subclasses of input type object."""
diff --git a/geoh5py/ui_json/input_file.py b/geoh5py/ui_json/input_file.py
index b3de7d6a6..2e4f4176f 100644
--- a/geoh5py/ui_json/input_file.py
+++ b/geoh5py/ui_json/input_file.py
@@ -24,7 +24,7 @@
import warnings
from copy import deepcopy
from pathlib import Path
-from typing import Any
+from typing import Any, cast
from uuid import UUID
from geoh5py import Workspace
@@ -640,7 +640,8 @@ def copy(
with Workspace.create(geoh5_path) as workspace:
input_file.copy_relatives(workspace, clear_cache=clear_cache)
- return InputFile(ui_json=stringify(ui_json), validate=validate)
+ demoted = cast(dict, stringify(ui_json))
+ return InputFile(ui_json=demoted, validate=validate)
def copy_relatives(self, parent: Workspace, clear_cache: bool = False):
"""
diff --git a/geoh5py/ui_json/ui_json.py b/geoh5py/ui_json/ui_json.py
index 450bca890..3903906c8 100644
--- a/geoh5py/ui_json/ui_json.py
+++ b/geoh5py/ui_json/ui_json.py
@@ -24,7 +24,6 @@
import logging
from pathlib import Path
from typing import Any
-from uuid import UUID
from pydantic import (
BaseModel,
@@ -34,23 +33,28 @@
)
from geoh5py import Workspace
-from geoh5py.groups import PropertyGroup, UIJsonGroup
-from geoh5py.shared import Entity
+from geoh5py.groups import UIJsonGroup
from geoh5py.shared.utils import (
- as_str_if_uuid,
+ copy_dict_relatives,
dict_mapper,
entity2uuid,
fetch_active_workspace,
)
-from geoh5py.ui_json.annotations import OptionalPath
-from geoh5py.ui_json.forms import BaseForm
-from geoh5py.ui_json.validation import ErrorPool, UIJsonError, get_validations
+from geoh5py.ui_json.annotations import OptionalPath, OptionalString
+from geoh5py.ui_json.forms import BaseForm, DependencyType, GroupForm
+from geoh5py.ui_json.validation import (
+ ErrorPool,
+ UIJsonError,
+ dependency_type_validation,
+ get_validations,
+ promote_or_catch,
+)
logger = logging.getLogger(__name__)
-class BaseUIJson(BaseModel):
+class UIJson(BaseModel):
"""
Base class for storing ui.json data on disk.
@@ -61,197 +65,227 @@ class BaseUIJson(BaseModel):
:params monitoring_directory: Directory to monitor for changes.
:params conda_environment: Conda environment to run the application.
:params workspace_geoh5: Path to the workspace geoh5 file.
+ :params out_group: Optional group form to hold the UIJson group.
+
+ :params _form_dependencies: Nested dictionaries describing the dependencies between forms,
+ where the key is the name of the form, and the value is a dictionary of
+ forms name and respective mirroring enabled state behaviour
+ (True: reflects, False: reverses).
+
+ :param _group_dependencies: Dictionary holding the name of the groups and
+ leading form.
"""
model_config = ConfigDict(
arbitrary_types_allowed=True, extra="allow", validate_assignment=True
)
- version: str
+ version: str | None = "0.0.0"
title: str
- geoh5: Path | None
- run_command: str
- monitoring_directory: OptionalPath
- conda_environment: str
- workspace_geoh5: OptionalPath | None = None
- _path: Path | None = None
- _groups: dict[str, list[str]]
-
- def model_post_init(self, context: Any, /) -> None:
- self._groups = self.get_groups()
-
- def __repr__(self) -> str:
- """Repr level shows a path if it exists or the title otherwise."""
- return f"UIJson('{self.title if self._path is None else str(self._path.name)}')"
+ geoh5: OptionalPath
+ run_command: str | None
+ monitoring_directory: OptionalPath = None
+ conda_environment: str | None
+ workspace_geoh5: OptionalPath = None
- def __str__(self) -> str:
- """String level shows the full json representation."""
+ out_group: GroupForm | OptionalString = None
- json_string = self.model_dump_json(indent=4, exclude_unset=True)
- for field in type(self).model_fields:
- value = getattr(self, field)
- if isinstance(value, BaseForm):
- type_string = type(value).__name__
- json_string = json_string.replace(
- f'"{field}": {{', f'"{field}": {type_string} {{'
- )
+ _form_dependencies: dict[str, dict[str, bool]]
+ _group_dependencies: dict[str, BaseForm]
- return f"{self!r} -> {json_string}"
+ def copy_relatives(self, parent: Workspace, clear_cache: bool = False):
+ """
+ Copy the entities referenced in the input file to a new workspace.
- @field_validator("geoh5", mode="after")
- @classmethod
- def workspace_path_exists(cls, path: Path | None) -> Path | None:
- if path is not None and not path.exists():
- raise FileNotFoundError(f"geoh5 path {path} does not exist.")
- return path
+ :param parent: The parent to copy the entities to.
+ :param clear_cache: Indicate whether to clear the cache.
+ """
+ if self.geoh5 is None:
+ return
- @field_validator("geoh5", mode="after")
- @classmethod
- def valid_geoh5_extension(cls, path: Path | None) -> Path | None:
- if path is not None and path.suffix != ".geoh5":
- raise ValueError(
- f"Workspace path: {path} must have a '.geoh5' file extension."
+ with Workspace(self.geoh5, mode="r") as geoh5:
+ params = self.to_params(workspace=geoh5)
+ params.pop("geoh5", None)
+ copy_dict_relatives(
+ params,
+ parent,
+ clear_cache=clear_cache,
)
- return path
- @classmethod
- def read(cls, path: str | Path) -> BaseUIJson:
+ def flatten(self, skip_disabled=False, active_only=False) -> dict[str, Any]:
"""
- Create a UIJson object from ui.json file.
+ Flatten the UIJson data to dictionary of key/value pairs.
- Raises errors if the file doesn't exist or is not a .ui.json file.
- Also validates at the Form and UIJson level whether the file is
- properly formatted. If called from the BaseUIJson class, forms
- will be inferred dynamically.
+ Chooses between value/property in data forms depending on the is_value
+ field.
- :param path: Path to the .ui.json file.
- :returns: UIJson object.
+ :param skip_disabled: If True, skips fields with 'enabled' set to False.
+ :param active_only: If True, skips fields that have not been explicitly set.
+
+ :return: Flattened dictionary of key/value pairs.
"""
+ data = {}
+ fields = self.model_fields_set if active_only else self.model_fields
+ # todo: strange ype issue: I did not changed this part
+ for field in fields:
+ if skip_disabled and not self.is_enabled(field):
+ continue
- if isinstance(path, str):
- path = Path(path)
+ value = getattr(self, field)
+ if isinstance(value, BaseForm):
+ value = value.flatten()
+ data[field] = value
- path = path.resolve()
+ return data
- if not path.exists():
- raise FileNotFoundError(f"File {path} does not exist.")
+ @classmethod
+ def from_dict(cls, data: dict) -> UIJson:
+ """
+ Create a UIJson instance from a dictionary.
- if "".join(path.suffixes[-2:]) != ".ui.json":
- raise ValueError(f"File {path} is not a .ui.json file.")
+ :param data: Dictionary representing the ui json object.
- with open(path, encoding="utf-8") as file:
- kwargs = json.load(file)
- kwargs = {
- key: (item if item != "" else None) for key, item in kwargs.items()
- }
-
- if cls == BaseUIJson:
- fields = {}
- for name, value in kwargs.items():
- if name in BaseUIJson.model_fields:
- continue
- if isinstance(value, dict):
- form_type = BaseForm.infer(value)
- logger.info(
- "Parameter: %s interpreted as a %s.", name, form_type.__name__
- )
- fields[name] = (form_type, ...)
- else:
- fields[name] = (type(value), ...)
+ :returns: UIJson object.
+ """
+ kwargs = {key: (item if item != "" else None) for key, item in data.items()}
- model = create_model( # type: ignore
- "UnknownUIJson",
- __base__=BaseUIJson,
- **fields,
- )
- uijson = model(**kwargs)
- else:
- uijson = cls(**kwargs)
+ ui_json_class = cls.infer(**kwargs)
- uijson._path = path # pylint: disable=protected-access
- return uijson
+ return ui_json_class(**kwargs)
- def write(self, path: Path):
+ @staticmethod
+ def infer(title="UnknownUIJson", **kwargs) -> type[UIJson]:
"""
- Write the UIJson object to file.
+ Create a UIJson subclass dynamically based on inferred form types.
- :param path: Path to write the .ui.json file.
- """
- self._path = path
- with open(path, "w", encoding="utf-8") as file:
- data = self.model_dump_json(indent=4, exclude_unset=True, by_alias=True)
- file.write(data)
+ For each keyword argument that is not already a field on :class:`UIJson`,
+ the function tries to infer the appropriate :class:`~geoh5py.ui_json.forms.BaseForm`
+ subclass from the value if it is a dict, or uses the value's type directly
+ otherwise.
- def get_groups(self) -> dict[str, list[str]]:
- """
- Returns grouped forms.
+ :param title: Name for the generated model class. Defaults to ``"UnknownUIJson"``.
+ :param kwargs: Named form data to include in the generated class.
- :returns: Group names and the parameters belonging to each
- group.
+ :return: A new :class:`UIJson` subclass whose extra fields match the inferred types.
"""
- groups: dict[str, list[str]] = {}
- for field in self.__class__.model_fields:
- form = getattr(self, field)
- if not isinstance(form, BaseForm):
+ fields = {}
+ for name, value in kwargs.items():
+ if name in UIJson.model_fields.keys():
continue
- name = getattr(form, "group", "")
- if name:
- groups[name] = [field] if name not in groups else groups[name] + [field]
+ if isinstance(value, dict):
+ form_type = BaseForm.infer(value)
+ fields[name] = (form_type, ...)
+ else:
+ fields[name] = (type(value), ...)
- return groups
+ model = create_model( # type: ignore
+ kwargs.get("title", title),
+ __base__=UIJson,
+ **fields,
+ )
+ return model
- def is_disabled(self, field: str) -> bool:
+ def is_enabled(self, field: str) -> bool:
"""
- Checks if a field is disabled based on form status.
+ Checks if a field is enabled based on form status and linkages.
- :param field: Field name to check.
- :returns: True if the field is disabled by its own enabled status or
- the groups enabled status, False otherwise.
+ :param field: Field name or form to check.
+ :returns: False if the field is disabled by its own enabled status or
+ the linkages enabled status, True otherwise.
"""
+ enabled = True
+ form = getattr(self, field)
- value = getattr(self, field)
- if not isinstance(value, BaseForm):
- return False
- if value.enabled is False:
+ # Only a key:value pair, cannot be disabled
+ if not isinstance(form, BaseForm):
return True
- disabled = False
- if value.group:
- group = next(v for k, v in self._groups.items() if field in v)
- for member in group:
- form = getattr(self, member)
- if form.group_optional:
- disabled = not form.enabled
- break
+ if not form.enabled:
+ return False
- return disabled
+ # Can still be disabled based on linkages
+ # Check if disabled based on group status
+ group = getattr(form, "group", "")
+ if group in self._group_dependencies:
+ enabled = self._group_dependencies[group].enabled
- def flatten(self, skip_disabled=False, active_only=False) -> dict[str, Any]:
+ # Then check on direct dependency
+ for name, mirror in self._form_dependencies[field].items():
+ # Not enabled as soon as False is encountered
+ if not enabled:
+ return False
+
+ codependent = getattr(self, name)
+
+ if mirror:
+ enabled = codependent.enabled
+ else:
+ enabled = not codependent.enabled
+
+ return enabled
+
+ def model_post_init(self, context: Any, /) -> None:
+ self._group_dependencies, self._form_dependencies = self._get_dependency_links()
+
+ @classmethod
+ def read(cls, path: str | Path) -> UIJson:
"""
- Flatten the UIJson data to dictionary of key/value pairs.
+ Create a UIJson instance from ui.json file.
- Chooses between value/property in data forms depending on the is_value
- field.
+ Raises errors if the file doesn't exist or is not a .ui.json file.
+ Also validates at the Form and UIJson level whether the file is
+ properly formatted.
- :param skip_disabled: If True, skips fields with 'enabled' set to False.
- :param active_only: If True, skips fields that have not been explicitly set.
+ Consider using the `load` method to get the UIJson class and data separately
+ if you want to handle validation errors yourself.
- :return: Flattened dictionary of key/value pairs.
+ :param path: Path to the .ui.json file.
+ :param validate: Whether to validate the ui json file.
+
+ :returns: UIJson object.
"""
- data = {}
- fields = self.model_fields_set if active_only else self.model_fields
- for field in fields:
- if skip_disabled and self.is_disabled(field):
+ kwargs = cls._load(path)
+
+ return cls.from_dict(kwargs)
+
+ def set_enabled(self, copy: bool = False, **states) -> UIJson:
+ """
+ Set the enabled state of fields, and handle the state of dependencies.
+
+ :param copy: If True, returns a new UIJson object with the updated values.
+ If False, updates the current UIJson object with the new values and returns itself.
+ :param states: Dictionary of field names and their enabled state to update.
+
+ :return: A UIJson object with the updated values.
+ """
+ if copy:
+ uijson = self.model_copy(deep=True)
+ else:
+ uijson = self
+
+ for field, value in states.items():
+ form = getattr(uijson, field, None)
+ if not isinstance(form, BaseForm):
continue
- value = getattr(self, field)
- if isinstance(value, BaseForm):
- value = value.flatten()
- data[field] = value
+ if not value and not form.is_optional:
+ raise ValueError(f"Field {field} enabled state cannot be False.")
- return data
+ form.enabled = value
+
+ # Mirror the state to dependencies
+ for name, mirror in self._form_dependencies[field].items():
+ # Set the link dependency state
+ codependent = getattr(self, name)
+
+ if mirror:
+ codependent.enabled = value
+ else:
+ codependent.enabled = not value
+
+ return uijson
- def set_values(self, copy: bool = False, **kwargs) -> BaseUIJson:
+ def set_values(self, copy: bool = False, **kwargs) -> UIJson:
"""
Fill the UIJson with new values.
@@ -259,37 +293,46 @@ def set_values(self, copy: bool = False, **kwargs) -> BaseUIJson:
If False, updates the current UIJson object with the new values and returns itself.
:param kwargs: Key/value pairs to update the UIJson with.
- :return: A new UIJson object with the updated values.
+ :return: A UIJson object with the updated values.
"""
if copy:
uijson = self.model_copy(deep=True)
else:
uijson = self
- demotion = [entity2uuid, as_str_if_uuid]
- for key, value in kwargs.items():
- form = getattr(uijson, key, None)
+ for field, value in kwargs.items():
+ form = getattr(uijson, field, None)
if isinstance(form, BaseForm):
- form.set_value(value)
+ if not (form.is_optional and value is None):
+ form.set_value(value)
+
+ self.set_enabled(copy=False, **{field: value is not None})
else:
- setattr(uijson, key, dict_mapper(value, demotion))
+ setattr(uijson, field, dict_mapper(value, [entity2uuid]))
return uijson
- def to_params(self, workspace: Workspace | None = None) -> dict[str, Any]:
+ def to_params(
+ self, workspace: Workspace | None = None, validate=True
+ ) -> dict[str, Any]:
"""
Promote, flatten and validate parameter/values dictionary.
:param workspace: Workspace to fetch entities from. Used for passing active
workspaces to avoid closing and flushing data.
+ :param validate: Whether to run cross validations on the data after
- :returns: If the data passes validation, to_params returns a promoted and
- flattened parameters/values dictionary that may be dumped into an application
- specific params (options) class.
+ :returns: A flattened parameters/values dictionary that may be dumped into an application
+ specific params (options) class. If validate=True, the content is validated and errors
+ are raised if any validations fail.
"""
-
data = self.flatten(skip_disabled=True, active_only=True)
- with fetch_active_workspace(workspace or Workspace(self.geoh5)) as geoh5:
+
+ with (
+ fetch_active_workspace(workspace)
+ if workspace
+ else Workspace(self.geoh5, mode="r") as geoh5
+ ):
if geoh5 is None:
raise ValueError("Workspace cannot be None.")
@@ -299,17 +342,15 @@ def to_params(self, workspace: Workspace | None = None) -> dict[str, Any]:
data[field] = geoh5
continue
- if isinstance(value, UUID):
- value = self._object_or_catch(geoh5, value)
- if isinstance(value, list) and value and isinstance(value[0], UUID):
- value = [self._object_or_catch(geoh5, uid) for uid in value]
-
- if isinstance(value, UIJsonError):
+ try:
+ value = promote_or_catch(geoh5, value)
+ except UIJsonError:
errors[field].append(value)
data[field] = value
- self.validate_data(data, errors)
+ if validate:
+ self._cross_validations(data, errors)
return data
@@ -342,11 +383,53 @@ def to_ui_json_group(
return ui_json_group
- def validate_data(
- self, params: dict[str, Any] | None = None, errors: dict[str, Any] | None = None
+ @field_validator("geoh5", mode="after")
+ @classmethod
+ def valid_geoh5_extension(cls, path: Path | None) -> Path | None:
+ """
+ Check if the input has a valid geoh5 extension.
+
+ :param path: Path to the file to check.
+ :return: Return Path if provided
+ """
+ if path is not None and path.suffix != ".geoh5":
+ raise ValueError(
+ f"Workspace path: {path} must have a '.geoh5' file extension."
+ )
+ return path
+
+ @field_validator("geoh5", mode="after")
+ @classmethod
+ def workspace_path_exists(cls, path: Path | None) -> Path | None:
+ """
+ Check if the workspace path exists.
+
+ :param path: Path to the file to check.
+ :return: Return Path if provided
+ """
+ if path is not None and not path.exists():
+ raise FileNotFoundError(f"geoh5 path {path} does not exist.")
+ return path
+
+ def write(self, path: Path) -> Path:
+ """
+ Write the UIJson object to file.
+
+ :param path: Path to write the .ui.json file.
+
+ :return: Return path to the ui_json file.
+ """
+ with open(path, "w", encoding="utf-8") as file:
+ data = self.model_dump_json(indent=4, exclude_unset=True, by_alias=True)
+ file.write(data)
+
+ return path
+
+ def _cross_validations(
+ self, params: dict[str, Any], errors: dict[str, Any] | None = None
) -> None:
"""
- Validate the UIJson data.
+ Extra validation related to inter-form dependencies and entity types.
:param params: Promoted and flattened parameters/values dictionary. The params
dictionary will be generated from the model values if not provided.
@@ -355,46 +438,116 @@ def validate_data(
:raises UIJsonError: If any validations fail.
"""
-
- if params is None:
- self.to_params()
- return
-
if errors is None:
errors = {k: [] for k in params}
- ui_json = self.model_dump(exclude_unset=True)
for field in self.model_fields_set:
- if self.is_disabled(field):
+ if not self.is_enabled(field):
continue
- form = ui_json[field]
- validations = get_validations(list(form) if isinstance(form, dict) else [])
+ form = getattr(self, field)
+ validations = get_validations(
+ list(form.model_fields_set) if isinstance(form, BaseForm) else []
+ )
for validation in validations:
try:
- validation(field, params, ui_json)
+ validation(field, params, self)
except UIJsonError as e:
errors[field].append(e)
ErrorPool(errors).throw()
- def _object_or_catch(
+ def _get_dependency_links(
self,
- workspace: Workspace,
- uuid: UUID,
- ) -> Entity | PropertyGroup | UIJsonError:
+ ) -> tuple[dict[str, BaseForm], dict[str, dict[str, bool]]]:
+ """
+ Returns dependency links between forms.
+
+ For each form, there can be a group dependency ('group') to a leading
+ form ('group_optional') and/or a direct dependency between forms ('dependency').
+
+ A direct dependency controls the enabled state tow ways, while the group dependency
+ controls the enabled state only from the lead form to its dependents.
+
+ :returns: Tuple of group dependencies and direct form dependencies.
"""
- Returns an object if it exists in the workspace or an error if not.
+ form_dependencies: dict[str, dict[str, bool]] = {}
+ group_dependencies: dict[str, BaseForm] = {}
- :param workspace: Workspace to fetch entities from.
- :param uuid: UUID of the object to fetch.
+ for name in self.__class__.model_fields.keys():
+ if name not in form_dependencies:
+ form_dependencies[name] = {}
- :returns: The object if it exists in the workspace or a placeholder error
- to be collected and raised later with any other UIJson level validation
- errors.
+ form = getattr(self, name)
+
+ if not isinstance(form, BaseForm):
+ continue
+
+ # Check for groupOptional dependency
+ # Only the leading form should have the groupOptional field
+ group_name: str = getattr(form, "group", "")
+ group_optional = getattr(form, "group_optional", False)
+ if group_optional:
+ group_dependencies[group_name] = form
+
+ # Check for direct dependency on other form
+ dependents_on = form.dependency
+
+ # If optional, enabled state only influences the form
+ if dependents_on and not getattr(form, "optional", False):
+ dependency_type_validation(dependents_on, self)
+ mirrors = getattr(form, "dependency_type", None) in [
+ DependencyType.ENABLED,
+ DependencyType.SHOW,
+ ]
+
+ # Add reverse linkage
+ if dependents_on not in form_dependencies:
+ form_dependencies[dependents_on] = {}
+
+ form_dependencies[dependents_on].update({name: mirrors})
+ form_dependencies[name][dependents_on] = mirrors
+
+ return group_dependencies, form_dependencies
+
+ @staticmethod
+ def _load(path: str | Path) -> dict:
"""
+ Load data and generate a UIJson class from file.
- obj = workspace.get_entity(uuid)
- if obj[0] is not None:
- return obj[0]
+ :param path: Path to the .ui.json file.
- return UIJsonError(f"Workspace does not contain an entity with uid: {uuid}.")
+ :return: UIJson class and dictionary representing the ui json object.
+ """
+ if isinstance(path, str):
+ path = Path(path)
+
+ path = path.resolve()
+
+ if not path.exists():
+ raise FileNotFoundError(f"File {path} does not exist.")
+
+ if "".join(path.suffixes[-2:]) != ".ui.json":
+ raise ValueError(f"File {path} is not a .ui.json file.")
+
+ with open(path, encoding="utf-8") as file:
+ kwargs = json.load(file)
+
+ return kwargs
+
+ def __repr__(self) -> str:
+ """Repr level shows the title."""
+ return f"UIJson('{self.title}')"
+
+ def __str__(self) -> str:
+ """String level shows the full json representation."""
+
+ json_string = self.model_dump_json(indent=4, exclude_unset=True)
+ for field in type(self).model_fields.keys():
+ value = getattr(self, field)
+ if isinstance(value, BaseForm):
+ type_string = type(value).__name__
+ json_string = json_string.replace(
+ f'"{field}": {{', f'"{field}": {type_string} {{'
+ )
+
+ return f"{self!r} -> {json_string}"
diff --git a/geoh5py/ui_json/validation.py b/geoh5py/ui_json/validation.py
index bb8ca0ef4..5e93f4715 100644
--- a/geoh5py/ui_json/validation.py
+++ b/geoh5py/ui_json/validation.py
@@ -22,11 +22,11 @@
from collections.abc import Callable
from copy import deepcopy
-from typing import Any, cast
+from typing import TYPE_CHECKING, Any, cast
from uuid import UUID
from warnings import warn
-from geoh5py.data import Data
+from geoh5py import Workspace
from geoh5py.groups import PropertyGroup
from geoh5py.objects import ObjectBase
from geoh5py.shared import Entity
@@ -43,9 +43,13 @@
UUIDValidator,
ValueValidator,
)
+from geoh5py.ui_json.forms import BoolForm
from geoh5py.ui_json.utils import requires_value
+if TYPE_CHECKING:
+ from geoh5py.ui_json.ui_json import UIJson
+
Validation = dict[str, Any]
@@ -360,75 +364,107 @@ def throw(self):
raise UIJsonError(message)
-def dependency_type_validation(
- name: str, data: dict[str, Any], json_dict: dict[str, Any]
-):
+def dependency_type_validation(name: str, ui_json: UIJson):
"""
- Validate that the dependency for is optional or bool type.
+ Validate that the for depending on is optional or bool type.
:param name: Name of the form
- :param data: Input data with known validations.
- :param json_dict: A dict representation of the UIJson object.
+ :param ui_json: A UIJson object.
"""
+ dependency_form = getattr(ui_json, name)
- dependency = json_dict[name]["dependency"]
- dependency_form = json_dict[dependency]
-
- if "optional" not in dependency_form and not isinstance(data[dependency], bool):
+ if "optional" not in dependency_form.model_fields_set and not isinstance(
+ dependency_form, BoolForm
+ ):
raise UIJsonError(
- f"Dependency {dependency} must be either optional or of boolean type."
+ f"Dependency form '{name}' must be either optional or of boolean type."
)
-def get_validations(form_keys: list[str]) -> list[Callable]:
+def get_validations(form: list[str]) -> list[Callable]:
"""
Get callable validations based on identifying form keys.
- :param form_keys: List of form keys.
+ :param form: Form to validate sub-keys with
:return: List of callable validations.
"""
- return [VALIDATIONS_MAP[k] for k in form_keys if k in VALIDATIONS_MAP]
+ return [VALIDATIONS_MAP[k] for k in form if k in VALIDATIONS_MAP]
-def mesh_type_validation(name: str, data: dict[str, Any], json_dict: dict[str, Any]):
+def mesh_type_validation(name: str, data: dict[str, Any], ui_json: UIJson):
"""
Validate that value is one of the provided mesh types.
:param name: Name of the form
:param data: Input data with known validations.
- :param json_dict: A dict representation of the UIJson object.
+ :param ui_json: A UIJson object.
"""
- mesh_types = json_dict[name]["mesh_type"]
+ form = getattr(ui_json, name)
+ mesh_types = form.mesh_type
obj = data[name]
if not isinstance(obj, tuple(mesh_types)):
raise UIJsonError(f"Object's mesh type must be one of {mesh_types}.")
-def parent_validation(name: str, data: dict[str, Any], json_dict: dict[str, Any]):
+def parent_validation(name: str, data: dict[str, Any], ui_json: UIJson):
"""
Validate that the data is a child of the parent object.
:param name: Name of the form
:param data: Input data with known validations.
- :param json_dict: A dict representation of the UIJson object.
+ :param ui_json: A UIJson object.
"""
- form = json_dict[name]
-
+ form = getattr(ui_json, name)
+ parent_name = form.parent
child = data[name]
- parent = data[form["parent"]]
- if not isinstance(parent, ObjectBase) or (
- isinstance(child, Data) and parent.get_entity(child.uid)[0] is None
- ):
- raise UIJsonError(f"{name} data is not a child of {form['parent']}.")
+ # Special case for DataRangeForm
+ if isinstance(child, dict):
+ child = data[name]["property"]
+
+ parent = data[parent_name]
+
+ child = child if isinstance(child, list) else [child]
+ missing_children = len(list(set(child) - set(parent.children))) > 0
+ if not isinstance(parent, ObjectBase) or missing_children:
+ raise UIJsonError(f"{name} data is not a child of {parent_name}.")
+
+
+def promote_or_catch(
+ workspace: Workspace,
+ value: Any,
+) -> Any:
+ """
+ Returns an object if it exists in the workspace or an error if not.
+
+ :param workspace: Workspace to fetch entities from.
+ :param value: UUID of the object to fetch.
+
+ :returns: The object if it exists in the workspace or a placeholder error
+ to be collected and raised later with any other UIJson level validation
+ errors.
+ """
+ if isinstance(value, list | tuple):
+ return [promote_or_catch(workspace, val) for val in value]
+
+ if isinstance(value, dict):
+ return {key: promote_or_catch(workspace, val) for key, val in value.items()}
+
+ if not isinstance(value, UUID):
+ return value
+
+ obj = workspace.get_entity(value)
+ if obj[0] is not None:
+ return obj[0]
+
+ raise UIJsonError(f"Workspace does not contain an entity with uid: {value}.")
VALIDATIONS_MAP = {
- "dependency": dependency_type_validation,
"mesh_type": mesh_type_validation,
"parent": parent_validation,
}
diff --git a/recipe.yaml b/recipe.yaml
index 55952ceec..852c3d019 100644
--- a/recipe.yaml
+++ b/recipe.yaml
@@ -2,7 +2,7 @@ schema_version: 1
context:
name: "geoh5py"
- version: "0.12.1rc2.dev279+3df1d110" # This will be replaced by the actual version in the build process
+ version: "0.0.0" # This will be replaced by the actual version in the build process
python_min: "3.12"
module_name: ${{ name|lower|replace("-", "_") }}
diff --git a/tests/ui_json_test.py b/tests/input_file_test.py
similarity index 100%
rename from tests/ui_json_test.py
rename to tests/input_file_test.py
diff --git a/tests/ui_json_utils_test.py b/tests/input_file_utils_test.py
similarity index 100%
rename from tests/ui_json_utils_test.py
rename to tests/input_file_utils_test.py
diff --git a/tests/ui_json/forms_test.py b/tests/ui_json/forms_test.py
index 9def33ce4..7b8ff0c48 100644
--- a/tests/ui_json/forms_test.py
+++ b/tests/ui_json/forms_test.py
@@ -20,6 +20,7 @@
from __future__ import annotations
+import json
import uuid
import numpy as np
@@ -53,11 +54,11 @@
all_subclasses,
indicator_attributes,
)
-from geoh5py.ui_json.ui_json import BaseUIJson
+from geoh5py.ui_json.ui_json import UIJson
def setup_from_uijson(workspace, form):
- class MyUIJson(BaseUIJson):
+ class MyUIJson(UIJson):
my_param: type(form)
uijson = MyUIJson.model_construct(
@@ -138,15 +139,6 @@ def test_base_form_serieralization(sample_form):
assert "dependencyType" in json
-def test_hide_dependency_type(tmp_path):
- with Workspace.create(tmp_path / "test.geoh5") as ws:
- form = StringForm(
- label="name", value="test", dependency="my_param", dependency_type="show"
- )
- form = setup_from_uijson(ws, form)
- assert form.dependency_type == "show"
-
-
def test_string_form():
form = StringForm(label="name", value="test")
assert form.label == "name"
@@ -458,6 +450,16 @@ def test_data_form():
assert form.association == [DataAssociationEnum.VERTEX, DataAssociationEnum.CELL]
assert form.data_type == [DataTypeEnum.FLOAT, DataTypeEnum.INTEGER]
+ data_uid_2 = str(uuid.uuid4())
+ form = DataForm(
+ label="name",
+ value=[data_uid, data_uid_2],
+ parent="my_param",
+ association="Vertex",
+ data_type="Float",
+ )
+ assert form.value == [uuid.UUID(data_uid), uuid.UUID(data_uid_2)]
+
def test_data_group_form():
group_uid = str(uuid.uuid4())
@@ -525,12 +527,10 @@ def test_data_or_value_form():
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
@@ -549,7 +549,7 @@ def test_multichoice_data_form():
multi_select=True,
)
assert form.label == "name"
- assert form.value == [uuid.UUID(data_uid_1)]
+ assert form.value == uuid.UUID(data_uid_1)
assert form.parent == "my_param"
assert form.association.name == "VERTEX"
assert form.data_type.name == "FLOAT"
@@ -596,7 +596,7 @@ def test_multichoice_data_form_serialization():
data_type="Float",
multi_select=True,
)
- data = form.model_dump()
+ data = json.loads(form.model_dump_json())
assert data["value"] == [data_uid_1, data_uid_2]
form = MultiSelectDataForm(
@@ -608,21 +608,18 @@ def test_multichoice_data_form_serialization():
multi_select=True,
)
data = form.model_dump()
- assert data["value"] == [data_uid_1]
+ assert data["value"] == uuid.UUID(data_uid_1)
form = MultiSelectDataForm(
label="name",
- value=[],
+ value=[data_uid_1, data_uid_2],
parent="my_param",
association="Vertex",
data_type="Float",
- is_value=False,
- property=[data_uid_1, data_uid_2],
multi_select=True,
)
data = form.model_dump()
- assert data["property"] == [data_uid_1, data_uid_2]
- assert data["value"] == []
+ assert data["value"] == [uuid.UUID(data_uid_1), uuid.UUID(data_uid_2)]
def test_data_range_form():
@@ -644,6 +641,29 @@ def test_data_range_form():
assert form.data_type.name == "FLOAT"
assert form.range_label == "value range"
+ assert isinstance(form.flatten(), dict)
+ assert (
+ set(form.flatten()).difference({"is_complement", "property", "value"}) == set()
+ )
+
+ # Set only the range
+ form.set_value([0.5, 1.5])
+ assert form.value == [0.5, 1.5]
+
+ # Set only the property
+ form.set_value(None)
+ assert form.property is None
+
+ # Set value as coming from form.flatten()
+ values = {
+ "is_complement": False,
+ "property": uuid.uuid4(),
+ "value": [0.0, 1.0],
+ }
+ form.set_value(values)
+
+ assert all(val == getattr(form, key) for key, val in values.items())
+
def test_flatten(sample_form):
param = sample_form(label="my_param", value=2)
diff --git a/tests/ui_json/set_dict_test.py b/tests/ui_json/set_dict_test.py
deleted file mode 100644
index a9d4bab0e..000000000
--- a/tests/ui_json/set_dict_test.py
+++ /dev/null
@@ -1,45 +0,0 @@
-# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
-# Copyright (c) 2020-2026 Mira Geoscience Ltd. '
-# '
-# This file is part of geoh5py. '
-# '
-# geoh5py is free software: you can redistribute it and/or modify '
-# it under the terms of the GNU Lesser General Public License as published by '
-# the Free Software Foundation, either version 3 of the License, or '
-# (at your option) any later version. '
-# '
-# geoh5py is distributed in the hope that it will be useful, '
-# but WITHOUT ANY WARRANTY; without even the implied warranty of '
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the '
-# GNU Lesser General Public License for more details. '
-# '
-# You should have received a copy of the GNU Lesser General Public License '
-# along with geoh5py. If not, see . '
-# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
-
-
-from __future__ import annotations
-
-from geoh5py.shared.utils import SetDict
-
-
-def test_dict_set_class():
- test = SetDict(a=1, b=[2, 3])
- assert repr(test) == "{'a': {1}, 'b': {2, 3}}"
- assert test["a"] == {1}
- test["a"] = [1, 2]
- assert test["a"] == {1, 2}
- test["a"] = {1, 2}
- assert test["a"] == {1, 2}
- test.update({"b": 4})
- assert test["b"] == {2, 3, 4}
- test.update({"c": "hello"})
- assert test["c"] == {"hello"}
- for v in test.values(): # pylint: disable=invalid-name
- assert isinstance(v, set)
- assert len(test) == 3
- assert list(test) == ["a", "b", "c"]
- assert repr(test) == "{'a': {1, 2}, 'b': {2, 3, 4}, 'c': {'hello'}}"
- assert test
- test = SetDict()
- assert not test
diff --git a/tests/ui_json/uijson_test.py b/tests/ui_json/uijson_test.py
index 82fcfc1d6..90841a4f0 100644
--- a/tests/ui_json/uijson_test.py
+++ b/tests/ui_json/uijson_test.py
@@ -22,6 +22,7 @@
import json
import logging
+from copy import deepcopy
import numpy as np
import pytest
@@ -42,97 +43,111 @@
RadioLabelForm,
StringForm,
)
-from geoh5py.ui_json.ui_json import BaseUIJson
+from geoh5py.ui_json.ui_json import UIJson
from geoh5py.ui_json.validation import UIJsonError
+SAMPLE = {
+ "version": "0.1.0",
+ "title": "my application",
+ "geoh5": "",
+ "run_command": "python -m my_module",
+ "run_command_boolean": True,
+ "monitoring_directory": "my_monitoring_directory",
+ "conda_environment": "my_conda_environment",
+ "conda_environment_boolean": False,
+ "workspace_geoh5": "",
+ "my_string_parameter": {
+ "label": "My string parameter",
+ "value": "my string value",
+ },
+ "my_integer_parameter": {
+ "label": "My integer parameter",
+ "value": 10,
+ },
+ "my_object_parameter": {
+ "label": "My object parameter",
+ "mesh_type": ["{202C5DB1-A56D-4004-9CAD-BAAFD8899406}"],
+ "value": "",
+ },
+ "my_other_object_parameter": {
+ "label": "My other object parameter",
+ "mesh_type": ["{202C5DB1-A56D-4004-9CAD-BAAFD8899406}"],
+ "value": "",
+ },
+ "my_data_parameter": {
+ "label": "My data parameter",
+ "parent": "my_object_parameter",
+ "association": "Vertex",
+ "data_type": "Float",
+ "value": "",
+ },
+ "my_data_or_value_parameter": {
+ "label": "My other data parameter",
+ "parent": "my_object_parameter",
+ "association": "Vertex",
+ "data_type": "Float",
+ "is_value": True,
+ "property": "",
+ "value": 0.0,
+ },
+ "my_multi_select_data_parameter": {
+ "label": "My multi-select data parameter",
+ "parent": "my_other_object_parameter",
+ "association": "Vertex",
+ "data_type": "Float",
+ "value": [""],
+ "multi_select": True,
+ },
+ "my_faulty_data_parameter": {
+ "label": "My faulty data parameter",
+ "parent": "my_other_object_parameter",
+ "association": "Vertex",
+ "data_type": "Float",
+ "value": "",
+ },
+ "my_absent_uid_parameter": {
+ "label": "My absent uid parameter",
+ "mesh_type": ["{202C5DB1-A56D-4004-9CAD-BAAFD8899406}"],
+ "value": "{00000000-0000-0000-0000-000000000000}",
+ },
+ "my_radio_label_parameter": {
+ "label": "my radio label parameter",
+ "original_label": "option 1",
+ "alternate_label": "option 2",
+ "value": "option_1",
+ },
+}
+
+
@pytest.fixture
def sample_uijson(tmp_path):
uijson_path = tmp_path / "test.ui.json"
- geoh5_path = tmp_path / "test.geoh5"
+ geoh5_path = tmp_path / f"{__name__}.geoh5"
+ ui_dict = deepcopy(SAMPLE)
with Workspace.create(geoh5_path) as workspace:
pts = Points.create(workspace, name="test", vertices=np.random.random((10, 3)))
data = pts.add_data({"my data": {"values": np.random.random(10)}})
other_pts = Points.create(
workspace, name="other test", vertices=np.random.random((10, 3))
)
+ ui_dict["geoh5"] = str(geoh5_path)
+ ui_dict["my_object_parameter"]["value"] = str(pts.uid)
+ ui_dict["my_other_object_parameter"]["value"] = str(other_pts.uid)
+ ui_dict["my_data_parameter"]["value"] = str(data.uid)
+ ui_dict["my_faulty_data_parameter"]["value"] = str(data.uid)
+ ui_dict["my_multi_select_data_parameter"]["value"] = [str(data.uid)]
+
with open(uijson_path, mode="w", encoding="utf8") as file:
- file.write(
- json.dumps(
- {
- "version": "0.1.0",
- "title": "my application",
- "geoh5": str(geoh5_path),
- "run_command": "python -m my_module",
- "run_command_boolean": True,
- "monitoring_directory": "my_monitoring_directory",
- "conda_environment": "my_conda_environment",
- "conda_environment_boolean": False,
- "workspace_geoh5": str(geoh5_path),
- "my_string_parameter": {
- "label": "My string parameter",
- "value": "my string value",
- },
- "my_integer_parameter": {
- "label": "My integer parameter",
- "value": 10,
- },
- "my_object_parameter": {
- "label": "My object parameter",
- "mesh_type": ["{202C5DB1-A56D-4004-9CAD-BAAFD8899406}"],
- "value": str(pts.uid),
- },
- "my_other_object_parameter": {
- "label": "My other object parameter",
- "mesh_type": ["{202C5DB1-A56D-4004-9CAD-BAAFD8899406}"],
- "value": str(other_pts.uid),
- },
- "my_data_parameter": {
- "label": "My data parameter",
- "parent": "my_object_parameter",
- "association": "Vertex",
- "data_type": "Float",
- "value": str(data.uid),
- },
- "my_data_or_value_parameter": {
- "label": "My other data parameter",
- "parent": "my_object_parameter",
- "association": "Vertex",
- "data_type": "Float",
- "is_value": True,
- "property": "",
- "value": 0.0,
- },
- "my_multi_select_data_parameter": {
- "label": "My multi-select data parameter",
- "parent": "my_other_object_parameter",
- "association": "Vertex",
- "data_type": "Float",
- "value": [str(data.uid)],
- "multi_select": True,
- },
- "my_faulty_data_parameter": {
- "label": "My faulty data parameter",
- "parent": "my_other_object_parameter",
- "association": "Vertex",
- "data_type": "Float",
- "value": str(data.uid),
- },
- "my_absent_uid_parameter": {
- "label": "My absent uid parameter",
- "mesh_type": ["{202C5DB1-A56D-4004-9CAD-BAAFD8899406}"],
- "value": "{00000000-0000-0000-0000-000000000000}",
- },
- }
- )
- )
+ file.write(json.dumps(ui_dict))
return uijson_path
def test_uijson(sample_uijson):
- class MyUIJson(BaseUIJson):
+ class MyUIJson(UIJson):
my_string_parameter: StringForm
my_integer_parameter: IntegerForm
+
my_object_parameter: ObjectForm
my_other_object_parameter: ObjectForm
my_data_parameter: DataForm
@@ -140,6 +155,7 @@ class MyUIJson(BaseUIJson):
my_multi_select_data_parameter: MultiSelectDataForm
my_faulty_data_parameter: DataForm
my_absent_uid_parameter: ObjectForm
+ my_radio_button_parameter: RadioLabelForm
uijson = MyUIJson.read(sample_uijson)
with pytest.raises(UIJsonError) as err:
@@ -164,9 +180,9 @@ def generate_test_uijson(workspace: Workspace, uijson, data: dict):
def test_allow_extra(tmp_path):
- ws = Workspace(tmp_path / "test.geoh5")
+ ws = Workspace(tmp_path / f"{__name__}.geoh5")
- class MyUIJson(BaseUIJson):
+ class MyUIJson(UIJson):
my_string_parameter: StringForm
kwargs = {
@@ -186,12 +202,12 @@ class MyUIJson(BaseUIJson):
def test_multiple_validations(tmp_path):
- ws = Workspace(tmp_path / "test.geoh5")
+ ws = Workspace(tmp_path / f"{__name__}.geoh5")
pts = Points.create(ws, name="test", vertices=np.random.random((10, 3)))
other_pts = pts.copy(name="other test")
data = pts.add_data({"my_data": {"values": np.random.randn(10)}})
- class MyUIJson(BaseUIJson):
+ class MyUIJson(UIJson):
my_object_parameter: ObjectForm
my_other_object_parameter: ObjectForm
my_data_parameter: DataForm
@@ -206,6 +222,7 @@ class MyUIJson(BaseUIJson):
"label": "other test",
"mesh_type": [Points],
"value": other_pts.uid,
+ "optional": True,
},
"my_data_parameter": {
"label": "data",
@@ -229,16 +246,13 @@ class MyUIJson(BaseUIJson):
"Object's mesh type must be one of []"
in str(err.value)
)
- assert "Dependency my_other_object_parameter must be either optional or" in str(
- err.value
- )
def test_validate_dependency_type_validation(tmp_path):
- ws = Workspace(tmp_path / "test.geoh5")
+ ws = Workspace(tmp_path / f"{__name__}.geoh5")
# BoolForm dependency is valid
- class MyUIJson(BaseUIJson):
+ class MyUIJson(UIJson):
my_parameter: BoolForm
my_dependent_parameter: StringForm
@@ -254,11 +268,12 @@ class MyUIJson(BaseUIJson):
},
}
uijson = generate_test_uijson(ws, uijson=MyUIJson, data=kwargs)
+
params = uijson.to_params()
assert params["my_dependent_parameter"] == "test"
# Optional non-bool dependency is valid
- class MyUIJson(BaseUIJson):
+ class MyUIJson(UIJson):
my_parameter: StringForm
my_dependent_parameter: StringForm
@@ -270,19 +285,19 @@ class MyUIJson(BaseUIJson):
# Non-optional non-bool dependency is invalid
kwargs["my_parameter"].pop("optional")
- msg = "Dependency my_parameter must be either optional or of boolean type"
+ msg = "Dependency form 'my_parameter' must be either optional or of boolean type"
with pytest.raises(UIJsonError, match=msg):
uijson = generate_test_uijson(ws, uijson=MyUIJson, data=kwargs)
_ = uijson.to_params()
def test_parent_child_validation(tmp_path):
- ws = Workspace(tmp_path / "test.geoh5")
+ ws = Workspace(tmp_path / f"{__name__}.geoh5")
pts = Points.create(ws, name="test", vertices=np.random.random((10, 3)))
data = pts.add_data({"my_data": {"values": np.random.randn(10)}})
other_pts = pts.copy(name="other test")
- class MyUIJson(BaseUIJson):
+ class MyUIJson(UIJson):
my_object_parameter: ObjectForm
my_data_parameter: DataForm
@@ -316,10 +331,10 @@ class MyUIJson(BaseUIJson):
def test_mesh_type_validation(tmp_path):
- ws = Workspace(tmp_path / "test.geoh5")
+ ws = Workspace(tmp_path / f"{__name__}.geoh5")
pts = Points.create(ws, name="test", vertices=np.random.random((10, 3)))
- class MyUIJson(BaseUIJson):
+ class MyUIJson(UIJson):
my_object_parameter: ObjectForm
kwargs = {
@@ -343,9 +358,9 @@ class MyUIJson(BaseUIJson):
def test_deprecated_annotation(tmp_path, caplog):
- geoh5 = Workspace(tmp_path / "test.geoh5")
+ geoh5 = Workspace(tmp_path / f"{__name__}.geoh5")
- class MyUIJson(BaseUIJson):
+ class MyUIJson(UIJson):
my_parameter: Deprecated
with caplog.at_level(logging.WARNING):
@@ -363,7 +378,7 @@ class MyUIJson(BaseUIJson):
def test_grouped_forms(tmp_path):
- class MyUIJson(BaseUIJson):
+ class MyUIJson(UIJson):
my_param: IntegerForm
my_grouped_param: FloatForm
my_other_grouped_param: FloatForm
@@ -372,6 +387,8 @@ class MyUIJson(BaseUIJson):
"my_param": {
"label": "a",
"value": 1,
+ "group": "my_group",
+ "groupOptional": True,
},
"my_grouped_param": {
"label": "b",
@@ -385,23 +402,22 @@ class MyUIJson(BaseUIJson):
},
}
- with Workspace(tmp_path / "test.geoh5") as ws:
+ with Workspace(tmp_path / f"{__name__}.geoh5") as ws:
uijson = generate_test_uijson(ws, uijson=MyUIJson, data=kwargs)
- groups = uijson._groups
- assert "my_group" in groups
- assert "my_grouped_param" in groups["my_group"]
- assert "my_other_grouped_param" in groups["my_group"]
+ dependencies = uijson._group_dependencies
+ assert dependencies.get("my_group") == uijson.my_param
+
+ uijson.set_enabled(my_param=False)
+
+ assert uijson.is_enabled("my_grouped_param") is False
+ assert uijson.is_enabled("my_other_grouped_param") is False
def test_disabled_forms(tmp_path):
- class MyUIJson(BaseUIJson):
+ class MyUIJson(UIJson):
my_param: IntegerForm
my_other_param: IntegerForm
- my_grouped_param: FloatForm
- my_other_grouped_param: FloatForm
- my_group_disabled_param: FloatForm
- my_other_group_disabled_param: FloatForm
kwargs = {
"my_param": {
@@ -413,122 +429,160 @@ class MyUIJson(BaseUIJson):
"value": 2,
"enabled": False,
},
- "my_grouped_param": {
- "label": "c",
- "group": "my_group",
- "value": 1.0,
- },
- "my_other_grouped_param": {
- "label": "d",
- "group": "my_group",
- "value": 2.0,
- },
- "my_group_disabled_param": {
- "label": "e",
- "group": "my_other_group",
+ }
+
+ with Workspace(tmp_path / f"{__name__}.geoh5") as ws:
+ uijson = generate_test_uijson(ws, uijson=MyUIJson, data=kwargs)
+
+ assert uijson.is_enabled("my_param")
+ assert not uijson.is_enabled("my_other_param")
+
+ params = uijson.to_params()
+ assert "my_param" in params
+ assert "my_other_param" not in params
+
+
+def test_disabled_group_optional_forms(tmp_path):
+ class MyUIJson(UIJson):
+ group_leader: FloatForm
+ dependent: FloatForm
+
+ kwargs = {
+ "group_leader": {
+ "label": "a",
+ "group": "some_group",
"group_optional": True,
"enabled": False,
"value": 3.0,
},
- "my_other_group_disabled_param": {
- "label": "f",
- "group": "my_other_group",
+ "dependent": {
+ "label": "b",
+ "group": "some_group",
"value": 4.0,
+ "enabled": True,
},
}
- with Workspace(tmp_path / "test.geoh5") as ws:
+ with Workspace(tmp_path / f"{__name__}.geoh5") as ws:
uijson = generate_test_uijson(ws, uijson=MyUIJson, data=kwargs)
- assert not uijson.is_disabled("my_param")
- assert uijson.is_disabled("my_other_param")
- assert not uijson.is_disabled("my_grouped_param")
- assert not uijson.is_disabled("my_other_grouped_param")
- assert uijson.is_disabled("my_group_disabled_param")
- assert uijson.is_disabled("my_other_group_disabled_param")
+ assert not uijson.is_enabled("group_leader")
+ assert not uijson.is_enabled("dependent")
params = uijson.to_params()
- assert "my_param" in params
- assert "my_other_param" not in params
-
+ assert "group_leader" not in params
+ assert "dependent" not in params
+
+
+@pytest.mark.parametrize(
+ ("dtype", "lead_state", "outcome"),
+ [
+ ("enabled", True, True),
+ ("enabled", False, False),
+ ("disabled", True, False),
+ ("disabled", False, True),
+ ("show", True, True),
+ ("show", False, False),
+ ("hide", True, False),
+ ("hide", False, True),
+ ],
+)
+def test_disabled_dependency_forms(tmp_path, dtype, lead_state, outcome):
+ class MyUIJson(UIJson):
+ leader: FloatForm
+ dependent: FloatForm
-def test_unknown_uijson(tmp_path):
- ws = Workspace.create(tmp_path / "test.geoh5")
- pts = Points.create(ws, name="my points", vertices=np.random.random((10, 3)))
- data = pts.add_data({"my data": {"values": np.random.random(10)}})
kwargs = {
- "version": "0.1.0",
- "title": "my application",
- "geoh5": str(tmp_path / "test.geoh5"),
- "run_command": "python -m my_module",
- "monitoring_directory": None,
- "conda_environment": "test",
- "workspace_geoh5": None,
- "my_string_parameter": {
- "label": "my string parameter",
- "value": "my string value",
- },
- "my_radio_label_parameter": {
- "label": "my radio label parameter",
- "original_label": "option 1",
- "alternate_label": "option 2",
- "value": "option_1",
- },
- "my_integer_parameter": {
- "label": "my integer parameter",
- "value": 10,
- },
- "my_object_parameter": {
- "label": "my object parameter",
- "mesh_type": "{202C5DB1-A56D-4004-9CAD-BAAFD8899406}",
- "value": str(pts.uid),
- },
- "my_data_parameter": {
- "label": "My data parameter",
- "parent": "my_object_parameter",
- "association": "Vertex",
- "data_type": "Float",
- "value": str(data.uid),
- },
- "my_data_or_value_parameter": {
- "label": "My other data parameter",
- "parent": "my_object_parameter",
- "association": "Vertex",
- "data_type": "Float",
- "is_value": True,
- "property": "",
- "value": 0.0,
+ "leader": {
+ "label": "a",
+ "group": "some_group",
+ "enabled": lead_state,
+ "optional": True,
+ "value": 3.0,
},
- "my_multi_choice_data_parameter": {
- "label": "My multi-choice data parameter",
- "parent": "my_object_parameter",
- "association": "Vertex",
- "data_type": "Float",
- "value": [str(data.uid)],
- "multi_select": True,
+ "dependent": {
+ "label": "b",
+ "group": "some_group",
+ "dependency": "leader",
+ "dependencyType": dtype,
+ "value": 4.0,
+ "enabled": True,
},
- "my_optional_parameter": {
- "label": "my optional parameter",
- "value": 2.0,
+ }
+
+ with Workspace(tmp_path / f"{__name__}.geoh5") as ws:
+ uijson = generate_test_uijson(ws, uijson=MyUIJson, data=kwargs)
+
+ assert uijson.is_enabled("dependent") == outcome
+
+ # Change the state and check the dependent
+ uijson.set_enabled(dependent=not outcome)
+ assert uijson.leader.enabled is not lead_state
+
+ # Do reverse change
+ uijson.set_enabled(leader=lead_state)
+ assert uijson.dependent.enabled is outcome
+
+
+@pytest.mark.parametrize(
+ ("lead_state", "dep_state", "outcome"),
+ [
+ (True, True, False),
+ (True, False, True),
+ (False, False, False),
+ ],
+)
+def test_double_dependency_state(tmp_path, lead_state, dep_state, outcome):
+ class MyUIJson(UIJson):
+ group_leader: FloatForm
+ dependent: FloatForm
+ sub_dependent: FloatForm
+
+ kwargs = {
+ "group_leader": {
+ "label": "a",
+ "group": "some_group",
+ "enabled": lead_state,
"optional": True,
- "enabled": False,
- },
- "my_group_optional_parameter": {
- "label": "my group optional parameter",
- "value": 3.0,
- "group": "my group",
"group_optional": True,
- "enabled": False,
+ "value": 3.0,
},
- "my_grouped_parameter": {
- "label": "my grouped parameter",
+ "dependent": {
+ "label": "b",
+ "group": "some_group",
"value": 4.0,
- "group": "my group",
+ "enabled": dep_state,
+ "optional": True,
+ },
+ "sub_dependent": {
+ "label": "c",
+ "group": "some_group",
+ "dependency": "dependent",
+ "dependencyType": "disabled",
+ "value": 4.0,
+ "enabled": True,
},
}
- with open(tmp_path / "test.ui.json", mode="w", encoding="utf8") as file:
- file.write(json.dumps(kwargs))
- uijson = BaseUIJson.read(tmp_path / "test.ui.json")
+
+ with Workspace(tmp_path / f"{__name__}.geoh5") as ws:
+ uijson = generate_test_uijson(ws, uijson=MyUIJson, data=kwargs)
+
+ assert uijson.is_enabled("sub_dependent") == outcome
+
+ # Setting at lower level doesn't change the lead group_optional
+ uijson.set_enabled(dependent=not dep_state)
+ assert uijson.group_leader.enabled is lead_state
+ # But changes the codependent
+ assert uijson.sub_dependent.enabled is dep_state
+
+ # Set state at the lead level doesn't change the state of low level
+ uijson.set_enabled(group_leader=not lead_state)
+ assert uijson.sub_dependent.enabled is not uijson.dependent.enabled
+ assert uijson.sub_dependent.enabled is dep_state
+
+
+def test_unknown_uijson(tmp_path, sample_uijson):
+ uijson = UIJson.read(sample_uijson)
uijson.write(tmp_path / "test_copy.ui.json")
assert isinstance(uijson.my_string_parameter, StringForm)
@@ -537,32 +591,38 @@ def test_unknown_uijson(tmp_path):
assert isinstance(uijson.my_object_parameter, ObjectForm)
assert isinstance(uijson.my_data_parameter, DataForm)
assert isinstance(uijson.my_data_or_value_parameter, DataOrValueForm)
- assert isinstance(uijson.my_multi_choice_data_parameter, MultiSelectDataForm)
- params = uijson.to_params()
- assert params["my_object_parameter"].uid == pts.uid
- assert params["my_data_parameter"].uid == data.uid
- assert params["my_data_or_value_parameter"] == 0.0
- assert params["my_multi_choice_data_parameter"][0].uid == data.uid
- assert "my_optional_parameter" not in params
- assert "my_group_optional_parameter" not in params
- assert "my_grouped_parameter" not in params
+ assert isinstance(uijson.my_multi_select_data_parameter, MultiSelectDataForm)
- re_loaded = BaseUIJson.read(tmp_path / "test_copy.ui.json")
+ params = uijson.to_params(validate=False)
+
+ with Workspace(tmp_path / f"{__name__}.geoh5") as ws:
+ assert params["my_object_parameter"].uid == ws.get_entity("test")[0].uid
+ assert params["my_data_parameter"].uid == ws.get_entity("my data")[0].uid
+ assert params["my_data_or_value_parameter"] == 0.0
+ assert (
+ params["my_multi_select_data_parameter"][0].uid
+ == ws.get_entity("my data")[0].uid
+ )
+ assert "my_optional_parameter" not in params
+ assert "my_group_optional_parameter" not in params
+ assert "my_grouped_parameter" not in params
+
+ re_loaded = UIJson.read(tmp_path / "test_copy.ui.json")
for name in uijson.model_fields_set:
assert getattr(re_loaded, name) == getattr(uijson, name)
def test_str_and_repr(tmp_path):
- Workspace.create(tmp_path / "test.geoh5")
+ Workspace.create(tmp_path / f"{__name__}.geoh5")
- class MyUIJson(BaseUIJson):
+ class MyUIJson(UIJson):
param: StringForm
uijson = MyUIJson(
version="0.1.0",
title="my application",
- geoh5=str(tmp_path / "test.geoh5"),
+ geoh5=str(tmp_path / f"{__name__}.geoh5"),
run_command="python -m my_module",
monitoring_directory=None,
conda_environment="test",
@@ -574,11 +634,6 @@ class MyUIJson(BaseUIJson):
repr_uijson = repr(uijson)
assert "UIJson('my application')" in repr_uijson
assert '"version": "0.1.0"' in str_uijson
- uijson.write(tmp_path / "test.ui.json")
- str_uijson = str(uijson)
- repr_uijson = repr(uijson)
- assert "UIJson('test.ui.json')" in repr_uijson
- assert '"version": "0.1.0"' in str_uijson
def test_geoh5_validate_extension(tmp_path):
@@ -586,7 +641,7 @@ def test_geoh5_validate_extension(tmp_path):
h5file.touch()
with pytest.raises(ValidationError, match="must have a '.geoh5' file extension."):
- _ = BaseUIJson(
+ _ = UIJson(
version="0.1.0",
title="my application",
geoh5=str(h5file),
@@ -603,9 +658,9 @@ def test_geoh5_validate_extension(tmp_path):
def test_fill_in_place(tmp_path):
- ws = Workspace(tmp_path / "test.geoh5")
+ ws = Workspace(tmp_path / f"{__name__}.geoh5")
- class MyUIJson(BaseUIJson):
+ class MyUIJson(UIJson):
my_string_parameter: StringForm
my_int_parameter: IntegerForm
@@ -625,9 +680,9 @@ class MyUIJson(BaseUIJson):
def test_fill_copy(tmp_path):
- ws = Workspace(tmp_path / "test.geoh5")
+ ws = Workspace(tmp_path / f"{__name__}.geoh5")
- class MyUIJson(BaseUIJson):
+ class MyUIJson(UIJson):
my_string_parameter: StringForm
uijson = generate_test_uijson(
@@ -648,9 +703,9 @@ class MyUIJson(BaseUIJson):
def test_fill_truthy_value_leaves_updates_empty(tmp_path):
"""A form with a truthy value not in kwargs produces no updates."""
- ws = Workspace(tmp_path / "test.geoh5")
+ ws = Workspace(tmp_path / f"{__name__}.geoh5")
- class MyUIJson(BaseUIJson):
+ class MyUIJson(UIJson):
my_param: FloatForm
uijson = generate_test_uijson(
@@ -666,9 +721,9 @@ class MyUIJson(BaseUIJson):
def test_fill_kwargs_re_enables_form(tmp_path):
- ws = Workspace(tmp_path / "test.geoh5")
+ ws = Workspace(tmp_path / f"{__name__}.geoh5")
- class MyUIJson(BaseUIJson):
+ class MyUIJson(UIJson):
my_param: FloatForm
uijson = generate_test_uijson(
@@ -685,11 +740,11 @@ class MyUIJson(BaseUIJson):
def test_fill_with_uuid_value(tmp_path):
- ws = Workspace(tmp_path / "test.geoh5")
+ ws = Workspace(tmp_path / f"{__name__}.geoh5")
pts = Points.create(ws, name="pts", vertices=np.random.random((10, 3)))
pts2 = Points.create(ws, name="pts2", vertices=np.random.random((10, 3)))
- class MyUIJson(BaseUIJson):
+ class MyUIJson(UIJson):
my_object_parameter: ObjectForm
uijson = generate_test_uijson(
@@ -714,9 +769,9 @@ class MyUIJson(BaseUIJson):
def test_to_ui_json_group_creates_group(tmp_path):
- ws = Workspace.create(tmp_path / "test.geoh5")
+ ws = Workspace.create(tmp_path / f"{__name__}.geoh5")
- class MyUIJson(BaseUIJson):
+ class MyUIJson(UIJson):
my_string_parameter: StringForm
uijson = generate_test_uijson(
@@ -731,9 +786,9 @@ class MyUIJson(BaseUIJson):
def test_to_ui_json_group_default_name(tmp_path):
- ws = Workspace.create(tmp_path / "test.geoh5")
+ ws = Workspace.create(tmp_path / f"{__name__}.geoh5")
- class MyUIJson(BaseUIJson):
+ class MyUIJson(UIJson):
my_string_parameter: StringForm
uijson = generate_test_uijson(
@@ -747,9 +802,9 @@ class MyUIJson(BaseUIJson):
def test_to_ui_json_group_custom_name(tmp_path):
- ws = Workspace.create(tmp_path / "test.geoh5")
+ ws = Workspace.create(tmp_path / f"{__name__}.geoh5")
- class MyUIJson(BaseUIJson):
+ class MyUIJson(UIJson):
my_string_parameter: StringForm
uijson = generate_test_uijson(
@@ -763,9 +818,9 @@ class MyUIJson(BaseUIJson):
def test_to_ui_json_group_out_group_properties(tmp_path):
- ws = Workspace.create(tmp_path / "test.geoh5")
+ ws = Workspace.create(tmp_path / f"{__name__}.geoh5")
- class MyUIJson(BaseUIJson):
+ class MyUIJson(UIJson):
my_string_parameter: StringForm
uijson = generate_test_uijson(
@@ -780,10 +835,10 @@ class MyUIJson(BaseUIJson):
def test_to_ui_json_group_without_workspace(tmp_path):
- geoh5_path = tmp_path / "test.geoh5"
+ geoh5_path = tmp_path / f"{__name__}.geoh5"
Workspace.create(geoh5_path)
- class MyUIJson(BaseUIJson):
+ class MyUIJson(UIJson):
my_string_parameter: StringForm
uijson = MyUIJson(