Skip to content

pyvelm.builders

builders

Fluent view and menu declaration builders (velmphp / Filament-style).

Preferred authoring::

from pyvelm.builders import ViewsData, ListView, FormView, Field, Menus

m = Menus("partners")
views_data = (
    ViewsData.make()
    .views(
        ListView.make("partner.list")
        .model("res.partner")
        .columns(["name", Field.make("active").toggle()])
        .form_view("partner.form"),
        FormView.make("partner.form")
        .model("res.partner")
        .section("identity", "Identity", ["name", "code"]),
    )
    .menus(
        m.group("business", "Business", icon="home").children([
            m.item("business.partners", "Partners").view("partner.list"),
        ]),
    )
)

Legacy function helpers (list_view, field, section, …) remain available and delegate to the same fluent classes.

DashboardView module-attribute

DashboardView = DashboardViewBuilder

A view_type="dashboard" view declaration.

DetailView module-attribute

DetailView = DetailViewBuilder

A view_type="detail" read-only record view declaration.

FormView module-attribute

FormView = FormViewBuilder

A view_type="form" view declaration.

GraphView module-attribute

GraphView = GraphViewBuilder

A view_type="graph" view declaration.

KanbanView module-attribute

KanbanView = KanbanViewBuilder

A view_type="kanban" view declaration.

ListView module-attribute

ListView = ListViewBuilder

A view_type="list" view declaration.

PivotView module-attribute

PivotView = PivotViewBuilder

A view_type="pivot" view declaration.

Action

Declares a toolbar action on list or form/detail views.

Source code in pyvelm/builders/action.py
class Action:
    """Declares a toolbar action on list or form/detail views."""

    def __init__(self, label: str) -> None:
        self._label = label
        self._url: str | None = None
        self._method: str | None = None
        self._confirm: str | None = None
        self._perm: str | None = None
        self._model: str | None = None
        self._policy: str | None = None
        self._full_page: bool | None = None
        self._form: dict | None = None
        self._form_view: str | None = None
        self._form_module: str | None = None

    @classmethod
    def make(cls, label: str) -> Action:
        return cls(label)

    def url(self, url: str) -> Action:
        self._url = url
        return self

    def method(self, method: str) -> Action:
        self._method = method.upper()
        return self

    def confirm(self, confirm: str) -> Action:
        self._confirm = confirm
        return self

    def perm(self, perm: str) -> Action:
        self._perm = perm
        return self

    def model(self, model: str) -> Action:
        self._model = model
        return self

    def policy(self, policy: str) -> Action:
        self._policy = policy
        return self

    def full_page(self, full_page: bool = True) -> Action:
        self._full_page = full_page
        return self

    def form_view(self, form_view: str, module: str | None = None) -> Action:
        self._form_view = form_view
        if module:
            self._form_module = module
        return self

    def form(
        self,
        form: ActionForm | Callable[[ActionForm], ActionForm],
    ) -> Action:
        schema = form if isinstance(form, ActionForm) else form(ActionForm.make())
        arch = schema.to_dict()
        if not arch.get("sections"):
            raise ValueError(
                f"Action {self._label!r} inline form requires at least one section."
            )
        self._form = arch
        if self._model is None and arch.get("model"):
            self._model = str(arch["model"])
        return self

    def to_dict(self) -> dict:
        if not self._label:
            raise ValueError("View action requires a label.")
        has_inline = bool(
            self._form and (self._form.get("sections") or [])
        )
        has_stored = bool(self._form_view)
        has_url = bool(self._url)
        if not has_inline and not has_stored and not has_url:
            raise ValueError(
                f"Action {self._label!r} requires url(), formView(), or form()."
            )
        if has_inline and not self._model:
            raise ValueError(
                f"Action {self._label!r} inline form requires model() "
                "on the action or ActionForm."
            )
        action: dict = {"label": self._label}
        if has_url:
            action["url"] = self._url
        if self._method:
            action["method"] = self._method
        if self._confirm:
            action["confirm"] = self._confirm
        if self._perm:
            action["perm"] = self._perm
        if self._model:
            action["model"] = self._model
        if self._policy:
            action["policy"] = self._policy
        if self._full_page is not None:
            action["full_page"] = self._full_page
        if has_stored:
            action["form_view"] = self._form_view
        if self._form_module:
            action["form_module"] = self._form_module
        if has_inline:
            action["form"] = self._form
        return action

ActionForm

Inline form schema for view-action dialogs.

Source code in pyvelm/builders/action.py
class ActionForm:
    """Inline form schema for view-action dialogs."""

    def __init__(self) -> None:
        self._sections: list[dict] = []
        self._cols: int | None = None
        self._model: str | None = None

    @classmethod
    def make(cls) -> ActionForm:
        return cls()

    def model(self, model: str) -> ActionForm:
        self._model = model
        return self

    def cols(self, cols: int) -> ActionForm:
        self._cols = cols
        return self

    def section(
        self,
        name: str,
        title: str,
        fields: list[Any],
    ) -> ActionForm:
        section = Section.make(name, title).fields(fields)
        if self._cols is not None:
            section.cols(self._cols)
        self._sections.append(section.to_dict())
        return self

    def to_dict(self) -> dict:
        arch: dict = {"sections": list(self._sections)}
        if self._cols is not None:
            arch["cols"] = self._cols
        if self._model:
            arch["model"] = self._model
        return arch

Field

Build a single field entry inside list / form / kanban archs.

Source code in pyvelm/builders/field.py
class Field:
    """Build a single field entry inside list / form / kanban archs."""

    def __init__(self, name: str) -> None:
        self._name = name
        self._options: dict[str, Any] = {}

    @classmethod
    def make(cls, name: str) -> Field:
        return cls(name)

    def widget(self, widget: WidgetHint) -> Field:
        self._options["widget"] = widget
        return self

    def toggle(self) -> Field:
        return self.widget("toggle")

    def label(self, label: str) -> Field:
        self._options["label"] = label
        return self

    def readonly(self, value: bool | Callable[..., bool] = True) -> Field:
        self._options["readonly"] = value
        return self

    def readonly_when(self, domain: Sequence | Callable[..., bool]) -> Field:
        """Read-only when a domain matches or a callable returns true."""
        self._options["readonly_when"] = (
            list(domain) if isinstance(domain, (list, tuple)) else domain
        )
        return self

    def required(self, value: bool | Callable[..., bool] = True) -> Field:
        self._options["required"] = value
        return self

    def required_when(self, domain: Sequence | Callable[..., bool]) -> Field:
        """Required when a domain matches or a callable returns true."""
        self._options["required_when"] = (
            list(domain) if isinstance(domain, (list, tuple)) else domain
        )
        return self

    def visible(self, value: bool | Callable[..., bool] = True) -> Field:
        self._options["visible"] = value
        return self

    def visible_when(self, domain: Sequence | Callable[..., bool]) -> Field:
        """Visible when a domain matches or a callable returns true.

        Domains support nested paths (``company_id.currency_id.code``) on
        live forms — M2O ids from the submitted form are browsed via env.
        """
        self._options["visible_when"] = (
            list(domain) if isinstance(domain, (list, tuple)) else domain
        )
        return self

    def hidden(self, value: bool | Callable[..., bool] = True) -> Field:
        self._options["hidden"] = value
        return self

    def visible_js(self, expr: str) -> Field:
        self._options["visible_js"] = expr
        return self

    def live(
        self,
        *,
        on_blur: bool = False,
        debounce: int | None = None,
    ) -> Field:
        """Re-render the form via HTMX when this field changes (Filament ``live()``).

        ``live()`` — on ``change``; ``live(debounce=200)`` — debounced;
        ``live(on_blur=True)`` — on blur only.
        """
        if on_blur:
            self._options["live"] = "blur"
        elif debounce is not None:
            self._options["live"] = debounce
        else:
            self._options["live"] = True
        return self

    def reactive(
        self,
        *,
        on_blur: bool = False,
        debounce: int | None = None,
    ) -> Field:
        """Alias for :meth:`live` (Filament / Livewire naming)."""
        return self.live(on_blur=on_blur, debounce=debounce)

    def depends_on(self, *fields: str) -> Field:
        """Declare sibling fields whose changes should refresh this field.

        Dependency sources without an explicit ``live()`` still trigger a
        form re-render so ``visible_when``, ``required_when``, and
        ``options_domain`` stay in sync.
        """
        self._options["depends_on"] = list(fields)
        return self

    def options_domain(self, domain_or_fn: Sequence | Callable[..., Sequence]) -> Field:
        """Filter Many2one / Many2many picker options (domain list or callable).

        **Static domain** — compiled to SQL for search; nested paths work::

            .options_domain([("company_id.currency_id.code", "=", "KES")])

        Sibling values: ``"company_id"`` or ``"$company_id"`` (also dotted
        ``$company_id.currency_id.code``).

        **Callable** — module-level named function (view arch serializes it).
        Receives ``(record, env, get)`` or ``(ctx,)`` and returns a domain list.
        Use when empty-state logic is easier in Python than in domain syntax.
        """
        self._options["options_domain"] = (
            list(domain_or_fn)
            if isinstance(domain_or_fn, (list, tuple))
            else domain_or_fn
        )
        return self

    def default(self, fn: Callable[..., Any]) -> Field:
        self._options["default"] = fn
        return self

    def colspan(self, colspan: int | str) -> Field:
        self._options["colspan"] = colspan  # type: ignore[typeddict-item]
        return self

    def set(self, **extra: Any) -> Field:
        self._options.update(extra)
        return self

    def to_dict(self) -> FieldRef:
        from ._normalize import field_specs

        opts = dict(self._options)
        if isinstance(opts.get("columns"), list):
            opts["columns"] = field_specs(opts["columns"])
        return {"name": self._name, **opts}  # type: ignore[typeddict-item]

readonly_when

readonly_when(domain: Sequence | Callable[..., bool]) -> Field

Read-only when a domain matches or a callable returns true.

Source code in pyvelm/builders/field.py
def readonly_when(self, domain: Sequence | Callable[..., bool]) -> Field:
    """Read-only when a domain matches or a callable returns true."""
    self._options["readonly_when"] = (
        list(domain) if isinstance(domain, (list, tuple)) else domain
    )
    return self

required_when

required_when(domain: Sequence | Callable[..., bool]) -> Field

Required when a domain matches or a callable returns true.

Source code in pyvelm/builders/field.py
def required_when(self, domain: Sequence | Callable[..., bool]) -> Field:
    """Required when a domain matches or a callable returns true."""
    self._options["required_when"] = (
        list(domain) if isinstance(domain, (list, tuple)) else domain
    )
    return self

visible_when

visible_when(domain: Sequence | Callable[..., bool]) -> Field

Visible when a domain matches or a callable returns true.

Domains support nested paths (company_id.currency_id.code) on live forms — M2O ids from the submitted form are browsed via env.

Source code in pyvelm/builders/field.py
def visible_when(self, domain: Sequence | Callable[..., bool]) -> Field:
    """Visible when a domain matches or a callable returns true.

    Domains support nested paths (``company_id.currency_id.code``) on
    live forms — M2O ids from the submitted form are browsed via env.
    """
    self._options["visible_when"] = (
        list(domain) if isinstance(domain, (list, tuple)) else domain
    )
    return self

live

live(*, on_blur: bool = False, debounce: int | None = None) -> Field

Re-render the form via HTMX when this field changes (Filament live()).

live() — on change; live(debounce=200) — debounced; live(on_blur=True) — on blur only.

Source code in pyvelm/builders/field.py
def live(
    self,
    *,
    on_blur: bool = False,
    debounce: int | None = None,
) -> Field:
    """Re-render the form via HTMX when this field changes (Filament ``live()``).

    ``live()`` — on ``change``; ``live(debounce=200)`` — debounced;
    ``live(on_blur=True)`` — on blur only.
    """
    if on_blur:
        self._options["live"] = "blur"
    elif debounce is not None:
        self._options["live"] = debounce
    else:
        self._options["live"] = True
    return self

reactive

reactive(*, on_blur: bool = False, debounce: int | None = None) -> Field

Alias for :meth:live (Filament / Livewire naming).

Source code in pyvelm/builders/field.py
def reactive(
    self,
    *,
    on_blur: bool = False,
    debounce: int | None = None,
) -> Field:
    """Alias for :meth:`live` (Filament / Livewire naming)."""
    return self.live(on_blur=on_blur, debounce=debounce)

depends_on

depends_on(*fields: str) -> Field

Declare sibling fields whose changes should refresh this field.

Dependency sources without an explicit live() still trigger a form re-render so visible_when, required_when, and options_domain stay in sync.

Source code in pyvelm/builders/field.py
def depends_on(self, *fields: str) -> Field:
    """Declare sibling fields whose changes should refresh this field.

    Dependency sources without an explicit ``live()`` still trigger a
    form re-render so ``visible_when``, ``required_when``, and
    ``options_domain`` stay in sync.
    """
    self._options["depends_on"] = list(fields)
    return self

options_domain

options_domain(domain_or_fn: Sequence | Callable[..., Sequence]) -> Field

Filter Many2one / Many2many picker options (domain list or callable).

Static domain — compiled to SQL for search; nested paths work::

.options_domain([("company_id.currency_id.code", "=", "KES")])

Sibling values: "company_id" or "$company_id" (also dotted $company_id.currency_id.code).

Callable — module-level named function (view arch serializes it). Receives (record, env, get) or (ctx,) and returns a domain list. Use when empty-state logic is easier in Python than in domain syntax.

Source code in pyvelm/builders/field.py
def options_domain(self, domain_or_fn: Sequence | Callable[..., Sequence]) -> Field:
    """Filter Many2one / Many2many picker options (domain list or callable).

    **Static domain** — compiled to SQL for search; nested paths work::

        .options_domain([("company_id.currency_id.code", "=", "KES")])

    Sibling values: ``"company_id"`` or ``"$company_id"`` (also dotted
    ``$company_id.currency_id.code``).

    **Callable** — module-level named function (view arch serializes it).
    Receives ``(record, env, get)`` or ``(ctx,)`` and returns a domain list.
    Use when empty-state logic is easier in Python than in domain syntax.
    """
    self._options["options_domain"] = (
        list(domain_or_fn)
        if isinstance(domain_or_fn, (list, tuple))
        else domain_or_fn
    )
    return self

KanbanCard

Kanban card layout block.

Source code in pyvelm/builders/layout.py
class KanbanCard:
    """Kanban card layout block."""

    def __init__(self, title: str) -> None:
        self._title = title
        self._subtitle: str | None = None
        self._fields: list[FieldRefLike | Any] | None = None
        self._badges: list[FieldRefLike | Any] | None = None
        self._image: str | None = None

    @classmethod
    def make(cls, title: str) -> KanbanCard:
        return cls(title)

    def subtitle(self, subtitle: str) -> KanbanCard:
        self._subtitle = subtitle
        return self

    def fields(self, fields: list[FieldRefLike | Any]) -> KanbanCard:
        self._fields = list(fields)
        return self

    def badges(self, badges: list[FieldRefLike | Any]) -> KanbanCard:
        self._badges = list(badges)
        return self

    def image(self, image: str) -> KanbanCard:
        self._image = image
        return self

    def to_dict(self) -> ArchKanbanCard:
        result: ArchKanbanCard = {"title": self._title}
        if self._subtitle is not None:
            result["subtitle"] = self._subtitle
        if self._fields is not None:
            result["fields"] = field_specs(self._fields)
        if self._badges is not None:
            result["badges"] = field_specs(self._badges)
        if self._image is not None:
            result["image"] = self._image
        return result

Notebook

Tabbed notebook block on a parent form.

Source code in pyvelm/builders/layout.py
class Notebook:
    """Tabbed notebook block on a parent form."""

    def __init__(self, name: str) -> None:
        self._name = name
        self._title: str | None = None
        self._pages: list[ArchPage] = []

    @classmethod
    def make(cls, name: str) -> Notebook:
        return cls(name)

    def title(self, title: str) -> Notebook:
        self._title = title
        return self

    def pages(self, pages: list[Page | ArchPage]) -> Notebook:
        self._pages = [
            p.to_dict() if isinstance(p, Page) else p for p in pages
        ]
        return self

    def to_dict(self) -> ArchNotebook:
        result: ArchNotebook = {"name": self._name, "pages": self._pages}
        if self._title is not None:
            result["title"] = self._title
        return result

Page

One tab page inside a form notebook.

Source code in pyvelm/builders/layout.py
class Page:
    """One tab page inside a form notebook."""

    def __init__(self, name: str, title: str) -> None:
        self._name = name
        self._title = title
        self._fields: list[FieldRefLike | Any] = []
        self._cols: int | None = None

    @classmethod
    def make(cls, name: str, title: str) -> Page:
        return cls(name, title)

    def fields(self, fields: list[FieldRefLike | Any]) -> Page:
        self._fields = list(fields)
        return self

    def cols(self, cols: int) -> Page:
        self._cols = cols
        return self

    def to_dict(self) -> ArchPage:
        result: ArchPage = {
            "name": self._name,
            "title": self._title,
            "fields": field_specs(self._fields),
        }
        if self._cols is not None:
            result["cols"] = self._cols
        return result

Section

Form section block.

Source code in pyvelm/builders/layout.py
class Section:
    """Form section block."""

    def __init__(self, name: str, title: str) -> None:
        self._name = name
        self._title = title
        self._fields: list[FieldRefLike | Any] = []
        self._cols: int | None = None

    @classmethod
    def make(cls, name: str, title: str) -> Section:
        return cls(name, title)

    def fields(self, fields: list[FieldRefLike | Any]) -> Section:
        self._fields = list(fields)
        return self

    def cols(self, cols: int) -> Section:
        self._cols = cols
        return self

    def to_dict(self) -> ArchSection:
        result: ArchSection = {
            "name": self._name,
            "title": self._title,
            "fields": field_specs(self._fields),
        }
        if self._cols is not None:
            result["cols"] = self._cols
        return result

MenuBranch

Menu group with optional nested children.

Source code in pyvelm/builders/menus.py
class MenuBranch:
    """Menu group with optional nested children."""

    __slots__ = ("menu", "_children", "_menu_module")

    def __init__(
        self,
        menu: Menu,
        *,
        menu_module: str,
        children: Sequence[Menu | MenuBranch | MenuItem] | None = None,
    ) -> None:
        self.menu = menu
        self._menu_module = menu_module
        self._children: list[Menu | MenuBranch | MenuItem] = []
        if children:
            self.children(children)

    @classmethod
    def make(cls, module: str, name: str, label: str) -> MenuBranch:
        return cls(menu_group(name, label, menu_module=module), menu_module=module)

    def children(
        self,
        entries: Sequence[Menu | MenuBranch | MenuItem],
    ) -> MenuBranch:
        parent_name = self.menu["name"]
        for entry in entries:
            _set_menu_parent_if_missing(
                entry, parent_name, menu_module=self._menu_module
            )
            self._children.append(entry)
        return self

    def sequence(self, sequence: int) -> MenuBranch:
        self.menu["sequence"] = sequence
        return self

    def icon(self, icon: str) -> MenuBranch:
        self.menu["icon"] = icon
        return self

    def parent(self, parent: str | tuple[str, str]) -> MenuBranch:
        self.menu["parent"] = _resolve_menu_parent(
            parent, menu_module=self._menu_module
        )
        return self

    def dev_only(self, dev_only: bool = True) -> MenuBranch:
        if dev_only:
            self.menu["dev_only"] = True
        return self

    def flatten(self) -> list[Menu]:
        out: list[Menu] = [self.menu]
        for child in self._children:
            if isinstance(child, MenuBranch):
                out.extend(child.flatten())
            elif isinstance(child, MenuItem):
                out.extend(child.flatten())
            else:
                out.append(child)
        return out

MenuItem

Fluent leaf menu entry (MenuItem.make(...).view('partner.list')).

Source code in pyvelm/builders/menus.py
class MenuItem:
    """Fluent leaf menu entry (``MenuItem.make(...).view('partner.list')``)."""

    __slots__ = (
        "_module",
        "_name",
        "_label",
        "_href",
        "_view",
        "_view_module",
        "_parent",
        "_icon",
        "_sequence",
        "_perm",
        "_model",
        "_policy",
        "_dev_only",
        "_built",
    )

    def __init__(self, module: str, name: str, label: str) -> None:
        self._module = module
        self._name = name
        self._label = label
        self._href: str | None = None
        self._view: str | None = None
        self._view_module: str | None = None
        self._parent: str | tuple[str, str] | None = None
        self._icon: str | None = None
        self._sequence = 10
        self._perm: str | None = None
        self._model: str | None = None
        self._policy: str | None = None
        self._dev_only = False
        self._built: Menu | None = None

    @classmethod
    def make(cls, module: str, name: str, label: str) -> MenuItem:
        return cls(module, name, label)

    def href(self, href: str) -> MenuItem:
        self._href = href
        return self

    def view(self, view: str, *, module: str | None = None) -> MenuItem:
        self._view = view
        self._view_module = module
        return self

    def parent(self, parent: str | tuple[str, str]) -> MenuItem:
        self._parent = parent
        return self

    def parent_ref(self, module_dot_name: str) -> MenuItem:
        self._parent = module_dot_name
        return self

    def icon(self, icon: str) -> MenuItem:
        self._icon = icon
        return self

    def sequence(self, sequence: int) -> MenuItem:
        self._sequence = sequence
        return self

    def perm(self, perm: str) -> MenuItem:
        self._perm = perm
        return self

    def model(self, model: str) -> MenuItem:
        self._model = model
        return self

    def policy(self, policy: str) -> MenuItem:
        self._policy = policy
        return self

    def dev_only(self, dev_only: bool = True) -> MenuItem:
        self._dev_only = dev_only
        return self

    def to_dict(self) -> Menu:
        if self._built is not None:
            return self._built
        resolved_parent: str | None = None
        if self._parent is not None:
            if isinstance(self._parent, tuple):
                resolved_parent = menu_ref(self._parent[0], self._parent[1])
            else:
                resolved_parent = _resolve_menu_parent(
                    self._parent, menu_module=self._module
                )
        self._built = menu_item(
            self._name,
            self._label,
            href=self._href,
            view=self._view,
            menu_module=self._module,
            view_module=self._view_module,
            parent=resolved_parent,
            icon=self._icon,
            sequence=self._sequence,
            perm=self._perm,
            model=self._model,
            policy=self._policy,
            dev_only=self._dev_only,
        )
        return self._built

    def flatten(self) -> list[Menu]:
        return [self.to_dict()]

Menus

Module-scoped fluent menu builder.

Source code in pyvelm/builders/menus.py
class Menus:
    """Module-scoped fluent menu builder."""

    def __init__(self, module: str) -> None:
        self.module = module

    def ref(self, name: str) -> str:
        return menu_ref(self.module, name)

    def parent(self, name: str, *, module: str | None = None) -> str:
        return menu_ref(module or self.module, name)

    def view(self, name: str, *, module: str | None = None) -> str:
        return view_href(module or self.module, name)

    def group(
        self,
        name: str,
        label: str,
        *,
        icon: str | None = None,
        sequence: int = 10,
        parent: str | tuple[str, str] | None = None,
        dev_only: bool = False,
    ) -> MenuBranch:
        result = menu_group(
            name,
            label,
            icon=icon,
            sequence=sequence,
            dev_only=dev_only,
            menu_module=self.module,
        )
        if parent is not None:
            result["parent"] = _resolve_menu_parent(
                parent, menu_module=self.module
            )
        return MenuBranch(result, menu_module=self.module)

    def item(
        self,
        name: str,
        label: str,
        *,
        href: str | None = None,
        view: str | None = None,
        view_module: str | None = None,
        parent: str | tuple[str, str] | None = None,
        icon: str | None = None,
        sequence: int = 10,
        perm: str | None = None,
        model: str | None = None,
        policy: str | None = None,
        dev_only: bool = False,
    ) -> MenuItem:
        item = MenuItem.make(self.module, name, label).sequence(sequence)
        if href is not None:
            item.href(href)
        if view is not None:
            item.view(view, module=view_module)
        if parent is not None:
            item.parent(parent)
        if icon is not None:
            item.icon(icon)
        if perm is not None:
            item.perm(perm)
        if model is not None:
            item.model(model)
        if policy is not None:
            item.policy(policy)
        if dev_only:
            item.dev_only(True)
        return item

ViewsData

Assign to views_data in a module DATA file.

Source code in pyvelm/builders/views_data.py
class ViewsData:
    """Assign to ``views_data`` in a module DATA file."""

    def __init__(self) -> None:
        self._views: list[Any] = []
        self._inherits: list[Any] = []
        self._menus: list[MenuBranch | MenuItem | Menu] = []

    @classmethod
    def make(cls) -> ViewsData:
        return cls()

    def views(self, *views: Any) -> ViewsData:
        self._views.extend(views)
        return self

    def view(self, view: Any) -> ViewsData:
        self._views.append(view)
        return self

    def inherits(self, *inherits: Any) -> ViewsData:
        self._inherits.extend(inherits)
        return self

    def inherit(self, inherit: Any) -> ViewsData:
        self._inherits.append(inherit)
        return self

    def menus(self, *menus: MenuBranch | MenuItem | Menu) -> ViewsData:
        self._menus.extend(menus)
        return self

    def menu(self, menu: MenuBranch | MenuItem | Menu) -> ViewsData:
        self._menus.append(menu)
        return self

    def _view_dicts(self) -> list[dict[str, Any]]:
        out: list[dict[str, Any]] = []
        for view in self._views:
            if hasattr(view, "to_dict"):
                out.append(view.to_dict())
            else:
                out.append(view)
        return out

    def _inherit_dicts(self) -> list[dict[str, Any]]:
        out: list[dict[str, Any]] = []
        for inherit in self._inherits:
            if hasattr(inherit, "to_dict"):
                out.append(inherit.to_dict())
            else:
                out.append(inherit)
        return out

    def to_dict(self) -> dict[str, Any]:
        data: dict[str, Any] = {}
        if self._views:
            data["VIEWS"] = self._view_dicts()
        if self._inherits:
            data["VIEW_INHERITS"] = self._inherit_dicts()
        if self._menus:
            flat: list[Menu] = []
            for entry in self._menus:
                flat.extend(flatten_menus([entry]))
            data["MENUS"] = flat
        return data