Skip to content

Commit b9d0311

Browse files
committed
Fix declarative
1 parent c94b7ca commit b9d0311

37 files changed

+2334
-308
lines changed

AGENTS.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ poetry add flask-inputfilter
2222
Create validation schemas by inheriting from `InputFilter`:
2323

2424
```python
25-
from flask_inputfilter import InputFilter, field
25+
from flask_inputfilter import InputFilter
26+
from flask_inputfilter.declarative import field, global_filter
2627
from flask_inputfilter.filters import ToIntegerFilter, StringTrimFilter
2728
from flask_inputfilter.validators import IsIntegerValidator, LengthValidator
2829

@@ -41,8 +42,7 @@ class UserInputFilter(InputFilter):
4142
)
4243

4344
# Global filters/validators apply to all fields
44-
_global_filters = [StringTrimFilter()]
45-
_global_validators = []
45+
global_filter(StringTrimFilter())
4646
```
4747

4848
### 2. Usage in Flask Routes

docs/source/changelog.rst

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,53 @@ Changelog
44
All notable changes to this project will be documented in this file.
55

66

7+
[0.7.2] - 2025-09-28
8+
--------------------
9+
10+
Changed
11+
^^^^^^^
12+
- Changed the way how to use the new decorator ``_condition``, ``_global_filter``, ``_global_validator`` and ``_model``.
13+
They should no longer be assigned to a variable, but should be set with the corresponding declarative method.
14+
15+
``_condition = [Example()]`` => ``condition(Example())``
16+
17+
``_global_filter = [Example()]`` => ``global_filter(Example())``
18+
19+
``_global_validator = [Example()]`` => ``global_validator(Example())``
20+
21+
``_model = Example`` => ``model(Example)``
22+
23+
The previous way is still supported, but is not recommended because it is not streightforward and could lead to confusion.
24+
The new methods also support multiple calls and also mass assignment.
25+
26+
Both of the following examples are valid and have the same effect:
27+
28+
.. code-block:: python
29+
30+
class ExampleInputFilter(InputFilter):
31+
field1: str = field()
32+
field2: str = field()
33+
condition(ExactlyOneOfCondition(['field1', 'field2']))
34+
35+
field3: str = field()
36+
field4: str = field()
37+
condition(AtLeastOneOfCondition(['field3', 'field4']))
38+
39+
40+
.. code-block:: python
41+
42+
class ExampleInputFilter(InputFilter):
43+
field1: str = field()
44+
field2: str = field()
45+
field3: str = field()
46+
field4: str = field()
47+
48+
condition(
49+
ExactlyOneOfCondition(['field1', 'field2']),
50+
AtLeastOneOfCondition(['field3', 'field4'])
51+
)
52+
53+
754
[0.7.1] - 2025-09-27
855
--------------------
956

@@ -24,13 +71,13 @@ Added
2471

2572
``self.add`` => ``field``
2673

27-
``self.add_condition`` => ``_conditions``
74+
``self.add_condition`` => ``condition``
2875

29-
``self.add_global_filter`` => ``_global_filters``
76+
``self.add_global_filter`` => ``global_filter``
3077

31-
``self.add_global_validator`` => ``_global_validators``
78+
``self.add_global_validator`` => ``global_validator``
3279

33-
``self.add_model`` => ``_model``
80+
``self.add_model`` => ``model``
3481

3582
**Before**:
3683
.. code-block:: python
@@ -65,13 +112,13 @@ Added
65112
validators=[IsIntegerValidator()]
66113
)
67114
68-
_conditions = [ExactlyOneOfCondition(['zipcode', 'city'])]
115+
condition(ExactlyOneOfCondition(['zipcode', 'city']))
69116
70-
_global_filters = [StringTrimFilter()]
117+
global_filter(StringTrimFilter())
71118
72-
_global_validators = [IsStringValidator()]
119+
global_validator(IsStringValidator())
73120
74-
_model = UserModel
121+
model(UserModel)
75122
76123
The Change is fully backward compatible, but the new way is more readable
77124
and maintainable.

docs/source/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ Definition
7777
validators=[IsStringValidator()]
7878
)
7979
80-
_conditions = [ExactlyOneOfCondition(['zipcode', 'city'])]
80+
condition(ExactlyOneOfCondition(['zipcode', 'city']))
8181
8282
Usage
8383
^^^^^

docs/source/options/condition.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Example
2424
validators=[IsStringValidator()]
2525
)
2626
27-
_conditions = [OneOfCondition(['id', 'name'])]
27+
condition(OneOfCondition(['id', 'name']))
2828
2929
Available Conditions
3030
--------------------

docs/source/options/deserialization.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ into an instance of the model class, if there is a model class set.
3636
username: str = field()
3737
email: str = field()
3838
39-
_model = User
39+
model(User)
4040
4141
Examples
4242
--------
@@ -59,7 +59,7 @@ You can also use deserialization in your Flask routes:
5959
class MyInputFilter(InputFilter):
6060
username: str = field()
6161
62-
_model = User
62+
model(User)
6363
6464
6565
app = Flask(__name__)
@@ -89,7 +89,7 @@ You can also use deserialization outside of Flask routes:
8989
class MyInputFilter(InputFilter):
9090
username: str = field()
9191
92-
_model = User
92+
model(User)
9393
9494
app = Flask(__name__)
9595

docs/source/options/filter.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Example
2626
filters=[StringTrimFilter()]
2727
)
2828
29-
_global_filters = [ToLowerFilter()]
29+
global_filter(ToLowerFilter())
3030
3131
Available Filters
3232
-----------------

docs/source/options/validator.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Example
2424
validators=[RangeValidator(min_value=0, max_value=10)]
2525
)
2626
27-
_global_validators = [IsIntegerValidator()]
27+
global_validator(IsIntegerValidator())
2828
2929
Available Validators
3030
--------------------

examples/basic/filters/user_inputfilter.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from dataclasses import dataclass
22

33
from flask_inputfilter import InputFilter
4-
from flask_inputfilter.declarative import field
4+
from flask_inputfilter.declarative import field, model
55
from flask_inputfilter.validators import IsIntegerValidator, IsStringValidator
66

77

@@ -13,7 +13,7 @@ class User:
1313

1414

1515
class UserInputFilter(InputFilter):
16-
_model = User
16+
model(User)
1717

1818
name: str = field(required=True, validators=[IsStringValidator()])
1919

flask_inputfilter/_input_filter.pxd

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,4 @@ cdef class InputFilter:
7575
cpdef void add_global_validator(self, BaseValidator validator)
7676
cpdef list[BaseValidator] get_global_validators(self)
7777
cdef void _set_methods(self, list methods)
78-
cdef void _register_decorator_components(self)
78+
cpdef void _register_decorator_components(self)

flask_inputfilter/_input_filter.pyx

Lines changed: 71 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
# cython: optimize.unpack_method_calls=True
1010
# cython: infer_types=True
1111

12+
import dataclasses
13+
import inspect
1214
import json
1315
import logging
1416
import sys
@@ -239,45 +241,65 @@ cdef class InputFilter:
239241
"""
240242
self.conditions.append(condition)
241243

242-
cdef void _register_decorator_components(self):
243-
"""Register decorator-based components from the current class only."""
244-
cdef object cls, attr_value, conditions, validators, filters
244+
cpdef void _register_decorator_components(self):
245+
"""Register decorator-based components from the current class and
246+
inheritance chain."""
247+
cdef object cls, attr_value, conditions, validators, filters, base_cls
245248
cdef str attr_name
246249
cdef list dir_attrs
247250
cdef FieldDescriptor field_desc
251+
cdef set added_conditions, added_global_validators, added_global_filters
252+
cdef object condition_id, validator_id, filter_id
253+
cdef object condition, validator, filter_instance
248254

249255
cls = self.__class__
250256
dir_attrs = dir(cls)
251257

252258
for attr_name in dir_attrs:
253-
if (<bytes>attr_name.encode('utf-8')).startswith(b"_"):
259+
if attr_name.startswith("_"):
254260
continue
261+
if hasattr(cls, attr_name):
262+
attr_value = getattr(cls, attr_name)
263+
if isinstance(attr_value, FieldDescriptor):
264+
self.fields[attr_name] = FieldModel(
265+
attr_value.required,
266+
attr_value.default,
267+
attr_value.fallback,
268+
attr_value.filters,
269+
attr_value.validators,
270+
attr_value.steps,
271+
attr_value.external_api,
272+
attr_value.copy,
273+
)
255274

256-
attr_value = getattr(cls, attr_name, None)
257-
if attr_value is not None and isinstance(attr_value, FieldDescriptor):
258-
field_desc = <FieldDescriptor>attr_value
259-
self.fields[attr_name] = FieldModel(
260-
field_desc.required,
261-
field_desc._default,
262-
field_desc.fallback,
263-
field_desc.filters,
264-
field_desc.validators,
265-
field_desc.steps,
266-
field_desc.external_api,
267-
field_desc.copy,
268-
)
269-
270-
conditions = getattr(cls, "_conditions", None)
271-
if conditions is not None:
272-
self.conditions.extend(conditions)
273-
274-
validators = getattr(cls, "_global_validators", None)
275-
if validators is not None:
276-
self.global_validators.extend(validators)
277-
278-
filters = getattr(cls, "_global_filters", None)
279-
if filters is not None:
280-
self.global_filters.extend(filters)
275+
added_conditions = set()
276+
added_global_validators = set()
277+
added_global_filters = set()
278+
279+
for base_cls in reversed(cls.__mro__):
280+
conditions = getattr(base_cls, "_conditions", None)
281+
if conditions is not None:
282+
for condition in conditions:
283+
condition_id = id(condition)
284+
if condition_id not in added_conditions:
285+
self.conditions.append(condition)
286+
added_conditions.add(condition_id)
287+
288+
validators = getattr(base_cls, "_global_validators", None)
289+
if validators is not None:
290+
for validator in validators:
291+
validator_id = id(validator)
292+
if validator_id not in added_global_validators:
293+
self.global_validators.append(validator)
294+
added_global_validators.add(validator_id)
295+
296+
filters = getattr(base_cls, "_global_filters", None)
297+
if filters is not None:
298+
for filter_instance in filters:
299+
filter_id = id(filter_instance)
300+
if filter_id not in added_global_filters:
301+
self.global_filters.append(filter_instance)
302+
added_global_filters.add(filter_id)
281303

282304
self.model_class = getattr(cls, "_model", self.model_class)
283305

@@ -731,7 +753,26 @@ cdef class InputFilter:
731753
if self.model_class is None:
732754
return self.validated_data
733755

734-
return self.model_class(**self.validated_data)
756+
try:
757+
return self.model_class(**self.validated_data)
758+
except TypeError:
759+
pass
760+
761+
cdef set field_names = set()
762+
763+
if dataclasses.is_dataclass(self.model_class):
764+
field_names = {f.name for f in dataclasses.fields(self.model_class)}
765+
elif hasattr(self.model_class, '__fields__'):
766+
field_names = set(self.model_class.__fields__.keys())
767+
elif hasattr(self.model_class, '__annotations__'):
768+
field_names = set(self.model_class.__annotations__.keys())
769+
else:
770+
sig = inspect.signature(self.model_class.__init__)
771+
field_names = set(sig.parameters.keys()) - {'self'}
772+
773+
cdef dict filtered_data = {k: v for k, v in self.validated_data.items() if k in field_names}
774+
775+
return self.model_class(**filtered_data)
735776

736777
cpdef void add_global_validator(self, BaseValidator validator):
737778
"""

0 commit comments

Comments
 (0)