Refuse a __main__ node where its body is written, not at import
A module defining nodes could not be run directly: the decorator refused `__main__` while the module body was still executing, so a `if __name__ == "__main__"` self-check beside the nodes was impossible and the checks had to live in a separate pytest file. The refusal now fires where the generated body is written — document() and shims(), both, since sync writes the document first — and the decorator hands the function back as it always did. Syncing a __main__-defined node is still refused, with the same words. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Hra4ndWMCLU5F3KjUuVAc
This commit is contained in:
@@ -388,11 +388,6 @@ def _check_signature(spec: NodeSpec) -> None:
|
|||||||
fn, where = spec.fn, f"node '{spec.id}'"
|
fn, where = spec.fn, f"node '{spec.id}'"
|
||||||
if inspect.iscoroutinefunction(fn):
|
if inspect.iscoroutinefunction(fn):
|
||||||
raise SyncError(f"{where}: async functions cannot be nodes")
|
raise SyncError(f"{where}: async functions cannot be nodes")
|
||||||
if fn.__module__ == "__main__":
|
|
||||||
raise SyncError(
|
|
||||||
f"{where}: {fn.__name__}() is defined in a script run directly, so the "
|
|
||||||
"generated node could not import it — put it in an importable module"
|
|
||||||
)
|
|
||||||
generator = inspect.isgeneratorfunction(fn)
|
generator = inspect.isgeneratorfunction(fn)
|
||||||
if generator and spec.single:
|
if generator and spec.single:
|
||||||
raise SyncError(
|
raise SyncError(
|
||||||
@@ -551,6 +546,21 @@ def _check(spec: NodeSpec, mode: str) -> dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _refuse_main(spec: NodeSpec) -> None:
|
||||||
|
"""A node the generated body would have no way to import.
|
||||||
|
|
||||||
|
Refused where the body is written rather than where the decorator is: a
|
||||||
|
module defining nodes is then still runnable as a script, which is what a
|
||||||
|
`__main__` self-check beside them needs.
|
||||||
|
"""
|
||||||
|
if spec.fn.__module__ == "__main__":
|
||||||
|
raise SyncError(
|
||||||
|
f"node '{spec.id}': {spec.fn.__name__}() is defined in a script run "
|
||||||
|
"directly, so the generated node could not import it — put it in an "
|
||||||
|
"importable module"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _code_of(spec: NodeSpec) -> dict[str, Any]:
|
def _code_of(spec: NodeSpec) -> dict[str, Any]:
|
||||||
"""What this node's function calls into, which its shim does not say.
|
"""What this node's function calls into, which its shim does not say.
|
||||||
|
|
||||||
@@ -559,6 +569,7 @@ def _code_of(spec: NodeSpec) -> dict[str, Any]:
|
|||||||
side is the only one that can work it out at all: it has imported the
|
side is the only one that can work it out at all: it has imported the
|
||||||
code, and the engine never does.
|
code, and the engine never does.
|
||||||
"""
|
"""
|
||||||
|
_refuse_main(spec)
|
||||||
files = reached(spec.fn)
|
files = reached(spec.fn)
|
||||||
return {"code_files": files, "code_digest": digest_of(files)}
|
return {"code_files": files, "code_digest": digest_of(files)}
|
||||||
|
|
||||||
@@ -762,6 +773,7 @@ def _shim(spec: NodeSpec) -> str:
|
|||||||
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.
|
||||||
"""
|
"""
|
||||||
|
_refuse_main(spec)
|
||||||
fn = spec.fn
|
fn = spec.fn
|
||||||
where = inspect.getsourcefile(fn) or fn.__module__
|
where = inspect.getsourcefile(fn) or fn.__module__
|
||||||
repo = import_root(fn)
|
repo = import_root(fn)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from fluksio.flow.schemas import FlowDef
|
|||||||
from fluksio.sdk import MARKER, Flow, Port, SyncError, node, use
|
from fluksio.sdk import MARKER, Flow, Port, SyncError, node, use
|
||||||
|
|
||||||
# The functions live here rather than in each test: a node's module is what the
|
# The functions live here rather than in each test: a node's module is what the
|
||||||
# generated body imports, and `__main__` is refused for exactly that reason.
|
# generated body imports, and `__main__` is refused at sync for that reason.
|
||||||
|
|
||||||
|
|
||||||
@node(provides=[Port("dataset", "artifact"), Port("rows", "int")])
|
@node(provides=[Port("dataset", "artifact"), Port("rows", "int")])
|
||||||
@@ -316,3 +316,27 @@ def test_a_negative_timeout_is_refused():
|
|||||||
def test_a_zero_timeout_means_no_limit():
|
def test_a_zero_timeout_means_no_limit():
|
||||||
decorated = node(requires=["a"], timeout=0)(one_default)
|
decorated = node(requires=["a"], timeout=0)(one_default)
|
||||||
assert decorated.__fluksio__.timeout == 0
|
assert decorated.__fluksio__.timeout == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_node_in_a_script_run_directly_is_refused_at_sync_not_at_import():
|
||||||
|
"""A study module has to be runnable as a script for a self-check.
|
||||||
|
|
||||||
|
The refusal belongs where the body is generated: the decorator hands the
|
||||||
|
function back untouched, so `python study.py` declares its nodes, calls
|
||||||
|
them and checks itself. What cannot be done is syncing them, because
|
||||||
|
nothing could import `__main__`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def probe(rows=1):
|
||||||
|
return {"score": float(rows)}
|
||||||
|
|
||||||
|
probe.__module__ = "__main__"
|
||||||
|
decorated = node(provides=[Port("score", "float")])(probe)
|
||||||
|
# Declared and callable, which is the whole point of running the file.
|
||||||
|
assert decorated(rows=2) == {"score": 2.0}
|
||||||
|
|
||||||
|
flow = Flow("mainflow", nodes=[decorated], outputs=["score"])
|
||||||
|
with pytest.raises(SyncError, match="run directly"):
|
||||||
|
flow.document()
|
||||||
|
with pytest.raises(SyncError, match="run directly"):
|
||||||
|
flow.shims()
|
||||||
|
|||||||
Reference in New Issue
Block a user