diff --git a/meshed/tests/test_ch_funcs.py b/meshed/tests/test_ch_funcs.py index 13dd0a9..77782ff 100644 --- a/meshed/tests/test_ch_funcs.py +++ b/meshed/tests/test_ch_funcs.py @@ -1,13 +1,45 @@ +"""Tests for ``meshed.dag.ch_funcs`` -- changing the functions of existing func nodes. + +Besides the behaviour of ``ch_funcs`` itself, this module pins how ``ch_funcs`` may be +*called*: it is built with ``i2.double_up_as_factory``, so it must decorate when given +func nodes and only make a factory when not given any -- whether the func nodes are +passed positionally or by keyword. +""" + +from functools import partial +from typing import NamedTuple + import meshed as ms import pytest import meshed.base import meshed.util -from meshed.dag import ch_funcs, _validate_func_mapping +from meshed.dag import DAG, ch_funcs, _validate_func_mapping from meshed.tests.objects_for_testing import f, g from meshed.base import compare_signatures -from i2 import Sig -from typing import NamedTuple +from i2 import Sig, double_up_as_factory + + +def _wrapped_by_keyword_is_supported() -> bool: + """Whether the installed ``i2`` accepts a decorator's wrapped object by keyword. + + Before `i2mint/i2#82 `_, + ``double_up_as_factory`` decided between "decorate this" and "make a factory" by + looking only at the first *positional* argument. A wrapped object passed by + keyword therefore landed in ``**kwargs`` and the decorator silently returned a + ``functools.partial`` factory instead of the decorated object. + """ + + @double_up_as_factory + def probe(obj=None, *, unused=None): + return obj + + return probe(obj=int) is int + + +#: Whether ``ch_funcs(func_nodes=...)`` decorates (True) or wrongly returns a factory +#: (False). Depends on the installed ``i2`` -- see ``_wrapped_by_keyword_is_supported``. +I2_SUPPORTS_WRAPPED_BY_KEYWORD = _wrapped_by_keyword_is_supported() @pytest.fixture @@ -25,18 +57,45 @@ def example_func_mapping(): def test_ch_funcs_no_change(example_func_nodes): + """Mapping every func node to the function it already has changes nothing.""" funcs = [f, g] nodes = list(example_func_nodes) names = [node.name for node in nodes] dummy_mapping = dict(zip(names, funcs)) - new_dag = ch_funcs( - func_nodes=nodes, - func_mapping=dummy_mapping, - ) - new_nodes = new_dag().func_nodes - assert nodes == new_nodes + new_dag = ch_funcs(nodes, func_mapping=dummy_mapping) + + assert isinstance(new_dag, DAG) + assert nodes == new_dag.func_nodes + + +@pytest.mark.skipif( + not I2_SUPPORTS_WRAPPED_BY_KEYWORD, + reason=( + "installed i2 predates i2mint/i2#82, so a double_up_as_factory decorator " + "given its wrapped object by keyword wrongly returns a factory" + ), +) +def test_ch_funcs_takes_func_nodes_by_keyword(example_func_nodes): + """``ch_funcs(func_nodes=...)`` must decorate, not return a factory. + + Regression pin for `i2mint/i2#82 `_: passing + the func nodes by keyword must mean exactly what passing them positionally means. + Before that fix it returned a ``functools.partial``, and call sites papered over it + with a trailing ``()`` -- which this test exists to stop coming back. + """ + funcs = [f, g] + nodes = list(example_func_nodes) + dummy_mapping = dict(zip([node.name for node in nodes], funcs)) + + new_dag = ch_funcs(func_nodes=nodes, func_mapping=dummy_mapping) + + assert not isinstance(new_dag, partial), "ch_funcs wrongly returned a factory" + assert isinstance(new_dag, DAG) + assert nodes == new_dag.func_nodes + # ...and it agrees with the positional form + assert new_dag.func_nodes == ch_funcs(nodes, func_mapping=dummy_mapping).func_nodes class FlagWithMessage(NamedTuple):