Skip to content

Commit 84b01d7

Browse files
committed
Cython decorator
1 parent 0988375 commit 84b01d7

18 files changed

+696
-43
lines changed

docs/source/changelog.rst

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,44 @@ Changelog
33

44
All notable changes to this project will be documented in this file.
55

6+
7+
[0.7.0] - 2025-09-25
8+
--------------------
9+
10+
Added
11+
^^^^^
12+
- Added that fields, conditons, global filters and global validators can be
13+
added as decorator and do not require a self.add, ... call in the __init__.
14+
15+
**Before**:
16+
.. code-block:: python
17+
18+
class UpdateZipcodeInputFilter(InputFilter):
19+
def __init__(self):
20+
super().__init__()
21+
self.add(
22+
'id',
23+
required=True,
24+
filters=[ToIntegerFilter(), ToNullFilter()],
25+
validators=[
26+
IsIntegerValidator()
27+
]
28+
)
29+
30+
**After**:
31+
32+
.. code-block:: python
33+
34+
class UpdateZipcodeInputFilter(InputFilter):
35+
id: int = field(
36+
required=True,
37+
filters=[ToIntegerFilter(), ToNullFilter()],
38+
validators=[
39+
IsIntegerValidator()
40+
]
41+
)
42+
43+
644
[0.6.3] - 2025-09-24
745
--------------------
846

flask_inputfilter/_input_filter.pxd

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ cdef class InputFilter:
4343
bint required=*,
4444
object default=*,
4545
object fallback=*,
46-
list filters=*,
47-
list validators=*,
46+
list[BaseFilter] filters=*,
47+
list[BaseValidator] validators=*,
4848
list steps=*,
4949
ExternalApiConfig external_api=*,
5050
str copy=*,
@@ -74,3 +74,5 @@ cdef class InputFilter:
7474
cpdef object serialize(self)
7575
cpdef void add_global_validator(self, BaseValidator validator)
7676
cpdef list[BaseValidator] get_global_validators(self)
77+
cdef void _set_methods(self, list methods)
78+
cdef void _register_decorator_components(self)

flask_inputfilter/_input_filter.pyx

Lines changed: 124 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
11
# cython: language=c++
2+
# cython: boundscheck=False
3+
# cython: wraparound=False
4+
# cython: cdivision=True
5+
# cython: overflowcheck=False
6+
# cython: initializedcheck=False
7+
# cython: nonecheck=False
8+
# cython: optimize.use_switch=True
9+
# cython: optimize.unpack_method_calls=True
10+
# cython: infer_types=True
211

312
import json
413
import logging
@@ -9,11 +18,14 @@ from flask import Response, g, request
918

1019
from flask_inputfilter.exceptions import ValidationError
1120

21+
from flask_inputfilter.declarative.cimports cimport FieldDescriptor
1222
from flask_inputfilter.mixins.cimports cimport DataMixin
1323
from flask_inputfilter.models.cimports cimport BaseCondition, BaseFilter, BaseValidator, ExternalApiConfig, FieldModel
1424

1525
from libcpp.vector cimport vector
1626
from libcpp.string cimport string
27+
from libcpp cimport bool as cbool
28+
from libcpp.algorithm cimport find
1729

1830
cdef dict _INTERNED_STRINGS = {
1931
"_condition": sys.intern("_condition"),
@@ -36,6 +48,44 @@ cdef dict _INTERNED_STRINGS = {
3648
cdef extern from "helper.h":
3749
vector[string] make_default_methods()
3850

51+
cdef cppclass ObjectPool[T]:
52+
ObjectPool()
53+
T* acquire()
54+
void release(T* obj)
55+
void release_all()
56+
void reserve(size_t capacity)
57+
size_t size()
58+
size_t available()
59+
60+
cdef cppclass FieldLookup:
61+
FieldLookup()
62+
void add_field(const string& name)
63+
bint has_field(const string& name)
64+
size_t get_index(const string& name)
65+
const vector[string]& get_field_names()
66+
void clear()
67+
void reserve(size_t capacity)
68+
69+
cdef cppclass StringIntern:
70+
@staticmethod
71+
const string& intern(const string& str)
72+
@staticmethod
73+
void clear()
74+
@staticmethod
75+
size_t count()
76+
77+
78+
# Fast C++ string processing
79+
cdef extern from "<string_view>" namespace "std":
80+
cdef cppclass string_view:
81+
string_view()
82+
string_view(const char* data)
83+
string_view(const char* data, size_t len)
84+
cbool empty()
85+
size_t size()
86+
const char* data()
87+
cbool starts_with(string_view prefix)
88+
3989
T = TypeVar("T")
4090

4191

@@ -57,8 +107,22 @@ cdef class InputFilter:
57107

58108
def __init__(self, methods: Optional[list[str]] = None) -> None:
59109
if methods is not None:
60-
self.methods.clear()
61-
[self.methods.push_back(method.encode()) for method in methods]
110+
self._set_methods(methods)
111+
112+
self._register_decorator_components()
113+
114+
cdef void _set_methods(self, list methods):
115+
"""Efficiently set HTTP methods using C++ vector operations."""
116+
cdef str method
117+
cdef bytes encoded_method
118+
cdef Py_ssize_t n = len(methods)
119+
120+
self.methods.clear()
121+
self.methods.reserve(n)
122+
123+
for method in methods:
124+
encoded_method = method.encode('utf-8')
125+
self.methods.push_back(string(encoded_method))
62126

63127
cpdef bint is_valid(self):
64128
"""
@@ -122,8 +186,14 @@ cdef class InputFilter:
122186
"""
123187

124188
cdef InputFilter input_filter = cls()
125-
cdef string request_method = request.method.encode()
126-
if not any(request_method == method for method in input_filter.methods):
189+
cdef bytes request_method_bytes = request.method.encode('utf-8')
190+
cdef string request_method = string(request_method_bytes)
191+
cdef vector[string].iterator method_it = find(
192+
input_filter.methods.begin(),
193+
input_filter.methods.end(),
194+
request_method
195+
)
196+
if method_it == input_filter.methods.end():
127197
return Response(status=405, response="Method Not Allowed")
128198

129199
if request.is_json:
@@ -206,6 +276,48 @@ cdef class InputFilter:
206276
"""
207277
self.conditions.append(condition)
208278

279+
cdef void _register_decorator_components(self):
280+
"""Register decorator-based components from the current class only."""
281+
cdef object cls, attr_value, conditions, validators, filters
282+
cdef str attr_name
283+
cdef list dir_attrs
284+
cdef FieldDescriptor field_desc
285+
286+
cls = self.__class__
287+
dir_attrs = dir(cls)
288+
289+
for attr_name in dir_attrs:
290+
if (<bytes>attr_name.encode('utf-8')).startswith(b"_"):
291+
continue
292+
293+
attr_value = getattr(cls, attr_name, None)
294+
if attr_value is not None and isinstance(attr_value, FieldDescriptor):
295+
field_desc = <FieldDescriptor>attr_value
296+
self.fields[attr_name] = FieldModel(
297+
field_desc.required,
298+
field_desc._default,
299+
field_desc.fallback,
300+
field_desc.filters,
301+
field_desc.validators,
302+
field_desc.steps,
303+
field_desc.external_api,
304+
field_desc.copy,
305+
)
306+
307+
conditions = getattr(cls, "_conditions", None)
308+
if conditions is not None:
309+
self.conditions.extend(conditions)
310+
311+
validators = getattr(cls, "_global_validators", None)
312+
if validators is not None:
313+
self.global_validators.extend(validators)
314+
315+
filters = getattr(cls, "_global_filters", None)
316+
if filters is not None:
317+
self.global_filters.extend(filters)
318+
319+
self.model_class = getattr(cls, "_model", self.model_class)
320+
209321
cpdef list get_conditions(self):
210322
"""
211323
Retrieve the list of all registered conditions.
@@ -307,14 +419,19 @@ cdef class InputFilter:
307419

308420
cdef:
309421
Py_ssize_t i, n = len(self.fields)
310-
dict result = {}
422+
dict result
311423
list field_names = list(self.fields.keys())
312424
str field
425+
object field_value
426+
427+
# Pre-allocate dictionary size for better performance
428+
result = {}
313429

314430
for i in range(n):
315431
field = field_names[i]
316-
if field in self.data:
317-
result[field] = self.data[field]
432+
field_value = self.data.get(field)
433+
if field_value is not None:
434+
result[field] = field_value
318435
return result
319436

320437
cpdef dict[str, Any] get_unfiltered_data(self):

flask_inputfilter/declarative/__init__.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1-
from .factory_functions import field
2-
from .field_descriptor import FieldDescriptor
1+
try:
2+
from ._field_descriptor import FieldDescriptor
3+
from ._factory_functions import field
4+
except ImportError:
5+
from .field_descriptor import FieldDescriptor
6+
from .factory_functions import field
37

48
__all__ = [
59
"FieldDescriptor",
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# cython: language_level=3
2+
3+
from flask_inputfilter.models.cimports cimport BaseFilter, BaseValidator, ExternalApiConfig
4+
from ._field_descriptor cimport FieldDescriptor
5+
6+
cpdef FieldDescriptor field(
7+
bint required=*,
8+
object default=*,
9+
object fallback=*,
10+
list[BaseFilter] filters=*,
11+
list[BaseValidator] validators=*,
12+
list steps=*,
13+
ExternalApiConfig external_api=*,
14+
str copy=*
15+
)
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# cython: language_level=3
2+
3+
from ._field_descriptor cimport FieldDescriptor
4+
from flask_inputfilter.models.cimports cimport BaseFilter, BaseValidator, ExternalApiConfig
5+
6+
cpdef FieldDescriptor field(
7+
bint required = False,
8+
object default = None,
9+
object fallback = None,
10+
list[BaseFilter] filters = None,
11+
list[BaseValidator] validators = None,
12+
list steps = None,
13+
ExternalApiConfig external_api = None,
14+
str copy = None,
15+
):
16+
"""
17+
Create a field descriptor for declarative field definition.
18+
19+
This function creates a FieldDescriptor that can be used as a class
20+
attribute to define input filter fields declaratively.
21+
22+
**Parameters:**
23+
24+
- **required** (*bool*): Whether the field is required. Default: False.
25+
- **default** (*Any*): The default value of the field. Default: None.
26+
- **fallback** (*Any*): The fallback value of the field, if
27+
validations fail or field is None, although it is required. Default: None.
28+
- **filters** (*Optional[list[BaseFilter]]*): The filters to apply to
29+
the field value. Default: None.
30+
- **validators** (*Optional[list[BaseValidator]]*): The validators to
31+
apply to the field value. Default: None.
32+
- **steps** (*Optional[list[Union[BaseFilter, BaseValidator]]]*): Allows
33+
to apply multiple filters and validators in a specific order. Default: None.
34+
- **external_api** (*Optional[ExternalApiConfig]*): Configuration for an
35+
external API call. Default: None.
36+
- **copy** (*Optional[str]*): The name of the field to copy the value
37+
from. Default: None.
38+
39+
**Returns:**
40+
41+
A field descriptor configured with the given parameters.
42+
43+
**Example:**
44+
45+
.. code-block:: python
46+
47+
from flask_inputfilter import InputFilter
48+
from flask_inputfilter.declarative import field
49+
from flask_inputfilter.validators import IsStringValidator
50+
51+
class UserInputFilter(InputFilter):
52+
name: str = field(required=True, validators=[IsStringValidator()])
53+
age: int = field(required=True, default=18)
54+
"""
55+
return FieldDescriptor(
56+
required=required,
57+
default=default,
58+
fallback=fallback,
59+
filters=filters,
60+
validators=validators,
61+
steps=steps,
62+
external_api=external_api,
63+
copy=copy,
64+
)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# cython: language_level=3
2+
3+
from flask_inputfilter.models.cimports cimport BaseFilter, BaseValidator, ExternalApiConfig
4+
5+
cdef class FieldDescriptor:
6+
cdef readonly:
7+
bint required
8+
object _default
9+
object fallback
10+
list[BaseFilter] filters
11+
list[BaseValidator] validators
12+
list steps
13+
ExternalApiConfig external_api
14+
str copy
15+
cdef public:
16+
str name
17+
18+
cpdef void __set_name__(self, object owner, str name)

0 commit comments

Comments
 (0)