Skip to content

Commit f936144

Browse files
⚗️ msgspec + nutree + FormioConfig rewrite
POC for a proper data types rewrite to deal with anything formio in the backend. See #6565
1 parent ec5e1c0 commit f936144

205 files changed

Lines changed: 12152 additions & 7420 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

bin/report_component_problems.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,57 @@ def check_component(component: Component) -> Iterator[str]:
193193
f"{language_code}."
194194
)
195195

196+
case {"type": "editgrid"}:
197+
if "groupLabel" not in component:
198+
yield "Missing 'groupLabel' property."
199+
200+
# detect inconsistent default values
201+
match component:
202+
case {
203+
"type": "textfield"
204+
| "email"
205+
| "phoneNumber"
206+
| "postcode"
207+
| "textarea"
208+
| "select"
209+
| "date"
210+
| "datetime"
211+
| "time",
212+
"multiple": True,
213+
"defaultValue": str(),
214+
}:
215+
yield "non-array defaultValue for 'multiple: true'"
216+
217+
case {
218+
"type": "textfield"
219+
| "email"
220+
| "phoneNumber"
221+
| "postcode"
222+
| "textarea"
223+
| "select"
224+
| "date"
225+
| "datetime"
226+
| "time",
227+
"multiple": True,
228+
"defaultValue": list() as dv,
229+
}:
230+
if None in dv:
231+
yield "None found in defaultValue for text-based component"
232+
233+
case {
234+
"type": "textfield"
235+
| "email"
236+
| "phoneNumber"
237+
| "postcode"
238+
| "textarea"
239+
| "select"
240+
| "date"
241+
| "datetime"
242+
| "time",
243+
"defaultValue": list(),
244+
} if not (multiple := component.get("multiple")):
245+
yield f"array defaultValue for 'multiple: {multiple}'"
246+
196247

197248
def check_component_html_usage(component: Component) -> list[str]:
198249
messages = []

bin/report_conditional_eq_properties.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def report_configurations() -> bool:
7171
known_conditional_keys = {"when", "show"}
7272
for form_definition in form_definitions.iterator(chunk_size=10):
7373
component_keys: set[str] = set()
74-
for component in form_definition.configuration_wrapper:
74+
for component in form_definition.iter_components():
7575
if not (conditional := component.get("conditional")):
7676
continue
7777
elif (

bin/report_logic_with_deprecated_clear_on_hide_behavior.py

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@
1010
import django
1111

1212
import click
13+
import msgspec
1314
from json_logic.meta import JSONLogicExpression
1415
from json_logic.meta.expressions import destructure
1516
from json_logic.typing import JSON
1617
from tabulate import tabulate
1718

19+
from formio_types import AnyComponent, EditGrid, FormioConfiguration, Selectboxes
20+
1821
SRC_DIR = Path(__file__).parent.parent / "src"
1922
sys.path.insert(0, str(SRC_DIR.resolve()))
2023

@@ -86,15 +89,11 @@ def analyze_rule(
8689
*,
8790
rule,
8891
form,
89-
component_map,
92+
component_map: dict[str, AnyComponent],
9093
components_with_affected_visibility,
9194
data,
9295
):
93-
from openforms.formio.service import (
94-
get_component_empty_value,
95-
iter_components,
96-
)
97-
from openforms.formio.typing import Component
96+
from openforms.formio.service import get_component_empty_value, iter_components
9897
from openforms.forms.constants import LogicActionTypes
9998
from openforms.variables.service import resolve_key
10099

@@ -110,14 +109,14 @@ def analyze_rule(
110109
continue
111110

112111
component = component_map[resolved_key]
113-
if component["type"] == "editgrid" and var_name != resolved_key:
112+
if isinstance(component, EditGrid) and var_name != resolved_key:
114113
# We expect data access "editgrid.x.child_key" here, so discard the
115114
# parent key and index. Assuming there are no nested editgrids here
116115
# :see_no_evil:
117116
_, child_key = var_name.removeprefix(f"{resolved_key}.").split(".", 1)
118117

119-
children_map: dict[str, Component] = {
120-
child["key"]: child
118+
children_map: dict[str, AnyComponent] = {
119+
child.key: child
121120
for child in iter_components(
122121
component, recursive=True, recurse_into_editgrid=False
123122
)
@@ -128,13 +127,13 @@ def analyze_rule(
128127

129128
# Visibility of component is not affected and/or component does not have
130129
# clearOnHide enabled, so it's not relevant
131-
if resolved_key not in components_with_affected_visibility or not component.get(
132-
"clearOnHide", True
130+
if resolved_key not in components_with_affected_visibility or not getattr(
131+
component, "clear_on_hide", True
133132
):
134133
continue
135134

136135
empty_value = get_component_empty_value(component)
137-
if component["type"] == "selectboxes":
136+
if isinstance(component, Selectboxes):
138137
# `get_component_empty_value` returns {"option_a": False, "option_b": False, etc...}
139138
# for a selectboxes component, which is not a useful in this
140139
# context. It is not possible to use a dictionary as a comparison
@@ -148,7 +147,7 @@ def analyze_rule(
148147
# current default that is set when a variable is missing from the
149148
# context. Note that all form variables should be present in the context
150149
# at the moment, but there is no such guarantee for nested data.
151-
if comp_value in [empty_value, None, component.get("defaultValue")]:
150+
if comp_value in [empty_value, None, getattr(component, "default_value", None)]:
152151
variable_names.add(var_name)
153152

154153
if variable_names:
@@ -218,8 +217,8 @@ def analyze_rule(
218217

219218

220219
def report_rules() -> bool:
221-
from openforms.formio.service import iter_components
222-
from openforms.formio.typing import Component
220+
221+
from openforms.formio.service import _fixup_component_properties, iter_components
223222
from openforms.formio.visibility import get_conditional
224223
from openforms.forms.models import Form
225224

@@ -233,18 +232,23 @@ def report_rules() -> bool:
233232

234233
# Mapping from component to step for quick access
235234
form_steps = form.formstep_set.select_related("form_definition")
236-
component_map: dict[str, Component] = {}
235+
component_map: dict[str, AnyComponent] = {}
237236
for form_step in form_steps:
238-
for component in iter_components(
237+
formio_configuration = msgspec.convert(
239238
form_step.form_definition.configuration,
239+
type=FormioConfiguration,
240+
dec_hook=_fixup_component_properties,
241+
)
242+
for component in iter_components(
243+
formio_configuration,
240244
recursive=True,
241245
recurse_into_editgrid=False,
242246
):
243-
component_map[component["key"]] = component
247+
component_map[component.key] = component
244248

245249
# Component with visibility affected by a conditional
246250
if get_conditional(component) is not None:
247-
components_with_affected_visibility.add(component["key"])
251+
components_with_affected_visibility.add(component.key)
248252

249253
# Components with visibility affected by logic rules
250254
for rule in form.formlogic_set.iterator():
@@ -261,7 +265,7 @@ def report_rules() -> bool:
261265

262266
component = component_map[key]
263267
children = {
264-
child["key"]
268+
child.key
265269
for child in iter_components(
266270
component, recursive=True, recurse_into_editgrid=False
267271
)

pyright.pyproject.toml

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,22 @@ include = [
3030
"src/openforms/config/forms.py",
3131
"src/openforms/config/api/",
3232
# Formio tooling
33+
"src/openforms/formio/components/translations.py",
34+
"src/openforms/formio/dynamic_config/date.py",
35+
"src/openforms/formio/dynamic_config/reference_lists.py",
36+
"src/openforms/formio/dynamic_config/tests/test_reference_lists_config.py",
37+
"src/openforms/formio/formatters/",
38+
"src/openforms/formio/rendering/default.py",
39+
"src/openforms/formio/rendering/nodes.py",
40+
"src/openforms/formio/rendering/tests/test_component_node.py",
41+
"src/openforms/formio/rendering/tests/test_custom_formio_components.py",
42+
"src/openforms/formio/rendering/tests/test_vanilla_formio_components.py",
3343
"src/openforms/formio/serializers.py",
3444
"src/openforms/formio/service.py",
35-
"src/openforms/formio/typing/",
36-
"src/openforms/formio/formatters/",
37-
"src/openforms/formio/dynamic_config/reference_lists.py",
3845
"src/openforms/formio/tests/test_datastructures.py",
46+
"src/openforms/formio/tests/test_visibility.py",
47+
"src/openforms/formio/typing/",
48+
# "src/openforms/formio/tests/test_component_translations.py",
3949
# Core forms app
4050
"src/openforms/forms/api/serializers/logic/action_serializers.py",
4151
"src/openforms/forms/api/v3/",
@@ -73,6 +83,8 @@ include = [
7383
"src/openforms/registrations/contrib/stuf_zds/plugin.py",
7484
"src/openforms/registrations/contrib/stuf_zds/typing.py",
7585
"src/openforms/registrations/contrib/zgw_apis/",
86+
"src/openforms/registrations/tests/test_component_pre_registration_tasks.py",
87+
"src/openforms/registrations/tests/test_process_variable_schema.py",
7688
# Translations
7789
"src/openforms/translations/api/views.py",
7890
"src/openforms/translations/admin.py",
@@ -92,6 +104,7 @@ include = [
92104
"src/openforms/submissions/logic/actions.py",
93105
"src/openforms/submissions/metrics.py",
94106
"src/openforms/submissions/query.py",
107+
"src/openforms/submissions/rendering/nodes.py",
95108
"src/openforms/submissions/report.py",
96109
"src/openforms/submissions/tests/form_logic/test_get_rules_to_evaluate.py",
97110
"src/openforms/submissions/tests/form_logic/test_rule_analysis.py",
@@ -117,7 +130,7 @@ exclude = [
117130
"src/openforms/authentication/contrib/eherkenning/tests/test_signicat_integration.py",
118131
"src/openforms/contrib/objects_api/tests/",
119132
"src/openforms/contrib/objects_api/json_schema.py",
120-
"src/openforms/formio/formatters/tests/",
133+
"src/openforms/formio/formatters/tests/test_default_formatters.py",
121134
"src/openforms/payments/management/commands/checkpaymentemaildupes.py",
122135
"src/openforms/payments/tests/",
123136
"src/openforms/payments/contrib/demo/tests/",

requirements/base.in

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ jsonschema_specifications
1313
jq
1414
lxml
1515
lxml-html-clean
16+
msgspec
1617
nh3
18+
nutree
1719
onlinepayments-sdk-python3 # Worldline SDK
1820
O365 # microsoft graph
1921
phonenumberslite

requirements/base.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,12 +365,16 @@ mozilla-django-oidc-db==1.1.1
365365
# django-digid-eherkenning
366366
msal==1.37.0
367367
# via o365
368+
msgspec==0.21.0
369+
# via -r requirements/base.in
368370
networkx==3.6.1
369371
# via -r requirements/base.in
370372
nh3==0.3.3
371373
# via -r requirements/base.in
372374
numpy==2.4.2
373375
# via shapely
376+
nutree==1.1.0
377+
# via -r requirements/base.in
374378
o365==2.1.9
375379
# via
376380
# -r requirements/base.in
@@ -648,6 +652,7 @@ typing-extensions==4.15.0
648652
# django-timeline-logger
649653
# grpcio
650654
# mozilla-django-oidc-db
655+
# nutree
651656
# opentelemetry-api
652657
# opentelemetry-exporter-otlp-proto-grpc
653658
# opentelemetry-exporter-otlp-proto-http

requirements/ci.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,10 @@ msal==1.37.0
633633
# -c requirements/base.txt
634634
# -r requirements/base.txt
635635
# o365
636+
msgspec==0.21.0
637+
# via
638+
# -c requirements/base.txt
639+
# -r requirements/base.txt
636640
networkx==3.6.1
637641
# via
638642
# -c requirements/base.txt
@@ -646,6 +650,10 @@ numpy==2.4.2
646650
# -c requirements/base.txt
647651
# -r requirements/base.txt
648652
# shapely
653+
nutree==1.1.0
654+
# via
655+
# -c requirements/base.txt
656+
# -r requirements/base.txt
649657
o365==2.1.9
650658
# via
651659
# -c requirements/base.txt
@@ -1161,6 +1169,7 @@ typing-extensions==4.15.0
11611169
# django-timeline-logger
11621170
# grpcio
11631171
# mozilla-django-oidc-db
1172+
# nutree
11641173
# opentelemetry-api
11651174
# opentelemetry-exporter-otlp-proto-grpc
11661175
# opentelemetry-exporter-otlp-proto-http

requirements/dev.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,10 @@ msal==1.37.0
694694
# -c requirements/ci.txt
695695
# -r requirements/ci.txt
696696
# o365
697+
msgspec==0.21.0
698+
# via
699+
# -c requirements/ci.txt
700+
# -r requirements/ci.txt
697701
networkx==3.6.1
698702
# via
699703
# -c requirements/ci.txt
@@ -707,6 +711,10 @@ numpy==2.4.2
707711
# -c requirements/ci.txt
708712
# -r requirements/ci.txt
709713
# shapely
714+
nutree==1.1.0
715+
# via
716+
# -c requirements/ci.txt
717+
# -r requirements/ci.txt
710718
o365==2.1.9
711719
# via
712720
# -c requirements/ci.txt
@@ -1294,6 +1302,7 @@ typing-extensions==4.15.0
12941302
# django-timeline-logger
12951303
# grpcio
12961304
# mozilla-django-oidc-db
1305+
# nutree
12971306
# opentelemetry-api
12981307
# opentelemetry-exporter-otlp-proto-grpc
12991308
# opentelemetry-exporter-otlp-proto-http

requirements/extensions.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,10 @@ msal==1.37.0
591591
# -c requirements/base.txt
592592
# -r requirements/base.txt
593593
# o365
594+
msgspec==0.21.0
595+
# via
596+
# -c requirements/base.txt
597+
# -r requirements/base.txt
594598
networkx==3.6.1
595599
# via
596600
# -c requirements/base.txt
@@ -604,6 +608,10 @@ numpy==2.4.2
604608
# -c requirements/base.txt
605609
# -r requirements/base.txt
606610
# shapely
611+
nutree==1.1.0
612+
# via
613+
# -c requirements/base.txt
614+
# -r requirements/base.txt
607615
o365==2.1.9
608616
# via
609617
# -c requirements/base.txt
@@ -1063,6 +1071,7 @@ typing-extensions==4.15.0
10631071
# django-timeline-logger
10641072
# grpcio
10651073
# mozilla-django-oidc-db
1074+
# nutree
10661075
# opentelemetry-api
10671076
# opentelemetry-exporter-otlp-proto-grpc
10681077
# opentelemetry-exporter-otlp-proto-http

requirements/type-checking.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -680,6 +680,10 @@ msal==1.37.0
680680
# -c requirements/ci.txt
681681
# -r requirements/ci.txt
682682
# o365
683+
msgspec==0.21.0
684+
# via
685+
# -c requirements/ci.txt
686+
# -r requirements/ci.txt
683687
networkx==3.6.1
684688
# via
685689
# -c requirements/ci.txt
@@ -693,6 +697,10 @@ numpy==2.4.2
693697
# -c requirements/ci.txt
694698
# -r requirements/ci.txt
695699
# shapely
700+
nutree==1.1.0
701+
# via
702+
# -c requirements/ci.txt
703+
# -r requirements/ci.txt
696704
o365==2.1.9
697705
# via
698706
# -c requirements/ci.txt
@@ -1281,6 +1289,7 @@ typing-extensions==4.15.0
12811289
# djangorestframework-stubs
12821290
# grpcio
12831291
# mozilla-django-oidc-db
1292+
# nutree
12841293
# opentelemetry-api
12851294
# opentelemetry-exporter-otlp-proto-grpc
12861295
# opentelemetry-exporter-otlp-proto-http

0 commit comments

Comments
 (0)