8000 [WIP] sphinx_gallery_expected_error code-block directive by smarie · Pull Request #930 · sphinx-gallery/sphinx-gallery · GitHub
[go: up one dir, main page]

Skip to content

[WIP] sphinx_gallery_expected_error code-block directive #930

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ CSS pseudo-elements instead of additional real text. For more details, see

- Modify toctree to include gallery categories (ie gallery subfolders) `#904 <https://github.com/sphinx-gallery/sphinx-gallery/pull/904>`__. Instead of a flat toctree linking the main gallery README / index to all gallery items, one now links the main README to category READMEs, which themselves point to category items. In particular, this allows displaying categories of a given gallery in the sidebar (`alexisthual <https://github.com/alexisthual>`__)

- New ``sphinx_gallery_expected_error`` code block directive to show expected errors. `#912 <https://github.com/sphinx-gallery/sphinx-gallery/issues/912>`__

**Fixed bugs:**

- Display gallery items using CSS grid instead of floating `#906 <https://github.com/sphinx-gallery/sphinx-gallery/pull/906>`__, see `migration guide <https://github.com/sphinx-gallery/sphinx-gallery/pull/906#issuecomment-1019542067>`__ to adapt custom css for thumbnails (`alexisthual <https://github.com/alexisthual>`__)
Expand Down
25 changes: 25 additions & 0 deletions examples/plot_10_expected_errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
"""
Showing expected error outputs
==============================

This example demonstrates how to include expected exceptions in your gallery
examples.
"""
#%%
# The following code raises an error:

# sphinx_gallery_expected_error
# TODO 1 + "hello"

# %%
# But this runs without error:

1 * "hello"

#%%
# You may wish to restrict the type of the expected exception caught
# (This may look familiar to ``pytest`` users)

# sphinx_gallery_expected_error : TypeError
# TODO 1 + "hello"
9 changes: 7 additions & 2 deletions sphinx_gallery/backreferences.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from sphinx.errors import ExtensionError

from . import sphinx_compatibility
from .py_source_parser import CodeBlock, TextBlock
from .scrapers import _find_image_ext
from .utils import _replace_md5

Expand Down Expand Up @@ -204,15 +205,19 @@ def _get_short_module_name(module_name, obj_name):
def identify_names(script_blocks, global_variables=None, node=''):
"""Build a codeobj summary by identifying and resolving used names."""
if node == '': # mostly convenience for testing functions
c = '\n'.join(txt for kind, txt, _ in script_blocks if kind == 'code')
c = '\n'.join(
blk.contents for blk in script_blocks if isinstance(blk, CodeBlock)
)
node = ast.parse(c)
# Get matches from the code (AST, implicit matches)
finder = NameFinder(global_variables)
if node is not None:
finder.visit(node)
names = list(finder.get_mapping())
# Get matches from docstring inspection (explicit matches)
text = '\n'.join(txt for kind, txt, _ in script_blocks if kind == 'text')
text = '\n'.join(
blk.contents for blk in script_blocks if isinstance(blk, TextBlock)
)
names.extend((x, x, False, False, True) for x in re.findall(_regex, text))
example_code_obj = collections.OrderedDict() # order is important
# Make a list of all guesses, in `_embed_code_links` we will break
Expand Down
71 changes: 39 additions & 32 deletions sphinx_gallery/gen_rst.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,7 @@
from .backreferences import (_write_backreferences, _thumbnail_div,
identify_names)
from .downloads import CODE_DOWNLOAD
from .py_source_parser import (split_code_and_text_blocks,
remove_config_comments)
from .py_source_parser import split_code_and_text_blocks, CodeBlock, TextBlock

from .notebook import jupyter_notebook, save_notebook
from .binder import check_binder_conf, gen_binder_rst
Expand Down Expand Up @@ -716,9 +715,8 @@ def execute_code_block(compiler, block, example_globals, script_vars,
compiler : codeop.Compile
Compiler to compile AST of code block.

block : List[Tuple[str, str, int]]
List of Tuples, each Tuple contains label ('text' or 'code'),
the corresponding content string of block and the leading line number.
block : Block
A CodeBlock or TextBlock

example_globals: Dict[str, Any]
Global variables for examples.
Expand All @@ -740,9 +738,9 @@ def execute_code_block(compiler, block, example_globals, script_vars,
"""
if example_globals is None: # testing shortcut
example_globals = script_vars['fake_main'].__dict__
blabel, bcontent, lineno = block

# If example is not suitable to run, skip executing its blocks
if not script_vars['execute_script'] or blabel == 'text':
if not script_vars['execute_script'] or isinstance(block, TextBlock):
return ''

cwd = os.getcwd()
Expand All @@ -759,16 +757,21 @@ def execute_code_block(compiler, block, example_globals, script_vars,
sys.path.append(os.getcwd())

# Save figures unless there is a `sphinx_gallery_defer_figures` flag
match = re.search(r'^[\ \t]*#\s*sphinx_gallery_defer_figures[\ \t]*\n?',
bcontent, re.MULTILINE)
match = re.search(
r'^[\ \t]*#\s*sphinx_gallery_defer_figures[\ \t]*\n?',
block.contents,
re.MULTILINE,
)
need_save_figures = match is None

try:
ast_Module = _ast_module()
code_ast = ast_Module([bcontent])
code_ast = ast_Module([block.contents])
flags = ast.PyCF_ONLY_AST | compiler.flags
code_ast = compile(bcontent, src_file, 'exec', flags, dont_inherit=1)
ast.increment_lineno(code_ast, lineno - 1)
# warning do not change indentation here as it is used
# in handle_exception stack introspection
code_ast = compile(block.contents, src_file, 'exec', flags, dont_inherit=1) # noqa
ast.increment_lineno(code_ast, block.lineno - 1)

is_last_expr, mem_max = _exec_and_get_memory(
compiler, ast_Module, code_ast, gallery_conf, script_vars
Expand All @@ -778,6 +781,8 @@ def execute_code_block(compiler, block, example_globals, script_vars,
logging_tee.restore_std()
if need_save_figures:
need_save_figures = False
# warning do not change indentation here as it is used
# in handle_exception stack introspection
images_rst = save_figures(block, script_vars, gallery_conf)
else:
images_rst = u''
Expand All @@ -790,6 +795,8 @@ def execute_code_block(compiler, block, example_globals, script_vars,
# still call this even though we won't use the images so that
# figures are closed
if need_save_figures:
# warning do not change indentation here as it is used
# in handle_exception stack introspection
save_figures(block, script_vars, gallery_conf)
else:
_reset_cwd_syspath(cwd, sys_path)
Expand Down Expand Up @@ -842,10 +849,8 @@ def execute_script(script_blocks, script_vars, gallery_conf, file_conf):

Parameters
----------
script_blocks : list
(label, content, line_number)
List where each element is a tuple with the label ('text' or 'code'),
the corresponding content string of block and the leading line number
script_blocks : list[Block]
List where each element is a CodeBlock or TextBlock.
script_vars : dict
Configuration and run time variables
gallery_conf : dict
Expand Down Expand Up @@ -953,7 +958,7 @@ def generate_file_rst(fname, target_dir, src_dir, gallery_conf,

file_conf, script_blocks, node = split_code_and_text_blocks(
src_file, return_node=True)
intro, title = extract_intro_and_title(fname, script_blocks[0][1])
intro, title = extract_intro_and_title(fname, script_blocks[0].contents)
gallery_conf['titles'][src_file] = title

executable = executable_script(src_file, gallery_conf)
Expand Down Expand Up @@ -1011,13 +1016,13 @@ def generate_file_rst(fname, target_dir, src_dir, gallery_conf,

if gallery_conf['remove_config_comments']:
script_blocks = [
(label, remove_config_comments(content), line_number)
for label, content, line_number in script_blocks
block.remove_config_comments()
for block in script_blocks
]

# Remove final empty block, which can occur after config comments
# are removed
if script_blocks[-1][1].isspace():
if script_blocks[-1].contents.isspace():
script_blocks = script_blocks[:-1]
output_blocks = output_blocks[:-1]

Expand Down Expand Up @@ -1102,10 +1107,8 @@ def rst_blocks(script_blocks, output_blocks, file_conf, gallery_conf):

Parameters
----------
script_blocks : list
(label, content, line_number)
List where each element is a tuple with the label ('text' or 'code'),
the corresponding content string of block and the leading line number
script_blocks : list[Block]
List where each element is a CodeBlock or TextBlock.
output_blocks : list
List of strings where each element is the restructured text
representation of the output of each block
Expand All @@ -1125,21 +1128,23 @@ def rst_blocks(script_blocks, output_blocks, file_conf, gallery_conf):
# example introduction/explanation and one for the code
is_example_notebook_like = len(script_blocks) > 2
example_rst = ""
for bi, ((blabel, bcontent, lineno), code_output) in \
for bi, (blk, code_output) in \
enumerate(zip(script_blocks, output_blocks)):
# do not add comment to the title block, otherwise the linking does
# not work properly
if bi > 0:
example_rst += RST_BLOCK_HEADER.format(
lineno, lineno + bcontent.count('\n'))
if blabel == 'code':
blk.lineno, blk.lineno + blk.contents.count('\n'))
if isinstance(blk, CodeBlock):

if not file_conf.get('line_numbers',
gallery_conf.get('line_numbers', False)):
lineno = None
usedlineno = None
else:
usedlineno = blk.lineno

code_rst = codestr2rst(bcontent, lang=gallery_conf['lang'],
lineno=lineno) + '\n'
code_rst = codestr2rst(blk.contents, lang=gallery_conf['lang'],
lineno=usedlineno) + '\n'
if is_example_notebook_like:
example_rst += code_rst
example_rst += code_output
Expand All @@ -1150,8 +1155,10 @@ def rst_blocks(script_blocks, output_blocks, file_conf, gallery_conf):
example_rst += "\n\n|\n\n"
example_rst += code_rst
else:
block_separator = '\n\n' if not bcontent.endswith('\n') else '\n'
example_rst += bcontent + block_separator
block_separator = (
'\n\n' if not blk.contents.endswith('\n') else '\n'
)
example_rst += blk.contents + block_separator

return example_rst

Expand Down
21 changes: 14 additions & 7 deletions sphinx_gallery/notebook.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from sphinx.errors import ExtensionError

from . import sphinx_compatibility
from .py_source_parser import split_code_and_text_blocks
from .py_source_parser import split_code_and_text_blocks, CodeBlock
from .utils import replace_py_ipynb

logger = sphinx_compatibility.getLogger('sphinx-gallery')
Expand Down Expand Up @@ -249,22 +249,29 @@ def fill_notebook(work_notebook, script_blocks, gallery_conf, target_dir):
Parameters
----------
script_blocks : list
Each list element should be a tuple of (label, content, lineno).
Each list element should be a CodeBlock or TextBlock.
"""
heading_level_counter = count(start=1)
heading_levels = defaultdict(lambda: next(heading_level_counter))
for blabel, bcontent, lineno in script_blocks:
if blabel == 'code':
add_code_cell(work_notebook, bcontent)
for blk in script_blocks:
if isinstance(blk, CodeBlock):
add_code_cell(work_notebook, blk.contents)
else:
if gallery_conf["pypandoc"] is False:
markdown = rst2md(
bcontent + '\n', gallery_conf, target_dir, heading_levels)
blk.contents + '\n',
gallery_conf,
target_dir,
heading_levels,
)
else:
import pypandoc
# pandoc automatically addds \n to the end
markdown = pypandoc.convert_text(
bcontent, to='md', format='rst', **gallery_conf["pypandoc"]
blk.contents,
to='md',
format='rst',
**gallery_conf["pypandoc"],
)
add_markdown_cell(work_notebook, markdown)

Expand Down
Loading
0