Skip to content

Commit a158b41

Browse files
committed
Update cython to optimize for larger objects
1 parent 6962266 commit a158b41

File tree

11 files changed

+142
-57
lines changed

11 files changed

+142
-57
lines changed

docs/source/changelog.rst

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

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

6+
[0.6.4] - 2025-08-12
7+
--------------------
8+
9+
Changed
10+
^^^^^^^
11+
- Increased freelist sizes for ``InputFilter``, ``FieldModel``, and ``ExternalApiConfig`` to reduce allocations
12+
- Replaced Python generators with C-style loops and added early exit conditions in filter/validator chains
13+
- Optimized dictionary operations and string lookups with extended interning
14+
- Improved ``has_unknown_fields`` with adaptive strategies based on data size
15+
16+
617
[0.6.3] - 2025-08-11
718
--------------------
819

flask_inputfilter/_input_filter.pyx

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
# cython: language=c++
2+
# cython: freelist=256
3+
# cython: boundscheck=False
4+
# cython: wraparound=False
5+
# cython: cdivision=True
26

37
import json
48
import logging
@@ -31,6 +35,12 @@ cdef dict _INTERNED_STRINGS = {
3135
"required": sys.intern("required"),
3236
"steps": sys.intern("steps"),
3337
"validators": sys.intern("validators"),
38+
"data": sys.intern("data"),
39+
"errors": sys.intern("errors"),
40+
"validated_data": sys.intern("validated_data"),
41+
"fields": sys.intern("fields"),
42+
"name": sys.intern("name"),
43+
"value": sys.intern("value"),
3444
}
3545

3646
cdef extern from "helper.h":
@@ -121,9 +131,18 @@ cdef class InputFilter:
121131
Union[Response, tuple[Any, dict[str, Any]]]: The response from the route function or an error response.
122132
"""
123133

124-
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):
134+
cdef:
135+
InputFilter input_filter = cls()
136+
string request_method = request.method.encode()
137+
Py_ssize_t i, n = input_filter.methods.size()
138+
bint method_allowed = False
139+
140+
for i in range(n):
141+
if request_method == input_filter.methods[i]:
142+
method_allowed = True
143+
break
144+
145+
if not method_allowed:
127146
return Response(status=405, response="Method Not Allowed")
128147

129148
if request.is_json:
@@ -310,11 +329,13 @@ cdef class InputFilter:
310329
dict result = {}
311330
list field_names = list(self.fields.keys())
312331
str field
332+
object value
313333

314334
for i in range(n):
315335
field = field_names[i]
316-
if field in self.data:
317-
result[field] = self.data[field]
336+
value = self.data.get(field)
337+
if value is not None:
338+
result[field] = value
318339
return result
319340

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

flask_inputfilter/mixins/data_mixin/_data_mixin.pyx

Lines changed: 53 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
# cython: language=c++
2+
# cython: boundscheck=False
3+
# cython: wraparound=False
4+
# cython: cdivision=True
25

36
from typing import Any
47

@@ -10,6 +13,7 @@ from flask_inputfilter.models.cimports cimport BaseFilter, BaseValidator, FieldM
1013

1114
# Compile-time constants for performance thresholds
1215
DEF LARGE_DATASET_THRESHOLD = 10
16+
DEF SMALL_DICT_THRESHOLD = 5
1317

1418

1519
cdef class DataMixin:
@@ -35,19 +39,22 @@ cdef class DataMixin:
3539
if not data and fields:
3640
return True
3741

38-
cdef set field_set
42+
cdef:
43+
set field_set
44+
Py_ssize_t field_count = len(fields)
45+
Py_ssize_t data_count = len(data)
3946

40-
# Use set operations for faster lookup when there are many fields
41-
if len(fields) > LARGE_DATASET_THRESHOLD:
47+
if field_count > LARGE_DATASET_THRESHOLD and data_count > SMALL_DICT_THRESHOLD:
4248
field_set = set(fields.keys())
4349
for field_name in data.keys():
4450
if field_name not in field_set:
4551
return True
46-
else:
47-
# Use direct dict lookup for smaller field counts
52+
elif data_count < field_count:
4853
for field_name in data.keys():
4954
if field_name not in fields:
5055
return True
56+
else:
57+
return bool(set(data.keys()) - set(fields.keys()))
5158

5259
return False
5360

@@ -72,19 +79,27 @@ cdef class DataMixin:
7279
"""
7380
cdef:
7481
dict[str, Any] filtered_data = {}
75-
Py_ssize_t i, n = len(data) if data else 0
76-
list keys = list(data.keys()) if n > 0 else []
77-
list values = list(data.values()) if n > 0 else []
82+
Py_ssize_t i, n = len(data) if data is not None else 0
83+
list keys
84+
list values
7885
str field_name
7986
object field_value
87+
FieldModel field_info
88+
89+
if n == 0:
90+
return filtered_data
91+
92+
keys = list(data.keys())
93+
values = list(data.values())
8094

8195
for i in range(n):
8296
field_name = keys[i]
8397
field_value = values[i]
84-
85-
if field_name in fields:
98+
99+
field_info = fields.get(field_name)
100+
if field_info is not None:
86101
field_value = ValidationMixin.apply_filters(
87-
fields[field_name].filters,
102+
field_info.filters,
88103
global_filters,
89104
field_value,
90105
)
@@ -125,8 +140,8 @@ cdef class DataMixin:
125140
fields, data, global_filters, global_validators
126141
)
127142

128-
# Check conditions if present and no errors yet
129-
if conditions and not errors:
143+
# Check conditions only if present and no errors yet
144+
if conditions is not None and len(conditions) > 0 and not errors:
130145
try:
131146
ValidationMixin.check_conditions(conditions, validated_data)
132147
except ValidationError as e:
@@ -150,13 +165,18 @@ cdef class DataMixin:
150165
cdef:
151166
Py_ssize_t i, n
152167
dict source_inputs = source_filter.get_inputs()
153-
list keys = list(source_inputs.keys()) if source_inputs else []
154-
list new_fields = list(source_inputs.values()) if source_inputs else []
168+
list keys
169+
list new_fields
170+
171+
if source_inputs:
172+
keys = list(source_inputs.keys())
173+
new_fields = list(source_inputs.values())
174+
n = len(keys)
155175

156-
# Merge fields efficiently
157-
n = len(keys)
158-
for i in range(n):
159-
target_filter.fields[keys[i]] = new_fields[i]
176+
for i in range(n):
177+
target_filter.fields[keys[i]] = new_fields[i]
178+
else:
179+
n = 0
160180

161181
# Merge conditions
162182
target_filter.conditions.extend(source_filter.conditions)
@@ -183,13 +203,19 @@ cdef class DataMixin:
183203
- **target_list** (*list*): The list to merge into.
184204
- **source_list** (*list*): The list to merge from.
185205
"""
186-
cdef dict existing_type_map
187-
206+
cdef:
207+
dict existing_type_map = {}
208+
Py_ssize_t i, n = len(target_list)
209+
object component
210+
type component_type
211+
212+
for i in range(n):
213+
existing_type_map[type(target_list[i])] = i
214+
188215
for component in source_list:
189-
existing_type_map = {
190-
type(v): i for i, v in enumerate(target_list)
191-
}
192-
if type(component) in existing_type_map:
193-
target_list[existing_type_map[type(component)]] = component
216+
component_type = type(component)
217+
if component_type in existing_type_map:
218+
target_list[existing_type_map[component_type]] = component
194219
else:
195-
target_list.append(component)
220+
target_list.append(component)
221+
existing_type_map[component_type] = len(target_list) - 1

flask_inputfilter/mixins/external_api_mixin/_external_api_mixin.pyx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# cython: language=c++
2+
# cython: boundscheck=False
3+
# cython: wraparound=False
24

35
import re
46
from typing import Any

flask_inputfilter/mixins/validation_mixin/_validation_mixin.pyx

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
# cython: language=c++
2+
# cython: boundscheck=False
3+
# cython: wraparound=False
4+
# cython: cdivision=True
5+
# cython: nonecheck=False
26

37
import cython
48
from typing import Any
@@ -38,18 +42,23 @@ cdef class ValidationMixin:
3842
return None
3943

4044
cdef:
41-
Py_ssize_t i, n
45+
Py_ssize_t i, n1, n2
4246
BaseFilter current_filter
4347

44-
n = len(filters1) if filters1 else 0
45-
for i in range(n):
48+
n1 = len(filters1) if filters1 is not None else 0
49+
n2 = len(filters2) if filters2 is not None else 0
50+
51+
for i in range(n1):
4652
current_filter = filters1[i]
4753
value = current_filter.apply(value)
54+
if value is None:
55+
return None
4856

49-
n = len(filters2) if filters2 else 0
50-
for i in range(n):
57+
for i in range(n2):
5158
current_filter = filters2[i]
5259
value = current_filter.apply(value)
60+
if value is None:
61+
return None
5362

5463
return value
5564

@@ -92,16 +101,16 @@ cdef class ValidationMixin:
92101
return None
93102

94103
cdef:
95-
Py_ssize_t i, n = len(steps) if steps else 0
104+
Py_ssize_t i, n = len(steps) if steps is not None else 0
96105
object current_step
97-
bint has_apply
106+
object apply_method
98107

99108
try:
100109
for i in range(n):
101110
current_step = steps[i]
102-
has_apply = hasattr(current_step, 'apply')
103-
if has_apply:
104-
value = current_step.apply(value)
111+
apply_method = getattr(current_step, 'apply', None)
112+
if apply_method is not None:
113+
value = apply_method(value)
105114
else:
106115
current_step.validate(value)
107116
except ValidationError:
@@ -213,17 +222,18 @@ cdef class ValidationMixin:
213222
return None
214223

215224
cdef:
216-
Py_ssize_t i, n
225+
Py_ssize_t i, n1, n2
217226
BaseValidator current_validator
218227

219228
try:
220-
n = len(validators1) if validators1 else 0
221-
for i in range(n):
229+
n1 = len(validators1) if validators1 is not None else 0
230+
n2 = len(validators2) if validators2 is not None else 0
231+
232+
for i in range(n1):
222233
current_validator = validators1[i]
223234
current_validator.validate(value)
224235

225-
n = len(validators2) if validators2 else 0
226-
for i in range(n):
236+
for i in range(n2):
227237
current_validator = validators2[i]
228238
current_validator.validate(value)
229239
except ValidationError:
@@ -267,15 +277,19 @@ cdef class ValidationMixin:
267277
cdef:
268278
dict[str, Any] validated_data = {}
269279
dict[str, str] errors = {}
270-
Py_ssize_t i, n = len(fields) if fields else 0
271-
272-
cdef:
273-
list field_names = list(fields.keys()) if n > 0 else []
274-
list field_infos = list(fields.values()) if n > 0 else []
280+
Py_ssize_t i, n = len(fields) if fields is not None else 0
281+
list field_names
282+
list field_infos
275283
str field_name
276284
FieldModel field_info
277285
object value
278286

287+
if n == 0:
288+
return validated_data, errors
289+
290+
field_names = list(fields.keys())
291+
field_infos = list(fields.values())
292+
279293
for i in range(n):
280294
field_name = field_names[i]
281295
field_info = field_infos[i]

flask_inputfilter/models/base_condition/_base_condition.pyx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# cython: language=c++
2+
# cython: boundscheck=False
3+
# cython: wraparound=False
24

35
from typing import Any
46

flask_inputfilter/models/base_filter/_base_filter.pyx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# cython: language=c++
2+
# cython: boundscheck=False
3+
# cython: wraparound=False
24

35
cdef class BaseFilter:
46
"""

flask_inputfilter/models/base_validator/_base_validator.pyx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# cython: language=c++
2+
# cython: boundscheck=False
3+
# cython: wraparound=False
24

35
cdef class BaseValidator:
46
"""

flask_inputfilter/models/external_api_config/_external_api_config.pyx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# cython: language=c++
22
# cython: freelist=256
3+
# cython: boundscheck=False
4+
# cython: wraparound=False
35

46
import cython
57
from typing import Any

flask_inputfilter/models/field_model/_field_model.pyx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
# cython: language=c++
2-
# cython: freelist=256
2+
# cython: freelist=512
3+
# cython: boundscheck=False
4+
# cython: wraparound=False
35

46
import cython
57
from typing import Any
68

79
from flask_inputfilter.models.cimports cimport BaseFilter, BaseValidator, ExternalApiConfig
810

911
cdef list EMPTY_LIST = []
12+
cdef tuple EMPTY_TUPLE = ()
1013

1114

1215
@cython.final
@@ -37,8 +40,8 @@ cdef class FieldModel:
3740
self.required = required
3841
self._default = default
3942
self.fallback = fallback
40-
self.filters = filters if filters is not None else EMPTY_LIST
41-
self.validators = validators if validators is not None else EMPTY_LIST
42-
self.steps = steps if steps is not None else EMPTY_LIST
43+
self.filters = EMPTY_LIST if filters is None or len(filters) == 0 else filters
44+
self.validators = EMPTY_LIST if validators is None or len(validators) == 0 else validators
45+
self.steps = EMPTY_LIST if steps is None or len(steps) == 0 else steps
4346
self.external_api = external_api
4447
self.copy = copy

0 commit comments

Comments
 (0)