Let a port be named after the function it feeds
The generated node body imported the function under its own name and then gave `process` the ports as parameters, so a port named like its function shadowed the import and the call became a value calling itself — `TypeError: 'str' object is not callable`, with the downstream node's missing arguments as the knock-on. The import is aliased now. Only the two names the body needs itself, `settings` and `_impl`, are refused at sync. Every shim's text moves once, so the next sync reports every code-defined node updated and each cached node misses a single time. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TXQv6KNyyvY7Z1etYTUUAd
This commit is contained in:
@@ -60,6 +60,9 @@ DTYPES = frozenset(
|
||||
ITEM_TYPES = frozenset({"float", "int", "str", "bool", "json", "record"})
|
||||
#: Settings the engine reads itself, mirroring ``nodes.base.RESERVED_SETTINGS``.
|
||||
RESERVED_SETTINGS = frozenset({"synchronous"})
|
||||
#: Names ``_shim`` needs inside the body it generates: ``settings`` would collide
|
||||
#: with its ``**settings``, ``_impl`` with the function it imports.
|
||||
SHIM_RESERVED = frozenset({"settings", "_impl"})
|
||||
#: Mirrors ``fluksio.flow.schemas.NAME_PATTERN``.
|
||||
NAME_PATTERN = re.compile(r"^[a-z][a-z0-9_]*$")
|
||||
#: First line of every generated node body. Its absence is how ``sync`` knows a
|
||||
@@ -495,6 +498,11 @@ def _check(spec: NodeSpec, mode: str) -> dict[str, Any]:
|
||||
p.kind is inspect.Parameter.VAR_KEYWORD for p in parameters.values()
|
||||
)
|
||||
ports = {port.port for port in spec.requires}
|
||||
for name in sorted(ports & SHIM_RESERVED):
|
||||
raise SyncError(
|
||||
f"{where}: '{name}' is a name the generated node body keeps for "
|
||||
"itself — rename the port"
|
||||
)
|
||||
|
||||
params: dict[str, Any] = {}
|
||||
for name, parameter in parameters.items():
|
||||
@@ -772,6 +780,10 @@ def _shim(spec: NodeSpec) -> str:
|
||||
The store therefore still holds a complete, runnable definition — the body
|
||||
simply happens to be generated, which is why it says so and says where the
|
||||
real thing is.
|
||||
|
||||
The import is aliased because ``process``'s parameters are the node's port
|
||||
names, and a port may be named after the very function it feeds — which
|
||||
would otherwise shadow the import inside the body.
|
||||
"""
|
||||
_refuse_main(spec)
|
||||
fn = spec.fn
|
||||
@@ -780,7 +792,7 @@ def _shim(spec: NodeSpec) -> str:
|
||||
ports = [port.port for port in spec.requires]
|
||||
signature = ", ".join(ports + ["**settings"])
|
||||
arguments = ", ".join([f"{port}={port}" for port in ports] + ["**settings"])
|
||||
call = f"{fn.__name__}({arguments})"
|
||||
call = f"_impl({arguments})"
|
||||
|
||||
lines = [
|
||||
f"{MARKER} from {where} — edit that file instead",
|
||||
@@ -798,7 +810,7 @@ def _shim(spec: NodeSpec) -> str:
|
||||
"",
|
||||
]
|
||||
lines += [
|
||||
f"from {fn.__module__} import {fn.__name__}",
|
||||
f"from {fn.__module__} import {fn.__name__} as _impl",
|
||||
"",
|
||||
"",
|
||||
f"def process({signature}):",
|
||||
|
||||
@@ -187,15 +187,15 @@ def test_shim_imports_rather_than_copies():
|
||||
shims = a_flow().shims()
|
||||
|
||||
assert shims["prepare"].startswith(MARKER)
|
||||
assert "from tests.sdk.test_build import prepare" in shims["prepare"]
|
||||
assert "from tests.sdk.test_build import prepare as _impl" in shims["prepare"]
|
||||
assert "def process(**settings):" in shims["prepare"]
|
||||
assert "return prepare(**settings)" in shims["prepare"]
|
||||
assert "return _impl(**settings)" in shims["prepare"]
|
||||
|
||||
|
||||
def test_shim_of_a_generator_delegates_and_keeps_its_return_value():
|
||||
"""A bare `yield from` streams but drops what the generator returns."""
|
||||
assert (
|
||||
"return (yield from fit(dataset=dataset, lr=lr, **settings))"
|
||||
"return (yield from _impl(dataset=dataset, lr=lr, **settings))"
|
||||
in a_flow().shims()["fit"]
|
||||
)
|
||||
|
||||
@@ -221,7 +221,31 @@ def test_a_generator_shim_publishes_both_the_stream_and_the_result():
|
||||
|
||||
|
||||
def test_shim_of_a_single_port_wraps_the_bare_return():
|
||||
assert "return {'score': evaluate(weights=weights" in a_flow().shims()["evaluate"]
|
||||
assert "return {'score': _impl(weights=weights" in a_flow().shims()["evaluate"]
|
||||
|
||||
|
||||
@node(requires=["scale"], provides=Port("scaled", "float"))
|
||||
def scale(scale):
|
||||
return scale * 2.0
|
||||
|
||||
|
||||
def test_a_port_named_like_its_function_still_calls_the_function():
|
||||
"""The body imports under an alias, so the port cannot shadow the function."""
|
||||
flow = Flow("collide", nodes=[scale], inputs=[Port("scale", "float", initial=1.0)])
|
||||
namespace: dict = {}
|
||||
exec(compile(flow.shims()["scale"], "<shim>", "exec"), namespace)
|
||||
|
||||
assert namespace["process"](scale=2.0) == {"scaled": 4.0}
|
||||
|
||||
|
||||
def reads_settings(settings):
|
||||
return settings
|
||||
|
||||
|
||||
def test_a_port_the_generated_body_reserves_is_refused():
|
||||
refused = node(requires=["settings"], provides=Port("out", "float"))(reads_settings)
|
||||
with pytest.raises(SyncError, match="keeps for itself"):
|
||||
Flow("reserved", nodes=[refused], inputs=[Port("settings", "float")])
|
||||
|
||||
|
||||
def test_every_shim_compiles_and_defines_process():
|
||||
|
||||
Reference in New Issue
Block a user