Repository: Lightning-AI/pytorch-lightning

Issue:
Creation of many data module instances incurs RecursionError
## 🐛 Bug

Thank you for a nice framework!

When I repeated hundreds of experiments, each time with a new instance of a single LightningDataModule class, RecursionError was raised. I also found that creating data modules and calling setup() were enough to reproduce the issue.
<!-- A clear and concise description of what the bug is. -->

### To Reproduce

Please look at the following code sample and error messages:
<!-- If you have a code sample, error messages, stack traces, please provide it here as well -->


#### Code sample

```
import pytorch_lightning as pl

class DummyDM(pl.LightningDataModule):
    def setup(self, stage=None):
        pass

if __name__ == "__main__":
    MAX_ITERS = 1000
    for i in range(MAX_ITERS):
        try:
            dm = DummyDM()
            dm.setup()
        except RecursionError:
            print(f"RecursionError occured in the {i}-th iteration!")
            raise
```

#### Error messages

```
RecursionError occured in the 998-th iteration!
Traceback (most recent call last):
  File "test_dm.py", line 18, in <module>
    dm.setup()
  File "/workspace/src/.venv/lib/python3.8/site-packages/pytorch_lightning/core/datamodule.py", line 85, in wrapped_fn
    return fn(*args, **kwargs)
  File "/workspace/src/.venv/lib/python3.8/site-packages/pytorch_lightning/core/datamodule.py", line 85, in wrapped_fn
    return fn(*args, **kwargs)
  File "/workspace/src/.venv/lib/python3.8/site-packages/pytorch_lightning/core/datamodule.py", line 85, in wrapped_fn
    return fn(*args, **kwargs)
  [Previous line repeated 995 more times]
  File "/workspace/src/.venv/lib/python3.8/site-packages/pytorch_lightning/core/datamodule.py", line 69, in wrapped_fn
    if fn.__name__ == 'setup':
RecursionError: maximum recursion depth exceeded in comparison
```
<!-- Ideally attach a minimal code sample to reproduce the decried issue. 
Minimal means having the shortest code but still preserving the bug. -->

### Expected behavior

The above code sample is expected to exit without any outputs.

### Environment

 - PyTorch Version (e.g., 1.0): 1.6.0
 - PytorchLightning Version: 0.9.0
 - OS (e.g., Linux): Linux
 - How you installed PyTorch (`conda`, `pip`, source): pip
 - Build command you used (if compiling from source): n/a
 - Python version: 3.8.2
 - CUDA/cuDNN version: 10.2
 - GPU models and configuration: 1080Ti


Discussion:
Hi! thanks for your contribution!, great first issue!

Resolving patch:
diff --git a/pytorch_lightning/core/datamodule.py b/pytorch_lightning/core/datamodule.py
--- a/pytorch_lightning/core/datamodule.py
+++ b/pytorch_lightning/core/datamodule.py
@@ -25,6 +25,10 @@
 
 
 class _DataModuleWrapper(type):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.__has_added_checks = False
+
     def __call__(cls, *args, **kwargs):
         """A wrapper for LightningDataModule that:
 
@@ -32,11 +36,12 @@ def __call__(cls, *args, **kwargs):
             2. Assures prepare_data() runs on rank 0
             3. Lets you check prepare_data and setup to see if they've been called
         """
-
-        # Track prepare_data calls and make sure it runs on rank zero
-        cls.prepare_data = track_data_hook_calls(rank_zero_only(cls.prepare_data))
-        # Track setup calls
-        cls.setup = track_data_hook_calls(cls.setup)
+        if not cls.__has_added_checks:
+            cls.__has_added_checks = True
+            # Track prepare_data calls and make sure it runs on rank zero
+            cls.prepare_data = track_data_hook_calls(rank_zero_only(cls.prepare_data))
+            # Track setup calls
+            cls.setup = track_data_hook_calls(cls.setup)
 
         # Get instance of LightningDataModule by mocking its __init__ via __call__
         obj = type.__call__(cls, *args, **kwargs)


Test patch:
diff --git a/tests/core/test_datamodules.py b/tests/core/test_datamodules.py
--- a/tests/core/test_datamodules.py
+++ b/tests/core/test_datamodules.py
@@ -68,6 +68,22 @@ def test_can_prepare_data(tmpdir):
     assert trainer.data_connector.can_prepare_data()
 
 
+def test_hooks_no_recursion_error(tmpdir):
+    # hooks were appended in cascade every tine a new data module was instantiated leading to a recursion error.
+    # See https://github.com/PyTorchLightning/pytorch-lightning/issues/3652
+    class DummyDM(LightningDataModule):
+        def setup(self, *args, **kwargs):
+            pass
+
+        def prepare_data(self, *args, **kwargs):
+            pass
+
+    for i in range(1005):
+        dm = DummyDM()
+        dm.setup()
+        dm.prepare_data()
+
+
 def test_base_datamodule(tmpdir):
     dm = TrialMNISTDataModule()
     dm.prepare_data()