Skip to content

Commit ecd25ee

Browse files
authored
Add betty.collections.MutableResolvedMapping (#3721)
1 parent b1b4da1 commit ecd25ee

File tree

4 files changed

+414
-2
lines changed

4 files changed

+414
-2
lines changed

betty/collections.py

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,20 @@
99
Iterable,
1010
Iterator,
1111
Mapping,
12+
MutableMapping,
1213
MutableSequence,
1314
Sequence,
1415
)
1516
from contextlib import suppress
17+
from itertools import chain
1618
from typing import Any, Generic, TypeVar, final, overload
1719

1820
from typing_extensions import override
1921

2022
from betty.functools import passthrough
23+
from betty.typing import Void
2124

25+
_T = TypeVar("_T")
2226
_KeyT = TypeVar("_KeyT")
2327
_ValueT = TypeVar("_ValueT")
2428
_ResolvableKeyT = TypeVar("_ResolvableKeyT")
@@ -286,3 +290,285 @@ def __delitem__(self, index: int | slice) -> None:
286290
@override
287291
def extend(self, values: Iterable[_ValueT | _ResolvableValueT]) -> None:
288292
self._upstream.extend(map(self._value_resolver, values))
293+
294+
295+
class ResolvedMapping(
296+
Mapping[_KeyT, _ValueT], Generic[_KeyT, _ResolvableKeyT, _ValueT]
297+
):
298+
"""
299+
A mutable mapping of resolved keys.
300+
"""
301+
302+
@override
303+
@abstractmethod
304+
def __getitem__(self, key: _KeyT | _ResolvableKeyT) -> _ValueT:
305+
pass
306+
307+
@overload
308+
def get(self, key: _KeyT | _ResolvableKeyT, default: _T, /) -> _ValueT | _T:
309+
pass
310+
311+
@overload
312+
def get(
313+
self, key: _KeyT | _ResolvableKeyT, default: None = None, /
314+
) -> _ValueT | None:
315+
pass
316+
317+
@override
318+
@abstractmethod
319+
def get(
320+
self, key: _KeyT | _ResolvableKeyT, default: _T | None = None, /
321+
) -> _ValueT | None:
322+
pass
323+
324+
325+
class MutableResolvedMapping(
326+
MutableMapping[_KeyT, _ValueT],
327+
MutableCollection[_KeyT],
328+
ResolvedMapping[_KeyT, _ResolvableKeyT, _ValueT],
329+
Generic[_KeyT, _ResolvableKeyT, _ValueT, _ResolvableValueT],
330+
):
331+
"""
332+
A mutable mapping of resolved keys and values.
333+
"""
334+
335+
@abstractmethod
336+
def __setitem__(
337+
self, key: _KeyT | _ResolvableKeyT, value: _ValueT | _ResolvableValueT
338+
) -> None:
339+
pass
340+
341+
@abstractmethod
342+
def __delitem__(self, key: _KeyT | _ResolvableKeyT) -> None:
343+
pass
344+
345+
@overload
346+
def update(
347+
self,
348+
other: Mapping[_KeyT | _ResolvableKeyT, _ValueT | _ResolvableValueT]
349+
| Iterable[tuple[_KeyT | _ResolvableKeyT, _ValueT | _ResolvableValueT]],
350+
/,
351+
**kwargs: _ValueT | _ResolvableValueT,
352+
) -> None:
353+
pass
354+
355+
@overload
356+
def update(self, **kwargs: _ValueT | _ResolvableValueT) -> None:
357+
pass
358+
359+
@override
360+
@abstractmethod
361+
def update(self, other=None, **kwargs) -> None: # ty:ignore[invalid-method-override]
362+
pass
363+
364+
@overload
365+
def setdefault(
366+
self: MutableMapping[_KeyT, _T | None],
367+
key: _KeyT | _ResolvableKeyT,
368+
default: None = None,
369+
/,
370+
) -> _T | None:
371+
pass
372+
373+
@overload
374+
def setdefault(
375+
self, key: _KeyT | _ResolvableKeyT, default: _ValueT | _ResolvableValueT, /
376+
) -> _ValueT:
377+
pass
378+
379+
@override
380+
@abstractmethod
381+
def setdefault(self, key, default):
382+
pass
383+
384+
@overload
385+
def pop(self, key: _KeyT | _ResolvableKeyT, /) -> _ValueT:
386+
pass
387+
388+
@overload
389+
def pop(
390+
self, key: _KeyT | _ResolvableKeyT, /, default: _ValueT | _ResolvableValueT
391+
) -> _ValueT:
392+
pass
393+
394+
@overload
395+
def pop(self, key: _KeyT | _ResolvableKeyT, /, default: _T) -> _ValueT | _T:
396+
pass
397+
398+
@override
399+
@abstractmethod
400+
def pop(self, key, default):
401+
pass
402+
403+
@override
404+
@abstractmethod
405+
def popitem(self) -> tuple[_KeyT, _ValueT]:
406+
pass
407+
408+
409+
class _ResolvedMappingProxy(ResolvedMapping[_KeyT, _ResolvableKeyT, _ValueT]):
410+
def __init__(
411+
self,
412+
upstream: Mapping[_KeyT, _ValueT],
413+
*,
414+
key_resolver: Callable[[_KeyT | _ResolvableKeyT], _KeyT] = passthrough,
415+
):
416+
self._upstream = upstream
417+
self._key_resolver = key_resolver
418+
419+
@final
420+
@override
421+
def __getitem__(self, key: _KeyT | _ResolvableKeyT) -> _ValueT:
422+
return self._upstream[self._key_resolver(key)]
423+
424+
@overload
425+
def get(self, key: _KeyT | _ResolvableKeyT, default: _T, /) -> _ValueT | _T:
426+
pass
427+
428+
@overload
429+
def get(
430+
self, key: _KeyT | _ResolvableKeyT, default: None = None, /
431+
) -> _ValueT | None:
432+
pass
433+
434+
@final
435+
@override
436+
def get(self, key, default=None):
437+
return self._upstream.get(self._key_resolver(key), default)
438+
439+
@final
440+
@override
441+
def __iter__(self) -> Iterator[_KeyT]:
442+
return iter(self._upstream)
443+
444+
@final
445+
@override
446+
def __len__(self) -> int:
447+
return len(self._upstream)
448+
449+
@final
450+
@override
451+
def __contains__(self, key: Any) -> bool:
452+
with suppress(Exception):
453+
key = self._key_resolver(key)
454+
return key in self._upstream
455+
456+
457+
@final
458+
class ResolvedMappingProxy(_ResolvedMappingProxy[_KeyT, _ResolvableKeyT, _ValueT]):
459+
"""
460+
Decorate another mapping to resolve any values before proxying them.
461+
"""
462+
463+
464+
@final
465+
class MutableResolvedMappingProxy(
466+
_ResolvedMappingProxy[_KeyT, _ResolvableKeyT, _ValueT],
467+
MutableResolvedMapping[_KeyT, _ResolvableKeyT, _ValueT, _ResolvableValueT],
468+
):
469+
"""
470+
Decorate another mapping to resolve any values before proxying them.
471+
"""
472+
473+
_upstream: MutableMapping[_KeyT, _ValueT]
474+
475+
def __init__(
476+
self,
477+
upstream: MutableMapping[_KeyT, _ValueT],
478+
*,
479+
key_resolver: Callable[[_KeyT | _ResolvableKeyT], _KeyT] = passthrough,
480+
value_resolver: Callable[[_ValueT | _ResolvableValueT], _ValueT] = passthrough,
481+
):
482+
super().__init__(upstream, key_resolver=key_resolver)
483+
self._value_resolver = value_resolver
484+
485+
def __setitem__(
486+
self, key: _KeyT | _ResolvableKeyT, value: _ValueT | _ResolvableValueT
487+
) -> None:
488+
self._upstream[self._key_resolver(key)] = self._value_resolver(value)
489+
490+
def __delitem__(self, key: _KeyT | _ResolvableKeyT) -> None:
491+
del self._upstream[self._key_resolver(key)]
492+
493+
@overload
494+
def update(
495+
self,
496+
other: Mapping[_KeyT | _ResolvableKeyT, _ValueT | _ResolvableValueT]
497+
| Iterable[tuple[_KeyT | _ResolvableKeyT, _ValueT | _ResolvableValueT]],
498+
/,
499+
**kwargs: _ValueT | _ResolvableValueT,
500+
) -> None:
501+
pass
502+
503+
@overload
504+
def update(self, **kwargs: _ValueT | _ResolvableValueT) -> None:
505+
pass
506+
507+
@override
508+
def update(self, other=None, **kwargs) -> None:
509+
items = kwargs.items()
510+
if isinstance(other, Mapping):
511+
items = chain(items, other.items())
512+
elif isinstance(other, Sequence):
513+
items = chain(items, other)
514+
self._upstream.update(
515+
{
516+
self._key_resolver(key): self._value_resolver(value) # ty:ignore[invalid-argument-type]
517+
for key, value in items
518+
}
519+
)
520+
521+
@overload
522+
def setdefault(
523+
self: MutableMapping[_KeyT, _T | None],
524+
key: _KeyT | _ResolvableKeyT,
525+
default: None = None,
526+
/,
527+
) -> _T | None:
528+
pass
529+
530+
@overload
531+
def setdefault(
532+
self, key: _KeyT | _ResolvableKeyT, default: _ValueT | _ResolvableValueT, /
533+
) -> _ValueT:
534+
pass
535+
536+
@override
537+
def setdefault(
538+
self,
539+
key,
540+
default=Void(), # noqa: B008
541+
):
542+
return self._upstream.setdefault(
543+
self._key_resolver(key),
544+
None if default is Void() else self._value_resolver(default), # ty:ignore[invalid-argument-type]
545+
) # ty:ignore[no-matching-overload]
546+
547+
@overload
548+
def pop(self, key: _KeyT | _ResolvableKeyT, /) -> _ValueT:
549+
pass
550+
551+
@overload
552+
def pop(
553+
self, key: _KeyT | _ResolvableKeyT, /, default: _ValueT | _ResolvableValueT
554+
) -> _ValueT:
555+
pass
556+
557+
@overload
558+
def pop(self, key: _KeyT | _ResolvableKeyT, /, default: _T) -> _ValueT | _T:
559+
pass
560+
561+
@override
562+
def pop(
563+
self,
564+
key,
565+
default=Void(), # noqa: B008
566+
):
567+
key = self._key_resolver(key)
568+
if default is Void():
569+
return self._upstream.pop(key)
570+
return self._upstream.pop(key, default)
571+
572+
@override
573+
def popitem(self) -> tuple[_KeyT, _ValueT]:
574+
return self._upstream.popitem()

betty/extension/gramps/config.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from betty.ancestry.event_type import EventTypeDefinition
1414
from betty.ancestry.place_type import PlaceTypeDefinition
1515
from betty.ancestry.presence_role import PresenceRoleDefinition
16+
from betty.collections import MutableResolvedMapping, MutableResolvedMappingProxy
1617
from betty.data import Data, Sample
1718
from betty.data.aggregate.collection.mapping import MappingDefinition
1819
from betty.data.aggregate.collection.sequence import SequenceDefinition
@@ -37,6 +38,7 @@
3738
from betty.plugin.config import (
3839
PluginConfiguration,
3940
ResolvablePluginConfiguration,
41+
resolve_plugin_configuration,
4042
resolve_plugin_configuration_mapping,
4143
)
4244
from betty.plugin.data import PluginConfigurationDefinition
@@ -70,12 +72,19 @@ def __init__(
7072
):
7173
super().__init__(
7274
MappingDefinition(
73-
cls=dict,
75+
cls=MutableResolvedMapping,
76+
factory=lambda items: MutableResolvedMappingProxy(
77+
resolve_plugin_configuration_mapping(items),
78+
value_resolver=resolve_plugin_configuration,
79+
),
7480
key=StrDefinition(label=gramps_label),
7581
value=PluginConfigurationDefinition(plugin_type),
7682
label=plugin_type.type().label_plural,
7783
),
78-
default=lambda: resolve_plugin_configuration_mapping(default), # ty:ignore[invalid-argument-type]
84+
default=lambda: MutableResolvedMappingProxy(
85+
resolve_plugin_configuration_mapping(default), # ty:ignore[invalid-argument-type]
86+
value_resolver=resolve_plugin_configuration,
87+
),
7988
)
8089

8190

betty/tests/coverage/test_coverage.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,9 @@ class MissingReason(Enum):
408408
"KeyedCollection": MissingReason.ABSTRACT,
409409
"MutableCollection": MissingReason.ABSTRACT,
410410
"MutableKeyedCollection": MissingReason.ABSTRACT,
411+
"MutableResolvedMapping": MissingReason.ABSTRACT,
411412
"MutableResolvedSequence": MissingReason.ABSTRACT,
413+
"ResolvedMapping": MissingReason.ABSTRACT,
412414
},
413415
"betty/config.py": {
414416
"Configurable": MissingReason.ABSTRACT,

0 commit comments

Comments
 (0)