-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #90 from pmeier/async-gallery
add support for async galleries
- Loading branch information
Showing
4 changed files
with
306 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
""" | ||
# Support for asynchronous code | ||
[PEP 429](https://peps.python.org/pep-0492), which was first implemented in | ||
[Python 3.5](https://docs.python.org/3/whatsnew/3.5.html#whatsnew-pep-492), added initial syntax for asynchronous | ||
programming in Python: `async` and `await`. | ||
While this was a major improvement in particular for UX development, one major | ||
downside is that it "poisons" the caller's code base. If you want to `await` a coroutine, you have to be inside a `async def` | ||
context. Doing so turns the function into a coroutine function and thus forces the caller to also `await` its results. | ||
Rinse and repeat until you reach the beginning of the stack. | ||
Since version `0.10.0`, `mkdocs-gallery` is now able to automatically detect code blocks using async programming, and to handle them nicely so that you don't have to wrap them. This feature is enabled by default and does not require any configuration option. Generated notebooks remain consistent with [`jupyter` notebooks](https://jupyter.org/), or rather the [`IPython` kernel](https://ipython.org/) running | ||
the code inside of them, that is equipped with | ||
[background handling to allow top-level asynchronous code](https://ipython.readthedocs.io/en/stable/interactive/autoawait.html). | ||
""" | ||
|
||
import asyncio | ||
import time | ||
|
||
|
||
async def afn(): | ||
start = time.time() | ||
await asyncio.sleep(0.3) | ||
stop = time.time() | ||
return stop - start | ||
|
||
|
||
f"I waited for {await afn():.1f} seconds!" | ||
|
||
|
||
# %% | ||
# 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. | ||
# | ||
# 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. | ||
# | ||
# ## Asynchronous Generators | ||
|
||
|
||
async def agen(): | ||
for chunk in "I'm an async iterator!".split(): | ||
yield chunk | ||
|
||
|
||
async for chunk in agen(): | ||
print(chunk, end=" ") | ||
|
||
|
||
# %% | ||
# ## Asynchronous Comprehensions | ||
|
||
" ".join([chunk async for chunk in agen()]) | ||
|
||
# %% | ||
# ## Asynchronous Context Managers | ||
|
||
import contextlib | ||
|
||
|
||
@contextlib.asynccontextmanager | ||
async def acm(): | ||
print("Entering asynchronous context manager!") | ||
yield | ||
print("Exiting asynchronous context manager!") | ||
|
||
|
||
async with acm(): | ||
print("Inside the context!") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
import ast | ||
import codeop | ||
import sys | ||
from textwrap import dedent | ||
|
||
import pytest | ||
|
||
from mkdocs_gallery.gen_single import _needs_async_handling, _parse_code | ||
|
||
SRC_FILE = __file__ | ||
COMPILER = codeop.Compile() | ||
COMPILER_FLAGS = codeop.Compile().flags | ||
|
||
|
||
needs_ast_unparse = pytest.mark.skipif( | ||
sys.version_info < (3, 9), reason="ast.unparse is only available for Python >= 3.9" | ||
) | ||
|
||
|
||
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 | ||
@pytest.mark.parametrize( | ||
("code", "needs"), | ||
[ | ||
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( | ||
""" | ||
async def afn(): | ||
return True | ||
assert await afn() | ||
""" | ||
), | ||
True, | ||
id="await", | ||
), | ||
pytest.param( | ||
dedent( | ||
""" | ||
async def agen(): | ||
yield True | ||
async for item in agen(): | ||
assert item | ||
""" | ||
), | ||
True, | ||
id="async_for", | ||
), | ||
pytest.param( | ||
dedent( | ||
""" | ||
async def agen(): | ||
yield True | ||
assert [item async for item in agen()] == [True] | ||
""" | ||
), | ||
True, | ||
id="async_comprehension", | ||
), | ||
pytest.param( | ||
dedent( | ||
""" | ||
import contextlib | ||
@contextlib.asynccontextmanager | ||
async def acm(): | ||
yield True | ||
async with acm() as ctx: | ||
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 | ||
|
||
# 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 | ||
|
||
if needs: | ||
assert not _needs_async_handling(code_unparsed, src_file=SRC_FILE, compiler_flags=COMPILER_FLAGS) | ||
|
||
exec(COMPILER(code_unparsed, SRC_FILE, "exec"), {}) | ||
|
||
|
||
@needs_ast_unparse | ||
def test_async_handling_locals(): | ||
sentinel = "sentinel" | ||
code = dedent( | ||
""" | ||
async def afn(): | ||
return True | ||
sentinel = {sentinel} | ||
assert await afn() | ||
""".format( | ||
sentinel=repr(sentinel) | ||
) | ||
) | ||
code_unparsed = ast.unparse(_parse_code(code, src_file=SRC_FILE, compiler_flags=COMPILER_FLAGS)) | ||
|
||
locals = {} | ||
exec(COMPILER(code_unparsed, SRC_FILE, "exec"), locals) | ||
|
||
assert "sentinel" in locals and locals["sentinel"] == sentinel | ||
|
||
|
||
@needs_ast_unparse | ||
def test_async_handling_last_expression(): | ||
code = dedent( | ||
""" | ||
async def afn(): | ||
return True | ||
result = await afn() | ||
assert result | ||
result | ||
""" | ||
) | ||
|
||
code_unparsed_ast = _parse_code(code, src_file=SRC_FILE, compiler_flags=COMPILER_FLAGS) | ||
code_unparsed = ast.unparse(code_unparsed_ast) | ||
|
||
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) |