Skip to content

Commit cdfa954

Browse files
authored
Fixes support for classmethod when defining a configclass (#901)
# Description Previously, the configclass instance did not properly parse classmethods. For instance, the following would fail: ```python from __future__ import annotations """Launch Isaac Sim Simulator first.""" from omni.isaac.lab.app import AppLauncher # launch omniverse app app_launcher = AppLauncher(headless=True) """Rest everything follows.""" from omni.isaac.lab.utils.configclass import configclass @configclass class DummyClass: a: int = 5 def instance_method(self): print("Value of a: ", self.a) @classmethod def class_method(cls, value: int) -> DummyClass: return cls(a=value) cfg = DummyClass() # check all methods are callable cfg.instance_method() new_cfg1 = cfg.class_method(20) # create the same config instance using class method new_cfg2 = DummyClass.class_method(20) ``` This MR fixes the checks to make sure class-methods remain bound to the class and do not become instance variables. ## 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` - [ ] 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 8fcbf85 commit cdfa954

File tree

7 files changed

+59
-7
lines changed

7 files changed

+59
-7
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.22.3"
4+
version = "0.22.4"
55

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

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

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

4+
0.22.4 (2024-08-29)
5+
~~~~~~~~~~~~~~~~~~~
6+
7+
Fixed
8+
^^^^^
9+
10+
* Fixed the support for class-bounded methods when creating a configclass
11+
out of them. Earlier, these methods were being made as instance methods
12+
which required initialization of the class to call the class-methods.
13+
14+
415
0.22.3 (2024-08-28)
516
~~~~~~~~~~~~~~~~~~~
617

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"""Sub-module that provides a wrapper around the Python 3.7 onwards ``dataclasses`` module."""
77

88
import inspect
9+
import types
910
from collections.abc import Callable
1011
from copy import deepcopy
1112
from dataclasses import MISSING, Field, dataclass, field, replace
@@ -392,6 +393,11 @@ def _skippable_class_member(key: str, value: Any, hints: dict | None = None) ->
392393
return True
393394
# skip functions bounded to class
394395
if callable(value):
396+
# FIXME: This doesn't yet work for static methods because they are essentially seen as function types.
397+
# check for class methods
398+
if isinstance(value, types.MethodType):
399+
return True
400+
# check for instance methods
395401
signature = inspect.signature(value)
396402
if "self" in signature.parameters or "cls" in signature.parameters:
397403
return True

source/extensions/omni.isaac.lab/test/sensors/test_camera.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ def test_camera_init_intrinsic_matrix(self):
249249
prim_path="/World/Camera_2",
250250
update_period=0,
251251
data_types=["distance_to_image_plane"],
252-
spawn=sim_utils.PinholeCameraCfg().from_intrinsic_matrix(
252+
spawn=sim_utils.PinholeCameraCfg.from_intrinsic_matrix(
253253
intrinsic_matrix=intrinsic_matrix,
254254
width=128,
255255
height=128,

source/extensions/omni.isaac.lab/test/sensors/test_ray_caster_camera.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ def test_camera_init_intrinsic_matrix(self):
221221
update_period=0,
222222
offset=RayCasterCameraCfg.OffsetCfg(pos=(0.0, 0.0, 0.0), rot=(1.0, 0.0, 0.0, 0.0), convention="world"),
223223
debug_vis=False,
224-
pattern_cfg=patterns.PinholeCameraPatternCfg().from_intrinsic_matrix(
224+
pattern_cfg=patterns.PinholeCameraPatternCfg.from_intrinsic_matrix(
225225
intrinsic_matrix=intrinsic_matrix,
226226
height=self.camera_cfg.pattern_cfg.height,
227227
width=self.camera_cfg.pattern_cfg.width,
@@ -665,7 +665,7 @@ def test_output_equal_to_usd_camera_intrinsics(self):
665665
mesh_prim_paths=["/World/defaultGroundPlane"],
666666
offset=RayCasterCameraCfg.OffsetCfg(pos=offset_pos, rot=offset_rot, convention="ros"),
667667
debug_vis=False,
668-
pattern_cfg=patterns.PinholeCameraPatternCfg().from_intrinsic_matrix(
668+
pattern_cfg=patterns.PinholeCameraPatternCfg.from_intrinsic_matrix(
669669
intrinsic_matrix=intrinsics,
670670
height=540,
671671
width=960,
@@ -677,7 +677,7 @@ def test_output_equal_to_usd_camera_intrinsics(self):
677677
camera_usd_cfg = CameraCfg(
678678
prim_path="/World/Camera_usd",
679679
offset=CameraCfg.OffsetCfg(pos=offset_pos, rot=offset_rot, convention="ros"),
680-
spawn=PinholeCameraCfg().from_intrinsic_matrix(
680+
spawn=PinholeCameraCfg.from_intrinsic_matrix(
681681
intrinsic_matrix=intrinsics,
682682
height=540,
683683
width=960,

source/extensions/omni.isaac.lab/test/sensors/test_tiled_camera.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ def test_output_equal_to_usd_camera_intrinsics(self):
315315
camera_tiled_cfg = TiledCameraCfg(
316316
prim_path="/World/Camera_tiled",
317317
offset=TiledCameraCfg.OffsetCfg(pos=offset_pos, rot=offset_rot, convention="ros"),
318-
spawn=sim_utils.PinholeCameraCfg().from_intrinsic_matrix(
318+
spawn=sim_utils.PinholeCameraCfg.from_intrinsic_matrix(
319319
intrinsic_matrix=intrinsics,
320320
height=540,
321321
width=960,
@@ -329,7 +329,7 @@ def test_output_equal_to_usd_camera_intrinsics(self):
329329
camera_usd_cfg = CameraCfg(
330330
prim_path="/World/Camera_usd",
331331
offset=CameraCfg.OffsetCfg(pos=offset_pos, rot=offset_rot, convention="ros"),
332-
spawn=sim_utils.PinholeCameraCfg().from_intrinsic_matrix(
332+
spawn=sim_utils.PinholeCameraCfg.from_intrinsic_matrix(
333333
intrinsic_matrix=intrinsics,
334334
height=540,
335335
width=960,

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
#
44
# SPDX-License-Identifier: BSD-3-Clause
55

6+
from __future__ import annotations
7+
68
# NOTE: While we don't actually use the simulation app in this test, we still need to launch it
79
# because warp is only available in the context of a running simulation
810
"""Launch Isaac Sim Simulator first."""
@@ -292,6 +294,20 @@ def set_a(self, a: int):
292294
self.a = a
293295

294296

297+
@configclass
298+
class ClassFunctionImplementedDemoCfg:
299+
"""Dummy configuration class with function members defined in the class."""
300+
301+
a: int = 5
302+
303+
def instance_method(self):
304+
print("Value of a: ", self.a)
305+
306+
@classmethod
307+
def class_method(cls, value: int) -> ClassFunctionImplementedDemoCfg:
308+
return cls(a=value)
309+
310+
295311
"""
296312
Dummy configuration: Nested dictionaries
297313
"""
@@ -618,12 +634,31 @@ def test_functions_config(self):
618634
self.assertEqual(cfg.func_in_dict["func"](), 1)
619635

620636
def test_function_impl_config(self):
637+
"""Tests having function defined in the class instance."""
621638
cfg = FunctionImplementedDemoCfg()
622639
# change value
623640
self.assertEqual(cfg.a, 5)
624641
cfg.set_a(10)
625642
self.assertEqual(cfg.a, 10)
626643

644+
def test_class_function_impl_config(self):
645+
"""Tests having class and static function defined in the class instance."""
646+
cfg = ClassFunctionImplementedDemoCfg()
647+
648+
# check that the annotations are correct
649+
self.assertDictEqual(cfg.__annotations__, {"a": "int"})
650+
651+
# check all methods are callable
652+
cfg.instance_method()
653+
new_cfg1 = cfg.class_method(20)
654+
# check value is correct
655+
self.assertEqual(new_cfg1.a, 20)
656+
657+
# create the same config instance using class method
658+
new_cfg2 = ClassFunctionImplementedDemoCfg.class_method(20)
659+
# check value is correct
660+
self.assertEqual(new_cfg2.a, 20)
661+
627662
def test_dict_conversion_functions_config(self):
628663
"""Tests conversion of config with functions into dictionary."""
629664
cfg = FunctionsDemoCfg()

0 commit comments

Comments
 (0)