textual-wtf 0.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,165 @@
1
+ """Form Composition Example with results display"""
2
+
3
+ from textual.app import App, ComposeResult
4
+ from textual.containers import Container
5
+ from textual.screen import Screen
6
+ from textual.widgets import Static
7
+ from textual_wtf import Form, StringField
8
+ from textual_wtf.demo.results_screen import ResultsDisplayScreen
9
+
10
+ # Define reusable forms
11
+ class AddressForm(Form):
12
+ """Reusable address form"""
13
+
14
+ street = StringField(label="Street Address", required=True)
15
+ city = StringField(label="City", required=True)
16
+ state = StringField(label="State/Province", required=True)
17
+ postal_code = StringField(label="Postal Code", required=True)
18
+
19
+
20
+ class PersonalInfoForm(Form):
21
+ """Personal information form"""
22
+
23
+ first_name = StringField(label="First Name", required=True)
24
+ last_name = StringField(label="Last Name", required=True)
25
+ email = StringField(label="Email", required=True)
26
+ phone = StringField(label="Phone")
27
+
28
+
29
+ # Compose forms together
30
+ class OrderForm(Form):
31
+ """Order form composed from reusable forms"""
32
+
33
+ # Personal information (no prefix - fields added directly)
34
+ personal = PersonalInfoForm.compose()
35
+
36
+ # Billing address (prefixed - fields added with prefix)
37
+ billing = AddressForm.compose(prefix="billing")
38
+
39
+ # Additional fields
40
+ notes = StringField(label="Order Notes")
41
+
42
+
43
+ class ResultsScreen(ResultsDisplayScreen):
44
+ """Utility Screen to display form results with SQL-style field lookup demo"""
45
+
46
+ def compose(self) -> ComposeResult:
47
+ with Container(id="results-container"):
48
+ yield Static(self.result_title, id="results-title")
49
+
50
+ if self.form:
51
+ lookup_lines = [
52
+ "Field Attribute Access Examples:",
53
+ f" form.street: {self.form.street.label}:",
54
+ f" [{self.form.street.value}]",
55
+ f" form.billing_street: {self.form.billing_street.label}:",
56
+ f" [{self.form.billing_street.value}]",
57
+ f" form.email: {self.form.email.label}",
58
+ f" [{self.form.email.value}]" "",
59
+ "Even though a composed form has a prefix, its fields can be accessed",
60
+ "without the prefix so long as they are unique.",
61
+ ]
62
+ yield Static("\n".join(lookup_lines), id="field-lookup")
63
+
64
+ yield from self.show_data()
65
+ yield from self.buttons()
66
+
67
+
68
+ class ShopScreen(Screen):
69
+ """Example application using composed forms"""
70
+
71
+ CSS = """
72
+ Screen {
73
+ align: center middle;
74
+ }
75
+
76
+ RenderedForm {
77
+ width: 80;
78
+ height: 100%;
79
+ max-height: 100%;
80
+ border: heavy $accent;
81
+ }
82
+ """
83
+
84
+ def compose(self):
85
+ """Compose the application"""
86
+ initial_data = {
87
+ # Personal info fields (no prefix)
88
+ 'first_name': 'John',
89
+ 'last_name': 'Doe',
90
+ 'email': 'john@example.com',
91
+ 'phone': '555-1234',
92
+ # Billing address (prefixed)
93
+ 'billing_street': '123 Main St',
94
+ 'billing_city': 'Springfield',
95
+ 'billing_state': 'IL',
96
+ 'billing_postal_code': '62701',
97
+ # Shipping address (prefixed)
98
+ 'shipping_street': '456 Oak Ave',
99
+ 'shipping_city': 'Chicago',
100
+ 'shipping_state': 'IL',
101
+ 'shipping_postal_code': '60601',
102
+ # Additional fields
103
+ 'same_as_billing': False,
104
+ 'notes': 'Please ring doorbell',
105
+ }
106
+
107
+ self.form = OrderForm(
108
+ title="Order Form - Composed from Reusable Forms", data=initial_data
109
+ )
110
+ yield self.form.render()
111
+
112
+ def on_form_submitted(self, event):
113
+ """Handle form submission"""
114
+ data = event.form.get_data()
115
+
116
+
117
+ def check_reset(cont):
118
+ if cont:
119
+ self.reset_form()
120
+ else:
121
+ self.dismiss(cont)
122
+
123
+ # Pass form for field lookup demo
124
+ self.app.push_screen(
125
+ ResultsScreen("Order Submitted!", data, event.form.form), check_reset
126
+ )
127
+
128
+ def on_form_cancelled(self, event):
129
+ """Handle form cancellation"""
130
+
131
+ def check_reset(cont):
132
+ if cont:
133
+ self.reset_form()
134
+ else:
135
+ self.dismiss(cont)
136
+
137
+ self.app.push_screen(ResultsScreen("Order Cancelled", None), check_reset)
138
+
139
+ def reset_form(self):
140
+ """Clear form and create fresh one"""
141
+ old_form = self.query_one("RenderedForm")
142
+ old_form.remove()
143
+
144
+ self.form = OrderForm(title="Order Form - Composed from Reusable Forms")
145
+ self.mount(self.form.render())
146
+
147
+
148
+ def ShopApp(App):
149
+
150
+ def on_mount(self):
151
+ self.app.push_screen(ShopScreen(), callback=self.exit_app)
152
+
153
+ def exit_app(self, result=None) -> None:
154
+ """Called when BasicFormScreen is dismissed."""
155
+ self.exit(result)
156
+
157
+
158
+ def main():
159
+ """Run the example"""
160
+ app = ShopApp()
161
+ app.run()
162
+
163
+
164
+ if __name__ == "__main__":
165
+ main()
@@ -0,0 +1,170 @@
1
+ """Form Composition Example with results display"""
2
+ from textual.app import App, ComposeResult
3
+ from textual.containers import Container
4
+ from textual.screen import Screen
5
+ from textual.widgets import Static
6
+ from textual_wtf import Form, StringField, BooleanField
7
+ from textual_wtf.demo.results_screen import ResultsDisplayScreen
8
+
9
+
10
+ # Define reusable forms
11
+ class AddressForm(Form):
12
+ """Reusable address form"""
13
+ street = StringField(label="Street Address", required=True)
14
+ city = StringField(label="City", required=True)
15
+ state = StringField(label="State/Province", required=True)
16
+ postal_code = StringField(label="Postal Code", required=True)
17
+
18
+
19
+ class PersonalInfoForm(Form):
20
+ """Personal information form"""
21
+ first_name = StringField(label="First Name", required=True)
22
+ last_name = StringField(label="Last Name", required=True)
23
+ email = StringField(label="Email", required=True)
24
+ phone = StringField(label="Phone")
25
+
26
+
27
+ # Compose forms together
28
+ class OrderForm(Form):
29
+ """Order form composed from reusable forms"""
30
+
31
+ # Personal information (no prefix - fields added directly)
32
+ personal = PersonalInfoForm.compose()
33
+
34
+ # Billing address (prefixed)
35
+ billing = AddressForm.compose(prefix='billing')
36
+
37
+ # Shipping address (prefixed)
38
+ shipping = AddressForm.compose(prefix='shipping')
39
+
40
+ # Additional fields
41
+ same_as_billing = BooleanField(label="Shipping same as billing")
42
+ notes = StringField(label="Order Notes")
43
+
44
+
45
+ class ResultsScreen(ResultsDisplayScreen):
46
+ """Utility Screen to display form results with SQL-style field lookup demo"""
47
+
48
+ def compose(self) -> ComposeResult:
49
+ with Container(id="results-container"):
50
+ yield Static(self.result_title, id="results-title")
51
+
52
+ yield from self.show_data()
53
+
54
+ # Demonstrate attribute-style field access
55
+ if self.form:
56
+ lookup_lines = [
57
+ "Field Attribute Access Examples:",
58
+ " form.billing_street: "
59
+ + self.form.billing_street.label,
60
+ f" [{self.form.billing_street.value}]",
61
+ " form.email: "
62
+ + self.form.email.label
63
+ + " (unqualified)",
64
+ f" [{self.form.email.value}]",
65
+ "",
66
+ "Note: In this case form.street would raise AmbiguousFieldError",
67
+ " because both billing_street and shipping_street exist",
68
+ ]
69
+ yield Static("\n".join(lookup_lines), id="field-lookup")
70
+
71
+ yield from self.buttons()
72
+
73
+
74
+ class ShopScreen(Screen):
75
+ """Example application using composed forms"""
76
+
77
+ CSS = """
78
+ Screen {
79
+ align: center middle;
80
+ }
81
+
82
+ RenderedForm {
83
+ width: 80;
84
+ height: 100%;
85
+ max-height: 100%;
86
+ border: heavy $accent;
87
+ }
88
+ """
89
+
90
+ def compose(self):
91
+ """Compose the application"""
92
+ initial_data = {
93
+ # Personal info fields (no prefix)
94
+ 'first_name': 'John',
95
+ 'last_name': 'Doe',
96
+ 'email': 'john@example.com',
97
+ 'phone': '555-1234',
98
+
99
+ # Billing address (prefixed)
100
+ 'billing_street': '123 Main St',
101
+ 'billing_city': 'Springfield',
102
+ 'billing_state': 'IL',
103
+ 'billing_postal_code': '62701',
104
+
105
+ # Shipping address (prefixed)
106
+ 'shipping_street': '456 Oak Ave',
107
+ 'shipping_city': 'Chicago',
108
+ 'shipping_state': 'IL',
109
+ 'shipping_postal_code': '60601',
110
+
111
+ # Additional fields
112
+ 'same_as_billing': False,
113
+ 'notes': 'Please ring doorbell'
114
+ }
115
+
116
+ self.form = OrderForm(
117
+ title="Order Form - Composed from Reusable Forms",
118
+ data=initial_data
119
+ )
120
+ yield self.form.render()
121
+
122
+ def on_form_submitted(self, event):
123
+ """Handle form submission"""
124
+ data = event.form.get_data()
125
+
126
+ def check_reset(cont):
127
+ if cont:
128
+ self.reset_form()
129
+ else:
130
+ self.dismiss(cont)
131
+
132
+ # Pass form for field lookup demo
133
+ self.app.push_screen(
134
+ ResultsScreen("Order Submitted!", data, event.form.form), check_reset
135
+ )
136
+
137
+ def on_form_cancelled(self, event):
138
+ """Handle form cancellation"""
139
+
140
+ def check_reset(cont):
141
+ if cont:
142
+ self.reset_form()
143
+ else:
144
+ self.dismiss(cont)
145
+
146
+ self.app.push_screen(ResultsScreen("Order Cancelled", None), check_reset)
147
+
148
+ def reset_form(self):
149
+ """Clear form and create fresh one"""
150
+ old_form = self.query_one("RenderedForm")
151
+ old_form.remove()
152
+
153
+ self.form = OrderForm(title="Order Form - Composed from Reusable Forms")
154
+ self.mount(self.form.render())
155
+
156
+
157
+ class ShopApp(App):
158
+
159
+ def compose(self):
160
+ yield ShopScreen()
161
+
162
+
163
+ def main():
164
+ """Run the example"""
165
+ app = ShopApp()
166
+ app.run()
167
+
168
+
169
+ if __name__ == "__main__":
170
+ main()
@@ -0,0 +1,88 @@
1
+ from textual.app import ComposeResult, on
2
+ from textual.containers import Center
3
+ from textual.screen import Screen
4
+ from textual.widgets import Button, Static
5
+
6
+ class ResultsDisplayScreen(Screen):
7
+ """
8
+ Screen to display form results.
9
+
10
+ Subclasses must add a compose() method to implement the visual rendering.
11
+ """
12
+
13
+ CSS = """
14
+ ResultsScreen {
15
+ align: center middle;
16
+ }
17
+
18
+ #results-container {
19
+ width: 80%;
20
+ height: auto;
21
+ border: heavy green;
22
+ padding: 1;
23
+ }
24
+
25
+ #results-title {
26
+ background: green;
27
+ color: white;
28
+ width: 100%;
29
+ height: 1;
30
+ content-align: center middle;
31
+ margin-bottom: 1;
32
+ }
33
+
34
+ #results-data {
35
+ background: $panel;
36
+ padding: 1;
37
+ margin: 1 0;
38
+ }
39
+
40
+ #field-lookup {
41
+ background: $panel;
42
+ padding: 1;
43
+ margin: 1 0;
44
+ border: solid blue;
45
+ }
46
+
47
+ #buttons {
48
+ align: center middle;
49
+ margin-top: 1;
50
+ }
51
+
52
+ Button {
53
+ margin: 0 1;
54
+ }
55
+ """
56
+
57
+ def __init__(self, title: str, data: dict = None, form=None):
58
+ super().__init__()
59
+ self.result_title = title
60
+ self.data = data
61
+ self.form = form
62
+
63
+ def compose(self) -> ComposeResult:
64
+ raise NotImplementedError("ResultsScreen subclasses must define a compose() method")
65
+
66
+ @on(Button.Pressed, "#new")
67
+ def new_pressed(self, event: Button.Pressed):
68
+ self.dismiss(True)
69
+
70
+ @on(Button.Pressed, "#exit")
71
+ def exit_pressed(self, event: Button.Pressed):
72
+ self.dismiss(False)
73
+
74
+ def show_data(self):
75
+
76
+ if self.data:
77
+ # Format data nicely
78
+ lines = []
79
+ for key, value in self.data.items():
80
+ lines.append(f"{key}: {value}")
81
+ yield Static("\n".join(lines), id="results-data")
82
+
83
+ def buttons(self) -> ComposeResult:
84
+ with Center(id="buttons"):
85
+ yield Button("New Form", variant="primary", id="new")
86
+ yield Button("Exit", id="exit")
87
+
88
+
@@ -0,0 +1,134 @@
1
+ """User registration form example with results display"""
2
+ from textual.app import App, ComposeResult
3
+ from textual.containers import Container, VerticalScroll
4
+ from textual.screen import Screen
5
+ from textual.widgets import Static
6
+ from textual_wtf import Form, StringField, BooleanField
7
+ from textual_wtf.validators import EmailValidator
8
+ from textual_wtf.demo.results_screen import ResultsDisplayScreen
9
+
10
+ class RegistrationForm(Form):
11
+ """User registration form"""
12
+ username = StringField(
13
+ label="Username",
14
+ required=True,
15
+ help_text="Choose a unique username"
16
+ )
17
+ email = StringField(
18
+ label="Email Address",
19
+ required=True,
20
+ validators=[EmailValidator()]
21
+ )
22
+ full_name = StringField(
23
+ label="Full Name",
24
+ required=True
25
+ )
26
+ agree_terms = BooleanField(
27
+ label="I agree to the terms and conditions",
28
+ )
29
+
30
+
31
+ class ResultsScreen(ResultsDisplayScreen):
32
+ """Utility Screen to display form results"""
33
+
34
+ def compose(self) -> ComposeResult:
35
+ with Container(id="results-container"):
36
+ yield Static(self.result_title, id="results-title")
37
+
38
+ yield from self.show_data()
39
+ yield from self.buttons()
40
+
41
+
42
+ class RegistrationScreen(Screen):
43
+ """User registration application"""
44
+
45
+ TITLE = "User Registration"
46
+ BINDINGS = [
47
+ ("q", "quit", "Quit"),
48
+ ]
49
+
50
+ CSS = """
51
+ Container {
52
+ align: center middle;
53
+ }
54
+
55
+ #info {
56
+ width: 60;
57
+ height: auto;
58
+ margin: 1 0;
59
+ padding: 1;
60
+ border: solid blue;
61
+ }
62
+
63
+ RenderedForm {
64
+ width: 60;
65
+ height: auto;
66
+ }
67
+ """
68
+
69
+ def compose(self) -> ComposeResult:
70
+ with Container():
71
+ with VerticalScroll():
72
+ yield Static(
73
+ "Welcome! Please fill out the registration form below.",
74
+ id="info"
75
+ )
76
+ self.form = RegistrationForm(title="Create Account")
77
+ yield self.form.render()
78
+
79
+ def on_form_submitted(self, event: Form.Submitted):
80
+ """Handle successful registration"""
81
+ data = event.form.get_data()
82
+
83
+ if not data['agree_terms']:
84
+ self.notify(
85
+ "You must agree to the terms and conditions",
86
+ severity="error"
87
+ )
88
+ return
89
+
90
+ def check_reset(cont):
91
+ if cont:
92
+ self.reset_form()
93
+ else:
94
+ self.dismiss(cont)
95
+
96
+ self.app.push_screen(
97
+ ResultsScreen("Registration Successful!", data), check_reset
98
+ )
99
+
100
+ def on_form_cancelled(self, event: Form.Cancelled):
101
+ """Handle registration cancellation"""
102
+
103
+ def check_reset(cont):
104
+ if cont:
105
+ self.reset_form()
106
+ else:
107
+ self.dismiss(cont)
108
+
109
+ self.app.push_screen(ResultsScreen("Registration Cancelled", None), check_reset)
110
+
111
+ def reset_form(self):
112
+ """Clear form and create fresh one"""
113
+ old_form = self.query_one("RenderedForm")
114
+ old_form.remove()
115
+ self.form = RegistrationForm(title="Create Account")
116
+ self.query_one("VerticalScroll").mount(self.form.render())
117
+
118
+
119
+ class RegistrationApp(App):
120
+
121
+ def on_mount(self):
122
+ self.app.push_screen(RegistrationScreen(), callback=self.exit_app)
123
+
124
+ def exit_app(self, result=None) -> None:
125
+ """Called when Screen is dismissed."""
126
+ self.exit(result)
127
+
128
+
129
+ def main():
130
+ RegistrationApp().run()
131
+
132
+ if __name__ == "__main__":
133
+ main()
134
+
@@ -0,0 +1,25 @@
1
+ """Exceptions for textual-wtf"""
2
+
3
+
4
+ class ValidationError(Exception):
5
+ """Raised when field validation fails"""
6
+
7
+ def __init__(self, message: str, code: str = None):
8
+ self.message = message
9
+ self.code = code
10
+ super().__init__(self.message)
11
+
12
+
13
+ class FieldError(Exception):
14
+ """Raised when there's an error with field configuration"""
15
+ pass
16
+
17
+
18
+ class FormError(Exception):
19
+ """Raised when there's an error with form configuration"""
20
+ pass
21
+
22
+
23
+ class AmbiguousFieldError(FieldError):
24
+ """Raised when an unqualified field name matches multiple fields"""
25
+ pass