Skip to content

Commit

Permalink
Add an ordered configuration mapping (#1860)
Browse files Browse the repository at this point in the history
  • Loading branch information
bartfeenstra authored Aug 6, 2024
1 parent 1f4dbee commit 7471c09
Show file tree
Hide file tree
Showing 14 changed files with 267 additions and 153 deletions.
9 changes: 4 additions & 5 deletions betty/ancestry/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@
from typing import Iterable, Any, TYPE_CHECKING, final, cast
from urllib.parse import quote

from typing_extensions import override

from betty.ancestry.event_type import EventType, UnknownEventType
from betty.ancestry.presence_role import PresenceRole, Subject, PresenceRoleSchema
from betty.classtools import repr_instance
Expand Down Expand Up @@ -60,6 +58,7 @@
)
from betty.serde.dump import DumpMapping, Dump
from betty.string import camel_case_to_kebab_case
from typing_extensions import override

if TYPE_CHECKING:
from betty.machine_name import MachineName
Expand Down Expand Up @@ -532,13 +531,13 @@ async def dump_linked_data(self, project: Project) -> DumpMapping[Dump]:
project.url_generator.generate(
self,
media_type="text/html",
locale=locale_configuration.locale,
locale=locale,
),
relationship="alternate",
media_type=MediaType("text/html"),
locale=locale_configuration.locale,
locale=locale,
)
for locale_configuration in project.configuration.locales
for locale in project.configuration.locales
),
)

Expand Down
6 changes: 3 additions & 3 deletions betty/assets/templates/head.html.j2
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@
{% endif %}
{% endif %}
<link rel="canonical" href="{{ page_resource | url(absolute=true) }}" hreflang="{{ localizer.locale }}" type="text/html">
{% for project_locale_configuration in project.configuration.locales %}
{% if project_locale_configuration.locale != localizer.locale %}
<link rel="alternate" href="{{ page_resource | url(locale=project_locale_configuration.locale) }}" hreflang="{{ project_locale_configuration.locale }}" type="text/html">
{% for locale in project.configuration.locales %}
{% if locale != localizer.locale %}
<link rel="alternate" href="{{ page_resource | url(locale=locale) }}" hreflang="{{ locale }}" type="text/html">

{% endif %}
{% endfor %}
Expand Down
5 changes: 1 addition & 4 deletions betty/cli/commands/new.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from urllib.parse import urlparse

import click

from betty.assertion import assert_path, assert_str, assert_locale
from betty.cli.commands import command, pass_app
from betty.cli.error import user_facing_error_to_bad_parameter
Expand Down Expand Up @@ -98,9 +97,7 @@ async def new(app: App) -> None: # noqa D103
)
)
)
locales = [
locale_configuration.locale for locale_configuration in configuration.locales
]
locales = list(configuration.locales)

configuration.title = _prompt_static_translations(
locales,
Expand Down
88 changes: 57 additions & 31 deletions betty/config/collections/mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,12 @@
TYPE_CHECKING,
)

from typing_extensions import override

from betty.assertion import assert_sequence, assert_mapping
from betty.config import Configuration
from betty.config.collections import ConfigurationCollection, ConfigurationKey
from betty.serde.dump import Dump, VoidableDump, minimize
from betty.typing import Void
from typing_extensions import override

if TYPE_CHECKING:
from collections.abc import MutableMapping
Expand All @@ -31,14 +30,10 @@
_ConfigurationKeyT = TypeVar("_ConfigurationKeyT", bound=ConfigurationKey)


class ConfigurationMapping(
class _ConfigurationMapping(
ConfigurationCollection[_ConfigurationKeyT, _ConfigurationT],
Generic[_ConfigurationKeyT, _ConfigurationT],
):
"""
A key-value mapping where values are :py:class:`betty.config.Configuration`.
"""

def __init__(
self,
configurations: Iterable[_ConfigurationT] | None = None,
Expand Down Expand Up @@ -94,30 +89,6 @@ def replace(self, *configurations: _ConfigurationT) -> None:
self.clear()
self.append(*configurations)

@override
def load(self, dump: Dump) -> None:
self.clear()
self.replace(
*assert_sequence(self.load_item)(
[
self._load_key(item_value_dump, item_key_dump)
for item_key_dump, item_value_dump in assert_mapping()(dump).items()
]
)
)

@override
def dump(self) -> VoidableDump:
dump = {}
for configuration_item in self._configurations.values():
item_dump = configuration_item.dump()
if item_dump is not Void:
item_dump, configuration_key = self._dump_key(item_dump)
if self._minimize_item_dump():
item_dump = minimize(item_dump)
dump[configuration_key] = item_dump
return minimize(dump)

@override
def prepend(self, *configurations: _ConfigurationT) -> None:
self.insert(0, *configurations)
Expand Down Expand Up @@ -147,6 +118,15 @@ def insert(self, index: int, *configurations: _ConfigurationT) -> None:
def _get_key(self, configuration: _ConfigurationT) -> _ConfigurationKeyT:
pass


class ConfigurationMapping(
_ConfigurationMapping[_ConfigurationKeyT, _ConfigurationT],
Generic[_ConfigurationKeyT, _ConfigurationT],
):
"""
A key-value mapping where values are :py:class:`betty.config.Configuration`.
"""

@abstractmethod
def _load_key(
self,
Expand All @@ -158,3 +138,49 @@ def _load_key(
@abstractmethod
def _dump_key(self, item_dump: VoidableDump) -> tuple[VoidableDump, str]:
pass

@override
def load(self, dump: Dump) -> None:
self.clear()
self.replace(
*assert_mapping(self.load_item)(
{
item_key_dump: self._load_key(item_value_dump, item_key_dump)
for item_key_dump, item_value_dump in assert_mapping()(dump).items()
}
).values()
)

@override
def dump(self) -> VoidableDump:
dump = {}
for configuration_item in self._configurations.values():
item_dump = configuration_item.dump()
if item_dump is not Void:
item_dump, configuration_key = self._dump_key(item_dump)
if self._minimize_item_dump():
item_dump = minimize(item_dump)
dump[configuration_key] = item_dump
return minimize(dump)


class OrderedConfigurationMapping(
_ConfigurationMapping[_ConfigurationKeyT, _ConfigurationT],
Generic[_ConfigurationKeyT, _ConfigurationT],
):
"""
An ordered key-value mapping where values are :py:class:`betty.config.Configuration`.
"""

@override
def load(self, dump: Dump) -> None:
self.replace(*assert_sequence(self.load_item)(dump))

@override
def dump(self) -> VoidableDump:
return minimize(
[
configuration_item.dump()
for configuration_item in self._configurations.values()
]
)
2 changes: 1 addition & 1 deletion betty/extension/cotton_candy/assets/templates/base.html.j2
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
<div class="nav-primary-expanded">
<ul class="nav-secondary">
{% set ns = namespace(available_locales=[]) %}
{% for available_locale_configuration in project.configuration.locales %}
{% for available_locale_configuration in project.configuration.locales.values() %}
{% set available_locale_data = available_locale_configuration.alias | locale_get_data %}
{% do ns.available_locales.append((available_locale_configuration.locale, available_locale_data.get_display_name())) %}
{% endfor %}
Expand Down
6 changes: 3 additions & 3 deletions betty/extension/nginx/assets/nginx.conf.j2
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ server {
location @localized_redirect {
{% if project.configuration.clean_urls %}
set_by_lua_block $locale_alias {
local available_locales = {'{{ project.configuration.locales | map(attribute='locale') | join("', '") }}'}
local available_locales = {'{{ project.configuration.locales | join("', '") }}'}
local locale_aliases = {}
{% for locale_configuration in project.configuration.locales %}
{% for locale_configuration in project.configuration.locales.values() %}
locale_aliases['{{ locale_configuration.locale }}'] = '{{ locale_configuration.alias }}'
{% endfor %}
local locale = require('content_negotiation').negotiate(ngx.req.get_headers()['Accept-Language'], available_locales)
Expand All @@ -81,7 +81,7 @@ server {
}

# Localized resources.
location ~* ^/({{ project.configuration.locales | map(attribute='alias') | join('|') }})(/|$) {
location ~* ^/({{ project.configuration.locales.values() | map(attribute='alias') | join('|') }})(/|$) {
set $locale $1;

{{ headers(
Expand Down
9 changes: 3 additions & 6 deletions betty/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,10 +180,7 @@ async def _run_jobs(job_context: GenerationContext) -> AsyncIterator[Task[None]]
yield _run_job(semaphore, _generate_json_schema, job_context)
yield _run_job(semaphore, _generate_openapi, job_context)

locales = [
locale_configuration.locale
for locale_configuration in project.configuration.locales
]
locales = list(project.configuration.locales.keys())

for locale in locales:
yield _run_job(semaphore, _generate_public, job_context, locale)
Expand Down Expand Up @@ -448,7 +445,7 @@ async def _generate_sitemap(
sitemap: MutableSequence[str] = []
sitemap_length = 0
sitemaps.append(sitemap)
for locale_configuration in project.configuration.locales:
for locale in project.configuration.locales:
for entity in project.ancestry:
if isinstance(entity.id, GeneratedEntityId):
continue
Expand All @@ -459,7 +456,7 @@ async def _generate_sitemap(
project.url_generator.generate(
entity,
absolute=True,
locale=locale_configuration.locale,
locale=locale,
media_type="text/html",
)
)
Expand Down
62 changes: 14 additions & 48 deletions betty/project/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,10 @@
TYPE_CHECKING,
TypeVar,
Iterator,
overload,
)
from urllib.parse import urlparse

from aiofiles.tempfile import TemporaryDirectory
from typing_extensions import override

from betty import fs, event_dispatcher
from betty import model
from betty.ancestry import Ancestry, Person, Event, Place, Source
Expand All @@ -53,7 +50,10 @@
Configuration,
Configurable,
)
from betty.config.collections.mapping import ConfigurationMapping
from betty.config.collections.mapping import (
ConfigurationMapping,
OrderedConfigurationMapping,
)
from betty.config.collections.sequence import ConfigurationSequence
from betty.core import CoreComponent
from betty.event_dispatcher import EventDispatcher, EventHandlerRegistry
Expand Down Expand Up @@ -84,6 +84,7 @@
)
from betty.serde.format import FormatRepository
from betty.typing import Void
from typing_extensions import override

if TYPE_CHECKING:
from betty.machine_name import MachineName
Expand Down Expand Up @@ -661,7 +662,7 @@ def dump(self) -> VoidableDump:


@final
class LocaleConfigurationSequence(ConfigurationSequence[LocaleConfiguration]):
class LocaleConfigurationMapping(OrderedConfigurationMapping[str, LocaleConfiguration]):
"""
Configure a project's locales.
"""
Expand All @@ -673,45 +674,6 @@ def __init__(
super().__init__(configurations)
self._ensure_locale()

@override
@overload
def __getitem__(self, configuration_key: int | str) -> LocaleConfiguration:
pass

@override
@overload
def __getitem__(self, configuration_key: slice) -> Sequence[LocaleConfiguration]:
pass

@override
def __getitem__(
self, configuration_key: int | slice | str
) -> LocaleConfiguration | Sequence[LocaleConfiguration]:
if isinstance(configuration_key, str):
for configuration in self:
if configuration.locale == configuration_key:
return configuration
raise KeyError
return self._configurations[configuration_key]

def __delitem__(self, configuration_key: int | str) -> None:
if isinstance(configuration_key, str):
for index, configuration in enumerate(self):
if configuration.locale == configuration_key:
self.remove(index)
return
raise KeyError
self.remove(configuration_key)

@override
def _pre_add(self, configuration: LocaleConfiguration) -> None:
try:
self[configuration.locale]
except KeyError:
pass
else:
raise ValueError(f'Cannot add locale "{configuration.locale}" twice.')

@override
def _post_remove(self, configuration: LocaleConfiguration) -> None:
super()._post_remove(configuration)
Expand All @@ -725,7 +687,7 @@ def _ensure_locale(self) -> None:
def update(self, other: Self) -> None:
# Prevent the events from being dispatched.
self._configurations.clear()
self.append(*other)
self.append(*other.values())

@override
def replace(self, *configurations: LocaleConfiguration) -> None:
Expand All @@ -740,12 +702,16 @@ def load_item(self, dump: Dump) -> LocaleConfiguration:
item.load(dump)
return item

@override
def _get_key(self, configuration: LocaleConfiguration) -> str:
return configuration.locale

@property
def default(self) -> LocaleConfiguration:
"""
The default language.
"""
return self._configurations[0]
return next(self.values())

@property
def multilingual(self) -> bool:
Expand Down Expand Up @@ -811,7 +777,7 @@ def __init__(
)
self._extensions = ExtensionConfigurationMapping(extensions or ())
self._debug = debug
self._locales = LocaleConfigurationSequence(locales or ())
self._locales = LocaleConfigurationMapping(locales or ())
self._lifetime_threshold = lifetime_threshold

@property
Expand Down Expand Up @@ -933,7 +899,7 @@ def clean_urls(self, clean_urls: bool) -> None:
self._clean_urls = clean_urls

@property
def locales(self) -> LocaleConfigurationSequence:
def locales(self) -> LocaleConfigurationMapping:
"""
The available locales.
"""
Expand Down
5 changes: 1 addition & 4 deletions betty/test_utils/config/collections/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,7 @@ async def test___iter__(self) -> None:
"""
Tests :py:meth:`betty.config.collections.ConfigurationCollection.__iter__` implementations.
"""
configurations = self.get_configurations()
sut = self.get_sut(configurations)
assert tuple(iter(sut)) == configurations
assert list(sut.values()) == []
raise NotImplementedError

async def test___len__(self) -> None:
"""
Expand Down
Loading

0 comments on commit 7471c09

Please sign in to comment.