Skip to content

Commit fecd903

Browse files
authored
Merge pull request #87 from vintasoftware/fix/remove-custom-_get_serializer_class
Refactor strategy to handle `RecursionError` between `get_serializer_class`, `get_read_serializer`, and `get_write_serializer`
2 parents ff845ca + 181e22c commit fecd903

8 files changed

Lines changed: 143 additions & 34 deletions

File tree

CHANGELOG.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ Change Log
1414
Unreleased
1515
~~~~~~~~~~
1616

17+
[1.4.0] - 2024-06-05
18+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
19+
Fixed
20+
_____
21+
* Fix a regression in the `get_read_serializer_class` and `get_write_serializer_class`
22+
methods to return `get_serializer_class` as default.
23+
1724
[1.3.0] - 2024-06-03
1825
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1926
Fixed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
==========================
2+
Cross-Library Integrations
3+
==========================
4+
5+
drf-spectacular
6+
---------------
7+
8+
If your project is using both `drf-rw-serializers` and `drf-spectacular`, there
9+
are specific configurations to be made. Detailed steps for this integration are
10+
provided in the `drf-spectacular documentation <https://drf-spectacular.readthedocs.io/en/latest/blueprints.html#drf-rw-serializers>`_.

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Contents:
1414
readme
1515
installation
1616
usage
17+
cross_library_integrations
1718
contributing
1819
authors
1920
changelog

drf_rw_serializers/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from __future__ import absolute_import, unicode_literals
77

8-
__version__ = "1.3.0"
8+
__version__ = "1.4.0"
99

1010
# pylint: disable=invalid-name
1111
default_app_config = "drf_rw_serializers.apps.DrfRwSerializersConfig"

drf_rw_serializers/generics.py

Lines changed: 31 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,35 +11,19 @@
1111

1212

1313
class GenericAPIView(generics.GenericAPIView):
14-
def _get_serializer_class(self):
15-
"""
16-
Return the class to use for the serializer.
17-
Defaults to using `self.serializer_class`.
18-
You may want to override this if you need to provide different
19-
serializations depending on the incoming request.
20-
(Eg. admins get full serialization, others get basic serialization)
21-
"""
22-
assert (
23-
self.serializer_class is not None
24-
or getattr(self, "read_serializer_class", None) is not None
25-
), (
26-
"'%s' should either include one of `serializer_class` and `read_serializer_class` "
27-
"attribute, or override one of the `get_serializer_class()`, "
28-
"`get_read_serializer_class()` method." % self.__class__.__name__
29-
)
30-
31-
return self.serializer_class
32-
3314
def get_serializer_class(self):
3415
"""
3516
Return the class to use for the serializer.
3617
Defaults to using `self.serializer_class`.
18+
3719
If the request method is GET, it tries to use `self.read_serializer_class`.
3820
If the request method is not GET, it tries to use `self.write_serializer_class`.
3921
If the specific serializer class for the request method is not set, it falls back to
4022
`self.serializer_class`.
23+
4124
You may want to override this if you need to provide different
4225
serializations depending on the incoming request.
26+
4327
(Eg. admins get full serialization, others get basic serialization)
4428
"""
4529
if hasattr(self, "request"):
@@ -52,7 +36,8 @@ def get_serializer_class(self):
5236
"attribute, or override the `get_read_serializer_class()` or "
5337
"`get_serializer_class()` method." % self.__class__.__name__
5438
)
55-
return self.get_read_serializer_class()
39+
# `default_to_serializer_class` is used to prevent a `RecursionError`
40+
return self.get_read_serializer_class(default_to_serializer_class=True)
5641

5742
if self.request.method in ["POST", "PUT", "PATCH", "DELETE"]:
5843
assert (
@@ -63,9 +48,19 @@ def get_serializer_class(self):
6348
"attribute, or override the `get_write_serializer_class()` or "
6449
"`get_serializer_class()` method." % self.__class__.__name__
6550
)
66-
return self.get_write_serializer_class()
51+
# `default_to_serializer_class` is used to prevent a `RecursionError`
52+
return self.get_write_serializer_class(default_to_serializer_class=True)
53+
54+
assert (
55+
self.serializer_class is not None
56+
or getattr(self, "read_serializer_class", None) is not None
57+
), (
58+
"'%s' should either include one of `serializer_class` and `read_serializer_class` "
59+
"attribute, or override one of the `get_serializer_class()`, "
60+
"`get_read_serializer_class()` method." % self.__class__.__name__
61+
)
6762

68-
return self._get_serializer_class()
63+
return self.serializer_class
6964

7065
def get_read_serializer(self, *args, **kwargs):
7166
"""
@@ -75,16 +70,21 @@ def get_read_serializer(self, *args, **kwargs):
7570
kwargs["context"] = self.get_serializer_context()
7671
return serializer_class(*args, **kwargs)
7772

78-
def get_read_serializer_class(self):
73+
def get_read_serializer_class(self, default_to_serializer_class: bool = False):
7974
"""
8075
Return the class to use for the serializer.
8176
Defaults to using `self.read_serializer_class`.
77+
8278
You may want to override this if you need to provide different
8379
serializations depending on the incoming request.
80+
8481
(Eg. admins get full serialization, others get basic serialization)
8582
"""
8683
if getattr(self, "read_serializer_class", None) is None:
87-
return self._get_serializer_class()
84+
if default_to_serializer_class:
85+
return self.serializer_class
86+
87+
return self.get_serializer_class()
8888

8989
return self.read_serializer_class
9090

@@ -97,16 +97,21 @@ def get_write_serializer(self, *args, **kwargs):
9797
kwargs["context"] = self.get_serializer_context()
9898
return serializer_class(*args, **kwargs)
9999

100-
def get_write_serializer_class(self):
100+
def get_write_serializer_class(self, default_to_serializer_class: bool = False):
101101
"""
102102
Return the class to use for the serializer.
103103
Defaults to using `self.write_serializer_class`.
104+
104105
You may want to override this if you need to provide different
105106
serializations depending on the incoming request.
107+
106108
(Eg. admins can send extra fields, others cannot)
107109
"""
108110
if getattr(self, "write_serializer_class", None) is None:
109-
return self._get_serializer_class()
111+
if default_to_serializer_class:
112+
return self.serializer_class
113+
114+
return self.get_serializer_class()
110115

111116
return self.write_serializer_class
112117

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "drf-rw-serializers"
3-
version = "1.3.0"
3+
version = "1.4.0"
44
description = "Generic views, viewsets and mixins that extend the Django REST Framework ones adding separated serializers for read and write operations"
55
authors = ["Vinta Software <contact@vinta.com.br>"]
66
license = "MIT"

test_settings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,5 @@ def root(*args):
3838
ROOT_URLCONF = "example_app.urls"
3939

4040
SECRET_KEY = "insecure-secret-key"
41+
42+
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"

tests/test_generics.py

Lines changed: 90 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,21 @@ def test_serializer_class_not_provided(self):
6161
),
6262
)
6363

64+
def test_get_serializer_class_override_provided(self):
65+
class GetSerializerClassView(generics.GenericAPIView):
66+
def get_serializer_class(self):
67+
return OrderedMealDetailsSerializer
68+
69+
self.assertEqual(
70+
GetSerializerClassView().get_serializer_class(), OrderedMealDetailsSerializer
71+
)
72+
self.assertEqual(
73+
GetSerializerClassView().get_read_serializer_class(), OrderedMealDetailsSerializer
74+
)
75+
self.assertEqual(
76+
GetSerializerClassView().get_write_serializer_class(), OrderedMealDetailsSerializer
77+
)
78+
6479
def test_no_request_provided(self):
6580
# Return serializer_class over read_serializer_class and write_serializer_class
6681
self.assertEqual(
@@ -108,18 +123,37 @@ def test_non_read_write_request_method_provided(self):
108123
self.FullSerializerView().get_serializer_class(), OrderedMealDetailsSerializer
109124
)
110125

126+
def test_all_get_serializer_class_override_provided(self):
127+
class GetSerializerClassView(generics.GenericAPIView):
128+
def get_serializer_class(self):
129+
return OrderedMealDetailsSerializer
130+
131+
def get_read_serializer_class(self, default_to_serializer_class: bool = False):
132+
return OrderListSerializer
133+
134+
def get_write_serializer_class(self, default_to_serializer_class: bool = False):
135+
return OrderCreateSerializer
136+
137+
self.assertEqual(
138+
GetSerializerClassView().get_serializer_class(), OrderedMealDetailsSerializer
139+
)
140+
self.assertEqual(GetSerializerClassView().get_read_serializer_class(), OrderListSerializer)
141+
self.assertEqual(
142+
GetSerializerClassView().get_write_serializer_class(), OrderCreateSerializer
143+
)
144+
111145

112146
class GenericAPIViewGetReadSerializerClassTests(BaseTestCase):
113147
def test_read_serializer_class_not_provided(self):
114148
class NoReadSerializerView(generics.GenericAPIView):
115149
pass
116150

117151
with mock.patch.object(
118-
NoReadSerializerView, "_get_serializer_class"
119-
) as mock__get_serializer_class:
152+
NoReadSerializerView, "get_serializer_class"
153+
) as mock_get_serializer_class:
120154
NoReadSerializerView().get_read_serializer_class()
121155

122-
mock__get_serializer_class.assert_called_once()
156+
mock_get_serializer_class.assert_called_once()
123157

124158
def test_read_serializer_class_provided(self):
125159
class ReadSerializerClassProvided(generics.GenericAPIView):
@@ -130,18 +164,43 @@ class ReadSerializerClassProvided(generics.GenericAPIView):
130164
OrderListSerializer,
131165
)
132166

167+
def test_use_serializer_class_fallback(self):
168+
class SerializerClassView(generics.GenericAPIView):
169+
serializer_class = OrderedMealDetailsSerializer
170+
171+
self.assertEqual(
172+
SerializerClassView().get_read_serializer_class(default_to_serializer_class=True),
173+
OrderedMealDetailsSerializer,
174+
)
175+
176+
with mock.patch.object(
177+
SerializerClassView, "get_serializer_class"
178+
) as mock_get_serializer_class:
179+
SerializerClassView().get_read_serializer_class(default_to_serializer_class=False)
180+
181+
mock_get_serializer_class.assert_called_once()
182+
183+
def test_get_read_serializer_class_override_provided(self):
184+
class GetReadSerializerClassView(generics.GenericAPIView):
185+
def get_read_serializer_class(self, default_to_serializer_class: bool = False):
186+
return OrderListSerializer
187+
188+
self.assertEqual(
189+
GetReadSerializerClassView().get_read_serializer_class(), OrderListSerializer
190+
)
191+
133192

134193
class GenericAPIViewGetWriteSerializerClassTests(BaseTestCase):
135194
def test_write_serializer_class_not_provided(self):
136195
class NoWriteSerializerView(generics.GenericAPIView):
137196
pass
138197

139198
with mock.patch.object(
140-
NoWriteSerializerView, "_get_serializer_class"
141-
) as mock__get_serializer_class:
199+
NoWriteSerializerView, "get_serializer_class"
200+
) as mock_get_serializer_class:
142201
NoWriteSerializerView().get_write_serializer_class()
143202

144-
mock__get_serializer_class.assert_called_once()
203+
mock_get_serializer_class.assert_called_once()
145204

146205
def test_write_serializer_class_provided(self):
147206
class WriteSerializerClassProvided(generics.GenericAPIView):
@@ -152,6 +211,31 @@ class WriteSerializerClassProvided(generics.GenericAPIView):
152211
OrderCreateSerializer,
153212
)
154213

214+
def test_use_serializer_class_fallback(self):
215+
class SerializerClassView(generics.GenericAPIView):
216+
serializer_class = OrderedMealDetailsSerializer
217+
218+
self.assertEqual(
219+
SerializerClassView().get_write_serializer_class(default_to_serializer_class=True),
220+
OrderedMealDetailsSerializer,
221+
)
222+
223+
with mock.patch.object(
224+
SerializerClassView, "get_serializer_class"
225+
) as mock_get_serializer_class:
226+
SerializerClassView().get_write_serializer_class(default_to_serializer_class=False)
227+
228+
mock_get_serializer_class.assert_called_once()
229+
230+
def test_get_write_serializer_class_override_provided(self):
231+
class GetWriteSerializerClassView(generics.GenericAPIView):
232+
def get_write_serializer_class(self, default_to_serializer_class: bool = False):
233+
return OrderCreateSerializer
234+
235+
self.assertEqual(
236+
GetWriteSerializerClassView().get_write_serializer_class(), OrderCreateSerializer
237+
)
238+
155239

156240
class OrderListCreateEndpointTests(BaseTestCase, TestListRequestSuccess, TestCreateRequestSuccess):
157241
def setUp(self):

0 commit comments

Comments
 (0)