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.
- textual_wtf/__init__.py +67 -0
- textual_wtf/bound_fields.py +245 -0
- textual_wtf/demo/__init__.py +0 -0
- textual_wtf/demo/advanced_form.py +139 -0
- textual_wtf/demo/basic_form.py +92 -0
- textual_wtf/demo/basic_form.tcss +7 -0
- textual_wtf/demo/launcher.py +124 -0
- textual_wtf/demo/nested_once_form.py +165 -0
- textual_wtf/demo/nested_twice_form.py +170 -0
- textual_wtf/demo/results_screen.py +88 -0
- textual_wtf/demo/user_registration.py +134 -0
- textual_wtf/exceptions.py +25 -0
- textual_wtf/fields.py +264 -0
- textual_wtf/forms.py +433 -0
- textual_wtf/py.typed +0 -0
- textual_wtf/validators.py +40 -0
- textual_wtf/version.py +1 -0
- textual_wtf/widgets.py +165 -0
- textual_wtf-0.8.0.dist-info/METADATA +223 -0
- textual_wtf-0.8.0.dist-info/RECORD +23 -0
- textual_wtf-0.8.0.dist-info/WHEEL +4 -0
- textual_wtf-0.8.0.dist-info/entry_points.txt +2 -0
- textual_wtf-0.8.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Validators for form fields"""
|
|
2
|
+
import re
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
from textual.validation import Validator, ValidationResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
SIMPLE_EMAIL_PAT = re.compile(r".+\@.+\..+")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class EvenInteger(Validator):
|
|
12
|
+
"""Example custom validator - validates even integers"""
|
|
13
|
+
|
|
14
|
+
def validate(self, value: str) -> ValidationResult:
|
|
15
|
+
try:
|
|
16
|
+
int_value = int(value)
|
|
17
|
+
except ValueError:
|
|
18
|
+
return self.success()
|
|
19
|
+
|
|
20
|
+
if int_value % 2 != 0:
|
|
21
|
+
return self.failure("Must be an even number")
|
|
22
|
+
return self.success()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Palindromic(Validator):
|
|
26
|
+
"""Example custom validator - validates palindromes"""
|
|
27
|
+
|
|
28
|
+
def validate(self, value: str) -> ValidationResult:
|
|
29
|
+
if value == value[::-1]:
|
|
30
|
+
return self.success()
|
|
31
|
+
return self.failure("Must be a palindrome")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class EmailValidator(Validator):
|
|
35
|
+
"""Simple email validator - rejects addresses like 'user@name' without TLD"""
|
|
36
|
+
|
|
37
|
+
def validate(self, value: str) -> ValidationResult:
|
|
38
|
+
if not value or not SIMPLE_EMAIL_PAT.match(value):
|
|
39
|
+
return self.failure("Must be a valid email address")
|
|
40
|
+
return self.success()
|
textual_wtf/version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.8.0"
|
textual_wtf/widgets.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Form widgets with validation support"""
|
|
2
|
+
from typing import List, Optional, TYPE_CHECKING, Tuple
|
|
3
|
+
from textual.widgets import Input, Checkbox, Select, TextArea, Static
|
|
4
|
+
from textual.containers import Center
|
|
5
|
+
from textual.validation import ValidationResult, Validator
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .fields import Field
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def widget_id_gen():
|
|
12
|
+
"""Generate unique widget IDs"""
|
|
13
|
+
count = 0
|
|
14
|
+
while True:
|
|
15
|
+
count += 1
|
|
16
|
+
yield f"field-widget-{count}"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
_id_gen = widget_id_gen()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FormWidgetMixin:
|
|
23
|
+
"""Mixin to add validation error display to widgets"""
|
|
24
|
+
|
|
25
|
+
async def on_input_changed(self, event):
|
|
26
|
+
"""Handle input changes and display validation errors"""
|
|
27
|
+
if not hasattr(self, 'parent') or self.parent is None:
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
container = self.parent
|
|
31
|
+
|
|
32
|
+
# ALWAYS clear previous errors first
|
|
33
|
+
await container.remove_children(".form-error")
|
|
34
|
+
|
|
35
|
+
if hasattr(event, 'validation_result') and event.validation_result is not None:
|
|
36
|
+
vr = event.validation_result
|
|
37
|
+
if not vr.is_valid:
|
|
38
|
+
# Show all errors from this validation
|
|
39
|
+
for msg in vr.failure_descriptions:
|
|
40
|
+
container.mount(Center(Static(msg), classes="form-error"))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class FormInput(Input, FormWidgetMixin):
|
|
44
|
+
"""Text input widget for forms"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, *, field: Optional['Field'] = None, valid_empty: bool = True,
|
|
47
|
+
validators: Optional[List[Validator]] = None, **kwargs):
|
|
48
|
+
kwargs.setdefault('id', next(_id_gen))
|
|
49
|
+
kwargs['validators'] = validators or []
|
|
50
|
+
super().__init__(**kwargs)
|
|
51
|
+
self.field = field
|
|
52
|
+
self.valid_empty = valid_empty
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class FormIntegerInput(Input, FormWidgetMixin):
|
|
56
|
+
"""Integer input widget for forms"""
|
|
57
|
+
|
|
58
|
+
def __init__(self, *, field: Optional['Field'] = None, valid_empty: bool = True,
|
|
59
|
+
validators: Optional[List[Validator]] = None, **kwargs):
|
|
60
|
+
kwargs.setdefault('id', next(_id_gen))
|
|
61
|
+
kwargs['type'] = 'integer'
|
|
62
|
+
kwargs['validators'] = validators or []
|
|
63
|
+
super().__init__(**kwargs)
|
|
64
|
+
self.field = field
|
|
65
|
+
self.valid_empty = valid_empty
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class FormTextArea(TextArea, FormWidgetMixin):
|
|
69
|
+
"""Multi-line text area widget for forms"""
|
|
70
|
+
|
|
71
|
+
def __init__(self, *, field: Optional['Field'] = None, text: str = "", **kwargs):
|
|
72
|
+
kwargs.setdefault('id', next(_id_gen))
|
|
73
|
+
super().__init__(text=text, **kwargs)
|
|
74
|
+
self.field = field
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def value(self):
|
|
78
|
+
"""Get text area value"""
|
|
79
|
+
return self.text
|
|
80
|
+
|
|
81
|
+
@value.setter
|
|
82
|
+
def value(self, v):
|
|
83
|
+
"""Set text area value"""
|
|
84
|
+
self.text = v if v is not None else ""
|
|
85
|
+
|
|
86
|
+
def validate(self, value):
|
|
87
|
+
"""Validate text area value"""
|
|
88
|
+
return ValidationResult()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class FormCheckbox(Checkbox):
|
|
92
|
+
"""Checkbox widget for forms"""
|
|
93
|
+
|
|
94
|
+
def __init__(self, *, field: Optional['Field'] = None, label: str = "", **kwargs):
|
|
95
|
+
kwargs.setdefault('id', next(_id_gen))
|
|
96
|
+
super().__init__(value=False, **kwargs)
|
|
97
|
+
self.field = field
|
|
98
|
+
if label:
|
|
99
|
+
self.label = label
|
|
100
|
+
|
|
101
|
+
def validate(self, value):
|
|
102
|
+
"""Validate checkbox value"""
|
|
103
|
+
return ValidationResult()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class AlwaysValid(Validator):
|
|
107
|
+
"""Validator that always passes"""
|
|
108
|
+
|
|
109
|
+
def validate(self, value):
|
|
110
|
+
return self.success()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class FormSelect(Select):
|
|
114
|
+
"""Select dropdown widget for forms"""
|
|
115
|
+
|
|
116
|
+
def __init__(self, *, field: Optional['Field'] = None,
|
|
117
|
+
choices: List[Tuple[str, str]], allow_blank: bool = False,
|
|
118
|
+
prompt: str = "Select an option", **kwargs):
|
|
119
|
+
kwargs.setdefault('id', next(_id_gen))
|
|
120
|
+
kwargs.setdefault('prompt', prompt)
|
|
121
|
+
|
|
122
|
+
# Convert tuples to Select.Option objects
|
|
123
|
+
options = [(label, value) for value, label in choices]
|
|
124
|
+
|
|
125
|
+
super().__init__(options=options, **kwargs)
|
|
126
|
+
self.field = field
|
|
127
|
+
self.allow_blank = allow_blank
|
|
128
|
+
|
|
129
|
+
def validate(self, value):
|
|
130
|
+
"""Validate select value"""
|
|
131
|
+
if value != Select.BLANK or self.allow_blank:
|
|
132
|
+
return AlwaysValid().success()
|
|
133
|
+
return AlwaysValid().failure("Please select a value")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class WidgetRegistry:
|
|
137
|
+
"""Registry for custom widgets"""
|
|
138
|
+
|
|
139
|
+
_widgets = {}
|
|
140
|
+
|
|
141
|
+
@classmethod
|
|
142
|
+
def register(cls, name: str):
|
|
143
|
+
"""Decorator to register a widget"""
|
|
144
|
+
def decorator(widget_class):
|
|
145
|
+
cls._widgets[name] = widget_class
|
|
146
|
+
return widget_class
|
|
147
|
+
return decorator
|
|
148
|
+
|
|
149
|
+
@classmethod
|
|
150
|
+
def get(cls, name: str):
|
|
151
|
+
"""Get a widget by name"""
|
|
152
|
+
return cls._widgets.get(name)
|
|
153
|
+
|
|
154
|
+
@classmethod
|
|
155
|
+
def list_widgets(cls):
|
|
156
|
+
"""List all registered widgets"""
|
|
157
|
+
return list(cls._widgets.keys())
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# Register built-in widgets
|
|
161
|
+
WidgetRegistry.register("input")(FormInput)
|
|
162
|
+
WidgetRegistry.register("integer_input")(FormIntegerInput)
|
|
163
|
+
WidgetRegistry.register("textarea")(FormTextArea)
|
|
164
|
+
WidgetRegistry.register("checkbox")(FormCheckbox)
|
|
165
|
+
WidgetRegistry.register("select")(FormSelect)
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: textual-wtf
|
|
3
|
+
Version: 0.8.0
|
|
4
|
+
Summary: A declarative forms library for Textual TUI applications
|
|
5
|
+
Author-email: Steve Holden <steve@holdenweb.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: forms,terminal,textual,tui,validation
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Requires-Python: <4.0,>=3.10
|
|
17
|
+
Requires-Dist: textual>=0.47.0
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# Textual Forms
|
|
21
|
+
|
|
22
|
+
A declarative, extensible forms library for [Textual](https://textual.textualize.io/) TUI applications.
|
|
23
|
+
|
|
24
|
+
## Features/Goals
|
|
25
|
+
|
|
26
|
+
- **Declarative Syntax**: Define forms using class-based field declarations
|
|
27
|
+
- **Flexible Widget Assignment**: Use different widgets for the same field type
|
|
28
|
+
- **Easy to Extend**: Add custom field types and widgets with clear patterns
|
|
29
|
+
- **Built-in Validation**: Uses standard textual validators
|
|
30
|
+
- **Simple Integration**: Drop forms into existing Textual apps with minimal code
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
## Warnings
|
|
34
|
+
|
|
35
|
+
This package is in a beta stage. No details should be regarded as final,
|
|
36
|
+
particularly naming details, but existing functionality won't be broken
|
|
37
|
+
without a good reason. Nevertheless we believe it is sufficiently close to
|
|
38
|
+
its final form to be rigorously exercised.
|
|
39
|
+
|
|
40
|
+
Form layout is at present embedded in the Form.render method, which returns a
|
|
41
|
+
RenderedForm widget making it difficult to provide flexibility in
|
|
42
|
+
representation. This mechanism has extremely low "textuality," and will be
|
|
43
|
+
replaced in release 0.9.
|
|
44
|
+
|
|
45
|
+
LLMs have been used in the development of this package, which is
|
|
46
|
+
subjected to serious scrutiny by a seasoned professional programmer, but this
|
|
47
|
+
is no guarantee of quality or correctness. _caveat usor_.
|
|
48
|
+
|
|
49
|
+
Some of the current tests are of dubious value, and may ultimately be removed.
|
|
50
|
+
|
|
51
|
+
## Running the Examples
|
|
52
|
+
|
|
53
|
+
If you have `uv` installed you can run the examples from the repository using the command
|
|
54
|
+
|
|
55
|
+
`uvx --from git+https://github.com/holdenweb/textual-wtf.git textual-wtf`
|
|
56
|
+
|
|
57
|
+
You will see a menu of demonstration examples, each of which is also a standalone program in the src/textual_wtf/demo directory.
|
|
58
|
+
as shown below.
|
|
59
|
+
|
|
60
|
+
| Menu item | Program name | Description |
|
|
61
|
+
|:-----------------------------|:-----------------------|:---------------------------------------------|
|
|
62
|
+
| 1. Basic Form | `basic_form.py` | A simple form with basic fields |
|
|
63
|
+
| 2. Advanced Form | `advanced_form.py` | A more complex form with some validations |
|
|
64
|
+
| 3. User Registration | `user_registration.py` | Rendered form integrated with other components |
|
|
65
|
+
| 4. Composition Example | `nested_once_form.py` | Simple nested form demonstration |
|
|
66
|
+
| 5. Multiply Included Example | `nested_twice_form.py` | Demonstrating form component re-use |
|
|
67
|
+
|
|
68
|
+
In a clone of the repository the command `uv run examples/<name>` should suffice.
|
|
69
|
+
|
|
70
|
+
`poetry` users should find that they can create a virtual environment and run
|
|
71
|
+
`poetry run textual-wtf` to start the demo. You can also build a distribution
|
|
72
|
+
(wheel and sdist) of the project with `poetry build` and install that
|
|
73
|
+
wherever required. No further testing in other development environments has
|
|
74
|
+
so far been reported.
|
|
75
|
+
|
|
76
|
+
## Installation
|
|
77
|
+
|
|
78
|
+
The package will shortly be released to PyPI at version 0.8.
|
|
79
|
+
Until then, install from this repository as follows.
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
python -m pip install textual_wtf@git+https://github.com/holdenweb/textual-wtf.git
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`uv` users can use
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
uv add textual_wtf@git+https://github.com/holdenweb/textual-wtf.git
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Quick Start
|
|
92
|
+
|
|
93
|
+
The most basic forms contain one or more fields. When a form is submitted a
|
|
94
|
+
`Form.Submitted` event is posted, whose `form` attribute allows access to
|
|
95
|
+
values using field names. Here's a simple example.
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from textual_wtf import Form, StringField, IntegerField, BooleanField
|
|
99
|
+
|
|
100
|
+
class UserForm(Form):
|
|
101
|
+
"""Simple user registration form"""
|
|
102
|
+
name = StringField(label="Name", required=True)
|
|
103
|
+
age = IntegerField(label="Age", min_value=0, max_value=130)
|
|
104
|
+
active = BooleanField(label="Active User")
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
With a little styling this is how the rendered form appears after it's been filled out.
|
|
108
|
+
|
|
109
|
+

|
|
110
|
+
|
|
111
|
+
Fields are named from the class variable to which they are assigned (in the
|
|
112
|
+
example above, "name", "age" and "active"). With the content shown the
|
|
113
|
+
form's `get_data` method will return `{'name': 'Steve Holden', 'age': 178;
|
|
114
|
+
'active': True}`. Note that the integer field has been converted to an
|
|
115
|
+
`int`, and the Boolean field to a `bool`.
|
|
116
|
+
|
|
117
|
+
Forms are themselves reusable components so a form can contain sub-forms, as
|
|
118
|
+
demonstrated in the `nested_once_form.py` example, to as many levels as
|
|
119
|
+
required.
|
|
120
|
+
|
|
121
|
+
A form can optionally be given a prefix, which is prepended to the names of
|
|
122
|
+
its fields with a "_" suffix. This lets you include multiple instances of the
|
|
123
|
+
same sub-form, as the `nested_twice_form.py` example demonstrates.
|
|
124
|
+
|
|
125
|
+
In the event of two fields receiving the same (fully-qualified) name an
|
|
126
|
+
exception is raised when the form is created.
|
|
127
|
+
|
|
128
|
+
When a form is rendered "Cancel" and "Submit" buttons are added at the
|
|
129
|
+
bottom of the form. When clicked these buttons raise `Form.Cancelled`
|
|
130
|
+
and `Form.Submitted` events respectively; each has a `form` attribute
|
|
131
|
+
which can be used to access the form.
|
|
132
|
+
This behaviour will be more configurable in later releases.
|
|
133
|
+
|
|
134
|
+
## Form Data
|
|
135
|
+
|
|
136
|
+
Once rendered, the simplest way to access the form's data is
|
|
137
|
+
by calling its `get_data` method.
|
|
138
|
+
This returns a dictionary where fields' values are keyed by their
|
|
139
|
+
fully-qualified names (i.e. including any sub-form prefixes,
|
|
140
|
+
with underscores separating the named components).
|
|
141
|
+
|
|
142
|
+
Users may find that in dealing with complex nested forms it
|
|
143
|
+
becomes tedious to use fully-qualified names.
|
|
144
|
+
An experimental `get_field` method takes a string argument. If that string is
|
|
145
|
+
the fully-qualified name of a field, or if there is only one field whose
|
|
146
|
+
fully-qualified name ends an underscore followed by the string, then it
|
|
147
|
+
returns a field object whose attributes include `name` and `value`.
|
|
148
|
+
|
|
149
|
+
This access mechanism is perhaps the most fluid part of the current design,
|
|
150
|
+
and discussions (feel free to raise a Github issue to start a discussion -
|
|
151
|
+
we'll move it into Discussions if it looks like developing) are encouraged.
|
|
152
|
+
For example, would it more usable to implement a read/write property with
|
|
153
|
+
dotted access? Should alternatives be offered?
|
|
154
|
+
|
|
155
|
+
## Current Field Types
|
|
156
|
+
|
|
157
|
+
- `StringField` - Text input (single line)
|
|
158
|
+
- `TextField` - Text input (multi-line)
|
|
159
|
+
- `IntegerField` - Integer input with validation
|
|
160
|
+
- `BooleanField` - Checkbox
|
|
161
|
+
- `ChoiceField` - Select dropdown
|
|
162
|
+
|
|
163
|
+
Documentation of the foundational classes (Forms, Fields and Widgets) is the
|
|
164
|
+
biggest current technical debt. The `examples` directory
|
|
165
|
+
contains some code that we hope will help you to evalute textual-wtf
|
|
166
|
+
and shape its direction with your feedback.
|
|
167
|
+
|
|
168
|
+
Additional example programs, particularly those demonstrating the
|
|
169
|
+
styling possibilities, would be especially valuable.
|
|
170
|
+
|
|
171
|
+
## Development
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
# Create a venv with dev dependencies
|
|
175
|
+
uv venv
|
|
176
|
+
|
|
177
|
+
# Run tests
|
|
178
|
+
uv run pytest
|
|
179
|
+
|
|
180
|
+
Run specific test
|
|
181
|
+
uv run pytest tests/test_fields.py
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Architecture
|
|
185
|
+
|
|
186
|
+
The library uses a three-layer architecture:
|
|
187
|
+
|
|
188
|
+
1. **Fields** - Handle data conversion and validation logic
|
|
189
|
+
2. **Widgets** - Handle UI rendering and user interaction
|
|
190
|
+
3. **Forms** - Coordinate fields and widgets into complete forms
|
|
191
|
+
|
|
192
|
+
No detailed architecture documentation is presently available.
|
|
193
|
+
|
|
194
|
+
## License
|
|
195
|
+
|
|
196
|
+
MIT License - see LICENSE file for details.
|
|
197
|
+
|
|
198
|
+
## Choice Field Format
|
|
199
|
+
|
|
200
|
+
When using `ChoiceField`, provide choices as a list of `(value, label)` tuples:
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
country = ChoiceField(
|
|
204
|
+
label="Country",
|
|
205
|
+
choices=[
|
|
206
|
+
("us", "United States"), # value, label
|
|
207
|
+
("uk", "United Kingdom"),
|
|
208
|
+
("ca", "Canada"),
|
|
209
|
+
]
|
|
210
|
+
)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
- The **value** (first element) is what gets stored in the form data
|
|
214
|
+
- The **label** (second element) is what the user sees in the dropdown
|
|
215
|
+
|
|
216
|
+
When the form is submitted, `form.get_data()['country']` will contain the
|
|
217
|
+
value (e.g., `"us"`), not the label.
|
|
218
|
+
|
|
219
|
+
## Older versions
|
|
220
|
+
|
|
221
|
+
The prehistory of the project is preserved in the `prototype` branch,
|
|
222
|
+
a somewhat chaotic mishmash of code that nevertheless proved the basic ideas
|
|
223
|
+
embodied in the current design to be usable in practice.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
textual_wtf/__init__.py,sha256=lo3oT72caUN0tiVmkVLY5JXGEisUwxA_xYPhgfdGzkQ,1345
|
|
2
|
+
textual_wtf/bound_fields.py,sha256=Jk5o81dRlvuGivR_XdtjOo7_SE-J8HuEVdrJrZhimG8,8570
|
|
3
|
+
textual_wtf/exceptions.py,sha256=GYef5rI0ygqXVHvBO5lrV_Ltu78FPkNvAyRSnFXe2vY,594
|
|
4
|
+
textual_wtf/fields.py,sha256=gODdtGJySuBycCeiUKvvU2k1nLRAdPMvALsrqKz1oSA,8782
|
|
5
|
+
textual_wtf/forms.py,sha256=tAXvf2p7OD2unBMbBA_uaOr2_6jzuMRzjVgfT_WlOco,14335
|
|
6
|
+
textual_wtf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
textual_wtf/validators.py,sha256=635-JKuC9N0GAvy7No-mID_HtcvNvhKDBBI6FW4W0sQ,1192
|
|
8
|
+
textual_wtf/version.py,sha256=iPlYCcIzuzW7T2HKDkmYlMkRI51dBLfNRxPPiWrfw9U,22
|
|
9
|
+
textual_wtf/widgets.py,sha256=d5a0DiTJtdlQsQkeTvhEsHLDYwqEVn_zC6v8DzyN3NI,5130
|
|
10
|
+
textual_wtf/demo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
textual_wtf/demo/advanced_form.py,sha256=sK9aRE8fp3OtDiWf9c3qpIMKz4pIjCpy5SE5J6o7t0g,3604
|
|
12
|
+
textual_wtf/demo/basic_form.py,sha256=x9RiosmkzDC0mYPdEMiGlRFqX0O9C5dJVyw1XdXdcpo,2655
|
|
13
|
+
textual_wtf/demo/basic_form.tcss,sha256=bRKdPZyIC1mLjfkmQiwcc3JwzcSDWI3EeLs42JeneSw,71
|
|
14
|
+
textual_wtf/demo/launcher.py,sha256=41DOguB7Dism9zav3NTBMJhtvkmi3bpfeocM3MvoPdk,3306
|
|
15
|
+
textual_wtf/demo/nested_once_form.py,sha256=Ta_86TDYgwg8QOjVyVgaRMKjPHAC4b64AR9ymURLR3M,5036
|
|
16
|
+
textual_wtf/demo/nested_twice_form.py,sha256=JDSDaO03p4p8tl562pNTnXXPN3Q5fDTqOvZ3dZ1L0SY,5063
|
|
17
|
+
textual_wtf/demo/results_screen.py,sha256=3DsL-WRXA9x9AdpT6r3HD3-snlEk9kzy_XwB7O4FsqU,2035
|
|
18
|
+
textual_wtf/demo/user_registration.py,sha256=LxyetdJmVs3G0ZoUr8PdGmFo-GEVPleCxdZZfEVAnJw,3540
|
|
19
|
+
textual_wtf-0.8.0.dist-info/METADATA,sha256=-IQ6VDcCKUpsGHo4mTE-ahcHmGBQ-UcftPzuVUSi-bE,8578
|
|
20
|
+
textual_wtf-0.8.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
21
|
+
textual_wtf-0.8.0.dist-info/entry_points.txt,sha256=jaJ4-T5M_61CTqJGCnRwAv6T7fVhWDzBaLKtTnoBbSM,63
|
|
22
|
+
textual_wtf-0.8.0.dist-info/licenses/LICENSE,sha256=OD-OyOXFRBQ9lM4Z-XGnP_WUfVObajlPYdVVP3c_7OM,1089
|
|
23
|
+
textual_wtf-0.8.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Steve Holden steve@holdenweb.com
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|