Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 48 additions & 18 deletions src/formbridge/forms/declared_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from collections.abc import Callable, Iterable, Iterator
from dataclasses import dataclass, fields, is_dataclass
from functools import cached_property
from functools import cached_property, singledispatch
from inspect import cleandoc
from typing import Annotated, ClassVar, Optional, Self, TypeVar, get_args, get_origin

Expand Down Expand Up @@ -59,35 +59,65 @@ class FormPage:


class DeclaredViews:
"""A family of widget classes, each registered under what it shows.

A class directly below this one starts a family. A member names the
declarations it shows in its own body (`shows = (int,)`), so adding a kind
of field is adding a class: nothing lists the members and nothing chooses
between them by testing types. A backend's field widgets and editors are
such families.
"""A capability mixin: a family of widget classes, each declaring what it shows.

A member declares what it shows as a class keyword
(`class IntEditor(ValueEditor, shows=(int,))`). Every class records its own
declaration, so a parent's never passes to a child and nothing reads a class
body. The family is the inheritance tree below the class asked: adding a kind
of field is adding a class, and nothing lists the members or tests types.
A backend's field widgets and editors are such families. A family root lists
this mixin before its widget base, so the `shows` keyword is taken here before
a framework's own __init_subclass__ sees it.
"""

shows: ClassVar[tuple[object, ...]] = ()
shown_by: ClassVar[dict[object, Callable]]

def __init_subclass__(cls, **kwargs: object) -> None:
def __init_subclass__(cls, *, shows: tuple[object, ...] = (), **kwargs: object) -> None:
super().__init_subclass__(**kwargs)
if DeclaredViews in cls.__bases__:
cls.shown_by = {}
for declaration in vars(cls).get("shows", ()):
cls.shown_by[declaration] = cls
cls.shows = shows

@classmethod
def members(cls) -> Iterator[type[Self]]:
"""This class and every class below it."""
yield cls
for member in cls.__subclasses__():
yield from member.members()

@classmethod
def showing(cls, declarations: Iterable[object]) -> Callable[..., Self]:
"""The member registered under the first of these declarations that has one."""
"""The member that shows the first of these declarations one of them shows."""
shown_by = {declaration: member for member in cls.members() for declaration in member.shows}
declarations = tuple(declarations)
for declaration in declarations:
if declaration in cls.shown_by:
return cls.shown_by[declaration]
if declaration in shown_by:
return shown_by[declaration]
raise LookupError(f"No {cls.__name__} shows any of {declarations!r}")


class DeclaredMetadata:
"""Annotation metadata that says what it declares, beyond its own class.

By default an item declares its class and that class's bases. A member
overrides `declares` when one instance declares more, such as choices prepared
for one field that name the editors it needs.
"""

def declares(self) -> Iterator[object]:
yield from type(self).__mro__[:-1]


@singledispatch
def metadata_declarations(item: object) -> Iterator[object]:
"""Foreign metadata declares its class and that class's bases."""
return iter(type(item).__mro__[:-1])


@metadata_declarations.register
def _(item: DeclaredMetadata) -> Iterator[object]:
return item.declares()


def declarations(annotation: object) -> Iterator[object]:
"""What an annotation declares, most specific first.

Expand All @@ -97,7 +127,7 @@ def declarations(annotation: object) -> Iterator[object]:
if get_origin(annotation) is Annotated:
base, *metadata = get_args(annotation)
for item in metadata:
yield from type(item).__mro__[:-1]
yield from metadata_declarations(item)
yield from declarations(base)
elif optional_member_type(annotation) is not None:
yield Optional
Expand Down
69 changes: 69 additions & 0 deletions tests/test_declared_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""A view family is the inheritance tree below it; each class declares what it shows."""

from typing import Annotated

import pytest

from formbridge.forms.declared_fields import DeclaredMetadata, DeclaredViews, declarations


class Number:
"""A declaration with a more specific kind below it."""


class Count(Number):
"""The more specific kind."""


class FieldWidget(DeclaredViews):
"""The family root: it shows nothing itself."""


class NumberWidget(FieldWidget, shows=(Number,)):
pass


class CountWidget(NumberWidget, shows=(Count,)):
pass


class StyledNumberWidget(NumberWidget):
"""A subclass declaring nothing shows nothing: its parent's declaration stays its parent's."""


def test_the_first_declaration_with_a_member_chooses_it():
assert FieldWidget.showing(Count.__mro__) is CountWidget
assert FieldWidget.showing(Number.__mro__) is NumberWidget


def test_a_class_shows_only_what_it_declares():
assert StyledNumberWidget.shows == ()
assert FieldWidget.shows == ()


def test_a_declaration_no_member_shows_is_refused():
with pytest.raises(LookupError):
FieldWidget.showing(str.__mro__)


class Prepared(DeclaredMetadata):
"""Metadata whose one instance names the kind that edits it."""

def __init__(self, kind: type) -> None:
self.kind = kind

def declares(self):
yield self.kind
yield from super().declares()


def test_metadata_that_declares_more_is_read_through_its_own_declaration():
annotation = Annotated[str, Prepared(Count)]
assert FieldWidget.showing(declarations(annotation)) is CountWidget
assert next(iter(declarations(Annotated[str, Number()]))) is Number


def test_a_family_asked_below_its_root_answers_from_its_own_tree():
assert NumberWidget.showing(Count.__mro__) is CountWidget
with pytest.raises(LookupError):
CountWidget.showing(Number.__mro__)
Loading