textual-wtf 0.8.1__tar.gz → 0.8.2__tar.gz

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.
Files changed (50) hide show
  1. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/PKG-INFO +1 -1
  2. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/pyproject.toml +1 -1
  3. textual_wtf-0.8.2/src/textual_wtf/layouts.py +212 -0
  4. textual_wtf-0.8.2/src/textual_wtf/version.py +1 -0
  5. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/src/textual_wtf/widgets.py +0 -2
  6. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/uv.lock +1 -1
  7. textual_wtf-0.8.1/src/textual_wtf/version.py +0 -1
  8. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/.coveragerc +0 -0
  9. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/.github/workflows/pypi.yaml +0 -0
  10. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/.gitignore +0 -0
  11. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/BOUNDFIELD_REFACTORING_SUMMARY.md +0 -0
  12. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/CHANGELOG-v0.9-dev1.md +0 -0
  13. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/CHANGELOG.md +0 -0
  14. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/IMPLEMENTATION_SUMMARY.md +0 -0
  15. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/INSTALLATION.md +0 -0
  16. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/LICENSE +0 -0
  17. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/Makefile +0 -0
  18. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/PACKAGE_SUMMARY.txt +0 -0
  19. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/QUICKSTART.md +0 -0
  20. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/README.md +0 -0
  21. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/REFACTORING_SUMMARY.md +0 -0
  22. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/docs/TESTING_GUIDE.md +0 -0
  23. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/examples/custom_layouts_demo.py +0 -0
  24. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/examples/test_new_features.py +0 -0
  25. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/src/textual_wtf/__init__.py +0 -0
  26. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/src/textual_wtf/bound_fields.py +0 -0
  27. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/src/textual_wtf/demo/__init__.py +0 -0
  28. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/src/textual_wtf/demo/advanced_form.py +0 -0
  29. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/src/textual_wtf/demo/basic_form.py +0 -0
  30. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/src/textual_wtf/demo/basic_form.tcss +0 -0
  31. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/src/textual_wtf/demo/launcher.py +0 -0
  32. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/src/textual_wtf/demo/nested_once_form.py +0 -0
  33. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/src/textual_wtf/demo/nested_twice_form.py +0 -0
  34. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/src/textual_wtf/demo/results_screen.py +0 -0
  35. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/src/textual_wtf/demo/user_registration.py +0 -0
  36. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/src/textual_wtf/exceptions.py +0 -0
  37. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/src/textual_wtf/fields.py +0 -0
  38. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/src/textual_wtf/forms.py +0 -0
  39. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/src/textual_wtf/py.typed +0 -0
  40. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/src/textual_wtf/validators.py +0 -0
  41. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/tests/__init__.py +0 -0
  42. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/tests/conftest.py +0 -0
  43. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/tests/test_composition.py +0 -0
  44. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/tests/test_exceptions.py +0 -0
  45. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/tests/test_fields.py +0 -0
  46. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/tests/test_forms.py +0 -0
  47. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/tests/test_integration.py +0 -0
  48. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/tests/test_user_input.py +0 -0
  49. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/tests/test_validators.py +0 -0
  50. {textual_wtf-0.8.1 → textual_wtf-0.8.2}/tests/test_widgets.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: textual-wtf
3
- Version: 0.8.1
3
+ Version: 0.8.2
4
4
  Summary: A declarative forms library for Textual TUI applications
5
5
  Author-email: Steve Holden <steve@holdenweb.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "textual-wtf"
3
- version = "0.8.1"
3
+ version = "0.8.2"
4
4
  description = "A declarative forms library for Textual TUI applications"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10, <4.0"
@@ -0,0 +1,212 @@
1
+ """Form layout classes for rendering forms"""
2
+ from typing import Optional, Set, TYPE_CHECKING
3
+ from textual import on
4
+ from textual.app import ComposeResult
5
+ from textual.containers import Vertical, Center, Horizontal, VerticalScroll
6
+ from textual.widgets import Button, Static, Label
7
+ from textual.message import Message
8
+
9
+ from .exceptions import FormError
10
+
11
+ if TYPE_CHECKING:
12
+ from .forms import Form
13
+
14
+
15
+ class FormLayout(VerticalScroll):
16
+ """
17
+ Base class for form layouts
18
+
19
+ Handles the visual presentation of a form. Subclass this to create
20
+ custom form layouts with different visual arrangements of fields.
21
+
22
+ The layout tracks which fields have been rendered to prevent duplicate
23
+ rendering (which would create multiple widgets for the same field).
24
+ """
25
+
26
+ DEFAULT_CSS = """
27
+ FormLayout {
28
+ keyline: thin green;
29
+ }
30
+
31
+ Vertical {
32
+ margin: 1;
33
+ }
34
+
35
+ #form-title {
36
+ background: blue;
37
+ height: auto;
38
+ margin: 1;
39
+ }
40
+
41
+ .subform-title {
42
+ background: white;
43
+ color: black;
44
+ height: auto;
45
+ padding: 0 1;
46
+ margin: 1 0 0 0;
47
+ }
48
+
49
+ .form-field {
50
+ height: auto;
51
+ }
52
+
53
+ .form-error {
54
+ color: red;
55
+ width: 1fr;
56
+ }
57
+
58
+ #buttons {
59
+ height: auto;
60
+ align: center middle;
61
+ margin: 0;
62
+ }
63
+
64
+ #outer-buttons {
65
+ height: auto;
66
+ }
67
+
68
+ Input {
69
+ height: auto;
70
+ }
71
+
72
+ TextArea {
73
+ height: 6;
74
+ }
75
+ """
76
+
77
+ def __init__(self, form: 'Form', id: Optional[str] = None, **kwargs):
78
+ """
79
+ Initialize form layout
80
+
81
+ Args:
82
+ form: The Form instance to render
83
+ id: Optional widget ID
84
+ **kwargs: Additional kwargs for VerticalScroll
85
+ """
86
+ super().__init__(id=id, **kwargs)
87
+ self.form = form
88
+ self._rendered_fields: Set[str] = set()
89
+
90
+ def _track_field_render(self, field_name: str) -> None:
91
+ """
92
+ Track that a field has been rendered
93
+
94
+ Raises FormError if field already rendered (prevents duplicate widgets).
95
+
96
+ Args:
97
+ field_name: Name of the field being rendered
98
+ """
99
+ if field_name in self._rendered_fields:
100
+ raise FormError(
101
+ f"Field '{field_name}' has already been rendered. "
102
+ f"Each field can only be rendered once per form."
103
+ )
104
+ self._rendered_fields.add(field_name)
105
+
106
+ def compose(self):
107
+ """
108
+ Compose the layout
109
+
110
+ Calls compose_form() to build the form UI. Subclasses should
111
+ override compose_form(), not this method.
112
+ """
113
+ yield from self.compose_form()
114
+
115
+ def compose_form(self):
116
+ """
117
+ Compose the form UI
118
+
119
+ Override this method in subclasses to create custom layouts.
120
+ Use form.fieldname() to render each field.
121
+
122
+ Example:
123
+ def compose_form(self):
124
+ yield Label("My Custom Form")
125
+ yield self.form.name()
126
+ yield self.form.email()
127
+ yield Button("Submit", id="submit")
128
+ """
129
+ raise NotImplementedError("FormLayout subclasses must implement compose_form()")
130
+
131
+ def get_data(self):
132
+ """Get current form data"""
133
+ return self.form.get_data()
134
+
135
+ def set_data(self, data):
136
+ """Set form data"""
137
+ return self.form.set_data(data)
138
+
139
+ async def validate(self):
140
+ """Validate all form fields"""
141
+ return await self.form.validate()
142
+
143
+ @on(Button.Pressed, "#submit")
144
+ async def submit_pressed(self, event: Button.Pressed) -> None:
145
+ """Handle submit button press"""
146
+ # Import here to avoid circular import
147
+ from .forms import Form
148
+
149
+ if await self.validate():
150
+ self.post_message(Form.Submitted(self))
151
+ else:
152
+ self.app.notify("Please fix the errors before submitting", severity="error")
153
+
154
+ @on(Button.Pressed, "#cancel")
155
+ async def cancel_pressed(self, event: Button.Pressed) -> None:
156
+ """Handle cancel button press"""
157
+ # Import here to avoid circular import
158
+ from .forms import Form
159
+
160
+ self.post_message(Form.Cancelled(self))
161
+
162
+ class DefaultFormLayout(FormLayout):
163
+ """
164
+ Default form layout
165
+
166
+ Renders forms in the traditional vertical style:
167
+ - Optional title
168
+ - Subform titles for composed forms
169
+ - Each field with label and widget
170
+ - Submit/Cancel buttons
171
+ """
172
+
173
+ def compose_form(self) -> ComposeResult:
174
+ """Compose the form in default vertical layout"""
175
+ # Optional title
176
+ if self.form.title is not None:
177
+ yield Vertical(
178
+ Center(Static(f"---- {self.form.title} ----")),
179
+ id="form-title"
180
+ )
181
+
182
+ # Track which subforms we've already rendered headers for
183
+ rendered_subforms = set()
184
+
185
+ # Render each field
186
+ for name, field in self.form.fields.items():
187
+ # Check if this field is part of a composed subform with a title
188
+ metadata = self.form._composition_metadata.get(name)
189
+ if metadata and metadata.get('title'):
190
+ subform_id = metadata['composed_from']
191
+
192
+ # Render subform title once per subform
193
+ if subform_id not in rendered_subforms:
194
+ yield Static(metadata['title'], classes="subform-title")
195
+ rendered_subforms.add(subform_id)
196
+
197
+ with Vertical(classes="form-field"):
198
+ if field.label:
199
+ yield Label(field.label)
200
+ # Call the field to get its widget
201
+ yield field()
202
+
203
+ # Submit/Cancel buttons
204
+ yield Vertical(
205
+ Horizontal(
206
+ Button("Cancel", id="cancel"),
207
+ Button("Submit", id="submit", variant="primary"),
208
+ id="buttons"
209
+ ),
210
+ id="outer-buttons"
211
+ )
212
+
@@ -0,0 +1 @@
1
+ __version__ = "0.8.2"
@@ -4,8 +4,6 @@ from textual.widgets import Input, Checkbox, Select, TextArea, Static
4
4
  from textual.containers import Center
5
5
  from textual.validation import ValidationResult, Validator
6
6
 
7
- import wingdbstub
8
-
9
7
  if TYPE_CHECKING:
10
8
  from .fields import Field
11
9
 
@@ -1168,7 +1168,7 @@ wheels = [
1168
1168
 
1169
1169
  [[package]]
1170
1170
  name = "textual-wtf"
1171
- version = "0.8.1"
1171
+ version = "0.8.2"
1172
1172
  source = { editable = "." }
1173
1173
  dependencies = [
1174
1174
  { name = "textual" },
@@ -1 +0,0 @@
1
- __version__ = "0.8.1"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes