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

LeanderCS / flask-inputfilter / #390

24 May 2025 01:03PM UTC coverage: 93.409% (-0.2%) from 93.644%
#390

Pull #56

coveralls-python

web-flow
Merge branch 'main' into 48
Pull Request #56: 48 | Update ArrayElementValidator and IsDataclassValidator

67 of 76 new or added lines in 4 files covered. (88.16%)

1 existing line in 1 file now uncovered.

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✔
UNCOV
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✔
NEW
106
                    raise ValidationError(
×
107
                        self.error_message
108
                        or f"Missing required field '{field_name}' in value '{value}'."
109
                    )
NEW
110
                continue
×
111

112
            field_value = value[field_name]
1✔
113

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

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