Skip to content

Commit 3333811

Browse files
committed
Add test for child recursion error.
1 parent 17c41b5 commit 3333811

File tree

2 files changed

+65
-0
lines changed

2 files changed

+65
-0
lines changed

src/django_unicorn/components/unicorn_template_response.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,11 +248,13 @@ def render(self):
248248
for child in descendant.children:
249249
init_script = f"{init_script} {child._init_script}"
250250
json_tags.append(child._json_tag)
251+
251252
# We need to delete this property here as it can cause RecursionError
252253
# when pickling child component. Tag element has previous_sibling
253254
# and next_sibling which would also be pickled and if they are big,
254255
# cause RecursionError
255256
del child._json_tag
257+
256258
descendants.append(child)
257259

258260
script_tag = soup.new_tag("script")
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import pickle
2+
import sys
3+
from unittest.mock import MagicMock
4+
5+
from bs4 import BeautifulSoup
6+
from django.test import RequestFactory
7+
8+
from django_unicorn.components import UnicornView
9+
from django_unicorn.components.unicorn_template_response import UnicornTemplateResponse
10+
11+
12+
class RecursionComponent(UnicornView):
13+
template_name = "templates/test_component.html"
14+
pass
15+
16+
17+
def test_child_component_cleanup_prevents_recursion_error():
18+
# 1. Create child component and simulate it having a large _json_tag (as if rendered)
19+
child = RecursionComponent(component_name="child", component_id="child_id")
20+
21+
# Create a deep DOM to ensure recursion error would happen if not cleaned up
22+
depth = 2000
23+
html_content = "<div>" + ("<span>" * depth) + "Hello" + ("</span>" * depth) + "</div>"
24+
soup = BeautifulSoup(html_content, "html.parser")
25+
child._json_tag = soup.find("div") # Simulate the tag attached during child render
26+
child._init_script = "Unicorn.componentInit({});"
27+
28+
# 2. Create parent component and link child
29+
parent = RecursionComponent(component_name="parent", component_id="parent_id")
30+
parent.children.append(child)
31+
32+
# 3. Setup UnicornTemplateResponse for parent
33+
request = RequestFactory().get("/")
34+
template = MagicMock()
35+
template.render.return_value = "<html><body><div unicorn:view>Parent Content</div></body></html>"
36+
37+
response = UnicornTemplateResponse(
38+
template=template,
39+
request=request,
40+
component=parent,
41+
init_js=True, # Important to trigger the logic that collects json_tags
42+
)
43+
44+
# Increase recursion limit to allow BS4 to process the deep tree without crashing during render
45+
# We want to verify that pickling works AFTER the tag is removed, not that BS4 crashes.
46+
original_recursion_limit = sys.getrecursionlimit()
47+
sys.setrecursionlimit(10000)
48+
49+
try:
50+
# 4. Render the parent
51+
# This should trigger the loop that collects children's json_tags and DELETES them
52+
response.render()
53+
54+
# 5. Verify _json_tag is deleted from child
55+
assert not hasattr(child, "_json_tag"), "child._json_tag should have been deleted"
56+
57+
# 6. Verify pickling parent works
58+
# If _json_tag was arguably still there, this might fail or pass depending on limit.
59+
# But since we verified it IS deleted, this confirms the object state is clean.
60+
pickle.dumps(parent)
61+
62+
finally:
63+
sys.setrecursionlimit(original_recursion_limit)

0 commit comments

Comments
 (0)