• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

LeanderCS / flask-inputfilter / #478

28 Sep 2025 09:11PM UTC coverage: 92.193% (-0.08%) from 92.276%
#478

push

coveralls-python

web-flow
Merge pull request #65 from LeanderCS/more-declaratives

Add more declaratives to prevent possible errors and make construction more direct

98 of 113 new or added lines in 14 files covered. (86.73%)

3 existing lines in 3 files now uncovered.

2102 of 2280 relevant lines covered (92.19%)

0.92 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

71.13
/flask_inputfilter/validators/is_dataclass_validator.py
1
from __future__ import annotations
1✔
2

3
import dataclasses
1✔
4
from typing import Any, ClassVar, Optional, Type, TypeVar, Union, _GenericAlias
1✔
5

6
from flask_inputfilter.exceptions import ValidationError
1✔
7
from flask_inputfilter.models import BaseValidator
1✔
8

9
T = TypeVar("T")
1✔
10

11

12
# Compatibility functions for Python 3.7 support
13
try:
1✔
14
    from typing import get_args, get_origin
1✔
15
except ImportError:
1✔
16
    # Fallback implementations for Python 3.7
17
    def get_origin(tp: Any) -> Optional[Type[Any]]:
1✔
18
        """
19
        Get the unsubscripted version of a type.
20

21
        This supports typing types like list, dict, etc. and their
22
        typing_extensions equivalents.
23
        """
24
        if isinstance(tp, _GenericAlias):
1✔
25
            return tp.__origin__
1✔
26
        return None
1✔
27

28
    def get_args(tp: Any) -> tuple[Any, ...]:
1✔
29
        """
30
        Get type arguments with all substitutions performed.
31

32
        For unions, basic types, and special typing forms, returns the type
33
        arguments. For example, for list[int] returns (int,).
34
        """
35
        if isinstance(tp, _GenericAlias):
1✔
36
            return tp.__args__
1✔
NEW
37
        return ()
×
38

39

40
class IsDataclassValidator(BaseValidator):
1✔
41
    """
42
    Validates that the provided value conforms to a specific dataclass type.
43

44
    **Parameters:**
45

46
    - **dataclass_type** (*Type[dict]*): The expected dataclass type.
47
    - **error_message** (*Optional[str]*): Custom error message if
48
      validation fails.
49

50
    **Expected Behavior:**
51

52
    Ensures the input is a dictionary and, that all expected keys are present.
53
    Raises a ``ValidationError`` if the structure does not match.
54
    All fields in the dataclass are validated against their types, including
55
    nested dataclasses, lists, and dictionaries.
56

57
    **Example Usage:**
58

59
    .. code-block:: python
60

61
        from dataclasses import dataclass
62

63
        @dataclass
64
        class User:
65
            id: int
66
            name: str
67

68
        class UserInputFilter(InputFilter):
69
            user: dict = field(validators=[
70
                IsDataclassValidator(dataclass_type=User)
71
            ])
72
    """
73

74
    __slots__ = ("dataclass_type", "error_message")
1✔
75

76
    _ERROR_TEMPLATES: ClassVar = {
1✔
77
        "not_dict": "The provided value is not a dict instance.",
78
        "not_dataclass": "'{dataclass_type}' is not a valid dataclass.",
79
        "missing_field": "Missing required field '{field_name}' in value "
80
        "'{value}'.",
81
        "type_mismatch": "Field '{field_name}' in value '{value}' is not of "
82
        "type '{expected_type}'.",
83
        "list_type": "Field '{field_name}' in value '{value}' is not a valid "
84
        "list of '{item_type}'.",
85
        "list_item": "Item at index {index} in field '{field_name}' is not "
86
        "of type '{expected_type}'.",
87
        "dict_type": "Field '{field_name}' in value '{value}' is not a valid "
88
        "dict with keys of type '{key_type}' and values of type "
89
        "'{value_type}'.",
90
        "dict_key": "Key '{key}' in field '{field_name}' is not of type "
91
        "'{expected_type}'.",
92
        "dict_value": "Value for key '{key}' in field '{field_name}' is not "
93
        "of type '{expected_type}'.",
94
        "union_mismatch": "Field '{field_name}' in value '{value}' does not "
95
        "match any of the types: {types}.",
96
        "unsupported_type": "Unsupported type '{field_type}' for field "
97
        "'{field_name}'.",
98
    }
99

100
    def __init__(
1✔
101
        self,
102
        dataclass_type: Type[T],
103
        error_message: Optional[str] = None,
104
    ) -> None:
105
        self.dataclass_type = dataclass_type
1✔
106
        self.error_message = error_message
1✔
107

108
        if not dataclasses.is_dataclass(self.dataclass_type):
1✔
109
            raise ValueError(
×
110
                self._format_error(
111
                    "not_dataclass", dataclass_type=self.dataclass_type
112
                )
113
            )
114

115
    def _format_error(self, error_type: str, **kwargs: Any) -> str:
1✔
116
        """Format error message using template or custom message."""
117
        if self.error_message:
1✔
118
            return self.error_message
1✔
119

120
        template = self._ERROR_TEMPLATES.get(error_type, "Validation error")
1✔
121
        return template.format(**kwargs)
1✔
122

123
    def validate(self, value: Any) -> None:
1✔
124
        """Validate that value conforms to the dataclass type."""
125
        self._validate_is_dict(value)
1✔
126

127
        for field in dataclasses.fields(self.dataclass_type):
1✔
128
            self._validate_field(field, value)
1✔
129

130
    def _validate_is_dict(self, value: Any) -> None:
1✔
131
        """Ensure value is a dictionary."""
132
        if not isinstance(value, dict):
1✔
133
            raise ValidationError(self._format_error("not_dict"))
1✔
134

135
    def _validate_field(
1✔
136
        self, field: dataclasses.Field, value: dict[str, Any]
137
    ) -> None:
138
        """Validate a single field of the dataclass."""
139
        field_name = field.name
1✔
140
        field_type = field.type
1✔
141

142
        if field_name not in value:
1✔
143
            if not IsDataclassValidator._has_default(field):
1✔
144
                raise ValidationError(
×
145
                    self._format_error(
146
                        "missing_field", field_name=field_name, value=value
147
                    )
148
                )
149
            return
1✔
150

151
        field_value = value[field_name]
1✔
152
        self._validate_field_type(field_name, field_value, field_type, value)
1✔
153

154
    @staticmethod
1✔
155
    def _has_default(field: dataclasses.Field) -> bool:
156
        """Check if a field has a default value."""
157
        return (
1✔
158
            field.default is not dataclasses.MISSING
159
            or field.default_factory is not dataclasses.MISSING
160
        )
161

162
    def _validate_field_type(
1✔
163
        self,
164
        field_name: str,
165
        field_value: Any,
166
        field_type: Type,
167
        parent_value: dict[str, Any],
168
    ) -> None:
169
        """Validate that a field value matches its expected type."""
170
        origin = get_origin(field_type)
1✔
171

172
        if origin is not None:
1✔
173
            self._validate_generic_type(
1✔
174
                field_name, field_value, field_type, origin, parent_value
175
            )
176
        elif dataclasses.is_dataclass(field_type):
1✔
177
            IsDataclassValidator._validate_nested_dataclass(
1✔
178
                field_value, field_type
179
            )
180
        else:
181
            self._validate_simple_type(
1✔
182
                field_name, field_value, field_type, parent_value
183
            )
184

185
    def _validate_generic_type(
1✔
186
        self,
187
        field_name: str,
188
        field_value: Any,
189
        field_type: Type,
190
        origin: Type,
191
        parent_value: dict[str, Any],
192
    ) -> None:
193
        """Validate generic types like list[T], dict[K, V], Optional[T]."""
194
        args = get_args(field_type)
1✔
195

196
        validators = {
1✔
197
            list: self._validate_list_type,
198
            dict: self._validate_dict_type,
199
            Union: self._validate_union_type,
200
        }
201

202
        validator = validators.get(origin)
1✔
203
        if validator:
1✔
204
            validator(field_name, field_value, args, parent_value)
1✔
205
        else:
206
            raise ValidationError(
×
207
                self._format_error(
208
                    "unsupported_type",
209
                    field_type=field_type,
210
                    field_name=field_name,
211
                )
212
            )
213

214
    def _validate_list_type(
1✔
215
        self,
216
        field_name: str,
217
        field_value: Any,
218
        args: tuple[Type, ...],
219
        parent_value: dict[str, Any],
220
    ) -> None:
221
        """Validate list[T] type."""
222
        if not isinstance(field_value, list):
×
223
            raise ValidationError(
×
224
                self._format_error(
225
                    "list_type",
226
                    field_name=field_name,
227
                    value=parent_value,
228
                    item_type=args[0],
229
                )
230
            )
231

232
        item_type = args[0]
×
233
        for i, item in enumerate(field_value):
×
234
            if not isinstance(item, item_type):
×
235
                raise ValidationError(
×
236
                    self._format_error(
237
                        "list_item",
238
                        index=i,
239
                        field_name=field_name,
240
                        expected_type=item_type,
241
                    )
242
                )
243

244
    def _validate_dict_type(
1✔
245
        self,
246
        field_name: str,
247
        field_value: Any,
248
        args: tuple[Type, ...],
249
        parent_value: dict[str, Any],
250
    ) -> None:
251
        """Validate dict[K, V] type."""
252
        if not isinstance(field_value, dict):
×
253
            raise ValidationError(
×
254
                self._format_error(
255
                    "dict_type",
256
                    field_name=field_name,
257
                    value=parent_value,
258
                    key_type=args[0],
259
                    value_type=args[1],
260
                )
261
            )
262

263
        key_type, value_type = args[0], args[1]
×
264
        for k, v in field_value.items():
×
265
            if not isinstance(k, key_type):
×
266
                raise ValidationError(
×
267
                    self._format_error(
268
                        "dict_key",
269
                        key=k,
270
                        field_name=field_name,
271
                        expected_type=key_type,
272
                    )
273
                )
274
            if not isinstance(v, value_type):
×
275
                raise ValidationError(
×
276
                    self._format_error(
277
                        "dict_value",
278
                        key=k,
279
                        field_name=field_name,
280
                        expected_type=value_type,
281
                    )
282
                )
283

284
    def _validate_union_type(
1✔
285
        self,
286
        field_name: str,
287
        field_value: Any,
288
        args: tuple[Type, ...],
289
        parent_value: dict[str, Any],
290
    ) -> None:
291
        """Validate Union types, particularly Optional[T]."""
292
        if None in args:
1✔
293
            if field_value is None:
×
294
                return
×
295

296
            non_none_types = [t for t in args if t is not None]
×
297
            if len(non_none_types) == 1:
×
298
                expected_type = non_none_types[0]
×
299
                if not isinstance(field_value, expected_type):
×
300
                    raise ValidationError(
×
301
                        self._format_error(
302
                            "type_mismatch",
303
                            field_name=field_name,
304
                            value=parent_value,
305
                            expected_type=expected_type,
306
                        )
307
                    )
308
                return
×
309

310
        if not any(isinstance(field_value, t) for t in args):
1✔
311
            types_str = ", ".join(str(t) for t in args)
×
312
            raise ValidationError(
×
313
                self._format_error(
314
                    "union_mismatch",
315
                    field_name=field_name,
316
                    value=parent_value,
317
                    types=types_str,
318
                )
319
            )
320

321
    @staticmethod
1✔
322
    def _validate_nested_dataclass(field_value: Any, field_type: Type) -> None:
323
        """Validate nested dataclass."""
324
        nested_validator = IsDataclassValidator(field_type)
1✔
325
        nested_validator.validate(field_value)
1✔
326

327
    def _validate_simple_type(
1✔
328
        self,
329
        field_name: str,
330
        field_value: Any,
331
        field_type: Type,
332
        parent_value: dict[str, Any],
333
    ) -> None:
334
        """Validate simple types like int, str, bool, etc."""
335
        if not isinstance(field_value, field_type):
1✔
336
            raise ValidationError(
1✔
337
                self._format_error(
338
                    "type_mismatch",
339
                    field_name=field_name,
340
                    value=parent_value,
341
                    expected_type=field_type,
342
                )
343
            )
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc