Coverage for src/mkdocs_gallery/gen_single.py: 82%

522 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-03-15 17:10 +0000

1# Authors: Sylvain MARIE <sylvain.marie@se.com> 

2# + All contributors to <https://github.com/smarie/mkdocs-gallery> 

3# 

4# Original idea and code: sphinx-gallery, <https://sphinx-gallery.github.io> 

5# License: 3-clause BSD, <https://github.com/smarie/mkdocs-gallery/blob/master/LICENSE> 

6""" 

7Generator for a single script example in a gallery. 

8""" 

9 

10from __future__ import absolute_import, division, print_function 

11 

12import ast 

13import codeop 

14import contextlib 

15import copy 

16import gc 

17import importlib 

18import os 

19import pickle 

20import re 

21import subprocess 

22import sys 

23import traceback 

24import warnings 

25from copy import deepcopy 

26from functools import partial 

27from io import StringIO 

28from pathlib import Path 

29from shutil import copyfile 

30from textwrap import indent, dedent 

31from time import time 

32from typing import List, Set, Tuple 

33 

34from tqdm import tqdm 

35 

36from . import glr_path_static, mkdocs_compatibility 

37from .backreferences import _thumbnail_div, _write_backreferences, identify_names 

38from .binder import check_binder_conf, gen_binder_md 

39from .errors import ExtensionError 

40from .gen_data_model import GalleryBase, GalleryScript, GalleryScriptResults 

41from .notebook import jupyter_notebook, save_notebook 

42from .py_source_parser import remove_config_comments, split_code_and_text_blocks 

43from .scrapers import ImageNotFoundError, _find_image_ext, clean_modules, save_figures 

44from .utils import _new_file, _replace_by_new_if_needed, optipng, rescale_image 

45 

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

47 

48 

49############################################################################### 

50 

51 

52class _LoggingTee(object): 

53 """A tee object to redirect streams to the logger.""" 

54 

55 def __init__(self, src_filename: Path): 

56 self.logger = logger 

57 self.src_filename = src_filename 

58 self.logger_buffer = "" 

59 self.set_std_and_reset_position() 

60 

61 def set_std_and_reset_position(self): 

62 if not isinstance(sys.stdout, _LoggingTee): 

63 self.origs = (sys.stdout, sys.stderr) 

64 sys.stdout = sys.stderr = self 

65 self.first_write = True 

66 self.output = StringIO() 

67 return self 

68 

69 def restore_std(self): 

70 sys.stdout.flush() 

71 sys.stderr.flush() 

72 sys.stdout, sys.stderr = self.origs 

73 

74 def write(self, data): 

75 self.output.write(data) 

76 

77 if self.first_write: 

78 self.logger.verbose("Output from %s", self.src_filename) # color='brown') 

79 self.first_write = False 

80 

81 data = self.logger_buffer + data 

82 lines = data.splitlines() 

83 if data and data[-1] not in "\r\n": 

84 # Wait to write last line if it's incomplete. It will write next 

85 # time or when the LoggingTee is flushed. 

86 self.logger_buffer = lines[-1] 

87 lines = lines[:-1] 

88 else: 

89 self.logger_buffer = "" 

90 

91 for line in lines: 

92 self.logger.verbose("%s", line) 

93 

94 def flush(self): 

95 self.output.flush() 

96 if self.logger_buffer: 96 ↛ 97line 96 didn't jump to line 97, because the condition on line 96 was never true

97 self.logger.verbose("%s", self.logger_buffer) 

98 self.logger_buffer = "" 

99 

100 # When called from a local terminal seaborn needs it in Python3 

101 def isatty(self): 

102 return self.output.isatty() 

103 

104 # When called in gen_single, conveniently use context managing 

105 def __enter__(self): 

106 return self 

107 

108 def __exit__(self, type_, value, tb): 

109 self.restore_std() 

110 

111 

112############################################################################### 

113# The following strings are used when we have several pictures: we use 

114# an html div tag that our CSS uses to turn the lists into horizontal 

115# lists. 

116HLIST_HEADER = """ 

117{: .mkd-glr-horizontal } 

118 

119""" 

120 

121HLIST_IMAGE_TEMPLATE = """ 

122 * 

123 

124 .. image:: /%s 

125 {: .mkd-glr-multi-img } 

126""" 

127 

128SINGLE_IMAGE = """ 

129.. image:: /%s 

130 {: .mkd-glr-single-img } 

131""" 

132 

133# Note: since this seems to be a one-liner, we use inline code. TODO check 

134CODE_OUTPUT = """Out: 

135{{: .mkd-glr-script-out }} 

136 

137```{{.shell .mkd-glr-script-out-disp }} 

138{0} 

139``` 

140\n""" 

141 

142TIMING_CONTENT = """ 

143**Total running time of the script:** ({0: .0f} minutes {1: .3f} seconds) 

144""" # Strange enough: this CSS class does not actually exist in sphinx-gallery {{: .mkd-glr-timing }} 

145 

146# TODO only if html ? .. only:: html 

147MKD_GLR_SIG = """\n 

148[Gallery generated by mkdocs-gallery](https://mkdocs-gallery.github.io){: .mkd-glr-signature } 

149""" 

150 

151# Header used to include raw html from data _repr_html_ 

152HTML_HEADER = """<div class="output_subarea output_html rendered_html output_result"> 

153{0} 

154</div> 

155""" 

156 

157 

158def codestr2md(codestr, lang: str = "python", lineno=None, is_exc: bool = False): 

159 """Return markdown code block from code string.""" 

160 

161 # if lineno is not None: 

162 # # Sphinx only starts numbering from the first non-empty line. 

163 # blank_lines = codestr.count('\n', 0, -len(codestr.lstrip())) 

164 # lineno = ' :lineno-start: {0}\n'.format(lineno + blank_lines) 

165 # else: 

166 # lineno = '' 

167 # code_directive = ".. code-block:: {0}\n{1}\n".format(lang, lineno) 

168 # indented_block = indent(codestr, ' ' * 4) 

169 # return code_directive + indented_block 

170 style = " .mkd-glr-script-err-disp" if is_exc else "" 

171 if lineno is not None: 171 ↛ 174line 171 didn't jump to line 174, because the condition on line 171 was never true

172 # Sphinx only starts numbering from the first non-empty line. TODO do we need this too ? 

173 # blank_lines = codestr.count('\n', 0, -len(codestr.lstrip())) 

174 return f'```{{.{lang} {style} linenums="{lineno}"}}\n{codestr}```\n' 

175 else: 

176 return f"```{{.{lang} {style}}}\n{codestr}```\n" 

177 

178 

179def _regroup(x): 

180 x = x.groups() 

181 return x[0] + x[1].split(".")[-1] + x[2] 

182 

183 

184def _sanitize_md(string): 

185 """Use regex to remove at least some sphinx directives. 

186 

187 TODO is this still needed ? 

188 """ 

189 # :class:`a.b.c <thing here>`, :ref:`abc <thing here>` --> thing here 

190 p, e = r"(\s|^):[^:\s]+:`", r"`(\W|$)" 

191 string = re.sub(p + r"\S+\s*<([^>`]+)>" + e, r"\1\2\3", string) 

192 # :class:`~a.b.c` --> c 

193 string = re.sub(p + r"~([^`]+)" + e, _regroup, string) 

194 # :class:`a.b.c` --> a.b.c 

195 string = re.sub(p + r"([^`]+)" + e, r"\1\2\3", string) 

196 

197 # ``whatever thing`` --> whatever thing 

198 p = r"(\s|^)`" 

199 string = re.sub(p + r"`([^`]+)`" + e, r"\1\2\3", string) 

200 # `whatever thing` --> whatever thing 

201 string = re.sub(p + r"([^`]+)" + e, r"\1\2\3", string) 

202 return string 

203 

204 

205# Find RST/Markdown title chars, 

206# i.e. lines that consist of (3 or more of the same) 7-bit non-ASCII chars. 

207# This conditional is not perfect but should hopefully be good enough. 

208RE_3_OR_MORE_NON_ASCII = r"([\W _])\1{3,}" # 3 or more identical chars 

209 

210RST_TITLE_MARKER = re.compile(rf"^[ ]*{RE_3_OR_MORE_NON_ASCII}[ ]*$") 

211MD_TITLE_MARKER = re.compile(r"^[ ]*[#]+[ ]*(.*)[ ]*$") # One or more starting hash with optional whitespaces before. 

212FIRST_NON_MARKER_WITHOUT_HASH = re.compile(rf"^[# ]*(?!{RE_3_OR_MORE_NON_ASCII})[# ]*(.+)", re.MULTILINE) 

213 

214 

215def extract_readme_title(file: Path, contents: str) -> str: 

216 """Same as `extract_intro_and_title` for the readme files in galleries, but does not return the introduction. 

217 

218 Parameters 

219 ---------- 

220 file : Path 

221 The readme file path (used for error messages only). 

222 

223 contents : str 

224 The already parsed readme contents 

225 

226 Returns 

227 ------- 

228 title : str 

229 The readme title 

230 """ 

231 # Remove html comments. 

232 contents = re.sub("(<!--.*?-->)", "", contents, flags=re.DOTALL) 

233 

234 match = FIRST_NON_MARKER_WITHOUT_HASH.search(contents) 

235 if match is None: 235 ↛ 236line 235 didn't jump to line 236, because the condition on line 235 was never true

236 raise ExtensionError(f"Could not find a title in readme file: {file}") 

237 

238 title = match.group(2).strip() 

239 return title 

240 

241 

242def extract_readme_last_subtitle(file: Path, contents: str) -> str: 

243 """Same as `extract_intro_and_title` for the readme files in galleries, but does not return the introduction. 

244 

245 Parameters 

246 ---------- 

247 file : Path 

248 The readme file path (used for error messages only). 

249 

250 contents : str 

251 The already parsed readme contents 

252 

253 Returns 

254 ------- 

255 last_subtitle : str 

256 The readme last title, or None. 

257 """ 

258 paragraphs = extract_paragraphs(contents) 

259 

260 # iterate from last paragraph 

261 last_subtitle = None 

262 for p in reversed(paragraphs): 262 ↛ 283line 262 didn't jump to line 283, because the loop on line 262 didn't complete

263 current_is_good = False 

264 for line in reversed(p.splitlines()): 

265 if current_is_good: 265 ↛ 266line 265 didn't jump to line 266, because the condition on line 265 was never true

266 last_subtitle = line 

267 break 

268 # Does this line contain a title ? 

269 # - md style 

270 md_match = MD_TITLE_MARKER.search(line) 

271 if md_match: 

272 last_subtitle = md_match.group(1) 

273 break 

274 

275 # - rst style 

276 rst_match = RST_TITLE_MARKER.search(line) 

277 if rst_match: 277 ↛ 278line 277 didn't jump to line 278, because the condition on line 277 was never true

278 current_is_good = True 

279 

280 if last_subtitle: 

281 break 

282 

283 return last_subtitle 

284 

285 

286def extract_paragraphs(doc: str) -> List[str]: 

287 # lstrip is just in case docstring has a '\n\n' at the beginning 

288 paragraphs = doc.lstrip().split("\n\n") 

289 

290 # remove comments and other syntax like `.. _link:` 

291 paragraphs = [p for p in paragraphs if not p.startswith(".. ") and len(p) > 0] 

292 

293 return paragraphs 

294 

295 

296def extract_intro_and_title(docstring: str, script: GalleryScript) -> Tuple[str, str]: 

297 """Extract and clean the first paragraph of module-level docstring. 

298 

299 The title is not saved in the `script` object in this process, users have to do it explicitly. 

300 

301 Parameters 

302 ---------- 

303 docstring : str 

304 The docstring extracted from the top of the script. 

305 

306 script : GalleryScript 

307 The script where the docstring was extracted from (used for error messages only). 

308 

309 Returns 

310 ------- 

311 title : str 

312 The title 

313 

314 introduction : str 

315 The introduction 

316 """ 

317 # Extract paragraphs from the text 

318 paragraphs = extract_paragraphs(docstring) 

319 if len(paragraphs) == 0: 319 ↛ 320line 319 didn't jump to line 320, because the condition on line 319 was never true

320 raise ExtensionError( 

321 f"Example docstring should have a header for the example title. " 

322 f"Please check the example file:\n {script.script_file}\n" 

323 ) 

324 

325 # Title is the first paragraph with any RST/Markdown title chars 

326 # removed, i.e. lines that consist of (3 or more of the same) 7-bit 

327 # non-ASCII chars. 

328 # This conditional is not perfect but should hopefully be good enough. 

329 title_paragraph = paragraphs[0] 

330 match = FIRST_NON_MARKER_WITHOUT_HASH.search(title_paragraph) 

331 if match is None: 331 ↛ 332line 331 didn't jump to line 332, because the condition on line 331 was never true

332 raise ExtensionError(f"Could not find a title in first paragraph:\n{title_paragraph}") 

333 

334 title = match.group(2).strip() 

335 

336 # Use the title if no other paragraphs are provided 

337 intro_paragraph = title if len(paragraphs) < 2 else paragraphs[1] 

338 

339 # Concatenate all lines of the first paragraph 

340 intro = re.sub("\n", " ", intro_paragraph) 

341 intro = _sanitize_md(intro) 

342 

343 # Truncate at 95 chars 

344 if len(intro) > 95: 

345 intro = intro[:95] + "..." 

346 

347 return title, intro 

348 

349 

350def create_thumb_from_image(script: GalleryScript, src_image_path: Path) -> Path: 

351 """Create a thumbnail image from the `src_image_path`. 

352 

353 Parameters 

354 ---------- 

355 script : GalleryScript 

356 The gallery script. 

357 

358 src_image_path : Path 

359 The source image path, with some flexibility about the extension. 

360 TODO do we actually need this flexibility here ? 

361 

362 Returns 

363 ------- 

364 actual_thumb_file : Path 

365 The actual thumbnail file generated. 

366 """ 

367 try: 

368 # Find the image, with flexibility about the actual extenstion ('png', 'svg', 'jpg', 'gif' are supported) 

369 src_image_path, ext = _find_image_ext(src_image_path) 

370 except ImageNotFoundError: 

371 # The source image does not exist ! 

372 try: 

373 # Does a thumbnail already exist ? with extenstion ('png', 'svg', 'jpg', 'gif') 

374 thumb_file, ext = _find_image_ext(script.get_thumbnail_file(".png")) 

375 # Yes - let's assume this one will suit the needs 

376 return thumb_file 

377 except ImageNotFoundError: 

378 # Create something to replace the thumbnail 

379 default_thumb_path = script.gallery_conf.get("default_thumb_file") 

380 if default_thumb_path is None: 380 ↛ 383line 380 didn't jump to line 383, because the condition on line 380 was never false

381 default_thumb_path = os.path.join(glr_path_static(), "no_image.png") 

382 

383 src_image_path, ext = _find_image_ext(Path(default_thumb_path)) 

384 

385 # Now let's create the thumbnail. 

386 # - First Make sure the thumb dir exists 

387 script.gallery.make_thumb_dir() 

388 

389 # - Then create the thum file by copying the src image, possibly rescaling it. 

390 thumb_file = script.get_thumbnail_file(ext) 

391 if ext in (".svg", ".gif"): 

392 # No need to rescale image 

393 copyfile(src_image_path, thumb_file) 

394 else: 

395 # Need to rescale image 

396 max_width, max_hegiht = script.gallery_conf["thumbnail_size"] 

397 rescale_image( 

398 in_file=src_image_path, 

399 out_file=thumb_file, 

400 max_width=max_width, 

401 max_height=max_hegiht, 

402 ) 

403 if "thumbnails" in script.gallery_conf["compress_images"]: 403 ↛ 404line 403 didn't jump to line 404, because the condition on line 403 was never true

404 optipng(thumb_file, script.gallery_conf["compress_images_args"]) 

405 

406 return thumb_file 

407 

408 

409def generate(gallery: GalleryBase, seen_backrefs: Set) -> Tuple[str, str, str, List[GalleryScriptResults]]: 

410 """ 

411 Generate the gallery md for an example directory, including the index. 

412 

413 Parameters 

414 ---------- 

415 gallery : GalleryBase 

416 The gallery or subgallery to process 

417 

418 seen_backrefs : Set 

419 Backrefs seen so far. 

420 

421 Returns 

422 ------- 

423 title : str 

424 The gallery title, that is, the title of the readme file. 

425 

426 root_subtitle : str 

427 The gallery suptitle that will be used in case the gallery has subsections. 

428 

429 index_md : str 

430 The markdown to include in the global gallery readme. 

431 

432 results : List[GalleryScriptResults] 

433 A list of processing results for all scripts in this gallery. 

434 """ 

435 # Read the gallery readme and add it to the index 

436 readme_contents = gallery.readme_file.read_text(encoding="utf-8") 

437 readme_title = extract_readme_title(gallery.readme_file, readme_contents) 

438 if gallery.has_subsections(): 

439 # parse and try to also extract the last subtitle 

440 last_readme_subtitle = extract_readme_last_subtitle(gallery.readme_file, readme_contents) 

441 else: 

442 # Dont look for the last subtitle 

443 last_readme_subtitle = None 

444 

445 # Create the destination dir if needed 

446 gallery.make_generated_dir() 

447 

448 all_thumbnail_entries = [] 

449 results = [] 

450 

451 for script in tqdm(gallery.scripts, desc=f"generating gallery for {gallery.generated_dir}... "): 

452 # Generate all files related to this example: download file, jupyter notebook, pickle, markdown... 

453 script_results = generate_file_md(script=script, seen_backrefs=seen_backrefs) 

454 results.append(script_results) 

455 

456 # Create the thumbnails-containing div <div class="mkd-glr-thumbcontainer" ...> to place in the readme 

457 thumb_div = _thumbnail_div(script_results) 

458 all_thumbnail_entries.append(thumb_div) 

459 

460 # Write the gallery summary index.md 

461 index_md = f"""<!-- {str(gallery.generated_dir_rel_project).replace(os.path.sep, '_')} --> 

462 

463{readme_contents} 

464 

465{"".join(all_thumbnail_entries)} 

466<div class="mkd-glr-clear"></div> 

467 

468 

469""" 

470 # Note: the "clear" is to disable floating elements again, now that the gallery section is over. 

471 

472 return readme_title, last_readme_subtitle, index_md, results 

473 

474 

475def is_failing_example(script: GalleryScript): 

476 return script.src_py_file in script.gallery_conf["failing_examples"] 

477 

478 

479def handle_exception(exc_info, script: GalleryScript): 

480 """Trim and format exception, maybe raise error, etc.""" 

481 from .gen_gallery import _expected_failing_examples 

482 

483 etype, exc, tb = exc_info 

484 stack = traceback.extract_tb(tb) 

485 # The full traceback will look something like: 

486 # 

487 # File "/home/larsoner/python/mkdocs-gallery/sphinx_gallery/gen_single.py... 

488 # mem_max, _ = gallery_conf['call_memory']( 

489 # File "/home/larsoner/python/mkdocs-gallery/sphinx_gallery/gen_galler... 

490 # mem, out = memory_usage(func, max_usage=True, retval=True, 

491 # File "/home/larsoner/.local/lib/python3.8/site-packages/memory_profi... 

492 # returned = f(*args, **kw) 

493 # File "/home/larsoner/python/mkdocs-gallery/sphinx_gallery/gen_single.py... 

494 # exec(self.code, self.fake_main.__dict__) 

495 # File "/home/larsoner/python/mkdocs-gallery/sphinx_gallery/tests/tiny... 

496 # raise RuntimeError('some error') 

497 # RuntimeError: some error 

498 # 

499 # But we should trim these to just the relevant trace at the user level, 

500 # so we inspect the traceback to find the start and stop points. 

501 start = 0 

502 stop = len(stack) 

503 root = os.path.dirname(__file__) + os.sep 

504 for ii, s in enumerate(stack, 1): 

505 # Trim our internal stack 

506 if s.filename.startswith(root + "gen_gallery.py") and s.name == "call_memory": 

507 start = max(ii, start) 

508 elif s.filename.startswith(root + "gen_single.py"): 

509 # SyntaxError 

510 if s.name == "execute_code_block" and ("compile(" in s.line or "save_figures" in s.line): 510 ↛ 511line 510 didn't jump to line 511, because the condition on line 510 was never true

511 start = max(ii, start) 

512 # Any other error 

513 elif s.name == "__call__": 

514 start = max(ii, start) 

515 # Our internal input() check 

516 elif s.name == "_check_input" and ii == len(stack): 516 ↛ 517line 516 didn't jump to line 517, because the condition on line 516 was never true

517 stop = ii - 1 

518 stack = stack[start:stop] 

519 

520 formatted_exception = "Traceback (most recent call last):\n" + "".join( 

521 traceback.format_list(stack) + traceback.format_exception_only(etype, exc) 

522 ) 

523 

524 src_file = script.src_py_file 

525 expected = src_file in _expected_failing_examples( 

526 gallery_conf=script.gallery_conf, 

527 mkdocs_conf=script.gallery.all_info.mkdocs_conf, 

528 ) 

529 if expected: 529 ↛ 534line 529 didn't jump to line 534, because the condition on line 529 was never false

530 # func, color = logger.info, 'blue' 

531 func = logger.info 

532 else: 

533 # func, color = logger.warning, 'red' 

534 func = logger.warning 

535 func(f"{src_file} failed to execute correctly: {formatted_exception}") # , color=color) 

536 

537 except_md = codestr2md(formatted_exception, lang="pytb", is_exc=True) 

538 

539 # Ensure it's marked as our style: this is now already done in codestr2md 

540 # except_md = "{: .mkd-glr-script-out }\n\n" + except_md 

541 return except_md, formatted_exception 

542 

543 

544# Adapted from github.com/python/cpython/blob/3.7/Lib/warnings.py 

545def _showwarning(message, category, filename, lineno, file=None, line=None): 

546 if file is None: 

547 file = sys.stderr 

548 if file is None: 

549 # sys.stderr is None when run with pythonw.exe: 

550 # warnings get lost 

551 return 

552 text = warnings.formatwarning(message, category, filename, lineno, line) 

553 try: 

554 file.write(text) 

555 except OSError: 

556 # the file (probably stderr) is invalid - this warning gets lost. 

557 pass 

558 

559 

560@contextlib.contextmanager 

561def patch_warnings(): 

562 """Patch warnings.showwarning to actually write out the warning.""" 

563 # Sphinx or logging or someone is patching warnings, but we want to 

564 # capture them, so let's patch over their patch... 

565 orig_showwarning = warnings.showwarning 

566 try: 

567 warnings.showwarning = _showwarning 

568 yield 

569 finally: 

570 warnings.showwarning = orig_showwarning 

571 

572 

573class _exec_once(object): 

574 """Deal with memory_usage calling functions more than once (argh).""" 

575 

576 def __init__(self, code, fake_main): 

577 self.code = code 

578 self.fake_main = fake_main 

579 self.run = False 

580 

581 def __call__(self): 

582 if not self.run: 582 ↛ exitline 582 didn't return from function '__call__', because the condition on line 582 was never false

583 self.run = True 

584 old_main = sys.modules.get("__main__", None) 

585 with patch_warnings(): 

586 sys.modules["__main__"] = self.fake_main 

587 try: 

588 exec(self.code, self.fake_main.__dict__) # noqa # our purpose is to execute code :) 

589 finally: 

590 if old_main is not None: 590 ↛ exitline 590 didn't return from function '__call__', because the condition on line 590 was never false

591 sys.modules["__main__"] = old_main 

592 

593 

594def _get_memory_base(gallery_conf): 

595 """Get the base amount of memory used by running a Python process.""" 

596 if not gallery_conf["plot_gallery"]: 

597 return 0.0 

598 # There might be a cleaner way to do this at some point 

599 from memory_profiler import memory_usage 

600 

601 if sys.platform in ("win32", "darwin"): 

602 sleep, timeout = (1, 2) 

603 else: 

604 sleep, timeout = (0.5, 1) 

605 proc = subprocess.Popen( 

606 [sys.executable, "-c", "import time, sys; time.sleep(%s); sys.exit(0)" % sleep], 

607 close_fds=True, 

608 ) 

609 memories = memory_usage(proc, interval=1e-3, timeout=timeout) 

610 kwargs = dict(timeout=timeout) if sys.version_info >= (3, 5) else {} 

611 proc.communicate(**kwargs) 

612 # On OSX sometimes the last entry can be None 

613 memories = [mem for mem in memories if mem is not None] + [0.0] 

614 memory_base = max(memories) 

615 return memory_base 

616 

617 

618def _ast_module(): 

619 """Get ast.Module function, dealing with: 

620 https://bugs.python.org/issue35894""" 

621 if sys.version_info >= (3, 8): 621 ↛ 624line 621 didn't jump to line 624, because the condition on line 621 was never false

622 ast_Module = partial(ast.Module, type_ignores=[]) 

623 else: 

624 ast_Module = ast.Module 

625 return ast_Module 

626 

627 

628def _check_reset_logging_tee(src_file: Path): 

629 # Helper to deal with our tests not necessarily calling parse_and_execute 

630 # but rather execute_code_block directly 

631 if isinstance(sys.stdout, _LoggingTee): 631 ↛ 634line 631 didn't jump to line 634, because the condition on line 631 was never false

632 logging_tee = sys.stdout 

633 else: 

634 logging_tee = _LoggingTee(src_file) 

635 logging_tee.set_std_and_reset_position() 

636 return logging_tee 

637 

638 

639def _exec_and_get_memory(compiler, ast_Module, code_ast, script: GalleryScript): 

640 """Execute ast, capturing output if last line is expression and get max memory usage.""" 

641 

642 src_file = script.src_py_file.as_posix() 

643 

644 # capture output if last line is expression 

645 is_last_expr = False 

646 

647 if len(code_ast.body) and isinstance(code_ast.body[-1], ast.Expr): 

648 is_last_expr = True 

649 last_val = code_ast.body.pop().value 

650 # exec body minus last expression 

651 mem_body, _ = script.gallery_conf["call_memory"]( 

652 _exec_once(compiler(code_ast, src_file, "exec"), script.run_vars.fake_main) 

653 ) 

654 # exec last expression, made into assignment 

655 body = [ast.Assign(targets=[ast.Name(id="___", ctx=ast.Store())], value=last_val)] 

656 last_val_ast = ast_Module(body=body) 

657 ast.fix_missing_locations(last_val_ast) 

658 mem_last, _ = script.gallery_conf["call_memory"]( 

659 _exec_once(compiler(last_val_ast, src_file, "exec"), script.run_vars.fake_main) 

660 ) 

661 mem_max = max(mem_body, mem_last) 

662 else: 

663 mem_max, _ = script.gallery_conf["call_memory"]( 

664 _exec_once(compiler(code_ast, src_file, "exec"), script.run_vars.fake_main) 

665 ) 

666 

667 return is_last_expr, mem_max 

668 

669 

670def _get_last_repr(gallery_conf, ___): 

671 """Get a repr of the last expression, using first method in 'capture_repr' 

672 available for the last expression.""" 

673 for meth in gallery_conf["capture_repr"]: 673 ↛ 687line 673 didn't jump to line 687, because the loop on line 673 didn't complete

674 try: 

675 last_repr = getattr(___, meth)() 

676 # for case when last statement is print() 

677 if last_repr is None or last_repr == "None": 

678 repr_meth = None 

679 else: 

680 repr_meth = meth 

681 except Exception: 

682 last_repr = None 

683 repr_meth = None 

684 else: 

685 if isinstance(last_repr, str): 685 ↛ 673line 685 didn't jump to line 673, because the condition on line 685 was never false

686 break 

687 return last_repr, repr_meth 

688 

689 

690def _get_code_output(is_last_expr, script: GalleryScript, logging_tee, images_md): 

691 """Obtain standard output and html output in md.""" 

692 

693 example_globals = script.run_vars.example_globals 

694 gallery_conf = script.gallery_conf 

695 

696 last_repr = None 

697 repr_meth = None 

698 if is_last_expr: 

699 # capture the last repr variable 

700 ___ = example_globals["___"] 

701 ignore_repr = False 

702 if gallery_conf["ignore_repr_types"]: 702 ↛ 703line 702 didn't jump to line 703, because the condition on line 702 was never true

703 ignore_repr = re.search(gallery_conf["ignore_repr_types"], str(type(___))) 

704 if gallery_conf["capture_repr"] != () and not ignore_repr: 704 ↛ 707line 704 didn't jump to line 707, because the condition on line 704 was never false

705 last_repr, repr_meth = _get_last_repr(gallery_conf, ___) 

706 

707 captured_std = logging_tee.output.getvalue().expandtabs() 

708 

709 # normal string output 

710 if repr_meth in ["__repr__", "__str__"] and last_repr: 

711 captured_std = f"{captured_std}\n{last_repr}" 

712 

713 if captured_std and not captured_std.isspace(): 

714 captured_std = CODE_OUTPUT.format(captured_std) 

715 else: 

716 captured_std = "" 

717 

718 # give html output its own header 

719 if repr_meth == "_repr_html_": 

720 captured_html = HTML_HEADER.format(indent(last_repr, " " * 4)) 

721 else: 

722 captured_html = "" 

723 

724 code_output = f""" 

725{images_md} 

726 

727{captured_std} 

728 

729{captured_html} 

730 

731""" 

732 return code_output 

733 

734 

735def _reset_cwd_syspath(cwd, path_to_remove): 

736 """Reset current working directory to `cwd` and remove `path_to_remove` from `sys.path`.""" 

737 if path_to_remove in sys.path: 

738 sys.path.remove(path_to_remove) 

739 os.chdir(cwd) 

740 

741 

742def _parse_code(bcontent, src_file, *, compiler_flags): 

743 code_ast = compile(bcontent, src_file, "exec", compiler_flags | ast.PyCF_ONLY_AST, dont_inherit=1) 

744 if _needs_async_handling(bcontent, src_file, compiler_flags=compiler_flags): 744 ↛ 745line 744 didn't jump to line 745, because the condition on line 744 was never true

745 code_ast = _apply_async_handling(code_ast, compiler_flags=compiler_flags) 

746 return code_ast 

747 

748 

749def _needs_async_handling(bcontent, src_file, *, compiler_flags) -> bool: 

750 try: 

751 compile(bcontent, src_file, "exec", compiler_flags, dont_inherit=1) 

752 except SyntaxError as error: 

753 # mkdocs-gallery supports top-level async code similar to jupyter notebooks. 

754 # Without handling, this will raise a SyntaxError. In such a case, we apply a 

755 # minimal async handling and try again. If the error persists, we bubble it up 

756 # and let the caller handle it. 

757 try: 

758 compile( 

759 f"async def __async_wrapper__():\n{indent(bcontent, ' ' * 4)}", 

760 src_file, 

761 "exec", 

762 compiler_flags, 

763 dont_inherit=1, 

764 ) 

765 except SyntaxError: 

766 # Raise the original error to avoid leaking the internal async handling to 

767 # generated output. 

768 raise error from None 

769 else: 

770 return True 

771 else: 

772 return False 

773 

774 

775def _apply_async_handling(code_ast, *, compiler_flags): 

776 async_handling = compile( 

777 dedent( 

778 """ 

779 async def __async_wrapper__(): 

780 # original AST goes here 

781 return locals() 

782 import asyncio as __asyncio__ 

783 __async_wrapper_locals__ = __asyncio__.run(__async_wrapper__()) 

784 __async_wrapper_result__ = __async_wrapper_locals__.pop("__async_wrapper_result__", None) 

785 globals().update(__async_wrapper_locals__) 

786 __async_wrapper_result__ 

787 """ 

788 ), 

789 "<_apply_async_handling()>", 

790 "exec", 

791 compiler_flags | ast.PyCF_ONLY_AST, 

792 dont_inherit=1, 

793 ) 

794 

795 *original_body, last_node = code_ast.body 

796 if isinstance(last_node, ast.Expr): 

797 last_node = ast.Assign( 

798 targets=[ast.Name(id="__async_wrapper_result__", ctx=ast.Store())], value=last_node.value 

799 ) 

800 original_body.append(last_node) 

801 

802 async_wrapper = async_handling.body[0] 

803 async_wrapper.body = [*original_body, *async_wrapper.body] 

804 

805 return ast.fix_missing_locations(async_handling) 

806 

807 

808def execute_code_block(compiler, block, script: GalleryScript): 

809 """Execute the code block of the example file. 

810 

811 Parameters 

812 ---------- 

813 compiler : codeop.Compile 

814 Compiler to compile AST of code block. 

815 

816 block : List[Tuple[str, str, int]] 

817 List of Tuples, each Tuple contains label ('text' or 'code'), 

818 the corresponding content string of block and the leading line number. 

819 

820 script: GalleryScript 

821 The gallery script 

822 

823 Returns 

824 ------- 

825 code_output : str 

826 Output of executing code in md. 

827 """ 

828 # if script.run_vars.example_globals is None: # testing shortcut 

829 # script.run_vars.example_globals = script.run_vars.fake_main.__dict__ 

830 

831 blabel, bcontent, lineno = block 

832 

833 # If example is not suitable to run anymore, skip executing its blocks 

834 if script.run_vars.stop_executing or blabel == "text": 

835 return "" 

836 

837 cwd = os.getcwd() 

838 # Redirect output to stdout 

839 src_file = script.src_py_file 

840 logging_tee = _check_reset_logging_tee(src_file) 

841 assert isinstance(logging_tee, _LoggingTee) # noqa 

842 

843 # First cd in the original example dir, so that any file 

844 # created by the example get created in this directory 

845 os.chdir(src_file.parent) 

846 

847 # Add the example dir to the path temporarily (will be removed after execution) 

848 new_path = os.getcwd() 

849 sys.path.append(new_path) 

850 

851 # Save figures unless there is a `mkdocs_gallery_defer_figures` flag 

852 match = re.search(r"^[\ \t]*#\s*mkdocs_gallery_defer_figures[\ \t]*\n?", bcontent, re.MULTILINE) 

853 need_save_figures = match is None 

854 

855 try: 

856 ast_Module = _ast_module() 

857 code_ast = _parse_code(bcontent, src_file, compiler_flags=compiler.flags) 

858 ast.increment_lineno(code_ast, lineno - 1) 

859 

860 is_last_expr, mem_max = _exec_and_get_memory(compiler, ast_Module, code_ast, script=script) 

861 script.run_vars.memory_used_in_blocks.append(mem_max) 

862 

863 # This should be inside the try block, e.g., in case of a savefig error 

864 logging_tee.restore_std() 

865 if need_save_figures: 865 ↛ 869line 865 didn't jump to line 869, because the condition on line 865 was never false

866 need_save_figures = False 

867 images_md = save_figures(block, script) 

868 else: 

869 images_md = "" 

870 

871 except Exception: 

872 logging_tee.restore_std() 

873 except_md, formatted_exception = handle_exception(sys.exc_info(), script) 

874 

875 # Breaks build on first example error 

876 if script.gallery_conf["abort_on_example_error"]: 876 ↛ 877line 876 didn't jump to line 877, because the condition on line 876 was never true

877 raise 

878 

879 # Stores failing file 

880 script.gallery_conf["failing_examples"][src_file] = formatted_exception 

881 

882 # Stop further execution on that script 

883 script.run_vars.stop_executing = True 

884 

885 code_output = "\n{0}\n\n\n\n".format(except_md) 

886 # still call this even though we won't use the images so that 

887 # figures are closed 

888 if need_save_figures: 888 ↛ 895line 888 didn't jump to line 895, because the condition on line 888 was never false

889 save_figures(block, script) 

890 else: 

891 _reset_cwd_syspath(cwd, new_path) 

892 

893 code_output = _get_code_output(is_last_expr, script, logging_tee, images_md) 

894 finally: 

895 _reset_cwd_syspath(cwd, new_path) 

896 logging_tee.restore_std() 896 ↛ exitline 896 didn't except from function 'execute_code_block', because the raise on line 877 wasn't executed

897 

898 # Sanitize ANSI escape characters from MD output 

899 ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") 

900 code_output = ansi_escape.sub("", code_output) 

901 

902 return code_output 

903 

904 

905def _check_input(prompt=None): 

906 raise ExtensionError("Cannot use input() builtin function in mkdocs-gallery examples") 

907 

908 

909def parse_and_execute(script: GalleryScript, script_blocks): 

910 """Execute and capture output from python script already in block structure 

911 

912 Parameters 

913 ---------- 

914 script : GalleryScript 

915 The script 

916 

917 script_blocks : list 

918 (label, content, line_number) 

919 List where each element is a tuple with the label ('text' or 'code'), 

920 the corresponding content string of block and the leading line number 

921 

922 Returns 

923 ------- 

924 output_blocks : list 

925 List of strings where each element is the restructured text 

926 representation of the output of each block 

927 

928 time_elapsed : float 

929 Time elapsed during execution 

930 

931 memory_used : float 

932 Memory used during execution 

933 """ 

934 # Examples may contain if __name__ == '__main__' guards for in example scikit-learn if the example uses 

935 # multiprocessing. Here we create a new __main__ module, and temporarily change sys.modules when running our example 

936 fake_main = importlib.util.module_from_spec(importlib.util.spec_from_loader("__main__", None)) 

937 script.run_vars.fake_main = fake_main 

938 

939 example_globals = fake_main.__dict__ 

940 example_globals.update( 

941 { 

942 # A lot of examples contains 'print(__doc__)' for example in 

943 # scikit-learn so that running the example prints some useful 

944 # information. Because the docstring has been separated from 

945 # the code blocks in mkdocs-gallery, __doc__ is actually 

946 # __builtin__.__doc__ in the execution context and we do not 

947 # want to print it 

948 "__doc__": "", 

949 # Don't ever support __file__: Issues #166 #212 

950 # Don't let them use input() 

951 "input": _check_input, 

952 } 

953 ) 

954 script.run_vars.example_globals = example_globals 

955 

956 # Manipulate the sys.argv before running the example 

957 # See https://github.com/sphinx-gallery/sphinx-gallery/pull/252 

958 

959 # Remember the original argv so that we can put them back after run 

960 argv_orig = sys.argv[:] 

961 

962 # Remember the original sys.path so that we can reset it after run 

963 sys_path_orig = deepcopy(sys.path) 

964 

965 # Python file is the original one (not the copy for download) 

966 sys.argv[0] = script.src_py_file.as_posix() 

967 

968 # Allow users to provide additional args through the 'reset_argv' option 

969 sys.argv[1:] = script.gallery_conf["reset_argv"](script) 

970 

971 # Perform a garbage collection before starting so that perf kpis are accurate (memory and time) 

972 gc.collect() 

973 

974 # Initial memory used 

975 memory_start, _ = script.gallery_conf["call_memory"](lambda: None) 

976 script.run_vars.memory_used_in_blocks = [memory_start] # include at least one entry to avoid max() ever failing 

977 

978 t_start = time() 

979 compiler = codeop.Compile() 

980 

981 # Execute block by block 

982 output_blocks = list() 

983 with _LoggingTee(script.src_py_file) as logging_tee: 

984 for block in script_blocks: 

985 logging_tee.set_std_and_reset_position() 

986 output_blocks.append(execute_code_block(compiler, block, script)) 

987 

988 # Compute the elapsed time 

989 time_elapsed = time() - t_start 

990 

991 # Set back the sys argv 

992 sys.argv = argv_orig 

993 

994 # Set back the sys path 

995 sys.path = sys_path_orig 

996 

997 # Write md5 checksum if the example was meant to run (no-plot shall not cache md5sum) and has built correctly 

998 script.write_final_md5_file() 

999 

1000 # Declare the example as "passing" 

1001 script.gallery_conf["passing_examples"].append(script) 

1002 

1003 script.run_vars.memory_delta = max(script.run_vars.memory_used_in_blocks) - memory_start 

1004 memory_used = script.gallery_conf["memory_base"] + script.run_vars.memory_delta 

1005 

1006 return output_blocks, time_elapsed, memory_used 

1007 

1008 

1009def generate_file_md(script: GalleryScript, seen_backrefs=None) -> GalleryScriptResults: 

1010 """Generate the md file for a given example. 

1011 

1012 Parameters 

1013 ---------- 

1014 script : GalleryScript 

1015 The script to process 

1016 

1017 seen_backrefs : set 

1018 The seen backreferences. 

1019 

1020 Returns 

1021 ------- 

1022 result: FileResult 

1023 The result of running this script 

1024 """ 

1025 seen_backrefs = set() if seen_backrefs is None else seen_backrefs 

1026 

1027 # Extract the contents of the script 

1028 file_conf, script_blocks, node = split_code_and_text_blocks(script.src_py_file, return_node=True) 

1029 

1030 # Extract the title and introduction from the module docstring and save the title in the object 

1031 script.title, intro = extract_intro_and_title(docstring=script_blocks[0][1], script=script) 

1032 

1033 # Copy source python script to target folder if it is not there/up to date, so that it can be served/downloaded 

1034 # Note: surprisingly this uses a md5 too, but not the final .md5 persisted on disk. 

1035 script.make_dwnld_py_file() 

1036 

1037 # Can the script be entirely skipped (both doc generation and execution) ? 

1038 if not script.has_changed_wrt_persisted_md5(): 

1039 # A priori we can... 

1040 skip_and_return = True 

1041 

1042 # ...however for executables (not shared modules) we might need to run anyway because of config 

1043 if script.is_executable_example(): 1043 ↛ 1055line 1043 didn't jump to line 1055, because the condition on line 1043 was never false

1044 if script.gallery_conf["run_stale_examples"]: 1044 ↛ 1046line 1044 didn't jump to line 1046, because the condition on line 1044 was never true

1045 # Run anyway because config says so. 

1046 skip_and_return = False 

1047 else: 

1048 # Add the example to the "stale examples" before returning 

1049 script.gallery_conf["stale_examples"].append(script.dwnld_py_file) 

1050 # If expected to fail, let's remove it from the 'expected_failing_examples' list, 

1051 # assuming it did when previously executed 

1052 if script.src_py_file in script.gallery_conf["expected_failing_examples"]: 

1053 script.gallery_conf["expected_failing_examples"].remove(script.src_py_file) 

1054 

1055 if skip_and_return: 1055 ↛ 1062line 1055 didn't jump to line 1062, because the condition on line 1055 was never false

1056 # Return with 0 exec time and mem usage, and the existing thumbnail 

1057 thumb_source_path = script.get_thumbnail_source(file_conf) 

1058 thumb_file = create_thumb_from_image(script, thumb_source_path) 

1059 return GalleryScriptResults(script=script, intro=intro, exec_time=0.0, memory=0.0, thumb=thumb_file) 

1060 

1061 # Reset matplotlib, seaborn, etc. if needed 

1062 if script.is_executable_example(): 

1063 clean_modules(gallery_conf=script.gallery_conf, file=script.src_py_file) 

1064 

1065 # Init the runtime vars. Create the images directory and init the image files template 

1066 script.init_before_processing() 

1067 

1068 if script.is_executable_example(): 

1069 # Note: this writes the md5 checksum if the example was meant to run 

1070 output_blocks, time_elapsed, memory_used = parse_and_execute(script, script_blocks) 

1071 logger.debug(f"{script.src_py_file} ran in : {time_elapsed:.2g} seconds\n") 

1072 else: 

1073 output_blocks = [""] * len(script_blocks) 

1074 time_elapsed = memory_used = 0.0 # don't let the output change 

1075 logger.debug(f"{script.src_py_file} parsed (not executed)\n") 

1076 

1077 # Create as many dummy images as required if needed (default none) so that references to script images 

1078 # Can still work, even if the script was not executed (in development mode typically, to go fast). 

1079 # See https://sphinx-gallery.github.io/stable/configuration.html#generating-dummy-images 

1080 nb_dummy_images_to_generate = file_conf.get("dummy_images", None) 

1081 if nb_dummy_images_to_generate is not None: 1081 ↛ 1082line 1081 didn't jump to line 1082, because the condition on line 1081 was never true

1082 if type(nb_dummy_images_to_generate) is not int: 

1083 raise ExtensionError("mkdocs_gallery: 'dummy_images' setting is not a number, got {dummy_image!r}") 

1084 

1085 stock_img = os.path.join(glr_path_static(), "no_image.png") 

1086 script.generate_n_dummy_images(img=stock_img, nb=nb_dummy_images_to_generate) 

1087 

1088 # Remove the mkdocs-gallery configuration comments from the script if needed 

1089 if script.gallery_conf["remove_config_comments"]: 1089 ↛ 1090line 1089 didn't jump to line 1090, because the condition on line 1089 was never true

1090 script_blocks = [ 

1091 (label, remove_config_comments(content), line_number) for label, content, line_number in script_blocks 

1092 ] 

1093 

1094 # Remove final empty block, which can occur after config comments are removed 

1095 if script_blocks[-1][1].isspace(): 1095 ↛ 1096line 1095 didn't jump to line 1096, because the condition on line 1095 was never true

1096 script_blocks = script_blocks[:-1] 

1097 output_blocks = output_blocks[:-1] 

1098 

1099 # Generate the markdown string containing the script prose, code and output. 

1100 example_md = generate_md_from_blocks(script_blocks, output_blocks, file_conf, script.gallery_conf) 

1101 

1102 # Write the generated markdown file 

1103 md_header, md_footer = get_example_md_wrapper(script, time_elapsed, memory_used) 

1104 full_md = md_header + example_md + md_footer 

1105 script.save_md_example(full_md) 

1106 

1107 # Create the image thumbnail for the gallery summary 

1108 if is_failing_example(script): 

1109 # Failing example thumbnail 

1110 thumb_source_path = Path(os.path.join(glr_path_static(), "broken_example.png")) 

1111 else: 

1112 # Get the thumbnail source image, possibly from config 

1113 thumb_source_path = script.get_thumbnail_source(file_conf) 

1114 

1115 thumb_file = create_thumb_from_image(script, thumb_source_path) 

1116 

1117 # Generate the jupyter notebook 

1118 example_nb = jupyter_notebook(script, script_blocks) 

1119 ipy_file = _new_file(script.ipynb_file) 

1120 save_notebook(example_nb, ipy_file) 

1121 _replace_by_new_if_needed(ipy_file, md5_mode="t") 

1122 

1123 # Write names 

1124 if script.gallery_conf["inspect_global_variables"]: 1124 ↛ 1127line 1124 didn't jump to line 1127, because the condition on line 1124 was never false

1125 global_variables = script.run_vars.example_globals 

1126 else: 

1127 global_variables = None 

1128 

1129 # TODO dig in just in case 

1130 example_code_obj = identify_names(script_blocks, global_variables, node) 

1131 if example_code_obj: 

1132 # Write a pickle file (.pickle) containing `example_code_obj` 

1133 codeobj_fname = _new_file(script.codeobj_file) 

1134 with open(codeobj_fname, "wb") as fid: 

1135 pickle.dump(example_code_obj, fid, pickle.HIGHEST_PROTOCOL) 

1136 _replace_by_new_if_needed(codeobj_fname) 

1137 

1138 backrefs = set( 

1139 "{module_short}.{name}".format(**cobj) 

1140 for cobjs in example_code_obj.values() 

1141 for cobj in cobjs 

1142 if cobj["module"].startswith(script.gallery_conf["doc_module"]) 

1143 ) 

1144 

1145 # Create results object 

1146 res = GalleryScriptResults( 

1147 script=script, 

1148 intro=intro, 

1149 exec_time=time_elapsed, 

1150 memory=memory_used, 

1151 thumb=thumb_file, 

1152 ) 

1153 

1154 # Write backreferences if required 

1155 if script.gallery_conf["backreferences_dir"] is not None: 1155 ↛ 1158line 1155 didn't jump to line 1158, because the condition on line 1155 was never false

1156 _write_backreferences(backrefs, seen_backrefs, script_results=res) 

1157 

1158 return res 

1159 

1160 

1161# TODO the note should only appear in html mode. (.. only:: html) 

1162# TODO maybe remove as much as possible the css for now? 

1163EXAMPLE_HEADER = """ 

1164<!-- 

1165 DO NOT EDIT. 

1166 THIS FILE WAS AUTOMATICALLY GENERATED BY mkdocs-gallery. 

1167 TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: 

1168 "{pyfile_to_edit}" 

1169 LINE NUMBERS ARE GIVEN BELOW. 

1170--> 

1171 

1172!!! note 

1173 

1174 Click [here](#download_links) 

1175 to download the full example code{opt_binder_text} 

1176 

1177""" # TODO there was a {{: .mkd-glr-example-title }} for the title but is it useful ? 

1178MD_BLOCK_HEADER = """\ 

1179<!-- GENERATED FROM PYTHON SOURCE LINES {0}-{1} --> 

1180 

1181""" 

1182 

1183 

1184def generate_md_from_blocks(script_blocks, output_blocks, file_conf, gallery_conf) -> str: 

1185 """Generate the md string containing the script prose, code and output. 

1186 

1187 Parameters 

1188 ---------- 

1189 script_blocks : list 

1190 (label, content, line_number) 

1191 List where each element is a tuple with the label ('text' or 'code'), 

1192 the corresponding content string of block and the leading line number 

1193 

1194 output_blocks : list 

1195 List of strings where each element is the restructured text 

1196 representation of the output of each block 

1197 

1198 file_conf : dict 

1199 File-specific settings given in source file comments as: 

1200 ``# mkdocs_gallery_<name> = <value>`` 

1201 

1202 gallery_conf : dict 

1203 Contains the configuration of mkdocs-gallery 

1204 

1205 Returns 

1206 ------- 

1207 out : str 

1208 The resulting markdown page. 

1209 """ 

1210 

1211 # A simple example has two blocks: one for the 

1212 # example introduction/explanation and one for the code 

1213 is_example_notebook_like = len(script_blocks) > 2 

1214 example_md = "" 

1215 for bi, ((blabel, bcontent, lineno), code_output) in enumerate(zip(script_blocks, output_blocks)): 

1216 # do not add comment to the title block (bi=0), otherwise the linking does not work properly 

1217 if bi > 0: 

1218 example_md += MD_BLOCK_HEADER.format(lineno, lineno + bcontent.count("\n")) 

1219 

1220 if blabel == "code": 

1221 if not file_conf.get("line_numbers", gallery_conf.get("line_numbers", False)): 1221 ↛ 1224line 1221 didn't jump to line 1224, because the condition on line 1221 was never false

1222 lineno = None 

1223 

1224 code_md = codestr2md(bcontent, lang=gallery_conf["lang"], lineno=lineno) + "\n" 

1225 if is_example_notebook_like: 

1226 example_md += code_md 

1227 example_md += code_output 

1228 else: 

1229 example_md += code_output 

1230 if "mkd-glr-script-out" in code_output: 

1231 # Add some vertical space after output 

1232 example_md += "\n\n<br />\n\n" # "|\n\n" 

1233 example_md += code_md 

1234 else: 

1235 block_separator = "\n\n" if not bcontent.endswith("\n") else "\n" 

1236 example_md += bcontent + block_separator 

1237 

1238 return example_md 

1239 

1240 

1241def get_example_md_wrapper(script: GalleryScript, time_elapsed: float, memory_used: float) -> Tuple[str, str]: 

1242 """Creates the headers and footers for the example markdown. Returns a template 

1243 

1244 Parameters 

1245 ---------- 

1246 script : GalleryScript 

1247 The script for which to generate the md. 

1248 

1249 time_elapsed : float 

1250 Time elapsed in seconds while executing file 

1251 

1252 memory_used : float 

1253 Additional memory used during the run. 

1254 

1255 Returns 

1256 ------- 

1257 md_before : str 

1258 Part of the final markdown that goes before the notebook / python script. 

1259 

1260 md_after : str 

1261 Part of the final markdown that goes after the notebook / python script. 

1262 """ 

1263 # Check binder configuration 

1264 binder_conf = check_binder_conf(script.gallery_conf.get("binder")) 

1265 use_binder = len(binder_conf) > 0 

1266 

1267 # Write header 

1268 src_relative = script.src_py_file_rel_project.as_posix() 

1269 binder_text = " or to run this example in your browser via Binder" if use_binder else "" 

1270 md_before = EXAMPLE_HEADER.format(pyfile_to_edit=src_relative, opt_binder_text=binder_text) 

1271 

1272 # Footer 

1273 md_after = "" 

1274 # Report Time and Memory 

1275 if time_elapsed >= script.gallery_conf["min_reported_time"]: 1275 ↛ 1279line 1275 didn't jump to line 1279, because the condition on line 1275 was never false

1276 time_m, time_s = divmod(time_elapsed, 60) 

1277 md_after += TIMING_CONTENT.format(time_m, time_s) 

1278 

1279 if script.gallery_conf["show_memory"]: 1279 ↛ 1280line 1279 didn't jump to line 1280, because the condition on line 1279 was never true

1280 md_after += f"**Estimated memory usage:** {memory_used:.0f} MB\n\n" 

1281 

1282 # Download buttons 

1283 # - Generate a binder URL if specified 

1284 binder_badge_md = gen_binder_md(script, binder_conf) if use_binder else "" 

1285 # - Rely on mkdocs-material for the icon 

1286 icon = ":fontawesome-solid-download:" 

1287 # - Generate the download buttons 

1288 # TODO why aren't they centered actually ? does .center work ? 

1289 md_after += f""" 

1290<div id="download_links"></div> 

1291 

1292{binder_badge_md} 

1293 

1294[{icon} Download Python source code: {script.dwnld_py_file.name}](./{script.dwnld_py_file.name}){{ .md-button .center}} 

1295 

1296[{icon} Download Jupyter notebook: {script.ipynb_file.name}](./{script.ipynb_file.name}){{ .md-button .center}} 

1297""" 

1298 

1299 # Add the "generated by mkdocs-gallery" footer 

1300 md_after += MKD_GLR_SIG 

1301 

1302 return md_before, md_after