Coverage for src/mkdocs_gallery/gen_data_model.py: 92%

433 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""" 

7Classes holding information related to gallery examples and exposing derived information, typically paths. 

8""" 

9 

10import os 

11import re 

12import stat 

13import weakref 

14from abc import ABC, abstractmethod 

15from pathlib import Path 

16from shutil import copyfile 

17from typing import Any, Dict, Iterable, List, Tuple, Union 

18 

19from .errors import ExtensionError 

20from .utils import ( 

21 _new_file, 

22 _replace_by_new_if_needed, 

23 _smart_copy_md5, 

24 get_md5sum, 

25 is_relative_to, 

26 matches_filepath_pattern, 

27) 

28 

29 

30def _has_readme(folder: Path) -> bool: 

31 return _get_readme(folder, raise_error=False) is not None 

32 

33 

34def _get_readme(dir_: Path, raise_error=True) -> Path: 

35 """Return the file path for the readme file, if found.""" 

36 

37 assert dir_.is_absolute() # noqa 

38 

39 # extensions = ['.txt'] + sorted(gallery_conf['app'].config['source_suffix']) 

40 extensions = [".txt"] + [".md"] # TODO should this be read from mkdocs config ? like above 

41 for ext in extensions: 

42 for fname in ("README", "Readme", "readme"): 

43 fpth = dir_ / (fname + ext) 

44 if fpth.is_file(): 

45 return fpth 

46 if raise_error: 46 ↛ 47line 46 didn't jump to line 47, because the condition on line 46 was never true

47 raise ExtensionError( 

48 "Example directory {0} does not have a README/Readme/readme file " 

49 "with one of the expected file extensions {1}. Please write one to " 

50 "introduce your gallery.".format(str(dir_), extensions) 

51 ) 

52 return None 

53 

54 

55class ImagePathIterator: 

56 """Iterate over image paths for a given example. 

57 

58 Parameters 

59 ---------- 

60 image_path : Path 

61 The template image path. 

62 """ 

63 

64 def __init__(self, script: "GalleryScript"): 

65 self._script = weakref.ref(script) 

66 self.paths = list() 

67 self._stop = 1000000 

68 

69 @property 

70 def script(self) -> "GalleryScript": 

71 return self._script() 

72 

73 def __len__(self): 

74 """Return the number of image paths already used.""" 

75 return len(self.paths) 

76 

77 def __iter__(self): 

78 """Iterate over paths.""" 

79 # we should really never have 1e6, let's prevent some user pain 

80 for _ii in range(self._stop): 80 ↛ 83line 80 didn't jump to line 83, because the loop on line 80 didn't complete

81 yield next(self) 

82 else: 

83 raise ExtensionError(f"Generated over {self._stop} images") 

84 

85 # def next(self): 

86 # return self.__next__() 

87 

88 def __next__(self): 

89 # The +1 here is because we start image numbering at 1 in filenames 

90 path = self.script.get_image_path(len(self) + 1) 

91 self.paths.append(path) 

92 return path 

93 

94 

95def gen_repr(hide: Union[str, Iterable] = (), show: Union[str, Iterable] = ()): 

96 """ 

97 Generate a repr for a slotted class with either a list of shown or hidden attr names. 

98 

99 Parameters 

100 ---------- 

101 hide 

102 show 

103 

104 Returns 

105 ------- 

106 __repr__ : Callable 

107 The generated repr implementation 

108 """ 

109 if show and hide: 109 ↛ 110line 109 didn't jump to line 110, because the condition on line 109 was never true

110 raise ValueError("Either provide show or hide") 

111 

112 if isinstance(show, str): 

113 show = (show,) 

114 

115 if isinstance(hide, str): 115 ↛ 116line 115 didn't jump to line 116, because the condition on line 115 was never true

116 hide = (hide,) 

117 

118 def __repr__(self): 

119 show_ = self.__slots__ if not show else show 

120 

121 attrs = ",".join("%s=%r" % (k, getattr(self, k)) for k in show_ if k not in hide) 

122 return "%s(%s)" % (type(self).__name__, attrs) 

123 

124 return __repr__ 

125 

126 

127class GalleryScriptResults: 

128 """Result of running a single gallery file""" 

129 

130 __slots__ = ("script", "intro", "exec_time", "memory", "thumb") 

131 

132 __repr__ = gen_repr() 

133 

134 def __init__( 

135 self, 

136 script: "GalleryScript", 

137 intro: str, 

138 exec_time: float, 

139 memory: float, 

140 thumb: Path, 

141 ): 

142 self.script = script 

143 self.intro = intro 

144 self.exec_time = exec_time 

145 self.memory = memory 

146 self.thumb = thumb 

147 

148 @property 

149 def thumb_rel_root_gallery(self) -> Path: 

150 """The thumbnail path file relative to the root gallery folder (not the subgallery)""" 

151 return self.thumb.relative_to(self.script.gallery.root.generated_dir) 

152 

153 

154class ScriptRunVars: 

155 """The variables created when a script is run.""" 

156 

157 __slots__ = ( 

158 "image_path_iterator", 

159 "example_globals", 

160 "memory_used_in_blocks", 

161 "memory_delta", 

162 "fake_main", 

163 "stop_executing", 

164 ) 

165 

166 __repr__ = gen_repr() 

167 

168 def __init__(self, image_path_iterator: ImagePathIterator): 

169 # The iterator returning the next image file paths 

170 self.image_path_iterator = image_path_iterator 

171 

172 # The dictionary of globals() for the script 

173 self.example_globals: Dict[str, Any] = None 

174 

175 # The memory used along execution (first entry is memory before running the first block) 

176 self.memory_used_in_blocks: List[float] = None 

177 

178 # The memory actually used by the code, i.e. the difference between the max used and the memory used before run. 

179 self.memory_delta: float = None 

180 

181 # A temporary __main__ 

182 self.fake_main = None 

183 

184 # A flag that might be set to True if there is an error during execution 

185 self.stop_executing = False 

186 

187 

188class GalleryScript: 

189 """Represents a gallery script and all related files (notebook, md5, etc.)""" 

190 

191 __slots__ = ( 

192 "__weakref__", 

193 "_gallery", 

194 "script_stem", 

195 "title", 

196 "_py_file_md5", 

197 "run_vars", 

198 ) 

199 

200 __repr__ = gen_repr(hide=("__weakref__", "_gallery")) 

201 

202 def __init__(self, gallery: "GalleryBase", script_src_file: Path): 

203 self._gallery = weakref.ref(gallery) 

204 

205 # Make sure the script complies with the gallery 

206 assert script_src_file.parent == gallery.scripts_dir # noqa 

207 assert script_src_file.suffix == ".py" # noqa 

208 

209 # Only save the stem 

210 self.script_stem = script_src_file.stem 

211 

212 # We do not know the title yet, nor the md5 hash of the script file 

213 self.title: str = None 

214 self._py_file_md5: str = None 

215 self.run_vars: ScriptRunVars = None 

216 

217 @property 

218 def gallery(self) -> "GalleryBase": 

219 """An alias for the gallery hosting this script.""" 

220 return self._gallery() 

221 

222 @property 

223 def gallery_conf(self) -> Dict: 

224 """An alias for the gallery conf""" 

225 return self.gallery.conf 

226 

227 @property 

228 def py_file_name(self) -> str: 

229 """The script name, e.g. 'my_demo.py'""" 

230 return f"{self.script_stem}.py" 

231 

232 @property 

233 def src_py_file(self) -> Path: 

234 """The absolute script file path, e.g. <project>/examples/my_script.py""" 

235 return self.gallery.scripts_dir / self.py_file_name 

236 

237 @property 

238 def src_py_file_rel_project(self) -> Path: 

239 """Return the relative path of script file with respect to the project root, for editing for example.""" 

240 return self.gallery.scripts_dir_rel_project / self.py_file_name 

241 

242 def is_executable_example(self) -> bool: 

243 """Tell if this script has to be executed according to gallery configuration: filename_pattern and global plot_gallery 

244 

245 This can be false for a local module (not matching the filename pattern), 

246 or if the gallery_conf['plot_gallery'] is set to False to accelerate the build (disabling all executions) 

247 

248 Returns 

249 ------- 

250 is_executable_example : bool 

251 True if script has to be executed 

252 """ 

253 filename_pattern = self.gallery_conf.get("filename_pattern") 

254 execute = matches_filepath_pattern(self.src_py_file, filename_pattern) and self.gallery_conf["plot_gallery"] 

255 return execute 

256 

257 @property 

258 def py_file_md5(self): 

259 """The md5 checksum of the python script.""" 

260 if self._py_file_md5 is None: 

261 self._py_file_md5 = get_md5sum(self.src_py_file, mode="t") 

262 return self._py_file_md5 

263 

264 @property 

265 def dwnld_py_file(self) -> Path: 

266 """The absolute path of the script in the generated gallery dir,e.g. <project>/generated/gallery/my_script.py""" 

267 return self.gallery.generated_dir / self.py_file_name 

268 

269 @property 

270 def dwnld_py_file_rel_site_root(self) -> Path: 

271 """Return the relative path of script in the generated gallery dir, wrt the mkdocs site root.""" 

272 return self.gallery.generated_dir_rel_site_root / self.py_file_name 

273 

274 @property 

275 def codeobj_file(self): 

276 """The code objects file to use to store example globals""" 

277 return self.gallery.generated_dir / f"{self.script_stem}_codeobj.pickle" 

278 

279 def make_dwnld_py_file(self): 

280 """Copy src file to target file. Use md5 to not overwrite if not necessary.""" 

281 

282 # Use the possibly already computed md5 if available 

283 md5 = None 

284 if self.dwnld_py_file.exists(): 

285 md5 = self.py_file_md5 

286 

287 _smart_copy_md5( 

288 src_file=self.src_py_file, 

289 dst_file=self.dwnld_py_file, 

290 src_md5=md5, 

291 md5_mode="t", 

292 ) 

293 

294 @property 

295 def ipynb_file(self) -> Path: 

296 """Return the jupyter notebook file to generate corresponding to the source `script_file`.""" 

297 return self.gallery.generated_dir / f"{self.script_stem}.ipynb" 

298 

299 @property 

300 def ipynb_file_rel_site_root(self) -> Path: 

301 """Return the jupyter notebook file to generate corresponding to the source `script_file`.""" 

302 return self.gallery.generated_dir_rel_site_root / f"{self.script_stem}.ipynb" 

303 

304 @property 

305 def md5_file(self): 

306 """The path of the persisted md5 file written at the end of processing.""" 

307 file = self.dwnld_py_file 

308 return file.with_name(file.name + ".md5") 

309 

310 def write_final_md5_file(self): 

311 """Writes the persisted md5 file.""" 

312 self.md5_file.write_text(self.py_file_md5) 

313 

314 def has_changed_wrt_persisted_md5(self) -> bool: 

315 """Check if the source md5 has changed with respect to the persisted .md5 file if any""" 

316 

317 # Compute the md5 of the src_file if needed 

318 actual_md5 = self.py_file_md5 

319 

320 # Grab the already computed md5 if it exists, and compare 

321 src_md5_file = self.md5_file 

322 if src_md5_file.exists(): 

323 ref_md5 = src_md5_file.read_text() 

324 md5_has_changed = actual_md5 != ref_md5 

325 else: 

326 md5_has_changed = True 

327 

328 return md5_has_changed 

329 

330 @property 

331 def image_name_template(self) -> str: 

332 """The image file name template for this script file.""" 

333 return "mkd_glr_%s_{0:03}.png" % self.script_stem 

334 

335 def get_image_path(self, number: int) -> Path: 

336 """Return the image path corresponding to the given image number, using the template.""" 

337 return self.gallery.images_dir / self.image_name_template.format(number) 

338 

339 def init_before_processing(self): 

340 # Make the images dir 

341 self.gallery.make_images_dir() 

342 

343 # Init the images iterator 

344 image_path_iterator = ImagePathIterator(self) 

345 

346 # Init the structure that will receive the run information. 

347 self.run_vars = ScriptRunVars(image_path_iterator=image_path_iterator) 

348 

349 def generate_n_dummy_images(self, img: Path, nb: int): 

350 """Use 'stock_img' as many times as needed""" 

351 for _, path in zip(range(nb), self.run_vars.image_path_iterator): 

352 if not os.path.isfile(path): 

353 copyfile(str(img), path) 

354 

355 @property 

356 def md_file(self) -> Path: 

357 """Return the markdown file (absolute path) to generate corresponding to the source `script_file`.""" 

358 return self.gallery.generated_dir / f"{self.script_stem}.md" 

359 

360 @property 

361 def md_file_rel_root_gallery(self) -> Path: 

362 """Return the markdown file relative to the root gallery folder of this gallery or subgallery""" 

363 return self.gallery.subpath / f"{self.script_stem}.md" 

364 

365 @property 

366 def md_file_rel_site_root(self) -> Path: 

367 """Return the markdown file relative to the mkdocs website source root""" 

368 return self.gallery.generated_dir_rel_site_root / f"{self.script_stem}.md" 

369 

370 def save_md_example(self, example_md_contents: str): 

371 """ 

372 

373 Parameters 

374 ---------- 

375 example_md_contents : str 

376 The markdown string to save 

377 """ 

378 # Write to `<py_file_name>.md.new` 

379 write_file_new = _new_file(self.md_file) 

380 write_file_new.write_text(example_md_contents, encoding="utf-8") 

381 

382 # Make it read-only so that people don't try to edit it 

383 mode = os.stat(write_file_new).st_mode 

384 ro_mask = 0x777 ^ (stat.S_IWRITE | stat.S_IWGRP | stat.S_IWOTH) 

385 os.chmod(write_file_new, mode & ro_mask) 

386 

387 # In case it wasn't in our pattern, only replace the file if it's still stale. 

388 _replace_by_new_if_needed(write_file_new, md5_mode="t") 

389 

390 def get_thumbnail_source(self, file_conf) -> Path: 

391 """Get the path to the image to use as the thumbnail. 

392 

393 Note that this image will be copied and possibly rescaled later to create the actual thumbnail. 

394 

395 Parameters 

396 ---------- 

397 file_conf : dict 

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

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

400 """ 

401 # Read specification of the figure to display as thumbnail from main text 

402 thumbnail_number = file_conf.get("thumbnail_number", None) 

403 thumbnail_path = file_conf.get("thumbnail_path", None) 

404 

405 # thumbnail_number has priority. 

406 if thumbnail_number is None and thumbnail_path is None: 

407 # If no number AND no path, set to default thumbnail_number 

408 thumbnail_number = 1 

409 

410 if thumbnail_number is not None: 

411 # Option 1: generate thumbnail from a numbered figure in the script 

412 

413 if not isinstance(thumbnail_number, int): 413 ↛ 414line 413 didn't jump to line 414, because the condition on line 413 was never true

414 raise ExtensionError( 

415 f"mkdocs_gallery_thumbnail_number setting is not a number, got " f"{thumbnail_number!r}" 

416 ) 

417 

418 # negative index means counting from the last one 

419 if thumbnail_number < 0: 419 ↛ 420line 419 didn't jump to line 420, because the condition on line 419 was never true

420 thumbnail_number += len(self.run_vars.image_path_iterator) + 1 

421 

422 # Generate the image path from the template (not from iterator) 

423 image_path = self.get_image_path(thumbnail_number) 

424 else: 

425 # Option 2: use an existing thumbnail image 

426 

427 # thumbnail_path is a relative path wrt website root dir 

428 image_path = self.gallery.all_info.mkdocs_docs_dir / thumbnail_path 

429 

430 return image_path 

431 

432 def get_thumbnail_file(self, ext: str) -> Path: 

433 """Return the thumbnail file to use, for the given image file extension""" 

434 assert ext[0] == "." # noqa 

435 return self.gallery.thumb_dir / ("mkd_glr_%s_thumb%s" % (self.script_stem, ext)) 

436 

437 

438class GalleryBase(ABC): 

439 """The common part between gallery roots and subsections.""" 

440 

441 __slots__ = ("title", "scripts", "_readme_file") 

442 

443 @property 

444 @abstractmethod 

445 def all_info(self) -> "AllInformation": 

446 """An alias to the global holder of all information.""" 

447 

448 @property 

449 @abstractmethod 

450 def root(self) -> "Gallery": 

451 """Return the actual root gallery. It may be self or self parent""" 

452 

453 @property 

454 @abstractmethod 

455 def subpath(self) -> Path: 

456 """Return the subpath for this subgallery. If this is not a subgallery, return `.`""" 

457 

458 @property 

459 @abstractmethod 

460 def conf(self) -> Dict: 

461 """An alias to the global gallery configuration.""" 

462 

463 @property 

464 @abstractmethod 

465 def scripts_dir(self) -> Path: 

466 """""" 

467 

468 @property 

469 @abstractmethod 

470 def scripts_dir_rel_project(self) -> Path: 

471 """The relative path (wrt project root) where this subgallery scripts are located""" 

472 

473 @property 

474 def readme_file(self) -> Path: 

475 """Return the file path to the readme file, or raise en error if none is found.""" 

476 try: 

477 return self._readme_file 

478 except AttributeError: 

479 self._readme_file = _get_readme(self.scripts_dir) 

480 return self._readme_file 

481 

482 @property 

483 def readme_file_rel_project(self) -> Path: 

484 """Return the file path to the readme file, relative to the project root.""" 

485 return self.readme_file.relative_to(self.all_info.project_root_dir) 

486 

487 @property 

488 def exec_times_md_file(self) -> Path: 

489 """The absolute path to the execution times markdown file associated with this gallery""" 

490 return self.generated_dir / "mg_execution_times.md" 

491 

492 def is_ignored_script_file(self, f: Path): 

493 """Return True if file `f` is ignored according to the 'ignore_pattern' configuration.""" 

494 return re.search(self.conf["ignore_pattern"], os.path.normpath(str(f))) is not None 

495 

496 def collect_script_files(self, apply_ignore_pattern: bool = True, sort_files: bool = True): 

497 """Collects script files to process in this gallery and sort them according to configuration. 

498 

499 Parameters 

500 ---------- 

501 apply_ignore_pattern : bool 

502 A boolean indicating if the 'ignore_pattern' gallery config option should be applied. 

503 

504 sort_files : bool 

505 A boolean indicating if the 'within_subsection_order' gallery config option should be applied. 

506 """ 

507 assert not hasattr(self, "scripts"), "This can only be called once!" # noqa 

508 

509 # get python files 

510 listdir = list(self.scripts_dir.glob("*.py")) 

511 

512 # limit which to look at based on regex (similar to filename_pattern) 

513 if apply_ignore_pattern: 513 ↛ 517line 513 didn't jump to line 517, because the condition on line 513 was never false

514 listdir = [f for f in listdir if not self.is_ignored_script_file(f)] 

515 

516 # sort them 

517 if sort_files: 517 ↛ 521line 517 didn't jump to line 521, because the condition on line 517 was never false

518 listdir = sorted(listdir, key=self.conf["within_subsection_order"]()) 

519 

520 # Convert to proper objects 

521 self.scripts: List[GalleryScript] = [GalleryScript(self, f) for f in listdir] 

522 

523 def get_all_script_files(self) -> List[Path]: 

524 """Return the list of all script file paths in this (sub)gallery""" 

525 return [f.src_py_file for f in self.scripts] 

526 

527 @property 

528 @abstractmethod 

529 def generated_dir(self) -> Path: 

530 """The absolute path where this (sub)gallery will be generated""" 

531 

532 @property 

533 @abstractmethod 

534 def generated_dir_rel_project(self) -> Path: 

535 """The relative path (wrt project root) where this subgallery will be generated""" 

536 

537 @property 

538 @abstractmethod 

539 def generated_dir_rel_site_root(self) -> Path: 

540 """The relative path (wrt mkdocs website root, e.g. docs/) where this subgallery will be generated""" 

541 

542 def make_generated_dir(self): 

543 """Make sure that the `generated_dir` exists""" 

544 if not self.generated_dir.exists(): 

545 self.generated_dir.mkdir(parents=True) 

546 

547 @property 

548 def images_dir(self) -> Path: 

549 """The absolute path of the directory where images will be generated.""" 

550 return self.generated_dir / "images" 

551 

552 def make_images_dir(self): 

553 """Make sure that the `images_dir` exists and is a folder""" 

554 if not self.images_dir.exists(): 

555 self.images_dir.mkdir(parents=True) 

556 else: 

557 assert self.images_dir.is_dir() # noqa 

558 

559 @property 

560 def thumb_dir(self) -> Path: 

561 """The absolute path of the directory where image thumbnails will be generated.""" 

562 return self.images_dir / "thumb" 

563 

564 def make_thumb_dir(self): 

565 """Make sure that the `thumb_dir` exists and is a folder""" 

566 if not self.thumb_dir.exists(): 

567 self.thumb_dir.mkdir(parents=True) 

568 else: 

569 assert self.thumb_dir.is_dir() # noqa 

570 

571 @abstractmethod 

572 def has_subsections(self) -> bool: 

573 """Return True if the gallery has at least one subsection""" 

574 

575 

576class GallerySubSection(GalleryBase): 

577 """Represents a subsection in a gallery.""" 

578 

579 __slots__ = ("__weakref__", "_parent", "subpath") 

580 

581 __repr__ = gen_repr(hide=("__weakref__", "_parent")) 

582 

583 def has_subsections(self) -> bool: 

584 return False 

585 

586 def __init__(self, parent: "Gallery", subpath: Path): 

587 """ 

588 

589 Parameters 

590 ---------- 

591 parent : Gallery 

592 The containing Gallery for this sub gallery (subsection) 

593 

594 subpath : Path 

595 The path to this subgallery, from its parent gallery. Must be relative. 

596 """ 

597 assert not subpath.is_absolute() # noqa 

598 self.subpath = subpath 

599 self._parent = weakref.ref(parent) 

600 

601 @property 

602 def all_info(self) -> "AllInformation": 

603 """Alias to access the weak reference""" 

604 return self.root.all_info 

605 

606 @property 

607 def conf(self): 

608 """An alias to the global gallery configuration.""" 

609 return self.root.conf 

610 

611 @property 

612 def root(self) -> "Gallery": 

613 """Access to the parent gallery through the weak reference.""" 

614 return self._parent() 

615 

616 @property 

617 def scripts_dir_rel_project(self): 

618 """The relative path (wrt project root) where this subgallery scripts are located""" 

619 return self.root.scripts_dir_rel_project / self.subpath 

620 

621 @property 

622 def scripts_dir(self): 

623 """The absolute path (wrt project root) where this subgallery scripts are located""" 

624 return self.root.scripts_dir / self.subpath 

625 

626 @property 

627 def generated_dir_rel_project(self): 

628 """The relative path (wrt project root) where this subgallery will be generated""" 

629 return self.root.generated_dir_rel_project / self.subpath 

630 

631 @property 

632 def generated_dir_rel_site_root(self) -> Path: 

633 """The relative path (wrt mkdocs website root, e.g. docs/) where this subgallery will be generated""" 

634 return self.root.generated_dir_rel_site_root / self.subpath 

635 

636 @property 

637 def generated_dir(self): 

638 """The absolute path where this subgallery will be generated""" 

639 return self.root.generated_dir / self.subpath 

640 

641 def list_downloadable_sources(self) -> List[Path]: 

642 """Return the list of all .py files in the subgallery generated folder""" 

643 return list(self.generated_dir.glob("*.py")) 

644 

645 

646class Gallery(GalleryBase): 

647 """Represent a root gallery: source path, destination path, etc. 

648 

649 Subgalleries are attached as a separate member. 

650 """ 

651 

652 __slots__ = ( 

653 "__weakref__", 

654 "scripts_dir_rel_project", 

655 "generated_dir_rel_project", 

656 "subsections", 

657 "_all_info", 

658 ) 

659 

660 __repr__ = gen_repr(hide=("__weakref__", "subsections", "_all_info")) 

661 

662 subpath = Path(".") 

663 

664 def __init__( 

665 self, 

666 all_info: "AllInformation", 

667 scripts_dir_rel_project: Path, 

668 generated_dir_rel_project: Path, 

669 ): 

670 """ 

671 

672 Parameters 

673 ---------- 

674 all_info : AllInformation 

675 The parent structure containing all configurations. 

676 

677 scripts_dir_rel_project : Path 

678 The source folder of the current gallery, containing the python files and the readme.md. 

679 For example ./docs/examples/. It must be relative to the project root. 

680 

681 generated_dir_rel_project : Path 

682 The target folder where the documents, notebooks and images from this sub-gallery should be generated. 

683 For example ./docs/generated/gallery (a subfolder of mkdocs src folder), or 

684 ./generated/gallery (not a subfolder of mkdocs src folder. TODO explicitly forbid this ?). 

685 It must be relative to the project root. 

686 

687 """ 

688 # note: this is the old examples_dir 

689 scripts_dir_rel_project = Path(scripts_dir_rel_project) 

690 assert not scripts_dir_rel_project.is_absolute() # noqa 

691 self.scripts_dir_rel_project = scripts_dir_rel_project 

692 

693 # note: this is the old gallery_dir/target_dirsite_root 

694 generated_dir_rel_project = Path(generated_dir_rel_project) 

695 assert not generated_dir_rel_project.is_absolute() # noqa 

696 self.generated_dir_rel_project = generated_dir_rel_project 

697 

698 self.subsections: Tuple[GallerySubSection] = None # type: ignore 

699 

700 # Make sure the gallery can see all information 

701 self._attach(all_info=all_info) 

702 

703 # Check that generated dir is inside docs dir 

704 if not is_relative_to(self.all_info.mkdocs_docs_dir, self.generated_dir): 704 ↛ 705line 704 didn't jump to line 705, because the condition on line 704 was never true

705 raise ValueError("Generated gallery dirs can only be located as subfolders of the mkdocs 'docs_dir'.") 

706 

707 def has_subsections(self) -> bool: 

708 return len(self.subsections) > 0 

709 

710 @property 

711 def root(self) -> "Gallery": 

712 """Self is the root of self.""" 

713 return self 

714 

715 @property 

716 def scripts_dir(self) -> Path: 

717 """The folder where python scripts are located, as an absolute path.""" 

718 return self.all_info.project_root_dir / self.scripts_dir_rel_project 

719 

720 @property 

721 def index_md(self) -> Path: 

722 """Path to this root gallery's index markdown page. Note that subgalleries do not have such a page""" 

723 return self.generated_dir / "index.md" 

724 

725 @property 

726 def index_md_rel_site_root(self) -> Path: 

727 """Path to this root gallery's index markdown page, relative to site root. 

728 Note that subgalleries do not have such a page""" 

729 return self.generated_dir_rel_site_root / "index.md" 

730 

731 @property 

732 def generated_dir(self) -> Path: 

733 """The folder where the gallery files will be generated, as an absolute path.""" 

734 return self.all_info.project_root_dir / self.generated_dir_rel_project 

735 

736 @property 

737 def generated_dir_rel_site_root(self) -> Path: 

738 """The folder where the gallery files will be generated, as an absolute path.""" 

739 return self.generated_dir.relative_to(self.all_info.mkdocs_docs_dir) 

740 

741 def populate_subsections(self): 

742 """Moved from the legacy `get_subsections`.""" 

743 

744 assert self.subsections is None, "This method can only be called once !" # noqa 

745 

746 # List all subfolders with a valid readme 

747 subfolders = [ 

748 subfolder for subfolder in self.scripts_dir.iterdir() if subfolder.is_dir() and _has_readme(subfolder) 

749 ] 

750 

751 # Sort them 

752 _sortkey = self.conf["subsection_order"] 

753 sortkey = _sortkey 

754 if _sortkey is not None: 754 ↛ 756line 754 didn't jump to line 756, because the condition on line 754 was never true

755 

756 def sortkey(subfolder: Path): 

757 # Apply on the string representation of the folder 

758 return sortkey(str(subfolder)) 

759 

760 sorted_subfolders = sorted(subfolders, key=sortkey) 

761 

762 self.subsections = tuple( 

763 (GallerySubSection(self, subpath=f.relative_to(self.scripts_dir)) for f in sorted_subfolders) 

764 ) 

765 

766 def collect_script_files( 

767 self, 

768 recurse: bool = True, 

769 apply_ignore_pattern: bool = True, 

770 sort_files: bool = True, 

771 ): 

772 """Collects script files to process in this gallery and sort them according to configuration. 

773 

774 Parameters 

775 ---------- 

776 recurse : bool 

777 If True, this will call collect_script_files on all subsections 

778 

779 apply_ignore_pattern : bool 

780 A boolean indicating if the 'ignore_pattern' gallery config option should be applied. 

781 

782 sort_files : bool 

783 A boolean indicating if the 'within_subsection_order' gallery config option should be applied. 

784 """ 

785 # All subsections first 

786 if recurse: 786 ↛ 791line 786 didn't jump to line 791, because the condition on line 786 was never false

787 for s in self.subsections: 

788 s.collect_script_files(apply_ignore_pattern=apply_ignore_pattern, sort_files=sort_files) 

789 

790 # Then the gallery itself 

791 GalleryBase.collect_script_files(self, apply_ignore_pattern=apply_ignore_pattern, sort_files=sort_files) 

792 

793 def get_all_script_files(self, recurse=True): 

794 res = GalleryBase.get_all_script_files(self) 

795 if recurse: 795 ↛ 798line 795 didn't jump to line 798, because the condition on line 795 was never false

796 for g in self.subsections: 

797 res += g.get_all_script_files() 

798 return res 

799 

800 def _attach(self, all_info: "AllInformation"): 

801 """Attach a weak reference to the parent object.""" 

802 self._all_info: "AllInformation" = weakref.ref(all_info) # type: ignore 

803 

804 @property 

805 def all_info(self) -> "AllInformation": 

806 """Alias to access the weak reference""" 

807 return self._all_info() 

808 

809 @property 

810 def conf(self): 

811 """An alias to the global gallery configuration.""" 

812 return self.all_info.gallery_conf 

813 

814 def list_downloadable_sources(self, recurse=True) -> List[Path]: 

815 """Return the list of all .py files in the gallery generated folder""" 

816 results = list(self.generated_dir.glob("*.py")) 

817 if recurse: 817 ↛ 821line 817 didn't jump to line 821, because the condition on line 817 was never false

818 for g in self.subsections: 

819 results += g.list_downloadable_sources() 

820 

821 return results 

822 

823 @property 

824 def zipfile_python(self) -> Path: 

825 return self.generated_dir / f"{self.generated_dir.name}_python.zip" 

826 

827 @property 

828 def zipfile_python_rel_index_md(self) -> Path: 

829 return Path(f"{self.generated_dir.name}_python.zip") 

830 

831 @property 

832 def zipfile_jupyter(self) -> Path: 

833 return self.generated_dir / f"{self.generated_dir.name}_jupyter.zip" 

834 

835 @property 

836 def zipfile_jupyter_rel_index_md(self) -> Path: 

837 return Path(f"{self.generated_dir.name}_jupyter.zip") 

838 

839 

840class AllInformation: 

841 """Represent all galleries as well as the global configuration.""" 

842 

843 __slots__ = ( 

844 "__weakref__", 

845 "galleries", 

846 "gallery_conf", 

847 "mkdocs_conf", 

848 "project_root_dir", 

849 ) 

850 

851 __repr__ = gen_repr(show="project_root_dir") 

852 

853 def __init__( 

854 self, 

855 gallery_conf: Dict[str, Any], 

856 mkdocs_conf: Dict[str, Any], 

857 project_root_dir: Path, 

858 gallery_elts: Tuple[Gallery, ...] = (), 

859 ): 

860 """ 

861 

862 Parameters 

863 ---------- 

864 gallery_conf : Dict[str, Any] 

865 The global mkdocs-gallery config. 

866 

867 mkdocs_docs_dir : Path 

868 The 'docs_dir' option in mkdocs. 

869 

870 mkdocs_site_dir : Path 

871 The 'site_dir' option in mkdocs 

872 

873 project_root_dir 

874 gallery_elts 

875 """ 

876 self.gallery_conf = gallery_conf 

877 

878 assert project_root_dir.is_absolute() # noqa 

879 self.project_root_dir = project_root_dir 

880 

881 self.mkdocs_conf = mkdocs_conf 

882 

883 self.galleries = list(gallery_elts) 

884 

885 @property 

886 def mkdocs_docs_dir(self) -> Path: 

887 return Path(self.mkdocs_conf["docs_dir"]) 

888 

889 @property 

890 def mkdocs_site_dir(self) -> Path: 

891 return Path(self.mkdocs_conf["site_dir"]) 

892 

893 def add_gallery(self, scripts_dir: Union[str, Path], generated_dir: Union[str, Path]): 

894 """Add a gallery to the list of known galleries. 

895 

896 Parameters 

897 ---------- 

898 scripts_dir : Union[str, Path] 

899 

900 

901 generated_dir : Union[str, Path] 

902 

903 """ 

904 

905 scripts_dir_rel_project = Path(scripts_dir).relative_to(self.project_root_dir) 

906 generated_dir_rel_project = Path(generated_dir).relative_to(self.project_root_dir) 

907 

908 # Create the gallery 

909 g = Gallery( 

910 all_info=self, 

911 scripts_dir_rel_project=scripts_dir_rel_project, 

912 generated_dir_rel_project=generated_dir_rel_project, 

913 ) 

914 

915 # Add it to the list 

916 self.galleries.append(g) 

917 

918 def populate_subsections(self): 

919 """From the legacy `get_subsections`.""" 

920 for g in self.galleries: 

921 g.populate_subsections() 

922 

923 def collect_script_files( 

924 self, 

925 do_subgalleries: bool = True, 

926 apply_ignore_pattern: bool = True, 

927 sort_files: bool = True, 

928 ): 

929 """Triggers the files collection in all galleries.""" 

930 for g in self.galleries: 

931 g.collect_script_files( 

932 recurse=do_subgalleries, 

933 apply_ignore_pattern=apply_ignore_pattern, 

934 sort_files=sort_files, 

935 ) 

936 

937 def get_all_script_files(self): 

938 return [f for g in self.galleries for f in g.get_all_script_files()] 

939 

940 @property 

941 def backrefs_dir(self) -> Path: 

942 """The absolute path to the backreferences dir""" 

943 return Path(self.gallery_conf["backreferences_dir"]) 

944 

945 def get_backreferences_file(self, module_name) -> Path: 

946 """Return the path to the backreferences file to use for `module_name`""" 

947 return self.backrefs_dir / f"{module_name}.examples" 

948 

949 @classmethod 

950 def from_cfg(self, gallery_conf: Dict, mkdocs_conf: Dict): 

951 """Factory to create this object from the configuration. 

952 

953 It creates all galleries and populates their subsections. 

954 This class method replaces `_prepare_gallery_dirs`. 

955 """ 

956 

957 # The project root directory 

958 project_root_dir = Path(os.path.abspath(mkdocs_conf["config_file_path"])).parent 

959 project_root2 = Path(os.getcwd()) 

960 if project_root2 != project_root_dir: 960 ↛ 961line 960 didn't jump to line 961, because the condition on line 960 was never true

961 raise ValueError("The project root dir is ambiguous ! Please report this issue to mkdocs-gallery.") 

962 

963 # Create the global object 

964 all_info = AllInformation( 

965 gallery_conf=gallery_conf, 

966 mkdocs_conf=mkdocs_conf, 

967 project_root_dir=project_root_dir, 

968 ) 

969 

970 # Source and destination of the galleries 

971 examples_dirs = gallery_conf["examples_dirs"] 

972 gallery_dirs = gallery_conf["gallery_dirs"] 

973 

974 if not isinstance(examples_dirs, list): 974 ↛ 975line 974 didn't jump to line 975, because the condition on line 974 was never true

975 examples_dirs = [examples_dirs] 

976 

977 if not isinstance(gallery_dirs, list): 977 ↛ 978line 977 didn't jump to line 978, because the condition on line 977 was never true

978 gallery_dirs = [gallery_dirs] 

979 

980 # Back references page 

981 backreferences_dir = gallery_conf["backreferences_dir"] 

982 if backreferences_dir: 982 ↛ 986line 982 didn't jump to line 986, because the condition on line 982 was never false

983 Path(backreferences_dir).mkdir(parents=True, exist_ok=True) 

984 

985 # Create galleries 

986 for e_dir, g_dir in zip(examples_dirs, gallery_dirs): 

987 all_info.add_gallery(scripts_dir=e_dir, generated_dir=g_dir) 

988 

989 # Scan all subsections 

990 all_info.populate_subsections() 

991 

992 return all_info