pydantic-forms 2.2.0__tar.gz → 2.3.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 (81) hide show
  1. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/.bumpversion.cfg +1 -1
  2. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/.github/workflows/run-linting-tests.yml +7 -10
  3. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/.github/workflows/run-unit-tests.yml +10 -12
  4. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/.pre-commit-config.yaml +6 -8
  5. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/PKG-INFO +5 -5
  6. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/pydantic_forms/__init__.py +1 -1
  7. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/pydantic_forms/core/asynchronous.py +1 -1
  8. pydantic_forms-2.3.0/pydantic_forms/core/shared.py +113 -0
  9. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/pydantic_forms/core/sync.py +1 -1
  10. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/pydantic_forms/types.py +3 -8
  11. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/pydantic_forms/validators/components/read_only.py +2 -2
  12. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/pyproject.toml +18 -13
  13. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/setup.cfg +1 -2
  14. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/tests/unit_tests/helpers.py +1 -1
  15. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/tests/unit_tests/test_accept.py +0 -2
  16. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/tests/unit_tests/test_choice_list.py +1 -7
  17. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/tests/unit_tests/test_constrained_list.py +3 -13
  18. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/tests/unit_tests/test_display_subscription.py +1 -7
  19. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/tests/unit_tests/test_list_of_two.py +4 -11
  20. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/tests/unit_tests/test_migration_summary.py +1 -7
  21. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/tests/unit_tests/test_read_only_field.py +3 -5
  22. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/tests/unit_tests/test_shared.py +1 -3
  23. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/tests/unit_tests/test_utils_from_serializable.py +0 -1
  24. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/tests/unit_tests/test_utils_to_serializable.py +4 -3
  25. pydantic_forms-2.2.0/pydantic_forms/core/shared.py +0 -91
  26. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/.coveragerc +0 -0
  27. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/.github/dependabot.yml +0 -0
  28. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/.github/workflows/publish-release.yaml +0 -0
  29. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/.gitignore +0 -0
  30. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/.stignore +0 -0
  31. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/LICENSE +0 -0
  32. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/README.md +0 -0
  33. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/codecov.yml +0 -0
  34. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/pydantic_forms/core/__init__.py +0 -0
  35. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/pydantic_forms/core/translations.py +0 -0
  36. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/pydantic_forms/exception_handlers/__init__.py +0 -0
  37. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/pydantic_forms/exception_handlers/fastapi.py +0 -0
  38. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/pydantic_forms/exceptions.py +0 -0
  39. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/pydantic_forms/py.typed +0 -0
  40. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/pydantic_forms/utils/__init__.py +0 -0
  41. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/pydantic_forms/utils/json.py +0 -0
  42. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/pydantic_forms/utils/schema.py +0 -0
  43. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/pydantic_forms/validators/__init__.py +0 -0
  44. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/pydantic_forms/validators/components/__init__.py +0 -0
  45. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/pydantic_forms/validators/components/accept.py +0 -0
  46. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/pydantic_forms/validators/components/callout.py +0 -0
  47. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/pydantic_forms/validators/components/choice.py +0 -0
  48. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/pydantic_forms/validators/components/choice_list.py +0 -0
  49. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/pydantic_forms/validators/components/contact_person.py +0 -0
  50. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/pydantic_forms/validators/components/contact_person_list.py +0 -0
  51. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/pydantic_forms/validators/components/display_subscription.py +0 -0
  52. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/pydantic_forms/validators/components/divider.py +0 -0
  53. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/pydantic_forms/validators/components/hidden.py +0 -0
  54. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/pydantic_forms/validators/components/label.py +0 -0
  55. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/pydantic_forms/validators/components/list_of_one.py +0 -0
  56. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/pydantic_forms/validators/components/list_of_two.py +0 -0
  57. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/pydantic_forms/validators/components/long_text.py +0 -0
  58. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/pydantic_forms/validators/components/migration_summary.py +0 -0
  59. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/pydantic_forms/validators/components/organisation_id.py +0 -0
  60. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/pydantic_forms/validators/components/timestamp.py +0 -0
  61. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/pydantic_forms/validators/components/unique_constrained_list.py +0 -0
  62. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/pydantic_forms/validators/helpers.py +0 -0
  63. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/setup.py +0 -0
  64. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/tests/__init__.py +0 -0
  65. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/tests/unit_tests/__init__.py +0 -0
  66. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/tests/unit_tests/exception_handlers/__init__.py +0 -0
  67. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/tests/unit_tests/exception_handlers/test_fastapi.py +0 -0
  68. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/tests/unit_tests/test_callout.py +0 -0
  69. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/tests/unit_tests/test_choice.py +0 -0
  70. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/tests/unit_tests/test_contact_person.py +0 -0
  71. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/tests/unit_tests/test_contact_person_list.py +0 -0
  72. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/tests/unit_tests/test_core.py +0 -0
  73. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/tests/unit_tests/test_core_async.py +0 -0
  74. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/tests/unit_tests/test_divider.py +0 -0
  75. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/tests/unit_tests/test_hidden.py +0 -0
  76. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/tests/unit_tests/test_label.py +0 -0
  77. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/tests/unit_tests/test_list_of_one.py +0 -0
  78. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/tests/unit_tests/test_long_text.py +0 -0
  79. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/tests/unit_tests/test_organisation_id.py +0 -0
  80. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/tests/unit_tests/test_timestamp.py +0 -0
  81. {pydantic_forms-2.2.0 → pydantic_forms-2.3.0}/tests/unit_tests/test_utils_dict_without_none.py +0 -0
@@ -1,5 +1,5 @@
1
1
  [bumpversion]
2
- current_version = 2.2.0
2
+ current_version = 2.3.0
3
3
  commit = False
4
4
  tag = False
5
5
  parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)((\-rc)(?P<build>\d+))?
@@ -15,7 +15,7 @@ jobs:
15
15
  runs-on: ubuntu-latest
16
16
  strategy:
17
17
  matrix:
18
- python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
18
+ python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
19
19
  fail-fast: false
20
20
 
21
21
  steps:
@@ -29,12 +29,9 @@ jobs:
29
29
  python -m pip install --upgrade pip
30
30
  pip install flit
31
31
  flit install --deps develop --extras fastapi
32
- - name: Check formatting
33
- run: |
34
- python -V | grep 10 || black --check .
35
- - name: Lint with ruff
36
- run: |
37
- ruff check .
38
- - name: Check with mypy
39
- run: |
40
- mypy pydantic_forms
32
+ - name: Run pre-commit hooks
33
+ uses: pre-commit/action@v3.0.1
34
+ # Use pre-commit mypy for now
35
+ # - name: Check with mypy
36
+ # run: |
37
+ # mypy pydantic_forms
@@ -9,35 +9,33 @@ jobs:
9
9
  runs-on: ubuntu-latest
10
10
  strategy:
11
11
  matrix:
12
- python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
12
+ python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
13
13
  pydantic-version: ['lockfile']
14
14
  fastapi-version: ['lockfile']
15
15
  # Test specific older releases to avoid an extremely large test matrix
16
16
  include:
17
- # Older pydantic releases with the oldest + latest supported python version
17
+ # Older pydantic releases with the oldest + second-latest supported python version.
18
+ # (using python 3.14 requires pydantic 2.12 which is currently the latest)
19
+ - python-version: '3.13'
20
+ pydantic-version: '2.11.10'
21
+ fastapi-version: 'lockfile'
18
22
  - python-version: '3.13'
19
23
  pydantic-version: '2.10.6'
20
24
  fastapi-version: 'lockfile'
21
- - python-version: '3.9'
25
+ - python-version: '3.10'
22
26
  pydantic-version: '2.10.6'
23
27
  fastapi-version: 'lockfile'
24
28
  - python-version: '3.13'
25
29
  pydantic-version: '2.9.2'
26
30
  fastapi-version: 'lockfile'
27
- - python-version: '3.9'
31
+ - python-version: '3.10'
28
32
  pydantic-version: '2.9.2'
29
33
  fastapi-version: 'lockfile'
30
- - python-version: '3.13'
31
- pydantic-version: '2.8.2'
32
- fastapi-version: 'lockfile'
33
- - python-version: '3.9'
34
- pydantic-version: '2.8.2'
35
- fastapi-version: 'lockfile'
36
34
  # Older fastapi releases with the oldest + latest supported python version
37
- - python-version: '3.13'
35
+ - python-version: '3.14'
38
36
  pydantic-version: 'lockfile'
39
37
  fastapi-version: '0.103.2'
40
- - python-version: '3.9'
38
+ - python-version: '3.10'
41
39
  pydantic-version: 'lockfile'
42
40
  fastapi-version: '0.103.2'
43
41
  fail-fast: false
@@ -1,22 +1,21 @@
1
1
  repos:
2
2
  - repo: https://github.com/psf/black
3
- rev: 25.1.0
3
+ rev: 25.11.0
4
4
  hooks:
5
5
  - id: black
6
- language_version: python3.9
7
6
  - repo: https://github.com/asottile/blacken-docs
8
- rev: 1.19.1
7
+ rev: 1.20.0
9
8
  hooks:
10
9
  - id: blacken-docs
11
10
  additional_dependencies: [black==24.2.0]
12
11
  - repo: https://github.com/astral-sh/ruff-pre-commit
13
12
  # Ruff version.
14
- rev: v0.9.10
13
+ rev: v0.14.5
15
14
  hooks:
16
15
  - id: ruff
17
16
  args: [--fix, --exit-non-zero-on-fix, --show-fixes]
18
17
  - repo: https://github.com/pre-commit/pre-commit-hooks
19
- rev: v5.0.0
18
+ rev: v6.0.0
20
19
  hooks:
21
20
  - id: trailing-whitespace
22
21
  - id: end-of-file-fixer
@@ -28,10 +27,9 @@ repos:
28
27
  - id: requirements-txt-fixer
29
28
  - id: detect-private-key
30
29
  - repo: https://github.com/pre-commit/mirrors-mypy
31
- rev: v1.15.0
30
+ rev: v1.18.2
32
31
  hooks:
33
32
  - id: mypy
34
- language_version: python3.9
35
33
  additional_dependencies:
36
34
  - pydantic
37
35
  - types-toml
@@ -50,6 +48,6 @@ repos:
50
48
  - id: python-check-mock-methods
51
49
  - id: rst-backticks
52
50
  - repo: https://github.com/shellcheck-py/shellcheck-py
53
- rev: v0.10.0.1
51
+ rev: v0.11.0.1
54
52
  hooks:
55
53
  - id: shellcheck
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pydantic-forms
3
- Version: 2.2.0
3
+ Version: 2.3.0
4
4
  Summary: Pydantic-forms engine.
5
5
  Home-page: https://github.com/workfloworchestrator/pydantic-forms
6
6
  Author: SURF
7
7
  Author-email: automation-beheer@surf.nl
8
- Requires-Python: >=3.9,<=3.14
8
+ Requires-Python: >=3.10,<3.15
9
9
  Description-Content-Type: text/markdown
10
10
  Classifier: Intended Audience :: Information Technology
11
11
  Classifier: Intended Audience :: System Administrators
@@ -28,16 +28,16 @@ Classifier: Intended Audience :: Developers
28
28
  Classifier: Intended Audience :: Telecommunications Industry
29
29
  Classifier: License :: OSI Approved :: Apache Software License
30
30
  Classifier: Programming Language :: Python :: 3 :: Only
31
- Classifier: Programming Language :: Python :: 3.9
32
31
  Classifier: Programming Language :: Python :: 3.10
33
32
  Classifier: Programming Language :: Python :: 3.11
34
33
  Classifier: Programming Language :: Python :: 3.12
35
34
  Classifier: Programming Language :: Python :: 3.13
35
+ Classifier: Programming Language :: Python :: 3.14
36
36
  Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
37
37
  Classifier: Topic :: Internet :: WWW/HTTP
38
38
  License-File: LICENSE
39
39
  Requires-Dist: more-itertools
40
- Requires-Dist: pydantic[email]>=2.7.4
40
+ Requires-Dist: pydantic[email]>=2.9.0
41
41
  Requires-Dist: pydantic-i18n==0.4.5
42
42
  Requires-Dist: toml ; extra == "dev"
43
43
  Requires-Dist: bumpversion ; extra == "dev"
@@ -56,7 +56,7 @@ Requires-Dist: apache-license-check ; extra == "test"
56
56
  Requires-Dist: black ; extra == "test"
57
57
  Requires-Dist: structlog ; extra == "test"
58
58
  Requires-Dist: jsonref ; extra == "test"
59
- Requires-Dist: mypy==1.6.1 ; extra == "test"
59
+ Requires-Dist: mypy==1.18.2 ; extra == "test"
60
60
  Requires-Dist: pytest ; extra == "test"
61
61
  Requires-Dist: pytest-cov ; extra == "test"
62
62
  Requires-Dist: pytest-asyncio ; extra == "test"
@@ -13,4 +13,4 @@
13
13
 
14
14
  """Pydantic-forms engine."""
15
15
 
16
- __version__ = "2.2.0"
16
+ __version__ = "2.3.0"
@@ -84,7 +84,7 @@ async def post_form(
84
84
  except ValidationError as e:
85
85
  # Todo: add extra_translation to tr
86
86
  tr = PydanticI18n(translations)
87
- raise FormValidationError(generated_form.__name__, e, tr, locale) from e # type: ignore
87
+ raise FormValidationError(generated_form.__name__, e, tr, locale) from e
88
88
 
89
89
  # Update state with validated_data
90
90
  current_state.update(form_validated_data.model_dump())
@@ -0,0 +1,113 @@
1
+ # Copyright 2019-2023 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 inspect import isasyncgenfunction, isgeneratorfunction
14
+ from typing import Any, Callable
15
+
16
+ import structlog
17
+ from pydantic import BaseModel, ConfigDict, PydanticUndefinedAnnotation, version
18
+
19
+ logger = structlog.get_logger(__name__)
20
+
21
+
22
+ PYDANTIC_VERSION = version.version_short()
23
+
24
+
25
+ class DisplayOnlyFieldType:
26
+ pass
27
+
28
+
29
+ class FormPage(BaseModel):
30
+ model_config = ConfigDict(
31
+ arbitrary_types_allowed=True,
32
+ title="unknown",
33
+ extra="forbid",
34
+ validate_default=True,
35
+ )
36
+
37
+ def __init__(self, **data: Any):
38
+ frozen_fields = {k: v for k, v in self.__class__.model_fields.items() if v.frozen}
39
+
40
+ def get_value(k: str, v: Any) -> Any:
41
+ if k in frozen_fields:
42
+ return frozen_fields[k].default
43
+ return v
44
+
45
+ mutable_data = {k: get_value(k, v) for k, v in data.items()}
46
+ super().__init__(**mutable_data)
47
+
48
+ if PYDANTIC_VERSION in ("2.9", "2.10", "2.11"):
49
+
50
+ @classmethod
51
+ def __pydantic_init_subclass__(cls, /, **kwargs: Any) -> None:
52
+ # The default and requiredness of a field is not a property of a field
53
+ # In the case of DisplayOnlyFieldTypes, we do kind of want that.
54
+ # Using this method we set the right properties after the form is created
55
+ needs_rebuild = False
56
+
57
+ for field in cls.model_fields.values():
58
+ if field.frozen:
59
+ field.validate_default = False
60
+ needs_rebuild = True
61
+
62
+ if needs_rebuild:
63
+ try:
64
+ # Fix for #39:
65
+ # Core schema used during validation is constructed before __pydantic_init_subclass__ which means
66
+ # that the field.validate_default change above doesn't take effect.
67
+ # As a workaround, we explicitly rebuild the model to reconstruct the core schema.
68
+ #
69
+ # Downside is that unresolved forward-refs trigger an exception; for now we catch/log/ignore it.
70
+ # From pydantic 2.12 a new hook __pydantic_on_complete__ can be used to perform the rebuild instead.
71
+ # https://github.com/pydantic/pydantic/pull/11762
72
+ cls.model_rebuild(force=True)
73
+ except PydanticUndefinedAnnotation as exc:
74
+ logger.warning(
75
+ "Failed to rebuild model due to undefined annotation, frozen fields may not work as expected",
76
+ undefined_annotation=exc.name,
77
+ model=cls.__name__,
78
+ )
79
+
80
+ else: # pydantic >= 2.12
81
+
82
+ @classmethod
83
+ def __pydantic_on_complete__(cls) -> None:
84
+ # The default and requiredness of a field is not a property of a field
85
+ # In the case of DisplayOnlyFieldTypes, we do kind of want that.
86
+ # Using this method we set the right properties after the form is created
87
+ needs_rebuild = False
88
+
89
+ for field in cls.model_fields.values():
90
+ if field.frozen:
91
+ field.validate_default = False
92
+ needs_rebuild = True
93
+
94
+ if needs_rebuild:
95
+ cls.model_rebuild(force=True)
96
+
97
+
98
+ FORMS: dict[str, Callable] = {}
99
+
100
+
101
+ def register_form(key: str, form: Callable) -> None:
102
+ logger.info("Current Forms", forms=FORMS, new_key=key)
103
+ if key in FORMS and form is not FORMS[key]:
104
+ raise Exception(f"Trying to re-register form {key} with a different function")
105
+
106
+ if not isasyncgenfunction(form) and not isgeneratorfunction(form):
107
+ raise Exception(f"Trying to register form {key} function {form} which is not an (async) generator function")
108
+
109
+ FORMS[key] = form
110
+
111
+
112
+ def list_forms() -> list[str]:
113
+ return list(FORMS.keys())
@@ -79,7 +79,7 @@ def post_form(
79
79
  except ValidationError as e:
80
80
  # Todo: add extra_translation to tr
81
81
  tr = PydanticI18n(translations)
82
- raise FormValidationError(generated_form.__name__, e, tr, locale) from e # type: ignore
82
+ raise FormValidationError(generated_form.__name__, e, tr, locale) from e
83
83
 
84
84
  # Update state with validated_data
85
85
  current_state.update(form_validated_data.model_dump())
@@ -1,16 +1,11 @@
1
1
  from enum import Enum
2
+ from types import UnionType
2
3
  from typing import Any, AsyncGenerator, Callable, Generator, Type, TypeVar, Union
3
4
 
5
+ from pydantic.main import BaseModel
4
6
  from typing_extensions import TypedDict
5
7
 
6
- try:
7
- # python3.10 introduces types.UnionType for the new union and optional type defs.
8
- from types import UnionType # type: ignore # 3.9 compatibility
9
-
10
- union_types = [Union, UnionType]
11
- except ImportError:
12
- union_types = [Union]
13
- from pydantic.main import BaseModel
8
+ union_types = [Union, UnionType]
14
9
 
15
10
  UUIDstr = str
16
11
  State = dict[str, Any]
@@ -93,7 +93,7 @@ def read_only_list(default: list[Any] | None = None) -> Any:
93
93
 
94
94
  return Annotated[
95
95
  list[default_item_type],
96
- Field(default, json_schema_extra=json_schema), # type: ignore[arg-type]
96
+ Field(default, json_schema_extra=json_schema),
97
97
  AfterValidator(validate_list),
98
98
  PlainSerializer(serialize_list, when_used="json"),
99
99
  ]
@@ -131,7 +131,7 @@ def read_only_field(default: Any, merge_type: Any | None = None) -> Any:
131
131
 
132
132
  read_only_type = Annotated[
133
133
  Literal[default], # type: ignore[valid-type]
134
- Field(default, json_schema_extra=json_schema), # type: ignore[arg-type]
134
+ Field(default, json_schema_extra=json_schema),
135
135
  BeforeValidator(validate),
136
136
  PlainSerializer(serialize_json, when_used="json"),
137
137
  ]
@@ -30,21 +30,21 @@ classifiers = [
30
30
  "Intended Audience :: Telecommunications Industry",
31
31
  "License :: OSI Approved :: Apache Software License",
32
32
  "Programming Language :: Python :: 3 :: Only",
33
- "Programming Language :: Python :: 3.9",
34
33
  "Programming Language :: Python :: 3.10",
35
34
  "Programming Language :: Python :: 3.11",
36
35
  "Programming Language :: Python :: 3.12",
37
36
  "Programming Language :: Python :: 3.13",
37
+ "Programming Language :: Python :: 3.14",
38
38
  "Topic :: Internet :: WWW/HTTP :: HTTP Servers",
39
39
  "Topic :: Internet :: WWW/HTTP",
40
40
  ]
41
41
  requires = [
42
42
  "more-itertools",
43
- "pydantic[email]>=2.7.4",
43
+ "pydantic[email]>=2.9.0",
44
44
  "pydantic-i18n==0.4.5"
45
45
  ]
46
46
  description-file = "README.md"
47
- requires-python = ">=3.9,<=3.14"
47
+ requires-python = ">=3.10,<3.15"
48
48
 
49
49
  [tool.flit.metadata.urls]
50
50
  Documentation = "https://github.com/workfloworchestrator/pydantic-forms/blob/main/README.md"
@@ -55,7 +55,7 @@ test = [
55
55
  "black",
56
56
  "structlog",
57
57
  "jsonref",
58
- "mypy==1.6.1",
58
+ "mypy==1.18.2",
59
59
  "pytest",
60
60
  "pytest-cov",
61
61
  "pytest-asyncio",
@@ -96,7 +96,7 @@ fastapi = [
96
96
 
97
97
  [tool.black]
98
98
  line-length = 120
99
- target-version = ["py39"]
99
+ target-version = ["py310"]
100
100
  exclude = '''
101
101
  (
102
102
  /(
@@ -126,9 +126,10 @@ exclude = [
126
126
  "venv",
127
127
  ".venv",
128
128
  "docs",
129
- "tests"
130
129
  ]
131
130
  lint.ignore = [
131
+ "B904",
132
+ "B905",
132
133
  "C417",
133
134
  "D100",
134
135
  "D101",
@@ -140,18 +141,22 @@ lint.ignore = [
140
141
  "D107",
141
142
  "D202",
142
143
  "E501",
143
- "N806",
144
- "B905",
145
- "N805",
146
- "B904",
147
- "N803",
148
144
  "N801",
149
- "N815",
150
145
  "N802",
146
+ "N803",
147
+ "N805",
148
+ "N806",
149
+ "N815",
150
+ "S101", # use of assert
151
151
  ]
152
152
  line-length = 120
153
153
  lint.select = ["B", "C", "D", "E", "F", "I", "N", "RET", "S", "T", "W"]
154
- target-version = "py39"
154
+ target-version = "py310"
155
+
156
+ [tool.ruff.lint.per-file-ignores]
157
+ "tests/*" = [
158
+ "D205", # blank line required
159
+ ]
155
160
 
156
161
  [tool.lint.flake8-tidy-imports]
157
162
  ban-relative-imports = "all"
@@ -36,8 +36,7 @@ disallow_untyped_decorators = True
36
36
  no_implicit_optional = True
37
37
  strict_optional = True
38
38
  namespace_packages = true
39
- ; TODO re-enable unused ignores when we stop supporting 3.9 https://github.com/python/mypy/issues/8823
40
- warn_unused_ignores = False
39
+ warn_unused_ignores = True
41
40
  warn_redundant_casts = True
42
41
  ; Disable due to flaky results in mypy itself
43
42
  ;warn_return_any = True
@@ -7,7 +7,7 @@ def assert_equal_ignore_key(expected, actual, ignore_keys):
7
7
  def deep_remove_keys(d, keys_to_ignore):
8
8
  if isinstance(d, dict):
9
9
  return {k: deep_remove_keys(v, keys_to_ignore) for k, v in d.items() if k not in keys_to_ignore}
10
- elif isinstance(d, list):
10
+ if isinstance(d, list):
11
11
  return [deep_remove_keys(i, keys_to_ignore) for i in d]
12
12
  return d
13
13
 
@@ -1,5 +1,3 @@
1
- from unittest.mock import ANY
2
-
3
1
  from pydantic_core import ValidationError
4
2
  from pytest import raises
5
3
 
@@ -4,8 +4,6 @@ from pydantic_core import ValidationError
4
4
  from pydantic_forms.core import FormPage
5
5
  from pydantic_forms.validators import Choice, choice_list
6
6
 
7
- from tests.unit_tests.helpers import PYDANTIC_VERSION
8
-
9
7
 
10
8
  def test_choice_list():
11
9
  class LegChoice(Choice):
@@ -152,15 +150,11 @@ def test_choice_list_constraint_at_least_one_item(Form):
152
150
 
153
151
  errors = exc_info.value.errors(include_url=False, include_context=False)
154
152
 
155
- if PYDANTIC_VERSION == "2.8":
156
- message = "List should have at least 1 item after validation, not 0"
157
- else:
158
- message = "Value should have at least 1 item after validation, not 0"
159
153
  expected = [
160
154
  {
161
155
  "input": [],
162
156
  "loc": ("choice",),
163
- "msg": message,
157
+ "msg": "Value should have at least 1 item after validation, not 0",
164
158
  "type": "too_short",
165
159
  }
166
160
  ]
@@ -3,9 +3,6 @@ from pytest import raises
3
3
 
4
4
  from pydantic_forms.core import FormPage
5
5
  from pydantic_forms.validators import unique_conlist
6
- from pydantic.version import version_short as pydantic_version_short
7
-
8
- from tests.unit_tests.helpers import PYDANTIC_VERSION
9
6
 
10
7
 
11
8
  def test_constrained_list_good():
@@ -77,15 +74,11 @@ def test_constrained_list_too_short():
77
74
 
78
75
  errors = exc_info.value.errors(include_url=False, include_context=False)
79
76
 
80
- if PYDANTIC_VERSION == "2.8":
81
- message = "List should have at least 1 item after validation, not 0"
82
- else:
83
- message = "Value should have at least 1 item after validation, not 0"
84
77
  expected = [
85
78
  {
86
79
  "input": [],
87
80
  "loc": ("v",),
88
- "msg": message,
81
+ "msg": "Value should have at least 1 item after validation, not 0",
89
82
  "type": "too_short",
90
83
  }
91
84
  ]
@@ -117,15 +110,12 @@ def test_constrained_list_inherit_constraints():
117
110
  UniqueConListModel(v=[])
118
111
 
119
112
  errors = exc_info.value.errors(include_url=False, include_context=False)
120
- if PYDANTIC_VERSION == "2.8":
121
- message = "List should have at least 1 item after validation, not 0"
122
- else:
123
- message = "Value should have at least 1 item after validation, not 0"
113
+
124
114
  assert errors == [
125
115
  {
126
116
  "input": [],
127
117
  "loc": ("v",),
128
- "msg": message,
118
+ "msg": "Value should have at least 1 item after validation, not 0",
129
119
  "type": "too_short",
130
120
  # "ctx": {"limit_value": 1},
131
121
  }
@@ -2,7 +2,6 @@ from uuid import uuid4
2
2
 
3
3
  from pydantic_forms.core import FormPage
4
4
  from pydantic_forms.validators import DisplaySubscription, Label, migration_summary
5
- from tests.unit_tests.helpers import PYDANTIC_VERSION
6
5
 
7
6
 
8
7
  def test_display_subscription():
@@ -36,11 +35,6 @@ def test_display_only_schema():
36
35
  label: Label
37
36
  summary: Summary
38
37
 
39
- if PYDANTIC_VERSION == "2.8":
40
- summary_ref = {"allOf": [{"$ref": "#/$defs/MigrationSummaryValue"}]}
41
- else:
42
- summary_ref = {"$ref": "#/$defs/MigrationSummaryValue"}
43
-
44
38
  expected = {
45
39
  "$defs": {"MigrationSummaryValue": {"properties": {}, "title": "MigrationSummaryValue", "type": "object"}},
46
40
  "additionalProperties": False,
@@ -59,7 +53,7 @@ def test_display_only_schema():
59
53
  "type": "string",
60
54
  },
61
55
  "summary": {
62
- **summary_ref,
56
+ "$ref": "#/$defs/MigrationSummaryValue",
63
57
  "default": None,
64
58
  "format": "summary",
65
59
  "type": "string",
@@ -3,7 +3,6 @@ from pydantic import ValidationError
3
3
 
4
4
  from pydantic_forms.core import FormPage
5
5
  from pydantic_forms.validators import ListOfTwo
6
- from tests.unit_tests.helpers import PYDANTIC_VERSION
7
6
 
8
7
 
9
8
  @pytest.fixture(name="Form")
@@ -23,15 +22,12 @@ def test_list_of_two_min_items(Form):
23
22
  assert Form(two=[1])
24
23
 
25
24
  errors = error_info.value.errors(include_url=False, include_context=False)
26
- if PYDANTIC_VERSION == "2.8":
27
- message = "List should have at least 2 items after validation, not 1"
28
- else:
29
- message = "Value should have at least 2 items after validation, not 1"
25
+
30
26
  expected = [
31
27
  {
32
28
  "input": [1],
33
29
  "loc": ("two",),
34
- "msg": message,
30
+ "msg": "Value should have at least 2 items after validation, not 1",
35
31
  "type": "too_short",
36
32
  }
37
33
  ]
@@ -43,15 +39,12 @@ def test_list_of_two_max_items(Form):
43
39
  assert Form(two=[1, 2, 3])
44
40
 
45
41
  errors = error_info.value.errors(include_url=False, include_context=False)
46
- if PYDANTIC_VERSION == "2.8":
47
- message = "List should have at most 2 items after validation, not 3"
48
- else:
49
- message = "Value should have at most 2 items after validation, not 3"
42
+
50
43
  expected = [
51
44
  {
52
45
  "input": [1, 2, 3],
53
46
  "loc": ("two",),
54
- "msg": message,
47
+ "msg": "Value should have at most 2 items after validation, not 3",
55
48
  "type": "too_long",
56
49
  },
57
50
  ]
@@ -2,7 +2,6 @@ from uuid import uuid4
2
2
 
3
3
  from pydantic_forms.core import FormPage
4
4
  from pydantic_forms.validators import DisplaySubscription, Label, migration_summary
5
- from tests.unit_tests.helpers import PYDANTIC_VERSION
6
5
 
7
6
 
8
7
  def test_display_default():
@@ -34,17 +33,12 @@ def test_migration_summary_schema():
34
33
  class Form(FormPage):
35
34
  ms: Summary
36
35
 
37
- if PYDANTIC_VERSION == "2.8":
38
- ms_ref = {"allOf": [{"$ref": "#/$defs/MigrationSummaryValue"}]}
39
- else:
40
- ms_ref = {"$ref": "#/$defs/MigrationSummaryValue"}
41
-
42
36
  expected = {
43
37
  "$defs": {"MigrationSummaryValue": {"properties": {}, "title": "MigrationSummaryValue", "type": "object"}},
44
38
  "additionalProperties": False,
45
39
  "properties": {
46
40
  "ms": {
47
- **ms_ref,
41
+ "$ref": "#/$defs/MigrationSummaryValue",
48
42
  "default": None,
49
43
  "format": "summary",
50
44
  "type": "string",
@@ -1,16 +1,14 @@
1
1
  import json
2
2
  from uuid import UUID
3
3
 
4
- from pydantic.config import JsonDict
5
4
  import pytest
6
5
  from pydantic import BaseModel, ValidationError
7
-
8
- from pydantic.version import version_short as pydantic_version_short
6
+ from pydantic.config import JsonDict
9
7
 
10
8
  from pydantic_forms.core import FormPage
11
9
  from pydantic_forms.types import strEnum
12
- from pydantic_forms.validators import read_only_field, read_only_list, LongText, OrganisationId
13
10
  from pydantic_forms.utils.schema import merge_json_schema
11
+ from pydantic_forms.validators import LongText, OrganisationId, read_only_field, read_only_list
14
12
  from tests.unit_tests.helpers import PYDANTIC_VERSION
15
13
 
16
14
 
@@ -36,7 +34,7 @@ def test_read_only_field_schema(read_only_value, schema_value, schema_type, othe
36
34
  class Form(FormPage):
37
35
  read_only: read_only_field(read_only_value)
38
36
 
39
- if PYDANTIC_VERSION in ("2.8", "2.9"):
37
+ if PYDANTIC_VERSION in ("2.9",):
40
38
  # Field that was removed in 2.10 https://github.com/pydantic/pydantic/pull/10692
41
39
  enum_value = str(read_only_value) if isinstance(read_only_value, UUID) else read_only_value
42
40
  enum = {"enum": [enum_value]}
@@ -1,7 +1,5 @@
1
- from typing import Annotated
2
-
3
1
  import pytest
4
- from pydantic import Field, BaseModel
2
+ from pydantic import Field
5
3
  from pydantic_core import ValidationError
6
4
 
7
5
  from pydantic_forms.core import FormPage
@@ -1,4 +1,3 @@
1
- import pytest
2
1
  from datetime import datetime
3
2
 
4
3
  from pydantic_forms.utils.json import from_serializable
@@ -1,12 +1,13 @@
1
1
  # Test UUID serialization
2
- from dataclasses import dataclass, asdict
2
+ from dataclasses import asdict, dataclass
3
3
  from datetime import datetime
4
- from ipaddress import IPv4Network, IPv6Network, IPv4Address, IPv6Address
4
+ from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network
5
5
  from uuid import UUID
6
6
 
7
- from pydantic_forms.utils.json import to_serializable
8
7
  import pytest
9
8
 
9
+ from pydantic_forms.utils.json import to_serializable
10
+
10
11
 
11
12
  def test_to_serializable_uuid():
12
13
  test_uuid = UUID("12345678123456781234567812345678")
@@ -1,91 +0,0 @@
1
- # Copyright 2019-2023 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 inspect import isasyncgenfunction, isgeneratorfunction
14
- from typing import Any, Callable
15
-
16
- import structlog
17
- from pydantic import BaseModel, ConfigDict, PydanticUndefinedAnnotation
18
-
19
- logger = structlog.get_logger(__name__)
20
-
21
-
22
- class DisplayOnlyFieldType:
23
- pass
24
-
25
-
26
- class FormPage(BaseModel):
27
- model_config = ConfigDict(
28
- arbitrary_types_allowed=True,
29
- title="unknown",
30
- extra="forbid",
31
- validate_default=True,
32
- )
33
-
34
- def __init__(self, **data: Any):
35
- frozen_fields = {k: v for k, v in self.__class__.model_fields.items() if v.frozen}
36
-
37
- def get_value(k: str, v: Any) -> Any:
38
- if k in frozen_fields:
39
- return frozen_fields[k].default
40
- return v
41
-
42
- mutable_data = {k: get_value(k, v) for k, v in data.items()}
43
- super().__init__(**mutable_data)
44
-
45
- @classmethod
46
- def __pydantic_init_subclass__(cls, /, **kwargs: Any) -> None:
47
- # The default and requiredness of a field is not a property of a field
48
- # In the case of DisplayOnlyFieldTypes, we do kind of want that.
49
- # Using this method we set the right properties after the form is created
50
- needs_rebuild = False
51
-
52
- for field in cls.model_fields.values():
53
- if field.frozen:
54
- field.validate_default = False
55
- needs_rebuild = True
56
-
57
- if needs_rebuild:
58
- try:
59
- # Fix for #39:
60
- # Core schema used during validation is constructed before __pydantic_init_subclass__ which means
61
- # that the field.validate_default change above doesn't take effect.
62
- # As a workaround, we explicitly rebuild the model to reconstruct the core schema.
63
- #
64
- # Downside is that unresolved forward-refs trigger an exception; for now we catch/log/ignore it.
65
- # From pydantic 2.12 a new hook __pydantic_on_complete__ can be used to perform the rebuild instead.
66
- # https://github.com/pydantic/pydantic/pull/11762
67
- cls.model_rebuild(force=True)
68
- except PydanticUndefinedAnnotation as exc:
69
- logger.warning(
70
- "Failed to rebuild model due to undefined annotation, frozen fields may not work as expected",
71
- undefined_annotation=exc.name,
72
- model=cls.__name__,
73
- )
74
-
75
-
76
- FORMS: dict[str, Callable] = {}
77
-
78
-
79
- def register_form(key: str, form: Callable) -> None:
80
- logger.info("Current Forms", forms=FORMS, new_key=key)
81
- if key in FORMS and form is not FORMS[key]:
82
- raise Exception(f"Trying to re-register form {key} with a different function")
83
-
84
- if not isasyncgenfunction(form) and not isgeneratorfunction(form):
85
- raise Exception(f"Trying to register form {key} function {form} which is not an (async) generator function")
86
-
87
- FORMS[key] = form
88
-
89
-
90
- def list_forms() -> list[str]:
91
- return list(FORMS.keys())
File without changes
File without changes
File without changes
File without changes