diff --git a/betty/app/__init__.py b/betty/app/__init__.py index b9a47a5d6..a1bf0ea26 100644 --- a/betty/app/__init__.py +++ b/betty/app/__init__.py @@ -65,8 +65,10 @@ AssertionFailed, Fields, OptionalField, - Asserter, AssertionChain, + assert_record, + assert_setattr, + assert_str, ) from betty.warnings import deprecate @@ -158,13 +160,12 @@ def load( ) -> Self: if configuration is None: configuration = cls() - asserter = Asserter() - asserter.assert_record( + assert_record( Fields( OptionalField( "locale", - AssertionChain(asserter.assert_str()) - | asserter.assert_setattr(configuration, "locale"), + AssertionChain(assert_str()) + | assert_setattr(configuration, "locale"), ), ), )(dump) diff --git a/betty/config.py b/betty/config.py index 79854aa07..f9029a221 100644 --- a/betty/config.py +++ b/betty/config.py @@ -41,7 +41,7 @@ from betty.serde.dump import Dumpable, Dump, minimize, VoidableDump, Void from betty.serde.error import SerdeErrorCollection from betty.serde.format import FormatRepository -from betty.serde.load import Asserter, Assertion +from betty.serde.load import Assertion, assert_dict, assert_mapping, assert_sequence if TYPE_CHECKING: from _weakref import ReferenceType @@ -58,7 +58,6 @@ class Configuration(Dumpable): def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) - self._asserter = Asserter() self._on_change_listeners: MutableSequence[ ReferenceType[_ConfigurationListener] ] = [] @@ -503,9 +502,8 @@ def load( configuration = cls() else: configuration._clear_without_dispatch() - asserter = Asserter() with SerdeErrorCollection().assert_valid(): - configuration.append(*asserter.assert_sequence(cls._item_type().load)(dump)) + configuration.append(*assert_sequence(cls._item_type().load)(dump)) return configuration @override @@ -658,9 +656,8 @@ def load( ) -> Self: if configuration is None: configuration = cls() - asserter = Asserter() - dict_dump = asserter.assert_dict()(dump) - mapping = asserter.assert_mapping(cls._item_type().load)( + dict_dump = assert_dict()(dump) + mapping = assert_mapping(cls._item_type().load)( {key: cls._load_key(value, key) for key, value in dict_dump.items()} ) configuration.replace(*mapping.values()) diff --git a/betty/extension/cotton_candy/__init__.py b/betty/extension/cotton_candy/__init__.py index dc8cd98e6..674ca75ab 100644 --- a/betty/extension/cotton_candy/__init__.py +++ b/betty/extension/cotton_candy/__init__.py @@ -40,8 +40,11 @@ AssertionFailed, Fields, OptionalField, - Asserter, AssertionChain, + assert_str, + assert_record, + assert_path, + assert_setattr, ) if TYPE_CHECKING: @@ -95,8 +98,7 @@ def load( dump: Dump, configuration: Self | None = None, ) -> Self: - asserter = Asserter() - hex_value = asserter.assert_str()(dump) + hex_value = assert_str()(dump) if configuration is None: configuration = cls(hex_value) else: @@ -201,8 +203,7 @@ def load( ) -> Self: if configuration is None: configuration = cls() - asserter = Asserter() - asserter.assert_record( + assert_record( Fields( OptionalField( "featured_entities", @@ -236,8 +237,8 @@ def load( ), OptionalField( "logo", - AssertionChain(asserter.assert_path()) - | asserter.assert_setattr(configuration, "logo"), + AssertionChain(assert_path()) + | assert_setattr(configuration, "logo"), ), ) )(dump) diff --git a/betty/extension/gramps/config.py b/betty/extension/gramps/config.py index 4959c90ae..1a1ab7484 100644 --- a/betty/extension/gramps/config.py +++ b/betty/extension/gramps/config.py @@ -11,11 +11,13 @@ from betty.config import Configuration, ConfigurationSequence from betty.serde.dump import minimize, Dump, VoidableDump from betty.serde.load import ( - Asserter, Fields, RequiredField, OptionalField, AssertionChain, + assert_record, + assert_path, + assert_setattr, ) if TYPE_CHECKING: @@ -61,13 +63,12 @@ def load( ) -> Self: if configuration is None: configuration = cls() - asserter = Asserter() - asserter.assert_record( + assert_record( Fields( RequiredField( "file", - AssertionChain(asserter.assert_path()) - | asserter.assert_setattr(configuration, "file_path"), + AssertionChain(assert_path()) + | assert_setattr(configuration, "file_path"), ), ) )(dump) @@ -128,8 +129,7 @@ def load( ) -> Self: if configuration is None: configuration = cls() - asserter = Asserter() - asserter.assert_record( + assert_record( Fields( OptionalField( "family_trees", diff --git a/betty/extension/nginx/config.py b/betty/extension/nginx/config.py index bd78d91ed..48daf83a8 100644 --- a/betty/extension/nginx/config.py +++ b/betty/extension/nginx/config.py @@ -6,7 +6,17 @@ from betty.config import Configuration from betty.serde.dump import Dump, VoidableDump, minimize, Void, VoidableDictDump -from betty.serde.load import Asserter, Fields, OptionalField, AssertionChain +from betty.serde.load import ( + Fields, + OptionalField, + AssertionChain, + assert_record, + assert_or, + assert_bool, + assert_none, + assert_setattr, + assert_str, +) class NginxConfiguration(Configuration): @@ -66,22 +76,17 @@ def load( ) -> Self: if configuration is None: configuration = cls() - asserter = Asserter() - asserter.assert_record( + assert_record( Fields( OptionalField( "https", - AssertionChain( - asserter.assert_or( - asserter.assert_bool(), asserter.assert_none() - ) - ) - | asserter.assert_setattr(configuration, "https"), + AssertionChain(assert_or(assert_bool(), assert_none())) + | assert_setattr(configuration, "https"), ), OptionalField( "www_directory_path", - AssertionChain(asserter.assert_str()) - | asserter.assert_setattr(configuration, "www_directory_path"), + AssertionChain(assert_str()) + | assert_setattr(configuration, "www_directory_path"), ), ) )(dump) diff --git a/betty/extension/wikipedia/config.py b/betty/extension/wikipedia/config.py index 33dbeece8..39f76b96c 100644 --- a/betty/extension/wikipedia/config.py +++ b/betty/extension/wikipedia/config.py @@ -8,7 +8,14 @@ from betty.config import Configuration from betty.serde.dump import Dump, VoidableDump, minimize, VoidableDictDump -from betty.serde.load import Asserter, Fields, OptionalField, AssertionChain +from betty.serde.load import ( + Fields, + OptionalField, + AssertionChain, + assert_record, + assert_bool, + assert_setattr, +) class WikipediaConfiguration(Configuration): @@ -45,13 +52,12 @@ def load( ) -> Self: if configuration is None: configuration = cls() - asserter = Asserter() - asserter.assert_record( + assert_record( Fields( OptionalField( "populate_images", - AssertionChain(asserter.assert_bool()) - | asserter.assert_setattr(configuration, "populate_images"), + AssertionChain(assert_bool()) + | assert_setattr(configuration, "populate_images"), ), ) )(dump) diff --git a/betty/project.py b/betty/project.py index fd6953786..a8e384c90 100644 --- a/betty/project.py +++ b/betty/project.py @@ -42,8 +42,18 @@ Assertion, RequiredField, OptionalField, - Asserter, AssertionChain, + assert_record, + assert_entity_type, + assert_setattr, + assert_str, + assert_field, + assert_extension_type, + assert_bool, + assert_dict, + assert_locale, + assert_int, + assert_positive_number, ) if TYPE_CHECKING: @@ -129,25 +139,24 @@ def load( ) -> Self: if configuration is None: configuration = cls() - asserter = Asserter() if isinstance(dump, dict) or not configuration.entity_type_is_constrained: - asserter.assert_record( + assert_record( Fields( RequiredField( "entity_type", - AssertionChain(asserter.assert_entity_type()) - | asserter.assert_setattr(configuration, "entity_type"), + AssertionChain(assert_entity_type()) + | assert_setattr(configuration, "entity_type"), ), OptionalField( "entity_id", - AssertionChain(asserter.assert_str()) - | asserter.assert_setattr(configuration, "entity_id"), + AssertionChain(assert_str()) + | assert_setattr(configuration, "entity_id"), ), ) )(dump) else: - asserter.assert_str()(dump) - asserter.assert_setattr(configuration, "entity_id")(dump) # type: ignore[arg-type] + assert_str()(dump) + assert_setattr(configuration, "entity_id")(dump) # type: ignore[arg-type] return configuration @override @@ -321,11 +330,10 @@ def load( dump: Dump, configuration: Self | None = None, ) -> Self: - asserter = Asserter() - extension_type = asserter.assert_field( + extension_type = assert_field( RequiredField( "extension", - asserter.assert_extension_type(), + assert_extension_type(), ) )(dump) if configuration is None: @@ -333,15 +341,15 @@ def load( else: # This MUST NOT fail. If it does, this is a bug in the calling code that must be fixed. assert extension_type is configuration.extension_type - asserter.assert_record( + assert_record( Fields( RequiredField( "extension", ), OptionalField( "enabled", - AssertionChain(asserter.assert_bool()) - | asserter.assert_setattr(configuration, "enabled"), + AssertionChain(assert_bool()) + | assert_setattr(configuration, "enabled"), ), OptionalField( "configuration", @@ -425,14 +433,13 @@ def _load_key( item_dump: Dump, key_dump: str, ) -> Dump: - asserter = Asserter() - dict_dump = asserter.assert_dict()(item_dump) + dict_dump = assert_dict()(item_dump) dict_dump["extension"] = key_dump return dict_dump @override def _dump_key(self, item_dump: VoidableDump) -> tuple[VoidableDump, str]: - dict_dump = self._asserter.assert_dict()(item_dump) + dict_dump = assert_dict()(item_dump) return dict_dump, dict_dump.pop("extension") def enable(self, *extension_types: type[Extension]) -> None: @@ -518,23 +525,22 @@ def load( dump: Dump, configuration: Self | None = None, ) -> Self: - asserter = Asserter() - entity_type = asserter.assert_field( + entity_type = assert_field( RequiredField[Any, type[Entity]]( "entity_type", - AssertionChain(asserter.assert_str()) | asserter.assert_entity_type(), + AssertionChain(assert_str()) | assert_entity_type(), ), )(dump) configuration = cls(entity_type) - asserter.assert_record( + assert_record( Fields( OptionalField( "entity_type", ), OptionalField( "generate_html_list", - AssertionChain(asserter.assert_bool()) - | asserter.assert_setattr(configuration, "generate_html_list"), + AssertionChain(assert_bool()) + | assert_setattr(configuration, "generate_html_list"), ), ) )(dump) @@ -574,15 +580,14 @@ def _load_key( item_dump: Dump, key_dump: str, ) -> Dump: - asserter = Asserter() - dict_dump = asserter.assert_dict()(item_dump) - asserter.assert_entity_type()(key_dump) + dict_dump = assert_dict()(item_dump) + assert_entity_type()(key_dump) dict_dump["entity_type"] = key_dump return dict_dump @override def _dump_key(self, item_dump: VoidableDump) -> tuple[VoidableDump, str]: - dict_dump = self._asserter.assert_dict()(item_dump) + dict_dump = assert_dict()(item_dump) return dict_dump, dict_dump.pop("entity_type") @override @@ -667,19 +672,18 @@ def load( dump: Dump, configuration: Self | None = None, ) -> Self: - asserter = Asserter() - locale = asserter.assert_field( - RequiredField("locale", asserter.assert_locale()), + locale = assert_field( + RequiredField("locale", assert_locale()), )(dump) if configuration is None: configuration = cls(locale) - asserter.assert_record( + assert_record( Fields( RequiredField("locale"), OptionalField( "alias", - AssertionChain(asserter.assert_str()) - | asserter.assert_setattr(configuration, "alias"), + AssertionChain(assert_str()) + | assert_setattr(configuration, "alias"), ), ) )(dump) @@ -719,14 +723,13 @@ def _load_key( item_dump: Dump, key_dump: str, ) -> Dump: - asserter = Asserter() - dict_item_dump = asserter.assert_dict()(item_dump) + dict_item_dump = assert_dict()(item_dump) dict_item_dump["locale"] = key_dump return dict_item_dump @override def _dump_key(self, item_dump: VoidableDump) -> tuple[VoidableDump, str]: - dict_item_dump = self._asserter.assert_dict()(item_dump) + dict_item_dump = assert_dict()(item_dump) return dict_item_dump, dict_item_dump.pop("locale") @override @@ -1008,7 +1011,7 @@ def lifetime_threshold(self) -> int: @lifetime_threshold.setter def lifetime_threshold(self, lifetime_threshold: int) -> None: - self._asserter.assert_positive_number()(lifetime_threshold) + assert_positive_number()(lifetime_threshold) self._lifetime_threshold = lifetime_threshold self._dispatch_change() @@ -1035,48 +1038,47 @@ def load( ) -> Self: if configuration is None: configuration = cls() - asserter = Asserter() - asserter.assert_record( + assert_record( Fields( OptionalField( "name", - AssertionChain(asserter.assert_str()) - | asserter.assert_setattr(configuration, "name"), + AssertionChain(assert_str()) + | assert_setattr(configuration, "name"), ), RequiredField( "base_url", - AssertionChain(asserter.assert_str()) - | asserter.assert_setattr(configuration, "base_url"), + AssertionChain(assert_str()) + | assert_setattr(configuration, "base_url"), ), OptionalField( "title", - AssertionChain(asserter.assert_str()) - | asserter.assert_setattr(configuration, "title"), + AssertionChain(assert_str()) + | assert_setattr(configuration, "title"), ), OptionalField( "author", - AssertionChain(asserter.assert_str()) - | asserter.assert_setattr(configuration, "author"), + AssertionChain(assert_str()) + | assert_setattr(configuration, "author"), ), OptionalField( "root_path", - AssertionChain(asserter.assert_str()) - | asserter.assert_setattr(configuration, "root_path"), + AssertionChain(assert_str()) + | assert_setattr(configuration, "root_path"), ), OptionalField( "clean_urls", - AssertionChain(asserter.assert_bool()) - | asserter.assert_setattr(configuration, "clean_urls"), + AssertionChain(assert_bool()) + | assert_setattr(configuration, "clean_urls"), ), OptionalField( "debug", - AssertionChain(asserter.assert_bool()) - | asserter.assert_setattr(configuration, "debug"), + AssertionChain(assert_bool()) + | assert_setattr(configuration, "debug"), ), OptionalField( "lifetime_threshold", - AssertionChain(asserter.assert_int()) - | asserter.assert_setattr(configuration, "lifetime_threshold"), + AssertionChain(assert_int()) + | assert_setattr(configuration, "lifetime_threshold"), ), OptionalField( "locales", diff --git a/betty/serde/load.py b/betty/serde/load.py index 373fa034d..942e33187 100644 --- a/betty/serde/load.py +++ b/betty/serde/load.py @@ -167,416 +167,431 @@ def __iter__(self) -> Iterator[_Field[Any, Any]]: _AssertionBuilder = "_AssertionBuilderFunction[ValueT, ReturnT] | _AssertionBuilderMethod[ValueT, ReturnT]" -class Asserter: - """ - Provide deserialization assertions. - """ - - def _assert_type_violation_error_message( - self, - asserted_type: type[DumpType], - ) -> Localizable: - messages = { - None: Str._("This must be none/null."), - bool: Str._("This must be a boolean."), - int: Str._("This must be a whole number."), - float: Str._("This must be a decimal number."), - str: Str._("This must be a string."), - list: Str._("This must be a list."), - dict: Str._("This must be a key-value mapping."), - } - return messages[asserted_type] # type: ignore[index] - - def _assert_type( - self, - value: Any, - value_required_type: type[_DumpTypeT], - value_disallowed_type: type[DumpType] | None = None, - ) -> _DumpTypeT: - if isinstance(value, value_required_type) and ( - value_disallowed_type is None - or not isinstance(value, value_disallowed_type) - ): - return value - raise AssertionFailed( - self._assert_type_violation_error_message( - value_required_type, # type: ignore[arg-type] - ) +def _assert_type_violation_error_message( + asserted_type: type[DumpType], +) -> Localizable: + messages = { + None: Str._("This must be none/null."), + bool: Str._("This must be a boolean."), + int: Str._("This must be a whole number."), + float: Str._("This must be a decimal number."), + str: Str._("This must be a string."), + list: Str._("This must be a list."), + dict: Str._("This must be a key-value mapping."), + } + return messages[asserted_type] # type: ignore[index] + + +def _assert_type( + value: Any, + value_required_type: type[_DumpTypeT], + value_disallowed_type: type[DumpType] | None = None, +) -> _DumpTypeT: + if isinstance(value, value_required_type) and ( + value_disallowed_type is None or not isinstance(value, value_disallowed_type) + ): + return value + raise AssertionFailed( + _assert_type_violation_error_message( + value_required_type, # type: ignore[arg-type] ) + ) - def assert_or( - self, - if_assertion: Assertion[_AssertionValueT, _AssertionReturnT], - else_assertion: Assertion[_AssertionValueT, _AssertionReturnU], - ) -> Assertion[_AssertionValueT, _AssertionReturnT | _AssertionReturnU]: - """ - Assert that at least one of the given assertions passed. - """ - def _assert_or(value: Any) -> _AssertionReturnT | _AssertionReturnU: - assertions = (if_assertion, else_assertion) - errors = SerdeErrorCollection() - for assertion in assertions: - try: - return assertion(value) - except SerdeError as e: - if e.raised(AssertionFailed): - errors.append(e) - raise errors +def assert_or( + if_assertion: Assertion[_AssertionValueT, _AssertionReturnT], + else_assertion: Assertion[_AssertionValueT, _AssertionReturnU], +) -> Assertion[_AssertionValueT, _AssertionReturnT | _AssertionReturnU]: + """ + Assert that at least one of the given assertions passed. + """ - return _assert_or + def _assert_or(value: Any) -> _AssertionReturnT | _AssertionReturnU: + assertions = (if_assertion, else_assertion) + errors = SerdeErrorCollection() + for assertion in assertions: + try: + return assertion(value) + except SerdeError as e: + if e.raised(AssertionFailed): + errors.append(e) + raise errors - def assert_none(self) -> Assertion[Any, None]: - """ - Assert that a value is ``None``. - """ + return _assert_or - def _assert_none(value: Any) -> None: - self._assert_type(value, type(None)) - return _assert_none +def assert_none() -> Assertion[Any, None]: + """ + Assert that a value is ``None``. + """ - def assert_bool(self) -> Assertion[Any, bool]: - """ - Assert that a value is a Python ``bool``. - """ + def _assert_none(value: Any) -> None: + _assert_type(value, type(None)) - def _assert_bool(value: Any) -> bool: - return self._assert_type(value, bool) + return _assert_none - return _assert_bool - def assert_int(self) -> Assertion[Any, int]: - """ - Assert that a value is a Python ``int``. - """ +def assert_bool() -> Assertion[Any, bool]: + """ + Assert that a value is a Python ``bool``. + """ - def _assert_int(value: Any) -> int: - return self._assert_type(value, int, bool) + def _assert_bool(value: Any) -> bool: + return _assert_type(value, bool) - return _assert_int + return _assert_bool - def assert_float(self) -> Assertion[Any, float]: - """ - Assert that a value is a Python ``float``. - """ - def _assert_float(value: Any) -> float: - return self._assert_type(value, float) +def assert_int() -> Assertion[Any, int]: + """ + Assert that a value is a Python ``int``. + """ - return _assert_float + def _assert_int(value: Any) -> int: + return _assert_type(value, int, bool) - def assert_number(self) -> Assertion[Any, Number]: - """ - Assert that a value is a number (a Python ``int`` or ``float``). - """ - return self.assert_or(self.assert_int(), self.assert_float()) + return _assert_int - def assert_positive_number(self) -> Assertion[Any, Number]: - """ - Assert that a vaue is a positive nu,ber. - """ - def _assert_positive_number( - value: Any, - ) -> Number: - value = self.assert_number()(value) - if value <= 0: - raise AssertionFailed(Str._("This must be a positive number.")) - return value +def assert_float() -> Assertion[Any, float]: + """ + Assert that a value is a Python ``float``. + """ - return _assert_positive_number + def _assert_float(value: Any) -> float: + return _assert_type(value, float) - def assert_str(self) -> Assertion[Any, str]: - """ - Assert that a value is a Python ``str``. - """ + return _assert_float - def _assert_str(value: Any) -> str: - return self._assert_type(value, str) - return _assert_str +def assert_number() -> Assertion[Any, Number]: + """ + Assert that a value is a number (a Python ``int`` or ``float``). + """ + return assert_or(assert_int(), assert_float()) - def assert_list(self) -> Assertion[Any, list[Any]]: - """ - Assert that a value is a Python ``list``. - """ - def _assert_list(value: Any) -> list[Any]: - return self._assert_type(value, list) +def assert_positive_number() -> Assertion[Any, Number]: + """ + Assert that a vaue is a positive nu,ber. + """ - return _assert_list + def _assert_positive_number( + value: Any, + ) -> Number: + value = assert_number()(value) + if value <= 0: + raise AssertionFailed(Str._("This must be a positive number.")) + return value - def assert_dict(self) -> Assertion[Any, dict[str, Any]]: - """ - Assert that a value is a Python ``dict``. - """ + return _assert_positive_number - def _assert_dict(value: Any) -> dict[str, Any]: - return self._assert_type(value, dict) - return _assert_dict +def assert_str() -> Assertion[Any, str]: + """ + Assert that a value is a Python ``str``. + """ - def assert_sequence( - self, item_assertion: Assertion[Any, _AssertionReturnT] - ) -> Assertion[Any, MutableSequence[_AssertionReturnT]]: - """ - Assert that a value is a sequence and that all item values are of the given type. - """ + def _assert_str(value: Any) -> str: + return _assert_type(value, str) - def _assert_sequence(value: Any) -> MutableSequence[_AssertionReturnT]: - list_value = self.assert_list()(value) - sequence: MutableSequence[_AssertionReturnT] = [] - with SerdeErrorCollection().assert_valid() as errors: - for value_item_index, value_item_value in enumerate(list_value): - with errors.catch(Str.plain(value_item_index)): - sequence.append(item_assertion(value_item_value)) - return sequence + return _assert_str - return _assert_sequence - def assert_mapping( - self, item_assertion: Assertion[Any, _AssertionReturnT] - ) -> Assertion[Any, MutableMapping[str, _AssertionReturnT]]: - """ - Assert that a value is a key-value mapping and assert that all item values are of the given type. - """ +def assert_list() -> Assertion[Any, list[Any]]: + """ + Assert that a value is a Python ``list``. + """ - def _assert_mapping(value: Any) -> MutableMapping[str, _AssertionReturnT]: - dict_value = self.assert_dict()(value) - mapping: MutableMapping[str, _AssertionReturnT] = {} - with SerdeErrorCollection().assert_valid() as errors: - for value_item_key, value_item_value in dict_value.items(): - with errors.catch(Str.plain(value_item_key)): - mapping[value_item_key] = item_assertion(value_item_value) - return mapping + def _assert_list(value: Any) -> list[Any]: + return _assert_type(value, list) - return _assert_mapping + return _assert_list - def assert_fields(self, fields: Fields) -> Assertion[Any, MutableMapping[str, Any]]: - """ - Assert that a value is a key-value mapping of arbitrary value types, and assert several of its values. - """ - def _assert_fields(value: Any) -> MutableMapping[str, Any]: - value_dict = self.assert_dict()(value) - mapping: MutableMapping[str, Any] = {} - with SerdeErrorCollection().assert_valid() as errors: - for field in fields: - with errors.catch(Str.plain(field.name)): - if field.name in value_dict: - if field.assertion: - mapping[field.name] = field.assertion( - value_dict[field.name] - ) - elif isinstance(field, RequiredField): - raise AssertionFailed(Str._("This field is required.")) - return mapping - - return _assert_fields - - @overload - def assert_field( - self, field: RequiredField[_AssertionValueT, _AssertionReturnT] - ) -> Assertion[_AssertionValueT, _AssertionReturnT]: - pass # pragma: no cover - - @overload - def assert_field( - self, field: OptionalField[_AssertionValueT, _AssertionReturnT] - ) -> Assertion[_AssertionValueT, _AssertionReturnT | type[Void]]: - pass # pragma: no cover - - def assert_field( - self, field: _Field[_AssertionValueT, _AssertionReturnT] - ) -> Assertion[_AssertionValueT, _AssertionReturnT | type[Void]]: - """ - Assert that a value is a key-value mapping of arbitrary value types, and assert a single of its values. - """ +def assert_dict() -> Assertion[Any, dict[str, Any]]: + """ + Assert that a value is a Python ``dict``. + """ - def _assert_field(value: Any) -> _AssertionReturnT | type[Void]: - fields = self.assert_fields(Fields(field))(value) - try: - return cast("_AssertionReturnT | type[Void]", fields[field.name]) - except KeyError: - if isinstance(field, RequiredField): - raise - return Void + def _assert_dict(value: Any) -> dict[str, Any]: + return _assert_type(value, dict) - return _assert_field + return _assert_dict - def assert_record(self, fields: Fields) -> Assertion[Any, MutableMapping[str, Any]]: - """ - Assert that a value is a record: a key-value mapping of arbitrary value types, with a known structure. - To validate a key-value mapping as a records, assertions for all possible keys - MUST be provided. Any keys present in the value for which no field assertions - are provided will cause the entire record assertion to fail. - """ - if not len(list(fields)): - raise ValueError("One or more fields are required.") - - def _assert_record(value: Any) -> MutableMapping[str, Any]: - dict_value = self.assert_dict()(value) - known_keys = {x.name for x in fields} - unknown_keys = set(dict_value.keys()) - known_keys - with SerdeErrorCollection().assert_valid() as errors: - for unknown_key in unknown_keys: - with errors.catch(Str.plain(unknown_key)): - raise AssertionFailed( - Str._( - "Unknown key: {unknown_key}. Did you mean {known_keys}?", - unknown_key=f'"{unknown_key}"', - known_keys=", ".join( - (f'"{x}"' for x in sorted(known_keys)) - ), +def assert_sequence( + item_assertion: Assertion[Any, _AssertionReturnT], +) -> Assertion[Any, MutableSequence[_AssertionReturnT]]: + """ + Assert that a value is a sequence and that all item values are of the given type. + """ + + def _assert_sequence(value: Any) -> MutableSequence[_AssertionReturnT]: + list_value = assert_list()(value) + sequence: MutableSequence[_AssertionReturnT] = [] + with SerdeErrorCollection().assert_valid() as errors: + for value_item_index, value_item_value in enumerate(list_value): + with errors.catch(Str.plain(value_item_index)): + sequence.append(item_assertion(value_item_value)) + return sequence + + return _assert_sequence + + +def assert_mapping( + item_assertion: Assertion[Any, _AssertionReturnT], +) -> Assertion[Any, MutableMapping[str, _AssertionReturnT]]: + """ + Assert that a value is a key-value mapping and assert that all item values are of the given type. + """ + + def _assert_mapping(value: Any) -> MutableMapping[str, _AssertionReturnT]: + dict_value = assert_dict()(value) + mapping: MutableMapping[str, _AssertionReturnT] = {} + with SerdeErrorCollection().assert_valid() as errors: + for value_item_key, value_item_value in dict_value.items(): + with errors.catch(Str.plain(value_item_key)): + mapping[value_item_key] = item_assertion(value_item_value) + return mapping + + return _assert_mapping + + +def assert_fields(fields: Fields) -> Assertion[Any, MutableMapping[str, Any]]: + """ + Assert that a value is a key-value mapping of arbitrary value types, and assert several of its values. + """ + + def _assert_fields(value: Any) -> MutableMapping[str, Any]: + value_dict = assert_dict()(value) + mapping: MutableMapping[str, Any] = {} + with SerdeErrorCollection().assert_valid() as errors: + for field in fields: + with errors.catch(Str.plain(field.name)): + if field.name in value_dict: + if field.assertion: + mapping[field.name] = field.assertion( + value_dict[field.name] ) + elif isinstance(field, RequiredField): + raise AssertionFailed(Str._("This field is required.")) + return mapping + + return _assert_fields + + +@overload +def assert_field( + field: RequiredField[_AssertionValueT, _AssertionReturnT], +) -> Assertion[_AssertionValueT, _AssertionReturnT]: + pass # pragma: no cover + + +@overload +def assert_field( + field: OptionalField[_AssertionValueT, _AssertionReturnT], +) -> Assertion[_AssertionValueT, _AssertionReturnT | type[Void]]: + pass # pragma: no cover + + +def assert_field( + field: _Field[_AssertionValueT, _AssertionReturnT], +) -> Assertion[_AssertionValueT, _AssertionReturnT | type[Void]]: + """ + Assert that a value is a key-value mapping of arbitrary value types, and assert a single of its values. + """ + + def _assert_field(value: Any) -> _AssertionReturnT | type[Void]: + fields = assert_fields(Fields(field))(value) + try: + return cast("_AssertionReturnT | type[Void]", fields[field.name]) + except KeyError: + if isinstance(field, RequiredField): + raise + return Void + + return _assert_field + + +def assert_record(fields: Fields) -> Assertion[Any, MutableMapping[str, Any]]: + """ + Assert that a value is a record: a key-value mapping of arbitrary value types, with a known structure. + + To validate a key-value mapping as a records, assertions for all possible keys + MUST be provided. Any keys present in the value for which no field assertions + are provided will cause the entire record assertion to fail. + """ + if not len(list(fields)): + raise ValueError("One or more fields are required.") + + def _assert_record(value: Any) -> MutableMapping[str, Any]: + dict_value = assert_dict()(value) + known_keys = {x.name for x in fields} + unknown_keys = set(dict_value.keys()) - known_keys + with SerdeErrorCollection().assert_valid() as errors: + for unknown_key in unknown_keys: + with errors.catch(Str.plain(unknown_key)): + raise AssertionFailed( + Str._( + "Unknown key: {unknown_key}. Did you mean {known_keys}?", + unknown_key=f'"{unknown_key}"', + known_keys=", ".join( + (f'"{x}"' for x in sorted(known_keys)) + ), ) - return self.assert_fields(fields)(dict_value) + ) + return assert_fields(fields)(dict_value) - return _assert_record + return _assert_record - def assert_path(self) -> Assertion[Any, Path]: - """ - Assert that a value is a path to a file or directory on disk that may or may not exist. - """ - def _assert_path(value: Any) -> Path: - self.assert_str()(value) - return Path(value).expanduser().resolve() +def assert_path() -> Assertion[Any, Path]: + """ + Assert that a value is a path to a file or directory on disk that may or may not exist. + """ + + def _assert_path(value: Any) -> Path: + assert_str()(value) + return Path(value).expanduser().resolve() - return _assert_path + return _assert_path - def assert_directory_path(self) -> Assertion[Any, Path]: - """ - Assert that a value is a path to an existing directory. - """ - def _assert_directory_path(value: Any) -> Path: - directory_path = self.assert_path()(value) - if directory_path.is_dir(): - return directory_path +def assert_directory_path() -> Assertion[Any, Path]: + """ + Assert that a value is a path to an existing directory. + """ + + def _assert_directory_path(value: Any) -> Path: + directory_path = assert_path()(value) + if directory_path.is_dir(): + return directory_path + raise AssertionFailed( + Str._( + '"{path}" is not a directory.', + path=value, + ) + ) + + return _assert_directory_path + + +def assert_locale() -> Assertion[Any, str]: + """ + Assert that a value is a valid `IETF BCP 47 language tag `_. + """ + + def _assert_locale( + value: Any, + ) -> str: + value = assert_str()(value) + try: + get_data(value) + return value + except LocaleNotFoundError: raise AssertionFailed( Str._( - '"{path}" is not a directory.', - path=value, + '"{locale}" is not a valid IETF BCP 47 language tag.', + locale=value, ) - ) + ) from None - return _assert_directory_path + return _assert_locale - def assert_locale(self) -> Assertion[Any, str]: - """ - Assert that a value is a valid `IETF BCP 47 language tag `_. - """ - def _assert_locale( - value: Any, - ) -> str: - value = self.assert_str()(value) - try: - get_data(value) - return value - except LocaleNotFoundError: - raise AssertionFailed( - Str._( - '"{locale}" is not a valid IETF BCP 47 language tag.', - locale=value, - ) - ) from None +def assert_setattr( + instance: object, attr_name: str +) -> Assertion[_AssertionValueT, _AssertionValueT]: + """ + Set a value for the given object's attribute. + """ - return _assert_locale + def _assert_setattr(value: _AssertionValueT) -> _AssertionValueT: + setattr(instance, attr_name, value) + return value - def assert_setattr( - self, instance: object, attr_name: str - ) -> Assertion[_AssertionValueT, _AssertionValueT]: - """ - Set a value for the given object's attribute. - """ + return _assert_setattr - def _assert_setattr(value: _AssertionValueT) -> _AssertionValueT: - setattr(instance, attr_name, value) - return value - return _assert_setattr +def assert_extension_type() -> Assertion[Any, type[Extension]]: + """ + Assert that a value is an extension type. - def assert_extension_type(self) -> Assertion[Any, type[Extension]]: - """ - Assert that a value is an extension type. + This assertion passes if the value is fully qualified :py:class:`betty.app.extension.Extension` subclass name. + """ - This assertion passes if the value is fully qualified :py:class:`betty.app.extension.Extension` subclass name. - """ + def _assert_extension_type( + value: Any, + ) -> type[Extension]: + from betty.app.extension import ( + get_extension_type, + ExtensionTypeImportError, + ExtensionTypeInvalidError, + ExtensionTypeError, + ) - def _assert_extension_type( - value: Any, - ) -> type[Extension]: - from betty.app.extension import ( - get_extension_type, - ExtensionTypeImportError, - ExtensionTypeInvalidError, - ExtensionTypeError, - ) + assert_str()(value) + try: + return get_extension_type(value) + except ExtensionTypeImportError: + raise AssertionFailed( + Str._( + 'Cannot find and import "{extension_type}".', + extension_type=str(value), + ) + ) from None + except ExtensionTypeInvalidError: + raise AssertionFailed( + Str._( + '"{extension_type}" is not a valid Betty extension type.', + extension_type=str(value), + ) + ) from None + except ExtensionTypeError: + raise AssertionFailed( + Str._( + 'Cannot determine the extension type for "{extension_type}". Did you perhaps make a typo, or could it be that the extension type comes from another package that is not yet installed?', + extension_type=str(value), + ) + ) from None - self.assert_str()(value) - try: - return get_extension_type(value) - except ExtensionTypeImportError: - raise AssertionFailed( - Str._( - 'Cannot find and import "{extension_type}".', - extension_type=str(value), - ) - ) from None - except ExtensionTypeInvalidError: - raise AssertionFailed( - Str._( - '"{extension_type}" is not a valid Betty extension type.', - extension_type=str(value), - ) - ) from None - except ExtensionTypeError: - raise AssertionFailed( - Str._( - 'Cannot determine the extension type for "{extension_type}". Did you perhaps make a typo, or could it be that the extension type comes from another package that is not yet installed?', - extension_type=str(value), - ) - ) from None + return _assert_extension_type - return _assert_extension_type - def assert_entity_type(self) -> Assertion[Any, type[Entity]]: - """ - Assert that a value is an entity type. +def assert_entity_type() -> Assertion[Any, type[Entity]]: + """ + Assert that a value is an entity type. - This assertion passes if the value is fully qualified :py:class:`betty.model.Entity` subclass name. - """ + This assertion passes if the value is fully qualified :py:class:`betty.model.Entity` subclass name. + """ - def _assert_entity_type( - value: Any, - ) -> type[Entity]: - self.assert_str()(value) - try: - return get_entity_type(value) - except EntityTypeImportError: - raise AssertionFailed( - Str._( - 'Cannot find and import "{entity_type}".', - entity_type=str(value), - ) - ) from None - except EntityTypeInvalidError: - raise AssertionFailed( - Str._( - '"{entity_type}" is not a valid Betty entity type.', - entity_type=str(value), - ) - ) from None - except EntityTypeError: - raise AssertionFailed( - Str._( - 'Cannot determine the entity type for "{entity_type}". Did you perhaps make a typo, or could it be that the entity type comes from another package that is not yet installed?', - entity_type=str(value), - ) - ) from None + def _assert_entity_type( + value: Any, + ) -> type[Entity]: + assert_str()(value) + try: + return get_entity_type(value) + except EntityTypeImportError: + raise AssertionFailed( + Str._( + 'Cannot find and import "{entity_type}".', + entity_type=str(value), + ) + ) from None + except EntityTypeInvalidError: + raise AssertionFailed( + Str._( + '"{entity_type}" is not a valid Betty entity type.', + entity_type=str(value), + ) + ) from None + except EntityTypeError: + raise AssertionFailed( + Str._( + 'Cannot determine the entity type for "{entity_type}". Did you perhaps make a typo, or could it be that the entity type comes from another package that is not yet installed?', + entity_type=str(value), + ) + ) from None - return _assert_entity_type + return _assert_entity_type diff --git a/betty/tests/app/test___init__.py b/betty/tests/app/test___init__.py index 5292ba0fd..e5c97db5f 100644 --- a/betty/tests/app/test___init__.py +++ b/betty/tests/app/test___init__.py @@ -13,7 +13,14 @@ from betty.config import Configuration from betty.model import Entity from betty.project import ExtensionConfiguration -from betty.serde.load import Fields, RequiredField, Asserter, AssertionChain +from betty.serde.load import ( + Fields, + RequiredField, + AssertionChain, + assert_record, + assert_int, + assert_setattr, +) if TYPE_CHECKING: from betty.serde.dump import Dump, VoidableDump @@ -50,13 +57,12 @@ def load( ) -> Self: if configuration is None: configuration = cls() - asserter = Asserter() - asserter.assert_record( + assert_record( Fields( RequiredField( "check", - AssertionChain(asserter.assert_int()) - | asserter.assert_setattr(configuration, "check"), + AssertionChain(assert_int()) + | assert_setattr(configuration, "check"), ), ) )(dump) diff --git a/betty/tests/coverage/test_coverage.py b/betty/tests/coverage/test_coverage.py index a1eb6951b..e3d17bde5 100644 --- a/betty/tests/coverage/test_coverage.py +++ b/betty/tests/coverage/test_coverage.py @@ -727,14 +727,12 @@ class TestKnownToBeMissing: }, }, "betty/serde/load.py": { - "Asserter": { - "assert_assertions": TestKnownToBeMissing, - "assert_entity_type": TestKnownToBeMissing, - "assert_extension_type": TestKnownToBeMissing, - "assert_locale": TestKnownToBeMissing, - "assert_none": TestKnownToBeMissing, - "assert_setattr": TestKnownToBeMissing, - }, + "assert_assertions": TestKnownToBeMissing, + "assert_entity_type": TestKnownToBeMissing, + "assert_extension_type": TestKnownToBeMissing, + "assert_locale": TestKnownToBeMissing, + "assert_none": TestKnownToBeMissing, + "assert_setattr": TestKnownToBeMissing, # This is an empty class. "AssertionFailed": TestKnownToBeMissing, "Fields": TestKnownToBeMissing, diff --git a/betty/tests/serde/test_load.py b/betty/tests/serde/test_load.py index a67550145..e2c19fac1 100644 --- a/betty/tests/serde/test_load.py +++ b/betty/tests/serde/test_load.py @@ -9,7 +9,6 @@ from betty.locale import Str from betty.serde.dump import Void from betty.serde.load import ( - Asserter, AssertionFailed, Number, Fields, @@ -17,6 +16,22 @@ RequiredField, Assertion, AssertionChain, + assert_or, + assert_bool, + assert_directory_path, + assert_path, + assert_record, + assert_str, + assert_int, + assert_float, + assert_positive_number, + assert_number, + assert_list, + assert_mapping, + assert_sequence, + assert_dict, + assert_fields, + assert_field, ) from betty.tests.serde import raises_error @@ -51,7 +66,7 @@ def _always_invalid(value: _T) -> _T: raise AssertionFailed(Str.plain("")) -class TestAsserter: +class TestAssertOr: @pytest.mark.parametrize( ("if_assertion", "else_assertion", "value"), [ @@ -60,47 +75,47 @@ class TestAsserter: (_always_invalid, _always_valid, 123), ], ) - async def test_assert_or_with_valid_AssertionChain( + async def test_with_valid_AssertionChain( self, if_assertion: Assertion[Any, bool], else_assertion: Assertion[Any, bool], value: int, ) -> None: - sut = Asserter() - assert sut.assert_or(if_assertion, else_assertion)(value) == value + assert assert_or(if_assertion, else_assertion)(value) == value - async def test_assert_or_with_invalid_AssertionChain(self) -> None: - sut = Asserter() + async def test_with_invalid_AssertionChain(self) -> None: with raises_error(error_type=AssertionFailed): - sut.assert_or(_always_invalid, _always_invalid)(123) + assert_or(_always_invalid, _always_invalid)(123) - async def test_assert_bool_with_valid_value(self) -> None: - sut = Asserter() - sut.assert_bool()(True) - async def test_assert_bool_with_invalid_value(self) -> None: - sut = Asserter() +class TestAssertBool: + async def test_with_valid_value(self) -> None: + assert_bool()(True) + + async def test_with_invalid_value(self) -> None: with raises_error(error_type=AssertionFailed): - sut.assert_bool()(123) + assert_bool()(123) + - async def test_assert_int_with_valid_value(self) -> None: - sut = Asserter() - sut.assert_int()(123) +class TestAssertInt: + async def test_with_valid_value(self) -> None: + assert_int()(123) - async def test_assert_int_with_invalid_value(self) -> None: - sut = Asserter() + async def test_with_invalid_value(self) -> None: with raises_error(error_type=AssertionFailed): - sut.assert_int()(False) + assert_int()(False) + - async def test_assert_float_with_valid_value(self) -> None: - sut = Asserter() - sut.assert_float()(1.23) +class TestAssertFloat: + async def test_with_valid_value(self) -> None: + assert_float()(1.23) - async def test_assert_float_with_invalid_value(self) -> None: - sut = Asserter() + async def test_with_invalid_value(self) -> None: with raises_error(error_type=AssertionFailed): - sut.assert_float()(False) + assert_float()(False) + +class TestAssertNumber: @pytest.mark.parametrize( "value", [ @@ -108,15 +123,15 @@ async def test_assert_float_with_invalid_value(self) -> None: 3.13, ], ) - async def test_assert_number_with_valid_value(self, value: Number) -> None: - sut = Asserter() - sut.assert_number()(value) + async def test_with_valid_value(self, value: Number) -> None: + assert_number()(value) - async def test_assert_number_with_invalid_value(self) -> None: - sut = Asserter() + async def test_with_invalid_value(self) -> None: with raises_error(error_type=AssertionFailed): - sut.assert_number()(False) + assert_number()(False) + +class TestAssertPositiveNumber: @pytest.mark.parametrize( "value", [ @@ -126,11 +141,8 @@ async def test_assert_number_with_invalid_value(self) -> None: 1.1, ], ) - async def test_assert_positive_number_with_valid_value( - self, value: int | float - ) -> None: - sut = Asserter() - sut.assert_positive_number()(1.23) + async def test_with_valid_value(self, value: int | float) -> None: + assert_positive_number()(1.23) @pytest.mark.parametrize( "value", @@ -140,246 +152,231 @@ async def test_assert_positive_number_with_valid_value( -1.0, ], ) - async def test_assert_positive_number_with_invalid_value( - self, value: int | float - ) -> None: - sut = Asserter() + async def test_with_invalid_value(self, value: int | float) -> None: with raises_error(error_type=AssertionFailed): - sut.assert_positive_number()(value) + assert_positive_number()(value) - async def test_assert_str_with_valid_value(self) -> None: - sut = Asserter() - sut.assert_str()("Hello, world!") - async def test_assert_str_with_invalid_value(self) -> None: - sut = Asserter() +class TestAssertStr: + async def test_with_valid_value(self) -> None: + assert_str()("Hello, world!") + + async def test_with_invalid_value(self) -> None: with raises_error(error_type=AssertionFailed): - sut.assert_str()(False) + assert_str()(False) + - async def test_assert_list_with_list(self) -> None: - sut = Asserter() - sut.assert_list()([]) +class TestAssertList: + async def test_with_list(self) -> None: + assert_list()([]) - async def test_assert_list_without_list(self) -> None: - sut = Asserter() + async def test_without_list(self) -> None: with raises_error(error_type=AssertionFailed): - sut.assert_list()(False) + assert_list()(False) - async def test_assert_sequence_without_list(self) -> None: - sut = Asserter() + +class TestAssertSequence: + async def test_without_list(self) -> None: with raises_error(error_type=AssertionFailed): - sut.assert_sequence(AssertionChain(sut.assert_str()))(False) + assert_sequence(AssertionChain(assert_str()))(False) - async def test_assert_sequence_with_invalid_item(self) -> None: - sut = Asserter() + async def test_with_invalid_item(self) -> None: with raises_error(error_type=AssertionFailed, error_contexts=["0"]): - sut.assert_sequence(AssertionChain(sut.assert_str()))([123]) + assert_sequence(AssertionChain(assert_str()))([123]) + + async def test_with_empty_list(self) -> None: + assert_sequence(AssertionChain(assert_str()))([]) - async def test_assert_sequence_with_empty_list(self) -> None: - sut = Asserter() - sut.assert_sequence(AssertionChain(sut.assert_str()))([]) + async def test_with_valid_sequence(self) -> None: + assert_sequence(AssertionChain(assert_str()))(["Hello!"]) - async def test_assert_sequence_with_valid_sequence(self) -> None: - sut = Asserter() - sut.assert_sequence(AssertionChain(sut.assert_str()))(["Hello!"]) - async def test_assert_dict_with_dict(self) -> None: - sut = Asserter() - sut.assert_dict()({}) +class TestAssertDict: + async def test_with_dict(self) -> None: + assert_dict()({}) - async def test_assert_dict_without_dict(self) -> None: - sut = Asserter() + async def test_without_dict(self) -> None: with raises_error(error_type=AssertionFailed): - sut.assert_dict()(False) + assert_dict()(False) - async def test_assert_fields_with_invalid_value(self) -> None: - sut = Asserter() + +class TestAssertFields: + async def test_with_invalid_value(self) -> None: with raises_error(error_type=AssertionFailed): - sut.assert_fields( + assert_fields( Fields( OptionalField( "hello", - AssertionChain(sut.assert_str()), + AssertionChain(assert_str()), ) ) )(None) - async def test_assert_fields_required_without_key(self) -> None: - sut = Asserter() + async def test_required_without_key(self) -> None: with raises_error(error_type=AssertionFailed, error_contexts=["hello"]): - sut.assert_fields( + assert_fields( Fields( RequiredField( "hello", - AssertionChain(sut.assert_str()), + AssertionChain(assert_str()), ) ) )({}) - async def test_assert_fields_optional_without_key(self) -> None: - sut = Asserter() + async def test_optional_without_key(self) -> None: expected: dict[str, Any] = {} - actual = sut.assert_fields( + actual = assert_fields( Fields( OptionalField( "hello", - AssertionChain(sut.assert_str()), + AssertionChain(assert_str()), ) ) )({}) assert expected == actual - async def test_assert_fields_required_key_with_key(self) -> None: - sut = Asserter() + async def test_required_key_with_key(self) -> None: expected = { "hello": "World!", } - actual = sut.assert_fields( + actual = assert_fields( Fields( RequiredField( "hello", - AssertionChain(sut.assert_str()), + AssertionChain(assert_str()), ) ) )({"hello": "World!"}) assert expected == actual - async def test_assert_fields_optional_key_with_key(self) -> None: - sut = Asserter() + async def test_optional_key_with_key(self) -> None: expected = { "hello": "World!", } - actual = sut.assert_fields( + actual = assert_fields( Fields( OptionalField( "hello", - AssertionChain(sut.assert_str()), + AssertionChain(assert_str()), ) ) )({"hello": "World!"}) assert expected == actual - async def test_assert_field_with_invalid_value(self) -> None: - sut = Asserter() + +class TestAssertField: + async def test_with_invalid_value(self) -> None: with raises_error(error_type=AssertionFailed): - sut.assert_field( + assert_field( OptionalField( "hello", - AssertionChain(sut.assert_str()), + AssertionChain(assert_str()), ) )(None) - async def test_assert_field_required_without_key(self) -> None: - sut = Asserter() + async def test_required_without_key(self) -> None: with raises_error(error_type=AssertionFailed, error_contexts=["hello"]): - sut.assert_field( + assert_field( RequiredField( "hello", - AssertionChain(sut.assert_str()), + AssertionChain(assert_str()), ) )({}) - async def test_assert_field_optional_without_key(self) -> None: - sut = Asserter() + async def test_optional_without_key(self) -> None: expected = Void - actual = sut.assert_field( + actual = assert_field( OptionalField( "hello", - AssertionChain(sut.assert_str()), + AssertionChain(assert_str()), ) )({}) assert expected == actual - async def test_assert_field_required_key_with_key(self) -> None: - sut = Asserter() + async def test_required_key_with_key(self) -> None: expected = "World!" - actual = sut.assert_field( + actual = assert_field( RequiredField( "hello", - AssertionChain(sut.assert_str()), + AssertionChain(assert_str()), ) )({"hello": "World!"}) assert expected == actual - async def test_assert_field_optional_key_with_key(self) -> None: - sut = Asserter() + async def test_optional_key_with_key(self) -> None: expected = "World!" - actual = sut.assert_field( + actual = assert_field( OptionalField( "hello", - AssertionChain(sut.assert_str()), + AssertionChain(assert_str()), ) )({"hello": "World!"}) assert expected == actual - async def test_assert_mapping_without_mapping(self) -> None: - sut = Asserter() + +class TestAssertMapping: + async def test_without_mapping(self) -> None: with raises_error(error_type=AssertionFailed): - sut.assert_mapping(AssertionChain(sut.assert_str()))(None) + assert_mapping(AssertionChain(assert_str()))(None) - async def test_assert_mapping_with_invalid_item(self) -> None: - sut = Asserter() + async def test_with_invalid_item(self) -> None: with raises_error(error_type=AssertionFailed, error_contexts=["hello"]): - sut.assert_mapping(AssertionChain(sut.assert_str()))({"hello": False}) + assert_mapping(AssertionChain(assert_str()))({"hello": False}) + + async def test_with_empty_dict(self) -> None: + assert_mapping(AssertionChain(assert_str()))({}) - async def test_assert_mapping_with_empty_dict(self) -> None: - sut = Asserter() - sut.assert_mapping(AssertionChain(sut.assert_str()))({}) + async def test_with_valid_mapping(self) -> None: + assert_mapping(AssertionChain(assert_str()))({"hello": "World!"}) - async def test_assert_mapping_with_valid_mapping(self) -> None: - sut = Asserter() - sut.assert_mapping(AssertionChain(sut.assert_str()))({"hello": "World!"}) - async def test_assert_record_with_optional_fields_without_items(self) -> None: - sut = Asserter() +class TestAssertRecord: + async def test_with_optional_fields_without_items(self) -> None: expected: dict[str, Any] = {} - actual = sut.assert_record( + actual = assert_record( Fields( OptionalField( "hello", - AssertionChain(sut.assert_str()), + AssertionChain(assert_str()), ), ) )({}) assert expected == actual - async def test_assert_record_with_optional_fields_with_items(self) -> None: - sut = Asserter() + async def test_with_optional_fields_with_items(self) -> None: expected = { "hello": "WORLD!", } - actual = sut.assert_record( + actual = assert_record( Fields( OptionalField( "hello", - AssertionChain(sut.assert_str()) | (lambda x: x.upper()), + AssertionChain(assert_str()) | (lambda x: x.upper()), ), ) )({"hello": "World!"}) assert expected == actual - async def test_assert_record_with_required_fields_without_items(self) -> None: - sut = Asserter() + async def test_with_required_fields_without_items(self) -> None: with raises_error(error_type=AssertionFailed): - sut.assert_record( + assert_record( Fields( RequiredField( "hello", - AssertionChain(sut.assert_str()), + AssertionChain(assert_str()), ), ) )({}) - async def test_assert_record_with_required_fields_with_items(self) -> None: - sut = Asserter() + async def test_with_required_fields_with_items(self) -> None: expected = { "hello": "WORLD!", } - actual = sut.assert_record( + actual = assert_record( Fields( RequiredField( "hello", - AssertionChain(sut.assert_str()) | (lambda x: x.upper()), + AssertionChain(assert_str()) | (lambda x: x.upper()), ), ) )( @@ -389,31 +386,29 @@ async def test_assert_record_with_required_fields_with_items(self) -> None: ) assert expected == actual - async def test_assert_path_without_str(self) -> None: - sut = Asserter() + +class TestAssertPath: + async def test_without_str(self) -> None: with raises_error(error_type=AssertionFailed): - sut.assert_path()(False) + assert_path()(False) + + async def test_with_valid_path(self) -> None: + assert_path()("~/../foo/bar") - async def test_assert_path_with_valid_path(self) -> None: - sut = Asserter() - sut.assert_path()("~/../foo/bar") - async def test_assert_directory_path_without_str(self) -> None: - sut = Asserter() +class TestAssertDirectoryPath: + async def test_without_str(self) -> None: with raises_error(error_type=AssertionFailed): - sut.assert_directory_path()(False) + assert_directory_path()(False) - async def test_assert_directory_path_without_existing_path(self) -> None: - sut = Asserter() + async def test_without_existing_path(self) -> None: with raises_error(error_type=AssertionFailed): - sut.assert_directory_path()("~/../foo/bar") + assert_directory_path()("~/../foo/bar") - async def test_assert_directory_path_without_directory_path(self) -> None: - sut = Asserter() + async def test_without_directory_path(self) -> None: with NamedTemporaryFile() as f, raises_error(error_type=AssertionFailed): - sut.assert_directory_path()(f.name) + assert_directory_path()(f.name) - async def test_assert_directory_path_with_valid_path(self) -> None: - sut = Asserter() + async def test_with_valid_path(self) -> None: async with TemporaryDirectory() as directory_path_str: - sut.assert_directory_path()(directory_path_str) + assert_directory_path()(directory_path_str) diff --git a/betty/tests/test_config.py b/betty/tests/test_config.py index da1da4c47..564fde946 100644 --- a/betty/tests/test_config.py +++ b/betty/tests/test_config.py @@ -14,7 +14,7 @@ ConfigurationSequence, ConfigurationKey, ) -from betty.serde.load import FormatError, Asserter +from betty.serde.load import FormatError, assert_dict if TYPE_CHECKING: from betty.serde.dump import Dump, VoidableDump @@ -300,13 +300,12 @@ def _load_key( item_dump: Dump, key_dump: str, ) -> Dump: - asserter = Asserter() - dict_item_dump = asserter.assert_dict()(item_dump) + dict_item_dump = assert_dict()(item_dump) dict_item_dump[key_dump] = key_dump return dict_item_dump def _dump_key(self, item_dump: VoidableDump) -> tuple[VoidableDump, str]: - dict_item_dump = self._asserter.assert_dict()(item_dump) + dict_item_dump = assert_dict()(item_dump) return dict_item_dump, dict_item_dump.pop("key") diff --git a/betty/tests/test_project.py b/betty/tests/test_project.py index 50b4a12d8..a24c4dec0 100644 --- a/betty/tests/test_project.py +++ b/betty/tests/test_project.py @@ -21,9 +21,10 @@ ) from betty.serde.load import ( AssertionFailed, - Asserter, Fields, RequiredField, + assert_bool, + assert_record, ) from betty.tests.serde import raises_error from betty.tests.test_config import ( @@ -973,10 +974,9 @@ def load( ) -> Self: if configuration is None: configuration = cls() - asserter = Asserter() - asserter.assert_record( + assert_record( Fields( - RequiredField("check", asserter.assert_bool()), + RequiredField("check", assert_bool()), ), )(dump) return configuration