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

LeanderCS / flask-inputfilter / #397

30 Jun 2025 03:41PM UTC coverage: 92.139% (-1.3%) from 93.409%
#397

push

coveralls-python

LeanderCS
Optimize InputFilter

213 of 245 new or added lines in 44 files covered. (86.94%)

36 existing lines in 3 files now uncovered.

1887 of 2048 relevant lines covered (92.14%)

0.92 hits per line

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

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

3
import dataclasses
1✔
4
from typing import (
1✔
5
    Any,
6
    Dict,
7
    Optional,
8
    Tuple,
9
    Type,
10
    TypeVar,
11
    Union,
12
    _GenericAlias,
13
)
14

15
from flask_inputfilter.exceptions import ValidationError
1✔
16
from flask_inputfilter.validators import BaseValidator
1✔
17

18
T = TypeVar("T")
1✔
19

20

21
# TODO: Replace with typing.get_origin when Python 3.7 support is dropped.
22
def get_origin(tp: Any) -> Optional[Type[Any]]:
1✔
23
    """
24
    Get the unsubscripted version of a type.
25

26
    This supports typing types like List, Dict, etc. and their
27
    typing_extensions equivalents.
28
    """
29
    if isinstance(tp, _GenericAlias):
1✔
30
        return tp.__origin__
1✔
31
    return None
1✔
32

33

34
# TODO: Replace with typing.get_args when Python 3.7 support is dropped.
35
def get_args(tp: Any) -> tuple[Any, ...]:
1✔
36
    """
37
    Get type arguments with all substitutions performed.
38

39
    For unions, basic types, and special typing forms, returns the type
40
    arguments. For example, for List[int] returns (int,).
41
    """
42
    if isinstance(tp, _GenericAlias):
1✔
43
        return tp.__args__
1✔
UNCOV
44
    return ()
×
45

46

47
class IsDataclassValidator(BaseValidator):
1✔
48
    """
49
    Validates that the provided value conforms to a specific dataclass type.
50

51
    **Parameters:**
52

53
    - **dataclass_type** (*Type[dict]*): The expected dataclass type.
54
    - **error_message** (*Optional[str]*): Custom error message if
55
        validation fails.
56

57
    **Expected Behavior:**
58

59
    Ensures the input is a dictionary and, that all expected keys are present.
60
    Raises a ``ValidationError`` if the structure does not match.
61
    All fields in the dataclass are validated against their types, including
62
    nested dataclasses, lists, and dictionaries.
63

64
    **Example Usage:**
65

66
    .. code-block:: python
67

68
        from dataclasses import dataclass
69

70
        @dataclass
71
        class User:
72
            id: int
73
            name: str
74

75
        class UserInputFilter(InputFilter):
76
            def __init__(self):
77
                super().__init__()
78

79
                self.add('user', validators=[
80
                    IsDataclassValidator(dataclass_type=User)
81
                ])
82
    """
83

84
    __slots__ = ("dataclass_type", "error_message")
1✔
85

86
    _ERROR_TEMPLATES = {
1✔
87
        "not_dict": "The provided value is not a dict instance.",
88
        "not_dataclass": "'{dataclass_type}' is not a valid dataclass.",
89
        "missing_field": "Missing required field '{field_name}' in value "
90
        "'{value}'.",
91
        "type_mismatch": "Field '{field_name}' in value '{value}' is not of "
92
        "type '{expected_type}'.",
93
        "list_type": "Field '{field_name}' in value '{value}' is not a valid "
94
        "list of '{item_type}'.",
95
        "list_item": "Item at index {index} in field '{field_name}' is not "
96
        "of type '{expected_type}'.",
97
        "dict_type": "Field '{field_name}' in value '{value}' is not a valid "
98
        "dict with keys of type '{key_type}' and values of type "
99
        "'{value_type}'.",
100
        "dict_key": "Key '{key}' in field '{field_name}' is not of type "
101
        "'{expected_type}'.",
102
        "dict_value": "Value for key '{key}' in field '{field_name}' is not "
103
        "of type '{expected_type}'.",
104
        "union_mismatch": "Field '{field_name}' in value '{value}' does not "
105
        "match any of the types: {types}.",
106
        "unsupported_type": "Unsupported type '{field_type}' for field "
107
        "'{field_name}'.",
108
    }
109

110
    def __init__(
1✔
111
        self,
112
        dataclass_type: Type[T],
113
        error_message: Optional[str] = None,
114
    ) -> None:
115
        self.dataclass_type = dataclass_type
1✔
116
        self.error_message = error_message
1✔
117

118
        if not dataclasses.is_dataclass(self.dataclass_type):
1✔
NEW
119
            raise ValueError(
×
120
                self._format_error(
121
                    "not_dataclass", dataclass_type=self.dataclass_type
122
                )
123
            )
124

125
    def _format_error(self, error_type: str, **kwargs) -> str:
1✔
126
        """Format error message using template or custom message."""
127
        if self.error_message:
1✔
128
            return self.error_message
1✔
129

130
        template = self._ERROR_TEMPLATES.get(error_type, "Validation error")
1✔
131
        return template.format(**kwargs)
1✔
132

133
    def validate(self, value: Any) -> None:
1✔
134
        """Validate that value conforms to the dataclass type."""
135
        self._validate_is_dict(value)
1✔
136

137
        for field in dataclasses.fields(self.dataclass_type):
1✔
138
            self._validate_field(field, value)
1✔
139

140
    def _validate_is_dict(self, value: Any) -> None:
1✔
141
        """Ensure value is a dictionary."""
142
        if not isinstance(value, dict):
1✔
143
            raise ValidationError(self._format_error("not_dict"))
1✔
144

145
    def _validate_field(
1✔
146
        self, field: dataclasses.Field, value: Dict[str, Any]
147
    ) -> None:
148
        """Validate a single field of the dataclass."""
149
        field_name = field.name
1✔
150
        field_type = field.type
1✔
151

152
        if field_name not in value:
1✔
153
            if not IsDataclassValidator._has_default(field):
1✔
NEW
154
                raise ValidationError(
×
155
                    self._format_error(
156
                        "missing_field", field_name=field_name, value=value
157
                    )
158
                )
159
            return
1✔
160

161
        field_value = value[field_name]
1✔
162
        self._validate_field_type(field_name, field_value, field_type, value)
1✔
163

164
    @staticmethod
1✔
165
    def _has_default(field: dataclasses.Field) -> bool:
166
        """Check if a field has a default value."""
167
        return (
1✔
168
            field.default is not dataclasses.MISSING
169
            or field.default_factory is not dataclasses.MISSING
170
        )
171

172
    def _validate_field_type(
1✔
173
        self,
174
        field_name: str,
175
        field_value: Any,
176
        field_type: Type,
177
        parent_value: Dict[str, Any],
178
    ) -> None:
179
        """Validate that a field value matches its expected type."""
180
        origin = get_origin(field_type)
1✔
181

182
        if origin is not None:
1✔
183
            self._validate_generic_type(
1✔
184
                field_name, field_value, field_type, origin, parent_value
185
            )
186
        elif dataclasses.is_dataclass(field_type):
1✔
187
            IsDataclassValidator._validate_nested_dataclass(
1✔
188
                field_value, field_type
189
            )
190
        else:
191
            self._validate_simple_type(
1✔
192
                field_name, field_value, field_type, parent_value
193
            )
194

195
    def _validate_generic_type(
1✔
196
        self,
197
        field_name: str,
198
        field_value: Any,
199
        field_type: Type,
200
        origin: Type,
201
        parent_value: Dict[str, Any],
202
    ) -> None:
203
        """Validate generic types like List[T], Dict[K, V], Optional[T]."""
204
        args = get_args(field_type)
1✔
205

206
        validators = {
1✔
207
            list: self._validate_list_type,
208
            dict: self._validate_dict_type,
209
            Union: self._validate_union_type,
210
        }
211

212
        validator = validators.get(origin)
1✔
213
        if validator:
1✔
214
            validator(field_name, field_value, args, parent_value)
1✔
215
        else:
UNCOV
216
            raise ValidationError(
×
217
                self._format_error(
218
                    "unsupported_type",
219
                    field_type=field_type,
220
                    field_name=field_name,
221
                )
222
            )
223

224
    def _validate_list_type(
1✔
225
        self,
226
        field_name: str,
227
        field_value: Any,
228
        args: Tuple[Type, ...],
229
        parent_value: Dict[str, Any],
230
    ) -> None:
231
        """Validate List[T] type."""
NEW
232
        if not isinstance(field_value, list):
×
UNCOV
233
            raise ValidationError(
×
234
                self._format_error(
235
                    "list_type",
236
                    field_name=field_name,
237
                    value=parent_value,
238
                    item_type=args[0],
239
                )
240
            )
241

NEW
242
        item_type = args[0]
×
NEW
243
        for i, item in enumerate(field_value):
×
NEW
244
            if not isinstance(item, item_type):
×
NEW
245
                raise ValidationError(
×
246
                    self._format_error(
247
                        "list_item",
248
                        index=i,
249
                        field_name=field_name,
250
                        expected_type=item_type,
251
                    )
252
                )
253

254
    def _validate_dict_type(
1✔
255
        self,
256
        field_name: str,
257
        field_value: Any,
258
        args: Tuple[Type, ...],
259
        parent_value: Dict[str, Any],
260
    ) -> None:
261
        """Validate Dict[K, V] type."""
NEW
262
        if not isinstance(field_value, dict):
×
NEW
263
            raise ValidationError(
×
264
                self._format_error(
265
                    "dict_type",
266
                    field_name=field_name,
267
                    value=parent_value,
268
                    key_type=args[0],
269
                    value_type=args[1],
270
                )
271
            )
272

NEW
273
        key_type, value_type = args[0], args[1]
×
NEW
274
        for k, v in field_value.items():
×
NEW
275
            if not isinstance(k, key_type):
×
NEW
276
                raise ValidationError(
×
277
                    self._format_error(
278
                        "dict_key",
279
                        key=k,
280
                        field_name=field_name,
281
                        expected_type=key_type,
282
                    )
283
                )
NEW
284
            if not isinstance(v, value_type):
×
NEW
285
                raise ValidationError(
×
286
                    self._format_error(
287
                        "dict_value",
288
                        key=k,
289
                        field_name=field_name,
290
                        expected_type=value_type,
291
                    )
292
                )
293

294
    def _validate_union_type(
1✔
295
        self,
296
        field_name: str,
297
        field_value: Any,
298
        args: Tuple[Type, ...],
299
        parent_value: Dict[str, Any],
300
    ) -> None:
301
        """Validate Union types, particularly Optional[T]."""
302
        if None in args:
1✔
NEW
303
            if field_value is None:
×
NEW
304
                return
×
305

NEW
306
            non_none_types = [t for t in args if t is not None]
×
NEW
307
            if len(non_none_types) == 1:
×
NEW
308
                expected_type = non_none_types[0]
×
NEW
309
                if not isinstance(field_value, expected_type):
×
UNCOV
310
                    raise ValidationError(
×
311
                        self._format_error(
312
                            "type_mismatch",
313
                            field_name=field_name,
314
                            value=parent_value,
315
                            expected_type=expected_type,
316
                        )
317
                    )
NEW
318
                return
×
319

320
        if not any(isinstance(field_value, t) for t in args):
1✔
NEW
321
            types_str = ", ".join(str(t) for t in args)
×
NEW
322
            raise ValidationError(
×
323
                self._format_error(
324
                    "union_mismatch",
325
                    field_name=field_name,
326
                    value=parent_value,
327
                    types=types_str,
328
                )
329
            )
330

331
    @staticmethod
1✔
332
    def _validate_nested_dataclass(field_value: Any, field_type: Type) -> None:
333
        """Validate nested dataclass."""
334
        nested_validator = IsDataclassValidator(field_type)
1✔
335
        nested_validator.validate(field_value)
1✔
336

337
    def _validate_simple_type(
1✔
338
        self,
339
        field_name: str,
340
        field_value: Any,
341
        field_type: Type,
342
        parent_value: Dict[str, Any],
343
    ) -> None:
344
        """Validate simple types like int, str, bool, etc."""
345
        if not isinstance(field_value, field_type):
1✔
346
            raise ValidationError(
1✔
347
                self._format_error(
348
                    "type_mismatch",
349
                    field_name=field_name,
350
                    value=parent_value,
351
                    expected_type=field_type,
352
                )
353
            )
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