From 28b3e6e8015704feeb7377846473eb9ccea62f17 Mon Sep 17 00:00:00 2001 From: Thor Whalen <1906276+thorwhalen@users.noreply.github.com> Date: Tue, 22 Sep 2026 15:15:51 +0000 Subject: [PATCH 1/2] feat: export NotSet sentinel and is_not_set() helper Additive only: `i2.NotSet` (the existing `i2.deco.NotSet`) and a new `i2.is_not_set(x)` predicate are exported so signature consumers (front, py2http, ...) can recognise the sentinel as "required / no default" before FuncFactory ever puts it in a signature. No existing behaviour changes. Refs #48 Co-Authored-By: Claude Opus 5 --- i2/__init__.py | 2 ++ i2/deco.py | 27 +++++++++++++++++++++++++++ i2/tests/test_deco.py | 17 +++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/i2/__init__.py b/i2/__init__.py index e7ecdf73..b784d57b 100644 --- a/i2/__init__.py +++ b/i2/__init__.py @@ -8,6 +8,8 @@ input_output_decorator, wrap_class_methods_input_and_output, double_up_as_factory, + NotSet, # Sentinel for "no value given" (distinct from None and Parameter.empty) + is_not_set, # Test whether a value (e.g. a signature default) is NotSet ) from i2.signatures import ( diff --git a/i2/deco.py b/i2/deco.py index 4964828c..3e9df0b5 100644 --- a/i2/deco.py +++ b/i2/deco.py @@ -55,6 +55,33 @@ def _not_set_repr(self): NotSet = mk_sentinel("NotSet", repr_=_not_set_repr) +NotSet.__doc__ = """Sentinel meaning "no value was given for this argument". + +It is distinct from ``None`` (which can be a legitimate value) and from +``inspect.Parameter.empty`` (which means "this parameter has no default"). +Code that reads signature defaults should test for it with :func:`is_not_set` +rather than by comparing to a private object. +""" + + +def is_not_set(x) -> bool: + """Return ``True`` if ``x`` is the ``NotSet`` sentinel, and ``False`` otherwise. + + Signature consumers (UI or schema generators, for example) can use it to treat a + ``NotSet`` default like ``inspect.Parameter.empty``, i.e. "required, no default": + + >>> from inspect import Parameter + >>> is_not_set(NotSet) + True + >>> is_not_set(None), is_not_set(Parameter.empty), is_not_set("NotSet") + (False, False, False) + >>> def default_or_empty(param): + ... return Parameter.empty if is_not_set(param.default) else param.default + >>> p = Parameter('x', Parameter.KEYWORD_ONLY, default=NotSet) + >>> default_or_empty(p) is Parameter.empty + True + """ + return x is NotSet # --------------------------------------------------------------------------------------- diff --git a/i2/tests/test_deco.py b/i2/tests/test_deco.py index b7523787..2acafa73 100644 --- a/i2/tests/test_deco.py +++ b/i2/tests/test_deco.py @@ -37,3 +37,20 @@ def test_func_factory_signature_keeps_required_params_required(): # The factory itself can still be called with none to all of the arguments assert factory()(1, 2, "a") == [1, 2, "a", "-"] assert factory(chk_size=3)(1, name="b") == [1, 3, "b", "-"] + + +def test_not_set_is_exported_and_recognisable(): + """``NotSet`` and ``is_not_set`` are public, so signature consumers can recognise + the sentinel without importing a private object (see i2mint/i2#48).""" + import pickle + + import i2 + from i2.deco import NotSet as deco_not_set + + assert i2.NotSet is deco_not_set + assert i2.is_not_set(i2.NotSet) + for other in (None, inspect.Parameter.empty, "NotSet", 0, False, object()): + assert not i2.is_not_set(other) + # Survives pickling as the same object (so identity checks stay valid) + assert pickle.loads(pickle.dumps(i2.NotSet)) is i2.NotSet + assert repr(i2.NotSet) == "NotSet" From d260142ea3dc70f8b25943910a2bc1f78ea1bcf4 Mon Sep 17 00:00:00 2001 From: Thor Whalen <1906276+thorwhalen@users.noreply.github.com> Date: Tue, 22 Sep 2026 15:17:49 +0000 Subject: [PATCH 2/2] docs: note that NotSet is falsy in its docstring (review follow-up) Co-Authored-By: Claude Opus 5 --- i2/deco.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/i2/deco.py b/i2/deco.py index 3e9df0b5..8b1e6031 100644 --- a/i2/deco.py +++ b/i2/deco.py @@ -60,7 +60,9 @@ def _not_set_repr(self): It is distinct from ``None`` (which can be a legitimate value) and from ``inspect.Parameter.empty`` (which means "this parameter has no default"). Code that reads signature defaults should test for it with :func:`is_not_set` -rather than by comparing to a private object. +rather than by comparing to a private object. Note that ``bool(NotSet)`` is +``False``, so ``if not default:`` would lump it together with ``None``, ``0`` and +``""``: use :func:`is_not_set` instead. """