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:
2026-08-29 13:50:35 +02:00
co-authored by Claude Opus 5
parent cfb3941cf3
commit 81649dbfca
2 changed files with 42 additions and 6 deletions
+17 -5
View File
@@ -388,11 +388,6 @@ def _check_signature(spec: NodeSpec) -> None:
fn, where = spec.fn, f"node '{spec.id}'"
if inspect.iscoroutinefunction(fn):
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)
if generator and spec.single:
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]:
"""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
code, and the engine never does.
"""
_refuse_main(spec)
files = reached(spec.fn)
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
real thing is.
"""
_refuse_main(spec)
fn = spec.fn
where = inspect.getsourcefile(fn) or fn.__module__
repo = import_root(fn)