Track mutations made through dict.values() and dict.items() on state vars

Merged
MA
7d ago
+273-5

All Submissions:

  • Have you followed the guidelines stated in CONTRIBUTING.md file?
  • Have you checked to ensure there aren't any other open Pull Requests for the desired changed?

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Changes To Core Features:

  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you written new tests for your core changes, as applicable?
  • Have you successfully ran tests with your changes locally?

Description

closes #7477

MutableProxy only re-wrapped nested mutables reached through __getitem__, __iter__, get() and setdefault(). The native dict.values() / dict.items() views handed out the nested dicts unwrapped, so a loop like

for item in self.inventory.values():
    item["stock"] -= 1

changed the backend value without marking the var dirty: the rendered value and any cached var over it stayed stale. The gap dates to the MutableProxy rewrite in #1748 (v0.2.8); before that, the eager ReflexDict conversion did mark the var dirty but rebuilt the container on each write, so only the first mutation in such a loop landed.

Fix. A proxied dict now serves values() and items() as collections.abc views that read each value back through the proxy. Mutable values come out wrapped under their key, so they mark the field dirty and refresh by that key in an async with context. The views keep native dict-view semantics: live, sized, reversible, searchable, usable in set operations, and isinstance of ValuesView / ItemsView. Pure-Python mappings already worked because their Mapping mixin methods get rebound to the proxy; this closes the gap for the C-level dict methods, guarded so a non-dict's values / items attribute is left alone.

Both views iterate through one shared generator that wraps straight from the wrapped dict, so they run at the existing per-element proxy fast-path cost rather than the ABC default of proxy[key] per element:

Path (10k-entry dict, per element)scalar valuesnested dicts
native view, before19 ns19 ns
proxied view, after54 ns~1.5 µs (proxy construction, same as d[k])

Tests. Three new tests in tests/units/istate/test_proxy.py fail before the fix and pass after it: dirty marking through both views (plus the live / reversed / membership semantics), the issue's cached-var scenario, and key-path refresh for proxies read through views in an async context.


🤖 Generated with Claude Code

https://claude.ai/code/session_01MzM34QeM3FSHoTgvC7hs1x


Generated by Claude Code