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

LeanderCS / flask-inputfilter / #389

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

Pull #56

coveralls-python

LeanderCS
48 | Remove typing_extensions dep
Pull Request #56: 48 | Update ArrayElementValidator and IsDataclassValidator

64 of 73 new or added lines in 4 files covered. (87.67%)

87 existing lines in 18 files 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
39
    dataclass type.
40

41
    **Parameters:**
42

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

46
    **Expected Behavior:**
47

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

51
    **Example Usage:**
52

53
    .. code-block:: python
54

55
        from dataclasses import dataclass
56

57
        @dataclass
58
        class User:
59
            id: int
60
            name: str
61

62
        class UserInputFilter(InputFilter):
63
            def __init__(self):
64
                super().__init__()
65

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

71
    __slots__ = ("dataclass_type", "error_message")
1✔
72

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

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

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

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

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

110
            field_value = value[field_name]
1✔
111

112
            origin = get_origin(field_type)
1✔
113
            args = get_args(field_type)
1✔
114

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