diff --git a/geoh5py/shared/exceptions.py b/geoh5py/shared/exceptions.py index 70e51125a..cab77c5e5 100644 --- a/geoh5py/shared/exceptions.py +++ b/geoh5py/shared/exceptions.py @@ -56,31 +56,6 @@ def message(cls, name, err): return f"Malformed ui.json dictionary for parameter '{name}'. {err}" -class UIJsonFormatError(BaseValidationError): - def __init__(self, name, msg): - super().__init__(f"Invalid UIJson format for parameter '{name}'. {msg}") - - @classmethod - def message(cls, name, value, validation): - pass - - -class AggregateValidationError(BaseValidationError): - def __init__( - self, - name: str, - value: list[BaseValidationError], - ): - super().__init__(AggregateValidationError.message(name, value)) - - @classmethod - def message(cls, name, value, validation=None): - msg = f"\n\nValidation of '{name}' collected {len(value)} errors:\n" - for i, err in enumerate(value): - msg += f"\t{i}. {err!s}\n" - return msg - - class OptionalValidationError(BaseValidationError): """Error if None value provided to non-optional parameter.""" @@ -140,24 +115,6 @@ def message(cls, name, value, validation=None): return f"Must provide at least one {name}. Options are: {opts}" -class TypeUIDValidationError(BaseValidationError): - """Error on type uid validation.""" - - def __init__(self, name: str, value, validation: list[str]): - super().__init__( - TypeUIDValidationError.message( - name, value.default_type_uid(), list(validation) - ) - ) - - @classmethod - def message(cls, name, value, validation): - return ( - f"Type uid '{value}' provided for '{name}' is invalid." - + iterable_message(validation) - ) - - class RequiredValidationError(BaseValidationError): def __init__(self, name: str): super().__init__(RequiredValidationError.message(name)) @@ -167,47 +124,6 @@ def message(cls, name, value=None, validation=None): return f"Missing required parameter: '{name}'." -class InCollectionValidationError(BaseValidationError): - collection = "Collection" - item = "data" - - def __init__(self, name: str, value: list[str]): - super().__init__(self.message(name, value)) - - @classmethod - def message(cls, name, value, validation=None): - _ = validation - return f"{cls.collection}: '{name}' is missing required {cls.item}(s): {value}." - - -class RequiredFormMemberValidationError(InCollectionValidationError): - collection = "Form" - item = "member" - - -class RequiredUIJsonParameterValidationError(InCollectionValidationError): - collection = "UIJson" - item = "parameter" - - -class RequiredWorkspaceObjectValidationError(InCollectionValidationError): - collection = "Workspace" - item = "object" - - -class RequiredObjectDataValidationError(BaseValidationError): - def __init__(self, name: str, value: list[tuple[str, str]]): - super().__init__(self.message(name, value)) - - @classmethod - def message(cls, name, value, validation=None): - _ = validation - return ( - f"Workspace: '{name}' object(s) {[k[0] for k in value]} " - f"are missing required children {[k[1] for k in value]}." - ) - - class ShapeValidationError(BaseValidationError): """Error on shape validation.""" diff --git a/geoh5py/ui_json/enforcers.py b/geoh5py/ui_json/enforcers.py deleted file mode 100644 index 7faf128df..000000000 --- a/geoh5py/ui_json/enforcers.py +++ /dev/null @@ -1,359 +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 abc import ABC, abstractmethod -from typing import Any -from uuid import UUID - -from geoh5py.shared.exceptions import ( - AggregateValidationError, - BaseValidationError, - InCollectionValidationError, - RequiredFormMemberValidationError, - RequiredObjectDataValidationError, - RequiredUIJsonParameterValidationError, - RequiredWorkspaceObjectValidationError, - TypeUIDValidationError, - TypeValidationError, - UUIDValidationError, - ValueValidationError, -) -from geoh5py.shared.utils import SetDict, is_uuid - - -class Enforcer(ABC): - """ - Base class for rule enforcers. - - :param enforcer_type: Type of enforcer. - :param validations: Value(s) to validate parameter value against. - """ - - enforcer_type: str = "" - - def __init__(self, validations: set): - self.validations = validations - - @abstractmethod - def rule(self, value: Any): - """True if 'value' adheres to enforcers rule.""" - - @abstractmethod - def enforce(self, name: str, value: Any): - """Enforces rule on 'name' parameter's 'value'.""" - - def __eq__(self, other) -> bool: - """Equal if same type and validations.""" - - is_equal = False - if isinstance(other, type(self)): - is_equal = other.validations == self.validations - - return is_equal - - def __str__(self): - return f"<{type(self).__name__}> : {self.validations}" - - -class TypeEnforcer(Enforcer): - """ - Enforces valid type(s). - - :param validations: Valid type(s) for parameter value. - :raises TypeValidationError: If value is not a valid type. - """ - - enforcer_type: str = "type" - - def __init__(self, validations: set[type]): - super().__init__(validations) - - def enforce(self, name: str, value: Any): - """Administers rule to enforce type validation.""" - - if not self.rule(value): - raise TypeValidationError( - name, type(value).__name__, [k.__name__ for k in self.validations] - ) - - def rule(self, value) -> bool: - """True if value is one of the valid types.""" - return any(isinstance(value, k) for k in self.validations.union({type(None)})) - - -class ValueEnforcer(Enforcer): - """ - Enforces restricted value choices. - - :param validations: Valid parameter value(s). - :raises ValueValidationError: If value is not a valid value - choice. - """ - - enforcer_type = "value" - - def __init__(self, validations: set[Any]): - super().__init__(validations) - - def enforce(self, name: str, value: Any): - """Administers rule to enforce value validation.""" - - if not self.rule(value): - raise ValueValidationError(name, value, list(self.validations)) - - def rule(self, value: Any) -> bool: - """True if value is a valid choice.""" - return value in self.validations - - -class TypeUIDEnforcer(Enforcer): - """ - Enforces restricted geoh5 entity_type uid(s). - - :param validations: Valid geoh5py object type uid(s). - :raises TypeValidationError: If value is not a valid type uid. - """ - - enforcer_type = "type_uid" - - def __init__(self, validations: set[str]): - super().__init__(validations) - - def enforce(self, name: str, value: Any): - """Administers rule to enforce type uid validation.""" - - if not self.rule(value): - raise TypeUIDValidationError(name, value, list(self.validations)) - - def rule(self, value: Any) -> bool: - """True if value is a valid type uid.""" - return self.validations == {""} or value.default_type_uid() in [ - UUID(k) for k in self.validations - ] - - -class UUIDEnforcer(Enforcer): - """ - Enforces valid uuid string. - - :param validations: No validations needed, can be empty set. - :raises UUIDValidationError: If value is not a valid uuid string. - """ - - enforcer_type = "uuid" - - def __init__(self, validations=None): - super().__init__(validations) - - def enforce(self, name: str, value: Any): - """Administers rule to check if valid uuid.""" - - if not self.rule(value): - raise UUIDValidationError( - name, - value, - ) - - def rule(self, value: Any) -> bool: - """True if value is a valid uuid string.""" - - if value is None: - return True - - return is_uuid(value) - - -class RequiredEnforcer(Enforcer): - """ - Enforces required items in a collection. - - :param validations: Items that are required in the collection. - :raises InCollectionValidationError: If collection is missing one of - the required parameters/members. - """ - - enforcer_type = "required" - validation_error = InCollectionValidationError - - def __init__(self, validations: set[str | tuple[str, str]]): - super().__init__(validations) - - def enforce(self, name: str, value: Any): - """Administers rule to check if required items in collection.""" - - if not self.rule(value): - raise self.validation_error( - name, - [k for k in self.validations if k not in self.collection(value)], - ) - - def rule(self, value: Any) -> bool: - """True if all required parameters are in 'value' collection.""" - return all(k in self.collection(value) for k in self.validations) - - def collection(self, value: Any) -> list[Any]: - """Returns the collection to check for required items.""" - return value - - -class RequiredUIJsonParameterEnforcer(RequiredEnforcer): - enforcer_type = "required_uijson_parameters" - validation_error = RequiredUIJsonParameterValidationError - - -class RequiredFormMemberEnforcer(RequiredEnforcer): - enforcer_type = "required_form_members" - validation_error = RequiredFormMemberValidationError - - -class RequiredWorkspaceObjectEnforcer(RequiredEnforcer): - enforcer_type = "required_workspace_object" - validation_error = RequiredWorkspaceObjectValidationError - - def rule(self, value: Any) -> bool: - """True if all objects are in the workspace.""" - validations = [value[k]["value"].uid for k in self.validations] - return all(k in self.collection(value) for k in validations) - - def collection(self, value: dict[str, Any]) -> list[UUID]: - return list(value["geoh5"].list_entities_name) - - -class RequiredObjectDataEnforcer(Enforcer): - enforcer_type = "required_object_data" - validation_error = RequiredObjectDataValidationError - - def enforce(self, name: str, value: Any): - """Administers rule to check if required items in collection.""" - - if not self.rule(value): - raise self.validation_error( - name, - [ - k - for i, k in enumerate(self.validations) - if value[k[1]]["value"].uid not in self.collection(value)[i] - ], - ) - - def rule(self, value: Any) -> bool: - """True if object/data have parent/child relationship.""" - return all( - value[k[1]]["value"].uid in self.collection(value)[i] - for i, k in enumerate(self.validations) - ) - - def collection(self, value: dict[str, Any]) -> list[list[UUID]]: - """Returns list of children for all parents in validations.""" - return [ - [c.uid for c in value[k[0]]["value"].children] for k in self.validations - ] - - -class EnforcerPool: - """ - Validate data on a collection of enforcers. - - :param name: Name of parameter. - :param enforcers: List (pool) of enforcers. - """ - - enforcer_types = { - "type": TypeEnforcer, - "value": ValueEnforcer, - "uuid": UUIDEnforcer, - "type_uid": TypeUIDEnforcer, - "required": RequiredEnforcer, - "required_uijson_parameters": RequiredUIJsonParameterEnforcer, - "required_form_members": RequiredFormMemberEnforcer, - "required_workspace_object": RequiredWorkspaceObjectEnforcer, - "required_object_data": RequiredObjectDataEnforcer, - } - - def __init__(self, name: str, enforcers: list[Enforcer]): - self.name = name - self.enforcers: list[Enforcer] = enforcers - self._errors: list[BaseValidationError] = [] - - @classmethod - def from_validations( - cls, - name: str, - validations: SetDict, - ) -> EnforcerPool: - """ - Create enforcers pool from validations. - - :param name: Name of parameter. - :param validations: Encodes validations as enforcer type and - validation key value pairs. - :param restricted_validations: 0. - - """ - - return cls(name, cls._recruit(validations)) - - @property - def validations(self) -> SetDict: - """Returns an enforcer type / validation dictionary from pool.""" - return SetDict(**{k.enforcer_type: k.validations for k in self.enforcers}) - - @staticmethod - def _recruit(validations: SetDict): - """Recruit enforcers from validations.""" - return [EnforcerPool._recruit_enforcer(k, v) for k, v in validations.items()] - - @staticmethod - def _recruit_enforcer(enforcer_type: str, validation: set) -> Enforcer: - """ - Create enforcer from enforcer type and validation. - - :param enforcer_type: Type of enforcer to create. - :param validation: Enforcer validation. - """ - - if enforcer_type not in EnforcerPool.enforcer_types: - raise ValueError(f"Invalid enforcer type: {enforcer_type}.") - - return EnforcerPool.enforcer_types[enforcer_type](validation) - - def enforce(self, value: Any): - """Enforce rules from all enforcers in the pool.""" - - for enforcer in self.enforcers: - self._capture_error(enforcer, value) - - self._raise_errors() - - def _capture_error(self, enforcer: Enforcer, value: Any): - """Catch and store 'BaseValidationError's for aggregation.""" - try: - enforcer.enforce(self.name, value) - except BaseValidationError as err: - self._errors.append(err) - - def _raise_errors(self): - """Raise errors if any exist, aggregate if more than one.""" - if self._errors: - if len(self._errors) > 1: - raise AggregateValidationError(self.name, self._errors) - raise self._errors.pop() diff --git a/geoh5py/ui_json/parameters.py b/geoh5py/ui_json/parameters.py deleted file mode 100644 index efce9cef9..000000000 --- a/geoh5py/ui_json/parameters.py +++ /dev/null @@ -1,171 +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 typing import Any -from uuid import UUID - -from geoh5py import Workspace -from geoh5py.groups import PropertyGroup -from geoh5py.shared.utils import SetDict -from geoh5py.ui_json.enforcers import EnforcerPool - - -Validation = dict[str, Any] - - -class Parameter: - """ - Basic parameter to store key/value data with validation capabilities. - - :param name: Parameter name. - :param value: The parameters value. - :param enforcers: A collection of enforcers. - """ - - static_validations: dict[str, Any] = {} - - def __init__(self, name: str, value: Any = None): - self.name: str = name - self._value: Any | None = None - self._enforcers: EnforcerPool = EnforcerPool.from_validations( - self.name, self.validations - ) - if value is not None: - self.value = value - - @property - def validations(self): - """Returns a dictionary of static validations.""" - return SetDict(**self.static_validations) - - @property - def value(self): - return self._value - - @value.setter - def value(self, val): - self._value = val - self.validate() - - def validate(self): - """Validates data against the pool of enforcers.""" - self._enforcers.enforce(self.value) - - def __str__(self): - return f"<{type(self).__name__}> : '{self.name}' -> {self.value}" - - -class DynamicallyRestrictedParameter(Parameter): - """Parameter whose validations are set at runtime.""" - - def __init__( - self, name: str, restrictions: Any, enforcer_type="type", value: Any = None - ): - self._restrictions = restrictions - self._enforcer_type = enforcer_type - super().__init__(name, value) - - @property - def restrictions(self): - if not isinstance(self._restrictions, list): - self._restrictions = {self._restrictions} - - return self._restrictions - - @property - def validations(self): - return SetDict(**{self._enforcer_type: self.restrictions}) - - -class ValueRestrictedParameter(DynamicallyRestrictedParameter): - """Parameter with a restricted set of values.""" - - def __init__(self, name: str, restrictions: Any, value: Any = None): - super().__init__(name, restrictions, "value", value) - - -class TypeRestrictedParameter(DynamicallyRestrictedParameter): - """Parameter with a restricted set of types known at runtime only.""" - - def __init__(self, name: str, restrictions: list[Any], value: Any = None): - super().__init__(name, restrictions, "type", value) - - -class TypeUIDRestrictedParameter(DynamicallyRestrictedParameter): - """Parameter with a restricted set of type uids known at runtime only.""" - - def __init__(self, name: str, restrictions: list[UUID], value: Any = None): - super().__init__(name, restrictions, "type_uid", value) - - -class StringParameter(Parameter): - """Parameter for string values.""" - - static_validations = {"type": str} - - -class IntegerParameter(Parameter): - """Parameter for integer values.""" - - static_validations = {"type": int} - - -class FloatParameter(Parameter): - """Parameter for float values.""" - - static_validations = {"type": float} - - -class NumericParameter(Parameter): - """Parameter for generic numeric values.""" - - static_validations = {"type": [int, float]} - - -class BoolParameter(Parameter): - """Parameter for boolean values.""" - - static_validations = {"type": bool} - - def __init__(self, name: str, value: bool = False): - super().__init__(name, value) - - -class StringListParameter(Parameter): - """Parameter for list of strings.""" - - static_validations = {"type": [list, str]} - - # TODO: introduce type alias handling so that TypeEnforcer(list[str], str) - # is possible - - -class WorkspaceParameter(Parameter): - """Parameter for workspace objects.""" - - static_validations = {"type": Workspace} - - -class PropertyGroupParameter(Parameter): - """Parameter for property group objects.""" - - static_validations = {"type": PropertyGroup} diff --git a/tests/ui_json/enforcers_test.py b/tests/ui_json/enforcers_test.py deleted file mode 100644 index 9e068ccdf..000000000 --- a/tests/ui_json/enforcers_test.py +++ /dev/null @@ -1,213 +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 - -import uuid - -import numpy as np -import pytest - -from geoh5py import Workspace -from geoh5py.objects import Points -from geoh5py.shared.exceptions import ( - AggregateValidationError, - RequiredFormMemberValidationError, - RequiredObjectDataValidationError, - RequiredUIJsonParameterValidationError, - RequiredWorkspaceObjectValidationError, - TypeValidationError, - UUIDValidationError, - ValueValidationError, -) -from geoh5py.shared.utils import SetDict -from geoh5py.ui_json.enforcers import ( - EnforcerPool, - RequiredEnforcer, - RequiredFormMemberEnforcer, - RequiredObjectDataEnforcer, - RequiredUIJsonParameterEnforcer, - RequiredWorkspaceObjectEnforcer, - TypeEnforcer, - UUIDEnforcer, - ValueEnforcer, -) - - -def test_enforcer_pool_recruit(): - validations = SetDict( - type={str}, - value={"onlythis"}, - uuid={None}, - required={"me"}, - required_uijson_parameters={"me", "you"}, - required_form_members={"label", "value"}, - required_workspace_object={"data"}, - required_object_data={"object"}, - ) - enforcers = EnforcerPool._recruit(validations) # pylint: disable=protected-access - - assert enforcers == [ - TypeEnforcer({str}), - ValueEnforcer({"onlythis"}), - UUIDEnforcer({None}), - RequiredEnforcer({"me"}), - RequiredUIJsonParameterEnforcer({"me", "you"}), - RequiredFormMemberEnforcer({"label", "value"}), - RequiredWorkspaceObjectEnforcer({"data"}), - RequiredObjectDataEnforcer({"object"}), - ] - - -def test_enforcer_pool_construction(): - pool = EnforcerPool("my_param", [TypeEnforcer({str})]) - assert pool.enforcers == [TypeEnforcer({str})] - - -def test_enforcer_pool_validations(): - validations = SetDict(type=str, value="onlythis") - pool = EnforcerPool.from_validations("my_param", validations) - assert pool.validations == validations - pool = EnforcerPool("my_param", [TypeEnforcer({str}), ValueEnforcer({"onlythis"})]) - assert pool.validations == validations - - -def test_enforcer_pool_from_validations(): - pool = EnforcerPool.from_validations("my_param", {"type": str}) - assert pool.enforcers == [TypeEnforcer(str)] - - -def test_enforcer_pool_raises_single_error(): - enforcers = EnforcerPool("my_param", [TypeEnforcer({str})]) - enforcers.enforce("1") - msg = "Type 'int' provided for 'my_param' is invalid. " - msg += "Must be: 'str'." - with pytest.raises(TypeValidationError, match=msg): - enforcers.enforce(1) - - -def test_enforcer_pool_raises_aggregate_error(): - enforcers = EnforcerPool( - "my_param", [TypeEnforcer({str}), ValueEnforcer({"onlythis"})] - ) - enforcers.enforce("onlythis") - msg = ( - "Validation of 'my_param' collected 2 errors:\n\t" - "0. Type 'int' provided for 'my_param' is invalid" - ) - with pytest.raises(AggregateValidationError, match=msg): - enforcers.enforce(1) - - -def test_enforcer_str(): - enforcer = TypeEnforcer(validations={str}) - assert str(enforcer) == " : {}" - - -def test_type_enforcer(): - enforcer = TypeEnforcer(validations={int}) - assert enforcer.validations == {int} - enforcer.enforce("test", 1) - msg = "Type 'float' provided for 'test' is invalid. Must be: 'int'." - with pytest.raises(TypeValidationError, match=msg): - enforcer.enforce("test", 1.0) - - enforcer = TypeEnforcer(validations=[int, str]) - assert enforcer.validations == [int, str] - - -def test_value_enforcer(): - enforcer = ValueEnforcer(validations=[1, 2, 3]) - enforcer.enforce("test", 1) - msg = "Value '4' provided for 'test' is invalid. " - msg += "Must be one of: '1', '2', '3'." - with pytest.raises(ValueValidationError, match=msg): - enforcer.enforce("test", 4) - - -def test_uuid_enforcer(): - enforcer = UUIDEnforcer() - enforcer.enforce("test", str(uuid.uuid4())) - msg = "Parameter 'test' with value 'notachance' " - msg += "is not a valid uuid string." - with pytest.raises(UUIDValidationError, match=msg): - enforcer.enforce("test", "notachance") - - -def test_required_uijson_parameter_enforcer(): - enforcer = RequiredUIJsonParameterEnforcer(["my_param"]) - msg = r"UIJson: 'my_param' is missing required parameter\(s\): \['my_param'\]." - with pytest.raises(RequiredUIJsonParameterValidationError, match=msg): - enforcer.enforce("my_param", {"label": "my param"}) - - -def test_required_form_member_enforcer(): - enforcer = RequiredFormMemberEnforcer(["my_member"]) - msg = r"Form: 'my_member' is missing required member\(s\): \['my_member'\]." - with pytest.raises(RequiredFormMemberValidationError, match=msg): - enforcer.enforce("my_member", {"label": "my member"}) - - -def test_required_workspace_object_enforcer(tmp_path): - geoh5 = Workspace(tmp_path / "working_file.geoh5") - pts = Points.create(geoh5, vertices=np.random.rand(10, 3), name="my_points") - other_geoh5 = Workspace(tmp_path / "other_file.geoh5") - other_pts = Points.create( - other_geoh5, vertices=np.random.rand(10, 3), name="my_other_points" - ) - - data = {"geoh5": geoh5, "my_points": {"value": pts}} - validations = ["my_points"] - enforcer = RequiredWorkspaceObjectEnforcer(validations) - enforcer.enforce(str(geoh5.h5file.stem), data) - - data["my_points"] = {"value": other_pts} - msg = r"Workspace: 'working_file' is missing required object\(s\): \['my_points'\]." - with pytest.raises(RequiredWorkspaceObjectValidationError, match=msg): - enforcer.enforce(str(geoh5.h5file.stem), data) - - -def test_required_object_data_enforcer(tmp_path): - geoh5 = Workspace(tmp_path / "working_file.geoh5") - pts = Points.create(geoh5, vertices=np.random.rand(10, 3), name="my_points") - my_data = pts.add_data({"my_data": {"values": np.random.rand(10)}}) - other_pts = Points.create( - geoh5, vertices=np.random.rand(10, 3), name="my_other_points" - ) - the_wrong_data = other_pts.add_data( - {"my_other_data": {"values": np.random.rand(10)}} - ) - - data = { - "geoh5": geoh5, - "object": {"value": pts}, - "data": {"value": my_data}, - } - validations = [("object", "data")] - enforcer = RequiredObjectDataEnforcer(validations) - enforcer.enforce(str(geoh5.h5file.stem), data) - - data["data"]["value"] = the_wrong_data - msg = ( - r"Workspace: 'working_file' object\(s\) \['object'\] " - r"are missing required children \['data'\]." - ) - with pytest.raises(RequiredObjectDataValidationError, match=msg): - enforcer.enforce(str(geoh5.h5file.stem), data) diff --git a/tests/ui_json/exceptions_test.py b/tests/ui_json/exceptions_test.py deleted file mode 100644 index b4316e017..000000000 --- a/tests/ui_json/exceptions_test.py +++ /dev/null @@ -1,59 +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 - -import pytest - -from geoh5py.shared.exceptions import ( - RequiredFormMemberValidationError, - RequiredObjectDataValidationError, - RequiredUIJsonParameterValidationError, - RequiredWorkspaceObjectValidationError, -) - - -def test_required_form_member_validation_error(): - msg = r"Form: 'test' is missing required member\(s\): \['member1', 'member2'\]." - with pytest.raises(RequiredFormMemberValidationError, match=msg): - raise RequiredFormMemberValidationError("test", ["member1", "member2"]) - - -def test_required_ui_json_parameter_validation_error(): - msg = r"UIJson: 'test' is missing required parameter\(s\): \['param1', 'param2'\]." - with pytest.raises(RequiredUIJsonParameterValidationError, match=msg): - raise RequiredUIJsonParameterValidationError("test", ["param1", "param2"]) - - -def test_required_workspace_object_validation_error(): - msg = r"Workspace: 'test' is missing required object\(s\): \['obj1', 'obj2'\]." - with pytest.raises(RequiredWorkspaceObjectValidationError, match=msg): - raise RequiredWorkspaceObjectValidationError("test", ["obj1", "obj2"]) - - -def test_required_object_data_validation_error(): - msg = ( - r"Workspace: 'test' object\(s\) \['object1', 'object2'] are " - r"missing required children \['data1', 'data2'\]." - ) - with pytest.raises(RequiredObjectDataValidationError, match=msg): - raise RequiredObjectDataValidationError( - "test", [("object1", "data1"), ("object2", "data2")] - ) diff --git a/tests/ui_json/parameter_test.py b/tests/ui_json/parameter_test.py deleted file mode 100644 index c71180873..000000000 --- a/tests/ui_json/parameter_test.py +++ /dev/null @@ -1,129 +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 - -import pytest - -from geoh5py.shared.exceptions import TypeValidationError, ValueValidationError -from geoh5py.ui_json.parameters import ( - BoolParameter, - FloatParameter, - IntegerParameter, - Parameter, - StringListParameter, - StringParameter, - TypeRestrictedParameter, - ValueRestrictedParameter, -) - - -# pylint: disable=protected-access - - -def test_skip_validation_on_none_value(): - param = StringParameter("my_param", None) - assert param.value is None - - -def test_parameter_validations_on_setting(): - param = StringParameter("my_param") - param.value = "me" - msg = "Type 'int' provided for 'my_param' is invalid. Must be: 'str'." - with pytest.raises(TypeValidationError, match=msg): - param.value = 1 - - -def test_parameter_str_representation(): - param = Parameter("my_param") - assert str(param) == " : 'my_param' -> None" - - -def test_string_parameter_type_validation(): - param = StringParameter("my_param") - msg = "Type 'int' provided for 'my_param' is invalid. " - msg += "Must be: 'str'." - with pytest.raises(TypeValidationError, match=msg): - param.value = 1 - - -def test_string_parameter_optional_validations(): - param = StringParameter("my_param") - param.value = None - param.value = "this is ok" - msg = "Type 'int' provided for 'my_param' is invalid. Must be: 'str'." - with pytest.raises(TypeValidationError, match=msg): - param.value = 1 - - -def test_integer_parameter_type_validation(): - param = IntegerParameter("my_param") - msg = "Type 'str' provided for 'my_param' is invalid. " - msg += "Must be: 'int'." - with pytest.raises(TypeValidationError, match=msg): - param.value = "1" - - -def test_float_parameter_type_validation(): - param = FloatParameter("my_param") - msg = "Type 'int' provided for 'my_param' is invalid. " - msg += "Must be: 'float'." - with pytest.raises(TypeValidationError, match=msg): - param.value = 1 - - -def test_bool_parameter_default(): - param = BoolParameter("my_param") - assert param.value is False - - -def test_bool_parameter_type_validation(): - param = BoolParameter("my_param") - msg = "Type 'str' provided for 'my_param' is invalid. " - msg += "Must be: 'bool'." - with pytest.raises(TypeValidationError, match=msg): - param.value = "butwhy?" - - -def test_string_list_parameter_type_validation(): - param = StringListParameter("my_param") - param.value = "this is ok" - param.value = ["this", "is", "also", "ok"] - msg = "Type 'int' provided for 'my_param' is invalid. Must be one of:" - with pytest.raises(TypeValidationError, match=msg) as info: - param.value = 1 - - assert all(k in str(info.value) for k in ["list", "str"]) - - -def test_type_restricted_parameter_type_validation(): - param = TypeRestrictedParameter("my_param", [str]) - param.value = "this is ok" - msg = "Type 'int' provided for 'my_param' is invalid. Must be: 'str'." - with pytest.raises(TypeValidationError, match=msg): - param.value = 1 - - -def test_value_restricted_parameter_type_validation(): - param = ValueRestrictedParameter("my_param", [1, 2, 3]) - param.value = 1 - msg = "Value '1' provided for 'my_param' is invalid. Must be one of: '1', '2', '3'." - with pytest.raises(ValueValidationError, match=msg): - param.value = "1"