Skip to content

fix: registration fence for script-defined tasks under Python 3.14#37

Merged
dcx merged 1 commit into
mainfrom
fix-annotation-fence-pep649
Jul 8, 2026
Merged

fix: registration fence for script-defined tasks under Python 3.14#37
dcx merged 1 commit into
mainfrom
fix-annotation-fence-pep649

Conversation

@dcx

@dcx dcx commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Fix: registration fence for script-defined tasks under Python 3.14

ws.tasks.register(fn) for a bodyless task defined in a run-as-script file (__main__) captures the function's definition as its artifact. If the signature annotates with a name only the script defines — a local class, or any non-shepherd import — that name won't travel with the artifact, so registration is supposed to refuse early with a "move it to an importable module" message.

That check relied on the def statement evaluating its annotations eagerly, which two things defeat:

  • Python 3.14 (PEP 649 / PEP 749) defers annotation evaluation by default, so the def no longer raises for an unresolved name and the check silently passed — surfacing the failure later, more confusingly, instead of at registration.
  • A string forward-reference annotation (thing: "LocalThing") is never evaluated by the def on any Python version, so it slipped through everywhere.

The fix

Force annotation evaluation explicitly (get_type_hints against the generated artifact's own globals, which carry only import shepherd as sp) instead of relying on the def to do it. An undefined name now raises NameError — and the teachable refusal — on every supported Python (3.11–3.14). No change for valid tasks: normal May[GitRepo, ...] / str / None annotations resolve cleanly.

Tests

Adds a string-forward-ref regression test that fails against the old check on every Python (not just 3.14), so the behavior is guarded on the interpreters CI already runs. Verified: the registration ergonomics suites pass on both 3.12 and 3.14.

🤖 Generated with Claude Code

…robust to PEP 649

Registering a bodyless __main__ task whose signature names something only the
script defines is supposed to refuse early. The check relied on the def
statement eager-evaluating annotations, which two cases defeat: Python 3.14
(PEP 649/749) defers evaluation so the def no longer raises, and a string
forward-ref is never evaluated by the def on any version.

Force evaluation explicitly with get_type_hints against the generated
artifact's own globals, so an undefined name raises NameError (and the
teachable refusal) on every supported Python. No change for valid tasks.

Adds a string-forward-ref regression test that fails against the old check on
every Python. Verified on 3.12 and 3.14.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dcx dcx merged commit 8e703a6 into main Jul 8, 2026
4 checks passed
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