Skip to content

Commit 3332863

Browse files
committed
Replace a deepcopy of the Q object custom deep copy
1 parent 3a81c0e commit 3332863

File tree

2 files changed

+76
-1
lines changed

2 files changed

+76
-1
lines changed

polymorphic/query_translate.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,28 @@ def tree_node_correct_field_specs(my_model, node):
8080
return potential_q_object
8181

8282

83+
def _deepcopy_q_object(q):
84+
"""
85+
Make a deepcopy of a Q-object.
86+
"""
87+
def _copy_child(child):
88+
if isinstance(child, tuple):
89+
return child # tuples are immutable, no need to make a copy.
90+
elif isinstance(child, Q):
91+
return _deepcopy_q_object(child)
92+
else:
93+
raise RuntimeError("Unknown child type: %s", type(child))
94+
95+
children = [_copy_child(c) for c in q.children]
96+
97+
if hasattr(q, 'copy'): # Django 4.2+
98+
obj = q.copy()
99+
obj.children = children
100+
else:
101+
obj = Q(*children, _connector=q.connector, _negated=q.negated)
102+
return obj
103+
104+
83105
def translate_polymorphic_filter_definitions_in_args(queryset_model, args, using=DEFAULT_DB_ALIAS):
84106
"""
85107
Translate the non-keyword argument list for PolymorphicQuerySet.filter()
@@ -92,7 +114,7 @@ def translate_polymorphic_filter_definitions_in_args(queryset_model, args, using
92114
Returns: modified Q objects
93115
"""
94116
return [
95-
translate_polymorphic_Q_object(queryset_model, copy.deepcopy(q), using=using) for q in args
117+
translate_polymorphic_Q_object(queryset_model, _deepcopy_q_object(q), using=using) for q in args
96118
]
97119

98120

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import copy
2+
import tempfile
3+
import pickle
4+
import threading
5+
6+
from django.db.models import Q
7+
from django.test import TestCase
8+
9+
from polymorphic.tests.models import Bottom, Middle, Top
10+
from polymorphic.query_translate import translate_polymorphic_filter_definitions_in_args
11+
12+
13+
class QueryTranslateTests(TestCase):
14+
15+
def test_translate_with_not_pickleable_query(self):
16+
"""
17+
In some cases, Django may attacha _thread object to the query and we
18+
will get the following when we try to deepcopy inside of
19+
translate_polymorphic_filter_definitions_in_args:
20+
21+
TypeError: cannot pickle '_thread.lock' object
22+
23+
24+
For this to trigger, we need to somehoe go down this path:
25+
26+
File "/perfdash/.venv/lib64/python3.12/site-packages/polymorphic/query_translate.py", line 95, in translate_polymorphic_filter_definitions_in_args
27+
translate_polymorphic_Q_object(queryset_model, copy.deepcopy(q), using=using) for q in args
28+
^^^^^^^^^^^^^^^^
29+
File "/usr/lib64/python3.12/copy.py", line 143, in deepcopy
30+
y = copier(memo)
31+
^^^^^^^^^^^^
32+
File "/perfdash/.venv/lib64/python3.12/site-packages/django/utils/tree.py", line 53, in __deepcopy__
33+
obj.children = copy.deepcopy(self.children, memodict)
34+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
35+
File "/usr/lib64/python3.12/copy.py", line 136, in deepcopy
36+
y = copier(x, memo)
37+
^^^^^^^^^^^^^^^
38+
39+
Internals in Django, somehow we must trigger this tree.py code in django via
40+
the deepcopy in order to trigger this.
41+
42+
"""
43+
44+
with tempfile.TemporaryFile() as fd:
45+
# verify this is definitely not pickleable
46+
with self.assertRaises(TypeError):
47+
pickle.dumps(threading.Lock())
48+
49+
# I know this doesn't make sense to pass as a Q(), but
50+
# I haven't found another way to trigger the copy.deepcopy failing.
51+
q = Q(blog__info='blog info') | Q(blog__info=threading.Lock())
52+
53+
translate_polymorphic_filter_definitions_in_args(Bottom, args=[q])

0 commit comments

Comments
 (0)