Skip to content

Commit 41a9dd4

Browse files
authored
Allows configclass to_dict operation to handle a list of configclasses (#1227)
# Description This PR add in the ability to properly convert configclass to dict if a configclass instance contains a list of configclasses. Fixes #1219 ## Type of change - Bug fix (non-breaking change which fixes an issue) ## Checklist - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there
1 parent 9c7238d commit 41a9dd4

File tree

4 files changed

+47
-5
lines changed

4 files changed

+47
-5
lines changed

source/extensions/omni.isaac.lab/config/extension.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22

33
# Note: Semantic Versioning is used: https://semver.org/
4-
version = "0.27.2"
4+
version = "0.27.3"
55

66
# Description
77
title = "Isaac Lab framework for Robot Learning"

source/extensions/omni.isaac.lab/docs/CHANGELOG.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
Changelog
22
---------
33

4+
0.27.3 (2024-10-22)
5+
~~~~~~~~~~~~~~~~~~~
6+
7+
Fixed
8+
^^^^^
9+
10+
* Fixed the issue with using list or tuples of ``configclass`` within a ``configclass``. Earlier, the list of
11+
configclass objects were not converted to dictionary properly when ``to_dict`` function was called.
12+
13+
414
0.27.2 (2024-10-21)
515
~~~~~~~~~~~~~~~~~~~
616

source/extensions/omni.isaac.lab/omni/isaac/lab/utils/dict.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@ def class_to_dict(obj: object) -> dict[str, Any]:
4040
# convert object to dictionary
4141
if isinstance(obj, dict):
4242
obj_dict = obj
43-
else:
43+
elif hasattr(obj, "__dict__"):
4444
obj_dict = obj.__dict__
45+
else:
46+
return obj
4547

4648
# convert to dictionary
4749
data = dict()
@@ -55,6 +57,8 @@ def class_to_dict(obj: object) -> dict[str, Any]:
5557
# check if attribute is a dictionary
5658
elif hasattr(value, "__dict__") or isinstance(value, dict):
5759
data[key] = class_to_dict(value)
60+
elif isinstance(value, (list, tuple)):
61+
data[key] = type(value)([class_to_dict(v) for v in value])
5862
else:
5963
data[key] = value
6064
return data

source/extensions/omni.isaac.lab/test/utils/test_configclass.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from collections.abc import Callable
2424
from dataclasses import MISSING, asdict, field
2525
from functools import wraps
26-
from typing import ClassVar
26+
from typing import Any, ClassVar
2727

2828
from omni.isaac.lab.utils.configclass import configclass
2929
from omni.isaac.lab.utils.dict import class_to_dict, dict_to_md5_hash, update_class_from_dict
@@ -85,6 +85,11 @@ def double(x):
8585
return 2 * x
8686

8787

88+
@configclass
89+
class ModifierCfg:
90+
params: dict[str, Any] = {"A": 1, "B": 2}
91+
92+
8893
@configclass
8994
class ViewerCfg:
9095
eye: list = [7.5, 7.5, 7.5] # field missing on purpose
@@ -113,6 +118,7 @@ class BasicDemoCfg:
113118
device_id: int = 0
114119
env: EnvCfg = EnvCfg()
115120
robot_default_state: RobotDefaultStateCfg = RobotDefaultStateCfg()
121+
list_config = [ModifierCfg(), ModifierCfg(params={"A": 3, "B": 4})]
116122

117123

118124
@configclass
@@ -381,6 +387,7 @@ class MissingChildDemoCfg(MissingParentDemoCfg):
381387
"dof_vel": [0.0, 0.0, 0.0, 0.0, 0.0, 1.0],
382388
},
383389
"device_id": 0,
390+
"list_config": [{"params": {"A": 1, "B": 2}}, {"params": {"A": 3, "B": 4}}],
384391
}
385392

386393
basic_demo_cfg_change_correct = {
@@ -392,6 +399,7 @@ class MissingChildDemoCfg(MissingParentDemoCfg):
392399
"dof_vel": [0.0, 0.0, 0.0, 0.0, 0.0, 1.0],
393400
},
394401
"device_id": 0,
402+
"list_config": [{"params": {"A": 1, "B": 2}}, {"params": {"A": 3, "B": 4}}],
395403
}
396404

397405
basic_demo_cfg_change_with_none_correct = {
@@ -403,6 +411,19 @@ class MissingChildDemoCfg(MissingParentDemoCfg):
403411
"dof_vel": [0.0, 0.0, 0.0, 0.0, 0.0, 1.0],
404412
},
405413
"device_id": 0,
414+
"list_config": [{"params": {"A": 1, "B": 2}}, {"params": {"A": 3, "B": 4}}],
415+
}
416+
417+
basic_demo_cfg_change_with_tuple_correct = {
418+
"env": {"num_envs": 56, "episode_length": 2000, "viewer": {"eye": [7.5, 7.5, 7.5], "lookat": [0.0, 0.0, 0.0]}},
419+
"robot_default_state": {
420+
"pos": (0.0, 0.0, 0.0),
421+
"rot": (1.0, 0.0, 0.0, 0.0),
422+
"dof_pos": (0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
423+
"dof_vel": [0.0, 0.0, 0.0, 0.0, 0.0, 1.0],
424+
},
425+
"device_id": 0,
426+
"list_config": [{"params": {"A": -1, "B": -2}}, {"params": {"A": -3, "B": -4}}],
406427
}
407428

408429
basic_demo_cfg_nested_dict_and_list = {
@@ -496,7 +517,7 @@ def test_dict_conversion(self):
496517

497518
def test_dict_conversion_order(self):
498519
"""Tests that order is conserved when converting to dictionary."""
499-
true_outer_order = ["device_id", "env", "robot_default_state"]
520+
true_outer_order = ["device_id", "env", "robot_default_state", "list_config"]
500521
true_env_order = ["num_envs", "episode_length", "viewer"]
501522
# create config
502523
cfg = BasicDemoCfg()
@@ -514,7 +535,7 @@ def test_dict_conversion_order(self):
514535
self.assertEqual(label, parsed_value)
515536
# check ordering when copied
516537
cfg_dict_copied = copy.deepcopy(cfg_dict)
517-
cfg_dict_copied.pop("robot_default_state")
538+
cfg_dict_copied.pop("list_config")
518539
# check ordering
519540
for label, parsed_value in zip(true_outer_order, cfg_dict_copied.keys()):
520541
self.assertEqual(label, parsed_value)
@@ -551,6 +572,13 @@ def test_config_update_dict_with_none(self):
551572
update_class_from_dict(cfg, cfg_dict)
552573
self.assertDictEqual(asdict(cfg), basic_demo_cfg_change_with_none_correct)
553574

575+
def test_config_update_dict_tuple(self):
576+
"""Test updating configclass using a dictionary that modifies a tuple."""
577+
cfg = BasicDemoCfg()
578+
cfg_dict = {"list_config": [{"params": {"A": -1, "B": -2}}, {"params": {"A": -3, "B": -4}}]}
579+
update_class_from_dict(cfg, cfg_dict)
580+
self.assertDictEqual(asdict(cfg), basic_demo_cfg_change_with_tuple_correct)
581+
554582
def test_config_update_nested_dict(self):
555583
"""Test updating configclass with sub-dictionaries."""
556584
cfg = NestedDictAndListCfg()

0 commit comments

Comments
 (0)