pydantic-forms 2.7.0__tar.gz → 2.8.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.
Files changed (80) hide show
  1. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/PKG-INFO +1 -1
  2. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/pydantic_forms/utils/json.py +1 -1
  3. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/pydantic_forms/validators/__init__.py +2 -0
  4. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/pydantic_forms/validators/components/accept.py +1 -1
  5. pydantic_forms-2.8.0/pydantic_forms/validators/components/layout.py +68 -0
  6. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/pydantic_forms/validators/components/read_only.py +3 -1
  7. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/pyproject.toml +2 -3
  8. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/pyproject.toml.orig +2 -3
  9. pydantic_forms-2.8.0/tests/unit_tests/test_layout.py +85 -0
  10. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/LICENSE +0 -0
  11. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/README.md +0 -0
  12. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/pydantic_forms/__init__.py +0 -0
  13. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/pydantic_forms/core/__init__.py +0 -0
  14. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/pydantic_forms/core/asynchronous.py +0 -0
  15. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/pydantic_forms/core/shared.py +0 -0
  16. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/pydantic_forms/core/sync.py +0 -0
  17. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/pydantic_forms/core/translations.py +0 -0
  18. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/pydantic_forms/exception_handlers/__init__.py +0 -0
  19. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/pydantic_forms/exception_handlers/fastapi.py +0 -0
  20. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/pydantic_forms/exceptions.py +0 -0
  21. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/pydantic_forms/py.typed +0 -0
  22. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/pydantic_forms/types.py +0 -0
  23. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/pydantic_forms/utils/__init__.py +0 -0
  24. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/pydantic_forms/utils/schema.py +0 -0
  25. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/pydantic_forms/validators/components/__init__.py +0 -0
  26. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/pydantic_forms/validators/components/buttons.py +0 -0
  27. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/pydantic_forms/validators/components/callout.py +0 -0
  28. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/pydantic_forms/validators/components/choice.py +0 -0
  29. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/pydantic_forms/validators/components/choice_list.py +0 -0
  30. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/pydantic_forms/validators/components/contact_person.py +0 -0
  31. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/pydantic_forms/validators/components/contact_person_list.py +0 -0
  32. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/pydantic_forms/validators/components/display_subscription.py +0 -0
  33. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/pydantic_forms/validators/components/divider.py +0 -0
  34. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/pydantic_forms/validators/components/hidden.py +0 -0
  35. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/pydantic_forms/validators/components/label.py +0 -0
  36. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/pydantic_forms/validators/components/list_of_one.py +0 -0
  37. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/pydantic_forms/validators/components/list_of_two.py +0 -0
  38. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/pydantic_forms/validators/components/long_text.py +0 -0
  39. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/pydantic_forms/validators/components/markdown.py +0 -0
  40. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/pydantic_forms/validators/components/migration_summary.py +0 -0
  41. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/pydantic_forms/validators/components/organisation_id.py +0 -0
  42. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/pydantic_forms/validators/components/timestamp.py +0 -0
  43. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/pydantic_forms/validators/components/unique_constrained_list.py +0 -0
  44. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/pydantic_forms/validators/constants.py +0 -0
  45. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/pydantic_forms/validators/helpers.py +0 -0
  46. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/tests/__init__.py +0 -0
  47. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/tests/paths.py +0 -0
  48. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/tests/unit_tests/__init__.py +0 -0
  49. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/tests/unit_tests/exception_handlers/__init__.py +0 -0
  50. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/tests/unit_tests/exception_handlers/test_fastapi.py +0 -0
  51. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/tests/unit_tests/helpers.py +0 -0
  52. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/tests/unit_tests/test_accept.py +0 -0
  53. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/tests/unit_tests/test_buttons.py +0 -0
  54. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/tests/unit_tests/test_callout.py +0 -0
  55. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/tests/unit_tests/test_choice.py +0 -0
  56. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/tests/unit_tests/test_choice_list.py +0 -0
  57. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/tests/unit_tests/test_constrained_list.py +0 -0
  58. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/tests/unit_tests/test_contact_person.py +0 -0
  59. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/tests/unit_tests/test_contact_person_list.py +0 -0
  60. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/tests/unit_tests/test_core.py +0 -0
  61. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/tests/unit_tests/test_core_async.py +0 -0
  62. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/tests/unit_tests/test_default_factory.py +0 -0
  63. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/tests/unit_tests/test_display_subscription.py +0 -0
  64. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/tests/unit_tests/test_divider.py +0 -0
  65. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/tests/unit_tests/test_documentation.py +0 -0
  66. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/tests/unit_tests/test_form_meta.py +0 -0
  67. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/tests/unit_tests/test_hidden.py +0 -0
  68. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/tests/unit_tests/test_label.py +0 -0
  69. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/tests/unit_tests/test_list_of_one.py +0 -0
  70. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/tests/unit_tests/test_list_of_two.py +0 -0
  71. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/tests/unit_tests/test_long_text.py +0 -0
  72. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/tests/unit_tests/test_markdown.py +0 -0
  73. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/tests/unit_tests/test_migration_summary.py +0 -0
  74. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/tests/unit_tests/test_organisation_id.py +0 -0
  75. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/tests/unit_tests/test_read_only_field.py +0 -0
  76. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/tests/unit_tests/test_shared.py +0 -0
  77. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/tests/unit_tests/test_timestamp.py +0 -0
  78. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/tests/unit_tests/test_utils_dict_without_none.py +0 -0
  79. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/tests/unit_tests/test_utils_from_serializable.py +0 -0
  80. {pydantic_forms-2.7.0 → pydantic_forms-2.8.0}/tests/unit_tests/test_utils_to_serializable.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pydantic-forms
3
- Version: 2.7.0
3
+ Version: 2.8.0
4
4
  Summary: Pydantic-forms engine.
5
5
  Author: SURF
6
6
  Author-email: SURF <automation-beheer@surf.nl>
@@ -177,7 +177,7 @@ if IS_ORJSON:
177
177
  logger.debug("Using orjson")
178
178
 
179
179
  def json_loads(s: Union[str, bytes, bytearray]) -> PY_JSON_TYPES:
180
- o = orjson.loads(s)
180
+ o = orjson.loads(bytes(s) if isinstance(s, bytearray) else s)
181
181
  if isinstance(o, list):
182
182
  return [from_serializable(dikt) for dikt in o]
183
183
  return from_serializable(o)
@@ -9,6 +9,7 @@ from pydantic_forms.validators.components.display_subscription import DisplaySub
9
9
  from pydantic_forms.validators.components.divider import Divider
10
10
  from pydantic_forms.validators.components.hidden import Hidden
11
11
  from pydantic_forms.validators.components.label import Label
12
+ from pydantic_forms.validators.components.layout import Layout
12
13
  from pydantic_forms.validators.components.list_of_one import ListOfOne
13
14
  from pydantic_forms.validators.components.list_of_two import ListOfTwo
14
15
  from pydantic_forms.validators.components.long_text import LongText
@@ -33,6 +34,7 @@ __all__ = (
33
34
  "Divider",
34
35
  "Hidden",
35
36
  "Label",
37
+ "Layout",
36
38
  "ListOfOne",
37
39
  "ListOfTwo",
38
40
  "LongText",
@@ -33,7 +33,7 @@ class Accept(str):
33
33
 
34
34
  data: ClassVar[Optional[AcceptData]] = None
35
35
 
36
- _enum_adapter = TypeAdapter(AcceptValues)
36
+ _enum_adapter: ClassVar[TypeAdapter[AcceptValues]] = TypeAdapter(AcceptValues)
37
37
 
38
38
  @classmethod
39
39
  def __get_pydantic_json_schema__(cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler) -> dict[str, Any]:
@@ -0,0 +1,68 @@
1
+ # Copyright 2019-2026 SURF.
2
+ # Licensed under the Apache License, Version 2.0 (the "License");
3
+ # you may not use this file except in compliance with the License.
4
+ # You may obtain a copy of the License at
5
+ #
6
+ # http://www.apache.org/licenses/LICENSE-2.0
7
+ #
8
+ # Unless required by applicable law or agreed to in writing, software
9
+ # distributed under the License is distributed on an "AS IS" BASIS,
10
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ # See the License for the specific language governing permissions and
12
+ # limitations under the License.
13
+ from collections.abc import Iterator
14
+ from dataclasses import dataclass
15
+ from typing import Final, Literal
16
+
17
+ from annotated_types import GroupedMetadata
18
+ from pydantic import Field, JsonValue
19
+
20
+ # Fixed: the pydantic-forms frontend renders a 12-column grid and clamps span and start to it
21
+ GRID_COLUMNS: Final = 12
22
+
23
+ LayoutAlign = Literal["start", "center", "end", "stretch"]
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class Layout(GroupedMetadata):
28
+ """Position a field on the 12-column form grid, e.g. `Annotated[str, Layout(span=6)]`.
29
+
30
+ Adds a camelCase `layout` object to the field's JSON schema.
31
+ Fields without one take the full width.
32
+
33
+ Args:
34
+ span: Width in columns (1-12), full width when unset.
35
+ start: Column (1-12) to start at, leaves the columns before it empty.
36
+ new_row: Always start on a new row, even when the current row has space left.
37
+ row_span: Height in rows, one row when unset. The following fields are placed next to it.
38
+ align: Vertical position of the field within its row.
39
+ """
40
+
41
+ span: int | None = None
42
+ start: int | None = None
43
+ new_row: bool = False
44
+ row_span: int | None = None
45
+ align: LayoutAlign | None = None
46
+
47
+ def __post_init__(self) -> None:
48
+ if self.span is not None and not 1 <= self.span <= GRID_COLUMNS:
49
+ raise ValueError(f"span must be between 1 and {GRID_COLUMNS}")
50
+ if self.start is not None and not 1 <= self.start <= GRID_COLUMNS:
51
+ raise ValueError(f"start must be between 1 and {GRID_COLUMNS}")
52
+ if self.start is not None and self.start + (self.span or 1) - 1 > GRID_COLUMNS:
53
+ raise ValueError(f"start + span exceeds the {GRID_COLUMNS} column grid")
54
+ if self.row_span is not None and self.row_span < 1:
55
+ raise ValueError("row_span must be at least 1")
56
+
57
+ def __iter__(self) -> Iterator[object]:
58
+ options: dict[str, JsonValue] = {
59
+ "span": self.span,
60
+ "start": self.start,
61
+ "newRow": self.new_row or None,
62
+ "rowSpan": self.row_span,
63
+ "align": self.align,
64
+ }
65
+ layout: dict[str, JsonValue] = {key: value for key, value in options.items() if value}
66
+ # An empty Layout() is the same as no Layout, the field takes the full width
67
+ if layout:
68
+ yield Field(json_schema_extra={"layout": layout})
@@ -123,7 +123,9 @@ def read_only_field(default: Any, merge_type: Any | None = None) -> Any:
123
123
  if isinstance(default, list):
124
124
  raise TypeError(f"Use {read_only_list.__name__}")
125
125
 
126
- dynamic_validator = TypeAdapter(default.__class__) if isinstance(default, STRINGY_TYPES) else None
126
+ dynamic_validator: TypeAdapter[Any] | None = (
127
+ TypeAdapter(default.__class__) if isinstance(default, STRINGY_TYPES) else None
128
+ )
127
129
 
128
130
  def validate(v: Any) -> Any:
129
131
  if dynamic_validator:
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "pydantic-forms"
3
3
  description = "Pydantic-forms engine."
4
- version = "2.7.0"
4
+ version = "2.8.0"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
7
7
  license-files = ["LICENSE"]
@@ -92,7 +92,7 @@ dev = [
92
92
  ]
93
93
 
94
94
  [build-system]
95
- requires = ["uv_build>=0.10,<0.13"]
95
+ requires = ["uv_build>=0.13,<0.14"]
96
96
  build-backend = "uv_build"
97
97
 
98
98
  [tool.uv]
@@ -240,7 +240,6 @@ disallow_incomplete_defs = false
240
240
  init_forbid_extra = true
241
241
  init_typed = true
242
242
  warn_required_dynamic_aliases = true
243
- warn_untyped_fields = true
244
243
 
245
244
  [tool.coverage.run]
246
245
  source = ["pydantic_forms"]
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "pydantic-forms"
3
3
  description = "Pydantic-forms engine."
4
- version = "2.7.0"
4
+ version = "2.8.0"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
7
7
  license-files = ["LICENSE"]
@@ -101,7 +101,7 @@ dev = [
101
101
  ]
102
102
 
103
103
  [build-system]
104
- requires = ["uv_build>=0.10,<0.13"]
104
+ requires = ["uv_build>=0.13,<0.14"]
105
105
  build-backend = "uv_build"
106
106
 
107
107
  [tool.uv]
@@ -238,7 +238,6 @@ disallow_incomplete_defs = false
238
238
  init_forbid_extra = true
239
239
  init_typed = true
240
240
  warn_required_dynamic_aliases = true
241
- warn_untyped_fields = true
242
241
 
243
242
  [tool.coverage.run]
244
243
  source = ["pydantic_forms"]
@@ -0,0 +1,85 @@
1
+ from typing import Annotated
2
+
3
+ import pytest
4
+ from pydantic import BaseModel, Field
5
+
6
+ from pydantic_forms.core import FormPage
7
+ from pydantic_forms.validators import Layout, LongText
8
+
9
+
10
+ def test_layout_json_schema_leaves_out_unset_options():
11
+ class Form(FormPage):
12
+ name: Annotated[str, Layout(span=6, new_row=True, align="end")]
13
+ city: Annotated[str, Layout(start=4)]
14
+ full: Annotated[str, Layout()]
15
+ tall: Annotated[str, Layout(span=6, row_span=2)]
16
+
17
+ properties = Form.model_json_schema()["properties"]
18
+ assert properties["name"]["layout"] == {"span": 6, "newRow": True, "align": "end"}
19
+ assert properties["city"]["layout"] == {"start": 4}
20
+ assert "layout" not in properties["full"]
21
+ assert properties["tall"]["layout"] == {"span": 6, "rowSpan": 2}
22
+
23
+
24
+ def test_layout_combines_with_other_schema_extras():
25
+ class Form(FormPage):
26
+ comments: Annotated[LongText, Layout(span=6)] = Field(title="Comments")
27
+
28
+ assert Form.model_json_schema()["properties"]["comments"] == {
29
+ "format": "long",
30
+ "layout": {"span": 6},
31
+ "title": "Comments",
32
+ "type": "string",
33
+ }
34
+
35
+
36
+ def test_layout_in_nested_model():
37
+ class Address(BaseModel):
38
+ street: Annotated[str, Layout(span=8)]
39
+ postal_code: Annotated[str, Layout(span=4)]
40
+
41
+ class Form(FormPage):
42
+ address: Address
43
+
44
+ properties = Form.model_json_schema()["$defs"]["Address"]["properties"]
45
+ assert properties["street"]["layout"] == {"span": 8}
46
+ assert properties["postal_code"]["layout"] == {"span": 4}
47
+
48
+
49
+ def test_layout_does_not_affect_validation():
50
+ class Form(FormPage):
51
+ name: Annotated[str, Layout(span=6)]
52
+
53
+ assert Form(name="Jane").model_dump() == {"name": "Jane"}
54
+
55
+
56
+ @pytest.mark.parametrize(
57
+ "kwargs",
58
+ [
59
+ {"span": 0},
60
+ {"span": 13},
61
+ {"start": 0},
62
+ {"start": 13},
63
+ {"start": 10, "span": 6},
64
+ {"start": 12, "span": 2},
65
+ {"row_span": 0},
66
+ ],
67
+ )
68
+ def test_layout_rejects_positions_outside_the_grid(kwargs):
69
+ with pytest.raises(ValueError):
70
+ Layout(**kwargs)
71
+
72
+
73
+ @pytest.mark.parametrize(
74
+ "kwargs",
75
+ [
76
+ {"span": 1},
77
+ {"span": 12},
78
+ {"start": 12},
79
+ {"start": 7, "span": 6},
80
+ {"start": 1, "span": 12},
81
+ {"row_span": 3},
82
+ ],
83
+ )
84
+ def test_layout_accepts_positions_within_the_grid(kwargs):
85
+ Layout(**kwargs)
File without changes
File without changes