Skip to content

Commit 022339d

Browse files
committed
2.7.0
1 parent 2e46915 commit 022339d

16 files changed

Lines changed: 202 additions & 24 deletions

‎CHANGELOG.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
## [2.7.0] - 2026-09-13
4+
5+
- `hidden` supports nested object paths (`config.token`) and list wildcards
6+
(`items.*.token`) through pytypehintweb 1.2.0. `OpenForm` validates paths
7+
against the target plan. The page delegates visibility to the widget compiler,
8+
sharing its traversal with prefill. Hidden fields now remain in the DOM with
9+
the native `hidden` attribute and keep their values and validation.
10+
311
## [2.6.2] - 2026-09-13
412

513
### Added

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# FuncToWeb 2.6.2
1+
# FuncToWeb 2.7.0
22

33
[![PyPI version](https://img.shields.io/pypi/v/func-to-web.svg)](https://pypi.org/project/func-to-web/)
44
[![Python](https://img.shields.io/pypi/pyversions/func-to-web.svg)](https://pypi.org/project/func-to-web/)
@@ -521,7 +521,7 @@ practice.
521521

522522
## Status
523523

524-
**FuncToWeb 2.6.2 is stable, used daily and actively maintained.**
524+
**FuncToWeb 2.7.0 is stable, used daily and actively maintained.**
525525

526526
The public API is the one described in
527527
[`docs/`](docs/index.md), and the known limitations are listed in

‎docs/open-form.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ OpenForm(edit_product, hidden=("product_id",))
7575
OpenForm(edit)
7676
```
7777

78+
`hidden` accepts root names and nested paths such as `"config.token"` or
79+
`"items.*.token"`; see [prefill.md](prefill.md#nested-fields).
80+
7881
`hidden` is a tuple of `str` with the usual semantics: it hides the field on the
7982
target form, without fixing or protecting it. A list, a bare string, or an
8083
element that is not a `str` raises a `TypeError` when the metadata is built.

‎docs/prefill.md‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ like any other value and the resolver recognizes it. See [files.md](files.md).
139139

140140
## Hidden parameters
141141

142-
`hidden` takes the widget out of the page. What happens to the parameter is
142+
`hidden` hides the widget and its label using the native `hidden` attribute. What happens to the parameter is
143143
exactly this:
144144

145145
* it stays compiled into the form, with its value (the prefill's, or its own
@@ -155,6 +155,29 @@ element exactly a `str`. Duplicates, ordering and unknown names are not its
155155
business, because hiding is a visual matter and a misspelled name does not
156156
break any contract.
157157

158+
### Nested fields
159+
160+
Use dotted paths for object fields and `*` for every item of a list:
161+
162+
```javascript
163+
{
164+
prefill: {config: {token: "abc", name: "Demo"}},
165+
hidden: ["config.token"]
166+
}
167+
```
168+
169+
`hidden: ["items.*.token"]` hides `token` in all items, including newly added
170+
ones. Nested lists use another `*`: `"items.*.*.token"`. Hiding `"config"`
171+
still hides the entire parameter. Optional and union wrappers add no segment;
172+
a path applies to every union branch containing the field. Numeric indices
173+
and branch-specific selectors are not supported.
174+
175+
Preloaded values and visibility descend through the same widget compiler.
176+
Hidden children retain their values, validation and uploads. They stay in the
177+
DOM with `hidden`, so select visible fields explicitly when inspecting a page.
178+
URL/SDK paths that resolve to no field have no effect. `OpenForm` checks paths
179+
against the target plan and rejects unresolved paths during registration.
180+
158181
### It hides, it does not lock
159182

160183
```text

‎docs/run.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ Which two directories were in force is not left to be guessed: `run()`
4848
announces them, with the version, on the line before the server starts.
4949

5050
```text
51-
FuncToWeb 2.6.2
51+
FuncToWeb 2.7.0
5252
UPLOADS DIR: /srv/functoweb/uploads
5353
RETURNS DIR: /tmp/FuncToWeb/returns
5454
```

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ classifiers = [
2525
]
2626

2727
dependencies = [
28-
"pytypehintweb==1.1.0",
28+
"pytypehintweb==1.2.0",
2929
"starlette==0.49.3",
3030
"uvicorn==0.38.0",
3131
]

‎src/func_to_web/config.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from sys import platform
44
from tempfile import gettempdir
55

6-
__version__ = "2.6.2"
6+
__version__ = "2.7.0"
77

88
NAME: str = "".join(part.title() for part in __name__.split(".")[0].split("_"))
99

‎src/func_to_web/models/functions.py‎

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,16 +148,41 @@ def _target_of(
148148

149149

150150
def _check_hidden(mark: OpenForm, target: WebFunction) -> None:
151-
names = {param.name for param in target.schema.params}
152-
153151
for name in mark.hidden:
154-
if name not in names:
152+
if not _has_hidden_path(target.plan, name):
155153
raise ReturnContractError(
156154
f"unknown hidden field {name!r} for OpenForm target "
157155
f"{target.slug!r}"
158156
)
159157

160158

159+
def _has_hidden_path(node: dict[str, Any], path: str) -> bool:
160+
"""Resolve visibility against the same plan the browser compiles.
161+
162+
Optional and choice wrappers do not add path components. A list consumes
163+
'*', so its rule also applies to items created later in the browser.
164+
"""
165+
kind = node["kind"]
166+
if kind in ("form", "object"):
167+
return any(
168+
path == field["name"] or (
169+
path.startswith(field["name"] + ".")
170+
and _has_hidden_path(field["node"], path[len(field["name"]) + 1:])
171+
)
172+
for field in node["fields"]
173+
)
174+
if kind == "list":
175+
return path == "*" or (
176+
path.startswith("*.") and _has_hidden_path(node["item"], path[2:])
177+
)
178+
if kind == "optional":
179+
return _has_hidden_path(node["node"], path)
180+
if kind == "choice":
181+
return any(_has_hidden_path(branch["node"], path)
182+
for branch in node["branches"])
183+
return False
184+
185+
161186
FunctionEntry: TypeAlias = Callable[..., Any] | WebFunction
162187
FunctionInput: TypeAlias = (
163188
WebFunctions | FunctionEntry | Iterable[FunctionEntry]

‎src/func_to_web/static/page.js‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,18 @@ import { runUploads } from "./upload.js";
88

99
const plan = JSON.parse(
1010
document.getElementById("functoweb-plan").textContent);
11-
const form = compileForm(plan);
11+
const hidden = JSON.parse(
12+
document.getElementById("functoweb-hidden").textContent);
13+
const form = compileForm(plan, { hidden });
1214

1315
const labels = new Map(plan.fields.map((field) => [field.name, field.label]));
1416

15-
const hidden = new Set(JSON.parse(
16-
document.getElementById("functoweb-hidden").textContent));
17-
1817
const autorun = JSON.parse(
1918
document.getElementById("functoweb-autorun").textContent);
2019

2120
const fields = document.getElementById("fields");
2221
for (const field of form.fields) {
23-
if (!hidden.has(field.name)) fields.append(field.widget.el);
22+
fields.append(field.widget.el);
2423
}
2524

2625
const result = document.getElementById("result");

‎tests/e2e/conftest.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
warm_uvicorn()
99

10-
READY = "#fields .pth-field"
10+
READY = "#fields .pth-field:visible"
1111

1212
TIMEOUT = 15000
1313

0 commit comments

Comments
 (0)