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
4 changes: 3 additions & 1 deletion meshed/dag.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@
extract_items,
ParameterMerger,
conservative_parameter_merge,
not_set_to_empty,
)
from meshed.itools import (
topological_sort,
Expand Down Expand Up @@ -1254,8 +1255,9 @@ def src_name_params(self, src_names: Iterable[str] | None = None):
for src_name in filter(src_names.__contains__, d):
params = d[src_name] # consider all the params that use it
# make version of these params that have the same name (namely src_name)
# (i2's NotSet sentinel default counts as "no default": see not_set_to_empty)
params_with_name_changed_to_src_name = [
p.replace(name=src_name) for p in params
not_set_to_empty(p).replace(name=src_name) for p in params
]
if len(params_with_name_changed_to_src_name) == 1:
# if there's only one param, yield it (there can be no conflict)
Expand Down
57 changes: 57 additions & 0 deletions meshed/tests/test_not_set_defaults.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""``i2``'s ``NotSet`` sentinel in a signature means "required / no default".

See i2mint/i2#48: once ``i2.FuncFactory`` shows ``NotSet`` defaults, a DAG holding
such a node must keep the signature it has today. Otherwise its positional order
changes silently (``NotSet``-defaulted params would be sorted after required ones),
and merging with a same-named, non-defaulted param would raise.
These tests use ``i2.deco.NotSet`` directly, so they pass with any i2 version.
"""

import pytest
from i2 import Sig
from i2.deco import NotSet

from meshed import DAG


def g(a):
return a


# ``g`` with a ``NotSet`` default, as a re-landed i2#88 ``FuncFactory`` would show.
def g_with_not_set(a=NotSet):
return a


g_with_not_set.__name__ = g.__name__


def h(z):
return z * 10


def k(a, z):
return a - z


def test_dag_signature_is_the_same_as_without_not_set():
dag, plain = DAG([g_with_not_set, h]), DAG([g, h])
assert str(Sig(dag)) == str(Sig(plain)) == "(a, z)"
assert dag(1, 2) == plain(1, 2) # positional order is kept


def test_not_set_param_merges_with_same_named_required_param():
dag = DAG([g_with_not_set, k]) # used to raise: "didn't have the same default"
assert str(Sig(dag)) == str(Sig(DAG([g, k])))


def test_not_set_root_is_required():
with pytest.raises(TypeError):
DAG([g_with_not_set])()


def test_real_defaults_are_kept():
def f(a=3):
return a

assert DAG([f])() == 3
28 changes: 28 additions & 0 deletions meshed/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,34 @@

from i2 import Sig, name_of_obj, LiteralVal, FuncFanout, Pipe

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 not_set_to_empty(param: Parameter) -> Parameter:
"""Return ``param``, but with no default if its default is ``i2``'s ``NotSet``.

``NotSet`` in a signature (e.g. an ``i2.FuncFactory``'s) means "no value given",
not a real default. A DAG treats such a param as required, so that it keeps its
positional order and merges with same-named params that have no default.

>>> from i2.deco import NotSet
>>> not_set_to_empty(Parameter('x', Parameter.KEYWORD_ONLY, default=NotSet))
<Parameter "x">
>>> not_set_to_empty(Parameter('x', Parameter.KEYWORD_ONLY, default=3))
<Parameter "x=3">
"""
if is_not_set(param.default):
return param.replace(default=Parameter.empty)
return param


T = TypeVar("T")


Expand Down
Loading