From 64aa78cd65038cdd368e25ccebdf35320d0080fc Mon Sep 17 00:00:00 2001 From: Octi Zhang Date: Thu, 26 Feb 2026 23:28:29 -0800 Subject: [PATCH 1/9] fix redundant string to callable --- source/isaaclab/isaaclab/utils/configclass.py | 15 ++++++++++++--- source/isaaclab/isaaclab/utils/dict.py | 9 +++++---- source/isaaclab/test/utils/test_configclass.py | 2 +- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/source/isaaclab/isaaclab/utils/configclass.py b/source/isaaclab/isaaclab/utils/configclass.py index 588e8234b578..cce899fce38b 100644 --- a/source/isaaclab/isaaclab/utils/configclass.py +++ b/source/isaaclab/isaaclab/utils/configclass.py @@ -217,11 +217,20 @@ def _wrap_resolvable_strings(value: Any, module_dir: str | None = None) -> Any: value = value.replace("{DIR}", module_dir) return ResolvableString(value) if isinstance(value, list): - return [_wrap_resolvable_strings(item, module_dir=module_dir) for item in value] + wrapped = [_wrap_resolvable_strings(item, module_dir=module_dir) for item in value] + if len(wrapped) == len(value) and all(new_item is old_item for new_item, old_item in zip(wrapped, value)): + return value + return wrapped if isinstance(value, tuple): - return tuple(_wrap_resolvable_strings(item, module_dir=module_dir) for item in value) + wrapped = tuple(_wrap_resolvable_strings(item, module_dir=module_dir) for item in value) + if len(wrapped) == len(value) and all(new_item is old_item for new_item, old_item in zip(wrapped, value)): + return value + return wrapped if isinstance(value, dict): - return {key: _wrap_resolvable_strings(item, module_dir=module_dir) for key, item in value.items()} + wrapped = {key: _wrap_resolvable_strings(item, module_dir=module_dir) for key, item in value.items()} + if len(wrapped) == len(value) and all(wrapped[key] is value[key] for key in value): + return value + return wrapped if hasattr(value, "__dataclass_fields__") and hasattr(value, "__dict__"): for key, item in value.__dict__.items(): nested_module_dir = _field_module_dir(value, key) diff --git a/source/isaaclab/isaaclab/utils/dict.py b/source/isaaclab/isaaclab/utils/dict.py index b80bcde6351e..6994a2b12b95 100644 --- a/source/isaaclab/isaaclab/utils/dict.py +++ b/source/isaaclab/isaaclab/utils/dict.py @@ -146,10 +146,11 @@ def update_class_from_dict(obj, data: dict[str, Any], _ns: str = "") -> None: # -- 3) callable attribute → keep string lazily resolvable -------------- elif callable(obj_mem): - # Do not eagerly import backend modules while applying config dictionaries. - # Keep callable strings lazy; they resolve only when actually invoked. - if isinstance(value, str) and not isinstance(value, ResolvableString): - value = ResolvableString(value) + if isinstance(value, str): + if not isinstance(value, ResolvableString): + value = ResolvableString(value) + elif isinstance(value, ResolvableString): + pass elif not callable(value): raise ValueError( f"[Config]: Incorrect type under namespace: {key_ns}." diff --git a/source/isaaclab/test/utils/test_configclass.py b/source/isaaclab/test/utils/test_configclass.py index 7ed90787b378..10f1a13b02fc 100644 --- a/source/isaaclab/test/utils/test_configclass.py +++ b/source/isaaclab/test/utils/test_configclass.py @@ -447,7 +447,7 @@ class MissingChildDemoCfg(MissingParentDemoCfg): basic_demo_cfg_nested_dict_and_list = { "dict_1": { - "dict_2": {"func": dummy_function2}, + "dict_2": {"func": "test_configclass:dummy_function2"}, }, "list_1": [ {"num_envs": 23, "episode_length": 3000, "viewer": {"eye": [5.0, 5.0, 5.0], "lookat": [0.0, 0.0, 0.0]}}, From 1b1301708f421f54df8e774148a7283b550de36c Mon Sep 17 00:00:00 2001 From: Octi Zhang Date: Thu, 26 Feb 2026 23:31:43 -0800 Subject: [PATCH 2/9] fix callable --- source/isaaclab/docs/CHANGELOG.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/source/isaaclab/docs/CHANGELOG.rst b/source/isaaclab/docs/CHANGELOG.rst index 507f9ba07a2d..f6b9d28839da 100644 --- a/source/isaaclab/docs/CHANGELOG.rst +++ b/source/isaaclab/docs/CHANGELOG.rst @@ -57,6 +57,10 @@ Changed for config defaults. ``"{DIR}.module:Symbol"`` now expands to the declaring config module directory before resolution. +* Updated :func:`~isaaclab.utils.dict.update_class_from_dict` to stop eagerly resolving + callable strings during updates. Callable-string inputs are now preserved as lazy + :class:`~isaaclab.utils.string.ResolvableString` values and resolve only on first use. + 4.2.2 (2026-02-26) ~~~~~~~~~~~~~~~~~~ From 7e04c434befbe5278abc5f5d47fabf5fa3746129 Mon Sep 17 00:00:00 2001 From: Octi Zhang Date: Thu, 26 Feb 2026 23:33:18 -0800 Subject: [PATCH 3/9] fix redundancy --- source/isaaclab/isaaclab/utils/dict.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/source/isaaclab/isaaclab/utils/dict.py b/source/isaaclab/isaaclab/utils/dict.py index 6994a2b12b95..6036931234e9 100644 --- a/source/isaaclab/isaaclab/utils/dict.py +++ b/source/isaaclab/isaaclab/utils/dict.py @@ -149,8 +149,6 @@ def update_class_from_dict(obj, data: dict[str, Any], _ns: str = "") -> None: if isinstance(value, str): if not isinstance(value, ResolvableString): value = ResolvableString(value) - elif isinstance(value, ResolvableString): - pass elif not callable(value): raise ValueError( f"[Config]: Incorrect type under namespace: {key_ns}." From 23c5986c2a3f431ffcfeb98bf63e0998c7bd9c1d Mon Sep 17 00:00:00 2001 From: Octi Zhang Date: Fri, 27 Feb 2026 02:47:22 -0800 Subject: [PATCH 4/9] fix revert bug --- source/isaaclab/test/utils/test_dict.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/source/isaaclab/test/utils/test_dict.py b/source/isaaclab/test/utils/test_dict.py index 2632bf5f55e9..464a26d878ed 100644 --- a/source/isaaclab/test/utils/test_dict.py +++ b/source/isaaclab/test/utils/test_dict.py @@ -53,7 +53,7 @@ def test_string_callable_function_conversion(): # convert function to string test_string = dict_utils.callable_to_string(_test_function) # convert string to function - test_function_2 = dict_utils.string_to_callable(test_string) + test_function_2 = string_utils.string_to_callable(test_string) # check that functions are the same assert _test_function(2) == test_function_2(2) @@ -64,7 +64,7 @@ def test_string_callable_function_with_lambda_in_name_conversion(): # convert function to string test_string = dict_utils.callable_to_string(_test_lambda_function) # convert string to function - test_function_2 = dict_utils.string_to_callable(test_string) + test_function_2 = string_utils.string_to_callable(test_string) # check that functions are the same assert _test_function(2) == test_function_2(2) @@ -77,7 +77,7 @@ def test_string_callable_lambda_conversion(): # convert function to string test_string = dict_utils.callable_to_string(func) # convert string to function - func_2 = dict_utils.string_to_callable(test_string) + func_2 = string_utils.string_to_callable(test_string) # check that functions are the same assert test_string == "lambda x: x**2" assert func(2) == func_2(2) From 592865c5e48d23737fd714632eb1edcf847b7460 Mon Sep 17 00:00:00 2001 From: Octi Zhang Date: Fri, 27 Feb 2026 04:30:01 -0800 Subject: [PATCH 5/9] fix bug --- source/isaaclab/isaaclab/utils/configclass.py | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/source/isaaclab/isaaclab/utils/configclass.py b/source/isaaclab/isaaclab/utils/configclass.py index cce899fce38b..1878a35f2f2e 100644 --- a/source/isaaclab/isaaclab/utils/configclass.py +++ b/source/isaaclab/isaaclab/utils/configclass.py @@ -208,7 +208,7 @@ def _field_module_dir(obj: Any, key: str | None = None) -> str | None: return module_name.rsplit(".", 1)[0] if "." in module_name else (module_name or None) -def _wrap_resolvable_strings(value: Any, module_dir: str | None = None) -> Any: +def _wrap_resolvable_strings(value: Any, module_dir: str | None = None, _seen: set[int] | None = None) -> Any: """Recursively wrap callable-like strings with :class:`ResolvableString`.""" if isinstance(value, str) and (_CALLABLE_STR_RE.match(value) or _CALLABLE_STR_WITH_DIR_RE.match(value)): if "{DIR}" in value: @@ -216,25 +216,36 @@ def _wrap_resolvable_strings(value: Any, module_dir: str | None = None) -> Any: raise ValueError(f"Cannot resolve '{{DIR}}' in '{value}' because no module context is available.") value = value.replace("{DIR}", module_dir) return ResolvableString(value) + is_dataclass_instance = hasattr(value, "__dataclass_fields__") and hasattr(value, "__dict__") + is_container = isinstance(value, (list, tuple, dict)) + if is_dataclass_instance or is_container: + if _seen is None: + _seen = set() + value_id = id(value) + if value_id in _seen: + return value + _seen.add(value_id) if isinstance(value, list): - wrapped = [_wrap_resolvable_strings(item, module_dir=module_dir) for item in value] + wrapped = [_wrap_resolvable_strings(item, module_dir=module_dir, _seen=_seen) for item in value] if len(wrapped) == len(value) and all(new_item is old_item for new_item, old_item in zip(wrapped, value)): return value return wrapped if isinstance(value, tuple): - wrapped = tuple(_wrap_resolvable_strings(item, module_dir=module_dir) for item in value) + wrapped = tuple(_wrap_resolvable_strings(item, module_dir=module_dir, _seen=_seen) for item in value) if len(wrapped) == len(value) and all(new_item is old_item for new_item, old_item in zip(wrapped, value)): return value return wrapped if isinstance(value, dict): - wrapped = {key: _wrap_resolvable_strings(item, module_dir=module_dir) for key, item in value.items()} + wrapped = { + key: _wrap_resolvable_strings(item, module_dir=module_dir, _seen=_seen) for key, item in value.items() + } if len(wrapped) == len(value) and all(wrapped[key] is value[key] for key in value): return value return wrapped - if hasattr(value, "__dataclass_fields__") and hasattr(value, "__dict__"): + if is_dataclass_instance: for key, item in value.__dict__.items(): nested_module_dir = _field_module_dir(value, key) - setattr(value, key, _wrap_resolvable_strings(item, module_dir=nested_module_dir)) + setattr(value, key, _wrap_resolvable_strings(item, module_dir=nested_module_dir, _seen=_seen)) return value From 8e5b9343be25bca435abcdcf40a1e95e550b6124 Mon Sep 17 00:00:00 2001 From: Octi Zhang Date: Fri, 27 Feb 2026 04:37:15 -0800 Subject: [PATCH 6/9] fix bug --- .../isaaclab/test/utils/test_configclass.py | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/source/isaaclab/test/utils/test_configclass.py b/source/isaaclab/test/utils/test_configclass.py index 10f1a13b02fc..eb36cde3546e 100644 --- a/source/isaaclab/test/utils/test_configclass.py +++ b/source/isaaclab/test/utils/test_configclass.py @@ -29,6 +29,7 @@ from isaaclab.utils.configclass import configclass from isaaclab.utils.dict import class_to_dict, dict_to_md5_hash, update_class_from_dict from isaaclab.utils.io import dump_yaml, load_yaml +from isaaclab.utils.string import ResolvableString """ Mock classes and functions. @@ -643,6 +644,47 @@ def test_config_update_nested_dict(): assert isinstance(cfg.list_1[1].viewer, ViewerCfg) +def test_wrap_resolvable_strings_handles_cyclic_containers(): + """Cyclic container graphs in config values should not recurse forever.""" + + @configclass + class CyclicContainerCfg: + payload: dict[str, Any] = field(default_factory=dict) + + def __post_init__(self): + cycle = {} + cycle["self"] = cycle + cycle["tuple"] = (cycle, {"back": cycle}) + self.payload = cycle + + cfg = CyclicContainerCfg() + + assert cfg.payload["self"] is cfg.payload + assert cfg.payload["tuple"][0] is cfg.payload + assert cfg.payload["tuple"][1]["back"] is cfg.payload + + +def test_dir_resolution_uses_declaring_class_for_inherited_field(): + """{DIR} expansion should use the field declaring class, not subclass module.""" + + @configclass + class _BaseCfg: + class_type: type | str = "{DIR}.base_mod:BaseSymbol" + + @configclass + class _ChildCfg(_BaseCfg): + pass + + # Simulate subclass declared in a different package than the parent config. + _BaseCfg.__module__ = "test_pkg.parent.base_cfg" + _ChildCfg.__module__ = "other_pkg.child.child_cfg" + + cfg = _ChildCfg() + + assert isinstance(cfg.class_type, ResolvableString) + assert str(cfg.class_type) == "test_pkg.parent.base_mod:BaseSymbol" + + def test_config_update_different_iterable_lengths(): """Iterables are whole replaced, even if their lengths are different.""" From ac529bc24852277833bdce441bf9fc6eeb776ee4 Mon Sep 17 00:00:00 2001 From: Antoine Richard Date: Fri, 27 Feb 2026 13:40:04 +0100 Subject: [PATCH 7/9] added test to validate {DIR} resolution --- .../isaaclab/test/utils/test_configclass.py | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/source/isaaclab/test/utils/test_configclass.py b/source/isaaclab/test/utils/test_configclass.py index eb36cde3546e..716c834cc3e1 100644 --- a/source/isaaclab/test/utils/test_configclass.py +++ b/source/isaaclab/test/utils/test_configclass.py @@ -26,7 +26,7 @@ import pytest import torch -from isaaclab.utils.configclass import configclass +from isaaclab.utils.configclass import _field_module_dir, configclass from isaaclab.utils.dict import class_to_dict, dict_to_md5_hash, update_class_from_dict from isaaclab.utils.io import dump_yaml, load_yaml from isaaclab.utils.string import ResolvableString @@ -1119,3 +1119,29 @@ def test_validity(): # check that no more than the expected missing fields are in the error message assert len(error_message.split("\n")) - 2 == len(validity_expected_fields) + + +def test_dir_resolution_in_subclass(): + """Test that {DIR} in inherited fields resolves relative to the declaring class's module.""" + + @configclass + class ParentCfg: + class_type: str = "{DIR}.my_module:MyClass" + name: str = "default" + + @configclass + class ChildCfg(ParentCfg): + extra: int = 42 + + # Pretend the parent lives in a real package and the child lives in a test file + ParentCfg.__module__ = "some_package.sub_package.parent_cfg" + ChildCfg.__module__ = "test_some_feature" + + parent = ParentCfg.__new__(ParentCfg) + child = ChildCfg.__new__(ChildCfg) + + # class_type should resolve to the parent's module dir in both cases + assert _field_module_dir(parent, "class_type") == "some_package.sub_package" + assert _field_module_dir(child, "class_type") == "some_package.sub_package" + # extra should resolve to the child's module dir + assert _field_module_dir(child, "extra") == "test_some_feature" From fbb393c59eb314b5a7e4d880b8d8aac5aa754b45 Mon Sep 17 00:00:00 2001 From: Octi Zhang Date: Fri, 27 Feb 2026 13:11:53 -0800 Subject: [PATCH 8/9] fixs is subclassing issue in resolvable string --- source/isaaclab/isaaclab/devices/device_base.py | 7 ++++--- .../isaaclab/isaaclab/devices/gamepad/se2_gamepad_cfg.py | 5 +++-- .../isaaclab/isaaclab/devices/gamepad/se3_gamepad_cfg.py | 5 +++-- source/isaaclab/isaaclab/devices/haply/se3_haply.py | 4 ++-- .../isaaclab/isaaclab/devices/keyboard/se2_keyboard_cfg.py | 5 +++-- .../isaaclab/isaaclab/devices/keyboard/se3_keyboard_cfg.py | 5 +++-- source/isaaclab/isaaclab/devices/retargeter_base.py | 4 ++-- .../isaaclab/devices/spacemouse/se2_spacemouse_cfg.py | 5 +++-- .../isaaclab/devices/spacemouse/se3_spacemouse_cfg.py | 5 +++-- .../isaaclab_teleop/deprecated/teleop_device_factory.py | 3 ++- 10 files changed, 28 insertions(+), 20 deletions(-) diff --git a/source/isaaclab/isaaclab/devices/device_base.py b/source/isaaclab/isaaclab/devices/device_base.py index edc75797d732..00c7100ab523 100644 --- a/source/isaaclab/isaaclab/devices/device_base.py +++ b/source/isaaclab/isaaclab/devices/device_base.py @@ -9,16 +9,17 @@ from abc import ABC, abstractmethod from collections.abc import Callable -from dataclasses import dataclass, field +from dataclasses import field from enum import Enum from typing import Any import torch from isaaclab.devices.retargeter_base import RetargeterBase, RetargeterCfg +from isaaclab.utils import configclass -@dataclass +@configclass class DeviceCfg: """Configuration for teleoperation devices.""" @@ -32,7 +33,7 @@ class DeviceCfg: class_type: type[DeviceBase] | None = None -@dataclass +@configclass class DevicesCfg: """Configuration for all supported teleoperation devices.""" diff --git a/source/isaaclab/isaaclab/devices/gamepad/se2_gamepad_cfg.py b/source/isaaclab/isaaclab/devices/gamepad/se2_gamepad_cfg.py index 32bd60dea52c..fc4239538070 100644 --- a/source/isaaclab/isaaclab/devices/gamepad/se2_gamepad_cfg.py +++ b/source/isaaclab/isaaclab/devices/gamepad/se2_gamepad_cfg.py @@ -7,16 +7,17 @@ from __future__ import annotations -from dataclasses import dataclass from typing import TYPE_CHECKING +from isaaclab.utils import configclass + from ..device_base import DeviceCfg if TYPE_CHECKING: from .se2_gamepad import Se2Gamepad -@dataclass +@configclass class Se2GamepadCfg(DeviceCfg): """Configuration for SE2 gamepad devices.""" diff --git a/source/isaaclab/isaaclab/devices/gamepad/se3_gamepad_cfg.py b/source/isaaclab/isaaclab/devices/gamepad/se3_gamepad_cfg.py index 3434a3ad1132..123105fa10dc 100644 --- a/source/isaaclab/isaaclab/devices/gamepad/se3_gamepad_cfg.py +++ b/source/isaaclab/isaaclab/devices/gamepad/se3_gamepad_cfg.py @@ -7,16 +7,17 @@ from __future__ import annotations -from dataclasses import dataclass from typing import TYPE_CHECKING +from isaaclab.utils import configclass + from ..device_base import DeviceCfg if TYPE_CHECKING: from .se3_gamepad import Se3Gamepad -@dataclass +@configclass class Se3GamepadCfg(DeviceCfg): """Configuration for SE3 gamepad devices.""" diff --git a/source/isaaclab/isaaclab/devices/haply/se3_haply.py b/source/isaaclab/isaaclab/devices/haply/se3_haply.py index 33bce2cfa96a..371616a2f8a4 100644 --- a/source/isaaclab/isaaclab/devices/haply/se3_haply.py +++ b/source/isaaclab/isaaclab/devices/haply/se3_haply.py @@ -12,7 +12,7 @@ import threading import time from collections.abc import Callable -from dataclasses import dataclass +from isaaclab.utils import configclass import numpy as np import torch @@ -377,7 +377,7 @@ async def _websocket_loop(self): break -@dataclass +@configclass class HaplyDeviceCfg(DeviceCfg): """Configuration for Haply device. diff --git a/source/isaaclab/isaaclab/devices/keyboard/se2_keyboard_cfg.py b/source/isaaclab/isaaclab/devices/keyboard/se2_keyboard_cfg.py index 1a138e1683d2..acecde3ae7d6 100644 --- a/source/isaaclab/isaaclab/devices/keyboard/se2_keyboard_cfg.py +++ b/source/isaaclab/isaaclab/devices/keyboard/se2_keyboard_cfg.py @@ -7,16 +7,17 @@ from __future__ import annotations -from dataclasses import dataclass from typing import TYPE_CHECKING +from isaaclab.utils import configclass + from ..device_base import DeviceCfg if TYPE_CHECKING: from .se2_keyboard import Se2Keyboard -@dataclass +@configclass class Se2KeyboardCfg(DeviceCfg): """Configuration for SE2 keyboard devices.""" diff --git a/source/isaaclab/isaaclab/devices/keyboard/se3_keyboard_cfg.py b/source/isaaclab/isaaclab/devices/keyboard/se3_keyboard_cfg.py index 36d6f48f1ae1..3ac55205360a 100644 --- a/source/isaaclab/isaaclab/devices/keyboard/se3_keyboard_cfg.py +++ b/source/isaaclab/isaaclab/devices/keyboard/se3_keyboard_cfg.py @@ -7,16 +7,17 @@ from __future__ import annotations -from dataclasses import dataclass from typing import TYPE_CHECKING +from isaaclab.utils import configclass + from ..device_base import DeviceCfg if TYPE_CHECKING: from .se3_keyboard import Se3Keyboard -@dataclass +@configclass class Se3KeyboardCfg(DeviceCfg): """Configuration for SE3 keyboard devices.""" diff --git a/source/isaaclab/isaaclab/devices/retargeter_base.py b/source/isaaclab/isaaclab/devices/retargeter_base.py index 3e5fc4e47c92..c8a32e030c59 100644 --- a/source/isaaclab/isaaclab/devices/retargeter_base.py +++ b/source/isaaclab/isaaclab/devices/retargeter_base.py @@ -16,12 +16,12 @@ import warnings from abc import ABC, abstractmethod -from dataclasses import dataclass +from isaaclab.utils import configclass from enum import Enum from typing import Any -@dataclass +@configclass class RetargeterCfg: """Base configuration for hand tracking retargeters. diff --git a/source/isaaclab/isaaclab/devices/spacemouse/se2_spacemouse_cfg.py b/source/isaaclab/isaaclab/devices/spacemouse/se2_spacemouse_cfg.py index 6cf5005fb760..91e6a823833a 100644 --- a/source/isaaclab/isaaclab/devices/spacemouse/se2_spacemouse_cfg.py +++ b/source/isaaclab/isaaclab/devices/spacemouse/se2_spacemouse_cfg.py @@ -7,16 +7,17 @@ from __future__ import annotations -from dataclasses import dataclass from typing import TYPE_CHECKING +from isaaclab.utils import configclass + from ..device_base import DeviceCfg if TYPE_CHECKING: from .se2_spacemouse import Se2SpaceMouse -@dataclass +@configclass class Se2SpaceMouseCfg(DeviceCfg): """Configuration for SE2 space mouse devices.""" diff --git a/source/isaaclab/isaaclab/devices/spacemouse/se3_spacemouse_cfg.py b/source/isaaclab/isaaclab/devices/spacemouse/se3_spacemouse_cfg.py index 0bf4b73b44f6..ba63dae831b6 100644 --- a/source/isaaclab/isaaclab/devices/spacemouse/se3_spacemouse_cfg.py +++ b/source/isaaclab/isaaclab/devices/spacemouse/se3_spacemouse_cfg.py @@ -7,16 +7,17 @@ from __future__ import annotations -from dataclasses import dataclass from typing import TYPE_CHECKING +from isaaclab.utils import configclass + from ..device_base import DeviceCfg if TYPE_CHECKING: from .se3_spacemouse import Se3SpaceMouse -@dataclass +@configclass class Se3SpaceMouseCfg(DeviceCfg): """Configuration for SE3 space mouse devices.""" diff --git a/source/isaaclab_teleop/isaaclab_teleop/deprecated/teleop_device_factory.py b/source/isaaclab_teleop/isaaclab_teleop/deprecated/teleop_device_factory.py index 73f0b9a9355f..cb8fe18e8e74 100644 --- a/source/isaaclab_teleop/isaaclab_teleop/deprecated/teleop_device_factory.py +++ b/source/isaaclab_teleop/isaaclab_teleop/deprecated/teleop_device_factory.py @@ -64,7 +64,8 @@ def create_teleop_device( f"Device configuration '{device_name}' does not declare class_type. " "Set cfg.class_type to the concrete DeviceBase subclass." ) - if not issubclass(device_constructor, DeviceBase): + check_cls = device_constructor._resolve() if hasattr(device_constructor, "_resolve") else device_constructor + if not issubclass(check_cls, DeviceBase): raise TypeError(f"class_type for '{device_name}' must be a subclass of DeviceBase; got {device_constructor}") # Try to create retargeters if they are configured From 29c564f293f8ea5a5359f2132142350cc4707c47 Mon Sep 17 00:00:00 2001 From: Octi Zhang Date: Fri, 27 Feb 2026 13:18:22 -0800 Subject: [PATCH 9/9] fix linter --- source/isaaclab/isaaclab/devices/haply/se3_haply.py | 3 ++- source/isaaclab/isaaclab/devices/retargeter_base.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/source/isaaclab/isaaclab/devices/haply/se3_haply.py b/source/isaaclab/isaaclab/devices/haply/se3_haply.py index 371616a2f8a4..f9e190de8a4a 100644 --- a/source/isaaclab/isaaclab/devices/haply/se3_haply.py +++ b/source/isaaclab/isaaclab/devices/haply/se3_haply.py @@ -12,11 +12,12 @@ import threading import time from collections.abc import Callable -from isaaclab.utils import configclass import numpy as np import torch +from isaaclab.utils import configclass + try: import websockets diff --git a/source/isaaclab/isaaclab/devices/retargeter_base.py b/source/isaaclab/isaaclab/devices/retargeter_base.py index c8a32e030c59..0859dd5342cf 100644 --- a/source/isaaclab/isaaclab/devices/retargeter_base.py +++ b/source/isaaclab/isaaclab/devices/retargeter_base.py @@ -16,10 +16,11 @@ import warnings from abc import ABC, abstractmethod -from isaaclab.utils import configclass from enum import Enum from typing import Any +from isaaclab.utils import configclass + @configclass class RetargeterCfg: