Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions i2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
29 changes: 29 additions & 0 deletions i2/deco.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,35 @@ 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. Note that ``bool(NotSet)`` is
``False``, so ``if not default:`` would lump it together with ``None``, ``0`` and
``""``: use :func:`is_not_set` instead.
"""


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


# ---------------------------------------------------------------------------------------
Expand Down
17 changes: 17 additions & 0 deletions i2/tests/test_deco.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Loading