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

LeanderCS / flask-inputfilter / #415

01 Jul 2025 11:14PM UTC coverage: 95.53% (-0.3%) from 95.792%
#415

push

coveralls-python

LeanderCS
Switch to ruff

69 of 81 new or added lines in 42 files covered. (85.19%)

5 existing lines in 3 files now uncovered.

1838 of 1924 relevant lines covered (95.53%)

0.96 hits per line

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

96.23
/flask_inputfilter/input_filter.py
1
from __future__ import annotations
1✔
2

3
import json
1✔
4
import logging
1✔
5
import sys
1✔
6
from typing import TYPE_CHECKING, Any, Optional, Type, TypeVar, Union
1✔
7

8
from flask import Response, g, request
1✔
9

10
from flask_inputfilter.exceptions import ValidationError
1✔
11
from flask_inputfilter.mixins import FieldMixin
1✔
12
from flask_inputfilter.models import BaseFilter, ExternalApiConfig, FieldModel
1✔
13

14
if TYPE_CHECKING:
1✔
NEW
15
    from collections.abc import Callable
×
16

NEW
17
    from flask_inputfilter.conditions import BaseCondition
×
NEW
18
    from flask_inputfilter.validators import BaseValidator
×
19

20
T = TypeVar("T")
1✔
21

22
_INTERNED_STRINGS = {
1✔
23
    "_condition": sys.intern("_condition"),
24
    "_error": sys.intern("_error"),
25
    "copy": sys.intern("copy"),
26
    "default": sys.intern("default"),
27
    "DELETE": sys.intern("DELETE"),
28
    "external_api": sys.intern("external_api"),
29
    "fallback": sys.intern("fallback"),
30
    "filters": sys.intern("filters"),
31
    "GET": sys.intern("GET"),
32
    "PATCH": sys.intern("PATCH"),
33
    "POST": sys.intern("POST"),
34
    "PUT": sys.intern("PUT"),
35
    "required": sys.intern("required"),
36
    "steps": sys.intern("steps"),
37
    "validators": sys.intern("validators"),
38
}
39

40

41
class InputFilter:
1✔
42
    """Base class for all input filters."""
43

44
    def __init__(self, methods: Optional[list[str]] = None) -> None:
1✔
45
        self.methods: list[str] = methods or [
1✔
46
            "DELETE",
47
            "GET",
48
            "PATCH",
49
            "POST",
50
            "PUT",
51
        ]
52
        self.fields: dict[str, FieldModel] = {}
1✔
53
        self.conditions: list[BaseCondition] = []
1✔
54
        self.global_filters: list[BaseFilter] = []
1✔
55
        self.global_validators: list[BaseValidator] = []
1✔
56
        self.data: dict[str, Any] = {}
1✔
57
        self.validated_data: dict[str, Any] = {}
1✔
58
        self.errors: dict[str, str] = {}
1✔
59
        self.model_class: Optional[Type[T]] = None
1✔
60

61
    def is_valid(self) -> bool:
1✔
62
        """
63
        Checks if the object's state or its attributes meet certain conditions
64
        to be considered valid. This function is typically used to ensure that
65
        the current state complies with specific requirements or rules.
66

67
        Returns:
68
            bool: Returns True if the state or attributes of the object fulfill
69
                all required conditions; otherwise, returns False.
70
        """
71
        try:
1✔
72
            self.validate_data()
1✔
73

74
        except ValidationError as e:
1✔
75
            self.errors = e.args[0]
1✔
76
            return False
1✔
77

78
        return True
1✔
79

80
    @classmethod
1✔
81
    def validate(
82
        cls,
83
    ) -> Callable:
84
        """
85
        Decorator for validating input data in routes.
86

87
        Args:
88
            cls
89

90
        Returns:
91
            Callable
92
        """
93

94
        def decorator(
1✔
95
            f: Callable,
96
        ) -> Callable:
97
            """
98
            Decorator function to validate input data for a Flask route.
99

100
            Args:
101
                f (Callable): The Flask route function to be decorated.
102

103
            Returns:
104
                Callable: The wrapped function with input validation.
105
            """
106

107
            def wrapper(
1✔
108
                *args, **kwargs
109
            ) -> Union[Response, tuple[Any, dict[str, Any]]]:
110
                """
111
                Wrapper function to handle input validation and error handling
112
                for the decorated route function.
113

114
                Args:
115
                    *args: Positional arguments for the route function.
116
                    **kwargs: Keyword arguments for the route function.
117

118
                Returns:
119
                    Union[Response, tuple[Any, dict[str, Any]]]: The response
120
                        from the route function or an error response.
121
                """
122
                input_filter = cls()
1✔
123
                if request.method not in input_filter.methods:
1✔
124
                    return Response(status=405)
1✔
125

126
                if request.is_json:
1✔
127
                    data = request.get_json(cache=True)
1✔
128
                    if not isinstance(data, dict):
1✔
129
                        data = {}
1✔
130
                else:
131
                    data = dict(request.args)
1✔
132

133
                try:
1✔
134
                    if kwargs:
1✔
135
                        data.update(kwargs)
1✔
136

137
                    input_filter.data = data
1✔
138
                    input_filter.validated_data = {}
1✔
139
                    input_filter.errors = {}
1✔
140

141
                    g.validated_data = input_filter.validate_data()
1✔
142

143
                except ValidationError as e:
1✔
144
                    return Response(
1✔
145
                        status=400,
146
                        response=json.dumps(e.args[0]),
147
                        mimetype="application/json",
148
                    )
149

UNCOV
150
                except Exception:
×
151
                    logging.exception(
×
152
                        "An unexpected exception occurred while "
153
                        "validating input data.",
154
                    )
UNCOV
155
                    return Response(status=500)
×
156

157
                return f(*args, **kwargs)
1✔
158

159
            return wrapper
1✔
160

161
        return decorator
1✔
162

163
    def validate_data(
1✔
164
        self, data: Optional[dict[str, Any]] = None
165
    ) -> Union[dict[str, Any], Type[T]]:
166
        """
167
        Validates input data against defined field rules, including applying
168
        filters, validators, custom logic steps, and fallback mechanisms. The
169
        validation process also ensures the required fields are handled
170
        appropriately and conditions are checked after processing.
171

172
        Args:
173
            data (dict[str, Any]): A dictionary containing the input data to
174
                be validated where keys represent field names and values
175
                represent the corresponding data.
176

177
        Returns:
178
            Union[dict[str, Any], Type[T]]: A dictionary containing the
179
                validated data with any modifications, default values,
180
                or processed values as per the defined validation rules.
181

182
        Raises:
183
            Any errors raised during external API calls, validation, or
184
                logical steps execution of the respective fields or conditions
185
                will propagate without explicit handling here.
186
        """
187
        data = data or self.data
1✔
188

189
        validated_data, errors = FieldMixin.validate_fields(
1✔
190
            self.fields, data, self.global_filters, self.global_validators
191
        )
192

193
        if self.conditions:
1✔
194
            self._check_all_conditions(validated_data, errors)
1✔
195

196
        if errors:
1✔
197
            raise ValidationError(errors)
1✔
198

199
        self.validated_data = validated_data
1✔
200
        return self.serialize()
1✔
201

202
    def add_condition(self, condition: BaseCondition) -> None:
1✔
203
        """
204
        Add a condition to the input filter.
205

206
        Args:
207
            condition (BaseCondition): The condition to add.
208
        """
209
        self.conditions.append(condition)
1✔
210

211
    def get_conditions(self) -> list[BaseCondition]:
1✔
212
        """
213
        Retrieve the list of all registered conditions.
214

215
        This function provides access to the conditions that have been
216
        registered and stored. Each condition in the returned list
217
        is represented as an instance of the BaseCondition type.
218

219
        Returns:
220
            list[BaseCondition]: A list containing all currently registered
221
                instances of BaseCondition.
222
        """
223
        return self.conditions
1✔
224

225
    def set_data(self, data: dict[str, Any]) -> None:
1✔
226
        """
227
        Filters and sets the provided data into the object's internal storage,
228
        ensuring that only the specified fields are considered and their values
229
        are processed through defined filters.
230

231
        Parameters:
232
            data (dict[str, Any]):
233
                The input dictionary containing key-value pairs where keys
234
                represent field names and values represent the associated
235
                data to be filtered and stored.
236
        """
237
        self.data = {}
1✔
238
        for field_name, field_value in data.items():
1✔
239
            if field_name in self.fields:
1✔
240
                field_value = FieldMixin.apply_filters(
1✔
241
                    self.fields[field_name].filters,
242
                    self.global_filters,
243
                    field_value,
244
                )
245

246
            self.data[field_name] = field_value
1✔
247

248
    def get_value(self, name: str) -> Any:
1✔
249
        """
250
        This method retrieves a value associated with the provided name. It
251
        searches for the value based on the given identifier and returns the
252
        corresponding result. If no value is found, it typically returns a
253
        default or fallback output. The method aims to provide flexibility in
254
        retrieving data without explicitly specifying the details of the
255
        underlying implementation.
256

257
        Args:
258
            name (str): A string that represents the identifier for which the
259
                 corresponding value is being retrieved. It is used to perform
260
                 the lookup.
261

262
        Returns:
263
            Any: The retrieved value associated with the given name. The
264
                 specific type of this value is dependent on the
265
                 implementation and the data being accessed.
266
        """
267
        return self.validated_data.get(name)
1✔
268

269
    def get_values(self) -> dict[str, Any]:
1✔
270
        """
271
        Retrieves a dictionary of key-value pairs from the current object. This
272
        method provides access to the internal state or configuration of the
273
        object in a dictionary format, where keys are strings and values can be
274
        of various types depending on the object's design.
275

276
        Returns:
277
            dict[str, Any]: A dictionary containing string keys and their
278
                            corresponding values of any data type.
279
        """
280
        return self.validated_data
1✔
281

282
    def get_raw_value(self, name: str) -> Any:
1✔
283
        """
284
        Fetches the raw value associated with the provided key.
285

286
        This method is used to retrieve the underlying value linked to the
287
        given key without applying any transformations or validations. It
288
        directly fetches the raw stored value and is typically used in
289
        scenarios where the raw data is needed for processing or debugging
290
        purposes.
291

292
        Args:
293
            name (str): The name of the key whose raw value is to be
294
                retrieved.
295

296
        Returns:
297
            Any: The raw value associated with the provided key.
298
        """
299
        return self.data.get(name)
1✔
300

301
    def get_raw_values(self) -> dict[str, Any]:
1✔
302
        """
303
        Retrieves raw values from a given source and returns them as a
304
        dictionary.
305

306
        This method is used to fetch and return unprocessed or raw data in
307
        the form of a dictionary where the keys are strings, representing
308
        the identifiers, and the values are of any data type.
309

310
        Returns:
311
            dict[str, Any]: A dictionary containing the raw values retrieved.
312
               The keys are strings representing the identifiers, and the
313
               values can be of any type, depending on the source
314
               being accessed.
315
        """
316
        if not self.fields:
1✔
317
            return {}
1✔
318

319
        return {
1✔
320
            field: self.data[field]
321
            for field in self.fields
322
            if field in self.data
323
        }
324

325
    def get_unfiltered_data(self) -> dict[str, Any]:
1✔
326
        """
327
        Fetches unfiltered data from the data source.
328

329
        This method retrieves data without any filtering, processing, or
330
        manipulations applied. It is intended to provide raw data that has
331
        not been altered since being retrieved from its source. The usage
332
        of this method should be limited to scenarios where unprocessed data
333
        is required, as it does not perform any validations or checks.
334

335
        Returns:
336
            dict[str, Any]: The unfiltered, raw data retrieved from the
337
                 data source. The return type may vary based on the
338
                 specific implementation of the data source.
339
        """
340
        return self.data
1✔
341

342
    def set_unfiltered_data(self, data: dict[str, Any]) -> None:
1✔
343
        """
344
        Sets unfiltered data for the current instance. This method assigns a
345
        given dictionary of data to the instance for further processing. It
346
        updates the internal state using the provided data.
347

348
        Parameters:
349
            data (dict[str, Any]): A dictionary containing the unfiltered
350
                data to be associated with the instance.
351
        """
352
        self.data = data
1✔
353

354
    def has_unknown(self) -> bool:
1✔
355
        """
356
        Checks whether any values in the current data do not have corresponding
357
        configurations in the defined fields.
358

359
        Returns:
360
            bool: True if there are any unknown fields; False otherwise.
361
        """
362
        if not self.data and self.fields:
1✔
363
            return True
1✔
364

365
        return any(
1✔
366
            field_name not in self.fields
367
            for field_name in self.data
368
        )
369

370
    def get_error_message(self, field_name: str) -> Optional[str]:
1✔
371
        """
372
        Retrieves and returns a predefined error message.
373

374
        This method is intended to provide a consistent error message
375
        to be used across the application when an error occurs. The
376
        message is predefined and does not accept any parameters.
377
        The exact content of the error message may vary based on
378
        specific implementation, but it is designed to convey meaningful
379
        information about the nature of an error.
380

381
        Args:
382
            field_name (str): The name of the field for which the error
383
                message is being retrieved.
384

385
        Returns:
386
            Optional[str]: A string representing the predefined error message.
387
        """
388
        return self.errors.get(field_name)
1✔
389

390
    def get_error_messages(self) -> dict[str, str]:
1✔
391
        """
392
        Retrieves all error messages associated with the fields in the input
393
        filter.
394

395
        This method aggregates and returns a dictionary of error messages
396
        where the keys represent field names, and the values are their
397
        respective error messages.
398

399
        Returns:
400
            dict[str, str]: A dictionary containing field names as keys and
401
                            their corresponding error messages as values.
402
        """
403
        return self.errors
1✔
404

405
    def add(
1✔
406
        self,
407
        name: str,
408
        required: bool = False,
409
        default: Any = None,
410
        fallback: Any = None,
411
        filters: Optional[list[BaseFilter]] = None,
412
        validators: Optional[list[BaseValidator]] = None,
413
        steps: Optional[list[Union[BaseFilter, BaseValidator]]] = None,
414
        external_api: Optional[ExternalApiConfig] = None,
415
        copy: Optional[str] = None,
416
    ) -> None:
417
        """
418
        Add the field to the input filter.
419

420
        Args:
421
            name (str): The name of the field.
422

423
            required (Optional[bool]): Whether the field is required.
424

425
            default (Optional[Any]): The default value of the field.
426

427
            fallback (Optional[Any]): The fallback value of the field, if
428
                validations fails or field None, although it is required.
429

430
            filters (Optional[list[BaseFilter]]): The filters to apply to
431
                the field value.
432

433
            validators (Optional[list[BaseValidator]]): The validators to
434
                apply to the field value.
435

436
            steps (Optional[list[Union[BaseFilter, BaseValidator]]]): Allows
437
                to apply multiple filters and validators in a specific order.
438

439
            external_api (Optional[ExternalApiConfig]): Configuration for an
440
                external API call.
441

442
            copy (Optional[str]): The name of the field to copy the value
443
                from.
444
        """
445
        if name in self.fields:
1✔
446
            raise ValueError(f"Field '{name}' already exists.")
1✔
447

448
        self.fields[name] = FieldModel(
1✔
449
            required,
450
            default,
451
            fallback,
452
            filters or [],
453
            validators or [],
454
            steps or [],
455
            external_api,
456
            copy,
457
        )
458

459
    def has(self, field_name: str) -> bool:
1✔
460
        """
461
        This method checks the existence of a specific field within the input
462
        filter values, identified by its field name. It does not return a
463
        value, serving purely as a validation or existence check mechanism.
464

465
        Args:
466
            field_name (str): The name of the field to check for existence.
467

468
        Returns:
469
            bool: True if the field exists in the input filter,
470
                otherwise False.
471
        """
472
        return field_name in self.fields
1✔
473

474
    def get_input(self, field_name: str) -> Optional[FieldModel]:
1✔
475
        """
476
        Represents a method to retrieve a field by its name.
477

478
        This method allows fetching the configuration of a specific field
479
        within the object, using its name as a string. It ensures
480
        compatibility with various field names and provides a generic
481
        return type to accommodate different data types for the fields.
482

483
        Args:
484
            field_name (str): A string representing the name of the field who
485
                        needs to be retrieved.
486

487
        Returns:
488
            Optional[FieldModel]: The field corresponding to the
489
                specified name.
490
        """
491
        return self.fields.get(field_name)
1✔
492

493
    def get_inputs(self) -> dict[str, FieldModel]:
1✔
494
        """
495
        Retrieve the dictionary of input fields associated with the object.
496

497
        Returns:
498
            dict[str, FieldModel]: Dictionary containing field names as
499
                keys and their corresponding FieldModel instances as values
500
        """
501
        return self.fields
1✔
502

503
    def remove(self, field_name: str) -> Optional[FieldModel]:
1✔
504
        """
505
        Removes the specified field from the instance or collection.
506

507
        This method is used to delete a specific field identified by
508
        its name. It ensures the designated field is removed entirely
509
        from the relevant data structure. No value is returned upon
510
        successful execution.
511

512
        Args:
513
            field_name (str): The name of the field to be removed.
514

515
        Returns:
516
            Any: The value of the removed field, if any.
517
        """
518
        return self.fields.pop(field_name, None)
1✔
519

520
    def count(self) -> int:
1✔
521
        """
522
        Counts the total number of elements in the collection.
523

524
        This method returns the total count of elements stored within the
525
        underlying data structure, providing a quick way to ascertain the
526
        size or number of entries available.
527

528
        Returns:
529
            int: The total number of elements in the collection.
530
        """
531
        return len(self.fields)
1✔
532

533
    def replace(
1✔
534
        self,
535
        name: str,
536
        required: bool = False,
537
        default: Any = None,
538
        fallback: Any = None,
539
        filters: Optional[list[BaseFilter]] = None,
540
        validators: Optional[list[BaseValidator]] = None,
541
        steps: Optional[list[Union[BaseFilter, BaseValidator]]] = None,
542
        external_api: Optional[ExternalApiConfig] = None,
543
        copy: Optional[str] = None,
544
    ) -> None:
545
        """
546
        Replaces a field in the input filter.
547

548
        Args:
549
             name (str): The name of the field.
550

551
            required (Optional[bool]): Whether the field is required.
552

553
            default (Optional[Any]): The default value of the field.
554

555
            fallback (Optional[Any]): The fallback value of the field, if
556
                validations fails or field None, although it is required.
557

558
            filters (Optional[list[BaseFilter]]): The filters to apply to
559
                the field value.
560

561
            validators (Optional[list[BaseValidator]]): The validators to
562
                apply to the field value.
563

564
            steps (Optional[list[Union[BaseFilter, BaseValidator]]]): Allows
565
                to apply multiple filters and validators in a specific order.
566

567
            external_api (Optional[ExternalApiConfig]): Configuration for an
568
                external API call.
569

570
            copy (Optional[str]): The name of the field to copy the value
571
                from.
572
        """
573
        self.fields[name] = FieldModel(
1✔
574
            required,
575
            default,
576
            fallback,
577
            filters or [],
578
            validators or [],
579
            steps or [],
580
            external_api,
581
            copy,
582
        )
583

584
    def add_global_filter(self, filter: BaseFilter) -> None:
1✔
585
        """
586
        Add a global filter to be applied to all fields.
587

588
        Args:
589
            filter: The filter to add.
590
        """
591
        self.global_filters.append(filter)
1✔
592

593
    def get_global_filters(self) -> list[BaseFilter]:
1✔
594
        """
595
        Retrieve all global filters associated with this InputFilter instance.
596

597
        This method returns a list of BaseFilter instances that have been
598
        added as global filters. These filters are applied universally to
599
        all fields during data processing.
600

601
        Returns:
602
            list[BaseFilter]: A list of global filters.
603
        """
604
        return self.global_filters
1✔
605

606
    def clear(self) -> None:
1✔
607
        """
608
        Resets all fields of the InputFilter instance to their initial empty
609
        state.
610

611
        This method clears the internal storage of fields, conditions, filters,
612
        validators, and data, effectively resetting the object as if it were
613
        newly initialized.
614
        """
615
        self.fields.clear()
1✔
616
        self.conditions.clear()
1✔
617
        self.global_filters.clear()
1✔
618
        self.global_validators.clear()
1✔
619
        self.data.clear()
1✔
620
        self.validated_data.clear()
1✔
621
        self.errors.clear()
1✔
622

623
    def merge(self, other: InputFilter) -> None:
1✔
624
        """
625
        Merges another InputFilter instance intelligently into the current
626
        instance.
627

628
        - Fields with the same name are merged recursively if possible,
629
            otherwise overwritten.
630
        - Conditions, are combined and duplicated.
631
        - Global filters and validators are merged without duplicates.
632

633
        Args:
634
            other (InputFilter): The InputFilter instance to merge.
635
        """
636
        if not isinstance(other, InputFilter):
1✔
637
            raise TypeError(
1✔
638
                "Can only merge with another InputFilter instance."
639
            )
640

641
        for key, new_field in other.get_inputs().items():
1✔
642
            self.fields[key] = new_field
1✔
643

644
        self.conditions += other.conditions
1✔
645

646
        for filter in other.global_filters:
1✔
647
            existing_type_map = {
1✔
648
                type(v): i for i, v in enumerate(self.global_filters)
649
            }
650
            if type(filter) in existing_type_map:
1✔
651
                self.global_filters[existing_type_map[type(filter)]] = filter
1✔
652
            else:
653
                self.global_filters.append(filter)
1✔
654

655
        for validator in other.global_validators:
1✔
656
            existing_type_map = {
1✔
657
                type(v): i for i, v in enumerate(self.global_validators)
658
            }
659
            if type(validator) in existing_type_map:
1✔
660
                self.global_validators[existing_type_map[type(validator)]] = (
1✔
661
                    validator
662
                )
663
            else:
664
                self.global_validators.append(validator)
1✔
665

666
    def set_model(self, model_class: Type[T]) -> None:
1✔
667
        """
668
        Set the model class for serialization.
669

670
        Args:
671
            model_class (Type[T]): The class to use for serialization.
672
        """
673
        self.model_class = model_class
1✔
674

675
    def serialize(self) -> Union[dict[str, Any], T]:
1✔
676
        """
677
        Serialize the validated data. If a model class is set, returns an
678
        instance of that class, otherwise returns the raw validated data.
679

680
        Returns:
681
            Union[dict[str, Any], T]: The serialized data.
682
        """
683
        if self.model_class is None:
1✔
684
            return self.validated_data
1✔
685

686
        return self.model_class(**self.validated_data)
1✔
687

688
    def add_global_validator(self, validator: BaseValidator) -> None:
1✔
689
        """
690
        Add a global validator to be applied to all fields.
691

692
        Args:
693
            validator (BaseValidator): The validator to add.
694
        """
695
        self.global_validators.append(validator)
1✔
696

697
    def get_global_validators(self) -> list[BaseValidator]:
1✔
698
        """
699
        Retrieve all global validators associated with this InputFilter
700
        instance.
701

702
        This method returns a list of BaseValidator instances that have been
703
        added as global validators. These validators are applied universally
704
        to all fields during validation.
705

706
        Returns:
707
            list[BaseValidator]: A list of global validators.
708
        """
709
        return self.global_validators
1✔
710

711
    def _check_all_conditions(self, validated_data, errors) -> None:
1✔
712
        """Check all conditions against validated data."""
713
        try:
1✔
714
            FieldMixin.check_conditions(self.conditions, validated_data)
1✔
715
        except ValidationError as e:
1✔
716
            errors["_condition"] = str(e)
1✔
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