Skip to content

Commit

Permalink
Merge pull request #94 from pmeier/no-asyncio-run
Browse files Browse the repository at this point in the history
don't call asyncio.run for async handling
  • Loading branch information
smarie authored Sep 6, 2024
2 parents 3cacd2c + 24aa12b commit de81b0c
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 45 deletions.
4 changes: 4 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

### 0.10.3 - Bugfixes

- Don't use `asyncio.run` for async handling. Fixes [#93](https://github.com/smarie/mkdocs-gallery/issues/93).

### 0.10.2 - Bugfixes

- **SECURITY** removed insecure polyfill extra javascript from example. Fixes [#99](https://github.com/smarie/mkdocs-gallery/issues/99).
Expand Down
11 changes: 2 additions & 9 deletions docs/examples/plot_12_async.py → examples/plot_12_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,9 @@ async def afn():

# %%
# Without any handling, the snippet above would trigger a `SyntaxError`, since we are using `await` outside of an
# asynchronous context. With the handling, it works just fine.
# asynchronous context. With the background handling, it works just fine.
#
# The background handling will only be applied if it is actually needed. Meaning, you can still run your asynchronous
# code manually if required.

asyncio.run(afn())


# %%
# Apart from `await` all other asynchronous syntax is supported as well.
# Apart from `await` that we used above, all other asynchronous syntax is supported as well.
#
# ## Asynchronous Generators

Expand Down
8 changes: 4 additions & 4 deletions src/mkdocs_gallery/gen_single.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
from .notebook import jupyter_notebook, save_notebook
from .py_source_parser import remove_config_comments, split_code_and_text_blocks
from .scrapers import ImageNotFoundError, _find_image_ext, clean_modules, save_figures
from .utils import _new_file, _replace_by_new_if_needed, optipng, rescale_image
from .utils import _new_file, _replace_by_new_if_needed, optipng, rescale_image, run_async

logger = mkdocs_compatibility.getLogger("mkdocs-gallery")

Expand Down Expand Up @@ -780,8 +780,7 @@ def _apply_async_handling(code_ast, *, compiler_flags):
async def __async_wrapper__():
# original AST goes here
return locals()
import asyncio as __asyncio__
__async_wrapper_locals__ = __asyncio__.run(__async_wrapper__())
__async_wrapper_locals__ = __run_async__(__async_wrapper__())
__async_wrapper_result__ = __async_wrapper_locals__.pop("__async_wrapper_result__", None)
globals().update(__async_wrapper_locals__)
__async_wrapper_result__
Expand Down Expand Up @@ -950,6 +949,7 @@ def parse_and_execute(script: GalleryScript, script_blocks):
# Don't ever support __file__: Issues #166 #212
# Don't let them use input()
"input": _check_input,
"__run_async__": run_async,
}
)
script.run_vars.example_globals = example_globals
Expand All @@ -960,7 +960,7 @@ def parse_and_execute(script: GalleryScript, script_blocks):
# Remember the original argv so that we can put them back after run
argv_orig = sys.argv[:]

# Remember the original sys.path so that we can reset it after run
# Remember the original sys.path so that we can reset it after run
sys_path_orig = deepcopy(sys.path)

# Python file is the original one (not the copy for download)
Expand Down
13 changes: 13 additions & 0 deletions src/mkdocs_gallery/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from __future__ import absolute_import, division, print_function

import asyncio
import hashlib
import os
import re
Expand Down Expand Up @@ -376,3 +377,15 @@ def is_relative_to(parentpath: Path, subpath: Path) -> bool:

except ValueError:
return False


def run_async(coro):
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = asyncio.new_event_loop()

try:
return loop.run_until_complete(coro)
finally:
loop.close()
79 changes: 47 additions & 32 deletions tests/test_gen_single.py
Original file line number Diff line number Diff line change
@@ -1,45 +1,47 @@
import ast
import asyncio
import codeop
import sys
from textwrap import dedent

import pytest

from mkdocs_gallery.gen_single import _needs_async_handling, _parse_code
from mkdocs_gallery.utils import run_async

SRC_FILE = __file__
COMPILER = codeop.Compile()
COMPILER_FLAGS = codeop.Compile().flags
COMPILER_FLAGS = COMPILER.flags


needs_ast_unparse = pytest.mark.skipif(
sys.version_info < (3, 9), reason="ast.unparse is only available for Python >= 3.9"
)


def make_globals():
return {"__run_async__": run_async}


def test_non_async_syntax_error():
with pytest.raises(SyntaxError, match="unexpected indent"):
_parse_code("foo = None\n bar = None", src_file=SRC_FILE, compiler_flags=COMPILER_FLAGS)


@needs_ast_unparse
def test_no_async_roundtrip():
code = "None"
assert not _needs_async_handling(code, src_file=SRC_FILE, compiler_flags=COMPILER_FLAGS)

code_unparsed = ast.unparse(_parse_code(code, src_file=SRC_FILE, compiler_flags=COMPILER_FLAGS))

assert code_unparsed == code


@needs_ast_unparse
@pytest.mark.parametrize(
("code", "needs"),
"code",
[
pytest.param("None", False, id="no_async"),
pytest.param(
dedent(
"""
async def afn():
return True
import asyncio
assert asyncio.run(afn())
"""
),
False,
id="asyncio_run",
),
pytest.param(
dedent(
"""
Expand All @@ -49,7 +51,6 @@ async def afn():
assert await afn()
"""
),
True,
id="await",
),
pytest.param(
Expand All @@ -62,7 +63,6 @@ async def agen():
assert item
"""
),
True,
id="async_for",
),
pytest.param(
Expand All @@ -74,7 +74,6 @@ async def agen():
assert [item async for item in agen()] == [True]
"""
),
True,
id="async_comprehension",
),
pytest.param(
Expand All @@ -90,25 +89,23 @@ async def acm():
assert ctx
"""
),
True,
id="async_context_manager",
),
],
)
def test_async_handling(code, needs):
assert _needs_async_handling(code, src_file=SRC_FILE, compiler_flags=COMPILER_FLAGS) is needs
def test_async_handling(code):
assert _needs_async_handling(code, src_file=SRC_FILE, compiler_flags=COMPILER_FLAGS)

# Since AST objects are quite involved to compare, we unparse again and check that nothing has changed. Note that
# since we are dealing with AST and not CST here, all whitespace is eliminated in the process and this needs to be
# reflected in the input as well.
code_stripped = "\n".join(line for line in code.splitlines() if line)
code_unparsed = ast.unparse(_parse_code(code, src_file=SRC_FILE, compiler_flags=COMPILER_FLAGS))
assert (code_unparsed == code_stripped) ^ needs
assert code_unparsed != code_stripped

if needs:
assert not _needs_async_handling(code_unparsed, src_file=SRC_FILE, compiler_flags=COMPILER_FLAGS)
assert not _needs_async_handling(code_unparsed, src_file=SRC_FILE, compiler_flags=COMPILER_FLAGS)

exec(COMPILER(code_unparsed, SRC_FILE, "exec"), {})
exec(COMPILER(code_unparsed, SRC_FILE, "exec"), make_globals())


@needs_ast_unparse
Expand All @@ -128,10 +125,10 @@ async def afn():
)
code_unparsed = ast.unparse(_parse_code(code, src_file=SRC_FILE, compiler_flags=COMPILER_FLAGS))

locals = {}
exec(COMPILER(code_unparsed, SRC_FILE, "exec"), locals)
globals_ = make_globals()
exec(COMPILER(code_unparsed, SRC_FILE, "exec"), globals_)

assert "sentinel" in locals and locals["sentinel"] == sentinel
assert "sentinel" in globals_ and globals_["sentinel"] == sentinel


@needs_ast_unparse
Expand All @@ -153,6 +150,24 @@ async def afn():
last = code_unparsed_ast.body[-1]
assert isinstance(last, ast.Expr)

locals = {}
exec(COMPILER(code_unparsed, SRC_FILE, "exec"), locals)
assert eval(ast.unparse(last.value), locals)
globals_ = make_globals()
exec(COMPILER(code_unparsed, SRC_FILE, "exec"), globals_)
assert eval(ast.unparse(last.value), globals_)


@needs_ast_unparse
def test_get_event_loop_after_async_handling():
# Non-regression test for https://github.com/smarie/mkdocs-gallery/issues/93
code = dedent(
"""
async def afn():
return True
assert await afn()
"""
)

code_unparsed = ast.unparse(_parse_code(code, src_file=SRC_FILE, compiler_flags=COMPILER_FLAGS))
exec(COMPILER(code_unparsed, SRC_FILE, "exec"), make_globals())

asyncio.events.get_event_loop()

0 comments on commit de81b0c

Please sign in to comment.