From 3fd212029ed598143a7544810a6000cf454996b1 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Sat, 16 Dec 2023 01:02:42 +0100 Subject: [PATCH 01/17] add support for async galleries --- docs/examples/plot_10_async.py | 25 +++++++++++++++++++++++++ src/mkdocs_gallery/gen_single.py | 21 +++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 docs/examples/plot_10_async.py diff --git a/docs/examples/plot_10_async.py b/docs/examples/plot_10_async.py new file mode 100644 index 00000000..e857db90 --- /dev/null +++ b/docs/examples/plot_10_async.py @@ -0,0 +1,25 @@ +""" +# Foo! + +Bar? Baz! +""" + +import asyncio +import time + +start = time.time() +await asyncio.sleep(1) +stop = time.time() +f"I waited for {stop - start} seconds!" + + +#%% +# More code! + +import asyncio +import time + +start = time.time() +await asyncio.sleep(0.3) +stop = time.time() +f"I waited for {stop - start} seconds!" diff --git a/src/mkdocs_gallery/gen_single.py b/src/mkdocs_gallery/gen_single.py index 55a64d7e..991ef443 100644 --- a/src/mkdocs_gallery/gen_single.py +++ b/src/mkdocs_gallery/gen_single.py @@ -914,6 +914,27 @@ def parse_and_execute(script: GalleryScript, script_blocks): t_start = time() compiler = codeop.Compile() + if script.script_stem.startswith("plot_10"): + fixed_script_blocks = [] + for script_block in script_blocks: + label, content, line_number = script_block + if label == "code": + *lines, last_line = [line for line in content.splitlines() if line] + start = next(idx for idx, c in enumerate(last_line) if c != " ") + lines.append(f"{last_line[:start]}return {last_line[start:]}") + content = "\n".join( + [ + "async def __async_wrapper__():", + *[f" {line}" for line in lines], + "import asyncio", + "asyncio.run(__async_wrapper__())", + ] + ) + script_block = (label, content, line_number) + + fixed_script_blocks.append(script_block) + script_blocks = fixed_script_blocks + # Execute block by block output_blocks = list() with _LoggingTee(script.src_py_file) as logging_tee: From c0deeb98d86b3a46a60f4ce5aa03c12d88375a60 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Mon, 18 Dec 2023 00:41:30 +0100 Subject: [PATCH 02/17] move async handling to AST --- src/mkdocs_gallery/gen_single.py | 87 +++++++++++++++++++++++--------- 1 file changed, 62 insertions(+), 25 deletions(-) diff --git a/src/mkdocs_gallery/gen_single.py b/src/mkdocs_gallery/gen_single.py index 991ef443..4ab97c3c 100644 --- a/src/mkdocs_gallery/gen_single.py +++ b/src/mkdocs_gallery/gen_single.py @@ -27,7 +27,7 @@ from io import StringIO from pathlib import Path from shutil import copyfile -from textwrap import indent +from textwrap import indent, dedent from time import time from typing import List, Set, Tuple @@ -739,6 +739,66 @@ def _reset_cwd_syspath(cwd, path_to_remove): os.chdir(cwd) +def _parse_code(bcontent, src_file, *, compiler_flags): + code_ast = compile(bcontent, src_file, "exec", compiler_flags | ast.PyCF_ONLY_AST, dont_inherit=1) + if _needs_async_handling(bcontent, compiler_flags=compiler_flags): + code_ast = _apply_async_handling(code_ast, compiler_flags=compiler_flags) + return code_ast + + +def _needs_async_handling(bcontent, *, compiler_flags) -> bool: + try: + compile(bcontent, "<_needs_async_handling()>", "exec", compiler_flags, dont_inherit=1) + except SyntaxError as error: + # FIXME + # bool(re.match(r"'(await|async for|async with)' outside( async)? function", str(error))) + # asynchronous comprehension outside of an asynchronous function + return "async" in str(error) + except Exception: + return False + else: + return False + + +def _apply_async_handling(code_ast, *, compiler_flags): + async_handling = compile( + dedent( + """ + async def __async_wrapper__(): + # original AST goes here + return locals() + import asyncio as __asyncio__ + __async_wrapper_locals__ = __asyncio__.run(__async_wrapper__()) + __async_wrapper_result__ = __async_wrapper_locals__.pop("__async_wrapper_result__", None) + globals().update(__async_wrapper_locals__) + __async_wrapper_result__ + """ + ), + "<_apply_async_handling()>", + "exec", + compiler_flags | ast.PyCF_ONLY_AST, + dont_inherit=1, + ) + + *original_body, last_node = code_ast.body + if isinstance(last_node, ast.Expr): + # FIXME + # ast.Assign(targets=[ast.Name(id="__async_wrapper_result__", ctx=ast.Store())], value=last_node) + last_node = compile( + f"__async_wrapper_result__ = ({ast.unparse(last_node)})", + "<_apply_async_handling()>", + "exec", + compiler_flags | ast.PyCF_ONLY_AST, + dont_inherit=1, + ).body[0] + original_body.append(last_node) + + async_wrapper = async_handling.body[0] + async_wrapper.body = [*original_body, *async_wrapper.body] + + return ast.fix_missing_locations(async_handling) + + def execute_code_block(compiler, block, script: GalleryScript): """Execute the code block of the example file. @@ -788,9 +848,7 @@ def execute_code_block(compiler, block, script: GalleryScript): try: ast_Module = _ast_module() - code_ast = ast_Module([bcontent]) - flags = ast.PyCF_ONLY_AST | compiler.flags - code_ast = compile(bcontent, src_file, "exec", flags, dont_inherit=1) + code_ast = _parse_code(bcontent, src_file, compiler_flags=compiler.flags) ast.increment_lineno(code_ast, lineno - 1) is_last_expr, mem_max = _exec_and_get_memory(compiler, ast_Module, code_ast, script=script) @@ -914,27 +972,6 @@ def parse_and_execute(script: GalleryScript, script_blocks): t_start = time() compiler = codeop.Compile() - if script.script_stem.startswith("plot_10"): - fixed_script_blocks = [] - for script_block in script_blocks: - label, content, line_number = script_block - if label == "code": - *lines, last_line = [line for line in content.splitlines() if line] - start = next(idx for idx, c in enumerate(last_line) if c != " ") - lines.append(f"{last_line[:start]}return {last_line[start:]}") - content = "\n".join( - [ - "async def __async_wrapper__():", - *[f" {line}" for line in lines], - "import asyncio", - "asyncio.run(__async_wrapper__())", - ] - ) - script_block = (label, content, line_number) - - fixed_script_blocks.append(script_block) - script_blocks = fixed_script_blocks - # Execute block by block output_blocks = list() with _LoggingTee(script.src_py_file) as logging_tee: From a0219676d0a2883db76d86c10f290a038d4487ad Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Mon, 18 Dec 2023 00:45:41 +0100 Subject: [PATCH 03/17] improve example --- docs/examples/plot_10_async.py | 45 +++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/docs/examples/plot_10_async.py b/docs/examples/plot_10_async.py index e857db90..e2b26329 100644 --- a/docs/examples/plot_10_async.py +++ b/docs/examples/plot_10_async.py @@ -1,7 +1,5 @@ """ -# Foo! - -Bar? Baz! +# Async support """ import asyncio @@ -13,13 +11,38 @@ f"I waited for {stop - start} seconds!" -#%% -# More code! +# %% +# ## Async Iterator -import asyncio -import time +class AsyncIterator: + async def __aiter__(self): + for chunk in "I'm an async iterator!".split(): + yield chunk -start = time.time() -await asyncio.sleep(0.3) -stop = time.time() -f"I waited for {stop - start} seconds!" + +async for chunk in AsyncIterator(): + print(chunk, end=" ") + +# %% +# ## Async comprehensions + +" ".join([chunk async for chunk in AsyncIterator()]) + +# %% +# ## Async content manager + + +class AsyncContextManager: + async def __aenter__(self): + print("Entering ...") + return self + + async def __aexit__(self, *exc_info): + print("Exiting ...") + + def __str__(self): + return "I'm an async context manager!" + + +async with AsyncContextManager() as acm: + print(acm) From 5e0e63693b5c951687d7f8519ec253a84d200ca8 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Fri, 22 Dec 2023 13:31:14 +0100 Subject: [PATCH 04/17] improve async detection --- src/mkdocs_gallery/gen_single.py | 33 ++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/src/mkdocs_gallery/gen_single.py b/src/mkdocs_gallery/gen_single.py index 4ab97c3c..ecbea70c 100644 --- a/src/mkdocs_gallery/gen_single.py +++ b/src/mkdocs_gallery/gen_single.py @@ -741,21 +741,34 @@ def _reset_cwd_syspath(cwd, path_to_remove): def _parse_code(bcontent, src_file, *, compiler_flags): code_ast = compile(bcontent, src_file, "exec", compiler_flags | ast.PyCF_ONLY_AST, dont_inherit=1) - if _needs_async_handling(bcontent, compiler_flags=compiler_flags): + if _needs_async_handling(bcontent, src_file, compiler_flags=compiler_flags): code_ast = _apply_async_handling(code_ast, compiler_flags=compiler_flags) return code_ast -def _needs_async_handling(bcontent, *, compiler_flags) -> bool: +def _needs_async_handling(bcontent, src_file, *, compiler_flags) -> bool: try: - compile(bcontent, "<_needs_async_handling()>", "exec", compiler_flags, dont_inherit=1) + compile(bcontent, src_file, "exec", compiler_flags, dont_inherit=1) except SyntaxError as error: - # FIXME - # bool(re.match(r"'(await|async for|async with)' outside( async)? function", str(error))) - # asynchronous comprehension outside of an asynchronous function - return "async" in str(error) - except Exception: - return False + # mkdocs-gallery supports top-level async code similar to jupyter notebooks. + # Without handling, this will raise a SyntaxError. In such a case, we apply a + # minimal async handling and try again. If the error persists, we bubble it up + # and let the caller handle it. If the error goes away, we continue with proper + # async handling below. + try: + compile( + f"async def __async_wrapper__():\n{indent(bcontent, ' ' * 4)}", + src_file, + "exec", + compiler_flags, + dont_inherit=1, + ) + except SyntaxError: + # Raise the original error to avoid leaking the internal async handling to + # generated output. + raise error from None + else: + return True else: return False @@ -953,7 +966,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) From be1af3b04bff4d3a71bd7980bc41c917cffe55dd Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Fri, 22 Dec 2023 13:34:16 +0100 Subject: [PATCH 05/17] fix wording --- src/mkdocs_gallery/gen_single.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/mkdocs_gallery/gen_single.py b/src/mkdocs_gallery/gen_single.py index ecbea70c..75f76a07 100644 --- a/src/mkdocs_gallery/gen_single.py +++ b/src/mkdocs_gallery/gen_single.py @@ -753,8 +753,7 @@ def _needs_async_handling(bcontent, src_file, *, compiler_flags) -> bool: # mkdocs-gallery supports top-level async code similar to jupyter notebooks. # Without handling, this will raise a SyntaxError. In such a case, we apply a # minimal async handling and try again. If the error persists, we bubble it up - # and let the caller handle it. If the error goes away, we continue with proper - # async handling below. + # and let the caller handle it. try: compile( f"async def __async_wrapper__():\n{indent(bcontent, ' ' * 4)}", From 031bacdddaeb0a3a85c6f4fa8f089fe5f35a6cf7 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Fri, 22 Dec 2023 14:10:27 +0100 Subject: [PATCH 06/17] fix async wrapper result --- src/mkdocs_gallery/gen_single.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/mkdocs_gallery/gen_single.py b/src/mkdocs_gallery/gen_single.py index 75f76a07..2adb1d1a 100644 --- a/src/mkdocs_gallery/gen_single.py +++ b/src/mkdocs_gallery/gen_single.py @@ -794,15 +794,9 @@ async def __async_wrapper__(): *original_body, last_node = code_ast.body if isinstance(last_node, ast.Expr): - # FIXME - # ast.Assign(targets=[ast.Name(id="__async_wrapper_result__", ctx=ast.Store())], value=last_node) - last_node = compile( - f"__async_wrapper_result__ = ({ast.unparse(last_node)})", - "<_apply_async_handling()>", - "exec", - compiler_flags | ast.PyCF_ONLY_AST, - dont_inherit=1, - ).body[0] + last_node = ast.Assign( + targets=[ast.Name(id="__async_wrapper_result__", ctx=ast.Store())], value=last_node.value + ) original_body.append(last_node) async_wrapper = async_handling.body[0] From 3106785faf01aa79115b06437a789e261ce39446 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Fri, 22 Dec 2023 14:12:15 +0100 Subject: [PATCH 07/17] revert unrelated --- src/mkdocs_gallery/gen_single.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mkdocs_gallery/gen_single.py b/src/mkdocs_gallery/gen_single.py index 2adb1d1a..0ee2f66b 100644 --- a/src/mkdocs_gallery/gen_single.py +++ b/src/mkdocs_gallery/gen_single.py @@ -959,7 +959,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) From 3be3717c7118b657dfd2ba350b08273db2476952 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Fri, 22 Dec 2023 14:27:06 +0100 Subject: [PATCH 08/17] Revert "revert unrelated" This reverts commit 3106785faf01aa79115b06437a789e261ce39446. --- src/mkdocs_gallery/gen_single.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mkdocs_gallery/gen_single.py b/src/mkdocs_gallery/gen_single.py index 0ee2f66b..2adb1d1a 100644 --- a/src/mkdocs_gallery/gen_single.py +++ b/src/mkdocs_gallery/gen_single.py @@ -959,7 +959,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) From 05eb1c4a05b939322de70d1d6b40d1d4f1ad67d6 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Fri, 22 Dec 2023 14:29:35 +0100 Subject: [PATCH 09/17] revert unrelated whitespace change --- src/mkdocs_gallery/gen_single.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mkdocs_gallery/gen_single.py b/src/mkdocs_gallery/gen_single.py index 2adb1d1a..3967e97f 100644 --- a/src/mkdocs_gallery/gen_single.py +++ b/src/mkdocs_gallery/gen_single.py @@ -959,7 +959,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) From 2cc158df6d3dfc9ae8d831934ba4a0e2a9815971 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Mon, 1 Jan 2024 23:56:41 +0100 Subject: [PATCH 10/17] write async example --- docs/examples/plot_10_async.py | 75 ++++++++++++++++++++++++---------- 1 file changed, 54 insertions(+), 21 deletions(-) diff --git a/docs/examples/plot_10_async.py b/docs/examples/plot_10_async.py index e2b26329..334c0032 100644 --- a/docs/examples/plot_10_async.py +++ b/docs/examples/plot_10_async.py @@ -1,48 +1,81 @@ """ -# Async support +# 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 +improved UX for asynchronous programming quite a bit, one major downside is that it +"poisons" your 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. + +While this might be acceptable for applications, e.g. a web framework, for scripts it is +usually a nuisance. [`jupyter` notebooks](https://jupyter.org/), or rather the +[`IPython` kernel](https://ipython.org/) running the code inside of them, have some +[background handling to allow top-level asynchronous code](https://ipython.readthedocs.io/en/stable/interactive/autoawait.html). + +And so does `mkdocs-gallery` to keep examples that require asynchronous code nice and +clean. """ import asyncio import time -start = time.time() -await asyncio.sleep(1) -stop = time.time() -f"I waited for {stop - start} seconds!" + +async def fn(): + start = time.time() + await asyncio.sleep(0.3) + stop = time.time() + return stop - start + + +f"I waited for {await fn():.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(fn()) # %% -# ## Async Iterator +# Apart from `await` all other asynchronous syntax is supported as well. +# +# ## Asynchronous Generators + -class AsyncIterator: - async def __aiter__(self): - for chunk in "I'm an async iterator!".split(): - yield chunk +async def gen(): + for chunk in "I'm an async iterator!".split(): + yield chunk -async for chunk in AsyncIterator(): +async for chunk in gen(): print(chunk, end=" ") + # %% -# ## Async comprehensions +# ## Asynchronous Comprehensions -" ".join([chunk async for chunk in AsyncIterator()]) +" ".join([chunk async for chunk in gen()]) # %% -# ## Async content manager +# ## Asynchronous Context Managers class AsyncContextManager: async def __aenter__(self): - print("Entering ...") + print("Entering asynchronous context manager!") return self async def __aexit__(self, *exc_info): - print("Exiting ...") - - def __str__(self): - return "I'm an async context manager!" + print("Exiting asynchronous context manager!") -async with AsyncContextManager() as acm: - print(acm) +async with AsyncContextManager(): + print("Inside the context!") From a2d3c77544aca71aaecb6cec1b72f74b123b5af9 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Tue, 2 Jan 2024 01:07:16 +0100 Subject: [PATCH 11/17] add tests --- tests/test_gen_single.py | 158 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 tests/test_gen_single.py diff --git a/tests/test_gen_single.py b/tests/test_gen_single.py new file mode 100644 index 00000000..2702bf8a --- /dev/null +++ b/tests/test_gen_single.py @@ -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) From 61e28fdeca355478871f521d03b06cf2f01badfa Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Tue, 2 Jan 2024 01:10:40 +0100 Subject: [PATCH 12/17] improve example --- docs/examples/plot_10_async.py | 58 ++++++++++++++++------------------ 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/docs/examples/plot_10_async.py b/docs/examples/plot_10_async.py index 334c0032..aa5a646d 100644 --- a/docs/examples/plot_10_async.py +++ b/docs/examples/plot_10_async.py @@ -2,46 +2,42 @@ # 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 -improved UX for asynchronous programming quite a bit, one major downside is that it -"poisons" your 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. - -While this might be acceptable for applications, e.g. a web framework, for scripts it is -usually a nuisance. [`jupyter` notebooks](https://jupyter.org/), or rather the -[`IPython` kernel](https://ipython.org/) running the code inside of them, have some +[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 improved UX for asynchronous programming quite a bit, one major +downside is that it "poisons" your 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. + +While this might be acceptable or not even an issue for applications, e.g. a web frameworks, for scripts it is usually +a nuisance. [`jupyter` notebooks](https://jupyter.org/), or rather the [`IPython` kernel](https://ipython.org/) running +the code inside of them, have some [background handling to allow top-level asynchronous code](https://ipython.readthedocs.io/en/stable/interactive/autoawait.html). -And so does `mkdocs-gallery` to keep examples that require asynchronous code nice and -clean. +And so does `mkdocs-gallery` to keep examples that require asynchronous code nice and clean. """ import asyncio import time -async def fn(): +async def afn(): start = time.time() await asyncio.sleep(0.3) stop = time.time() return stop - start -f"I waited for {await fn():.1f} seconds!" +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. +# 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. +# 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(fn()) +asyncio.run(afn()) # %% @@ -50,32 +46,32 @@ async def fn(): # ## Asynchronous Generators -async def gen(): +async def agen(): for chunk in "I'm an async iterator!".split(): yield chunk -async for chunk in gen(): +async for chunk in agen(): print(chunk, end=" ") # %% # ## Asynchronous Comprehensions -" ".join([chunk async for chunk in gen()]) +" ".join([chunk async for chunk in agen()]) # %% # ## Asynchronous Context Managers +import contextlib -class AsyncContextManager: - async def __aenter__(self): - print("Entering asynchronous context manager!") - return self - async def __aexit__(self, *exc_info): - print("Exiting asynchronous context manager!") +@contextlib.asynccontextmanager +async def acm(): + print("Entering asynchronous context manager!") + yield + print("Exiting asynchronous context manager!") -async with AsyncContextManager(): +async with acm(): print("Inside the context!") From 7c83812905e2f84bcc8892f255ceca747471d677 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Tue, 2 Jan 2024 01:13:57 +0100 Subject: [PATCH 13/17] up the number scheme --- .../{plot_0_sin.py => plot_00_sin.py} | 10 +++--- .../{plot_1_exp.py => plot_01_exp.py} | 15 ++++---- .../{plot_2_seaborn.py => plot_02_seaborn.py} | 2 +- ...apture_repr.py => plot_03_capture_repr.py} | 31 +++++++++-------- ...umbnail.py => plot_04_choose_thumbnail.py} | 12 +++---- ...bnail.py => plot_04b_provide_thumbnail.py} | 16 ++++----- ...where.py => plot_05_unicode_everywhere.py} | 16 ++++----- ...fier.py => plot_06_function_identifier.py} | 34 +++++++++++-------- ...plot_7_sys_argv.py => plot_07_sys_argv.py} | 9 +++-- ..._8_animations.py => plot_08_animations.py} | 4 +-- .../{plot_9_plotly.py => plot_09_plotly.py} | 32 ++++++++++------- 11 files changed, 97 insertions(+), 84 deletions(-) rename docs/examples/{plot_0_sin.py => plot_00_sin.py} (97%) rename docs/examples/{plot_1_exp.py => plot_01_exp.py} (80%) rename docs/examples/{plot_2_seaborn.py => plot_02_seaborn.py} (93%) rename docs/examples/{plot_3_capture_repr.py => plot_03_capture_repr.py} (95%) rename docs/examples/{plot_4_choose_thumbnail.py => plot_04_choose_thumbnail.py} (83%) rename docs/examples/{plot_4b_provide_thumbnail.py => plot_04b_provide_thumbnail.py} (79%) rename docs/examples/{plot_5_unicode_everywhere.py => plot_05_unicode_everywhere.py} (76%) rename docs/examples/{plot_6_function_identifier.py => plot_06_function_identifier.py} (73%) rename docs/examples/{plot_7_sys_argv.py => plot_07_sys_argv.py} (72%) rename docs/examples/{plot_8_animations.py => plot_08_animations.py} (93%) rename docs/examples/{plot_9_plotly.py => plot_09_plotly.py} (75%) diff --git a/docs/examples/plot_0_sin.py b/docs/examples/plot_00_sin.py similarity index 97% rename from docs/examples/plot_0_sin.py rename to docs/examples/plot_00_sin.py index b6c31fc1..0b3659c1 100644 --- a/docs/examples/plot_0_sin.py +++ b/docs/examples/plot_00_sin.py @@ -45,20 +45,20 @@ y = np.sin(x) plt.plot(x, y) -plt.xlabel(r'$x$') -plt.ylabel(r'$\sin(x)$') +plt.xlabel(r"$x$") +plt.ylabel(r"$\sin(x)$") # To avoid matplotlib text output plt.show() -#%% +# %% # To include embedded Markdown, use a line of >= 20 ``#``'s or ``#%%`` between # your Markdown and your code (see [syntax](../../index.md#3-add-gallery-examples)). This separates your example # into distinct text and code blocks. You can continue writing code below the # embedded Markdown text block: -print('This example shows a sin plot!') +print("This example shows a sin plot!") -#%% +# %% # LaTeX syntax in the text blocks does not require backslashes to be escaped: # # $$ diff --git a/docs/examples/plot_1_exp.py b/docs/examples/plot_01_exp.py similarity index 80% rename from docs/examples/plot_1_exp.py rename to docs/examples/plot_01_exp.py index 4c133c1a..831c0086 100644 --- a/docs/examples/plot_1_exp.py +++ b/docs/examples/plot_01_exp.py @@ -27,17 +27,18 @@ def main(): plt.figure() plt.plot(x, y) - plt.xlabel('$x$') - plt.ylabel('$\exp(x)$') - plt.title('Exponential function') + plt.xlabel("$x$") + plt.ylabel("$\exp(x)$") + plt.title("Exponential function") plt.figure() plt.plot(x, -np.exp(-x)) - plt.xlabel('$x$') - plt.ylabel('$-\exp(-x)$') - plt.title('Negative exponential\nfunction') + plt.xlabel("$x$") + plt.ylabel("$-\exp(-x)$") + plt.title("Negative exponential\nfunction") # To avoid matplotlib text output plt.show() -if __name__ == '__main__': + +if __name__ == "__main__": main() diff --git a/docs/examples/plot_2_seaborn.py b/docs/examples/plot_02_seaborn.py similarity index 93% rename from docs/examples/plot_2_seaborn.py rename to docs/examples/plot_02_seaborn.py index 4ffb7b2a..a29718b0 100644 --- a/docs/examples/plot_2_seaborn.py +++ b/docs/examples/plot_02_seaborn.py @@ -26,7 +26,7 @@ rs = np.random.RandomState(8) for _ in range(15): x = np.linspace(0, 30 / 2, 30) - y = np.sin(x) + rs.normal(0, 1.5) + rs.normal(0, .3, 30) + y = np.sin(x) + rs.normal(0, 1.5) + rs.normal(0, 0.3, 30) y_array = np.append(y_array, y) x_array = np.append(x_array, x) diff --git a/docs/examples/plot_3_capture_repr.py b/docs/examples/plot_03_capture_repr.py similarity index 95% rename from docs/examples/plot_3_capture_repr.py rename to docs/examples/plot_03_capture_repr.py index ce00fc54..74f778af 100644 --- a/docs/examples/plot_3_capture_repr.py +++ b/docs/examples/plot_03_capture_repr.py @@ -11,7 +11,7 @@ is demonstrated in this example. Differences in outputs that would be captured with other `capture_repr` settings are also explained. """ -#%% +# %% # Nothing is captured for the code block below because no data is directed to # standard output and the last statement is an assignment, not an expression. @@ -19,15 +19,15 @@ a = 2 b = 10 -#%% +# %% # If you did wish to capture the value of `b`, you would need to use: # example 2 a = 2 b = 10 -b # this is an expression +b # this is an expression -#%% +# %% # Mkdocs-Gallery first attempts to capture the `_repr_html_` of `b` as this # is the first 'representation' method in the `capture_repr` tuple. As this # method does not exist for `b`, Mkdocs-Gallery moves on and tries to capture @@ -40,10 +40,10 @@ # example 3 import pandas as pd -df = pd.DataFrame(data = {'col1': [1, 2], 'col2': [3, 4]}) +df = pd.DataFrame(data={"col1": [1, 2], "col2": [3, 4]}) df -#%% +# %% # The pandas dataframe `df` has both a `__repr__` and `_repr_html_` # method. As `_repr_html_` appears first in the `capture_repr` tuple, the # `_repr_html_` is captured in preference to `__repr__`. @@ -53,17 +53,18 @@ # example 4 import numpy as np import statsmodels.iolib.table + statsmodels.iolib.table.SimpleTable(np.zeros((3, 3))) -#%% +# %% # For the example below, there is data directed to standard output and the last # statement is an expression. # example 5 -print('Hello world') +print("Hello world") a + b -#%% +# %% # `print()` outputs to standard output, which is always captured. The # string `'Hello world'` is thus captured. A 'representation' of the last # expression is also captured. Again, since this expression `a + b` does not @@ -86,22 +87,22 @@ import matplotlib.pyplot as plt -plt.plot([1,2,3]) +plt.plot([1, 2, 3]) -#%% +# %% # To avoid capturing the text representation, you can assign the last Matplotlib # expression to a temporary variable: -_ = plt.plot([1,2,3]) +_ = plt.plot([1, 2, 3]) -#%% +# %% # Alternatively, you can add `plt.show()`, which does not return anything, # to the end of the code block: -plt.plot([1,2,3]) +plt.plot([1, 2, 3]) plt.show() -#%% +# %% # The `capture_repr` configuration # -------------------------------- # diff --git a/docs/examples/plot_4_choose_thumbnail.py b/docs/examples/plot_04_choose_thumbnail.py similarity index 83% rename from docs/examples/plot_4_choose_thumbnail.py rename to docs/examples/plot_04_choose_thumbnail.py index 8ec6d2b9..432beace 100644 --- a/docs/examples/plot_4_choose_thumbnail.py +++ b/docs/examples/plot_04_choose_thumbnail.py @@ -24,19 +24,19 @@ def main(): plt.figure() plt.plot(x, y) - plt.xlabel('$x$') - plt.ylabel('$\exp(x)$') + plt.xlabel("$x$") + plt.ylabel("$\exp(x)$") # The next line sets the thumbnail for the second figure in the gallery # (plot with negative exponential in orange) # mkdocs_gallery_thumbnail_number = 2 plt.figure() - plt.plot(x, -np.exp(-x), color='orange', linewidth=4) - plt.xlabel('$x$') - plt.ylabel('$-\exp(-x)$') + plt.plot(x, -np.exp(-x), color="orange", linewidth=4) + plt.xlabel("$x$") + plt.ylabel("$-\exp(-x)$") # To avoid matplotlib text output plt.show() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/docs/examples/plot_4b_provide_thumbnail.py b/docs/examples/plot_04b_provide_thumbnail.py similarity index 79% rename from docs/examples/plot_4b_provide_thumbnail.py rename to docs/examples/plot_04b_provide_thumbnail.py index 2f6e0d0d..49e5a547 100644 --- a/docs/examples/plot_4b_provide_thumbnail.py +++ b/docs/examples/plot_04b_provide_thumbnail.py @@ -11,11 +11,12 @@ """ import numpy as np import matplotlib.pyplot as plt + # mkdocs_gallery_thumbnail_path = '_static/demo.png' # %% -x = np.linspace(0, 4*np.pi, 301) +x = np.linspace(0, 4 * np.pi, 301) y1 = np.sin(x) y2 = np.cos(x) @@ -24,8 +25,8 @@ # ------ plt.figure() -plt.plot(x, y1, label='sin') -plt.plot(x, y2, label='cos') +plt.plot(x, y1, label="sin") +plt.plot(x, y2, label="cos") plt.legend() plt.show() @@ -34,10 +35,9 @@ # ------ plt.figure() -plt.plot(x, y1, label='sin') -plt.plot(x, y2, label='cos') +plt.plot(x, y1, label="sin") +plt.plot(x, y2, label="cos") plt.legend() -plt.xscale('log') -plt.yscale('log') +plt.xscale("log") +plt.yscale("log") plt.show() - diff --git a/docs/examples/plot_5_unicode_everywhere.py b/docs/examples/plot_05_unicode_everywhere.py similarity index 76% rename from docs/examples/plot_5_unicode_everywhere.py rename to docs/examples/plot_05_unicode_everywhere.py index 38b4f0ca..719f50a6 100644 --- a/docs/examples/plot_5_unicode_everywhere.py +++ b/docs/examples/plot_05_unicode_everywhere.py @@ -15,7 +15,7 @@ import numpy as np import matplotlib.pyplot as plt -plt.rcParams['font.size'] = 20 +plt.rcParams["font.size"] = 20 plt.rcParams["font.monospace"] = ["DejaVu Sans Mono"] plt.rcParams["font.family"] = "monospace" @@ -23,15 +23,15 @@ x = np.random.randn(100) * 2 + 1 y = np.random.randn(100) * 6 + 3 s = np.random.rand(*x.shape) * 800 + 500 -plt.scatter(x, y, s, marker=r'$\oint$') +plt.scatter(x, y, s, marker=r"$\oint$") x = np.random.randn(60) * 7 - 4 y = np.random.randn(60) * 3 - 2 -s = s[:x.size] -plt.scatter(x, y, s, alpha=0.5, c='g', marker=r'$\clubsuit$') -plt.xlabel('⇒') -plt.ylabel('⇒') -plt.title('♲' * 10) -print('Std out capture 😎') +s = s[: x.size] +plt.scatter(x, y, s, alpha=0.5, c="g", marker=r"$\clubsuit$") +plt.xlabel("⇒") +plt.ylabel("⇒") +plt.title("♲" * 10) +print("Std out capture 😎") # To avoid matplotlib text output plt.show() diff --git a/docs/examples/plot_6_function_identifier.py b/docs/examples/plot_06_function_identifier.py similarity index 73% rename from docs/examples/plot_6_function_identifier.py rename to docs/examples/plot_06_function_identifier.py index 91c0b713..946981ff 100644 --- a/docs/examples/plot_6_function_identifier.py +++ b/docs/examples/plot_06_function_identifier.py @@ -18,10 +18,10 @@ from mkdocs_gallery.backreferences import identify_names from mkdocs_gallery.py_source_parser import split_code_and_text_blocks -filename = os.__file__.replace('.pyc', '.py') +filename = os.__file__.replace(".pyc", ".py") _, script_blocks = split_code_and_text_blocks(filename) names = identify_names(script_blocks) -figheight = len(names) + .5 +figheight = len(names) + 0.5 fontsize = 12.5 @@ -40,17 +40,23 @@ fig = plt.figure(figsize=(7.5, 8)) for i, (name, obj) in enumerate(names.items()): - fig.text(0.55, (float(len(names)) - 0.5 - i) / figheight, - name, - ha="right", - size=fontsize, - transform=fig.transFigure, - bbox=dict(boxstyle='square', fc="w", ec="k")) - fig.text(0.6, (float(len(names)) - 0.5 - i) / figheight, - obj[0]["module"], - ha="left", - size=fontsize, - transform=fig.transFigure, - bbox=dict(boxstyle='larrow,pad=0.1', fc="w", ec="k")) + fig.text( + 0.55, + (float(len(names)) - 0.5 - i) / figheight, + name, + ha="right", + size=fontsize, + transform=fig.transFigure, + bbox=dict(boxstyle="square", fc="w", ec="k"), + ) + fig.text( + 0.6, + (float(len(names)) - 0.5 - i) / figheight, + obj[0]["module"], + ha="left", + size=fontsize, + transform=fig.transFigure, + bbox=dict(boxstyle="larrow,pad=0.1", fc="w", ec="k"), + ) plt.draw() diff --git a/docs/examples/plot_7_sys_argv.py b/docs/examples/plot_07_sys_argv.py similarity index 72% rename from docs/examples/plot_7_sys_argv.py rename to docs/examples/plot_07_sys_argv.py index 4ecbfc37..4f0117a2 100644 --- a/docs/examples/plot_7_sys_argv.py +++ b/docs/examples/plot_07_sys_argv.py @@ -16,8 +16,7 @@ import argparse import sys -parser = argparse.ArgumentParser(description='Toy parser') -parser.add_argument('--option', default='default', - help='a dummy optional argument') -print('sys.argv:', sys.argv) -print('parsed args:', parser.parse_args()) +parser = argparse.ArgumentParser(description="Toy parser") +parser.add_argument("--option", default="default", help="a dummy optional argument") +print("sys.argv:", sys.argv) +print("parsed args:", parser.parse_args()) diff --git a/docs/examples/plot_8_animations.py b/docs/examples/plot_08_animations.py similarity index 93% rename from docs/examples/plot_8_animations.py rename to docs/examples/plot_08_animations.py index 213027f9..83eb88f0 100644 --- a/docs/examples/plot_8_animations.py +++ b/docs/examples/plot_08_animations.py @@ -19,11 +19,11 @@ def _update_line(num): line.set_data(data[..., :num]) - return line, + return (line,) fig, ax = plt.subplots() data = np.random.RandomState(0).rand(2, 25) -line, = ax.plot([], [], 'r-') +(line,) = ax.plot([], [], "r-") ax.set(xlim=(0, 1), ylim=(0, 1)) ani = animation.FuncAnimation(fig, _update_line, 25, interval=100, blit=True) diff --git a/docs/examples/plot_9_plotly.py b/docs/examples/plot_09_plotly.py similarity index 75% rename from docs/examples/plot_9_plotly.py rename to docs/examples/plot_09_plotly.py index 200e2a6f..b59d6893 100644 --- a/docs/examples/plot_9_plotly.py +++ b/docs/examples/plot_09_plotly.py @@ -30,38 +30,44 @@ import numpy as np df = px.data.tips() -fig = px.bar(df, x='sex', y='total_bill', facet_col='day', color='smoker', barmode='group', - template='presentation+plotly' - ) +fig = px.bar( + df, x="sex", y="total_bill", facet_col="day", color="smoker", barmode="group", template="presentation+plotly" +) fig.update_layout(height=400) fig -#%% +# %% # In addition to the classical scatter or bar charts, plotly provides a large # variety of traces, such as the sunburst hierarchical trace of the following # example. plotly is an interactive library: click on one of the continents # for a more detailed view of the drill-down. df = px.data.gapminder().query("year == 2007") -fig = px.sunburst(df, path=['continent', 'country'], values='pop', - color='lifeExp', hover_data=['iso_alpha'], - color_continuous_scale='RdBu', - color_continuous_midpoint=np.average(df['lifeExp'], weights=df['pop'])) -fig.update_layout(title_text='Life expectancy of countries and continents') +fig = px.sunburst( + df, + path=["continent", "country"], + values="pop", + color="lifeExp", + hover_data=["iso_alpha"], + color_continuous_scale="RdBu", + color_continuous_midpoint=np.average(df["lifeExp"], weights=df["pop"]), +) +fig.update_layout(title_text="Life expectancy of countries and continents") fig -#%% +# %% # While plotly express is often the high-level entry point of the plotly # library, complex figures mixing different types of traces can be made # with the low-level `graph_objects` imperative API. from plotly.subplots import make_subplots import plotly.graph_objects as go -fig = make_subplots(rows=1, cols=2, specs=[[{}, {'type':'domain'}]]) + +fig = make_subplots(rows=1, cols=2, specs=[[{}, {"type": "domain"}]]) fig.add_trace(go.Bar(x=[2018, 2019, 2020], y=[3, 2, 5], showlegend=False), 1, 1) -fig.add_trace(go.Pie(labels=['A', 'B', 'C'], values=[1, 3, 6]), 1, 2) -fig.update_layout(height=400, template='presentation', yaxis_title_text='revenue') +fig.add_trace(go.Pie(labels=["A", "B", "C"], values=[1, 3, 6]), 1, 2) +fig.update_layout(height=400, template="presentation", yaxis_title_text="revenue") fig # mkdocs_gallery_thumbnail_path = '_static/plotly_logo.png' From ccb74d993fdc875c3445484e3c2bd5543123bb70 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Tue, 2 Jan 2024 01:14:53 +0100 Subject: [PATCH 14/17] Revert "up the number scheme" This reverts commit 7c83812905e2f84bcc8892f255ceca747471d677. --- .../{plot_00_sin.py => plot_0_sin.py} | 10 +++--- .../{plot_01_exp.py => plot_1_exp.py} | 15 ++++---- .../{plot_02_seaborn.py => plot_2_seaborn.py} | 2 +- ...capture_repr.py => plot_3_capture_repr.py} | 31 ++++++++--------- ...humbnail.py => plot_4_choose_thumbnail.py} | 12 +++---- ...mbnail.py => plot_4b_provide_thumbnail.py} | 16 ++++----- ...ywhere.py => plot_5_unicode_everywhere.py} | 16 ++++----- ...ifier.py => plot_6_function_identifier.py} | 34 ++++++++----------- ...plot_07_sys_argv.py => plot_7_sys_argv.py} | 9 ++--- ..._08_animations.py => plot_8_animations.py} | 4 +-- .../{plot_09_plotly.py => plot_9_plotly.py} | 32 +++++++---------- 11 files changed, 84 insertions(+), 97 deletions(-) rename docs/examples/{plot_00_sin.py => plot_0_sin.py} (97%) rename docs/examples/{plot_01_exp.py => plot_1_exp.py} (80%) rename docs/examples/{plot_02_seaborn.py => plot_2_seaborn.py} (93%) rename docs/examples/{plot_03_capture_repr.py => plot_3_capture_repr.py} (95%) rename docs/examples/{plot_04_choose_thumbnail.py => plot_4_choose_thumbnail.py} (83%) rename docs/examples/{plot_04b_provide_thumbnail.py => plot_4b_provide_thumbnail.py} (79%) rename docs/examples/{plot_05_unicode_everywhere.py => plot_5_unicode_everywhere.py} (76%) rename docs/examples/{plot_06_function_identifier.py => plot_6_function_identifier.py} (73%) rename docs/examples/{plot_07_sys_argv.py => plot_7_sys_argv.py} (72%) rename docs/examples/{plot_08_animations.py => plot_8_animations.py} (93%) rename docs/examples/{plot_09_plotly.py => plot_9_plotly.py} (75%) diff --git a/docs/examples/plot_00_sin.py b/docs/examples/plot_0_sin.py similarity index 97% rename from docs/examples/plot_00_sin.py rename to docs/examples/plot_0_sin.py index 0b3659c1..b6c31fc1 100644 --- a/docs/examples/plot_00_sin.py +++ b/docs/examples/plot_0_sin.py @@ -45,20 +45,20 @@ y = np.sin(x) plt.plot(x, y) -plt.xlabel(r"$x$") -plt.ylabel(r"$\sin(x)$") +plt.xlabel(r'$x$') +plt.ylabel(r'$\sin(x)$') # To avoid matplotlib text output plt.show() -# %% +#%% # To include embedded Markdown, use a line of >= 20 ``#``'s or ``#%%`` between # your Markdown and your code (see [syntax](../../index.md#3-add-gallery-examples)). This separates your example # into distinct text and code blocks. You can continue writing code below the # embedded Markdown text block: -print("This example shows a sin plot!") +print('This example shows a sin plot!') -# %% +#%% # LaTeX syntax in the text blocks does not require backslashes to be escaped: # # $$ diff --git a/docs/examples/plot_01_exp.py b/docs/examples/plot_1_exp.py similarity index 80% rename from docs/examples/plot_01_exp.py rename to docs/examples/plot_1_exp.py index 831c0086..4c133c1a 100644 --- a/docs/examples/plot_01_exp.py +++ b/docs/examples/plot_1_exp.py @@ -27,18 +27,17 @@ def main(): plt.figure() plt.plot(x, y) - plt.xlabel("$x$") - plt.ylabel("$\exp(x)$") - plt.title("Exponential function") + plt.xlabel('$x$') + plt.ylabel('$\exp(x)$') + plt.title('Exponential function') plt.figure() plt.plot(x, -np.exp(-x)) - plt.xlabel("$x$") - plt.ylabel("$-\exp(-x)$") - plt.title("Negative exponential\nfunction") + plt.xlabel('$x$') + plt.ylabel('$-\exp(-x)$') + plt.title('Negative exponential\nfunction') # To avoid matplotlib text output plt.show() - -if __name__ == "__main__": +if __name__ == '__main__': main() diff --git a/docs/examples/plot_02_seaborn.py b/docs/examples/plot_2_seaborn.py similarity index 93% rename from docs/examples/plot_02_seaborn.py rename to docs/examples/plot_2_seaborn.py index a29718b0..4ffb7b2a 100644 --- a/docs/examples/plot_02_seaborn.py +++ b/docs/examples/plot_2_seaborn.py @@ -26,7 +26,7 @@ rs = np.random.RandomState(8) for _ in range(15): x = np.linspace(0, 30 / 2, 30) - y = np.sin(x) + rs.normal(0, 1.5) + rs.normal(0, 0.3, 30) + y = np.sin(x) + rs.normal(0, 1.5) + rs.normal(0, .3, 30) y_array = np.append(y_array, y) x_array = np.append(x_array, x) diff --git a/docs/examples/plot_03_capture_repr.py b/docs/examples/plot_3_capture_repr.py similarity index 95% rename from docs/examples/plot_03_capture_repr.py rename to docs/examples/plot_3_capture_repr.py index 74f778af..ce00fc54 100644 --- a/docs/examples/plot_03_capture_repr.py +++ b/docs/examples/plot_3_capture_repr.py @@ -11,7 +11,7 @@ is demonstrated in this example. Differences in outputs that would be captured with other `capture_repr` settings are also explained. """ -# %% +#%% # Nothing is captured for the code block below because no data is directed to # standard output and the last statement is an assignment, not an expression. @@ -19,15 +19,15 @@ a = 2 b = 10 -# %% +#%% # If you did wish to capture the value of `b`, you would need to use: # example 2 a = 2 b = 10 -b # this is an expression +b # this is an expression -# %% +#%% # Mkdocs-Gallery first attempts to capture the `_repr_html_` of `b` as this # is the first 'representation' method in the `capture_repr` tuple. As this # method does not exist for `b`, Mkdocs-Gallery moves on and tries to capture @@ -40,10 +40,10 @@ # example 3 import pandas as pd -df = pd.DataFrame(data={"col1": [1, 2], "col2": [3, 4]}) +df = pd.DataFrame(data = {'col1': [1, 2], 'col2': [3, 4]}) df -# %% +#%% # The pandas dataframe `df` has both a `__repr__` and `_repr_html_` # method. As `_repr_html_` appears first in the `capture_repr` tuple, the # `_repr_html_` is captured in preference to `__repr__`. @@ -53,18 +53,17 @@ # example 4 import numpy as np import statsmodels.iolib.table - statsmodels.iolib.table.SimpleTable(np.zeros((3, 3))) -# %% +#%% # For the example below, there is data directed to standard output and the last # statement is an expression. # example 5 -print("Hello world") +print('Hello world') a + b -# %% +#%% # `print()` outputs to standard output, which is always captured. The # string `'Hello world'` is thus captured. A 'representation' of the last # expression is also captured. Again, since this expression `a + b` does not @@ -87,22 +86,22 @@ import matplotlib.pyplot as plt -plt.plot([1, 2, 3]) +plt.plot([1,2,3]) -# %% +#%% # To avoid capturing the text representation, you can assign the last Matplotlib # expression to a temporary variable: -_ = plt.plot([1, 2, 3]) +_ = plt.plot([1,2,3]) -# %% +#%% # Alternatively, you can add `plt.show()`, which does not return anything, # to the end of the code block: -plt.plot([1, 2, 3]) +plt.plot([1,2,3]) plt.show() -# %% +#%% # The `capture_repr` configuration # -------------------------------- # diff --git a/docs/examples/plot_04_choose_thumbnail.py b/docs/examples/plot_4_choose_thumbnail.py similarity index 83% rename from docs/examples/plot_04_choose_thumbnail.py rename to docs/examples/plot_4_choose_thumbnail.py index 432beace..8ec6d2b9 100644 --- a/docs/examples/plot_04_choose_thumbnail.py +++ b/docs/examples/plot_4_choose_thumbnail.py @@ -24,19 +24,19 @@ def main(): plt.figure() plt.plot(x, y) - plt.xlabel("$x$") - plt.ylabel("$\exp(x)$") + plt.xlabel('$x$') + plt.ylabel('$\exp(x)$') # The next line sets the thumbnail for the second figure in the gallery # (plot with negative exponential in orange) # mkdocs_gallery_thumbnail_number = 2 plt.figure() - plt.plot(x, -np.exp(-x), color="orange", linewidth=4) - plt.xlabel("$x$") - plt.ylabel("$-\exp(-x)$") + plt.plot(x, -np.exp(-x), color='orange', linewidth=4) + plt.xlabel('$x$') + plt.ylabel('$-\exp(-x)$') # To avoid matplotlib text output plt.show() -if __name__ == "__main__": +if __name__ == '__main__': main() diff --git a/docs/examples/plot_04b_provide_thumbnail.py b/docs/examples/plot_4b_provide_thumbnail.py similarity index 79% rename from docs/examples/plot_04b_provide_thumbnail.py rename to docs/examples/plot_4b_provide_thumbnail.py index 49e5a547..2f6e0d0d 100644 --- a/docs/examples/plot_04b_provide_thumbnail.py +++ b/docs/examples/plot_4b_provide_thumbnail.py @@ -11,12 +11,11 @@ """ import numpy as np import matplotlib.pyplot as plt - # mkdocs_gallery_thumbnail_path = '_static/demo.png' # %% -x = np.linspace(0, 4 * np.pi, 301) +x = np.linspace(0, 4*np.pi, 301) y1 = np.sin(x) y2 = np.cos(x) @@ -25,8 +24,8 @@ # ------ plt.figure() -plt.plot(x, y1, label="sin") -plt.plot(x, y2, label="cos") +plt.plot(x, y1, label='sin') +plt.plot(x, y2, label='cos') plt.legend() plt.show() @@ -35,9 +34,10 @@ # ------ plt.figure() -plt.plot(x, y1, label="sin") -plt.plot(x, y2, label="cos") +plt.plot(x, y1, label='sin') +plt.plot(x, y2, label='cos') plt.legend() -plt.xscale("log") -plt.yscale("log") +plt.xscale('log') +plt.yscale('log') plt.show() + diff --git a/docs/examples/plot_05_unicode_everywhere.py b/docs/examples/plot_5_unicode_everywhere.py similarity index 76% rename from docs/examples/plot_05_unicode_everywhere.py rename to docs/examples/plot_5_unicode_everywhere.py index 719f50a6..38b4f0ca 100644 --- a/docs/examples/plot_05_unicode_everywhere.py +++ b/docs/examples/plot_5_unicode_everywhere.py @@ -15,7 +15,7 @@ import numpy as np import matplotlib.pyplot as plt -plt.rcParams["font.size"] = 20 +plt.rcParams['font.size'] = 20 plt.rcParams["font.monospace"] = ["DejaVu Sans Mono"] plt.rcParams["font.family"] = "monospace" @@ -23,15 +23,15 @@ x = np.random.randn(100) * 2 + 1 y = np.random.randn(100) * 6 + 3 s = np.random.rand(*x.shape) * 800 + 500 -plt.scatter(x, y, s, marker=r"$\oint$") +plt.scatter(x, y, s, marker=r'$\oint$') x = np.random.randn(60) * 7 - 4 y = np.random.randn(60) * 3 - 2 -s = s[: x.size] -plt.scatter(x, y, s, alpha=0.5, c="g", marker=r"$\clubsuit$") -plt.xlabel("⇒") -plt.ylabel("⇒") -plt.title("♲" * 10) -print("Std out capture 😎") +s = s[:x.size] +plt.scatter(x, y, s, alpha=0.5, c='g', marker=r'$\clubsuit$') +plt.xlabel('⇒') +plt.ylabel('⇒') +plt.title('♲' * 10) +print('Std out capture 😎') # To avoid matplotlib text output plt.show() diff --git a/docs/examples/plot_06_function_identifier.py b/docs/examples/plot_6_function_identifier.py similarity index 73% rename from docs/examples/plot_06_function_identifier.py rename to docs/examples/plot_6_function_identifier.py index 946981ff..91c0b713 100644 --- a/docs/examples/plot_06_function_identifier.py +++ b/docs/examples/plot_6_function_identifier.py @@ -18,10 +18,10 @@ from mkdocs_gallery.backreferences import identify_names from mkdocs_gallery.py_source_parser import split_code_and_text_blocks -filename = os.__file__.replace(".pyc", ".py") +filename = os.__file__.replace('.pyc', '.py') _, script_blocks = split_code_and_text_blocks(filename) names = identify_names(script_blocks) -figheight = len(names) + 0.5 +figheight = len(names) + .5 fontsize = 12.5 @@ -40,23 +40,17 @@ fig = plt.figure(figsize=(7.5, 8)) for i, (name, obj) in enumerate(names.items()): - fig.text( - 0.55, - (float(len(names)) - 0.5 - i) / figheight, - name, - ha="right", - size=fontsize, - transform=fig.transFigure, - bbox=dict(boxstyle="square", fc="w", ec="k"), - ) - fig.text( - 0.6, - (float(len(names)) - 0.5 - i) / figheight, - obj[0]["module"], - ha="left", - size=fontsize, - transform=fig.transFigure, - bbox=dict(boxstyle="larrow,pad=0.1", fc="w", ec="k"), - ) + fig.text(0.55, (float(len(names)) - 0.5 - i) / figheight, + name, + ha="right", + size=fontsize, + transform=fig.transFigure, + bbox=dict(boxstyle='square', fc="w", ec="k")) + fig.text(0.6, (float(len(names)) - 0.5 - i) / figheight, + obj[0]["module"], + ha="left", + size=fontsize, + transform=fig.transFigure, + bbox=dict(boxstyle='larrow,pad=0.1', fc="w", ec="k")) plt.draw() diff --git a/docs/examples/plot_07_sys_argv.py b/docs/examples/plot_7_sys_argv.py similarity index 72% rename from docs/examples/plot_07_sys_argv.py rename to docs/examples/plot_7_sys_argv.py index 4f0117a2..4ecbfc37 100644 --- a/docs/examples/plot_07_sys_argv.py +++ b/docs/examples/plot_7_sys_argv.py @@ -16,7 +16,8 @@ import argparse import sys -parser = argparse.ArgumentParser(description="Toy parser") -parser.add_argument("--option", default="default", help="a dummy optional argument") -print("sys.argv:", sys.argv) -print("parsed args:", parser.parse_args()) +parser = argparse.ArgumentParser(description='Toy parser') +parser.add_argument('--option', default='default', + help='a dummy optional argument') +print('sys.argv:', sys.argv) +print('parsed args:', parser.parse_args()) diff --git a/docs/examples/plot_08_animations.py b/docs/examples/plot_8_animations.py similarity index 93% rename from docs/examples/plot_08_animations.py rename to docs/examples/plot_8_animations.py index 83eb88f0..213027f9 100644 --- a/docs/examples/plot_08_animations.py +++ b/docs/examples/plot_8_animations.py @@ -19,11 +19,11 @@ def _update_line(num): line.set_data(data[..., :num]) - return (line,) + return line, fig, ax = plt.subplots() data = np.random.RandomState(0).rand(2, 25) -(line,) = ax.plot([], [], "r-") +line, = ax.plot([], [], 'r-') ax.set(xlim=(0, 1), ylim=(0, 1)) ani = animation.FuncAnimation(fig, _update_line, 25, interval=100, blit=True) diff --git a/docs/examples/plot_09_plotly.py b/docs/examples/plot_9_plotly.py similarity index 75% rename from docs/examples/plot_09_plotly.py rename to docs/examples/plot_9_plotly.py index b59d6893..200e2a6f 100644 --- a/docs/examples/plot_09_plotly.py +++ b/docs/examples/plot_9_plotly.py @@ -30,44 +30,38 @@ import numpy as np df = px.data.tips() -fig = px.bar( - df, x="sex", y="total_bill", facet_col="day", color="smoker", barmode="group", template="presentation+plotly" -) +fig = px.bar(df, x='sex', y='total_bill', facet_col='day', color='smoker', barmode='group', + template='presentation+plotly' + ) fig.update_layout(height=400) fig -# %% +#%% # In addition to the classical scatter or bar charts, plotly provides a large # variety of traces, such as the sunburst hierarchical trace of the following # example. plotly is an interactive library: click on one of the continents # for a more detailed view of the drill-down. df = px.data.gapminder().query("year == 2007") -fig = px.sunburst( - df, - path=["continent", "country"], - values="pop", - color="lifeExp", - hover_data=["iso_alpha"], - color_continuous_scale="RdBu", - color_continuous_midpoint=np.average(df["lifeExp"], weights=df["pop"]), -) -fig.update_layout(title_text="Life expectancy of countries and continents") +fig = px.sunburst(df, path=['continent', 'country'], values='pop', + color='lifeExp', hover_data=['iso_alpha'], + color_continuous_scale='RdBu', + color_continuous_midpoint=np.average(df['lifeExp'], weights=df['pop'])) +fig.update_layout(title_text='Life expectancy of countries and continents') fig -# %% +#%% # While plotly express is often the high-level entry point of the plotly # library, complex figures mixing different types of traces can be made # with the low-level `graph_objects` imperative API. from plotly.subplots import make_subplots import plotly.graph_objects as go - -fig = make_subplots(rows=1, cols=2, specs=[[{}, {"type": "domain"}]]) +fig = make_subplots(rows=1, cols=2, specs=[[{}, {'type':'domain'}]]) fig.add_trace(go.Bar(x=[2018, 2019, 2020], y=[3, 2, 5], showlegend=False), 1, 1) -fig.add_trace(go.Pie(labels=["A", "B", "C"], values=[1, 3, 6]), 1, 2) -fig.update_layout(height=400, template="presentation", yaxis_title_text="revenue") +fig.add_trace(go.Pie(labels=['A', 'B', 'C'], values=[1, 3, 6]), 1, 2) +fig.update_layout(height=400, template='presentation', yaxis_title_text='revenue') fig # mkdocs_gallery_thumbnail_path = '_static/plotly_logo.png' From 5e55faaae41cf9dfb7890883b29ecc7fa71cdf5e Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Tue, 2 Jan 2024 01:16:48 +0100 Subject: [PATCH 15/17] fix example number --- docs/examples/{plot_10_async.py => plot_12_async.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/examples/{plot_10_async.py => plot_12_async.py} (100%) diff --git a/docs/examples/plot_10_async.py b/docs/examples/plot_12_async.py similarity index 100% rename from docs/examples/plot_10_async.py rename to docs/examples/plot_12_async.py From 1e09f6232485fb8de6e1bb841f02724a48d94d6f Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Tue, 2 Jan 2024 01:24:21 +0100 Subject: [PATCH 16/17] add entry to changelog --- docs/changelog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index 680bacdc..b3d415e2 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # Changelog +### 0.10.0 - Support for asynchronous code + +- Gallery scripts now support top-level asynchronous code. PR [#90](https://github.com/smarie/mkdocs-gallery/pull/90) by [pmeier](https://github.com/pmeier) + ### 0.9.0 - Pyvista - Pyvista can now be used in gallery examples as in `sphinx-gallery`. PR [#91](https://github.com/smarie/mkdocs-gallery/pull/91) by [Louis-Pujol](https://github.com/Louis-Pujol) From 8e16e816109db2da38d4fb2792d1264ac403e952 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Tue, 16 Jan 2024 11:46:34 +0100 Subject: [PATCH 17/17] Update docs/examples/plot_12_async.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sylvain Marié --- docs/examples/plot_12_async.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/examples/plot_12_async.py b/docs/examples/plot_12_async.py index aa5a646d..4aa6f3ec 100644 --- a/docs/examples/plot_12_async.py +++ b/docs/examples/plot_12_async.py @@ -3,17 +3,16 @@ [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 improved UX for asynchronous programming quite a bit, one major -downside is that it "poisons" your code base. If you want to `await` a coroutine, you have to be inside a `async def` +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. -While this might be acceptable or not even an issue for applications, e.g. a web frameworks, for scripts it is usually -a nuisance. [`jupyter` notebooks](https://jupyter.org/), or rather the [`IPython` kernel](https://ipython.org/) running -the code inside of them, have some +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). - -And so does `mkdocs-gallery` to keep examples that require asynchronous code nice and clean. """ import asyncio