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 @@ -10,6 +10,8 @@
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
DefaultPlaceholder, # Base for signature defaults that stand for a value
is_placeholder_default, # NotSet or a DefaultPlaceholder: "no concrete default"
)

from i2.signatures import (
Expand Down
30 changes: 30 additions & 0 deletions i2/deco.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,36 @@ def is_not_set(x) -> bool:
return x is NotSet


class DefaultPlaceholder:
"""Base class for signature defaults that stand for a value rather than being one.

A default whose value is only known at call time (e.g. ``methodize``'s
``default_from``, whose default is read from the instance) is shown in the
signature as an instance of a subclass. It is not a concrete value, so code that
serializes signature defaults (form generators, OpenAPI/JSON schemas) should treat
it as "no default": see :func:`is_placeholder_default`.
"""


def is_placeholder_default(x) -> bool:
"""Return ``True`` if ``x`` is a signature default that is not a concrete value.

That is, the ``NotSet`` sentinel or a :class:`DefaultPlaceholder` (such as
``i2.wrapper.InstanceAttrDefault``). Signature consumers that need a concrete,
serializable default (UI or schema generators) can treat these like
``inspect.Parameter.empty``:

>>> from inspect import Parameter
>>> class Later(DefaultPlaceholder):
... pass
>>> is_placeholder_default(NotSet), is_placeholder_default(Later())
(True, True)
>>> is_placeholder_default(None), is_placeholder_default(Parameter.empty)
(False, False)
"""
return is_not_set(x) or isinstance(x, DefaultPlaceholder)


# ---------------------------------------------------------------------------------------
# TODO: https://github.com/i2mint/i2/issues/48
class FuncFactory:
Expand Down
22 changes: 22 additions & 0 deletions i2/tests/test_partials_and_methodize.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,28 @@ class K:
assert (k.bar(3), k.bar(3, 10), k.bar(3, z=0)) == (7, 31, 1)


def test_methodize_default_from_json_dump_of_signature_defaults():
"""A default_from default is a placeholder: JSON consumers skip it, like NotSet."""
import json

from i2 import NotSet, is_placeholder_default

def foo(x, y, z, w=NotSet, v=1):
return x + y * z

@dataclass
class K:
x: int
zoo: int = 2
bar = methodize(foo, bind="x", default_from=[("z", "zoo")])

defaults = Sig(K(1).bar).defaults
with pytest.raises(TypeError):
json.dumps(defaults) # what a naive consumer would meet
concrete = {k: v for k, v in defaults.items() if not is_placeholder_default(v)}
assert json.loads(json.dumps(concrete)) == {"v": 1}


def test_methodize_keeps_self_before_position_only_params():
"""i2mint/i2#118: ``self`` used to land after the ``/``."""

Expand Down
15 changes: 10 additions & 5 deletions i2/wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
)
from i2.errors import OverwritesNotAllowed
from i2.multi_object import Pipe
from i2.deco import double_up_as_factory
from i2.deco import double_up_as_factory, DefaultPlaceholder
from i2.util import deprecation_of

# ---------------------------------------------------------------------------------------
Expand Down Expand Up @@ -1854,14 +1854,19 @@ def pairs():
return param_to_attr


class InstanceAttrDefault:
class InstanceAttrDefault(DefaultPlaceholder):
"""The default of a method param that is sourced from an instance attribute.

Shown in the method's signature as ``self.<attr>`` (see ``methodize``'s
``default_from``).
``default_from``). It is a ``DefaultPlaceholder``, not a value: its value depends
on the instance, which a schema or form generator does not have, so such code
should treat it as "no default" (``i2.is_placeholder_default``).

>>> InstanceAttrDefault('zoo')
self.zoo
>>> from i2 import is_placeholder_default
>>> is_placeholder_default(InstanceAttrDefault('zoo'))
True
"""

def __init__(self, attr: str, *, instance_arg_name: str = "self"):
Expand Down Expand Up @@ -2175,8 +2180,8 @@ def methodize(
16

The signature default is an ``InstanceAttrDefault`` marker (shown ``self.zoo``),
not a value: code that serializes signature defaults (e.g. to JSON) should not
expect a JSON-able one there.
not a value: code that serializes signature defaults (e.g. to JSON) should skip
it, as it skips ``NotSet``, by testing ``i2.is_placeholder_default``.
"""
return func_to_method_func(
func,
Expand Down
Loading