From 98cd197471406875ca43f8c16abec50fc1e615df Mon Sep 17 00:00:00 2001 From: Ben Roberts Date: Sat, 19 Oct 2024 12:11:48 +0100 Subject: [PATCH] Don't proxy special methods to model in ProxyModel.__getattr__ ProxyModel.__getattr__ previously proxied all attribute lookups to the model class, but some third party libraries (e.g. DRF) will make calls which should be handled by the ProxyModel instance rather than the proxied class. For example, deepcopy invokes `__reduce_ex__()` that pickles an instance and needs access to `__getstate__()` which does note exist on a class. Proxying calls to the model is required in some cases, e.g. for access to _meta. This change avoids proxying any special methods (those starting with `__`) to the model. Fixes DRF schema generation for a serializer which contains a field using QuerySetSequence. Adds test cases to verify behaviour of method proxying. Fixes #107 --- queryset_sequence/__init__.py | 4 +++- tests/test_proxymodel.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 tests/test_proxymodel.py diff --git a/queryset_sequence/__init__.py b/queryset_sequence/__init__.py index 1e65220..39d1481 100644 --- a/queryset_sequence/__init__.py +++ b/queryset_sequence/__init__.py @@ -493,7 +493,9 @@ def __init__(self, model=None): self.DoesNotExist = ObjectDoesNotExist def __getattr__(self, name): - return getattr(self._model, name) + if name == '_meta': + return getattr(self._model, name) + return super().__getattr(name) class QuerySetSequence: diff --git a/tests/test_proxymodel.py b/tests/test_proxymodel.py new file mode 100644 index 0000000..ebd5bf8 --- /dev/null +++ b/tests/test_proxymodel.py @@ -0,0 +1,34 @@ +from django.core.exceptions import ObjectDoesNotExist +from django.test import TestCase + +from queryset_sequence import ProxyModel +from tests.models import Article + + +class TestProxyModel(TestCase): + """Tests calls to proxy model are handled as expected""" + + def test_no_model_doesnotexist(self): + """When no model is defined, generic ObjectDoesNotExist exception is returned""" + proxy = ProxyModel(model=None) + self.assertIs(proxy.DoesNotExist, ObjectDoesNotExist) + + def test_model_doesnotexist(self): + """When a model is defined, model-specific DoesNotExist exception is returned""" + proxy = ProxyModel(model=Article) + self.assertIs(proxy.DoesNotExist, Article.DoesNotExist) + + def test_model_meta(self): + """When a model is defined, model._meta is accessible""" + proxy = ProxyModel(model=Article) + self.assertEqual(proxy._meta.model_name, "article") + + def test_no_model_meta(self): + """When a model is not defined, accessing model meta should fail""" + proxy = ProxyModel(model=None) + self.assertRaises(AttributeError, lambda: proxy._meta) + + def test_model_special_methods_are_not_proxied(self): + """When a model is defined, special methods are not proxied to the model""" + proxy = ProxyModel(model=Article) + self.assertIsNot(proxy.__getstate__, Article.__getstate__)