Skip to content

fix: narrow KeyFirstEnvironment to hide only ruamel-specific attrs#794

Closed
oboehmer wants to merge 8 commits intomainfrom
fix/ruaml-default-fallback
Closed

fix: narrow KeyFirstEnvironment to hide only ruamel-specific attrs#794
oboehmer wants to merge 8 commits intomainfrom
fix/ruaml-default-fallback

Conversation

@oboehmer
Copy link
Copy Markdown
Collaborator

@oboehmer oboehmer commented Apr 21, 2026

Description

Fix KeyFirstEnvironment to match the contract that CommentedMap/CommentedSeq must behave identically to plain dict/list under standard Jinja2. The original implementation resolved ALL dot-access on Mappings key-first, which broke dict methods like .items(), .get(), .keys(), .values() when those names also existed as YAML keys. The fix narrows the intercept to only ruamel-specific metadata attributes (blocklist + yaml_* prefix), letting everything else follow standard Jinja2 getattr→getitem resolution.

Templates that use collision keys like items or keys must use bracket notation ({{ obj['items'] }}), which matches standard dict behavior.

Closes

Related Issue(s)

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Refactoring / Technical debt (internal improvements with no user-facing changes)
  • Documentation update
  • Chore (build process, CI, tooling, dependencies)
  • Other (please describe):

Test Framework Affected

  • PyATS
  • Robot Framework
  • Both
  • N/A (not test-framework specific)

Network as Code (NaC) Architecture Affected

  • ACI (APIC)
  • NDO (Nexus Dashboard Orchestrator)
  • NDFC / VXLAN-EVPN (Nexus Dashboard Fabric Controller)
  • Catalyst SD-WAN (SDWAN Manager / vManage)
  • Catalyst Center (DNA Center)
  • ISE (Identity Services Engine)
  • FMC (Firepower Management Center)
  • Meraki (Cloud-managed)
  • NX-OS (Nexus Direct-to-Device)
  • IOS-XE (Direct-to-Device)
  • IOS-XR (Direct-to-Device)
  • Hyperfabric
  • All architectures
  • N/A (architecture-agnostic)

Platform Tested

nac-test supports macOS and Linux only

  • macOS (version tested: Darwin)
  • Linux (distro/version tested: )

Key Changes

  • robot_writer.py: Rewrite KeyFirstEnvironment.getattr() — blocklist-first resolution with separate _RUAMEL_MAP_ATTRS / _RUAMEL_SEQ_ATTRS constants; add CommentedSeq handling
  • test_robot_writer_environment.py (new): Dedicated unit tests for KeyFirstEnvironment — 6 test classes covering contract equivalence, key precedence, missing-key behavior, method preservation, attr-getter filters, and collision+method interaction
  • test_robot_writer.py: Removed old TestKeyFirstEnvironment (superseded by dedicated test file)
  • Integration fixtures: Expanded with test_methods.robot (dict method calls), test_selectattr.robot (selectattr/rejectattr/map); collision key templates use bracket notation
  • Integration assertions: re.search() with msg= anchors for precise rendered-output validation

Testing Done

  • Unit tests added/updated
  • Integration tests performed
  • Manual testing performed:
    • PyATS tests executed successfully
    • Robot Framework tests executed successfully
    • D2D/SSH tests executed successfully (if applicable)
    • HTML reports generated correctly
  • All existing tests pass (pytest / pre-commit run -a)

Test Commands Used

uv run pytest tests/unit/robot/test_robot_writer_environment.py tests/unit/robot/test_robot_writer.py tests/integration/test_cli_rendering.py -v
pre-commit run --all-files

Checklist

  • Code follows project style guidelines (pre-commit run -a passes)
  • Self-review of code completed
  • Code is commented where necessary (especially complex logic)
  • Documentation updated (if applicable)
  • No new warnings introduced
  • Changes work on both macOS and Linux
  • CHANGELOG.md updated (if applicable)

Additional Notes

This supersedes the narrow fix from #789 with a principled approach: instead of key-first resolution for everything, only ruamel-specific metadata attributes are intercepted. Dict methods resolve normally (matching plain dict behavior), and templates must use bracket notation for keys that collide with dict method names.

Ensure Mapping attribute access prefers keys and returns Undefined on missing keys,
preventing ruamel attribute collisions (e.g. .tag) from yielding None.

Update integration fixture to use global defaults for fallback values, run the
case via --robot for exit-code validation, and add unit tests for the custom
Jinja environment behavior.
@oboehmer oboehmer added the bug Something isn't working label Apr 21, 2026
The original key-first resolution was too broad — it resolved ALL dot-access
on Mappings key-first, causing `{{ obj.items }}` to return a key value instead
of the dict method. This broke the contract that CommentedMap/CommentedSeq
must behave identically to plain dict/list under standard Jinja2.

Narrow the intercept to only ruamel-specific blocklist attrs (_RUAMEL_MAP_ATTRS,
_RUAMEL_SEQ_ATTRS, yaml_* prefix). Everything else falls through to standard
Jinja2 getattr→getitem resolution, preserving .items(), .get(), .keys(),
.values() as callable methods.

Implementation:
- robot_writer.py: Reorder getattr() — check blocklist first, then key-in-obj
  inside that branch; add CommentedSeq handling with separate blocklist
- Add blocklist constants with documented rationale

Tests:
- Split KeyFirstEnvironment tests into dedicated test_robot_writer_environment.py
  (was mixed into test_robot_writer.py)
- 6 test classes covering: contract equivalence, key precedence, missing-key
  behavior, method preservation, attr-getter filters, collision+method interaction
- Integration fixtures expanded: test_methods.robot (dict method calls),
  test_selectattr.robot (selectattr/rejectattr/map with collision keys)
- Integration assertions use re.search() with msg= anchors for precise matching
- Collision key templates use bracket notation (child['items']) per contract
@oboehmer oboehmer changed the title fix: Jinja missing keys are undefined fix: narrow KeyFirstEnvironment to hide only ruamel-specific attrs Apr 21, 2026
@oboehmer
Copy link
Copy Markdown
Collaborator Author

closing this in favor of #796

@oboehmer oboehmer closed this Apr 22, 2026
@oboehmer oboehmer deleted the fix/ruaml-default-fallback branch April 23, 2026 06:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Jinja dict key 'tag' renders as None (dict attribute collision)

1 participant