Fixes hydra to discover presets in deeply nested dictionary#5029
Fixes hydra to discover presets in deeply nested dictionary#5029ooctipus merged 8 commits intoisaac-sim:developfrom
Conversation
Greptile SummaryThis PR fixes
Confidence Score: 2/5
Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[collect_presets cfg] --> B{Is cfg PresetCfg?}
B -->|Yes| C[Extract fields as preset dict]
B -->|No| D[Iterate dir cfg attributes]
D --> E{Has __dataclass_fields__?}
E -->|Yes, PresetCfg| F[Extract preset dict + recurse alternatives]
E -->|Yes, regular| G[Recurse collect_presets]
E -->|No| H{isinstance dict?}
H -->|Yes| I["_collect_from_dict (NEW)"]
H -->|No| J[Skip]
I --> K{dict_val has __dataclass_fields__?}
K -->|Yes| L[Recurse collect_presets]
K -->|No| M{dict_val is dict?}
M -->|Yes| N["Recurse _collect_from_dict (NEW)"]
M -->|No| O[Skip]
style I fill:#90EE90
style N fill:#90EE90
style H fill:#90EE90
|
aa2fc28 to
7c0fafe
Compare
…sts pending unification with PR isaac-sim#5029
…sts pending unification with PR isaac-sim#5029
6a34683 to
cd8f347
Compare
There was a problem hiding this comment.
🤖 Isaac Lab Review Bot
Summary
This PR fixes collect_presets and resolve_preset_defaults to discover and resolve PresetCfg instances inside deeply nested dicts (e.g., EventTerm.params.terms.*.params). It also introduces _collect_fields to pick up non-dataclass class attributes (like ObjectCfg.shapes) that the old __dataclass_fields__ loop missed, and consolidates the duplicated preset resolution logic from parse_cfg.py into hydra.py. The approach is correct and the test coverage is excellent.
Architecture Impact
parse_cfg.py: Removes 4 functions (_is_preset_cfg,_is_old_style_preset,_resolve_presets_to_default,apply_named_preset) and replaces the resolution call inparse_env_cfgwithresolve_preset_defaultsfromhydra.py. These removed functions were only used internally inparse_cfg.pyand byenv_test_utils.py, both of which are updated in this PR. No external callers are affected.env_test_utils.py: Replacesapply_named_presetwithapply_overrides+collect_presetsfromhydra.py. This is functionally equivalent and now supports deep nesting.dexsuite_env_cfg.py: Addsnewton = cubetoObjectCfgso thatpresets=newtonhas a matching alternative. Self-contained change.- Old-style preset support removed: The
_is_old_style_presethandler (for configs with apresets: dictattribute) is removed. No configs in the current codebase use this pattern, so this is safe.
Implementation Verdict
Minor fixes needed — Correct approach, 3 issues to address (1 CHANGELOG/versioning problem, 1 semantic correctness issue, 1 missing version bump).
Test Coverage
✅ Excellent. The PR adds 379 lines of tests covering:
- Deep nested dict preset discovery (
test_collect_presets_deep_nested_dicts) - Deep nested dict resolution (
test_resolve_preset_defaults_deep_nested_dicts) - Global preset application to deeply nested presets (
test_deep_nested_dict_global_preset) - Path-specific selection (
test_deep_nested_dict_path_selection) - Mixed global + path overrides (
test_deep_nested_dict_mixed_global_and_path) _collect_fieldsclass-vs-instance priority and dynamic class attrspreset()factory function edge cases- Error handling in
apply_overrides parse_overridesedge cases_parse_valtype conversion- Idempotency of
resolve_preset_defaults
All tests directly exercise the specific code paths that were broken. The deep-nesting tests would fail on the develop branch (before the fix) and pass after. This is thorough, well-structured test coverage.
CI Status
- ✅ pre-commit: passed
- ✅ labeler: passed
- ✅ Build Base Docker Image: passed
- ⏳ isaaclab_tasks [1/3], [2/3], [3/3]: pending (awaiting GPU runners)
- ⏳ license-check: pending
Branch Status
develop. The CHANGELOG will likely conflict since both the PR and develop added entries at the top. The PR currently uses version 1.5.15 but develop already has a 1.5.15 entry (semantic segmentation additions). After rebasing, the new entry should be 1.5.16 and config/extension.toml should be bumped to match.
Findings
🟡 Warning: CHANGELOG.rst — Version number collision and missing extension.toml bump
The PR branch was based before develop's 1.5.15 entry was added. The new CHANGELOG entry uses version 1.5.15, which collides with the existing 1.5.15 on develop. After rebase, this should be 1.5.16, and source/isaaclab_tasks/config/extension.toml must be updated from 1.5.15 to 1.5.16 to match. The PR checklist claims the extension.toml was updated but the file is not in the changeset.
🟡 Warning: hydra.py:_collect_fields — cls_val is not None sentinel issue
The is not None check means that if a class-level preset value is legitimately None (e.g., after MyCfg.some_preset = None), the function incorrectly falls back to the instance attribute. While unusual in current usage, this breaks the stated contract of "class-level values take priority." Using a sentinel would be more correct.
🔵 Improvement: hydra.py — _resolve_from_dict placement inconsistency
_collect_from_dict and _collect_fields are nested inside collect_presets, but the analogous _resolve_from_dict is at module level. Both approaches work, but the inconsistency will confuse the next person reading this code. Either nest _resolve_from_dict inside resolve_preset_defaults (matching the pattern), or pull all helpers to module level.
| d = {} | ||
| for fn in preset_obj.__dataclass_fields__: | ||
| cls_val = getattr(cls, fn, None) | ||
| d[fn] = cls_val if cls_val is not None else getattr(preset_obj, fn) |
There was a problem hiding this comment.
cls_val is not None silently falls back to the instance attr when the class-level value IS None. This breaks the stated contract ("class-level values take priority") for the edge case where someone sets MyCfg.some_preset = None at runtime. A sentinel makes the intent explicit:
| d[fn] = cls_val if cls_val is not None else getattr(preset_obj, fn) | |
| _MISSING = object() | |
| for fn in preset_obj.__dataclass_fields__: | |
| cls_val = getattr(cls, fn, _MISSING) | |
| d[fn] = cls_val if cls_val is not _MISSING else getattr(preset_obj, fn) |
| def _resolve_from_dict(d: dict) -> None: | ||
| """Recursively resolve preset defaults in dict values, including nested dicts.""" | ||
| for dict_key, dict_val in d.items(): | ||
| if isinstance(dict_val, PresetCfg) and hasattr(dict_val, "__dataclass_fields__"): |
There was a problem hiding this comment.
Style inconsistency: _collect_from_dict and _collect_fields are nested inside collect_presets, but _resolve_from_dict is at module level. For consistency, either nest this inside resolve_preset_defaults or pull all helpers to module level.
There was a problem hiding this comment.
🤖 Isaac Lab Review Bot
Summary
This PR fixes collect_presets and resolve_preset_defaults to recursively traverse nested dicts (e.g. EventTerm.params.terms.*.params), discovering PresetCfg instances that were previously invisible. It also consolidates duplicate preset-resolution logic from parse_cfg.py into hydra.py, introduces a _collect_fields helper that prefers class-level attrs over instance attrs (for robot-specific module mutations), and adds a newton preset to dexsuite ObjectCfg.
Architecture Impact
parse_cfg.py: Removes_resolve_presets_to_default,_is_preset_cfg,_is_old_style_preset, andapply_named_preset— all internal/private. The public-facingparse_env_cfgnow delegates tohydra.resolve_preset_defaultsinstead of the local duplicate. No external callers of the removed functions exist.env_test_utils.py: Replacesapply_named_presetusage withapply_overrides+collect_presetsfromhydra.py, which is the canonical path.hydra.py: New_resolve_from_dictmodule-level function and two closures (_collect_fields,_collect_from_dict) insidecollect_presets. Cross-module impact is minimal — changes are self-contained within the hydra/preset subsystem.
Implementation Verdict
Minor fixes needed — Core logic is correct and well-tested. Two issues to address: CHANGELOG version/date ordering and missing extension.toml version bump.
Test Coverage
Excellent. 19 new tests cover:
- Deep nested dict preset discovery (
test_collect_presets_deep_nested_dicts) - Deep nested dict resolution (
test_resolve_preset_defaults_deep_nested_dicts) - Full override pipeline with deep nesting (auto-default, global preset, path selection, mixed)
_collect_fieldsclass-vs-instance priority and dynamic class attrspreset()factory edge casesapply_overrideserror handling (unknown group, unknown name, conflicts)parse_overridesedge cases_parse_valtype conversion- Scalar override within preset path
resolve_preset_defaultsidempotency
Tests directly exercise the exact code paths that were broken and would catch regressions.
CI Status
- ✅ pre-commit: passed
- ⏳ isaaclab_tasks [1/3, 2/3, 3/3]: in progress
- ⏳ environments_training: in progress
- ⏳ license-check, Build Latest Docs, Build cuRobo Docker Image: in progress
- All other checks: passed or skipped (not applicable)
Findings
🟡 Warning: CHANGELOG.rst — Version 1.5.16 has date 2026-03-24, older than 1.5.15 (2026-03-25). A newer version should have a newer or equal date. Also, the 1.5.14 entry (golden images update) was dropped — this existing entry from develop should be preserved.
🟡 Warning: extension.toml — Version not bumped. CHANGELOG adds 1.5.16 but config/extension.toml still says 1.5.15.
🔵 Improvement: hydra.py _collect_fields — None sentinel conflates "no class attr" with "class attr is None". If a class-level attr is explicitly set to None at runtime (overriding a non-None dataclass default), the fallback to the instance attr silently ignores the class mutation. Using a sentinel object instead of None would be more correct. Low-risk in practice since class-level mutations to None are unusual for presets.
| --------- | ||
|
|
||
| 1.5.15 (2026-03-25) | ||
| 1.5.16 (2026-03-24) |
There was a problem hiding this comment.
Two issues:
- Date
2026-03-24is older than the 1.5.15 entry below (2026-03-25). A higher version must have a newer-or-equal date. - The 1.5.14 entry (golden images update) was dropped from
develop. It should be preserved between 1.5.15 and 1.5.13. config/extension.tomlstill saysversion = "1.5.15"— bump it to1.5.16.
| 1.5.16 (2026-03-24) | |
| 1.5.16 (2026-03-26) |
| cls = type(preset_obj) | ||
| d = {} | ||
| for fn in preset_obj.__dataclass_fields__: | ||
| cls_val = getattr(cls, fn, None) |
There was a problem hiding this comment.
Using None as the sentinel for "class has no such attr" means an intentional cls.field = None mutation (overriding a non-None default) silently falls through to the instance value. A dedicated sentinel avoids this:
| cls_val = getattr(cls, fn, None) | |
| _MISSING = object() | |
| cls = type(preset_obj) | |
| d = {} | |
| for fn in preset_obj.__dataclass_fields__: | |
| cls_val = getattr(cls, fn, _MISSING) | |
| d[fn] = cls_val if cls_val is not _MISSING else getattr(preset_obj, fn) |
Low-risk today, but prevents a subtle bug if anyone later sets a preset alternative to None.
02c40a3 to
aad1884
Compare
…m#5029) # Description This PR robustify hydra to discover presets in deeply nested dictionary and provide test cases to verify the behavior. Before deeply nested preset are undiscovered ## Type of change <!-- As you go through the list, delete the ones that are not applicable. --> - Bug fix (non-breaking change which fixes an issue) ## Screenshots Please attach before and after screenshots of the change if applicable. <!-- Example: | Before | After | | ------ | ----- | | _gif/png before_ | _gif/png after_ | To upload images to a PR -- simply drag and drop an image while in edit mode and it should upload the image directly. You can then paste that source into the above before/after sections. --> ## Checklist - [x] I have read and understood the [contribution guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html) - [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 <!-- As you go through the checklist above, you can mark something as done by putting an x character in it For example, - [x] I have done this task - [ ] I have not done this task --> --------- Co-authored-by: AntoineRichard <antoiner@nvidia.com>
…m#5029) # Description This PR robustify hydra to discover presets in deeply nested dictionary and provide test cases to verify the behavior. Before deeply nested preset are undiscovered ## Type of change <!-- As you go through the list, delete the ones that are not applicable. --> - Bug fix (non-breaking change which fixes an issue) ## Screenshots Please attach before and after screenshots of the change if applicable. <!-- Example: | Before | After | | ------ | ----- | | _gif/png before_ | _gif/png after_ | To upload images to a PR -- simply drag and drop an image while in edit mode and it should upload the image directly. You can then paste that source into the above before/after sections. --> ## Checklist - [x] I have read and understood the [contribution guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html) - [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 <!-- As you go through the checklist above, you can mark something as done by putting an x character in it For example, - [x] I have done this task - [ ] I have not done this task --> --------- Co-authored-by: AntoineRichard <antoiner@nvidia.com>
Description
This PR robustify hydra to discover presets in deeply nested dictionary
and provide test cases to verify the behavior.
Before deeply nested preset are undiscovered
Type of change
Screenshots
Please attach before and after screenshots of the change if applicable.
Checklist
pre-commitchecks with./isaaclab.sh --formatconfig/extension.tomlfileCONTRIBUTORS.mdor my name already exists there