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"})
|
ITEM_TYPES = frozenset({"float", "int", "str", "bool", "json", "record"})
|
||||||
#: Settings the engine reads itself, mirroring ``nodes.base.RESERVED_SETTINGS``.
|
#: Settings the engine reads itself, mirroring ``nodes.base.RESERVED_SETTINGS``.
|
||||||
RESERVED_SETTINGS = frozenset({"synchronous"})
|
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``.
|
#: Mirrors ``fluksio.flow.schemas.NAME_PATTERN``.
|
||||||
NAME_PATTERN = re.compile(r"^[a-z][a-z0-9_]*$")
|
NAME_PATTERN = re.compile(r"^[a-z][a-z0-9_]*$")
|
||||||
#: First line of every generated node body. Its absence is how ``sync`` knows a
|
#: 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()
|
p.kind is inspect.Parameter.VAR_KEYWORD for p in parameters.values()
|
||||||
)
|
)
|
||||||
ports = {port.port for port in spec.requires}
|
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] = {}
|
params: dict[str, Any] = {}
|
||||||
for name, parameter in parameters.items():
|
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
|
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
|
simply happens to be generated, which is why it says so and says where the
|
||||||
real thing is.
|
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)
|
_refuse_main(spec)
|
||||||
fn = spec.fn
|
fn = spec.fn
|
||||||
@@ -780,7 +792,7 @@ def _shim(spec: NodeSpec) -> str:
|
|||||||
ports = [port.port for port in spec.requires]
|
ports = [port.port for port in spec.requires]
|
||||||
signature = ", ".join(ports + ["**settings"])
|
signature = ", ".join(ports + ["**settings"])
|
||||||
arguments = ", ".join([f"{port}={port}" for port in ports] + ["**settings"])
|
arguments = ", ".join([f"{port}={port}" for port in ports] + ["**settings"])
|
||||||
call = f"{fn.__name__}({arguments})"
|
call = f"_impl({arguments})"
|
||||||
|
|
||||||
lines = [
|
lines = [
|
||||||
f"{MARKER} from {where} — edit that file instead",
|
f"{MARKER} from {where} — edit that file instead",
|
||||||
@@ -798,7 +810,7 @@ def _shim(spec: NodeSpec) -> str:
|
|||||||
"",
|
"",
|
||||||
]
|
]
|
||||||
lines += [
|
lines += [
|
||||||
f"from {fn.__module__} import {fn.__name__}",
|
f"from {fn.__module__} import {fn.__name__} as _impl",
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
f"def process({signature}):",
|
f"def process({signature}):",
|
||||||
|
|||||||
@@ -187,15 +187,15 @@ def test_shim_imports_rather_than_copies():
|
|||||||
shims = a_flow().shims()
|
shims = a_flow().shims()
|
||||||
|
|
||||||
assert shims["prepare"].startswith(MARKER)
|
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 "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():
|
def test_shim_of_a_generator_delegates_and_keeps_its_return_value():
|
||||||
"""A bare `yield from` streams but drops what the generator returns."""
|
"""A bare `yield from` streams but drops what the generator returns."""
|
||||||
assert (
|
assert (
|
||||||
"return (yield from fit(dataset=dataset, lr=lr, **settings))"
|
"return (yield from _impl(dataset=dataset, lr=lr, **settings))"
|
||||||
in a_flow().shims()["fit"]
|
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():
|
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():
|
def test_every_shim_compiles_and_defines_process():
|
||||||
|
|||||||
Reference in New Issue
Block a user