Unverified Commit 41a9dd44 authored by jtigue-bdai's avatar jtigue-bdai Committed by GitHub

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
parent 9c7238d2
[package]
# Note: Semantic Versioning is used: https://semver.org/
version = "0.27.2"
version = "0.27.3"
# Description
title = "Isaac Lab framework for Robot Learning"
......
Changelog
---------
0.27.3 (2024-10-22)
~~~~~~~~~~~~~~~~~~~
Fixed
^^^^^
* Fixed the issue with using list or tuples of ``configclass`` within a ``configclass``. Earlier, the list of
configclass objects were not converted to dictionary properly when ``to_dict`` function was called.
0.27.2 (2024-10-21)
~~~~~~~~~~~~~~~~~~~
......
......@@ -40,8 +40,10 @@ def class_to_dict(obj: object) -> dict[str, Any]:
# convert object to dictionary
if isinstance(obj, dict):
obj_dict = obj
else:
elif hasattr(obj, "__dict__"):
obj_dict = obj.__dict__
else:
return obj
# convert to dictionary
data = dict()
......@@ -55,6 +57,8 @@ def class_to_dict(obj: object) -> dict[str, Any]:
# check if attribute is a dictionary
elif hasattr(value, "__dict__") or isinstance(value, dict):
data[key] = class_to_dict(value)
elif isinstance(value, (list, tuple)):
data[key] = type(value)([class_to_dict(v) for v in value])
else:
data[key] = value
return data
......
......@@ -23,7 +23,7 @@ import unittest
from collections.abc import Callable
from dataclasses import MISSING, asdict, field
from functools import wraps
from typing import ClassVar
from typing import Any, ClassVar
from omni.isaac.lab.utils.configclass import configclass
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):
return 2 * x
@configclass
class ModifierCfg:
params: dict[str, Any] = {"A": 1, "B": 2}
@configclass
class ViewerCfg:
eye: list = [7.5, 7.5, 7.5] # field missing on purpose
......@@ -113,6 +118,7 @@ class BasicDemoCfg:
device_id: int = 0
env: EnvCfg = EnvCfg()
robot_default_state: RobotDefaultStateCfg = RobotDefaultStateCfg()
list_config = [ModifierCfg(), ModifierCfg(params={"A": 3, "B": 4})]
@configclass
......@@ -381,6 +387,7 @@ basic_demo_cfg_correct = {
"dof_vel": [0.0, 0.0, 0.0, 0.0, 0.0, 1.0],
},
"device_id": 0,
"list_config": [{"params": {"A": 1, "B": 2}}, {"params": {"A": 3, "B": 4}}],
}
basic_demo_cfg_change_correct = {
......@@ -392,6 +399,7 @@ basic_demo_cfg_change_correct = {
"dof_vel": [0.0, 0.0, 0.0, 0.0, 0.0, 1.0],
},
"device_id": 0,
"list_config": [{"params": {"A": 1, "B": 2}}, {"params": {"A": 3, "B": 4}}],
}
basic_demo_cfg_change_with_none_correct = {
......@@ -403,6 +411,19 @@ basic_demo_cfg_change_with_none_correct = {
"dof_vel": [0.0, 0.0, 0.0, 0.0, 0.0, 1.0],
},
"device_id": 0,
"list_config": [{"params": {"A": 1, "B": 2}}, {"params": {"A": 3, "B": 4}}],
}
basic_demo_cfg_change_with_tuple_correct = {
"env": {"num_envs": 56, "episode_length": 2000, "viewer": {"eye": [7.5, 7.5, 7.5], "lookat": [0.0, 0.0, 0.0]}},
"robot_default_state": {
"pos": (0.0, 0.0, 0.0),
"rot": (1.0, 0.0, 0.0, 0.0),
"dof_pos": (0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
"dof_vel": [0.0, 0.0, 0.0, 0.0, 0.0, 1.0],
},
"device_id": 0,
"list_config": [{"params": {"A": -1, "B": -2}}, {"params": {"A": -3, "B": -4}}],
}
basic_demo_cfg_nested_dict_and_list = {
......@@ -496,7 +517,7 @@ class TestConfigClass(unittest.TestCase):
def test_dict_conversion_order(self):
"""Tests that order is conserved when converting to dictionary."""
true_outer_order = ["device_id", "env", "robot_default_state"]
true_outer_order = ["device_id", "env", "robot_default_state", "list_config"]
true_env_order = ["num_envs", "episode_length", "viewer"]
# create config
cfg = BasicDemoCfg()
......@@ -514,7 +535,7 @@ class TestConfigClass(unittest.TestCase):
self.assertEqual(label, parsed_value)
# check ordering when copied
cfg_dict_copied = copy.deepcopy(cfg_dict)
cfg_dict_copied.pop("robot_default_state")
cfg_dict_copied.pop("list_config")
# check ordering
for label, parsed_value in zip(true_outer_order, cfg_dict_copied.keys()):
self.assertEqual(label, parsed_value)
......@@ -551,6 +572,13 @@ class TestConfigClass(unittest.TestCase):
update_class_from_dict(cfg, cfg_dict)
self.assertDictEqual(asdict(cfg), basic_demo_cfg_change_with_none_correct)
def test_config_update_dict_tuple(self):
"""Test updating configclass using a dictionary that modifies a tuple."""
cfg = BasicDemoCfg()
cfg_dict = {"list_config": [{"params": {"A": -1, "B": -2}}, {"params": {"A": -3, "B": -4}}]}
update_class_from_dict(cfg, cfg_dict)
self.assertDictEqual(asdict(cfg), basic_demo_cfg_change_with_tuple_correct)
def test_config_update_nested_dict(self):
"""Test updating configclass with sub-dictionaries."""
cfg = NestedDictAndListCfg()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment