From 5da3a37108d0d09b973ee00b8deb464ed7203994 Mon Sep 17 00:00:00 2001 From: Thor Whalen <1906276+thorwhalen@users.noreply.github.com> Date: Tue, 22 Sep 2026 15:25:52 +0000 Subject: [PATCH] fix: don't prefill or type widgets from i2's NotSet signature default The legacy spec builders (`infer_type`, `get_func_args_specs`, `page_funcs.*get_func_args_specs`, `get_func_elements_commands`) read defaults through the new `util.signature_defaults(sig)`, which drops params whose default is i2's `NotSet` sentinel. So a signature with NotSet defaults (as a re-landed i2mint/i2#88 FuncFactory would show) gives the same widgets as the plain one. No-op for current signatures. The `mk_app` path goes through front, fixed separately (i2mint/front#34). Uses `i2.is_not_set` when available, else falls back on `i2.deco.NotSet`. Refs i2mint/i2#48 Co-Authored-By: Claude Opus 5 --- streamlitfront/base.py | 16 +++++--- streamlitfront/page_funcs.py | 22 +++++++---- streamlitfront/tests/test_not_set_defaults.py | 39 +++++++++++++++++++ streamlitfront/util.py | 23 +++++++++++ 4 files changed, 88 insertions(+), 12 deletions(-) create mode 100644 streamlitfront/tests/test_not_set_defaults.py diff --git a/streamlitfront/base.py b/streamlitfront/base.py index 16292e3..ca45a31 100644 --- a/streamlitfront/base.py +++ b/streamlitfront/base.py @@ -14,7 +14,11 @@ from streamlitfront.spec_maker import SpecMaker from streamlitfront.session_state import get_state, _SessionState -from streamlitfront.util import func_name, build_element_factory +from streamlitfront.util import ( + func_name, + build_element_factory, + signature_defaults, +) # --------------------- types/protocols/interfaces -------------------------------------- @@ -132,10 +136,11 @@ def dispatch_funcs( def infer_type(sig, name): + defaults = signature_defaults(sig) # i2's NotSet sentinel is not a default if name in sig.annotations: return sig.annotations[name] - elif name in sig.defaults: - dflt = sig.defaults[name] + elif name in defaults: + dflt = defaults[name] if dflt is not None: return type(dflt) else: @@ -189,6 +194,7 @@ def get_func_args_specs( element_factory_for_annot or _get_dflt_element_factory_for_annot() ) sig = Sig(func) + defaults = signature_defaults(sig) # i2's NotSet sentinel is not a default func_args_specs = {name: {} for name in sig.names} for name in sig.names: d = func_args_specs[name] @@ -200,8 +206,8 @@ def get_func_args_specs( missing, dflt_element_factory, ) - if name in sig.defaults: - dflt = sig.defaults[name] + if name in defaults: + dflt = defaults[name] if dflt is not None: # TODO: type-to-element conditions must be in configs if isinstance(dflt, (list, tuple, set)): diff --git a/streamlitfront/page_funcs.py b/streamlitfront/page_funcs.py index 3fc882d..6fec3ac 100644 --- a/streamlitfront/page_funcs.py +++ b/streamlitfront/page_funcs.py @@ -10,7 +10,12 @@ BasePageFunc, _get_dflt_element_factory_for_annot, ) -from streamlitfront.util import build_factory, Command, NodeGetter +from streamlitfront.util import ( + build_factory, + Command, + NodeGetter, + signature_defaults, +) # TODO: Extract the page setup (view_title, etc.) and make it injectable. @@ -30,6 +35,7 @@ def get_func_elements_commands( element_factory_for_annot or _get_dflt_element_factory_for_annot() ) sig = Sig(func) + defaults = signature_defaults(sig) # i2's NotSet sentinel is not a default NodeGetter(state["views"]) func_args_specs = {name: {} for name in sig.names} @@ -46,8 +52,8 @@ def get_func_elements_commands( dflt_element_factory, ) - if name in sig.defaults: - dflt = sig.defaults[name] + if name in defaults: + dflt = defaults[name] if dflt is not None: # TODO: type-to-element conditions must be in configs if isinstance(dflt, (list, tuple, set)): @@ -68,6 +74,7 @@ def get_func_args_specs( element_factory_for_annot or _get_dflt_element_factory_for_annot() ) sig = Sig(func) + defaults = signature_defaults(sig) # i2's NotSet sentinel is not a default func_args_specs = {name: {} for name in sig.names} for name in sig.names: d = func_args_specs[name] @@ -79,8 +86,8 @@ def get_func_args_specs( missing, dflt_element_factory, ) - if name in sig.defaults: - dflt = sig.defaults[name] + if name in defaults: + dflt = defaults[name] if dflt is not None: # TODO: type-to-element conditions must be in configs if isinstance(dflt, (list, tuple, set)): @@ -127,6 +134,7 @@ def special_get_func_args_specs( element_factory_for_annot or _get_dflt_element_factory_for_annot() ) sig = Sig(func) + defaults = signature_defaults(sig) # i2's NotSet sentinel is not a default func_args_specs = {name: {} for name in sig.names} for name in sig.names: d = func_args_specs[name] @@ -138,8 +146,8 @@ def special_get_func_args_specs( missing, dflt_element_factory, ) - if name in sig.defaults: - dflt = sig.defaults[name] + if name in defaults: + dflt = defaults[name] if dflt is not None: # TODO: type-to-element conditions must be in configs if isinstance(dflt, (list, tuple, set)): diff --git a/streamlitfront/tests/test_not_set_defaults.py b/streamlitfront/tests/test_not_set_defaults.py new file mode 100644 index 0000000..72cd6f2 --- /dev/null +++ b/streamlitfront/tests/test_not_set_defaults.py @@ -0,0 +1,39 @@ +"""``i2``'s ``NotSet`` sentinel in a signature means "required / no default". + +See i2mint/i2#48: once ``i2.FuncFactory`` shows ``NotSet`` defaults, widgets must not +be prefilled with the sentinel, nor typed after it. These tests use +``i2.deco.NotSet`` directly, so they pass with any i2 version. +""" + +from i2 import Sig +from i2.deco import NotSet + +from streamlitfront import base, page_funcs + + +def foo(a: int, b, c: str = "hi", d=2): + return a, b, c, d + + +# ``foo`` with ``NotSet`` defaults, as a re-landed i2#88 ``FuncFactory`` would show. +# Defined separately since ``Sig.__call__`` sets ``__signature__`` in place. +def foo_with_not_set(a: int = NotSet, b=NotSet, c: str = "hi", d=2): + return a, b, c, d + + +def test_infer_type_ignores_not_set(): + for name in "abcd": + assert base.infer_type(Sig(foo_with_not_set), name) is base.infer_type( + Sig(foo), name + ) + + +def test_func_args_specs_are_not_prefilled_with_not_set(): + for get_specs in ( + base.get_func_args_specs, + page_funcs.get_func_args_specs, + page_funcs.special_get_func_args_specs, + ): + assert get_specs(foo_with_not_set) == get_specs(foo) + _, factory_kwargs = get_specs(foo_with_not_set)["a"]["element_factory"] + assert "value" not in factory_kwargs diff --git a/streamlitfront/util.py b/streamlitfront/util.py index 203bb6d..57c1381 100644 --- a/streamlitfront/util.py +++ b/streamlitfront/util.py @@ -6,6 +6,29 @@ from i2.signatures import Sig, name_of_obj from i2._deprecated import Command as _Command +try: + from i2 import is_not_set +except ImportError: # older i2: same sentinel, not exported from the root yet + from i2.deco import NotSet as _NotSet + + def is_not_set(x) -> bool: + """Return True iff ``x`` is ``i2``'s ``NotSet`` sentinel.""" + return x is _NotSet + + +def signature_defaults(sig) -> dict: + """Return ``sig.defaults`` without the params whose default is ``i2``'s ``NotSet``. + + ``NotSet`` in a signature means "no value given", not a real default, so a widget + must not be prefilled with it, nor typed after it. + + >>> from i2.deco import NotSet + >>> def foo(a, b=NotSet, c=3): ... + >>> signature_defaults(Sig(foo)) + {'c': 3} + """ + return {k: v for k, v in sig.defaults.items() if not is_not_set(v)} + # TODO: Consider using functools.partial (or subclass thereof) instead of Command class Command(_Command):