Repository: wagtail/wagtail

Issue:
Django Unittest issues with `StructValue.__init__()`
<!--
Found a bug? Please fill out the sections below. 👍
-->

### Issue Summary

Recently we've bumped packages in our project and some tests which are using `setUpTestData` in test suite has started failing.
Below I'll give more context. 
In our project we `django.TestCase` to test our headless API. Some of our tests require data to be populated before each test. To improve performance we've decided to use `setUpTestData` method in some of test suites. Small example :
```python
class TestSomeViewSet(TestCase):
    @classmethod
    def setUpTestData(cls):
        root_page = Page.objects.get(slug='root')
        cls.tested_page_1 = MyTestPage(
            title='test app resource 1', resources=json.dumps(MOCK_RESOURCES_1) #some dict mocks imported from another module
        )
        cls.tested_page_2 = MyTestPage(
            title='test app resource 2', resources=json.dumps(MOCK_RESOURCES_2)
        )
        cls.parent_page = ParentPage(title='test.com')
        cls.another_parent_page = AnotherParentPage(title='region 1', timezone=settings.TIMEZONES[0])
        cls.language_1 = LanguagePage(title='language 1')
        cls.language_2 = LanguagePage(title='language 2')

        root_page.add_child(instance=cls.parent_page)
        cls.site_config_1.add_child(instance=cls.another_parent_page)
        cls.region_1.add_child(instance=cls.language_1)
        cls.region_1.add_child(instance=cls.language_2)
        cls.language_1.add_child(instance=cls.tested_page_1)
        cls.language_2.add_child(instance=cls.app_tested_page_2)

        cls.parent_page.save()
        cls.another_parent_page.save()
        cls.language_1.save()
        cls.language_2.save()
        cls.tested_page_1.save()
        cls.tested_page_2.save()

    def test_list_view_returns_expected_response(self):
        url = reverse('app-resources-list')
        response = self.client.get(url)
        expected_response = get_configuration_list_expected_response( #this function generates expected output
            [self.tested_page_1, self.tested_page_2]
```
This test fails with with packages versions described below in "Technical details". Here is traceback:
```
/usr/local/lib/python3.10/site-packages/django/test/testcases.py:1384: in __get__
    data = deepcopy(self.data, memo)
/usr/local/lib/python3.10/copy.py:172: in deepcopy
    y = _reconstruct(x, memo, *rv)
/usr/local/lib/python3.10/copy.py:271: in _reconstruct
    state = deepcopy(state, memo)
/usr/local/lib/python3.10/copy.py:146: in deepcopy
    y = copier(x, memo)
/usr/local/lib/python3.10/copy.py:231: in _deepcopy_dict
    y[deepcopy(key, memo)] = deepcopy(value, memo)
/usr/local/lib/python3.10/copy.py:172: in deepcopy
    y = _reconstruct(x, memo, *rv)
/usr/local/lib/python3.10/copy.py:271: in _reconstruct
    state = deepcopy(state, memo)
/usr/local/lib/python3.10/copy.py:146: in deepcopy
    y = copier(x, memo)
/usr/local/lib/python3.10/copy.py:231: in _deepcopy_dict
    y[deepcopy(key, memo)] = deepcopy(value, memo)
/usr/local/lib/python3.10/copy.py:146: in deepcopy
    y = copier(x, memo)
/usr/local/lib/python3.10/copy.py:206: in _deepcopy_list
    append(deepcopy(a, memo))
/usr/local/lib/python3.10/copy.py:172: in deepcopy
    y = _reconstruct(x, memo, *rv)
/usr/local/lib/python3.10/copy.py:271: in _reconstruct
    state = deepcopy(state, memo)
/usr/local/lib/python3.10/copy.py:146: in deepcopy
    y = copier(x, memo)
/usr/local/lib/python3.10/copy.py:231: in _deepcopy_dict
    y[deepcopy(key, memo)] = deepcopy(value, memo)
/usr/local/lib/python3.10/copy.py:172: in deepcopy
    y = _reconstruct(x, memo, *rv)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

x = StructValue([('name', 'application'), ('content', '{"config": "data"}')])
memo = {139851395249680: <Locale: English - United States>, 139851395379648: {'_state': <django.db.models.base.ModelState obj...le': <Locale: English - United States>, 'owner': None, 'site_page': <SiteConfig: test.com>}}, 139851395528192: {}, ...}
func = <class 'wagtail.blocks.struct_block.StructValue'>, args = ()
state = {'block': <applications.configuration.models.ResourceBlock object at 0x7f31b22fc400>}
listiter = None, dictiter = <odict_iterator object at 0x7f31b0c25760>
deepcopy = <function deepcopy at 0x7f31b6b82830>

    def _reconstruct(x, memo, func, args,
                     state=None, listiter=None, dictiter=None,
                     deepcopy=deepcopy):
        deep = memo is not None
        if deep and args:
            args = (deepcopy(arg, memo) for arg in args)
>       y = func(*args)
E       TypeError: StructValue.__init__() missing 1 required positional argument: 'block'

/usr/local/lib/python3.10/copy.py:265: TypeError
```

When we run those test with `setUp` instead of `setUpTestData` method or we change versions of Wagtail and Django to:

- Wagtail <= 4.1 and Django < 4.1.x
- Wagtail 4.0.x and Django == 4.1

tests pass without any issues.

It looks like `StructValue` class might not support deepcopy-ing, but this is only suggestion, cuz when I was reading django release notes I found this (1st point from list): https://docs.djangoproject.com/en/4.1/releases/4.1/#features-removed-in-4-1

### Technical details

-   Python version: 3.10.5.
-   Django version: 4.1.4
-   Wagtail version: 4.1.1



Discussion:
@marekarendt are you able to narrow the error down to a particular object/line in `setUpTestData`?
`django.test.testcases.py` 
```python
class TestData:
    """
    Descriptor to provide TestCase instance isolation for attributes assigned
    during the setUpTestData() phase.

    Allow safe alteration of objects assigned in setUpTestData() by test
    methods by exposing deep copies instead of the original objects.

    Objects are deep copied using a memo kept on the test case instance in
    order to maintain their original relationships.
    """

    memo_attr = "_testdata_memo"

    def __init__(self, name, data):
        self.name = name
        self.data = data

    def get_memo(self, testcase):
        try:
            memo = getattr(testcase, self.memo_attr)
        except AttributeError:
            memo = {}
            setattr(testcase, self.memo_attr, memo)
        return memo

    def __get__(self, instance, owner):
        if instance is None:
            return self.data
        memo = self.get_memo(instance)
        data = deepcopy(self.data, memo)
        setattr(instance, self.name, data)
        return data

    def __repr__(self):
        return "<TestData: name=%r, data=%r>" % (self.name, self.data)
```
line: `data = deepcopy(self.data, memo)`
It looks like whenever we are trying to create object that uses streamfield we got this error
```python
cls.tested_page_1 = MyTestPage(
            title='test app resource 1', resources=json.dumps(MOCK_RESOURCES_1)
        )
```

If setUpTestData does not have streamfield, tests pass.

Is it what You've asked for ?
FYI I think we hit a similar issue in Wagtail itself, hence https://github.com/wagtail/wagtail/commit/aa3ab79384eaf958696124b9403a120dd5bca935. Would be nice to fix the root cause (likely by adding `__deepcopy__()` methods) instead of having to ditch `setUpTestData()`.

Resolving patch:
diff --git a/wagtail/blocks/struct_block.py b/wagtail/blocks/struct_block.py
--- a/wagtail/blocks/struct_block.py
+++ b/wagtail/blocks/struct_block.py
@@ -70,6 +70,9 @@ def bound_blocks(self):
             ]
         )
 
+    def __reduce__(self):
+        return (self.__class__, (self.block,), None, None, iter(self.items()))
+
 
 class PlaceholderBoundBlock(BoundBlock):
     """


Test patch:
diff --git a/wagtail/tests/test_blocks.py b/wagtail/tests/test_blocks.py
--- a/wagtail/tests/test_blocks.py
+++ b/wagtail/tests/test_blocks.py
@@ -1,6 +1,7 @@
 # -*- coding: utf-8 -*
 import base64
 import collections
+import copy
 import json
 import unittest
 from decimal import Decimal
@@ -2157,6 +2158,19 @@ def test_render_structvalue_with_extra_context(self):
         result = value.render_as_block(context={"language": "fr"})
         self.assertEqual(result, """<h1 lang="fr">Bonjour</h1>monde <i>italique</i>""")
 
+    def test_copy_structvalue(self):
+        block = SectionBlock()
+        value = block.to_python({"title": "Hello", "body": "world"})
+        copied = copy.copy(value)
+
+        # Ensure we have a new object
+        self.assertIsNot(value, copied)
+
+        # Check copy operation
+        self.assertIsInstance(copied, blocks.StructValue)
+        self.assertIs(value.block, copied.block)
+        self.assertEqual(value, copied)
+
 
 class TestStructBlockWithCustomStructValue(SimpleTestCase):
     def test_initialisation(self):