diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5c39aa38a..a998b6343 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,31 @@ repos: -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: "v5.0.0" hooks: - - id: trailing-whitespace + - id: check-builtin-literals + - id: check-case-conflict + - id: check-toml + - id: check-yaml + - id: debug-statements + - id: end-of-file-fixer + - id: trailing-whitespace + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.9.7 + hooks: + - id: ruff-format + exclude: examples + - id: ruff + args: ["--fix", "--show-fixes"] + + - repo: https://github.com/rbubley/mirrors-prettier + rev: "v3.4.2" + hooks: + - id: prettier + types_or: [yaml, markdown, html, css, scss, javascript, json] + + - repo: https://github.com/abravalheri/validate-pyproject + rev: "v0.23" + hooks: + - id: validate-pyproject + additional_dependencies: ["validate-pyproject-schema-store[all]"] diff --git a/Makefile b/Makefile index dba1164f0..8cf13b9e9 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: docs clean +make.PHONY: docs clean SPHINXOPTS = diff --git a/docs/_scripts/prep_docs.py b/docs/_scripts/prep_docs.py index 1098567d1..bc502f407 100644 --- a/docs/_scripts/prep_docs.py +++ b/docs/_scripts/prep_docs.py @@ -1,39 +1,70 @@ """ALL pre-rendering and pre-preparation of docs should occur in this file. +This script is called **before** Sphinx builds the documentation. + Note: make no assumptions about the working directory from which this script will be called. """ + +import os import sys -from pathlib import Path from importlib.metadata import version +from pathlib import Path from packaging.version import parse +from scripts_logger import setup_logger + +logger = setup_logger(__name__) + +# Set up paths to docs and npe2 docs source DOCS = Path(__file__).parent.parent.absolute() -NPE = DOCS.parent.absolute() / 'npe2' +logger.debug(f"DOCS: {DOCS}") +NPE = DOCS.parent.absolute() / "npe2" +logger.debug(f"NPE: {NPE}") + def prep_npe2(): - # some plugin docs live in npe2 for testing purposes + """Preps the npe2 plugin engine prior to Sphinx docs build. + + Some plugin-related docs live in the npe2 repo to simplify + plugin testing. + """ + logger.debug("Preparing npe2 plugin") + # Checks if the path to npe2 repo exist. If so, bail. if NPE.exists(): + logger.debug("NPE2 plugin already present") return from subprocess import check_call npe2_version = version("npe2") - + logger.debug(f"npe2 version: {npe2_version}") check_call(f"rm -rf {NPE}".split()) + logger.debug("removing NPE directory succeeded") check_call(f"git clone https://github.com/napari/npe2 {NPE}".split()) + if not parse(npe2_version).is_devrelease: check_call(f"git checkout tags/v{npe2_version}".split(), cwd=NPE) - check_call([sys.executable, f"{NPE}/_docs/render.py", DOCS / 'plugins']) + + check_call([sys.executable, f"{NPE}/_docs/render.py", DOCS / "plugins"]) check_call(f"rm -rf {NPE}".split()) def main(): prep_npe2() - __import__('update_preference_docs').main() - __import__('update_event_docs').main() - __import__('update_ui_sections_docs').main() + logger.debug("Prep npe2 complete") + __import__("update_preference_docs").main() + logger.debug("update_preference_docs succeeded") + __import__("update_event_docs").main() + logger.debug("update_event_docs succeeded") + __import__("update_ui_sections_docs").main() + logger.debug("update_ui_sections_docs succeeded") if __name__ == "__main__": + # Example usage within a script + current_script_name = os.path.basename(__file__) + # Get the name of the current script + logger = setup_logger(current_script_name) + main() diff --git a/docs/_scripts/scripts_logger.py b/docs/_scripts/scripts_logger.py new file mode 100644 index 000000000..8ba3a4923 --- /dev/null +++ b/docs/_scripts/scripts_logger.py @@ -0,0 +1,59 @@ +"""Create a logger for a directory of scripts to aid in debugging.""" + +import logging +import os +import sys + + +def setup_logger(script_name, log_directory="logs"): + """Sets up a logger for a specific script. + + Args: + script_name (str): The name of the script (e.g., "my_script.py"). + log_directory (str, optional): The directory to store log files. Defaults to "logs". + + Returns: + logging.Logger: A configured logger instance. + """ + # Create log directory if it doesn't exist + if not os.path.exists(log_directory): + os.makedirs(log_directory) + + # Extract the script name without the extension + script_name_no_ext = os.path.splitext(script_name)[0] + + # Create a logger + logger = logging.getLogger(script_name_no_ext) + logger.setLevel(logging.DEBUG) # Set the minimum logging level + + # Create a file handler + # log_file_path = os.path.join(log_directory, f"{script_name_no_ext}.log") + # file_handler = logging.FileHandler(log_file_path) + # file_handler.setLevel(logging.DEBUG) + + handler = logging.StreamHandler(sys.stdout) + + # Create a formatter + formatter = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + ) + # file_handler.setFormatter(formatter) + handler.setFormatter(formatter) + + # Add the file handler to the logger + # logger.addHandler(file_handler) + logger.addHandler(handler) + return logger + + +if __name__ == "__main__": + # Example usage within a script + current_script_name = os.path.basename(__file__) + # Get the name of the current script + logger = setup_logger(current_script_name) + + logger.debug("This is a debug message.") + logger.info("This is an info message.") + logger.warning("This is a warning message.") + logger.error("This is an error message.") + logger.critical("This is a critical message.") diff --git a/docs/_scripts/update_event_docs.py b/docs/_scripts/update_event_docs.py index 572c5469c..5e1f40b15 100644 --- a/docs/_scripts/update_event_docs.py +++ b/docs/_scripts/update_event_docs.py @@ -1,5 +1,6 @@ import ast import inspect +import os from dataclasses import dataclass from pathlib import Path from types import ModuleType @@ -15,6 +16,10 @@ from napari.components.viewer_model import ViewerModel from napari.utils.events import EventedModel +from scripts_logger import setup_logger + +logger = setup_logger(__name__) + DOCS = Path(__file__).parent.parent @@ -28,49 +33,50 @@ class Ev: def access_at(self): """Where this event can be accessed (in code)""" if issubclass(self.model, layers.Layer): - return f'layer.events.{self.name}' + return f"layer.events.{self.name}" if issubclass(self.model, LayerList): - if self.name.startswith('selection.'): - return f'layers.selection.events.{self.name[10:]}' - return f'layers.events.{self.name}' + if self.name.startswith("selection."): + return f"layers.selection.events.{self.name[10:]}" + return f"layers.events.{self.name}" if issubclass(self.model, ViewerModel): - return f'viewer.events.{self.name}' + return f"viewer.events.{self.name}" for name, field_ in napari.Viewer.__fields__.items(): if field_.type_ is self.model: - return f'viewer.{name}.events.{self.name}' - return '' + return f"viewer.{name}.events.{self.name}" + return "" def type_name(self): - if cls_name := getattr(self.type_, '__name__', None): + if cls_name := getattr(self.type_, "__name__", None): return cls_name - name = str(self.type_) if self.type_ else '' + name = str(self.type_) if self.type_ else "" return name.replace("typing.", "") def ev_model_row(self) -> List[str]: return [ - f'`{self.model.__name__}`', - f'`{self.name}`', - f'`{self.access_at()}`', - self.description or '', - f'`{self.type_name()}`', + f"`{self.model.__name__}`", + f"`{self.name}`", + f"`{self.access_at()}`", + self.description or "", + f"`{self.type_name()}`", ] def layer_row(self) -> List[str]: return [ - f'`{self.model.__name__}`', - f'`{self.name}`', - f'`{self.access_at()}`', - self.description or '', - '', + f"`{self.model.__name__}`", + f"`{self.name}`", + f"`{self.access_at()}`", + self.description or "", + "", ] def walk_modules( - module: ModuleType, pkg='napari', _walked=None + module: ModuleType, pkg="napari", _walked=None ) -> Iterator[ModuleType]: """walk all modules in pkg, starting with `module`.""" + logger.debug(f"walking {pkg}") if not _walked: _walked = set() yield module @@ -87,6 +93,7 @@ def walk_modules( def iter_classes(module: ModuleType) -> Iterator[Type]: """iter all classes in module""" + logger.debug(f"walking {module}") for name in dir(module): attr = getattr(module, name) if inspect.isclass(attr) and attr.__module__ == module.__name__: @@ -94,14 +101,14 @@ def iter_classes(module: ModuleType) -> Iterator[Type]: def class_doc_attrs(kls: Type) -> Dict[str, Parameter]: - docs = {p.name: " ".join(p.desc) for p in ClassDoc(kls).get('Attributes')} - docs.update( - {p.name: " ".join(p.desc) for p in ClassDoc(kls).get('Parameters')} - ) + logger.debug(f"walking {kls}") + docs = {p.name: " ".join(p.desc) for p in ClassDoc(kls).get("Attributes")} + docs.update({p.name: " ".join(p.desc) for p in ClassDoc(kls).get("Parameters")}) return docs def iter_evented_model_events(module: ModuleType = napari) -> Iterator[Ev]: + logger.debug(f"walking evented model events {module}") for mod in walk_modules(module): for kls in iter_classes(mod): if not issubclass(kls, EventedModel): @@ -110,17 +117,14 @@ def iter_evented_model_events(module: ModuleType = napari) -> Iterator[Ev]: for name, field_ in kls.__fields__.items(): finfo = field_.field_info if finfo.allow_mutation: - descr = ( - f"{finfo.title.lower()}" - if finfo.title - else docs.get(name) - ) + descr = f"{finfo.title.lower()}" if finfo.title else docs.get(name) yield Ev(name, kls, descr, field_.type_) def iter_evented_container_events( module: ModuleType = napari, container_class=LayerList ) -> Iterator[Ev]: + logger.debug(f"walking evented container events {module}") for mod in walk_modules(module): for kls in iter_classes(mod): if not issubclass(kls, container_class): @@ -130,13 +134,13 @@ def iter_evented_container_events( for name, emitter in kls_instance.events._emitters.items(): descr = docs.get(name) yield Ev(name, kls, descr, type_=None) - if hasattr(kls_instance, 'selection'): + if hasattr(kls_instance, "selection"): selection = kls_instance.selection for name, emitter in selection.events._emitters.items(): - if name.startswith('_'): + if name.startswith("_"): # skip private emitters continue - name = 'selection.' + name + name = "selection." + name descr = docs.get(name) yield Ev(name, kls, descr, type_=None) @@ -147,11 +151,12 @@ def __init__(self) -> None: self._emitters: List[str] = [] def visit_Call(self, node: ast.Call): - if getattr(node.func, 'id', None) == 'EmitterGroup': + if getattr(node.func, "id", None) == "EmitterGroup": self._emitters.extend([name.arg for name in node.keywords]) # type: ignore def base_event_names() -> List[str]: + logger.debug("walking base event names") from napari.layers.base import base root = ast.parse(Path(base.__file__).read_text()) @@ -161,6 +166,7 @@ def base_event_names() -> List[str]: def iter_layer_events() -> Iterator[Ev]: + logger.debug("walking layer events") basenames = base_event_names() docs = class_doc_attrs(layers.Layer) for name in basenames: @@ -194,10 +200,11 @@ def iter_layer_events() -> Iterator[Ev]: def merge_image_and_label_rows(rows: List[List[str]]): """Merge events common to _ImageBase or IntensityVisualizationMixin.""" + logger.debug(f"merging {len(rows)} rows") # find events that are common across both Image, Labels and Surface layers. - image_events = {r[1] for r in rows if r[0] == '`Image`'} - labels_events = {r[1] for r in rows if r[0] == '`Labels`'} - surface_events = {r[1] for r in rows if r[0] == '`Surface`'} + image_events = {r[1] for r in rows if r[0] == "`Image`"} + labels_events = {r[1] for r in rows if r[0] == "`Labels`"} + surface_events = {r[1] for r in rows if r[0] == "`Surface`"} common_events = image_events & labels_events & surface_events # common only to Image and Labels imagebase_events = (image_events & labels_events) - common_events @@ -206,24 +213,24 @@ def merge_image_and_label_rows(rows: List[List[str]]): rows = [ r for r in rows - if not (r[0] in ['`Labels`', '`Surface`'] and r[1] in common_events) + if not (r[0] in ["`Labels`", "`Surface`"] and r[1] in common_events) ] rows = [ r for r in rows - if not (r[0] in ['`Labels`', '`Surface`'] and r[1] in imagebase_events) + if not (r[0] in ["`Labels`", "`Surface`"] and r[1] in imagebase_events) ] # modify the class name of the Image entries to mention Labels, Surface rows = [ - ['`Image`, `Labels`'] + r[1:] - if r[0] == '`Image`' and r[1] in imagebase_events + ["`Image`, `Labels`"] + r[1:] + if r[0] == "`Image`" and r[1] in imagebase_events else r for r in rows ] rows = [ - ['`Image`, `Labels`, `Surface`'] + r[1:] - if r[0] == '`Image`' and r[1] in common_events + ["`Image`, `Labels`, `Surface`"] + r[1:] + if r[0] == "`Image`" and r[1] in common_events else r for r in rows ] @@ -232,45 +239,43 @@ def merge_image_and_label_rows(rows: List[List[str]]): def main(): HEADER = [ - 'Event', - 'Description', - 'Event.value type', + "Event", + "Description", + "Event.value type", ] # Do viewer events rows = [ - ev.ev_model_row()[2:] - for ev in iter_evented_model_events() - if ev.access_at() + ev.ev_model_row()[2:] for ev in iter_evented_model_events() if ev.access_at() ] table1 = table_repr(rows, padding=2, header=HEADER, divide_rows=False) - (DOCS / 'guides' / '_viewer_events.md').write_text(table1) + (DOCS / "guides" / "_viewer_events.md").write_text(table1) # Do LayerList events rows = [ ev.layer_row()[2:] - for ev in iter_evented_container_events( - napari, container_class=LayerList - ) + for ev in iter_evented_container_events(napari, container_class=LayerList) if ev.access_at() ] table2 = table_repr(rows, padding=2, header=HEADER, divide_rows=False) - (DOCS / 'guides' / '_layerlist_events.md').write_text(table2) + (DOCS / "guides" / "_layerlist_events.md").write_text(table2) # Do layer events HEADER = [ - 'Class', - 'Event', - 'Description', - 'Event.value type', - ] - rows = [ - [ev.layer_row()[0]] + ev.layer_row()[2:] for ev in iter_layer_events() + "Class", + "Event", + "Description", + "Event.value type", ] + rows = [[ev.layer_row()[0]] + ev.layer_row()[2:] for ev in iter_layer_events()] rows = merge_image_and_label_rows(rows) table3 = table_repr(rows, padding=2, header=HEADER, divide_rows=False) - (DOCS / 'guides' / '_layer_events.md').write_text(table3) + (DOCS / "guides" / "_layer_events.md").write_text(table3) -if __name__ == '__main__': +if __name__ == "__main__": + # Example usage within a script + current_script_name = os.path.basename(__file__) + # Get the name of the current script + logger = setup_logger(current_script_name) main() diff --git a/docs/_scripts/update_preference_docs.py b/docs/_scripts/update_preference_docs.py index ef60c7fef..5c15c16a5 100644 --- a/docs/_scripts/update_preference_docs.py +++ b/docs/_scripts/update_preference_docs.py @@ -1,15 +1,20 @@ +import os from pathlib import Path from jinja2 import Template -from napari._pydantic_compat import ModelMetaclass from qtpy.QtCore import QTimer from qtpy.QtWidgets import QMessageBox from napari._qt.dialogs.preferences_dialog import PreferencesDialog from napari._qt.qt_event_loop import get_qapp from napari._qt.qt_resources import get_stylesheet +from napari._pydantic_compat import ModelMetaclass from napari.settings import NapariSettings +from scripts_logger import setup_logger + +logger = setup_logger(__name__) + DOCS = REPO_ROOT_PATH = Path(__file__).resolve().parent.parent GUIDES_PATH = DOCS / "guides" IMAGES_PATH = DOCS / "images" / "_autogenerated" @@ -98,7 +103,7 @@ def generate_images(): Generate images from `CORE_SETTINGS`. and save them in the developer section of the docs. """ - + logger.debug("Generating images") app = get_qapp() pref = PreferencesDialog() pref.setStyleSheet(get_stylesheet("dark")) @@ -131,10 +136,10 @@ def grab(): def create_preferences_docs(): """Create preferences docs from SETTINGS using a jinja template.""" + logger.debug("Creating preferences docs") sections = {} for name, field in NapariSettings.__fields__.items(): - if not isinstance(field.type_, ModelMetaclass): continue @@ -142,7 +147,7 @@ def create_preferences_docs(): title = field.field_info.title or name sections[title.lower()] = { "title": title, - "description": field.field_info.description or '', + "description": field.field_info.description or "", "fields": [ { "field": n, @@ -150,10 +155,10 @@ def create_preferences_docs(): "description": f.field_info.description, "default": repr(f.get_default()), "ui": n not in excluded, - "type": repr(f._type_display()).replace('.typing', ''), + "type": repr(f._type_display()).replace(".typing", ""), } for n, f in sorted(field.type_.__fields__.items()) - if n not in ('schema_version') + if n not in ("schema_version") ], } @@ -176,4 +181,9 @@ def main(): if __name__ == "__main__": + # Example usage within a script + current_script_name = os.path.basename(__file__) + # Get the name of the current script + logger = setup_logger(current_script_name) + main() diff --git a/docs/_scripts/update_ui_sections_docs.py b/docs/_scripts/update_ui_sections_docs.py index 9fd8f6bb9..a5ab07246 100644 --- a/docs/_scripts/update_ui_sections_docs.py +++ b/docs/_scripts/update_ui_sections_docs.py @@ -1,21 +1,26 @@ # ---- Standard library imports import json +import os from pathlib import Path +# ---- Third-party imports +import seedir as sd +from pydeps import cli, colors, dot, py2depgraph +from pydeps.pydeps import depgraph_to_dotsrc +from pydeps.target import Target + # ---- Napari imports +from napari._qt import dialogs +from napari._qt import qt_viewer from napari._qt.containers import qt_layer_list from napari._qt.layer_controls import qt_layer_controls_container -from napari._qt.widgets import qt_viewer_status_bar from napari._qt._qapp_model import qactions -from napari._qt import qt_viewer -from napari._qt import dialogs +from napari._qt.widgets import qt_viewer_status_bar from napari_console import qt_console -# ---- Third-party imports -from pydeps import cli, colors, dot, py2depgraph -from pydeps.pydeps import depgraph_to_dotsrc -from pydeps.target import Target -import seedir as sd +from scripts_logger import setup_logger + +logger = setup_logger(__name__) # ---- General constants # Docs paths @@ -32,6 +37,8 @@ DIALOGS_MODULE_PATH = Path(dialogs.__file__).parent CONSOLE_MODULE_PATH = Path(qt_console.__file__).parent +logger.debug("paths successfully set in update ui sections") + # ---- Utility functions def generate_dependencies_graph(options): @@ -206,13 +213,8 @@ def generate_mermaid_diagram( if "imports" in dependency: dep_imports = dependency["imports"] for dep_import_name in dep_imports: - if ( - dep_import_name != dep_name - and dep_import_name not in dep_name - ): - mermaid_diagram_content += ( - f"\t{dep_name} --> {dep_import_name}\n" - ) + if dep_import_name != dep_name and dep_import_name not in dep_name: + mermaid_diagram_content += f"\t{dep_name} --> {dep_import_name}\n" if graph_urls_prefix and dependency["path"]: module_path = Path(dependency["path"]) # Check if module is outside napari, like @@ -222,9 +224,7 @@ def generate_mermaid_diagram( NAPARI_ROOT_DIRECTORY_PATH ).as_posix() module_url = f"{graph_urls_prefix}{module_relative_path}" - mermaid_diagram_content += ( - f'\tclick {dep_name} "{module_url}" _blank\n' - ) + mermaid_diagram_content += f'\tclick {dep_name} "{module_url}" _blank\n' dep_name_parent = ".".join(dep_name.split(".")[:-1]) if dep_name_parent not in subgraphs: subgraphs[dep_name_parent] = [dep_name] @@ -234,18 +234,14 @@ def generate_mermaid_diagram( external_nodes.append(dep_name) for subgraph_key, subgraph_value in subgraphs.items(): - mermaid_diagram_content += ( - f"\tsubgraph module.{subgraph_key}[{subgraph_key}]\n" - ) + mermaid_diagram_content += f"\tsubgraph module.{subgraph_key}[{subgraph_key}]\n" for dep_subgraph_name in subgraph_value: mermaid_diagram_content += f"\t\t {dep_subgraph_name}\n" mermaid_diagram_content += "\tend\n" mermaid_diagram_content += f"\tclass module.{subgraph_key} subgraphs\n" if external_nodes: - mermaid_diagram_content += ( - "\tsubgraph module.external[external]\n" - ) + mermaid_diagram_content += "\tsubgraph module.external[external]\n" for external_node in external_nodes: mermaid_diagram_content += f"\t\t {external_node}\n" mermaid_diagram_content += "\tend\n" @@ -256,17 +252,11 @@ def generate_mermaid_diagram( "\tclassDef subgraphs fill:white,strock:black,color:black;" ) if graph_node_default_style: - mermaid_diagram_content += ( - f"\tclassDef default {graph_node_default_style}\n" - ) + mermaid_diagram_content += f"\tclassDef default {graph_node_default_style}\n" if graph_link_default_style: - mermaid_diagram_content += ( - f"\tlinkStyle default {graph_link_default_style}\n" - ) + mermaid_diagram_content += f"\tlinkStyle default {graph_link_default_style}\n" if graph_node_external_style: - mermaid_diagram_content += ( - f"\tclassDef external {graph_node_external_style}\n" - ) + mermaid_diagram_content += f"\tclassDef external {graph_node_external_style}\n" for external_dep in external_nodes: mermaid_diagram_content += f"\tclass {external_dep} external\n" @@ -310,7 +300,9 @@ def generate_docs_ui_section_page( page_content = f"## {section_name}\n" page_content += "### Dependencies diagram (related `napari` modules)\n" page_content += mermaid_diagram - page_content += "### Source code directory layout (related to modules inside `napari`)\n" + page_content += ( + "### Source code directory layout (related to modules inside `napari`)\n" + ) page_content += directory_layout if output_file: output_file.parent.mkdir(exist_ok=True, parents=True) @@ -388,7 +380,10 @@ def generate_docs_ui_section( # ---- Main and UI sections parameters def main(): - # General 'settings' + logger.debug("Empty ui sections list created") + ui_sections = [] + + # --- mermaid settings mermaid_graph_base_settings = { "graph_orientation": "LR", "graph_node_default_style": "fill:#00c3ff,color:black;", @@ -396,9 +391,8 @@ def main(): "graph_link_default_style": "stroke:#00c3ff", "graph_urls_prefix": "https://github.com/napari/napari/tree/main/napari/", } - ui_sections = [] - # ---- Layer list section parameters + # --- Layer list section parameters layer_list_section_name = "Layers list" layer_list_output_page = UI_SECTIONS_DOCS_ROOT_PATH / "layers_list_ui.md" layer_list_pydeps_args = [ @@ -433,6 +427,8 @@ def main(): "--show-deps", "--no-output", ] + + logger.debug("adding layer list to ui section") ui_sections.append( ( layer_list_section_name, @@ -443,10 +439,9 @@ def main(): ) # ---- Layer controls section parameters + logger.debug("start layer controls") layer_controls_section_name = "Layers controls" - layer_controls_output_page = ( - UI_SECTIONS_DOCS_ROOT_PATH / "layers_controls_ui.md" - ) + layer_controls_output_page = UI_SECTIONS_DOCS_ROOT_PATH / "layers_controls_ui.md" layer_controls_pydeps_args = [ f"{LAYER_CONTROLS_MODULE_PATH}", "--exclude", @@ -475,6 +470,8 @@ def main(): "--show-deps", "--no-output", ] + + logger.debug("adding layer controls to ui section") ui_sections.append( ( layer_controls_section_name, @@ -514,6 +511,7 @@ def main(): "--show-deps", "--no-output", ] + logger.debug("adding application status bar to ui section") ui_sections.append( ( application_status_bar_section_name, @@ -565,6 +563,7 @@ def main(): "--show-deps", "--no-output", ] + logger.debug("adding application menus to ui section") ui_sections.append( ( application_menus_section_name, @@ -606,6 +605,7 @@ def main(): "--show-deps", "--no-output", ] + logger.debug("adding viewer to ui section") ui_sections.append( ( viewer_section_name, @@ -660,6 +660,7 @@ def main(): "--show-deps", "--no-output", ] + logger.debug("adding dialogs to ui section") ui_sections.append( ( dialogs_section_name, @@ -687,6 +688,7 @@ def main(): "--show-deps", "--no-output", ] + logger.debug("adding console to ui section") ui_sections.append( ( console_section_name, @@ -696,6 +698,8 @@ def main(): ) ) + logger.debug("getting ready to iterate over sections") + logger.debug(f"ui sections {ui_sections}") for ( section_name, output_page, @@ -711,5 +715,9 @@ def main(): if __name__ == "__main__": - # ---- Call main + # Example usage within a script + current_script_name = os.path.basename(__file__) + # Get the name of the current script + logger = setup_logger(current_script_name) + main() diff --git a/docs/_static/custom.css b/docs/_static/custom.css index ecd8fabcf..d4c258b44 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -1,3 +1,5 @@ +@import url("https://fonts.googleapis.com/css2?family=Barlow:wght@400;500;600;700&display=swap"); + /* Sphinx-Gallery has compatible CSS to fix default sphinx themes Tested for Sphinx 1.3.1 for all themes: default, alabaster, sphinxdoc, @@ -6,7 +8,11 @@ Tested for Read the Docs theme 0.1.7 */ div.sphx-glr-download a { background-color: rgb(255, 255, 255) !important; - background-image: linear-gradient(to bottom, rgb(255, 255, 255), #ffffff) !important; + background-image: linear-gradient( + to bottom, + rgb(255, 255, 255), + #ffffff + ) !important; border-radius: 4px; border: 1px solid #ffffff !important; color: #000; @@ -17,7 +23,8 @@ div.sphx-glr-download a { } div.sphx-glr-download a:hover { - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 5px rgba(0, 0, 0, 0.25); text-decoration: none; background-image: none; @@ -25,18 +32,26 @@ div.sphx-glr-download a:hover { } /* Workaround for https: //github.com/napari/docs/pull/423#issuecomment-2141165872 */ -.bd-content .sd-tab-set>label { +.bd-content .sd-tab-set > label { background-color: var(--napari-gray); color: #222832; } /* Version warning banner color */ #bd-header-version-warning { - background-color: color-mix(in srgb, var(--pst-color-secondary-bg), transparent 30%); + background-color: color-mix( + in srgb, + var(--pst-color-secondary-bg), + transparent 30% + ); } #bd-header-version-warning .pst-button-link-to-stable-version { - background-color: color-mix(in srgb, var(--pst-color-secondary-bg), transparent 0%); + background-color: color-mix( + in srgb, + var(--pst-color-secondary-bg), + transparent 0% + ); border-color: var(--pst-color-secondary-bg); color: #222832; font-weight: 700; @@ -47,4 +62,972 @@ div.sphx-glr-download a:hover { border-color: var(--pst-color-secondary-bg); color: #222832; font-weight: 700; -} \ No newline at end of file +} + +/*************************** + napari custom colors +***************************/ + +html { + --napari-primary-blue: #80d1ff; + --napari-deep-blue: #009bf2; + --napari-light-blue: #d2efff; + --napari-dark-gray: #686868; + --napari-gray: #f7f7f7; + --pst-font-family-base: "Barlow", var(--pst-font-family-base-system); + --pst-font-family-heading: "Barlow", var(--pst-font-family-base-system); + --pst-font-family-monospace: "JetBrains Mono", + var(--pst-font-family-monospace-system); + --pst-font-size-base: 16px; + --pst-color-headerlink: var(--napari-dark-gray); + --pst-color-headerlink-hover: var(--napari-deep-blue); + --pst-color-surface: var(--napari-gray) !important; +} + +html[data-theme="light"] { + --pst-color-primary: black; + --pst-color-secondary: var(--napari-primary-blue); + --pst-color-background: white; + --napari-color-text-base: black; + --pst-color-text-base: var(--napari-color-text-base); + --pst-color-link: black; + --pst-color-link-hover: black !important; + --pst-color-inline-code: black !important; + --pst-color-inline-code-links: black !important; + --pst-color-on-background: white; + --pst-color-text-muted: var(--napari-dark-gray); + --pst-color-border: var(--napari-gray); + --pst-color-target: var(--napari-gray); +} + +/* Dark theme is currently unset - design/accessibility assessment is needed here */ +html[data-theme="dark"] { + --pst-color-primary: black; + --pst-color-secondary: var(--napari-primary-blue); + --pst-color-background: white; + --napari-color-text-base: black; + --pst-color-text-base: var(--napari-color-text-base); + --pst-color-link: black; + --pst-color-link-hover: black !important; + --pst-color-inline-code: black !important; + --pst-color-inline-code-links: black !important; + --pst-color-on-background: white; + --pst-color-text-muted: var(--napari-dark-gray); + --pst-color-border: var(--napari-gray); + --pst-color-target: var(--napari-gray); + --pst-color-secondary-bg: #e0c7ff; + --pst-color-primary-text: white; +} + +/* HTML elements */ +body { + margin: 0; + font-family: + "Barlow", + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + Roboto, + "Helvetica Neue", + Arial, + "Noto Sans", + "Liberation Sans", + sans-serif, + "Apple Color Emoji", + "Segoe UI Emoji", + "Segoe UI Symbol", + "Noto Color Emoji"; + text-align: left; +} + +p, +.line-block .line { + line-height: 1.5; + font-size: 1.0625rem; +} + +h1 { + font-size: 2.1875rem; + line-height: 125%; + font-weight: bolder; +} + +/*************************** + Textual elements +***************************/ + +h1 { + font-weight: 700; + color: var(--napari-color-text-base) !important; +} + +h2 { + font-weight: 700; + color: var(--napari-color-text-base) !important; +} + +h3 { + font-weight: 700; + color: var(--napari-color-text-base) !important; +} + +h4 { + font-weight: 700; + color: var(--napari-color-text-base) !important; +} + +h5 { + font-weight: 700; + color: var(--napari-color-text-base) !important; +} + +h6 { + font-weight: 700; + color: var(--napari-color-text-base) !important; +} + +a.headerlink { + color: var(--napari-dark-gray); +} + +.sd-card-hover:hover { + border-color: var(--napari-color-text-base) !important; + transform: scale(1.01); +} + +.prev-next-area p { + color: var(--napari-color-text-base); +} + +/*************************** + napari footer +***************************/ + +.napari-footer { + display: flex; + flex-shrink: 0; + flex-direction: row; + flex-wrap: wrap; + width: 100%; + align-items: center; + justify-content: space-between; + background-color: black; + padding-top: 1.5rem; + padding-bottom: 0.5rem; + margin-top: 75px; +} + +@media (min-width: 495px) { + .napari-footer { + padding-left: 3rem; + padding-right: 3rem; + } + + .napari-footer a > span { + font-size: 0.875rem; + line-height: 1.25rem; + } +} + +.napari-footer p { + color: white; +} + +.napari-footer a { + white-space: nowrap; + text-decoration-line: none; + margin-right: 1rem; + margin-bottom: 1rem; + color: white !important; + display: flex; + flex-direction: row; + align-items: center; +} + +.napari-footer a:hover { + color: white; +} + +.napari-footer a:last-child { + margin-right: 0; +} + +.napari-footer a > svg { + margin-right: 0.25rem; + display: inline-block; + height: 1em; + width: 1em; +} + +.napari-footer .footer-item { + display: flex; + flex-wrap: wrap; + align-items: center; +} + +.napari-footer .footer-item { + display: flex; + flex-wrap: wrap; + align-items: center; +} + +.napari-footer .footer-item--with-napari-copyright { + width: 100%; + justify-content: flex-end; +} + +.napari-footer .napari-copyright { + display: flex; + flex-direction: column; +} + +.napari-footer .napari-copyright, +.napari-footer .napari-copyright .copyright { + font-weight: 600; + font-size: 0.5625rem; + margin-bottom: auto; +} + +@media (min-width: 780px) { + .napari-footer .footer-item--with-napari-copyright { + width: max-content; + } + + .napari-footer .footer-item--with-napari-copyright { + justify-content: flex-start; + } +} + +@media (min-width: 495px) { + .napari-footer .napari-copyright, + .napari-footer .napari-copyright .copyright { + font-size: 0.875rem; + line-height: 1.25rem; + } +} + +.napari-footer .napari-copyright .sphinx-link, +.napari-footer .napari-copyright .sphinx-version { + display: flex; + justify-content: flex-start; +} + +.napari-footer, +.napari-copyright, +.sphinx-link, +> :not([hidden]), +~ :not([hidden]) { + margin-right: calc(0.25rem); + margin-left: calc(0.25rem); +} + +.napari-footer .napari-copyright .sphinx-version { + color: white; +} + +.napari-footer .footer-icon__hover-blue .footer-icon__light-blue { + display: none; +} + +.napari-footer .footer-icon__hover-blue:hover .footer-icon__regular { + display: none; +} + +.napari-footer .footer-icon__hover-blue:hover .footer-icon__light-blue { + display: block; +} + +/* Recommended by PST so that components will display horizontally */ +.footer-items__start, +.footer-items__end { + flex-direction: row; +} + +.bd-footer__inner { + display: flex; + flex-shrink: 0; + flex-direction: row; + flex-wrap: wrap; + width: 100%; + max-width: 100%; + align-items: center; + justify-content: space-between; + background-color: black; +} + +/**************************** + Nav bar section +*****************************/ + +.navbar { + background-color: var(--napari-primary-blue) !important; + box-shadow: none; +} + +.navbar-brand { + vertical-align: middle; + font-size: 1.25rem; + color: black !important; + position: relative; + font-weight: bolder; +} +.navbar-brand:hover, +.navbar-brand:focus { + text-decoration: none !important; + color: var(--napari-color-text-base) !important; +} + +/* Navbar text */ +.bd-header ul.navbar-nav > li.nav-item > .nav-link { + color: var(--napari-color-text-base); + font-size: 1.0625rem; + font-weight: 500 !important; + border-bottom: 3px solid transparent; + padding: 16px 1.0625rem 16px !important; +} + +.bd-header ul.navbar-nav > li.nav-item > .nav-link:hover { + color: var(--napari-color-text-base); + font-size: 1.0625rem; + font-weight: 500 !important; + border-bottom: 3px solid var(--napari-color-text-base); +} + +.bd-header ul.navbar-nav > li.nav-item.current > .nav-link::before { + border-bottom: 0 solid var(--napari-color-text-base); +} + +.bd-header ul.navbar-nav > li.nav-item.current > .nav-link { + border-bottom: 3px solid var(--napari-color-text-base); + font-weight: 700 !important; +} + +.bd-header ul.navbar-nav { + height: var(--pst-header-height); +} + +.bd-header ul.navbar-nav > li.nav-item { + margin-inline: 0; +} + +.bd-header ul.navbar-nav > li.nav-item.dropdown > .dropdown-toggle { + color: var(--napari-color-text-base); + font-size: 1.0625rem; + font-weight: 500 !important; + border-bottom: 3px solid transparent; + padding: 16px 1.0625rem 16px !important; + height: var(--pst-header-height); +} + +.bd-header ul.navbar-nav > li.nav-item.dropdown > .dropdown-toggle:hover { + box-shadow: none; + text-decoration: none; + border-bottom: 3px solid var(--napari-color-text-base); +} + +.bd-header ul.navbar-nav li a.nav-link.dropdown-item { + color: var(--napari-color-text-base); + font-weight: 500; +} + +html .pst-navbar-icon, +html .pst-navbar-icon:hover { + color: var(--napari-color-text-base); +} + +/*************************** + version switcher +***************************/ + +button.btn.version-switcher__button { + padding-top: 0; + font-size: 0.875rem; + font-weight: 600; + border-style: none; + color: var(--napari-color-text-base); +} + +button.btn.version-switcher__button:hover { + color: var(--napari-color-text-base); +} + +.version-switcher__menu a.list-group-item { + background-color: var(--pst-color-background); + color: var(--napari-color-text-base); + padding: 0.5rem 0.5rem; + font-size: 0.875rem; +} + +.version-switcher__menu a.list-group-item:hover { + color: var(--napari-color-text-base); +} + +.version-switcher__menu, +button.version-switcher__button { + min-width: max-content; + border-radius: unset; +} + +/*************************** + sidebar +***************************/ + +/* Remove "Section Navigation" caption */ +.bd-links__title { + display: none; +} + +/* Move chevron to the left */ +.bd-sidebar-primary li.has-children > details > summary .toctree-toggle { + right: unset; +} + +/* Fonts and styles */ +.bd-sidebar a.reference, +.bd-sidebar .caption-text { + font-size: 0.875rem; + line-height: 1.25rem; +} + +.bd-sidebar-primary .sidebar-primary-items__end { + margin-bottom: 0; + margin-top: 0; +} + +.bd-sidebar .toctree-l1 a { + padding-left: 32px; +} + +.bd-sidebar .toctree-l2 { + margin-left: -0.2rem; + border-left: 1px solid var(--napari-color-text-base); +} + +.bd-sidebar .toctree-l2 label { + left: 4px; +} + +.bd-sidebar .toctree-l2 a { + font-size: 0.875rem; + line-height: 1.5rem; + text-decoration-line: none; + border-left: 2px solid transparent; + color: var(--napari-color-text-base) !important; +} + +.bd-sidebar .toctree-l2 a:hover, +.bd-sidebar .toctree-l2 a.current:hover { + border-left: 2px solid var(--napari-primary-blue); +} + +.bd-sidebar .toctree-l2 a.current { + border-left: 2px solid var(--napari-color-text-base); +} + +.bd-sidebar .toctree-l3 label { + left: 6px; +} + +.bd-sidebar .toctree-l3 a { + font-size: 0.875rem; + line-height: 1.5rem; + text-decoration-line: none; + border-left: 2px solid transparent; + color: var(--napari-color-text-base) !important; + padding-left: 36px; +} + +.bd-sidebar .toctree-l3 a:hover, +.bd-sidebar .toctree-l3 a.current:hover { + border-left: 2px solid var(--napari-primary-blue); + margin-left: -1rem; + padding-left: 52px; +} + +.bd-sidebar .toctree-l3 a.current { + border-left: 2px solid var(--napari-color-text-base); + margin-left: -1rem; + padding-left: 52px; +} + +.bd-sidebar .toctree-l4 label { + left: 8px; +} + +.bd-sidebar .toctree-l4 a { + font-size: 0.875rem; + line-height: 1.5rem; + text-decoration-line: none; + border-left: 2px solid transparent; + color: var(--napari-color-text-base) !important; +} + +.bd-sidebar .toctree-l4 a:hover, +.bd-sidebar .toctree-l4 a.current:hover { + border-left: 2px solid var(--napari-primary-blue); + margin-left: -2rem; + padding-left: 68px; +} + +.bd-sidebar .toctree-l4 a.current { + border-left: 2px solid var(--napari-color-text-base); + margin-left: -2rem; + padding-left: 68px; +} + +.bd-sidebar .toctree-l5 label { + left: 10px; +} + +.bd-sidebar .toctree-l5 a { + font-size: 0.875rem; + line-height: 1.5rem; + text-decoration-line: none; + border-left: 2px solid transparent; + color: var(--napari-color-text-base) !important; +} + +.bd-sidebar .toctree-l5 a:hover, +.bd-sidebar .toctree-l5 a.current:hover { + border-left: 2px solid var(--napari-primary-blue); +} + +.bd-sidebar .toctree-l5 a.current { + border-left: 2px solid var(--napari-color-text-base); +} + +/* also related to navbar */ +.navbar-nav li a:focus, +.navbar-nav li a:hover, +.navbar-nav li.current > a { + color: var(--napari-color-text-base); +} + +nav.bd-links li > a { + color: var(--napari-color-text-base); + display: block; + line-height: 1.25rem; +} + +nav.bd-links li > a:active, +nav.bd-links li > a:hover { + color: var(--napari-color-text-base); +} + +nav.bd-links li > a:hover { + text-decoration: none !important; +} + +nav.bd-links .current > a { + box-shadow: none !important; +} + +/*************************** +search +***************************/ + +.bd-search { + border: 1px solid transparent; +} + +.bd-search:focus-within { + box-shadow: 0 0 0 0.1875rem var(--napari-primary-blue); +} + +.form-control { + border: 1px transparent; +} + +.form-control:focus, +.form-control:focus-visible { + background-color: var(--pst-color-background); + border: none; + box-shadow: none; + color: var(--pst-color-text-muted); + outline: none; +} + +/*************************** +page toc sidebar +***************************/ + +.onthispage { + border-style: none; + padding: 1px 0 5px; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 600; + text-transform: uppercase; + color: var(--napari-color-text-base) !important; + margin: 0 !important; +} + +.page-toc { + .section-nav { + padding-left: 0; + border-bottom: none; + } + + .onthispage { + color: var(--napari-color-text-base); + font-weight: var(--pst-sidebar-header-font-weight); + margin-bottom: 1rem; + } +} + +.toc-entry a.nav-link.active:hover { + color: var(--napari-color-text-base); +} + +.toc-entry a.nav-link:active, +.toc-entry a.nav-link:hover { + color: var(--napari-color-text-base); +} + +nav.page-toc { + border-left: 1px solid var(--napari-color-text-base); + padding-left: 1rem; +} + +.sidebar-secondary-item { + border-left: none !important; +} + +.toc-entry a.nav-link, +.toc-entry a > code { + color: var(--napari-color-text-base); +} + +.toc-entry > .nav-link { + border-left: 3px solid transparent; +} + +.toc-entry > .nav-link:hover { + border-left: 3px solid var(--napari-primary-blue); +} + +.toc-entry a.nav-link:hover { + text-decoration: none !important; +} + +.toc-entry a.nav-link.active { + box-shadow: none !important; + border-left: 3px solid var(--napari-color-text-base); +} + +.toc-h3.nav-item.toc-entry.active a { + margin-left: -2rem; + padding-left: 2rem; +} + +.toc-h3.nav-item.toc-entry a { + margin-left: -2rem; + padding-left: 2rem; +} + +.toc-h4.nav-item.toc-entry.active a { + margin-left: -3rem; + padding-left: 3rem; +} + +.toc-h4.nav-item.toc-entry a { + margin-left: -3rem; + padding-left: 3rem; +} + +.toc-h4.nav-item.toc-entry a:hover { + margin-left: -3rem; + padding-left: 3rem; +} + +/*************************** + napari calendar +***************************/ + +:root { + --fc-border-color: var(--napari-primary-blue); + --fc-daygrid-event-dot-width: 5px; + --fc-button-bg-color: var(--napari-primary-blue); + --fc-button-border-color: var(--napari-primary-blue); + --fc-button-text-color: var(--napari-color-text-base); + --fc-button-active-bg-color: var(--napari-deep-blue); + --fc-button-active-border-color: var(--napari-deep-blue); + --fc-button-hover-bg-color: var(--napari-deep-blue); + --fc-button-hover-border-color: var(--napari-deep-blue); + --fc-event-bg-color: var(--napari-light-blue); + --fc-event-border-color: var(--napari-light-blue); + --fc-event-text-color: var(--napari-color-text-base); +} + +.fc .fc-button:focus { + box-shadow: none; +} + +.fc-event-time { + margin-right: 3px; + min-width: fit-content; +} + +.fc-day-today .fc-daygrid-day-number { + background-color: var(--napari-primary-blue); +} + +.fc .fc-daygrid-day.fc-day-today { + background-color: unset; +} + +/*************************** + Admonitions +***************************/ + +.admonition, +div.admonition { + --color: #80d1ff; + border: var(--color) solid 1px !important; + border-radius: 0 !important; + box-shadow: none !important; + /*border-color: rgba(var(--pst-color-admonition-default), 1); */ + padding-left: 0 !important; + font-size: 0.938rem; + font-weight: 500; +} + +.admonition > .admonition-title, +div.admonition > .admonition-title { + text-transform: uppercase; + background: var(--color) !important; + font-size: 0.938rem !important; + font-weight: 700; +} + +/* Remove admonition title icon */ +div.admonition > .admonition-title:after, +.admonition > .admonition-title:after { + display: none; +} + +/* Padding and spacing */ +div.admonition.warning > ul.simple { + padding: 1.1rem !important; +} + +div.admonition > p, +div.admonition > ul.simple p { + font-size: 0.938rem; +} + +div.admonition > p { + padding-top: 0.5rem; + padding-bottom: 0.4rem; +} + +/* Toggle button */ +.admonition.toggle-hidden { + height: 40px; +} + +.admonition .toggle-button { + top: 0; + right: 0; + z-index: 10; + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + float: unset; +} + +.admonition .toggle-button::before { + display: none; +} + +.admonition .toggle-button svg { + transition-property: transform; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; + transform: rotate(45deg); +} + +.admonition .toggle-button.toggle-button-hidden svg { + transform: rotate(0); +} + +/* Attention */ + +.admonition.attention { + --color: #d8f97d; +} + +/* Caution */ + +.admonition.caution { + --color: #ffc580; +} + +/* Warning */ + +.admonition.warning { + --color: #ffa680; +} + +/* Danger */ + +.admonition.danger { + --color: #ff8080; +} + +/* Error */ + +.admonition.error { + --color: #fade7d; +} + +/* Hint */ + +.admonition.hint { + --color: #8094ff; +} + +/* Tip */ + +.admonition.tip { + --color: #cf80ff; +} + +/* Important */ + +.admonition.important { + --color: #f1f379; +} + +/* Note */ + +.admonition.note { + --color: #80ffe0; +} + +/*************************** + Page container +***************************/ + +#pst-back-to-top { + background-color: var(--napari-light-blue); + color: var(--napari-dark-gray); +} + +/*************************** + Calendar popup +***************************/ + +/* The Modal (background) */ +.modal { + /* Hidden by default */ + position: fixed; + /* Stay in place */ + z-index: 1; + /* Sit on top */ + padding-top: 100px; + /* Location of the box */ + left: 0; + top: 0; + width: 100%; + /* Full width */ + height: 100%; + /* Full height */ + overflow: auto; + /* Enable scroll if needed */ + background-color: rgb(0, 0, 0); + /* Fallback color */ + background-color: rgba(0, 0, 0, 0.4); + /* Black w/ opacity */ +} + +/* Modal Content */ +.modal-content { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: #fefefe; + margin: auto; + padding: 0; + border: 1px solid #888; + box-shadow: + 0 4px 8px 0 rgba(0, 0, 0, 0.2), + 0 6px 20px 0 rgba(0, 0, 0, 0.19); +} + +/* Modal Content */ +.modal-content { + width: 30%; +} + +@media (max-width: 780px) { + .modal-content { + width: 50%; + } +} + +@media (max-width: 495px) { + .modal-content { + width: 80%; + } +} + +/* Add Animation */ +@-webkit-keyframes animatetop { + from { + top: -300px; + opacity: 0; + } + + to { + top: 0; + opacity: 1; + } +} + +@keyframes animatetop { + from { + top: -300px; + opacity: 0; + } + + to { + top: 0; + opacity: 1; + } +} + +/* The Close Button */ +.close { + color: white; + float: right; + font-size: 28px; + font-weight: bold; + padding-right: 12px; + padding-top: 4px; +} + +.close:hover, +.close:focus { + color: #000; + text-decoration: none; + cursor: pointer; +} + +.modal-header { + padding: 0 0 0 12px; + background-color: var(--napari-primary-blue); + color: white; + display: block; +} + +.modal-header h3 { + margin-top: 1rem; +} + +.modal-body { + padding: 12px; +} diff --git a/docs/_templates/footer.html b/docs/_templates/footer.html new file mode 100644 index 000000000..ff6eee7b2 --- /dev/null +++ b/docs/_templates/footer.html @@ -0,0 +1,23 @@ +{% if theme_footer_start or theme_footer_center or theme_footer_end %} +
+{% endif %} diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html new file mode 100644 index 000000000..a715e0f18 --- /dev/null +++ b/docs/_templates/layout.html @@ -0,0 +1,3 @@ +{%- extends "pydata_sphinx_theme/layout.html" %} {%- block footer %} + +{%- endblock %} diff --git a/docs/_templates/napari-copyright.html b/docs/_templates/napari-copyright.html new file mode 100644 index 000000000..a6f1538b1 --- /dev/null +++ b/docs/_templates/napari-copyright.html @@ -0,0 +1,21 @@ +
+ {%- if hasdoc('copyright') %} {% trans path=pathto('copyright'),
+ copyright=copyright|e %}Copyright {{ copyright }}{%
+ endtrans %}
+ {%- else %} {% trans copyright=copyright|e %}Copyright {{ copyright }}{%
+ endtrans %}
+ {%- endif %}
+