Skip to content

Commit db0f571

Browse files
committed
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
1 parent 3fe2994 commit db0f571

File tree

2 files changed

+40
-4
lines changed

2 files changed

+40
-4
lines changed

queryset_sequence/__init__.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,11 @@ def __init__(self, model=None):
493493
self.DoesNotExist = ObjectDoesNotExist
494494

495495
def __getattr__(self, name):
496-
return getattr(self._model, name)
496+
if name == "_meta":
497+
return getattr(self._model, name)
498+
if hasattr(super(), '__getattr__'):
499+
return super().__getattr__(name)
500+
raise AttributeError(name)
497501

498502

499503
class QuerySetSequence:
@@ -846,9 +850,7 @@ def values_list(self, *fields, flat=False, named=False):
846850
clone._iterable_class = (
847851
NamedValuesListIterable
848852
if named
849-
else FlatValuesListIterable
850-
if flat
851-
else ValuesListIterable
853+
else FlatValuesListIterable if flat else ValuesListIterable
852854
)
853855

854856
return clone

tests/test_proxymodel.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from django.core.exceptions import ObjectDoesNotExist
2+
from django.test import TestCase
3+
4+
from queryset_sequence import ProxyModel
5+
from tests.models import Article
6+
7+
8+
class TestProxyModel(TestCase):
9+
"""Tests calls to proxy model are handled as expected"""
10+
11+
def test_no_model_doesnotexist(self):
12+
"""When no model is defined, generic ObjectDoesNotExist exception is returned"""
13+
proxy = ProxyModel(model=None)
14+
self.assertIs(proxy.DoesNotExist, ObjectDoesNotExist)
15+
16+
def test_model_doesnotexist(self):
17+
"""When a model is defined, model-specific DoesNotExist exception is returned"""
18+
proxy = ProxyModel(model=Article)
19+
self.assertIs(proxy.DoesNotExist, Article.DoesNotExist)
20+
21+
def test_model_meta(self):
22+
"""When a model is defined, model._meta is accessible"""
23+
proxy = ProxyModel(model=Article)
24+
self.assertEqual(proxy._meta.model_name, "article")
25+
26+
def test_no_model_meta(self):
27+
"""When a model is not defined, accessing model meta should fail"""
28+
proxy = ProxyModel(model=None)
29+
self.assertRaises(AttributeError, lambda: proxy._meta)
30+
31+
def test_model_special_methods_are_not_proxied(self):
32+
"""When a model is defined, special methods are not proxied to the model"""
33+
proxy = ProxyModel(model=Article)
34+
self.assertIsNot(proxy.__str__, Article.__str__)

0 commit comments

Comments
 (0)