Skip to content

Fix test_ch_funcs fossil of the double_up_as_factory keyword bug - #76

Merged
thorwhalen merged 1 commit into
masterfrom
claude/fix-ch-funcs-keyword-fossil
Aug 4, 2026
Merged

Fix test_ch_funcs fossil of the double_up_as_factory keyword bug#76
thorwhalen merged 1 commit into
masterfrom
claude/fix-ch-funcs-keyword-fossil

Conversation

@thorwhalen

@thorwhalen thorwhalen commented Aug 3, 2026

Copy link
Copy Markdown
Member

Land this BEFORE (or together with) i2mint/i2#82

i2mint/i2#82 fixes double_up_as_factory so that a decorator given its wrapped object
by keyword decorates instead of silently returning a functools.partial factory.

meshed.dag.ch_funcs is built with double_up_as_factory, and one meshed test was
written against the broken behaviour. That test goes pass -> fail the moment i2#82
lands. This PR removes the fossil, so it should land first (or at the same time).

It is written to be safe to merge now, before i2#82 ships — see "Ordering" below.

The fossil

ch_funcs's first parameter is func_nodes. Before i2#82:

ch_funcs(func_nodes=nodes, func_mapping=mapping)   # -> functools.partial  (wrong)
ch_funcs(nodes, func_mapping=mapping)              # -> DAG                (right)

test_ch_funcs_no_change used the keyword form and then added a trailing () to turn
the unexpected factory into the DAG it actually wanted:

new_dag = ch_funcs(func_nodes=nodes, func_mapping=dummy_mapping)
new_nodes = new_dag().func_nodes   # the () only made sense because of the bug

After i2#82, ch_funcs(func_nodes=...) returns the DAG directly, so that trailing ()
calls the DAG, which raises:

TypeError: missing a required argument: 'a'

Verified A/B against a local i2 checkout: meshed/tests/test_ch_funcs.py gives
3 passed with i2 master and 1 failed, 2 passed with the i2#82 branch, deterministically.

The fix

  • test_ch_funcs_no_change now passes the func nodes positionally and reads
    new_dag.func_nodes directly. That is what the test always meant to assert
    ("an identity mapping changes nothing"), and it behaves identically on old and new i2,
    so this test keeps covering its intent regardless of which i2 is installed.
  • New test_ch_funcs_takes_func_nodes_by_keyword pins that ch_funcs(func_nodes=...)
    returns a DAG and not a partial, and that it agrees with the positional form —
    so the fossil cannot silently come back.
  • Added the missing module docstring.

Ordering / why this is safe to merge before i2#82

The new pin asserts behaviour that only the fixed i2 has. Rather than pin an i2 version
that does not exist yet, it feature-detects (_wrapped_by_keyword_is_supported()
builds a throwaway double_up_as_factory decorator and checks it). So:

installed i2 pin
current release (pre-#82) skipped, with a reason naming i2#82
with i2#82 runs and enforces

The pin is not vacuous: with the guard temporarily removed and pre-#82 i2 installed it
fails with AssertionError: ch_funcs wrongly returned a factory.

Test results

meshed is already red on master — 3 pre-existing doctest failures unrelated to this
change (meshed/dag.py, meshed/makers.py, meshed/scrap/cached_dag.py). Those are
untouched here. Full suite (pytest --doctest-modules):

i2 meshed result
master before this PR 3 failed, 157 passed, 1 skipped
i2#82 branch before this PR 4 failed, 156 passed, 1 skipped <- the regression
master this PR 3 failed, 157 passed, 2 skipped (pin skipped)
i2#82 branch this PR 3 failed, 158 passed, 1 skipped (pin runs)

Same 3 pre-existing failures in every case: the baseline is not worsened, and the
regression is gone.

Ecosystem survey

I searched the whole local package ecosystem for other call sites of this shape — passing
the wrapped object by keyword to a double_up_as_factory-built decorator and then calling
the result — both statically (every such decorator in i2, meshed, front, larder, py2http,
plunk, wip_qh) and dynamically (a temporary probe in double_up_as_factory logging every
call that takes the new code path, run across all 46 i2 dependents' suites).

This test was the only one in executed code. A re-review of the survey turned up a second occurrence of the same fossil in meshed/scrap/notebook.ipynb — a 2023 scratch notebook whose saved output shows the buggy functools.partial directly. That one is dead code (scrap/ is in this repo's CI paths-to-ignore, and no plugin collects notebooks), so it changes no test result; it is left untouched rather than edited, because rewriting its source without re-running it would desynchronise the cells from their stored outputs. Details are in the i2#82 thread.

https://claude.ai/code/session_01Kug7UUbVeCQgruvNXUq63c

`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
@thorwhalen

Copy link
Copy Markdown
Member Author

CI red is pre-existing and lives in i2, not in this PR

Root-caused the Validation (3.10) failure. It is not caused by this PR and cannot be fixed from this repo.

The failure

-chunker,wfs -> map -> chks_iter
+chunker,wfs,iterables -> map -> chks_iter

It is pre-existing

  • This PR changes exactly one file, meshed/tests/test_ch_funcs.py. It touches neither makers.py, util.py, nor dag.py.
  • master's own last CI run (2025-11-18) already failed.

Root cause: i2 resolves signatures by __name__ collision

meshed.makers builds placeholder functions via meshed.util.mk_place_holder_func, which names each node's function after its func_label — so the node for map(chunker, wfs) gets a function literally named map, carrying an explicit __signature__ of (chunker, wfs).

i2's _robust_signature_of_callable consults its curated sigs_for_sigless_builtin_name table keyed on __name__ alone, before trying inspect.signature. So any callable named map is handed the builtin map's signature (func, iterable, /, *iterables) — regardless of its real one:

f = mk_place_holder_func(['chunker', 'wfs'], name='map')
inspect.signature(f)   # (chunker, wfs)   <- correct
i2.Sig(f)              # (func, iterable, /, *iterables)   <- wrong

Hence the phantom iterables input on every map node. The extra input is a defect, not new correct behaviour — so updating the expected output would bake the bug into the docs. The doctest is a genuine bug report and is right to be red.

Introduced by i2 commit 0e72e58 ("fix: tests for 3.12", 2025-10-15), which moved the name lookup ahead of inspect.signature to fix operator instances on 3.12. That part is legitimate; keying it on __name__ for all callables is not. Shipped in i2 0.1.57 and still present in 0.1.64, which is what CI installs.

Fix

i2mint/i2#83 — the curated tables are consulted only for callables that don't declare a signature of their own, so operator instances keep their curated signatures while Python-defined callables keep theirs. Verified: no resolution changes across all ~170 builtins/functools/operator callables, and meshed/makers.py doctests pass on Python 3.10 with it.

Status

This PR is blocked on an i2 release containing i2mint/i2#83. meshed's install_requires is an unpinned i2, so CI installs i2 from PyPI; nothing in this repo can turn that job green honestly.

The other two locally-observed failures — unrelated, left alone

  • meshed/dag.pyDAG([my_chunker, list]) raises ValueError: non-default argument follows default argument. Python-version issue, not i2: same i2 code passes on 3.10 and fails on 3.12 (3.11+ tightened inspect.Signature validation across the positional-only/positional-or-keyword boundary). CI is 3.10-only, so this job passes there — confirmed meshed/dag.py::meshed.dag PASSED in the CI log.
  • meshed/scrap/cached_dag.py — long-standing breakage; the doctest itself carries a TODO saying it stopped working when code_to_dag gained signature support. In CI's paths-to-ignore (examples,scrap), so never collected.

Neither shares the root cause above, and neither affects CI. Left untouched.

@thorwhalen
thorwhalen merged commit 9cc92df into master Aug 4, 2026
12 of 15 checks passed
@thorwhalen
thorwhalen deleted the claude/fix-ch-funcs-keyword-fossil branch August 4, 2026 13:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant