Skip to content

Commit c226cbd

Browse files
author
Nikolay Dolzhenkov
committed
bringing back warning when deprecated filtering configuration is used
1 parent d27896c commit c226cbd

File tree

2 files changed

+74
-19
lines changed

2 files changed

+74
-19
lines changed

ninja/filter_schema.py

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import warnings
12
from typing import Any, List, Optional, TypeVar, Union, cast
23

34
from django.core.exceptions import ImproperlyConfigured
@@ -6,6 +7,7 @@
67
from pydantic.fields import FieldInfo
78
from typing_extensions import Literal
89

10+
from .constants import NOT_SET
911
from .schema import Schema
1012

1113
# XOR is available only in Django 4.1+: https://docs.djangoproject.com/en/4.1/ref/models/querysets/#xor
@@ -28,7 +30,7 @@ class MyFilterSchema(FilterSchema):
2830

2931
def __init__(
3032
self,
31-
q: Union[str, List[str]],
33+
q: Union[str, List[str], None],
3234
*,
3335
expression_connector: ExpressionConnector = DEFAULT_FIELD_LEVEL_EXPRESSION_CONNECTOR,
3436
ignore_none: bool = DEFAULT_IGNORE_NONE,
@@ -107,43 +109,48 @@ def _get_field_q_expression(
107109
self,
108110
field_name: str,
109111
field_info: FieldInfo,
110-
default: Union[str, List[str], None] = None,
111112
) -> Union[str, List[str], None]:
112113
filter_lookup = self._get_filter_lookup(field_name, field_info)
113114
if filter_lookup:
114-
return filter_lookup.q if filter_lookup.q is not None else default
115+
return filter_lookup.q
115116

116117
# Legacy approach, consider removing in future versions
117-
field_extra = cast(dict, field_info.json_schema_extra) or {}
118-
return cast(Union[str, List[str], None], field_extra.get("q", default))
118+
return cast(
119+
Union[str, List[str], None],
120+
self._get_from_deprecated_field_extra(field_name, field_info, "q"),
121+
)
119122

120123
def _get_field_expression_connector(
121124
self,
122125
field_name: str,
123126
field_info: FieldInfo,
124-
default: Union[ExpressionConnector, None] = None,
125127
) -> Union[ExpressionConnector, None]:
126128
filter_lookup = self._get_filter_lookup(field_name, field_info)
127129
if filter_lookup:
128-
return filter_lookup.expression_connector or default
130+
return filter_lookup.expression_connector
129131

130132
# Legacy approach, consider removing in future versions
131-
field_extra = cast(dict, field_info.json_schema_extra) or {}
132133
return cast(
133-
Union[ExpressionConnector, None],
134-
field_extra.get("expression_connector", default),
134+
Union[ExpressionConnector | None],
135+
self._get_from_deprecated_field_extra(
136+
field_name, field_info, "expression_connector"
137+
),
135138
)
136139

137140
def _get_field_ignore_none(
138-
self, field_name: str, field_info: FieldInfo, default: Union[bool, None] = None
141+
self, field_name: str, field_info: FieldInfo
139142
) -> Union[bool, None]:
140143
filter_lookup = self._get_filter_lookup(field_name, field_info)
141144
if filter_lookup:
142145
return filter_lookup.ignore_none
143146

144147
# Legacy approach, consider removing in future versions
145-
field_extra = cast(dict, field_info.json_schema_extra) or {}
146-
return cast(Union[bool, None], field_extra.get("ignore_none", default))
148+
return cast(
149+
Union[bool, None],
150+
self._get_from_deprecated_field_extra(
151+
field_name, field_info, "ignore_none"
152+
),
153+
)
147154

148155
def _resolve_field_expression(
149156
self, field_name: str, field_value: Any, field_info: FieldInfo
@@ -153,8 +160,9 @@ def _resolve_field_expression(
153160
return cast(Q, func(field_value))
154161

155162
q_expression = self._get_field_q_expression(field_name, field_info)
156-
expression_connector = self._get_field_expression_connector(
157-
field_name, field_info, default=DEFAULT_FIELD_LEVEL_EXPRESSION_CONNECTOR
163+
expression_connector = (
164+
self._get_field_expression_connector(field_name, field_info)
165+
or DEFAULT_FIELD_LEVEL_EXPRESSION_CONNECTOR
158166
)
159167

160168
if not q_expression:
@@ -191,11 +199,14 @@ def _connect_fields(self) -> Q:
191199
ignore_none = (
192200
False
193201
if class_ignore_none is False
194-
else self._get_field_ignore_none(
195-
field_name,
196-
field_info,
197-
DEFAULT_IGNORE_NONE,
202+
else field_ignore_none
203+
if (
204+
field_ignore_none := self._get_field_ignore_none(
205+
field_name, field_info
206+
)
198207
)
208+
is not None
209+
else DEFAULT_IGNORE_NONE
199210
)
200211

201212
# Resolve Q expression for a field even if we skip it due to None value
@@ -213,3 +224,27 @@ def _connect_fields(self) -> Q:
213224
)
214225

215226
return q
227+
228+
def _get_from_deprecated_field_extra(
229+
self, field_name: str, field_info: FieldInfo, attr: str
230+
) -> Union[Any, None]:
231+
"""
232+
Backward-compatible shim which looks up filtering parameters in the Field's **extra kwargs.
233+
Consider removing this method in favor of FilterLookup annotation class.
234+
"""
235+
field_extra = cast(dict, field_info.json_schema_extra) or {}
236+
value = field_extra.get(attr, NOT_SET)
237+
238+
if value is not NOT_SET:
239+
warnings.warn(
240+
f"Using Pydantic Field with extra keyword arguments ('{attr}') "
241+
f"in field {self.__class__.__name__}.{field_name} is deprecated. Please use ninja.FilterLookup instead:\n"
242+
f" from typing import Annotated\n"
243+
f" from ninja import FilterLookup, FilterSchema\n\n"
244+
f" class {self.__class__.__name__}(FilterSchema):\n"
245+
f" {field_name}: Annotated[Optional[...], FilterLookup(q='...', ...)] = None",
246+
DeprecationWarning,
247+
stacklevel=4,
248+
)
249+
return value
250+
return None

tests/test_filter_schema.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,3 +401,23 @@ class DummyFilterSchema(FilterSchema):
401401
filter_instance = DummyFilterSchema(name="test")
402402
with pytest.raises(ImproperlyConfigured):
403403
filter_instance.get_filter_expression()
404+
405+
406+
def test_pydantic_field_with_extra_warns():
407+
"""Test that using pydantic Field with 'extra' attribute shows deprecation warning"""
408+
import warnings
409+
410+
class DummyFilterSchema(FilterSchema):
411+
name: Optional[str] = Field(None, q="name__icontains")
412+
413+
filter_instance = DummyFilterSchema()
414+
415+
with warnings.catch_warnings(record=True) as w:
416+
warnings.simplefilter("always")
417+
filter_instance.get_filter_expression()
418+
419+
# Check that a deprecation warning was issued
420+
assert len(w) == 1
421+
assert issubclass(w[0].category, DeprecationWarning)
422+
assert "deprecated" in str(w[0].message).lower()
423+
assert "FilterLookup" in str(w[0].message)

0 commit comments

Comments
 (0)