Repository: pyg-team/pytorch_geometric
Pull request #10124 (2025-03-19 14:48:50)

Fix in recursive ConfigMixin for iterables


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c0e0b685f..b76cf93ba 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -51,6 +51,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
 
 ### Fixed
 
+- Fixed `_recursive_config()` for `torch.nn.ModuleList`([#10124](https://github.com/pyg-team/pytorch_geometric/pull/10124))
 - Fixed the `k_hop_subgraph()` method for directed graphs ([#9756](https://github.com/pyg-team/pytorch_geometric/pull/9756))
 - Fixed `utils.group_cat` concatenating dimension ([#9766](https://github.com/pyg-team/pytorch_geometric/pull/9766))
 - Fixed `WebQSDataset.process` raising exceptions ([#9665](https://github.com/pyg-team/pytorch_geometric/pull/9665))
diff --git a/torch_geometric/config_mixin.py b/torch_geometric/config_mixin.py
index 46c921b37..f6e357eb8 100644
--- a/torch_geometric/config_mixin.py
+++ b/torch_geometric/config_mixin.py
@@ -1,7 +1,7 @@
 import inspect
 from dataclasses import fields, is_dataclass
 from importlib import import_module
-from typing import Any, Dict
+from typing import Any, Dict, Iterable
 
 from torch_geometric.config_store import (
     class_from_dataclass,
@@ -71,7 +71,7 @@ def _recursive_config(value: Any) -> Any:
         return value.config()
     if is_torch_instance(value, ConfigMixin):
         return value.config()
-    if isinstance(value, (tuple, list)):
+    if isinstance(value, (tuple, list, Iterable)):
         return [_recursive_config(v) for v in value]
     if isinstance(value, dict):
         return {k: _recursive_config(v) for k, v in value.items()}


Test patch:
diff --git a/test/test_config_mixin.py b/test/test_config_mixin.py
index 1fb053d96..ae146a23c 100644
--- a/test/test_config_mixin.py
+++ b/test/test_config_mixin.py
@@ -1,12 +1,16 @@
 from dataclasses import asdict, dataclass, is_dataclass
+from typing import Sequence
 
+import pytest
 import torch
 
 from torch_geometric.config_mixin import ConfigMixin
 from torch_geometric.config_store import clear_config_store, register
 
 
-def teardown_function() -> None:
+@pytest.fixture(scope="session", autouse=True)
+def teardown_once():
+    yield  # This allows tests to run before teardown is executed
     clear_config_store()
 
 
@@ -28,6 +32,22 @@ class Module(Base):
         self.data = data
 
 
+@register(with_target=True)
+class SubModule(Base):
+    def __init__(self, p: float):
+        super().__init__()
+        self.p = p
+
+
+@register(with_target=True)
+class CompoundModule(torch.nn.Module, ConfigMixin):
+    def __init__(self, z: int, module: Module, submodules: list[SubModule]):
+        super().__init__()
+        self.z = z
+        self.module = module
+        self.submodules = torch.nn.ModuleList(submodules)
+
+
 def test_config_mixin() -> None:
     x = 0
     data = Dataclass(x=1, y=2)
@@ -84,3 +104,39 @@ def test_config_mixin() -> None:
     assert isinstance(model.data, dict)
     assert model.data['x'] == 1
     assert model.data['y'] == 2
+
+
+def test_config_mixin_compound() -> None:
+    submodules = [SubModule(1.41), SubModule(3.14)]
+    module = Module(x=0, data=Dataclass(x=1, y=2))
+    model = CompoundModule(z=3, module=module, submodules=submodules)
+    cfg = model.config()
+    assert is_dataclass(cfg)
+    assert cfg._target_ == 'test_config_mixin.CompoundModule'
+    assert cfg.z == 3
+    assert cfg.module._target_ == 'test_config_mixin.Module'
+    assert cfg.module.x == 0
+    assert isinstance(cfg.module.data, Dataclass)
+    assert cfg.module.data.x == 1
+    assert cfg.module.data.y == 2
+    assert len(cfg.submodules) == 2
+    assert isinstance(cfg.submodules, Sequence)
+    assert cfg.submodules[0]._target_ == 'test_config_mixin.SubModule'
+    assert cfg.submodules[0].p == 1.41
+    assert cfg.submodules[1]._target_ == 'test_config_mixin.SubModule'
+    assert cfg.submodules[1].p == 3.14
+
+    model = CompoundModule.from_config(cfg)
+    assert isinstance(model, CompoundModule)
+    assert model.z == 3
+    assert isinstance(model.module, Module)
+    assert model.module.x == 0
+    assert isinstance(model.module.data, Dataclass)
+    assert model.module.data.x == 1
+    assert model.module.data.y == 2
+    assert isinstance(model.submodules, torch.nn.ModuleList)
+    assert len(model.submodules) == 2
+    assert isinstance(model.submodules[0], SubModule)
+    assert model.submodules[0].p == 1.41
+    assert isinstance(model.submodules[1], SubModule)
+    assert model.submodules[1].p == 3.14