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

LeanderCS / flask-inputfilter / #419

02 Jul 2025 08:04PM UTC coverage: 94.487% (-1.3%) from 95.792%
#419

Pull #60

coveralls-python

LeanderCS
Move complex logic outside of base InputFilter class
Pull Request #60: Optimize

292 of 328 new or added lines in 108 files covered. (89.02%)

10 existing lines in 2 files now uncovered.

1868 of 1977 relevant lines covered (94.49%)

0.94 hits per line

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

93.48
/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 DataMixin
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.models import BaseCondition, BaseValidator
×
18

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

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

39

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

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

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

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

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

77
        return True
1✔
78

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

86
        Args:
87
            cls
88

89
        Returns:
90
            Callable
91
        """
92

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

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

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

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

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

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

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

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

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

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

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

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

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

158
            return wrapper
1✔
159

160
        return decorator
1✔
161

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

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

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

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

188
        validated_data, errors = DataMixin.validate_with_conditions(
1✔
189
            self.fields,
190
            data,
191
            self.global_filters,
192
            self.global_validators,
193
            self.conditions,
194
        )
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 = DataMixin.filter_data(
1✔
238
            data, self.fields, self.global_filters
239
        )
240

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

250
        Args:
251
            name (str): A string that represents the identifier for which the
252
                 corresponding value is being retrieved. It is used to perform
253
                 the lookup.
254

255
        Returns:
256
            Any: The retrieved value associated with the given name. The
257
                 specific type of this value is dependent on the
258
                 implementation and the data being accessed.
259
        """
260
        return self.validated_data.get(name)
1✔
261

262
    def get_values(self) -> dict[str, Any]:
1✔
263
        """
264
        Retrieves a dictionary of key-value pairs from the current object. This
265
        method provides access to the internal state or configuration of the
266
        object in a dictionary format, where keys are strings and values can be
267
        of various types depending on the object's design.
268

269
        Returns:
270
            dict[str, Any]: A dictionary containing string keys and their
271
                            corresponding values of any data type.
272
        """
273
        return self.validated_data
1✔
274

275
    def get_raw_value(self, name: str) -> Any:
1✔
276
        """
277
        Fetches the raw value associated with the provided key.
278

279
        This method is used to retrieve the underlying value linked to the
280
        given key without applying any transformations or validations. It
281
        directly fetches the raw stored value and is typically used in
282
        scenarios where the raw data is needed for processing or debugging
283
        purposes.
284

285
        Args:
286
            name (str): The name of the key whose raw value is to be
287
                retrieved.
288

289
        Returns:
290
            Any: The raw value associated with the provided key.
291
        """
292
        return self.data.get(name)
1✔
293

294
    def get_raw_values(self) -> dict[str, Any]:
1✔
295
        """
296
        Retrieves raw values from a given source and returns them as a
297
        dictionary.
298

299
        This method is used to fetch and return unprocessed or raw data in
300
        the form of a dictionary where the keys are strings, representing
301
        the identifiers, and the values are of any data type.
302

303
        Returns:
304
            dict[str, Any]: A dictionary containing the raw values retrieved.
305
               The keys are strings representing the identifiers, and the
306
               values can be of any type, depending on the source
307
               being accessed.
308
        """
309
        if not self.fields:
1✔
310
            return {}
1✔
311

312
        # Use optimized intersection for larger datasets
313
        if len(self.fields) > 10:
1✔
NEW
314
            field_set = set(self.fields.keys())
×
NEW
315
            data_set = set(self.data.keys())
×
NEW
316
            common_fields = field_set & data_set
×
NEW
317
            return {field: self.data[field] for field in common_fields}
×
318
        return {
1✔
319
            field: self.data[field]
320
            for field in self.fields
321
            if field in self.data
322
        }
323

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

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

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

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

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

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

358
        Returns:
359
            bool: True if there are any unknown fields; False otherwise.
360
        """
361
        return DataMixin.has_unknown_fields(self.data, self.fields)
1✔
362

363
    def get_error_message(self, field_name: str) -> Optional[str]:
1✔
364
        """
365
        Retrieves and returns a predefined error message.
366

367
        This method is intended to provide a consistent error message
368
        to be used across the application when an error occurs. The
369
        message is predefined and does not accept any parameters.
370
        The exact content of the error message may vary based on
371
        specific implementation, but it is designed to convey meaningful
372
        information about the nature of an error.
373

374
        Args:
375
            field_name (str): The name of the field for which the error
376
                message is being retrieved.
377

378
        Returns:
379
            Optional[str]: A string representing the predefined error message.
380
        """
381
        return self.errors.get(field_name)
1✔
382

383
    def get_error_messages(self) -> dict[str, str]:
1✔
384
        """
385
        Retrieves all error messages associated with the fields in the input
386
        filter.
387

388
        This method aggregates and returns a dictionary of error messages
389
        where the keys represent field names, and the values are their
390
        respective error messages.
391

392
        Returns:
393
            dict[str, str]: A dictionary containing field names as keys and
394
                            their corresponding error messages as values.
395
        """
396
        return self.errors
1✔
397

398
    def add(
1✔
399
        self,
400
        name: str,
401
        required: bool = False,
402
        default: Any = None,
403
        fallback: Any = None,
404
        filters: Optional[list[BaseFilter]] = None,
405
        validators: Optional[list[BaseValidator]] = None,
406
        steps: Optional[list[Union[BaseFilter, BaseValidator]]] = None,
407
        external_api: Optional[ExternalApiConfig] = None,
408
        copy: Optional[str] = None,
409
    ) -> None:
410
        """
411
        Add the field to the input filter.
412

413
        Args:
414
            name (str): The name of the field.
415

416
            required (Optional[bool]): Whether the field is required.
417

418
            default (Optional[Any]): The default value of the field.
419

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

423
            filters (Optional[list[BaseFilter]]): The filters to apply to
424
                the field value.
425

426
            validators (Optional[list[BaseValidator]]): The validators to
427
                apply to the field value.
428

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

432
            external_api (Optional[ExternalApiConfig]): Configuration for an
433
                external API call.
434

435
            copy (Optional[str]): The name of the field to copy the value
436
                from.
437
        """
438
        if name in self.fields:
1✔
439
            raise ValueError(f"Field '{name}' already exists.")
1✔
440

441
        self.fields[name] = FieldModel(
1✔
442
            required,
443
            default,
444
            fallback,
445
            filters or [],
446
            validators or [],
447
            steps or [],
448
            external_api,
449
            copy,
450
        )
451

452
    def has(self, field_name: str) -> bool:
1✔
453
        """
454
        This method checks the existence of a specific field within the input
455
        filter values, identified by its field name. It does not return a
456
        value, serving purely as a validation or existence check mechanism.
457

458
        Args:
459
            field_name (str): The name of the field to check for existence.
460

461
        Returns:
462
            bool: True if the field exists in the input filter,
463
                otherwise False.
464
        """
465
        return field_name in self.fields
1✔
466

467
    def get_input(self, field_name: str) -> Optional[FieldModel]:
1✔
468
        """
469
        Represents a method to retrieve a field by its name.
470

471
        This method allows fetching the configuration of a specific field
472
        within the object, using its name as a string. It ensures
473
        compatibility with various field names and provides a generic
474
        return type to accommodate different data types for the fields.
475

476
        Args:
477
            field_name (str): A string representing the name of the field who
478
                        needs to be retrieved.
479

480
        Returns:
481
            Optional[FieldModel]: The field corresponding to the
482
                specified name.
483
        """
484
        return self.fields.get(field_name)
1✔
485

486
    def get_inputs(self) -> dict[str, FieldModel]:
1✔
487
        """
488
        Retrieve the dictionary of input fields associated with the object.
489

490
        Returns:
491
            dict[str, FieldModel]: Dictionary containing field names as
492
                keys and their corresponding FieldModel instances as values
493
        """
494
        return self.fields
1✔
495

496
    def remove(self, field_name: str) -> Optional[FieldModel]:
1✔
497
        """
498
        Removes the specified field from the instance or collection.
499

500
        This method is used to delete a specific field identified by
501
        its name. It ensures the designated field is removed entirely
502
        from the relevant data structure. No value is returned upon
503
        successful execution.
504

505
        Args:
506
            field_name (str): The name of the field to be removed.
507

508
        Returns:
509
            Any: The value of the removed field, if any.
510
        """
511
        return self.fields.pop(field_name, None)
1✔
512

513
    def count(self) -> int:
1✔
514
        """
515
        Counts the total number of elements in the collection.
516

517
        This method returns the total count of elements stored within the
518
        underlying data structure, providing a quick way to ascertain the
519
        size or number of entries available.
520

521
        Returns:
522
            int: The total number of elements in the collection.
523
        """
524
        return len(self.fields)
1✔
525

526
    def replace(
1✔
527
        self,
528
        name: str,
529
        required: bool = False,
530
        default: Any = None,
531
        fallback: Any = None,
532
        filters: Optional[list[BaseFilter]] = None,
533
        validators: Optional[list[BaseValidator]] = None,
534
        steps: Optional[list[Union[BaseFilter, BaseValidator]]] = None,
535
        external_api: Optional[ExternalApiConfig] = None,
536
        copy: Optional[str] = None,
537
    ) -> None:
538
        """
539
        Replaces a field in the input filter.
540

541
        Args:
542
             name (str): The name of the field.
543

544
            required (Optional[bool]): Whether the field is required.
545

546
            default (Optional[Any]): The default value of the field.
547

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

551
            filters (Optional[list[BaseFilter]]): The filters to apply to
552
                the field value.
553

554
            validators (Optional[list[BaseValidator]]): The validators to
555
                apply to the field value.
556

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

560
            external_api (Optional[ExternalApiConfig]): Configuration for an
561
                external API call.
562

563
            copy (Optional[str]): The name of the field to copy the value
564
                from.
565
        """
566
        self.fields[name] = FieldModel(
1✔
567
            required,
568
            default,
569
            fallback,
570
            filters or [],
571
            validators or [],
572
            steps or [],
573
            external_api,
574
            copy,
575
        )
576

577
    def add_global_filter(self, filter: BaseFilter) -> None:
1✔
578
        """
579
        Add a global filter to be applied to all fields.
580

581
        Args:
582
            filter: The filter to add.
583
        """
584
        self.global_filters.append(filter)
1✔
585

586
    def get_global_filters(self) -> list[BaseFilter]:
1✔
587
        """
588
        Retrieve all global filters associated with this InputFilter instance.
589

590
        This method returns a list of BaseFilter instances that have been
591
        added as global filters. These filters are applied universally to
592
        all fields during data processing.
593

594
        Returns:
595
            list[BaseFilter]: A list of global filters.
596
        """
597
        return self.global_filters
1✔
598

599
    def clear(self) -> None:
1✔
600
        """
601
        Resets all fields of the InputFilter instance to their initial empty
602
        state.
603

604
        This method clears the internal storage of fields, conditions, filters,
605
        validators, and data, effectively resetting the object as if it were
606
        newly initialized.
607
        """
608
        self.fields.clear()
1✔
609
        self.conditions.clear()
1✔
610
        self.global_filters.clear()
1✔
611
        self.global_validators.clear()
1✔
612
        self.data.clear()
1✔
613
        self.validated_data.clear()
1✔
614
        self.errors.clear()
1✔
615

616
    def merge(self, other: InputFilter) -> None:
1✔
617
        """
618
        Merges another InputFilter instance intelligently into the current
619
        instance.
620

621
        - Fields with the same name are merged recursively if possible,
622
            otherwise overwritten.
623
        - Conditions are combined and duplicated.
624
        - Global filters and validators are merged without duplicates.
625

626
        Args:
627
            other (InputFilter): The InputFilter instance to merge.
628
        """
629
        if not isinstance(other, InputFilter):
1✔
630
            raise TypeError(
1✔
631
                "Can only merge with another InputFilter instance."
632
            )
633

634
        DataMixin.merge_input_filters(self, other)
1✔
635

636
    def set_model(self, model_class: Type[T]) -> None:
1✔
637
        """
638
        Set the model class for serialization.
639

640
        Args:
641
            model_class (Type[T]): The class to use for serialization.
642
        """
643
        self.model_class = model_class
1✔
644

645
    def serialize(self) -> Union[dict[str, Any], T]:
1✔
646
        """
647
        Serialize the validated data. If a model class is set, returns an
648
        instance of that class, otherwise returns the raw validated data.
649

650
        Returns:
651
            Union[dict[str, Any], T]: The serialized data.
652
        """
653
        if self.model_class is None:
1✔
654
            return self.validated_data
1✔
655

656
        return self.model_class(**self.validated_data)
1✔
657

658
    def add_global_validator(self, validator: BaseValidator) -> None:
1✔
659
        """
660
        Add a global validator to be applied to all fields.
661

662
        Args:
663
            validator (BaseValidator): The validator to add.
664
        """
665
        self.global_validators.append(validator)
1✔
666

667
    def get_global_validators(self) -> list[BaseValidator]:
1✔
668
        """
669
        Retrieve all global validators associated with this InputFilter
670
        instance.
671

672
        This method returns a list of BaseValidator instances that have been
673
        added as global validators. These validators are applied universally
674
        to all fields during validation.
675

676
        Returns:
677
            list[BaseValidator]: A list of global validators.
678
        """
679
        return self.global_validators
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