From e74ef848dfcaeebe2199440247cf39ba46c35edd Mon Sep 17 00:00:00 2001 From: Pamella Bezerra Date: Wed, 5 Jun 2024 11:41:04 -0300 Subject: [PATCH 1/5] Fix django warning --- test_settings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test_settings.py b/test_settings.py index f87427f..2d84fd8 100644 --- a/test_settings.py +++ b/test_settings.py @@ -38,3 +38,5 @@ def root(*args): ROOT_URLCONF = "example_app.urls" SECRET_KEY = "insecure-secret-key" + +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" From ca742bcf1f6e12a9a64170d322100d9f56b4cefd Mon Sep 17 00:00:00 2001 From: Pamella Bezerra Date: Wed, 5 Jun 2024 12:12:15 -0300 Subject: [PATCH 2/5] Remove custom _get_serializer_class. Use another strategy to handle RecursionError between get_serializer_class, get_read_serializer, and get_write_serializer. --- drf_rw_serializers/generics.py | 57 +++++++++++--------- tests/test_generics.py | 96 +++++++++++++++++++++++++++++++--- 2 files changed, 121 insertions(+), 32 deletions(-) diff --git a/drf_rw_serializers/generics.py b/drf_rw_serializers/generics.py index 506500d..9032e19 100644 --- a/drf_rw_serializers/generics.py +++ b/drf_rw_serializers/generics.py @@ -11,35 +11,19 @@ class GenericAPIView(generics.GenericAPIView): - def _get_serializer_class(self): - """ - Return the class to use for the serializer. - Defaults to using `self.serializer_class`. - You may want to override this if you need to provide different - serializations depending on the incoming request. - (Eg. admins get full serialization, others get basic serialization) - """ - assert ( - self.serializer_class is not None - or getattr(self, "read_serializer_class", None) is not None - ), ( - "'%s' should either include one of `serializer_class` and `read_serializer_class` " - "attribute, or override one of the `get_serializer_class()`, " - "`get_read_serializer_class()` method." % self.__class__.__name__ - ) - - return self.serializer_class - def get_serializer_class(self): """ Return the class to use for the serializer. Defaults to using `self.serializer_class`. + If the request method is GET, it tries to use `self.read_serializer_class`. If the request method is not GET, it tries to use `self.write_serializer_class`. If the specific serializer class for the request method is not set, it falls back to `self.serializer_class`. + You may want to override this if you need to provide different serializations depending on the incoming request. + (Eg. admins get full serialization, others get basic serialization) """ if hasattr(self, "request"): @@ -52,7 +36,8 @@ def get_serializer_class(self): "attribute, or override the `get_read_serializer_class()` or " "`get_serializer_class()` method." % self.__class__.__name__ ) - return self.get_read_serializer_class() + # `use_serializer_class` is used to prevent a `RecursionError` + return self.get_read_serializer_class(use_serializer_class=True) if self.request.method in ["POST", "PUT", "PATCH", "DELETE"]: assert ( @@ -63,9 +48,19 @@ def get_serializer_class(self): "attribute, or override the `get_write_serializer_class()` or " "`get_serializer_class()` method." % self.__class__.__name__ ) - return self.get_write_serializer_class() + # `use_serializer_class` is used to prevent a `RecursionError` + return self.get_write_serializer_class(use_serializer_class=True) + + assert ( + self.serializer_class is not None + or getattr(self, "read_serializer_class", None) is not None + ), ( + "'%s' should either include one of `serializer_class` and `read_serializer_class` " + "attribute, or override one of the `get_serializer_class()`, " + "`get_read_serializer_class()` method." % self.__class__.__name__ + ) - return self._get_serializer_class() + return self.serializer_class def get_read_serializer(self, *args, **kwargs): """ @@ -75,16 +70,21 @@ def get_read_serializer(self, *args, **kwargs): kwargs["context"] = self.get_serializer_context() return serializer_class(*args, **kwargs) - def get_read_serializer_class(self): + def get_read_serializer_class(self, use_serializer_class: bool = False): """ Return the class to use for the serializer. Defaults to using `self.read_serializer_class`. + You may want to override this if you need to provide different serializations depending on the incoming request. + (Eg. admins get full serialization, others get basic serialization) """ if getattr(self, "read_serializer_class", None) is None: - return self._get_serializer_class() + if use_serializer_class: + return self.serializer_class + + return self.get_serializer_class() return self.read_serializer_class @@ -97,16 +97,21 @@ def get_write_serializer(self, *args, **kwargs): kwargs["context"] = self.get_serializer_context() return serializer_class(*args, **kwargs) - def get_write_serializer_class(self): + def get_write_serializer_class(self, use_serializer_class: bool = False): """ Return the class to use for the serializer. Defaults to using `self.write_serializer_class`. + You may want to override this if you need to provide different serializations depending on the incoming request. + (Eg. admins can send extra fields, others cannot) """ if getattr(self, "write_serializer_class", None) is None: - return self._get_serializer_class() + if use_serializer_class: + return self.serializer_class + + return self.get_serializer_class() return self.write_serializer_class diff --git a/tests/test_generics.py b/tests/test_generics.py index 50b0ec2..45efd55 100644 --- a/tests/test_generics.py +++ b/tests/test_generics.py @@ -61,6 +61,21 @@ def test_serializer_class_not_provided(self): ), ) + def test_get_serializer_class_override_provided(self): + class GetSerializerClassView(generics.GenericAPIView): + def get_serializer_class(self): + return OrderedMealDetailsSerializer + + self.assertEqual( + GetSerializerClassView().get_serializer_class(), OrderedMealDetailsSerializer + ) + self.assertEqual( + GetSerializerClassView().get_read_serializer_class(), OrderedMealDetailsSerializer + ) + self.assertEqual( + GetSerializerClassView().get_write_serializer_class(), OrderedMealDetailsSerializer + ) + def test_no_request_provided(self): # Return serializer_class over read_serializer_class and write_serializer_class self.assertEqual( @@ -108,6 +123,25 @@ def test_non_read_write_request_method_provided(self): self.FullSerializerView().get_serializer_class(), OrderedMealDetailsSerializer ) + def test_all_get_serializer_class_override_provided(self): + class GetSerializerClassView(generics.GenericAPIView): + def get_serializer_class(self): + return OrderedMealDetailsSerializer + + def get_read_serializer_class(self, use_serializer_class: bool = False): + return OrderListSerializer + + def get_write_serializer_class(self, use_serializer_class: bool = False): + return OrderCreateSerializer + + self.assertEqual( + GetSerializerClassView().get_serializer_class(), OrderedMealDetailsSerializer + ) + self.assertEqual(GetSerializerClassView().get_read_serializer_class(), OrderListSerializer) + self.assertEqual( + GetSerializerClassView().get_write_serializer_class(), OrderCreateSerializer + ) + class GenericAPIViewGetReadSerializerClassTests(BaseTestCase): def test_read_serializer_class_not_provided(self): @@ -115,11 +149,11 @@ class NoReadSerializerView(generics.GenericAPIView): pass with mock.patch.object( - NoReadSerializerView, "_get_serializer_class" - ) as mock__get_serializer_class: + NoReadSerializerView, "get_serializer_class" + ) as mock_get_serializer_class: NoReadSerializerView().get_read_serializer_class() - mock__get_serializer_class.assert_called_once() + mock_get_serializer_class.assert_called_once() def test_read_serializer_class_provided(self): class ReadSerializerClassProvided(generics.GenericAPIView): @@ -130,6 +164,31 @@ class ReadSerializerClassProvided(generics.GenericAPIView): OrderListSerializer, ) + def test_use_serializer_class_fallback(self): + class SerializerClassView(generics.GenericAPIView): + serializer_class = OrderedMealDetailsSerializer + + self.assertEqual( + SerializerClassView().get_read_serializer_class(use_serializer_class=True), + OrderedMealDetailsSerializer, + ) + + with mock.patch.object( + SerializerClassView, "get_serializer_class" + ) as mock_get_serializer_class: + SerializerClassView().get_read_serializer_class(use_serializer_class=False) + + mock_get_serializer_class.assert_called_once() + + def test_get_read_serializer_class_override_provided(self): + class GetReadSerializerClassView(generics.GenericAPIView): + def get_read_serializer_class(self, use_serializer_class: bool = False): + return OrderListSerializer + + self.assertEqual( + GetReadSerializerClassView().get_read_serializer_class(), OrderListSerializer + ) + class GenericAPIViewGetWriteSerializerClassTests(BaseTestCase): def test_write_serializer_class_not_provided(self): @@ -137,11 +196,11 @@ class NoWriteSerializerView(generics.GenericAPIView): pass with mock.patch.object( - NoWriteSerializerView, "_get_serializer_class" - ) as mock__get_serializer_class: + NoWriteSerializerView, "get_serializer_class" + ) as mock_get_serializer_class: NoWriteSerializerView().get_write_serializer_class() - mock__get_serializer_class.assert_called_once() + mock_get_serializer_class.assert_called_once() def test_write_serializer_class_provided(self): class WriteSerializerClassProvided(generics.GenericAPIView): @@ -152,6 +211,31 @@ class WriteSerializerClassProvided(generics.GenericAPIView): OrderCreateSerializer, ) + def test_use_serializer_class_fallback(self): + class SerializerClassView(generics.GenericAPIView): + serializer_class = OrderedMealDetailsSerializer + + self.assertEqual( + SerializerClassView().get_write_serializer_class(use_serializer_class=True), + OrderedMealDetailsSerializer, + ) + + with mock.patch.object( + SerializerClassView, "get_serializer_class" + ) as mock_get_serializer_class: + SerializerClassView().get_write_serializer_class(use_serializer_class=False) + + mock_get_serializer_class.assert_called_once() + + def test_get_write_serializer_class_override_provided(self): + class GetWriteSerializerClassView(generics.GenericAPIView): + def get_write_serializer_class(self, use_serializer_class: bool = False): + return OrderCreateSerializer + + self.assertEqual( + GetWriteSerializerClassView().get_write_serializer_class(), OrderCreateSerializer + ) + class OrderListCreateEndpointTests(BaseTestCase, TestListRequestSuccess, TestCreateRequestSuccess): def setUp(self): From 75185357b1898954aba01a59e282748852e74c9e Mon Sep 17 00:00:00 2001 From: Pamella Bezerra Date: Wed, 5 Jun 2024 16:13:45 -0300 Subject: [PATCH 3/5] Mention drf-spectacular configuration in docs --- docs/cross_library_integrations.rst | 10 ++++++++++ docs/index.rst | 1 + 2 files changed, 11 insertions(+) create mode 100644 docs/cross_library_integrations.rst diff --git a/docs/cross_library_integrations.rst b/docs/cross_library_integrations.rst new file mode 100644 index 0000000..e5510dc --- /dev/null +++ b/docs/cross_library_integrations.rst @@ -0,0 +1,10 @@ +========================== +Cross-Library Integrations +========================== + +drf-spectacular +--------------- + +If your project is using both `drf-rw-serializers` and `drf-spectacular`, there +are specific configurations to be made. Detailed steps for this integration are +provided in the `drf-spectacular documentation `_. diff --git a/docs/index.rst b/docs/index.rst index d0d2b2a..7157652 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -14,6 +14,7 @@ Contents: readme installation usage + cross_library_integrations contributing authors changelog From 722e7bafdbd7a43d414ccf6f02c2e296992ad6e0 Mon Sep 17 00:00:00 2001 From: Pamella Bezerra Date: Wed, 5 Jun 2024 16:16:11 -0300 Subject: [PATCH 4/5] Rename arg for clarity --- drf_rw_serializers/generics.py | 16 ++++++++-------- tests/test_generics.py | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/drf_rw_serializers/generics.py b/drf_rw_serializers/generics.py index 9032e19..58f5b9e 100644 --- a/drf_rw_serializers/generics.py +++ b/drf_rw_serializers/generics.py @@ -36,8 +36,8 @@ def get_serializer_class(self): "attribute, or override the `get_read_serializer_class()` or " "`get_serializer_class()` method." % self.__class__.__name__ ) - # `use_serializer_class` is used to prevent a `RecursionError` - return self.get_read_serializer_class(use_serializer_class=True) + # `default_to_serializer_class` is used to prevent a `RecursionError` + return self.get_read_serializer_class(default_to_serializer_class=True) if self.request.method in ["POST", "PUT", "PATCH", "DELETE"]: assert ( @@ -48,8 +48,8 @@ def get_serializer_class(self): "attribute, or override the `get_write_serializer_class()` or " "`get_serializer_class()` method." % self.__class__.__name__ ) - # `use_serializer_class` is used to prevent a `RecursionError` - return self.get_write_serializer_class(use_serializer_class=True) + # `default_to_serializer_class` is used to prevent a `RecursionError` + return self.get_write_serializer_class(default_to_serializer_class=True) assert ( self.serializer_class is not None @@ -70,7 +70,7 @@ def get_read_serializer(self, *args, **kwargs): kwargs["context"] = self.get_serializer_context() return serializer_class(*args, **kwargs) - def get_read_serializer_class(self, use_serializer_class: bool = False): + def get_read_serializer_class(self, default_to_serializer_class: bool = False): """ Return the class to use for the serializer. Defaults to using `self.read_serializer_class`. @@ -81,7 +81,7 @@ def get_read_serializer_class(self, use_serializer_class: bool = False): (Eg. admins get full serialization, others get basic serialization) """ if getattr(self, "read_serializer_class", None) is None: - if use_serializer_class: + if default_to_serializer_class: return self.serializer_class return self.get_serializer_class() @@ -97,7 +97,7 @@ def get_write_serializer(self, *args, **kwargs): kwargs["context"] = self.get_serializer_context() return serializer_class(*args, **kwargs) - def get_write_serializer_class(self, use_serializer_class: bool = False): + def get_write_serializer_class(self, default_to_serializer_class: bool = False): """ Return the class to use for the serializer. Defaults to using `self.write_serializer_class`. @@ -108,7 +108,7 @@ def get_write_serializer_class(self, use_serializer_class: bool = False): (Eg. admins can send extra fields, others cannot) """ if getattr(self, "write_serializer_class", None) is None: - if use_serializer_class: + if default_to_serializer_class: return self.serializer_class return self.get_serializer_class() diff --git a/tests/test_generics.py b/tests/test_generics.py index 45efd55..e1f2d61 100644 --- a/tests/test_generics.py +++ b/tests/test_generics.py @@ -128,10 +128,10 @@ class GetSerializerClassView(generics.GenericAPIView): def get_serializer_class(self): return OrderedMealDetailsSerializer - def get_read_serializer_class(self, use_serializer_class: bool = False): + def get_read_serializer_class(self, default_to_serializer_class: bool = False): return OrderListSerializer - def get_write_serializer_class(self, use_serializer_class: bool = False): + def get_write_serializer_class(self, default_to_serializer_class: bool = False): return OrderCreateSerializer self.assertEqual( @@ -169,20 +169,20 @@ class SerializerClassView(generics.GenericAPIView): serializer_class = OrderedMealDetailsSerializer self.assertEqual( - SerializerClassView().get_read_serializer_class(use_serializer_class=True), + SerializerClassView().get_read_serializer_class(default_to_serializer_class=True), OrderedMealDetailsSerializer, ) with mock.patch.object( SerializerClassView, "get_serializer_class" ) as mock_get_serializer_class: - SerializerClassView().get_read_serializer_class(use_serializer_class=False) + SerializerClassView().get_read_serializer_class(default_to_serializer_class=False) mock_get_serializer_class.assert_called_once() def test_get_read_serializer_class_override_provided(self): class GetReadSerializerClassView(generics.GenericAPIView): - def get_read_serializer_class(self, use_serializer_class: bool = False): + def get_read_serializer_class(self, default_to_serializer_class: bool = False): return OrderListSerializer self.assertEqual( @@ -216,20 +216,20 @@ class SerializerClassView(generics.GenericAPIView): serializer_class = OrderedMealDetailsSerializer self.assertEqual( - SerializerClassView().get_write_serializer_class(use_serializer_class=True), + SerializerClassView().get_write_serializer_class(default_to_serializer_class=True), OrderedMealDetailsSerializer, ) with mock.patch.object( SerializerClassView, "get_serializer_class" ) as mock_get_serializer_class: - SerializerClassView().get_write_serializer_class(use_serializer_class=False) + SerializerClassView().get_write_serializer_class(default_to_serializer_class=False) mock_get_serializer_class.assert_called_once() def test_get_write_serializer_class_override_provided(self): class GetWriteSerializerClassView(generics.GenericAPIView): - def get_write_serializer_class(self, use_serializer_class: bool = False): + def get_write_serializer_class(self, default_to_serializer_class: bool = False): return OrderCreateSerializer self.assertEqual( From 181e22c05a1e967e2b0bc0f4a5d121230e0ef311 Mon Sep 17 00:00:00 2001 From: Pamella Bezerra Date: Wed, 5 Jun 2024 16:28:13 -0300 Subject: [PATCH 5/5] Release 1.4.0 --- CHANGELOG.rst | 7 +++++++ drf_rw_serializers/__init__.py | 2 +- pyproject.toml | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index cc7c144..7711711 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,6 +14,13 @@ Change Log Unreleased ~~~~~~~~~~ +[1.4.0] - 2024-06-05 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Fixed +_____ +* Fix a regression in the `get_read_serializer_class` and `get_write_serializer_class` +methods to return `get_serializer_class` as default. + [1.3.0] - 2024-06-03 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Fixed diff --git a/drf_rw_serializers/__init__.py b/drf_rw_serializers/__init__.py index 120b3f8..6544f35 100644 --- a/drf_rw_serializers/__init__.py +++ b/drf_rw_serializers/__init__.py @@ -5,7 +5,7 @@ from __future__ import absolute_import, unicode_literals -__version__ = "1.3.0" +__version__ = "1.4.0" # pylint: disable=invalid-name default_app_config = "drf_rw_serializers.apps.DrfRwSerializersConfig" diff --git a/pyproject.toml b/pyproject.toml index e74d42a..e83a3c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "drf-rw-serializers" -version = "1.3.0" +version = "1.4.0" description = "Generic views, viewsets and mixins that extend the Django REST Framework ones adding separated serializers for read and write operations" authors = ["Vinta Software "] license = "MIT"