Keep isaaclab.utils.configclass bound to the decorator - #7646
Keep isaaclab.utils.configclass bound to the decorator#7646pascal-roth wants to merge 4 commits into
Conversation
The sub-module isaaclab.utils.configclass and the decorator it defines share a name. Importing the sub-module makes the import machinery bind the module object onto isaaclab.utils, shadowing the attribute that lazy_loader attaches from the .pyi stub. From that point on `from isaaclab.utils import configclass` returns a module and `@configclass` fails with "TypeError: 'module' object is not callable". The outcome depends only on which of the two was imported first, so downstream code breaks without ever touching the sub-module: importing isaaclab_rl, isaaclab_newton, isaaclab_contrib or isaaclab_teleop is enough, since they all import the sub-module directly. Give isaaclab.utils a module type that skips exactly that assignment, so the attribute exported by the stub wins regardless of import order. The sub-module stays importable through `from isaaclab.utils.configclass import ...`.
The PR should not merge until aliased direct imports of Findings
|
| if name in __all__ and isinstance(value, types.ModuleType) and value.__name__ == f"{__name__}.{name}": | ||
| return |
There was a problem hiding this comment.
Aliased submodule import breaks
With import isaaclab.utils.configclass as configclass_module, Python resolves the alias through isaaclab.utils.configclass. This guard discards the submodule assignment, so the alias receives the decorator instead of the module. Callers then fail when accessing module members such as configclass_module.checked_apply. The new test imports the submodule only for its side effect, so it does not cover this valid import form.
There was a problem hiding this comment.
Isaac Lab Review Bot
The fix makes the documented from isaaclab.utils import configclass import order-independent, but suppressing the child-module binding also changes established dotted-import and parent-attribute behavior for isaaclab.utils.configclass. This compatibility impact should be preserved or explicitly documented with migration guidance.
- Design and architecture: The custom module type narrowly guards same-named stub exports, but it intentionally breaks the normal package invariant that an imported child module is available as an attribute of its parent. That tradeoff affects consumers beyond the documented decorator import and needs maintainer action before merge.
- API: The decorator import and
from isaaclab.utils.configclass import ...remain functional. However,import isaaclab.utils.configclass as mcan bind the decorator rather than the module, while dotted access and string-based resolution throughisaaclab.utils.configclassno longer reach the child module. Preserve module-valued access or document the changed behavior and migration path in the changelog. - Implementation: The guard is narrowly scoped by
__all__, module type, and expected child-module name, and the subprocess test validates the targeted import-order regression. It does not cover the newly changed dotted-import and parent-attribute access paths identified in finding 0.
Minor fixes needed. Posted 1 actionable finding inline.
Automated review; human maintainers own approval decisions.
| """ | ||
|
|
||
| def __setattr__(self, name: str, value: Any) -> None: | ||
| if name in __all__ and isinstance(value, types.ModuleType) and value.__name__ == f"{__name__}.{name}": |
There was a problem hiding this comment.
🟡 Warning · Api — Sub-module unreachable via parent attribute path
Suppressing this assignment makes isaaclab.utils.configclass resolve to the decorator forever. import isaaclab.utils.configclass as m silently binds the function (IMPORT_FROM prefers getattr and only falls back to sys.modules on AttributeError), and dotted access such as isaaclab.utils.configclass._field_module_dir or mock.patch("isaaclab.utils.configclass.X") now raises AttributeError. Either preserve module-valued access (e.g. make the sub-module itself callable) or record this changed behavior with migration guidance in the changelog fragment.
Review pointed out that suppressing the child-module binding kept the decorator reachable but cost the other half: `import isaaclab.utils.configclass as m` and `isaaclab.utils.configclass._field_module_dir` resolved to the decorator and raised AttributeError. Both halves cannot live in one attribute as long as they are two objects, so make them one: give the sub-module a module type that forwards __call__ to the decorator, and have isaaclab.utils always hand out that sub-module. Every import form now works in either order, which was not true before this branch either - resolving the decorator first already made the sub-module unreachable through the parent package.
|
Thanks — both findings are correct, and I've pushed 8b48c86 to address them. The finding reproduces. With the first version, on the
But module-valued access was already order-dependent before this PR. Same matrix on unpatched
Fix: make them one object, as @isaaclab-review-bot suggested.
No behaviour is removed relative to One documented consequence worth a maintainer's eye: |
|
Some history that may be useful for triage: this is the second time the collision has been paid for. #5647 ("Fix lazy import for configclass and provide upper bound for python", May 2026) rewrote 436 The result is that the collision is still live for anyone outside the repository. This PR fixes the collision at the source instead, so neither the internal rewrite nor a downstream |
|
run-ci |
|
nice!! @pascal-roth do you mind also update all |
PR isaac-sim#5647 rewrote 436 imports across the repository to `from isaaclab.utils.configclass import configclass` to work around the name collision between the sub-module and the decorator it defines. The collision is fixed at the source now, so the workaround is no longer needed: put the tree back on the shorter, documented `from isaaclab.utils import configclass`, including the eight documentation pages and the skills examples that PR rewrote. Mechanical change - the only edits are the import line itself and the import sorting that follows from it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Pushed 9951152, which does the follow-up I offered above: the tree is back on
Verification: This commit is independent of the fix itself. If you would rather land the fix alone and do the |
|
run-ci |
Description
isaaclab/utils/__init__.pyattaches its exports lazily from__init__.pyi. One of those exports,configclass, has the same name as the sub-module that defines it,isaaclab.utils.configclass.Whenever that sub-module is imported, the import machinery binds the module object onto
isaaclab.utils, which shadows the lazily attached decorator. From that point on:Which object the name resolves to depends only on which of the two was imported first, so downstream
code breaks without ever importing the sub-module itself. Importing
isaaclab_rl,isaaclab_newton,isaaclab_contriborisaaclab_teleopis enough, since they all usefrom isaaclab.utils.configclass import configclassinternally:configclassis a documented export ofisaaclab.utils—docs/source/api/lab/isaaclab.utils.rstlists it under the module's Functions rubric — and this is the import form Isaac Lab 1.x/2.x code
uses, so downstream projects hit this while porting to 3.0 and have to rewrite every
configclassimport.
lazy_loaderguards against this collision, but only when the attribute happens to beresolved before the sub-module is imported, so the mitigation is order-dependent.
The mirror image is broken today too: resolving the decorator first makes
lazy_loaderwrite itinto the package
__dict__, after which the sub-module is unreachable as an attribute ofisaaclab.utils—import isaaclab.utils.configclass as mbinds the function, andisaaclab.utils.configclass._field_module_dirraisesAttributeError. One attribute slot cannothold two objects, so whichever import runs first wins and the other form breaks.
This PR makes them one object.
isaaclab.utils.configclassgets aModuleTypesubclass thatforwards
__call__to the decorator, andisaaclab.utils.__getattr__always hands out thatsub-module rather than letting
lazy_loadercache the bare function. The attribute is then the sameobject regardless of import order, and it is both a module and callable, so the decorator, aliased
imports, dotted attribute access,
from isaaclab.utils.configclass import ...,importlib.import_module,mock.patchandstring_to_callableall work in either order. Nothingthat works on
developstops working, so no migration is required.configclassis currently the only name in the repository where a.pyistub export collides with asibling sub-module of the same name, so nothing else changes behaviour.
Fixes # (issue)
Type of change
Release backport
developChecklist
pre-commitchecks with./isaaclab.sh --formatsource/<pkg>/changelog.d/for every touched packageCONTRIBUTORS.mdor my name already exists there