Repository navigation
Accept a covariant Mapping of elements in ui.dictionary and .batch - #10653
kirangadhave merged 2 commits into
Conversation
|
I have read the CLA Document and I hereby sign the CLA |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
All contributors have signed the CLA ✍️ ✅ |
There was a problem hiding this comment.
Pull request overview
This PR addresses a static typing limitation in marimo’s UI container APIs by widening elements inputs from invariant dict[...] to covariant Mapping[...], allowing type checkers to accept dictionaries of UIElement subclasses (e.g. dict[str, ui.checkbox]) without casts, while preserving existing runtime behavior by cloning into a fresh dict.
Changes:
- Widened
elementsparameter types toMapping[...]inui.dictionary,ui.batch, and the sharedvalidate_and_clonehelper (still returning a concretedict). - Added regression tests ensuring read-only mappings (e.g.
MappingProxyType) are accepted and cloned correctly.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| tests/_plugins/ui/_impl/test_dictionary.py | Adds coverage that ui.dictionary accepts and clones a read-only Mapping. |
| tests/_plugins/ui/_impl/test_batch.py | Adds coverage that validate_and_clone accepts and clones a read-only Mapping. |
| marimo/_plugins/ui/_impl/dictionary.py | Widens dictionary.__init__ elements typing to Mapping[...]. |
| marimo/_plugins/ui/_impl/batch.py | Widens validate_and_clone and batch.__init__ elements typing to Mapping[...]. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| self, | ||
| elements: dict[str, UIElement[Any, Any]], | ||
| elements: Mapping[str, UIElement[Any, Any]], | ||
| *, | ||
| label: str = "", | ||
| on_change: Callable[[dict[str, object]], None] | None = None, |
| def __init__( | ||
| self, | ||
| html: Html, | ||
| elements: dict[str, UIElement[Any, Any]], | ||
| elements: Mapping[str, UIElement[Any, Any]], | ||
| on_change: Callable[[dict[str, object]], None] | None = None, |
kirangadhave
left a comment
There was a problem hiding this comment.
Thanks @winklemad, I have a few change requests below. Also please rebase the branch from origin/main
| md("Example {thing}").batch(thing={"key": "value"}) # type: ignore | ||
|
|
||
|
|
||
| def test_validate_and_clone_accepts_readonly_mapping() -> None: |
There was a problem hiding this comment.
this test will run without any changes to batch.py because Python doesn't enforce annotations on runtime.
Typing only change requires a type-level test. See tests/_ast/test_app_typing.py for reference.
| assert dictionary.value == {"a": 2, "b": "goodbye", "c": 1} | ||
|
|
||
|
|
||
| def test_dictionary_accepts_readonly_mapping() -> None: |
There was a problem hiding this comment.
There was a problem hiding this comment.
docstring still refers to dict
| elements (Mapping): A dict of the batched elements (clones of the originals). |
There was a problem hiding this comment.
docstring still refers to dict
| elements (Mapping): A dict of the wrapped elements (clones of the originals). |
| def test_dictionary_accepts_readonly_mapping() -> None: | ||
| # `elements` is typed as a covariant Mapping, so any read-only mapping of | ||
| # UI elements (not just a plain dict) must be accepted and cloned. | ||
| from types import MappingProxyType |
There was a problem hiding this comment.
import at module level similar to other imports
| from types import MappingProxyType | ||
|
|
||
| from marimo._plugins.ui._impl.batch import validate_and_clone |
There was a problem hiding this comment.
import at module level similar to other imports
| md("Example {thing}").batch(thing={"key": "value"}) # type: ignore | ||
|
|
||
|
|
||
| def test_validate_and_clone_accepts_readonly_mapping() -> None: |
There was a problem hiding this comment.
nit: move the test to bottom. it separates two similar tests.
`dict` is invariant, so annotating the `elements` parameter as `dict[str, UIElement[Any, Any]]` made type checkers reject valid subclass dictionaries such as `dict[str, mo.ui.checkbox]`, forcing users to add casts. The constructors only read `elements` (they clone it into a fresh dict via `validate_and_clone`), so widen the parameter to a read-only `Mapping` in `ui.dictionary`, the internal `batch` element, and `validate_and_clone`. Runtime behaviour is unchanged. Add regression tests that build from a read-only `MappingProxyType`.
- Add basedpyright type-level tests (TestDictionaryTyping) that fail on the old invariant dict[...] param and pass on the covariant Mapping[...] one, so the typing change is actually guarded (runtime tests can't enforce annotations) - Update the batch/dictionary docstrings that still referred to dict - Hoist the MappingProxyType / validate_and_clone imports to module scope - Move the validate_and_clone test next to the other batch tests
1d71f14 to
ca31563
Compare
|
Thanks @kirangadhave — addressed all of these in ca31563:
Let me know if you'd like anything else. |
This pull request was authored by a coding agent.
📝 Summary
Closes #10631
dictis invariant, so typing theelementsparameter asdict[str, UIElement[Any, Any]]makes static type checkers (mypy, Pyright/Pylance) reject valid subclass dictionaries — e.g.dict[str, mo.ui.checkbox]— even though they are perfectly fine at runtime. Users have to work around it withcast(dict[str, Any], elements).Reproduction under mypy (marimo's CI type checker) on
main:Fix
The affected constructors only read
elements— they immediately clone it into a freshdictviavalidate_and_clone— so the parameter can be a read-only, covariantMapping. Widened in:ui.dictionary.__init__batchelement__init__validate_and_clone(the shared helper, per the maintainer's note on the issue)validate_and_clonestill returns adict, and the internal_batch_base(which stores and mutates the mapping) keeps itsdicttype since it always receives the cloned dict.ui.arrayalready uses the covariantSequence, so it needs no change. Runtime behaviour is unchanged.Verification
mypy marimoreports no new errors in the changed files.ui.dictionaryand callvalidate_and_clonefrom a read-onlyMappingProxyType(a non-dictMapping), which fails if the parameter is ever narrowed back todict.test_dictionary.py+test_batch.pypass (11 tests);ruff format/ruff checkclean.📋 Pre-Review Checklist
bug, contribution invited by a maintainer).✅ Merge Checklist