Skip to content

Latest commit

 

History

History
601 lines (435 loc) · 17.6 KB

File metadata and controls

601 lines (435 loc) · 17.6 KB

textual-wtf — Python API Reference

Status: Design specification. Describes the target API. Still under development.


Contents

  1. Field
  2. Field subclasses
  3. BoundField
  4. Form
  5. FormLayout
  6. Validators
  7. Exceptions
  8. Constants and type aliases

0. Introduction

This document describes the design of a forms suystem for Textual. The user begins by declaring a Form subclass, in which Field and embedded Form instances are assigned to class variables. The FormMetaclass extracts these Field definitions, saving them in the fields class variable.

When a Form instance is created, each of the fields is bound to the new instance as a BoundField. Thus the Field holds the configuration data for a field, which is the same for all instances of that Form, while the BoundField holds the field data for a specific Form instance.

1. Field

class Field

Immutable declarative configuration for a single form field. Defined at class level on a Form subclass and shared across all instances of that form. Never holds runtime state.

Constructor

Field(
    label: str,
    *,
    initial: Any = None,
    required: bool = False,
    disabled: bool = False,
    validators: list[Validator | Callable] = (),
    help_text: str = "",
    label_style: LabelStyle = "above",
    help_style: HelpStyle = "below",
    widget_class: type | None = None,
    **widget_kwargs: Any,
)
Parameter Description
label Human-readable label shown in the UI.
label_style How the label is presented. One of "above", "beside", "placeholder". See Constants.
help_style How help text is presented. One of "below", "tooltip".
widget_class The Textual widget class to instantiate for this field. If None, the subclass default is used.
**b_field_kwargs Additional keyword arguments forwarded to the BoundField constructor. Those not used by the BoundField are later merged with (and overridden by) any kwargs passed to BoundField.__call__() at render time.

Methods

bind

def bind(
    self,
    form: BaseForm,
    name: str,
    initial: Any = None,
) -> BoundField

Returns a BoundField for this Field within a specific form instance. Called by BaseForm.__init__() and collected in the BoundField's field dictionary under the field's name; not normally called directly.


2. Field subclasses

All subclasses accept every parameter of Field in addition to their own. Each is a thin wrapper around the appropriate Textual widget to provide a more uniform interface.

StringField

class StringField(Field)

Single-line text. Default widget_class: FormInput.

No additional parameters.


IntegerField

class IntegerField(Field)

Integer numeric input. Default widget_class: FormInput with type="integer" (blocks non-numeric keystrokes at the widget level).

Extra parameter Description
min_value: int | None = None Minimum acceptable value. If set, a MinValue validator is added automatically.
max_value: int | None = None Maximum acceptable value. If set, a MaxValue validator is added automatically.

BooleanField

class BooleanField(Field)

Boolean toggle. Default widget_class: FormCheckbox.

No additional parameters.


ChoiceField

class ChoiceField(Field)

Selection from a fixed list. Default widget_class: FormSelect.

Extra parameter Description
choices: list[tuple[str, Any]] Required. List of (display_label, value) pairs.

TextField

class TextField(Field)

Multi-line text. Default widget_class: FormTextArea.

No additional parameters.


3. BoundField

class BoundField(Container)

Mutable runtime state for one field within one form instance. Also a Textual Container widget, so it can be configured for horizontal or vertical orientations: when mounted, it composes its own label, inner input widget, optional help text, and error display.

Created by Field.bind() during BaseForm.__init__(), which stores them in the Form instances' bound_fields dictionary . Not instantiated directly.

Constructor

BoundField(
    field: Field,
    form: BaseForm,
    name: str,
    data: dict[str, Any]
)

Not part of the public API; called only by Field.bind().

Reactive attributes

These are Textual reactive attributes. Watchers update the DOM automatically.

Attribute Type Description
value Any Current Python value. Setting it updates the inner widget; the inner widget's watch_value watcher keeps this in sync when the user types.
has_error bool True when the field has at least one validation error.
error_messages list[str] Error messages accumulated during validation. Displayed in the error label inside the widget tree.

Properties (some delegated to Field, read-only)

Property Type Description
label str Human-readable label.
default Any Default value from the Field declaration.
required bool Whether the field is required.
help_text str Help guidance string.
label_style LabelStyle How the label is presented; overridable per-call via __call__.
help_style HelpStyle How help text is presented; overridable per-call via __call__.
validators list Field-level validators from the Field declaration.
field Field The underlying shared Field configuration object.
form BaseForm The owning form instance.
name str The field name (its key in form.bound_fields).

Mutable instance attributes

Attribute Type Description
disabled bool Independently mutable per instance. Initialised from field.disabled. Setting this updates the inner widget's disabled state.
errors list[str] Current validation error messages. Populated by validate(); cleared before each validation pass.
is_dirty bool True once the user has interacted with the field (changed its value from initial).

Convert a Python value to the representation expected by the widget (typically a string).

__call__

def __call__(
    self,
    *,
    label_style: LabelStyle | None = None,
    help_style: HelpStyle | None = None,
    disabled: bool | None = None,
    validators: list | None = None,
    **widget_kwargs: Any,
) -> BoundField

Configure this BoundField for rendering and return a fully-configured widget. Used when composing the Form to yield the field into the widget tree.

Any keyword argument supplied here takes precedence over the corresponding Field declaration. widget_kwargs are merged with (and override) BoundField.widget_kwargs.

Raises FormError if this field has already been yielded in the current layout (duplicate-render protection).

# Typical usage in compose_form():
yield self.form.name()                          # all defaults
yield self.form.age(disabled=True)              # disable for this render
yield self.form.role(label_style="beside")      # override label style
yield self.form.notes(help_style="tooltip")     # override help style

Returns the BoundField's widget for inclusion in its FieldLayout object.

Convert a Python value to the representation expected by the widget (typically a string).

validate

def validate(self) -> bool

Calls widget.validate(), returning True if the field validates. Called automatically change or blur via the on_change and on_blur watchers; also called for every field by BaseForm.validate() on submission.

compose (Textual override)

def compose(self) -> ComposeResult

Yields a FieldLayout object that contains the BoundField's widget subtree from the current label_style and help_style:

  • "above": Label stacked above the inner widget.
  • "beside": Label and inner widget in a Horizontal.
  • "placeholder": no Label widget; field.label passed as placeholder to the inner widget.

Help text: visible Static below the widget ("below") or assigned to widget.tooltip ("tooltip").

Error display: a Label with reactive binding to error_messages, hidden when has_error is False.

Not normally overridden; control presentation via label_style and help_style instead.


4. Form

class BaseForm
class Form(BaseForm)  # public alias

Declarative base class for form definitions. FormMetaclass processes the class body at definition time.

Class-level attributes (set on subclasses)

Attribute Type Default Description
layout_class type[FormLayout] | None None (→ DefaultFormLayout) The FormLayout subclass used by render() when no layout_class is passed to the constructor.
label_style LabelStyle "above" Default label_style for all fields in this form, unless overridden per-field in the Field declaration or per-render in BoundField.__call__().
help_style HelpStyle "below" Default help_style for all fields.

Constructor

Form(
    data: dict[str, Any] | None = None,
    *,
    layout_class: type[FormLayout] | None = None,
    label_style: str | None = None,
)
Parameter Description
data Optional initial data dict {field_name: value}. Values are applied as each BoundField's initial value.
layout_class Overrides Form.layout_class for this instance only.
label_style Overrides Form.label_style for this instance only.

Field access

form.fieldname          # returns BoundField; raises AttributeError if unknown
form.get_field(name)    # equivalent; kept for backward compatibility
form.bound_fields       # dict[str, BoundField], ordered by declaration
form.fields             # alias for bound_fields

For embedded forms, unqualified names (e.g. form.street) resolve when unambiguous across all prefixed fields; raise AmbiguousFieldError otherwise. Qualified names (e.g. form.billing_street) always resolve directly.

Methods

clean

def clean(self, raw_value: Any) -> Any

Full form-level cleaning pipeline: run validate to validate each field. Raise ValidationError after validation is complete if one or more fields fail to validate. If validate returns True, follow that by form-specific checks which can examine all the fields' data. Called only on submission.

build_layout

Instantiate and return the FormLayout for this Form instance. Uses self._layout_class (resolved from constructor arg → class attr → DefaultFormLayout). The returned FormLayout is ready to mount in a Textual compose().

validate / is_valid

def validate(self) -> bool
def is_valid(self) -> bool   # alias

Call bound_field.validate() for every field. Return True only if all fields are valid, otherwise False. Populates BoundField.errors for any failing fields.

get_data

def get_data(self) -> dict[str, Any]

Return {name: bound_field.value} for all fields.

set_data

def set_data(self, data: dict[str, Any]) -> None

Set bound_field.value for each key present in data. Keys not present in the form are ignored.

Class method

embed

@classmethod
def embed(
    cls,
    prefix: str,
    title: str = "",
) -> EmbeddedForm

Return an EmbeddedForm marker for use inside another form class body. FormMetaclass expands it in place, prefixing all field names with f"{prefix}_". Name collisions with existing fields raise FormError at class-definition time.

class OrderForm(Form):
    billing  = AddressForm.embed(prefix="billing")
    shipping = AddressForm.embed(prefix="shipping")
    notes    = TextField(label="Notes")

Messages (Textual)

Form.Submitted

@dataclass
class Form.Submitted(Message):
    layout: FormLayout    # the FormLayout that posted the message
    form: BaseForm        # convenience alias: layout.form

Posted when the user submits the form (Submit button or Enter key).

Form.Cancelled

@dataclass
class Form.Cancelled(Message):
    layout: FormLayout
    form: BaseForm

Posted when the user cancels the form (Cancel button or Escape key).


5. FormLayout

class FormLayout(VerticalScroll)

Base class for form renderers. Subclass and override compose_form() to create custom layouts. The base class handles button events, keyboard shortcuts, and duplicate-field protection.

Constructor

FormLayout(
    form: BaseForm,
    id: str | None = None,
    **kwargs: Any,
)

**kwargs are forwarded to VerticalScroll.

Override in subclasses

compose

def compose(self) -> ComposeResult

Define the form's visual structure. Yield BoundField widgets via the callable interface, plus any other Textual widgets (buttons, labels, containers) needed for the layout.

class TwoColumnLayout(FormLayout):
    def compose_form(self):
        with Horizontal():
            yield self.form.first_name(label_style="above")
            yield self.form.last_name(label_style="above")
        yield self.form.email()
        yield self.form.notes(help_style="tooltip")
        with Horizontal(id="buttons"):
            yield Button("Submit", id="submit", variant="primary")
            yield Button("Cancel", id="cancel")

Each BoundField may only be yielded once per layout; a second yield raises FormError.

Attributes

Attribute Type Description
form BaseForm The form instance passed to the constructor.

Button and keyboard handling (base class behaviour)

The base class responds to:

  • Button press with id="submit" → posts Form.Submitted (calls form.validate() first; only posts if valid).
  • Button press with id="cancel" → posts Form.Cancelled.
  • Key("enter") → triggers submit.
  • Key("escape") → triggers cancel.

DefaultFormLayout also adds a form title (if Form has a title attribute) and the Submit/Cancel buttons automatically, so compose() in DefaultFormLayout subclasses need not yield buttons explicitly.

DefaultFormLayout

class DefaultFormLayout(FormLayout)

Renders all fields in declaration order with label_style and help_style taken from the form's class-level defaults. Adds a title bar and Submit/Cancel buttons. Equivalent to the old RenderedForm behaviour.

Used automatically by Form.render() when no layout_class is specified.

Not normally subclassed directly; for custom layouts, subclass FormLayout.


6. Validators

class Validator(ABC)

Validators are subclasses of textual.validation.Validator, which thereby inherit Validator's success() and failure() methods. Alternatively, any callable with signature (value: Any) -> None that raises ValidationError on failure may be used directly.

Validator.validate

@abstractmethod
def validate(self, value: Any) -> None

Return self.success() when validation succeeds, otherwise return self.failure(message) where message explains the problem..

Built-in validators

Class Parameters Raises if…
Required value is None, "", or empty sequence
MinLength(n) n: int len(value) < n
MaxLength(n) n: int len(value) > n
MinValue(n) n: int | float value < n
MaxValue(n) n: int | float value > n
EmailValidator value does not match a valid email pattern

7. Exceptions

Exception Raised when
ValidationError(message) A validator or Field.clean() rejects a value. Carries message: str.
FieldError(message) Field configuration is invalid (e.g. unknown parameter, bad choices format).
FormError(message) Form definition or rendering error: name collision in composition, duplicate field render, or invalid layout_class.
AmbiguousFieldError(name, candidates) Unqualified attribute access (form.street) matches more than one field across composed sub-forms. Carries name: str and candidates: list[str].

8. Constants and type aliases

LabelStyle = Literal["above", "beside", "placeholder"]
HelpStyle  = Literal["below", "tooltip"]
LabelStyle Visual effect
"above" Label widget rendered above the input. Default.
"beside" Label widget rendered to the left of the input in a Horizontal.
"placeholder" No Label widget; field.label used as the input's placeholder text.
HelpStyle Visual effect
"below" Static help text always visible below the input. Default.
"tooltip" Help text assigned to widget.tooltip; shown on hover.

Appendix: label_style × help_style combinations

help_style="below" help_style="tooltip"
label_style="above" Label above, help text below Label above, help on hover
label_style="beside" Label left, help text below Label left, help on hover
label_style="placeholder" Placeholder label, help text below Placeholder label, help on hover