textual-wtf 0.8.0__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 (52) hide show
  1. textual_wtf-0.8.2/.github/workflows/pypi.yaml +76 -0
  2. textual_wtf-0.8.2/CHANGELOG-v0.9-dev1.md +188 -0
  3. textual_wtf-0.8.2/CHANGELOG.md +139 -0
  4. textual_wtf-0.8.2/IMPLEMENTATION_SUMMARY.md +327 -0
  5. {textual_wtf-0.8.0 → textual_wtf-0.8.2}/PKG-INFO +1 -1
  6. textual_wtf-0.8.2/examples/custom_layouts_demo.py +195 -0
  7. textual_wtf-0.8.2/examples/test_new_features.py +342 -0
  8. {textual_wtf-0.8.0 → textual_wtf-0.8.2}/pyproject.toml +14 -1
  9. {textual_wtf-0.8.0 → textual_wtf-0.8.2}/src/textual_wtf/__init__.py +5 -1
  10. {textual_wtf-0.8.0 → textual_wtf-0.8.2}/src/textual_wtf/bound_fields.py +47 -2
  11. {textual_wtf-0.8.0 → textual_wtf-0.8.2}/src/textual_wtf/demo/advanced_form.py +2 -2
  12. {textual_wtf-0.8.0 → textual_wtf-0.8.2}/src/textual_wtf/demo/basic_form.py +1 -1
  13. {textual_wtf-0.8.0 → textual_wtf-0.8.2}/src/textual_wtf/demo/nested_once_form.py +2 -2
  14. {textual_wtf-0.8.0 → textual_wtf-0.8.2}/src/textual_wtf/demo/nested_twice_form.py +2 -2
  15. {textual_wtf-0.8.0 → textual_wtf-0.8.2}/src/textual_wtf/demo/user_registration.py +2 -2
  16. {textual_wtf-0.8.0 → textual_wtf-0.8.2}/src/textual_wtf/fields.py +7 -1
  17. {textual_wtf-0.8.0 → textual_wtf-0.8.2}/src/textual_wtf/forms.py +62 -169
  18. textual_wtf-0.8.2/src/textual_wtf/layouts.py +212 -0
  19. textual_wtf-0.8.2/src/textual_wtf/version.py +1 -0
  20. {textual_wtf-0.8.0 → textual_wtf-0.8.2}/src/textual_wtf/widgets.py +26 -24
  21. {textual_wtf-0.8.0 → textual_wtf-0.8.2}/uv.lock +1 -1
  22. textual_wtf-0.8.0/docs/BOUNDFIELD.md +0 -44
  23. textual_wtf-0.8.0/images/rendered_user_form.png +0 -0
  24. textual_wtf-0.8.0/src/textual_wtf/version.py +0 -1
  25. {textual_wtf-0.8.0 → textual_wtf-0.8.2}/.coveragerc +0 -0
  26. {textual_wtf-0.8.0 → textual_wtf-0.8.2}/.gitignore +0 -0
  27. {textual_wtf-0.8.0 → textual_wtf-0.8.2}/BOUNDFIELD_REFACTORING_SUMMARY.md +0 -0
  28. {textual_wtf-0.8.0 → textual_wtf-0.8.2}/INSTALLATION.md +0 -0
  29. {textual_wtf-0.8.0 → textual_wtf-0.8.2}/LICENSE +0 -0
  30. {textual_wtf-0.8.0 → textual_wtf-0.8.2}/Makefile +0 -0
  31. {textual_wtf-0.8.0 → textual_wtf-0.8.2}/PACKAGE_SUMMARY.txt +0 -0
  32. {textual_wtf-0.8.0 → textual_wtf-0.8.2}/QUICKSTART.md +0 -0
  33. {textual_wtf-0.8.0 → textual_wtf-0.8.2}/README.md +0 -0
  34. {textual_wtf-0.8.0 → textual_wtf-0.8.2}/REFACTORING_SUMMARY.md +0 -0
  35. {textual_wtf-0.8.0 → textual_wtf-0.8.2}/docs/TESTING_GUIDE.md +0 -0
  36. {textual_wtf-0.8.0 → textual_wtf-0.8.2}/src/textual_wtf/demo/__init__.py +0 -0
  37. {textual_wtf-0.8.0 → textual_wtf-0.8.2}/src/textual_wtf/demo/basic_form.tcss +0 -0
  38. {textual_wtf-0.8.0 → textual_wtf-0.8.2}/src/textual_wtf/demo/launcher.py +0 -0
  39. {textual_wtf-0.8.0 → textual_wtf-0.8.2}/src/textual_wtf/demo/results_screen.py +0 -0
  40. {textual_wtf-0.8.0 → textual_wtf-0.8.2}/src/textual_wtf/exceptions.py +0 -0
  41. {textual_wtf-0.8.0 → textual_wtf-0.8.2}/src/textual_wtf/py.typed +0 -0
  42. {textual_wtf-0.8.0 → textual_wtf-0.8.2}/src/textual_wtf/validators.py +0 -0
  43. {textual_wtf-0.8.0 → textual_wtf-0.8.2}/tests/__init__.py +0 -0
  44. {textual_wtf-0.8.0 → textual_wtf-0.8.2}/tests/conftest.py +0 -0
  45. {textual_wtf-0.8.0 → textual_wtf-0.8.2}/tests/test_composition.py +0 -0
  46. {textual_wtf-0.8.0 → textual_wtf-0.8.2}/tests/test_exceptions.py +0 -0
  47. {textual_wtf-0.8.0 → textual_wtf-0.8.2}/tests/test_fields.py +0 -0
  48. {textual_wtf-0.8.0 → textual_wtf-0.8.2}/tests/test_forms.py +0 -0
  49. {textual_wtf-0.8.0 → textual_wtf-0.8.2}/tests/test_integration.py +0 -0
  50. {textual_wtf-0.8.0 → textual_wtf-0.8.2}/tests/test_user_input.py +0 -0
  51. {textual_wtf-0.8.0 → textual_wtf-0.8.2}/tests/test_validators.py +0 -0
  52. {textual_wtf-0.8.0 → textual_wtf-0.8.2}/tests/test_widgets.py +0 -0
@@ -0,0 +1,76 @@
1
+ name: Publish to PyPI and TestPyPI
2
+
3
+ on: push
4
+
5
+ jobs:
6
+
7
+ build:
8
+ name: Build distribution 📦
9
+ runs-on: ubuntu-latest
10
+
11
+ steps:
12
+ - uses: actions/checkout@v6
13
+ with:
14
+ persist-credentials: false
15
+ - name: Set up Python
16
+ uses: actions/setup-python@v6
17
+ with:
18
+ python-version: "3.x"
19
+ - name: Install pypa/build
20
+ run: >-
21
+ python3 -m
22
+ pip install
23
+ build
24
+ --user
25
+ - name: Build a binary wheel and a source tarball
26
+ run: python3 -m build
27
+ - name: Store the distribution packages
28
+ uses: actions/upload-artifact@v5
29
+ with:
30
+ name: python-package-distributions
31
+ path: dist/
32
+
33
+ publish-to-pypi:
34
+ name: >-
35
+ Publish Python 🐍 distribution 📦 to PyPI
36
+ if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes
37
+ needs:
38
+ - build
39
+ runs-on: ubuntu-latest
40
+ environment:
41
+ name: pypi
42
+ url: https://pypi.org/p/textual-wtf # Replace <package-name> with your PyPI project name
43
+ permissions:
44
+ id-token: write # IMPORTANT: mandatory for trusted publishing
45
+ steps:
46
+ - name: Download all the dists
47
+ uses: actions/download-artifact@v6
48
+ with:
49
+ name: python-package-distributions
50
+ path: dist/
51
+ - name: Publish distribution 📦 to PyPI
52
+ uses: pypa/gh-action-pypi-publish@release/v1
53
+
54
+ publish-to-testpypi:
55
+ name: Publish Python 🐍 distribution 📦 to TestPyPI
56
+ needs:
57
+ - build
58
+ runs-on: ubuntu-latest
59
+
60
+ environment:
61
+ name: testpypi
62
+ url: https://test.pypi.org/p/<package-name>
63
+
64
+ permissions:
65
+ id-token: write # IMPORTANT: mandatory for trusted publishing
66
+
67
+ steps:
68
+ - name: Download all the dists
69
+ uses: actions/download-artifact@v6
70
+ with:
71
+ name: python-package-distributions
72
+ path: dist/
73
+ - name: Publish distribution 📦 to TestPyPI
74
+ uses: pypa/gh-action-pypi-publish@release/v1
75
+ with:
76
+ repository-url: https://test.pypi.org/legacy/
@@ -0,0 +1,188 @@
1
+ # textual-wtf v0.9-dev1 - Changelog
2
+
3
+ ## Major Architectural Changes
4
+
5
+ ### 1. Callable BoundFields (WTForms-style API)
6
+
7
+ **File: `bound_fields.py`**
8
+
9
+ - Added `__call__()` method to BoundField class
10
+ - Returns widget configured with keyword arguments
11
+ - Tracks field rendering to prevent duplicates
12
+ - Example: `form.name(placeholder="Enter name", disabled=True)`
13
+
14
+ - Updated `create_widget()` method
15
+ - Now accepts `**kwargs` for runtime configuration
16
+ - Merges runtime kwargs with field's widget_kwargs
17
+ - Syncs initial value to widget on creation
18
+
19
+ **File: `fields.py`**
20
+
21
+ - Updated `Field.create_widget()` method
22
+ - Added optional `widget_kwargs` parameter
23
+ - Merges base configuration with runtime overrides
24
+
25
+ ### 2. FormLayout Architecture
26
+
27
+ **File: `layouts.py` (NEW)**
28
+
29
+ Created new layout system with two classes:
30
+
31
+ **FormLayout (base class)**
32
+ - Inherits from `VerticalScroll`
33
+ - Tracks rendered fields via `_rendered_fields` set
34
+ - Provides `_track_field_render()` to prevent duplicate field rendering
35
+ - Abstract `compose_form()` method for subclasses to implement
36
+ - Delegates to form for data operations (get_data, set_data, validate)
37
+
38
+ **DefaultFormLayout**
39
+ - Implements default vertical form layout
40
+ - Replicates previous RenderedForm behavior
41
+ - Renders: title → subform titles → fields with labels → buttons
42
+ - Handles submit/cancel button events
43
+ - Posts Form.Submitted and Form.Cancelled messages
44
+
45
+ ### 3. Form Class Updates
46
+
47
+ **File: `forms.py`**
48
+
49
+ **Removed:**
50
+ - `RenderedForm` class (replaced by FormLayout system)
51
+ - Unused imports (on, Vertical, Center, Horizontal, VerticalScroll, Button, Static, Label)
52
+
53
+ **Modified BaseForm:**
54
+ - Added `layout_class` class attribute (defaults to DefaultFormLayout)
55
+ - Updated `__init__()`:
56
+ - Added `layout_class` parameter (overrides class-level default)
57
+ - Removed `render_type` parameter
58
+ - Layout selection logic: instance param → class attribute → DefaultFormLayout
59
+
60
+ - Updated `render()` method:
61
+ - Creates layout instance using `self._layout_class`
62
+ - Sets `self._current_layout` for field rendering tracking
63
+ - Returns layout instance (not RenderedForm)
64
+
65
+ **Modified Form:**
66
+ - Updated `Submitted` and `Cancelled` message classes:
67
+ - Accept `layout` parameter instead of `r_form`
68
+ - Store as `self.layout`
69
+ - Provide `self.form` for backward compatibility
70
+
71
+ ### 4. Public API Updates
72
+
73
+ **File: `__init__.py`**
74
+
75
+ Added exports:
76
+ - `FormLayout`
77
+ - `DefaultFormLayout`
78
+
79
+ Fixed typo in `__all__` (was `"__version__,"` now `"__version__"`)
80
+
81
+ ## Benefits of These Changes
82
+
83
+ 1. **Cleaner API**: Fields are now callable, making layout code more intuitive
84
+ ```python
85
+ yield self.form.name(placeholder="Your name")
86
+ ```
87
+
88
+ 2. **Flexible Layouts**: Easy to create custom form layouts by subclassing FormLayout
89
+ ```python
90
+ class TwoColumnLayout(FormLayout):
91
+ def compose_form(self):
92
+ with Horizontal():
93
+ yield self.form.name()
94
+ yield self.form.email()
95
+ ```
96
+
97
+ 3. **Safety**: Prevents duplicate field rendering (1:1 field-to-widget mapping)
98
+ - Raises `FormError` if same field rendered twice
99
+
100
+ 4. **Backward Compatible**: Existing code continues to work with DefaultFormLayout
101
+
102
+ ## Migration Guide
103
+
104
+ ### Before (v0.8)
105
+ ```python
106
+ class MyForm(Form):
107
+ name = StringField(label="Name")
108
+
109
+ form = MyForm()
110
+ rendered = form.render()
111
+ ```
112
+
113
+ ### After (v0.9-dev1)
114
+ ```python
115
+ # Still works exactly the same!
116
+ class MyForm(Form):
117
+ name = StringField(label="Name")
118
+
119
+ form = MyForm()
120
+ rendered = form.render()
121
+
122
+ # But now you can also do custom layouts:
123
+ class MyLayout(FormLayout):
124
+ def compose_form(self):
125
+ yield Label("Custom Layout")
126
+ yield self.form.name(placeholder="Enter name")
127
+ yield Button("Submit", id="submit")
128
+
129
+ form = MyForm(layout_class=MyLayout)
130
+ rendered = form.render()
131
+ ```
132
+
133
+ ## Breaking Changes
134
+
135
+ ⚠️ **Potential Breaking Changes:**
136
+
137
+ 1. **RenderedForm class removed**
138
+ - If code directly instantiated `RenderedForm`, use `DefaultFormLayout` instead
139
+ - Message handlers that checked `isinstance(x, RenderedForm)` need updating
140
+
141
+ 2. **Message structure changed**
142
+ - `Form.Submitted` and `Form.Cancelled` now receive `layout` instead of `r_form`
143
+ - `message.form` now refers to the layout (backward compatible)
144
+ - To access actual form: `message.layout.form`
145
+
146
+ 3. **render_type parameter removed**
147
+ - Use `layout_class` parameter instead
148
+ - Applies to both `BaseForm.__init__()` and subclasses
149
+
150
+ ## Testing Requirements
151
+
152
+ The following areas need test updates:
153
+
154
+ 1. **Test files referencing RenderedForm:**
155
+ - Replace `RenderedForm` imports with `DefaultFormLayout`
156
+ - Update assertions checking widget type
157
+
158
+ 2. **Test message handling:**
159
+ - Update to use `message.layout` instead of `message.form` for layout
160
+ - Or use `message.form` (backward compatible alias)
161
+
162
+ 3. **New tests needed:**
163
+ - BoundField `__call__()` with various kwargs
164
+ - Duplicate field rendering raises FormError
165
+ - Custom layout classes
166
+ - Field rendering tracking
167
+
168
+ ## Files Modified
169
+
170
+ - `src/textual_wtf/bound_fields.py` - Added __call__ method
171
+ - `src/textual_wtf/fields.py` - Updated create_widget()
172
+ - `src/textual_wtf/forms.py` - Removed RenderedForm, updated BaseForm/Form
173
+ - `src/textual_wtf/layouts.py` - NEW FILE
174
+ - `src/textual_wtf/__init__.py` - Updated exports
175
+
176
+ ## Files Needing Updates
177
+
178
+ - All test files that import or reference `RenderedForm`
179
+ - Demo applications in `src/textual_wtf/demo/` (likely need updates)
180
+ - Documentation files
181
+
182
+ ## Next Steps
183
+
184
+ 1. Update test suite to use new architecture
185
+ 2. Update demo applications
186
+ 3. Update documentation
187
+ 4. Add new tests for layout system
188
+ 5. Consider whether to add validation that all fields were rendered
@@ -0,0 +1,139 @@
1
+ # Changelog
2
+
3
+ ## [0.9.0-dev1] - 2025-02-02
4
+
5
+ ### Breaking Changes
6
+
7
+ **Major architectural refactoring: Form/Layout separation**
8
+
9
+ This release introduces a clean separation between form logic (data and validation) and form presentation (layout and rendering). This is a **breaking change** but provides much better flexibility and follows Textual best practices.
10
+
11
+ #### What Changed
12
+
13
+ 1. **RenderedForm removed** - Forms are no longer widgets themselves
14
+ 2. **FormLayout introduced** - New base class for creating form layouts
15
+ 3. **Form.render() now returns FormLayout** - Not a breaking API change, but returns a different type
16
+ 4. **BoundField is now callable** - WTForms-style widget rendering: `field()`
17
+
18
+ #### Migration Guide
19
+
20
+ **Before (v0.8.x):**
21
+ ```python
22
+ form = MyForm(data={'name': 'Alice'})
23
+ app.mount(form.render()) # Returns RenderedForm (a widget)
24
+
25
+ # Message handling
26
+ @on(Form.Submitted)
27
+ def handle_submit(self, event):
28
+ data = event.form.get_data() # event.form was RenderedForm
29
+ ```
30
+
31
+ **After (v0.9.x):**
32
+ ```python
33
+ form = MyForm(data={'name': 'Alice'})
34
+ app.mount(form.render()) # Returns DefaultFormLayout (a widget)
35
+
36
+ # Message handling - slightly different
37
+ @on(Form.Submitted)
38
+ def handle_submit(self, event):
39
+ data = event.form.get_data() # event.form is now the Form instance
40
+ # event.layout is the FormLayout widget
41
+ ```
42
+
43
+ #### New Capabilities
44
+
45
+ **1. Custom Layouts**
46
+
47
+ You can now create custom layouts by subclassing `FormLayout`:
48
+
49
+ ```python
50
+ from textual_wtf import FormLayout
51
+ from textual.containers import Horizontal, Vertical
52
+
53
+ class TwoColumnLayout(FormLayout):
54
+ def compose_form(self):
55
+ with Horizontal():
56
+ with Vertical():
57
+ yield self.render_field('first_name')
58
+ yield self.render_field('email')
59
+ with Vertical():
60
+ yield self.render_field('last_name')
61
+ yield self.render_field('phone')
62
+
63
+ # Render submit buttons
64
+ # ... (see DefaultFormLayout for button example)
65
+
66
+ # Use it
67
+ form = ContactForm()
68
+ app.mount(TwoColumnLayout(form))
69
+ ```
70
+
71
+ **2. Same Form, Different Layouts**
72
+
73
+ Since Form is just data, you can render the same form instance with different layouts:
74
+
75
+ ```python
76
+ form = UserProfileForm()
77
+
78
+ # Desktop view
79
+ desktop_panel.mount(DefaultFormLayout(form))
80
+
81
+ # Mobile view (after unmounting desktop)
82
+ mobile_panel.mount(CompactFormLayout(form))
83
+ ```
84
+
85
+ **3. Callable BoundFields (WTForms-style)**
86
+
87
+ Fields can now be called to get their widgets:
88
+
89
+ ```python
90
+ class CustomLayout(FormLayout):
91
+ def compose_form(self):
92
+ # Call field to get widget
93
+ yield self.form.fields['name']()
94
+
95
+ # Or with configuration
96
+ yield self.form.fields['email'](placeholder="Enter email")
97
+ ```
98
+
99
+ **4. Duplicate Rendering Prevention**
100
+
101
+ The layout system prevents accidentally rendering the same field twice:
102
+
103
+ ```python
104
+ class BadLayout(FormLayout):
105
+ def compose_form(self):
106
+ yield self.render_field('name')
107
+ yield self.render_field('name') # Raises DuplicateFieldRenderError
108
+ ```
109
+
110
+ ### Added
111
+
112
+ - `FormLayout` - Base class for custom form layouts
113
+ - `DefaultFormLayout` - Default vertical layout (replicates old RenderedForm behavior)
114
+ - `DuplicateFieldRenderError` - Exception raised when field rendered twice
115
+ - `BoundField.__call__()` - Make fields callable to get widgets
116
+ - `Field.create_widget(override_kwargs)` - Accept optional kwargs for widget configuration
117
+
118
+ ### Changed
119
+
120
+ - `Form` is no longer a widget - use `form.render()` to get a FormLayout widget
121
+ - `Form.Submitted` message now has `.layout` (FormLayout) and `.form` (Form) attributes
122
+ - `Form.Cancelled` message now has `.layout` (FormLayout) and `.form` (Form) attributes
123
+ - Validation logic moved from Form to FormLayout
124
+
125
+ ### Removed
126
+
127
+ - `RenderedForm` class - replaced by `DefaultFormLayout`
128
+ - `BaseForm.render_type` parameter - use `form.render(layout_class=...)` instead
129
+ - `BaseForm.validate()` method - now on FormLayout
130
+
131
+ ### Internal
132
+
133
+ - Better separation of concerns: Form = business logic, Layout = presentation
134
+ - Layouts are composable Textual widgets
135
+ - Forms are pure Python objects (no widget inheritance)
136
+
137
+ ## [0.8.0] - Previous Release
138
+
139
+ (See previous changelog entries...)