From d781f788af2428284dcb7f4f2cc12a8fc5ecefef Mon Sep 17 00:00:00 2001 From: Bart Feenstra Date: Sat, 19 Oct 2024 11:29:38 +0100 Subject: [PATCH] Make Configurable require Configuration --- betty/app/__init__.py | 3 +- betty/assertion/__init__.py | 22 ++- betty/assets/locale/betty.pot | 5 +- betty/assets/locale/de-DE/betty.po | 5 +- betty/assets/locale/fr-FR/betty.po | 5 +- betty/assets/locale/nl-NL/betty.po | 5 +- betty/assets/locale/uk/betty.po | 5 +- betty/cli/commands/new.py | 11 +- betty/config/__init__.py | 25 ++- betty/plugin/config.py | 87 +++++++++- betty/project/__init__.py | 17 +- betty/project/config.py | 108 ++++-------- betty/project/extension/__init__.py | 18 +- .../extension/cotton_candy/__init__.py | 13 +- betty/project/extension/gramps/__init__.py | 8 +- betty/project/extension/webpack/__init__.py | 6 +- betty/project/extension/wikipedia/__init__.py | 7 +- .../test_utils/config/collections/__init__.py | 48 +++--- .../test_utils/config/collections/mapping.py | 5 +- .../test_utils/config/collections/sequence.py | 5 +- betty/test_utils/jinja2.py | 2 +- betty/test_utils/plugin/config.py | 8 +- .../test_utils/project/extension/__init__.py | 2 +- betty/tests/cli/commands/test_new.py | 2 +- .../tests/config/collections/test_mapping.py | 48 +++--- .../tests/config/collections/test_sequence.py | 16 +- betty/tests/config/test___init__.py | 13 +- betty/tests/coverage/test_coverage.py | 4 + betty/tests/plugin/test_config.py | 146 ++++++++++++++++- .../extension/cotton_candy/test_search.py | 22 +-- .../project/extension/demo/test___init__.py | 3 +- .../extension/deriver/test___init__.py | 3 +- .../project/extension/gramps/test_config.py | 4 +- .../extension/http_api_doc/test___init__.py | 3 +- .../project/extension/maps/test___init__.py | 3 +- .../extension/privatizer/test___init__.py | 3 +- .../project/extension/trees/test___init__.py | 3 +- .../extension/webpack/test___init__.py | 8 +- .../project/extension/webpack/test_build.py | 2 +- .../extension/wikipedia/test___init__.py | 9 +- betty/tests/project/test___init__.py | 44 +++-- betty/tests/project/test_config.py | 155 +++++------------- playwright/tests/cotton_candy/search.spec.ts | 2 +- 43 files changed, 501 insertions(+), 412 deletions(-) diff --git a/betty/app/__init__.py b/betty/app/__init__.py index d5ffae290..c3f2386ec 100644 --- a/betty/app/__init__.py +++ b/betty/app/__init__.py @@ -55,8 +55,7 @@ def __init__( cache_factory: Callable[[Self], Cache[Any]], fetcher: Fetcher | None = None, ): - super().__init__() - self._configuration = configuration + super().__init__(configuration=configuration) self._assets: AssetRepository | None = None self._localization_initialized = False self._localizer: Localizer | None = None diff --git a/betty/assertion/__init__.py b/betty/assertion/__init__.py index cced36f3c..4f4c49e62 100644 --- a/betty/assertion/__init__.py +++ b/betty/assertion/__init__.py @@ -25,7 +25,7 @@ from betty.error import FileNotFound, UserFacingError from betty.locale import get_data, UNDETERMINED_LOCALE from betty.locale.localizable import _, Localizable, plain, join, do_you_mean -from betty.typing import Void, Voidable +from betty.typing import Void, Voidable, internal Number: TypeAlias = int | float @@ -90,8 +90,16 @@ def __call__(self, value: _AssertionValueT) -> _AssertionReturnT: return self._assertion(value) +@internal @dataclass(frozen=True) -class _Field(Generic[_AssertionValueT, _AssertionReturnT]): +class Field(Generic[_AssertionValueT, _AssertionReturnT]): + """ + A key-value mapping field. + + Do not instantiate this class directly. Use :py:class:`betty.assertion.RequiredField` or + :py:class:`betty.assertion.OptionalField` instead. + """ + name: str assertion: Assertion[_AssertionValueT, _AssertionReturnT] | None = None @@ -100,7 +108,7 @@ class _Field(Generic[_AssertionValueT, _AssertionReturnT]): @dataclass(frozen=True) class RequiredField( Generic[_AssertionValueT, _AssertionReturnT], - _Field[_AssertionValueT, _AssertionReturnT], + Field[_AssertionValueT, _AssertionReturnT], ): """ A required key-value mapping field. @@ -113,7 +121,7 @@ class RequiredField( @dataclass(frozen=True) class OptionalField( Generic[_AssertionValueT, _AssertionReturnT], - _Field[_AssertionValueT, _AssertionReturnT], + Field[_AssertionValueT, _AssertionReturnT], ): """ An optional key-value mapping field. @@ -370,7 +378,7 @@ def _assert_mapping( def assert_fields( - *fields: _Field[Any, Any], + *fields: Field[Any, Any], ) -> AssertionChain[Any, MutableMapping[str, Any]]: """ Assert that a value is a key-value mapping of arbitrary value types, and assert several of its values. @@ -409,7 +417,7 @@ def assert_field( def assert_field( - field: _Field[_AssertionValueT, _AssertionReturnT], + field: Field[_AssertionValueT, _AssertionReturnT], ) -> ( AssertionChain[_AssertionValueT, _AssertionReturnT] | AssertionChain[_AssertionValueT, Voidable[_AssertionReturnT]] @@ -430,7 +438,7 @@ def _assert_field( def assert_record( - *fields: _Field[Any, Any], + *fields: Field[Any, Any], ) -> AssertionChain[Any, MutableMapping[str, Any]]: """ Assert that a value is a record: a key-value mapping of arbitrary value types, with a known structure. diff --git a/betty/assets/locale/betty.pot b/betty/assets/locale/betty.pot index 8e25c69c1..8d795bfd5 100644 --- a/betty/assets/locale/betty.pot +++ b/betty/assets/locale/betty.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Betty VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2024-10-12 23:15+0100\n" +"POT-Creation-Date: 2024-10-21 00:53+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -1005,9 +1005,6 @@ msgstr "" msgid "{event_type} of {subjects}" msgstr "" -msgid "{extension_type} is not configurable." -msgstr "" - msgid "{extension} does not have an assets directory." msgstr "" diff --git a/betty/assets/locale/de-DE/betty.po b/betty/assets/locale/de-DE/betty.po index d82f24e71..f41473b0d 100644 --- a/betty/assets/locale/de-DE/betty.po +++ b/betty/assets/locale/de-DE/betty.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: Betty VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2024-10-12 23:15+0100\n" +"POT-Creation-Date: 2024-10-21 00:53+0100\n" "PO-Revision-Date: 2024-02-08 13:24+0000\n" "Last-Translator: Bart Feenstra \n" "Language: de\n" @@ -1198,9 +1198,6 @@ msgstr "{event_type} ({event_description}) von {subjects}" msgid "{event_type} of {subjects}" msgstr "{event_type} von {subjects}" -msgid "{extension_type} is not configurable." -msgstr "{extension_type} ist nicht konfigurierbar." - msgid "{extension} does not have an assets directory." msgstr "" diff --git a/betty/assets/locale/fr-FR/betty.po b/betty/assets/locale/fr-FR/betty.po index 2c3bfd287..3b6be54db 100644 --- a/betty/assets/locale/fr-FR/betty.po +++ b/betty/assets/locale/fr-FR/betty.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2024-10-12 23:15+0100\n" +"POT-Creation-Date: 2024-10-21 00:53+0100\n" "PO-Revision-Date: 2024-02-08 13:24+0000\n" "Last-Translator: Bart Feenstra \n" "Language: fr\n" @@ -1141,9 +1141,6 @@ msgstr "{event_type} ({event_description}) de {subjects}" msgid "{event_type} of {subjects}" msgstr "{event_type} de {subjects}" -msgid "{extension_type} is not configurable." -msgstr "" - msgid "{extension} does not have an assets directory." msgstr "" diff --git a/betty/assets/locale/nl-NL/betty.po b/betty/assets/locale/nl-NL/betty.po index c939e9962..1bdad0f93 100644 --- a/betty/assets/locale/nl-NL/betty.po +++ b/betty/assets/locale/nl-NL/betty.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2024-10-12 23:15+0100\n" +"POT-Creation-Date: 2024-10-21 00:53+0100\n" "PO-Revision-Date: 2024-02-11 15:31+0000\n" "Last-Translator: Bart Feenstra \n" "Language: nl\n" @@ -1233,9 +1233,6 @@ msgstr "{event_type} ({event_description}) van {subjects}" msgid "{event_type} of {subjects}" msgstr "{event_type} van {subjects}" -msgid "{extension_type} is not configurable." -msgstr "\"{extension_type}\" kan niet ingesteld worden." - msgid "{extension} does not have an assets directory." msgstr "{extension} heeft geen assets-directory." diff --git a/betty/assets/locale/uk/betty.po b/betty/assets/locale/uk/betty.po index 00615bf2c..23eb6ae3e 100644 --- a/betty/assets/locale/uk/betty.po +++ b/betty/assets/locale/uk/betty.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: Betty VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2024-10-12 23:15+0100\n" +"POT-Creation-Date: 2024-10-21 00:53+0100\n" "PO-Revision-Date: 2024-02-08 13:08+0000\n" "Last-Translator: Rainer Thieringer \n" "Language: uk\n" @@ -1119,9 +1119,6 @@ msgstr "" msgid "{event_type} of {subjects}" msgstr "{event_type} {subjects}" -msgid "{extension_type} is not configurable." -msgstr "" - msgid "{extension} does not have an assets directory." msgstr "" diff --git a/betty/cli/commands/new.py b/betty/cli/commands/new.py index 1ce9211a0..1ee9a20c1 100644 --- a/betty/cli/commands/new.py +++ b/betty/cli/commands/new.py @@ -82,15 +82,12 @@ async def new() -> None: configuration_file_path, ) - configuration.extensions.enable(CottonCandy) - configuration.extensions.enable(Deriver) - configuration.extensions.enable(Privatizer) - configuration.extensions.enable(Wikipedia) + await configuration.extensions.enable( + CottonCandy, Deriver, Privatizer, Wikipedia + ) webpack_requirement = await Webpack.requirement() if webpack_requirement.is_met(): - configuration.extensions.enable(HttpApiDoc) - configuration.extensions.enable(Maps) - configuration.extensions.enable(Trees) + await configuration.extensions.enable(HttpApiDoc, Maps, Trees) configuration.locales.replace( LocaleConfiguration( diff --git a/betty/config/__init__.py b/betty/config/__init__.py index 964df7e2a..3c4b60a8b 100644 --- a/betty/config/__init__.py +++ b/betty/config/__init__.py @@ -4,9 +4,10 @@ from __future__ import annotations +from abc import abstractmethod from collections.abc import Callable from contextlib import chdir -from typing import Generic, TypeVar, TypeAlias, TYPE_CHECKING, Self +from typing import Generic, TypeVar, TypeAlias, TYPE_CHECKING, Self, Any import aiofiles from aiofiles.os import makedirs @@ -46,20 +47,32 @@ class Configurable(Generic[_ConfigurationT]): Any configurable object. """ - _configuration: _ConfigurationT + def __init__(self, *args: Any, configuration: _ConfigurationT, **kwargs: Any): + super().__init__(*args, **kwargs) + self._configuration = configuration @property def configuration(self) -> _ConfigurationT: """ The object's configuration. """ - if not hasattr(self, "_configuration"): - raise RuntimeError( - f"{self} has no configuration. {type(self)}.__init__() must ensure it is set." - ) return self._configuration +class DefaultConfigurable(Configurable[_ConfigurationT], Generic[_ConfigurationT]): + """ + A configurable type that can provide its own default configuration. + """ + + @classmethod + @abstractmethod + def new_default_configuration(cls) -> _ConfigurationT: + """ + Create this extension's default configuration. + """ + pass + + async def assert_configuration_file( configuration: _ConfigurationT, ) -> AssertionChain[Path, _ConfigurationT]: diff --git a/betty/plugin/config.py b/betty/plugin/config.py index 741ed5886..0bb5728e1 100644 --- a/betty/plugin/config.py +++ b/betty/plugin/config.py @@ -2,8 +2,9 @@ Provide plugin configuration. """ -from collections.abc import Sequence -from typing import TypeVar, Generic, cast +from __future__ import annotations + +from typing import TypeVar, Generic, cast, Sequence, Any, TYPE_CHECKING from typing_extensions import override @@ -12,18 +13,24 @@ assert_record, OptionalField, assert_setattr, + Field, + assert_field, ) -from betty.config import Configuration +from betty.config import Configuration, DefaultConfigurable from betty.config.collections.mapping import ConfigurationMapping -from betty.locale.localizable import ShorthandStaticTranslations from betty.locale.localizable.config import ( OptionalStaticTranslationsLocalizableConfigurationAttr, RequiredStaticTranslationsLocalizableConfigurationAttr, ) from betty.machine_name import assert_machine_name, MachineName -from betty.plugin import Plugin -from betty.serde.dump import Dump, DumpMapping +from betty.plugin import Plugin, PluginRepository +from betty.plugin.assertion import assert_plugin + +if TYPE_CHECKING: + from betty.locale.localizable import ShorthandStaticTranslations + from betty.serde.dump import Dump, DumpMapping +_PluginT = TypeVar("_PluginT", bound=Plugin) _PluginCoT = TypeVar("_PluginCoT", bound=Plugin, covariant=True) @@ -131,3 +138,71 @@ def load_item(self, dump: Dump) -> PluginConfiguration: @classmethod def _create_default_item(cls, configuration_key: str) -> PluginConfiguration: return PluginConfiguration(configuration_key, {}) + + +class PluginInstanceConfiguration(Configuration, Generic[_PluginT]): + """ + Configure a single plugin instance. + + Plugins that extend :py:class:`betty.config.DefaultConfigurable` may receive their configuration from + :py:attr:`betty.plugin.config.PluginInstanceConfiguration.plugin_configuration` / the `"configuration"` dump key. + """ + + def __init__( + self, + plugin: type[_PluginT], + *, + plugin_repository: PluginRepository[_PluginT], + plugin_configuration: Configuration | None = None, + ): + if plugin_configuration and not issubclass(plugin, DefaultConfigurable): + raise ValueError( + f"{plugin} is not configurable (it must extend {DefaultConfigurable}), but configuration was given." + ) + if ( + issubclass(plugin, DefaultConfigurable) # type: ignore[redundant-expr] + and not plugin_configuration # type: ignore[unreachable] + ): + plugin_configuration = plugin.new_default_configuration() # type: ignore[unreachable] + super().__init__() + self._plugin = plugin + self._plugin_configuration = plugin_configuration + self._plugin_repository = plugin_repository + + @property + def plugin(self) -> type[_PluginT]: + """ + The plugin. + """ + return self._plugin + + @property + def plugin_configuration(self) -> Configuration | None: + """ + Get the plugin's own configuration. + """ + return self._plugin_configuration + + @override + def load(self, dump: Dump) -> None: + id_field = RequiredField( + "id", + assert_plugin(self._plugin_repository) | assert_setattr(self, "_plugin"), + ) + plugin = assert_field(id_field)(dump) + fields = [id_field, *self._fields()] + if issubclass(plugin, DefaultConfigurable): + configuration = plugin.new_default_configuration() + self._plugin_configuration = configuration + fields.append(RequiredField("configuration", configuration.load)) + assert_record(*fields)(dump) + + def _fields(self) -> Sequence[Field[Any, Any]]: + return [] + + @override + def dump(self) -> DumpMapping[Dump]: + dump: DumpMapping[Dump] = {"id": self.plugin.plugin_id()} + if issubclass(self.plugin, DefaultConfigurable): # type: ignore[redundant-expr] + dump["configuration"] = self.plugin_configuration.dump() # type: ignore[unreachable] + return dump diff --git a/betty/project/__init__.py b/betty/project/__init__.py index 4c841cbbd..def296d27 100644 --- a/betty/project/__init__.py +++ b/betty/project/__init__.py @@ -98,9 +98,8 @@ def __init__( *, ancestry: Ancestry, ): - super().__init__() + super().__init__(configuration=configuration) self._app = app - self._configuration = configuration self._ancestry = ancestry self._assets: AssetRepository | None = None @@ -339,11 +338,11 @@ async def _init_extensions(self) -> ProjectExtensions: for project_extension_configuration in self.configuration.extensions.values(): if project_extension_configuration.enabled: extension_requirement = ( - await project_extension_configuration.extension_type.requirement() + await project_extension_configuration.plugin.requirement() ) extension_requirement.assert_met() extension_types_enabled_in_configuration.add( - project_extension_configuration.extension_type + project_extension_configuration.plugin ) extension_types_sorter = TopologicalSorter[type[Extension]]() @@ -363,11 +362,11 @@ async def _init_extensions(self) -> ProjectExtensions: isinstance(extension, ConfigurableExtension) and extension_type in self.configuration.extensions ): - extension.configuration.update( - self.configuration.extensions[ - extension_type - ].extension_configuration - ) + extension_configuration = self.configuration.extensions[ + extension_type + ].plugin_configuration + if extension_configuration: + extension.configuration.update(extension_configuration) if isinstance(extension, Theme): theme_count += 1 extensions_batch.append(extension) diff --git a/betty/project/config.py b/betty/project/config.py index e8064ddbb..8ea9f85b7 100644 --- a/betty/project/config.py +++ b/betty/project/config.py @@ -26,7 +26,6 @@ OptionalField, assert_str, assert_bool, - Assertion, assert_fields, assert_locale, assert_positive_number, @@ -35,6 +34,8 @@ assert_mapping, assert_none, assert_or, + Field, + assert_field, ) from betty.assertion.error import AssertionFailed from betty.config import Configuration @@ -61,6 +62,7 @@ PluginConfigurationPluginConfigurationMapping, PluginConfiguration, PluginConfigurationMapping, + PluginInstanceConfiguration, ) from betty.project import extension from betty.project.extension import Extension, ConfigurableExtension @@ -68,10 +70,7 @@ from betty.serde.format import Format, format_for, FORMAT_REPOSITORY if TYPE_CHECKING: - from betty.serde.dump import ( - Dump, - DumpMapping, - ) + from betty.serde.dump import Dump, DumpMapping from collections.abc import Sequence from betty.machine_name import MachineName from pathlib import Path @@ -250,7 +249,7 @@ def _pre_add(self, configuration: EntityReference[_EntityT]) -> None: @final -class ExtensionConfiguration(Configuration): +class ExtensionConfiguration(PluginInstanceConfiguration[Extension]): """ Configure a single extension for a project. """ @@ -262,21 +261,16 @@ def __init__( enabled: bool = True, extension_configuration: Configuration | None = None, ): - super().__init__() - self._extension_type = extension_type - self._enabled = enabled - if extension_configuration is None and issubclass( + if not extension_configuration and issubclass( extension_type, ConfigurableExtension ): - extension_configuration = extension_type.default_configuration() - self._set_extension_configuration(extension_configuration) - - @property - def extension_type(self) -> type[Extension]: - """ - The extension type. - """ - return self._extension_type + extension_configuration = extension_type.new_default_configuration() + super().__init__( + extension_type, + plugin_configuration=extension_configuration, + plugin_repository=extension.EXTENSION_REPOSITORY, + ) + self._enabled = enabled @property def enabled(self) -> bool: @@ -289,60 +283,16 @@ def enabled(self) -> bool: def enabled(self, enabled: bool) -> None: self._enabled = enabled - @property - def extension_configuration(self) -> Configuration | None: - """ - Get the extension's own configuration. - """ - return self._extension_configuration - - def _set_extension_configuration( - self, extension_configuration: Configuration | None - ) -> None: - self._extension_configuration = extension_configuration - @override - def load(self, dump: Dump) -> None: - assert_record( - RequiredField( - "extension", - assert_plugin(extension.EXTENSION_REPOSITORY) - | assert_setattr(self, "_extension_type"), - ), - OptionalField("enabled", assert_bool() | assert_setattr(self, "enabled")), - OptionalField( - "configuration", - self._assert_load_extension_configuration(self.extension_type), - ), - )(dump) - - def _assert_load_extension_configuration( - self, extension_type: type[Extension] - ) -> Assertion[Any, Configuration]: - def _assertion(value: Any) -> Configuration: - extension_configuration = self._extension_configuration - if isinstance(extension_configuration, Configuration): - extension_configuration.load(value) - return extension_configuration - raise AssertionFailed( - _("{extension_type} is not configurable.").format( - extension_type=extension_type.plugin_id() - ) - ) - - return _assertion + def _fields(self) -> Sequence[Field[Any, Any]]: + return [ + OptionalField("enabled", assert_bool() | assert_setattr(self, "enabled")) + ] @override def dump(self) -> DumpMapping[Dump]: - dump: DumpMapping[Dump] = { - "extension": self.extension_type.plugin_id(), - "enabled": self.enabled, - } - if ( - issubclass(self.extension_type, ConfigurableExtension) - and self.extension_configuration - ): - dump["configuration"] = self.extension_configuration.dump() + dump = super().dump() + dump["enabled"] = self.enabled return dump @@ -362,26 +312,32 @@ def __init__( @override def load_item(self, dump: Dump) -> ExtensionConfiguration: - fields_dump = assert_fields( - RequiredField("extension", assert_plugin(extension.EXTENSION_REPOSITORY)) + extension_type = assert_field( + RequiredField("id", assert_plugin(extension.EXTENSION_REPOSITORY)) )(dump) - configuration = ExtensionConfiguration(fields_dump["extension"]) + configuration = ExtensionConfiguration( + extension_type, + extension_configuration=extension_type.new_default_configuration() + # @todo Move this check to the parent class. + if issubclass(extension_type, ConfigurableExtension) + else None, + ) configuration.load(dump) return configuration @override def _get_key(self, configuration: ExtensionConfiguration) -> type[Extension]: - return configuration.extension_type + return configuration.plugin @override def _load_key(self, item_dump: DumpMapping[Dump], key_dump: str) -> None: - item_dump["extension"] = key_dump + item_dump["id"] = key_dump @override def _dump_key(self, item_dump: DumpMapping[Dump]) -> str: - return cast(str, item_dump.pop("extension")) + return cast(str, item_dump.pop("id")) - def enable(self, *extension_types: type[Extension]) -> None: + async def enable(self, *extension_types: type[Extension]) -> None: """ Enable the given extensions. """ diff --git a/betty/project/extension/__init__.py b/betty/project/extension/__init__.py index 4120cfc4c..4c8a2de65 100644 --- a/betty/project/extension/__init__.py +++ b/betty/project/extension/__init__.py @@ -2,12 +2,11 @@ from __future__ import annotations -from abc import abstractmethod from typing import TypeVar, TYPE_CHECKING, Generic, Self, Sequence from typing_extensions import override -from betty.config import Configurable, Configuration +from betty.config import Configuration, DefaultConfigurable from betty.core import CoreComponent from betty.locale.localizable import Localizable, _, call from betty.plugin import ( @@ -113,23 +112,16 @@ class Theme(Extension): class ConfigurableExtension( - Extension, Generic[_ConfigurationT], Configurable[_ConfigurationT] + DefaultConfigurable[_ConfigurationT], Extension, Generic[_ConfigurationT] ): """ A configurable extension. """ - def __init__(self, project: Project): - super().__init__(project) - self._configuration = self.default_configuration() - + @override @classmethod - @abstractmethod - def default_configuration(cls) -> _ConfigurationT: - """ - Get this extension's default configuration. - """ - pass + async def new_for_project(cls, project: Project) -> Self: + return cls(project, configuration=cls.new_default_configuration()) async def sort_extension_type_graph( diff --git a/betty/project/extension/cotton_candy/__init__.py b/betty/project/extension/cotton_candy/__init__.py index 8ec0313cc..053658664 100644 --- a/betty/project/extension/cotton_candy/__init__.py +++ b/betty/project/extension/cotton_candy/__init__.py @@ -126,8 +126,14 @@ class CottonCandy( _plugin_description = _("Cotton Candy is Betty's default theme.") @private - def __init__(self, project: Project, public_css_paths: Sequence[str]): - super().__init__(project) + def __init__( + self, + project: Project, + public_css_paths: Sequence[str], + *, + configuration: CottonCandyConfiguration, + ): + super().__init__(project, configuration=configuration) self._public_css_paths = public_css_paths @override @@ -137,6 +143,7 @@ async def new_for_project(cls, project: Project) -> Self: return cls( project, [static_url_generator.generate("/css/cotton-candy.css")], + configuration=cls.new_default_configuration(), ) @override @@ -182,7 +189,7 @@ def public_css_paths(self) -> Sequence[str]: @override @classmethod - def default_configuration(cls) -> CottonCandyConfiguration: + def new_default_configuration(cls) -> CottonCandyConfiguration: return CottonCandyConfiguration() @override diff --git a/betty/project/extension/gramps/__init__.py b/betty/project/extension/gramps/__init__.py index bbc0d1af8..3fee504e0 100644 --- a/betty/project/extension/gramps/__init__.py +++ b/betty/project/extension/gramps/__init__.py @@ -9,11 +9,11 @@ from typing_extensions import override -from betty.project.extension.gramps.config import GrampsConfiguration from betty.gramps.loader import GrampsLoader from betty.locale.localizable import static, _ from betty.plugin import ShorthandPluginBase from betty.project.extension import ConfigurableExtension +from betty.project.extension.gramps.config import GrampsConfiguration from betty.project.load import LoadAncestryEvent if TYPE_CHECKING: @@ -22,9 +22,7 @@ async def _load_ancestry(event: LoadAncestryEvent) -> None: project = event.project - gramps_configuration = project.configuration.extensions[ - Gramps - ].extension_configuration + gramps_configuration = project.configuration.extensions[Gramps].plugin_configuration assert isinstance(gramps_configuration, GrampsConfiguration) for family_tree_configuration in gramps_configuration.family_trees: file_path = family_tree_configuration.file_path @@ -65,7 +63,7 @@ class Gramps(ShorthandPluginBase, ConfigurableExtension[GrampsConfiguration]): @override @classmethod - def default_configuration(cls) -> GrampsConfiguration: + def new_default_configuration(cls) -> GrampsConfiguration: return GrampsConfiguration() @override diff --git a/betty/project/extension/webpack/__init__.py b/betty/project/extension/webpack/__init__.py index 04ce9ef18..51877ff00 100644 --- a/betty/project/extension/webpack/__init__.py +++ b/betty/project/extension/webpack/__init__.py @@ -58,13 +58,13 @@ async def _prebuild_webpack_assets() -> None: async with App.new_temporary() as app, app: job_context = Context() async with Project.new_temporary(app) as project: - project.configuration.extensions.enable(Webpack) - project.configuration.extensions.enable( + await project.configuration.extensions.enable( + Webpack, *( await extension.EXTENSION_REPOSITORY.select( WebpackEntryPointProvider # type: ignore[type-abstract] ) - ) + ), ) async with project: extensions = await project.extensions diff --git a/betty/project/extension/wikipedia/__init__.py b/betty/project/extension/wikipedia/__init__.py index fc72042c9..b58686223 100644 --- a/betty/project/extension/wikipedia/__init__.py +++ b/betty/project/extension/wikipedia/__init__.py @@ -54,8 +54,10 @@ def __init__( self, project: Project, wikipedia_contributors_copyright_notice: CopyrightNotice, + *, + configuration: WikipediaConfiguration, ): - super().__init__(project) + super().__init__(project, configuration=configuration) self._wikipedia_contributors_copyright_notice = ( wikipedia_contributors_copyright_notice ) @@ -67,6 +69,7 @@ async def new_for_project(cls, project: Project) -> Self: return cls( project, await project.copyright_notices.new_target("wikipedia-contributors"), + configuration=cls.new_default_configuration(), ) _plugin_id = "wikipedia" @@ -150,5 +153,5 @@ def assets_directory_path(cls) -> Path | None: @override @classmethod - def default_configuration(cls) -> WikipediaConfiguration: + def new_default_configuration(cls) -> WikipediaConfiguration: return WikipediaConfiguration() diff --git a/betty/test_utils/config/collections/__init__.py b/betty/test_utils/config/collections/__init__.py index 94e92d40c..1ae0a1614 100644 --- a/betty/test_utils/config/collections/__init__.py +++ b/betty/test_utils/config/collections/__init__.py @@ -18,7 +18,7 @@ class ConfigurationCollectionTestBase(Generic[_ConfigurationKeyT, _Configuration A base class for testing :py:class:`betty.config.collections.ConfigurationCollection` implementations. """ - def get_sut( + async def get_sut( self, configurations: Iterable[_ConfigurationT] | None = None ) -> ConfigurationCollection[_ConfigurationKeyT, _ConfigurationT]: """ @@ -36,7 +36,7 @@ def get_configuration_keys( """ raise NotImplementedError - def get_configurations( + async def get_configurations( self, ) -> tuple[_ConfigurationT, _ConfigurationT, _ConfigurationT, _ConfigurationT]: """ @@ -48,8 +48,8 @@ async def test_load_item(self) -> None: """ Tests :py:meth:`betty.config.collections.ConfigurationCollection.load_item` implementations. """ - configurations = self.get_configurations() - sut = self.get_sut(configurations) + configurations = await self.get_configurations() + sut = await self.get_sut(configurations) for configuration in configurations: sut.load_item(configuration.dump()) @@ -57,10 +57,10 @@ async def test_replace_without_items(self) -> None: """ Tests :py:meth:`betty.config.collections.ConfigurationCollection.replace` implementations. """ - sut = self.get_sut() + sut = await self.get_sut() sut.clear() assert len(sut) == 0 - self.get_configurations() + await self.get_configurations() sut.replace() assert len(sut) == 0 @@ -68,10 +68,10 @@ async def test_replace_with_items(self) -> None: """ Tests :py:meth:`betty.config.collections.ConfigurationCollection.replace` implementations. """ - sut = self.get_sut() + sut = await self.get_sut() sut.clear() assert len(sut) == 0 - configurations = self.get_configurations() + configurations = await self.get_configurations() sut.replace(*configurations) assert len(sut) == len(configurations) @@ -79,32 +79,32 @@ async def test___getitem__(self) -> None: """ Tests :py:meth:`betty.config.collections.ConfigurationCollection.__getitem__` implementations. """ - configuration = self.get_configurations()[0] - sut = self.get_sut([configuration]) + configuration = (await self.get_configurations())[0] + sut = await self.get_sut([configuration]) assert list(sut.values()) == [configuration] async def test_keys(self) -> None: """ Tests :py:meth:`betty.config.collections.ConfigurationCollection.keys` implementations. """ - configurations = self.get_configurations() - sut = self.get_sut(configurations) + configurations = await self.get_configurations() + sut = await self.get_sut(configurations) assert list(sut.keys()) == [*self.get_configuration_keys()] async def test_values(self) -> None: """ Tests :py:meth:`betty.config.collections.ConfigurationCollection.values` implementations. """ - configurations = self.get_configurations() - sut = self.get_sut(configurations) + configurations = await self.get_configurations() + sut = await self.get_sut(configurations) assert list(sut.values()) == [*configurations] async def test___delitem__(self) -> None: """ Tests :py:meth:`betty.config.collections.ConfigurationCollection.__delitem__` implementations. """ - configuration = self.get_configurations()[0] - sut = self.get_sut([configuration]) + configuration = (await self.get_configurations())[0] + sut = await self.get_sut([configuration]) del sut[self.get_configuration_keys()[0]] assert list(sut.values()) == [] @@ -118,8 +118,8 @@ async def test___len__(self) -> None: """ Tests :py:meth:`betty.config.collections.ConfigurationCollection.__len__` implementations. """ - configurations = self.get_configurations() - sut = self.get_sut( + configurations = await self.get_configurations() + sut = await self.get_sut( [ configurations[0], configurations[1], @@ -131,8 +131,8 @@ async def test_prepend(self) -> None: """ Tests :py:meth:`betty.config.collections.ConfigurationCollection.prepend` implementations. """ - configurations = self.get_configurations() - sut = self.get_sut( + configurations = await self.get_configurations() + sut = await self.get_sut( [ configurations[1], ] @@ -144,8 +144,8 @@ async def test_append(self) -> None: """ Tests :py:meth:`betty.config.collections.ConfigurationCollection.append` implementations. """ - configurations = self.get_configurations() - sut = self.get_sut( + configurations = await self.get_configurations() + sut = await self.get_sut( [ configurations[0], ] @@ -159,8 +159,8 @@ async def test_insert(self) -> None: """ Tests :py:meth:`betty.config.collections.ConfigurationCollection.insert` implementations. """ - configurations = self.get_configurations() - sut = self.get_sut( + configurations = await self.get_configurations() + sut = await self.get_sut( [ configurations[0], configurations[1], diff --git a/betty/test_utils/config/collections/mapping.py b/betty/test_utils/config/collections/mapping.py index 0985cfc20..e4b294b19 100644 --- a/betty/test_utils/config/collections/mapping.py +++ b/betty/test_utils/config/collections/mapping.py @@ -5,6 +5,7 @@ from __future__ import annotations from typing import Generic, TypeVar + from typing_extensions import override from betty.config import Configuration @@ -21,8 +22,8 @@ class _ConfigurationMappingTestBase( ): @override async def test___iter__(self) -> None: - configurations = self.get_configurations() - sut = self.get_sut( + configurations = await self.get_configurations() + sut = await self.get_sut( [ configurations[0], configurations[1], diff --git a/betty/test_utils/config/collections/sequence.py b/betty/test_utils/config/collections/sequence.py index 461495ce5..7b423625f 100644 --- a/betty/test_utils/config/collections/sequence.py +++ b/betty/test_utils/config/collections/sequence.py @@ -5,6 +5,7 @@ from __future__ import annotations from typing import Generic, TypeVar + from typing_extensions import override from betty.config import Configuration @@ -26,8 +27,8 @@ def get_configuration_keys(self) -> tuple[int, int, int, int]: @override async def test___iter__(self) -> None: - configurations = self.get_configurations() - sut = self.get_sut( + configurations = await self.get_configurations() + sut = await self.get_sut( [ configurations[0], configurations[1], diff --git a/betty/test_utils/jinja2.py b/betty/test_utils/jinja2.py index 2cdd62fac..a287330c5 100644 --- a/betty/test_utils/jinja2.py +++ b/betty/test_utils/jinja2.py @@ -45,7 +45,7 @@ async def _assert_template( if locale is not None: data["localizer"] = await app.localizers.get(locale) if extensions is not None: - project.configuration.extensions.enable(*extensions) + await project.configuration.extensions.enable(*extensions) async with project: jinja2_environment = await project.jinja2_environment if autoescape is not None: diff --git a/betty/test_utils/plugin/config.py b/betty/test_utils/plugin/config.py index c3ccce3be..06e0570c3 100644 --- a/betty/test_utils/plugin/config.py +++ b/betty/test_utils/plugin/config.py @@ -25,17 +25,17 @@ class PluginConfigurationMappingTestBase( """ @override - def get_sut( + async def get_sut( self, configurations: Iterable[_PluginConfigurationT] | None = None ) -> PluginConfigurationMapping[_PluginCoT, _PluginConfigurationT]: raise NotImplementedError - def test_plugins(self) -> None: + async def test_plugins(self) -> None: """ Tests :py:meth:`betty.plugin.config.PluginConfigurationMapping.plugins` implementations. """ - configurations = self.get_configurations() - sut = self.get_sut(configurations) + configurations = await self.get_configurations() + sut = await self.get_sut(configurations) for configuration, plugin in zip(configurations, sut.plugins, strict=True): assert plugin.plugin_id() == configuration.id assert plugin.plugin_label() == configuration.label diff --git a/betty/test_utils/project/extension/__init__.py b/betty/test_utils/project/extension/__init__.py index 15567d70a..ec20e3dac 100644 --- a/betty/test_utils/project/extension/__init__.py +++ b/betty/test_utils/project/extension/__init__.py @@ -122,5 +122,5 @@ class DummyConfigurableExtension( @override @classmethod - def default_configuration(cls) -> DummyConfigurableExtensionConfiguration: + def new_default_configuration(cls) -> DummyConfigurableExtensionConfiguration: return DummyConfigurableExtensionConfiguration() diff --git a/betty/tests/cli/commands/test_new.py b/betty/tests/cli/commands/test_new.py index 04b92f23e..d09372c32 100644 --- a/betty/tests/cli/commands/test_new.py +++ b/betty/tests/cli/commands/test_new.py @@ -122,6 +122,6 @@ async def test_click_command_with_gramps( assert Gramps in configuration.extensions family_trees = cast( GrampsConfiguration, - configuration.extensions[Gramps].extension_configuration, + configuration.extensions[Gramps].plugin_configuration, ).family_trees assert family_trees[0].file_path == gramps_family_tree_file_path diff --git a/betty/tests/config/collections/test_mapping.py b/betty/tests/config/collections/test_mapping.py index a235f4484..a349325ae 100644 --- a/betty/tests/config/collections/test_mapping.py +++ b/betty/tests/config/collections/test_mapping.py @@ -50,13 +50,13 @@ class TestConfigurationMapping( def get_configuration_keys(self) -> tuple[str, str, str, str]: return "foo", "bar", "baz", "qux" - def get_sut( + async def get_sut( self, configurations: (Iterable[ConfigurationMappingTestConfiguration] | None) = None, ) -> ConfigurationMappingTestConfigurationMapping: return ConfigurationMappingTestConfigurationMapping(configurations) - def get_configurations( + async def get_configurations( self, ) -> tuple[ ConfigurationMappingTestConfiguration, @@ -79,25 +79,25 @@ def get_configurations( ), ) - def test_load_without_items(self) -> None: - sut = self.get_sut() + async def test_load_without_items(self) -> None: + sut = await self.get_sut() sut.load({}) assert len(sut) == 0 - def test_load_with_items(self) -> None: - sut = self.get_sut() - configurations = self.get_configurations() + async def test_load_with_items(self) -> None: + sut = await self.get_sut() + configurations = await self.get_configurations() sut.load({item.key: item.dump() for item in configurations}) assert len(sut) == len(configurations) - def test_dump_without_items(self) -> None: - sut = self.get_sut() + async def test_dump_without_items(self) -> None: + sut = await self.get_sut() dump = sut.dump() assert dump == {} - def test_dump_with_items(self) -> None: - configurations = self.get_configurations() - sut = self.get_sut() + async def test_dump_with_items(self) -> None: + configurations = await self.get_configurations() + sut = await self.get_sut() sut.replace(*configurations) dump = sut.dump() assert isinstance(dump, Mapping) @@ -131,7 +131,7 @@ class TestOrderedConfigurationMapping( def get_configuration_keys(self) -> tuple[str, str, str, str]: return "foo", "bar", "baz", "qux" - def get_sut( + async def get_sut( self, configurations: (Iterable[ConfigurationMappingTestConfiguration] | None) = None, ) -> OrderedConfigurationMappingTestOrderedConfigurationMapping: @@ -139,7 +139,7 @@ def get_sut( configurations ) - def get_configurations( + async def get_configurations( self, ) -> tuple[ ConfigurationMappingTestConfiguration, @@ -162,25 +162,25 @@ def get_configurations( ), ) - def test_load_without_items(self) -> None: - sut = self.get_sut() + async def test_load_without_items(self) -> None: + sut = await self.get_sut() sut.load([]) assert len(sut) == 0 - def test_load_with_items(self) -> None: - sut = self.get_sut() - configurations = self.get_configurations() + async def test_load_with_items(self) -> None: + sut = await self.get_sut() + configurations = await self.get_configurations() sut.load([item.dump() for item in configurations]) assert len(sut) == len(configurations) - def test_dump_without_items(self) -> None: - sut = self.get_sut() + async def test_dump_without_items(self) -> None: + sut = await self.get_sut() dump = sut.dump() assert dump == [] - def test_dump_with_items(self) -> None: - configurations = self.get_configurations() - sut = self.get_sut() + async def test_dump_with_items(self) -> None: + configurations = await self.get_configurations() + sut = await self.get_sut() sut.replace(*configurations) dump = sut.dump() assert isinstance(dump, Sequence) diff --git a/betty/tests/config/collections/test_sequence.py b/betty/tests/config/collections/test_sequence.py index e6674392f..848587b47 100644 --- a/betty/tests/config/collections/test_sequence.py +++ b/betty/tests/config/collections/test_sequence.py @@ -32,7 +32,7 @@ def dump(self) -> Dump: class TestConfigurationSequence( ConfigurationSequenceTestBase[ConfigurationSequenceTestConfiguration] ): - def get_sut( + async def get_sut( self, configurations: ( Iterable[ConfigurationSequenceTestConfiguration] | None @@ -40,7 +40,7 @@ def get_sut( ) -> ConfigurationSequenceTestConfigurationSequence: return ConfigurationSequenceTestConfigurationSequence(configurations) - def get_configurations( + async def get_configurations( self, ) -> tuple[ ConfigurationSequenceTestConfiguration, @@ -56,24 +56,24 @@ def get_configurations( ) async def test_load_without_items(self) -> None: - sut = self.get_sut() + sut = await self.get_sut() sut.load([]) assert len(sut) == 0 async def test_load_with_items(self) -> None: - sut = self.get_sut() - configurations = self.get_configurations() + sut = await self.get_sut() + configurations = await self.get_configurations() sut.load([item.dump() for item in configurations]) assert len(sut) == len(configurations) async def test_dump_without_items(self) -> None: - sut = self.get_sut() + sut = await self.get_sut() dump = sut.dump() assert dump == [] async def test_dump_with_items(self) -> None: - configurations = self.get_configurations() - sut = self.get_sut() + configurations = await self.get_configurations() + sut = await self.get_sut() sut.replace(*configurations) dump = sut.dump() assert isinstance(dump, Sequence) diff --git a/betty/tests/config/test___init__.py b/betty/tests/config/test___init__.py index 6a0285a91..692364cab 100644 --- a/betty/tests/config/test___init__.py +++ b/betty/tests/config/test___init__.py @@ -41,18 +41,11 @@ def test_update(self) -> None: class TestConfigurable: class _DummyConfigurable(Configurable[DummyConfiguration]): - def __init__(self, configuration: DummyConfiguration | None = None): - if configuration is not None: - self._configuration = configuration + pass - def test_configuration_without_configuration(self) -> None: - sut = self._DummyConfigurable() - with pytest.raises(RuntimeError): - sut.configuration # noqa B018 - - def test_configuration_with_configuration(self) -> None: + def test_configuration(self) -> None: configuration = DummyConfiguration() - sut = self._DummyConfigurable(configuration) + sut = self._DummyConfigurable(configuration=configuration) assert sut.configuration is configuration diff --git a/betty/tests/coverage/test_coverage.py b/betty/tests/coverage/test_coverage.py index 3edbe5f32..ca10a503e 100644 --- a/betty/tests/coverage/test_coverage.py +++ b/betty/tests/coverage/test_coverage.py @@ -68,6 +68,7 @@ class MissingReason(Enum): "betty/__init__.py": MissingReason.SHOULD_BE_COVERED, "betty/app/factory.py": MissingReason.ABSTRACT, "betty/assertion/__init__.py": { + "Field": MissingReason.INTERNAL, "OptionalField": MissingReason.DATACLASS, "RequiredField": MissingReason.DATACLASS, }, @@ -114,6 +115,9 @@ class MissingReason(Enum): "__aexit__": MissingReason.SHOULD_BE_COVERED, }, }, + "betty/config/__init__.py": { + "DefaultConfigurable": MissingReason.ABSTRACT, + }, "betty/config/collections/__init__.py": MissingReason.ABSTRACT, "betty/contextlib.py": { "SynchronizedContextManager": { diff --git a/betty/tests/plugin/test_config.py b/betty/tests/plugin/test_config.py index d1a0015bd..32b5113d1 100644 --- a/betty/tests/plugin/test_config.py +++ b/betty/tests/plugin/test_config.py @@ -1,8 +1,11 @@ from collections.abc import Iterable -from typing import TYPE_CHECKING import pytest +from typing_extensions import override +from betty.assertion import assert_record, RequiredField, assert_bool, assert_setattr +from betty.assertion.error import AssertionFailed +from betty.config import Configuration, DefaultConfigurable from betty.config.collections import ConfigurationCollection from betty.locale import UNDETERMINED_LOCALE from betty.locale.localizable import ShorthandStaticTranslations @@ -11,11 +14,13 @@ from betty.plugin.config import ( PluginConfiguration, PluginConfigurationPluginConfigurationMapping, + PluginInstanceConfiguration, ) +from betty.plugin.static import StaticPluginRepository +from betty.serde.dump import Dump +from betty.test_utils.assertion.error import raises_error from betty.test_utils.config.collections.mapping import ConfigurationMappingTestBase - -if TYPE_CHECKING: - from betty.serde.dump import Dump +from betty.test_utils.plugin import DummyPlugin class TestPluginConfiguration: @@ -169,7 +174,7 @@ async def test_description( class TestPluginConfigurationPluginConfigurationMapping( ConfigurationMappingTestBase[MachineName, PluginConfiguration] ): - def get_sut( + async def get_sut( self, configurations: Iterable[PluginConfiguration] | None = None ) -> ConfigurationCollection[MachineName, PluginConfiguration]: return PluginConfigurationPluginConfigurationMapping(configurations) @@ -184,7 +189,7 @@ def get_configuration_keys( "hello-world-4", ) - def get_configurations( + async def get_configurations( self, ) -> tuple[ PluginConfiguration, @@ -198,3 +203,132 @@ def get_configurations( PluginConfiguration(self.get_configuration_keys()[2], ""), PluginConfiguration(self.get_configuration_keys()[3], ""), ) + + +class TestPluginInstanceConfiguration: + class _DummyDefaultConfigurablePluginConfiguration(Configuration): + def __init__(self, *, check: bool = False): + super().__init__() + self.check = check + + @override + def load(self, dump: Dump) -> None: + assert_record( + RequiredField("check", assert_bool() | assert_setattr(self, "check")) + )(dump) + + @override + def dump(self) -> Dump: + return { + "check": self.check, + } + + class _DummyDefaultConfigurablePlugin( + DefaultConfigurable[_DummyDefaultConfigurablePluginConfiguration], DummyPlugin + ): + @override + @classmethod + def new_default_configuration( + cls, + ) -> "TestPluginInstanceConfiguration._DummyDefaultConfigurablePluginConfiguration": + return TestPluginInstanceConfiguration._DummyDefaultConfigurablePluginConfiguration() + + def test___init___with_configuration_without_configurable_plugin_should_error(self): + plugin = DummyPlugin + with pytest.raises(ValueError): # noqa PT011 + PluginInstanceConfiguration( + plugin, + plugin_repository=StaticPluginRepository(plugin), + plugin_configuration=self._DummyDefaultConfigurablePluginConfiguration(), + ) + + def test_plugin(self) -> None: + plugin = DummyPlugin + sut = PluginInstanceConfiguration( + plugin, plugin_repository=StaticPluginRepository(plugin) + ) + assert sut.plugin == plugin + + def test_plugin_configuration(self) -> None: + plugin = self._DummyDefaultConfigurablePlugin + plugin_configuration = self._DummyDefaultConfigurablePluginConfiguration() + sut = PluginInstanceConfiguration( + plugin, + plugin_configuration=plugin_configuration, + plugin_repository=StaticPluginRepository(plugin), + ) + assert sut.plugin_configuration is plugin_configuration + + def test_load_without_id(self) -> None: + plugin = DummyPlugin + with raises_error(error_type=AssertionFailed): + ( + PluginInstanceConfiguration( + plugin, plugin_repository=StaticPluginRepository(plugin) + ) + ).load({}) + + def test_load_minimal(self) -> None: + plugin = DummyPlugin + sut = PluginInstanceConfiguration( + plugin, plugin_repository=StaticPluginRepository(plugin) + ) + sut.load({"id": DummyPlugin.plugin_id()}) + assert sut.plugin == DummyPlugin + + def test_load_with_configuration(self) -> None: + plugin = self._DummyDefaultConfigurablePlugin + sut = PluginInstanceConfiguration( + plugin, plugin_repository=StaticPluginRepository(plugin) + ) + sut.load( + { + "id": self._DummyDefaultConfigurablePlugin.plugin_id(), + "configuration": { + "check": True, + }, + } + ) + plugin_configuration = sut.plugin_configuration + assert isinstance( + plugin_configuration, self._DummyDefaultConfigurablePluginConfiguration + ) + assert plugin_configuration.check + + def test_load_with_configuration_for_non_configurable_plugin_should_error( + self, + ) -> None: + plugin = DummyPlugin + sut = PluginInstanceConfiguration( + plugin, plugin_repository=StaticPluginRepository(plugin) + ) + with pytest.raises(AssertionFailed): + sut.load( + { + "id": DummyPlugin.plugin_id(), + "configuration": {}, + } + ) + + def test_dump_should_dump_minimal(self) -> None: + plugin = DummyPlugin + sut = PluginInstanceConfiguration( + plugin, plugin_repository=StaticPluginRepository(plugin) + ) + expected = { + "id": DummyPlugin.plugin_id(), + } + assert sut.dump() == expected + + def test_dump_should_dump_plugin_configuration(self) -> None: + plugin = self._DummyDefaultConfigurablePlugin + sut = PluginInstanceConfiguration( + plugin, plugin_repository=StaticPluginRepository(plugin) + ) + expected = { + "id": self._DummyDefaultConfigurablePlugin.plugin_id(), + "configuration": { + "check": False, + }, + } + assert sut.dump() == expected diff --git a/betty/tests/project/extension/cotton_candy/test_search.py b/betty/tests/project/extension/cotton_candy/test_search.py index 2b4e1b61f..564cf14c4 100644 --- a/betty/tests/project/extension/cotton_candy/test_search.py +++ b/betty/tests/project/extension/cotton_candy/test_search.py @@ -19,7 +19,7 @@ class TestIndex: async def test_build_empty(self, new_temporary_app: App) -> None: async with Project.new_temporary(new_temporary_app) as project: - project.configuration.extensions.enable(CottonCandy) + await project.configuration.extensions.enable(CottonCandy) project.configuration.locales["en-US"].alias = "en" project.configuration.locales.append( LocaleConfiguration( @@ -42,7 +42,7 @@ async def test_build_person_without_names(self, new_temporary_app: App) -> None: person = Person(id=person_id) async with Project.new_temporary(new_temporary_app) as project: - project.configuration.extensions.enable(CottonCandy) + await project.configuration.extensions.enable(CottonCandy) project.configuration.locales["en-US"].alias = "en" project.configuration.locales.append( LocaleConfiguration( @@ -74,7 +74,7 @@ async def test_build_private_person(self, new_temporary_app: App) -> None: ) async with Project.new_temporary(new_temporary_app) as project: - project.configuration.extensions.enable(CottonCandy) + await project.configuration.extensions.enable(CottonCandy) project.configuration.locales["en-US"].alias = "en" project.configuration.locales.append( LocaleConfiguration( @@ -112,7 +112,7 @@ async def test_build_person_with_individual_name( ) async with Project.new_temporary(new_temporary_app) as project: - project.configuration.extensions.enable(CottonCandy) + await project.configuration.extensions.enable(CottonCandy) project.configuration.locales["en-US"].alias = "en" project.configuration.locales.append( LocaleConfiguration( @@ -152,7 +152,7 @@ async def test_build_person_with_affiliation_name( ) async with Project.new_temporary(new_temporary_app) as project: - project.configuration.extensions.enable(CottonCandy) + await project.configuration.extensions.enable(CottonCandy) project.configuration.locales["en-US"].alias = "en" project.configuration.locales.append( LocaleConfiguration( @@ -194,7 +194,7 @@ async def test_build_person_with_individual_and_affiliation_names( ) async with Project.new_temporary(new_temporary_app) as project: - project.configuration.extensions.enable(CottonCandy) + await project.configuration.extensions.enable(CottonCandy) project.configuration.locales["en-US"].alias = "en" project.configuration.locales.append( LocaleConfiguration( @@ -239,7 +239,7 @@ async def test_build_place( ) async with Project.new_temporary(new_temporary_app) as project: - project.configuration.extensions.enable(CottonCandy) + await project.configuration.extensions.enable(CottonCandy) project.configuration.locales["en-US"].alias = "en" project.configuration.locales.append( LocaleConfiguration( @@ -271,7 +271,7 @@ async def test_build_private_place(self, new_temporary_app: App) -> None: ) async with Project.new_temporary(new_temporary_app) as project: - project.configuration.extensions.enable(CottonCandy) + await project.configuration.extensions.enable(CottonCandy) project.configuration.locales["en-US"].alias = "en" project.ancestry.add(place) async with project: @@ -292,7 +292,7 @@ async def test_build_file_without_description(self, new_temporary_app: App) -> N ) async with Project.new_temporary(new_temporary_app) as project: - project.configuration.extensions.enable(CottonCandy) + await project.configuration.extensions.enable(CottonCandy) project.configuration.locales["en-US"].alias = "en" project.configuration.locales.append( LocaleConfiguration( @@ -329,7 +329,7 @@ async def test_build_file( ) async with Project.new_temporary(new_temporary_app) as project: - project.configuration.extensions.enable(CottonCandy) + await project.configuration.extensions.enable(CottonCandy) project.configuration.locales["en-US"].alias = "en" project.configuration.locales.append( LocaleConfiguration( @@ -367,7 +367,7 @@ async def test_build_private_file(self, new_temporary_app: App) -> None: ) async with Project.new_temporary(new_temporary_app) as project: - project.configuration.extensions.enable(CottonCandy) + await project.configuration.extensions.enable(CottonCandy) project.configuration.locales["en-US"].alias = "en" project.ancestry.add(file) async with project: diff --git a/betty/tests/project/extension/demo/test___init__.py b/betty/tests/project/extension/demo/test___init__.py index ef5250311..1c3547273 100644 --- a/betty/tests/project/extension/demo/test___init__.py +++ b/betty/tests/project/extension/demo/test___init__.py @@ -11,7 +11,6 @@ from betty.ancestry.source import Source from betty.app import App from betty.project import Project -from betty.project.config import ExtensionConfiguration from betty.project.extension.demo import Demo from betty.project.load import load from betty.test_utils.project.extension import ExtensionTestBase @@ -38,7 +37,7 @@ async def test_load( app, Project.new_temporary(app) as project, ): - project.configuration.extensions.append(ExtensionConfiguration(Demo)) + await project.configuration.extensions.enable(Demo) async with project: await load(project) assert len(project.ancestry[Person]) != 0 diff --git a/betty/tests/project/extension/deriver/test___init__.py b/betty/tests/project/extension/deriver/test___init__.py index ee2edde5b..1d4c0f758 100644 --- a/betty/tests/project/extension/deriver/test___init__.py +++ b/betty/tests/project/extension/deriver/test___init__.py @@ -18,7 +18,6 @@ from betty.date import DateRange, Date from betty.model.collections import record_added from betty.project import Project -from betty.project.config import ExtensionConfiguration from betty.project.extension.deriver import Deriver from betty.project.load import load from betty.test_utils.ancestry.event_type import DummyEventType @@ -92,7 +91,7 @@ async def test_post_load(self, new_temporary_app: App) -> None: Presence(person, Subject(), event) async with Project.new_temporary(new_temporary_app) as project: - project.configuration.extensions.append(ExtensionConfiguration(Deriver)) + await project.configuration.extensions.enable(Deriver) project.ancestry.add(person) async with project: async with record_added(project.ancestry) as added: diff --git a/betty/tests/project/extension/gramps/test_config.py b/betty/tests/project/extension/gramps/test_config.py index 2659f1f30..908249fba 100644 --- a/betty/tests/project/extension/gramps/test_config.py +++ b/betty/tests/project/extension/gramps/test_config.py @@ -31,12 +31,12 @@ class TestFamilyTreeConfigurationSequence( ConfigurationSequenceTestBase[FamilyTreeConfiguration] ): - def get_sut( + async def get_sut( self, configurations: Iterable[FamilyTreeConfiguration] | None = None ) -> FamilyTreeConfigurationSequence: return FamilyTreeConfigurationSequence(configurations) - def get_configurations( + async def get_configurations( self, ) -> tuple[ FamilyTreeConfiguration, diff --git a/betty/tests/project/extension/http_api_doc/test___init__.py b/betty/tests/project/extension/http_api_doc/test___init__.py index 8b4aef450..fc13a0f69 100644 --- a/betty/tests/project/extension/http_api_doc/test___init__.py +++ b/betty/tests/project/extension/http_api_doc/test___init__.py @@ -2,7 +2,6 @@ from betty.app import App from betty.project import Project -from betty.project.config import ExtensionConfiguration from betty.project.extension.http_api_doc import HttpApiDoc from betty.project.generate import generate from betty.test_utils.project.extension.webpack import WebpackEntryPointProviderTestBase @@ -15,7 +14,7 @@ def get_sut_class(self) -> type[HttpApiDoc]: async def test_generate(self, new_temporary_app: App) -> None: async with Project.new_temporary(new_temporary_app) as project: - project.configuration.extensions.append(ExtensionConfiguration(HttpApiDoc)) + await project.configuration.extensions.enable(HttpApiDoc) async with project: await generate(project) assert ( diff --git a/betty/tests/project/extension/maps/test___init__.py b/betty/tests/project/extension/maps/test___init__.py index f586bb33c..be00a5d61 100644 --- a/betty/tests/project/extension/maps/test___init__.py +++ b/betty/tests/project/extension/maps/test___init__.py @@ -3,7 +3,6 @@ from betty.app import App from betty.project import Project -from betty.project.config import ExtensionConfiguration from betty.project.extension.maps import Maps from betty.project.generate import generate from betty.test_utils.project.extension.webpack import WebpackEntryPointProviderTestBase @@ -17,7 +16,7 @@ def get_sut_class(self) -> type[Maps]: async def test_generate(self, new_temporary_app: App) -> None: async with Project.new_temporary(new_temporary_app) as project: project.configuration.debug = True - project.configuration.extensions.append(ExtensionConfiguration(Maps)) + await project.configuration.extensions.enable(Maps) async with project: await generate(project) async with aiofiles.open( diff --git a/betty/tests/project/extension/privatizer/test___init__.py b/betty/tests/project/extension/privatizer/test___init__.py index 08973b057..9e351a6b9 100644 --- a/betty/tests/project/extension/privatizer/test___init__.py +++ b/betty/tests/project/extension/privatizer/test___init__.py @@ -15,7 +15,6 @@ from betty.ancestry.presence_role.presence_roles import Subject from betty.ancestry.source import Source from betty.project import Project -from betty.project.config import ExtensionConfiguration from betty.project.extension.privatizer import Privatizer from betty.project.load import load from betty.test_utils.project.extension import ExtensionTestBase @@ -57,7 +56,7 @@ async def test_post_load(self, new_temporary_app: App) -> None: FileReference(citation, citation_file) async with Project.new_temporary(new_temporary_app) as project: - project.configuration.extensions.append(ExtensionConfiguration(Privatizer)) + await project.configuration.extensions.enable(Privatizer) project.ancestry.add(person, source, citation) async with project: await load(project) diff --git a/betty/tests/project/extension/trees/test___init__.py b/betty/tests/project/extension/trees/test___init__.py index 3799195a3..6144e85ac 100644 --- a/betty/tests/project/extension/trees/test___init__.py +++ b/betty/tests/project/extension/trees/test___init__.py @@ -3,7 +3,6 @@ from betty.app import App from betty.project import Project -from betty.project.config import ExtensionConfiguration from betty.project.extension.trees import Trees from betty.project.generate import generate from betty.test_utils.project.extension.webpack import WebpackEntryPointProviderTestBase @@ -17,7 +16,7 @@ def get_sut_class(self) -> type[Trees]: async def test_generate(self, new_temporary_app: App) -> None: async with Project.new_temporary(new_temporary_app) as project: project.configuration.debug = True - project.configuration.extensions.append(ExtensionConfiguration(Trees)) + await project.configuration.extensions.enable(Trees) async with project: await generate(project) async with aiofiles.open( diff --git a/betty/tests/project/extension/webpack/test___init__.py b/betty/tests/project/extension/webpack/test___init__.py index a22c466b5..0037712f3 100644 --- a/betty/tests/project/extension/webpack/test___init__.py +++ b/betty/tests/project/extension/webpack/test___init__.py @@ -70,7 +70,7 @@ async def test_generate_with_npm( await f.write(self._SENTINEL) async with Project.new_temporary(new_temporary_app) as project: - project.configuration.extensions.enable(Webpack) + await project.configuration.extensions.enable(Webpack) async with project: await generate(project) @@ -98,7 +98,7 @@ async def test_generate_without_npm_with_prebuild( fs.PREBUILT_ASSETS_DIRECTORY_PATH = tmp_path try: async with Project.new_temporary(new_temporary_app) as project: - project.configuration.extensions.enable(Webpack) + await project.configuration.extensions.enable(Webpack) async with project: await generate(project) async with aiofiles.open( @@ -127,7 +127,7 @@ async def test_generate_without_npm_without_prebuild( tmp_path / "project" / "betty.json" ), ) - project.configuration.extensions.enable(Webpack) + await project.configuration.extensions.enable(Webpack) async with project: with pytest.raises(RequirementError): await generate(project) @@ -156,7 +156,7 @@ async def test_prebuild( try: job_context = Context() async with Project.new_temporary(new_temporary_app) as project: - project.configuration.extensions.enable(Webpack) + await project.configuration.extensions.enable(Webpack) async with project: extensions = await project.extensions webpack = extensions[Webpack] diff --git a/betty/tests/project/extension/webpack/test_build.py b/betty/tests/project/extension/webpack/test_build.py index a109ac4cd..6b5fbc943 100644 --- a/betty/tests/project/extension/webpack/test_build.py +++ b/betty/tests/project/extension/webpack/test_build.py @@ -43,7 +43,7 @@ async def test_build(self, new_temporary_app: App, tmp_path: Path) -> None: async with Project.new_temporary(new_temporary_app) as project: project.configuration.debug = debug if with_entry_point_provider: - project.configuration.extensions.enable( + await project.configuration.extensions.enable( DummyEntryPointProviderExtension ) job_context = Context() diff --git a/betty/tests/project/extension/wikipedia/test___init__.py b/betty/tests/project/extension/wikipedia/test___init__.py index 287401cb5..aa3dce3f0 100644 --- a/betty/tests/project/extension/wikipedia/test___init__.py +++ b/betty/tests/project/extension/wikipedia/test___init__.py @@ -9,7 +9,6 @@ from betty.fetch.static import StaticFetcher from betty.job import Context from betty.project import Project -from betty.project.config import ExtensionConfiguration from betty.project.extension.wikipedia import Wikipedia from betty.project.load import load from betty.test_utils.project.extension import ExtensionTestBase @@ -49,7 +48,7 @@ async def test_filter(self, mocker: MockerFixture, new_temporary_app: App) -> No ] async with Project.new_temporary(new_temporary_app) as project: - project.configuration.extensions.append(ExtensionConfiguration(Wikipedia)) + await project.configuration.extensions.enable(Wikipedia) async with project: jinja2_environment = await project.jinja2_environment actual = await jinja2_environment.from_string( @@ -68,7 +67,7 @@ async def test_post_load( m_populate = mocker.patch("betty.wikipedia._Populator.populate") async with Project.new_temporary(new_temporary_app) as project: - project.configuration.extensions.append(ExtensionConfiguration(Wikipedia)) + await project.configuration.extensions.enable(Wikipedia) async with project: await load(project) @@ -76,7 +75,7 @@ async def test_post_load( async def test_retriever(self, new_temporary_app: App) -> None: async with Project.new_temporary(new_temporary_app) as project: - project.configuration.extensions.enable(Wikipedia) + await project.configuration.extensions.enable(Wikipedia) async with project: extensions = await project.extensions wikipedia = extensions[Wikipedia] @@ -89,7 +88,7 @@ async def test_globals(self, new_temporary_app: App) -> None: app, Project.new_temporary(app) as project, ): - project.configuration.extensions.enable(Wikipedia) + await project.configuration.extensions.enable(Wikipedia) async with project: extensions = await project.extensions sut = extensions[Wikipedia] diff --git a/betty/tests/project/test___init__.py b/betty/tests/project/test___init__.py index dafbc32e6..98b3a4920 100644 --- a/betty/tests/project/test___init__.py +++ b/betty/tests/project/test___init__.py @@ -140,7 +140,7 @@ async def test_new_temporary_with_configuration( @pytest.mark.usefixtures("_extensions") async def test_bootstrap(self, new_temporary_app: App) -> None: async with Project.new_temporary(new_temporary_app) as sut: - sut.configuration.extensions.enable(DummyExtension) + await sut.configuration.extensions.enable(DummyExtension) async with sut: extensions = await sut.extensions extension = extensions[DummyExtension.plugin_id()] @@ -149,7 +149,7 @@ async def test_bootstrap(self, new_temporary_app: App) -> None: @pytest.mark.usefixtures("_extensions") async def test_extensions_with_one_extension(self, new_temporary_app: App) -> None: async with Project.new_temporary(new_temporary_app) as sut: - sut.configuration.extensions.enable(DummyExtension) + await sut.configuration.extensions.enable(DummyExtension) async with sut: extensions = await sut.extensions extension = extensions[DummyExtension.plugin_id()] @@ -180,7 +180,7 @@ async def test_extensions_with_one_extension_with_single_chained_dependency( self, new_temporary_app: App ) -> None: async with Project.new_temporary(new_temporary_app) as sut: - sut.configuration.extensions.enable( + await sut.configuration.extensions.enable( _DependsOnNonConfigurableExtensionExtensionExtension ) async with sut: @@ -203,11 +203,9 @@ async def test_extensions_with_multiple_extensions_with_duplicate_dependencies( self, new_temporary_app: App ) -> None: async with Project.new_temporary(new_temporary_app) as sut: - sut.configuration.extensions.enable( - _DependsOnNonConfigurableExtensionExtension - ) - sut.configuration.extensions.enable( - _AlsoDependsOnNonConfigurableExtensionExtension + await sut.configuration.extensions.enable( + _DependsOnNonConfigurableExtensionExtension, + _AlsoDependsOnNonConfigurableExtensionExtension, ) async with sut: extensions = [list(batch) for batch in await sut.extensions] @@ -227,7 +225,7 @@ async def test_extensions_with_multiple_extensions_with_cyclic_dependencies( self, new_temporary_app: App ) -> None: async with Project.new_temporary(new_temporary_app) as sut: - sut.configuration.extensions.enable(_CyclicDependencyOneExtension) + await sut.configuration.extensions.enable(_CyclicDependencyOneExtension) with pytest.raises(CyclicDependencyError): # noqa PT012 async with sut: pass # pragma: no cover @@ -237,9 +235,8 @@ async def test_extensions_with_comes_before_with_other_extension( self, new_temporary_app: App ) -> None: async with Project.new_temporary(new_temporary_app) as sut: - sut.configuration.extensions.enable(DummyExtension) - sut.configuration.extensions.enable( - _ComesBeforeNonConfigurableExtensionExtension + await sut.configuration.extensions.enable( + DummyExtension, _ComesBeforeNonConfigurableExtensionExtension ) async with sut: extensions = [list(batch) for batch in await sut.extensions] @@ -256,7 +253,7 @@ async def test_extensions_with_comes_before_without_other_extension( self, new_temporary_app: App ) -> None: async with Project.new_temporary(new_temporary_app) as sut: - sut.configuration.extensions.enable( + await sut.configuration.extensions.enable( _ComesBeforeNonConfigurableExtensionExtension ) async with sut: @@ -272,10 +269,9 @@ async def test_extensions_with_comes_after_with_other_extension( self, new_temporary_app: App ) -> None: async with Project.new_temporary(new_temporary_app) as sut: - sut.configuration.extensions.enable( - _ComesAfterNonConfigurableExtensionExtension + await sut.configuration.extensions.enable( + _ComesAfterNonConfigurableExtensionExtension, DummyExtension ) - sut.configuration.extensions.enable(DummyExtension) async with sut: extensions = [list(batch) for batch in await sut.extensions] assert len(extensions) == 2 @@ -291,7 +287,7 @@ async def test_extensions_with_comes_after_without_other_extension( self, new_temporary_app: App ) -> None: async with Project.new_temporary(new_temporary_app) as sut: - sut.configuration.extensions.enable( + await sut.configuration.extensions.enable( _ComesAfterNonConfigurableExtensionExtension ) async with sut: @@ -331,7 +327,7 @@ async def test_assets_with_extension_without_assets_directory( self, new_temporary_app: App ) -> None: async with Project.new_temporary(new_temporary_app) as sut: - sut.configuration.extensions.enable(DummyExtension) + await sut.configuration.extensions.enable(DummyExtension) async with sut: assets = await sut.assets assert len(assets.assets_directory_paths) == 2 @@ -346,7 +342,9 @@ def assets_directory_path(cls) -> Path | None: return tmp_path / cls.plugin_id() / "assets" async with Project.new_temporary(new_temporary_app) as sut: - sut.configuration.extensions.enable(_DummyExtensionWithAssetsDirectory) + await sut.configuration.extensions.enable( + _DummyExtensionWithAssetsDirectory + ) async with sut: assets = await sut.assets assert len(assets.assets_directory_paths) == 3 @@ -622,7 +620,7 @@ async def test___iter___with_extensions_in_a_single_batch( ) -> None: async with Project.new_temporary(new_temporary_app) as project, project: extension_one = DummyExtension(project) - extension_two = DummyConfigurableExtension(project) + extension_two = await DummyConfigurableExtension.new_for_project(project) sut = ProjectExtensions([[extension_one, extension_two]]) actual = [list(batch) for batch in iter(sut)] assert len(actual) == 1 @@ -635,7 +633,7 @@ async def test___iter___with_extensions_in_multiple_batches( ) -> None: async with Project.new_temporary(new_temporary_app) as project, project: extension_one = DummyExtension(project) - extension_two = DummyConfigurableExtension(project) + extension_two = await DummyConfigurableExtension.new_for_project(project) sut = ProjectExtensions([[extension_one], [extension_two]]) actual = [list(batch) for batch in iter(sut)] assert len(actual) == 2 @@ -653,7 +651,7 @@ async def test_flatten_with_extensions_in_a_single_batch( ) -> None: async with Project.new_temporary(new_temporary_app) as project, project: extension_one = DummyExtension(project) - extension_two = DummyConfigurableExtension(project) + extension_two = await DummyConfigurableExtension.new_for_project(project) sut = ProjectExtensions([[extension_one, extension_two]]) actual = list(sut.flatten()) assert len(actual) == 2 @@ -665,7 +663,7 @@ async def test_flatten_with_extensions_in_multiple_batches( ) -> None: async with Project.new_temporary(new_temporary_app) as project, project: extension_one = DummyExtension(project) - extension_two = DummyConfigurableExtension(project) + extension_two = await DummyConfigurableExtension.new_for_project(project) sut = ProjectExtensions([[extension_one], [extension_two]]) actual = list(sut.flatten()) assert len(actual) == 2 diff --git a/betty/tests/project/test_config.py b/betty/tests/project/test_config.py index fdba4871c..668c2240e 100644 --- a/betty/tests/project/test_config.py +++ b/betty/tests/project/test_config.py @@ -38,7 +38,6 @@ from betty.project.config import ProjectConfiguration from betty.project.extension import Extension from betty.test_utils.assertion.error import raises_error -from betty.test_utils.config import DummyConfiguration from betty.test_utils.config.collections.mapping import ConfigurationMappingTestBase from betty.test_utils.config.collections.sequence import ConfigurationSequenceTestBase from betty.test_utils.model import DummyEntity @@ -230,12 +229,12 @@ def _entity_types(self, mocker: MockerFixture) -> None: new=StaticPluginRepository(EntityReferenceSequenceTestEntity), ) - def get_sut( + async def get_sut( self, configurations: Iterable[EntityReference[Entity]] | None = None ) -> EntityReferenceSequence[Entity]: return EntityReferenceSequence(configurations) - def get_configurations( + async def get_configurations( self, ) -> tuple[ EntityReference[Entity], @@ -344,7 +343,7 @@ class TestLocaleConfigurationMapping( ConfigurationMappingTestBase[str, LocaleConfiguration] ): @override - def get_sut( + async def get_sut( self, configurations: Iterable[Configuration] | None = None ) -> LocaleConfigurationMapping: return LocaleConfigurationMapping(configurations) # type: ignore[arg-type] @@ -354,7 +353,7 @@ def get_configuration_keys(self) -> tuple[str, str, str, str]: return ("en", "nl", "uk", "fr") @override - def get_configurations( + async def get_configurations( self, ) -> tuple[ LocaleConfiguration, @@ -370,8 +369,8 @@ def get_configurations( ) async def test___delitem__(self) -> None: - configurations = self.get_configurations() - sut = self.get_sut([configurations[0]]) + configurations = await self.get_configurations() + sut = await self.get_sut([configurations[0]]) del sut[configurations[0].locale] with pytest.raises(KeyError): sut[configurations[0].locale] @@ -379,8 +378,8 @@ async def test___delitem__(self) -> None: assert DEFAULT_LOCALE in sut async def test___delitem___with_locale(self) -> None: - configurations = self.get_configurations() - sut = self.get_sut([configurations[0], configurations[1]]) + configurations = await self.get_configurations() + sut = await self.get_sut([configurations[0], configurations[1]]) del sut[configurations[0].locale] with pytest.raises(KeyError): sut[configurations[0].locale] @@ -413,29 +412,29 @@ async def test_default_without_explicit_default(self) -> None: @override async def test_replace_without_items(self) -> None: - sut = self.get_sut() + sut = await self.get_sut() sut.clear() assert len(sut) == 1 - self.get_configurations() + await self.get_configurations() sut.replace() assert len(sut) == 1 @override async def test_replace_with_items(self) -> None: - sut = self.get_sut() + sut = await self.get_sut() sut.clear() assert len(sut) == 1 - configurations = self.get_configurations() + configurations = await self.get_configurations() sut.replace(*configurations) assert len(sut) == len(configurations) - def test_multilingual_with_one_configuration(self) -> None: - sut = self.get_sut() + async def test_multilingual_with_one_configuration(self) -> None: + sut = await self.get_sut() assert not sut.multilingual - def test_multilingual_with_multiple_configurations(self) -> None: - sut = self.get_sut() - sut.replace(*self.get_configurations()) + async def test_multilingual_with_multiple_configurations(self) -> None: + sut = await self.get_sut() + sut.replace(*await self.get_configurations()) assert sut.multilingual @@ -444,94 +443,28 @@ class TestExtensionConfiguration: def _extensions(self, mocker: MockerFixture) -> None: mocker.patch( "betty.project.extension.EXTENSION_REPOSITORY", - new=StaticPluginRepository(DummyExtension, DummyConfigurableExtension), + new=StaticPluginRepository(DummyExtension), ) - async def test_extension_type(self) -> None: - extension_type = DummyExtension - sut = ExtensionConfiguration(extension_type) - assert sut.extension_type == extension_type - async def test_enabled(self) -> None: - enabled = True sut = ExtensionConfiguration( DummyExtension, - enabled=enabled, - ) - assert sut.enabled == enabled - sut.enabled = False - - async def test_extension_configuration(self) -> None: - extension_type_configuration = DummyConfiguration() - sut = ExtensionConfiguration( - DummyConfigurableExtension, - extension_configuration=extension_type_configuration, + enabled=True, ) - assert sut.extension_configuration == extension_type_configuration - - async def test_load_without_extension(self) -> None: - with raises_error(error_type=AssertionFailed): - ExtensionConfiguration(DummyExtension).load({}) - - async def test_load_with_extension(self) -> None: - sut = ExtensionConfiguration(DummyExtension) - sut.load({"extension": DummyConfigurableExtension.plugin_id()}) - assert sut.extension_type == DummyConfigurableExtension assert sut.enabled + sut.enabled = False + assert not sut.enabled async def test_load_with_enabled(self) -> None: sut = ExtensionConfiguration(DummyExtension) - sut.load( - {"extension": DummyConfigurableExtension.plugin_id(), "enabled": False} - ) + sut.load({"id": DummyExtension.plugin_id(), "enabled": False}) assert not sut.enabled - async def test_load_with_configuration(self) -> None: - sut = ExtensionConfiguration(DummyConfigurableExtension) - sut.load( - { - "extension": DummyConfigurableExtension.plugin_id(), - "configuration": { - "check": True, - }, - } - ) - extension_configuration = sut.extension_configuration - assert isinstance( - extension_configuration, DummyConfigurableExtensionConfiguration - ) - assert extension_configuration.check - - async def test_load_with_configuration_for_non_configurable_extension_should_error( - self, - ) -> None: - sut = ExtensionConfiguration(DummyExtension) - with pytest.raises(AssertionFailed): - sut.load( - { - "extension": DummyExtension.plugin_id(), - "configuration": { - "check": True, - }, - } - ) - - async def test_dump_should_dump_minimal(self) -> None: + async def test_dump_should_dump_enabled(self) -> None: sut = ExtensionConfiguration(DummyExtension) expected = { - "extension": DummyExtension.plugin_id(), - "enabled": True, - } - assert sut.dump() == expected - - async def test_dump_should_dump_extension_configuration(self) -> None: - sut = ExtensionConfiguration(DummyConfigurableExtension) - expected = { - "extension": DummyConfigurableExtension.plugin_id(), + "id": DummyExtension.plugin_id(), "enabled": True, - "configuration": { - "check": False, - }, } assert sut.dump() == expected @@ -565,12 +498,12 @@ def get_configuration_keys( ExtensionTypeConfigurationMappingTestExtension3, ) - def get_sut( + async def get_sut( self, configurations: Iterable[ExtensionConfiguration] | None = None ) -> ExtensionConfigurationMapping: return ExtensionConfigurationMapping(configurations) - def get_configurations( + async def get_configurations( self, ) -> tuple[ ExtensionConfiguration, @@ -594,7 +527,7 @@ def _extensions(self, mocker: MockerFixture) -> None: async def test_enable(self) -> None: sut = ExtensionConfigurationMapping() - sut.enable(DummyExtension) + await sut.enable(DummyExtension) assert sut[DummyExtension].enabled @@ -731,12 +664,12 @@ def get_configuration_keys( EntityTypeConfigurationMappingTestEntity3, ) - def get_sut( + async def get_sut( self, configurations: Iterable[EntityTypeConfiguration] | None = None ) -> EntityTypeConfigurationMapping: return EntityTypeConfigurationMapping(configurations) - def get_configurations( + async def get_configurations( self, ) -> tuple[ EntityTypeConfiguration, @@ -826,7 +759,7 @@ def get_configuration_keys(self) -> tuple[str, str, str, str]: return "foo", "bar", "baz", "qux" @override - def get_configurations( + async def get_configurations( self, ) -> tuple[ CopyrightNoticeConfiguration, @@ -842,7 +775,7 @@ def get_configurations( ) @override - def get_sut( + async def get_sut( self, configurations: Iterable[CopyrightNoticeConfiguration] | None = None ) -> CopyrightNoticeConfigurationMapping: return CopyrightNoticeConfigurationMapping(configurations) @@ -922,7 +855,7 @@ def get_configuration_keys(self) -> tuple[str, str, str, str]: return "foo", "bar", "baz", "qux" @override - def get_configurations( + async def get_configurations( self, ) -> tuple[ LicenseConfiguration, @@ -938,7 +871,7 @@ def get_configurations( ) @override - def get_sut( + async def get_sut( self, configurations: Iterable[LicenseConfiguration] | None = None ) -> LicenseConfigurationMapping: return LicenseConfigurationMapping(configurations) @@ -952,7 +885,7 @@ def get_configuration_keys(self) -> tuple[str, str, str, str]: return "foo", "bar", "baz", "qux" @override - def get_configurations( + async def get_configurations( self, ) -> tuple[ PluginConfiguration, @@ -968,7 +901,7 @@ def get_configurations( ) @override - def get_sut( + async def get_sut( self, configurations: Iterable[PluginConfiguration] | None = None ) -> EventTypeConfigurationMapping: return EventTypeConfigurationMapping(configurations) @@ -982,7 +915,7 @@ def get_configuration_keys(self) -> tuple[str, str, str, str]: return "foo", "bar", "baz", "qux" @override - def get_configurations( + async def get_configurations( self, ) -> tuple[ PluginConfiguration, @@ -998,7 +931,7 @@ def get_configurations( ) @override - def get_sut( + async def get_sut( self, configurations: Iterable[PluginConfiguration] | None = None ) -> PlaceTypeConfigurationMapping: return PlaceTypeConfigurationMapping(configurations) @@ -1012,7 +945,7 @@ def get_configuration_keys(self) -> tuple[str, str, str, str]: return "foo", "bar", "baz", "qux" @override - def get_configurations( + async def get_configurations( self, ) -> tuple[ PluginConfiguration, @@ -1028,7 +961,7 @@ def get_configurations( ) @override - def get_sut( + async def get_sut( self, configurations: Iterable[PluginConfiguration] | None = None ) -> PresenceRoleConfigurationMapping: return PresenceRoleConfigurationMapping(configurations) @@ -1042,7 +975,7 @@ def get_configuration_keys(self) -> tuple[str, str, str, str]: return "foo", "bar", "baz", "qux" @override - def get_configurations( + async def get_configurations( self, ) -> tuple[ PluginConfiguration, @@ -1058,7 +991,7 @@ def get_configurations( ) @override - def get_sut( + async def get_sut( self, configurations: Iterable[PluginConfiguration] | None = None ) -> GenderConfigurationMapping: return GenderConfigurationMapping(configurations) @@ -1350,7 +1283,7 @@ async def test_load_should_load_one_extension_with_configuration( actual = sut.extensions[DummyConfigurableExtension] assert actual.enabled assert isinstance( - actual.extension_configuration, DummyConfigurableExtensionConfiguration + actual.plugin_configuration, DummyConfigurableExtensionConfiguration ) async def test_load_should_load_one_extension_without_configuration( @@ -1368,7 +1301,7 @@ async def test_load_should_load_one_extension_without_configuration( sut.load(dump) actual = sut.extensions[_DummyNonConfigurableExtension] assert actual.enabled - assert actual.extension_configuration is None + assert actual.plugin_configuration is None async def test_load_extension_with_invalid_configuration_should_raise_error( self, tmp_path: Path @@ -1600,7 +1533,7 @@ async def test_dump_should_dump_one_extension_without_configuration( self, tmp_path: Path ) -> None: sut = await ProjectConfiguration.new(tmp_path / "betty.json") - sut.extensions.enable(_DummyNonConfigurableExtension) + await sut.extensions.enable(_DummyNonConfigurableExtension) dump = sut.dump() expected = {_DummyNonConfigurableExtension.plugin_id(): {"enabled": True}} assert dump["extensions"] == expected diff --git a/playwright/tests/cotton_candy/search.spec.ts b/playwright/tests/cotton_candy/search.spec.ts index fd74abf10..ab7116b69 100644 --- a/playwright/tests/cotton_candy/search.spec.ts +++ b/playwright/tests/cotton_candy/search.spec.ts @@ -13,7 +13,7 @@ const test = base.extend<{ await generateSite(temporaryDirectoryPath, { url: await server.getPublicUrl(), extensions: { - 'cotton-candy': {}, + 'cotton-candy': {"configuration":{}}, 'gramps': { enabled: true, configuration: {