Coverage for src/mkdocs_gallery/gen_data_model.py: 92%
432 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-09-30 08:26 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2024-09-30 08:26 +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"""
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
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)
30def _has_readme(folder: Path) -> bool:
31 return _get_readme(folder, raise_error=False) is not None
34def _get_readme(dir_: Path, raise_error=True) -> Path:
35 """Return the file path for the readme file, if found."""
37 assert dir_.is_absolute() # noqa
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
55class ImagePathIterator:
56 """Iterate over image paths for a given example.
58 Parameters
59 ----------
60 image_path : Path
61 The template image path.
62 """
64 def __init__(self, script: "GalleryScript"):
65 self._script = weakref.ref(script)
66 self.paths = list()
67 self._stop = 1000000
69 @property
70 def script(self) -> "GalleryScript":
71 return self._script()
73 def __len__(self):
74 """Return the number of image paths already used."""
75 return len(self.paths)
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")
85 # def next(self):
86 # return self.__next__()
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
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.
99 Parameters
100 ----------
101 hide
102 show
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")
112 if isinstance(show, str):
113 show = (show,)
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,)
118 def __repr__(self):
119 show_ = self.__slots__ if not show else show
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)
124 return __repr__
127class GalleryScriptResults:
128 """Result of running a single gallery file"""
130 __slots__ = ("script", "intro", "exec_time", "memory", "thumb")
132 __repr__ = gen_repr()
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
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)
154class ScriptRunVars:
155 """The variables created when a script is run."""
157 __slots__ = (
158 "image_path_iterator",
159 "example_globals",
160 "memory_used_in_blocks",
161 "memory_delta",
162 "fake_main",
163 "stop_executing",
164 )
166 __repr__ = gen_repr()
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
172 # The dictionary of globals() for the script
173 self.example_globals: Dict[str, Any] = None
175 # The memory used along execution (first entry is memory before running the first block)
176 self.memory_used_in_blocks: List[float] = None
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
181 # A temporary __main__
182 self.fake_main = None
184 # A flag that might be set to True if there is an error during execution
185 self.stop_executing = False
188class GalleryScript:
189 """Represents a gallery script and all related files (notebook, md5, etc.)"""
191 __slots__ = (
192 "__weakref__",
193 "_gallery",
194 "script_stem",
195 "title",
196 "_py_file_md5",
197 "run_vars",
198 )
200 __repr__ = gen_repr(hide=("__weakref__", "_gallery"))
202 def __init__(self, gallery: "GalleryBase", script_src_file: Path):
203 self._gallery = weakref.ref(gallery)
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
209 # Only save the stem
210 self.script_stem = script_src_file.stem
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
217 @property
218 def gallery(self) -> "GalleryBase":
219 """An alias for the gallery hosting this script."""
220 return self._gallery()
222 @property
223 def gallery_conf(self) -> Dict:
224 """An alias for the gallery conf"""
225 return self.gallery.conf
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"
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
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
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
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)
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
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
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
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
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"
279 def make_dwnld_py_file(self):
280 """Copy src file to target file. Use md5 to not overwrite if not necessary."""
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
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 )
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"
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"
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")
310 def write_final_md5_file(self):
311 """Writes the persisted md5 file."""
312 self.md5_file.write_text(self.py_file_md5)
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"""
317 # Compute the md5 of the src_file if needed
318 actual_md5 = self.py_file_md5
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
328 return md5_has_changed
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
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)
339 def init_before_processing(self):
340 # Make the images dir
341 self.gallery.make_images_dir()
343 # Init the images iterator
344 image_path_iterator = ImagePathIterator(self)
346 # Init the structure that will receive the run information.
347 self.run_vars = ScriptRunVars(image_path_iterator=image_path_iterator)
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)
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"
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"
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"
370 def save_md_example(self, example_md_contents: str):
371 """
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")
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)
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")
390 def get_thumbnail_source(self, file_conf) -> Path:
391 """Get the path to the image to use as the thumbnail.
393 Note that this image will be copied and possibly rescaled later to create the actual thumbnail.
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)
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
410 if thumbnail_number is not None:
411 # Option 1: generate thumbnail from a numbered figure in the script
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 )
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
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
427 # thumbnail_path is a relative path wrt website root dir
428 image_path = self.gallery.all_info.mkdocs_docs_dir / thumbnail_path
430 return image_path
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))
438class GalleryBase(ABC):
439 """The common part between gallery roots and subsections."""
441 __slots__ = ("title", "scripts", "_readme_file")
443 @property
444 @abstractmethod
445 def all_info(self) -> "AllInformation":
446 """An alias to the global holder of all information."""
448 @property
449 @abstractmethod
450 def root(self) -> "Gallery":
451 """Return the actual root gallery. It may be self or self parent"""
453 @property
454 @abstractmethod
455 def subpath(self) -> Path:
456 """Return the subpath for this subgallery. If this is not a subgallery, return `.`"""
458 @property
459 @abstractmethod
460 def conf(self) -> Dict:
461 """An alias to the global gallery configuration."""
463 @property
464 @abstractmethod
465 def scripts_dir(self) -> Path:
466 """"""
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"""
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
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)
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"
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
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.
499 Parameters
500 ----------
501 apply_ignore_pattern : bool
502 A boolean indicating if the 'ignore_pattern' gallery config option should be applied.
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
509 # get python files
510 listdir = list(self.scripts_dir.glob("*.py"))
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 always true
514 listdir = [f for f in listdir if not self.is_ignored_script_file(f)]
516 # sort them
517 if sort_files: 517 ↛ 521line 517 didn't jump to line 521 because the condition on line 517 was always true
518 listdir = sorted(listdir, key=self.conf["within_subsection_order"]())
520 # Convert to proper objects
521 self.scripts: List[GalleryScript] = [GalleryScript(self, f) for f in listdir]
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]
527 @property
528 @abstractmethod
529 def generated_dir(self) -> Path:
530 """The absolute path where this (sub)gallery will be generated"""
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"""
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"""
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)
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"
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
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"
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
571 @abstractmethod
572 def has_subsections(self) -> bool:
573 """Return True if the gallery has at least one subsection"""
576class GallerySubSection(GalleryBase):
577 """Represents a subsection in a gallery."""
579 __slots__ = ("__weakref__", "_parent", "subpath")
581 __repr__ = gen_repr(hide=("__weakref__", "_parent"))
583 def has_subsections(self) -> bool:
584 return False
586 def __init__(self, parent: "Gallery", subpath: Path):
587 """
589 Parameters
590 ----------
591 parent : Gallery
592 The containing Gallery for this sub gallery (subsection)
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)
601 @property
602 def all_info(self) -> "AllInformation":
603 """Alias to access the weak reference"""
604 return self.root.all_info
606 @property
607 def conf(self):
608 """An alias to the global gallery configuration."""
609 return self.root.conf
611 @property
612 def root(self) -> "Gallery":
613 """Access to the parent gallery through the weak reference."""
614 return self._parent()
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
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
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
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
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
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"))
646class Gallery(GalleryBase):
647 """Represent a root gallery: source path, destination path, etc.
649 Subgalleries are attached as a separate member.
650 """
652 __slots__ = (
653 "__weakref__",
654 "scripts_dir_rel_project",
655 "generated_dir_rel_project",
656 "subsections",
657 "_all_info",
658 )
660 __repr__ = gen_repr(hide=("__weakref__", "subsections", "_all_info"))
662 subpath = Path(".")
664 def __init__(
665 self,
666 all_info: "AllInformation",
667 scripts_dir_rel_project: Path,
668 generated_dir_rel_project: Path,
669 ):
670 """
672 Parameters
673 ----------
674 all_info : AllInformation
675 The parent structure containing all configurations.
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.
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.
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
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
698 self.subsections: Tuple[GallerySubSection] = None # type: ignore
700 # Make sure the gallery can see all information
701 self._attach(all_info=all_info)
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'.")
707 def has_subsections(self) -> bool:
708 return len(self.subsections) > 0
710 @property
711 def root(self) -> "Gallery":
712 """Self is the root of self."""
713 return self
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
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"
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"
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
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)
741 def populate_subsections(self):
742 """Moved from the legacy `get_subsections`."""
744 assert self.subsections is None, "This method can only be called once !" # noqa
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 ]
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
756 def sortkey(subfolder: Path):
757 # Apply on the string representation of the folder
758 return sortkey(str(subfolder))
760 sorted_subfolders = sorted(subfolders, key=sortkey)
762 self.subsections = tuple(
763 (GallerySubSection(self, subpath=f.relative_to(self.scripts_dir)) for f in sorted_subfolders)
764 )
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.
774 Parameters
775 ----------
776 recurse : bool
777 If True, this will call collect_script_files on all subsections
779 apply_ignore_pattern : bool
780 A boolean indicating if the 'ignore_pattern' gallery config option should be applied.
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 always true
787 for s in self.subsections:
788 s.collect_script_files(apply_ignore_pattern=apply_ignore_pattern, sort_files=sort_files)
790 # Then the gallery itself
791 GalleryBase.collect_script_files(self, apply_ignore_pattern=apply_ignore_pattern, sort_files=sort_files)
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 always true
796 for g in self.subsections:
797 res += g.get_all_script_files()
798 return res
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
804 @property
805 def all_info(self) -> "AllInformation":
806 """Alias to access the weak reference"""
807 return self._all_info()
809 @property
810 def conf(self):
811 """An alias to the global gallery configuration."""
812 return self.all_info.gallery_conf
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 always true
818 for g in self.subsections:
819 results += g.list_downloadable_sources()
821 return results
823 @property
824 def zipfile_python(self) -> Path:
825 return self.generated_dir / f"{self.generated_dir.name}_python.zip"
827 @property
828 def zipfile_python_rel_index_md(self) -> Path:
829 return Path(f"{self.generated_dir.name}_python.zip")
831 @property
832 def zipfile_jupyter(self) -> Path:
833 return self.generated_dir / f"{self.generated_dir.name}_jupyter.zip"
835 @property
836 def zipfile_jupyter_rel_index_md(self) -> Path:
837 return Path(f"{self.generated_dir.name}_jupyter.zip")
840class AllInformation:
841 """Represent all galleries as well as the global configuration."""
843 __slots__ = (
844 "__weakref__",
845 "galleries",
846 "gallery_conf",
847 "mkdocs_conf",
848 "project_root_dir",
849 )
851 __repr__ = gen_repr(show="project_root_dir")
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 """
862 Parameters
863 ----------
864 gallery_conf : Dict[str, Any]
865 The global mkdocs-gallery config.
867 mkdocs_docs_dir : Path
868 The 'docs_dir' option in mkdocs.
870 mkdocs_site_dir : Path
871 The 'site_dir' option in mkdocs
873 project_root_dir
874 gallery_elts
875 """
876 self.gallery_conf = gallery_conf
878 assert project_root_dir.is_absolute() # noqa
879 self.project_root_dir = project_root_dir
881 self.mkdocs_conf = mkdocs_conf
883 self.galleries = list(gallery_elts)
885 @property
886 def mkdocs_docs_dir(self) -> Path:
887 return Path(self.mkdocs_conf["docs_dir"])
889 @property
890 def mkdocs_site_dir(self) -> Path:
891 return Path(self.mkdocs_conf["site_dir"])
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.
896 Parameters
897 ----------
898 scripts_dir : Union[str, Path]
901 generated_dir : Union[str, Path]
903 """
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)
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 )
915 # Add it to the list
916 self.galleries.append(g)
918 def populate_subsections(self):
919 """From the legacy `get_subsections`."""
920 for g in self.galleries:
921 g.populate_subsections()
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 )
937 def get_all_script_files(self):
938 return [f for g in self.galleries for f in g.get_all_script_files()]
940 @property
941 def backrefs_dir(self) -> Path:
942 """The absolute path to the backreferences dir"""
943 return Path(self.gallery_conf["backreferences_dir"])
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"
949 @classmethod
950 def from_cfg(self, gallery_conf: Dict, mkdocs_conf: Dict):
951 """Factory to create this object from the configuration.
953 It creates all galleries and populates their subsections.
954 This class method replaces `_prepare_gallery_dirs`.
955 """
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.")
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 )
970 # Source and destination of the galleries
971 examples_dirs = gallery_conf["examples_dirs"]
972 gallery_dirs = gallery_conf["gallery_dirs"]
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]
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]
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 always true
983 Path(backreferences_dir).mkdir(parents=True, exist_ok=True)
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)
989 # Scan all subsections
990 all_info.populate_subsections()
992 return all_info