Skip to content

Commit

Permalink
Make Configurable require Configuration (#2146)
Browse files Browse the repository at this point in the history
  • Loading branch information
bartfeenstra authored Oct 21, 2024
1 parent f649457 commit 1e7e5aa
Show file tree
Hide file tree
Showing 43 changed files with 501 additions and 412 deletions.
3 changes: 1 addition & 2 deletions betty/app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 15 additions & 7 deletions betty/assertion/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand All @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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]]
Expand All @@ -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.
Expand Down
5 changes: 1 addition & 4 deletions betty/assets/locale/betty.pot
Original file line number Diff line number Diff line change
Expand Up @@ -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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <[email protected]>\n"
Expand Down Expand Up @@ -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 ""

Expand Down
5 changes: 1 addition & 4 deletions betty/assets/locale/de-DE/betty.po
Original file line number Diff line number Diff line change
Expand Up @@ -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 <[email protected]>\n"
"Language: de\n"
Expand Down Expand Up @@ -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 ""

Expand Down
5 changes: 1 addition & 4 deletions betty/assets/locale/fr-FR/betty.po
Original file line number Diff line number Diff line change
Expand Up @@ -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 <[email protected]>\n"
"Language: fr\n"
Expand Down Expand Up @@ -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 ""

Expand Down
5 changes: 1 addition & 4 deletions betty/assets/locale/nl-NL/betty.po
Original file line number Diff line number Diff line change
Expand Up @@ -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 <[email protected]>\n"
"Language: nl\n"
Expand Down Expand Up @@ -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."

Expand Down
5 changes: 1 addition & 4 deletions betty/assets/locale/uk/betty.po
Original file line number Diff line number Diff line change
Expand Up @@ -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 <[email protected]>\n"
"Language: uk\n"
Expand Down Expand Up @@ -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 ""

Expand Down
11 changes: 4 additions & 7 deletions betty/cli/commands/new.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
25 changes: 19 additions & 6 deletions betty/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]:
Expand Down
87 changes: 81 additions & 6 deletions betty/plugin/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)


Expand Down Expand Up @@ -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
Loading

0 comments on commit 1e7e5aa

Please sign in to comment.