From 6183c5ae26e7cfd4cc9048f49b0aff739bd99e65 Mon Sep 17 00:00:00 2001 From: Thor Whalen <1906276+thorwhalen@users.noreply.github.com> Date: Mon, 3 Aug 2026 20:47:46 +0100 Subject: [PATCH] Fix test_ch_funcs fossil of the double_up_as_factory keyword bug `ch_funcs` is built with `i2.double_up_as_factory`, whose first parameter is `func_nodes`. Before i2mint/i2#82, passing the wrapped object by keyword landed it in `**kwargs`, so `ch_funcs(func_nodes=..., func_mapping=...)` silently returned a `functools.partial` factory instead of a `DAG`. `test_ch_funcs_no_change` was written against that broken behaviour: it added a trailing `()` to turn the unexpected factory into the DAG it wanted. Once i2 is fixed, `ch_funcs(func_nodes=...)` returns the DAG directly and that trailing `()` calls the DAG instead, raising `TypeError: missing a required argument: 'a'`. Changes: - `test_ch_funcs_no_change` now passes the func nodes positionally and reads `new_dag.func_nodes` directly. This is what the test always meant to assert, and it behaves identically on both the old and the new i2. - New `test_ch_funcs_takes_func_nodes_by_keyword` pins that `ch_funcs(func_nodes=...)` returns a `DAG` and not a `partial`, so the fossil cannot silently come back. It is feature-gated on the installed i2 rather than on a version number, so this commit is safe to land before i2#82 ships and starts enforcing itself the moment it does. - Added the module docstring. Claude-Session: https://claude.ai/code/session_01Kug7UUbVeCQgruvNXUq63c --- meshed/tests/test_ch_funcs.py | 77 +++++++++++++++++++++++++++++++---- 1 file changed, 68 insertions(+), 9 deletions(-) diff --git a/meshed/tests/test_ch_funcs.py b/meshed/tests/test_ch_funcs.py index 13dd0a9b..77782ff3 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):