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

LeanderCS / flask-inputfilter / #396

24 May 2025 03:18PM CUT coverage: 93.409%. Remained the same
#396

push

coveralls-python

LeanderCS
Fix autobuild of docs

1885 of 2018 relevant lines covered (93.41%)

0.93 hits per line

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

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

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

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

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

11

12
# TODO: Replace with typing.get_origin when Python 3.7 support is dropped.
13
def get_origin(tp: Any) -> Optional[Type[Any]]:
1✔
14
    """Get the unsubscripted version of a type.
15

16
    This supports typing types like List, Dict, etc. and their
17
    typing_extensions equivalents.
18
    """
19
    if isinstance(tp, _GenericAlias):
1✔
20
        return tp.__origin__
1✔
21
    return None
1✔
22

23

24
# TODO: Replace with typing.get_args when Python 3.7 support is dropped.
25
def get_args(tp: Any) -> tuple[Any, ...]:
1✔
26
    """Get type arguments with all substitutions performed.
27

28
    For unions, basic types, and special typing forms, returns
29
    the type arguments. For example, for List[int] returns (int,).
30
    """
31
    if isinstance(tp, _GenericAlias):
1✔
32
        return tp.__args__
1✔
33
    return ()
1✔
34

35

36
class IsDataclassValidator(BaseValidator):
1✔
37
    """
38
    Validates that the provided value conforms to a specific dataclass type.
39

40
    **Parameters:**
41

42
    - **dataclass_type** (*Type[dict]*): The expected dataclass type.
43
    - **error_message** (*Optional[str]*): Custom error message if
44
        validation fails.
45

46
    **Expected Behavior:**
47

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

53
    **Example Usage:**
54

55
    .. code-block:: python
56

57
        from dataclasses import dataclass
58

59
        @dataclass
60
        class User:
61
            id: int
62
            name: str
63

64
        class UserInputFilter(InputFilter):
65
            def __init__(self):
66
                super().__init__()
67

68
                self.add('user', validators=[
69
                    IsDataclassValidator(dataclass_type=User)
70
                ])
71
    """
72

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

75
    def __init__(
1✔
76
        self,
77
        dataclass_type: Type[T],
78
        error_message: Optional[str] = None,
79
    ) -> None:
80
        self.dataclass_type = dataclass_type
1✔
81
        self.error_message = error_message
1✔
82

83
    def validate(self, value: Any) -> None:
1✔
84
        if not isinstance(value, dict):
1✔
85
            raise ValidationError(
1✔
86
                self.error_message
87
                or "The provided value is not a dict instance."
88
            )
89

90
        if not dataclasses.is_dataclass(self.dataclass_type):
1✔
91
            raise ValidationError(
×
92
                self.error_message
93
                or f"'{self.dataclass_type}' is not a valid dataclass."
94
            )
95

96
        for field in dataclasses.fields(self.dataclass_type):
1✔
97
            field_name = field.name
1✔
98
            field_type = field.type
1✔
99
            has_default = (
1✔
100
                field.default is not dataclasses.MISSING
101
                or field.default_factory is not dataclasses.MISSING
102
            )
103

104
            if field_name not in value:
1✔
105
                if not has_default:
1✔
106
                    raise ValidationError(
×
107
                        self.error_message
108
                        or f"Missing required field '{field_name}' in "
109
                        f"value '{value}'."
110
                    )
111
                continue
×
112

113
            field_value = value[field_name]
1✔
114

115
            origin = get_origin(field_type)
1✔
116
            args = get_args(field_type)
1✔
117

118
            if origin is not None:
1✔
119
                if origin is list:
1✔
120
                    if not isinstance(field_value, list) or not all(
×
121
                        isinstance(item, args[0]) for item in field_value
122
                    ):
123
                        raise ValidationError(
×
124
                            self.error_message
125
                            or f"Field '{field_name}' in value '{value}' is "
126
                            f"not a valid list of '{args[0]}'."
127
                        )
128
                elif origin is dict:
1✔
129
                    if not isinstance(field_value, dict) or not all(
×
130
                        isinstance(k, args[0]) and isinstance(v, args[1])
131
                        for k, v in field_value.items()
132
                    ):
133
                        raise ValidationError(
×
134
                            self.error_message
135
                            or f"Field '{field_name}' in value '{value}' is "
136
                            f"not a valid dict with keys of type "
137
                            f"'{args[0]}' and values of type '{args[1]}'."
138
                        )
139
                elif origin is Union and type(None) in args:
1✔
140
                    if field_value is not None and not isinstance(
1✔
141
                        field_value, args[0]
142
                    ):
143
                        raise ValidationError(
×
144
                            self.error_message
145
                            or f"Field '{field_name}' in value '{value}' is "
146
                            f"not of type '{args[0]}'."
147
                        )
148
                else:
149
                    raise ValidationError(
×
150
                        self.error_message
151
                        or f"Unsupported type '{field_type}' for field "
152
                        f"'{field_name}'."
153
                    )
154
            elif dataclasses.is_dataclass(field_type):
1✔
155
                IsDataclassValidator(field_type).validate(field_value)
1✔
156
            else:
157
                if not isinstance(field_value, field_type):
1✔
158
                    raise ValidationError(
1✔
159
                        self.error_message
160
                        or f"Field '{field_name}' in value '{value}' is not "
161
                        f"of type '{field_type}'."
162
                    )
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

© 2025 Coveralls, Inc