Skip to content

Commit e7175e6

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

File tree

11 files changed

+167
-60
lines changed

11 files changed

+167
-60
lines changed

docs/source/changelog.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,20 @@ 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+
12+
Changed
13+
^^^^^^^
14+
- Increased freelist sizes for ``InputFilter``, ``FieldModel``, and ``ExternalApiConfig`` to reduce allocations
15+
- Replaced Python generators with C-style loops and added early exit conditions in filter/validator chains
16+
- Optimized dictionary operations and string lookups with extended interning
17+
- Improved ``has_unknown_fields`` with adaptive strategies based on data size
18+
19+
620
[0.6.3] - 2025-08-11
721
--------------------
822

flask_inputfilter/_input_filter.pyx

Lines changed: 27 additions & 6 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):
@@ -347,7 +368,7 @@ cdef class InputFilter:
347368
"""
348369
self.data = data
349370

350-
cpdef bint has_unknown(self):
371+
cpdef inline bint has_unknown(self):
351372
"""
352373
Checks whether any values in the current data do not have
353374
corresponding configurations in the defined fields.

flask_inputfilter/mixins/data_mixin/_data_mixin.pyx

Lines changed: 61 additions & 26 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,11 +13,13 @@ 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:
1620

1721
@staticmethod
22+
@cython.inline
1823
cdef bint has_unknown_fields(
1924
dict[str, Any] data,
2025
dict[str, FieldModel] fields
@@ -35,19 +40,22 @@ cdef class DataMixin:
3540
if not data and fields:
3641
return True
3742

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

40-
# Use set operations for faster lookup when there are many fields
41-
if len(fields) > LARGE_DATASET_THRESHOLD:
48+
if field_count > LARGE_DATASET_THRESHOLD and data_count > SMALL_DICT_THRESHOLD:
4249
field_set = set(fields.keys())
4350
for field_name in data.keys():
4451
if field_name not in field_set:
4552
return True
46-
else:
47-
# Use direct dict lookup for smaller field counts
53+
elif data_count < field_count:
4854
for field_name in data.keys():
4955
if field_name not in fields:
5056
return True
57+
else:
58+
return bool(set(data.keys()) - set(fields.keys()))
5159

5260
return False
5361

@@ -72,19 +80,30 @@ cdef class DataMixin:
7280
"""
7381
cdef:
7482
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 []
83+
Py_ssize_t i, n = len(data) if data is not None else 0
84+
list keys
85+
list values
7886
str field_name
7987
object field_value
88+
FieldModel field_info
89+
90+
# Early return for empty data
91+
if n == 0:
92+
return filtered_data
93+
94+
# Pre-allocate lists
95+
keys = list(data.keys())
96+
values = list(data.values())
8097

8198
for i in range(n):
8299
field_name = keys[i]
83100
field_value = values[i]
84101

85-
if field_name in fields:
102+
# Cache field lookup
103+
field_info = fields.get(field_name)
104+
if field_info is not None:
86105
field_value = ValidationMixin.apply_filters(
87-
fields[field_name].filters,
106+
field_info.filters,
88107
global_filters,
89108
field_value,
90109
)
@@ -94,6 +113,7 @@ cdef class DataMixin:
94113
return filtered_data
95114

96115
@staticmethod
116+
@cython.inline
97117
cdef tuple validate_with_conditions(
98118
dict[str, FieldModel] fields,
99119
dict[str, Any] data,
@@ -125,8 +145,8 @@ cdef class DataMixin:
125145
fields, data, global_filters, global_validators
126146
)
127147

128-
# Check conditions if present and no errors yet
129-
if conditions and not errors:
148+
# Check conditions only if present and no errors yet
149+
if conditions is not None and len(conditions) > 0 and not errors:
130150
try:
131151
ValidationMixin.check_conditions(conditions, validated_data)
132152
except ValidationError as e:
@@ -150,13 +170,19 @@ cdef class DataMixin:
150170
cdef:
151171
Py_ssize_t i, n
152172
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 []
155-
156-
# Merge fields efficiently
157-
n = len(keys)
158-
for i in range(n):
159-
target_filter.fields[keys[i]] = new_fields[i]
173+
list keys
174+
list new_fields
175+
176+
if source_inputs:
177+
keys = list(source_inputs.keys())
178+
new_fields = list(source_inputs.values())
179+
n = len(keys)
180+
181+
# Batch update fields
182+
for i in range(n):
183+
target_filter.fields[keys[i]] = new_fields[i]
184+
else:
185+
n = 0
160186

161187
# Merge conditions
162188
target_filter.conditions.extend(source_filter.conditions)
@@ -174,6 +200,7 @@ cdef class DataMixin:
174200
)
175201

176202
@staticmethod
203+
@cython.inline
177204
cdef void _merge_component_list(list target_list, list source_list):
178205
"""
179206
Helper method to merge component lists avoiding duplicates by type.
@@ -183,13 +210,21 @@ cdef class DataMixin:
183210
- **target_list** (*list*): The list to merge into.
184211
- **source_list** (*list*): The list to merge from.
185212
"""
186-
cdef dict existing_type_map
213+
cdef:
214+
dict existing_type_map = {}
215+
Py_ssize_t i, n = len(target_list)
216+
object component
217+
type component_type
218+
219+
# Build type map once
220+
for i in range(n):
221+
existing_type_map[type(target_list[i])] = i
187222

223+
# Process source components
188224
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
225+
component_type = type(component)
226+
if component_type in existing_type_map:
227+
target_list[existing_type_map[component_type]] = component
194228
else:
195-
target_list.append(component)
229+
target_list.append(component)
230+
existing_type_map[component_type] = len(target_list) - 1

flask_inputfilter/mixins/external_api_mixin/_external_api_mixin.pyx

Lines changed: 4 additions & 1 deletion
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
@@ -112,7 +114,8 @@ cdef class ExternalApiMixin:
112114
return result.get(data_key) if data_key else result
113115

114116
@staticmethod
115-
cdef inline str replace_placeholders(
117+
@cython.inline
118+
cdef str replace_placeholders(
116119
str value,
117120
dict[str, Any] validated_data
118121
):

0 commit comments

Comments
 (0)