diff --git a/src/markdown_exec/formatters/_exec_python.py b/src/markdown_exec/formatters/_exec_python.py new file mode 100644 index 0000000..4bd7813 --- /dev/null +++ b/src/markdown_exec/formatters/_exec_python.py @@ -0,0 +1,8 @@ +"""Special module without future annotations for executing Python code.""" + +from typing import Any, Dict, Optional + + +def exec_python(code: str, filename: str, exec_globals: Optional[Dict[str, Any]] = None) -> None: + compiled = compile(code, filename=filename, mode="exec") + exec(compiled, exec_globals) # noqa: S102 diff --git a/src/markdown_exec/formatters/python.py b/src/markdown_exec/formatters/python.py index c4f4453..6f9153b 100644 --- a/src/markdown_exec/formatters/python.py +++ b/src/markdown_exec/formatters/python.py @@ -11,6 +11,7 @@ from types import ModuleType from typing import Any +from markdown_exec.formatters._exec_python import exec_python from markdown_exec.formatters.base import ExecutionError, base_format from markdown_exec.rendering import code_block @@ -67,8 +68,7 @@ def _run_python( exec_globals["print"] = partial(_buffer_print, buffer) try: - compiled = compile(code, filename=code_block_id, mode="exec") - exec(compiled, exec_globals) # noqa: S102 + exec_python(code, code_block_id, exec_globals) except Exception as error: # noqa: BLE001 trace = traceback.TracebackException.from_exception(error) for frame in trace.stack: diff --git a/tests/test_python.py b/tests/test_python.py index f1779b0..2b4cde1 100644 --- a/tests/test_python.py +++ b/tests/test_python.py @@ -2,6 +2,7 @@ from __future__ import annotations +import re from textwrap import dedent from typing import TYPE_CHECKING @@ -203,3 +204,28 @@ def func(): ), ) assert "_code_block_n" in html + + +def test_future_annotations_do_not_leak_into_user_code(md: Markdown) -> None: + """Assert future annotations do not leak into user code. + + Parameters: + md: A Markdown instance (fixture). + """ + html = md.convert( + dedent( + """ + ```python exec="1" + class Int: + ... + + def f(x: Int) -> None: + return x + 1.0 + + print(f"`{f.__annotations__['x']}`") + ``` + """, + ), + ) + assert "Int" not in html + assert re.search(r"class '_code_block_n\d+_\.Int'", html)