nicegui-autoform 0.1.0__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.
- nicegui_autoform-0.1.0/.github/workflows/release.yml +53 -0
- nicegui_autoform-0.1.0/.github/workflows/tests.yml +43 -0
- nicegui_autoform-0.1.0/.gitignore +9 -0
- nicegui_autoform-0.1.0/LICENSE +21 -0
- nicegui_autoform-0.1.0/PKG-INFO +206 -0
- nicegui_autoform-0.1.0/README.md +176 -0
- nicegui_autoform-0.1.0/examples/argparse_demo.py +51 -0
- nicegui_autoform-0.1.0/examples/click_demo.py +48 -0
- nicegui_autoform-0.1.0/examples/cyclopts_demo.py +88 -0
- nicegui_autoform-0.1.0/examples/data/README.md +13 -0
- nicegui_autoform-0.1.0/examples/data/measurements.csv +13 -0
- nicegui_autoform-0.1.0/examples/data/notes.txt +4 -0
- nicegui_autoform-0.1.0/examples/typer_demo.py +58 -0
- nicegui_autoform-0.1.0/pyproject.toml +69 -0
- nicegui_autoform-0.1.0/src/nicegui_autoform/__init__.py +48 -0
- nicegui_autoform-0.1.0/src/nicegui_autoform/_compat.py +51 -0
- nicegui_autoform-0.1.0/src/nicegui_autoform/_introspect.py +99 -0
- nicegui_autoform-0.1.0/src/nicegui_autoform/adapters/__init__.py +60 -0
- nicegui_autoform-0.1.0/src/nicegui_autoform/adapters/_argparse.py +133 -0
- nicegui_autoform-0.1.0/src/nicegui_autoform/adapters/_click.py +215 -0
- nicegui_autoform-0.1.0/src/nicegui_autoform/adapters/_cyclopts.py +158 -0
- nicegui_autoform-0.1.0/src/nicegui_autoform/adapters/_typer.py +28 -0
- nicegui_autoform-0.1.0/src/nicegui_autoform/adapters/plain.py +150 -0
- nicegui_autoform-0.1.0/src/nicegui_autoform/form.py +251 -0
- nicegui_autoform-0.1.0/src/nicegui_autoform/spec.py +192 -0
- nicegui_autoform-0.1.0/src/nicegui_autoform/validate.py +79 -0
- nicegui_autoform-0.1.0/src/nicegui_autoform/values.py +103 -0
- nicegui_autoform-0.1.0/src/nicegui_autoform/widgets.py +242 -0
- nicegui_autoform-0.1.0/tests/conftest.py +9 -0
- nicegui_autoform-0.1.0/tests/fixtures_cli.py +156 -0
- nicegui_autoform-0.1.0/tests/test_adapter_argparse.py +130 -0
- nicegui_autoform-0.1.0/tests/test_adapter_click.py +196 -0
- nicegui_autoform-0.1.0/tests/test_adapter_cyclopts.py +178 -0
- nicegui_autoform-0.1.0/tests/test_adapter_plain.py +132 -0
- nicegui_autoform-0.1.0/tests/test_adapter_typer.py +115 -0
- nicegui_autoform-0.1.0/tests/test_examples.py +66 -0
- nicegui_autoform-0.1.0/tests/test_form.py +381 -0
- nicegui_autoform-0.1.0/tests/test_parity.py +71 -0
- nicegui_autoform-0.1.0/tests/test_spec.py +123 -0
- nicegui_autoform-0.1.0/tests/test_validate.py +78 -0
- nicegui_autoform-0.1.0/tests/test_values.py +87 -0
- nicegui_autoform-0.1.0/tests/test_widgets.py +56 -0
- nicegui_autoform-0.1.0/uv.lock +1538 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v7
|
|
12
|
+
- uses: astral-sh/setup-uv@v10.2.0 # no floating v10 major tag published yet
|
|
13
|
+
with:
|
|
14
|
+
enable-cache: true
|
|
15
|
+
cache-dependency-glob: uv.lock
|
|
16
|
+
cache-suffix: release
|
|
17
|
+
# A tag that disagrees with pyproject.toml would publish a version nobody
|
|
18
|
+
# can reproduce from the tag, so fail before anything is built.
|
|
19
|
+
- name: Check tag matches project version
|
|
20
|
+
run: |
|
|
21
|
+
tag="${GITHUB_REF_NAME#v}"
|
|
22
|
+
version="$(uv version --short)"
|
|
23
|
+
if [ "$tag" != "$version" ]; then
|
|
24
|
+
echo "tag $GITHUB_REF_NAME does not match project version $version" >&2
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
27
|
+
- run: uv build
|
|
28
|
+
- uses: actions/upload-artifact@v7
|
|
29
|
+
with:
|
|
30
|
+
name: dist
|
|
31
|
+
path: dist/
|
|
32
|
+
|
|
33
|
+
publish:
|
|
34
|
+
needs: build
|
|
35
|
+
runs-on: ubuntu-latest
|
|
36
|
+
# Trusted publishing: PyPI mints a short-lived token from this job's OIDC
|
|
37
|
+
# identity, so there is no API token to store. Configure the publisher at
|
|
38
|
+
# https://pypi.org/manage/account/publishing/ with workflow "release.yml"
|
|
39
|
+
# and environment "pypi".
|
|
40
|
+
environment:
|
|
41
|
+
name: pypi
|
|
42
|
+
url: https://pypi.org/p/nicegui-autoform
|
|
43
|
+
permissions:
|
|
44
|
+
id-token: write
|
|
45
|
+
steps:
|
|
46
|
+
- uses: actions/download-artifact@v8
|
|
47
|
+
with:
|
|
48
|
+
name: dist
|
|
49
|
+
path: dist/
|
|
50
|
+
- uses: astral-sh/setup-uv@v10.2.0 # no floating v10 major tag published yet
|
|
51
|
+
# --check-url skips files PyPI already has, so re-running the job after a
|
|
52
|
+
# partial failure is not an error.
|
|
53
|
+
- run: uv publish --trusted-publishing always --check-url https://pypi.org/simple/
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
name: tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
concurrency:
|
|
8
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
9
|
+
cancel-in-progress: true
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
test:
|
|
13
|
+
# Every version the README claims support for. 3.15 is still a release
|
|
14
|
+
# candidate, so uv installs it rather than actions/setup-python.
|
|
15
|
+
strategy:
|
|
16
|
+
fail-fast: false
|
|
17
|
+
matrix:
|
|
18
|
+
python-version: ["3.13", "3.14", "3.15"]
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v7
|
|
22
|
+
- uses: astral-sh/setup-uv@v10.2.0 # no floating v10 major tag published yet
|
|
23
|
+
with:
|
|
24
|
+
enable-cache: true
|
|
25
|
+
cache-dependency-glob: uv.lock
|
|
26
|
+
# Without this the three matrix jobs race for one cache key and two
|
|
27
|
+
# of them fail to save.
|
|
28
|
+
cache-suffix: py${{ matrix.python-version }}
|
|
29
|
+
- run: uv sync --frozen --python ${{ matrix.python-version }}
|
|
30
|
+
- run: uv run --frozen --python ${{ matrix.python-version }} pytest -q
|
|
31
|
+
|
|
32
|
+
lint:
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
steps:
|
|
35
|
+
- uses: actions/checkout@v7
|
|
36
|
+
- uses: astral-sh/setup-uv@v10.2.0 # no floating v10 major tag published yet
|
|
37
|
+
with:
|
|
38
|
+
enable-cache: true
|
|
39
|
+
cache-dependency-glob: uv.lock
|
|
40
|
+
cache-suffix: lint
|
|
41
|
+
- run: uv run --frozen ruff check .
|
|
42
|
+
- run: uv run --frozen ruff format --check .
|
|
43
|
+
- run: uv run --frozen ty check src tests examples
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Owen Solberg
|
|
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.
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: nicegui-autoform
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Render a NiceGUI web form from a command line interface.
|
|
5
|
+
Project-URL: Homepage, https://github.com/odoublewen/nicegui-autoform
|
|
6
|
+
Project-URL: Issues, https://github.com/odoublewen/nicegui-autoform/issues
|
|
7
|
+
Author: Owen Solberg
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: argparse,cli,click,cyclopts,forms,nicegui,typer
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Web Environment
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
17
|
+
Requires-Python: >=3.13
|
|
18
|
+
Requires-Dist: nicegui>=3.12
|
|
19
|
+
Provides-Extra: all
|
|
20
|
+
Requires-Dist: click>=8.1; extra == 'all'
|
|
21
|
+
Requires-Dist: cyclopts>=4.0; extra == 'all'
|
|
22
|
+
Requires-Dist: typer>=0.13; extra == 'all'
|
|
23
|
+
Provides-Extra: click
|
|
24
|
+
Requires-Dist: click>=8.1; extra == 'click'
|
|
25
|
+
Provides-Extra: cyclopts
|
|
26
|
+
Requires-Dist: cyclopts>=4.0; extra == 'cyclopts'
|
|
27
|
+
Provides-Extra: typer
|
|
28
|
+
Requires-Dist: typer>=0.13; extra == 'typer'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# nicegui-autoform
|
|
32
|
+
|
|
33
|
+
[](https://github.com/odoublewen/nicegui-autoform/actions/workflows/tests.yml)
|
|
34
|
+
|
|
35
|
+
Render a [NiceGUI](https://nicegui.io) web form from a command line interface.
|
|
36
|
+
|
|
37
|
+
If you already have a function wired up as a CLI, `AutoForm` reads that CLI's own
|
|
38
|
+
parameter metadata -- types, defaults, help text, choices, flags -- and builds a form
|
|
39
|
+
that calls the same function.
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from nicegui import ui
|
|
43
|
+
from nicegui_autoform import AutoForm
|
|
44
|
+
|
|
45
|
+
AutoForm(app, command="train") # a cyclopts App, click Command, Typer app, ...
|
|
46
|
+
ui.run()
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Supported frameworks
|
|
50
|
+
|
|
51
|
+
| Framework | Install | Notes |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| [cyclopts](https://cyclopts.readthedocs.io) | `nicegui-autoform[cyclopts]` | Including CLIs defined with a `@dataclass` |
|
|
54
|
+
| [click](https://click.palletsprojects.com) | `nicegui-autoform[click]` | |
|
|
55
|
+
| [Typer](https://typer.tiangolo.com) | `nicegui-autoform[typer]` | |
|
|
56
|
+
| `argparse` | built in | No callback exists, so `on_submit` is required |
|
|
57
|
+
| Plain `@dataclass` or annotated function | built in | No CLI framework needed |
|
|
58
|
+
|
|
59
|
+
The core package depends only on NiceGUI. CLI frameworks are optional extras and are
|
|
60
|
+
imported lazily, so nothing is imported that you do not already use.
|
|
61
|
+
|
|
62
|
+
## Install
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
uv add "nicegui-autoform[all]"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Supported versions
|
|
69
|
+
|
|
70
|
+
Python 3.13, 3.14 and 3.15. The dependency floors below were measured by running
|
|
71
|
+
the test suite against each version rather than guessed:
|
|
72
|
+
|
|
73
|
+
| Dependency | Floor | Why not lower |
|
|
74
|
+
|---|---|---|
|
|
75
|
+
| NiceGUI | 3.12 | NiceGUI's own `user` test fixture only works from 3.12, so earlier versions cannot be verified end to end |
|
|
76
|
+
| cyclopts | 4.0 | 3.x has a different `ArgumentCollection` API |
|
|
77
|
+
| click | 8.1 | 8.0 predates the parameter metadata the adapter reads |
|
|
78
|
+
| Typer | 0.13 | 0.12 cannot build its commands against current click |
|
|
79
|
+
|
|
80
|
+
One caveat on click 8.1 and 8.2: they report an option with no default the same
|
|
81
|
+
way as `default=None`, so those two cases are indistinguishable. From click 8.3
|
|
82
|
+
onwards an unset default is a distinct `UNSET` sentinel and the form can tell
|
|
83
|
+
"no default" from "defaults to None".
|
|
84
|
+
|
|
85
|
+
## How it works
|
|
86
|
+
|
|
87
|
+
Each framework has an adapter that produces a `CommandSpec` -- a flat list of
|
|
88
|
+
`ParamSpec` leaves plus the containers needed to rebuild nested objects. Everything
|
|
89
|
+
downstream (widget selection, validation, rendering) works on that spec alone, so all
|
|
90
|
+
frameworks get identical behaviour and a new framework only needs a new adapter.
|
|
91
|
+
|
|
92
|
+
On submit the form validates its values against the spec and then calls the command's
|
|
93
|
+
own function directly. It does not shell out or re-parse a command line.
|
|
94
|
+
|
|
95
|
+
## Usage
|
|
96
|
+
|
|
97
|
+
### Choosing a command
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
AutoForm(cyclopts_app, command="train")
|
|
101
|
+
AutoForm(typer_app, command="train")
|
|
102
|
+
AutoForm(click_group, command="train")
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
A single-command CLI needs no `command=`.
|
|
106
|
+
|
|
107
|
+
### Overriding the callback
|
|
108
|
+
|
|
109
|
+
By default the form calls the command's own function. Pass `on_submit` to intercept it:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
async def run(*args, **kwargs):
|
|
113
|
+
ui.notify(f"running with {kwargs}")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
AutoForm(app, command="train", on_submit=run)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`on_submit` may be sync or async. For an `argparse.ArgumentParser` there is no callback
|
|
120
|
+
to default to, so `on_submit` is required and receives an `argparse.Namespace`.
|
|
121
|
+
|
|
122
|
+
### Nested dataclasses
|
|
123
|
+
|
|
124
|
+
A cyclopts command taking a dataclass renders its fields as a labelled section, and the
|
|
125
|
+
dataclass is rebuilt before the callback is called:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
@dataclass
|
|
129
|
+
class Config:
|
|
130
|
+
"""Training configuration.
|
|
131
|
+
|
|
132
|
+
Parameters
|
|
133
|
+
----------
|
|
134
|
+
epochs: int
|
|
135
|
+
number of epochs
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
data: Path
|
|
139
|
+
epochs: int = 10
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@app.command
|
|
143
|
+
def train(config: Config, seed: int = 0): ...
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Help text comes from the dataclass's own numpydoc docstring, exactly as cyclopts'
|
|
147
|
+
`--help` renders it.
|
|
148
|
+
|
|
149
|
+
### Widgets and exclusions
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from nicegui_autoform import WidgetKind
|
|
153
|
+
|
|
154
|
+
AutoForm(
|
|
155
|
+
app,
|
|
156
|
+
command="train",
|
|
157
|
+
exclude=["debug", "config.seed"], # bare name or dotted path
|
|
158
|
+
widgets={"notes": WidgetKind.TEXTAREA},
|
|
159
|
+
initial={"epochs": 50},
|
|
160
|
+
)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
`Path` parameters render as a file upload by default. The upload is written to a private
|
|
164
|
+
temporary directory under its **original** name, so the callback receives a real, readable
|
|
165
|
+
`Path` whose `.name` is the file the user picked -- useful when the function derives an
|
|
166
|
+
output name or switches on the extension. Only the basename of the client-supplied filename
|
|
167
|
+
is ever used, so an uploaded name cannot escape that directory.
|
|
168
|
+
|
|
169
|
+
Use `widgets={"out": WidgetKind.PATH_TEXT}` for an output path that should be typed rather
|
|
170
|
+
than uploaded.
|
|
171
|
+
|
|
172
|
+
## Examples
|
|
173
|
+
|
|
174
|
+
Four runnable demos, one per framework:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
uv run python examples/cyclopts_demo.py # nested dataclass as a section
|
|
178
|
+
uv run python examples/click_demo.py # IntRange, Choice, repeatable option
|
|
179
|
+
uv run python examples/typer_demo.py # rich_help_panel as a section
|
|
180
|
+
uv run python examples/argparse_demo.py # on_submit receives a Namespace
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Each has a required `data` upload. `examples/data/measurements.csv` (12 rows of
|
|
184
|
+
dose/response data) is there to drop into it, and `examples/data/notes.txt` shows that a
|
|
185
|
+
non-CSV extension survives the round trip. On submit each demo reads the uploaded file and
|
|
186
|
+
reports its name and line count, so you can see the real file reached the function.
|
|
187
|
+
|
|
188
|
+
## Development
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
uv sync
|
|
192
|
+
uv run pytest
|
|
193
|
+
uv run ruff check
|
|
194
|
+
uv run ruff format
|
|
195
|
+
uv run ty check src tests examples
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
To check a dependency floor, run the suite against that exact version:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
uv run --isolated --with "click==8.1.8" pytest
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## License
|
|
205
|
+
|
|
206
|
+
MIT
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# nicegui-autoform
|
|
2
|
+
|
|
3
|
+
[](https://github.com/odoublewen/nicegui-autoform/actions/workflows/tests.yml)
|
|
4
|
+
|
|
5
|
+
Render a [NiceGUI](https://nicegui.io) web form from a command line interface.
|
|
6
|
+
|
|
7
|
+
If you already have a function wired up as a CLI, `AutoForm` reads that CLI's own
|
|
8
|
+
parameter metadata -- types, defaults, help text, choices, flags -- and builds a form
|
|
9
|
+
that calls the same function.
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from nicegui import ui
|
|
13
|
+
from nicegui_autoform import AutoForm
|
|
14
|
+
|
|
15
|
+
AutoForm(app, command="train") # a cyclopts App, click Command, Typer app, ...
|
|
16
|
+
ui.run()
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Supported frameworks
|
|
20
|
+
|
|
21
|
+
| Framework | Install | Notes |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| [cyclopts](https://cyclopts.readthedocs.io) | `nicegui-autoform[cyclopts]` | Including CLIs defined with a `@dataclass` |
|
|
24
|
+
| [click](https://click.palletsprojects.com) | `nicegui-autoform[click]` | |
|
|
25
|
+
| [Typer](https://typer.tiangolo.com) | `nicegui-autoform[typer]` | |
|
|
26
|
+
| `argparse` | built in | No callback exists, so `on_submit` is required |
|
|
27
|
+
| Plain `@dataclass` or annotated function | built in | No CLI framework needed |
|
|
28
|
+
|
|
29
|
+
The core package depends only on NiceGUI. CLI frameworks are optional extras and are
|
|
30
|
+
imported lazily, so nothing is imported that you do not already use.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
uv add "nicegui-autoform[all]"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Supported versions
|
|
39
|
+
|
|
40
|
+
Python 3.13, 3.14 and 3.15. The dependency floors below were measured by running
|
|
41
|
+
the test suite against each version rather than guessed:
|
|
42
|
+
|
|
43
|
+
| Dependency | Floor | Why not lower |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| NiceGUI | 3.12 | NiceGUI's own `user` test fixture only works from 3.12, so earlier versions cannot be verified end to end |
|
|
46
|
+
| cyclopts | 4.0 | 3.x has a different `ArgumentCollection` API |
|
|
47
|
+
| click | 8.1 | 8.0 predates the parameter metadata the adapter reads |
|
|
48
|
+
| Typer | 0.13 | 0.12 cannot build its commands against current click |
|
|
49
|
+
|
|
50
|
+
One caveat on click 8.1 and 8.2: they report an option with no default the same
|
|
51
|
+
way as `default=None`, so those two cases are indistinguishable. From click 8.3
|
|
52
|
+
onwards an unset default is a distinct `UNSET` sentinel and the form can tell
|
|
53
|
+
"no default" from "defaults to None".
|
|
54
|
+
|
|
55
|
+
## How it works
|
|
56
|
+
|
|
57
|
+
Each framework has an adapter that produces a `CommandSpec` -- a flat list of
|
|
58
|
+
`ParamSpec` leaves plus the containers needed to rebuild nested objects. Everything
|
|
59
|
+
downstream (widget selection, validation, rendering) works on that spec alone, so all
|
|
60
|
+
frameworks get identical behaviour and a new framework only needs a new adapter.
|
|
61
|
+
|
|
62
|
+
On submit the form validates its values against the spec and then calls the command's
|
|
63
|
+
own function directly. It does not shell out or re-parse a command line.
|
|
64
|
+
|
|
65
|
+
## Usage
|
|
66
|
+
|
|
67
|
+
### Choosing a command
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
AutoForm(cyclopts_app, command="train")
|
|
71
|
+
AutoForm(typer_app, command="train")
|
|
72
|
+
AutoForm(click_group, command="train")
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
A single-command CLI needs no `command=`.
|
|
76
|
+
|
|
77
|
+
### Overriding the callback
|
|
78
|
+
|
|
79
|
+
By default the form calls the command's own function. Pass `on_submit` to intercept it:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
async def run(*args, **kwargs):
|
|
83
|
+
ui.notify(f"running with {kwargs}")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
AutoForm(app, command="train", on_submit=run)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`on_submit` may be sync or async. For an `argparse.ArgumentParser` there is no callback
|
|
90
|
+
to default to, so `on_submit` is required and receives an `argparse.Namespace`.
|
|
91
|
+
|
|
92
|
+
### Nested dataclasses
|
|
93
|
+
|
|
94
|
+
A cyclopts command taking a dataclass renders its fields as a labelled section, and the
|
|
95
|
+
dataclass is rebuilt before the callback is called:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
@dataclass
|
|
99
|
+
class Config:
|
|
100
|
+
"""Training configuration.
|
|
101
|
+
|
|
102
|
+
Parameters
|
|
103
|
+
----------
|
|
104
|
+
epochs: int
|
|
105
|
+
number of epochs
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
data: Path
|
|
109
|
+
epochs: int = 10
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@app.command
|
|
113
|
+
def train(config: Config, seed: int = 0): ...
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Help text comes from the dataclass's own numpydoc docstring, exactly as cyclopts'
|
|
117
|
+
`--help` renders it.
|
|
118
|
+
|
|
119
|
+
### Widgets and exclusions
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from nicegui_autoform import WidgetKind
|
|
123
|
+
|
|
124
|
+
AutoForm(
|
|
125
|
+
app,
|
|
126
|
+
command="train",
|
|
127
|
+
exclude=["debug", "config.seed"], # bare name or dotted path
|
|
128
|
+
widgets={"notes": WidgetKind.TEXTAREA},
|
|
129
|
+
initial={"epochs": 50},
|
|
130
|
+
)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`Path` parameters render as a file upload by default. The upload is written to a private
|
|
134
|
+
temporary directory under its **original** name, so the callback receives a real, readable
|
|
135
|
+
`Path` whose `.name` is the file the user picked -- useful when the function derives an
|
|
136
|
+
output name or switches on the extension. Only the basename of the client-supplied filename
|
|
137
|
+
is ever used, so an uploaded name cannot escape that directory.
|
|
138
|
+
|
|
139
|
+
Use `widgets={"out": WidgetKind.PATH_TEXT}` for an output path that should be typed rather
|
|
140
|
+
than uploaded.
|
|
141
|
+
|
|
142
|
+
## Examples
|
|
143
|
+
|
|
144
|
+
Four runnable demos, one per framework:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
uv run python examples/cyclopts_demo.py # nested dataclass as a section
|
|
148
|
+
uv run python examples/click_demo.py # IntRange, Choice, repeatable option
|
|
149
|
+
uv run python examples/typer_demo.py # rich_help_panel as a section
|
|
150
|
+
uv run python examples/argparse_demo.py # on_submit receives a Namespace
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Each has a required `data` upload. `examples/data/measurements.csv` (12 rows of
|
|
154
|
+
dose/response data) is there to drop into it, and `examples/data/notes.txt` shows that a
|
|
155
|
+
non-CSV extension survives the round trip. On submit each demo reads the uploaded file and
|
|
156
|
+
reports its name and line count, so you can see the real file reached the function.
|
|
157
|
+
|
|
158
|
+
## Development
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
uv sync
|
|
162
|
+
uv run pytest
|
|
163
|
+
uv run ruff check
|
|
164
|
+
uv run ruff format
|
|
165
|
+
uv run ty check src tests examples
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
To check a dependency floor, run the suite against that exact version:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
uv run --isolated --with "click==8.1.8" pytest
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## License
|
|
175
|
+
|
|
176
|
+
MIT
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Argparse.
|
|
2
|
+
|
|
3
|
+
A parser has no callback, so ``on_submit`` is required and receives the
|
|
4
|
+
``Namespace`` the form assembled.
|
|
5
|
+
|
|
6
|
+
uv run python examples/argparse_demo.py
|
|
7
|
+
|
|
8
|
+
Upload examples/data/measurements.csv into the Data field to submit.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from nicegui import ui
|
|
17
|
+
|
|
18
|
+
from nicegui_autoform import AutoForm
|
|
19
|
+
|
|
20
|
+
parser = argparse.ArgumentParser(prog="train", description="Train a model.")
|
|
21
|
+
parser.add_argument("data", type=Path, help="the input data file")
|
|
22
|
+
parser.add_argument("--epochs", type=int, default=10, help="number of epochs")
|
|
23
|
+
parser.add_argument("--mode", choices=["fast", "slow"], default="fast")
|
|
24
|
+
parser.add_argument("--verbose", action="store_true", help="log every step")
|
|
25
|
+
parser.add_argument("--tag", dest="tags", action="append", default=[], help="repeatable label")
|
|
26
|
+
|
|
27
|
+
advanced = parser.add_argument_group("Advanced")
|
|
28
|
+
advanced.add_argument("--workers", type=int, default=4, help="parallel workers")
|
|
29
|
+
advanced.add_argument("--seed", type=int, default=0)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def run(args: argparse.Namespace) -> None:
|
|
33
|
+
"""A parser has no callback of its own, so this receives the Namespace."""
|
|
34
|
+
lines = args.data.read_text().splitlines()
|
|
35
|
+
ui.notify(
|
|
36
|
+
f"{args.data.name} ({len(lines)} lines) with "
|
|
37
|
+
f"epochs={args.epochs}, mode={args.mode}, verbose={args.verbose}, "
|
|
38
|
+
f"tags={args.tags}, workers={args.workers}, seed={args.seed}",
|
|
39
|
+
type="positive",
|
|
40
|
+
multi_line=True,
|
|
41
|
+
close_button=True,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@ui.page("/")
|
|
46
|
+
def index() -> None:
|
|
47
|
+
ui.label("argparse").classes("text-h5")
|
|
48
|
+
AutoForm(parser, on_submit=run)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
ui.run(title="argparse demo", reload=False)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Click.
|
|
2
|
+
|
|
3
|
+
uv run python examples/click_demo.py
|
|
4
|
+
|
|
5
|
+
Upload examples/data/measurements.csv into the Data field to submit.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
from nicegui import ui
|
|
14
|
+
|
|
15
|
+
from nicegui_autoform import AutoForm
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def summarise(data: Path) -> str:
|
|
19
|
+
"""Prove the upload arrived: the callback gets a real, readable file."""
|
|
20
|
+
lines = data.read_text().splitlines()
|
|
21
|
+
return f"{data.name} ({len(lines)} lines, {data.stat().st_size} bytes)"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@click.command("train")
|
|
25
|
+
@click.argument("data", type=click.Path(path_type=Path))
|
|
26
|
+
@click.option("--epochs", type=int, default=10, help="number of epochs")
|
|
27
|
+
@click.option("--workers", type=click.IntRange(1, 64), default=4, help="parallel workers")
|
|
28
|
+
@click.option("--mode", type=click.Choice(["fast", "slow"]), default="fast")
|
|
29
|
+
@click.option("--verbose/--no-verbose", default=False, help="log every step")
|
|
30
|
+
@click.option("--tag", "tags", multiple=True, help="repeatable label")
|
|
31
|
+
def train(data, epochs, workers, mode, verbose, tags):
|
|
32
|
+
"""Train a model."""
|
|
33
|
+
ui.notify(
|
|
34
|
+
f"train(data={summarise(data)}, epochs={epochs}, workers={workers}, "
|
|
35
|
+
f"mode={mode}, verbose={verbose}, tags={list(tags)})",
|
|
36
|
+
type="positive",
|
|
37
|
+
multi_line=True,
|
|
38
|
+
close_button=True,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@ui.page("/")
|
|
43
|
+
def index() -> None:
|
|
44
|
+
ui.label("click").classes("text-h5")
|
|
45
|
+
AutoForm(train)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
ui.run(title="click demo", reload=False)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Cyclopts, including a nested dataclass rendered as a section.
|
|
2
|
+
|
|
3
|
+
uv run python examples/cyclopts_demo.py
|
|
4
|
+
|
|
5
|
+
Upload examples/data/measurements.csv into the Data field to submit.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import enum
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Annotated
|
|
14
|
+
|
|
15
|
+
import cyclopts
|
|
16
|
+
from nicegui import ui
|
|
17
|
+
|
|
18
|
+
from nicegui_autoform import AutoForm
|
|
19
|
+
|
|
20
|
+
app = cyclopts.App(name="trainer")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def summarise(data: Path) -> str:
|
|
24
|
+
"""Prove the upload arrived: the callback gets a real, readable file."""
|
|
25
|
+
lines = data.read_text().splitlines()
|
|
26
|
+
return f"{data.name} ({len(lines)} lines, {data.stat().st_size} bytes)"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Mode(enum.StrEnum):
|
|
30
|
+
fast = "fast"
|
|
31
|
+
slow = "slow"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class Tuning:
|
|
36
|
+
"""Tuning.
|
|
37
|
+
|
|
38
|
+
Parameters
|
|
39
|
+
----------
|
|
40
|
+
lr: float
|
|
41
|
+
learning rate
|
|
42
|
+
workers: int
|
|
43
|
+
parallel worker processes
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
lr: float = 0.001
|
|
47
|
+
workers: int = 4
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
DEFAULT_TUNING = Tuning()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@app.command
|
|
54
|
+
def train(
|
|
55
|
+
data: Path,
|
|
56
|
+
epochs: int = 10,
|
|
57
|
+
mode: Mode = Mode.fast,
|
|
58
|
+
verbose: Annotated[bool, cyclopts.Parameter(help="log every step")] = False,
|
|
59
|
+
tags: list[str] = [], # noqa: B006 - cyclopts reads the annotation, not the object
|
|
60
|
+
tuning: Tuning = DEFAULT_TUNING,
|
|
61
|
+
):
|
|
62
|
+
"""Train a model.
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
data: Path
|
|
67
|
+
the input data file
|
|
68
|
+
epochs: int
|
|
69
|
+
number of epochs
|
|
70
|
+
mode: Mode
|
|
71
|
+
scheduling mode
|
|
72
|
+
"""
|
|
73
|
+
ui.notify(
|
|
74
|
+
f"train(data={summarise(data)}, epochs={epochs}, mode={mode}, "
|
|
75
|
+
f"verbose={verbose}, tags={tags}, tuning={tuning})",
|
|
76
|
+
type="positive",
|
|
77
|
+
multi_line=True,
|
|
78
|
+
close_button=True,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@ui.page("/")
|
|
83
|
+
def index() -> None:
|
|
84
|
+
ui.label("cyclopts").classes("text-h5")
|
|
85
|
+
AutoForm(app, command="train")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
ui.run(title="cyclopts demo", reload=False)
|