diff --git a/packages/code-runtime/code-runtime-python/py/bootstrap.py b/packages/code-runtime/code-runtime-python/py/bootstrap.py index 42434c9cd3..6988c07cb0 100644 --- a/packages/code-runtime/code-runtime-python/py/bootstrap.py +++ b/packages/code-runtime/code-runtime-python/py/bootstrap.py @@ -1300,7 +1300,11 @@ async def _run(channel: ProtocolChannel) -> None: ast.copy_location(wrapper, anchor) wrapped = ast.Module(body=[wrapper], type_ignores=[]) ast.fix_missing_locations(wrapped) - code = compile(wrapped, "", "exec") + # `dont_inherit=True` stops this module's `from __future__ import + # annotations` (line 14) from leaking into the program's compile: PEP 563 + # would otherwise stringify the program's type annotations, changing the + # semantics of a legal program that reads `f.__annotations__` at runtime. + code = compile(wrapped, "", "exec", dont_inherit=True) exec(code, ns) # noqa: S102 -- defines __dsh_main__; executing model code is the point value = await ns["__dsh_main__"]() die_if_cpu_exhausted(cpu_seconds) diff --git a/packages/code-runtime/code-runtime-python/tests/runtime.spec.ts b/packages/code-runtime/code-runtime-python/tests/runtime.spec.ts index 57ff90929a..2e75e721ea 100644 --- a/packages/code-runtime/code-runtime-python/tests/runtime.spec.ts +++ b/packages/code-runtime/code-runtime-python/tests/runtime.spec.ts @@ -2689,6 +2689,24 @@ describe('PythonCodeRuntime — budgets, termination, disposal', () => { expect(result.value).toBe("read: ''") }, 15_000) + it('keeps runtime type annotations unevaluated-as-strings when the program reads them', async () => { + // bootstrap.py imports `from __future__ import annotations`; without + // dont_inherit=True on compile(), that PEP 563 flag leaks into the program's + // compiled code and stringifies its type annotations, changing the semantics + // of a legal program that reads `f.__annotations__` at runtime. + const { runtime } = await setup() + const result = await runtime.run({ + program: [ + 'def f(x: int) -> int:', + ' return x', + 'return f.__annotations__["x"].__name__', + ].join('\n'), + bindings: [], + }) + expect(result.error).toBeUndefined() + expect(result.value).toBe('int') + }, 15_000) + it('reaps a same-group child that ignores SIGTERM and releases the pipes before close', async () => { // The same-group counterpart to the setsid-orphan case above. A descendant // left in the child's OWN process group (no setsid, so `kill(-pid)` reaches